Compare commits
110 Commits
dev
...
de28107287
| Author | SHA1 | Date | |
|---|---|---|---|
|
de28107287
|
|||
|
3f6fcc0106
|
|||
|
71119ac4c4
|
|||
|
c91eaaf1a5
|
|||
|
77802d3dc9
|
|||
|
8669a2c9ef
|
|||
|
d589df860e
|
|||
|
db25ab4e5f
|
|||
|
e855a414a4
|
|||
|
2823788765
|
|||
|
fc4baca21d
|
|||
|
e188d743f0
|
|||
|
f7dd806885
|
|||
|
326e777b7f
|
|||
|
0eb0667d2a
|
|||
|
cd97f6156f
|
|||
|
3979027471
|
|||
|
d3f3fba413
|
|||
|
7b77069744
|
|||
|
585a261bb8
|
|||
|
2631c22e52
|
|||
|
af3edb1d30
|
|||
|
ba72ef2759
|
|||
|
2639d88492
|
|||
|
04812cf56b
|
|||
|
112e30804e
|
|||
|
7e5554cfcd
|
|||
|
7e91f267e3
|
|||
|
21bb9a729f
|
|||
|
3b4b6b4e8e
|
|||
|
253d186fdf
|
|||
|
cead8fe91e
|
|||
|
c602ce0726
|
|||
|
4dfb500370
|
|||
|
93b22ea14b
|
|||
|
c9363ee34b
|
|||
|
e9232631de
|
|||
|
402965503f
|
|||
|
7653028241
|
|||
|
f0ef28bd19
|
|||
|
e0485ecc32
|
|||
|
893131fe02
|
|||
|
0e5b6d7f98
|
|||
|
310214fc20
|
|||
|
18c03c194b
|
|||
|
b115d3d598
|
|||
|
da4c58b631
|
|||
|
086b588445
|
|||
|
3f325126f1
|
|||
|
1b350eb22a
|
|||
|
167df010a9
|
|||
|
47647217f1
|
|||
|
5839f1d394
|
|||
|
21264d8ff7
|
|||
|
0e592c1600
|
|||
|
2f0c4b6a97
|
|||
|
4ba02420ed
|
|||
|
3a6fb2a6f0
|
|||
|
0c6baba099
|
|||
|
35a421472d
|
|||
|
6732eb6a22
|
|||
|
f5cfbd7296
|
|||
|
627a32c7a3
|
|||
|
5246715d78
|
|||
|
60ffc569a5
|
|||
|
a3800bfad6
|
|||
|
454108d871
|
|||
|
cb6fe19033
|
|||
|
3d69514a39
|
|||
|
96159e5b80
|
|||
|
9b3b391fef
|
|||
|
3286ed2934
|
|||
|
c9c0debb2b
|
|||
|
c1879dfdc8
|
|||
|
640686296e
|
|||
|
a3b1241fca
|
|||
|
596ad2ccbe
|
|||
|
1607897fc9
|
|||
|
9d6094173d
|
|||
|
5efbf660c6
|
|||
|
8fedafd261
|
|||
|
1d4f317bb5
|
|||
|
a85e561863
|
|||
|
d84427b039
|
|||
|
3338522d40
|
|||
|
3a91e834b7
|
|||
|
3d8bc944e3
|
|||
|
c596767c37
|
|||
|
b2cbea5383
|
|||
|
2e0efd1cb9
|
|||
|
61d229b100
|
|||
|
32d19ae291
|
|||
|
c8f072c930
|
|||
|
4d047247f1
|
|||
|
b11ae055c3
|
|||
|
b2e7ecc92c
|
|||
|
23893a4ac1
|
|||
|
54a7625c1b
|
|||
|
8ed9d6942a
|
|||
|
92cd20f36f
|
|||
|
8b200d14c6
|
|||
|
58117cc6f6
|
|||
|
e777f832e6
|
|||
|
0e24b46525
|
|||
|
cba2e83074
|
|||
|
cda1f455b9
|
|||
|
96cc7c221f
|
|||
|
f81f26a5cb
|
|||
|
4cc1c0f68b
|
|||
|
1bd81cdf6c
|
6
.gitignore
vendored
6
.gitignore
vendored
@@ -17,6 +17,10 @@ build/
|
|||||||
# Local configuration file (sdk path, etc)
|
# Local configuration file (sdk path, etc)
|
||||||
local.properties
|
local.properties
|
||||||
|
|
||||||
|
# Key Store
|
||||||
|
keystore.properties
|
||||||
|
*.jks
|
||||||
|
|
||||||
# Eclipse project files
|
# Eclipse project files
|
||||||
.classpath
|
.classpath
|
||||||
.project
|
.project
|
||||||
@@ -43,3 +47,5 @@ _sandbox
|
|||||||
|
|
||||||
# Android Studio captures folder
|
# Android Studio captures folder
|
||||||
captures/
|
captures/
|
||||||
|
|
||||||
|
.kotlin/
|
||||||
|
|||||||
123
.idea/codeStyles/Project.xml
generated
Normal file
123
.idea/codeStyles/Project.xml
generated
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
<component name="ProjectCodeStyleConfiguration">
|
||||||
|
<code_scheme name="Project" version="173">
|
||||||
|
<JetCodeStyleSettings>
|
||||||
|
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||||
|
</JetCodeStyleSettings>
|
||||||
|
<codeStyleSettings language="XML">
|
||||||
|
<option name="FORCE_REARRANGE_MODE" value="1" />
|
||||||
|
<indentOptions>
|
||||||
|
<option name="CONTINUATION_INDENT_SIZE" value="4" />
|
||||||
|
</indentOptions>
|
||||||
|
<arrangement>
|
||||||
|
<rules>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>xmlns:android</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>xmlns:.*</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
<order>BY_NAME</order>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>.*:id</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>.*:name</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>name</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>style</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>.*</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>^$</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
<order>BY_NAME</order>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>.*</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
<order>ANDROID_ATTRIBUTE_ORDER</order>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<rule>
|
||||||
|
<match>
|
||||||
|
<AND>
|
||||||
|
<NAME>.*</NAME>
|
||||||
|
<XML_ATTRIBUTE />
|
||||||
|
<XML_NAMESPACE>.*</XML_NAMESPACE>
|
||||||
|
</AND>
|
||||||
|
</match>
|
||||||
|
<order>BY_NAME</order>
|
||||||
|
</rule>
|
||||||
|
</section>
|
||||||
|
</rules>
|
||||||
|
</arrangement>
|
||||||
|
</codeStyleSettings>
|
||||||
|
<codeStyleSettings language="kotlin">
|
||||||
|
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||||
|
</codeStyleSettings>
|
||||||
|
</code_scheme>
|
||||||
|
</component>
|
||||||
5
.idea/codeStyles/codeStyleConfig.xml
generated
Normal file
5
.idea/codeStyles/codeStyleConfig.xml
generated
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<component name="ProjectCodeStyleConfiguration">
|
||||||
|
<state>
|
||||||
|
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
|
||||||
|
</state>
|
||||||
|
</component>
|
||||||
1
app/.gitignore
vendored
1
app/.gitignore
vendored
@@ -1,2 +1,3 @@
|
|||||||
/build
|
/build
|
||||||
/src/main/res/raw/dependencies.json
|
/src/main/res/raw/dependencies.json
|
||||||
|
/release/
|
||||||
|
|||||||
@@ -1,33 +1,72 @@
|
|||||||
import com.android.build.gradle.internal.api.BaseVariantOutputImpl
|
|
||||||
import com.mikepenz.aboutlibraries.plugin.AboutLibrariesTask
|
import com.mikepenz.aboutlibraries.plugin.AboutLibrariesTask
|
||||||
|
import java.io.FileInputStream
|
||||||
|
import java.time.LocalDateTime
|
||||||
|
import java.time.ZoneOffset
|
||||||
|
import java.time.format.DateTimeFormatter
|
||||||
|
import java.util.Properties
|
||||||
|
|
||||||
|
val localDateTime: LocalDateTime = LocalDateTime.now(ZoneOffset.UTC)
|
||||||
|
val baseVersionCode = 1
|
||||||
|
val baseVersionName = "0.0.0"
|
||||||
|
|
||||||
|
val keystoreProperties = rootProject.file("keystore.properties").run {
|
||||||
|
if (!exists()) {
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
Properties().apply {
|
||||||
|
load(FileInputStream(this@run))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
alias(libs.plugins.androidApplication)
|
alias(libs.plugins.androidApplication)
|
||||||
alias(libs.plugins.jetbrainsKotlinAndroid)
|
alias(libs.plugins.jetbrainsKotlinAndroid)
|
||||||
|
alias(libs.plugins.compose.compiler)
|
||||||
alias(libs.plugins.ksp)
|
alias(libs.plugins.ksp)
|
||||||
alias(libs.plugins.aboutlibraries)
|
alias(libs.plugins.aboutlibraries)
|
||||||
alias(libs.plugins.hilt)
|
alias(libs.plugins.hilt)
|
||||||
alias(libs.plugins.protobuf)
|
alias(libs.plugins.protobuf)
|
||||||
|
alias(libs.plugins.kotlinxSerialization)
|
||||||
|
alias(libs.plugins.secrets)
|
||||||
|
alias(libs.plugins.room)
|
||||||
|
alias(libs.plugins.parcelize)
|
||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "top.fatweb.oxygen.toolbox"
|
namespace = "top.fatweb.oxygen.toolbox"
|
||||||
compileSdk = 34
|
compileSdk = 35
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "top.fatweb.oxygen.toolbox"
|
applicationId = "top.fatweb.oxygen.toolbox"
|
||||||
minSdk = 21
|
minSdk = 24
|
||||||
targetSdk = 34
|
targetSdk = 35
|
||||||
versionCode = 1
|
versionCode = baseVersionCode
|
||||||
versionName = "1.0.0-SNAPSHOT"
|
versionName = "$baseVersionName${
|
||||||
|
if (baseVersionCode % 100 != 0) ".${
|
||||||
|
localDateTime.format(
|
||||||
|
DateTimeFormatter.ofPattern("yyMMdd")
|
||||||
|
)
|
||||||
|
}" else ""
|
||||||
|
}"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
||||||
vectorDrawables {
|
vectorDrawables {
|
||||||
useSupportLibrary = true
|
useSupportLibrary = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Required when setting minSdkVersion to 20 or lower
|
setProperty("archivesBaseName", "$applicationId-v$versionName($versionCode)")
|
||||||
multiDexEnabled = true
|
}
|
||||||
|
|
||||||
|
signingConfigs {
|
||||||
|
keystoreProperties?.let {
|
||||||
|
create("release") {
|
||||||
|
storeFile = rootProject.file(it["storeFile"] as String)
|
||||||
|
storePassword = it["storePassword"] as String
|
||||||
|
keyAlias = it["keyAlias"] as String
|
||||||
|
keyPassword = it["keyPassword"] as String
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
@@ -38,13 +77,7 @@ android {
|
|||||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||||
"proguard-rules.pro"
|
"proguard-rules.pro"
|
||||||
)
|
)
|
||||||
}
|
signingConfig = signingConfigs.findByName("release")
|
||||||
}
|
|
||||||
|
|
||||||
android.applicationVariants.all {
|
|
||||||
outputs.all {
|
|
||||||
(this as BaseVariantOutputImpl).outputFileName =
|
|
||||||
"${project.name}_${defaultConfig.versionName}-${defaultConfig.versionCode}_${buildType.name}.apk"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,9 +93,7 @@ android {
|
|||||||
}
|
}
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
compose = true
|
compose = true
|
||||||
}
|
buildConfig = true
|
||||||
composeOptions {
|
|
||||||
kotlinCompilerExtensionVersion = "1.5.10"
|
|
||||||
}
|
}
|
||||||
packaging {
|
packaging {
|
||||||
resources {
|
resources {
|
||||||
@@ -101,11 +132,7 @@ aboutLibraries {
|
|||||||
registerAndroidTasks = false
|
registerAndroidTasks = false
|
||||||
configPath = "libConfig"
|
configPath = "libConfig"
|
||||||
outputFileName = "dependencies.json"
|
outputFileName = "dependencies.json"
|
||||||
exclusionPatterns = listOf(
|
exclusionPatterns = listOf<Regex>().map { it.toPattern() }
|
||||||
Regex("androidx.*"),
|
|
||||||
Regex("org.jetbrains.*"),
|
|
||||||
Regex("com.google.guava:listenablefuture")
|
|
||||||
).map { it.toPattern() }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
task("exportLibrariesToJson", AboutLibrariesTask::class) {
|
task("exportLibrariesToJson", AboutLibrariesTask::class) {
|
||||||
@@ -116,12 +143,26 @@ task("exportLibrariesToJson", AboutLibrariesTask::class) {
|
|||||||
afterEvaluate {
|
afterEvaluate {
|
||||||
tasks.findByName("preBuild")?.dependsOn(tasks.findByName("exportLibrariesToJson"))
|
tasks.findByName("preBuild")?.dependsOn(tasks.findByName("exportLibrariesToJson"))
|
||||||
tasks.findByName("kspDebugKotlin")?.dependsOn(tasks.findByName("generateDebugProto"))
|
tasks.findByName("kspDebugKotlin")?.dependsOn(tasks.findByName("generateDebugProto"))
|
||||||
|
tasks.findByName("kspReleaseKotlin")?.dependsOn(tasks.findByName("generateReleaseProto"))
|
||||||
|
}
|
||||||
|
|
||||||
|
secrets {
|
||||||
|
defaultPropertiesFileName = "secrets.defaults.properties"
|
||||||
|
}
|
||||||
|
|
||||||
|
ksp {
|
||||||
|
arg("room.generateKotlin", "true")
|
||||||
|
}
|
||||||
|
|
||||||
|
room {
|
||||||
|
schemaDirectory("$projectDir/schemas")
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
coreLibraryDesugaring(libs.desugar.jdk.libs)
|
coreLibraryDesugaring(libs.desugar.jdk.libs)
|
||||||
|
|
||||||
testImplementation(libs.junit)
|
testImplementation(libs.junit)
|
||||||
|
testImplementation(libs.paging.common)
|
||||||
|
|
||||||
androidTestImplementation(platform(libs.androidx.compose.bom))
|
androidTestImplementation(platform(libs.androidx.compose.bom))
|
||||||
androidTestImplementation(libs.androidx.ui.test.junit4)
|
androidTestImplementation(libs.androidx.ui.test.junit4)
|
||||||
@@ -142,6 +183,7 @@ dependencies {
|
|||||||
implementation(libs.material.icons.core)
|
implementation(libs.material.icons.core)
|
||||||
implementation(libs.material.icons.extended)
|
implementation(libs.material.icons.extended)
|
||||||
implementation(libs.material3.window.size)
|
implementation(libs.material3.window.size)
|
||||||
|
implementation(libs.animation.graphics)
|
||||||
implementation(libs.androidx.core.ktx)
|
implementation(libs.androidx.core.ktx)
|
||||||
implementation(libs.androidx.activity.compose)
|
implementation(libs.androidx.activity.compose)
|
||||||
implementation(libs.androidx.appcompat)
|
implementation(libs.androidx.appcompat)
|
||||||
@@ -150,6 +192,7 @@ dependencies {
|
|||||||
implementation(libs.lifecycle.viewmodel.compose)
|
implementation(libs.lifecycle.viewmodel.compose)
|
||||||
implementation(libs.androidx.core.splashscreen)
|
implementation(libs.androidx.core.splashscreen)
|
||||||
implementation(libs.hilt.android)
|
implementation(libs.hilt.android)
|
||||||
|
implementation(libs.androidx.web.kit)
|
||||||
ksp(libs.hilt.android)
|
ksp(libs.hilt.android)
|
||||||
ksp(libs.hilt.compiler)
|
ksp(libs.hilt.compiler)
|
||||||
implementation(libs.coil.kt)
|
implementation(libs.coil.kt)
|
||||||
@@ -160,4 +203,17 @@ dependencies {
|
|||||||
implementation(libs.protobuf.kotlin.lite)
|
implementation(libs.protobuf.kotlin.lite)
|
||||||
implementation(libs.androidx.navigation.compose)
|
implementation(libs.androidx.navigation.compose)
|
||||||
implementation(libs.androidx.hilt.navigation.compose)
|
implementation(libs.androidx.hilt.navigation.compose)
|
||||||
|
implementation(libs.kotlinx.serialization.json)
|
||||||
|
implementation(libs.retrofit.core)
|
||||||
|
implementation(libs.retrofit.kotlin.serialization)
|
||||||
|
implementation(libs.okhttp.logging)
|
||||||
|
implementation(libs.paging.runtime)
|
||||||
|
implementation(libs.paging.compose)
|
||||||
|
implementation(libs.androidsvg.aar)
|
||||||
|
implementation(libs.compose.webview)
|
||||||
|
ksp(libs.room.compiler)
|
||||||
|
implementation(libs.room.runtime)
|
||||||
|
implementation(libs.room.ktx)
|
||||||
|
implementation(libs.timber)
|
||||||
|
implementation(libs.compose.shimmer)
|
||||||
}
|
}
|
||||||
13
app/proguard-rules.pro
vendored
13
app/proguard-rules.pro
vendored
@@ -12,6 +12,16 @@
|
|||||||
# public *;
|
# public *;
|
||||||
#}
|
#}
|
||||||
|
|
||||||
|
# Keep DataStore fields
|
||||||
|
-keepclassmembers class * extends com.google.protobuf.GeneratedMessageLite* {
|
||||||
|
<fields>;
|
||||||
|
}
|
||||||
|
|
||||||
|
# Keep SerialName annotation
|
||||||
|
-keepclassmembers class * {
|
||||||
|
@kotlinx.serialization.SerialName <fields>;
|
||||||
|
}
|
||||||
|
|
||||||
# Uncomment this to preserve the line number information for
|
# Uncomment this to preserve the line number information for
|
||||||
# debugging stack traces.
|
# debugging stack traces.
|
||||||
#-keepattributes SourceFile,LineNumberTable
|
#-keepattributes SourceFile,LineNumberTable
|
||||||
@@ -19,3 +29,6 @@
|
|||||||
# If you keep the line number information, uncomment this to
|
# If you keep the line number information, uncomment this to
|
||||||
# hide the original source file name.
|
# hide the original source file name.
|
||||||
#-renamesourcefileattribute SourceFile
|
#-renamesourcefileattribute SourceFile
|
||||||
|
|
||||||
|
-dontwarn kotlinx.serialization.KSerializer
|
||||||
|
-dontwarn kotlinx.serialization.Serializable
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 1,
|
||||||
|
"identityHash": "34c5a37d790e5542a93e0dc27bb3f4f1",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "tools",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `toolId` TEXT NOT NULL, `icon` TEXT NOT NULL, `platform` TEXT NOT NULL, `description` TEXT, `base` TEXT, `authorUsername` TEXT NOT NULL, `authorNickname` TEXT NOT NULL, `authorAvatar` TEXT NOT NULL, `ver` TEXT NOT NULL, `keywords` TEXT NOT NULL, `categories` TEXT NOT NULL, `source` TEXT, `dist` TEXT, `entryPoint` TEXT NOT NULL, `createTime` TEXT NOT NULL, `updateTime` TEXT NOT NULL, `isStar` INTEGER NOT NULL, `isInstalled` INTEGER NOT NULL, `upgrade` TEXT DEFAULT NULL, PRIMARY KEY(`id`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "name",
|
||||||
|
"columnName": "name",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "toolId",
|
||||||
|
"columnName": "toolId",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "icon",
|
||||||
|
"columnName": "icon",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "platform",
|
||||||
|
"columnName": "platform",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "description",
|
||||||
|
"columnName": "description",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "base",
|
||||||
|
"columnName": "base",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "authorUsername",
|
||||||
|
"columnName": "authorUsername",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "authorNickname",
|
||||||
|
"columnName": "authorNickname",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "authorAvatar",
|
||||||
|
"columnName": "authorAvatar",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "ver",
|
||||||
|
"columnName": "ver",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "keywords",
|
||||||
|
"columnName": "keywords",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "categories",
|
||||||
|
"columnName": "categories",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "source",
|
||||||
|
"columnName": "source",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "dist",
|
||||||
|
"columnName": "dist",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "entryPoint",
|
||||||
|
"columnName": "entryPoint",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "createTime",
|
||||||
|
"columnName": "createTime",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "updateTime",
|
||||||
|
"columnName": "updateTime",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isStar",
|
||||||
|
"columnName": "isStar",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isInstalled",
|
||||||
|
"columnName": "isInstalled",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "upgrade",
|
||||||
|
"columnName": "upgrade",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false,
|
||||||
|
"defaultValue": "NULL"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"views": [],
|
||||||
|
"setupQueries": [
|
||||||
|
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
||||||
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '34c5a37d790e5542a93e0dc27bb3f4f1')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,10 @@
|
|||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||||
|
android:maxSdkVersion="32"
|
||||||
|
tools:ignore="ScopedStorage" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".OxygenApplication"
|
android:name=".OxygenApplication"
|
||||||
@@ -27,6 +31,13 @@
|
|||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.VIEW"/>
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
<data android:scheme="oxygen" android:host="opentool"/>
|
||||||
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
|
|||||||
365
app/src/main/assets/template/global-variables-dark.css
Normal file
365
app/src/main/assets/template/global-variables-dark.css
Normal file
@@ -0,0 +1,365 @@
|
|||||||
|
:root {
|
||||||
|
--blue: #1677FF;
|
||||||
|
--blue1: #111a2c;
|
||||||
|
--blue2: #112545;
|
||||||
|
--blue3: #15325b;
|
||||||
|
--blue4: #15417e;
|
||||||
|
--blue5: #1554ad;
|
||||||
|
--blue6: #1668dc;
|
||||||
|
--blue7: #3c89e8;
|
||||||
|
--blue8: #65a9f3;
|
||||||
|
--blue9: #8dc5f8;
|
||||||
|
--blue10: #b7dcfa;
|
||||||
|
--purple: #722ED1;
|
||||||
|
--purple1: #1a1325;
|
||||||
|
--purple2: #24163a;
|
||||||
|
--purple3: #301c4d;
|
||||||
|
--purple4: #3e2069;
|
||||||
|
--purple5: #51258f;
|
||||||
|
--purple6: #642ab5;
|
||||||
|
--purple7: #854eca;
|
||||||
|
--purple8: #ab7ae0;
|
||||||
|
--purple9: #cda8f0;
|
||||||
|
--purple10: #ebd7fa;
|
||||||
|
--cyan: #13C2C2;
|
||||||
|
--cyan1: #112123;
|
||||||
|
--cyan2: #113536;
|
||||||
|
--cyan3: #144848;
|
||||||
|
--cyan4: #146262;
|
||||||
|
--cyan5: #138585;
|
||||||
|
--cyan6: #13a8a8;
|
||||||
|
--cyan7: #33bcb7;
|
||||||
|
--cyan8: #58d1c9;
|
||||||
|
--cyan9: #84e2d8;
|
||||||
|
--cyan10: #b2f1e8;
|
||||||
|
--green: #52C41A;
|
||||||
|
--green1: #162312;
|
||||||
|
--green2: #1d3712;
|
||||||
|
--green3: #274916;
|
||||||
|
--green4: #306317;
|
||||||
|
--green5: #3c8618;
|
||||||
|
--green6: #49aa19;
|
||||||
|
--green7: #6abe39;
|
||||||
|
--green8: #8fd460;
|
||||||
|
--green9: #b2e58b;
|
||||||
|
--green10: #d5f2bb;
|
||||||
|
--magenta: #EB2F96;
|
||||||
|
--magenta1: #291321;
|
||||||
|
--magenta2: #40162f;
|
||||||
|
--magenta3: #551c3b;
|
||||||
|
--magenta4: #75204f;
|
||||||
|
--magenta5: #a02669;
|
||||||
|
--magenta6: #cb2b83;
|
||||||
|
--magenta7: #e0529c;
|
||||||
|
--magenta8: #f37fb7;
|
||||||
|
--magenta9: #f8a8cc;
|
||||||
|
--magenta10: #fad2e3;
|
||||||
|
--pink: #EB2F96;
|
||||||
|
--pink1: #291321;
|
||||||
|
--pink2: #40162f;
|
||||||
|
--pink3: #551c3b;
|
||||||
|
--pink4: #75204f;
|
||||||
|
--pink5: #a02669;
|
||||||
|
--pink6: #cb2b83;
|
||||||
|
--pink7: #e0529c;
|
||||||
|
--pink8: #f37fb7;
|
||||||
|
--pink9: #f8a8cc;
|
||||||
|
--pink10: #fad2e3;
|
||||||
|
--red: #F5222D;
|
||||||
|
--red1: #2a1215;
|
||||||
|
--red2: #431418;
|
||||||
|
--red3: #58181c;
|
||||||
|
--red4: #791a1f;
|
||||||
|
--red5: #a61d24;
|
||||||
|
--red6: #d32029;
|
||||||
|
--red7: #e84749;
|
||||||
|
--red8: #f37370;
|
||||||
|
--red9: #f89f9a;
|
||||||
|
--red10: #fac8c3;
|
||||||
|
--orange: #FA8C16;
|
||||||
|
--orange1: #2b1d11;
|
||||||
|
--orange2: #442a11;
|
||||||
|
--orange3: #593815;
|
||||||
|
--orange4: #7c4a15;
|
||||||
|
--orange5: #aa6215;
|
||||||
|
--orange6: #d87a16;
|
||||||
|
--orange7: #e89a3c;
|
||||||
|
--orange8: #f3b765;
|
||||||
|
--orange9: #f8cf8d;
|
||||||
|
--orange10: #fae3b7;
|
||||||
|
--yellow: #FADB14;
|
||||||
|
--yellow1: #2b2611;
|
||||||
|
--yellow2: #443b11;
|
||||||
|
--yellow3: #595014;
|
||||||
|
--yellow4: #7c6e14;
|
||||||
|
--yellow5: #aa9514;
|
||||||
|
--yellow6: #d8bd14;
|
||||||
|
--yellow7: #e8d639;
|
||||||
|
--yellow8: #f3ea62;
|
||||||
|
--yellow9: #f8f48b;
|
||||||
|
--yellow10: #fafab5;
|
||||||
|
--volcano: #FA541C;
|
||||||
|
--volcano1: #2b1611;
|
||||||
|
--volcano2: #441d12;
|
||||||
|
--volcano3: #592716;
|
||||||
|
--volcano4: #7c3118;
|
||||||
|
--volcano5: #aa3e19;
|
||||||
|
--volcano6: #d84a1b;
|
||||||
|
--volcano7: #e87040;
|
||||||
|
--volcano8: #f3956a;
|
||||||
|
--volcano9: #f8b692;
|
||||||
|
--volcano10: #fad4bc;
|
||||||
|
--geekblue: #2F54EB;
|
||||||
|
--geekblue1: #131629;
|
||||||
|
--geekblue2: #161d40;
|
||||||
|
--geekblue3: #1c2755;
|
||||||
|
--geekblue4: #203175;
|
||||||
|
--geekblue5: #263ea0;
|
||||||
|
--geekblue6: #2b4acb;
|
||||||
|
--geekblue7: #5273e0;
|
||||||
|
--geekblue8: #7f9ef3;
|
||||||
|
--geekblue9: #a8c1f8;
|
||||||
|
--geekblue10: #d2e0fa;
|
||||||
|
--gold: #FAAD14;
|
||||||
|
--gold1: #2b2111;
|
||||||
|
--gold2: #443111;
|
||||||
|
--gold3: #594214;
|
||||||
|
--gold4: #7c5914;
|
||||||
|
--gold5: #aa7714;
|
||||||
|
--gold6: #d89614;
|
||||||
|
--gold7: #e8b339;
|
||||||
|
--gold8: #f3cc62;
|
||||||
|
--gold9: #f8df8b;
|
||||||
|
--gold10: #faedb5;
|
||||||
|
--lime: #A0D911;
|
||||||
|
--lime1: #1f2611;
|
||||||
|
--lime2: #2e3c10;
|
||||||
|
--lime3: #3e4f13;
|
||||||
|
--lime4: #536d13;
|
||||||
|
--lime5: #6f9412;
|
||||||
|
--lime6: #8bbb11;
|
||||||
|
--lime7: #a9d134;
|
||||||
|
--lime8: #c9e75d;
|
||||||
|
--lime9: #e4f88b;
|
||||||
|
--lime10: #f0fab5;
|
||||||
|
--colorPrimary: #453fa2;
|
||||||
|
--colorSuccess: #49aa19;
|
||||||
|
--colorWarning: #d89614;
|
||||||
|
--colorError: #dc4446;
|
||||||
|
--colorInfo: #1668dc;
|
||||||
|
--colorLink: #1668dc;
|
||||||
|
--colorTextBase: #fff;
|
||||||
|
--colorBgBase: #000;
|
||||||
|
--fontFamily: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial,
|
||||||
|
'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
|
||||||
|
'Noto Color Emoji';
|
||||||
|
--fontFamilyCode: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace;
|
||||||
|
--fontSize: 14;
|
||||||
|
--lineWidth: 1;
|
||||||
|
--lineType: solid;
|
||||||
|
--motionUnit: 0.1;
|
||||||
|
--motionBase: 0;
|
||||||
|
--motionEaseOutCirc: cubic-bezier(0.08, 0.82, 0.17, 1);
|
||||||
|
--motionEaseInOutCirc: cubic-bezier(0.78, 0.14, 0.15, 0.86);
|
||||||
|
--motionEaseOut: cubic-bezier(0.215, 0.61, 0.355, 1);
|
||||||
|
--motionEaseInOut: cubic-bezier(0.645, 0.045, 0.355, 1);
|
||||||
|
--motionEaseOutBack: cubic-bezier(0.12, 0.4, 0.29, 1.46);
|
||||||
|
--motionEaseInBack: cubic-bezier(0.71, -0.46, 0.88, 0.6);
|
||||||
|
--motionEaseInQuint: cubic-bezier(0.755, 0.05, 0.855, 0.06);
|
||||||
|
--motionEaseOutQuint: cubic-bezier(0.23, 1, 0.32, 1);
|
||||||
|
--borderRadius: 6;
|
||||||
|
--sizeUnit: 4;
|
||||||
|
--sizeStep: 4;
|
||||||
|
--sizePopupArrow: 16;
|
||||||
|
--controlHeight: 32;
|
||||||
|
--zIndexBase: 0;
|
||||||
|
--zIndexPopupBase: 1000;
|
||||||
|
--opacityImage: 1;
|
||||||
|
--colorLinkHover: #4E47BB;
|
||||||
|
--colorText: rgba(255, 255, 255, 0.85);
|
||||||
|
--colorTextSecondary: rgba(255, 255, 255, 0.65);
|
||||||
|
--colorTextTertiary: rgba(255, 255, 255, 0.45);
|
||||||
|
--colorTextQuaternary: rgba(255, 255, 255, 0.25);
|
||||||
|
--colorFill: rgba(255, 255, 255, 0.18);
|
||||||
|
--colorFillSecondary: rgba(255, 255, 255, 0.12);
|
||||||
|
--colorFillTertiary: rgba(255, 255, 255, 0.08);
|
||||||
|
--colorFillQuaternary: rgba(255, 255, 255, 0.04);
|
||||||
|
--colorBgSolid: rgba(255, 255, 255, 0.95);
|
||||||
|
--colorBgSolidHover: rgb(255, 255, 255);
|
||||||
|
--colorBgSolidActive: rgba(255, 255, 255, 0.9);
|
||||||
|
--colorBgLayout: #000000;
|
||||||
|
--colorBgContainer: #141414;
|
||||||
|
--colorBgElevated: #1f1f1f;
|
||||||
|
--colorBgSpotlight: #424242;
|
||||||
|
--colorBgBlur: rgba(255, 255, 255, 0.04);
|
||||||
|
--colorBorder: #424242;
|
||||||
|
--colorBorderSecondary: #303030;
|
||||||
|
--colorPrimaryBg: #161622;
|
||||||
|
--colorPrimaryBgHover: #1c1b34;
|
||||||
|
--colorPrimaryBorder: #252346;
|
||||||
|
--colorPrimaryBorderHover: #2e2b5f;
|
||||||
|
--colorPrimaryHover: #6b62b5;
|
||||||
|
--colorPrimaryActive: #3a3581;
|
||||||
|
--colorPrimaryTextHover: #6b62b5;
|
||||||
|
--colorPrimaryText: #453fa2;
|
||||||
|
--colorPrimaryTextActive: #3a3581;
|
||||||
|
--colorSuccessBg: #162312;
|
||||||
|
--colorSuccessBgHover: #1d3712;
|
||||||
|
--colorSuccessBorder: #274916;
|
||||||
|
--colorSuccessBorderHover: #306317;
|
||||||
|
--colorSuccessHover: #306317;
|
||||||
|
--colorSuccessActive: #3c8618;
|
||||||
|
--colorSuccessTextHover: #6abe39;
|
||||||
|
--colorSuccessText: #49aa19;
|
||||||
|
--colorSuccessTextActive: #3c8618;
|
||||||
|
--colorErrorBg: #2c1618;
|
||||||
|
--colorErrorBgHover: #451d1f;
|
||||||
|
--colorErrorBgFilledHover: #441e1f;
|
||||||
|
--colorErrorBgActive: #5b2526;
|
||||||
|
--colorErrorBorder: #5b2526;
|
||||||
|
--colorErrorBorderHover: #7e2e2f;
|
||||||
|
--colorErrorHover: #e86e6b;
|
||||||
|
--colorErrorActive: #ad393a;
|
||||||
|
--colorErrorTextHover: #e86e6b;
|
||||||
|
--colorErrorText: #dc4446;
|
||||||
|
--colorErrorTextActive: #ad393a;
|
||||||
|
--colorWarningBg: #2b2111;
|
||||||
|
--colorWarningBgHover: #443111;
|
||||||
|
--colorWarningBorder: #594214;
|
||||||
|
--colorWarningBorderHover: #7c5914;
|
||||||
|
--colorWarningHover: #7c5914;
|
||||||
|
--colorWarningActive: #aa7714;
|
||||||
|
--colorWarningTextHover: #e8b339;
|
||||||
|
--colorWarningText: #d89614;
|
||||||
|
--colorWarningTextActive: #aa7714;
|
||||||
|
--colorInfoBg: #111a2c;
|
||||||
|
--colorInfoBgHover: #112545;
|
||||||
|
--colorInfoBorder: #15325b;
|
||||||
|
--colorInfoBorderHover: #15417e;
|
||||||
|
--colorInfoHover: #15417e;
|
||||||
|
--colorInfoActive: #1554ad;
|
||||||
|
--colorInfoTextHover: #3c89e8;
|
||||||
|
--colorInfoText: #1668dc;
|
||||||
|
--colorInfoTextActive: #1554ad;
|
||||||
|
--colorLinkActive: #1554ad;
|
||||||
|
--colorBgMask: rgba(0, 0, 0, 0.45);
|
||||||
|
--colorWhite: #fff;
|
||||||
|
--fontSizeSM: 12;
|
||||||
|
--fontSizeLG: 16;
|
||||||
|
--fontSizeXL: 20;
|
||||||
|
--fontSizeHeading1: 38;
|
||||||
|
--fontSizeHeading2: 30;
|
||||||
|
--fontSizeHeading3: 24;
|
||||||
|
--fontSizeHeading4: 20;
|
||||||
|
--fontSizeHeading5: 16;
|
||||||
|
--lineHeight: 1.5714285714285714;
|
||||||
|
--lineHeightLG: 1.5;
|
||||||
|
--lineHeightSM: 1.6666666666666667;
|
||||||
|
--lineHeightHeading1: 1.2105263157894737;
|
||||||
|
--lineHeightHeading2: 1.2666666666666666;
|
||||||
|
--lineHeightHeading3: 1.3333333333333333;
|
||||||
|
--lineHeightHeading4: 1.4;
|
||||||
|
--lineHeightHeading5: 1.5;
|
||||||
|
--sizeXXL: 48;
|
||||||
|
--sizeXL: 32;
|
||||||
|
--sizeLG: 24;
|
||||||
|
--sizeMD: 20;
|
||||||
|
--sizeMS: 16;
|
||||||
|
--size: 16;
|
||||||
|
--sizeSM: 12;
|
||||||
|
--sizeXS: 8;
|
||||||
|
--sizeXXS: 4;
|
||||||
|
--controlHeightSM: 24;
|
||||||
|
--controlHeightXS: 16;
|
||||||
|
--controlHeightLG: 40;
|
||||||
|
--motionDurationFast: 0.1s;
|
||||||
|
--motionDurationMid: 0.2s;
|
||||||
|
--motionDurationSlow: 0.3s;
|
||||||
|
--lineWidthBold: 2;
|
||||||
|
--borderRadiusXS: 2;
|
||||||
|
--borderRadiusSM: 4;
|
||||||
|
--borderRadiusLG: 8;
|
||||||
|
--borderRadiusOuter: 4;
|
||||||
|
--colorFillContent: rgba(255, 255, 255, 0.12);
|
||||||
|
--colorFillContentHover: rgba(255, 255, 255, 0.18);
|
||||||
|
--colorFillAlter: rgba(255, 255, 255, 0.04);
|
||||||
|
--colorBgContainerDisabled: rgba(255, 255, 255, 0.08);
|
||||||
|
--colorBorderBg: #141414;
|
||||||
|
--colorSplit: rgba(253, 253, 253, 0.12);
|
||||||
|
--colorTextPlaceholder: rgba(255, 255, 255, 0.25);
|
||||||
|
--colorTextDisabled: rgba(255, 255, 255, 0.25);
|
||||||
|
--colorTextHeading: rgba(255, 255, 255, 0.85);
|
||||||
|
--colorTextLabel: rgba(255, 255, 255, 0.65);
|
||||||
|
--colorTextDescription: rgba(255, 255, 255, 0.45);
|
||||||
|
--colorTextLightSolid: #fff;
|
||||||
|
--colorHighlight: #dc4446;
|
||||||
|
--colorBgTextHover: rgba(255, 255, 255, 0.12);
|
||||||
|
--colorBgTextActive: rgba(255, 255, 255, 0.18);
|
||||||
|
--colorIcon: rgba(255, 255, 255, 0.45);
|
||||||
|
--colorIconHover: rgba(255, 255, 255, 0.85);
|
||||||
|
--colorErrorOutline: rgba(238, 38, 56, 0.11);
|
||||||
|
--colorWarningOutline: rgba(173, 107, 0, 0.15);
|
||||||
|
--fontSizeIcon: 12;
|
||||||
|
--lineWidthFocus: 3;
|
||||||
|
--controlOutlineWidth: 2;
|
||||||
|
--controlInteractiveSize: 16;
|
||||||
|
--controlItemBgHover: rgba(255, 255, 255, 0.08);
|
||||||
|
--controlItemBgActive: #161622;
|
||||||
|
--controlItemBgActiveHover: #1c1b34;
|
||||||
|
--controlItemBgActiveDisabled: rgba(255, 255, 255, 0.18);
|
||||||
|
--controlOutline: rgba(53, 53, 253, 0.06);
|
||||||
|
--fontWeightStrong: 600;
|
||||||
|
--opacityLoading: 0.65;
|
||||||
|
--linkDecoration: none;
|
||||||
|
--linkHoverDecoration: none;
|
||||||
|
--linkFocusDecoration: none;
|
||||||
|
--controlPaddingHorizontal: 12;
|
||||||
|
--controlPaddingHorizontalSM: 8;
|
||||||
|
--paddingXXS: 4;
|
||||||
|
--paddingXS: 8;
|
||||||
|
--paddingSM: 12;
|
||||||
|
--padding: 16;
|
||||||
|
--paddingMD: 20;
|
||||||
|
--paddingLG: 24;
|
||||||
|
--paddingXL: 32;
|
||||||
|
--paddingContentHorizontalLG: 24;
|
||||||
|
--paddingContentVerticalLG: 16;
|
||||||
|
--paddingContentHorizontal: 16;
|
||||||
|
--paddingContentVertical: 12;
|
||||||
|
--paddingContentHorizontalSM: 16;
|
||||||
|
--paddingContentVerticalSM: 8;
|
||||||
|
--marginXXS: 4;
|
||||||
|
--marginXS: 8;
|
||||||
|
--marginSM: 12;
|
||||||
|
--margin: 16;
|
||||||
|
--marginMD: 20;
|
||||||
|
--marginLG: 24;
|
||||||
|
--marginXL: 32;
|
||||||
|
--marginXXL: 48;
|
||||||
|
--boxShadow: 0 6px 16px 0 rgba(0, 0, 0, 0.08),
|
||||||
|
0 3px 6px -4px rgba(0, 0, 0, 0.12),
|
||||||
|
0 9px 28px 8px rgba(0, 0, 0, 0.05);
|
||||||
|
--boxShadowSecondary: 0 6px 16px 0 rgba(0, 0, 0, 0.08),
|
||||||
|
0 3px 6px -4px rgba(0, 0, 0, 0.12),
|
||||||
|
0 9px 28px 8px rgba(0, 0, 0, 0.05);
|
||||||
|
--boxShadowTertiary: 0 1px 2px 0 rgba(0, 0, 0, 0.03),
|
||||||
|
0 1px 6px -1px rgba(0, 0, 0, 0.02),
|
||||||
|
0 2px 4px 0 rgba(0, 0, 0, 0.02);
|
||||||
|
--screenXS: 480;
|
||||||
|
--screenXSMin: 480;
|
||||||
|
--screenXSMax: 575;
|
||||||
|
--screenSM: 576;
|
||||||
|
--screenSMMin: 576;
|
||||||
|
--screenSMMax: 767;
|
||||||
|
--screenMD: 768;
|
||||||
|
--screenMDMin: 768;
|
||||||
|
--screenMDMax: 991;
|
||||||
|
--screenLG: 992;
|
||||||
|
--screenLGMin: 992;
|
||||||
|
--screenLGMax: 1199;
|
||||||
|
--screenXL: 1200;
|
||||||
|
--screenXLMin: 1200;
|
||||||
|
--screenXLMax: 1599;
|
||||||
|
--screenXXL: 1600;
|
||||||
|
--screenXXLMin: 1600;
|
||||||
|
}
|
||||||
518
app/src/main/assets/template/global-variables-dark.js
Normal file
518
app/src/main/assets/template/global-variables-dark.js
Normal file
@@ -0,0 +1,518 @@
|
|||||||
|
const globalVariables = {
|
||||||
|
OxygenTheme: {
|
||||||
|
isDarkMode: true,
|
||||||
|
blue: '#1677FF',
|
||||||
|
purple: '#722ED1',
|
||||||
|
cyan: '#13C2C2',
|
||||||
|
green: '#52C41A',
|
||||||
|
magenta: '#EB2F96',
|
||||||
|
pink: '#EB2F96',
|
||||||
|
red: '#F5222D',
|
||||||
|
orange: '#FA8C16',
|
||||||
|
yellow: '#FADB14',
|
||||||
|
volcano: '#FA541C',
|
||||||
|
geekblue: '#2F54EB',
|
||||||
|
gold: '#FAAD14',
|
||||||
|
lime: '#A0D911',
|
||||||
|
colorPrimary: '#453fa2',
|
||||||
|
colorSuccess: '#49aa19',
|
||||||
|
colorWarning: '#d89614',
|
||||||
|
colorError: '#dc4446',
|
||||||
|
colorInfo: '#1668dc',
|
||||||
|
colorLink: '#1668dc',
|
||||||
|
colorTextBase: '#fff',
|
||||||
|
colorBgBase: '#000',
|
||||||
|
fontFamily:
|
||||||
|
"-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial,\n'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',\n'Noto Color Emoji'",
|
||||||
|
fontFamilyCode: "'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace",
|
||||||
|
fontSize: 14,
|
||||||
|
lineWidth: 1,
|
||||||
|
lineType: 'solid',
|
||||||
|
motionUnit: 0.1,
|
||||||
|
motionBase: 0,
|
||||||
|
motionEaseOutCirc: 'cubic-bezier(0.08, 0.82, 0.17, 1)',
|
||||||
|
motionEaseInOutCirc: 'cubic-bezier(0.78, 0.14, 0.15, 0.86)',
|
||||||
|
motionEaseOut: 'cubic-bezier(0.215, 0.61, 0.355, 1)',
|
||||||
|
motionEaseInOut: 'cubic-bezier(0.645, 0.045, 0.355, 1)',
|
||||||
|
motionEaseOutBack: 'cubic-bezier(0.12, 0.4, 0.29, 1.46)',
|
||||||
|
motionEaseInBack: 'cubic-bezier(0.71, -0.46, 0.88, 0.6)',
|
||||||
|
motionEaseInQuint: 'cubic-bezier(0.755, 0.05, 0.855, 0.06)',
|
||||||
|
motionEaseOutQuint: 'cubic-bezier(0.23, 1, 0.32, 1)',
|
||||||
|
borderRadius: 6,
|
||||||
|
sizeUnit: 4,
|
||||||
|
sizeStep: 4,
|
||||||
|
sizePopupArrow: 16,
|
||||||
|
controlHeight: 32,
|
||||||
|
zIndexBase: 0,
|
||||||
|
zIndexPopupBase: 1000,
|
||||||
|
opacityImage: 1,
|
||||||
|
motion: true,
|
||||||
|
colorLinkHover: '#4E47BB',
|
||||||
|
'blue-1': '#111a2c',
|
||||||
|
blue1: '#111a2c',
|
||||||
|
'blue-2': '#112545',
|
||||||
|
blue2: '#112545',
|
||||||
|
'blue-3': '#15325b',
|
||||||
|
blue3: '#15325b',
|
||||||
|
'blue-4': '#15417e',
|
||||||
|
blue4: '#15417e',
|
||||||
|
'blue-5': '#1554ad',
|
||||||
|
blue5: '#1554ad',
|
||||||
|
'blue-6': '#1668dc',
|
||||||
|
blue6: '#1668dc',
|
||||||
|
'blue-7': '#3c89e8',
|
||||||
|
blue7: '#3c89e8',
|
||||||
|
'blue-8': '#65a9f3',
|
||||||
|
blue8: '#65a9f3',
|
||||||
|
'blue-9': '#8dc5f8',
|
||||||
|
blue9: '#8dc5f8',
|
||||||
|
'blue-10': '#b7dcfa',
|
||||||
|
blue10: '#b7dcfa',
|
||||||
|
'purple-1': '#1a1325',
|
||||||
|
purple1: '#1a1325',
|
||||||
|
'purple-2': '#24163a',
|
||||||
|
purple2: '#24163a',
|
||||||
|
'purple-3': '#301c4d',
|
||||||
|
purple3: '#301c4d',
|
||||||
|
'purple-4': '#3e2069',
|
||||||
|
purple4: '#3e2069',
|
||||||
|
'purple-5': '#51258f',
|
||||||
|
purple5: '#51258f',
|
||||||
|
'purple-6': '#642ab5',
|
||||||
|
purple6: '#642ab5',
|
||||||
|
'purple-7': '#854eca',
|
||||||
|
purple7: '#854eca',
|
||||||
|
'purple-8': '#ab7ae0',
|
||||||
|
purple8: '#ab7ae0',
|
||||||
|
'purple-9': '#cda8f0',
|
||||||
|
purple9: '#cda8f0',
|
||||||
|
'purple-10': '#ebd7fa',
|
||||||
|
purple10: '#ebd7fa',
|
||||||
|
'cyan-1': '#112123',
|
||||||
|
cyan1: '#112123',
|
||||||
|
'cyan-2': '#113536',
|
||||||
|
cyan2: '#113536',
|
||||||
|
'cyan-3': '#144848',
|
||||||
|
cyan3: '#144848',
|
||||||
|
'cyan-4': '#146262',
|
||||||
|
cyan4: '#146262',
|
||||||
|
'cyan-5': '#138585',
|
||||||
|
cyan5: '#138585',
|
||||||
|
'cyan-6': '#13a8a8',
|
||||||
|
cyan6: '#13a8a8',
|
||||||
|
'cyan-7': '#33bcb7',
|
||||||
|
cyan7: '#33bcb7',
|
||||||
|
'cyan-8': '#58d1c9',
|
||||||
|
cyan8: '#58d1c9',
|
||||||
|
'cyan-9': '#84e2d8',
|
||||||
|
cyan9: '#84e2d8',
|
||||||
|
'cyan-10': '#b2f1e8',
|
||||||
|
cyan10: '#b2f1e8',
|
||||||
|
'green-1': '#162312',
|
||||||
|
green1: '#162312',
|
||||||
|
'green-2': '#1d3712',
|
||||||
|
green2: '#1d3712',
|
||||||
|
'green-3': '#274916',
|
||||||
|
green3: '#274916',
|
||||||
|
'green-4': '#306317',
|
||||||
|
green4: '#306317',
|
||||||
|
'green-5': '#3c8618',
|
||||||
|
green5: '#3c8618',
|
||||||
|
'green-6': '#49aa19',
|
||||||
|
green6: '#49aa19',
|
||||||
|
'green-7': '#6abe39',
|
||||||
|
green7: '#6abe39',
|
||||||
|
'green-8': '#8fd460',
|
||||||
|
green8: '#8fd460',
|
||||||
|
'green-9': '#b2e58b',
|
||||||
|
green9: '#b2e58b',
|
||||||
|
'green-10': '#d5f2bb',
|
||||||
|
green10: '#d5f2bb',
|
||||||
|
'magenta-1': '#291321',
|
||||||
|
magenta1: '#291321',
|
||||||
|
'magenta-2': '#40162f',
|
||||||
|
magenta2: '#40162f',
|
||||||
|
'magenta-3': '#551c3b',
|
||||||
|
magenta3: '#551c3b',
|
||||||
|
'magenta-4': '#75204f',
|
||||||
|
magenta4: '#75204f',
|
||||||
|
'magenta-5': '#a02669',
|
||||||
|
magenta5: '#a02669',
|
||||||
|
'magenta-6': '#cb2b83',
|
||||||
|
magenta6: '#cb2b83',
|
||||||
|
'magenta-7': '#e0529c',
|
||||||
|
magenta7: '#e0529c',
|
||||||
|
'magenta-8': '#f37fb7',
|
||||||
|
magenta8: '#f37fb7',
|
||||||
|
'magenta-9': '#f8a8cc',
|
||||||
|
magenta9: '#f8a8cc',
|
||||||
|
'magenta-10': '#fad2e3',
|
||||||
|
magenta10: '#fad2e3',
|
||||||
|
'pink-1': '#291321',
|
||||||
|
pink1: '#291321',
|
||||||
|
'pink-2': '#40162f',
|
||||||
|
pink2: '#40162f',
|
||||||
|
'pink-3': '#551c3b',
|
||||||
|
pink3: '#551c3b',
|
||||||
|
'pink-4': '#75204f',
|
||||||
|
pink4: '#75204f',
|
||||||
|
'pink-5': '#a02669',
|
||||||
|
pink5: '#a02669',
|
||||||
|
'pink-6': '#cb2b83',
|
||||||
|
pink6: '#cb2b83',
|
||||||
|
'pink-7': '#e0529c',
|
||||||
|
pink7: '#e0529c',
|
||||||
|
'pink-8': '#f37fb7',
|
||||||
|
pink8: '#f37fb7',
|
||||||
|
'pink-9': '#f8a8cc',
|
||||||
|
pink9: '#f8a8cc',
|
||||||
|
'pink-10': '#fad2e3',
|
||||||
|
pink10: '#fad2e3',
|
||||||
|
'red-1': '#2a1215',
|
||||||
|
red1: '#2a1215',
|
||||||
|
'red-2': '#431418',
|
||||||
|
red2: '#431418',
|
||||||
|
'red-3': '#58181c',
|
||||||
|
red3: '#58181c',
|
||||||
|
'red-4': '#791a1f',
|
||||||
|
red4: '#791a1f',
|
||||||
|
'red-5': '#a61d24',
|
||||||
|
red5: '#a61d24',
|
||||||
|
'red-6': '#d32029',
|
||||||
|
red6: '#d32029',
|
||||||
|
'red-7': '#e84749',
|
||||||
|
red7: '#e84749',
|
||||||
|
'red-8': '#f37370',
|
||||||
|
red8: '#f37370',
|
||||||
|
'red-9': '#f89f9a',
|
||||||
|
red9: '#f89f9a',
|
||||||
|
'red-10': '#fac8c3',
|
||||||
|
red10: '#fac8c3',
|
||||||
|
'orange-1': '#2b1d11',
|
||||||
|
orange1: '#2b1d11',
|
||||||
|
'orange-2': '#442a11',
|
||||||
|
orange2: '#442a11',
|
||||||
|
'orange-3': '#593815',
|
||||||
|
orange3: '#593815',
|
||||||
|
'orange-4': '#7c4a15',
|
||||||
|
orange4: '#7c4a15',
|
||||||
|
'orange-5': '#aa6215',
|
||||||
|
orange5: '#aa6215',
|
||||||
|
'orange-6': '#d87a16',
|
||||||
|
orange6: '#d87a16',
|
||||||
|
'orange-7': '#e89a3c',
|
||||||
|
orange7: '#e89a3c',
|
||||||
|
'orange-8': '#f3b765',
|
||||||
|
orange8: '#f3b765',
|
||||||
|
'orange-9': '#f8cf8d',
|
||||||
|
orange9: '#f8cf8d',
|
||||||
|
'orange-10': '#fae3b7',
|
||||||
|
orange10: '#fae3b7',
|
||||||
|
'yellow-1': '#2b2611',
|
||||||
|
yellow1: '#2b2611',
|
||||||
|
'yellow-2': '#443b11',
|
||||||
|
yellow2: '#443b11',
|
||||||
|
'yellow-3': '#595014',
|
||||||
|
yellow3: '#595014',
|
||||||
|
'yellow-4': '#7c6e14',
|
||||||
|
yellow4: '#7c6e14',
|
||||||
|
'yellow-5': '#aa9514',
|
||||||
|
yellow5: '#aa9514',
|
||||||
|
'yellow-6': '#d8bd14',
|
||||||
|
yellow6: '#d8bd14',
|
||||||
|
'yellow-7': '#e8d639',
|
||||||
|
yellow7: '#e8d639',
|
||||||
|
'yellow-8': '#f3ea62',
|
||||||
|
yellow8: '#f3ea62',
|
||||||
|
'yellow-9': '#f8f48b',
|
||||||
|
yellow9: '#f8f48b',
|
||||||
|
'yellow-10': '#fafab5',
|
||||||
|
yellow10: '#fafab5',
|
||||||
|
'volcano-1': '#2b1611',
|
||||||
|
volcano1: '#2b1611',
|
||||||
|
'volcano-2': '#441d12',
|
||||||
|
volcano2: '#441d12',
|
||||||
|
'volcano-3': '#592716',
|
||||||
|
volcano3: '#592716',
|
||||||
|
'volcano-4': '#7c3118',
|
||||||
|
volcano4: '#7c3118',
|
||||||
|
'volcano-5': '#aa3e19',
|
||||||
|
volcano5: '#aa3e19',
|
||||||
|
'volcano-6': '#d84a1b',
|
||||||
|
volcano6: '#d84a1b',
|
||||||
|
'volcano-7': '#e87040',
|
||||||
|
volcano7: '#e87040',
|
||||||
|
'volcano-8': '#f3956a',
|
||||||
|
volcano8: '#f3956a',
|
||||||
|
'volcano-9': '#f8b692',
|
||||||
|
volcano9: '#f8b692',
|
||||||
|
'volcano-10': '#fad4bc',
|
||||||
|
volcano10: '#fad4bc',
|
||||||
|
'geekblue-1': '#131629',
|
||||||
|
geekblue1: '#131629',
|
||||||
|
'geekblue-2': '#161d40',
|
||||||
|
geekblue2: '#161d40',
|
||||||
|
'geekblue-3': '#1c2755',
|
||||||
|
geekblue3: '#1c2755',
|
||||||
|
'geekblue-4': '#203175',
|
||||||
|
geekblue4: '#203175',
|
||||||
|
'geekblue-5': '#263ea0',
|
||||||
|
geekblue5: '#263ea0',
|
||||||
|
'geekblue-6': '#2b4acb',
|
||||||
|
geekblue6: '#2b4acb',
|
||||||
|
'geekblue-7': '#5273e0',
|
||||||
|
geekblue7: '#5273e0',
|
||||||
|
'geekblue-8': '#7f9ef3',
|
||||||
|
geekblue8: '#7f9ef3',
|
||||||
|
'geekblue-9': '#a8c1f8',
|
||||||
|
geekblue9: '#a8c1f8',
|
||||||
|
'geekblue-10': '#d2e0fa',
|
||||||
|
geekblue10: '#d2e0fa',
|
||||||
|
'gold-1': '#2b2111',
|
||||||
|
gold1: '#2b2111',
|
||||||
|
'gold-2': '#443111',
|
||||||
|
gold2: '#443111',
|
||||||
|
'gold-3': '#594214',
|
||||||
|
gold3: '#594214',
|
||||||
|
'gold-4': '#7c5914',
|
||||||
|
gold4: '#7c5914',
|
||||||
|
'gold-5': '#aa7714',
|
||||||
|
gold5: '#aa7714',
|
||||||
|
'gold-6': '#d89614',
|
||||||
|
gold6: '#d89614',
|
||||||
|
'gold-7': '#e8b339',
|
||||||
|
gold7: '#e8b339',
|
||||||
|
'gold-8': '#f3cc62',
|
||||||
|
gold8: '#f3cc62',
|
||||||
|
'gold-9': '#f8df8b',
|
||||||
|
gold9: '#f8df8b',
|
||||||
|
'gold-10': '#faedb5',
|
||||||
|
gold10: '#faedb5',
|
||||||
|
'lime-1': '#1f2611',
|
||||||
|
lime1: '#1f2611',
|
||||||
|
'lime-2': '#2e3c10',
|
||||||
|
lime2: '#2e3c10',
|
||||||
|
'lime-3': '#3e4f13',
|
||||||
|
lime3: '#3e4f13',
|
||||||
|
'lime-4': '#536d13',
|
||||||
|
lime4: '#536d13',
|
||||||
|
'lime-5': '#6f9412',
|
||||||
|
lime5: '#6f9412',
|
||||||
|
'lime-6': '#8bbb11',
|
||||||
|
lime6: '#8bbb11',
|
||||||
|
'lime-7': '#a9d134',
|
||||||
|
lime7: '#a9d134',
|
||||||
|
'lime-8': '#c9e75d',
|
||||||
|
lime8: '#c9e75d',
|
||||||
|
'lime-9': '#e4f88b',
|
||||||
|
lime9: '#e4f88b',
|
||||||
|
'lime-10': '#f0fab5',
|
||||||
|
lime10: '#f0fab5',
|
||||||
|
colorText: 'rgba(255, 255, 255, 0.85)',
|
||||||
|
colorTextSecondary: 'rgba(255, 255, 255, 0.65)',
|
||||||
|
colorTextTertiary: 'rgba(255, 255, 255, 0.45)',
|
||||||
|
colorTextQuaternary: 'rgba(255, 255, 255, 0.25)',
|
||||||
|
colorFill: 'rgba(255, 255, 255, 0.18)',
|
||||||
|
colorFillSecondary: 'rgba(255, 255, 255, 0.12)',
|
||||||
|
colorFillTertiary: 'rgba(255, 255, 255, 0.08)',
|
||||||
|
colorFillQuaternary: 'rgba(255, 255, 255, 0.04)',
|
||||||
|
colorBgSolid: 'rgba(255, 255, 255, 0.95)',
|
||||||
|
colorBgSolidHover: 'rgb(255, 255, 255)',
|
||||||
|
colorBgSolidActive: 'rgba(255, 255, 255, 0.9)',
|
||||||
|
colorBgLayout: '#000000',
|
||||||
|
colorBgContainer: '#141414',
|
||||||
|
colorBgElevated: '#1f1f1f',
|
||||||
|
colorBgSpotlight: '#424242',
|
||||||
|
colorBgBlur: 'rgba(255, 255, 255, 0.04)',
|
||||||
|
colorBorder: '#424242',
|
||||||
|
colorBorderSecondary: '#303030',
|
||||||
|
colorPrimaryBg: '#161622',
|
||||||
|
colorPrimaryBgHover: '#1c1b34',
|
||||||
|
colorPrimaryBorder: '#252346',
|
||||||
|
colorPrimaryBorderHover: '#2e2b5f',
|
||||||
|
colorPrimaryHover: '#6b62b5',
|
||||||
|
colorPrimaryActive: '#3a3581',
|
||||||
|
colorPrimaryTextHover: '#6b62b5',
|
||||||
|
colorPrimaryText: '#453fa2',
|
||||||
|
colorPrimaryTextActive: '#3a3581',
|
||||||
|
colorSuccessBg: '#162312',
|
||||||
|
colorSuccessBgHover: '#1d3712',
|
||||||
|
colorSuccessBorder: '#274916',
|
||||||
|
colorSuccessBorderHover: '#306317',
|
||||||
|
colorSuccessHover: '#306317',
|
||||||
|
colorSuccessActive: '#3c8618',
|
||||||
|
colorSuccessTextHover: '#6abe39',
|
||||||
|
colorSuccessText: '#49aa19',
|
||||||
|
colorSuccessTextActive: '#3c8618',
|
||||||
|
colorErrorBg: '#2c1618',
|
||||||
|
colorErrorBgHover: '#451d1f',
|
||||||
|
colorErrorBgFilledHover: '#441e1f',
|
||||||
|
colorErrorBgActive: '#5b2526',
|
||||||
|
colorErrorBorder: '#5b2526',
|
||||||
|
colorErrorBorderHover: '#7e2e2f',
|
||||||
|
colorErrorHover: '#e86e6b',
|
||||||
|
colorErrorActive: '#ad393a',
|
||||||
|
colorErrorTextHover: '#e86e6b',
|
||||||
|
colorErrorText: '#dc4446',
|
||||||
|
colorErrorTextActive: '#ad393a',
|
||||||
|
colorWarningBg: '#2b2111',
|
||||||
|
colorWarningBgHover: '#443111',
|
||||||
|
colorWarningBorder: '#594214',
|
||||||
|
colorWarningBorderHover: '#7c5914',
|
||||||
|
colorWarningHover: '#7c5914',
|
||||||
|
colorWarningActive: '#aa7714',
|
||||||
|
colorWarningTextHover: '#e8b339',
|
||||||
|
colorWarningText: '#d89614',
|
||||||
|
colorWarningTextActive: '#aa7714',
|
||||||
|
colorInfoBg: '#111a2c',
|
||||||
|
colorInfoBgHover: '#112545',
|
||||||
|
colorInfoBorder: '#15325b',
|
||||||
|
colorInfoBorderHover: '#15417e',
|
||||||
|
colorInfoHover: '#15417e',
|
||||||
|
colorInfoActive: '#1554ad',
|
||||||
|
colorInfoTextHover: '#3c89e8',
|
||||||
|
colorInfoText: '#1668dc',
|
||||||
|
colorInfoTextActive: '#1554ad',
|
||||||
|
colorLinkActive: '#1554ad',
|
||||||
|
colorBgMask: 'rgba(0, 0, 0, 0.45)',
|
||||||
|
colorWhite: '#fff',
|
||||||
|
fontSizeSM: 12,
|
||||||
|
fontSizeLG: 16,
|
||||||
|
fontSizeXL: 20,
|
||||||
|
fontSizeHeading1: 38,
|
||||||
|
fontSizeHeading2: 30,
|
||||||
|
fontSizeHeading3: 24,
|
||||||
|
fontSizeHeading4: 20,
|
||||||
|
fontSizeHeading5: 16,
|
||||||
|
lineHeight: 1.5714285714285714,
|
||||||
|
lineHeightLG: 1.5,
|
||||||
|
lineHeightSM: 1.6666666666666667,
|
||||||
|
fontHeight: 22,
|
||||||
|
fontHeightLG: 24,
|
||||||
|
fontHeightSM: 20,
|
||||||
|
lineHeightHeading1: 1.2105263157894737,
|
||||||
|
lineHeightHeading2: 1.2666666666666666,
|
||||||
|
lineHeightHeading3: 1.3333333333333333,
|
||||||
|
lineHeightHeading4: 1.4,
|
||||||
|
lineHeightHeading5: 1.5,
|
||||||
|
sizeXXL: 48,
|
||||||
|
sizeXL: 32,
|
||||||
|
sizeLG: 24,
|
||||||
|
sizeMD: 20,
|
||||||
|
sizeMS: 16,
|
||||||
|
size: 16,
|
||||||
|
sizeSM: 12,
|
||||||
|
sizeXS: 8,
|
||||||
|
sizeXXS: 4,
|
||||||
|
controlHeightSM: 24,
|
||||||
|
controlHeightXS: 16,
|
||||||
|
controlHeightLG: 40,
|
||||||
|
motionDurationFast: '0.1s',
|
||||||
|
motionDurationMid: '0.2s',
|
||||||
|
motionDurationSlow: '0.3s',
|
||||||
|
lineWidthBold: 2,
|
||||||
|
borderRadiusXS: 2,
|
||||||
|
borderRadiusSM: 4,
|
||||||
|
borderRadiusLG: 8,
|
||||||
|
borderRadiusOuter: 4,
|
||||||
|
colorFillContent: 'rgba(255, 255, 255, 0.12)',
|
||||||
|
colorFillContentHover: 'rgba(255, 255, 255, 0.18)',
|
||||||
|
colorFillAlter: 'rgba(255, 255, 255, 0.04)',
|
||||||
|
colorBgContainerDisabled: 'rgba(255, 255, 255, 0.08)',
|
||||||
|
colorBorderBg: '#141414',
|
||||||
|
colorSplit: 'rgba(253, 253, 253, 0.12)',
|
||||||
|
colorTextPlaceholder: 'rgba(255, 255, 255, 0.25)',
|
||||||
|
colorTextDisabled: 'rgba(255, 255, 255, 0.25)',
|
||||||
|
colorTextHeading: 'rgba(255, 255, 255, 0.85)',
|
||||||
|
colorTextLabel: 'rgba(255, 255, 255, 0.65)',
|
||||||
|
colorTextDescription: 'rgba(255, 255, 255, 0.45)',
|
||||||
|
colorTextLightSolid: '#fff',
|
||||||
|
colorHighlight: '#dc4446',
|
||||||
|
colorBgTextHover: 'rgba(255, 255, 255, 0.12)',
|
||||||
|
colorBgTextActive: 'rgba(255, 255, 255, 0.18)',
|
||||||
|
colorIcon: 'rgba(255, 255, 255, 0.45)',
|
||||||
|
colorIconHover: 'rgba(255, 255, 255, 0.85)',
|
||||||
|
colorErrorOutline: 'rgba(238, 38, 56, 0.11)',
|
||||||
|
colorWarningOutline: 'rgba(173, 107, 0, 0.15)',
|
||||||
|
fontSizeIcon: 12,
|
||||||
|
lineWidthFocus: 3,
|
||||||
|
controlOutlineWidth: 2,
|
||||||
|
controlInteractiveSize: 16,
|
||||||
|
controlItemBgHover: 'rgba(255, 255, 255, 0.08)',
|
||||||
|
controlItemBgActive: '#161622',
|
||||||
|
controlItemBgActiveHover: '#1c1b34',
|
||||||
|
controlItemBgActiveDisabled: 'rgba(255, 255, 255, 0.18)',
|
||||||
|
controlTmpOutline: 'rgba(255, 255, 255, 0.04)',
|
||||||
|
controlOutline: 'rgba(53, 53, 253, 0.06)',
|
||||||
|
fontWeightStrong: 600,
|
||||||
|
opacityLoading: 0.65,
|
||||||
|
linkDecoration: 'none',
|
||||||
|
linkHoverDecoration: 'none',
|
||||||
|
linkFocusDecoration: 'none',
|
||||||
|
controlPaddingHorizontal: 12,
|
||||||
|
controlPaddingHorizontalSM: 8,
|
||||||
|
paddingXXS: 4,
|
||||||
|
paddingXS: 8,
|
||||||
|
paddingSM: 12,
|
||||||
|
padding: 16,
|
||||||
|
paddingMD: 20,
|
||||||
|
paddingLG: 24,
|
||||||
|
paddingXL: 32,
|
||||||
|
paddingContentHorizontalLG: 24,
|
||||||
|
paddingContentVerticalLG: 16,
|
||||||
|
paddingContentHorizontal: 16,
|
||||||
|
paddingContentVertical: 12,
|
||||||
|
paddingContentHorizontalSM: 16,
|
||||||
|
paddingContentVerticalSM: 8,
|
||||||
|
marginXXS: 4,
|
||||||
|
marginXS: 8,
|
||||||
|
marginSM: 12,
|
||||||
|
margin: 16,
|
||||||
|
marginMD: 20,
|
||||||
|
marginLG: 24,
|
||||||
|
marginXL: 32,
|
||||||
|
marginXXL: 48,
|
||||||
|
boxShadow:
|
||||||
|
'\n 0 6px 16px 0 rgba(0, 0, 0, 0.08),\n 0 3px 6px -4px rgba(0, 0, 0, 0.12),\n 0 9px 28px 8px rgba(0, 0, 0, 0.05)\n ',
|
||||||
|
boxShadowSecondary:
|
||||||
|
'\n 0 6px 16px 0 rgba(0, 0, 0, 0.08),\n 0 3px 6px -4px rgba(0, 0, 0, 0.12),\n 0 9px 28px 8px rgba(0, 0, 0, 0.05)\n ',
|
||||||
|
boxShadowTertiary:
|
||||||
|
'\n 0 1px 2px 0 rgba(0, 0, 0, 0.03),\n 0 1px 6px -1px rgba(0, 0, 0, 0.02),\n 0 2px 4px 0 rgba(0, 0, 0, 0.02)\n ',
|
||||||
|
screenXS: 480,
|
||||||
|
screenXSMin: 480,
|
||||||
|
screenXSMax: 575,
|
||||||
|
screenSM: 576,
|
||||||
|
screenSMMin: 576,
|
||||||
|
screenSMMax: 767,
|
||||||
|
screenMD: 768,
|
||||||
|
screenMDMin: 768,
|
||||||
|
screenMDMax: 991,
|
||||||
|
screenLG: 992,
|
||||||
|
screenLGMin: 992,
|
||||||
|
screenLGMax: 1199,
|
||||||
|
screenXL: 1200,
|
||||||
|
screenXLMin: 1200,
|
||||||
|
screenXLMax: 1599,
|
||||||
|
screenXXL: 1600,
|
||||||
|
screenXXLMin: 1600,
|
||||||
|
boxShadowPopoverArrow: '2px 2px 5px rgba(0, 0, 0, 0.05)',
|
||||||
|
boxShadowCard:
|
||||||
|
'\n 0 1px 2px -2px rgba(0, 0, 0, 0.16),\n 0 3px 6px 0 rgba(0, 0, 0, 0.12),\n 0 5px 12px 4px rgba(0, 0, 0, 0.09)\n ',
|
||||||
|
boxShadowDrawerRight:
|
||||||
|
'\n -6px 0 16px 0 rgba(0, 0, 0, 0.08),\n -3px 0 6px -4px rgba(0, 0, 0, 0.12),\n -9px 0 28px 8px rgba(0, 0, 0, 0.05)\n ',
|
||||||
|
boxShadowDrawerLeft:
|
||||||
|
'\n 6px 0 16px 0 rgba(0, 0, 0, 0.08),\n 3px 0 6px -4px rgba(0, 0, 0, 0.12),\n 9px 0 28px 8px rgba(0, 0, 0, 0.05)\n ',
|
||||||
|
boxShadowDrawerUp:
|
||||||
|
'\n 0 6px 16px 0 rgba(0, 0, 0, 0.08),\n 0 3px 6px -4px rgba(0, 0, 0, 0.12),\n 0 9px 28px 8px rgba(0, 0, 0, 0.05)\n ',
|
||||||
|
boxShadowDrawerDown:
|
||||||
|
'\n 0 -6px 16px 0 rgba(0, 0, 0, 0.08),\n 0 -3px 6px -4px rgba(0, 0, 0, 0.12),\n 0 -9px 28px 8px rgba(0, 0, 0, 0.05)\n ',
|
||||||
|
boxShadowTabsOverflowLeft: 'inset 10px 0 8px -8px rgba(0, 0, 0, 0.08)',
|
||||||
|
boxShadowTabsOverflowRight: 'inset -10px 0 8px -8px rgba(0, 0, 0, 0.08)',
|
||||||
|
boxShadowTabsOverflowTop: 'inset 0 10px 8px -8px rgba(0, 0, 0, 0.08)',
|
||||||
|
boxShadowTabsOverflowBottom: 'inset 0 -10px 8px -8px rgba(0, 0, 0, 0.08)'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const key in globalVariables) {
|
||||||
|
globalThis[key] = globalVariables[key]
|
||||||
|
}
|
||||||
365
app/src/main/assets/template/global-variables-light.css
Normal file
365
app/src/main/assets/template/global-variables-light.css
Normal file
@@ -0,0 +1,365 @@
|
|||||||
|
:root {
|
||||||
|
--blue: #1677FF;
|
||||||
|
--blue1: #e6f4ff;
|
||||||
|
--blue2: #bae0ff;
|
||||||
|
--blue3: #91caff;
|
||||||
|
--blue4: #69b1ff;
|
||||||
|
--blue5: #4096ff;
|
||||||
|
--blue6: #1677ff;
|
||||||
|
--blue7: #0958d9;
|
||||||
|
--blue8: #003eb3;
|
||||||
|
--blue9: #002c8c;
|
||||||
|
--blue10: #001d66;
|
||||||
|
--purple: #722ED1;
|
||||||
|
--purple1: #f9f0ff;
|
||||||
|
--purple2: #efdbff;
|
||||||
|
--purple3: #d3adf7;
|
||||||
|
--purple4: #b37feb;
|
||||||
|
--purple5: #9254de;
|
||||||
|
--purple6: #722ed1;
|
||||||
|
--purple7: #531dab;
|
||||||
|
--purple8: #391085;
|
||||||
|
--purple9: #22075e;
|
||||||
|
--purple10: #120338;
|
||||||
|
--cyan: #13C2C2;
|
||||||
|
--cyan1: #e6fffb;
|
||||||
|
--cyan2: #b5f5ec;
|
||||||
|
--cyan3: #87e8de;
|
||||||
|
--cyan4: #5cdbd3;
|
||||||
|
--cyan5: #36cfc9;
|
||||||
|
--cyan6: #13c2c2;
|
||||||
|
--cyan7: #08979c;
|
||||||
|
--cyan8: #006d75;
|
||||||
|
--cyan9: #00474f;
|
||||||
|
--cyan10: #002329;
|
||||||
|
--green: #52C41A;
|
||||||
|
--green1: #f6ffed;
|
||||||
|
--green2: #d9f7be;
|
||||||
|
--green3: #b7eb8f;
|
||||||
|
--green4: #95de64;
|
||||||
|
--green5: #73d13d;
|
||||||
|
--green6: #52c41a;
|
||||||
|
--green7: #389e0d;
|
||||||
|
--green8: #237804;
|
||||||
|
--green9: #135200;
|
||||||
|
--green10: #092b00;
|
||||||
|
--magenta: #EB2F96;
|
||||||
|
--magenta1: #fff0f6;
|
||||||
|
--magenta2: #ffd6e7;
|
||||||
|
--magenta3: #ffadd2;
|
||||||
|
--magenta4: #ff85c0;
|
||||||
|
--magenta5: #f759ab;
|
||||||
|
--magenta6: #eb2f96;
|
||||||
|
--magenta7: #c41d7f;
|
||||||
|
--magenta8: #9e1068;
|
||||||
|
--magenta9: #780650;
|
||||||
|
--magenta10: #520339;
|
||||||
|
--pink: #EB2F96;
|
||||||
|
--pink1: #fff0f6;
|
||||||
|
--pink2: #ffd6e7;
|
||||||
|
--pink3: #ffadd2;
|
||||||
|
--pink4: #ff85c0;
|
||||||
|
--pink5: #f759ab;
|
||||||
|
--pink6: #eb2f96;
|
||||||
|
--pink7: #c41d7f;
|
||||||
|
--pink8: #9e1068;
|
||||||
|
--pink9: #780650;
|
||||||
|
--pink10: #520339;
|
||||||
|
--red: #F5222D;
|
||||||
|
--red1: #fff1f0;
|
||||||
|
--red2: #ffccc7;
|
||||||
|
--red3: #ffa39e;
|
||||||
|
--red4: #ff7875;
|
||||||
|
--red5: #ff4d4f;
|
||||||
|
--red6: #f5222d;
|
||||||
|
--red7: #cf1322;
|
||||||
|
--red8: #a8071a;
|
||||||
|
--red9: #820014;
|
||||||
|
--red10: #5c0011;
|
||||||
|
--orange: #FA8C16;
|
||||||
|
--orange1: #fff7e6;
|
||||||
|
--orange2: #ffe7ba;
|
||||||
|
--orange3: #ffd591;
|
||||||
|
--orange4: #ffc069;
|
||||||
|
--orange5: #ffa940;
|
||||||
|
--orange6: #fa8c16;
|
||||||
|
--orange7: #d46b08;
|
||||||
|
--orange8: #ad4e00;
|
||||||
|
--orange9: #873800;
|
||||||
|
--orange10: #612500;
|
||||||
|
--yellow: #FADB14;
|
||||||
|
--yellow1: #feffe6;
|
||||||
|
--yellow2: #ffffb8;
|
||||||
|
--yellow3: #fffb8f;
|
||||||
|
--yellow4: #fff566;
|
||||||
|
--yellow5: #ffec3d;
|
||||||
|
--yellow6: #fadb14;
|
||||||
|
--yellow7: #d4b106;
|
||||||
|
--yellow8: #ad8b00;
|
||||||
|
--yellow9: #876800;
|
||||||
|
--yellow10: #614700;
|
||||||
|
--volcano: #FA541C;
|
||||||
|
--volcano1: #fff2e8;
|
||||||
|
--volcano2: #ffd8bf;
|
||||||
|
--volcano3: #ffbb96;
|
||||||
|
--volcano4: #ff9c6e;
|
||||||
|
--volcano5: #ff7a45;
|
||||||
|
--volcano6: #fa541c;
|
||||||
|
--volcano7: #d4380d;
|
||||||
|
--volcano8: #ad2102;
|
||||||
|
--volcano9: #871400;
|
||||||
|
--volcano10: #610b00;
|
||||||
|
--geekblue: #2F54EB;
|
||||||
|
--geekblue1: #f0f5ff;
|
||||||
|
--geekblue2: #d6e4ff;
|
||||||
|
--geekblue3: #adc6ff;
|
||||||
|
--geekblue4: #85a5ff;
|
||||||
|
--geekblue5: #597ef7;
|
||||||
|
--geekblue6: #2f54eb;
|
||||||
|
--geekblue7: #1d39c4;
|
||||||
|
--geekblue8: #10239e;
|
||||||
|
--geekblue9: #061178;
|
||||||
|
--geekblue10: #030852;
|
||||||
|
--gold: #FAAD14;
|
||||||
|
--gold1: #fffbe6;
|
||||||
|
--gold2: #fff1b8;
|
||||||
|
--gold3: #ffe58f;
|
||||||
|
--gold4: #ffd666;
|
||||||
|
--gold5: #ffc53d;
|
||||||
|
--gold6: #faad14;
|
||||||
|
--gold7: #d48806;
|
||||||
|
--gold8: #ad6800;
|
||||||
|
--gold9: #874d00;
|
||||||
|
--gold10: #613400;
|
||||||
|
--lime: #A0D911;
|
||||||
|
--lime1: #fcffe6;
|
||||||
|
--lime2: #f4ffb8;
|
||||||
|
--lime3: #eaff8f;
|
||||||
|
--lime4: #d3f261;
|
||||||
|
--lime5: #bae637;
|
||||||
|
--lime6: #a0d911;
|
||||||
|
--lime7: #7cb305;
|
||||||
|
--lime8: #5b8c00;
|
||||||
|
--lime9: #3f6600;
|
||||||
|
--lime10: #254000;
|
||||||
|
--colorPrimary: #4e47bb;
|
||||||
|
--colorSuccess: #52c41a;
|
||||||
|
--colorWarning: #faad14;
|
||||||
|
--colorError: #ff4d4f;
|
||||||
|
--colorInfo: #1677ff;
|
||||||
|
--colorLink: #1677ff;
|
||||||
|
--colorTextBase: #000;
|
||||||
|
--colorBgBase: #fff;
|
||||||
|
--fontFamily: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial,
|
||||||
|
'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
|
||||||
|
'Noto Color Emoji';
|
||||||
|
--fontFamilyCode: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace;
|
||||||
|
--fontSize: 14;
|
||||||
|
--lineWidth: 1;
|
||||||
|
--lineType: solid;
|
||||||
|
--motionUnit: 0.1;
|
||||||
|
--motionBase: 0;
|
||||||
|
--motionEaseOutCirc: cubic-bezier(0.08, 0.82, 0.17, 1);
|
||||||
|
--motionEaseInOutCirc: cubic-bezier(0.78, 0.14, 0.15, 0.86);
|
||||||
|
--motionEaseOut: cubic-bezier(0.215, 0.61, 0.355, 1);
|
||||||
|
--motionEaseInOut: cubic-bezier(0.645, 0.045, 0.355, 1);
|
||||||
|
--motionEaseOutBack: cubic-bezier(0.12, 0.4, 0.29, 1.46);
|
||||||
|
--motionEaseInBack: cubic-bezier(0.71, -0.46, 0.88, 0.6);
|
||||||
|
--motionEaseInQuint: cubic-bezier(0.755, 0.05, 0.855, 0.06);
|
||||||
|
--motionEaseOutQuint: cubic-bezier(0.23, 1, 0.32, 1);
|
||||||
|
--borderRadius: 6;
|
||||||
|
--sizeUnit: 4;
|
||||||
|
--sizeStep: 4;
|
||||||
|
--sizePopupArrow: 16;
|
||||||
|
--controlHeight: 32;
|
||||||
|
--zIndexBase: 0;
|
||||||
|
--zIndexPopupBase: 1000;
|
||||||
|
--opacityImage: 1;
|
||||||
|
--colorLinkHover: #4E47BB;
|
||||||
|
--colorText: rgba(0, 0, 0, 0.88);
|
||||||
|
--colorTextSecondary: rgba(0, 0, 0, 0.65);
|
||||||
|
--colorTextTertiary: rgba(0, 0, 0, 0.45);
|
||||||
|
--colorTextQuaternary: rgba(0, 0, 0, 0.25);
|
||||||
|
--colorFill: rgba(0, 0, 0, 0.15);
|
||||||
|
--colorFillSecondary: rgba(0, 0, 0, 0.06);
|
||||||
|
--colorFillTertiary: rgba(0, 0, 0, 0.04);
|
||||||
|
--colorFillQuaternary: rgba(0, 0, 0, 0.02);
|
||||||
|
--colorBgSolid: rgb(0, 0, 0);
|
||||||
|
--colorBgSolidHover: rgba(0, 0, 0, 0.75);
|
||||||
|
--colorBgSolidActive: rgba(0, 0, 0, 0.95);
|
||||||
|
--colorBgLayout: #f5f5f5;
|
||||||
|
--colorBgContainer: #ffffff;
|
||||||
|
--colorBgElevated: #ffffff;
|
||||||
|
--colorBgSpotlight: rgba(0, 0, 0, 0.85);
|
||||||
|
--colorBgBlur: transparent;
|
||||||
|
--colorBorder: #d9d9d9;
|
||||||
|
--colorBorderSecondary: #f0f0f0;
|
||||||
|
--colorPrimaryBg: #eeebfa;
|
||||||
|
--colorPrimaryBgHover: #e2dfed;
|
||||||
|
--colorPrimaryBorder: #c6c1e0;
|
||||||
|
--colorPrimaryBorderHover: #9d94d4;
|
||||||
|
--colorPrimaryHover: #756bc7;
|
||||||
|
--colorPrimaryActive: #343194;
|
||||||
|
--colorPrimaryTextHover: #756bc7;
|
||||||
|
--colorPrimaryText: #4e47bb;
|
||||||
|
--colorPrimaryTextActive: #343194;
|
||||||
|
--colorSuccessBg: #f6ffed;
|
||||||
|
--colorSuccessBgHover: #d9f7be;
|
||||||
|
--colorSuccessBorder: #b7eb8f;
|
||||||
|
--colorSuccessBorderHover: #95de64;
|
||||||
|
--colorSuccessHover: #95de64;
|
||||||
|
--colorSuccessActive: #389e0d;
|
||||||
|
--colorSuccessTextHover: #73d13d;
|
||||||
|
--colorSuccessText: #52c41a;
|
||||||
|
--colorSuccessTextActive: #389e0d;
|
||||||
|
--colorErrorBg: #fff2f0;
|
||||||
|
--colorErrorBgHover: #fff1f0;
|
||||||
|
--colorErrorBgFilledHover: #ffdfdc;
|
||||||
|
--colorErrorBgActive: #ffccc7;
|
||||||
|
--colorErrorBorder: #ffccc7;
|
||||||
|
--colorErrorBorderHover: #ffa39e;
|
||||||
|
--colorErrorHover: #ff7875;
|
||||||
|
--colorErrorActive: #d9363e;
|
||||||
|
--colorErrorTextHover: #ff7875;
|
||||||
|
--colorErrorText: #ff4d4f;
|
||||||
|
--colorErrorTextActive: #d9363e;
|
||||||
|
--colorWarningBg: #fffbe6;
|
||||||
|
--colorWarningBgHover: #fff1b8;
|
||||||
|
--colorWarningBorder: #ffe58f;
|
||||||
|
--colorWarningBorderHover: #ffd666;
|
||||||
|
--colorWarningHover: #ffd666;
|
||||||
|
--colorWarningActive: #d48806;
|
||||||
|
--colorWarningTextHover: #ffc53d;
|
||||||
|
--colorWarningText: #faad14;
|
||||||
|
--colorWarningTextActive: #d48806;
|
||||||
|
--colorInfoBg: #e6f4ff;
|
||||||
|
--colorInfoBgHover: #bae0ff;
|
||||||
|
--colorInfoBorder: #91caff;
|
||||||
|
--colorInfoBorderHover: #69b1ff;
|
||||||
|
--colorInfoHover: #69b1ff;
|
||||||
|
--colorInfoActive: #0958d9;
|
||||||
|
--colorInfoTextHover: #4096ff;
|
||||||
|
--colorInfoText: #1677ff;
|
||||||
|
--colorInfoTextActive: #0958d9;
|
||||||
|
--colorLinkActive: #0958d9;
|
||||||
|
--colorBgMask: rgba(0, 0, 0, 0.45);
|
||||||
|
--colorWhite: #fff;
|
||||||
|
--fontSizeSM: 12;
|
||||||
|
--fontSizeLG: 16;
|
||||||
|
--fontSizeXL: 20;
|
||||||
|
--fontSizeHeading1: 38;
|
||||||
|
--fontSizeHeading2: 30;
|
||||||
|
--fontSizeHeading3: 24;
|
||||||
|
--fontSizeHeading4: 20;
|
||||||
|
--fontSizeHeading5: 16;
|
||||||
|
--lineHeight: 1.5714285714285714;
|
||||||
|
--lineHeightLG: 1.5;
|
||||||
|
--lineHeightSM: 1.6666666666666667;
|
||||||
|
--lineHeightHeading1: 1.2105263157894737;
|
||||||
|
--lineHeightHeading2: 1.2666666666666666;
|
||||||
|
--lineHeightHeading3: 1.3333333333333333;
|
||||||
|
--lineHeightHeading4: 1.4;
|
||||||
|
--lineHeightHeading5: 1.5;
|
||||||
|
--sizeXXL: 48;
|
||||||
|
--sizeXL: 32;
|
||||||
|
--sizeLG: 24;
|
||||||
|
--sizeMD: 20;
|
||||||
|
--sizeMS: 16;
|
||||||
|
--size: 16;
|
||||||
|
--sizeSM: 12;
|
||||||
|
--sizeXS: 8;
|
||||||
|
--sizeXXS: 4;
|
||||||
|
--controlHeightSM: 24;
|
||||||
|
--controlHeightXS: 16;
|
||||||
|
--controlHeightLG: 40;
|
||||||
|
--motionDurationFast: 0.1s;
|
||||||
|
--motionDurationMid: 0.2s;
|
||||||
|
--motionDurationSlow: 0.3s;
|
||||||
|
--lineWidthBold: 2;
|
||||||
|
--borderRadiusXS: 2;
|
||||||
|
--borderRadiusSM: 4;
|
||||||
|
--borderRadiusLG: 8;
|
||||||
|
--borderRadiusOuter: 4;
|
||||||
|
--colorFillContent: rgba(0, 0, 0, 0.06);
|
||||||
|
--colorFillContentHover: rgba(0, 0, 0, 0.15);
|
||||||
|
--colorFillAlter: rgba(0, 0, 0, 0.02);
|
||||||
|
--colorBgContainerDisabled: rgba(0, 0, 0, 0.04);
|
||||||
|
--colorBorderBg: #ffffff;
|
||||||
|
--colorSplit: rgba(5, 5, 5, 0.06);
|
||||||
|
--colorTextPlaceholder: rgba(0, 0, 0, 0.25);
|
||||||
|
--colorTextDisabled: rgba(0, 0, 0, 0.25);
|
||||||
|
--colorTextHeading: rgba(0, 0, 0, 0.88);
|
||||||
|
--colorTextLabel: rgba(0, 0, 0, 0.65);
|
||||||
|
--colorTextDescription: rgba(0, 0, 0, 0.45);
|
||||||
|
--colorTextLightSolid: #fff;
|
||||||
|
--colorHighlight: #ff4d4f;
|
||||||
|
--colorBgTextHover: rgba(0, 0, 0, 0.06);
|
||||||
|
--colorBgTextActive: rgba(0, 0, 0, 0.15);
|
||||||
|
--colorIcon: rgba(0, 0, 0, 0.45);
|
||||||
|
--colorIconHover: rgba(0, 0, 0, 0.88);
|
||||||
|
--colorErrorOutline: rgba(255, 38, 5, 0.06);
|
||||||
|
--colorWarningOutline: rgba(255, 215, 5, 0.1);
|
||||||
|
--fontSizeIcon: 12;
|
||||||
|
--lineWidthFocus: 3;
|
||||||
|
--controlOutlineWidth: 2;
|
||||||
|
--controlInteractiveSize: 16;
|
||||||
|
--controlItemBgHover: rgba(0, 0, 0, 0.04);
|
||||||
|
--controlItemBgActive: #eeebfa;
|
||||||
|
--controlItemBgActiveHover: #e2dfed;
|
||||||
|
--controlItemBgActiveDisabled: rgba(0, 0, 0, 0.15);
|
||||||
|
--controlOutline: rgba(42, 5, 192, 0.08);
|
||||||
|
--fontWeightStrong: 600;
|
||||||
|
--opacityLoading: 0.65;
|
||||||
|
--linkDecoration: none;
|
||||||
|
--linkHoverDecoration: none;
|
||||||
|
--linkFocusDecoration: none;
|
||||||
|
--controlPaddingHorizontal: 12;
|
||||||
|
--controlPaddingHorizontalSM: 8;
|
||||||
|
--paddingXXS: 4;
|
||||||
|
--paddingXS: 8;
|
||||||
|
--paddingSM: 12;
|
||||||
|
--padding: 16;
|
||||||
|
--paddingMD: 20;
|
||||||
|
--paddingLG: 24;
|
||||||
|
--paddingXL: 32;
|
||||||
|
--paddingContentHorizontalLG: 24;
|
||||||
|
--paddingContentVerticalLG: 16;
|
||||||
|
--paddingContentHorizontal: 16;
|
||||||
|
--paddingContentVertical: 12;
|
||||||
|
--paddingContentHorizontalSM: 16;
|
||||||
|
--paddingContentVerticalSM: 8;
|
||||||
|
--marginXXS: 4;
|
||||||
|
--marginXS: 8;
|
||||||
|
--marginSM: 12;
|
||||||
|
--margin: 16;
|
||||||
|
--marginMD: 20;
|
||||||
|
--marginLG: 24;
|
||||||
|
--marginXL: 32;
|
||||||
|
--marginXXL: 48;
|
||||||
|
--boxShadow: 0 6px 16px 0 rgba(0, 0, 0, 0.08),
|
||||||
|
0 3px 6px -4px rgba(0, 0, 0, 0.12),
|
||||||
|
0 9px 28px 8px rgba(0, 0, 0, 0.05);
|
||||||
|
--boxShadowSecondary: 0 6px 16px 0 rgba(0, 0, 0, 0.08),
|
||||||
|
0 3px 6px -4px rgba(0, 0, 0, 0.12),
|
||||||
|
0 9px 28px 8px rgba(0, 0, 0, 0.05);
|
||||||
|
--boxShadowTertiary: 0 1px 2px 0 rgba(0, 0, 0, 0.03),
|
||||||
|
0 1px 6px -1px rgba(0, 0, 0, 0.02),
|
||||||
|
0 2px 4px 0 rgba(0, 0, 0, 0.02);
|
||||||
|
--screenXS: 480;
|
||||||
|
--screenXSMin: 480;
|
||||||
|
--screenXSMax: 575;
|
||||||
|
--screenSM: 576;
|
||||||
|
--screenSMMin: 576;
|
||||||
|
--screenSMMax: 767;
|
||||||
|
--screenMD: 768;
|
||||||
|
--screenMDMin: 768;
|
||||||
|
--screenMDMax: 991;
|
||||||
|
--screenLG: 992;
|
||||||
|
--screenLGMin: 992;
|
||||||
|
--screenLGMax: 1199;
|
||||||
|
--screenXL: 1200;
|
||||||
|
--screenXLMin: 1200;
|
||||||
|
--screenXLMax: 1599;
|
||||||
|
--screenXXL: 1600;
|
||||||
|
--screenXXLMin: 1600;
|
||||||
|
}
|
||||||
519
app/src/main/assets/template/global-variables-light.js
Normal file
519
app/src/main/assets/template/global-variables-light.js
Normal file
@@ -0,0 +1,519 @@
|
|||||||
|
const globalVariables = {
|
||||||
|
OxygenTheme: {
|
||||||
|
isDarkMode: false,
|
||||||
|
blue: '#1677FF',
|
||||||
|
purple: '#722ED1',
|
||||||
|
cyan: '#13C2C2',
|
||||||
|
green: '#52C41A',
|
||||||
|
magenta: '#EB2F96',
|
||||||
|
pink: '#EB2F96',
|
||||||
|
red: '#F5222D',
|
||||||
|
orange: '#FA8C16',
|
||||||
|
yellow: '#FADB14',
|
||||||
|
volcano: '#FA541C',
|
||||||
|
geekblue: '#2F54EB',
|
||||||
|
gold: '#FAAD14',
|
||||||
|
lime: '#A0D911',
|
||||||
|
colorPrimary: '#4e47bb',
|
||||||
|
colorSuccess: '#52c41a',
|
||||||
|
colorWarning: '#faad14',
|
||||||
|
colorError: '#ff4d4f',
|
||||||
|
colorInfo: '#1677ff',
|
||||||
|
colorLink: '#1677ff',
|
||||||
|
colorTextBase: '#000',
|
||||||
|
colorBgBase: '#fff',
|
||||||
|
fontFamily:
|
||||||
|
"-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial,\n'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',\n'Noto Color Emoji'",
|
||||||
|
fontFamilyCode: "'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace",
|
||||||
|
fontSize: 14,
|
||||||
|
lineWidth: 1,
|
||||||
|
lineType: 'solid',
|
||||||
|
motionUnit: 0.1,
|
||||||
|
motionBase: 0,
|
||||||
|
motionEaseOutCirc: 'cubic-bezier(0.08, 0.82, 0.17, 1)',
|
||||||
|
motionEaseInOutCirc: 'cubic-bezier(0.78, 0.14, 0.15, 0.86)',
|
||||||
|
motionEaseOut: 'cubic-bezier(0.215, 0.61, 0.355, 1)',
|
||||||
|
motionEaseInOut: 'cubic-bezier(0.645, 0.045, 0.355, 1)',
|
||||||
|
motionEaseOutBack: 'cubic-bezier(0.12, 0.4, 0.29, 1.46)',
|
||||||
|
motionEaseInBack: 'cubic-bezier(0.71, -0.46, 0.88, 0.6)',
|
||||||
|
motionEaseInQuint: 'cubic-bezier(0.755, 0.05, 0.855, 0.06)',
|
||||||
|
motionEaseOutQuint: 'cubic-bezier(0.23, 1, 0.32, 1)',
|
||||||
|
borderRadius: 6,
|
||||||
|
sizeUnit: 4,
|
||||||
|
sizeStep: 4,
|
||||||
|
sizePopupArrow: 16,
|
||||||
|
controlHeight: 32,
|
||||||
|
zIndexBase: 0,
|
||||||
|
zIndexPopupBase: 1000,
|
||||||
|
opacityImage: 1,
|
||||||
|
motion: true,
|
||||||
|
colorLinkHover: '#4E47BB',
|
||||||
|
'blue-1': '#e6f4ff',
|
||||||
|
blue1: '#e6f4ff',
|
||||||
|
'blue-2': '#bae0ff',
|
||||||
|
blue2: '#bae0ff',
|
||||||
|
'blue-3': '#91caff',
|
||||||
|
blue3: '#91caff',
|
||||||
|
'blue-4': '#69b1ff',
|
||||||
|
blue4: '#69b1ff',
|
||||||
|
'blue-5': '#4096ff',
|
||||||
|
blue5: '#4096ff',
|
||||||
|
'blue-6': '#1677ff',
|
||||||
|
blue6: '#1677ff',
|
||||||
|
'blue-7': '#0958d9',
|
||||||
|
blue7: '#0958d9',
|
||||||
|
'blue-8': '#003eb3',
|
||||||
|
blue8: '#003eb3',
|
||||||
|
'blue-9': '#002c8c',
|
||||||
|
blue9: '#002c8c',
|
||||||
|
'blue-10': '#001d66',
|
||||||
|
blue10: '#001d66',
|
||||||
|
'purple-1': '#f9f0ff',
|
||||||
|
purple1: '#f9f0ff',
|
||||||
|
'purple-2': '#efdbff',
|
||||||
|
purple2: '#efdbff',
|
||||||
|
'purple-3': '#d3adf7',
|
||||||
|
purple3: '#d3adf7',
|
||||||
|
'purple-4': '#b37feb',
|
||||||
|
purple4: '#b37feb',
|
||||||
|
'purple-5': '#9254de',
|
||||||
|
purple5: '#9254de',
|
||||||
|
'purple-6': '#722ed1',
|
||||||
|
purple6: '#722ed1',
|
||||||
|
'purple-7': '#531dab',
|
||||||
|
purple7: '#531dab',
|
||||||
|
'purple-8': '#391085',
|
||||||
|
purple8: '#391085',
|
||||||
|
'purple-9': '#22075e',
|
||||||
|
purple9: '#22075e',
|
||||||
|
'purple-10': '#120338',
|
||||||
|
purple10: '#120338',
|
||||||
|
'cyan-1': '#e6fffb',
|
||||||
|
cyan1: '#e6fffb',
|
||||||
|
'cyan-2': '#b5f5ec',
|
||||||
|
cyan2: '#b5f5ec',
|
||||||
|
'cyan-3': '#87e8de',
|
||||||
|
cyan3: '#87e8de',
|
||||||
|
'cyan-4': '#5cdbd3',
|
||||||
|
cyan4: '#5cdbd3',
|
||||||
|
'cyan-5': '#36cfc9',
|
||||||
|
cyan5: '#36cfc9',
|
||||||
|
'cyan-6': '#13c2c2',
|
||||||
|
cyan6: '#13c2c2',
|
||||||
|
'cyan-7': '#08979c',
|
||||||
|
cyan7: '#08979c',
|
||||||
|
'cyan-8': '#006d75',
|
||||||
|
cyan8: '#006d75',
|
||||||
|
'cyan-9': '#00474f',
|
||||||
|
cyan9: '#00474f',
|
||||||
|
'cyan-10': '#002329',
|
||||||
|
cyan10: '#002329',
|
||||||
|
'green-1': '#f6ffed',
|
||||||
|
green1: '#f6ffed',
|
||||||
|
'green-2': '#d9f7be',
|
||||||
|
green2: '#d9f7be',
|
||||||
|
'green-3': '#b7eb8f',
|
||||||
|
green3: '#b7eb8f',
|
||||||
|
'green-4': '#95de64',
|
||||||
|
green4: '#95de64',
|
||||||
|
'green-5': '#73d13d',
|
||||||
|
green5: '#73d13d',
|
||||||
|
'green-6': '#52c41a',
|
||||||
|
green6: '#52c41a',
|
||||||
|
'green-7': '#389e0d',
|
||||||
|
green7: '#389e0d',
|
||||||
|
'green-8': '#237804',
|
||||||
|
green8: '#237804',
|
||||||
|
'green-9': '#135200',
|
||||||
|
green9: '#135200',
|
||||||
|
'green-10': '#092b00',
|
||||||
|
green10: '#092b00',
|
||||||
|
'magenta-1': '#fff0f6',
|
||||||
|
magenta1: '#fff0f6',
|
||||||
|
'magenta-2': '#ffd6e7',
|
||||||
|
magenta2: '#ffd6e7',
|
||||||
|
'magenta-3': '#ffadd2',
|
||||||
|
magenta3: '#ffadd2',
|
||||||
|
'magenta-4': '#ff85c0',
|
||||||
|
magenta4: '#ff85c0',
|
||||||
|
'magenta-5': '#f759ab',
|
||||||
|
magenta5: '#f759ab',
|
||||||
|
'magenta-6': '#eb2f96',
|
||||||
|
magenta6: '#eb2f96',
|
||||||
|
'magenta-7': '#c41d7f',
|
||||||
|
magenta7: '#c41d7f',
|
||||||
|
'magenta-8': '#9e1068',
|
||||||
|
magenta8: '#9e1068',
|
||||||
|
'magenta-9': '#780650',
|
||||||
|
magenta9: '#780650',
|
||||||
|
'magenta-10': '#520339',
|
||||||
|
magenta10: '#520339',
|
||||||
|
'pink-1': '#fff0f6',
|
||||||
|
pink1: '#fff0f6',
|
||||||
|
'pink-2': '#ffd6e7',
|
||||||
|
pink2: '#ffd6e7',
|
||||||
|
'pink-3': '#ffadd2',
|
||||||
|
pink3: '#ffadd2',
|
||||||
|
'pink-4': '#ff85c0',
|
||||||
|
pink4: '#ff85c0',
|
||||||
|
'pink-5': '#f759ab',
|
||||||
|
pink5: '#f759ab',
|
||||||
|
'pink-6': '#eb2f96',
|
||||||
|
pink6: '#eb2f96',
|
||||||
|
'pink-7': '#c41d7f',
|
||||||
|
pink7: '#c41d7f',
|
||||||
|
'pink-8': '#9e1068',
|
||||||
|
pink8: '#9e1068',
|
||||||
|
'pink-9': '#780650',
|
||||||
|
pink9: '#780650',
|
||||||
|
'pink-10': '#520339',
|
||||||
|
pink10: '#520339',
|
||||||
|
'red-1': '#fff1f0',
|
||||||
|
red1: '#fff1f0',
|
||||||
|
'red-2': '#ffccc7',
|
||||||
|
red2: '#ffccc7',
|
||||||
|
'red-3': '#ffa39e',
|
||||||
|
red3: '#ffa39e',
|
||||||
|
'red-4': '#ff7875',
|
||||||
|
red4: '#ff7875',
|
||||||
|
'red-5': '#ff4d4f',
|
||||||
|
red5: '#ff4d4f',
|
||||||
|
'red-6': '#f5222d',
|
||||||
|
red6: '#f5222d',
|
||||||
|
'red-7': '#cf1322',
|
||||||
|
red7: '#cf1322',
|
||||||
|
'red-8': '#a8071a',
|
||||||
|
red8: '#a8071a',
|
||||||
|
'red-9': '#820014',
|
||||||
|
red9: '#820014',
|
||||||
|
'red-10': '#5c0011',
|
||||||
|
red10: '#5c0011',
|
||||||
|
'orange-1': '#fff7e6',
|
||||||
|
orange1: '#fff7e6',
|
||||||
|
'orange-2': '#ffe7ba',
|
||||||
|
orange2: '#ffe7ba',
|
||||||
|
'orange-3': '#ffd591',
|
||||||
|
orange3: '#ffd591',
|
||||||
|
'orange-4': '#ffc069',
|
||||||
|
orange4: '#ffc069',
|
||||||
|
'orange-5': '#ffa940',
|
||||||
|
orange5: '#ffa940',
|
||||||
|
'orange-6': '#fa8c16',
|
||||||
|
orange6: '#fa8c16',
|
||||||
|
'orange-7': '#d46b08',
|
||||||
|
orange7: '#d46b08',
|
||||||
|
'orange-8': '#ad4e00',
|
||||||
|
orange8: '#ad4e00',
|
||||||
|
'orange-9': '#873800',
|
||||||
|
orange9: '#873800',
|
||||||
|
'orange-10': '#612500',
|
||||||
|
orange10: '#612500',
|
||||||
|
'yellow-1': '#feffe6',
|
||||||
|
yellow1: '#feffe6',
|
||||||
|
'yellow-2': '#ffffb8',
|
||||||
|
yellow2: '#ffffb8',
|
||||||
|
'yellow-3': '#fffb8f',
|
||||||
|
yellow3: '#fffb8f',
|
||||||
|
'yellow-4': '#fff566',
|
||||||
|
yellow4: '#fff566',
|
||||||
|
'yellow-5': '#ffec3d',
|
||||||
|
yellow5: '#ffec3d',
|
||||||
|
'yellow-6': '#fadb14',
|
||||||
|
yellow6: '#fadb14',
|
||||||
|
'yellow-7': '#d4b106',
|
||||||
|
yellow7: '#d4b106',
|
||||||
|
'yellow-8': '#ad8b00',
|
||||||
|
yellow8: '#ad8b00',
|
||||||
|
'yellow-9': '#876800',
|
||||||
|
yellow9: '#876800',
|
||||||
|
'yellow-10': '#614700',
|
||||||
|
yellow10: '#614700',
|
||||||
|
'volcano-1': '#fff2e8',
|
||||||
|
volcano1: '#fff2e8',
|
||||||
|
'volcano-2': '#ffd8bf',
|
||||||
|
volcano2: '#ffd8bf',
|
||||||
|
'volcano-3': '#ffbb96',
|
||||||
|
volcano3: '#ffbb96',
|
||||||
|
'volcano-4': '#ff9c6e',
|
||||||
|
volcano4: '#ff9c6e',
|
||||||
|
'volcano-5': '#ff7a45',
|
||||||
|
volcano5: '#ff7a45',
|
||||||
|
'volcano-6': '#fa541c',
|
||||||
|
volcano6: '#fa541c',
|
||||||
|
'volcano-7': '#d4380d',
|
||||||
|
volcano7: '#d4380d',
|
||||||
|
'volcano-8': '#ad2102',
|
||||||
|
volcano8: '#ad2102',
|
||||||
|
'volcano-9': '#871400',
|
||||||
|
volcano9: '#871400',
|
||||||
|
'volcano-10': '#610b00',
|
||||||
|
volcano10: '#610b00',
|
||||||
|
'geekblue-1': '#f0f5ff',
|
||||||
|
geekblue1: '#f0f5ff',
|
||||||
|
'geekblue-2': '#d6e4ff',
|
||||||
|
geekblue2: '#d6e4ff',
|
||||||
|
'geekblue-3': '#adc6ff',
|
||||||
|
geekblue3: '#adc6ff',
|
||||||
|
'geekblue-4': '#85a5ff',
|
||||||
|
geekblue4: '#85a5ff',
|
||||||
|
'geekblue-5': '#597ef7',
|
||||||
|
geekblue5: '#597ef7',
|
||||||
|
'geekblue-6': '#2f54eb',
|
||||||
|
geekblue6: '#2f54eb',
|
||||||
|
'geekblue-7': '#1d39c4',
|
||||||
|
geekblue7: '#1d39c4',
|
||||||
|
'geekblue-8': '#10239e',
|
||||||
|
geekblue8: '#10239e',
|
||||||
|
'geekblue-9': '#061178',
|
||||||
|
geekblue9: '#061178',
|
||||||
|
'geekblue-10': '#030852',
|
||||||
|
geekblue10: '#030852',
|
||||||
|
'gold-1': '#fffbe6',
|
||||||
|
gold1: '#fffbe6',
|
||||||
|
'gold-2': '#fff1b8',
|
||||||
|
gold2: '#fff1b8',
|
||||||
|
'gold-3': '#ffe58f',
|
||||||
|
gold3: '#ffe58f',
|
||||||
|
'gold-4': '#ffd666',
|
||||||
|
gold4: '#ffd666',
|
||||||
|
'gold-5': '#ffc53d',
|
||||||
|
gold5: '#ffc53d',
|
||||||
|
'gold-6': '#faad14',
|
||||||
|
gold6: '#faad14',
|
||||||
|
'gold-7': '#d48806',
|
||||||
|
gold7: '#d48806',
|
||||||
|
'gold-8': '#ad6800',
|
||||||
|
gold8: '#ad6800',
|
||||||
|
'gold-9': '#874d00',
|
||||||
|
gold9: '#874d00',
|
||||||
|
'gold-10': '#613400',
|
||||||
|
gold10: '#613400',
|
||||||
|
'lime-1': '#fcffe6',
|
||||||
|
lime1: '#fcffe6',
|
||||||
|
'lime-2': '#f4ffb8',
|
||||||
|
lime2: '#f4ffb8',
|
||||||
|
'lime-3': '#eaff8f',
|
||||||
|
lime3: '#eaff8f',
|
||||||
|
'lime-4': '#d3f261',
|
||||||
|
lime4: '#d3f261',
|
||||||
|
'lime-5': '#bae637',
|
||||||
|
lime5: '#bae637',
|
||||||
|
'lime-6': '#a0d911',
|
||||||
|
lime6: '#a0d911',
|
||||||
|
'lime-7': '#7cb305',
|
||||||
|
lime7: '#7cb305',
|
||||||
|
'lime-8': '#5b8c00',
|
||||||
|
lime8: '#5b8c00',
|
||||||
|
'lime-9': '#3f6600',
|
||||||
|
lime9: '#3f6600',
|
||||||
|
'lime-10': '#254000',
|
||||||
|
lime10: '#254000',
|
||||||
|
colorText: 'rgba(0, 0, 0, 0.88)',
|
||||||
|
colorTextSecondary: 'rgba(0, 0, 0, 0.65)',
|
||||||
|
colorTextTertiary: 'rgba(0, 0, 0, 0.45)',
|
||||||
|
colorTextQuaternary: 'rgba(0, 0, 0, 0.25)',
|
||||||
|
colorFill: 'rgba(0, 0, 0, 0.15)',
|
||||||
|
colorFillSecondary: 'rgba(0, 0, 0, 0.06)',
|
||||||
|
colorFillTertiary: 'rgba(0, 0, 0, 0.04)',
|
||||||
|
colorFillQuaternary: 'rgba(0, 0, 0, 0.02)',
|
||||||
|
colorBgSolid: 'rgb(0, 0, 0)',
|
||||||
|
colorBgSolidHover: 'rgba(0, 0, 0, 0.75)',
|
||||||
|
colorBgSolidActive: 'rgba(0, 0, 0, 0.95)',
|
||||||
|
colorBgLayout: '#f5f5f5',
|
||||||
|
colorBgContainer: '#ffffff',
|
||||||
|
colorBgElevated: '#ffffff',
|
||||||
|
colorBgSpotlight: 'rgba(0, 0, 0, 0.85)',
|
||||||
|
colorBgBlur: 'transparent',
|
||||||
|
colorBorder: '#d9d9d9',
|
||||||
|
colorBorderSecondary: '#f0f0f0',
|
||||||
|
colorPrimaryBg: '#eeebfa',
|
||||||
|
colorPrimaryBgHover: '#e2dfed',
|
||||||
|
colorPrimaryBorder: '#c6c1e0',
|
||||||
|
colorPrimaryBorderHover: '#9d94d4',
|
||||||
|
colorPrimaryHover: '#756bc7',
|
||||||
|
colorPrimaryActive: '#343194',
|
||||||
|
colorPrimaryTextHover: '#756bc7',
|
||||||
|
colorPrimaryText: '#4e47bb',
|
||||||
|
colorPrimaryTextActive: '#343194',
|
||||||
|
colorSuccessBg: '#f6ffed',
|
||||||
|
colorSuccessBgHover: '#d9f7be',
|
||||||
|
colorSuccessBorder: '#b7eb8f',
|
||||||
|
colorSuccessBorderHover: '#95de64',
|
||||||
|
colorSuccessHover: '#95de64',
|
||||||
|
colorSuccessActive: '#389e0d',
|
||||||
|
colorSuccessTextHover: '#73d13d',
|
||||||
|
colorSuccessText: '#52c41a',
|
||||||
|
colorSuccessTextActive: '#389e0d',
|
||||||
|
colorErrorBg: '#fff2f0',
|
||||||
|
colorErrorBgHover: '#fff1f0',
|
||||||
|
colorErrorBgFilledHover: '#ffdfdc',
|
||||||
|
colorErrorBgActive: '#ffccc7',
|
||||||
|
colorErrorBorder: '#ffccc7',
|
||||||
|
colorErrorBorderHover: '#ffa39e',
|
||||||
|
colorErrorHover: '#ff7875',
|
||||||
|
colorErrorActive: '#d9363e',
|
||||||
|
colorErrorTextHover: '#ff7875',
|
||||||
|
colorErrorText: '#ff4d4f',
|
||||||
|
colorErrorTextActive: '#d9363e',
|
||||||
|
colorWarningBg: '#fffbe6',
|
||||||
|
colorWarningBgHover: '#fff1b8',
|
||||||
|
colorWarningBorder: '#ffe58f',
|
||||||
|
colorWarningBorderHover: '#ffd666',
|
||||||
|
colorWarningHover: '#ffd666',
|
||||||
|
colorWarningActive: '#d48806',
|
||||||
|
colorWarningTextHover: '#ffc53d',
|
||||||
|
colorWarningText: '#faad14',
|
||||||
|
colorWarningTextActive: '#d48806',
|
||||||
|
colorInfoBg: '#e6f4ff',
|
||||||
|
colorInfoBgHover: '#bae0ff',
|
||||||
|
colorInfoBorder: '#91caff',
|
||||||
|
colorInfoBorderHover: '#69b1ff',
|
||||||
|
colorInfoHover: '#69b1ff',
|
||||||
|
colorInfoActive: '#0958d9',
|
||||||
|
colorInfoTextHover: '#4096ff',
|
||||||
|
colorInfoText: '#1677ff',
|
||||||
|
colorInfoTextActive: '#0958d9',
|
||||||
|
colorLinkActive: '#0958d9',
|
||||||
|
colorBgMask: 'rgba(0, 0, 0, 0.45)',
|
||||||
|
colorWhite: '#fff',
|
||||||
|
fontSizeSM: 12,
|
||||||
|
fontSizeLG: 16,
|
||||||
|
fontSizeXL: 20,
|
||||||
|
fontSizeHeading1: 38,
|
||||||
|
fontSizeHeading2: 30,
|
||||||
|
fontSizeHeading3: 24,
|
||||||
|
fontSizeHeading4: 20,
|
||||||
|
fontSizeHeading5: 16,
|
||||||
|
lineHeight: 1.5714285714285714,
|
||||||
|
lineHeightLG: 1.5,
|
||||||
|
lineHeightSM: 1.6666666666666667,
|
||||||
|
fontHeight: 22,
|
||||||
|
fontHeightLG: 24,
|
||||||
|
fontHeightSM: 20,
|
||||||
|
lineHeightHeading1: 1.2105263157894737,
|
||||||
|
lineHeightHeading2: 1.2666666666666666,
|
||||||
|
lineHeightHeading3: 1.3333333333333333,
|
||||||
|
lineHeightHeading4: 1.4,
|
||||||
|
lineHeightHeading5: 1.5,
|
||||||
|
sizeXXL: 48,
|
||||||
|
sizeXL: 32,
|
||||||
|
sizeLG: 24,
|
||||||
|
sizeMD: 20,
|
||||||
|
sizeMS: 16,
|
||||||
|
size: 16,
|
||||||
|
sizeSM: 12,
|
||||||
|
sizeXS: 8,
|
||||||
|
sizeXXS: 4,
|
||||||
|
controlHeightSM: 24,
|
||||||
|
controlHeightXS: 16,
|
||||||
|
controlHeightLG: 40,
|
||||||
|
motionDurationFast: '0.1s',
|
||||||
|
motionDurationMid: '0.2s',
|
||||||
|
motionDurationSlow: '0.3s',
|
||||||
|
lineWidthBold: 2,
|
||||||
|
borderRadiusXS: 2,
|
||||||
|
borderRadiusSM: 4,
|
||||||
|
borderRadiusLG: 8,
|
||||||
|
borderRadiusOuter: 4,
|
||||||
|
colorFillContent: 'rgba(0, 0, 0, 0.06)',
|
||||||
|
colorFillContentHover: 'rgba(0, 0, 0, 0.15)',
|
||||||
|
colorFillAlter: 'rgba(0, 0, 0, 0.02)',
|
||||||
|
colorBgContainerDisabled: 'rgba(0, 0, 0, 0.04)',
|
||||||
|
colorBorderBg: '#ffffff',
|
||||||
|
colorSplit: 'rgba(5, 5, 5, 0.06)',
|
||||||
|
colorTextPlaceholder: 'rgba(0, 0, 0, 0.25)',
|
||||||
|
colorTextDisabled: 'rgba(0, 0, 0, 0.25)',
|
||||||
|
colorTextHeading: 'rgba(0, 0, 0, 0.88)',
|
||||||
|
colorTextLabel: 'rgba(0, 0, 0, 0.65)',
|
||||||
|
colorTextDescription: 'rgba(0, 0, 0, 0.45)',
|
||||||
|
colorTextLightSolid: '#fff',
|
||||||
|
colorHighlight: '#ff4d4f',
|
||||||
|
colorBgTextHover: 'rgba(0, 0, 0, 0.06)',
|
||||||
|
colorBgTextActive: 'rgba(0, 0, 0, 0.15)',
|
||||||
|
colorIcon: 'rgba(0, 0, 0, 0.45)',
|
||||||
|
colorIconHover: 'rgba(0, 0, 0, 0.88)',
|
||||||
|
colorErrorOutline: 'rgba(255, 38, 5, 0.06)',
|
||||||
|
colorWarningOutline: 'rgba(255, 215, 5, 0.1)',
|
||||||
|
fontSizeIcon: 12,
|
||||||
|
lineWidthFocus: 3,
|
||||||
|
controlOutlineWidth: 2,
|
||||||
|
controlInteractiveSize: 16,
|
||||||
|
controlItemBgHover: 'rgba(0, 0, 0, 0.04)',
|
||||||
|
controlItemBgActive: '#eeebfa',
|
||||||
|
controlItemBgActiveHover: '#e2dfed',
|
||||||
|
controlItemBgActiveDisabled: 'rgba(0, 0, 0, 0.15)',
|
||||||
|
controlTmpOutline: 'rgba(0, 0, 0, 0.02)',
|
||||||
|
controlOutline: 'rgba(42, 5, 192, 0.08)',
|
||||||
|
fontWeightStrong: 600,
|
||||||
|
opacityLoading: 0.65,
|
||||||
|
linkDecoration: 'none',
|
||||||
|
linkHoverDecoration: 'none',
|
||||||
|
linkFocusDecoration: 'none',
|
||||||
|
controlPaddingHorizontal: 12,
|
||||||
|
controlPaddingHorizontalSM: 8,
|
||||||
|
paddingXXS: 4,
|
||||||
|
paddingXS: 8,
|
||||||
|
paddingSM: 12,
|
||||||
|
padding: 16,
|
||||||
|
paddingMD: 20,
|
||||||
|
paddingLG: 24,
|
||||||
|
paddingXL: 32,
|
||||||
|
paddingContentHorizontalLG: 24,
|
||||||
|
paddingContentVerticalLG: 16,
|
||||||
|
paddingContentHorizontal: 16,
|
||||||
|
paddingContentVertical: 12,
|
||||||
|
paddingContentHorizontalSM: 16,
|
||||||
|
paddingContentVerticalSM: 8,
|
||||||
|
marginXXS: 4,
|
||||||
|
marginXS: 8,
|
||||||
|
marginSM: 12,
|
||||||
|
margin: 16,
|
||||||
|
marginMD: 20,
|
||||||
|
marginLG: 24,
|
||||||
|
marginXL: 32,
|
||||||
|
marginXXL: 48,
|
||||||
|
boxShadow:
|
||||||
|
'\n 0 6px 16px 0 rgba(0, 0, 0, 0.08),\n 0 3px 6px -4px rgba(0, 0, 0, 0.12),\n 0 9px 28px 8px rgba(0, 0, 0, 0.05)\n ',
|
||||||
|
boxShadowSecondary:
|
||||||
|
'\n 0 6px 16px 0 rgba(0, 0, 0, 0.08),\n 0 3px 6px -4px rgba(0, 0, 0, 0.12),\n 0 9px 28px 8px rgba(0, 0, 0, 0.05)\n ',
|
||||||
|
boxShadowTertiary:
|
||||||
|
'\n 0 1px 2px 0 rgba(0, 0, 0, 0.03),\n 0 1px 6px -1px rgba(0, 0, 0, 0.02),\n 0 2px 4px 0 rgba(0, 0, 0, 0.02)\n ',
|
||||||
|
screenXS: 480,
|
||||||
|
screenXSMin: 480,
|
||||||
|
screenXSMax: 575,
|
||||||
|
screenSM: 576,
|
||||||
|
screenSMMin: 576,
|
||||||
|
screenSMMax: 767,
|
||||||
|
screenMD: 768,
|
||||||
|
screenMDMin: 768,
|
||||||
|
screenMDMax: 991,
|
||||||
|
screenLG: 992,
|
||||||
|
screenLGMin: 992,
|
||||||
|
screenLGMax: 1199,
|
||||||
|
screenXL: 1200,
|
||||||
|
screenXLMin: 1200,
|
||||||
|
screenXLMax: 1599,
|
||||||
|
screenXXL: 1600,
|
||||||
|
screenXXLMin: 1600,
|
||||||
|
boxShadowPopoverArrow: '2px 2px 5px rgba(0, 0, 0, 0.05)',
|
||||||
|
boxShadowCard:
|
||||||
|
'\n 0 1px 2px -2px rgba(0, 0, 0, 0.16),\n 0 3px 6px 0 rgba(0, 0, 0, 0.12),\n 0 5px 12px 4px rgba(0, 0, 0, 0.09)\n ',
|
||||||
|
boxShadowDrawerRight:
|
||||||
|
'\n -6px 0 16px 0 rgba(0, 0, 0, 0.08),\n -3px 0 6px -4px rgba(0, 0, 0, 0.12),\n -9px 0 28px 8px rgba(0, 0, 0, 0.05)\n ',
|
||||||
|
boxShadowDrawerLeft:
|
||||||
|
'\n 6px 0 16px 0 rgba(0, 0, 0, 0.08),\n 3px 0 6px -4px rgba(0, 0, 0, 0.12),\n 9px 0 28px 8px rgba(0, 0, 0, 0.05)\n ',
|
||||||
|
boxShadowDrawerUp:
|
||||||
|
'\n 0 6px 16px 0 rgba(0, 0, 0, 0.08),\n 0 3px 6px -4px rgba(0, 0, 0, 0.12),\n 0 9px 28px 8px rgba(0, 0, 0, 0.05)\n ',
|
||||||
|
boxShadowDrawerDown:
|
||||||
|
'\n 0 -6px 16px 0 rgba(0, 0, 0, 0.08),\n 0 -3px 6px -4px rgba(0, 0, 0, 0.12),\n 0 -9px 28px 8px rgba(0, 0, 0, 0.05)\n ',
|
||||||
|
boxShadowTabsOverflowLeft: 'inset 10px 0 8px -8px rgba(0, 0, 0, 0.08)',
|
||||||
|
boxShadowTabsOverflowRight: 'inset -10px 0 8px -8px rgba(0, 0, 0, 0.08)',
|
||||||
|
boxShadowTabsOverflowTop: 'inset 0 10px 8px -8px rgba(0, 0, 0, 0.08)',
|
||||||
|
boxShadowTabsOverflowBottom: 'inset 0 -10px 8px -8px rgba(0, 0, 0, 0.08)',
|
||||||
|
isDarkMode: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const key in globalVariables) {
|
||||||
|
globalThis[key] = globalVariables[key]
|
||||||
|
}
|
||||||
23
app/src/main/assets/template/tool-view.html
Normal file
23
app/src/main/assets/template/tool-view.html
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8"/>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||||
|
<title>Preview</title>
|
||||||
|
<script id="global-js-variables">
|
||||||
|
{{replace_global_js_variables}}
|
||||||
|
</script>
|
||||||
|
<style id="global-css-variables">
|
||||||
|
{{replace_global_css_variables}}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script type="module" id="appDictSrc">{{replace_dict_code}}</script>
|
||||||
|
<script type="module" id="appBaseSrc">{{replace_base_code}}</script>
|
||||||
|
<div id="root">
|
||||||
|
<div style="position:absolute;top: 0;left:0;width:100%;height:100%;display: flex;justify-content: center;align-items: center;color: #777;">
|
||||||
|
Loading...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -2,13 +2,15 @@ package top.fatweb.oxygen.toolbox
|
|||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.SystemBarStyle
|
import androidx.activity.SystemBarStyle
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
import androidx.activity.enableEdgeToEdge
|
import androidx.activity.enableEdgeToEdge
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
|
import androidx.compose.material3.windowsizeclass.ExperimentalMaterial3WindowSizeClassApi
|
||||||
import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
|
import androidx.compose.material3.windowsizeclass.calculateWindowSizeClass
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -17,11 +19,14 @@ import androidx.compose.runtime.LaunchedEffect
|
|||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||||
import androidx.lifecycle.Lifecycle
|
import androidx.lifecycle.Lifecycle
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.lifecycle.repeatOnLifecycle
|
import androidx.lifecycle.repeatOnLifecycle
|
||||||
|
import androidx.webkit.WebViewCompat
|
||||||
import dagger.hilt.EntryPoint
|
import dagger.hilt.EntryPoint
|
||||||
import dagger.hilt.InstallIn
|
import dagger.hilt.InstallIn
|
||||||
import dagger.hilt.android.AndroidEntryPoint
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
@@ -32,13 +37,15 @@ import kotlinx.coroutines.flow.first
|
|||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import top.fatweb.oxygen.toolbox.model.DarkThemeConfig
|
import top.fatweb.oxygen.toolbox.model.userdata.DarkThemeConfig
|
||||||
import top.fatweb.oxygen.toolbox.model.LanguageConfig
|
import top.fatweb.oxygen.toolbox.model.userdata.LanguageConfig
|
||||||
import top.fatweb.oxygen.toolbox.model.LaunchPageConfig
|
import top.fatweb.oxygen.toolbox.model.userdata.LaunchPageConfig
|
||||||
import top.fatweb.oxygen.toolbox.model.ThemeBrandConfig
|
import top.fatweb.oxygen.toolbox.model.userdata.ThemeBrandConfig
|
||||||
import top.fatweb.oxygen.toolbox.monitor.NetworkMonitor
|
import top.fatweb.oxygen.toolbox.monitor.NetworkMonitor
|
||||||
import top.fatweb.oxygen.toolbox.monitor.TimeZoneMonitor
|
import top.fatweb.oxygen.toolbox.monitor.TimeZoneMonitor
|
||||||
import top.fatweb.oxygen.toolbox.repository.UserDataRepository
|
import top.fatweb.oxygen.toolbox.navigation.PREVIEW_ARG
|
||||||
|
import top.fatweb.oxygen.toolbox.navigation.navigateToToolView
|
||||||
|
import top.fatweb.oxygen.toolbox.repository.userdata.UserDataRepository
|
||||||
import top.fatweb.oxygen.toolbox.ui.OxygenApp
|
import top.fatweb.oxygen.toolbox.ui.OxygenApp
|
||||||
import top.fatweb.oxygen.toolbox.ui.rememberOxygenAppState
|
import top.fatweb.oxygen.toolbox.ui.rememberOxygenAppState
|
||||||
import top.fatweb.oxygen.toolbox.ui.theme.OxygenTheme
|
import top.fatweb.oxygen.toolbox.ui.theme.OxygenTheme
|
||||||
@@ -46,8 +53,6 @@ import top.fatweb.oxygen.toolbox.ui.util.LocalTimeZone
|
|||||||
import top.fatweb.oxygen.toolbox.ui.util.LocaleUtils
|
import top.fatweb.oxygen.toolbox.ui.util.LocaleUtils
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
const val TAG = "MainActivity"
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
|
@OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
@@ -87,18 +92,19 @@ class MainActivity : ComponentActivity() {
|
|||||||
LaunchedEffect(locale) {
|
LaunchedEffect(locale) {
|
||||||
LocaleUtils.switchLocale(this@MainActivity, locale)
|
LocaleUtils.switchLocale(this@MainActivity, locale)
|
||||||
}
|
}
|
||||||
|
UseIsFirstLaunch(viewModel) {
|
||||||
|
CheckWebView(it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val darkTheme = shouldUseDarkTheme(uiState)
|
val darkTheme = shouldUseDarkTheme(uiState)
|
||||||
LaunchedEffect(darkTheme) {
|
LaunchedEffect(darkTheme) {
|
||||||
enableEdgeToEdge(
|
enableEdgeToEdge(
|
||||||
statusBarStyle = SystemBarStyle.auto(
|
statusBarStyle = SystemBarStyle.auto(
|
||||||
android.graphics.Color.TRANSPARENT,
|
lightScrim = android.graphics.Color.TRANSPARENT,
|
||||||
android.graphics.Color.TRANSPARENT
|
darkScrim = android.graphics.Color.TRANSPARENT
|
||||||
) { darkTheme },
|
) { darkTheme },
|
||||||
navigationBarStyle = SystemBarStyle.auto(
|
navigationBarStyle = SystemBarStyle.dark(android.graphics.Color.TRANSPARENT)
|
||||||
lightScrim, darkScrim
|
|
||||||
) { darkTheme }
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,9 +117,7 @@ class MainActivity : ComponentActivity() {
|
|||||||
|
|
||||||
val currentTimeZone by appState.currentTimeZone.collectAsStateWithLifecycle()
|
val currentTimeZone by appState.currentTimeZone.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
CompositionLocalProvider(
|
CompositionLocalProvider(LocalTimeZone provides currentTimeZone) {
|
||||||
LocalTimeZone provides currentTimeZone
|
|
||||||
) {
|
|
||||||
OxygenTheme(
|
OxygenTheme(
|
||||||
darkTheme = darkTheme,
|
darkTheme = darkTheme,
|
||||||
androidTheme = shouldUseAndroidTheme(uiState),
|
androidTheme = shouldUseAndroidTheme(uiState),
|
||||||
@@ -122,10 +126,21 @@ class MainActivity : ComponentActivity() {
|
|||||||
OxygenApp(appState)
|
OxygenApp(appState)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Log.d(TAG, "onCreate: C")
|
|
||||||
}
|
|
||||||
|
|
||||||
Log.d(TAG, "onCreate: D")
|
LaunchedEffect(intent.data) {
|
||||||
|
intent.data?.run {
|
||||||
|
val pathSegments = pathSegments
|
||||||
|
val preview = getBooleanQueryParameter(PREVIEW_ARG, false)
|
||||||
|
if (pathSegments.size == 2) {
|
||||||
|
appState.navController.navigateToToolView(
|
||||||
|
username = pathSegments[0],
|
||||||
|
toolId = pathSegments[1],
|
||||||
|
preview = preview
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@EntryPoint
|
@EntryPoint
|
||||||
@@ -137,67 +152,108 @@ class MainActivity : ComponentActivity() {
|
|||||||
override fun attachBaseContext(newBase: Context) {
|
override fun attachBaseContext(newBase: Context) {
|
||||||
val userDataRepository =
|
val userDataRepository =
|
||||||
EntryPointAccessors.fromApplication<UserDataRepositoryEntryPoint>(newBase).userDataRepository
|
EntryPointAccessors.fromApplication<UserDataRepositoryEntryPoint>(newBase).userDataRepository
|
||||||
super.attachBaseContext(LocaleUtils.attachBaseContext(newBase, runBlocking {
|
super.attachBaseContext(
|
||||||
userDataRepository.userData.first().languageConfig
|
LocaleUtils.attachBaseContext(
|
||||||
}))
|
context = newBase,
|
||||||
|
languageConfig = runBlocking {
|
||||||
|
userDataRepository.userData.first().languageConfig
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun shouldUseDarkTheme(
|
private fun UseIsFirstLaunch(viewModel: MainActivityViewModel, callback: @Composable (ondDismiss: () -> Unit) -> Unit) {
|
||||||
uiState: MainActivityUiState
|
val uiState by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
): Boolean = when (uiState) {
|
if (!whatIsFirstLaunch(uiState)) {
|
||||||
MainActivityUiState.Loading -> isSystemInDarkTheme()
|
return
|
||||||
is MainActivityUiState.Success -> when (uiState.userData.darkThemeConfig) {
|
}
|
||||||
DarkThemeConfig.FOLLOW_SYSTEM -> isSystemInDarkTheme()
|
|
||||||
DarkThemeConfig.LIGHT -> false
|
callback {
|
||||||
DarkThemeConfig.DARK -> true
|
viewModel.updateIsNotFirstLaunch()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun shouldUseAndroidTheme(
|
private fun whatIsFirstLaunch(uiState: MainActivityUiState): Boolean =
|
||||||
uiState: MainActivityUiState
|
when (uiState) {
|
||||||
): Boolean = when (uiState) {
|
MainActivityUiState.Loading -> false
|
||||||
MainActivityUiState.Loading -> false
|
is MainActivityUiState.Success ->
|
||||||
is MainActivityUiState.Success -> when (uiState.userData.themeBrandConfig) {
|
!uiState.userData.isNotFirstLaunch
|
||||||
ThemeBrandConfig.DEFAULT -> false
|
}
|
||||||
ThemeBrandConfig.ANDROID -> true
|
|
||||||
|
@Composable
|
||||||
|
private fun CheckWebView(onDismiss: () -> Unit) {
|
||||||
|
val versionName = WebViewCompat.getCurrentWebViewPackage(LocalContext.current)?.versionName
|
||||||
|
if (versionName == null) {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
title = {Text(text = stringResource(R.string.core_web_view_warning))},
|
||||||
|
text = { Text(text = stringResource(R.string.core_cannot_load_web_view_version)) },
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(onClick = onDismiss) {
|
||||||
|
Text(text = stringResource(R.string.core_dismiss))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (versionName.split(".").first().toInt() < 80) {
|
||||||
|
AlertDialog(
|
||||||
|
onDismissRequest = onDismiss,
|
||||||
|
title = {Text(text = stringResource(R.string.core_web_view_warning))},
|
||||||
|
text = { Text(text = stringResource(R.string.core_web_view_version_too_low)) },
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(onClick = onDismiss) {
|
||||||
|
Text(text = stringResource(R.string.core_dismiss))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun shouldUseDynamicColor(
|
private fun shouldUseDarkTheme(uiState: MainActivityUiState): Boolean =
|
||||||
uiState: MainActivityUiState
|
when (uiState) {
|
||||||
): Boolean = when (uiState) {
|
MainActivityUiState.Loading -> isSystemInDarkTheme()
|
||||||
MainActivityUiState.Loading -> true
|
is MainActivityUiState.Success ->
|
||||||
is MainActivityUiState.Success -> uiState.userData.useDynamicColor
|
when (uiState.userData.darkThemeConfig) {
|
||||||
}
|
DarkThemeConfig.FollowSystem -> isSystemInDarkTheme()
|
||||||
|
DarkThemeConfig.Light -> false
|
||||||
|
DarkThemeConfig.Dark -> true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun whatLocale(
|
private fun shouldUseAndroidTheme(uiState: MainActivityUiState): Boolean =
|
||||||
uiState: MainActivityUiState
|
when (uiState) {
|
||||||
): LanguageConfig = when (uiState) {
|
MainActivityUiState.Loading -> false
|
||||||
MainActivityUiState.Loading -> LanguageConfig.FOLLOW_SYSTEM
|
is MainActivityUiState.Success ->
|
||||||
is MainActivityUiState.Success -> uiState.userData.languageConfig
|
when (uiState.userData.themeBrandConfig) {
|
||||||
}
|
ThemeBrandConfig.Default -> false
|
||||||
|
ThemeBrandConfig.Android -> true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun whatLaunchPage(
|
private fun shouldUseDynamicColor(uiState: MainActivityUiState): Boolean =
|
||||||
uiState: MainActivityUiState
|
when (uiState) {
|
||||||
): LaunchPageConfig = when (uiState) {
|
MainActivityUiState.Loading -> true
|
||||||
MainActivityUiState.Loading -> LaunchPageConfig.TOOLS
|
is MainActivityUiState.Success -> uiState.userData.useDynamicColor
|
||||||
is MainActivityUiState.Success -> uiState.userData.launchPageConfig
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
@Composable
|
||||||
* The default light scrim, as defined by androidx and the platform:
|
private fun whatLocale(uiState: MainActivityUiState): LanguageConfig =
|
||||||
* https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:activity/activity/src/main/java/androidx/activity/EdgeToEdge.kt;l=35-38;drc=27e7d52e8604a080133e8b842db10c89b4482598
|
when (uiState) {
|
||||||
*/
|
MainActivityUiState.Loading -> LanguageConfig.FollowSystem
|
||||||
private val lightScrim = android.graphics.Color.argb(0xe6, 0xFF, 0xFF, 0xFF)
|
is MainActivityUiState.Success -> uiState.userData.languageConfig
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
@Composable
|
||||||
* The default dark scrim, as defined by androidx and the platform:
|
private fun whatLaunchPage(uiState: MainActivityUiState): LaunchPageConfig =
|
||||||
* https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:activity/activity/src/main/java/androidx/activity/EdgeToEdge.kt;l=40-44;drc=27e7d52e8604a080133e8b842db10c89b4482598
|
when (uiState) {
|
||||||
*/
|
MainActivityUiState.Loading -> LaunchPageConfig.Tools
|
||||||
private val darkScrim = android.graphics.Color.argb(0x80, 0x1b, 0x1b, 0x1b)
|
is MainActivityUiState.Success -> uiState.userData.launchPageConfig
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,22 +7,29 @@ import kotlinx.coroutines.flow.SharingStarted
|
|||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import top.fatweb.oxygen.toolbox.model.UserData
|
import kotlinx.coroutines.launch
|
||||||
import top.fatweb.oxygen.toolbox.repository.UserDataRepository
|
import top.fatweb.oxygen.toolbox.model.userdata.UserData
|
||||||
|
import top.fatweb.oxygen.toolbox.repository.userdata.UserDataRepository
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
class MainActivityViewModel @Inject constructor(
|
class MainActivityViewModel @Inject constructor(
|
||||||
userDataRepository: UserDataRepository
|
private val userDataRepository: UserDataRepository
|
||||||
) : ViewModel() {
|
) : ViewModel() {
|
||||||
val uiState: StateFlow<MainActivityUiState> = userDataRepository.userData.map {
|
val uiState: StateFlow<MainActivityUiState> = userDataRepository.userData.map {
|
||||||
MainActivityUiState.Success(it)
|
MainActivityUiState.Success(it)
|
||||||
}.stateIn(
|
}.stateIn(
|
||||||
scope = viewModelScope,
|
scope = viewModelScope,
|
||||||
initialValue = MainActivityUiState.Loading,
|
initialValue = MainActivityUiState.Loading,
|
||||||
started = SharingStarted.WhileSubscribed(5.seconds.inWholeMilliseconds)
|
started = SharingStarted.WhileSubscribed(stopTimeoutMillis = 5.seconds.inWholeMilliseconds)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
fun updateIsNotFirstLaunch() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
userDataRepository.updateIsNotFirstLaunch()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed interface MainActivityUiState {
|
sealed interface MainActivityUiState {
|
||||||
|
|||||||
@@ -2,11 +2,19 @@ package top.fatweb.oxygen.toolbox
|
|||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import dagger.hilt.android.HiltAndroidApp
|
import dagger.hilt.android.HiltAndroidApp
|
||||||
import top.fatweb.oxygen.toolbox.repository.UserDataRepository
|
import timber.log.Timber
|
||||||
|
import top.fatweb.oxygen.toolbox.repository.userdata.UserDataRepository
|
||||||
|
import top.fatweb.oxygen.toolbox.util.OxygenLogTree
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@HiltAndroidApp
|
@HiltAndroidApp
|
||||||
class OxygenApplication : Application() {
|
class OxygenApplication : Application() {
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var userDataRepository: UserDataRepository
|
lateinit var userDataRepository: UserDataRepository
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
|
||||||
|
Timber.plant(if (BuildConfig.DEBUG) Timber.DebugTree() else OxygenLogTree(this))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package top.fatweb.oxygen.toolbox.data.lib
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
|
import kotlinx.coroutines.flow.flow
|
||||||
|
import kotlinx.coroutines.flow.flowOn
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import top.fatweb.oxygen.toolbox.R
|
||||||
|
import top.fatweb.oxygen.toolbox.model.lib.Dependencies
|
||||||
|
import top.fatweb.oxygen.toolbox.network.Dispatcher
|
||||||
|
import top.fatweb.oxygen.toolbox.network.OxygenDispatchers
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class DepDataSource @Inject constructor(
|
||||||
|
@ApplicationContext private val context: Context,
|
||||||
|
@Dispatcher(OxygenDispatchers.IO) private val ioDispatcher: CoroutineDispatcher
|
||||||
|
) {
|
||||||
|
private val json = Json { ignoreUnknownKeys = true }
|
||||||
|
|
||||||
|
val dependencies = flow {
|
||||||
|
val inputStream = context.resources.openRawResource(R.raw.dependencies)
|
||||||
|
val jsonString = inputStream.bufferedReader().use { it.readText() }
|
||||||
|
val dependencies = json.decodeFromString<Dependencies>(jsonString)
|
||||||
|
emit(dependencies)
|
||||||
|
}.flowOn(ioDispatcher)
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package top.fatweb.oxygen.toolbox.data.network
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import top.fatweb.oxygen.toolbox.model.Result
|
||||||
|
import top.fatweb.oxygen.toolbox.network.model.PageVo
|
||||||
|
import top.fatweb.oxygen.toolbox.network.model.ResponseResult
|
||||||
|
import top.fatweb.oxygen.toolbox.network.model.ToolBaseVo
|
||||||
|
import top.fatweb.oxygen.toolbox.network.model.ToolVo
|
||||||
|
|
||||||
|
interface OxygenNetworkDataSource {
|
||||||
|
suspend fun getStore(
|
||||||
|
searchValue: String = "",
|
||||||
|
currentPage: Int = 1
|
||||||
|
): ResponseResult<PageVo<ToolVo>>
|
||||||
|
|
||||||
|
fun detail(
|
||||||
|
username: String,
|
||||||
|
toolId: String,
|
||||||
|
ver: String = "latest",
|
||||||
|
platform: ToolBaseVo.Platform = ToolBaseVo.Platform.Android
|
||||||
|
): Flow<Result<ToolVo>>
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package top.fatweb.oxygen.toolbox.data.tool
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
|
import kotlinx.coroutines.flow.flow
|
||||||
|
import kotlinx.coroutines.flow.flowOn
|
||||||
|
import top.fatweb.oxygen.toolbox.network.Dispatcher
|
||||||
|
import top.fatweb.oxygen.toolbox.network.OxygenDispatchers
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class ToolDataSource @Inject constructor(
|
||||||
|
@ApplicationContext private val context: Context,
|
||||||
|
@Dispatcher(OxygenDispatchers.IO) private val ioDispatcher: CoroutineDispatcher
|
||||||
|
) {
|
||||||
|
val toolViewTemplate = flow {
|
||||||
|
emit(
|
||||||
|
context.assets.open("template/tool-view.html")
|
||||||
|
.bufferedReader()
|
||||||
|
.use {
|
||||||
|
it.readText()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}.flowOn(ioDispatcher)
|
||||||
|
|
||||||
|
fun getGlobalJsVariables(isDarkMode: Boolean) = flow {
|
||||||
|
emit(
|
||||||
|
context.assets.open(
|
||||||
|
if (isDarkMode) "template/global-variables-dark.js"
|
||||||
|
else "template/global-variables-light.js"
|
||||||
|
)
|
||||||
|
.bufferedReader()
|
||||||
|
.use {
|
||||||
|
it.readText()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}.flowOn(ioDispatcher)
|
||||||
|
|
||||||
|
fun getGlobalCssVariables(isDarkMode: Boolean) = flow {
|
||||||
|
emit(
|
||||||
|
context.assets.open(
|
||||||
|
if (isDarkMode) "template/global-variables-dark.css"
|
||||||
|
else "template/global-variables-light.css"
|
||||||
|
)
|
||||||
|
.bufferedReader()
|
||||||
|
.use {
|
||||||
|
it.readText()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}.flowOn(ioDispatcher)
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
package top.fatweb.oxygen.toolbox.data.tool
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.room.Database
|
||||||
|
import androidx.room.Room
|
||||||
|
import androidx.room.RoomDatabase
|
||||||
|
import top.fatweb.oxygen.toolbox.data.tool.dao.ToolDao
|
||||||
|
import top.fatweb.oxygen.toolbox.model.tool.ToolEntity
|
||||||
|
|
||||||
|
@Database(
|
||||||
|
entities = [ToolEntity::class],
|
||||||
|
version = 1,
|
||||||
|
autoMigrations = [],
|
||||||
|
exportSchema = true
|
||||||
|
)
|
||||||
|
abstract class ToolDatabase : RoomDatabase() {
|
||||||
|
abstract fun toolDao(): ToolDao
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
@Volatile
|
||||||
|
private var INSTANCE: ToolDatabase? = null
|
||||||
|
|
||||||
|
fun getInstance(context: Context): ToolDatabase =
|
||||||
|
INSTANCE ?: synchronized(this) {
|
||||||
|
Room.databaseBuilder(
|
||||||
|
context = context,
|
||||||
|
klass = ToolDatabase::class.java,
|
||||||
|
name = "tools.db"
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
.also { INSTANCE = it }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package top.fatweb.oxygen.toolbox.data.tool.dao
|
||||||
|
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Delete
|
||||||
|
import androidx.room.Insert
|
||||||
|
import androidx.room.OnConflictStrategy
|
||||||
|
import androidx.room.Query
|
||||||
|
import androidx.room.Update
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import top.fatweb.oxygen.toolbox.model.tool.ToolEntity
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
interface ToolDao {
|
||||||
|
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||||
|
suspend fun insertTool(tool: ToolEntity)
|
||||||
|
|
||||||
|
@Update
|
||||||
|
suspend fun updateTool(tool: ToolEntity)
|
||||||
|
|
||||||
|
@Delete
|
||||||
|
suspend fun deleteTool(tool: ToolEntity)
|
||||||
|
|
||||||
|
@Query("SELECT * FROM tools " +
|
||||||
|
"WHERE id = :id")
|
||||||
|
fun selectToolById(id: Long): Flow<ToolEntity?>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM tools " +
|
||||||
|
"WHERE :searchValue = '' " +
|
||||||
|
"OR name LIKE '%' || :searchValue || '%' COLLATE NOCASE " +
|
||||||
|
"OR keywords LIKE '%\"%' || :searchValue || '%\"%' COLLATE NOCASE " +
|
||||||
|
"ORDER BY updateTime DESC")
|
||||||
|
fun selectAllTools(searchValue: String): Flow<List<ToolEntity>>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM tools " +
|
||||||
|
"WHERE isStar = 1 " +
|
||||||
|
"AND (:searchValue = '' " +
|
||||||
|
"OR name LIKE '%' || :searchValue || '%' COLLATE NOCASE " +
|
||||||
|
"OR keywords LIKE '%\"%' || :searchValue || '%\"%' COLLATE NOCASE" +
|
||||||
|
") " +
|
||||||
|
"ORDER BY updateTime DESC")
|
||||||
|
fun selectStarTools(searchValue: String): Flow<List<ToolEntity>>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM tools " +
|
||||||
|
"WHERE authorUsername = :username " +
|
||||||
|
"and toolId = :toolId LIMIT 1")
|
||||||
|
fun selectToolByUsernameAndToolId(username: String, toolId: String): Flow<ToolEntity?>
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
package top.fatweb.oxygen.toolbox.datastore
|
package top.fatweb.oxygen.toolbox.data.userdata
|
||||||
|
|
||||||
import androidx.datastore.core.DataMigration
|
import androidx.datastore.core.DataMigration
|
||||||
|
import top.fatweb.oxygen.toolbox.data.UserPreferences
|
||||||
|
import top.fatweb.oxygen.toolbox.data.copy
|
||||||
|
|
||||||
internal object IntToStringIdsMigration : DataMigration<UserPreferences> {
|
internal object IntToStringIdsMigration : DataMigration<UserPreferences> {
|
||||||
override suspend fun cleanUp() = Unit
|
override suspend fun cleanUp() = Unit
|
||||||
@@ -1,12 +1,18 @@
|
|||||||
package top.fatweb.oxygen.toolbox.datastore
|
package top.fatweb.oxygen.toolbox.data.userdata
|
||||||
|
|
||||||
import androidx.datastore.core.DataStore
|
import androidx.datastore.core.DataStore
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import top.fatweb.oxygen.toolbox.model.DarkThemeConfig
|
import top.fatweb.oxygen.toolbox.data.DarkThemeConfigProto
|
||||||
import top.fatweb.oxygen.toolbox.model.LanguageConfig
|
import top.fatweb.oxygen.toolbox.data.LanguageConfigProto
|
||||||
import top.fatweb.oxygen.toolbox.model.LaunchPageConfig
|
import top.fatweb.oxygen.toolbox.data.LaunchPageConfigProto
|
||||||
import top.fatweb.oxygen.toolbox.model.ThemeBrandConfig
|
import top.fatweb.oxygen.toolbox.data.ThemeBrandConfigProto
|
||||||
import top.fatweb.oxygen.toolbox.model.UserData
|
import top.fatweb.oxygen.toolbox.data.UserPreferences
|
||||||
|
import top.fatweb.oxygen.toolbox.data.copy
|
||||||
|
import top.fatweb.oxygen.toolbox.model.userdata.DarkThemeConfig
|
||||||
|
import top.fatweb.oxygen.toolbox.model.userdata.LanguageConfig
|
||||||
|
import top.fatweb.oxygen.toolbox.model.userdata.LaunchPageConfig
|
||||||
|
import top.fatweb.oxygen.toolbox.model.userdata.ThemeBrandConfig
|
||||||
|
import top.fatweb.oxygen.toolbox.model.userdata.UserData
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class OxygenPreferencesDataSource @Inject constructor(
|
class OxygenPreferencesDataSource @Inject constructor(
|
||||||
@@ -20,23 +26,23 @@ class OxygenPreferencesDataSource @Inject constructor(
|
|||||||
LanguageConfigProto.UNRECOGNIZED,
|
LanguageConfigProto.UNRECOGNIZED,
|
||||||
LanguageConfigProto.LANGUAGE_CONFIG_UNSPECIFIED,
|
LanguageConfigProto.LANGUAGE_CONFIG_UNSPECIFIED,
|
||||||
LanguageConfigProto.LANGUAGE_CONFIG_FOLLOW_SYSTEM
|
LanguageConfigProto.LANGUAGE_CONFIG_FOLLOW_SYSTEM
|
||||||
-> LanguageConfig.FOLLOW_SYSTEM
|
-> LanguageConfig.FollowSystem
|
||||||
|
|
||||||
LanguageConfigProto.LANGUAGE_CONFIG_CHINESE
|
LanguageConfigProto.LANGUAGE_CONFIG_CHINESE
|
||||||
-> LanguageConfig.CHINESE
|
-> LanguageConfig.Chinese
|
||||||
|
|
||||||
LanguageConfigProto.LANGUAGE_CONFIG_ENGLISH
|
LanguageConfigProto.LANGUAGE_CONFIG_ENGLISH
|
||||||
-> LanguageConfig.ENGLISH
|
-> LanguageConfig.English
|
||||||
},
|
},
|
||||||
launchPageConfig = when (it.launchPageConfig) {
|
launchPageConfig = when (it.launchPageConfig) {
|
||||||
null,
|
null,
|
||||||
LaunchPageConfigProto.UNRECOGNIZED,
|
LaunchPageConfigProto.UNRECOGNIZED,
|
||||||
LaunchPageConfigProto.LAUNCH_PAGE_CONFIG_UNSPECIFIED,
|
LaunchPageConfigProto.LAUNCH_PAGE_CONFIG_UNSPECIFIED,
|
||||||
LaunchPageConfigProto.LAUNCH_PAGE_CONFIG_TOOLS
|
LaunchPageConfigProto.LAUNCH_PAGE_CONFIG_TOOLS
|
||||||
-> LaunchPageConfig.TOOLS
|
-> LaunchPageConfig.Tools
|
||||||
|
|
||||||
LaunchPageConfigProto.LAUNCH_PAGE_CONFIG_STAR
|
LaunchPageConfigProto.LAUNCH_PAGE_CONFIG_STAR
|
||||||
-> LaunchPageConfig.STAR
|
-> LaunchPageConfig.Star
|
||||||
},
|
},
|
||||||
themeBrandConfig = when (it.themeBrandConfig) {
|
themeBrandConfig = when (it.themeBrandConfig) {
|
||||||
null,
|
null,
|
||||||
@@ -44,10 +50,10 @@ class OxygenPreferencesDataSource @Inject constructor(
|
|||||||
ThemeBrandConfigProto.THEME_BRAND_CONFIG_UNSPECIFIED,
|
ThemeBrandConfigProto.THEME_BRAND_CONFIG_UNSPECIFIED,
|
||||||
ThemeBrandConfigProto.THEME_BRAND_CONFIG_DEFAULT
|
ThemeBrandConfigProto.THEME_BRAND_CONFIG_DEFAULT
|
||||||
->
|
->
|
||||||
ThemeBrandConfig.DEFAULT
|
ThemeBrandConfig.Default
|
||||||
|
|
||||||
ThemeBrandConfigProto.THEME_BRAND_CONFIG_ANDROID
|
ThemeBrandConfigProto.THEME_BRAND_CONFIG_ANDROID
|
||||||
-> ThemeBrandConfig.ANDROID
|
-> ThemeBrandConfig.Android
|
||||||
},
|
},
|
||||||
darkThemeConfig = when (it.darkThemeConfig) {
|
darkThemeConfig = when (it.darkThemeConfig) {
|
||||||
null,
|
null,
|
||||||
@@ -55,15 +61,16 @@ class OxygenPreferencesDataSource @Inject constructor(
|
|||||||
DarkThemeConfigProto.DARK_THEME_CONFIG_UNSPECIFIED,
|
DarkThemeConfigProto.DARK_THEME_CONFIG_UNSPECIFIED,
|
||||||
DarkThemeConfigProto.DARK_THEME_CONFIG_FOLLOW_SYSTEM
|
DarkThemeConfigProto.DARK_THEME_CONFIG_FOLLOW_SYSTEM
|
||||||
->
|
->
|
||||||
DarkThemeConfig.FOLLOW_SYSTEM
|
DarkThemeConfig.FollowSystem
|
||||||
|
|
||||||
DarkThemeConfigProto.DARK_THEME_CONFIG_LIGHT
|
DarkThemeConfigProto.DARK_THEME_CONFIG_LIGHT
|
||||||
-> DarkThemeConfig.LIGHT
|
-> DarkThemeConfig.Light
|
||||||
|
|
||||||
DarkThemeConfigProto.DARK_THEME_CONFIG_DARK
|
DarkThemeConfigProto.DARK_THEME_CONFIG_DARK
|
||||||
-> DarkThemeConfig.DARK
|
-> DarkThemeConfig.Dark
|
||||||
},
|
},
|
||||||
useDynamicColor = it.useDynamicColor
|
useDynamicColor = it.useDynamicColor,
|
||||||
|
isNotFirstLaunch = it.isNotFirstLaunch
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,9 +78,9 @@ class OxygenPreferencesDataSource @Inject constructor(
|
|||||||
userPreferences.updateData {
|
userPreferences.updateData {
|
||||||
it.copy {
|
it.copy {
|
||||||
this.languageConfig = when (languageConfig) {
|
this.languageConfig = when (languageConfig) {
|
||||||
LanguageConfig.FOLLOW_SYSTEM -> LanguageConfigProto.LANGUAGE_CONFIG_FOLLOW_SYSTEM
|
LanguageConfig.FollowSystem -> LanguageConfigProto.LANGUAGE_CONFIG_FOLLOW_SYSTEM
|
||||||
LanguageConfig.CHINESE -> LanguageConfigProto.LANGUAGE_CONFIG_CHINESE
|
LanguageConfig.Chinese -> LanguageConfigProto.LANGUAGE_CONFIG_CHINESE
|
||||||
LanguageConfig.ENGLISH -> LanguageConfigProto.LANGUAGE_CONFIG_ENGLISH
|
LanguageConfig.English -> LanguageConfigProto.LANGUAGE_CONFIG_ENGLISH
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -83,8 +90,8 @@ class OxygenPreferencesDataSource @Inject constructor(
|
|||||||
userPreferences.updateData {
|
userPreferences.updateData {
|
||||||
it.copy {
|
it.copy {
|
||||||
this.launchPageConfig = when (launchPageConfig) {
|
this.launchPageConfig = when (launchPageConfig) {
|
||||||
LaunchPageConfig.TOOLS -> LaunchPageConfigProto.LAUNCH_PAGE_CONFIG_TOOLS
|
LaunchPageConfig.Tools -> LaunchPageConfigProto.LAUNCH_PAGE_CONFIG_TOOLS
|
||||||
LaunchPageConfig.STAR -> LaunchPageConfigProto.LAUNCH_PAGE_CONFIG_STAR
|
LaunchPageConfig.Star -> LaunchPageConfigProto.LAUNCH_PAGE_CONFIG_STAR
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -94,8 +101,8 @@ class OxygenPreferencesDataSource @Inject constructor(
|
|||||||
userPreferences.updateData {
|
userPreferences.updateData {
|
||||||
it.copy {
|
it.copy {
|
||||||
this.themeBrandConfig = when (themeBrandConfig) {
|
this.themeBrandConfig = when (themeBrandConfig) {
|
||||||
ThemeBrandConfig.DEFAULT -> ThemeBrandConfigProto.THEME_BRAND_CONFIG_DEFAULT
|
ThemeBrandConfig.Default -> ThemeBrandConfigProto.THEME_BRAND_CONFIG_DEFAULT
|
||||||
ThemeBrandConfig.ANDROID -> ThemeBrandConfigProto.THEME_BRAND_CONFIG_ANDROID
|
ThemeBrandConfig.Android -> ThemeBrandConfigProto.THEME_BRAND_CONFIG_ANDROID
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -105,9 +112,9 @@ class OxygenPreferencesDataSource @Inject constructor(
|
|||||||
userPreferences.updateData {
|
userPreferences.updateData {
|
||||||
it.copy {
|
it.copy {
|
||||||
this.darkThemeConfig = when (darkThemeConfig) {
|
this.darkThemeConfig = when (darkThemeConfig) {
|
||||||
DarkThemeConfig.FOLLOW_SYSTEM -> DarkThemeConfigProto.DARK_THEME_CONFIG_FOLLOW_SYSTEM
|
DarkThemeConfig.FollowSystem -> DarkThemeConfigProto.DARK_THEME_CONFIG_FOLLOW_SYSTEM
|
||||||
DarkThemeConfig.LIGHT -> DarkThemeConfigProto.DARK_THEME_CONFIG_LIGHT
|
DarkThemeConfig.Light -> DarkThemeConfigProto.DARK_THEME_CONFIG_LIGHT
|
||||||
DarkThemeConfig.DARK -> DarkThemeConfigProto.DARK_THEME_CONFIG_DARK
|
DarkThemeConfig.Dark -> DarkThemeConfigProto.DARK_THEME_CONFIG_DARK
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -120,4 +127,12 @@ class OxygenPreferencesDataSource @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun updateIsNotFirstLaunch() {
|
||||||
|
userPreferences.updateData {
|
||||||
|
it.copy {
|
||||||
|
this.isNotFirstLaunch = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
package top.fatweb.oxygen.toolbox.datastore
|
package top.fatweb.oxygen.toolbox.data.userdata
|
||||||
|
|
||||||
import androidx.datastore.core.CorruptionException
|
import androidx.datastore.core.CorruptionException
|
||||||
import androidx.datastore.core.Serializer
|
import androidx.datastore.core.Serializer
|
||||||
import com.google.protobuf.InvalidProtocolBufferException
|
import com.google.protobuf.InvalidProtocolBufferException
|
||||||
|
import top.fatweb.oxygen.toolbox.data.UserPreferences
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
@@ -14,7 +15,10 @@ class UserPreferencesSerializer @Inject constructor() : Serializer<UserPreferenc
|
|||||||
try {
|
try {
|
||||||
UserPreferences.parseFrom(input)
|
UserPreferences.parseFrom(input)
|
||||||
} catch (exception: InvalidProtocolBufferException) {
|
} catch (exception: InvalidProtocolBufferException) {
|
||||||
throw CorruptionException("Cannot read proto.", exception)
|
throw CorruptionException(
|
||||||
|
message = "Cannot read proto.",
|
||||||
|
cause = exception
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun writeTo(t: UserPreferences, output: OutputStream) {
|
override suspend fun writeTo(t: UserPreferences, output: OutputStream) {
|
||||||
@@ -23,6 +23,8 @@ internal object CoroutineScopesModule {
|
|||||||
@Singleton
|
@Singleton
|
||||||
@ApplicationScope
|
@ApplicationScope
|
||||||
fun providesCoroutineScope(
|
fun providesCoroutineScope(
|
||||||
@Dispatcher(OxygenDispatchers.Default) dispatcher: CoroutineDispatcher
|
@Dispatcher(OxygenDispatchers.Default)
|
||||||
): CoroutineScope = CoroutineScope(SupervisorJob() + dispatcher)
|
dispatcher: CoroutineDispatcher
|
||||||
|
): CoroutineScope =
|
||||||
|
CoroutineScope(SupervisorJob() + dispatcher)
|
||||||
}
|
}
|
||||||
@@ -8,18 +8,33 @@ import top.fatweb.oxygen.toolbox.monitor.ConnectivityManagerNetworkMonitor
|
|||||||
import top.fatweb.oxygen.toolbox.monitor.NetworkMonitor
|
import top.fatweb.oxygen.toolbox.monitor.NetworkMonitor
|
||||||
import top.fatweb.oxygen.toolbox.monitor.TimeZoneBroadcastMonitor
|
import top.fatweb.oxygen.toolbox.monitor.TimeZoneBroadcastMonitor
|
||||||
import top.fatweb.oxygen.toolbox.monitor.TimeZoneMonitor
|
import top.fatweb.oxygen.toolbox.monitor.TimeZoneMonitor
|
||||||
import top.fatweb.oxygen.toolbox.repository.OfflineFirstUserDataRepository
|
import top.fatweb.oxygen.toolbox.repository.lib.DepRepository
|
||||||
import top.fatweb.oxygen.toolbox.repository.UserDataRepository
|
import top.fatweb.oxygen.toolbox.repository.lib.impl.LocalDepRepository
|
||||||
|
import top.fatweb.oxygen.toolbox.repository.tool.StoreRepository
|
||||||
|
import top.fatweb.oxygen.toolbox.repository.tool.ToolRepository
|
||||||
|
import top.fatweb.oxygen.toolbox.repository.tool.impl.NetworkStoreRepository
|
||||||
|
import top.fatweb.oxygen.toolbox.repository.tool.impl.OfflineToolRepository
|
||||||
|
import top.fatweb.oxygen.toolbox.repository.userdata.UserDataRepository
|
||||||
|
import top.fatweb.oxygen.toolbox.repository.userdata.impl.LocalUserDataRepository
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
@InstallIn(SingletonComponent::class)
|
@InstallIn(SingletonComponent::class)
|
||||||
abstract class DataModule {
|
abstract class DataModule {
|
||||||
@Binds
|
|
||||||
internal abstract fun bindsUserDataRepository(userDataRepository: OfflineFirstUserDataRepository): UserDataRepository
|
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
internal abstract fun bindsNetworkMonitor(networkMonitor: ConnectivityManagerNetworkMonitor): NetworkMonitor
|
internal abstract fun bindsNetworkMonitor(networkMonitor: ConnectivityManagerNetworkMonitor): NetworkMonitor
|
||||||
|
|
||||||
@Binds
|
@Binds
|
||||||
internal abstract fun bindsTimeZoneMonitor(timeZoneMonitor: TimeZoneBroadcastMonitor): TimeZoneMonitor
|
internal abstract fun bindsTimeZoneMonitor(timeZoneMonitor: TimeZoneBroadcastMonitor): TimeZoneMonitor
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
internal abstract fun bindsUserDataRepository(userDataRepository: LocalUserDataRepository): UserDataRepository
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
internal abstract fun bindsDepRepository(depRepository: LocalDepRepository): DepRepository
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
internal abstract fun bindsStoreRepository(storeRepository: NetworkStoreRepository): StoreRepository
|
||||||
|
|
||||||
|
@Binds
|
||||||
|
internal abstract fun bindsToolRepository(toolRepository: OfflineToolRepository): ToolRepository
|
||||||
}
|
}
|
||||||
@@ -11,9 +11,9 @@ import dagger.hilt.android.qualifiers.ApplicationContext
|
|||||||
import dagger.hilt.components.SingletonComponent
|
import dagger.hilt.components.SingletonComponent
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import top.fatweb.oxygen.toolbox.datastore.IntToStringIdsMigration
|
import top.fatweb.oxygen.toolbox.data.UserPreferences
|
||||||
import top.fatweb.oxygen.toolbox.datastore.UserPreferences
|
import top.fatweb.oxygen.toolbox.data.userdata.IntToStringIdsMigration
|
||||||
import top.fatweb.oxygen.toolbox.datastore.UserPreferencesSerializer
|
import top.fatweb.oxygen.toolbox.data.userdata.UserPreferencesSerializer
|
||||||
import top.fatweb.oxygen.toolbox.network.Dispatcher
|
import top.fatweb.oxygen.toolbox.network.Dispatcher
|
||||||
import top.fatweb.oxygen.toolbox.network.OxygenDispatchers
|
import top.fatweb.oxygen.toolbox.network.OxygenDispatchers
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package top.fatweb.oxygen.toolbox.di
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.Provides
|
||||||
|
import dagger.hilt.InstallIn
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import dagger.hilt.components.SingletonComponent
|
||||||
|
import top.fatweb.oxygen.toolbox.data.tool.ToolDatabase
|
||||||
|
import top.fatweb.oxygen.toolbox.data.tool.dao.ToolDao
|
||||||
|
|
||||||
|
@Module
|
||||||
|
@InstallIn(SingletonComponent::class)
|
||||||
|
object DatabaseModule {
|
||||||
|
@Provides
|
||||||
|
fun provideToolDao(@ApplicationContext context: Context): ToolDao =
|
||||||
|
ToolDatabase.getInstance(context).toolDao()
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package top.fatweb.oxygen.toolbox.di
|
||||||
|
|
||||||
|
import dagger.Module
|
||||||
|
import dagger.Provides
|
||||||
|
import dagger.hilt.InstallIn
|
||||||
|
import dagger.hilt.components.SingletonComponent
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import okhttp3.Call
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.logging.HttpLoggingInterceptor
|
||||||
|
import top.fatweb.oxygen.toolbox.BuildConfig
|
||||||
|
import top.fatweb.oxygen.toolbox.data.network.OxygenNetworkDataSource
|
||||||
|
import top.fatweb.oxygen.toolbox.network.retrofit.RetrofitOxygenNetwork
|
||||||
|
import top.fatweb.oxygen.toolbox.util.HttpLogger
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Module
|
||||||
|
@InstallIn(SingletonComponent::class)
|
||||||
|
internal object NetworkModule {
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun providesNetworkJson(): Json = Json {
|
||||||
|
ignoreUnknownKeys = true
|
||||||
|
}
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun okHttpCallFactory(): Call.Factory =
|
||||||
|
OkHttpClient.Builder()
|
||||||
|
.addInterceptor(
|
||||||
|
HttpLoggingInterceptor(HttpLogger())
|
||||||
|
.apply {
|
||||||
|
level =
|
||||||
|
if (BuildConfig.DEBUG) HttpLoggingInterceptor.Level.BODY else HttpLoggingInterceptor.Level.BASIC
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
@Provides
|
||||||
|
@Singleton
|
||||||
|
fun providesOxygenNetworkDataSource(
|
||||||
|
networkJson: Json,
|
||||||
|
okhttpCallFactory: dagger.Lazy<Call.Factory>
|
||||||
|
): OxygenNetworkDataSource =
|
||||||
|
RetrofitOxygenNetwork(networkJson, okhttpCallFactory)
|
||||||
|
}
|
||||||
@@ -1,20 +1,77 @@
|
|||||||
package top.fatweb.oxygen.toolbox.icon
|
package top.fatweb.oxygen.toolbox.icon
|
||||||
|
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
|
import android.graphics.drawable.PictureDrawable
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.AccessTime
|
||||||
|
import androidx.compose.material.icons.filled.Build
|
||||||
|
import androidx.compose.material.icons.filled.Cancel
|
||||||
|
import androidx.compose.material.icons.filled.Close
|
||||||
|
import androidx.compose.material.icons.filled.Code
|
||||||
|
import androidx.compose.material.icons.filled.Delete
|
||||||
|
import androidx.compose.material.icons.filled.Download
|
||||||
|
import androidx.compose.material.icons.filled.Fullscreen
|
||||||
|
import androidx.compose.material.icons.filled.FullscreenExit
|
||||||
|
import androidx.compose.material.icons.filled.Inbox
|
||||||
import androidx.compose.material.icons.filled.MoreVert
|
import androidx.compose.material.icons.filled.MoreVert
|
||||||
|
import androidx.compose.material.icons.filled.Reorder
|
||||||
|
import androidx.compose.material.icons.filled.Upgrade
|
||||||
import androidx.compose.material.icons.outlined.Home
|
import androidx.compose.material.icons.outlined.Home
|
||||||
|
import androidx.compose.material.icons.outlined.Info
|
||||||
import androidx.compose.material.icons.outlined.StarBorder
|
import androidx.compose.material.icons.outlined.StarBorder
|
||||||
|
import androidx.compose.material.icons.outlined.Store
|
||||||
import androidx.compose.material.icons.rounded.ArrowBackIosNew
|
import androidx.compose.material.icons.rounded.ArrowBackIosNew
|
||||||
|
import androidx.compose.material.icons.rounded.CheckCircle
|
||||||
import androidx.compose.material.icons.rounded.Home
|
import androidx.compose.material.icons.rounded.Home
|
||||||
|
import androidx.compose.material.icons.rounded.KeyboardArrowDown
|
||||||
import androidx.compose.material.icons.rounded.Search
|
import androidx.compose.material.icons.rounded.Search
|
||||||
import androidx.compose.material.icons.rounded.Star
|
import androidx.compose.material.icons.rounded.Star
|
||||||
|
import androidx.compose.material.icons.rounded.Store
|
||||||
|
import androidx.compose.ui.graphics.ImageBitmap
|
||||||
|
import androidx.compose.ui.graphics.asImageBitmap
|
||||||
|
import androidx.core.graphics.drawable.toBitmap
|
||||||
|
import com.caverock.androidsvg.SVG
|
||||||
|
import top.fatweb.oxygen.toolbox.util.decodeToByteArray
|
||||||
|
import top.fatweb.oxygen.toolbox.util.decodeToString
|
||||||
|
import kotlin.io.encoding.Base64
|
||||||
|
import kotlin.io.encoding.ExperimentalEncodingApi
|
||||||
|
|
||||||
object OxygenIcons {
|
object OxygenIcons {
|
||||||
|
val ArrowDown = Icons.Rounded.KeyboardArrowDown
|
||||||
|
val Back = Icons.Rounded.ArrowBackIosNew
|
||||||
|
val Box = Icons.Default.Inbox
|
||||||
|
val Close = Icons.Default.Close
|
||||||
|
val Code = Icons.Default.Code
|
||||||
|
val Delete = Icons.Default.Delete
|
||||||
|
val Download = Icons.Default.Download
|
||||||
|
val Error = Icons.Default.Cancel
|
||||||
|
val FullScreen = Icons.Default.Fullscreen
|
||||||
|
val FullScreenExit = Icons.Default.FullscreenExit
|
||||||
val Home = Icons.Rounded.Home
|
val Home = Icons.Rounded.Home
|
||||||
val HomeBorder = Icons.Outlined.Home
|
val HomeBorder = Icons.Outlined.Home
|
||||||
|
val Info = Icons.Outlined.Info
|
||||||
|
val MoreVert = Icons.Default.MoreVert
|
||||||
|
val Reorder = Icons.Default.Reorder
|
||||||
|
val Search = Icons.Rounded.Search
|
||||||
val Star = Icons.Rounded.Star
|
val Star = Icons.Rounded.Star
|
||||||
val StarBorder = Icons.Outlined.StarBorder
|
val StarBorder = Icons.Outlined.StarBorder
|
||||||
val Search = Icons.Rounded.Search
|
val Store = Icons.Rounded.Store
|
||||||
val MoreVert = Icons.Default.MoreVert
|
val StoreBorder = Icons.Outlined.Store
|
||||||
val Back = Icons.Rounded.ArrowBackIosNew
|
val Success = Icons.Rounded.CheckCircle
|
||||||
|
val Time = Icons.Default.AccessTime
|
||||||
|
val Tool = Icons.Default.Build
|
||||||
|
val Upgrade = Icons.Default.Upgrade
|
||||||
|
|
||||||
|
@OptIn(ExperimentalEncodingApi::class)
|
||||||
|
fun fromSvgBase64(base64String: String): ImageBitmap {
|
||||||
|
val svg = SVG.getFromString(Base64.decodeToString(base64String))
|
||||||
|
val drawable = PictureDrawable(svg.renderToPicture())
|
||||||
|
return drawable.toBitmap().asImageBitmap()
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalEncodingApi::class)
|
||||||
|
fun fromPngBase64(base64String: String): ImageBitmap {
|
||||||
|
val byteArray = Base64.decodeToByteArray(base64String)
|
||||||
|
return BitmapFactory.decodeByteArray(byteArray, 0, byteArray.size).asImageBitmap()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package top.fatweb.oxygen.toolbox.model
|
||||||
|
|
||||||
|
import androidx.room.TypeConverter
|
||||||
|
import kotlinx.datetime.LocalDateTime
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import top.fatweb.oxygen.toolbox.model.tool.ToolEntity.Platform
|
||||||
|
|
||||||
|
class Converters {
|
||||||
|
private val json = Json { ignoreUnknownKeys = true }
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
fun fromPlatform(platform: Platform): String = platform.name
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
fun toPlatform(name: String): Platform = Platform.valueOf(name)
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
fun fromStringList(stringList: List<String>): String = json.encodeToString(stringList)
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
fun toStringList(stringList: String): List<String> = json.decodeFromString(stringList)
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
fun fromLocalDateTime(localDateTime: LocalDateTime): String = localDateTime.toString()
|
||||||
|
|
||||||
|
@TypeConverter
|
||||||
|
fun toLocalDateTime(string: String): LocalDateTime = LocalDateTime.parse(string)
|
||||||
|
}
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
package top.fatweb.oxygen.toolbox.model
|
|
||||||
|
|
||||||
enum class DarkThemeConfig {
|
|
||||||
FOLLOW_SYSTEM,
|
|
||||||
LIGHT,
|
|
||||||
DARK,
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
package top.fatweb.oxygen.toolbox.model
|
|
||||||
|
|
||||||
enum class LanguageConfig(val code: String? = null) {
|
|
||||||
FOLLOW_SYSTEM,
|
|
||||||
CHINESE("cn"),
|
|
||||||
ENGLISH("en")
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
package top.fatweb.oxygen.toolbox.model
|
|
||||||
|
|
||||||
enum class LaunchPageConfig {
|
|
||||||
TOOLS,
|
|
||||||
STAR
|
|
||||||
}
|
|
||||||
13
app/src/main/kotlin/top/fatweb/oxygen/toolbox/model/Page.kt
Normal file
13
app/src/main/kotlin/top/fatweb/oxygen/toolbox/model/Page.kt
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
package top.fatweb.oxygen.toolbox.model
|
||||||
|
|
||||||
|
data class Page<T>(
|
||||||
|
val total: Long,
|
||||||
|
|
||||||
|
val pages: Long,
|
||||||
|
|
||||||
|
val size: Long,
|
||||||
|
|
||||||
|
val current: Long,
|
||||||
|
|
||||||
|
val records: List<T>
|
||||||
|
)
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package top.fatweb.oxygen.toolbox.model
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.catch
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.onStart
|
||||||
|
import top.fatweb.oxygen.toolbox.network.model.ResponseResult
|
||||||
|
|
||||||
|
sealed interface Result<out T> {
|
||||||
|
data class Success<T>(val data: T) : Result<T>
|
||||||
|
data class Fail(val message: String): Result<Nothing>
|
||||||
|
data class Error(val exception: Throwable) : Result<Nothing>
|
||||||
|
data object Loading : Result<Nothing>
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T> Flow<ResponseResult<T>>.asResult(): Flow<Result<T>> = map<ResponseResult<T>, Result<T>> {
|
||||||
|
if (it.success) {
|
||||||
|
Result.Success(it.data!!)
|
||||||
|
} else {
|
||||||
|
Result.Fail(it.msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onStart { emit(Result.Loading) }
|
||||||
|
.catch { emit(Result.Error(it)) }
|
||||||
|
|
||||||
|
fun <T, R> Result<T>.asExternalModel(block: (T) -> R): Result<R> =
|
||||||
|
when (this) {
|
||||||
|
is Result.Success -> {
|
||||||
|
Result.Success(block(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
is Result.Fail -> {
|
||||||
|
Result.Fail(message)
|
||||||
|
}
|
||||||
|
|
||||||
|
is Result.Error -> {
|
||||||
|
Result.Error(exception)
|
||||||
|
}
|
||||||
|
|
||||||
|
Result.Loading -> Result.Loading
|
||||||
|
}
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
package top.fatweb.oxygen.toolbox.model
|
|
||||||
|
|
||||||
enum class ThemeBrandConfig {
|
|
||||||
DEFAULT,
|
|
||||||
ANDROID
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package top.fatweb.oxygen.toolbox.model.lib
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Dependencies(
|
||||||
|
val metadata: Metadata,
|
||||||
|
|
||||||
|
val libraries: List<Library>,
|
||||||
|
|
||||||
|
val licenses: Map<String, License>
|
||||||
|
)
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package top.fatweb.oxygen.toolbox.model.lib
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Developer(
|
||||||
|
val name: String? = null,
|
||||||
|
|
||||||
|
val organisationUrl: String? = null
|
||||||
|
)
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package top.fatweb.oxygen.toolbox.model.lib
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Funding(
|
||||||
|
val platform: String,
|
||||||
|
|
||||||
|
val url: String
|
||||||
|
)
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package top.fatweb.oxygen.toolbox.model.lib
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Library(
|
||||||
|
val uniqueId: String,
|
||||||
|
|
||||||
|
val artifactVersion: String? = null,
|
||||||
|
|
||||||
|
val name: String? = null,
|
||||||
|
|
||||||
|
val description: String? = null,
|
||||||
|
|
||||||
|
val website: String? = null,
|
||||||
|
|
||||||
|
val developers: List<Developer>,
|
||||||
|
|
||||||
|
val organization: Organization? = null,
|
||||||
|
|
||||||
|
val scm: Scm? = null,
|
||||||
|
|
||||||
|
val licenses: List<String>,
|
||||||
|
|
||||||
|
val funding: List<Funding>,
|
||||||
|
|
||||||
|
val tag: String? = null
|
||||||
|
)
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package top.fatweb.oxygen.toolbox.model.lib
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class License(
|
||||||
|
val name: String,
|
||||||
|
|
||||||
|
val url: String? = null,
|
||||||
|
|
||||||
|
val year: String? = null,
|
||||||
|
|
||||||
|
val content: String? = null,
|
||||||
|
|
||||||
|
val internalHash: String? = null,
|
||||||
|
|
||||||
|
val hash: String,
|
||||||
|
|
||||||
|
val spdxId: String? = null
|
||||||
|
)
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package top.fatweb.oxygen.toolbox.model.lib
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Metadata(
|
||||||
|
val generated: String
|
||||||
|
)
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package top.fatweb.oxygen.toolbox.model.lib
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Organization(
|
||||||
|
val name: String,
|
||||||
|
|
||||||
|
val url: String? = null
|
||||||
|
)
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
package top.fatweb.oxygen.toolbox.model.lib
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Scm(
|
||||||
|
val connection: String? = null,
|
||||||
|
|
||||||
|
val developerConnection: String? = null,
|
||||||
|
|
||||||
|
val url: String? = null
|
||||||
|
)
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
package top.fatweb.oxygen.toolbox.model.tool
|
||||||
|
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.PrimaryKey
|
||||||
|
import androidx.room.TypeConverters
|
||||||
|
import kotlinx.datetime.LocalDateTime
|
||||||
|
import top.fatweb.oxygen.toolbox.model.Converters
|
||||||
|
|
||||||
|
@Entity(tableName = "tools")
|
||||||
|
@TypeConverters(Converters::class)
|
||||||
|
data class ToolEntity(
|
||||||
|
@PrimaryKey
|
||||||
|
val id: Long,
|
||||||
|
|
||||||
|
val name: String,
|
||||||
|
|
||||||
|
val toolId: String,
|
||||||
|
|
||||||
|
val icon: String,
|
||||||
|
|
||||||
|
val platform: Platform,
|
||||||
|
|
||||||
|
val description: String? = null,
|
||||||
|
|
||||||
|
val base: String? = null,
|
||||||
|
|
||||||
|
val authorUsername: String,
|
||||||
|
|
||||||
|
val authorNickname: String,
|
||||||
|
|
||||||
|
val authorAvatar: String,
|
||||||
|
|
||||||
|
val ver: String,
|
||||||
|
|
||||||
|
val keywords: List<String>,
|
||||||
|
|
||||||
|
val categories: List<String>,
|
||||||
|
|
||||||
|
val source: String? = null,
|
||||||
|
|
||||||
|
val dist: String? = null,
|
||||||
|
|
||||||
|
val entryPoint: String,
|
||||||
|
|
||||||
|
val createTime: LocalDateTime,
|
||||||
|
|
||||||
|
val updateTime: LocalDateTime,
|
||||||
|
|
||||||
|
val isStar: Boolean = false,
|
||||||
|
|
||||||
|
var isInstalled: Boolean = false,
|
||||||
|
|
||||||
|
@ColumnInfo(defaultValue = "NULL")
|
||||||
|
var upgrade: String? = null
|
||||||
|
) {
|
||||||
|
constructor(toolId: String, authorUsername: String, ver: String, upgrade: String? = null) :
|
||||||
|
this(
|
||||||
|
id = -1,
|
||||||
|
name = "Unknown",
|
||||||
|
toolId = toolId,
|
||||||
|
icon = "",
|
||||||
|
platform = Platform.Android,
|
||||||
|
authorUsername = authorUsername,
|
||||||
|
authorNickname = "Unknown",
|
||||||
|
authorAvatar = "",
|
||||||
|
ver = ver,
|
||||||
|
keywords = emptyList(),
|
||||||
|
categories = emptyList(),
|
||||||
|
entryPoint = "",
|
||||||
|
createTime = LocalDateTime(
|
||||||
|
year = 1970,
|
||||||
|
monthNumber = 1,
|
||||||
|
dayOfMonth = 1,
|
||||||
|
hour = 0,
|
||||||
|
minute = 0
|
||||||
|
),
|
||||||
|
updateTime = LocalDateTime(
|
||||||
|
year = 1970,
|
||||||
|
monthNumber = 1,
|
||||||
|
dayOfMonth = 1,
|
||||||
|
hour = 0,
|
||||||
|
minute = 0
|
||||||
|
),
|
||||||
|
upgrade = upgrade
|
||||||
|
)
|
||||||
|
|
||||||
|
enum class Platform {
|
||||||
|
Web,
|
||||||
|
|
||||||
|
Desktop,
|
||||||
|
|
||||||
|
Android
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package top.fatweb.oxygen.toolbox.model.tool
|
||||||
|
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import top.fatweb.oxygen.toolbox.icon.OxygenIcons
|
||||||
|
|
||||||
|
data class ToolGroup(
|
||||||
|
val id: String,
|
||||||
|
|
||||||
|
val icon: ImageVector = OxygenIcons.Box,
|
||||||
|
|
||||||
|
val title: String,
|
||||||
|
|
||||||
|
val tools: List<ToolEntity> = emptyList()
|
||||||
|
)
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package top.fatweb.oxygen.toolbox.model.userdata
|
||||||
|
|
||||||
|
enum class DarkThemeConfig {
|
||||||
|
FollowSystem,
|
||||||
|
Light,
|
||||||
|
Dark,
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package top.fatweb.oxygen.toolbox.model.userdata
|
||||||
|
|
||||||
|
enum class LanguageConfig(val code: String? = null) {
|
||||||
|
FollowSystem,
|
||||||
|
Chinese("cn"),
|
||||||
|
English("en")
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package top.fatweb.oxygen.toolbox.model.userdata
|
||||||
|
|
||||||
|
enum class LaunchPageConfig {
|
||||||
|
Tools,
|
||||||
|
Star
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package top.fatweb.oxygen.toolbox.model.userdata
|
||||||
|
|
||||||
|
enum class ThemeBrandConfig {
|
||||||
|
Default,
|
||||||
|
Android
|
||||||
|
}
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
package top.fatweb.oxygen.toolbox.model
|
package top.fatweb.oxygen.toolbox.model.userdata
|
||||||
|
|
||||||
data class UserData(
|
data class UserData(
|
||||||
val languageConfig: LanguageConfig,
|
val languageConfig: LanguageConfig,
|
||||||
val launchPageConfig: LaunchPageConfig,
|
val launchPageConfig: LaunchPageConfig,
|
||||||
val themeBrandConfig: ThemeBrandConfig,
|
val themeBrandConfig: ThemeBrandConfig,
|
||||||
val darkThemeConfig: DarkThemeConfig,
|
val darkThemeConfig: DarkThemeConfig,
|
||||||
val useDynamicColor: Boolean
|
val useDynamicColor: Boolean,
|
||||||
|
val isNotFirstLaunch: Boolean
|
||||||
)
|
)
|
||||||
@@ -57,13 +57,8 @@ internal class ConnectivityManagerNetworkMonitor @Inject constructor(
|
|||||||
}
|
}
|
||||||
.conflate()
|
.conflate()
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
private fun ConnectivityManager.isCurrentlyConnected() =
|
||||||
private fun ConnectivityManager.isCurrentlyConnected() = when {
|
activeNetwork
|
||||||
VERSION.SDK_INT >= VERSION_CODES.M ->
|
?.let(::getNetworkCapabilities)
|
||||||
activeNetwork
|
?.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) == true
|
||||||
?.let(::getNetworkCapabilities)
|
|
||||||
?.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
|
||||||
|
|
||||||
else -> activeNetworkInfo?.isConnected
|
|
||||||
} ?: false
|
|
||||||
}
|
}
|
||||||
@@ -43,8 +43,8 @@ class TimeZoneBroadcastMonitor @Inject constructor(
|
|||||||
val zonIdFromIntent = if (Build.VERSION.SDK_INT < VERSION_CODES.R) {
|
val zonIdFromIntent = if (Build.VERSION.SDK_INT < VERSION_CODES.R) {
|
||||||
null
|
null
|
||||||
} else {
|
} else {
|
||||||
intent.getStringExtra(Intent.EXTRA_TIMEZONE)?.let { timeZoneId ->
|
intent.getStringExtra(Intent.EXTRA_TIMEZONE)?.run {
|
||||||
val zoneId = ZoneId.of(timeZoneId, ZoneId.SHORT_IDS)
|
val zoneId = ZoneId.of(this, ZoneId.SHORT_IDS)
|
||||||
zoneId.toKotlinTimeZone()
|
zoneId.toKotlinTimeZone()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -64,5 +64,9 @@ class TimeZoneBroadcastMonitor @Inject constructor(
|
|||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
.conflate()
|
.conflate()
|
||||||
.flowOn(ioDispatcher)
|
.flowOn(ioDispatcher)
|
||||||
.shareIn(appScope, SharingStarted.WhileSubscribed(5.seconds.inWholeMilliseconds), 1)
|
.shareIn(
|
||||||
|
scope = appScope,
|
||||||
|
started = SharingStarted.WhileSubscribed(stopTimeoutMillis = 5.seconds.inWholeMilliseconds),
|
||||||
|
replay = 1
|
||||||
|
)
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package top.fatweb.oxygen.toolbox.navigation
|
||||||
|
|
||||||
|
import androidx.compose.animation.slideInHorizontally
|
||||||
|
import androidx.compose.animation.slideOutHorizontally
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import androidx.navigation.NavGraphBuilder
|
||||||
|
import androidx.navigation.NavOptions
|
||||||
|
import androidx.navigation.compose.composable
|
||||||
|
import top.fatweb.oxygen.toolbox.ui.about.AboutRoute
|
||||||
|
|
||||||
|
const val ABOUT_ROUTE = "about_route"
|
||||||
|
|
||||||
|
fun NavController.navigateToAbout(navOptions: NavOptions? = null) =
|
||||||
|
navigate(route = ABOUT_ROUTE, navOptions = navOptions)
|
||||||
|
|
||||||
|
fun NavGraphBuilder.aboutScreen(
|
||||||
|
onNavigateToLibraries: () -> Unit,
|
||||||
|
onBackClick: () -> Unit
|
||||||
|
) {
|
||||||
|
composable(
|
||||||
|
route = ABOUT_ROUTE,
|
||||||
|
enterTransition = {
|
||||||
|
slideInHorizontally { it }
|
||||||
|
},
|
||||||
|
popEnterTransition = null,
|
||||||
|
popExitTransition = {
|
||||||
|
slideOutHorizontally { it }
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
AboutRoute(
|
||||||
|
onNavigateToLibraries = onNavigateToLibraries,
|
||||||
|
onBackClick = onBackClick
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package top.fatweb.oxygen.toolbox.navigation
|
||||||
|
|
||||||
|
import androidx.compose.animation.slideInHorizontally
|
||||||
|
import androidx.compose.animation.slideOutHorizontally
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import androidx.navigation.NavGraphBuilder
|
||||||
|
import androidx.navigation.NavOptions
|
||||||
|
import androidx.navigation.compose.composable
|
||||||
|
import top.fatweb.oxygen.toolbox.ui.about.LibrariesRoute
|
||||||
|
|
||||||
|
const val LIBRARIES_ROUTE = "libraries_route"
|
||||||
|
|
||||||
|
fun NavController.navigateToLibraries(navOptions: NavOptions? = null) =
|
||||||
|
navigate(route = LIBRARIES_ROUTE, navOptions = navOptions)
|
||||||
|
|
||||||
|
fun NavGraphBuilder.librariesScreen(
|
||||||
|
onBackClick: () -> Unit
|
||||||
|
) {
|
||||||
|
composable(
|
||||||
|
route = LIBRARIES_ROUTE,
|
||||||
|
enterTransition = {
|
||||||
|
slideInHorizontally { it }
|
||||||
|
},
|
||||||
|
popEnterTransition = null,
|
||||||
|
popExitTransition = {
|
||||||
|
slideOutHorizontally { it }
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
LibrariesRoute(onBackClick = onBackClick)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,31 +1,73 @@
|
|||||||
package top.fatweb.oxygen.toolbox.navigation
|
package top.fatweb.oxygen.toolbox.navigation
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import androidx.navigation.NavDestination
|
||||||
import androidx.navigation.compose.NavHost
|
import androidx.navigation.compose.NavHost
|
||||||
import top.fatweb.oxygen.toolbox.ui.OxygenAppState
|
import top.fatweb.oxygen.toolbox.ui.OxygenAppState
|
||||||
|
import top.fatweb.oxygen.toolbox.ui.util.LocalFullScreen
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun OxygenNavHost(
|
fun OxygenNavHost(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
appState: OxygenAppState,
|
appState: OxygenAppState,
|
||||||
onShowSnackbar: suspend (String, String?) -> Boolean,
|
startDestination: String,
|
||||||
startDestination: String
|
isVertical: Boolean,
|
||||||
|
searchValue: String,
|
||||||
|
searchCount: Int,
|
||||||
|
onShowSnackbar: suspend (message: String, action: String?) -> Boolean
|
||||||
) {
|
) {
|
||||||
val navController = appState.navController
|
val navController = appState.navController
|
||||||
|
val fullScreen = LocalFullScreen.current
|
||||||
|
|
||||||
|
LaunchedEffect(navController) {
|
||||||
|
navController.addOnDestinationChangedListener(object :
|
||||||
|
NavController.OnDestinationChangedListener {
|
||||||
|
override fun onDestinationChanged(
|
||||||
|
controller: NavController,
|
||||||
|
destination: NavDestination,
|
||||||
|
arguments: Bundle?
|
||||||
|
) {
|
||||||
|
fullScreen.onStateChange.invoke(false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
NavHost(
|
NavHost(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
navController = navController,
|
navController = navController,
|
||||||
startDestination = startDestination
|
startDestination = startDestination
|
||||||
) {
|
) {
|
||||||
searchScreen(
|
aboutScreen(
|
||||||
|
onBackClick = navController::popBackStack,
|
||||||
|
onNavigateToLibraries = navController::navigateToLibraries
|
||||||
|
)
|
||||||
|
librariesScreen(
|
||||||
onBackClick = navController::popBackStack
|
onBackClick = navController::popBackStack
|
||||||
)
|
)
|
||||||
|
toolStoreScreen(
|
||||||
|
isVertical = isVertical,
|
||||||
|
searchValue = searchValue,
|
||||||
|
searchCount = searchCount,
|
||||||
|
onNavigateToToolView = navController::navigateToToolView
|
||||||
|
)
|
||||||
toolsScreen(
|
toolsScreen(
|
||||||
|
isVertical = isVertical,
|
||||||
|
searchValue = searchValue,
|
||||||
|
onShowSnackbar = onShowSnackbar,
|
||||||
|
onNavigateToToolView = navController::navigateToToolView,
|
||||||
|
onNavigateToToolStore = { appState.navigateToTopLevelDestination(TopLevelDestination.ToolStore) }
|
||||||
)
|
)
|
||||||
starScreen(
|
starScreen(
|
||||||
|
isVertical = isVertical,
|
||||||
|
searchValue = searchValue,
|
||||||
|
onNavigateToToolView = navController::navigateToToolView
|
||||||
|
)
|
||||||
|
toolViewScreen(
|
||||||
|
onBackClick = navController::popBackStack
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
package top.fatweb.oxygen.toolbox.navigation
|
|
||||||
|
|
||||||
import androidx.navigation.NavController
|
|
||||||
import androidx.navigation.NavGraphBuilder
|
|
||||||
import androidx.navigation.NavOptions
|
|
||||||
import androidx.navigation.compose.composable
|
|
||||||
import top.fatweb.oxygen.toolbox.ui.search.SearchRoute
|
|
||||||
|
|
||||||
const val SEARCH_ROUTE = "search_route"
|
|
||||||
|
|
||||||
fun NavController.navigateToSearch(navOptions: NavOptions? = null) =
|
|
||||||
navigate(SEARCH_ROUTE, navOptions)
|
|
||||||
|
|
||||||
fun NavGraphBuilder.searchScreen(
|
|
||||||
onBackClick: () -> Unit
|
|
||||||
) {
|
|
||||||
composable(
|
|
||||||
route = SEARCH_ROUTE
|
|
||||||
) {
|
|
||||||
SearchRoute(onBackClick = onBackClick)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,18 +1,49 @@
|
|||||||
package top.fatweb.oxygen.toolbox.navigation
|
package top.fatweb.oxygen.toolbox.navigation
|
||||||
|
|
||||||
|
import androidx.compose.animation.slideInHorizontally
|
||||||
|
import androidx.compose.animation.slideInVertically
|
||||||
|
import androidx.compose.animation.slideOutHorizontally
|
||||||
|
import androidx.compose.animation.slideOutVertically
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import androidx.navigation.NavGraphBuilder
|
import androidx.navigation.NavGraphBuilder
|
||||||
import androidx.navigation.NavOptions
|
import androidx.navigation.NavOptions
|
||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
|
import top.fatweb.oxygen.toolbox.ui.star.StarRoute
|
||||||
|
|
||||||
const val STAR_ROUTE = "star_route"
|
const val STAR_ROUTE = "star_route"
|
||||||
|
|
||||||
fun NavController.navigateToStar(navOptions: NavOptions) = navigate(STAR_ROUTE, navOptions)
|
fun NavController.navigateToStar(navOptions: NavOptions) =
|
||||||
|
navigate(route = STAR_ROUTE, navOptions = navOptions)
|
||||||
|
|
||||||
fun NavGraphBuilder.starScreen() {
|
fun NavGraphBuilder.starScreen(
|
||||||
|
isVertical: Boolean,
|
||||||
|
searchValue: String,
|
||||||
|
onNavigateToToolView: (username: String, toolId: String, preview: Boolean) -> Unit
|
||||||
|
) {
|
||||||
composable(
|
composable(
|
||||||
route = STAR_ROUTE
|
route = STAR_ROUTE,
|
||||||
) {
|
enterTransition = {
|
||||||
|
when (initialState.destination.route) {
|
||||||
|
TOOL_STORE_ROUTE, TOOLS_ROUTE ->
|
||||||
|
if (isVertical) slideInHorizontally { it }
|
||||||
|
else slideInVertically { it }
|
||||||
|
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
exitTransition = {
|
||||||
|
when (targetState.destination.route) {
|
||||||
|
TOOL_STORE_ROUTE, TOOLS_ROUTE ->
|
||||||
|
if (isVertical) slideOutHorizontally { it }
|
||||||
|
else slideOutVertically { it }
|
||||||
|
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
StarRoute(
|
||||||
|
searchValue = searchValue,
|
||||||
|
onNavigateToToolView = onNavigateToToolView
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package top.fatweb.oxygen.toolbox.navigation
|
||||||
|
|
||||||
|
import androidx.compose.animation.slideInHorizontally
|
||||||
|
import androidx.compose.animation.slideInVertically
|
||||||
|
import androidx.compose.animation.slideOutHorizontally
|
||||||
|
import androidx.compose.animation.slideOutVertically
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import androidx.navigation.NavGraphBuilder
|
||||||
|
import androidx.navigation.NavOptions
|
||||||
|
import androidx.navigation.compose.composable
|
||||||
|
import top.fatweb.oxygen.toolbox.ui.store.ToolStoreRoute
|
||||||
|
|
||||||
|
const val TOOL_STORE_ROUTE = "tool_store_route"
|
||||||
|
|
||||||
|
fun NavController.navigateToToolStore(navOptions: NavOptions? = null) =
|
||||||
|
navigate(route = TOOL_STORE_ROUTE, navOptions = navOptions)
|
||||||
|
|
||||||
|
fun NavGraphBuilder.toolStoreScreen(
|
||||||
|
isVertical: Boolean,
|
||||||
|
searchValue: String,
|
||||||
|
searchCount: Int,
|
||||||
|
onNavigateToToolView: (username: String, toolId: String, preview: Boolean) -> Unit
|
||||||
|
) {
|
||||||
|
composable(
|
||||||
|
route = TOOL_STORE_ROUTE,
|
||||||
|
enterTransition = {
|
||||||
|
when (initialState.destination.route) {
|
||||||
|
TOOLS_ROUTE, STAR_ROUTE ->
|
||||||
|
if (isVertical) slideInHorizontally { -it }
|
||||||
|
else slideInVertically { -it }
|
||||||
|
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
exitTransition = {
|
||||||
|
when (targetState.destination.route) {
|
||||||
|
TOOLS_ROUTE, STAR_ROUTE ->
|
||||||
|
if (isVertical) slideOutHorizontally { -it }
|
||||||
|
else slideOutVertically { -it }
|
||||||
|
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
ToolStoreRoute(
|
||||||
|
searchValue = searchValue,
|
||||||
|
searchCount = searchCount,
|
||||||
|
onNavigateToToolView = onNavigateToToolView
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
package top.fatweb.oxygen.toolbox.navigation
|
||||||
|
|
||||||
|
import androidx.lifecycle.SavedStateHandle
|
||||||
|
import androidx.navigation.NavController
|
||||||
|
import androidx.navigation.NavGraphBuilder
|
||||||
|
import androidx.navigation.NavOptionsBuilder
|
||||||
|
import androidx.navigation.NavType
|
||||||
|
import androidx.navigation.compose.composable
|
||||||
|
import androidx.navigation.navArgument
|
||||||
|
import top.fatweb.oxygen.toolbox.ui.view.ToolViewRoute
|
||||||
|
import java.net.URLDecoder
|
||||||
|
import java.net.URLEncoder
|
||||||
|
import kotlin.text.Charsets.UTF_8
|
||||||
|
|
||||||
|
private val URL_CHARACTER_ENCODING = UTF_8.name()
|
||||||
|
|
||||||
|
internal const val USER_NAME_ARG = "username"
|
||||||
|
internal const val TOOL_ID_ARG = "toolId"
|
||||||
|
internal const val PREVIEW_ARG = "preview"
|
||||||
|
const val TOOL_VIEW_ROUTE = "tool_view_route"
|
||||||
|
|
||||||
|
internal class ToolViewArgs(
|
||||||
|
val username: String,
|
||||||
|
val toolId: String,
|
||||||
|
val preview: Boolean
|
||||||
|
) {
|
||||||
|
constructor(savedStateHandle: SavedStateHandle) :
|
||||||
|
this(
|
||||||
|
URLDecoder.decode(
|
||||||
|
checkNotNull(savedStateHandle[USER_NAME_ARG]),
|
||||||
|
URL_CHARACTER_ENCODING
|
||||||
|
),
|
||||||
|
URLDecoder.decode(
|
||||||
|
checkNotNull(savedStateHandle[TOOL_ID_ARG]),
|
||||||
|
URL_CHARACTER_ENCODING
|
||||||
|
),
|
||||||
|
checkNotNull(savedStateHandle[PREVIEW_ARG])
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun NavController.navigateToToolView(
|
||||||
|
username: String,
|
||||||
|
toolId: String,
|
||||||
|
preview: Boolean,
|
||||||
|
navOptions: NavOptionsBuilder.() -> Unit = {}
|
||||||
|
) {
|
||||||
|
val encodedUsername = URLEncoder.encode(username, URL_CHARACTER_ENCODING)
|
||||||
|
val encodedToolId = URLEncoder.encode(toolId, URL_CHARACTER_ENCODING)
|
||||||
|
val newRoute = "$TOOL_VIEW_ROUTE/$encodedUsername/$encodedToolId?$PREVIEW_ARG=$preview"
|
||||||
|
navigate(newRoute) {
|
||||||
|
navOptions()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun NavGraphBuilder.toolViewScreen(
|
||||||
|
onBackClick: () -> Unit
|
||||||
|
) {
|
||||||
|
composable(
|
||||||
|
route = "${TOOL_VIEW_ROUTE}/{$USER_NAME_ARG}/{$TOOL_ID_ARG}?$PREVIEW_ARG={$PREVIEW_ARG}",
|
||||||
|
arguments = listOf(
|
||||||
|
navArgument(USER_NAME_ARG) { type = NavType.StringType },
|
||||||
|
navArgument(TOOL_ID_ARG) { type = NavType.StringType },
|
||||||
|
navArgument(PREVIEW_ARG) { type = NavType.BoolType; defaultValue = false }
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
ToolViewRoute(
|
||||||
|
onBackClick = onBackClick
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,16 +1,61 @@
|
|||||||
package top.fatweb.oxygen.toolbox.navigation
|
package top.fatweb.oxygen.toolbox.navigation
|
||||||
|
|
||||||
|
import androidx.compose.animation.slideInHorizontally
|
||||||
|
import androidx.compose.animation.slideInVertically
|
||||||
|
import androidx.compose.animation.slideOutHorizontally
|
||||||
|
import androidx.compose.animation.slideOutVertically
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
import androidx.navigation.NavGraphBuilder
|
import androidx.navigation.NavGraphBuilder
|
||||||
import androidx.navigation.NavOptions
|
import androidx.navigation.NavOptions
|
||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
|
import top.fatweb.oxygen.toolbox.ui.tools.ToolsRoute
|
||||||
|
|
||||||
const val TOOLS_ROUTE = "tools_route"
|
const val TOOLS_ROUTE = "tools_route"
|
||||||
|
|
||||||
fun NavController.navigateToTools(navOptions: NavOptions) = navigate(TOOLS_ROUTE, navOptions)
|
fun NavController.navigateToTools(navOptions: NavOptions) =
|
||||||
|
navigate(route = TOOLS_ROUTE, navOptions = navOptions)
|
||||||
|
|
||||||
fun NavGraphBuilder.toolsScreen() {
|
fun NavGraphBuilder.toolsScreen(
|
||||||
|
isVertical: Boolean,
|
||||||
|
searchValue: String,
|
||||||
|
onNavigateToToolView: (username: String, toolId: String, preview: Boolean) -> Unit,
|
||||||
|
onNavigateToToolStore: () -> Unit,
|
||||||
|
onShowSnackbar: suspend (message: String, action: String?) -> Boolean
|
||||||
|
) {
|
||||||
composable(
|
composable(
|
||||||
route = TOOLS_ROUTE
|
route = TOOLS_ROUTE,
|
||||||
) { }
|
enterTransition = {
|
||||||
|
when (initialState.destination.route) {
|
||||||
|
TOOL_STORE_ROUTE ->
|
||||||
|
if (isVertical) slideInHorizontally { it }
|
||||||
|
else slideInVertically { it }
|
||||||
|
|
||||||
|
STAR_ROUTE ->
|
||||||
|
if (isVertical) slideInHorizontally { -it }
|
||||||
|
else slideInVertically { -it }
|
||||||
|
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
exitTransition = {
|
||||||
|
when (targetState.destination.route) {
|
||||||
|
TOOL_STORE_ROUTE ->
|
||||||
|
if (isVertical) slideOutHorizontally { it }
|
||||||
|
else slideOutVertically { it }
|
||||||
|
|
||||||
|
STAR_ROUTE ->
|
||||||
|
if (isVertical) slideOutHorizontally { -it }
|
||||||
|
else slideOutVertically { -it }
|
||||||
|
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
ToolsRoute(
|
||||||
|
searchValue = searchValue,
|
||||||
|
onNavigateToToolView = onNavigateToToolView,
|
||||||
|
onNavigateToToolStore = onNavigateToToolStore,
|
||||||
|
onShowSnackbar = onShowSnackbar
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,23 +1,35 @@
|
|||||||
package top.fatweb.oxygen.toolbox.navigation
|
package top.fatweb.oxygen.toolbox.navigation
|
||||||
|
|
||||||
|
import androidx.annotation.StringRes
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import top.fatweb.oxygen.toolbox.R
|
import top.fatweb.oxygen.toolbox.R
|
||||||
import top.fatweb.oxygen.toolbox.icon.OxygenIcons
|
import top.fatweb.oxygen.toolbox.icon.OxygenIcons
|
||||||
|
|
||||||
enum class TopLevelDestination(
|
enum class TopLevelDestination(
|
||||||
|
val route: String,
|
||||||
val selectedIcon: ImageVector,
|
val selectedIcon: ImageVector,
|
||||||
val unselectedIcon: ImageVector,
|
val unselectedIcon: ImageVector,
|
||||||
val iconTextId: Int,
|
@StringRes val iconTextId: Int,
|
||||||
val titleTextId: Int
|
@StringRes val titleTextId: Int
|
||||||
) {
|
) {
|
||||||
TOOLS(
|
ToolStore(
|
||||||
|
route = "tool_store_route",
|
||||||
|
selectedIcon = OxygenIcons.Store,
|
||||||
|
unselectedIcon = OxygenIcons.StoreBorder,
|
||||||
|
iconTextId = R.string.feature_store_title,
|
||||||
|
titleTextId = R.string.feature_store_title
|
||||||
|
),
|
||||||
|
|
||||||
|
Tools(
|
||||||
|
route = "tools_route",
|
||||||
selectedIcon = OxygenIcons.Home,
|
selectedIcon = OxygenIcons.Home,
|
||||||
unselectedIcon = OxygenIcons.HomeBorder,
|
unselectedIcon = OxygenIcons.HomeBorder,
|
||||||
iconTextId = R.string.feature_tools_title,
|
iconTextId = R.string.feature_tools_title,
|
||||||
titleTextId = R.string.feature_tools_title
|
titleTextId = R.string.feature_tools_title
|
||||||
),
|
),
|
||||||
|
|
||||||
STAR(
|
Star(
|
||||||
|
route = "star_route",
|
||||||
selectedIcon = OxygenIcons.Star,
|
selectedIcon = OxygenIcons.Star,
|
||||||
unselectedIcon = OxygenIcons.StarBorder,
|
unselectedIcon = OxygenIcons.StarBorder,
|
||||||
iconTextId = R.string.feature_star_title,
|
iconTextId = R.string.feature_star_title,
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package top.fatweb.oxygen.toolbox.network.model
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import top.fatweb.oxygen.toolbox.model.Page
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class PageVo<T>(
|
||||||
|
val total: Long,
|
||||||
|
|
||||||
|
val pages: Long,
|
||||||
|
|
||||||
|
val size: Long,
|
||||||
|
|
||||||
|
val current: Long,
|
||||||
|
|
||||||
|
val records: List<T>
|
||||||
|
)
|
||||||
|
|
||||||
|
fun <T, R> PageVo<T>.asExternalModel(block: (T) -> R): Page<R> =
|
||||||
|
Page(
|
||||||
|
total = total,
|
||||||
|
pages = pages,
|
||||||
|
size = size,
|
||||||
|
current = current,
|
||||||
|
records = records.map(block)
|
||||||
|
)
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package top.fatweb.oxygen.toolbox.network.model
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ResponseResult<T>(
|
||||||
|
val code: Long,
|
||||||
|
|
||||||
|
val success: Boolean,
|
||||||
|
|
||||||
|
val msg: String,
|
||||||
|
|
||||||
|
val data: T? = null
|
||||||
|
)
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package top.fatweb.oxygen.toolbox.network.model
|
||||||
|
|
||||||
|
import kotlinx.datetime.LocalDateTime
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import top.fatweb.oxygen.toolbox.model.tool.ToolEntity
|
||||||
|
import top.fatweb.oxygen.toolbox.network.serializer.LocalDateTimeSerializer
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ToolBaseVo(
|
||||||
|
val id: Long,
|
||||||
|
|
||||||
|
val name: String,
|
||||||
|
|
||||||
|
val source: ToolDataVo? = null,
|
||||||
|
|
||||||
|
val dist: ToolDataVo,
|
||||||
|
|
||||||
|
val platform: Platform? = null,
|
||||||
|
|
||||||
|
val compiled: Boolean? = null,
|
||||||
|
|
||||||
|
@Serializable(LocalDateTimeSerializer::class)
|
||||||
|
val createTime: LocalDateTime? = null,
|
||||||
|
|
||||||
|
@Serializable(LocalDateTimeSerializer::class)
|
||||||
|
val updateTime: LocalDateTime? = null
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
enum class Platform {
|
||||||
|
@SerialName("WEB")
|
||||||
|
Web,
|
||||||
|
|
||||||
|
@SerialName("DESKTOP")
|
||||||
|
Desktop,
|
||||||
|
|
||||||
|
@SerialName("ANDROID")
|
||||||
|
Android;
|
||||||
|
|
||||||
|
override fun toString(): String =
|
||||||
|
javaClass.getField(name).getAnnotation(SerialName::class.java)!!.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun ToolBaseVo.Platform.asExternalModel() = ToolEntity.Platform.valueOf(this.name)
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package top.fatweb.oxygen.toolbox.network.model
|
||||||
|
|
||||||
|
import kotlinx.datetime.LocalDateTime
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import top.fatweb.oxygen.toolbox.network.serializer.LocalDateTimeSerializer
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ToolCategoryVo(
|
||||||
|
val id: Long,
|
||||||
|
|
||||||
|
val name: String,
|
||||||
|
|
||||||
|
val enable: Boolean,
|
||||||
|
|
||||||
|
@Serializable(LocalDateTimeSerializer::class)
|
||||||
|
val createTime: LocalDateTime,
|
||||||
|
|
||||||
|
@Serializable(LocalDateTimeSerializer::class)
|
||||||
|
val updateTime: LocalDateTime
|
||||||
|
)
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package top.fatweb.oxygen.toolbox.network.model
|
||||||
|
|
||||||
|
import kotlinx.datetime.LocalDateTime
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import top.fatweb.oxygen.toolbox.network.serializer.LocalDateTimeSerializer
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ToolDataVo(
|
||||||
|
val id: Long,
|
||||||
|
|
||||||
|
val data: String,
|
||||||
|
|
||||||
|
@Serializable(LocalDateTimeSerializer::class)
|
||||||
|
val createTime: LocalDateTime? = null,
|
||||||
|
|
||||||
|
@Serializable(LocalDateTimeSerializer::class)
|
||||||
|
val updateTime: LocalDateTime? = null
|
||||||
|
)
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
package top.fatweb.oxygen.toolbox.network.model
|
||||||
|
|
||||||
|
import kotlinx.datetime.LocalDateTime
|
||||||
|
import kotlinx.serialization.SerialName
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import top.fatweb.oxygen.toolbox.model.tool.ToolEntity
|
||||||
|
import top.fatweb.oxygen.toolbox.network.serializer.LocalDateTimeSerializer
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ToolVo(
|
||||||
|
val id: Long,
|
||||||
|
|
||||||
|
val name: String,
|
||||||
|
|
||||||
|
val toolId: String,
|
||||||
|
|
||||||
|
val icon: String,
|
||||||
|
|
||||||
|
val platform: ToolBaseVo.Platform,
|
||||||
|
|
||||||
|
val description: String? = null,
|
||||||
|
|
||||||
|
val base: ToolBaseVo? = null,
|
||||||
|
|
||||||
|
val author: UserWithInfoVo,
|
||||||
|
|
||||||
|
val ver: String,
|
||||||
|
|
||||||
|
val keywords: List<String>,
|
||||||
|
|
||||||
|
val categories: List<ToolCategoryVo>,
|
||||||
|
|
||||||
|
val source: ToolDataVo? = null,
|
||||||
|
|
||||||
|
val dist: ToolDataVo? = null,
|
||||||
|
|
||||||
|
val entryPoint: String,
|
||||||
|
|
||||||
|
val publish: Long,
|
||||||
|
|
||||||
|
val review: ReviewType,
|
||||||
|
|
||||||
|
@Serializable(LocalDateTimeSerializer::class)
|
||||||
|
val createTime: LocalDateTime,
|
||||||
|
|
||||||
|
@Serializable(LocalDateTimeSerializer::class)
|
||||||
|
val updateTime: LocalDateTime
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
enum class ReviewType {
|
||||||
|
@SerialName("NONE")
|
||||||
|
None,
|
||||||
|
|
||||||
|
@SerialName("PROCESSING")
|
||||||
|
Processing,
|
||||||
|
|
||||||
|
@SerialName("PASS")
|
||||||
|
Pass,
|
||||||
|
|
||||||
|
@SerialName("REJECT")
|
||||||
|
Reject
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun ToolVo.asExternalModel() = ToolEntity(
|
||||||
|
id = id,
|
||||||
|
name = name,
|
||||||
|
toolId = toolId,
|
||||||
|
icon = icon,
|
||||||
|
platform = platform.asExternalModel(),
|
||||||
|
description = description,
|
||||||
|
base = base?.dist?.data,
|
||||||
|
authorUsername = author.username,
|
||||||
|
authorNickname = author.userInfo.nickname,
|
||||||
|
authorAvatar = author.userInfo.avatar,
|
||||||
|
ver = ver,
|
||||||
|
keywords = keywords,
|
||||||
|
categories = categories.map { it.name },
|
||||||
|
source = source?.data,
|
||||||
|
dist = dist?.data,
|
||||||
|
entryPoint = entryPoint,
|
||||||
|
createTime = createTime,
|
||||||
|
updateTime = updateTime
|
||||||
|
)
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package top.fatweb.oxygen.toolbox.network.model
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class UserWithInfoVo(
|
||||||
|
val id: Long,
|
||||||
|
|
||||||
|
val username: String,
|
||||||
|
|
||||||
|
val userInfo: UserInfoVo
|
||||||
|
) {
|
||||||
|
@Serializable
|
||||||
|
data class UserInfoVo(
|
||||||
|
val id: Long,
|
||||||
|
|
||||||
|
val nickname: String,
|
||||||
|
|
||||||
|
val avatar: String
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package top.fatweb.oxygen.toolbox.network.paging
|
||||||
|
|
||||||
|
import androidx.paging.PagingSource
|
||||||
|
import androidx.paging.PagingState
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import top.fatweb.oxygen.toolbox.data.network.OxygenNetworkDataSource
|
||||||
|
import top.fatweb.oxygen.toolbox.data.tool.dao.ToolDao
|
||||||
|
import top.fatweb.oxygen.toolbox.model.tool.ToolEntity
|
||||||
|
import top.fatweb.oxygen.toolbox.network.model.ToolVo
|
||||||
|
import top.fatweb.oxygen.toolbox.network.model.asExternalModel
|
||||||
|
|
||||||
|
internal class ToolStorePagingSource(
|
||||||
|
private val oxygenNetworkDataSource: OxygenNetworkDataSource,
|
||||||
|
private val toolDao: ToolDao,
|
||||||
|
private val searchValue: String
|
||||||
|
) : PagingSource<Int, ToolEntity>() {
|
||||||
|
override fun getRefreshKey(state: PagingState<Int, ToolEntity>): Int? =
|
||||||
|
state.anchorPosition?.let {
|
||||||
|
val anchorPage = state.closestPageToPosition(it)
|
||||||
|
anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, ToolEntity> {
|
||||||
|
return try {
|
||||||
|
val currentPage = params.key ?: 1
|
||||||
|
val (_, success, msg, data) = oxygenNetworkDataSource.getStore(
|
||||||
|
searchValue = searchValue,
|
||||||
|
currentPage = currentPage
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
return LoadResult.Error(RuntimeException(msg))
|
||||||
|
}
|
||||||
|
val (_, pages, _, _, records) = data!!
|
||||||
|
|
||||||
|
LoadResult.Page(
|
||||||
|
data = records.map(ToolVo::asExternalModel).map { toolEntity ->
|
||||||
|
toolDao.selectToolByUsernameAndToolId(
|
||||||
|
username = toolEntity.authorUsername,
|
||||||
|
toolId = toolEntity.toolId
|
||||||
|
).first()?.let {
|
||||||
|
if (it.id == toolEntity.id) {
|
||||||
|
it
|
||||||
|
} else {
|
||||||
|
it.copy(upgrade = toolEntity.ver).also { copy ->
|
||||||
|
toolDao.updateTool(copy)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} ?: toolEntity
|
||||||
|
},
|
||||||
|
prevKey = if (currentPage == 1) null else currentPage - 1,
|
||||||
|
nextKey = if (currentPage < pages) currentPage + 1 else null
|
||||||
|
)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
LoadResult.Error(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
package top.fatweb.oxygen.toolbox.network.retrofit
|
||||||
|
|
||||||
|
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.flow
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import okhttp3.Call
|
||||||
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
|
import retrofit2.Retrofit
|
||||||
|
import retrofit2.http.GET
|
||||||
|
import retrofit2.http.Path
|
||||||
|
import retrofit2.http.Query
|
||||||
|
import top.fatweb.oxygen.toolbox.BuildConfig
|
||||||
|
import top.fatweb.oxygen.toolbox.data.network.OxygenNetworkDataSource
|
||||||
|
import top.fatweb.oxygen.toolbox.model.Result
|
||||||
|
import top.fatweb.oxygen.toolbox.model.asResult
|
||||||
|
import top.fatweb.oxygen.toolbox.network.model.PageVo
|
||||||
|
import top.fatweb.oxygen.toolbox.network.model.ResponseResult
|
||||||
|
import top.fatweb.oxygen.toolbox.network.model.ToolBaseVo
|
||||||
|
import top.fatweb.oxygen.toolbox.network.model.ToolVo
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
private interface RetrofitOxygenNetworkApi {
|
||||||
|
@GET(value = "/tool/store")
|
||||||
|
suspend fun getStore(
|
||||||
|
@Query("currentPage") currentPage: Int,
|
||||||
|
@Query("searchValue") searchValue: String,
|
||||||
|
@Query("platform") platform: ToolBaseVo.Platform? = ToolBaseVo.Platform.Android
|
||||||
|
): ResponseResult<PageVo<ToolVo>>
|
||||||
|
|
||||||
|
@GET(value = "/tool/detail/{username}/{toolId}/{ver}")
|
||||||
|
suspend fun detail(
|
||||||
|
@Path("username") username: String,
|
||||||
|
@Path("toolId") toolId: String,
|
||||||
|
@Path("ver") ver: String,
|
||||||
|
@Query("platform") platform: ToolBaseVo.Platform? = ToolBaseVo.Platform.Android
|
||||||
|
): ResponseResult<ToolVo>
|
||||||
|
}
|
||||||
|
|
||||||
|
private const val API_BASE_URL = BuildConfig.API_URL
|
||||||
|
|
||||||
|
internal class RetrofitOxygenNetwork @Inject constructor(
|
||||||
|
networkJson: Json,
|
||||||
|
okhttpCallFactory: dagger.Lazy<Call.Factory>
|
||||||
|
) : OxygenNetworkDataSource {
|
||||||
|
private val networkApi = Retrofit.Builder()
|
||||||
|
.baseUrl(API_BASE_URL)
|
||||||
|
.callFactory { okhttpCallFactory.get().newCall(it) }
|
||||||
|
.addConverterFactory(
|
||||||
|
networkJson.asConverterFactory("application/json".toMediaType())
|
||||||
|
)
|
||||||
|
.build()
|
||||||
|
.create(RetrofitOxygenNetworkApi::class.java)
|
||||||
|
|
||||||
|
override suspend fun getStore(
|
||||||
|
searchValue: String,
|
||||||
|
currentPage: Int
|
||||||
|
): ResponseResult<PageVo<ToolVo>> =
|
||||||
|
networkApi.getStore(searchValue = searchValue, currentPage = currentPage)
|
||||||
|
|
||||||
|
override fun detail(
|
||||||
|
username: String,
|
||||||
|
toolId: String,
|
||||||
|
ver: String,
|
||||||
|
platform: ToolBaseVo.Platform
|
||||||
|
): Flow<Result<ToolVo>> =
|
||||||
|
flow {
|
||||||
|
emit(
|
||||||
|
networkApi.detail(
|
||||||
|
username = username,
|
||||||
|
toolId = toolId,
|
||||||
|
ver = ver,
|
||||||
|
platform = platform
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}.asResult()
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package top.fatweb.oxygen.toolbox.network.serializer
|
||||||
|
|
||||||
|
import kotlinx.datetime.LocalDateTime
|
||||||
|
import kotlinx.serialization.KSerializer
|
||||||
|
import kotlinx.serialization.descriptors.PrimitiveKind
|
||||||
|
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
|
||||||
|
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||||
|
import kotlinx.serialization.encoding.Decoder
|
||||||
|
import kotlinx.serialization.encoding.Encoder
|
||||||
|
|
||||||
|
object LocalDateTimeSerializer : KSerializer<LocalDateTime> {
|
||||||
|
override val descriptor: SerialDescriptor =
|
||||||
|
PrimitiveSerialDescriptor(serialName = "LocalDateTime", kind = PrimitiveKind.STRING)
|
||||||
|
|
||||||
|
override fun deserialize(decoder: Decoder): LocalDateTime =
|
||||||
|
LocalDateTime.parse(input = decoder.decodeString().removeSuffix("Z"))
|
||||||
|
|
||||||
|
override fun serialize(encoder: Encoder, value: LocalDateTime) {
|
||||||
|
encoder.encodeString(value.toString().padEnd(length = 24, padChar = 'Z'))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package top.fatweb.oxygen.toolbox.repository.lib
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import top.fatweb.oxygen.toolbox.model.lib.Dependencies
|
||||||
|
|
||||||
|
interface DepRepository {
|
||||||
|
fun searchName(name: String): Flow<Dependencies>
|
||||||
|
|
||||||
|
fun getSearchNameCount(): Flow<Int>
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package top.fatweb.oxygen.toolbox.repository.lib.impl
|
||||||
|
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
|
import kotlinx.coroutines.flow.flowOf
|
||||||
|
import top.fatweb.oxygen.toolbox.data.lib.DepDataSource
|
||||||
|
import top.fatweb.oxygen.toolbox.model.lib.Dependencies
|
||||||
|
import top.fatweb.oxygen.toolbox.repository.lib.DepRepository
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class LocalDepRepository @Inject constructor(
|
||||||
|
private val depDataSource: DepDataSource
|
||||||
|
) : DepRepository {
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
override fun searchName(name: String): Flow<Dependencies> =
|
||||||
|
depDataSource.dependencies.flatMapLatest { dependencies ->
|
||||||
|
flowOf(dependencies.copy(
|
||||||
|
libraries = dependencies.libraries.filter {
|
||||||
|
it.name?.lowercase()?.contains(Regex("^.*${name.lowercase()}.*$")) ?: false
|
||||||
|
}
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
override fun getSearchNameCount(): Flow<Int> =
|
||||||
|
depDataSource.dependencies.flatMapLatest { flowOf(it.libraries.size) }
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package top.fatweb.oxygen.toolbox.repository.tool
|
||||||
|
|
||||||
|
import androidx.paging.PagingData
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import top.fatweb.oxygen.toolbox.model.Result
|
||||||
|
import top.fatweb.oxygen.toolbox.model.tool.ToolEntity
|
||||||
|
|
||||||
|
interface StoreRepository {
|
||||||
|
suspend fun getStore(
|
||||||
|
searchValue: String
|
||||||
|
): Flow<PagingData<ToolEntity>>
|
||||||
|
|
||||||
|
fun detail(
|
||||||
|
username: String,
|
||||||
|
toolId: String,
|
||||||
|
ver: String = "latest"
|
||||||
|
): Flow<Result<ToolEntity>>
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package top.fatweb.oxygen.toolbox.repository.tool
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import top.fatweb.oxygen.toolbox.model.tool.ToolEntity
|
||||||
|
|
||||||
|
interface ToolRepository {
|
||||||
|
val toolViewTemplate: Flow<String>
|
||||||
|
|
||||||
|
fun getGlobalJsVariables(isDarkMode: Boolean): Flow<String>
|
||||||
|
|
||||||
|
fun getGlobalCssVariables(isDarkMode: Boolean): Flow<String>
|
||||||
|
|
||||||
|
fun getAllToolsStream(searchValue: String): Flow<List<ToolEntity>>
|
||||||
|
|
||||||
|
fun getStarToolsStream(searchValue: String): Flow<List<ToolEntity>>
|
||||||
|
|
||||||
|
fun getToolById(id: Long): Flow<ToolEntity?>
|
||||||
|
|
||||||
|
fun getToolByUsernameAndToolId(username: String, toolId: String): Flow<ToolEntity?>
|
||||||
|
|
||||||
|
suspend fun saveTool(toolEntity: ToolEntity)
|
||||||
|
|
||||||
|
suspend fun updateTool(toolEntity: ToolEntity)
|
||||||
|
|
||||||
|
suspend fun removeTool(toolEntity: ToolEntity)
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package top.fatweb.oxygen.toolbox.repository.tool.impl
|
||||||
|
|
||||||
|
import androidx.paging.Pager
|
||||||
|
import androidx.paging.PagingConfig
|
||||||
|
import androidx.paging.PagingData
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import top.fatweb.oxygen.toolbox.data.network.OxygenNetworkDataSource
|
||||||
|
import top.fatweb.oxygen.toolbox.data.tool.dao.ToolDao
|
||||||
|
import top.fatweb.oxygen.toolbox.model.Result
|
||||||
|
import top.fatweb.oxygen.toolbox.model.asExternalModel
|
||||||
|
import top.fatweb.oxygen.toolbox.model.tool.ToolEntity
|
||||||
|
import top.fatweb.oxygen.toolbox.network.model.ToolVo
|
||||||
|
import top.fatweb.oxygen.toolbox.network.model.asExternalModel
|
||||||
|
import top.fatweb.oxygen.toolbox.network.paging.ToolStorePagingSource
|
||||||
|
import top.fatweb.oxygen.toolbox.repository.tool.StoreRepository
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
private const val PAGE_SIZE = 20
|
||||||
|
|
||||||
|
internal class NetworkStoreRepository @Inject constructor(
|
||||||
|
private val oxygenNetworkDataSource: OxygenNetworkDataSource,
|
||||||
|
private val toolDao: ToolDao
|
||||||
|
) : StoreRepository {
|
||||||
|
override suspend fun getStore(
|
||||||
|
searchValue: String
|
||||||
|
): Flow<PagingData<ToolEntity>> =
|
||||||
|
Pager(
|
||||||
|
config = PagingConfig(pageSize = PAGE_SIZE),
|
||||||
|
pagingSourceFactory = {
|
||||||
|
ToolStorePagingSource(
|
||||||
|
oxygenNetworkDataSource = oxygenNetworkDataSource,
|
||||||
|
toolDao = toolDao,
|
||||||
|
searchValue = searchValue
|
||||||
|
)
|
||||||
|
}
|
||||||
|
).flow
|
||||||
|
|
||||||
|
override fun detail(
|
||||||
|
username: String,
|
||||||
|
toolId: String,
|
||||||
|
ver: String
|
||||||
|
): Flow<Result<ToolEntity>> =
|
||||||
|
oxygenNetworkDataSource.detail(
|
||||||
|
username = username,
|
||||||
|
toolId = toolId,
|
||||||
|
ver = ver
|
||||||
|
).map {
|
||||||
|
it.asExternalModel(ToolVo::asExternalModel)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package top.fatweb.oxygen.toolbox.repository.tool.impl
|
||||||
|
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import top.fatweb.oxygen.toolbox.data.tool.ToolDataSource
|
||||||
|
import top.fatweb.oxygen.toolbox.data.tool.dao.ToolDao
|
||||||
|
import top.fatweb.oxygen.toolbox.model.tool.ToolEntity
|
||||||
|
import top.fatweb.oxygen.toolbox.repository.tool.ToolRepository
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
class OfflineToolRepository @Inject constructor(
|
||||||
|
private val toolDataSource: ToolDataSource,
|
||||||
|
private val toolDao: ToolDao
|
||||||
|
) : ToolRepository {
|
||||||
|
override val toolViewTemplate: Flow<String>
|
||||||
|
get() = toolDataSource.toolViewTemplate
|
||||||
|
|
||||||
|
override fun getGlobalJsVariables(isDarkMode: Boolean): Flow<String> =
|
||||||
|
toolDataSource.getGlobalJsVariables(isDarkMode)
|
||||||
|
|
||||||
|
override fun getGlobalCssVariables(isDarkMode: Boolean): Flow<String> =
|
||||||
|
toolDataSource.getGlobalCssVariables(isDarkMode)
|
||||||
|
|
||||||
|
override fun getAllToolsStream(searchValue: String): Flow<List<ToolEntity>> =
|
||||||
|
toolDao.selectAllTools(searchValue)
|
||||||
|
|
||||||
|
override fun getStarToolsStream(searchValue: String): Flow<List<ToolEntity>> =
|
||||||
|
toolDao.selectStarTools(searchValue)
|
||||||
|
|
||||||
|
override fun getToolById(id: Long): Flow<ToolEntity?> =
|
||||||
|
toolDao.selectToolById(id)
|
||||||
|
|
||||||
|
override fun getToolByUsernameAndToolId(username: String, toolId: String): Flow<ToolEntity?> =
|
||||||
|
toolDao.selectToolByUsernameAndToolId(username, toolId)
|
||||||
|
|
||||||
|
override suspend fun saveTool(toolEntity: ToolEntity) =
|
||||||
|
toolDao.insertTool(toolEntity.copy(isInstalled = true))
|
||||||
|
|
||||||
|
override suspend fun updateTool(toolEntity: ToolEntity) =
|
||||||
|
toolDao.updateTool(toolEntity)
|
||||||
|
|
||||||
|
override suspend fun removeTool(toolEntity: ToolEntity) =
|
||||||
|
toolDao.deleteTool(toolEntity)
|
||||||
|
}
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
package top.fatweb.oxygen.toolbox.repository
|
package top.fatweb.oxygen.toolbox.repository.userdata
|
||||||
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import top.fatweb.oxygen.toolbox.model.DarkThemeConfig
|
import top.fatweb.oxygen.toolbox.model.userdata.DarkThemeConfig
|
||||||
import top.fatweb.oxygen.toolbox.model.LanguageConfig
|
import top.fatweb.oxygen.toolbox.model.userdata.LanguageConfig
|
||||||
import top.fatweb.oxygen.toolbox.model.LaunchPageConfig
|
import top.fatweb.oxygen.toolbox.model.userdata.LaunchPageConfig
|
||||||
import top.fatweb.oxygen.toolbox.model.ThemeBrandConfig
|
import top.fatweb.oxygen.toolbox.model.userdata.ThemeBrandConfig
|
||||||
import top.fatweb.oxygen.toolbox.model.UserData
|
import top.fatweb.oxygen.toolbox.model.userdata.UserData
|
||||||
|
|
||||||
interface UserDataRepository {
|
interface UserDataRepository {
|
||||||
val userData: Flow<UserData>
|
val userData: Flow<UserData>
|
||||||
@@ -19,4 +19,6 @@ interface UserDataRepository {
|
|||||||
suspend fun setDarkThemeConfig(darkThemeConfig: DarkThemeConfig)
|
suspend fun setDarkThemeConfig(darkThemeConfig: DarkThemeConfig)
|
||||||
|
|
||||||
suspend fun setUseDynamicColor(useDynamicColor: Boolean)
|
suspend fun setUseDynamicColor(useDynamicColor: Boolean)
|
||||||
|
|
||||||
|
suspend fun updateIsNotFirstLaunch()
|
||||||
}
|
}
|
||||||
@@ -1,15 +1,16 @@
|
|||||||
package top.fatweb.oxygen.toolbox.repository
|
package top.fatweb.oxygen.toolbox.repository.userdata.impl
|
||||||
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import top.fatweb.oxygen.toolbox.datastore.OxygenPreferencesDataSource
|
import top.fatweb.oxygen.toolbox.data.userdata.OxygenPreferencesDataSource
|
||||||
import top.fatweb.oxygen.toolbox.model.DarkThemeConfig
|
import top.fatweb.oxygen.toolbox.model.userdata.DarkThemeConfig
|
||||||
import top.fatweb.oxygen.toolbox.model.LanguageConfig
|
import top.fatweb.oxygen.toolbox.model.userdata.LanguageConfig
|
||||||
import top.fatweb.oxygen.toolbox.model.LaunchPageConfig
|
import top.fatweb.oxygen.toolbox.model.userdata.LaunchPageConfig
|
||||||
import top.fatweb.oxygen.toolbox.model.ThemeBrandConfig
|
import top.fatweb.oxygen.toolbox.model.userdata.ThemeBrandConfig
|
||||||
import top.fatweb.oxygen.toolbox.model.UserData
|
import top.fatweb.oxygen.toolbox.model.userdata.UserData
|
||||||
|
import top.fatweb.oxygen.toolbox.repository.userdata.UserDataRepository
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
internal class OfflineFirstUserDataRepository @Inject constructor(
|
internal class LocalUserDataRepository @Inject constructor(
|
||||||
private val oxygenPreferencesDataSource: OxygenPreferencesDataSource
|
private val oxygenPreferencesDataSource: OxygenPreferencesDataSource
|
||||||
) : UserDataRepository {
|
) : UserDataRepository {
|
||||||
override val userData: Flow<UserData> =
|
override val userData: Flow<UserData> =
|
||||||
@@ -34,4 +35,8 @@ internal class OfflineFirstUserDataRepository @Inject constructor(
|
|||||||
override suspend fun setUseDynamicColor(useDynamicColor: Boolean) {
|
override suspend fun setUseDynamicColor(useDynamicColor: Boolean) {
|
||||||
oxygenPreferencesDataSource.setUseDynamicColor(useDynamicColor)
|
oxygenPreferencesDataSource.setUseDynamicColor(useDynamicColor)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override suspend fun updateIsNotFirstLaunch() {
|
||||||
|
oxygenPreferencesDataSource.updateIsNotFirstLaunch()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,16 +1,14 @@
|
|||||||
package top.fatweb.oxygen.toolbox.ui
|
package top.fatweb.oxygen.toolbox.ui
|
||||||
|
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
import androidx.compose.foundation.layout.WindowInsetsSides
|
|
||||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.only
|
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.safeDrawing
|
|
||||||
import androidx.compose.foundation.layout.safeDrawingPadding
|
import androidx.compose.foundation.layout.safeDrawingPadding
|
||||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
@@ -22,21 +20,30 @@ import androidx.compose.material3.SnackbarResult
|
|||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.saveable.rememberSaveable
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.core.view.WindowInsetsCompat
|
||||||
|
import androidx.core.view.WindowInsetsControllerCompat
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import androidx.navigation.NavDestination
|
import androidx.navigation.NavDestination
|
||||||
import androidx.navigation.NavDestination.Companion.hierarchy
|
import androidx.navigation.NavDestination.Companion.hierarchy
|
||||||
import top.fatweb.oxygen.toolbox.R
|
import top.fatweb.oxygen.toolbox.R
|
||||||
import top.fatweb.oxygen.toolbox.icon.OxygenIcons
|
import top.fatweb.oxygen.toolbox.icon.OxygenIcons
|
||||||
import top.fatweb.oxygen.toolbox.model.LaunchPageConfig
|
import top.fatweb.oxygen.toolbox.model.userdata.LaunchPageConfig
|
||||||
|
import top.fatweb.oxygen.toolbox.navigation.ABOUT_ROUTE
|
||||||
import top.fatweb.oxygen.toolbox.navigation.OxygenNavHost
|
import top.fatweb.oxygen.toolbox.navigation.OxygenNavHost
|
||||||
import top.fatweb.oxygen.toolbox.navigation.STAR_ROUTE
|
import top.fatweb.oxygen.toolbox.navigation.STAR_ROUTE
|
||||||
import top.fatweb.oxygen.toolbox.navigation.TOOLS_ROUTE
|
import top.fatweb.oxygen.toolbox.navigation.TOOLS_ROUTE
|
||||||
@@ -48,113 +55,205 @@ import top.fatweb.oxygen.toolbox.ui.component.OxygenNavigationBarItem
|
|||||||
import top.fatweb.oxygen.toolbox.ui.component.OxygenNavigationRail
|
import top.fatweb.oxygen.toolbox.ui.component.OxygenNavigationRail
|
||||||
import top.fatweb.oxygen.toolbox.ui.component.OxygenNavigationRailItem
|
import top.fatweb.oxygen.toolbox.ui.component.OxygenNavigationRailItem
|
||||||
import top.fatweb.oxygen.toolbox.ui.component.OxygenTopAppBar
|
import top.fatweb.oxygen.toolbox.ui.component.OxygenTopAppBar
|
||||||
|
import top.fatweb.oxygen.toolbox.ui.component.SearchButtonPosition
|
||||||
import top.fatweb.oxygen.toolbox.ui.settings.SettingsDialog
|
import top.fatweb.oxygen.toolbox.ui.settings.SettingsDialog
|
||||||
import top.fatweb.oxygen.toolbox.ui.theme.GradientColors
|
import top.fatweb.oxygen.toolbox.ui.theme.GradientColors
|
||||||
import top.fatweb.oxygen.toolbox.ui.theme.LocalGradientColors
|
import top.fatweb.oxygen.toolbox.ui.theme.LocalGradientColors
|
||||||
|
import top.fatweb.oxygen.toolbox.ui.util.FullScreen
|
||||||
|
import top.fatweb.oxygen.toolbox.ui.util.LocalFullScreen
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun OxygenApp(appState: OxygenAppState) {
|
fun OxygenApp(appState: OxygenAppState) {
|
||||||
val shouldShowGradientBackground =
|
val shouldShowGradientBackground =
|
||||||
appState.currentTopLevelDestination == TopLevelDestination.TOOLS
|
appState.currentDestination?.route == ABOUT_ROUTE
|
||||||
var showSettingsDialog by rememberSaveable {
|
var showSettingsDialog by rememberSaveable {
|
||||||
mutableStateOf(false)
|
mutableStateOf(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
OxygenBackground {
|
val context = LocalContext.current
|
||||||
OxygenGradientBackground(
|
val window = (context as ComponentActivity).window
|
||||||
gradientColors = if (shouldShowGradientBackground) LocalGradientColors.current else GradientColors()
|
val windowInsetsController = WindowInsetsControllerCompat(window, window.decorView)
|
||||||
) {
|
var isFullScreen by remember { mutableStateOf(false) }
|
||||||
val destination = appState.currentTopLevelDestination
|
|
||||||
|
|
||||||
val snackbarHostState = remember { SnackbarHostState() }
|
val fullScreen = FullScreen(
|
||||||
|
enable = isFullScreen,
|
||||||
|
onStateChange = {
|
||||||
|
isFullScreen = it
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
val isOffline by appState.isOffline.collectAsStateWithLifecycle()
|
DisposableEffect(Unit) {
|
||||||
|
windowInsetsController.systemBarsBehavior =
|
||||||
|
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||||
|
val listener = WindowInsetsControllerCompat.OnControllableInsetsChangedListener { _, _ ->
|
||||||
|
isFullScreen = false
|
||||||
|
}
|
||||||
|
windowInsetsController.addOnControllableInsetsChangedListener(listener)
|
||||||
|
onDispose {
|
||||||
|
windowInsetsController.removeOnControllableInsetsChangedListener(listener)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val noConnectMessage = stringResource(R.string.no_connect)
|
LaunchedEffect(isFullScreen) {
|
||||||
|
if (isFullScreen) {
|
||||||
|
windowInsetsController.hide(WindowInsetsCompat.Type.systemBars())
|
||||||
|
} else {
|
||||||
|
windowInsetsController.show(WindowInsetsCompat.Type.systemBars())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
LaunchedEffect(isOffline) {
|
CompositionLocalProvider(LocalFullScreen provides fullScreen) {
|
||||||
if (isOffline) {
|
OxygenBackground {
|
||||||
snackbarHostState.showSnackbar(
|
OxygenGradientBackground(
|
||||||
message = noConnectMessage,
|
gradientColors = if (shouldShowGradientBackground) LocalGradientColors.current else GradientColors()
|
||||||
duration = SnackbarDuration.Indefinite
|
) {
|
||||||
|
val destination = appState.currentTopLevelDestination
|
||||||
|
|
||||||
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
|
|
||||||
|
val isOffline by appState.isOffline.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
|
val noConnectMessage = stringResource(R.string.core_no_connect)
|
||||||
|
|
||||||
|
var canScroll by remember { mutableStateOf(true) }
|
||||||
|
val topAppBarScrollBehavior =
|
||||||
|
if (canScroll) TopAppBarDefaults.enterAlwaysScrollBehavior() else TopAppBarDefaults.pinnedScrollBehavior()
|
||||||
|
|
||||||
|
var activeSearch by remember { mutableStateOf(false) }
|
||||||
|
var searchValue by remember { mutableStateOf("") }
|
||||||
|
var searchCount by remember { mutableIntStateOf(0) }
|
||||||
|
|
||||||
|
LaunchedEffect(activeSearch) {
|
||||||
|
canScroll = !activeSearch
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(destination) {
|
||||||
|
activeSearch = false
|
||||||
|
searchValue = ""
|
||||||
|
if (searchCount == 0) {
|
||||||
|
searchCount++
|
||||||
|
} else {
|
||||||
|
searchCount = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(isOffline) {
|
||||||
|
if (isOffline) {
|
||||||
|
snackbarHostState.showSnackbar(
|
||||||
|
message = noConnectMessage,
|
||||||
|
duration = SnackbarDuration.Indefinite
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showSettingsDialog) {
|
||||||
|
SettingsDialog(
|
||||||
|
onDismiss = { showSettingsDialog = false },
|
||||||
|
onNavigateToLibraries = appState::navigateToLibraries,
|
||||||
|
onNavigateToAbout = appState::navigateToAbout
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (showSettingsDialog) {
|
Scaffold(
|
||||||
SettingsDialog(
|
modifier = Modifier
|
||||||
onDismiss = { showSettingsDialog = false }
|
.nestedScroll(connection = topAppBarScrollBehavior.nestedScrollConnection),
|
||||||
)
|
containerColor = Color.Transparent,
|
||||||
}
|
contentColor = MaterialTheme.colorScheme.onBackground,
|
||||||
|
contentWindowInsets = WindowInsets(left = 0, top = 0, right = 0, bottom = 0),
|
||||||
Scaffold(
|
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
|
||||||
containerColor = Color.Transparent,
|
bottomBar = {
|
||||||
contentColor = MaterialTheme.colorScheme.onBackground,
|
AnimatedVisibility(
|
||||||
contentWindowInsets = WindowInsets(0, 0, 0, 0),
|
visible = appState.shouldShowBottomBar && destination != null
|
||||||
snackbarHost = { SnackbarHost(snackbarHostState) },
|
) {
|
||||||
bottomBar = {
|
OxygenBottomBar(
|
||||||
if (appState.shouldShowBottomBar && destination != null) {
|
destinations = appState.topLevelDestinations,
|
||||||
OxygenBottomBar(
|
currentDestination = appState.currentDestination,
|
||||||
destinations = appState.topLevelDestinations,
|
onNavigateToDestination = appState::navigateToTopLevelDestination
|
||||||
onNavigateToDestination = appState::navigateToTopLevelDestination,
|
|
||||||
currentDestination = appState.currentDestination
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
) { padding ->
|
|
||||||
Row(
|
|
||||||
Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.padding(padding)
|
|
||||||
.consumeWindowInsets(padding)
|
|
||||||
.windowInsetsPadding(
|
|
||||||
WindowInsets.safeDrawing.only(
|
|
||||||
WindowInsetsSides.Horizontal
|
|
||||||
)
|
)
|
||||||
)
|
}
|
||||||
) {
|
|
||||||
if (appState.shouldShowNavRail && destination != null) {
|
|
||||||
OxygenNavRail(
|
|
||||||
modifier = Modifier.safeDrawingPadding(),
|
|
||||||
destinations = appState.topLevelDestinations,
|
|
||||||
onNavigateToDestination = appState::navigateToTopLevelDestination,
|
|
||||||
currentDestination = appState.currentDestination
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
) { padding ->
|
||||||
Column(
|
Row(
|
||||||
Modifier.fillMaxSize()
|
Modifier
|
||||||
|
.fillMaxSize()
|
||||||
) {
|
) {
|
||||||
if (destination != null) {
|
AnimatedVisibility(
|
||||||
OxygenTopAppBar(
|
visible = appState.shouldShowNavRail && destination != null
|
||||||
titleRes = destination.titleTextId,
|
) {
|
||||||
navigationIcon = OxygenIcons.Search,
|
OxygenNavRail(
|
||||||
navigationIconContentDescription = stringResource(R.string.feature_settings_top_app_bar_navigation_icon_description),
|
modifier = Modifier
|
||||||
actionIcon = OxygenIcons.MoreVert,
|
.padding(padding)
|
||||||
actionIconContentDescription = stringResource(R.string.feature_settings_top_app_bar_action_icon_description),
|
.consumeWindowInsets(padding)
|
||||||
colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
|
.safeDrawingPadding(),
|
||||||
containerColor = Color.Transparent
|
destinations = appState.topLevelDestinations,
|
||||||
),
|
currentDestination = appState.currentDestination,
|
||||||
onNavigationClick = { appState.navigateToSearch() },
|
onNavigateToDestination = appState::navigateToTopLevelDestination
|
||||||
onActionClick = { showSettingsDialog = true }
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
OxygenNavHost(
|
Column(
|
||||||
appState = appState,
|
Modifier.fillMaxSize()
|
||||||
onShowSnackbar = { message, action ->
|
) {
|
||||||
snackbarHostState.showSnackbar(
|
AnimatedVisibility(
|
||||||
message = message,
|
visible = destination != null
|
||||||
actionLabel = action,
|
) {
|
||||||
duration = SnackbarDuration.Short
|
OxygenTopAppBar(
|
||||||
) == SnackbarResult.ActionPerformed
|
scrollBehavior = topAppBarScrollBehavior,
|
||||||
},
|
title = {
|
||||||
startDestination = when (appState.launchPageConfig) {
|
destination?.let {
|
||||||
LaunchPageConfig.TOOLS -> TOOLS_ROUTE
|
Text(
|
||||||
LaunchPageConfig.STAR -> STAR_ROUTE
|
text = stringResource(destination.titleTextId),
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
navigationIcon = OxygenIcons.Search,
|
||||||
|
navigationIconContentDescription = stringResource(R.string.feature_settings_top_app_bar_navigation_icon_description),
|
||||||
|
actionIcon = OxygenIcons.MoreVert,
|
||||||
|
actionIconContentDescription = stringResource(R.string.feature_settings_top_app_bar_action_icon_description),
|
||||||
|
activeSearch = activeSearch,
|
||||||
|
searchButtonPosition = SearchButtonPosition.Navigation,
|
||||||
|
query = searchValue,
|
||||||
|
colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
|
||||||
|
containerColor = Color.Transparent,
|
||||||
|
scrolledContainerColor = Color.Transparent
|
||||||
|
),
|
||||||
|
onNavigationClick = { activeSearch = true },
|
||||||
|
onActionClick = { showSettingsDialog = true },
|
||||||
|
onQueryChange = {
|
||||||
|
searchValue = it
|
||||||
|
},
|
||||||
|
onSearch = {
|
||||||
|
searchCount++
|
||||||
|
},
|
||||||
|
onCancelSearch = {
|
||||||
|
searchValue = ""
|
||||||
|
activeSearch = false
|
||||||
|
searchCount = 0
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
OxygenNavHost(
|
||||||
|
appState = appState,
|
||||||
|
startDestination = when (appState.launchPageConfig) {
|
||||||
|
LaunchPageConfig.Tools -> TOOLS_ROUTE
|
||||||
|
LaunchPageConfig.Star -> STAR_ROUTE
|
||||||
|
},
|
||||||
|
isVertical = appState.shouldShowBottomBar,
|
||||||
|
searchValue = searchValue,
|
||||||
|
searchCount = searchCount,
|
||||||
|
onShowSnackbar = { message, action ->
|
||||||
|
snackbarHostState.showSnackbar(
|
||||||
|
message = message,
|
||||||
|
actionLabel = action,
|
||||||
|
duration = SnackbarDuration.Short
|
||||||
|
) == SnackbarResult.ActionPerformed
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -166,8 +265,8 @@ fun OxygenApp(appState: OxygenAppState) {
|
|||||||
private fun OxygenBottomBar(
|
private fun OxygenBottomBar(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
destinations: List<TopLevelDestination>,
|
destinations: List<TopLevelDestination>,
|
||||||
onNavigateToDestination: (TopLevelDestination) -> Unit,
|
currentDestination: NavDestination?,
|
||||||
currentDestination: NavDestination?
|
onNavigateToDestination: (TopLevelDestination) -> Unit
|
||||||
) {
|
) {
|
||||||
OxygenNavigationBar(
|
OxygenNavigationBar(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
@@ -181,13 +280,13 @@ private fun OxygenBottomBar(
|
|||||||
icon = {
|
icon = {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = destination.unselectedIcon,
|
imageVector = destination.unselectedIcon,
|
||||||
contentDescription = null
|
contentDescription = stringResource(destination.iconTextId)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
selectedIcon = {
|
selectedIcon = {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = destination.selectedIcon,
|
imageVector = destination.selectedIcon,
|
||||||
contentDescription = null
|
contentDescription = stringResource(destination.iconTextId)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
onClick = { onNavigateToDestination(destination) }
|
onClick = { onNavigateToDestination(destination) }
|
||||||
@@ -200,8 +299,8 @@ private fun OxygenBottomBar(
|
|||||||
private fun OxygenNavRail(
|
private fun OxygenNavRail(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
destinations: List<TopLevelDestination>,
|
destinations: List<TopLevelDestination>,
|
||||||
onNavigateToDestination: (TopLevelDestination) -> Unit,
|
currentDestination: NavDestination?,
|
||||||
currentDestination: NavDestination?
|
onNavigateToDestination: (TopLevelDestination) -> Unit
|
||||||
) {
|
) {
|
||||||
OxygenNavigationRail(
|
OxygenNavigationRail(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
@@ -215,13 +314,13 @@ private fun OxygenNavRail(
|
|||||||
icon = {
|
icon = {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = destination.unselectedIcon,
|
imageVector = destination.unselectedIcon,
|
||||||
contentDescription = null
|
contentDescription = stringResource(destination.iconTextId)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
selectedIcon = {
|
selectedIcon = {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = destination.selectedIcon,
|
imageVector = destination.selectedIcon,
|
||||||
contentDescription = null
|
contentDescription = stringResource(destination.iconTextId)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
onClick = { onNavigateToDestination(destination) }
|
onClick = { onNavigateToDestination(destination) }
|
||||||
@@ -232,5 +331,5 @@ private fun OxygenNavRail(
|
|||||||
|
|
||||||
private fun NavDestination?.isTopLevelDestinationInHierarchy(destination: TopLevelDestination) =
|
private fun NavDestination?.isTopLevelDestinationInHierarchy(destination: TopLevelDestination) =
|
||||||
this?.hierarchy?.any {
|
this?.hierarchy?.any {
|
||||||
it.route?.contains(destination.name, true) ?: false
|
it.route?.equals(destination.route) == true
|
||||||
} ?: false
|
} == true
|
||||||
|
|||||||
@@ -17,14 +17,17 @@ import kotlinx.coroutines.flow.SharingStarted
|
|||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.datetime.TimeZone
|
import kotlinx.datetime.TimeZone
|
||||||
import top.fatweb.oxygen.toolbox.model.LaunchPageConfig
|
import top.fatweb.oxygen.toolbox.model.userdata.LaunchPageConfig
|
||||||
import top.fatweb.oxygen.toolbox.monitor.NetworkMonitor
|
import top.fatweb.oxygen.toolbox.monitor.NetworkMonitor
|
||||||
import top.fatweb.oxygen.toolbox.monitor.TimeZoneMonitor
|
import top.fatweb.oxygen.toolbox.monitor.TimeZoneMonitor
|
||||||
import top.fatweb.oxygen.toolbox.navigation.STAR_ROUTE
|
import top.fatweb.oxygen.toolbox.navigation.STAR_ROUTE
|
||||||
import top.fatweb.oxygen.toolbox.navigation.TOOLS_ROUTE
|
import top.fatweb.oxygen.toolbox.navigation.TOOLS_ROUTE
|
||||||
|
import top.fatweb.oxygen.toolbox.navigation.TOOL_STORE_ROUTE
|
||||||
import top.fatweb.oxygen.toolbox.navigation.TopLevelDestination
|
import top.fatweb.oxygen.toolbox.navigation.TopLevelDestination
|
||||||
import top.fatweb.oxygen.toolbox.navigation.navigateToSearch
|
import top.fatweb.oxygen.toolbox.navigation.navigateToAbout
|
||||||
|
import top.fatweb.oxygen.toolbox.navigation.navigateToLibraries
|
||||||
import top.fatweb.oxygen.toolbox.navigation.navigateToStar
|
import top.fatweb.oxygen.toolbox.navigation.navigateToStar
|
||||||
|
import top.fatweb.oxygen.toolbox.navigation.navigateToToolStore
|
||||||
import top.fatweb.oxygen.toolbox.navigation.navigateToTools
|
import top.fatweb.oxygen.toolbox.navigation.navigateToTools
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
@@ -56,21 +59,24 @@ fun rememberOxygenAppState(
|
|||||||
|
|
||||||
@Stable
|
@Stable
|
||||||
class OxygenAppState(
|
class OxygenAppState(
|
||||||
val windowSizeClass: WindowSizeClass,
|
private val windowSizeClass: WindowSizeClass,
|
||||||
networkMonitor: NetworkMonitor,
|
networkMonitor: NetworkMonitor,
|
||||||
timeZoneMonitor: TimeZoneMonitor,
|
timeZoneMonitor: TimeZoneMonitor,
|
||||||
coroutineScope: CoroutineScope,
|
coroutineScope: CoroutineScope,
|
||||||
val navController: NavHostController,
|
val navController: NavHostController,
|
||||||
val launchPageConfig: LaunchPageConfig
|
val launchPageConfig: LaunchPageConfig
|
||||||
) {
|
) {
|
||||||
|
val topLevelDestinations: List<TopLevelDestination> = TopLevelDestination.entries
|
||||||
|
|
||||||
val currentDestination: NavDestination?
|
val currentDestination: NavDestination?
|
||||||
@Composable get() = navController
|
@Composable get() = navController
|
||||||
.currentBackStackEntryAsState().value?.destination
|
.currentBackStackEntryAsState().value?.destination
|
||||||
|
|
||||||
val currentTopLevelDestination: TopLevelDestination?
|
val currentTopLevelDestination: TopLevelDestination?
|
||||||
@Composable get() = when (currentDestination?.route) {
|
@Composable get() = when (currentDestination?.route) {
|
||||||
TOOLS_ROUTE -> TopLevelDestination.TOOLS
|
TOOL_STORE_ROUTE -> TopLevelDestination.ToolStore
|
||||||
STAR_ROUTE -> TopLevelDestination.STAR
|
TOOLS_ROUTE -> TopLevelDestination.Tools
|
||||||
|
STAR_ROUTE -> TopLevelDestination.Star
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,16 +91,14 @@ class OxygenAppState(
|
|||||||
.stateIn(
|
.stateIn(
|
||||||
scope = coroutineScope,
|
scope = coroutineScope,
|
||||||
initialValue = false,
|
initialValue = false,
|
||||||
started = SharingStarted.WhileSubscribed(5.seconds.inWholeMilliseconds)
|
started = SharingStarted.WhileSubscribed(stopTimeoutMillis = 5.seconds.inWholeMilliseconds)
|
||||||
)
|
)
|
||||||
|
|
||||||
val topLevelDestinations: List<TopLevelDestination> = TopLevelDestination.entries
|
|
||||||
|
|
||||||
val currentTimeZone = timeZoneMonitor.currentTimeZone
|
val currentTimeZone = timeZoneMonitor.currentTimeZone
|
||||||
.stateIn(
|
.stateIn(
|
||||||
scope = coroutineScope,
|
scope = coroutineScope,
|
||||||
initialValue = TimeZone.currentSystemDefault(),
|
initialValue = TimeZone.currentSystemDefault(),
|
||||||
started = SharingStarted.WhileSubscribed(5.seconds.inWholeMilliseconds)
|
started = SharingStarted.WhileSubscribed(stopTimeoutMillis = 5.seconds.inWholeMilliseconds)
|
||||||
)
|
)
|
||||||
|
|
||||||
fun navigateToTopLevelDestination(topLevelDestination: TopLevelDestination) {
|
fun navigateToTopLevelDestination(topLevelDestination: TopLevelDestination) {
|
||||||
@@ -108,10 +112,13 @@ class OxygenAppState(
|
|||||||
}
|
}
|
||||||
|
|
||||||
when (topLevelDestination) {
|
when (topLevelDestination) {
|
||||||
TopLevelDestination.TOOLS -> navController.navigateToTools(topLevelNavOptions)
|
TopLevelDestination.ToolStore -> navController.navigateToToolStore(topLevelNavOptions)
|
||||||
TopLevelDestination.STAR -> navController.navigateToStar(topLevelNavOptions)
|
TopLevelDestination.Tools -> navController.navigateToTools(topLevelNavOptions)
|
||||||
|
TopLevelDestination.Star -> navController.navigateToStar(topLevelNavOptions)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun navigateToSearch() = navController.navigateToSearch()
|
fun navigateToLibraries() = navController.navigateToLibraries()
|
||||||
|
|
||||||
|
fun navigateToAbout() = navController.navigateToAbout()
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,218 @@
|
|||||||
|
package top.fatweb.oxygen.toolbox.ui.about
|
||||||
|
|
||||||
|
import androidx.compose.animation.core.AnimationConstants
|
||||||
|
import androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi
|
||||||
|
import androidx.compose.animation.graphics.res.animatedVectorResource
|
||||||
|
import androidx.compose.animation.graphics.res.rememberAnimatedVectorPainter
|
||||||
|
import androidx.compose.animation.graphics.vector.AnimatedImageVector
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
|
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||||
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.only
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.safeDrawing
|
||||||
|
import androidx.compose.foundation.layout.safeDrawingPadding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.webkit.WebViewCompat
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import top.fatweb.oxygen.toolbox.R
|
||||||
|
import top.fatweb.oxygen.toolbox.icon.OxygenIcons
|
||||||
|
import top.fatweb.oxygen.toolbox.ui.component.OxygenTopAppBar
|
||||||
|
import top.fatweb.oxygen.toolbox.ui.theme.OxygenPreviews
|
||||||
|
import top.fatweb.oxygen.toolbox.ui.theme.OxygenTheme
|
||||||
|
import top.fatweb.oxygen.toolbox.ui.util.ResourcesUtils
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun AboutRoute(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onBackClick: () -> Unit,
|
||||||
|
onNavigateToLibraries: () -> Unit
|
||||||
|
) {
|
||||||
|
AboutScreen(
|
||||||
|
modifier = modifier.safeDrawingPadding(),
|
||||||
|
onBackClick = onBackClick,
|
||||||
|
onNavigateToLibraries = onNavigateToLibraries
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
internal fun AboutScreen(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onBackClick: () -> Unit,
|
||||||
|
onNavigateToLibraries: () -> Unit
|
||||||
|
) {
|
||||||
|
val scrollState = rememberScrollState()
|
||||||
|
val topAppBarScrollBehavior =
|
||||||
|
TopAppBarDefaults.pinnedScrollBehavior(canScroll = { scrollState.maxValue > 0 })
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
modifier = Modifier
|
||||||
|
.nestedScroll(connection = topAppBarScrollBehavior.nestedScrollConnection),
|
||||||
|
containerColor = Color.Transparent,
|
||||||
|
contentWindowInsets = WindowInsets(left = 0, top = 0, right = 0, bottom = 0),
|
||||||
|
) { padding ->
|
||||||
|
Column(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(padding)
|
||||||
|
.consumeWindowInsets(padding)
|
||||||
|
.windowInsetsPadding(
|
||||||
|
WindowInsets.safeDrawing.only(
|
||||||
|
WindowInsetsSides.Horizontal
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.verticalScroll(state = scrollState),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
OxygenTopAppBar(
|
||||||
|
scrollBehavior = topAppBarScrollBehavior,
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.feature_settings_more_about),
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
},
|
||||||
|
navigationIcon = OxygenIcons.Back,
|
||||||
|
navigationIconContentDescription = stringResource(R.string.core_back),
|
||||||
|
colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
|
||||||
|
containerColor = Color.Transparent,
|
||||||
|
scrolledContainerColor = Color.Transparent
|
||||||
|
),
|
||||||
|
onNavigationClick = onBackClick
|
||||||
|
)
|
||||||
|
Spacer(Modifier.height(64.dp))
|
||||||
|
AboutAppInfo()
|
||||||
|
Spacer(Modifier.weight(1f))
|
||||||
|
AboutFooter(
|
||||||
|
onNavigateToLibraries = onNavigateToLibraries
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalAnimationGraphicsApi::class)
|
||||||
|
@Composable
|
||||||
|
private fun AboutAppInfo(
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val logo = AnimatedImageVector.animatedVectorResource(R.drawable.ic_launcher)
|
||||||
|
var atEnd by remember { mutableStateOf(false) }
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
delay(AnimationConstants.DefaultDurationMillis.toLong())
|
||||||
|
atEnd = true
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = modifier,
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
modifier = Modifier
|
||||||
|
.width(160.dp),
|
||||||
|
painter = rememberAnimatedVectorPainter(animatedImageVector = logo, atEnd = atEnd),
|
||||||
|
contentDescription = stringResource(R.string.app_full_name)
|
||||||
|
)
|
||||||
|
Spacer(Modifier.height(16.dp))
|
||||||
|
Text(
|
||||||
|
style = MaterialTheme.typography.headlineMedium,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
text = stringResource(R.string.app_name)
|
||||||
|
)
|
||||||
|
Spacer(Modifier.height(8.dp))
|
||||||
|
Text(
|
||||||
|
style = MaterialTheme.typography.titleMedium,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = MaterialTheme.colorScheme.outline,
|
||||||
|
text = stringResource(R.string.app_description)
|
||||||
|
)
|
||||||
|
Spacer(Modifier.height(8.dp))
|
||||||
|
Text(
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.outline,
|
||||||
|
text = "${ResourcesUtils.getAppVersionName(LocalContext.current)} (${
|
||||||
|
stringResource(
|
||||||
|
if (ResourcesUtils.getAppVersionCode(LocalContext.current) % 100 == 0L)
|
||||||
|
R.string.core_ga_version
|
||||||
|
else
|
||||||
|
R.string.core_beta_version
|
||||||
|
)
|
||||||
|
})"
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
color = MaterialTheme.colorScheme.outline,
|
||||||
|
text = "WebView: ${
|
||||||
|
WebViewCompat.getCurrentWebViewPackage(context)?.versionName ?: stringResource(
|
||||||
|
R.string.core_unknown
|
||||||
|
)
|
||||||
|
}"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun AboutFooter(
|
||||||
|
modifier: Modifier = Modifier, onNavigateToLibraries: () -> Unit
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = modifier.padding(32.dp)
|
||||||
|
) {
|
||||||
|
TextButton(
|
||||||
|
onClick = onNavigateToLibraries
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
text = stringResource(R.string.feature_settings_open_source_license)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OxygenPreviews
|
||||||
|
@Composable
|
||||||
|
private fun AboutAppInfoPreview() {
|
||||||
|
OxygenTheme {
|
||||||
|
AboutAppInfo()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OxygenPreviews
|
||||||
|
@Composable
|
||||||
|
private fun AboutScreenPreview() {
|
||||||
|
OxygenTheme {
|
||||||
|
AboutScreen(onBackClick = {}, onNavigateToLibraries = {})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package top.fatweb.oxygen.toolbox.ui.about
|
||||||
|
|
||||||
|
import androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridScope
|
||||||
|
import androidx.compose.foundation.lazy.staggeredgrid.items
|
||||||
|
import top.fatweb.oxygen.toolbox.ui.component.LibraryCard
|
||||||
|
|
||||||
|
fun LazyStaggeredGridScope.librariesPanel(
|
||||||
|
librariesScreenUiState: LibrariesScreenUiState,
|
||||||
|
onClickLicense: (key: String) -> Unit
|
||||||
|
) {
|
||||||
|
when (librariesScreenUiState) {
|
||||||
|
LibrariesScreenUiState.Loading, LibrariesScreenUiState.Nothing, LibrariesScreenUiState.NotFound -> Unit
|
||||||
|
|
||||||
|
is LibrariesScreenUiState.Success -> {
|
||||||
|
items(
|
||||||
|
items = librariesScreenUiState.dependencies.libraries,
|
||||||
|
key = { it.uniqueId }
|
||||||
|
) {
|
||||||
|
LibraryCard(
|
||||||
|
library = it,
|
||||||
|
licenses = librariesScreenUiState.dependencies.licenses.filter { entry ->
|
||||||
|
it.licenses.contains(entry.key)
|
||||||
|
},
|
||||||
|
onClickLicense = onClickLicense
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,292 @@
|
|||||||
|
package top.fatweb.oxygen.toolbox.ui.about
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.activity.compose.ReportDrawnWhen
|
||||||
|
import androidx.compose.foundation.gestures.Orientation
|
||||||
|
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.WindowInsets
|
||||||
|
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||||
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.heightIn
|
||||||
|
import androidx.compose.foundation.layout.only
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.safeDrawing
|
||||||
|
import androidx.compose.foundation.layout.systemBars
|
||||||
|
import androidx.compose.foundation.layout.widthIn
|
||||||
|
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||||
|
import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid
|
||||||
|
import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells
|
||||||
|
import androidx.compose.foundation.lazy.staggeredgrid.rememberLazyStaggeredGridState
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.verticalScroll
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
|
import androidx.compose.ui.platform.LocalConfiguration
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import top.fatweb.oxygen.toolbox.R
|
||||||
|
import top.fatweb.oxygen.toolbox.icon.OxygenIcons
|
||||||
|
import top.fatweb.oxygen.toolbox.ui.component.Indicator
|
||||||
|
import top.fatweb.oxygen.toolbox.ui.component.OxygenTopAppBar
|
||||||
|
import top.fatweb.oxygen.toolbox.ui.component.scrollbar.DraggableScrollbar
|
||||||
|
import top.fatweb.oxygen.toolbox.ui.component.scrollbar.rememberDraggableScroller
|
||||||
|
import top.fatweb.oxygen.toolbox.ui.component.scrollbar.scrollbarState
|
||||||
|
import top.fatweb.oxygen.toolbox.ui.theme.OxygenPreviews
|
||||||
|
import top.fatweb.oxygen.toolbox.ui.theme.OxygenTheme
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun LibrariesRoute(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
viewModel: LibrariesScreenViewModel = hiltViewModel(),
|
||||||
|
onBackClick: () -> Unit
|
||||||
|
) {
|
||||||
|
val librariesScreenUiState by viewModel.librariesScreenUiState.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
|
LibrariesScreen(
|
||||||
|
modifier = modifier,
|
||||||
|
librariesScreenUiState = librariesScreenUiState,
|
||||||
|
onBackClick = onBackClick,
|
||||||
|
onSearch = { viewModel.onSearchValueChange(it) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
internal fun LibrariesScreen(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
librariesScreenUiState: LibrariesScreenUiState,
|
||||||
|
onBackClick: () -> Unit,
|
||||||
|
onSearch: (String) -> Unit
|
||||||
|
) {
|
||||||
|
val configuration = LocalConfiguration.current
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
val isLibrariesLoading = librariesScreenUiState is LibrariesScreenUiState.Loading
|
||||||
|
|
||||||
|
ReportDrawnWhen { !isLibrariesLoading }
|
||||||
|
|
||||||
|
val itemsAvailable = howManyItems(librariesScreenUiState)
|
||||||
|
|
||||||
|
val state = rememberLazyStaggeredGridState()
|
||||||
|
val scrollbarState = state.scrollbarState(itemsAvailable = itemsAvailable)
|
||||||
|
|
||||||
|
var showDialog by remember { mutableStateOf(false) }
|
||||||
|
var dialogTitle by remember { mutableStateOf("") }
|
||||||
|
var dialogContent by remember { mutableStateOf("") }
|
||||||
|
var dialogUrl by remember { mutableStateOf("") }
|
||||||
|
|
||||||
|
var canScroll by remember { mutableStateOf(true) }
|
||||||
|
val topAppBarScrollBehavior =
|
||||||
|
if (canScroll) TopAppBarDefaults.enterAlwaysScrollBehavior() else TopAppBarDefaults.pinnedScrollBehavior()
|
||||||
|
|
||||||
|
var activeSearch by remember { mutableStateOf(false) }
|
||||||
|
var searchValue by remember { mutableStateOf("") }
|
||||||
|
|
||||||
|
LaunchedEffect(activeSearch) {
|
||||||
|
canScroll = !activeSearch
|
||||||
|
}
|
||||||
|
|
||||||
|
Scaffold(
|
||||||
|
modifier = Modifier
|
||||||
|
.nestedScroll(connection = topAppBarScrollBehavior.nestedScrollConnection),
|
||||||
|
containerColor = Color.Transparent,
|
||||||
|
contentWindowInsets = WindowInsets(left = 0, top = 0, right = 0, bottom = 0),
|
||||||
|
) { padding ->
|
||||||
|
Column(
|
||||||
|
modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(padding)
|
||||||
|
.consumeWindowInsets(padding)
|
||||||
|
.windowInsetsPadding(
|
||||||
|
WindowInsets.safeDrawing.only(
|
||||||
|
WindowInsetsSides.Horizontal
|
||||||
|
)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
OxygenTopAppBar(
|
||||||
|
scrollBehavior = topAppBarScrollBehavior,
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.feature_settings_open_source_license),
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
},
|
||||||
|
navigationIcon = OxygenIcons.Back,
|
||||||
|
navigationIconContentDescription = stringResource(R.string.core_back),
|
||||||
|
actionIcon = OxygenIcons.Search,
|
||||||
|
actionIconContentDescription = stringResource(R.string.core_search),
|
||||||
|
activeSearch = activeSearch,
|
||||||
|
query = searchValue,
|
||||||
|
colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
|
||||||
|
containerColor = Color.Transparent,
|
||||||
|
scrolledContainerColor = Color.Transparent
|
||||||
|
),
|
||||||
|
onNavigationClick = onBackClick,
|
||||||
|
onActionClick = {
|
||||||
|
activeSearch = true
|
||||||
|
},
|
||||||
|
onQueryChange = {
|
||||||
|
searchValue = it
|
||||||
|
onSearch(it)
|
||||||
|
},
|
||||||
|
onSearch = onSearch,
|
||||||
|
onCancelSearch = {
|
||||||
|
searchValue = ""
|
||||||
|
activeSearch = false
|
||||||
|
onSearch("")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
Box {
|
||||||
|
when (librariesScreenUiState) {
|
||||||
|
LibrariesScreenUiState.Loading -> {
|
||||||
|
Indicator()
|
||||||
|
}
|
||||||
|
|
||||||
|
LibrariesScreenUiState.Nothing -> {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.verticalScroll(state = rememberScrollState()),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
Text(text = stringResource(R.string.core_nothing))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LibrariesScreenUiState.NotFound -> {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.verticalScroll(state = rememberScrollState()),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
Text(text = stringResource(R.string.core_nothing_found))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is LibrariesScreenUiState.Success -> {
|
||||||
|
val handleOnClickLicense = { key: String ->
|
||||||
|
val license = librariesScreenUiState.dependencies.licenses[key]
|
||||||
|
if (license != null) {
|
||||||
|
showDialog = true
|
||||||
|
dialogTitle = license.name
|
||||||
|
dialogContent = license.content ?: ""
|
||||||
|
dialogUrl = license.url ?: ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LazyVerticalStaggeredGrid(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize(),
|
||||||
|
columns = StaggeredGridCells.Adaptive(300.dp),
|
||||||
|
contentPadding = PaddingValues(16.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
|
verticalItemSpacing = 24.dp,
|
||||||
|
state = state
|
||||||
|
) {
|
||||||
|
librariesPanel(
|
||||||
|
librariesScreenUiState = librariesScreenUiState,
|
||||||
|
onClickLicense = handleOnClickLicense
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
state.DraggableScrollbar(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxHeight()
|
||||||
|
.windowInsetsPadding(WindowInsets.systemBars)
|
||||||
|
.padding(horizontal = 2.dp)
|
||||||
|
.align(Alignment.CenterEnd),
|
||||||
|
state = scrollbarState,
|
||||||
|
orientation = Orientation.Vertical,
|
||||||
|
onThumbMoved = state.rememberDraggableScroller(itemsAvailable = itemsAvailable)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showDialog) {
|
||||||
|
AlertDialog(
|
||||||
|
modifier = Modifier
|
||||||
|
.widthIn(max = configuration.screenWidthDp.dp - 80.dp)
|
||||||
|
.heightIn(max = configuration.screenHeightDp.dp - 40.dp),
|
||||||
|
onDismissRequest = { showDialog = false },
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
text = dialogTitle,
|
||||||
|
style = MaterialTheme.typography.titleLarge
|
||||||
|
)
|
||||||
|
},
|
||||||
|
text = {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.verticalScroll(state = rememberScrollState())
|
||||||
|
) {
|
||||||
|
Text(text = dialogContent)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
confirmButton = {
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
TextButton(onClick = {
|
||||||
|
context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(dialogUrl)))
|
||||||
|
}) {
|
||||||
|
Text(text = stringResource(R.string.core_website))
|
||||||
|
}
|
||||||
|
TextButton(onClick = { showDialog = false }) {
|
||||||
|
Text(text = stringResource(R.string.core_close))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun howManyItems(librariesScreenUiState: LibrariesScreenUiState) =
|
||||||
|
when (librariesScreenUiState) {
|
||||||
|
LibrariesScreenUiState.Loading, LibrariesScreenUiState.Nothing, LibrariesScreenUiState.NotFound -> 0
|
||||||
|
|
||||||
|
is LibrariesScreenUiState.Success -> librariesScreenUiState.dependencies.libraries.size
|
||||||
|
}
|
||||||
|
|
||||||
|
@OxygenPreviews
|
||||||
|
@Composable
|
||||||
|
private fun LibrariesScreenLoadingPreview() {
|
||||||
|
OxygenTheme {
|
||||||
|
LibrariesScreen(
|
||||||
|
librariesScreenUiState = LibrariesScreenUiState.Loading,
|
||||||
|
onBackClick = {},
|
||||||
|
onSearch = {})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
package top.fatweb.oxygen.toolbox.ui.about
|
||||||
|
|
||||||
|
import androidx.lifecycle.SavedStateHandle
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
|
import kotlinx.coroutines.flow.flowOf
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
|
import top.fatweb.oxygen.toolbox.model.lib.Dependencies
|
||||||
|
import top.fatweb.oxygen.toolbox.repository.lib.DepRepository
|
||||||
|
import javax.inject.Inject
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
|
@HiltViewModel
|
||||||
|
class LibrariesScreenViewModel @Inject constructor(
|
||||||
|
private val depRepository: DepRepository,
|
||||||
|
private val savedStateHandle: SavedStateHandle
|
||||||
|
) : ViewModel() {
|
||||||
|
private val searchValue = savedStateHandle.getStateFlow(SEARCH_VALUE, "")
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
val librariesScreenUiState: StateFlow<LibrariesScreenUiState> =
|
||||||
|
depRepository.getSearchNameCount()
|
||||||
|
.flatMapLatest { totalCount ->
|
||||||
|
if (totalCount < SEARCH_MIN_COUNT) {
|
||||||
|
flowOf(LibrariesScreenUiState.Nothing)
|
||||||
|
} else {
|
||||||
|
searchValue.flatMapLatest { value ->
|
||||||
|
depRepository.searchName(value).map {
|
||||||
|
if (it.libraries.isEmpty()) {
|
||||||
|
LibrariesScreenUiState.NotFound
|
||||||
|
} else {
|
||||||
|
LibrariesScreenUiState.Success(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.stateIn(
|
||||||
|
scope = viewModelScope,
|
||||||
|
initialValue = LibrariesScreenUiState.Loading,
|
||||||
|
started = SharingStarted.WhileSubscribed(stopTimeoutMillis = 5.seconds.inWholeMilliseconds)
|
||||||
|
)
|
||||||
|
|
||||||
|
fun onSearchValueChange(value: String) {
|
||||||
|
savedStateHandle[SEARCH_VALUE] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed interface LibrariesScreenUiState {
|
||||||
|
data object Loading : LibrariesScreenUiState
|
||||||
|
|
||||||
|
data object Nothing : LibrariesScreenUiState
|
||||||
|
|
||||||
|
data object NotFound : LibrariesScreenUiState
|
||||||
|
|
||||||
|
data class Success(val dependencies: Dependencies) : LibrariesScreenUiState
|
||||||
|
}
|
||||||
|
|
||||||
|
private const val SEARCH_MIN_COUNT = 1
|
||||||
|
private const val SEARCH_VALUE = "searchValue"
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
package top.fatweb.oxygen.toolbox.ui.component
|
package top.fatweb.oxygen.toolbox.ui.component
|
||||||
|
|
||||||
import android.content.res.Configuration
|
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
@@ -15,12 +14,12 @@ import androidx.compose.ui.draw.drawWithCache
|
|||||||
import androidx.compose.ui.geometry.Offset
|
import androidx.compose.ui.geometry.Offset
|
||||||
import androidx.compose.ui.graphics.Brush
|
import androidx.compose.ui.graphics.Brush
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
|
||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import top.fatweb.oxygen.toolbox.ui.theme.GradientColors
|
import top.fatweb.oxygen.toolbox.ui.theme.GradientColors
|
||||||
import top.fatweb.oxygen.toolbox.ui.theme.LocalBackgroundTheme
|
import top.fatweb.oxygen.toolbox.ui.theme.LocalBackgroundTheme
|
||||||
import top.fatweb.oxygen.toolbox.ui.theme.LocalGradientColors
|
import top.fatweb.oxygen.toolbox.ui.theme.LocalGradientColors
|
||||||
|
import top.fatweb.oxygen.toolbox.ui.theme.OxygenPreviews
|
||||||
import top.fatweb.oxygen.toolbox.ui.theme.OxygenTheme
|
import top.fatweb.oxygen.toolbox.ui.theme.OxygenTheme
|
||||||
import kotlin.math.tan
|
import kotlin.math.tan
|
||||||
|
|
||||||
@@ -92,11 +91,7 @@ fun OxygenGradientBackground(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO, name = "Light theme")
|
@OxygenPreviews
|
||||||
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES, name = "Dark theme")
|
|
||||||
annotation class ThemePreviews
|
|
||||||
|
|
||||||
@ThemePreviews
|
|
||||||
@Composable
|
@Composable
|
||||||
fun BackgroundDefault() {
|
fun BackgroundDefault() {
|
||||||
OxygenTheme(dynamicColor = false) {
|
OxygenTheme(dynamicColor = false) {
|
||||||
@@ -104,7 +99,7 @@ fun BackgroundDefault() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ThemePreviews
|
@OxygenPreviews
|
||||||
@Composable
|
@Composable
|
||||||
fun BackgroundDynamic() {
|
fun BackgroundDynamic() {
|
||||||
OxygenTheme(dynamicColor = true) {
|
OxygenTheme(dynamicColor = true) {
|
||||||
@@ -112,7 +107,7 @@ fun BackgroundDynamic() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ThemePreviews
|
@OxygenPreviews
|
||||||
@Composable
|
@Composable
|
||||||
fun BackgroundAndroid() {
|
fun BackgroundAndroid() {
|
||||||
OxygenTheme(androidTheme = true) {
|
OxygenTheme(androidTheme = true) {
|
||||||
@@ -120,7 +115,7 @@ fun BackgroundAndroid() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ThemePreviews
|
@OxygenPreviews
|
||||||
@Composable
|
@Composable
|
||||||
fun GradientBackgroundDefault() {
|
fun GradientBackgroundDefault() {
|
||||||
OxygenTheme(dynamicColor = false) {
|
OxygenTheme(dynamicColor = false) {
|
||||||
@@ -128,7 +123,7 @@ fun GradientBackgroundDefault() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ThemePreviews
|
@OxygenPreviews
|
||||||
@Composable
|
@Composable
|
||||||
fun GradientBackgroundDynamic() {
|
fun GradientBackgroundDynamic() {
|
||||||
OxygenTheme(dynamicColor = true) {
|
OxygenTheme(dynamicColor = true) {
|
||||||
@@ -136,7 +131,7 @@ fun GradientBackgroundDynamic() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ThemePreviews
|
@OxygenPreviews
|
||||||
@Composable
|
@Composable
|
||||||
fun GradientBackgroundAndroid() {
|
fun GradientBackgroundAndroid() {
|
||||||
OxygenTheme(androidTheme = true) {
|
OxygenTheme(androidTheme = true) {
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package top.fatweb.oxygen.toolbox.ui.component
|
||||||
|
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.text.LinkAnnotation
|
||||||
|
import androidx.compose.ui.text.SpanStyle
|
||||||
|
import androidx.compose.ui.text.buildAnnotatedString
|
||||||
|
import androidx.compose.ui.text.withStyle
|
||||||
|
import top.fatweb.oxygen.toolbox.ui.util.ResourcesUtils
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ClickableText(
|
||||||
|
@StringRes text: Int,
|
||||||
|
@StringRes replaceText: Int,
|
||||||
|
onClick: () -> Unit
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val primaryColor = MaterialTheme.colorScheme.primary
|
||||||
|
|
||||||
|
val annotatedString = buildAnnotatedString {
|
||||||
|
val clickablePart = ResourcesUtils.getString(
|
||||||
|
context = context,
|
||||||
|
resId = replaceText
|
||||||
|
)
|
||||||
|
val mainText = ResourcesUtils.getString(
|
||||||
|
context = context,
|
||||||
|
resId = text,
|
||||||
|
clickablePart
|
||||||
|
)
|
||||||
|
append(mainText.substringBefore(clickablePart))
|
||||||
|
pushLink(LinkAnnotation.Clickable(
|
||||||
|
tag = "Click",
|
||||||
|
linkInteractionListener = { onClick() }
|
||||||
|
))
|
||||||
|
withStyle(style = SpanStyle(color = primaryColor)) {
|
||||||
|
append(clickablePart)
|
||||||
|
}
|
||||||
|
pop()
|
||||||
|
append(mainText.substringAfter(clickablePart))
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(text = annotatedString)
|
||||||
|
}
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
package top.fatweb.oxygen.toolbox.ui.component
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.selection.selectable
|
||||||
|
import androidx.compose.foundation.selection.selectableGroup
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
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.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.semantics.Role
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import top.fatweb.oxygen.toolbox.icon.OxygenIcons
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DialogTitle(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
text: String
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
modifier = modifier.padding(16.dp),
|
||||||
|
text = text,
|
||||||
|
style = MaterialTheme.typography.titleLarge
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DialogSectionTitle(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
text: String
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
modifier = modifier.padding(top = 16.dp, bottom = 8.dp),
|
||||||
|
text = text,
|
||||||
|
style = MaterialTheme.typography.titleMedium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DialogSectionGroup(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
content: @Composable () -> Unit
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = modifier.selectableGroup()
|
||||||
|
) {
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DialogChooserRow(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
text: String,
|
||||||
|
selected: Boolean,
|
||||||
|
onClick: () -> Unit
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.selectable(
|
||||||
|
selected = selected,
|
||||||
|
role = Role.RadioButton,
|
||||||
|
onClick = onClick
|
||||||
|
)
|
||||||
|
.padding(12.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
RadioButton(
|
||||||
|
selected = selected,
|
||||||
|
onClick = null
|
||||||
|
)
|
||||||
|
Spacer(Modifier.width(8.dp))
|
||||||
|
Text(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DialogClickerRow(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
icon: ImageVector? = null,
|
||||||
|
text: String,
|
||||||
|
onClick: () -> Unit
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable(
|
||||||
|
onClick = onClick
|
||||||
|
)
|
||||||
|
.padding(12.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(imageVector = icon ?: OxygenIcons.Reorder, contentDescription = null)
|
||||||
|
Spacer(Modifier.width(8.dp))
|
||||||
|
Text(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package top.fatweb.oxygen.toolbox.ui.component
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.LocalContentColor
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.shadow
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun Indicator(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
containerColor: Color = MaterialTheme.colorScheme.surfaceContainerHigh,
|
||||||
|
contentColor: Color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
CompositionLocalProvider(value = LocalContentColor provides contentColor) {
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.size(SpinnerContainerSize)
|
||||||
|
.shadow(
|
||||||
|
elevation = Elevation,
|
||||||
|
shape = CircleShape,
|
||||||
|
clip = true
|
||||||
|
)
|
||||||
|
.background(color = containerColor, shape = CircleShape)
|
||||||
|
) {
|
||||||
|
CircularProgressIndicator(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.Center)
|
||||||
|
.size(SpinnerSize),
|
||||||
|
strokeWidth = StrokeWidth,
|
||||||
|
color = LocalContentColor.current
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val SpinnerContainerSize = 40.dp
|
||||||
|
private val Elevation = 3.dp
|
||||||
|
private val StrokeWidth = 2.5.dp
|
||||||
|
private val SpinnerSize = 16.dp
|
||||||
File diff suppressed because one or more lines are too long
@@ -17,6 +17,7 @@ import androidx.compose.ui.graphics.Color
|
|||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import top.fatweb.oxygen.toolbox.navigation.TopLevelDestination
|
import top.fatweb.oxygen.toolbox.navigation.TopLevelDestination
|
||||||
|
import top.fatweb.oxygen.toolbox.ui.theme.OxygenPreviews
|
||||||
import top.fatweb.oxygen.toolbox.ui.theme.OxygenTheme
|
import top.fatweb.oxygen.toolbox.ui.theme.OxygenTheme
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -26,16 +27,15 @@ fun RowScope.OxygenNavigationBarItem(
|
|||||||
label: @Composable (() -> Unit)? = null,
|
label: @Composable (() -> Unit)? = null,
|
||||||
icon: @Composable () -> Unit,
|
icon: @Composable () -> Unit,
|
||||||
selectedIcon: @Composable () -> Unit,
|
selectedIcon: @Composable () -> Unit,
|
||||||
onClick: () -> Unit,
|
|
||||||
enabled: Boolean = true,
|
enabled: Boolean = true,
|
||||||
alwaysShowLabel: Boolean = false
|
alwaysShowLabel: Boolean = false,
|
||||||
|
onClick: () -> Unit
|
||||||
) {
|
) {
|
||||||
NavigationBarItem(
|
NavigationBarItem(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
selected = selected,
|
selected = selected,
|
||||||
label = label,
|
label = label,
|
||||||
icon = if (selected) selectedIcon else icon,
|
icon = if (selected) selectedIcon else icon,
|
||||||
onClick = onClick,
|
|
||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
alwaysShowLabel = alwaysShowLabel,
|
alwaysShowLabel = alwaysShowLabel,
|
||||||
colors = NavigationBarItemDefaults.colors(
|
colors = NavigationBarItemDefaults.colors(
|
||||||
@@ -44,7 +44,8 @@ fun RowScope.OxygenNavigationBarItem(
|
|||||||
selectedTextColor = OxygenNavigationDefaults.navigationSelectedItemColor(),
|
selectedTextColor = OxygenNavigationDefaults.navigationSelectedItemColor(),
|
||||||
unselectedTextColor = OxygenNavigationDefaults.navigationContentColor(),
|
unselectedTextColor = OxygenNavigationDefaults.navigationContentColor(),
|
||||||
indicatorColor = OxygenNavigationDefaults.navigationIndicatorColor()
|
indicatorColor = OxygenNavigationDefaults.navigationIndicatorColor()
|
||||||
)
|
),
|
||||||
|
onClick = onClick
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,16 +69,15 @@ fun OxygenNavigationRailItem(
|
|||||||
label: @Composable (() -> Unit)? = null,
|
label: @Composable (() -> Unit)? = null,
|
||||||
icon: @Composable () -> Unit,
|
icon: @Composable () -> Unit,
|
||||||
selectedIcon: @Composable () -> Unit,
|
selectedIcon: @Composable () -> Unit,
|
||||||
onClick: () -> Unit,
|
|
||||||
enabled: Boolean = true,
|
enabled: Boolean = true,
|
||||||
alwaysShowLabel: Boolean = true
|
alwaysShowLabel: Boolean = true,
|
||||||
|
onClick: () -> Unit
|
||||||
) {
|
) {
|
||||||
NavigationRailItem(
|
NavigationRailItem(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
selected = selected,
|
selected = selected,
|
||||||
label = label,
|
label = label,
|
||||||
icon = if (selected) selectedIcon else icon,
|
icon = if (selected) selectedIcon else icon,
|
||||||
onClick = onClick,
|
|
||||||
enabled = enabled,
|
enabled = enabled,
|
||||||
alwaysShowLabel = alwaysShowLabel,
|
alwaysShowLabel = alwaysShowLabel,
|
||||||
colors = NavigationRailItemDefaults.colors(
|
colors = NavigationRailItemDefaults.colors(
|
||||||
@@ -86,7 +86,8 @@ fun OxygenNavigationRailItem(
|
|||||||
selectedTextColor = OxygenNavigationDefaults.navigationSelectedItemColor(),
|
selectedTextColor = OxygenNavigationDefaults.navigationSelectedItemColor(),
|
||||||
unselectedTextColor = OxygenNavigationDefaults.navigationContentColor(),
|
unselectedTextColor = OxygenNavigationDefaults.navigationContentColor(),
|
||||||
indicatorColor = OxygenNavigationDefaults.navigationIndicatorColor()
|
indicatorColor = OxygenNavigationDefaults.navigationIndicatorColor()
|
||||||
)
|
),
|
||||||
|
onClick = onClick
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,9 +117,9 @@ object OxygenNavigationDefaults {
|
|||||||
fun navigationIndicatorColor() = MaterialTheme.colorScheme.primaryContainer
|
fun navigationIndicatorColor() = MaterialTheme.colorScheme.primaryContainer
|
||||||
}
|
}
|
||||||
|
|
||||||
@ThemePreviews
|
@OxygenPreviews
|
||||||
@Composable
|
@Composable
|
||||||
fun OxygenNavigationBarPreview() {
|
private fun OxygenNavigationBarPreview() {
|
||||||
val items = TopLevelDestination.entries
|
val items = TopLevelDestination.entries
|
||||||
|
|
||||||
OxygenTheme {
|
OxygenTheme {
|
||||||
@@ -130,13 +131,13 @@ fun OxygenNavigationBarPreview() {
|
|||||||
icon = {
|
icon = {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = item.unselectedIcon,
|
imageVector = item.unselectedIcon,
|
||||||
contentDescription = stringResource(item.titleTextId)
|
contentDescription = stringResource(item.iconTextId)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
selectedIcon = {
|
selectedIcon = {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = item.selectedIcon, contentDescription = stringResource(
|
imageVector = item.selectedIcon, contentDescription = stringResource(
|
||||||
item.titleTextId
|
item.iconTextId
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -147,9 +148,9 @@ fun OxygenNavigationBarPreview() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ThemePreviews
|
@OxygenPreviews
|
||||||
@Composable
|
@Composable
|
||||||
fun OxygenNavigationRailPreview() {
|
private fun OxygenNavigationRailPreview() {
|
||||||
val items = TopLevelDestination.entries
|
val items = TopLevelDestination.entries
|
||||||
|
|
||||||
OxygenTheme {
|
OxygenTheme {
|
||||||
@@ -161,13 +162,13 @@ fun OxygenNavigationRailPreview() {
|
|||||||
icon = {
|
icon = {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = item.unselectedIcon,
|
imageVector = item.unselectedIcon,
|
||||||
contentDescription = stringResource(item.titleTextId)
|
contentDescription = stringResource(item.iconTextId)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
selectedIcon = {
|
selectedIcon = {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = item.selectedIcon, contentDescription = stringResource(
|
imageVector = item.selectedIcon, contentDescription = stringResource(
|
||||||
item.titleTextId
|
item.iconTextId
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,20 +1,44 @@
|
|||||||
package top.fatweb.oxygen.toolbox.ui.component
|
package top.fatweb.oxygen.toolbox.ui.component
|
||||||
|
|
||||||
import androidx.annotation.StringRes
|
import androidx.compose.animation.core.animateIntAsState
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.material3.CenterAlignedTopAppBar
|
import androidx.compose.material3.CenterAlignedTopAppBar
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.IconButton
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextField
|
||||||
|
import androidx.compose.material3.TextFieldDefaults
|
||||||
import androidx.compose.material3.TopAppBarColors
|
import androidx.compose.material3.TopAppBarColors
|
||||||
import androidx.compose.material3.TopAppBarDefaults
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
|
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
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.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.input.key.Key
|
||||||
|
import androidx.compose.ui.input.key.key
|
||||||
|
import androidx.compose.ui.input.key.onKeyEvent
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import top.fatweb.oxygen.toolbox.R
|
||||||
import top.fatweb.oxygen.toolbox.icon.OxygenIcons
|
import top.fatweb.oxygen.toolbox.icon.OxygenIcons
|
||||||
|
import top.fatweb.oxygen.toolbox.ui.theme.OxygenPreviews
|
||||||
import top.fatweb.oxygen.toolbox.ui.theme.OxygenTheme
|
import top.fatweb.oxygen.toolbox.ui.theme.OxygenTheme
|
||||||
import android.R as androidR
|
import android.R as androidR
|
||||||
|
|
||||||
@@ -22,47 +46,151 @@ import android.R as androidR
|
|||||||
@Composable
|
@Composable
|
||||||
fun OxygenTopAppBar(
|
fun OxygenTopAppBar(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
@StringRes titleRes: Int,
|
expandedHeight: Dp = 48.dp,
|
||||||
navigationIcon: ImageVector,
|
scrollBehavior: TopAppBarScrollBehavior? = null,
|
||||||
navigationIconContentDescription: String,
|
title: @Composable () -> Unit = {},
|
||||||
actionIcon: ImageVector,
|
navigationIcon: ImageVector? = null,
|
||||||
actionIconContentDescription: String,
|
navigationIconContentDescription: String? = null,
|
||||||
|
actionIcon: ImageVector? = null,
|
||||||
|
actionIconContentDescription: String? = null,
|
||||||
|
activeSearch: Boolean = false,
|
||||||
|
searchButtonPosition: SearchButtonPosition = SearchButtonPosition.Action,
|
||||||
|
query: String = "",
|
||||||
colors: TopAppBarColors = TopAppBarDefaults.centerAlignedTopAppBarColors(),
|
colors: TopAppBarColors = TopAppBarDefaults.centerAlignedTopAppBarColors(),
|
||||||
onNavigationClick: () -> Unit = {},
|
onNavigationClick: () -> Unit = {},
|
||||||
onActionClick: () -> Unit = {}
|
onActionClick: () -> Unit = {},
|
||||||
|
onQueryChange: (String) -> Unit = {},
|
||||||
|
onSearch: (String) -> Unit = {},
|
||||||
|
onCancelSearch: () -> Unit = {}
|
||||||
) {
|
) {
|
||||||
|
val topInset by animateIntAsState(
|
||||||
|
targetValue = if (scrollBehavior != null && -scrollBehavior.state.heightOffset >= with(
|
||||||
|
LocalDensity.current
|
||||||
|
) { expandedHeight.toPx() }
|
||||||
|
) 0
|
||||||
|
else TopAppBarDefaults.windowInsets.getTop(LocalDensity.current),
|
||||||
|
label = ""
|
||||||
|
)
|
||||||
|
val keyboardController = LocalSoftwareKeyboardController.current
|
||||||
|
val focusRequester = remember { FocusRequester() }
|
||||||
|
|
||||||
|
val onSearchExplicitlyTriggered = {
|
||||||
|
keyboardController?.hide()
|
||||||
|
onSearch(query)
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(activeSearch) {
|
||||||
|
if (activeSearch) {
|
||||||
|
focusRequester.requestFocus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
CenterAlignedTopAppBar(
|
CenterAlignedTopAppBar(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
title = { Text(stringResource(titleRes)) },
|
scrollBehavior = scrollBehavior,
|
||||||
|
expandedHeight = expandedHeight,
|
||||||
|
title = {
|
||||||
|
if (activeSearch) TextField(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(Color.Transparent)
|
||||||
|
.focusRequester(focusRequester)
|
||||||
|
.onKeyEvent {
|
||||||
|
if (it.key == Key.Enter) {
|
||||||
|
onSearchExplicitlyTriggered()
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
value = query,
|
||||||
|
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
|
||||||
|
keyboardActions = KeyboardActions(
|
||||||
|
onSearch = {
|
||||||
|
onSearchExplicitlyTriggered()
|
||||||
|
}
|
||||||
|
),
|
||||||
|
colors = TextFieldDefaults.colors(
|
||||||
|
unfocusedContainerColor = Color.Transparent,
|
||||||
|
focusedContainerColor = Color.Transparent,
|
||||||
|
unfocusedIndicatorColor = Color.Transparent,
|
||||||
|
focusedIndicatorColor = Color.Transparent
|
||||||
|
),
|
||||||
|
leadingIcon = {
|
||||||
|
Icon(
|
||||||
|
imageVector = OxygenIcons.Search,
|
||||||
|
contentDescription = stringResource(R.string.core_search)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
maxLines = 1,
|
||||||
|
singleLine = true,
|
||||||
|
textStyle = MaterialTheme.typography.titleSmall,
|
||||||
|
onValueChange = {
|
||||||
|
if ("\n" !in it) onQueryChange(it)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
else title()
|
||||||
|
},
|
||||||
navigationIcon = {
|
navigationIcon = {
|
||||||
IconButton(onClick = onNavigationClick) {
|
if (activeSearch && searchButtonPosition == SearchButtonPosition.Navigation) IconButton(
|
||||||
|
onClick = onCancelSearch
|
||||||
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = navigationIcon,
|
imageVector = OxygenIcons.Close,
|
||||||
contentDescription = navigationIconContentDescription,
|
contentDescription = stringResource(R.string.core_close)
|
||||||
tint = MaterialTheme.colorScheme.onSurface
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
else navigationIcon?.let {
|
||||||
|
IconButton(onClick = onNavigationClick) {
|
||||||
|
Icon(
|
||||||
|
imageVector = navigationIcon,
|
||||||
|
contentDescription = navigationIconContentDescription,
|
||||||
|
tint = MaterialTheme.colorScheme.onSurface
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
actions = {
|
actions = {
|
||||||
IconButton(onClick = onActionClick) {
|
if (activeSearch && searchButtonPosition == SearchButtonPosition.Action) IconButton(
|
||||||
|
onClick = onCancelSearch
|
||||||
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = actionIcon,
|
imageVector = OxygenIcons.Close,
|
||||||
contentDescription = actionIconContentDescription,
|
contentDescription = stringResource(R.string.core_close)
|
||||||
tint = MaterialTheme.colorScheme.onSurface
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
else actionIcon?.let {
|
||||||
|
IconButton(onClick = onActionClick) {
|
||||||
|
Icon(
|
||||||
|
imageVector = actionIcon,
|
||||||
|
contentDescription = actionIconContentDescription,
|
||||||
|
tint = MaterialTheme.colorScheme.onSurface
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
colors = colors
|
colors = colors,
|
||||||
|
windowInsets = WindowInsets(top = topInset)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum class SearchButtonPosition {
|
||||||
|
Navigation, Action
|
||||||
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Preview
|
@OxygenPreviews
|
||||||
@Composable
|
@Composable
|
||||||
private fun OxygenTopAppBarPreview() {
|
private fun OxygenTopAppBarPreview() {
|
||||||
OxygenTheme {
|
OxygenTheme {
|
||||||
OxygenTopAppBar(
|
OxygenTopAppBar(
|
||||||
titleRes = androidR.string.untitled,
|
title = {
|
||||||
|
Text(
|
||||||
|
text = stringResource(androidR.string.untitled),
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
},
|
||||||
navigationIcon = OxygenIcons.Search,
|
navigationIcon = OxygenIcons.Search,
|
||||||
navigationIconContentDescription = "Navigation icon",
|
navigationIconContentDescription = "Navigation icon",
|
||||||
actionIcon = OxygenIcons.MoreVert,
|
actionIcon = OxygenIcons.MoreVert,
|
||||||
|
|||||||
@@ -0,0 +1,385 @@
|
|||||||
|
package top.fatweb.oxygen.toolbox.ui.component
|
||||||
|
|
||||||
|
import androidx.compose.animation.core.LinearEasing
|
||||||
|
import androidx.compose.animation.core.RepeatMode
|
||||||
|
import androidx.compose.animation.core.infiniteRepeatable
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.combinedClickable
|
||||||
|
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.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.CardDefaults
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.valentinilk.shimmer.LocalShimmerTheme
|
||||||
|
import com.valentinilk.shimmer.ShimmerBounds
|
||||||
|
import com.valentinilk.shimmer.rememberShimmer
|
||||||
|
import com.valentinilk.shimmer.shimmer
|
||||||
|
import com.valentinilk.shimmer.shimmerSpec
|
||||||
|
import top.fatweb.oxygen.toolbox.R
|
||||||
|
import top.fatweb.oxygen.toolbox.icon.OxygenIcons
|
||||||
|
import top.fatweb.oxygen.toolbox.model.tool.ToolEntity
|
||||||
|
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
|
@Composable
|
||||||
|
fun ToolCard(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
tool: ToolEntity,
|
||||||
|
specifyVer: String? = null,
|
||||||
|
actionIcon: ImageVector? = null,
|
||||||
|
actionIconContentDescription: String = "",
|
||||||
|
onAction: () -> Unit = {},
|
||||||
|
onClick: () -> Unit = {},
|
||||||
|
onLongClick: () -> Unit = {}
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
modifier = modifier
|
||||||
|
.clip(RoundedCornerShape(8.dp))
|
||||||
|
.combinedClickable(
|
||||||
|
onClick = onClick,
|
||||||
|
onLongClick = onLongClick
|
||||||
|
),
|
||||||
|
shape = RoundedCornerShape(8.dp),
|
||||||
|
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(16.dp)
|
||||||
|
) {
|
||||||
|
ToolHeader(
|
||||||
|
ver = specifyVer ?: tool.ver,
|
||||||
|
actionIcon = actionIcon,
|
||||||
|
actionIconContentDescription = actionIconContentDescription,
|
||||||
|
onAction = onAction
|
||||||
|
)
|
||||||
|
Spacer(Modifier.height(16.dp))
|
||||||
|
ToolIcon(icon = tool.icon)
|
||||||
|
Spacer(Modifier.height(16.dp))
|
||||||
|
ToolInfo(
|
||||||
|
toolName = tool.name,
|
||||||
|
toolId = tool.toolId,
|
||||||
|
toolDesc = tool.description
|
||||||
|
)
|
||||||
|
Spacer(Modifier.height(16.dp))
|
||||||
|
AuthorInfo(
|
||||||
|
avatar = tool.authorAvatar,
|
||||||
|
nickname = tool.authorNickname
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ToolHeader(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
ver: String,
|
||||||
|
actionIcon: ImageVector?,
|
||||||
|
actionIconContentDescription: String,
|
||||||
|
onAction: () -> Unit
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = modifier
|
||||||
|
.height(28.dp)
|
||||||
|
) {
|
||||||
|
ToolVer(ver = ver)
|
||||||
|
Spacer(Modifier.weight(1f))
|
||||||
|
actionIcon?.let {
|
||||||
|
ToolAction(
|
||||||
|
actionIcon = actionIcon,
|
||||||
|
actionIconContentDescription = actionIconContentDescription,
|
||||||
|
onAction = onAction
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ToolVer(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
ver: String
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxHeight(),
|
||||||
|
shape = RoundedCornerShape(8.dp),
|
||||||
|
colors = CardDefaults.cardColors(contentColor = MaterialTheme.colorScheme.onSecondaryContainer)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxHeight()
|
||||||
|
.background(color = MaterialTheme.colorScheme.surfaceContainer)
|
||||||
|
.padding(horizontal = 8.dp, vertical = 4.dp),
|
||||||
|
Arrangement.Center,
|
||||||
|
Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
text = ver
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ToolAction(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
actionIcon: ImageVector,
|
||||||
|
actionIconContentDescription: String,
|
||||||
|
onAction: () -> Unit
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxHeight()
|
||||||
|
.clip(RoundedCornerShape(8.dp))
|
||||||
|
.clickable(
|
||||||
|
onClick = onAction
|
||||||
|
),
|
||||||
|
shape = RoundedCornerShape(8.dp),
|
||||||
|
colors = CardDefaults.cardColors(contentColor = MaterialTheme.colorScheme.onSecondaryContainer)
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxHeight()
|
||||||
|
.background(color = MaterialTheme.colorScheme.surfaceContainer)
|
||||||
|
.padding(horizontal = 6.dp, vertical = 6.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = actionIcon,
|
||||||
|
contentDescription = actionIconContentDescription
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ToolIcon(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
icon: String
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth(),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
modifier = Modifier.size(80.dp),
|
||||||
|
bitmap = OxygenIcons.fromSvgBase64(icon),
|
||||||
|
contentDescription = ""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ToolInfo(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
toolName: String,
|
||||||
|
toolId: String,
|
||||||
|
toolDesc: String?
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = modifier.fillMaxWidth(),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
fontWeight = FontWeight.ExtraBold,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
text = toolName
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
text = "ID: $toolId"
|
||||||
|
)
|
||||||
|
toolDesc?.let {
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.outline,
|
||||||
|
text = "${stringResource(R.string.feature_tools_description)}: $it"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun AuthorInfo(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
avatar: String,
|
||||||
|
nickname: String
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally)
|
||||||
|
) {
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(24.dp),
|
||||||
|
shape = RoundedCornerShape(12.dp)
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
modifier = Modifier
|
||||||
|
.background(MaterialTheme.colorScheme.surfaceContainer),
|
||||||
|
bitmap = OxygenIcons.fromPngBase64(avatar), contentDescription = "Avatar"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
text = nickname
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ToolCardSkeleton(
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val shimmer = rememberShimmer(
|
||||||
|
shimmerBounds = ShimmerBounds.Window,
|
||||||
|
theme = LocalShimmerTheme.current.copy(
|
||||||
|
animationSpec = infiniteRepeatable(
|
||||||
|
animation = shimmerSpec(
|
||||||
|
durationMillis = 1_500,
|
||||||
|
easing = LinearEasing,
|
||||||
|
delayMillis = 200
|
||||||
|
),
|
||||||
|
repeatMode = RepeatMode.Restart
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
Card(
|
||||||
|
modifier = modifier
|
||||||
|
.clip(RoundedCornerShape(8.dp)),
|
||||||
|
shape = RoundedCornerShape(8.dp),
|
||||||
|
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(16.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.height(28.dp)
|
||||||
|
) {
|
||||||
|
Surface(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxHeight()
|
||||||
|
.width(64.dp)
|
||||||
|
.shimmer(shimmer),
|
||||||
|
shape = RoundedCornerShape(8.dp),
|
||||||
|
color = MaterialTheme.colorScheme.surfaceContainer
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
Spacer(Modifier.height(16.dp))
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth(),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(80.dp)
|
||||||
|
.shimmer(shimmer),
|
||||||
|
shape = RoundedCornerShape(8.dp),
|
||||||
|
color = MaterialTheme.colorScheme.surfaceContainer
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
Spacer(Modifier.height(16.dp))
|
||||||
|
Column(
|
||||||
|
modifier = modifier.fillMaxWidth(),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.spacedBy(6.dp)
|
||||||
|
) {
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(width = 40.dp, height = 22.dp)
|
||||||
|
.shimmer(shimmer),
|
||||||
|
shape = RoundedCornerShape(4.dp),
|
||||||
|
color = MaterialTheme.colorScheme.surfaceContainer
|
||||||
|
) {}
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(width = 60.dp, height = 20.dp)
|
||||||
|
.shimmer(shimmer),
|
||||||
|
shape = RoundedCornerShape(4.dp),
|
||||||
|
color = MaterialTheme.colorScheme.surfaceContainer
|
||||||
|
) {}
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(2.dp)
|
||||||
|
) {
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(width = 120.dp, height = 12.dp)
|
||||||
|
.shimmer(shimmer),
|
||||||
|
shape = RoundedCornerShape(4.dp),
|
||||||
|
color = MaterialTheme.colorScheme.surfaceContainer
|
||||||
|
) {}
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(width = 120.dp, height = 12.dp)
|
||||||
|
.shimmer(shimmer),
|
||||||
|
shape = RoundedCornerShape(4.dp),
|
||||||
|
color = MaterialTheme.colorScheme.surfaceContainer
|
||||||
|
) {}
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(width = 80.dp, height = 12.dp)
|
||||||
|
.shimmer(shimmer),
|
||||||
|
shape = RoundedCornerShape(4.dp),
|
||||||
|
color = MaterialTheme.colorScheme.surfaceContainer
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Spacer(Modifier.height(16.dp))
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally)
|
||||||
|
) {
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(24.dp)
|
||||||
|
.shimmer(shimmer),
|
||||||
|
shape = RoundedCornerShape(12.dp),
|
||||||
|
color = MaterialTheme.colorScheme.surfaceContainer
|
||||||
|
) {}
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(width = 120.dp, height = 12.dp)
|
||||||
|
.shimmer(shimmer),
|
||||||
|
shape = RoundedCornerShape(4.dp),
|
||||||
|
color = MaterialTheme.colorScheme.surfaceContainer
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const val DEFAULT_TOOL_CARD_SKELETON_COUNT = 20
|
||||||
@@ -0,0 +1,206 @@
|
|||||||
|
/*
|
||||||
|
package top.fatweb.oxygen.toolbox.ui.component
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
|
import androidx.compose.foundation.layout.FlowRow
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.CardDefaults
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.rotate
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import top.fatweb.oxygen.toolbox.data.tool.ToolDataSource
|
||||||
|
import top.fatweb.oxygen.toolbox.icon.OxygenIcons
|
||||||
|
import top.fatweb.oxygen.toolbox.model.tool.Tool
|
||||||
|
import top.fatweb.oxygen.toolbox.model.tool.ToolGroup
|
||||||
|
import top.fatweb.oxygen.toolbox.ui.theme.OxygenPreviews
|
||||||
|
import top.fatweb.oxygen.toolbox.ui.theme.OxygenTheme
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ToolGroupCard(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
toolGroup: ToolGroup
|
||||||
|
) {
|
||||||
|
val (_, icon, title, tools) = toolGroup
|
||||||
|
|
||||||
|
var isExpanded by remember { mutableStateOf(true) }
|
||||||
|
|
||||||
|
Card(
|
||||||
|
modifier = modifier,
|
||||||
|
shape = RoundedCornerShape(8.dp),
|
||||||
|
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface)
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
|
ToolGroupTitle(
|
||||||
|
modifier = Modifier.padding(16.dp),
|
||||||
|
icon = icon,
|
||||||
|
title = title,
|
||||||
|
isExpanded = isExpanded,
|
||||||
|
onClick = { isExpanded = !isExpanded }
|
||||||
|
)
|
||||||
|
AnimatedVisibility(visible = isExpanded) {
|
||||||
|
ToolGroupContent(modifier = Modifier.padding(16.dp), toolList = tools)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ToolGroupTitle(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
icon: ImageVector,
|
||||||
|
title: String,
|
||||||
|
isExpanded: Boolean,
|
||||||
|
onClick: (() -> Unit)? = null
|
||||||
|
) {
|
||||||
|
Surface(onClick = onClick ?: {}) {
|
||||||
|
Row(
|
||||||
|
modifier = modifier,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Icon(modifier = Modifier.size(18.dp), imageVector = icon, contentDescription = title)
|
||||||
|
Spacer(Modifier.width(10.dp))
|
||||||
|
Text(text = title, style = MaterialTheme.typography.titleMedium)
|
||||||
|
Spacer(Modifier.weight(1f))
|
||||||
|
SwitchableIcon(icon = OxygenIcons.ArrowDown, switched = !isExpanded)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalLayoutApi::class)
|
||||||
|
@Composable
|
||||||
|
fun ToolGroupContent(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
toolList: List<Tool>
|
||||||
|
) {
|
||||||
|
FlowRow(
|
||||||
|
modifier = modifier,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
toolList.map {
|
||||||
|
ToolGroupItem(icon = it.icon, title = it.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ToolGroupItem(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
icon: ImageVector,
|
||||||
|
title: String,
|
||||||
|
onClick: (() -> Unit)? = null
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
modifier = modifier,
|
||||||
|
shape = RoundedCornerShape(100),
|
||||||
|
onClick = onClick ?: { }
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.padding(8.dp)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(modifier = Modifier.size(16.dp), imageVector = icon, contentDescription = null)
|
||||||
|
Spacer(Modifier.width(4.dp))
|
||||||
|
Text(text = title, style = MaterialTheme.typography.bodyMedium)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SwitchableIcon(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
icon: ImageVector,
|
||||||
|
switched: Boolean,
|
||||||
|
defaultRotate: Float = 0f,
|
||||||
|
switchedRotate: Float = 180f,
|
||||||
|
) {
|
||||||
|
val rotate by animateFloatAsState(
|
||||||
|
if (switched) switchedRotate else defaultRotate,
|
||||||
|
label = "Rotate"
|
||||||
|
)
|
||||||
|
|
||||||
|
Icon(
|
||||||
|
modifier = modifier
|
||||||
|
.rotate(rotate), imageVector = icon, contentDescription = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@OxygenPreviews
|
||||||
|
@Composable
|
||||||
|
private fun ToolGroupCardPreview() {
|
||||||
|
val groups = runBlocking { ToolDataSource().tool.first() }
|
||||||
|
|
||||||
|
OxygenTheme {
|
||||||
|
LazyColumn {
|
||||||
|
itemsIndexed(groups) { index, item ->
|
||||||
|
if (index != 0) {
|
||||||
|
Spacer(Modifier.height(10.dp))
|
||||||
|
}
|
||||||
|
ToolGroupCard(
|
||||||
|
toolGroup = item
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OxygenPreviews
|
||||||
|
@Composable
|
||||||
|
fun SwitchableIconPreview() {
|
||||||
|
var switched by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
OxygenTheme {
|
||||||
|
Surface(
|
||||||
|
onClick = { switched = !switched }
|
||||||
|
) {
|
||||||
|
SwitchableIcon(icon = OxygenIcons.ArrowDown, switched = switched)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OxygenPreviews
|
||||||
|
@Composable
|
||||||
|
fun ToolGroupItemPreview() {
|
||||||
|
OxygenTheme {
|
||||||
|
ToolGroupItem(icon = OxygenIcons.Time, title = "Time Screen")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OxygenPreviews
|
||||||
|
@Composable
|
||||||
|
fun ToolGroupContentPreview() {
|
||||||
|
OxygenTheme {
|
||||||
|
ToolGroupContent(toolList = runBlocking {
|
||||||
|
ToolDataSource().tool.first().map { it.tools }.flatten()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}*/
|
||||||
@@ -0,0 +1,237 @@
|
|||||||
|
package top.fatweb.oxygen.toolbox.ui.component.scrollbar
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import androidx.compose.animation.animateColorAsState
|
||||||
|
import androidx.compose.animation.core.Spring
|
||||||
|
import androidx.compose.animation.core.SpringSpec
|
||||||
|
import androidx.compose.foundation.gestures.Orientation
|
||||||
|
import androidx.compose.foundation.gestures.ScrollableState
|
||||||
|
import androidx.compose.foundation.interaction.InteractionSource
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.interaction.collectIsDraggedAsState
|
||||||
|
import androidx.compose.foundation.interaction.collectIsHoveredAsState
|
||||||
|
import androidx.compose.foundation.interaction.collectIsPressedAsState
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.State
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.geometry.Size
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.ColorProducer
|
||||||
|
import androidx.compose.ui.graphics.Outline
|
||||||
|
import androidx.compose.ui.graphics.drawOutline
|
||||||
|
import androidx.compose.ui.graphics.drawscope.ContentDrawScope
|
||||||
|
import androidx.compose.ui.node.DrawModifierNode
|
||||||
|
import androidx.compose.ui.node.ModifierNodeElement
|
||||||
|
import androidx.compose.ui.node.invalidateDraw
|
||||||
|
import androidx.compose.ui.unit.LayoutDirection
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The time period for showing the scrollbar thumb after interacting with it, before it fades away
|
||||||
|
*/
|
||||||
|
private const val SCROLLBAR_INACTIVE_TO_DORMANT_TIME_IN_MS = 2_000L
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A [Scrollbar] that allows for fast scrolling of content by dragging its thumb.
|
||||||
|
* Its thumb disappears when the scrolling container is dormant.
|
||||||
|
* @param modifier a [Modifier] for the [Scrollbar]
|
||||||
|
* @param state the driving state for the [Scrollbar]
|
||||||
|
* @param orientation the orientation of the scrollbar
|
||||||
|
* @param onThumbMoved the fast scroll implementation
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun ScrollableState.DraggableScrollbar(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
state: ScrollbarState,
|
||||||
|
orientation: Orientation,
|
||||||
|
onThumbMoved: (Float) -> Unit
|
||||||
|
) {
|
||||||
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
|
Scrollbar(
|
||||||
|
modifier = modifier,
|
||||||
|
orientation = orientation,
|
||||||
|
interactionSource = interactionSource,
|
||||||
|
state = state,
|
||||||
|
thumb = {
|
||||||
|
DraggableScrollbarThumb(
|
||||||
|
interactionSource = interactionSource,
|
||||||
|
orientation = orientation
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onThumbMoved = onThumbMoved
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A scrollbar thumb that is intended to also be a touch target for fast scrolling.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
private fun ScrollableState.DraggableScrollbarThumb(
|
||||||
|
interactionSource: InteractionSource,
|
||||||
|
orientation: Orientation
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.run {
|
||||||
|
when (orientation) {
|
||||||
|
Orientation.Vertical -> width(2.dp).fillMaxHeight()
|
||||||
|
Orientation.Horizontal -> height(2.dp).fillMaxWidth()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.scrollThumb(this, interactionSource)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A simple [Scrollbar].
|
||||||
|
* Its thumb disappears when the scrolling container is dormant.
|
||||||
|
* @param modifier a [Modifier] for the [Scrollbar]
|
||||||
|
* @param state the driving state for the [Scrollbar]
|
||||||
|
* @param orientation the orientation of the scrollbar
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun ScrollableState.DecorativeScrollbar(
|
||||||
|
modifier: Modifier,
|
||||||
|
state: ScrollbarState,
|
||||||
|
orientation: Orientation
|
||||||
|
) {
|
||||||
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
|
Scrollbar(
|
||||||
|
modifier = modifier,
|
||||||
|
orientation = orientation,
|
||||||
|
interactionSource = interactionSource,
|
||||||
|
state = state,
|
||||||
|
thumb = {
|
||||||
|
DecorativeScrollbarThumb(
|
||||||
|
interactionSource = interactionSource,
|
||||||
|
orientation = orientation
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A decorative scrollbar thumb used solely for communicating a user's position in a list.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
private fun ScrollableState.DecorativeScrollbarThumb(
|
||||||
|
interactionSource: InteractionSource,
|
||||||
|
orientation: Orientation
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.run {
|
||||||
|
when (orientation) {
|
||||||
|
Orientation.Vertical -> width(2.dp).fillMaxHeight()
|
||||||
|
Orientation.Horizontal -> height(2.dp).fillMaxWidth()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.scrollThumb(this, interactionSource)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun Modifier.scrollThumb(
|
||||||
|
scrollbarState: ScrollableState,
|
||||||
|
interactionSource: InteractionSource
|
||||||
|
): Modifier {
|
||||||
|
val colorState =
|
||||||
|
scrollbarThumbColor(scrollableState = scrollbarState, interactionSource = interactionSource)
|
||||||
|
return this then ScrollThumbElement { colorState.value }
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("ModifierNodeInspectableProperties")
|
||||||
|
private data class ScrollThumbElement(val colorProducer: ColorProducer) :
|
||||||
|
ModifierNodeElement<ScrollThumbNode>() {
|
||||||
|
override fun create(): ScrollThumbNode = ScrollThumbNode(colorProducer)
|
||||||
|
|
||||||
|
override fun update(node: ScrollThumbNode) {
|
||||||
|
node.colorProducer = colorProducer
|
||||||
|
node.invalidateDraw()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class ScrollThumbNode(var colorProducer: ColorProducer) : DrawModifierNode,
|
||||||
|
Modifier.Node() {
|
||||||
|
private val shape = RoundedCornerShape(16.dp)
|
||||||
|
|
||||||
|
// Naive cache outline calculation if size is th same
|
||||||
|
private var lastSize: Size? = null
|
||||||
|
private var lastLayoutDirection: LayoutDirection? = null
|
||||||
|
private var lastoutline: Outline? = null
|
||||||
|
|
||||||
|
override fun ContentDrawScope.draw() {
|
||||||
|
val color = colorProducer()
|
||||||
|
val outline =
|
||||||
|
if (size == lastSize && layoutDirection == lastLayoutDirection) {
|
||||||
|
lastoutline!!
|
||||||
|
} else {
|
||||||
|
shape.createOutline(size, layoutDirection, this)
|
||||||
|
}
|
||||||
|
if (color != Color.Unspecified) drawOutline(outline, color)
|
||||||
|
|
||||||
|
lastoutline = outline
|
||||||
|
lastSize = size
|
||||||
|
lastLayoutDirection = layoutDirection
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The color of the scrollbar thumb as a function of its interaction state.
|
||||||
|
* @param interactionSource source of interactions in the scrolling container
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
private fun scrollbarThumbColor(
|
||||||
|
scrollableState: ScrollableState,
|
||||||
|
interactionSource: InteractionSource
|
||||||
|
): State<Color> {
|
||||||
|
var state by remember { mutableStateOf(ThumbState.Dormant) }
|
||||||
|
val pressed by interactionSource.collectIsPressedAsState()
|
||||||
|
val hovered by interactionSource.collectIsHoveredAsState()
|
||||||
|
val dragged by interactionSource.collectIsDraggedAsState()
|
||||||
|
val active =
|
||||||
|
(scrollableState.canScrollForward || scrollableState.canScrollBackward && (pressed || hovered || dragged || scrollableState.isScrollInProgress))
|
||||||
|
|
||||||
|
val color = animateColorAsState(
|
||||||
|
targetValue = when (state) {
|
||||||
|
ThumbState.Active -> MaterialTheme.colorScheme.onSurface.copy(0.5f)
|
||||||
|
ThumbState.Inactive -> MaterialTheme.colorScheme.onSurface.copy(0.2f)
|
||||||
|
ThumbState.Dormant -> Color.Transparent
|
||||||
|
},
|
||||||
|
animationSpec = SpringSpec(
|
||||||
|
stiffness = Spring.StiffnessLow
|
||||||
|
),
|
||||||
|
label = "Scrollbar thumb color"
|
||||||
|
)
|
||||||
|
LaunchedEffect(active) {
|
||||||
|
when (active) {
|
||||||
|
true -> state = ThumbState.Active
|
||||||
|
false -> if (state == ThumbState.Active) {
|
||||||
|
state = ThumbState.Inactive
|
||||||
|
delay(SCROLLBAR_INACTIVE_TO_DORMANT_TIME_IN_MS)
|
||||||
|
state = ThumbState.Dormant
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return color
|
||||||
|
}
|
||||||
|
|
||||||
|
private enum class ThumbState {
|
||||||
|
Active,
|
||||||
|
Inactive,
|
||||||
|
Dormant,
|
||||||
|
}
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
package top.fatweb.oxygen.toolbox.ui.component.scrollbar
|
||||||
|
|
||||||
|
import androidx.compose.foundation.gestures.ScrollableState
|
||||||
|
import kotlin.math.abs
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Linearly interpolates the index for the first item in [visibleItems] for smooth scrollbar
|
||||||
|
* progression.
|
||||||
|
* @param visibleItems a list of items currently visible in the layout.
|
||||||
|
* @param itemSize a lookup function for the size of an item in the layout.
|
||||||
|
* @param offset a lookup function for the offset of an item relative to the start of the view port.
|
||||||
|
* @param nextItemOnMainAxis a lookup function for the next item on the main axis in the direction
|
||||||
|
* of the scroll.
|
||||||
|
* @param itemIndex a lookup function for index of an item in the layout relative to
|
||||||
|
* the total amount of items available.
|
||||||
|
*
|
||||||
|
* @return a [Float] in the range [firstItemPosition..nextItemPosition) where nextItemPosition
|
||||||
|
* is the index of the consecutive item along the major axis.
|
||||||
|
* */
|
||||||
|
internal inline fun <LazyState : ScrollableState, LazyStateItem> LazyState.interpolateFirstItemIndex(
|
||||||
|
visibleItems: List<LazyStateItem>,
|
||||||
|
crossinline itemSize: LazyState.(LazyStateItem) -> Int,
|
||||||
|
crossinline offset: LazyState.(LazyStateItem) -> Int,
|
||||||
|
crossinline nextItemOnMainAxis: LazyState.(LazyStateItem) -> LazyStateItem?,
|
||||||
|
crossinline itemIndex: (LazyStateItem) -> Int
|
||||||
|
): Float {
|
||||||
|
if (visibleItems.isEmpty()) return 0f
|
||||||
|
|
||||||
|
val firstItem = visibleItems.first()
|
||||||
|
val firstItemIndex = itemIndex(firstItem)
|
||||||
|
|
||||||
|
if (firstItemIndex < 0) return Float.NaN
|
||||||
|
|
||||||
|
val firstItemSize = itemSize(firstItem)
|
||||||
|
if (firstItemSize == 0) return Float.NaN
|
||||||
|
|
||||||
|
val itemOffset = offset(firstItem).toFloat()
|
||||||
|
val offsetPercentage = abs(itemOffset) / firstItemSize
|
||||||
|
|
||||||
|
val nextItem = nextItemOnMainAxis(firstItem) ?: return firstItemIndex + offsetPercentage
|
||||||
|
|
||||||
|
val nextItemIndex = itemIndex(nextItem)
|
||||||
|
|
||||||
|
return firstItemIndex + ((nextItemIndex - firstItemIndex) * offsetPercentage)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the percentage of an item that is currently visible in the view port.
|
||||||
|
* @param itemSize the size of the item
|
||||||
|
* @param itemStartOffset the start offset of the item relative to the view port start
|
||||||
|
* @param viewportStartOffset the start offset of the view port
|
||||||
|
* @param viewportEndOffset the end offset of the view port
|
||||||
|
*/
|
||||||
|
internal fun itemVisibilityPercentage(
|
||||||
|
itemSize: Int,
|
||||||
|
itemStartOffset: Int,
|
||||||
|
viewportStartOffset: Int,
|
||||||
|
viewportEndOffset: Int
|
||||||
|
): Float {
|
||||||
|
if (itemSize == 0) return 0f
|
||||||
|
val itemEndOffset = itemStartOffset + itemSize
|
||||||
|
val startOffset = when {
|
||||||
|
itemStartOffset > viewportStartOffset -> 0
|
||||||
|
else -> abs(abs(viewportStartOffset) - abs(itemStartOffset))
|
||||||
|
}
|
||||||
|
val endOffset = when {
|
||||||
|
itemEndOffset < viewportEndOffset -> 0
|
||||||
|
else -> abs(abs(itemEndOffset) - abs(viewportEndOffset))
|
||||||
|
}
|
||||||
|
val size = itemSize.toFloat()
|
||||||
|
|
||||||
|
return (size - startOffset - endOffset) / size
|
||||||
|
}
|
||||||
@@ -0,0 +1,402 @@
|
|||||||
|
package top.fatweb.oxygen.toolbox.ui.component.scrollbar
|
||||||
|
|
||||||
|
import androidx.compose.foundation.gestures.Orientation
|
||||||
|
import androidx.compose.foundation.gestures.detectHorizontalDragGestures
|
||||||
|
import androidx.compose.foundation.gestures.detectTapGestures
|
||||||
|
import androidx.compose.foundation.gestures.detectVerticalDragGestures
|
||||||
|
import androidx.compose.foundation.hoverable
|
||||||
|
import androidx.compose.foundation.interaction.DragInteraction
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.interaction.PressInteraction
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
|
import androidx.compose.runtime.mutableLongStateOf
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.runtime.snapshotFlow
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.input.pointer.PointerInputChange
|
||||||
|
import androidx.compose.ui.input.pointer.pointerInput
|
||||||
|
import androidx.compose.ui.layout.Layout
|
||||||
|
import androidx.compose.ui.layout.onGloballyPositioned
|
||||||
|
import androidx.compose.ui.layout.positionInRoot
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.IntOffset
|
||||||
|
import androidx.compose.ui.unit.IntSize
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.util.packFloats
|
||||||
|
import androidx.compose.ui.util.unpackFloat1
|
||||||
|
import androidx.compose.ui.util.unpackFloat2
|
||||||
|
import kotlinx.coroutines.TimeoutCancellationException
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.withTimeout
|
||||||
|
import kotlin.math.max
|
||||||
|
import kotlin.math.min
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The delay between scrolls when a user long presses on the scrollbar track to initiate a scroll
|
||||||
|
* instead of dragging the scrollbar thumb.
|
||||||
|
*/
|
||||||
|
private const val SCROLLBAR_PRESS_DELAY_MS = 10L
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The percentage displacement of the scrollbar when scrolled by long presses on the scrollbar
|
||||||
|
* track.
|
||||||
|
*/
|
||||||
|
private const val SCROLLBAR_PRESS_DELTA_PCT = 0.02f
|
||||||
|
|
||||||
|
class ScrollbarState {
|
||||||
|
private var packedValue by mutableLongStateOf(0L)
|
||||||
|
|
||||||
|
internal fun onScroll(stateValue: ScrollbarStateValue) {
|
||||||
|
packedValue = stateValue.packedValue
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the thumb size of the scrollbar as a percentage of the total track size
|
||||||
|
*/
|
||||||
|
val thumbSizePercent
|
||||||
|
get() = unpackFloat1(packedValue)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the distance the thumb has traveled as a percentage of total track size
|
||||||
|
*/
|
||||||
|
val thumbMovedPercent
|
||||||
|
get() = unpackFloat2(packedValue)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the max distance the thumb can travel as a percentage of total track size
|
||||||
|
*/
|
||||||
|
val thumbTrackSizePercent
|
||||||
|
get() = 1f - thumbSizePercent
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the size of the scrollbar track in pixels
|
||||||
|
*/
|
||||||
|
private val ScrollbarTrack.size
|
||||||
|
get() = unpackFloat2(packedValue) - unpackFloat1(packedValue)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the position of the scrollbar thumb on the track as a percentage
|
||||||
|
*/
|
||||||
|
private fun ScrollbarTrack.thumbPosition(
|
||||||
|
dimension: Float
|
||||||
|
): Float = max(
|
||||||
|
a = min(
|
||||||
|
a = dimension / size,
|
||||||
|
b = 1f
|
||||||
|
),
|
||||||
|
b = 0f
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class definition for the core properties of a scroll bar
|
||||||
|
*/
|
||||||
|
@Immutable
|
||||||
|
@JvmInline
|
||||||
|
value class ScrollbarStateValue internal constructor(
|
||||||
|
internal val packedValue: Long
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class definition for the core properties of a scroll bar track
|
||||||
|
*/
|
||||||
|
@Immutable
|
||||||
|
@JvmInline
|
||||||
|
private value class ScrollbarTrack(
|
||||||
|
val packedValue: Long
|
||||||
|
) {
|
||||||
|
constructor(
|
||||||
|
max: Float,
|
||||||
|
min: Float
|
||||||
|
) : this(packFloats(max, min))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a [ScrollbarStateValue] with the listed properties
|
||||||
|
* @param thumbSizePercent the thumb size of the scrollbar as a percentage of the total track size.
|
||||||
|
* Refers to either the thumb width (for horizontal scrollbars)
|
||||||
|
* or height (for vertical scrollbars).
|
||||||
|
* @param thumbMovedPercent the distance the thumb has traveled as a percentage of total
|
||||||
|
* track size.
|
||||||
|
*/
|
||||||
|
fun scrollbarStateValue(
|
||||||
|
thumbSizePercent: Float,
|
||||||
|
thumbMovedPercent: Float,
|
||||||
|
) = ScrollbarStateValue(
|
||||||
|
packFloats(
|
||||||
|
val1 = thumbSizePercent,
|
||||||
|
val2 = thumbMovedPercent,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the value of [offset] along the axis specified by [this]
|
||||||
|
*/
|
||||||
|
internal fun Orientation.valueOf(offset: Offset) = when (this) {
|
||||||
|
Orientation.Horizontal -> offset.x
|
||||||
|
Orientation.Vertical -> offset.y
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the value of [intSize] along the axis specified by [this]
|
||||||
|
*/
|
||||||
|
internal fun Orientation.valueOf(intSize: IntSize) = when (this) {
|
||||||
|
Orientation.Horizontal -> intSize.width
|
||||||
|
Orientation.Vertical -> intSize.height
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the value of [intOffset] along the axis specified by [this]
|
||||||
|
*/
|
||||||
|
internal fun Orientation.valueOf(intOffset: IntOffset) = when (this) {
|
||||||
|
Orientation.Horizontal -> intOffset.x
|
||||||
|
Orientation.Vertical -> intOffset.y
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Composable for drawing a scrollbar
|
||||||
|
* @param orientation the scroll direction of the scrollbar
|
||||||
|
* @param state the state describing the position of the scrollbar
|
||||||
|
* @param interactionSource allows for observing the state of the scroll bar
|
||||||
|
* @param thumb a composable for drawing the scrollbar thumb
|
||||||
|
* @param minThumbSize the minimum size of the scrollbar thumb
|
||||||
|
* @param onThumbMoved an function for reacting to scroll bar displacements caused by direct
|
||||||
|
* interactions on the scrollbar thumb by the user, for example implementing a fast scroll
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun Scrollbar(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
orientation: Orientation,
|
||||||
|
state: ScrollbarState,
|
||||||
|
interactionSource: MutableInteractionSource? = null,
|
||||||
|
thumb: @Composable () -> Unit,
|
||||||
|
minThumbSize: Dp = 40.dp,
|
||||||
|
onThumbMoved: ((Float) -> Unit)? = null,
|
||||||
|
) {
|
||||||
|
// Using Offset.Unspecified and Float.NaN instead of null
|
||||||
|
// to prevent unnecessary boxing of primitives
|
||||||
|
var pressedOffset by remember { mutableStateOf(Offset.Unspecified) }
|
||||||
|
var draggedOffset by remember { mutableStateOf(Offset.Unspecified) }
|
||||||
|
|
||||||
|
|
||||||
|
// Used to immediately show drag feedback in the UI while the scrolling implementation
|
||||||
|
// catches up
|
||||||
|
var interactionThumbTravelPercent by remember { mutableFloatStateOf(Float.NaN) }
|
||||||
|
|
||||||
|
var track by remember { mutableStateOf(ScrollbarTrack(packedValue = 0)) }
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.run {
|
||||||
|
val withHover = interactionSource?.let(::hoverable) ?: this
|
||||||
|
when (orientation) {
|
||||||
|
Orientation.Vertical -> withHover.fillMaxHeight()
|
||||||
|
Orientation.Horizontal -> withHover.fillMaxWidth()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onGloballyPositioned { coordinates ->
|
||||||
|
val scrollbarStartCoordinate = orientation.valueOf(coordinates.positionInRoot())
|
||||||
|
track = ScrollbarTrack(
|
||||||
|
max = scrollbarStartCoordinate,
|
||||||
|
min = scrollbarStartCoordinate + orientation.valueOf(coordinates.size)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// Process scrollbar presses
|
||||||
|
.pointerInput(Unit) {
|
||||||
|
detectTapGestures(
|
||||||
|
onPress = { offset ->
|
||||||
|
try {
|
||||||
|
// Wait for a long press before scrolling
|
||||||
|
withTimeout(viewConfiguration.longPressTimeoutMillis) {
|
||||||
|
tryAwaitRelease()
|
||||||
|
}
|
||||||
|
} catch (_: TimeoutCancellationException) {
|
||||||
|
// Start the press triggered scroll
|
||||||
|
val initialPress = PressInteraction.Press(offset)
|
||||||
|
interactionSource?.tryEmit(initialPress)
|
||||||
|
|
||||||
|
pressedOffset = offset
|
||||||
|
interactionSource?.tryEmit(
|
||||||
|
when {
|
||||||
|
tryAwaitRelease() -> PressInteraction.Release(initialPress)
|
||||||
|
else -> PressInteraction.Cancel(initialPress)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// End the press
|
||||||
|
pressedOffset = Offset.Unspecified
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// Process scrollbar drags
|
||||||
|
.pointerInput(Unit) {
|
||||||
|
var dragInteraction: DragInteraction.Start? = null
|
||||||
|
val onDragStart: (Offset) -> Unit = { offset ->
|
||||||
|
val start = DragInteraction.Start()
|
||||||
|
dragInteraction = start
|
||||||
|
interactionSource?.tryEmit(start)
|
||||||
|
draggedOffset = offset
|
||||||
|
}
|
||||||
|
val onDragEnd: () -> Unit = {
|
||||||
|
dragInteraction?.run { interactionSource?.tryEmit(DragInteraction.Stop(this)) }
|
||||||
|
draggedOffset = Offset.Unspecified
|
||||||
|
}
|
||||||
|
val onDragCancel: () -> Unit = {
|
||||||
|
dragInteraction?.run { interactionSource?.tryEmit(DragInteraction.Cancel(this)) }
|
||||||
|
draggedOffset = Offset.Unspecified
|
||||||
|
}
|
||||||
|
val onDrag: (change: PointerInputChange, dragAmount: Float) -> Unit =
|
||||||
|
onDrag@{ _, delta ->
|
||||||
|
if (draggedOffset == Offset.Unspecified) return@onDrag
|
||||||
|
draggedOffset = when (orientation) {
|
||||||
|
Orientation.Vertical -> draggedOffset.copy(
|
||||||
|
y = draggedOffset.y + delta
|
||||||
|
)
|
||||||
|
|
||||||
|
Orientation.Horizontal -> draggedOffset.copy(
|
||||||
|
x = draggedOffset.x + delta
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
when (orientation) {
|
||||||
|
Orientation.Horizontal -> detectHorizontalDragGestures(
|
||||||
|
onDragStart = onDragStart,
|
||||||
|
onDragEnd = onDragEnd,
|
||||||
|
onDragCancel = onDragCancel,
|
||||||
|
onHorizontalDrag = onDrag
|
||||||
|
)
|
||||||
|
|
||||||
|
Orientation.Vertical -> detectVerticalDragGestures(
|
||||||
|
onDragStart = onDragStart,
|
||||||
|
onDragEnd = onDragEnd,
|
||||||
|
onDragCancel = onDragCancel,
|
||||||
|
onVerticalDrag = onDrag
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
// scrollbar thumb container
|
||||||
|
Layout(content = { thumb() }) { measurableList, constraints ->
|
||||||
|
val measurable = measurableList.first()
|
||||||
|
|
||||||
|
val thumbSizePx = max(
|
||||||
|
a = state.thumbSizePercent * track.size,
|
||||||
|
b = minThumbSize.toPx()
|
||||||
|
)
|
||||||
|
|
||||||
|
val trackSizePx = when (state.thumbTrackSizePercent) {
|
||||||
|
0f -> track.size
|
||||||
|
else -> (track.size - thumbSizePx) / state.thumbTrackSizePercent
|
||||||
|
}
|
||||||
|
|
||||||
|
val thumbTravelPercent = max(
|
||||||
|
a = min(
|
||||||
|
a = when {
|
||||||
|
interactionThumbTravelPercent.isNaN() -> state.thumbMovedPercent
|
||||||
|
else -> interactionThumbTravelPercent
|
||||||
|
},
|
||||||
|
b = state.thumbTrackSizePercent
|
||||||
|
),
|
||||||
|
b = 0f
|
||||||
|
)
|
||||||
|
|
||||||
|
val thumbMovedPx = trackSizePx * thumbTravelPercent
|
||||||
|
|
||||||
|
val y = when (orientation) {
|
||||||
|
Orientation.Horizontal -> 0
|
||||||
|
Orientation.Vertical -> thumbMovedPx.roundToInt()
|
||||||
|
}
|
||||||
|
val x = when (orientation) {
|
||||||
|
Orientation.Horizontal -> thumbMovedPx.roundToInt()
|
||||||
|
Orientation.Vertical -> 0
|
||||||
|
}
|
||||||
|
|
||||||
|
val updatedConstraints = when (orientation) {
|
||||||
|
Orientation.Horizontal -> {
|
||||||
|
constraints.copy(
|
||||||
|
minWidth = thumbSizePx.roundToInt(),
|
||||||
|
maxWidth = thumbSizePx.roundToInt()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Orientation.Vertical -> {
|
||||||
|
constraints.copy(
|
||||||
|
minHeight = thumbSizePx.roundToInt(),
|
||||||
|
maxHeight = thumbSizePx.roundToInt()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val placeable = measurable.measure(updatedConstraints)
|
||||||
|
layout(placeable.width, placeable.height) {
|
||||||
|
placeable.place(x, y)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (onThumbMoved == null) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Process presses
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
snapshotFlow { pressedOffset }.collect { pressedOffset ->
|
||||||
|
// Press ended, reset interactionThumbTravelPercent
|
||||||
|
if (pressedOffset == Offset.Unspecified) {
|
||||||
|
interactionThumbTravelPercent = Float.NaN
|
||||||
|
return@collect
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentThumbMovedPercent = state.thumbMovedPercent
|
||||||
|
val destinationThumbMovedPercent = track.thumbPosition(
|
||||||
|
dimension = orientation.valueOf(pressedOffset)
|
||||||
|
)
|
||||||
|
val isPositive = currentThumbMovedPercent < destinationThumbMovedPercent
|
||||||
|
val delta = SCROLLBAR_PRESS_DELTA_PCT * if (isPositive) 1f else -1f
|
||||||
|
|
||||||
|
while (currentThumbMovedPercent != destinationThumbMovedPercent) {
|
||||||
|
currentThumbMovedPercent = when {
|
||||||
|
isPositive -> min(
|
||||||
|
a = currentThumbMovedPercent + delta,
|
||||||
|
b = destinationThumbMovedPercent
|
||||||
|
)
|
||||||
|
|
||||||
|
else -> max(
|
||||||
|
a = currentThumbMovedPercent + delta,
|
||||||
|
b = destinationThumbMovedPercent
|
||||||
|
)
|
||||||
|
}
|
||||||
|
onThumbMoved(currentThumbMovedPercent)
|
||||||
|
interactionThumbTravelPercent = currentThumbMovedPercent
|
||||||
|
delay(SCROLLBAR_PRESS_DELAY_MS)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
snapshotFlow { draggedOffset }.collect { draggedOffset ->
|
||||||
|
if (draggedOffset == Offset.Unspecified) {
|
||||||
|
interactionThumbTravelPercent = Float.NaN
|
||||||
|
return@collect
|
||||||
|
}
|
||||||
|
|
||||||
|
val currentTravel = track.thumbPosition(
|
||||||
|
dimension = orientation.valueOf(draggedOffset)
|
||||||
|
)
|
||||||
|
onThumbMoved(currentTravel)
|
||||||
|
interactionThumbTravelPercent = currentTravel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,221 @@
|
|||||||
|
package top.fatweb.oxygen.toolbox.ui.component.scrollbar
|
||||||
|
|
||||||
|
import androidx.compose.foundation.gestures.Orientation
|
||||||
|
import androidx.compose.foundation.lazy.LazyListItemInfo
|
||||||
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
|
import androidx.compose.foundation.lazy.grid.LazyGridItemInfo
|
||||||
|
import androidx.compose.foundation.lazy.grid.LazyGridState
|
||||||
|
import androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridItemInfo
|
||||||
|
import androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridState
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.snapshotFlow
|
||||||
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
import kotlinx.coroutines.flow.filterNotNull
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates a [ScrollbarState] driven by the changes in a [LazyListState].
|
||||||
|
*
|
||||||
|
* @param itemsAvailable the total amount of items available to scroll in the lazy list.
|
||||||
|
* @param itemIndex a lookup function for index of an item in the list relative to [itemsAvailable].
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun LazyListState.scrollbarState(
|
||||||
|
itemsAvailable: Int,
|
||||||
|
itemIndex: (LazyListItemInfo) -> Int = LazyListItemInfo::index
|
||||||
|
): ScrollbarState {
|
||||||
|
val state = remember { ScrollbarState() }
|
||||||
|
LaunchedEffect(this, itemsAvailable) {
|
||||||
|
snapshotFlow {
|
||||||
|
if (itemsAvailable == 0) return@snapshotFlow null
|
||||||
|
|
||||||
|
val visibleItemsInfo = layoutInfo.visibleItemsInfo
|
||||||
|
if (visibleItemsInfo.isEmpty()) return@snapshotFlow null
|
||||||
|
|
||||||
|
val firstIndex = min(
|
||||||
|
a = interpolateFirstItemIndex(
|
||||||
|
visibleItems = visibleItemsInfo,
|
||||||
|
itemSize = { it.size },
|
||||||
|
offset = { it.offset },
|
||||||
|
nextItemOnMainAxis = { first -> visibleItemsInfo.find { it != first } },
|
||||||
|
itemIndex = itemIndex
|
||||||
|
),
|
||||||
|
b = itemsAvailable.toFloat()
|
||||||
|
)
|
||||||
|
if (firstIndex.isNaN()) return@snapshotFlow null
|
||||||
|
|
||||||
|
val itemsVisible = visibleItemsInfo.floatSumOf { itemInfo ->
|
||||||
|
itemVisibilityPercentage(
|
||||||
|
itemSize = itemInfo.size,
|
||||||
|
itemStartOffset = itemInfo.index,
|
||||||
|
viewportStartOffset = layoutInfo.viewportStartOffset,
|
||||||
|
viewportEndOffset = layoutInfo.viewportEndOffset
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val thumbTravelPercent = min(
|
||||||
|
a = firstIndex / itemsAvailable,
|
||||||
|
b = 1f
|
||||||
|
)
|
||||||
|
val thumbSizePercent = min(
|
||||||
|
a = itemsVisible / itemsAvailable,
|
||||||
|
b = 1f
|
||||||
|
)
|
||||||
|
scrollbarStateValue(
|
||||||
|
thumbSizePercent = thumbSizePercent,
|
||||||
|
thumbMovedPercent = when {
|
||||||
|
layoutInfo.reverseLayout -> 1f - thumbTravelPercent
|
||||||
|
else -> thumbTravelPercent
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.filterNotNull()
|
||||||
|
.distinctUntilChanged()
|
||||||
|
.collect { state.onScroll(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculates a [ScrollbarState] driven by the changes in a [LazyGridState]
|
||||||
|
*
|
||||||
|
* @param itemsAvailable the total amount of items available to scroll in the grid.
|
||||||
|
* @param itemIndex a lookup function for index of an item in the grid relative to [itemsAvailable].
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun LazyGridState.scrollbarState(
|
||||||
|
itemsAvailable: Int,
|
||||||
|
itemIndex: (LazyGridItemInfo) -> Int = LazyGridItemInfo::index
|
||||||
|
): ScrollbarState {
|
||||||
|
val state = remember { ScrollbarState() }
|
||||||
|
LaunchedEffect(this, itemsAvailable) {
|
||||||
|
snapshotFlow {
|
||||||
|
if (itemsAvailable == 0) return@snapshotFlow null
|
||||||
|
|
||||||
|
val visibleItemsInfo = layoutInfo.visibleItemsInfo
|
||||||
|
if (visibleItemsInfo.isEmpty()) return@snapshotFlow null
|
||||||
|
|
||||||
|
val firstIndex = min(
|
||||||
|
a = interpolateFirstItemIndex(
|
||||||
|
visibleItems = visibleItemsInfo,
|
||||||
|
itemSize = { layoutInfo.orientation.valueOf(it.size) },
|
||||||
|
offset = { layoutInfo.orientation.valueOf(it.offset) },
|
||||||
|
nextItemOnMainAxis = { first ->
|
||||||
|
when (layoutInfo.orientation) {
|
||||||
|
Orientation.Vertical -> visibleItemsInfo.find {
|
||||||
|
it != first && it.row != first.row
|
||||||
|
}
|
||||||
|
|
||||||
|
Orientation.Horizontal -> visibleItemsInfo.find {
|
||||||
|
it != first && it.column != first.column
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
itemIndex = itemIndex
|
||||||
|
),
|
||||||
|
b = itemsAvailable.toFloat()
|
||||||
|
)
|
||||||
|
if (firstIndex.isNaN()) return@snapshotFlow null
|
||||||
|
|
||||||
|
val itemsVisible = visibleItemsInfo.floatSumOf { itemInfo ->
|
||||||
|
itemVisibilityPercentage(
|
||||||
|
itemSize = layoutInfo.orientation.valueOf(itemInfo.size),
|
||||||
|
itemStartOffset = layoutInfo.orientation.valueOf(itemInfo.offset),
|
||||||
|
viewportStartOffset = layoutInfo.viewportStartOffset,
|
||||||
|
viewportEndOffset = layoutInfo.viewportEndOffset
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val thumbTravelPercent = min(
|
||||||
|
a = firstIndex / itemsAvailable,
|
||||||
|
b = 1f
|
||||||
|
)
|
||||||
|
val thumbSizePercent = min(
|
||||||
|
a = itemsVisible / itemsAvailable.toFloat(),
|
||||||
|
b = 1f
|
||||||
|
)
|
||||||
|
scrollbarStateValue(
|
||||||
|
thumbSizePercent = thumbSizePercent,
|
||||||
|
thumbMovedPercent = when {
|
||||||
|
layoutInfo.reverseLayout -> 1f - thumbTravelPercent
|
||||||
|
else -> thumbTravelPercent
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.filterNotNull()
|
||||||
|
.distinctUntilChanged()
|
||||||
|
.collect { state.onScroll(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remembers a [ScrollbarState] driven by the changes in a [LazyStaggeredGridState]
|
||||||
|
*
|
||||||
|
* @param itemsAvailable the total amount of items available to scroll in the staggered grid.
|
||||||
|
* @param itemIndex a lookup function for index of an item in the staggered grid relative
|
||||||
|
* to [itemsAvailable].
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun LazyStaggeredGridState.scrollbarState(
|
||||||
|
itemsAvailable: Int,
|
||||||
|
itemIndex: (LazyStaggeredGridItemInfo) -> Int = LazyStaggeredGridItemInfo::index
|
||||||
|
): ScrollbarState {
|
||||||
|
val state = remember { ScrollbarState() }
|
||||||
|
LaunchedEffect(this, itemsAvailable) {
|
||||||
|
snapshotFlow {
|
||||||
|
if (itemsAvailable == 0) return@snapshotFlow null
|
||||||
|
|
||||||
|
val visibleItemsInfo = layoutInfo.visibleItemsInfo
|
||||||
|
if (visibleItemsInfo.isEmpty()) return@snapshotFlow null
|
||||||
|
|
||||||
|
val firstIndex = min(
|
||||||
|
a = interpolateFirstItemIndex(
|
||||||
|
visibleItems = visibleItemsInfo,
|
||||||
|
itemSize = { layoutInfo.orientation.valueOf(it.size) },
|
||||||
|
offset = { layoutInfo.orientation.valueOf(it.offset) },
|
||||||
|
nextItemOnMainAxis = { first ->
|
||||||
|
visibleItemsInfo.find { it != first && it.lane == first.lane }
|
||||||
|
},
|
||||||
|
itemIndex = itemIndex
|
||||||
|
),
|
||||||
|
b = itemsAvailable.toFloat()
|
||||||
|
)
|
||||||
|
if (firstIndex.isNaN()) return@snapshotFlow null
|
||||||
|
|
||||||
|
val itemsVisible = visibleItemsInfo.floatSumOf { itemInfo ->
|
||||||
|
itemVisibilityPercentage(
|
||||||
|
itemSize = layoutInfo.orientation.valueOf(itemInfo.size),
|
||||||
|
itemStartOffset = layoutInfo.orientation.valueOf(itemInfo.offset),
|
||||||
|
viewportStartOffset = layoutInfo.viewportStartOffset,
|
||||||
|
viewportEndOffset = layoutInfo.viewportEndOffset
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val thumbTravelPercent = min(
|
||||||
|
a = firstIndex / itemsAvailable,
|
||||||
|
b = 1f
|
||||||
|
)
|
||||||
|
val thumbSizePercent = min(
|
||||||
|
a = itemsVisible / itemsAvailable,
|
||||||
|
b = 1f
|
||||||
|
)
|
||||||
|
scrollbarStateValue(
|
||||||
|
thumbSizePercent = thumbSizePercent,
|
||||||
|
thumbMovedPercent = thumbTravelPercent
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.filterNotNull()
|
||||||
|
.distinctUntilChanged()
|
||||||
|
.collect { state.onScroll(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
private inline fun <T> List<T>.floatSumOf(selector: (T) -> Float): Float =
|
||||||
|
fold(initial = 0f) { accumulator, listItem -> accumulator + selector(listItem) }
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user