Compare commits
81 Commits
master
...
3b4b6b4e8e
| Author | SHA1 | Date | |
|---|---|---|---|
|
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
|
2
.gitignore
vendored
2
.gitignore
vendored
@@ -43,3 +43,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/
|
||||||
|
|||||||
@@ -4,10 +4,15 @@ import com.mikepenz.aboutlibraries.plugin.AboutLibrariesTask
|
|||||||
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 {
|
||||||
@@ -25,9 +30,6 @@ android {
|
|||||||
vectorDrawables {
|
vectorDrawables {
|
||||||
useSupportLibrary = true
|
useSupportLibrary = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Required when setting minSdkVersion to 20 or lower
|
|
||||||
multiDexEnabled = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
@@ -60,9 +62,10 @@ android {
|
|||||||
}
|
}
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
compose = true
|
compose = true
|
||||||
|
buildConfig = true
|
||||||
}
|
}
|
||||||
composeOptions {
|
composeOptions {
|
||||||
kotlinCompilerExtensionVersion = "1.5.10"
|
kotlinCompilerExtensionVersion = "1.5.12"
|
||||||
}
|
}
|
||||||
packaging {
|
packaging {
|
||||||
resources {
|
resources {
|
||||||
@@ -101,11 +104,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 +115,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)
|
||||||
@@ -160,4 +173,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,7 @@
|
|||||||
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" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".OxygenApplication"
|
android:name=".OxygenApplication"
|
||||||
@@ -27,6 +28,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>
|
||||||
|
|
||||||
|
|||||||
18
app/src/main/assets/template/tool-view.html
Normal file
18
app/src/main/assets/template/tool-view.html
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Preview</title>
|
||||||
|
<!-- es-module-shims -->
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<script type="module" id="appSrc">{{replace_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;">
|
||||||
|
Loading...
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -2,7 +2,6 @@ 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
|
||||||
@@ -32,13 +31,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 +47,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() {
|
||||||
@@ -93,12 +92,10 @@ class MainActivity : ComponentActivity() {
|
|||||||
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 +108,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 +117,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 +143,57 @@ 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 shouldUseDarkTheme(uiState: MainActivityUiState): Boolean =
|
||||||
uiState: MainActivityUiState
|
when (uiState) {
|
||||||
): Boolean = when (uiState) {
|
MainActivityUiState.Loading -> isSystemInDarkTheme()
|
||||||
MainActivityUiState.Loading -> isSystemInDarkTheme()
|
is MainActivityUiState.Success ->
|
||||||
is MainActivityUiState.Success -> when (uiState.userData.darkThemeConfig) {
|
when (uiState.userData.darkThemeConfig) {
|
||||||
DarkThemeConfig.FOLLOW_SYSTEM -> isSystemInDarkTheme()
|
DarkThemeConfig.FollowSystem -> isSystemInDarkTheme()
|
||||||
DarkThemeConfig.LIGHT -> false
|
DarkThemeConfig.Light -> false
|
||||||
DarkThemeConfig.DARK -> true
|
DarkThemeConfig.Dark -> true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun shouldUseAndroidTheme(
|
private fun shouldUseAndroidTheme(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) {
|
when (uiState.userData.themeBrandConfig) {
|
||||||
ThemeBrandConfig.DEFAULT -> false
|
ThemeBrandConfig.Default -> false
|
||||||
ThemeBrandConfig.ANDROID -> true
|
ThemeBrandConfig.Android -> true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun shouldUseDynamicColor(
|
private fun shouldUseDynamicColor(uiState: MainActivityUiState): Boolean =
|
||||||
uiState: MainActivityUiState
|
when (uiState) {
|
||||||
): Boolean = when (uiState) {
|
MainActivityUiState.Loading -> true
|
||||||
MainActivityUiState.Loading -> true
|
is MainActivityUiState.Success -> uiState.userData.useDynamicColor
|
||||||
is MainActivityUiState.Success -> uiState.userData.useDynamicColor
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun whatLocale(
|
private fun whatLocale(uiState: MainActivityUiState): LanguageConfig =
|
||||||
uiState: MainActivityUiState
|
when (uiState) {
|
||||||
): LanguageConfig = when (uiState) {
|
MainActivityUiState.Loading -> LanguageConfig.FollowSystem
|
||||||
MainActivityUiState.Loading -> LanguageConfig.FOLLOW_SYSTEM
|
is MainActivityUiState.Success -> uiState.userData.languageConfig
|
||||||
is MainActivityUiState.Success -> uiState.userData.languageConfig
|
}
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun whatLaunchPage(
|
private fun whatLaunchPage(uiState: MainActivityUiState): LaunchPageConfig =
|
||||||
uiState: MainActivityUiState
|
when (uiState) {
|
||||||
): LaunchPageConfig = when (uiState) {
|
MainActivityUiState.Loading -> LaunchPageConfig.Tools
|
||||||
MainActivityUiState.Loading -> LaunchPageConfig.TOOLS
|
is MainActivityUiState.Success -> uiState.userData.launchPageConfig
|
||||||
is MainActivityUiState.Success -> uiState.userData.launchPageConfig
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The default light scrim, as defined by androidx and the platform:
|
|
||||||
* https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:activity/activity/src/main/java/androidx/activity/EdgeToEdge.kt;l=35-38;drc=27e7d52e8604a080133e8b842db10c89b4482598
|
|
||||||
*/
|
|
||||||
private val lightScrim = android.graphics.Color.argb(0xe6, 0xFF, 0xFF, 0xFF)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The default dark scrim, as defined by androidx and the platform:
|
|
||||||
* https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:activity/activity/src/main/java/androidx/activity/EdgeToEdge.kt;l=40-44;drc=27e7d52e8604a080133e8b842db10c89b4482598
|
|
||||||
*/
|
|
||||||
private val darkScrim = android.graphics.Color.argb(0x80, 0x1b, 0x1b, 0x1b)
|
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ 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 top.fatweb.oxygen.toolbox.model.userdata.UserData
|
||||||
import top.fatweb.oxygen.toolbox.repository.UserDataRepository
|
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
|
||||||
|
|
||||||
@@ -21,7 +21,7 @@ class MainActivityViewModel @Inject constructor(
|
|||||||
}.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)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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,25 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
@@ -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,13 +61,13 @@ 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
|
||||||
)
|
)
|
||||||
@@ -71,9 +77,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 +89,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 +100,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 +111,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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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,73 @@
|
|||||||
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.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 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,4 +1,4 @@
|
|||||||
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,
|
||||||
@@ -58,12 +58,13 @@ internal class ConnectivityManagerNetworkMonitor @Inject constructor(
|
|||||||
.conflate()
|
.conflate()
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
private fun ConnectivityManager.isCurrentlyConnected() = when {
|
private fun ConnectivityManager.isCurrentlyConnected() =
|
||||||
VERSION.SDK_INT >= VERSION_CODES.M ->
|
when {
|
||||||
activeNetwork
|
VERSION.SDK_INT >= VERSION_CODES.M ->
|
||||||
?.let(::getNetworkCapabilities)
|
activeNetwork
|
||||||
?.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
?.let(::getNetworkCapabilities)
|
||||||
|
?.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||||
|
|
||||||
else -> activeNetworkInfo?.isConnected
|
else -> activeNetworkInfo?.isConnected
|
||||||
} ?: false
|
} ?: 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,8 +9,11 @@ import top.fatweb.oxygen.toolbox.ui.OxygenAppState
|
|||||||
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
|
||||||
NavHost(
|
NavHost(
|
||||||
@@ -18,14 +21,33 @@ fun OxygenNavHost(
|
|||||||
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,22 @@
|
|||||||
|
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 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,37 @@
|
|||||||
|
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 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>
|
||||||
@@ -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> =
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package top.fatweb.oxygen.toolbox.ui
|
package top.fatweb.oxygen.toolbox.ui
|
||||||
|
|
||||||
|
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
|
||||||
@@ -11,6 +12,7 @@ import androidx.compose.foundation.layout.padding
|
|||||||
import androidx.compose.foundation.layout.safeDrawing
|
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.foundation.layout.windowInsetsPadding
|
||||||
|
import androidx.compose.material3.BottomAppBar
|
||||||
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
|
||||||
@@ -24,19 +26,22 @@ import androidx.compose.material3.TopAppBarDefaults
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
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.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
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,6 +53,7 @@ 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
|
||||||
@@ -56,7 +62,7 @@ import top.fatweb.oxygen.toolbox.ui.theme.LocalGradientColors
|
|||||||
@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)
|
||||||
}
|
}
|
||||||
@@ -71,7 +77,29 @@ fun OxygenApp(appState: OxygenAppState) {
|
|||||||
|
|
||||||
val isOffline by appState.isOffline.collectAsStateWithLifecycle()
|
val isOffline by appState.isOffline.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
val noConnectMessage = stringResource(R.string.no_connect)
|
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) {
|
LaunchedEffect(isOffline) {
|
||||||
if (isOffline) {
|
if (isOffline) {
|
||||||
@@ -84,22 +112,32 @@ fun OxygenApp(appState: OxygenAppState) {
|
|||||||
|
|
||||||
if (showSettingsDialog) {
|
if (showSettingsDialog) {
|
||||||
SettingsDialog(
|
SettingsDialog(
|
||||||
onDismiss = { showSettingsDialog = false }
|
onDismiss = { showSettingsDialog = false },
|
||||||
|
onNavigateToLibraries = appState::navigateToLibraries,
|
||||||
|
onNavigateToAbout = appState::navigateToAbout
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
|
modifier = Modifier
|
||||||
|
.nestedScroll(connection = topAppBarScrollBehavior.nestedScrollConnection),
|
||||||
containerColor = Color.Transparent,
|
containerColor = Color.Transparent,
|
||||||
contentColor = MaterialTheme.colorScheme.onBackground,
|
contentColor = MaterialTheme.colorScheme.onBackground,
|
||||||
contentWindowInsets = WindowInsets(0, 0, 0, 0),
|
contentWindowInsets = WindowInsets(left = 0, top = 0, right = 0, bottom = 0),
|
||||||
snackbarHost = { SnackbarHost(snackbarHostState) },
|
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
|
||||||
bottomBar = {
|
bottomBar = {
|
||||||
if (appState.shouldShowBottomBar && destination != null) {
|
AnimatedVisibility(
|
||||||
OxygenBottomBar(
|
visible = appState.shouldShowBottomBar && destination != null
|
||||||
destinations = appState.topLevelDestinations,
|
) {
|
||||||
onNavigateToDestination = appState::navigateToTopLevelDestination,
|
BottomAppBar(
|
||||||
currentDestination = appState.currentDestination
|
windowInsets = WindowInsets(0)
|
||||||
)
|
) {
|
||||||
|
OxygenBottomBar(
|
||||||
|
destinations = appState.topLevelDestinations,
|
||||||
|
currentDestination = appState.currentDestination,
|
||||||
|
onNavigateToDestination = appState::navigateToTopLevelDestination
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
) { padding ->
|
) { padding ->
|
||||||
@@ -114,45 +152,70 @@ fun OxygenApp(appState: OxygenAppState) {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
if (appState.shouldShowNavRail && destination != null) {
|
AnimatedVisibility(
|
||||||
|
visible = appState.shouldShowNavRail && destination != null
|
||||||
|
) {
|
||||||
OxygenNavRail(
|
OxygenNavRail(
|
||||||
modifier = Modifier.safeDrawingPadding(),
|
modifier = Modifier.safeDrawingPadding(),
|
||||||
destinations = appState.topLevelDestinations,
|
destinations = appState.topLevelDestinations,
|
||||||
onNavigateToDestination = appState::navigateToTopLevelDestination,
|
currentDestination = appState.currentDestination,
|
||||||
currentDestination = appState.currentDestination
|
onNavigateToDestination = appState::navigateToTopLevelDestination
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
Modifier.fillMaxSize()
|
Modifier.fillMaxSize()
|
||||||
) {
|
) {
|
||||||
if (destination != null) {
|
AnimatedVisibility(
|
||||||
|
visible = destination != null
|
||||||
|
) {
|
||||||
OxygenTopAppBar(
|
OxygenTopAppBar(
|
||||||
titleRes = destination.titleTextId,
|
scrollBehavior = topAppBarScrollBehavior,
|
||||||
|
title = {
|
||||||
|
destination?.let { Text(text = stringResource(destination.titleTextId)) }
|
||||||
|
},
|
||||||
navigationIcon = OxygenIcons.Search,
|
navigationIcon = OxygenIcons.Search,
|
||||||
navigationIconContentDescription = stringResource(R.string.feature_settings_top_app_bar_navigation_icon_description),
|
navigationIconContentDescription = stringResource(R.string.feature_settings_top_app_bar_navigation_icon_description),
|
||||||
actionIcon = OxygenIcons.MoreVert,
|
actionIcon = OxygenIcons.MoreVert,
|
||||||
actionIconContentDescription = stringResource(R.string.feature_settings_top_app_bar_action_icon_description),
|
actionIconContentDescription = stringResource(R.string.feature_settings_top_app_bar_action_icon_description),
|
||||||
|
activeSearch = activeSearch,
|
||||||
|
searchButtonPosition = SearchButtonPosition.Navigation,
|
||||||
|
query = searchValue,
|
||||||
colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
|
colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
|
||||||
containerColor = Color.Transparent
|
containerColor = Color.Transparent,
|
||||||
|
scrolledContainerColor = Color.Transparent
|
||||||
),
|
),
|
||||||
onNavigationClick = { appState.navigateToSearch() },
|
onNavigationClick = { activeSearch = true },
|
||||||
onActionClick = { showSettingsDialog = true }
|
onActionClick = { showSettingsDialog = true },
|
||||||
|
onQueryChange = {
|
||||||
|
searchValue = it
|
||||||
|
},
|
||||||
|
onSearch = {
|
||||||
|
searchCount++
|
||||||
|
},
|
||||||
|
onCancelSearch = {
|
||||||
|
searchValue = ""
|
||||||
|
activeSearch = false
|
||||||
|
searchCount = 0
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
OxygenNavHost(
|
OxygenNavHost(
|
||||||
appState = appState,
|
appState = appState,
|
||||||
|
startDestination = when (appState.launchPageConfig) {
|
||||||
|
LaunchPageConfig.Tools -> TOOLS_ROUTE
|
||||||
|
LaunchPageConfig.Star -> STAR_ROUTE
|
||||||
|
},
|
||||||
|
isVertical = appState.shouldShowBottomBar,
|
||||||
|
searchValue = searchValue,
|
||||||
|
searchCount = searchCount,
|
||||||
onShowSnackbar = { message, action ->
|
onShowSnackbar = { message, action ->
|
||||||
snackbarHostState.showSnackbar(
|
snackbarHostState.showSnackbar(
|
||||||
message = message,
|
message = message,
|
||||||
actionLabel = action,
|
actionLabel = action,
|
||||||
duration = SnackbarDuration.Short
|
duration = SnackbarDuration.Short
|
||||||
) == SnackbarResult.ActionPerformed
|
) == SnackbarResult.ActionPerformed
|
||||||
},
|
|
||||||
startDestination = when (appState.launchPageConfig) {
|
|
||||||
LaunchPageConfig.TOOLS -> TOOLS_ROUTE
|
|
||||||
LaunchPageConfig.STAR -> STAR_ROUTE
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -166,8 +229,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 +244,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 +263,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 +278,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 +295,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) ?: false
|
||||||
} ?: false
|
} ?: false
|
||||||
|
|||||||
@@ -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,179 @@
|
|||||||
|
package top.fatweb.oxygen.toolbox.ui.about
|
||||||
|
|
||||||
|
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.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.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.res.vectorResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
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))
|
||||||
|
},
|
||||||
|
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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun AboutAppInfo(
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = modifier,
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
imageVector = ImageVector.vectorResource(R.drawable.ic_oxygen),
|
||||||
|
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)}(${
|
||||||
|
ResourcesUtils.getAppVersionCode(
|
||||||
|
LocalContext.current
|
||||||
|
)
|
||||||
|
})"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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,290 @@
|
|||||||
|
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.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))
|
||||||
|
},
|
||||||
|
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(modifier = Modifier) {
|
||||||
|
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(
|
||||||
|
modifier = Modifier,
|
||||||
|
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,42 @@
|
|||||||
|
package top.fatweb.oxygen.toolbox.ui.component
|
||||||
|
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
import androidx.compose.foundation.text.ClickableText
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
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: (Int) -> 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))
|
||||||
|
pushStringAnnotation(tag = "Click", annotation = clickablePart)
|
||||||
|
withStyle(style = SpanStyle(color = primaryColor)) {
|
||||||
|
append(clickablePart)
|
||||||
|
}
|
||||||
|
pop()
|
||||||
|
append(mainText.substringAfter(clickablePart))
|
||||||
|
}
|
||||||
|
|
||||||
|
ClickableText(text = annotatedString, onClick = onClick)
|
||||||
|
}
|
||||||
@@ -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,42 @@
|
|||||||
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.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 +44,138 @@ import android.R as androidR
|
|||||||
@Composable
|
@Composable
|
||||||
fun OxygenTopAppBar(
|
fun OxygenTopAppBar(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
@StringRes titleRes: Int,
|
scrollBehavior: TopAppBarScrollBehavior? = null,
|
||||||
navigationIcon: ImageVector,
|
title: @Composable () -> Unit = {},
|
||||||
navigationIconContentDescription: String,
|
navigationIcon: ImageVector? = null,
|
||||||
actionIcon: ImageVector,
|
navigationIconContentDescription: String? = null,
|
||||||
actionIconContentDescription: String,
|
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(
|
||||||
|
if (scrollBehavior != null && -scrollBehavior.state.heightOffset >= with(LocalDensity.current) { 64.0.dp.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,
|
||||||
|
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,
|
||||||
|
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)) },
|
||||||
navigationIcon = OxygenIcons.Search,
|
navigationIcon = OxygenIcons.Search,
|
||||||
navigationIconContentDescription = "Navigation icon",
|
navigationIconContentDescription = "Navigation icon",
|
||||||
actionIcon = OxygenIcons.MoreVert,
|
actionIcon = OxygenIcons.MoreVert,
|
||||||
|
|||||||
@@ -0,0 +1,386 @@
|
|||||||
|
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(
|
||||||
|
modifier = Modifier,
|
||||||
|
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 (e: 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) }
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
package top.fatweb.oxygen.toolbox.ui.component.scrollbar
|
||||||
|
|
||||||
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
|
import androidx.compose.foundation.lazy.grid.LazyGridState
|
||||||
|
import androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridState
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberUpdatedState
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remembers a function to react to [Scrollbar] thumb position displacements for a [LazyListState]
|
||||||
|
* @param itemsAvailable the amount of items in the list.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun LazyListState.rememberDraggableScroller(
|
||||||
|
itemsAvailable: Int
|
||||||
|
): (Float) -> Unit = rememberDraggableScroller(
|
||||||
|
itemsAvailable = itemsAvailable, scroll = ::scrollToItem
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remembers a function to react to [Scrollbar] thumb position displacements for a [LazyGridState]
|
||||||
|
* @param itemsAvailable the amount of items in the grid.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun LazyGridState.rememberDraggableScroller(
|
||||||
|
itemsAvailable: Int
|
||||||
|
): (Float) -> Unit = rememberDraggableScroller(
|
||||||
|
itemsAvailable = itemsAvailable,
|
||||||
|
scroll = ::scrollToItem
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remembers a function to react to [Scrollbar] thumb position displacements for a
|
||||||
|
* [LazyStaggeredGridState]
|
||||||
|
* @param itemsAvailable the amount of items in the staggered grid.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun LazyStaggeredGridState.rememberDraggableScroller(
|
||||||
|
itemsAvailable: Int
|
||||||
|
): (Float) -> Unit = rememberDraggableScroller(
|
||||||
|
itemsAvailable = itemsAvailable,
|
||||||
|
scroll = ::scrollToItem
|
||||||
|
)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generic function to react to [Scrollbar] thumb displacements in a lazy layout.
|
||||||
|
* @param itemsAvailable the total amount of items available to scroll in the layout.
|
||||||
|
* @param scroll a function to be invoked when an index has been identified to scroll to.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
private inline fun rememberDraggableScroller(
|
||||||
|
itemsAvailable: Int,
|
||||||
|
crossinline scroll: suspend (index: Int) -> Unit
|
||||||
|
): (Float) -> Unit {
|
||||||
|
var percentage by remember { mutableFloatStateOf(Float.NaN) }
|
||||||
|
val itemCount by rememberUpdatedState(itemsAvailable)
|
||||||
|
|
||||||
|
LaunchedEffect(percentage) {
|
||||||
|
if (percentage.isNaN()) return@LaunchedEffect
|
||||||
|
val indexToFind = (itemCount * percentage).roundToInt()
|
||||||
|
scroll(indexToFind)
|
||||||
|
}
|
||||||
|
|
||||||
|
return remember {
|
||||||
|
{ newPercentage -> percentage = newPercentage }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
package top.fatweb.oxygen.toolbox.ui.search
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.safeDrawingPadding
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import top.fatweb.oxygen.toolbox.icon.OxygenIcons
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
internal fun SearchRoute(
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
onBackClick: () -> Unit,
|
|
||||||
// searchViewmodel: SearchViewModel = hiltViewModel()
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.safeDrawingPadding()
|
|
||||||
) {
|
|
||||||
IconButton(onClick = onBackClick) {
|
|
||||||
Icon(imageVector = OxygenIcons.Back, contentDescription = null)
|
|
||||||
}
|
|
||||||
Text("Search")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
package top.fatweb.oxygen.toolbox.ui.search
|
|
||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@HiltViewModel
|
|
||||||
class SearchViewModel @Inject constructor(
|
|
||||||
) : ViewModel()
|
|
||||||
@@ -1,55 +1,56 @@
|
|||||||
package top.fatweb.oxygen.toolbox.ui.settings
|
package top.fatweb.oxygen.toolbox.ui.settings
|
||||||
|
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.foundation.clickable
|
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.ColumnScope
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.foundation.layout.heightIn
|
import androidx.compose.foundation.layout.heightIn
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.width
|
|
||||||
import androidx.compose.foundation.layout.widthIn
|
import androidx.compose.foundation.layout.widthIn
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
import androidx.compose.foundation.selection.selectable
|
|
||||||
import androidx.compose.foundation.selection.selectableGroup
|
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.HorizontalDivider
|
import androidx.compose.material3.HorizontalDivider
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.RadioButton
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalConfiguration
|
import androidx.compose.ui.platform.LocalConfiguration
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.semantics.Role
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.window.DialogProperties
|
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
import androidx.hilt.navigation.compose.hiltViewModel
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import top.fatweb.oxygen.toolbox.R
|
import top.fatweb.oxygen.toolbox.R
|
||||||
import top.fatweb.oxygen.toolbox.model.DarkThemeConfig
|
import top.fatweb.oxygen.toolbox.icon.OxygenIcons
|
||||||
import top.fatweb.oxygen.toolbox.model.LanguageConfig
|
import top.fatweb.oxygen.toolbox.model.userdata.DarkThemeConfig
|
||||||
import top.fatweb.oxygen.toolbox.model.LaunchPageConfig
|
import top.fatweb.oxygen.toolbox.model.userdata.LanguageConfig
|
||||||
import top.fatweb.oxygen.toolbox.model.ThemeBrandConfig
|
import top.fatweb.oxygen.toolbox.model.userdata.LaunchPageConfig
|
||||||
import top.fatweb.oxygen.toolbox.ui.component.ThemePreviews
|
import top.fatweb.oxygen.toolbox.model.userdata.ThemeBrandConfig
|
||||||
|
import top.fatweb.oxygen.toolbox.model.userdata.UserData
|
||||||
|
import top.fatweb.oxygen.toolbox.ui.component.DialogChooserRow
|
||||||
|
import top.fatweb.oxygen.toolbox.ui.component.DialogClickerRow
|
||||||
|
import top.fatweb.oxygen.toolbox.ui.component.DialogSectionGroup
|
||||||
|
import top.fatweb.oxygen.toolbox.ui.component.DialogSectionTitle
|
||||||
|
import top.fatweb.oxygen.toolbox.ui.component.Indicator
|
||||||
|
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 top.fatweb.oxygen.toolbox.ui.theme.supportsDynamicTheming
|
import top.fatweb.oxygen.toolbox.ui.theme.supportsDynamicTheming
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SettingsDialog(
|
fun SettingsDialog(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
onDismiss: () -> Unit,
|
viewModel: SettingsViewModel = hiltViewModel(),
|
||||||
viewModel: SettingsViewModel = hiltViewModel()
|
onNavigateToLibraries: () -> Unit,
|
||||||
|
onNavigateToAbout: () -> Unit,
|
||||||
|
onDismiss: () -> Unit
|
||||||
) {
|
) {
|
||||||
val settingsUiState by viewModel.settingsUiState.collectAsStateWithLifecycle()
|
val settingsUiState by viewModel.settingsUiState.collectAsStateWithLifecycle()
|
||||||
SettingsDialog(
|
SettingsDialog(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
settingsUiState = settingsUiState,
|
settingsUiState = settingsUiState,
|
||||||
|
onNavigateToLibraries = onNavigateToLibraries,
|
||||||
|
onNavigateToAbout = onNavigateToAbout,
|
||||||
onDismiss = onDismiss,
|
onDismiss = onDismiss,
|
||||||
onChangeLanguageConfig = viewModel::updateLanguageConfig,
|
onChangeLanguageConfig = viewModel::updateLanguageConfig,
|
||||||
onChangeLaunchPageConfig = viewModel::updateLaunchPageConfig,
|
onChangeLaunchPageConfig = viewModel::updateLaunchPageConfig,
|
||||||
@@ -63,6 +64,8 @@ fun SettingsDialog(
|
|||||||
fun SettingsDialog(
|
fun SettingsDialog(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
settingsUiState: SettingsUiState,
|
settingsUiState: SettingsUiState,
|
||||||
|
onNavigateToLibraries: () -> Unit,
|
||||||
|
onNavigateToAbout: () -> Unit,
|
||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
supportDynamicColor: Boolean = supportsDynamicTheming(),
|
supportDynamicColor: Boolean = supportsDynamicTheming(),
|
||||||
onChangeLanguageConfig: (languageConfig: LanguageConfig) -> Unit,
|
onChangeLanguageConfig: (languageConfig: LanguageConfig) -> Unit,
|
||||||
@@ -77,7 +80,6 @@ fun SettingsDialog(
|
|||||||
modifier = modifier
|
modifier = modifier
|
||||||
.widthIn(max = configuration.screenWidthDp.dp - 80.dp)
|
.widthIn(max = configuration.screenWidthDp.dp - 80.dp)
|
||||||
.heightIn(max = configuration.screenHeightDp.dp - 40.dp),
|
.heightIn(max = configuration.screenHeightDp.dp - 40.dp),
|
||||||
properties = DialogProperties(usePlatformDefaultWidth = false),
|
|
||||||
onDismissRequest = onDismiss,
|
onDismissRequest = onDismiss,
|
||||||
title = {
|
title = {
|
||||||
Text(
|
Text(
|
||||||
@@ -92,16 +94,16 @@ fun SettingsDialog(
|
|||||||
) {
|
) {
|
||||||
when (settingsUiState) {
|
when (settingsUiState) {
|
||||||
SettingsUiState.Loading -> {
|
SettingsUiState.Loading -> {
|
||||||
Text(
|
Indicator()
|
||||||
modifier = Modifier.padding(vertical = 16.dp),
|
|
||||||
text = stringResource(R.string.feature_settings_loading)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
is SettingsUiState.Success -> {
|
is SettingsUiState.Success -> {
|
||||||
SettingsPanel(
|
SettingsPanel(
|
||||||
settings = settingsUiState.settings,
|
settings = settingsUiState.settings,
|
||||||
supportDynamicColor = supportDynamicColor,
|
supportDynamicColor = supportDynamicColor,
|
||||||
|
onNavigateToLibraries = onNavigateToLibraries,
|
||||||
|
onNavigateToAbout = onNavigateToAbout,
|
||||||
|
onDismiss = onDismiss,
|
||||||
onChangeLanguageConfig = onChangeLanguageConfig,
|
onChangeLanguageConfig = onChangeLanguageConfig,
|
||||||
onChangeLaunchPageConfig = onChangeLaunchPageConfig,
|
onChangeLaunchPageConfig = onChangeLaunchPageConfig,
|
||||||
onchangeThemeBrandConfig = onchangeThemeBrandConfig,
|
onchangeThemeBrandConfig = onchangeThemeBrandConfig,
|
||||||
@@ -114,157 +116,136 @@ fun SettingsDialog(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
confirmButton = {
|
confirmButton = {
|
||||||
Text(
|
TextButton(onClick = { onDismiss() }) {
|
||||||
modifier = Modifier
|
Text(
|
||||||
.padding(horizontal = 8.dp)
|
text = stringResource(R.string.core_ok),
|
||||||
.clickable { onDismiss() },
|
style = MaterialTheme.typography.labelLarge,
|
||||||
text = stringResource(R.string.feature_settings_dismiss_dialog_button_text),
|
color = MaterialTheme.colorScheme.primary
|
||||||
style = MaterialTheme.typography.labelLarge,
|
)
|
||||||
color = MaterialTheme.colorScheme.primary
|
}
|
||||||
)
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ColumnScope.SettingsPanel(
|
private fun ColumnScope.SettingsPanel(
|
||||||
settings: UserEditableSettings,
|
settings: UserData,
|
||||||
supportDynamicColor: Boolean,
|
supportDynamicColor: Boolean,
|
||||||
|
onNavigateToLibraries: () -> Unit,
|
||||||
|
onNavigateToAbout: () -> Unit,
|
||||||
|
onDismiss: () -> Unit,
|
||||||
onChangeLanguageConfig: (languageConfig: LanguageConfig) -> Unit,
|
onChangeLanguageConfig: (languageConfig: LanguageConfig) -> Unit,
|
||||||
onChangeLaunchPageConfig: (launchPageConfig: LaunchPageConfig) -> Unit,
|
onChangeLaunchPageConfig: (launchPageConfig: LaunchPageConfig) -> Unit,
|
||||||
onchangeThemeBrandConfig: (themeBrandConfig: ThemeBrandConfig) -> Unit,
|
onchangeThemeBrandConfig: (themeBrandConfig: ThemeBrandConfig) -> Unit,
|
||||||
onChangeDarkThemeConfig: (darkThemeConfig: DarkThemeConfig) -> Unit,
|
onChangeDarkThemeConfig: (darkThemeConfig: DarkThemeConfig) -> Unit,
|
||||||
onchangeUseDynamicColor: (useDynamicColor: Boolean) -> Unit
|
onchangeUseDynamicColor: (useDynamicColor: Boolean) -> Unit
|
||||||
) {
|
) {
|
||||||
SettingsDialogSectionTitle(text = stringResource(R.string.feature_settings_language))
|
DialogSectionTitle(text = stringResource(R.string.feature_settings_language))
|
||||||
Column(
|
DialogSectionGroup {
|
||||||
modifier = Modifier.selectableGroup()
|
DialogChooserRow(
|
||||||
) {
|
|
||||||
SettingsDialogThemeChooserRow(
|
|
||||||
text = stringResource(R.string.feature_settings_language_system_default),
|
text = stringResource(R.string.feature_settings_language_system_default),
|
||||||
selected = settings.languageConfig == LanguageConfig.FOLLOW_SYSTEM,
|
selected = settings.languageConfig == LanguageConfig.FollowSystem,
|
||||||
onClick = { onChangeLanguageConfig(LanguageConfig.FOLLOW_SYSTEM) }
|
onClick = { onChangeLanguageConfig(LanguageConfig.FollowSystem) }
|
||||||
)
|
)
|
||||||
SettingsDialogThemeChooserRow(
|
DialogChooserRow(
|
||||||
text = stringResource(R.string.feature_settings_language_chinese),
|
text = stringResource(R.string.feature_settings_language_chinese),
|
||||||
selected = settings.languageConfig == LanguageConfig.CHINESE,
|
selected = settings.languageConfig == LanguageConfig.Chinese,
|
||||||
onClick = { onChangeLanguageConfig(LanguageConfig.CHINESE) }
|
onClick = { onChangeLanguageConfig(LanguageConfig.Chinese) }
|
||||||
)
|
)
|
||||||
SettingsDialogThemeChooserRow(
|
DialogChooserRow(
|
||||||
text = stringResource(R.string.feature_settings_language_english),
|
text = stringResource(R.string.feature_settings_language_english),
|
||||||
selected = settings.languageConfig == LanguageConfig.ENGLISH,
|
selected = settings.languageConfig == LanguageConfig.English,
|
||||||
onClick = { onChangeLanguageConfig(LanguageConfig.ENGLISH) }
|
onClick = { onChangeLanguageConfig(LanguageConfig.English) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
SettingsDialogSectionTitle(text = stringResource(R.string.feature_settings_launch_page))
|
DialogSectionTitle(text = stringResource(R.string.feature_settings_launch_page))
|
||||||
Column(
|
DialogSectionGroup {
|
||||||
modifier = Modifier.selectableGroup()
|
DialogChooserRow(
|
||||||
) {
|
|
||||||
SettingsDialogThemeChooserRow(
|
|
||||||
text = stringResource(R.string.feature_settings_launch_page_tools),
|
text = stringResource(R.string.feature_settings_launch_page_tools),
|
||||||
selected = settings.launchPageConfig == LaunchPageConfig.TOOLS,
|
selected = settings.launchPageConfig == LaunchPageConfig.Tools,
|
||||||
onClick = { onChangeLaunchPageConfig(LaunchPageConfig.TOOLS) }
|
onClick = { onChangeLaunchPageConfig(LaunchPageConfig.Tools) }
|
||||||
)
|
)
|
||||||
SettingsDialogThemeChooserRow(
|
DialogChooserRow(
|
||||||
text = stringResource(R.string.feature_settings_launch_page_star),
|
text = stringResource(R.string.feature_settings_launch_page_star),
|
||||||
selected = settings.launchPageConfig == LaunchPageConfig.STAR,
|
selected = settings.launchPageConfig == LaunchPageConfig.Star,
|
||||||
onClick = { onChangeLaunchPageConfig(LaunchPageConfig.STAR) }
|
onClick = { onChangeLaunchPageConfig(LaunchPageConfig.Star) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
SettingsDialogSectionTitle(text = stringResource(R.string.feature_settings_theme_brand))
|
DialogSectionTitle(text = stringResource(R.string.feature_settings_theme_brand))
|
||||||
Column(
|
DialogSectionGroup {
|
||||||
modifier = Modifier.selectableGroup()
|
DialogChooserRow(
|
||||||
) {
|
|
||||||
SettingsDialogThemeChooserRow(
|
|
||||||
text = stringResource(R.string.feature_settings_theme_brand_default),
|
text = stringResource(R.string.feature_settings_theme_brand_default),
|
||||||
selected = settings.themeBrandConfig == ThemeBrandConfig.DEFAULT,
|
selected = settings.themeBrandConfig == ThemeBrandConfig.Default,
|
||||||
onClick = { onchangeThemeBrandConfig(ThemeBrandConfig.DEFAULT) }
|
onClick = { onchangeThemeBrandConfig(ThemeBrandConfig.Default) }
|
||||||
)
|
)
|
||||||
SettingsDialogThemeChooserRow(
|
DialogChooserRow(
|
||||||
text = stringResource(R.string.feature_settings_theme_brand_android),
|
text = stringResource(R.string.feature_settings_theme_brand_android),
|
||||||
selected = settings.themeBrandConfig == ThemeBrandConfig.ANDROID,
|
selected = settings.themeBrandConfig == ThemeBrandConfig.Android,
|
||||||
onClick = { onchangeThemeBrandConfig(ThemeBrandConfig.ANDROID) }
|
onClick = { onchangeThemeBrandConfig(ThemeBrandConfig.Android) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
AnimatedVisibility(visible = settings.themeBrandConfig == ThemeBrandConfig.DEFAULT && supportDynamicColor) {
|
AnimatedVisibility(visible = settings.themeBrandConfig == ThemeBrandConfig.Default && supportDynamicColor) {
|
||||||
Column(
|
DialogSectionGroup {
|
||||||
modifier = Modifier.selectableGroup()
|
DialogSectionTitle(text = stringResource(R.string.feature_settings_dynamic_color))
|
||||||
) {
|
DialogChooserRow(
|
||||||
SettingsDialogSectionTitle(text = stringResource(R.string.feature_settings_dynamic_color))
|
|
||||||
SettingsDialogThemeChooserRow(
|
|
||||||
text = stringResource(R.string.feature_settings_dynamic_color_enable),
|
text = stringResource(R.string.feature_settings_dynamic_color_enable),
|
||||||
selected = settings.useDynamicColor,
|
selected = settings.useDynamicColor,
|
||||||
onClick = { onchangeUseDynamicColor(true) }
|
onClick = { onchangeUseDynamicColor(true) }
|
||||||
)
|
)
|
||||||
SettingsDialogThemeChooserRow(
|
DialogChooserRow(
|
||||||
text = stringResource(R.string.feature_settings_dynamic_color_disable),
|
text = stringResource(R.string.feature_settings_dynamic_color_disable),
|
||||||
selected = !settings.useDynamicColor,
|
selected = !settings.useDynamicColor,
|
||||||
onClick = { onchangeUseDynamicColor(false) }
|
onClick = { onchangeUseDynamicColor(false) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
SettingsDialogSectionTitle(text = stringResource(R.string.feature_settings_dark_mode))
|
DialogSectionTitle(text = stringResource(R.string.feature_settings_dark_mode))
|
||||||
Column(
|
DialogSectionGroup {
|
||||||
modifier = Modifier.selectableGroup()
|
DialogChooserRow(
|
||||||
) {
|
|
||||||
SettingsDialogThemeChooserRow(
|
|
||||||
text = stringResource(R.string.feature_settings_dark_mode_system_default),
|
text = stringResource(R.string.feature_settings_dark_mode_system_default),
|
||||||
selected = settings.darkThemeConfig == DarkThemeConfig.FOLLOW_SYSTEM,
|
selected = settings.darkThemeConfig == DarkThemeConfig.FollowSystem,
|
||||||
onClick = { onChangeDarkThemeConfig(DarkThemeConfig.FOLLOW_SYSTEM) }
|
onClick = { onChangeDarkThemeConfig(DarkThemeConfig.FollowSystem) }
|
||||||
)
|
)
|
||||||
SettingsDialogThemeChooserRow(
|
DialogChooserRow(
|
||||||
text = stringResource(R.string.feature_settings_dark_mode_light),
|
text = stringResource(R.string.feature_settings_dark_mode_light),
|
||||||
selected = settings.darkThemeConfig == DarkThemeConfig.LIGHT,
|
selected = settings.darkThemeConfig == DarkThemeConfig.Light,
|
||||||
onClick = { onChangeDarkThemeConfig(DarkThemeConfig.LIGHT) }
|
onClick = { onChangeDarkThemeConfig(DarkThemeConfig.Light) }
|
||||||
)
|
)
|
||||||
SettingsDialogThemeChooserRow(
|
DialogChooserRow(
|
||||||
text = stringResource(R.string.feature_settings_dark_mode_dark),
|
text = stringResource(R.string.feature_settings_dark_mode_dark),
|
||||||
selected = settings.darkThemeConfig == DarkThemeConfig.DARK,
|
selected = settings.darkThemeConfig == DarkThemeConfig.Dark,
|
||||||
onClick = { onChangeDarkThemeConfig(DarkThemeConfig.DARK) }
|
onClick = { onChangeDarkThemeConfig(DarkThemeConfig.Dark) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
DialogSectionTitle(text = stringResource(R.string.feature_settings_more))
|
||||||
|
DialogSectionGroup {
|
||||||
|
DialogClickerRow(
|
||||||
|
icon = OxygenIcons.Code,
|
||||||
|
text = stringResource(R.string.feature_settings_open_source_license),
|
||||||
|
onClick = {
|
||||||
|
onNavigateToLibraries()
|
||||||
|
onDismiss()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
DialogClickerRow(
|
||||||
|
icon = OxygenIcons.Info,
|
||||||
|
text = stringResource(R.string.feature_settings_more_about),
|
||||||
|
onClick = {
|
||||||
|
onNavigateToAbout()
|
||||||
|
onDismiss()
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun SettingsDialogSectionTitle(text: String) {
|
|
||||||
Text(
|
|
||||||
modifier = Modifier.padding(top = 16.dp, bottom = 8.dp),
|
|
||||||
text = text,
|
|
||||||
style = MaterialTheme.typography.titleMedium
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@OxygenPreviews
|
||||||
private fun SettingsDialogThemeChooserRow(
|
|
||||||
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 = Modifier.width(8.dp))
|
|
||||||
Text(text)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@ThemePreviews
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun SettingsDialogLoadingPreview() {
|
private fun SettingsDialogLoadingPreview() {
|
||||||
OxygenTheme {
|
OxygenTheme {
|
||||||
SettingsDialog(
|
SettingsDialog(
|
||||||
|
onNavigateToLibraries = {},
|
||||||
|
onNavigateToAbout = {},
|
||||||
onDismiss = { },
|
onDismiss = { },
|
||||||
settingsUiState = SettingsUiState.Loading,
|
settingsUiState = SettingsUiState.Loading,
|
||||||
onChangeLanguageConfig = {},
|
onChangeLanguageConfig = {},
|
||||||
@@ -276,18 +257,20 @@ private fun SettingsDialogLoadingPreview() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ThemePreviews
|
@OxygenPreviews
|
||||||
@Composable
|
@Composable
|
||||||
private fun SettingDialogPreview() {
|
private fun SettingDialogPreview() {
|
||||||
OxygenTheme {
|
OxygenTheme {
|
||||||
SettingsDialog(
|
SettingsDialog(
|
||||||
|
onNavigateToLibraries = {},
|
||||||
|
onNavigateToAbout = {},
|
||||||
onDismiss = {},
|
onDismiss = {},
|
||||||
settingsUiState = SettingsUiState.Success(
|
settingsUiState = SettingsUiState.Success(
|
||||||
UserEditableSettings(
|
UserData(
|
||||||
languageConfig = LanguageConfig.FOLLOW_SYSTEM,
|
languageConfig = LanguageConfig.FollowSystem,
|
||||||
launchPageConfig = LaunchPageConfig.TOOLS,
|
launchPageConfig = LaunchPageConfig.Tools,
|
||||||
themeBrandConfig = ThemeBrandConfig.DEFAULT,
|
themeBrandConfig = ThemeBrandConfig.Default,
|
||||||
darkThemeConfig = DarkThemeConfig.FOLLOW_SYSTEM,
|
darkThemeConfig = DarkThemeConfig.FollowSystem,
|
||||||
useDynamicColor = true
|
useDynamicColor = true
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user