Compare commits

108 Commits

Author SHA1 Message Date
71119ac4c4 Fix(OxygenTopAppBar): Fix the bug that cannot hide completely when scrolling 2024-11-06 14:12:06 +08:00
c91eaaf1a5 Build(gradle): Upgrade dependencies 2024-11-05 17:57:41 +08:00
77802d3dc9 Refactor(WebView): Change background color to transparent 2024-11-05 17:45:25 +08:00
8669a2c9ef Feat(Tool): Support theme 2024-11-05 11:45:56 +08:00
d589df860e Refactor(ToolView): Remove safeDrawingPadding 2024-10-15 12:12:19 +08:00
db25ab4e5f Feat(App): Support sliding down the notification bar to exit full screen 2024-10-15 12:01:30 +08:00
e855a414a4 Feat(App): Support full screen 2024-10-14 18:07:03 +08:00
2823788765 Feat(ToolView): Support alert, confirm and prompt 2024-10-14 16:49:10 +08:00
fc4baca21d Build(gradle): Upgrade dependencies 2024-10-14 16:48:06 +08:00
e188d743f0 Refactor(.gitignore): Ignore key store 2024-10-12 15:59:37 +08:00
f7dd806885 Refactor(FileUtils): Optimize code 2024-10-12 15:59:07 +08:00
326e777b7f Refactor(TopAppBar): Optimize size of TopAppBar 2024-10-12 14:48:52 +08:00
0eb0667d2a Perf(ToolView): Optimize webview loading performance 2024-10-12 11:55:15 +08:00
cd97f6156f Refactor(minSdk): Upgrade minSdk from 21 to 24 2024-10-12 09:49:39 +08:00
3979027471 Refactor(NativeWebApi): Change data transfer type from ByteArray to Base64 String 2024-10-11 09:42:43 +08:00
d3f3fba413 Feat(ToolView): Support filltering file types 2024-10-11 09:42:40 +08:00
7b77069744 Refactor(SplashScreen): Optimize the splash screen of lower version 2024-10-11 09:42:35 +08:00
585a261bb8 Feat(ToolView): Support upload and download file 2024-09-30 15:06:28 +08:00
2631c22e52 Refactor(BottomAppBar): Adapt virtual button navigation bar 2024-09-30 12:09:43 +08:00
af3edb1d30 Refactor(ToolViewScreenViewModel): Save tool detail in cache 2024-09-29 15:52:12 +08:00
ba72ef2759 Refactor(Any): Optimize codes 2024-09-27 09:21:13 +08:00
2639d88492 Refactor(ClickableText): Replace ClickableText with Text 2024-09-26 18:44:59 +08:00
04812cf56b Build(gradle): Upgrade dependencies 2024-09-26 18:44:03 +08:00
112e30804e Refactor(Icon): Update APP icon and add startup animation 2024-09-24 17:55:14 +08:00
7e5554cfcd Refactor(Gradle): Optimize build script
Optimize version number rule. Optimize generated file name.
2024-09-02 16:36:46 +08:00
7e91f267e3 Refactor(AboutScreen): Optimize version info 2024-09-02 16:35:11 +08:00
21bb9a729f Build(Gradle): Auto sign release package 2024-09-01 23:02:07 +08:00
3b4b6b4e8e Refactor(ToolCard): Optimize text align 2024-08-28 18:11:08 +08:00
253d186fdf Refactor(ToolStore): Optimize user experience
Added prompts for errors when loading more and errors when reloading. Add autoload next page.
2024-08-28 15:14:42 +08:00
cead8fe91e Build(Gradle): Upgrade gradle from 8.9 to 8.10 2024-08-28 11:22:11 +08:00
c602ce0726 Refactor(ToolsScreen and ToolStarScreen): Remove showing indicator when loading 2024-08-27 18:37:01 +08:00
4dfb500370 Build(Gradle): Upgrade dependencies 2024-08-27 17:29:38 +08:00
93b22ea14b Feat(ToolsScreen and ToolStarScreen): Show skeleton when loading tool 2024-08-27 17:24:27 +08:00
c9363ee34b Refactor(ToolStore): Save storeDate state when switch navigation 2024-08-26 15:46:04 +08:00
e9232631de Feat(ToolStore): Support pull down to refresh 2024-08-26 14:55:04 +08:00
402965503f Refactor(Indicator): Optimize indicator 2024-08-26 14:50:34 +08:00
7653028241 Refactor(Scroll): Optimize scroll 2024-08-26 14:35:13 +08:00
f0ef28bd19 Refactor(ToolStore): Optimize the prompt when there is nothing 2024-08-21 18:03:10 +08:00
e0485ecc32 Feat(ToolView): Add native api
Add native api to support clipboard operations
2024-08-21 16:52:24 +08:00
893131fe02 Refactor(ToolView): Automatically raise the view when opening the keyboard 2024-08-21 12:07:16 +08:00
0e5b6d7f98 Refactor(LibrariesScreen): Optimize prompts when there is no content or no content found 2024-08-21 11:06:19 +08:00
310214fc20 Refactor(Animation): Optimize animation 2024-08-21 10:54:54 +08:00
18c03c194b Style(Code): Optimize code style 2024-08-20 18:24:27 +08:00
b115d3d598 Fix(TopAppBar): Fix the bug that the TopAppBar can be scrolled and hidden when the search box is opened 2024-08-20 16:54:36 +08:00
da4c58b631 Feat(ToolStarScreen): Support star tool 2024-08-20 15:32:09 +08:00
086b588445 Refactor(ToolViewScreenViewModel): Remove unused import directive 2024-08-20 13:49:17 +08:00
3f325126f1 Refactor(Background): Optimize background color 2024-08-20 13:42:25 +08:00
1b350eb22a Refactor(Code structure): Optimize code structure 2024-08-20 11:23:37 +08:00
167df010a9 Feat(ToolStore and Tools): Support query by tool name or keywords 2024-08-19 17:48:42 +08:00
47647217f1 Refactor(Navigation): Optimize navigation bar switching transition animation 2024-08-18 21:40:10 +08:00
5839f1d394 Refactor(ToolStore): Auto refresh status after install or upgrade tool 2024-08-18 14:46:27 +08:00
21264d8ff7 Refactor(ToolStorePagingSource): Optimize page key 2024-08-18 14:45:07 +08:00
0e592c1600 Refactor(LibrariesScreen): Optimize drawing padding 2024-08-18 13:47:04 +08:00
2f0c4b6a97 Refactor(LibrariesScreen): Optimize top app bar 2024-08-16 14:50:49 +08:00
4ba02420ed Feat(Logger): Support persistent logging 2024-08-16 14:39:36 +08:00
3a6fb2a6f0 Fix(Navigation): Fixed the bug that can not higlight store navigation icon
Fixed the bug that the store navigation icon cannot be automatically highlighted when switching to the ToolStore page.
2024-08-16 14:13:55 +08:00
0c6baba099 Fix(Tool): Fixed the bug of being unable to load stores and tools after compiling to release 2024-08-15 17:17:52 +08:00
35a421472d Refactor(ToolView): Optimize preview code logic 2024-08-14 17:18:18 +08:00
6732eb6a22 Feat(ToolStore): Support upgrade tool 2024-08-14 15:26:10 +08:00
f5cfbd7296 Refactor(Enum): Change all caps to camel case 2024-08-14 15:23:56 +08:00
627a32c7a3 Build(Gradle): Upgrade dependencies 2024-08-14 15:14:58 +08:00
5246715d78 Refactor(ToolStore): Optimize the way to obtain installation status 2024-08-13 18:07:07 +08:00
60ffc569a5 Feat(ToolView): Add preview tooltip 2024-08-12 17:39:32 +08:00
a3800bfad6 Feat(ToolView): Support load tool offline 2024-08-12 16:01:19 +08:00
454108d871 Feat(ToolScreen): Support uninstall tool 2024-08-12 14:37:18 +08:00
cb6fe19033 Refactor(SettingDialog): Componentized settings page elements 2024-08-12 14:35:51 +08:00
3d69514a39 Build(Gradle): Upgrade dependencies 2024-08-12 14:32:17 +08:00
96159e5b80 Refactor(ToolStore): Optimize save install tool info 2024-08-09 16:49:26 +08:00
9b3b391fef Refactor(Function): Make non-essentially public methods private 2024-08-09 14:39:54 +08:00
3286ed2934 Refactor(ToolStore): Support dynamic display tool installation button 2024-08-09 13:49:53 +08:00
c9c0debb2b Feat(Tool): Add tool store
Add tool store, support install tool online.
2024-08-08 17:56:45 +08:00
c1879dfdc8 Style(LibrariesScreenViewModel): Reformat code 2024-08-08 17:51:38 +08:00
640686296e Refactor(Context): Provide context using @ApplicationContext 2024-08-08 10:30:23 +08:00
a3b1241fca Refactor(AppScrollbars): Reduce scroll bar width 2024-08-08 10:26:48 +08:00
596ad2ccbe Refactor(Navigation): Remove handleOnCanScrollChange 2024-08-08 10:24:29 +08:00
1607897fc9 Build(Gradle): Upgrade dependencies 2024-08-06 11:41:45 +08:00
9d6094173d Build(Gradle): Upgrade dependencies 2024-07-17 17:28:06 +08:00
5efbf660c6 Refactor(OxygenApp): Optimize app bar
Optimize app bar when scroll
2024-07-17 17:27:11 +08:00
8fedafd261 Refactor(LibrariesScreen): Optimize app bar
Optimize app bar when scroll
2024-07-17 17:26:11 +08:00
1d4f317bb5 Refactor(AboutScreen): Optimize app bar
Optimize app bar when scroll
2024-07-17 17:24:52 +08:00
a85e561863 Fix(Tool): Fixed the bug of getting tool exception when tool desc is null 2024-07-17 15:37:02 +08:00
d84427b039 Refactor(ToolViewScreen): Optimize layout 2024-05-19 17:31:51 +08:00
3338522d40 Feat(Tool): Launch tool by scheme 2024-05-11 19:00:41 +08:00
3a91e834b7 Feat(ToolView): Add ToolView to support execute tool 2024-05-11 16:28:11 +08:00
3d8bc944e3 Feat(ToolScreen): Finish tool store list 2024-05-11 03:26:02 +08:00
c596767c37 Feat(ToolScreen): Support get tool online
Support get tool online in page
2024-05-10 05:47:30 +08:00
b2cbea5383 Build(Libs): Update dependencies 2024-05-07 09:08:42 +08:00
2e0efd1cb9 Refactor(LibrariesScreen): Add search
Add search and optimize top bar
2024-04-25 15:33:38 +08:00
61d229b100 Refactor(TopBar): Optimize TopBar
Optimize TopBar in AboutScreen and LibrariesScreen
2024-04-25 11:18:11 +08:00
32d19ae291 Feat(LibrariesScreen): Finish LibrariesScreen
Implement open source license display in LibrariesScreen
2024-04-24 17:31:06 +08:00
c8f072c930 Refactor(SettingDialog): Optimize confirmButton
Change Text to TextButton
2024-04-24 17:28:38 +08:00
4d047247f1 Refactor(Animation): Optimize navigation switch animation
Optimize AboutNavigation and LibrariesNavigation switch animation
2024-04-24 17:23:28 +08:00
b11ae055c3 Refactor(Preview): Change all preview to OxygenPreviews
Change all preview to OxygenPreviews. Fix can not preview OxygenTopAppBar bug.
2024-04-24 17:20:55 +08:00
b2e7ecc92c Refactor(Kotlin): Upgrade kotlin version
Upgrade kotlin version from 1.9.22 to 1.9.23
2024-04-24 17:15:25 +08:00
23893a4ac1 Refactor(Data): Rename datastore into data
Rename top.fatweb.oxygen.toolbox.datastore into top.fatweb.oxygen.toolbox.data
2024-04-24 17:13:39 +08:00
54a7625c1b Refactor(ToolBar): Optimize top bar and bottom bar
Completely hide the top bar and bottom bar when scrolling
2024-04-23 15:04:00 +08:00
8ed9d6942a Refactor(Libraries): Upgrade libraries
agp: 8.3.1 -> 8.3.2
composeBom: 2024.03.00 -> 2024.04.01
coreKtx: 1.12.0 -> 1.13.0
activityCompose: 1.8.2 -> 1.9.0
coil: 2.5.0 -> 2.6.0
androidxDataStore: 1.0.0 -> 1.1.0
2024-04-23 10:57:00 +08:00
92cd20f36f Feat(About): Add about page
Add about page, add libraries page framework. Add page switching animation and navigation bar animation.
2024-04-03 18:12:00 +08:00
8b200d14c6 Refactor(Drawable): Optimize drawable
Optimize drawable resources
2024-04-03 18:09:38 +08:00
58117cc6f6 Refactor(Icons): Add icons
Add icons and optimize sorting
2024-04-03 18:08:48 +08:00
e777f832e6 Feat(UI): Automatically hide app bar
Automatically hide top and bottom app bars when page scrolls
2024-04-02 10:22:51 +08:00
0e24b46525 Feat(ToolGroupCard): Automatically manage expansion and hiding of cards
Automatically manage the expansion and hiding of cards when clicking on the card title
2024-04-02 10:22:31 +08:00
cba2e83074 Build(Release): Fix R8 minify
Fix application cannot start after enabling minify
2024-04-01 17:28:31 +08:00
cda1f455b9 Build(libraries): Update composeBom
Update composeBom from '2024.02.02' to '2024.03.00'
2024-04-01 17:22:13 +08:00
96cc7c221f Optimize - ToolGroupCard - performance optimization 2024-03-31 23:36:07 +08:00
f81f26a5cb Feat - ToolsScreen - add tool group list 2024-03-29 18:09:33 +08:00
4cc1c0f68b Optimize - NavigationBarStyle - remove navigation bar white background 2024-03-26 18:52:18 +08:00
1bd81cdf6c Optimize - agp - upgrade version 2024-03-26 18:50:53 +08:00
188 changed files with 9717 additions and 1196 deletions

6
.gitignore vendored
View File

@@ -17,6 +17,10 @@ build/
# Local configuration file (sdk path, etc)
local.properties
# Key Store
keystore.properties
*.jks
# Eclipse project files
.classpath
.project
@@ -43,3 +47,5 @@ _sandbox
# Android Studio captures folder
captures/
.kotlin/

123
.idea/codeStyles/Project.xml generated Normal file
View 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
View File

@@ -0,0 +1,5 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="USE_PER_PROJECT_SETTINGS" value="true" />
</state>
</component>

3
app/.gitignore vendored
View File

@@ -1,2 +1,3 @@
/build
/src/main/res/raw/dependencies.json
/src/main/res/raw/dependencies.json
/release/

View File

@@ -1,33 +1,72 @@
import com.android.build.gradle.internal.api.BaseVariantOutputImpl
import com.mikepenz.aboutlibraries.plugin.AboutLibrariesTask
import java.io.FileInputStream
import java.time.LocalDateTime
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter
import java.util.Properties
val localDateTime: LocalDateTime = LocalDateTime.now(ZoneOffset.UTC)
val baseVersionCode = 1
val baseVersionName = "0.0.0"
val keystoreProperties = rootProject.file("keystore.properties").run {
if (!exists()) {
null
} else {
Properties().apply {
load(FileInputStream(this@run))
}
}
}
plugins {
alias(libs.plugins.androidApplication)
alias(libs.plugins.jetbrainsKotlinAndroid)
alias(libs.plugins.compose.compiler)
alias(libs.plugins.ksp)
alias(libs.plugins.aboutlibraries)
alias(libs.plugins.hilt)
alias(libs.plugins.protobuf)
alias(libs.plugins.kotlinxSerialization)
alias(libs.plugins.secrets)
alias(libs.plugins.room)
alias(libs.plugins.parcelize)
}
android {
namespace = "top.fatweb.oxygen.toolbox"
compileSdk = 34
compileSdk = 35
defaultConfig {
applicationId = "top.fatweb.oxygen.toolbox"
minSdk = 21
targetSdk = 34
versionCode = 1
versionName = "1.0.0-SNAPSHOT"
minSdk = 24
targetSdk = 35
versionCode = baseVersionCode
versionName = "$baseVersionName${
if (baseVersionCode % 100 != 0) ".${
localDateTime.format(
DateTimeFormatter.ofPattern("yyMMdd")
)
}" else ""
}"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
vectorDrawables {
useSupportLibrary = true
}
// Required when setting minSdkVersion to 20 or lower
multiDexEnabled = true
setProperty("archivesBaseName", "$applicationId-v$versionName($versionCode)")
}
signingConfigs {
keystoreProperties?.let {
create("release") {
storeFile = rootProject.file(it["storeFile"] as String)
storePassword = it["storePassword"] as String
keyAlias = it["keyAlias"] as String
keyPassword = it["keyPassword"] as String
}
}
}
buildTypes {
@@ -38,13 +77,7 @@ android {
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
android.applicationVariants.all {
outputs.all {
(this as BaseVariantOutputImpl).outputFileName =
"${project.name}_${defaultConfig.versionName}-${defaultConfig.versionCode}_${buildType.name}.apk"
signingConfig = signingConfigs.findByName("release")
}
}
@@ -60,9 +93,7 @@ android {
}
buildFeatures {
compose = true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.5.10"
buildConfig = true
}
packaging {
resources {
@@ -101,11 +132,7 @@ aboutLibraries {
registerAndroidTasks = false
configPath = "libConfig"
outputFileName = "dependencies.json"
exclusionPatterns = listOf(
Regex("androidx.*"),
Regex("org.jetbrains.*"),
Regex("com.google.guava:listenablefuture")
).map { it.toPattern() }
exclusionPatterns = listOf<Regex>().map { it.toPattern() }
}
task("exportLibrariesToJson", AboutLibrariesTask::class) {
@@ -116,12 +143,26 @@ task("exportLibrariesToJson", AboutLibrariesTask::class) {
afterEvaluate {
tasks.findByName("preBuild")?.dependsOn(tasks.findByName("exportLibrariesToJson"))
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 {
coreLibraryDesugaring(libs.desugar.jdk.libs)
testImplementation(libs.junit)
testImplementation(libs.paging.common)
androidTestImplementation(platform(libs.androidx.compose.bom))
androidTestImplementation(libs.androidx.ui.test.junit4)
@@ -142,6 +183,7 @@ dependencies {
implementation(libs.material.icons.core)
implementation(libs.material.icons.extended)
implementation(libs.material3.window.size)
implementation(libs.animation.graphics)
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.activity.compose)
implementation(libs.androidx.appcompat)
@@ -160,4 +202,17 @@ dependencies {
implementation(libs.protobuf.kotlin.lite)
implementation(libs.androidx.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)
}

View File

@@ -12,10 +12,23 @@
# 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
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
#-renamesourcefileattribute SourceFile
-dontwarn kotlinx.serialization.KSerializer
-dontwarn kotlinx.serialization.Serializable

View File

@@ -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')"
]
}
}

View File

@@ -3,6 +3,10 @@
xmlns:tools="http://schemas.android.com/tools">
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
android:maxSdkVersion="32"
tools:ignore="ScopedStorage" />
<application
android:name=".OxygenApplication"
@@ -27,6 +31,13 @@
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</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>
</application>

View File

@@ -0,0 +1,365 @@
:root {
--blue: #1677FF;
--blue1: #111a2c;
--blue2: #112545;
--blue3: #15325b;
--blue4: #15417e;
--blue5: #1554ad;
--blue6: #1668dc;
--blue7: #3c89e8;
--blue8: #65a9f3;
--blue9: #8dc5f8;
--blue10: #b7dcfa;
--purple: #722ED1;
--purple1: #1a1325;
--purple2: #24163a;
--purple3: #301c4d;
--purple4: #3e2069;
--purple5: #51258f;
--purple6: #642ab5;
--purple7: #854eca;
--purple8: #ab7ae0;
--purple9: #cda8f0;
--purple10: #ebd7fa;
--cyan: #13C2C2;
--cyan1: #112123;
--cyan2: #113536;
--cyan3: #144848;
--cyan4: #146262;
--cyan5: #138585;
--cyan6: #13a8a8;
--cyan7: #33bcb7;
--cyan8: #58d1c9;
--cyan9: #84e2d8;
--cyan10: #b2f1e8;
--green: #52C41A;
--green1: #162312;
--green2: #1d3712;
--green3: #274916;
--green4: #306317;
--green5: #3c8618;
--green6: #49aa19;
--green7: #6abe39;
--green8: #8fd460;
--green9: #b2e58b;
--green10: #d5f2bb;
--magenta: #EB2F96;
--magenta1: #291321;
--magenta2: #40162f;
--magenta3: #551c3b;
--magenta4: #75204f;
--magenta5: #a02669;
--magenta6: #cb2b83;
--magenta7: #e0529c;
--magenta8: #f37fb7;
--magenta9: #f8a8cc;
--magenta10: #fad2e3;
--pink: #EB2F96;
--pink1: #291321;
--pink2: #40162f;
--pink3: #551c3b;
--pink4: #75204f;
--pink5: #a02669;
--pink6: #cb2b83;
--pink7: #e0529c;
--pink8: #f37fb7;
--pink9: #f8a8cc;
--pink10: #fad2e3;
--red: #F5222D;
--red1: #2a1215;
--red2: #431418;
--red3: #58181c;
--red4: #791a1f;
--red5: #a61d24;
--red6: #d32029;
--red7: #e84749;
--red8: #f37370;
--red9: #f89f9a;
--red10: #fac8c3;
--orange: #FA8C16;
--orange1: #2b1d11;
--orange2: #442a11;
--orange3: #593815;
--orange4: #7c4a15;
--orange5: #aa6215;
--orange6: #d87a16;
--orange7: #e89a3c;
--orange8: #f3b765;
--orange9: #f8cf8d;
--orange10: #fae3b7;
--yellow: #FADB14;
--yellow1: #2b2611;
--yellow2: #443b11;
--yellow3: #595014;
--yellow4: #7c6e14;
--yellow5: #aa9514;
--yellow6: #d8bd14;
--yellow7: #e8d639;
--yellow8: #f3ea62;
--yellow9: #f8f48b;
--yellow10: #fafab5;
--volcano: #FA541C;
--volcano1: #2b1611;
--volcano2: #441d12;
--volcano3: #592716;
--volcano4: #7c3118;
--volcano5: #aa3e19;
--volcano6: #d84a1b;
--volcano7: #e87040;
--volcano8: #f3956a;
--volcano9: #f8b692;
--volcano10: #fad4bc;
--geekblue: #2F54EB;
--geekblue1: #131629;
--geekblue2: #161d40;
--geekblue3: #1c2755;
--geekblue4: #203175;
--geekblue5: #263ea0;
--geekblue6: #2b4acb;
--geekblue7: #5273e0;
--geekblue8: #7f9ef3;
--geekblue9: #a8c1f8;
--geekblue10: #d2e0fa;
--gold: #FAAD14;
--gold1: #2b2111;
--gold2: #443111;
--gold3: #594214;
--gold4: #7c5914;
--gold5: #aa7714;
--gold6: #d89614;
--gold7: #e8b339;
--gold8: #f3cc62;
--gold9: #f8df8b;
--gold10: #faedb5;
--lime: #A0D911;
--lime1: #1f2611;
--lime2: #2e3c10;
--lime3: #3e4f13;
--lime4: #536d13;
--lime5: #6f9412;
--lime6: #8bbb11;
--lime7: #a9d134;
--lime8: #c9e75d;
--lime9: #e4f88b;
--lime10: #f0fab5;
--colorPrimary: #453fa2;
--colorSuccess: #49aa19;
--colorWarning: #d89614;
--colorError: #dc4446;
--colorInfo: #1668dc;
--colorLink: #1668dc;
--colorTextBase: #fff;
--colorBgBase: #000;
--fontFamily: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial,
'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
'Noto Color Emoji';
--fontFamilyCode: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace;
--fontSize: 14;
--lineWidth: 1;
--lineType: solid;
--motionUnit: 0.1;
--motionBase: 0;
--motionEaseOutCirc: cubic-bezier(0.08, 0.82, 0.17, 1);
--motionEaseInOutCirc: cubic-bezier(0.78, 0.14, 0.15, 0.86);
--motionEaseOut: cubic-bezier(0.215, 0.61, 0.355, 1);
--motionEaseInOut: cubic-bezier(0.645, 0.045, 0.355, 1);
--motionEaseOutBack: cubic-bezier(0.12, 0.4, 0.29, 1.46);
--motionEaseInBack: cubic-bezier(0.71, -0.46, 0.88, 0.6);
--motionEaseInQuint: cubic-bezier(0.755, 0.05, 0.855, 0.06);
--motionEaseOutQuint: cubic-bezier(0.23, 1, 0.32, 1);
--borderRadius: 6;
--sizeUnit: 4;
--sizeStep: 4;
--sizePopupArrow: 16;
--controlHeight: 32;
--zIndexBase: 0;
--zIndexPopupBase: 1000;
--opacityImage: 1;
--colorLinkHover: #4E47BB;
--colorText: rgba(255, 255, 255, 0.85);
--colorTextSecondary: rgba(255, 255, 255, 0.65);
--colorTextTertiary: rgba(255, 255, 255, 0.45);
--colorTextQuaternary: rgba(255, 255, 255, 0.25);
--colorFill: rgba(255, 255, 255, 0.18);
--colorFillSecondary: rgba(255, 255, 255, 0.12);
--colorFillTertiary: rgba(255, 255, 255, 0.08);
--colorFillQuaternary: rgba(255, 255, 255, 0.04);
--colorBgSolid: rgba(255, 255, 255, 0.95);
--colorBgSolidHover: rgb(255, 255, 255);
--colorBgSolidActive: rgba(255, 255, 255, 0.9);
--colorBgLayout: #000000;
--colorBgContainer: #141414;
--colorBgElevated: #1f1f1f;
--colorBgSpotlight: #424242;
--colorBgBlur: rgba(255, 255, 255, 0.04);
--colorBorder: #424242;
--colorBorderSecondary: #303030;
--colorPrimaryBg: #161622;
--colorPrimaryBgHover: #1c1b34;
--colorPrimaryBorder: #252346;
--colorPrimaryBorderHover: #2e2b5f;
--colorPrimaryHover: #6b62b5;
--colorPrimaryActive: #3a3581;
--colorPrimaryTextHover: #6b62b5;
--colorPrimaryText: #453fa2;
--colorPrimaryTextActive: #3a3581;
--colorSuccessBg: #162312;
--colorSuccessBgHover: #1d3712;
--colorSuccessBorder: #274916;
--colorSuccessBorderHover: #306317;
--colorSuccessHover: #306317;
--colorSuccessActive: #3c8618;
--colorSuccessTextHover: #6abe39;
--colorSuccessText: #49aa19;
--colorSuccessTextActive: #3c8618;
--colorErrorBg: #2c1618;
--colorErrorBgHover: #451d1f;
--colorErrorBgFilledHover: #441e1f;
--colorErrorBgActive: #5b2526;
--colorErrorBorder: #5b2526;
--colorErrorBorderHover: #7e2e2f;
--colorErrorHover: #e86e6b;
--colorErrorActive: #ad393a;
--colorErrorTextHover: #e86e6b;
--colorErrorText: #dc4446;
--colorErrorTextActive: #ad393a;
--colorWarningBg: #2b2111;
--colorWarningBgHover: #443111;
--colorWarningBorder: #594214;
--colorWarningBorderHover: #7c5914;
--colorWarningHover: #7c5914;
--colorWarningActive: #aa7714;
--colorWarningTextHover: #e8b339;
--colorWarningText: #d89614;
--colorWarningTextActive: #aa7714;
--colorInfoBg: #111a2c;
--colorInfoBgHover: #112545;
--colorInfoBorder: #15325b;
--colorInfoBorderHover: #15417e;
--colorInfoHover: #15417e;
--colorInfoActive: #1554ad;
--colorInfoTextHover: #3c89e8;
--colorInfoText: #1668dc;
--colorInfoTextActive: #1554ad;
--colorLinkActive: #1554ad;
--colorBgMask: rgba(0, 0, 0, 0.45);
--colorWhite: #fff;
--fontSizeSM: 12;
--fontSizeLG: 16;
--fontSizeXL: 20;
--fontSizeHeading1: 38;
--fontSizeHeading2: 30;
--fontSizeHeading3: 24;
--fontSizeHeading4: 20;
--fontSizeHeading5: 16;
--lineHeight: 1.5714285714285714;
--lineHeightLG: 1.5;
--lineHeightSM: 1.6666666666666667;
--lineHeightHeading1: 1.2105263157894737;
--lineHeightHeading2: 1.2666666666666666;
--lineHeightHeading3: 1.3333333333333333;
--lineHeightHeading4: 1.4;
--lineHeightHeading5: 1.5;
--sizeXXL: 48;
--sizeXL: 32;
--sizeLG: 24;
--sizeMD: 20;
--sizeMS: 16;
--size: 16;
--sizeSM: 12;
--sizeXS: 8;
--sizeXXS: 4;
--controlHeightSM: 24;
--controlHeightXS: 16;
--controlHeightLG: 40;
--motionDurationFast: 0.1s;
--motionDurationMid: 0.2s;
--motionDurationSlow: 0.3s;
--lineWidthBold: 2;
--borderRadiusXS: 2;
--borderRadiusSM: 4;
--borderRadiusLG: 8;
--borderRadiusOuter: 4;
--colorFillContent: rgba(255, 255, 255, 0.12);
--colorFillContentHover: rgba(255, 255, 255, 0.18);
--colorFillAlter: rgba(255, 255, 255, 0.04);
--colorBgContainerDisabled: rgba(255, 255, 255, 0.08);
--colorBorderBg: #141414;
--colorSplit: rgba(253, 253, 253, 0.12);
--colorTextPlaceholder: rgba(255, 255, 255, 0.25);
--colorTextDisabled: rgba(255, 255, 255, 0.25);
--colorTextHeading: rgba(255, 255, 255, 0.85);
--colorTextLabel: rgba(255, 255, 255, 0.65);
--colorTextDescription: rgba(255, 255, 255, 0.45);
--colorTextLightSolid: #fff;
--colorHighlight: #dc4446;
--colorBgTextHover: rgba(255, 255, 255, 0.12);
--colorBgTextActive: rgba(255, 255, 255, 0.18);
--colorIcon: rgba(255, 255, 255, 0.45);
--colorIconHover: rgba(255, 255, 255, 0.85);
--colorErrorOutline: rgba(238, 38, 56, 0.11);
--colorWarningOutline: rgba(173, 107, 0, 0.15);
--fontSizeIcon: 12;
--lineWidthFocus: 3;
--controlOutlineWidth: 2;
--controlInteractiveSize: 16;
--controlItemBgHover: rgba(255, 255, 255, 0.08);
--controlItemBgActive: #161622;
--controlItemBgActiveHover: #1c1b34;
--controlItemBgActiveDisabled: rgba(255, 255, 255, 0.18);
--controlOutline: rgba(53, 53, 253, 0.06);
--fontWeightStrong: 600;
--opacityLoading: 0.65;
--linkDecoration: none;
--linkHoverDecoration: none;
--linkFocusDecoration: none;
--controlPaddingHorizontal: 12;
--controlPaddingHorizontalSM: 8;
--paddingXXS: 4;
--paddingXS: 8;
--paddingSM: 12;
--padding: 16;
--paddingMD: 20;
--paddingLG: 24;
--paddingXL: 32;
--paddingContentHorizontalLG: 24;
--paddingContentVerticalLG: 16;
--paddingContentHorizontal: 16;
--paddingContentVertical: 12;
--paddingContentHorizontalSM: 16;
--paddingContentVerticalSM: 8;
--marginXXS: 4;
--marginXS: 8;
--marginSM: 12;
--margin: 16;
--marginMD: 20;
--marginLG: 24;
--marginXL: 32;
--marginXXL: 48;
--boxShadow: 0 6px 16px 0 rgba(0, 0, 0, 0.08),
0 3px 6px -4px rgba(0, 0, 0, 0.12),
0 9px 28px 8px rgba(0, 0, 0, 0.05);
--boxShadowSecondary: 0 6px 16px 0 rgba(0, 0, 0, 0.08),
0 3px 6px -4px rgba(0, 0, 0, 0.12),
0 9px 28px 8px rgba(0, 0, 0, 0.05);
--boxShadowTertiary: 0 1px 2px 0 rgba(0, 0, 0, 0.03),
0 1px 6px -1px rgba(0, 0, 0, 0.02),
0 2px 4px 0 rgba(0, 0, 0, 0.02);
--screenXS: 480;
--screenXSMin: 480;
--screenXSMax: 575;
--screenSM: 576;
--screenSMMin: 576;
--screenSMMax: 767;
--screenMD: 768;
--screenMDMin: 768;
--screenMDMax: 991;
--screenLG: 992;
--screenLGMin: 992;
--screenLGMax: 1199;
--screenXL: 1200;
--screenXLMin: 1200;
--screenXLMax: 1599;
--screenXXL: 1600;
--screenXXLMin: 1600;
}

View File

@@ -0,0 +1,517 @@
const globalVariables = {
OxygenTheme: {
blue: '#1677FF',
purple: '#722ED1',
cyan: '#13C2C2',
green: '#52C41A',
magenta: '#EB2F96',
pink: '#EB2F96',
red: '#F5222D',
orange: '#FA8C16',
yellow: '#FADB14',
volcano: '#FA541C',
geekblue: '#2F54EB',
gold: '#FAAD14',
lime: '#A0D911',
colorPrimary: '#453fa2',
colorSuccess: '#49aa19',
colorWarning: '#d89614',
colorError: '#dc4446',
colorInfo: '#1668dc',
colorLink: '#1668dc',
colorTextBase: '#fff',
colorBgBase: '#000',
fontFamily:
"-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial,\n'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',\n'Noto Color Emoji'",
fontFamilyCode: "'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace",
fontSize: 14,
lineWidth: 1,
lineType: 'solid',
motionUnit: 0.1,
motionBase: 0,
motionEaseOutCirc: 'cubic-bezier(0.08, 0.82, 0.17, 1)',
motionEaseInOutCirc: 'cubic-bezier(0.78, 0.14, 0.15, 0.86)',
motionEaseOut: 'cubic-bezier(0.215, 0.61, 0.355, 1)',
motionEaseInOut: 'cubic-bezier(0.645, 0.045, 0.355, 1)',
motionEaseOutBack: 'cubic-bezier(0.12, 0.4, 0.29, 1.46)',
motionEaseInBack: 'cubic-bezier(0.71, -0.46, 0.88, 0.6)',
motionEaseInQuint: 'cubic-bezier(0.755, 0.05, 0.855, 0.06)',
motionEaseOutQuint: 'cubic-bezier(0.23, 1, 0.32, 1)',
borderRadius: 6,
sizeUnit: 4,
sizeStep: 4,
sizePopupArrow: 16,
controlHeight: 32,
zIndexBase: 0,
zIndexPopupBase: 1000,
opacityImage: 1,
motion: true,
colorLinkHover: '#4E47BB',
'blue-1': '#111a2c',
blue1: '#111a2c',
'blue-2': '#112545',
blue2: '#112545',
'blue-3': '#15325b',
blue3: '#15325b',
'blue-4': '#15417e',
blue4: '#15417e',
'blue-5': '#1554ad',
blue5: '#1554ad',
'blue-6': '#1668dc',
blue6: '#1668dc',
'blue-7': '#3c89e8',
blue7: '#3c89e8',
'blue-8': '#65a9f3',
blue8: '#65a9f3',
'blue-9': '#8dc5f8',
blue9: '#8dc5f8',
'blue-10': '#b7dcfa',
blue10: '#b7dcfa',
'purple-1': '#1a1325',
purple1: '#1a1325',
'purple-2': '#24163a',
purple2: '#24163a',
'purple-3': '#301c4d',
purple3: '#301c4d',
'purple-4': '#3e2069',
purple4: '#3e2069',
'purple-5': '#51258f',
purple5: '#51258f',
'purple-6': '#642ab5',
purple6: '#642ab5',
'purple-7': '#854eca',
purple7: '#854eca',
'purple-8': '#ab7ae0',
purple8: '#ab7ae0',
'purple-9': '#cda8f0',
purple9: '#cda8f0',
'purple-10': '#ebd7fa',
purple10: '#ebd7fa',
'cyan-1': '#112123',
cyan1: '#112123',
'cyan-2': '#113536',
cyan2: '#113536',
'cyan-3': '#144848',
cyan3: '#144848',
'cyan-4': '#146262',
cyan4: '#146262',
'cyan-5': '#138585',
cyan5: '#138585',
'cyan-6': '#13a8a8',
cyan6: '#13a8a8',
'cyan-7': '#33bcb7',
cyan7: '#33bcb7',
'cyan-8': '#58d1c9',
cyan8: '#58d1c9',
'cyan-9': '#84e2d8',
cyan9: '#84e2d8',
'cyan-10': '#b2f1e8',
cyan10: '#b2f1e8',
'green-1': '#162312',
green1: '#162312',
'green-2': '#1d3712',
green2: '#1d3712',
'green-3': '#274916',
green3: '#274916',
'green-4': '#306317',
green4: '#306317',
'green-5': '#3c8618',
green5: '#3c8618',
'green-6': '#49aa19',
green6: '#49aa19',
'green-7': '#6abe39',
green7: '#6abe39',
'green-8': '#8fd460',
green8: '#8fd460',
'green-9': '#b2e58b',
green9: '#b2e58b',
'green-10': '#d5f2bb',
green10: '#d5f2bb',
'magenta-1': '#291321',
magenta1: '#291321',
'magenta-2': '#40162f',
magenta2: '#40162f',
'magenta-3': '#551c3b',
magenta3: '#551c3b',
'magenta-4': '#75204f',
magenta4: '#75204f',
'magenta-5': '#a02669',
magenta5: '#a02669',
'magenta-6': '#cb2b83',
magenta6: '#cb2b83',
'magenta-7': '#e0529c',
magenta7: '#e0529c',
'magenta-8': '#f37fb7',
magenta8: '#f37fb7',
'magenta-9': '#f8a8cc',
magenta9: '#f8a8cc',
'magenta-10': '#fad2e3',
magenta10: '#fad2e3',
'pink-1': '#291321',
pink1: '#291321',
'pink-2': '#40162f',
pink2: '#40162f',
'pink-3': '#551c3b',
pink3: '#551c3b',
'pink-4': '#75204f',
pink4: '#75204f',
'pink-5': '#a02669',
pink5: '#a02669',
'pink-6': '#cb2b83',
pink6: '#cb2b83',
'pink-7': '#e0529c',
pink7: '#e0529c',
'pink-8': '#f37fb7',
pink8: '#f37fb7',
'pink-9': '#f8a8cc',
pink9: '#f8a8cc',
'pink-10': '#fad2e3',
pink10: '#fad2e3',
'red-1': '#2a1215',
red1: '#2a1215',
'red-2': '#431418',
red2: '#431418',
'red-3': '#58181c',
red3: '#58181c',
'red-4': '#791a1f',
red4: '#791a1f',
'red-5': '#a61d24',
red5: '#a61d24',
'red-6': '#d32029',
red6: '#d32029',
'red-7': '#e84749',
red7: '#e84749',
'red-8': '#f37370',
red8: '#f37370',
'red-9': '#f89f9a',
red9: '#f89f9a',
'red-10': '#fac8c3',
red10: '#fac8c3',
'orange-1': '#2b1d11',
orange1: '#2b1d11',
'orange-2': '#442a11',
orange2: '#442a11',
'orange-3': '#593815',
orange3: '#593815',
'orange-4': '#7c4a15',
orange4: '#7c4a15',
'orange-5': '#aa6215',
orange5: '#aa6215',
'orange-6': '#d87a16',
orange6: '#d87a16',
'orange-7': '#e89a3c',
orange7: '#e89a3c',
'orange-8': '#f3b765',
orange8: '#f3b765',
'orange-9': '#f8cf8d',
orange9: '#f8cf8d',
'orange-10': '#fae3b7',
orange10: '#fae3b7',
'yellow-1': '#2b2611',
yellow1: '#2b2611',
'yellow-2': '#443b11',
yellow2: '#443b11',
'yellow-3': '#595014',
yellow3: '#595014',
'yellow-4': '#7c6e14',
yellow4: '#7c6e14',
'yellow-5': '#aa9514',
yellow5: '#aa9514',
'yellow-6': '#d8bd14',
yellow6: '#d8bd14',
'yellow-7': '#e8d639',
yellow7: '#e8d639',
'yellow-8': '#f3ea62',
yellow8: '#f3ea62',
'yellow-9': '#f8f48b',
yellow9: '#f8f48b',
'yellow-10': '#fafab5',
yellow10: '#fafab5',
'volcano-1': '#2b1611',
volcano1: '#2b1611',
'volcano-2': '#441d12',
volcano2: '#441d12',
'volcano-3': '#592716',
volcano3: '#592716',
'volcano-4': '#7c3118',
volcano4: '#7c3118',
'volcano-5': '#aa3e19',
volcano5: '#aa3e19',
'volcano-6': '#d84a1b',
volcano6: '#d84a1b',
'volcano-7': '#e87040',
volcano7: '#e87040',
'volcano-8': '#f3956a',
volcano8: '#f3956a',
'volcano-9': '#f8b692',
volcano9: '#f8b692',
'volcano-10': '#fad4bc',
volcano10: '#fad4bc',
'geekblue-1': '#131629',
geekblue1: '#131629',
'geekblue-2': '#161d40',
geekblue2: '#161d40',
'geekblue-3': '#1c2755',
geekblue3: '#1c2755',
'geekblue-4': '#203175',
geekblue4: '#203175',
'geekblue-5': '#263ea0',
geekblue5: '#263ea0',
'geekblue-6': '#2b4acb',
geekblue6: '#2b4acb',
'geekblue-7': '#5273e0',
geekblue7: '#5273e0',
'geekblue-8': '#7f9ef3',
geekblue8: '#7f9ef3',
'geekblue-9': '#a8c1f8',
geekblue9: '#a8c1f8',
'geekblue-10': '#d2e0fa',
geekblue10: '#d2e0fa',
'gold-1': '#2b2111',
gold1: '#2b2111',
'gold-2': '#443111',
gold2: '#443111',
'gold-3': '#594214',
gold3: '#594214',
'gold-4': '#7c5914',
gold4: '#7c5914',
'gold-5': '#aa7714',
gold5: '#aa7714',
'gold-6': '#d89614',
gold6: '#d89614',
'gold-7': '#e8b339',
gold7: '#e8b339',
'gold-8': '#f3cc62',
gold8: '#f3cc62',
'gold-9': '#f8df8b',
gold9: '#f8df8b',
'gold-10': '#faedb5',
gold10: '#faedb5',
'lime-1': '#1f2611',
lime1: '#1f2611',
'lime-2': '#2e3c10',
lime2: '#2e3c10',
'lime-3': '#3e4f13',
lime3: '#3e4f13',
'lime-4': '#536d13',
lime4: '#536d13',
'lime-5': '#6f9412',
lime5: '#6f9412',
'lime-6': '#8bbb11',
lime6: '#8bbb11',
'lime-7': '#a9d134',
lime7: '#a9d134',
'lime-8': '#c9e75d',
lime8: '#c9e75d',
'lime-9': '#e4f88b',
lime9: '#e4f88b',
'lime-10': '#f0fab5',
lime10: '#f0fab5',
colorText: 'rgba(255, 255, 255, 0.85)',
colorTextSecondary: 'rgba(255, 255, 255, 0.65)',
colorTextTertiary: 'rgba(255, 255, 255, 0.45)',
colorTextQuaternary: 'rgba(255, 255, 255, 0.25)',
colorFill: 'rgba(255, 255, 255, 0.18)',
colorFillSecondary: 'rgba(255, 255, 255, 0.12)',
colorFillTertiary: 'rgba(255, 255, 255, 0.08)',
colorFillQuaternary: 'rgba(255, 255, 255, 0.04)',
colorBgSolid: 'rgba(255, 255, 255, 0.95)',
colorBgSolidHover: 'rgb(255, 255, 255)',
colorBgSolidActive: 'rgba(255, 255, 255, 0.9)',
colorBgLayout: '#000000',
colorBgContainer: '#141414',
colorBgElevated: '#1f1f1f',
colorBgSpotlight: '#424242',
colorBgBlur: 'rgba(255, 255, 255, 0.04)',
colorBorder: '#424242',
colorBorderSecondary: '#303030',
colorPrimaryBg: '#161622',
colorPrimaryBgHover: '#1c1b34',
colorPrimaryBorder: '#252346',
colorPrimaryBorderHover: '#2e2b5f',
colorPrimaryHover: '#6b62b5',
colorPrimaryActive: '#3a3581',
colorPrimaryTextHover: '#6b62b5',
colorPrimaryText: '#453fa2',
colorPrimaryTextActive: '#3a3581',
colorSuccessBg: '#162312',
colorSuccessBgHover: '#1d3712',
colorSuccessBorder: '#274916',
colorSuccessBorderHover: '#306317',
colorSuccessHover: '#306317',
colorSuccessActive: '#3c8618',
colorSuccessTextHover: '#6abe39',
colorSuccessText: '#49aa19',
colorSuccessTextActive: '#3c8618',
colorErrorBg: '#2c1618',
colorErrorBgHover: '#451d1f',
colorErrorBgFilledHover: '#441e1f',
colorErrorBgActive: '#5b2526',
colorErrorBorder: '#5b2526',
colorErrorBorderHover: '#7e2e2f',
colorErrorHover: '#e86e6b',
colorErrorActive: '#ad393a',
colorErrorTextHover: '#e86e6b',
colorErrorText: '#dc4446',
colorErrorTextActive: '#ad393a',
colorWarningBg: '#2b2111',
colorWarningBgHover: '#443111',
colorWarningBorder: '#594214',
colorWarningBorderHover: '#7c5914',
colorWarningHover: '#7c5914',
colorWarningActive: '#aa7714',
colorWarningTextHover: '#e8b339',
colorWarningText: '#d89614',
colorWarningTextActive: '#aa7714',
colorInfoBg: '#111a2c',
colorInfoBgHover: '#112545',
colorInfoBorder: '#15325b',
colorInfoBorderHover: '#15417e',
colorInfoHover: '#15417e',
colorInfoActive: '#1554ad',
colorInfoTextHover: '#3c89e8',
colorInfoText: '#1668dc',
colorInfoTextActive: '#1554ad',
colorLinkActive: '#1554ad',
colorBgMask: 'rgba(0, 0, 0, 0.45)',
colorWhite: '#fff',
fontSizeSM: 12,
fontSizeLG: 16,
fontSizeXL: 20,
fontSizeHeading1: 38,
fontSizeHeading2: 30,
fontSizeHeading3: 24,
fontSizeHeading4: 20,
fontSizeHeading5: 16,
lineHeight: 1.5714285714285714,
lineHeightLG: 1.5,
lineHeightSM: 1.6666666666666667,
fontHeight: 22,
fontHeightLG: 24,
fontHeightSM: 20,
lineHeightHeading1: 1.2105263157894737,
lineHeightHeading2: 1.2666666666666666,
lineHeightHeading3: 1.3333333333333333,
lineHeightHeading4: 1.4,
lineHeightHeading5: 1.5,
sizeXXL: 48,
sizeXL: 32,
sizeLG: 24,
sizeMD: 20,
sizeMS: 16,
size: 16,
sizeSM: 12,
sizeXS: 8,
sizeXXS: 4,
controlHeightSM: 24,
controlHeightXS: 16,
controlHeightLG: 40,
motionDurationFast: '0.1s',
motionDurationMid: '0.2s',
motionDurationSlow: '0.3s',
lineWidthBold: 2,
borderRadiusXS: 2,
borderRadiusSM: 4,
borderRadiusLG: 8,
borderRadiusOuter: 4,
colorFillContent: 'rgba(255, 255, 255, 0.12)',
colorFillContentHover: 'rgba(255, 255, 255, 0.18)',
colorFillAlter: 'rgba(255, 255, 255, 0.04)',
colorBgContainerDisabled: 'rgba(255, 255, 255, 0.08)',
colorBorderBg: '#141414',
colorSplit: 'rgba(253, 253, 253, 0.12)',
colorTextPlaceholder: 'rgba(255, 255, 255, 0.25)',
colorTextDisabled: 'rgba(255, 255, 255, 0.25)',
colorTextHeading: 'rgba(255, 255, 255, 0.85)',
colorTextLabel: 'rgba(255, 255, 255, 0.65)',
colorTextDescription: 'rgba(255, 255, 255, 0.45)',
colorTextLightSolid: '#fff',
colorHighlight: '#dc4446',
colorBgTextHover: 'rgba(255, 255, 255, 0.12)',
colorBgTextActive: 'rgba(255, 255, 255, 0.18)',
colorIcon: 'rgba(255, 255, 255, 0.45)',
colorIconHover: 'rgba(255, 255, 255, 0.85)',
colorErrorOutline: 'rgba(238, 38, 56, 0.11)',
colorWarningOutline: 'rgba(173, 107, 0, 0.15)',
fontSizeIcon: 12,
lineWidthFocus: 3,
controlOutlineWidth: 2,
controlInteractiveSize: 16,
controlItemBgHover: 'rgba(255, 255, 255, 0.08)',
controlItemBgActive: '#161622',
controlItemBgActiveHover: '#1c1b34',
controlItemBgActiveDisabled: 'rgba(255, 255, 255, 0.18)',
controlTmpOutline: 'rgba(255, 255, 255, 0.04)',
controlOutline: 'rgba(53, 53, 253, 0.06)',
fontWeightStrong: 600,
opacityLoading: 0.65,
linkDecoration: 'none',
linkHoverDecoration: 'none',
linkFocusDecoration: 'none',
controlPaddingHorizontal: 12,
controlPaddingHorizontalSM: 8,
paddingXXS: 4,
paddingXS: 8,
paddingSM: 12,
padding: 16,
paddingMD: 20,
paddingLG: 24,
paddingXL: 32,
paddingContentHorizontalLG: 24,
paddingContentVerticalLG: 16,
paddingContentHorizontal: 16,
paddingContentVertical: 12,
paddingContentHorizontalSM: 16,
paddingContentVerticalSM: 8,
marginXXS: 4,
marginXS: 8,
marginSM: 12,
margin: 16,
marginMD: 20,
marginLG: 24,
marginXL: 32,
marginXXL: 48,
boxShadow:
'\n 0 6px 16px 0 rgba(0, 0, 0, 0.08),\n 0 3px 6px -4px rgba(0, 0, 0, 0.12),\n 0 9px 28px 8px rgba(0, 0, 0, 0.05)\n ',
boxShadowSecondary:
'\n 0 6px 16px 0 rgba(0, 0, 0, 0.08),\n 0 3px 6px -4px rgba(0, 0, 0, 0.12),\n 0 9px 28px 8px rgba(0, 0, 0, 0.05)\n ',
boxShadowTertiary:
'\n 0 1px 2px 0 rgba(0, 0, 0, 0.03),\n 0 1px 6px -1px rgba(0, 0, 0, 0.02),\n 0 2px 4px 0 rgba(0, 0, 0, 0.02)\n ',
screenXS: 480,
screenXSMin: 480,
screenXSMax: 575,
screenSM: 576,
screenSMMin: 576,
screenSMMax: 767,
screenMD: 768,
screenMDMin: 768,
screenMDMax: 991,
screenLG: 992,
screenLGMin: 992,
screenLGMax: 1199,
screenXL: 1200,
screenXLMin: 1200,
screenXLMax: 1599,
screenXXL: 1600,
screenXXLMin: 1600,
boxShadowPopoverArrow: '2px 2px 5px rgba(0, 0, 0, 0.05)',
boxShadowCard:
'\n 0 1px 2px -2px rgba(0, 0, 0, 0.16),\n 0 3px 6px 0 rgba(0, 0, 0, 0.12),\n 0 5px 12px 4px rgba(0, 0, 0, 0.09)\n ',
boxShadowDrawerRight:
'\n -6px 0 16px 0 rgba(0, 0, 0, 0.08),\n -3px 0 6px -4px rgba(0, 0, 0, 0.12),\n -9px 0 28px 8px rgba(0, 0, 0, 0.05)\n ',
boxShadowDrawerLeft:
'\n 6px 0 16px 0 rgba(0, 0, 0, 0.08),\n 3px 0 6px -4px rgba(0, 0, 0, 0.12),\n 9px 0 28px 8px rgba(0, 0, 0, 0.05)\n ',
boxShadowDrawerUp:
'\n 0 6px 16px 0 rgba(0, 0, 0, 0.08),\n 0 3px 6px -4px rgba(0, 0, 0, 0.12),\n 0 9px 28px 8px rgba(0, 0, 0, 0.05)\n ',
boxShadowDrawerDown:
'\n 0 -6px 16px 0 rgba(0, 0, 0, 0.08),\n 0 -3px 6px -4px rgba(0, 0, 0, 0.12),\n 0 -9px 28px 8px rgba(0, 0, 0, 0.05)\n ',
boxShadowTabsOverflowLeft: 'inset 10px 0 8px -8px rgba(0, 0, 0, 0.08)',
boxShadowTabsOverflowRight: 'inset -10px 0 8px -8px rgba(0, 0, 0, 0.08)',
boxShadowTabsOverflowTop: 'inset 0 10px 8px -8px rgba(0, 0, 0, 0.08)',
boxShadowTabsOverflowBottom: 'inset 0 -10px 8px -8px rgba(0, 0, 0, 0.08)'
}
}
for (const key in globalVariables) {
globalThis[key] = globalVariables[key]
}

View File

@@ -0,0 +1,365 @@
:root {
--blue: #1677FF;
--blue1: #e6f4ff;
--blue2: #bae0ff;
--blue3: #91caff;
--blue4: #69b1ff;
--blue5: #4096ff;
--blue6: #1677ff;
--blue7: #0958d9;
--blue8: #003eb3;
--blue9: #002c8c;
--blue10: #001d66;
--purple: #722ED1;
--purple1: #f9f0ff;
--purple2: #efdbff;
--purple3: #d3adf7;
--purple4: #b37feb;
--purple5: #9254de;
--purple6: #722ed1;
--purple7: #531dab;
--purple8: #391085;
--purple9: #22075e;
--purple10: #120338;
--cyan: #13C2C2;
--cyan1: #e6fffb;
--cyan2: #b5f5ec;
--cyan3: #87e8de;
--cyan4: #5cdbd3;
--cyan5: #36cfc9;
--cyan6: #13c2c2;
--cyan7: #08979c;
--cyan8: #006d75;
--cyan9: #00474f;
--cyan10: #002329;
--green: #52C41A;
--green1: #f6ffed;
--green2: #d9f7be;
--green3: #b7eb8f;
--green4: #95de64;
--green5: #73d13d;
--green6: #52c41a;
--green7: #389e0d;
--green8: #237804;
--green9: #135200;
--green10: #092b00;
--magenta: #EB2F96;
--magenta1: #fff0f6;
--magenta2: #ffd6e7;
--magenta3: #ffadd2;
--magenta4: #ff85c0;
--magenta5: #f759ab;
--magenta6: #eb2f96;
--magenta7: #c41d7f;
--magenta8: #9e1068;
--magenta9: #780650;
--magenta10: #520339;
--pink: #EB2F96;
--pink1: #fff0f6;
--pink2: #ffd6e7;
--pink3: #ffadd2;
--pink4: #ff85c0;
--pink5: #f759ab;
--pink6: #eb2f96;
--pink7: #c41d7f;
--pink8: #9e1068;
--pink9: #780650;
--pink10: #520339;
--red: #F5222D;
--red1: #fff1f0;
--red2: #ffccc7;
--red3: #ffa39e;
--red4: #ff7875;
--red5: #ff4d4f;
--red6: #f5222d;
--red7: #cf1322;
--red8: #a8071a;
--red9: #820014;
--red10: #5c0011;
--orange: #FA8C16;
--orange1: #fff7e6;
--orange2: #ffe7ba;
--orange3: #ffd591;
--orange4: #ffc069;
--orange5: #ffa940;
--orange6: #fa8c16;
--orange7: #d46b08;
--orange8: #ad4e00;
--orange9: #873800;
--orange10: #612500;
--yellow: #FADB14;
--yellow1: #feffe6;
--yellow2: #ffffb8;
--yellow3: #fffb8f;
--yellow4: #fff566;
--yellow5: #ffec3d;
--yellow6: #fadb14;
--yellow7: #d4b106;
--yellow8: #ad8b00;
--yellow9: #876800;
--yellow10: #614700;
--volcano: #FA541C;
--volcano1: #fff2e8;
--volcano2: #ffd8bf;
--volcano3: #ffbb96;
--volcano4: #ff9c6e;
--volcano5: #ff7a45;
--volcano6: #fa541c;
--volcano7: #d4380d;
--volcano8: #ad2102;
--volcano9: #871400;
--volcano10: #610b00;
--geekblue: #2F54EB;
--geekblue1: #f0f5ff;
--geekblue2: #d6e4ff;
--geekblue3: #adc6ff;
--geekblue4: #85a5ff;
--geekblue5: #597ef7;
--geekblue6: #2f54eb;
--geekblue7: #1d39c4;
--geekblue8: #10239e;
--geekblue9: #061178;
--geekblue10: #030852;
--gold: #FAAD14;
--gold1: #fffbe6;
--gold2: #fff1b8;
--gold3: #ffe58f;
--gold4: #ffd666;
--gold5: #ffc53d;
--gold6: #faad14;
--gold7: #d48806;
--gold8: #ad6800;
--gold9: #874d00;
--gold10: #613400;
--lime: #A0D911;
--lime1: #fcffe6;
--lime2: #f4ffb8;
--lime3: #eaff8f;
--lime4: #d3f261;
--lime5: #bae637;
--lime6: #a0d911;
--lime7: #7cb305;
--lime8: #5b8c00;
--lime9: #3f6600;
--lime10: #254000;
--colorPrimary: #4e47bb;
--colorSuccess: #52c41a;
--colorWarning: #faad14;
--colorError: #ff4d4f;
--colorInfo: #1677ff;
--colorLink: #1677ff;
--colorTextBase: #000;
--colorBgBase: #fff;
--fontFamily: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial,
'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
'Noto Color Emoji';
--fontFamilyCode: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace;
--fontSize: 14;
--lineWidth: 1;
--lineType: solid;
--motionUnit: 0.1;
--motionBase: 0;
--motionEaseOutCirc: cubic-bezier(0.08, 0.82, 0.17, 1);
--motionEaseInOutCirc: cubic-bezier(0.78, 0.14, 0.15, 0.86);
--motionEaseOut: cubic-bezier(0.215, 0.61, 0.355, 1);
--motionEaseInOut: cubic-bezier(0.645, 0.045, 0.355, 1);
--motionEaseOutBack: cubic-bezier(0.12, 0.4, 0.29, 1.46);
--motionEaseInBack: cubic-bezier(0.71, -0.46, 0.88, 0.6);
--motionEaseInQuint: cubic-bezier(0.755, 0.05, 0.855, 0.06);
--motionEaseOutQuint: cubic-bezier(0.23, 1, 0.32, 1);
--borderRadius: 6;
--sizeUnit: 4;
--sizeStep: 4;
--sizePopupArrow: 16;
--controlHeight: 32;
--zIndexBase: 0;
--zIndexPopupBase: 1000;
--opacityImage: 1;
--colorLinkHover: #4E47BB;
--colorText: rgba(0, 0, 0, 0.88);
--colorTextSecondary: rgba(0, 0, 0, 0.65);
--colorTextTertiary: rgba(0, 0, 0, 0.45);
--colorTextQuaternary: rgba(0, 0, 0, 0.25);
--colorFill: rgba(0, 0, 0, 0.15);
--colorFillSecondary: rgba(0, 0, 0, 0.06);
--colorFillTertiary: rgba(0, 0, 0, 0.04);
--colorFillQuaternary: rgba(0, 0, 0, 0.02);
--colorBgSolid: rgb(0, 0, 0);
--colorBgSolidHover: rgba(0, 0, 0, 0.75);
--colorBgSolidActive: rgba(0, 0, 0, 0.95);
--colorBgLayout: #f5f5f5;
--colorBgContainer: #ffffff;
--colorBgElevated: #ffffff;
--colorBgSpotlight: rgba(0, 0, 0, 0.85);
--colorBgBlur: transparent;
--colorBorder: #d9d9d9;
--colorBorderSecondary: #f0f0f0;
--colorPrimaryBg: #eeebfa;
--colorPrimaryBgHover: #e2dfed;
--colorPrimaryBorder: #c6c1e0;
--colorPrimaryBorderHover: #9d94d4;
--colorPrimaryHover: #756bc7;
--colorPrimaryActive: #343194;
--colorPrimaryTextHover: #756bc7;
--colorPrimaryText: #4e47bb;
--colorPrimaryTextActive: #343194;
--colorSuccessBg: #f6ffed;
--colorSuccessBgHover: #d9f7be;
--colorSuccessBorder: #b7eb8f;
--colorSuccessBorderHover: #95de64;
--colorSuccessHover: #95de64;
--colorSuccessActive: #389e0d;
--colorSuccessTextHover: #73d13d;
--colorSuccessText: #52c41a;
--colorSuccessTextActive: #389e0d;
--colorErrorBg: #fff2f0;
--colorErrorBgHover: #fff1f0;
--colorErrorBgFilledHover: #ffdfdc;
--colorErrorBgActive: #ffccc7;
--colorErrorBorder: #ffccc7;
--colorErrorBorderHover: #ffa39e;
--colorErrorHover: #ff7875;
--colorErrorActive: #d9363e;
--colorErrorTextHover: #ff7875;
--colorErrorText: #ff4d4f;
--colorErrorTextActive: #d9363e;
--colorWarningBg: #fffbe6;
--colorWarningBgHover: #fff1b8;
--colorWarningBorder: #ffe58f;
--colorWarningBorderHover: #ffd666;
--colorWarningHover: #ffd666;
--colorWarningActive: #d48806;
--colorWarningTextHover: #ffc53d;
--colorWarningText: #faad14;
--colorWarningTextActive: #d48806;
--colorInfoBg: #e6f4ff;
--colorInfoBgHover: #bae0ff;
--colorInfoBorder: #91caff;
--colorInfoBorderHover: #69b1ff;
--colorInfoHover: #69b1ff;
--colorInfoActive: #0958d9;
--colorInfoTextHover: #4096ff;
--colorInfoText: #1677ff;
--colorInfoTextActive: #0958d9;
--colorLinkActive: #0958d9;
--colorBgMask: rgba(0, 0, 0, 0.45);
--colorWhite: #fff;
--fontSizeSM: 12;
--fontSizeLG: 16;
--fontSizeXL: 20;
--fontSizeHeading1: 38;
--fontSizeHeading2: 30;
--fontSizeHeading3: 24;
--fontSizeHeading4: 20;
--fontSizeHeading5: 16;
--lineHeight: 1.5714285714285714;
--lineHeightLG: 1.5;
--lineHeightSM: 1.6666666666666667;
--lineHeightHeading1: 1.2105263157894737;
--lineHeightHeading2: 1.2666666666666666;
--lineHeightHeading3: 1.3333333333333333;
--lineHeightHeading4: 1.4;
--lineHeightHeading5: 1.5;
--sizeXXL: 48;
--sizeXL: 32;
--sizeLG: 24;
--sizeMD: 20;
--sizeMS: 16;
--size: 16;
--sizeSM: 12;
--sizeXS: 8;
--sizeXXS: 4;
--controlHeightSM: 24;
--controlHeightXS: 16;
--controlHeightLG: 40;
--motionDurationFast: 0.1s;
--motionDurationMid: 0.2s;
--motionDurationSlow: 0.3s;
--lineWidthBold: 2;
--borderRadiusXS: 2;
--borderRadiusSM: 4;
--borderRadiusLG: 8;
--borderRadiusOuter: 4;
--colorFillContent: rgba(0, 0, 0, 0.06);
--colorFillContentHover: rgba(0, 0, 0, 0.15);
--colorFillAlter: rgba(0, 0, 0, 0.02);
--colorBgContainerDisabled: rgba(0, 0, 0, 0.04);
--colorBorderBg: #ffffff;
--colorSplit: rgba(5, 5, 5, 0.06);
--colorTextPlaceholder: rgba(0, 0, 0, 0.25);
--colorTextDisabled: rgba(0, 0, 0, 0.25);
--colorTextHeading: rgba(0, 0, 0, 0.88);
--colorTextLabel: rgba(0, 0, 0, 0.65);
--colorTextDescription: rgba(0, 0, 0, 0.45);
--colorTextLightSolid: #fff;
--colorHighlight: #ff4d4f;
--colorBgTextHover: rgba(0, 0, 0, 0.06);
--colorBgTextActive: rgba(0, 0, 0, 0.15);
--colorIcon: rgba(0, 0, 0, 0.45);
--colorIconHover: rgba(0, 0, 0, 0.88);
--colorErrorOutline: rgba(255, 38, 5, 0.06);
--colorWarningOutline: rgba(255, 215, 5, 0.1);
--fontSizeIcon: 12;
--lineWidthFocus: 3;
--controlOutlineWidth: 2;
--controlInteractiveSize: 16;
--controlItemBgHover: rgba(0, 0, 0, 0.04);
--controlItemBgActive: #eeebfa;
--controlItemBgActiveHover: #e2dfed;
--controlItemBgActiveDisabled: rgba(0, 0, 0, 0.15);
--controlOutline: rgba(42, 5, 192, 0.08);
--fontWeightStrong: 600;
--opacityLoading: 0.65;
--linkDecoration: none;
--linkHoverDecoration: none;
--linkFocusDecoration: none;
--controlPaddingHorizontal: 12;
--controlPaddingHorizontalSM: 8;
--paddingXXS: 4;
--paddingXS: 8;
--paddingSM: 12;
--padding: 16;
--paddingMD: 20;
--paddingLG: 24;
--paddingXL: 32;
--paddingContentHorizontalLG: 24;
--paddingContentVerticalLG: 16;
--paddingContentHorizontal: 16;
--paddingContentVertical: 12;
--paddingContentHorizontalSM: 16;
--paddingContentVerticalSM: 8;
--marginXXS: 4;
--marginXS: 8;
--marginSM: 12;
--margin: 16;
--marginMD: 20;
--marginLG: 24;
--marginXL: 32;
--marginXXL: 48;
--boxShadow: 0 6px 16px 0 rgba(0, 0, 0, 0.08),
0 3px 6px -4px rgba(0, 0, 0, 0.12),
0 9px 28px 8px rgba(0, 0, 0, 0.05);
--boxShadowSecondary: 0 6px 16px 0 rgba(0, 0, 0, 0.08),
0 3px 6px -4px rgba(0, 0, 0, 0.12),
0 9px 28px 8px rgba(0, 0, 0, 0.05);
--boxShadowTertiary: 0 1px 2px 0 rgba(0, 0, 0, 0.03),
0 1px 6px -1px rgba(0, 0, 0, 0.02),
0 2px 4px 0 rgba(0, 0, 0, 0.02);
--screenXS: 480;
--screenXSMin: 480;
--screenXSMax: 575;
--screenSM: 576;
--screenSMMin: 576;
--screenSMMax: 767;
--screenMD: 768;
--screenMDMin: 768;
--screenMDMax: 991;
--screenLG: 992;
--screenLGMin: 992;
--screenLGMax: 1199;
--screenXL: 1200;
--screenXLMin: 1200;
--screenXLMax: 1599;
--screenXXL: 1600;
--screenXXLMin: 1600;
}

View File

@@ -0,0 +1,518 @@
const globalVariables = {
OxygenTheme: {
blue: '#1677FF',
purple: '#722ED1',
cyan: '#13C2C2',
green: '#52C41A',
magenta: '#EB2F96',
pink: '#EB2F96',
red: '#F5222D',
orange: '#FA8C16',
yellow: '#FADB14',
volcano: '#FA541C',
geekblue: '#2F54EB',
gold: '#FAAD14',
lime: '#A0D911',
colorPrimary: '#4e47bb',
colorSuccess: '#52c41a',
colorWarning: '#faad14',
colorError: '#ff4d4f',
colorInfo: '#1677ff',
colorLink: '#1677ff',
colorTextBase: '#000',
colorBgBase: '#fff',
fontFamily:
"-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial,\n'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',\n'Noto Color Emoji'",
fontFamilyCode: "'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, Courier, monospace",
fontSize: 14,
lineWidth: 1,
lineType: 'solid',
motionUnit: 0.1,
motionBase: 0,
motionEaseOutCirc: 'cubic-bezier(0.08, 0.82, 0.17, 1)',
motionEaseInOutCirc: 'cubic-bezier(0.78, 0.14, 0.15, 0.86)',
motionEaseOut: 'cubic-bezier(0.215, 0.61, 0.355, 1)',
motionEaseInOut: 'cubic-bezier(0.645, 0.045, 0.355, 1)',
motionEaseOutBack: 'cubic-bezier(0.12, 0.4, 0.29, 1.46)',
motionEaseInBack: 'cubic-bezier(0.71, -0.46, 0.88, 0.6)',
motionEaseInQuint: 'cubic-bezier(0.755, 0.05, 0.855, 0.06)',
motionEaseOutQuint: 'cubic-bezier(0.23, 1, 0.32, 1)',
borderRadius: 6,
sizeUnit: 4,
sizeStep: 4,
sizePopupArrow: 16,
controlHeight: 32,
zIndexBase: 0,
zIndexPopupBase: 1000,
opacityImage: 1,
motion: true,
colorLinkHover: '#4E47BB',
'blue-1': '#e6f4ff',
blue1: '#e6f4ff',
'blue-2': '#bae0ff',
blue2: '#bae0ff',
'blue-3': '#91caff',
blue3: '#91caff',
'blue-4': '#69b1ff',
blue4: '#69b1ff',
'blue-5': '#4096ff',
blue5: '#4096ff',
'blue-6': '#1677ff',
blue6: '#1677ff',
'blue-7': '#0958d9',
blue7: '#0958d9',
'blue-8': '#003eb3',
blue8: '#003eb3',
'blue-9': '#002c8c',
blue9: '#002c8c',
'blue-10': '#001d66',
blue10: '#001d66',
'purple-1': '#f9f0ff',
purple1: '#f9f0ff',
'purple-2': '#efdbff',
purple2: '#efdbff',
'purple-3': '#d3adf7',
purple3: '#d3adf7',
'purple-4': '#b37feb',
purple4: '#b37feb',
'purple-5': '#9254de',
purple5: '#9254de',
'purple-6': '#722ed1',
purple6: '#722ed1',
'purple-7': '#531dab',
purple7: '#531dab',
'purple-8': '#391085',
purple8: '#391085',
'purple-9': '#22075e',
purple9: '#22075e',
'purple-10': '#120338',
purple10: '#120338',
'cyan-1': '#e6fffb',
cyan1: '#e6fffb',
'cyan-2': '#b5f5ec',
cyan2: '#b5f5ec',
'cyan-3': '#87e8de',
cyan3: '#87e8de',
'cyan-4': '#5cdbd3',
cyan4: '#5cdbd3',
'cyan-5': '#36cfc9',
cyan5: '#36cfc9',
'cyan-6': '#13c2c2',
cyan6: '#13c2c2',
'cyan-7': '#08979c',
cyan7: '#08979c',
'cyan-8': '#006d75',
cyan8: '#006d75',
'cyan-9': '#00474f',
cyan9: '#00474f',
'cyan-10': '#002329',
cyan10: '#002329',
'green-1': '#f6ffed',
green1: '#f6ffed',
'green-2': '#d9f7be',
green2: '#d9f7be',
'green-3': '#b7eb8f',
green3: '#b7eb8f',
'green-4': '#95de64',
green4: '#95de64',
'green-5': '#73d13d',
green5: '#73d13d',
'green-6': '#52c41a',
green6: '#52c41a',
'green-7': '#389e0d',
green7: '#389e0d',
'green-8': '#237804',
green8: '#237804',
'green-9': '#135200',
green9: '#135200',
'green-10': '#092b00',
green10: '#092b00',
'magenta-1': '#fff0f6',
magenta1: '#fff0f6',
'magenta-2': '#ffd6e7',
magenta2: '#ffd6e7',
'magenta-3': '#ffadd2',
magenta3: '#ffadd2',
'magenta-4': '#ff85c0',
magenta4: '#ff85c0',
'magenta-5': '#f759ab',
magenta5: '#f759ab',
'magenta-6': '#eb2f96',
magenta6: '#eb2f96',
'magenta-7': '#c41d7f',
magenta7: '#c41d7f',
'magenta-8': '#9e1068',
magenta8: '#9e1068',
'magenta-9': '#780650',
magenta9: '#780650',
'magenta-10': '#520339',
magenta10: '#520339',
'pink-1': '#fff0f6',
pink1: '#fff0f6',
'pink-2': '#ffd6e7',
pink2: '#ffd6e7',
'pink-3': '#ffadd2',
pink3: '#ffadd2',
'pink-4': '#ff85c0',
pink4: '#ff85c0',
'pink-5': '#f759ab',
pink5: '#f759ab',
'pink-6': '#eb2f96',
pink6: '#eb2f96',
'pink-7': '#c41d7f',
pink7: '#c41d7f',
'pink-8': '#9e1068',
pink8: '#9e1068',
'pink-9': '#780650',
pink9: '#780650',
'pink-10': '#520339',
pink10: '#520339',
'red-1': '#fff1f0',
red1: '#fff1f0',
'red-2': '#ffccc7',
red2: '#ffccc7',
'red-3': '#ffa39e',
red3: '#ffa39e',
'red-4': '#ff7875',
red4: '#ff7875',
'red-5': '#ff4d4f',
red5: '#ff4d4f',
'red-6': '#f5222d',
red6: '#f5222d',
'red-7': '#cf1322',
red7: '#cf1322',
'red-8': '#a8071a',
red8: '#a8071a',
'red-9': '#820014',
red9: '#820014',
'red-10': '#5c0011',
red10: '#5c0011',
'orange-1': '#fff7e6',
orange1: '#fff7e6',
'orange-2': '#ffe7ba',
orange2: '#ffe7ba',
'orange-3': '#ffd591',
orange3: '#ffd591',
'orange-4': '#ffc069',
orange4: '#ffc069',
'orange-5': '#ffa940',
orange5: '#ffa940',
'orange-6': '#fa8c16',
orange6: '#fa8c16',
'orange-7': '#d46b08',
orange7: '#d46b08',
'orange-8': '#ad4e00',
orange8: '#ad4e00',
'orange-9': '#873800',
orange9: '#873800',
'orange-10': '#612500',
orange10: '#612500',
'yellow-1': '#feffe6',
yellow1: '#feffe6',
'yellow-2': '#ffffb8',
yellow2: '#ffffb8',
'yellow-3': '#fffb8f',
yellow3: '#fffb8f',
'yellow-4': '#fff566',
yellow4: '#fff566',
'yellow-5': '#ffec3d',
yellow5: '#ffec3d',
'yellow-6': '#fadb14',
yellow6: '#fadb14',
'yellow-7': '#d4b106',
yellow7: '#d4b106',
'yellow-8': '#ad8b00',
yellow8: '#ad8b00',
'yellow-9': '#876800',
yellow9: '#876800',
'yellow-10': '#614700',
yellow10: '#614700',
'volcano-1': '#fff2e8',
volcano1: '#fff2e8',
'volcano-2': '#ffd8bf',
volcano2: '#ffd8bf',
'volcano-3': '#ffbb96',
volcano3: '#ffbb96',
'volcano-4': '#ff9c6e',
volcano4: '#ff9c6e',
'volcano-5': '#ff7a45',
volcano5: '#ff7a45',
'volcano-6': '#fa541c',
volcano6: '#fa541c',
'volcano-7': '#d4380d',
volcano7: '#d4380d',
'volcano-8': '#ad2102',
volcano8: '#ad2102',
'volcano-9': '#871400',
volcano9: '#871400',
'volcano-10': '#610b00',
volcano10: '#610b00',
'geekblue-1': '#f0f5ff',
geekblue1: '#f0f5ff',
'geekblue-2': '#d6e4ff',
geekblue2: '#d6e4ff',
'geekblue-3': '#adc6ff',
geekblue3: '#adc6ff',
'geekblue-4': '#85a5ff',
geekblue4: '#85a5ff',
'geekblue-5': '#597ef7',
geekblue5: '#597ef7',
'geekblue-6': '#2f54eb',
geekblue6: '#2f54eb',
'geekblue-7': '#1d39c4',
geekblue7: '#1d39c4',
'geekblue-8': '#10239e',
geekblue8: '#10239e',
'geekblue-9': '#061178',
geekblue9: '#061178',
'geekblue-10': '#030852',
geekblue10: '#030852',
'gold-1': '#fffbe6',
gold1: '#fffbe6',
'gold-2': '#fff1b8',
gold2: '#fff1b8',
'gold-3': '#ffe58f',
gold3: '#ffe58f',
'gold-4': '#ffd666',
gold4: '#ffd666',
'gold-5': '#ffc53d',
gold5: '#ffc53d',
'gold-6': '#faad14',
gold6: '#faad14',
'gold-7': '#d48806',
gold7: '#d48806',
'gold-8': '#ad6800',
gold8: '#ad6800',
'gold-9': '#874d00',
gold9: '#874d00',
'gold-10': '#613400',
gold10: '#613400',
'lime-1': '#fcffe6',
lime1: '#fcffe6',
'lime-2': '#f4ffb8',
lime2: '#f4ffb8',
'lime-3': '#eaff8f',
lime3: '#eaff8f',
'lime-4': '#d3f261',
lime4: '#d3f261',
'lime-5': '#bae637',
lime5: '#bae637',
'lime-6': '#a0d911',
lime6: '#a0d911',
'lime-7': '#7cb305',
lime7: '#7cb305',
'lime-8': '#5b8c00',
lime8: '#5b8c00',
'lime-9': '#3f6600',
lime9: '#3f6600',
'lime-10': '#254000',
lime10: '#254000',
colorText: 'rgba(0, 0, 0, 0.88)',
colorTextSecondary: 'rgba(0, 0, 0, 0.65)',
colorTextTertiary: 'rgba(0, 0, 0, 0.45)',
colorTextQuaternary: 'rgba(0, 0, 0, 0.25)',
colorFill: 'rgba(0, 0, 0, 0.15)',
colorFillSecondary: 'rgba(0, 0, 0, 0.06)',
colorFillTertiary: 'rgba(0, 0, 0, 0.04)',
colorFillQuaternary: 'rgba(0, 0, 0, 0.02)',
colorBgSolid: 'rgb(0, 0, 0)',
colorBgSolidHover: 'rgba(0, 0, 0, 0.75)',
colorBgSolidActive: 'rgba(0, 0, 0, 0.95)',
colorBgLayout: '#f5f5f5',
colorBgContainer: '#ffffff',
colorBgElevated: '#ffffff',
colorBgSpotlight: 'rgba(0, 0, 0, 0.85)',
colorBgBlur: 'transparent',
colorBorder: '#d9d9d9',
colorBorderSecondary: '#f0f0f0',
colorPrimaryBg: '#eeebfa',
colorPrimaryBgHover: '#e2dfed',
colorPrimaryBorder: '#c6c1e0',
colorPrimaryBorderHover: '#9d94d4',
colorPrimaryHover: '#756bc7',
colorPrimaryActive: '#343194',
colorPrimaryTextHover: '#756bc7',
colorPrimaryText: '#4e47bb',
colorPrimaryTextActive: '#343194',
colorSuccessBg: '#f6ffed',
colorSuccessBgHover: '#d9f7be',
colorSuccessBorder: '#b7eb8f',
colorSuccessBorderHover: '#95de64',
colorSuccessHover: '#95de64',
colorSuccessActive: '#389e0d',
colorSuccessTextHover: '#73d13d',
colorSuccessText: '#52c41a',
colorSuccessTextActive: '#389e0d',
colorErrorBg: '#fff2f0',
colorErrorBgHover: '#fff1f0',
colorErrorBgFilledHover: '#ffdfdc',
colorErrorBgActive: '#ffccc7',
colorErrorBorder: '#ffccc7',
colorErrorBorderHover: '#ffa39e',
colorErrorHover: '#ff7875',
colorErrorActive: '#d9363e',
colorErrorTextHover: '#ff7875',
colorErrorText: '#ff4d4f',
colorErrorTextActive: '#d9363e',
colorWarningBg: '#fffbe6',
colorWarningBgHover: '#fff1b8',
colorWarningBorder: '#ffe58f',
colorWarningBorderHover: '#ffd666',
colorWarningHover: '#ffd666',
colorWarningActive: '#d48806',
colorWarningTextHover: '#ffc53d',
colorWarningText: '#faad14',
colorWarningTextActive: '#d48806',
colorInfoBg: '#e6f4ff',
colorInfoBgHover: '#bae0ff',
colorInfoBorder: '#91caff',
colorInfoBorderHover: '#69b1ff',
colorInfoHover: '#69b1ff',
colorInfoActive: '#0958d9',
colorInfoTextHover: '#4096ff',
colorInfoText: '#1677ff',
colorInfoTextActive: '#0958d9',
colorLinkActive: '#0958d9',
colorBgMask: 'rgba(0, 0, 0, 0.45)',
colorWhite: '#fff',
fontSizeSM: 12,
fontSizeLG: 16,
fontSizeXL: 20,
fontSizeHeading1: 38,
fontSizeHeading2: 30,
fontSizeHeading3: 24,
fontSizeHeading4: 20,
fontSizeHeading5: 16,
lineHeight: 1.5714285714285714,
lineHeightLG: 1.5,
lineHeightSM: 1.6666666666666667,
fontHeight: 22,
fontHeightLG: 24,
fontHeightSM: 20,
lineHeightHeading1: 1.2105263157894737,
lineHeightHeading2: 1.2666666666666666,
lineHeightHeading3: 1.3333333333333333,
lineHeightHeading4: 1.4,
lineHeightHeading5: 1.5,
sizeXXL: 48,
sizeXL: 32,
sizeLG: 24,
sizeMD: 20,
sizeMS: 16,
size: 16,
sizeSM: 12,
sizeXS: 8,
sizeXXS: 4,
controlHeightSM: 24,
controlHeightXS: 16,
controlHeightLG: 40,
motionDurationFast: '0.1s',
motionDurationMid: '0.2s',
motionDurationSlow: '0.3s',
lineWidthBold: 2,
borderRadiusXS: 2,
borderRadiusSM: 4,
borderRadiusLG: 8,
borderRadiusOuter: 4,
colorFillContent: 'rgba(0, 0, 0, 0.06)',
colorFillContentHover: 'rgba(0, 0, 0, 0.15)',
colorFillAlter: 'rgba(0, 0, 0, 0.02)',
colorBgContainerDisabled: 'rgba(0, 0, 0, 0.04)',
colorBorderBg: '#ffffff',
colorSplit: 'rgba(5, 5, 5, 0.06)',
colorTextPlaceholder: 'rgba(0, 0, 0, 0.25)',
colorTextDisabled: 'rgba(0, 0, 0, 0.25)',
colorTextHeading: 'rgba(0, 0, 0, 0.88)',
colorTextLabel: 'rgba(0, 0, 0, 0.65)',
colorTextDescription: 'rgba(0, 0, 0, 0.45)',
colorTextLightSolid: '#fff',
colorHighlight: '#ff4d4f',
colorBgTextHover: 'rgba(0, 0, 0, 0.06)',
colorBgTextActive: 'rgba(0, 0, 0, 0.15)',
colorIcon: 'rgba(0, 0, 0, 0.45)',
colorIconHover: 'rgba(0, 0, 0, 0.88)',
colorErrorOutline: 'rgba(255, 38, 5, 0.06)',
colorWarningOutline: 'rgba(255, 215, 5, 0.1)',
fontSizeIcon: 12,
lineWidthFocus: 3,
controlOutlineWidth: 2,
controlInteractiveSize: 16,
controlItemBgHover: 'rgba(0, 0, 0, 0.04)',
controlItemBgActive: '#eeebfa',
controlItemBgActiveHover: '#e2dfed',
controlItemBgActiveDisabled: 'rgba(0, 0, 0, 0.15)',
controlTmpOutline: 'rgba(0, 0, 0, 0.02)',
controlOutline: 'rgba(42, 5, 192, 0.08)',
fontWeightStrong: 600,
opacityLoading: 0.65,
linkDecoration: 'none',
linkHoverDecoration: 'none',
linkFocusDecoration: 'none',
controlPaddingHorizontal: 12,
controlPaddingHorizontalSM: 8,
paddingXXS: 4,
paddingXS: 8,
paddingSM: 12,
padding: 16,
paddingMD: 20,
paddingLG: 24,
paddingXL: 32,
paddingContentHorizontalLG: 24,
paddingContentVerticalLG: 16,
paddingContentHorizontal: 16,
paddingContentVertical: 12,
paddingContentHorizontalSM: 16,
paddingContentVerticalSM: 8,
marginXXS: 4,
marginXS: 8,
marginSM: 12,
margin: 16,
marginMD: 20,
marginLG: 24,
marginXL: 32,
marginXXL: 48,
boxShadow:
'\n 0 6px 16px 0 rgba(0, 0, 0, 0.08),\n 0 3px 6px -4px rgba(0, 0, 0, 0.12),\n 0 9px 28px 8px rgba(0, 0, 0, 0.05)\n ',
boxShadowSecondary:
'\n 0 6px 16px 0 rgba(0, 0, 0, 0.08),\n 0 3px 6px -4px rgba(0, 0, 0, 0.12),\n 0 9px 28px 8px rgba(0, 0, 0, 0.05)\n ',
boxShadowTertiary:
'\n 0 1px 2px 0 rgba(0, 0, 0, 0.03),\n 0 1px 6px -1px rgba(0, 0, 0, 0.02),\n 0 2px 4px 0 rgba(0, 0, 0, 0.02)\n ',
screenXS: 480,
screenXSMin: 480,
screenXSMax: 575,
screenSM: 576,
screenSMMin: 576,
screenSMMax: 767,
screenMD: 768,
screenMDMin: 768,
screenMDMax: 991,
screenLG: 992,
screenLGMin: 992,
screenLGMax: 1199,
screenXL: 1200,
screenXLMin: 1200,
screenXLMax: 1599,
screenXXL: 1600,
screenXXLMin: 1600,
boxShadowPopoverArrow: '2px 2px 5px rgba(0, 0, 0, 0.05)',
boxShadowCard:
'\n 0 1px 2px -2px rgba(0, 0, 0, 0.16),\n 0 3px 6px 0 rgba(0, 0, 0, 0.12),\n 0 5px 12px 4px rgba(0, 0, 0, 0.09)\n ',
boxShadowDrawerRight:
'\n -6px 0 16px 0 rgba(0, 0, 0, 0.08),\n -3px 0 6px -4px rgba(0, 0, 0, 0.12),\n -9px 0 28px 8px rgba(0, 0, 0, 0.05)\n ',
boxShadowDrawerLeft:
'\n 6px 0 16px 0 rgba(0, 0, 0, 0.08),\n 3px 0 6px -4px rgba(0, 0, 0, 0.12),\n 9px 0 28px 8px rgba(0, 0, 0, 0.05)\n ',
boxShadowDrawerUp:
'\n 0 6px 16px 0 rgba(0, 0, 0, 0.08),\n 0 3px 6px -4px rgba(0, 0, 0, 0.12),\n 0 9px 28px 8px rgba(0, 0, 0, 0.05)\n ',
boxShadowDrawerDown:
'\n 0 -6px 16px 0 rgba(0, 0, 0, 0.08),\n 0 -3px 6px -4px rgba(0, 0, 0, 0.12),\n 0 -9px 28px 8px rgba(0, 0, 0, 0.05)\n ',
boxShadowTabsOverflowLeft: 'inset 10px 0 8px -8px rgba(0, 0, 0, 0.08)',
boxShadowTabsOverflowRight: 'inset -10px 0 8px -8px rgba(0, 0, 0, 0.08)',
boxShadowTabsOverflowTop: 'inset 0 10px 8px -8px rgba(0, 0, 0, 0.08)',
boxShadowTabsOverflowBottom: 'inset 0 -10px 8px -8px rgba(0, 0, 0, 0.08)',
isDarkMode: false
}
}
for (const key in globalVariables) {
globalThis[key] = globalVariables[key]
}

View File

@@ -0,0 +1,23 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<title>Preview</title>
<script id="global-js-variables">
{{replace_global_js_variables}}
</script>
<style id="global-css-variables">
{{replace_global_css_variables}}
</style>
</head>
<body>
<script type="module" id="appDictSrc">{{replace_dict_code}}</script>
<script type="module" id="appBaseSrc">{{replace_base_code}}</script>
<div id="root">
<div style="position:absolute;top: 0;left:0;width:100%;height:100%;display: flex;justify-content: center;align-items: center;color: #777;">
Loading...
</div>
</div>
</body>
</html>

View File

@@ -2,7 +2,6 @@ package top.fatweb.oxygen.toolbox
import android.content.Context
import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.SystemBarStyle
import androidx.activity.compose.setContent
@@ -32,13 +31,15 @@ import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import top.fatweb.oxygen.toolbox.model.DarkThemeConfig
import top.fatweb.oxygen.toolbox.model.LanguageConfig
import top.fatweb.oxygen.toolbox.model.LaunchPageConfig
import top.fatweb.oxygen.toolbox.model.ThemeBrandConfig
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.monitor.NetworkMonitor
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.rememberOxygenAppState
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 javax.inject.Inject
const val TAG = "MainActivity"
@OptIn(ExperimentalMaterial3WindowSizeClassApi::class)
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
@@ -93,12 +92,10 @@ class MainActivity : ComponentActivity() {
LaunchedEffect(darkTheme) {
enableEdgeToEdge(
statusBarStyle = SystemBarStyle.auto(
android.graphics.Color.TRANSPARENT,
android.graphics.Color.TRANSPARENT
lightScrim = android.graphics.Color.TRANSPARENT,
darkScrim = android.graphics.Color.TRANSPARENT
) { darkTheme },
navigationBarStyle = SystemBarStyle.auto(
lightScrim, darkScrim
) { darkTheme }
navigationBarStyle = SystemBarStyle.dark(android.graphics.Color.TRANSPARENT)
)
}
@@ -111,9 +108,7 @@ class MainActivity : ComponentActivity() {
val currentTimeZone by appState.currentTimeZone.collectAsStateWithLifecycle()
CompositionLocalProvider(
LocalTimeZone provides currentTimeZone
) {
CompositionLocalProvider(LocalTimeZone provides currentTimeZone) {
OxygenTheme(
darkTheme = darkTheme,
androidTheme = shouldUseAndroidTheme(uiState),
@@ -122,10 +117,21 @@ class MainActivity : ComponentActivity() {
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
@@ -137,67 +143,57 @@ class MainActivity : ComponentActivity() {
override fun attachBaseContext(newBase: Context) {
val userDataRepository =
EntryPointAccessors.fromApplication<UserDataRepositoryEntryPoint>(newBase).userDataRepository
super.attachBaseContext(LocaleUtils.attachBaseContext(newBase, runBlocking {
userDataRepository.userData.first().languageConfig
}))
super.attachBaseContext(
LocaleUtils.attachBaseContext(
context = newBase,
languageConfig = runBlocking {
userDataRepository.userData.first().languageConfig
}
)
)
}
}
@Composable
private fun shouldUseDarkTheme(
uiState: MainActivityUiState
): Boolean = when (uiState) {
MainActivityUiState.Loading -> isSystemInDarkTheme()
is MainActivityUiState.Success -> when (uiState.userData.darkThemeConfig) {
DarkThemeConfig.FOLLOW_SYSTEM -> isSystemInDarkTheme()
DarkThemeConfig.LIGHT -> false
DarkThemeConfig.DARK -> true
private fun shouldUseDarkTheme(uiState: MainActivityUiState): Boolean =
when (uiState) {
MainActivityUiState.Loading -> isSystemInDarkTheme()
is MainActivityUiState.Success ->
when (uiState.userData.darkThemeConfig) {
DarkThemeConfig.FollowSystem -> isSystemInDarkTheme()
DarkThemeConfig.Light -> false
DarkThemeConfig.Dark -> true
}
}
}
@Composable
private fun shouldUseAndroidTheme(
uiState: MainActivityUiState
): Boolean = when (uiState) {
MainActivityUiState.Loading -> false
is MainActivityUiState.Success -> when (uiState.userData.themeBrandConfig) {
ThemeBrandConfig.DEFAULT -> false
ThemeBrandConfig.ANDROID -> true
private fun shouldUseAndroidTheme(uiState: MainActivityUiState): Boolean =
when (uiState) {
MainActivityUiState.Loading -> false
is MainActivityUiState.Success ->
when (uiState.userData.themeBrandConfig) {
ThemeBrandConfig.Default -> false
ThemeBrandConfig.Android -> true
}
}
}
@Composable
private fun shouldUseDynamicColor(
uiState: MainActivityUiState
): Boolean = when (uiState) {
MainActivityUiState.Loading -> true
is MainActivityUiState.Success -> uiState.userData.useDynamicColor
}
private fun shouldUseDynamicColor(uiState: MainActivityUiState): Boolean =
when (uiState) {
MainActivityUiState.Loading -> true
is MainActivityUiState.Success -> uiState.userData.useDynamicColor
}
@Composable
private fun whatLocale(
uiState: MainActivityUiState
): LanguageConfig = when (uiState) {
MainActivityUiState.Loading -> LanguageConfig.FOLLOW_SYSTEM
is MainActivityUiState.Success -> uiState.userData.languageConfig
}
private fun whatLocale(uiState: MainActivityUiState): LanguageConfig =
when (uiState) {
MainActivityUiState.Loading -> LanguageConfig.FollowSystem
is MainActivityUiState.Success -> uiState.userData.languageConfig
}
@Composable
private fun whatLaunchPage(
uiState: MainActivityUiState
): LaunchPageConfig = when (uiState) {
MainActivityUiState.Loading -> LaunchPageConfig.TOOLS
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)
private fun whatLaunchPage(uiState: MainActivityUiState): LaunchPageConfig =
when (uiState) {
MainActivityUiState.Loading -> LaunchPageConfig.Tools
is MainActivityUiState.Success -> uiState.userData.launchPageConfig
}

View File

@@ -7,8 +7,8 @@ import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import top.fatweb.oxygen.toolbox.model.UserData
import top.fatweb.oxygen.toolbox.repository.UserDataRepository
import top.fatweb.oxygen.toolbox.model.userdata.UserData
import top.fatweb.oxygen.toolbox.repository.userdata.UserDataRepository
import javax.inject.Inject
import kotlin.time.Duration.Companion.seconds
@@ -21,11 +21,11 @@ class MainActivityViewModel @Inject constructor(
}.stateIn(
scope = viewModelScope,
initialValue = MainActivityUiState.Loading,
started = SharingStarted.WhileSubscribed(5.seconds.inWholeMilliseconds)
started = SharingStarted.WhileSubscribed(stopTimeoutMillis = 5.seconds.inWholeMilliseconds)
)
}
sealed interface MainActivityUiState {
data object Loading : MainActivityUiState
data class Success(val userData: UserData) : MainActivityUiState
}
}

View File

@@ -2,11 +2,19 @@ package top.fatweb.oxygen.toolbox
import android.app.Application
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
@HiltAndroidApp
class OxygenApplication : Application() {
@Inject
lateinit var userDataRepository: UserDataRepository
}
override fun onCreate() {
super.onCreate()
Timber.plant(if (BuildConfig.DEBUG) Timber.DebugTree() else OxygenLogTree(this))
}
}

View File

@@ -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)
}

View File

@@ -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>>
}

View File

@@ -0,0 +1,51 @@
package top.fatweb.oxygen.toolbox.data.tool
import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
import top.fatweb.oxygen.toolbox.network.Dispatcher
import top.fatweb.oxygen.toolbox.network.OxygenDispatchers
import javax.inject.Inject
class ToolDataSource @Inject constructor(
@ApplicationContext private val context: Context,
@Dispatcher(OxygenDispatchers.IO) private val ioDispatcher: CoroutineDispatcher
) {
val toolViewTemplate = flow {
emit(
context.assets.open("template/tool-view.html")
.bufferedReader()
.use {
it.readText()
}
)
}.flowOn(ioDispatcher)
fun getGlobalJsVariables(isDarkMode: Boolean) = flow {
emit(
context.assets.open(
if (isDarkMode) "template/global-variables-dark.js"
else "template/global-variables-light.js"
)
.bufferedReader()
.use {
it.readText()
}
)
}.flowOn(ioDispatcher)
fun getGlobalCssVariables(isDarkMode: Boolean) = flow {
emit(
context.assets.open(
if (isDarkMode) "template/global-variables-dark.css"
else "template/global-variables-light.css"
)
.bufferedReader()
.use {
it.readText()
}
)
}.flowOn(ioDispatcher)
}

View File

@@ -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 }
}
}
}

View File

@@ -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?>
}

View File

@@ -1,6 +1,8 @@
package top.fatweb.oxygen.toolbox.datastore
package top.fatweb.oxygen.toolbox.data.userdata
import androidx.datastore.core.DataMigration
import top.fatweb.oxygen.toolbox.data.UserPreferences
import top.fatweb.oxygen.toolbox.data.copy
internal object IntToStringIdsMigration : DataMigration<UserPreferences> {
override suspend fun cleanUp() = Unit
@@ -9,4 +11,4 @@ internal object IntToStringIdsMigration : DataMigration<UserPreferences> {
currentData.copy { }
override suspend fun shouldMigrate(currentData: UserPreferences): Boolean = false
}
}

View File

@@ -1,12 +1,18 @@
package top.fatweb.oxygen.toolbox.datastore
package top.fatweb.oxygen.toolbox.data.userdata
import androidx.datastore.core.DataStore
import kotlinx.coroutines.flow.map
import top.fatweb.oxygen.toolbox.model.DarkThemeConfig
import top.fatweb.oxygen.toolbox.model.LanguageConfig
import top.fatweb.oxygen.toolbox.model.LaunchPageConfig
import top.fatweb.oxygen.toolbox.model.ThemeBrandConfig
import top.fatweb.oxygen.toolbox.model.UserData
import top.fatweb.oxygen.toolbox.data.DarkThemeConfigProto
import top.fatweb.oxygen.toolbox.data.LanguageConfigProto
import top.fatweb.oxygen.toolbox.data.LaunchPageConfigProto
import top.fatweb.oxygen.toolbox.data.ThemeBrandConfigProto
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
class OxygenPreferencesDataSource @Inject constructor(
@@ -20,23 +26,23 @@ class OxygenPreferencesDataSource @Inject constructor(
LanguageConfigProto.UNRECOGNIZED,
LanguageConfigProto.LANGUAGE_CONFIG_UNSPECIFIED,
LanguageConfigProto.LANGUAGE_CONFIG_FOLLOW_SYSTEM
-> LanguageConfig.FOLLOW_SYSTEM
-> LanguageConfig.FollowSystem
LanguageConfigProto.LANGUAGE_CONFIG_CHINESE
-> LanguageConfig.CHINESE
-> LanguageConfig.Chinese
LanguageConfigProto.LANGUAGE_CONFIG_ENGLISH
-> LanguageConfig.ENGLISH
-> LanguageConfig.English
},
launchPageConfig = when (it.launchPageConfig) {
null,
LaunchPageConfigProto.UNRECOGNIZED,
LaunchPageConfigProto.LAUNCH_PAGE_CONFIG_UNSPECIFIED,
LaunchPageConfigProto.LAUNCH_PAGE_CONFIG_TOOLS
-> LaunchPageConfig.TOOLS
-> LaunchPageConfig.Tools
LaunchPageConfigProto.LAUNCH_PAGE_CONFIG_STAR
-> LaunchPageConfig.STAR
-> LaunchPageConfig.Star
},
themeBrandConfig = when (it.themeBrandConfig) {
null,
@@ -44,10 +50,10 @@ class OxygenPreferencesDataSource @Inject constructor(
ThemeBrandConfigProto.THEME_BRAND_CONFIG_UNSPECIFIED,
ThemeBrandConfigProto.THEME_BRAND_CONFIG_DEFAULT
->
ThemeBrandConfig.DEFAULT
ThemeBrandConfig.Default
ThemeBrandConfigProto.THEME_BRAND_CONFIG_ANDROID
-> ThemeBrandConfig.ANDROID
-> ThemeBrandConfig.Android
},
darkThemeConfig = when (it.darkThemeConfig) {
null,
@@ -55,13 +61,13 @@ class OxygenPreferencesDataSource @Inject constructor(
DarkThemeConfigProto.DARK_THEME_CONFIG_UNSPECIFIED,
DarkThemeConfigProto.DARK_THEME_CONFIG_FOLLOW_SYSTEM
->
DarkThemeConfig.FOLLOW_SYSTEM
DarkThemeConfig.FollowSystem
DarkThemeConfigProto.DARK_THEME_CONFIG_LIGHT
-> DarkThemeConfig.LIGHT
-> DarkThemeConfig.Light
DarkThemeConfigProto.DARK_THEME_CONFIG_DARK
-> DarkThemeConfig.DARK
-> DarkThemeConfig.Dark
},
useDynamicColor = it.useDynamicColor
)
@@ -71,9 +77,9 @@ class OxygenPreferencesDataSource @Inject constructor(
userPreferences.updateData {
it.copy {
this.languageConfig = when (languageConfig) {
LanguageConfig.FOLLOW_SYSTEM -> LanguageConfigProto.LANGUAGE_CONFIG_FOLLOW_SYSTEM
LanguageConfig.CHINESE -> LanguageConfigProto.LANGUAGE_CONFIG_CHINESE
LanguageConfig.ENGLISH -> LanguageConfigProto.LANGUAGE_CONFIG_ENGLISH
LanguageConfig.FollowSystem -> LanguageConfigProto.LANGUAGE_CONFIG_FOLLOW_SYSTEM
LanguageConfig.Chinese -> LanguageConfigProto.LANGUAGE_CONFIG_CHINESE
LanguageConfig.English -> LanguageConfigProto.LANGUAGE_CONFIG_ENGLISH
}
}
}
@@ -83,8 +89,8 @@ class OxygenPreferencesDataSource @Inject constructor(
userPreferences.updateData {
it.copy {
this.launchPageConfig = when (launchPageConfig) {
LaunchPageConfig.TOOLS -> LaunchPageConfigProto.LAUNCH_PAGE_CONFIG_TOOLS
LaunchPageConfig.STAR -> LaunchPageConfigProto.LAUNCH_PAGE_CONFIG_STAR
LaunchPageConfig.Tools -> LaunchPageConfigProto.LAUNCH_PAGE_CONFIG_TOOLS
LaunchPageConfig.Star -> LaunchPageConfigProto.LAUNCH_PAGE_CONFIG_STAR
}
}
}
@@ -94,8 +100,8 @@ class OxygenPreferencesDataSource @Inject constructor(
userPreferences.updateData {
it.copy {
this.themeBrandConfig = when (themeBrandConfig) {
ThemeBrandConfig.DEFAULT -> ThemeBrandConfigProto.THEME_BRAND_CONFIG_DEFAULT
ThemeBrandConfig.ANDROID -> ThemeBrandConfigProto.THEME_BRAND_CONFIG_ANDROID
ThemeBrandConfig.Default -> ThemeBrandConfigProto.THEME_BRAND_CONFIG_DEFAULT
ThemeBrandConfig.Android -> ThemeBrandConfigProto.THEME_BRAND_CONFIG_ANDROID
}
}
}
@@ -105,9 +111,9 @@ class OxygenPreferencesDataSource @Inject constructor(
userPreferences.updateData {
it.copy {
this.darkThemeConfig = when (darkThemeConfig) {
DarkThemeConfig.FOLLOW_SYSTEM -> DarkThemeConfigProto.DARK_THEME_CONFIG_FOLLOW_SYSTEM
DarkThemeConfig.LIGHT -> DarkThemeConfigProto.DARK_THEME_CONFIG_LIGHT
DarkThemeConfig.DARK -> DarkThemeConfigProto.DARK_THEME_CONFIG_DARK
DarkThemeConfig.FollowSystem -> DarkThemeConfigProto.DARK_THEME_CONFIG_FOLLOW_SYSTEM
DarkThemeConfig.Light -> DarkThemeConfigProto.DARK_THEME_CONFIG_LIGHT
DarkThemeConfig.Dark -> DarkThemeConfigProto.DARK_THEME_CONFIG_DARK
}
}
}

View File

@@ -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.Serializer
import com.google.protobuf.InvalidProtocolBufferException
import top.fatweb.oxygen.toolbox.data.UserPreferences
import java.io.InputStream
import java.io.OutputStream
import javax.inject.Inject
@@ -14,10 +15,13 @@ class UserPreferencesSerializer @Inject constructor() : Serializer<UserPreferenc
try {
UserPreferences.parseFrom(input)
} 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) {
t.writeTo(output)
}
}
}

View File

@@ -23,6 +23,8 @@ internal object CoroutineScopesModule {
@Singleton
@ApplicationScope
fun providesCoroutineScope(
@Dispatcher(OxygenDispatchers.Default) dispatcher: CoroutineDispatcher
): CoroutineScope = CoroutineScope(SupervisorJob() + dispatcher)
}
@Dispatcher(OxygenDispatchers.Default)
dispatcher: CoroutineDispatcher
): CoroutineScope =
CoroutineScope(SupervisorJob() + dispatcher)
}

View File

@@ -8,18 +8,33 @@ import top.fatweb.oxygen.toolbox.monitor.ConnectivityManagerNetworkMonitor
import top.fatweb.oxygen.toolbox.monitor.NetworkMonitor
import top.fatweb.oxygen.toolbox.monitor.TimeZoneBroadcastMonitor
import top.fatweb.oxygen.toolbox.monitor.TimeZoneMonitor
import top.fatweb.oxygen.toolbox.repository.OfflineFirstUserDataRepository
import top.fatweb.oxygen.toolbox.repository.UserDataRepository
import top.fatweb.oxygen.toolbox.repository.lib.DepRepository
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
@InstallIn(SingletonComponent::class)
abstract class DataModule {
@Binds
internal abstract fun bindsUserDataRepository(userDataRepository: OfflineFirstUserDataRepository): UserDataRepository
@Binds
internal abstract fun bindsNetworkMonitor(networkMonitor: ConnectivityManagerNetworkMonitor): NetworkMonitor
@Binds
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
}

View File

@@ -11,9 +11,9 @@ import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import top.fatweb.oxygen.toolbox.datastore.IntToStringIdsMigration
import top.fatweb.oxygen.toolbox.datastore.UserPreferences
import top.fatweb.oxygen.toolbox.datastore.UserPreferencesSerializer
import top.fatweb.oxygen.toolbox.data.UserPreferences
import top.fatweb.oxygen.toolbox.data.userdata.IntToStringIdsMigration
import top.fatweb.oxygen.toolbox.data.userdata.UserPreferencesSerializer
import top.fatweb.oxygen.toolbox.network.Dispatcher
import top.fatweb.oxygen.toolbox.network.OxygenDispatchers
import javax.inject.Singleton
@@ -38,4 +38,4 @@ object DataStoreModule {
) {
context.dataStoreFile("user_preferences.pb")
}
}
}

View File

@@ -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()
}

View File

@@ -19,4 +19,4 @@ object DispatchersModule {
@Provides
@Dispatcher(OxygenDispatchers.Default)
fun providesDefaultDispatcher(): CoroutineDispatcher = Dispatchers.Default
}
}

View File

@@ -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)
}

View File

@@ -1,20 +1,77 @@
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.filled.AccessTime
import androidx.compose.material.icons.filled.Build
import androidx.compose.material.icons.filled.Cancel
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Code
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Download
import androidx.compose.material.icons.filled.Fullscreen
import androidx.compose.material.icons.filled.FullscreenExit
import androidx.compose.material.icons.filled.Inbox
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.Reorder
import androidx.compose.material.icons.filled.Upgrade
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.Store
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.KeyboardArrowDown
import androidx.compose.material.icons.rounded.Search
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 {
val ArrowDown = Icons.Rounded.KeyboardArrowDown
val Back = Icons.Rounded.ArrowBackIosNew
val Box = Icons.Default.Inbox
val Close = Icons.Default.Close
val Code = Icons.Default.Code
val Delete = Icons.Default.Delete
val Download = Icons.Default.Download
val Error = Icons.Default.Cancel
val FullScreen = Icons.Default.Fullscreen
val FullScreenExit = Icons.Default.FullscreenExit
val Home = Icons.Rounded.Home
val 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 StarBorder = Icons.Outlined.StarBorder
val Search = Icons.Rounded.Search
val MoreVert = Icons.Default.MoreVert
val Back = Icons.Rounded.ArrowBackIosNew
}
val Store = Icons.Rounded.Store
val StoreBorder = Icons.Outlined.Store
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()
}
}

View File

@@ -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)
}

View File

@@ -1,7 +0,0 @@
package top.fatweb.oxygen.toolbox.model
enum class DarkThemeConfig {
FOLLOW_SYSTEM,
LIGHT,
DARK,
}

View File

@@ -1,7 +0,0 @@
package top.fatweb.oxygen.toolbox.model
enum class LanguageConfig(val code: String? = null) {
FOLLOW_SYSTEM,
CHINESE("cn"),
ENGLISH("en")
}

View File

@@ -1,6 +0,0 @@
package top.fatweb.oxygen.toolbox.model
enum class LaunchPageConfig {
TOOLS,
STAR
}

View 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>
)

View File

@@ -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
}

View File

@@ -1,6 +0,0 @@
package top.fatweb.oxygen.toolbox.model
enum class ThemeBrandConfig {
DEFAULT,
ANDROID
}

View File

@@ -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>
)

View File

@@ -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
)

View File

@@ -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
)

View File

@@ -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
)

View File

@@ -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
)

View File

@@ -0,0 +1,8 @@
package top.fatweb.oxygen.toolbox.model.lib
import kotlinx.serialization.Serializable
@Serializable
data class Metadata(
val generated: String
)

View File

@@ -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
)

View File

@@ -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
)

View File

@@ -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
}
}

View File

@@ -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()
)

View File

@@ -0,0 +1,7 @@
package top.fatweb.oxygen.toolbox.model.userdata
enum class DarkThemeConfig {
FollowSystem,
Light,
Dark,
}

View File

@@ -0,0 +1,7 @@
package top.fatweb.oxygen.toolbox.model.userdata
enum class LanguageConfig(val code: String? = null) {
FollowSystem,
Chinese("cn"),
English("en")
}

View File

@@ -0,0 +1,6 @@
package top.fatweb.oxygen.toolbox.model.userdata
enum class LaunchPageConfig {
Tools,
Star
}

View File

@@ -0,0 +1,6 @@
package top.fatweb.oxygen.toolbox.model.userdata
enum class ThemeBrandConfig {
Default,
Android
}

View File

@@ -1,4 +1,4 @@
package top.fatweb.oxygen.toolbox.model
package top.fatweb.oxygen.toolbox.model.userdata
data class UserData(
val languageConfig: LanguageConfig,

View File

@@ -57,13 +57,8 @@ internal class ConnectivityManagerNetworkMonitor @Inject constructor(
}
.conflate()
@Suppress("DEPRECATION")
private fun ConnectivityManager.isCurrentlyConnected() = when {
VERSION.SDK_INT >= VERSION_CODES.M ->
activeNetwork
?.let(::getNetworkCapabilities)
?.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
else -> activeNetworkInfo?.isConnected
} ?: false
}
private fun ConnectivityManager.isCurrentlyConnected() =
activeNetwork
?.let(::getNetworkCapabilities)
?.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) == true
}

View File

@@ -4,4 +4,4 @@ import kotlinx.coroutines.flow.Flow
interface NetworkMonitor {
val isOnline: Flow<Boolean>
}
}

View File

@@ -43,8 +43,8 @@ class TimeZoneBroadcastMonitor @Inject constructor(
val zonIdFromIntent = if (Build.VERSION.SDK_INT < VERSION_CODES.R) {
null
} else {
intent.getStringExtra(Intent.EXTRA_TIMEZONE)?.let { timeZoneId ->
val zoneId = ZoneId.of(timeZoneId, ZoneId.SHORT_IDS)
intent.getStringExtra(Intent.EXTRA_TIMEZONE)?.run {
val zoneId = ZoneId.of(this, ZoneId.SHORT_IDS)
zoneId.toKotlinTimeZone()
}
}
@@ -64,5 +64,9 @@ class TimeZoneBroadcastMonitor @Inject constructor(
.distinctUntilChanged()
.conflate()
.flowOn(ioDispatcher)
.shareIn(appScope, SharingStarted.WhileSubscribed(5.seconds.inWholeMilliseconds), 1)
}
.shareIn(
scope = appScope,
started = SharingStarted.WhileSubscribed(stopTimeoutMillis = 5.seconds.inWholeMilliseconds),
replay = 1
)
}

View File

@@ -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
)
}
}

View File

@@ -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)
}
}

View File

@@ -1,31 +1,73 @@
package top.fatweb.oxygen.toolbox.navigation
import android.os.Bundle
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier
import androidx.navigation.NavController
import androidx.navigation.NavDestination
import androidx.navigation.compose.NavHost
import top.fatweb.oxygen.toolbox.ui.OxygenAppState
import top.fatweb.oxygen.toolbox.ui.util.LocalFullScreen
@Composable
fun OxygenNavHost(
modifier: Modifier = Modifier,
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 fullScreen = LocalFullScreen.current
LaunchedEffect(navController) {
navController.addOnDestinationChangedListener(object :
NavController.OnDestinationChangedListener {
override fun onDestinationChanged(
controller: NavController,
destination: NavDestination,
arguments: Bundle?
) {
fullScreen.onStateChange.invoke(false)
}
})
}
NavHost(
modifier = modifier,
navController = navController,
startDestination = startDestination
) {
searchScreen(
aboutScreen(
onBackClick = navController::popBackStack,
onNavigateToLibraries = navController::navigateToLibraries
)
librariesScreen(
onBackClick = navController::popBackStack
)
toolStoreScreen(
isVertical = isVertical,
searchValue = searchValue,
searchCount = searchCount,
onNavigateToToolView = navController::navigateToToolView
)
toolsScreen(
isVertical = isVertical,
searchValue = searchValue,
onShowSnackbar = onShowSnackbar,
onNavigateToToolView = navController::navigateToToolView,
onNavigateToToolStore = { appState.navigateToTopLevelDestination(TopLevelDestination.ToolStore) }
)
starScreen(
isVertical = isVertical,
searchValue = searchValue,
onNavigateToToolView = navController::navigateToToolView
)
toolViewScreen(
onBackClick = navController::popBackStack
)
}
}
}

View File

@@ -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)
}
}

View File

@@ -1,18 +1,49 @@
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.star.StarRoute
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(
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
)
}
}
}

View File

@@ -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
)
}
}

View File

@@ -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
)
}
}

View File

@@ -1,16 +1,61 @@
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.tools.ToolsRoute
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(
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
)
}
}

View File

@@ -1,26 +1,38 @@
package top.fatweb.oxygen.toolbox.navigation
import androidx.annotation.StringRes
import androidx.compose.ui.graphics.vector.ImageVector
import top.fatweb.oxygen.toolbox.R
import top.fatweb.oxygen.toolbox.icon.OxygenIcons
enum class TopLevelDestination(
val route: String,
val selectedIcon: ImageVector,
val unselectedIcon: ImageVector,
val iconTextId: Int,
val titleTextId: Int
@StringRes val iconTextId: 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,
unselectedIcon = OxygenIcons.HomeBorder,
iconTextId = R.string.feature_tools_title,
titleTextId = R.string.feature_tools_title
),
STAR(
Star(
route = "star_route",
selectedIcon = OxygenIcons.Star,
unselectedIcon = OxygenIcons.StarBorder,
iconTextId = R.string.feature_star_title,
titleTextId = R.string.feature_star_title
)
}
}

View File

@@ -9,4 +9,4 @@ annotation class Dispatcher(val oxygenDispatcher: OxygenDispatchers)
enum class OxygenDispatchers {
Default,
IO
}
}

View File

@@ -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)
)

View File

@@ -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
)

View File

@@ -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)

View File

@@ -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
)

View File

@@ -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
)

View File

@@ -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
)

View File

@@ -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
)
}

View File

@@ -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)
}
}
}

View File

@@ -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()
}

View File

@@ -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'))
}
}

View File

@@ -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>
}

View File

@@ -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) }
}

View File

@@ -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>>
}

View File

@@ -0,0 +1,26 @@
package top.fatweb.oxygen.toolbox.repository.tool
import kotlinx.coroutines.flow.Flow
import top.fatweb.oxygen.toolbox.model.tool.ToolEntity
interface ToolRepository {
val toolViewTemplate: Flow<String>
fun getGlobalJsVariables(isDarkMode: Boolean): Flow<String>
fun getGlobalCssVariables(isDarkMode: Boolean): Flow<String>
fun getAllToolsStream(searchValue: String): Flow<List<ToolEntity>>
fun getStarToolsStream(searchValue: String): Flow<List<ToolEntity>>
fun getToolById(id: Long): Flow<ToolEntity?>
fun getToolByUsernameAndToolId(username: String, toolId: String): Flow<ToolEntity?>
suspend fun saveTool(toolEntity: ToolEntity)
suspend fun updateTool(toolEntity: ToolEntity)
suspend fun removeTool(toolEntity: ToolEntity)
}

View File

@@ -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)
}
}

View File

@@ -0,0 +1,43 @@
package top.fatweb.oxygen.toolbox.repository.tool.impl
import kotlinx.coroutines.flow.Flow
import top.fatweb.oxygen.toolbox.data.tool.ToolDataSource
import top.fatweb.oxygen.toolbox.data.tool.dao.ToolDao
import top.fatweb.oxygen.toolbox.model.tool.ToolEntity
import top.fatweb.oxygen.toolbox.repository.tool.ToolRepository
import javax.inject.Inject
class OfflineToolRepository @Inject constructor(
private val toolDataSource: ToolDataSource,
private val toolDao: ToolDao
) : ToolRepository {
override val toolViewTemplate: Flow<String>
get() = toolDataSource.toolViewTemplate
override fun getGlobalJsVariables(isDarkMode: Boolean): Flow<String> =
toolDataSource.getGlobalJsVariables(isDarkMode)
override fun getGlobalCssVariables(isDarkMode: Boolean): Flow<String> =
toolDataSource.getGlobalCssVariables(isDarkMode)
override fun getAllToolsStream(searchValue: String): Flow<List<ToolEntity>> =
toolDao.selectAllTools(searchValue)
override fun getStarToolsStream(searchValue: String): Flow<List<ToolEntity>> =
toolDao.selectStarTools(searchValue)
override fun getToolById(id: Long): Flow<ToolEntity?> =
toolDao.selectToolById(id)
override fun getToolByUsernameAndToolId(username: String, toolId: String): Flow<ToolEntity?> =
toolDao.selectToolByUsernameAndToolId(username, toolId)
override suspend fun saveTool(toolEntity: ToolEntity) =
toolDao.insertTool(toolEntity.copy(isInstalled = true))
override suspend fun updateTool(toolEntity: ToolEntity) =
toolDao.updateTool(toolEntity)
override suspend fun removeTool(toolEntity: ToolEntity) =
toolDao.deleteTool(toolEntity)
}

View File

@@ -1,11 +1,11 @@
package top.fatweb.oxygen.toolbox.repository
package top.fatweb.oxygen.toolbox.repository.userdata
import kotlinx.coroutines.flow.Flow
import top.fatweb.oxygen.toolbox.model.DarkThemeConfig
import top.fatweb.oxygen.toolbox.model.LanguageConfig
import top.fatweb.oxygen.toolbox.model.LaunchPageConfig
import top.fatweb.oxygen.toolbox.model.ThemeBrandConfig
import top.fatweb.oxygen.toolbox.model.UserData
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
interface UserDataRepository {
val userData: Flow<UserData>
@@ -19,4 +19,4 @@ interface UserDataRepository {
suspend fun setDarkThemeConfig(darkThemeConfig: DarkThemeConfig)
suspend fun setUseDynamicColor(useDynamicColor: Boolean)
}
}

View File

@@ -1,15 +1,16 @@
package top.fatweb.oxygen.toolbox.repository
package top.fatweb.oxygen.toolbox.repository.userdata.impl
import kotlinx.coroutines.flow.Flow
import top.fatweb.oxygen.toolbox.datastore.OxygenPreferencesDataSource
import top.fatweb.oxygen.toolbox.model.DarkThemeConfig
import top.fatweb.oxygen.toolbox.model.LanguageConfig
import top.fatweb.oxygen.toolbox.model.LaunchPageConfig
import top.fatweb.oxygen.toolbox.model.ThemeBrandConfig
import top.fatweb.oxygen.toolbox.model.UserData
import top.fatweb.oxygen.toolbox.data.userdata.OxygenPreferencesDataSource
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 top.fatweb.oxygen.toolbox.repository.userdata.UserDataRepository
import javax.inject.Inject
internal class OfflineFirstUserDataRepository @Inject constructor(
internal class LocalUserDataRepository @Inject constructor(
private val oxygenPreferencesDataSource: OxygenPreferencesDataSource
) : UserDataRepository {
override val userData: Flow<UserData> =
@@ -34,4 +35,4 @@ internal class OfflineFirstUserDataRepository @Inject constructor(
override suspend fun setUseDynamicColor(useDynamicColor: Boolean) {
oxygenPreferencesDataSource.setUseDynamicColor(useDynamicColor)
}
}
}

View File

@@ -1,16 +1,14 @@
package top.fatweb.oxygen.toolbox.ui
import androidx.activity.ComponentActivity
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
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.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.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
@@ -22,21 +20,30 @@ import androidx.compose.material3.SnackbarResult
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavDestination
import androidx.navigation.NavDestination.Companion.hierarchy
import top.fatweb.oxygen.toolbox.R
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.STAR_ROUTE
import top.fatweb.oxygen.toolbox.navigation.TOOLS_ROUTE
@@ -48,113 +55,205 @@ import top.fatweb.oxygen.toolbox.ui.component.OxygenNavigationBarItem
import top.fatweb.oxygen.toolbox.ui.component.OxygenNavigationRail
import top.fatweb.oxygen.toolbox.ui.component.OxygenNavigationRailItem
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.theme.GradientColors
import top.fatweb.oxygen.toolbox.ui.theme.LocalGradientColors
import top.fatweb.oxygen.toolbox.ui.util.FullScreen
import top.fatweb.oxygen.toolbox.ui.util.LocalFullScreen
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun OxygenApp(appState: OxygenAppState) {
val shouldShowGradientBackground =
appState.currentTopLevelDestination == TopLevelDestination.TOOLS
appState.currentDestination?.route == ABOUT_ROUTE
var showSettingsDialog by rememberSaveable {
mutableStateOf(false)
}
OxygenBackground {
OxygenGradientBackground(
gradientColors = if (shouldShowGradientBackground) LocalGradientColors.current else GradientColors()
) {
val destination = appState.currentTopLevelDestination
val context = LocalContext.current
val window = (context as ComponentActivity).window
val windowInsetsController = WindowInsetsControllerCompat(window, window.decorView)
var isFullScreen by remember { mutableStateOf(false) }
val snackbarHostState = remember { SnackbarHostState() }
val fullScreen = FullScreen(
enable = isFullScreen,
onStateChange = {
isFullScreen = it
}
)
val isOffline by appState.isOffline.collectAsStateWithLifecycle()
DisposableEffect(Unit) {
windowInsetsController.systemBarsBehavior =
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
val listener = WindowInsetsControllerCompat.OnControllableInsetsChangedListener { _, _ ->
isFullScreen = false
}
windowInsetsController.addOnControllableInsetsChangedListener(listener)
onDispose {
windowInsetsController.removeOnControllableInsetsChangedListener(listener)
}
}
val noConnectMessage = stringResource(R.string.no_connect)
LaunchedEffect(isFullScreen) {
if (isFullScreen) {
windowInsetsController.hide(WindowInsetsCompat.Type.systemBars())
} else {
windowInsetsController.show(WindowInsetsCompat.Type.systemBars())
}
}
LaunchedEffect(isOffline) {
if (isOffline) {
snackbarHostState.showSnackbar(
message = noConnectMessage,
duration = SnackbarDuration.Indefinite
CompositionLocalProvider(LocalFullScreen provides fullScreen) {
OxygenBackground {
OxygenGradientBackground(
gradientColors = if (shouldShowGradientBackground) LocalGradientColors.current else GradientColors()
) {
val destination = appState.currentTopLevelDestination
val snackbarHostState = remember { SnackbarHostState() }
val isOffline by appState.isOffline.collectAsStateWithLifecycle()
val noConnectMessage = stringResource(R.string.core_no_connect)
var canScroll by remember { mutableStateOf(true) }
val topAppBarScrollBehavior =
if (canScroll) TopAppBarDefaults.enterAlwaysScrollBehavior() else TopAppBarDefaults.pinnedScrollBehavior()
var activeSearch by remember { mutableStateOf(false) }
var searchValue by remember { mutableStateOf("") }
var searchCount by remember { mutableIntStateOf(0) }
LaunchedEffect(activeSearch) {
canScroll = !activeSearch
}
LaunchedEffect(destination) {
activeSearch = false
searchValue = ""
if (searchCount == 0) {
searchCount++
} else {
searchCount = 0
}
}
LaunchedEffect(isOffline) {
if (isOffline) {
snackbarHostState.showSnackbar(
message = noConnectMessage,
duration = SnackbarDuration.Indefinite
)
}
}
if (showSettingsDialog) {
SettingsDialog(
onDismiss = { showSettingsDialog = false },
onNavigateToLibraries = appState::navigateToLibraries,
onNavigateToAbout = appState::navigateToAbout
)
}
}
if (showSettingsDialog) {
SettingsDialog(
onDismiss = { showSettingsDialog = false }
)
}
Scaffold(
containerColor = Color.Transparent,
contentColor = MaterialTheme.colorScheme.onBackground,
contentWindowInsets = WindowInsets(0, 0, 0, 0),
snackbarHost = { SnackbarHost(snackbarHostState) },
bottomBar = {
if (appState.shouldShowBottomBar && destination != null) {
OxygenBottomBar(
destinations = appState.topLevelDestinations,
onNavigateToDestination = appState::navigateToTopLevelDestination,
currentDestination = appState.currentDestination
)
}
}
) { padding ->
Row(
Modifier
.fillMaxSize()
.padding(padding)
.consumeWindowInsets(padding)
.windowInsetsPadding(
WindowInsets.safeDrawing.only(
WindowInsetsSides.Horizontal
Scaffold(
modifier = Modifier
.nestedScroll(connection = topAppBarScrollBehavior.nestedScrollConnection),
containerColor = Color.Transparent,
contentColor = MaterialTheme.colorScheme.onBackground,
contentWindowInsets = WindowInsets(left = 0, top = 0, right = 0, bottom = 0),
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
bottomBar = {
AnimatedVisibility(
visible = appState.shouldShowBottomBar && destination != null
) {
OxygenBottomBar(
destinations = appState.topLevelDestinations,
currentDestination = appState.currentDestination,
onNavigateToDestination = appState::navigateToTopLevelDestination
)
)
) {
if (appState.shouldShowNavRail && destination != null) {
OxygenNavRail(
modifier = Modifier.safeDrawingPadding(),
destinations = appState.topLevelDestinations,
onNavigateToDestination = appState::navigateToTopLevelDestination,
currentDestination = appState.currentDestination
)
}
}
Column(
Modifier.fillMaxSize()
) { padding ->
Row(
Modifier
.fillMaxSize()
) {
if (destination != null) {
OxygenTopAppBar(
titleRes = destination.titleTextId,
navigationIcon = OxygenIcons.Search,
navigationIconContentDescription = stringResource(R.string.feature_settings_top_app_bar_navigation_icon_description),
actionIcon = OxygenIcons.MoreVert,
actionIconContentDescription = stringResource(R.string.feature_settings_top_app_bar_action_icon_description),
colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
containerColor = Color.Transparent
),
onNavigationClick = { appState.navigateToSearch() },
onActionClick = { showSettingsDialog = true }
AnimatedVisibility(
visible = appState.shouldShowNavRail && destination != null
) {
OxygenNavRail(
modifier = Modifier
.padding(padding)
.consumeWindowInsets(padding)
.safeDrawingPadding(),
destinations = appState.topLevelDestinations,
currentDestination = appState.currentDestination,
onNavigateToDestination = appState::navigateToTopLevelDestination
)
}
OxygenNavHost(
appState = appState,
onShowSnackbar = { message, action ->
snackbarHostState.showSnackbar(
message = message,
actionLabel = action,
duration = SnackbarDuration.Short
) == SnackbarResult.ActionPerformed
},
startDestination = when (appState.launchPageConfig) {
LaunchPageConfig.TOOLS -> TOOLS_ROUTE
LaunchPageConfig.STAR -> STAR_ROUTE
Column(
Modifier.fillMaxSize()
) {
AnimatedVisibility(
visible = destination != null
) {
OxygenTopAppBar(
scrollBehavior = topAppBarScrollBehavior,
title = {
destination?.let {
Text(
text = stringResource(destination.titleTextId),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
}
},
navigationIcon = OxygenIcons.Search,
navigationIconContentDescription = stringResource(R.string.feature_settings_top_app_bar_navigation_icon_description),
actionIcon = OxygenIcons.MoreVert,
actionIconContentDescription = stringResource(R.string.feature_settings_top_app_bar_action_icon_description),
activeSearch = activeSearch,
searchButtonPosition = SearchButtonPosition.Navigation,
query = searchValue,
colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
containerColor = Color.Transparent,
scrolledContainerColor = Color.Transparent
),
onNavigationClick = { activeSearch = true },
onActionClick = { showSettingsDialog = true },
onQueryChange = {
searchValue = it
},
onSearch = {
searchCount++
},
onCancelSearch = {
searchValue = ""
activeSearch = false
searchCount = 0
}
)
}
)
OxygenNavHost(
appState = appState,
startDestination = when (appState.launchPageConfig) {
LaunchPageConfig.Tools -> TOOLS_ROUTE
LaunchPageConfig.Star -> STAR_ROUTE
},
isVertical = appState.shouldShowBottomBar,
searchValue = searchValue,
searchCount = searchCount,
onShowSnackbar = { message, action ->
snackbarHostState.showSnackbar(
message = message,
actionLabel = action,
duration = SnackbarDuration.Short
) == SnackbarResult.ActionPerformed
}
)
}
}
}
}
@@ -166,8 +265,8 @@ fun OxygenApp(appState: OxygenAppState) {
private fun OxygenBottomBar(
modifier: Modifier = Modifier,
destinations: List<TopLevelDestination>,
onNavigateToDestination: (TopLevelDestination) -> Unit,
currentDestination: NavDestination?
currentDestination: NavDestination?,
onNavigateToDestination: (TopLevelDestination) -> Unit
) {
OxygenNavigationBar(
modifier = modifier
@@ -181,13 +280,13 @@ private fun OxygenBottomBar(
icon = {
Icon(
imageVector = destination.unselectedIcon,
contentDescription = null
contentDescription = stringResource(destination.iconTextId)
)
},
selectedIcon = {
Icon(
imageVector = destination.selectedIcon,
contentDescription = null
contentDescription = stringResource(destination.iconTextId)
)
},
onClick = { onNavigateToDestination(destination) }
@@ -200,8 +299,8 @@ private fun OxygenBottomBar(
private fun OxygenNavRail(
modifier: Modifier = Modifier,
destinations: List<TopLevelDestination>,
onNavigateToDestination: (TopLevelDestination) -> Unit,
currentDestination: NavDestination?
currentDestination: NavDestination?,
onNavigateToDestination: (TopLevelDestination) -> Unit
) {
OxygenNavigationRail(
modifier = modifier
@@ -215,13 +314,13 @@ private fun OxygenNavRail(
icon = {
Icon(
imageVector = destination.unselectedIcon,
contentDescription = null
contentDescription = stringResource(destination.iconTextId)
)
},
selectedIcon = {
Icon(
imageVector = destination.selectedIcon,
contentDescription = null
contentDescription = stringResource(destination.iconTextId)
)
},
onClick = { onNavigateToDestination(destination) }
@@ -232,5 +331,5 @@ private fun OxygenNavRail(
private fun NavDestination?.isTopLevelDestinationInHierarchy(destination: TopLevelDestination) =
this?.hierarchy?.any {
it.route?.contains(destination.name, true) ?: false
} ?: false
it.route?.equals(destination.route) == true
} == true

View File

@@ -17,14 +17,17 @@ import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
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.TimeZoneMonitor
import top.fatweb.oxygen.toolbox.navigation.STAR_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.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.navigateToToolStore
import top.fatweb.oxygen.toolbox.navigation.navigateToTools
import kotlin.time.Duration.Companion.seconds
@@ -56,21 +59,24 @@ fun rememberOxygenAppState(
@Stable
class OxygenAppState(
val windowSizeClass: WindowSizeClass,
private val windowSizeClass: WindowSizeClass,
networkMonitor: NetworkMonitor,
timeZoneMonitor: TimeZoneMonitor,
coroutineScope: CoroutineScope,
val navController: NavHostController,
val launchPageConfig: LaunchPageConfig
) {
val topLevelDestinations: List<TopLevelDestination> = TopLevelDestination.entries
val currentDestination: NavDestination?
@Composable get() = navController
.currentBackStackEntryAsState().value?.destination
val currentTopLevelDestination: TopLevelDestination?
@Composable get() = when (currentDestination?.route) {
TOOLS_ROUTE -> TopLevelDestination.TOOLS
STAR_ROUTE -> TopLevelDestination.STAR
TOOL_STORE_ROUTE -> TopLevelDestination.ToolStore
TOOLS_ROUTE -> TopLevelDestination.Tools
STAR_ROUTE -> TopLevelDestination.Star
else -> null
}
@@ -85,16 +91,14 @@ class OxygenAppState(
.stateIn(
scope = coroutineScope,
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
.stateIn(
scope = coroutineScope,
initialValue = TimeZone.currentSystemDefault(),
started = SharingStarted.WhileSubscribed(5.seconds.inWholeMilliseconds)
started = SharingStarted.WhileSubscribed(stopTimeoutMillis = 5.seconds.inWholeMilliseconds)
)
fun navigateToTopLevelDestination(topLevelDestination: TopLevelDestination) {
@@ -108,10 +112,13 @@ class OxygenAppState(
}
when (topLevelDestination) {
TopLevelDestination.TOOLS -> navController.navigateToTools(topLevelNavOptions)
TopLevelDestination.STAR -> navController.navigateToStar(topLevelNavOptions)
TopLevelDestination.ToolStore -> navController.navigateToToolStore(topLevelNavOptions)
TopLevelDestination.Tools -> navController.navigateToTools(topLevelNavOptions)
TopLevelDestination.Star -> navController.navigateToStar(topLevelNavOptions)
}
}
fun navigateToSearch() = navController.navigateToSearch()
}
fun navigateToLibraries() = navController.navigateToLibraries()
fun navigateToAbout() = navController.navigateToAbout()
}

View File

@@ -0,0 +1,207 @@
package top.fatweb.oxygen.toolbox.ui.about
import androidx.compose.animation.core.AnimationConstants
import androidx.compose.animation.graphics.ExperimentalAnimationGraphicsApi
import androidx.compose.animation.graphics.res.animatedVectorResource
import androidx.compose.animation.graphics.res.rememberAnimatedVectorPainter
import androidx.compose.animation.graphics.vector.AnimatedImageVector
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.safeDrawingPadding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.delay
import top.fatweb.oxygen.toolbox.R
import top.fatweb.oxygen.toolbox.icon.OxygenIcons
import top.fatweb.oxygen.toolbox.ui.component.OxygenTopAppBar
import top.fatweb.oxygen.toolbox.ui.theme.OxygenPreviews
import top.fatweb.oxygen.toolbox.ui.theme.OxygenTheme
import top.fatweb.oxygen.toolbox.ui.util.ResourcesUtils
@Composable
internal fun AboutRoute(
modifier: Modifier = Modifier,
onBackClick: () -> Unit,
onNavigateToLibraries: () -> Unit
) {
AboutScreen(
modifier = modifier.safeDrawingPadding(),
onBackClick = onBackClick,
onNavigateToLibraries = onNavigateToLibraries
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
internal fun AboutScreen(
modifier: Modifier = Modifier,
onBackClick: () -> Unit,
onNavigateToLibraries: () -> Unit
) {
val scrollState = rememberScrollState()
val topAppBarScrollBehavior =
TopAppBarDefaults.pinnedScrollBehavior(canScroll = { scrollState.maxValue > 0 })
Scaffold(
modifier = Modifier
.nestedScroll(connection = topAppBarScrollBehavior.nestedScrollConnection),
containerColor = Color.Transparent,
contentWindowInsets = WindowInsets(left = 0, top = 0, right = 0, bottom = 0),
) { padding ->
Column(
modifier = modifier
.fillMaxSize()
.padding(padding)
.consumeWindowInsets(padding)
.windowInsetsPadding(
WindowInsets.safeDrawing.only(
WindowInsetsSides.Horizontal
)
)
.verticalScroll(state = scrollState),
horizontalAlignment = Alignment.CenterHorizontally
) {
OxygenTopAppBar(
scrollBehavior = topAppBarScrollBehavior,
title = {
Text(
text = stringResource(R.string.feature_settings_more_about),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
},
navigationIcon = OxygenIcons.Back,
navigationIconContentDescription = stringResource(R.string.core_back),
colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
containerColor = Color.Transparent,
scrolledContainerColor = Color.Transparent
),
onNavigationClick = onBackClick
)
Spacer(Modifier.height(64.dp))
AboutAppInfo()
Spacer(Modifier.weight(1f))
AboutFooter(
onNavigateToLibraries = onNavigateToLibraries
)
}
}
}
@OptIn(ExperimentalAnimationGraphicsApi::class)
@Composable
private fun AboutAppInfo(
modifier: Modifier = Modifier
) {
val logo = AnimatedImageVector.animatedVectorResource(R.drawable.ic_launcher)
var atEnd by remember { mutableStateOf(false) }
LaunchedEffect(Unit) {
delay(AnimationConstants.DefaultDurationMillis.toLong())
atEnd = true
}
Column(
modifier = modifier,
horizontalAlignment = Alignment.CenterHorizontally
) {
Image(
modifier = Modifier
.width(160.dp),
painter = rememberAnimatedVectorPainter(animatedImageVector = logo, atEnd = atEnd),
contentDescription = stringResource(R.string.app_full_name)
)
Spacer(Modifier.height(16.dp))
Text(
style = MaterialTheme.typography.headlineMedium,
fontWeight = FontWeight.Bold,
text = stringResource(R.string.app_name)
)
Spacer(Modifier.height(8.dp))
Text(
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.outline,
text = stringResource(R.string.app_description)
)
Spacer(Modifier.height(8.dp))
Text(
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.outline,
text = "${ResourcesUtils.getAppVersionName(LocalContext.current)} (${
stringResource(
if (ResourcesUtils.getAppVersionCode(LocalContext.current) % 100 == 0L)
R.string.core_ga_version
else
R.string.core_beta_version
)
})"
)
}
}
@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 = {})
}
}

View File

@@ -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
)
}
}
}
}

View File

@@ -0,0 +1,292 @@
package top.fatweb.oxygen.toolbox.ui.about
import android.content.Intent
import android.net.Uri
import androidx.activity.compose.ReportDrawnWhen
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
import androidx.compose.foundation.layout.consumeWindowInsets
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.only
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.safeDrawing
import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.widthIn
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid
import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells
import androidx.compose.foundation.lazy.staggeredgrid.rememberLazyStaggeredGridState
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import top.fatweb.oxygen.toolbox.R
import top.fatweb.oxygen.toolbox.icon.OxygenIcons
import top.fatweb.oxygen.toolbox.ui.component.Indicator
import top.fatweb.oxygen.toolbox.ui.component.OxygenTopAppBar
import top.fatweb.oxygen.toolbox.ui.component.scrollbar.DraggableScrollbar
import top.fatweb.oxygen.toolbox.ui.component.scrollbar.rememberDraggableScroller
import top.fatweb.oxygen.toolbox.ui.component.scrollbar.scrollbarState
import top.fatweb.oxygen.toolbox.ui.theme.OxygenPreviews
import top.fatweb.oxygen.toolbox.ui.theme.OxygenTheme
@Composable
internal fun LibrariesRoute(
modifier: Modifier = Modifier,
viewModel: LibrariesScreenViewModel = hiltViewModel(),
onBackClick: () -> Unit
) {
val librariesScreenUiState by viewModel.librariesScreenUiState.collectAsStateWithLifecycle()
LibrariesScreen(
modifier = modifier,
librariesScreenUiState = librariesScreenUiState,
onBackClick = onBackClick,
onSearch = { viewModel.onSearchValueChange(it) }
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
internal fun LibrariesScreen(
modifier: Modifier = Modifier,
librariesScreenUiState: LibrariesScreenUiState,
onBackClick: () -> Unit,
onSearch: (String) -> Unit
) {
val configuration = LocalConfiguration.current
val context = LocalContext.current
val isLibrariesLoading = librariesScreenUiState is LibrariesScreenUiState.Loading
ReportDrawnWhen { !isLibrariesLoading }
val itemsAvailable = howManyItems(librariesScreenUiState)
val state = rememberLazyStaggeredGridState()
val scrollbarState = state.scrollbarState(itemsAvailable = itemsAvailable)
var showDialog by remember { mutableStateOf(false) }
var dialogTitle by remember { mutableStateOf("") }
var dialogContent by remember { mutableStateOf("") }
var dialogUrl by remember { mutableStateOf("") }
var canScroll by remember { mutableStateOf(true) }
val topAppBarScrollBehavior =
if (canScroll) TopAppBarDefaults.enterAlwaysScrollBehavior() else TopAppBarDefaults.pinnedScrollBehavior()
var activeSearch by remember { mutableStateOf(false) }
var searchValue by remember { mutableStateOf("") }
LaunchedEffect(activeSearch) {
canScroll = !activeSearch
}
Scaffold(
modifier = Modifier
.nestedScroll(connection = topAppBarScrollBehavior.nestedScrollConnection),
containerColor = Color.Transparent,
contentWindowInsets = WindowInsets(left = 0, top = 0, right = 0, bottom = 0),
) { padding ->
Column(
modifier
.fillMaxWidth()
.padding(padding)
.consumeWindowInsets(padding)
.windowInsetsPadding(
WindowInsets.safeDrawing.only(
WindowInsetsSides.Horizontal
)
)
) {
OxygenTopAppBar(
scrollBehavior = topAppBarScrollBehavior,
title = {
Text(
text = stringResource(R.string.feature_settings_open_source_license),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
},
navigationIcon = OxygenIcons.Back,
navigationIconContentDescription = stringResource(R.string.core_back),
actionIcon = OxygenIcons.Search,
actionIconContentDescription = stringResource(R.string.core_search),
activeSearch = activeSearch,
query = searchValue,
colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
containerColor = Color.Transparent,
scrolledContainerColor = Color.Transparent
),
onNavigationClick = onBackClick,
onActionClick = {
activeSearch = true
},
onQueryChange = {
searchValue = it
onSearch(it)
},
onSearch = onSearch,
onCancelSearch = {
searchValue = ""
activeSearch = false
onSearch("")
}
)
Box {
when (librariesScreenUiState) {
LibrariesScreenUiState.Loading -> {
Indicator()
}
LibrariesScreenUiState.Nothing -> {
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(state = rememberScrollState()),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(text = stringResource(R.string.core_nothing))
}
}
LibrariesScreenUiState.NotFound -> {
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(state = rememberScrollState()),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
Text(text = stringResource(R.string.core_nothing_found))
}
}
is LibrariesScreenUiState.Success -> {
val handleOnClickLicense = { key: String ->
val license = librariesScreenUiState.dependencies.licenses[key]
if (license != null) {
showDialog = true
dialogTitle = license.name
dialogContent = license.content ?: ""
dialogUrl = license.url ?: ""
}
}
LazyVerticalStaggeredGrid(
modifier = Modifier
.fillMaxSize(),
columns = StaggeredGridCells.Adaptive(300.dp),
contentPadding = PaddingValues(16.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalItemSpacing = 24.dp,
state = state
) {
librariesPanel(
librariesScreenUiState = librariesScreenUiState,
onClickLicense = handleOnClickLicense
)
}
state.DraggableScrollbar(
modifier = Modifier
.fillMaxHeight()
.windowInsetsPadding(WindowInsets.systemBars)
.padding(horizontal = 2.dp)
.align(Alignment.CenterEnd),
state = scrollbarState,
orientation = Orientation.Vertical,
onThumbMoved = state.rememberDraggableScroller(itemsAvailable = itemsAvailable)
)
}
}
}
}
}
if (showDialog) {
AlertDialog(
modifier = Modifier
.widthIn(max = configuration.screenWidthDp.dp - 80.dp)
.heightIn(max = configuration.screenHeightDp.dp - 40.dp),
onDismissRequest = { showDialog = false },
title = {
Text(
text = dialogTitle,
style = MaterialTheme.typography.titleLarge
)
},
text = {
Column(
modifier = Modifier.verticalScroll(state = rememberScrollState())
) {
Text(text = dialogContent)
}
},
confirmButton = {
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
TextButton(onClick = {
context.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(dialogUrl)))
}) {
Text(text = stringResource(R.string.core_website))
}
TextButton(onClick = { showDialog = false }) {
Text(text = stringResource(R.string.core_close))
}
}
}
)
}
}
private fun howManyItems(librariesScreenUiState: LibrariesScreenUiState) =
when (librariesScreenUiState) {
LibrariesScreenUiState.Loading, LibrariesScreenUiState.Nothing, LibrariesScreenUiState.NotFound -> 0
is LibrariesScreenUiState.Success -> librariesScreenUiState.dependencies.libraries.size
}
@OxygenPreviews
@Composable
private fun LibrariesScreenLoadingPreview() {
OxygenTheme {
LibrariesScreen(
librariesScreenUiState = LibrariesScreenUiState.Loading,
onBackClick = {},
onSearch = {})
}
}

View File

@@ -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"

View File

@@ -1,6 +1,5 @@
package top.fatweb.oxygen.toolbox.ui.component
import android.content.res.Configuration
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
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.graphics.Brush
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 top.fatweb.oxygen.toolbox.ui.theme.GradientColors
import top.fatweb.oxygen.toolbox.ui.theme.LocalBackgroundTheme
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 kotlin.math.tan
@@ -92,11 +91,7 @@ fun OxygenGradientBackground(
}
}
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO, name = "Light theme")
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES, name = "Dark theme")
annotation class ThemePreviews
@ThemePreviews
@OxygenPreviews
@Composable
fun BackgroundDefault() {
OxygenTheme(dynamicColor = false) {
@@ -104,7 +99,7 @@ fun BackgroundDefault() {
}
}
@ThemePreviews
@OxygenPreviews
@Composable
fun BackgroundDynamic() {
OxygenTheme(dynamicColor = true) {
@@ -112,7 +107,7 @@ fun BackgroundDynamic() {
}
}
@ThemePreviews
@OxygenPreviews
@Composable
fun BackgroundAndroid() {
OxygenTheme(androidTheme = true) {
@@ -120,7 +115,7 @@ fun BackgroundAndroid() {
}
}
@ThemePreviews
@OxygenPreviews
@Composable
fun GradientBackgroundDefault() {
OxygenTheme(dynamicColor = false) {
@@ -128,7 +123,7 @@ fun GradientBackgroundDefault() {
}
}
@ThemePreviews
@OxygenPreviews
@Composable
fun GradientBackgroundDynamic() {
OxygenTheme(dynamicColor = true) {
@@ -136,7 +131,7 @@ fun GradientBackgroundDynamic() {
}
}
@ThemePreviews
@OxygenPreviews
@Composable
fun GradientBackgroundAndroid() {
OxygenTheme(androidTheme = true) {

View File

@@ -0,0 +1,46 @@
package top.fatweb.oxygen.toolbox.ui.component
import androidx.annotation.StringRes
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.LinkAnnotation
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.withStyle
import top.fatweb.oxygen.toolbox.ui.util.ResourcesUtils
@Composable
fun ClickableText(
@StringRes text: Int,
@StringRes replaceText: Int,
onClick: () -> Unit
) {
val context = LocalContext.current
val primaryColor = MaterialTheme.colorScheme.primary
val annotatedString = buildAnnotatedString {
val clickablePart = ResourcesUtils.getString(
context = context,
resId = replaceText
)
val mainText = ResourcesUtils.getString(
context = context,
resId = text,
clickablePart
)
append(mainText.substringBefore(clickablePart))
pushLink(LinkAnnotation.Clickable(
tag = "Click",
linkInteractionListener = { onClick() }
))
withStyle(style = SpanStyle(color = primaryColor)) {
append(clickablePart)
}
pop()
append(mainText.substringAfter(clickablePart))
}
Text(text = annotatedString)
}

View File

@@ -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)
}
}

View File

@@ -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

View File

@@ -17,6 +17,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import top.fatweb.oxygen.toolbox.navigation.TopLevelDestination
import top.fatweb.oxygen.toolbox.ui.theme.OxygenPreviews
import top.fatweb.oxygen.toolbox.ui.theme.OxygenTheme
@Composable
@@ -26,16 +27,15 @@ fun RowScope.OxygenNavigationBarItem(
label: @Composable (() -> Unit)? = null,
icon: @Composable () -> Unit,
selectedIcon: @Composable () -> Unit,
onClick: () -> Unit,
enabled: Boolean = true,
alwaysShowLabel: Boolean = false
alwaysShowLabel: Boolean = false,
onClick: () -> Unit
) {
NavigationBarItem(
modifier = modifier,
selected = selected,
label = label,
icon = if (selected) selectedIcon else icon,
onClick = onClick,
enabled = enabled,
alwaysShowLabel = alwaysShowLabel,
colors = NavigationBarItemDefaults.colors(
@@ -44,7 +44,8 @@ fun RowScope.OxygenNavigationBarItem(
selectedTextColor = OxygenNavigationDefaults.navigationSelectedItemColor(),
unselectedTextColor = OxygenNavigationDefaults.navigationContentColor(),
indicatorColor = OxygenNavigationDefaults.navigationIndicatorColor()
)
),
onClick = onClick
)
}
@@ -68,16 +69,15 @@ fun OxygenNavigationRailItem(
label: @Composable (() -> Unit)? = null,
icon: @Composable () -> Unit,
selectedIcon: @Composable () -> Unit,
onClick: () -> Unit,
enabled: Boolean = true,
alwaysShowLabel: Boolean = true
alwaysShowLabel: Boolean = true,
onClick: () -> Unit
) {
NavigationRailItem(
modifier = modifier,
selected = selected,
label = label,
icon = if (selected) selectedIcon else icon,
onClick = onClick,
enabled = enabled,
alwaysShowLabel = alwaysShowLabel,
colors = NavigationRailItemDefaults.colors(
@@ -86,7 +86,8 @@ fun OxygenNavigationRailItem(
selectedTextColor = OxygenNavigationDefaults.navigationSelectedItemColor(),
unselectedTextColor = OxygenNavigationDefaults.navigationContentColor(),
indicatorColor = OxygenNavigationDefaults.navigationIndicatorColor()
)
),
onClick = onClick
)
}
@@ -116,9 +117,9 @@ object OxygenNavigationDefaults {
fun navigationIndicatorColor() = MaterialTheme.colorScheme.primaryContainer
}
@ThemePreviews
@OxygenPreviews
@Composable
fun OxygenNavigationBarPreview() {
private fun OxygenNavigationBarPreview() {
val items = TopLevelDestination.entries
OxygenTheme {
@@ -130,13 +131,13 @@ fun OxygenNavigationBarPreview() {
icon = {
Icon(
imageVector = item.unselectedIcon,
contentDescription = stringResource(item.titleTextId)
contentDescription = stringResource(item.iconTextId)
)
},
selectedIcon = {
Icon(
imageVector = item.selectedIcon, contentDescription = stringResource(
item.titleTextId
item.iconTextId
)
)
},
@@ -147,9 +148,9 @@ fun OxygenNavigationBarPreview() {
}
}
@ThemePreviews
@OxygenPreviews
@Composable
fun OxygenNavigationRailPreview() {
private fun OxygenNavigationRailPreview() {
val items = TopLevelDestination.entries
OxygenTheme {
@@ -161,13 +162,13 @@ fun OxygenNavigationRailPreview() {
icon = {
Icon(
imageVector = item.unselectedIcon,
contentDescription = stringResource(item.titleTextId)
contentDescription = stringResource(item.iconTextId)
)
},
selectedIcon = {
Icon(
imageVector = item.selectedIcon, contentDescription = stringResource(
item.titleTextId
item.iconTextId
)
)
},
@@ -176,4 +177,4 @@ fun OxygenNavigationRailPreview() {
}
}
}
}
}

View File

@@ -1,20 +1,44 @@
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.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults
import androidx.compose.material3.TopAppBarColors
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.material3.TopAppBarScrollBehavior
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.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.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.tooling.preview.Preview
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import top.fatweb.oxygen.toolbox.R
import top.fatweb.oxygen.toolbox.icon.OxygenIcons
import top.fatweb.oxygen.toolbox.ui.theme.OxygenPreviews
import top.fatweb.oxygen.toolbox.ui.theme.OxygenTheme
import android.R as androidR
@@ -22,51 +46,155 @@ import android.R as androidR
@Composable
fun OxygenTopAppBar(
modifier: Modifier = Modifier,
@StringRes titleRes: Int,
navigationIcon: ImageVector,
navigationIconContentDescription: String,
actionIcon: ImageVector,
actionIconContentDescription: String,
expandedHeight: Dp = 48.dp,
scrollBehavior: TopAppBarScrollBehavior? = null,
title: @Composable () -> Unit = {},
navigationIcon: ImageVector? = null,
navigationIconContentDescription: String? = null,
actionIcon: ImageVector? = null,
actionIconContentDescription: String? = null,
activeSearch: Boolean = false,
searchButtonPosition: SearchButtonPosition = SearchButtonPosition.Action,
query: String = "",
colors: TopAppBarColors = TopAppBarDefaults.centerAlignedTopAppBarColors(),
onNavigationClick: () -> Unit = {},
onActionClick: () -> Unit = {}
onActionClick: () -> Unit = {},
onQueryChange: (String) -> Unit = {},
onSearch: (String) -> Unit = {},
onCancelSearch: () -> Unit = {}
) {
val topInset by animateIntAsState(
targetValue = if (scrollBehavior != null && -scrollBehavior.state.heightOffset >= with(
LocalDensity.current
) { expandedHeight.toPx() }
) 0
else TopAppBarDefaults.windowInsets.getTop(LocalDensity.current),
label = ""
)
val keyboardController = LocalSoftwareKeyboardController.current
val focusRequester = remember { FocusRequester() }
val onSearchExplicitlyTriggered = {
keyboardController?.hide()
onSearch(query)
}
LaunchedEffect(activeSearch) {
if (activeSearch) {
focusRequester.requestFocus()
}
}
CenterAlignedTopAppBar(
modifier = modifier,
title = { Text(stringResource(titleRes)) },
scrollBehavior = scrollBehavior,
expandedHeight = expandedHeight,
title = {
if (activeSearch) TextField(
modifier = Modifier
.fillMaxWidth()
.background(Color.Transparent)
.focusRequester(focusRequester)
.onKeyEvent {
if (it.key == Key.Enter) {
onSearchExplicitlyTriggered()
true
} else {
false
}
},
value = query,
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
keyboardActions = KeyboardActions(
onSearch = {
onSearchExplicitlyTriggered()
}
),
colors = TextFieldDefaults.colors(
unfocusedContainerColor = Color.Transparent,
focusedContainerColor = Color.Transparent,
unfocusedIndicatorColor = Color.Transparent,
focusedIndicatorColor = Color.Transparent
),
leadingIcon = {
Icon(
imageVector = OxygenIcons.Search,
contentDescription = stringResource(R.string.core_search)
)
},
maxLines = 1,
singleLine = true,
textStyle = MaterialTheme.typography.titleSmall,
onValueChange = {
if ("\n" !in it) onQueryChange(it)
}
)
else title()
},
navigationIcon = {
IconButton(onClick = onNavigationClick) {
if (activeSearch && searchButtonPosition == SearchButtonPosition.Navigation) IconButton(
onClick = onCancelSearch
) {
Icon(
imageVector = navigationIcon,
contentDescription = navigationIconContentDescription,
tint = MaterialTheme.colorScheme.onSurface
imageVector = OxygenIcons.Close,
contentDescription = stringResource(R.string.core_close)
)
}
else navigationIcon?.let {
IconButton(onClick = onNavigationClick) {
Icon(
imageVector = navigationIcon,
contentDescription = navigationIconContentDescription,
tint = MaterialTheme.colorScheme.onSurface
)
}
}
},
actions = {
IconButton(onClick = onActionClick) {
if (activeSearch && searchButtonPosition == SearchButtonPosition.Action) IconButton(
onClick = onCancelSearch
) {
Icon(
imageVector = actionIcon,
contentDescription = actionIconContentDescription,
tint = MaterialTheme.colorScheme.onSurface
imageVector = OxygenIcons.Close,
contentDescription = stringResource(R.string.core_close)
)
}
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)
@Preview
@OxygenPreviews
@Composable
private fun OxygenTopAppBarPreview() {
OxygenTheme {
OxygenTopAppBar(
titleRes = androidR.string.untitled,
title = {
Text(
text = stringResource(androidR.string.untitled),
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
},
navigationIcon = OxygenIcons.Search,
navigationIconContentDescription = "Navigation icon",
actionIcon = OxygenIcons.MoreVert,
actionIconContentDescription = "Action icon"
)
}
}
}

View File

@@ -0,0 +1,385 @@
package top.fatweb.oxygen.toolbox.ui.component
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.valentinilk.shimmer.LocalShimmerTheme
import com.valentinilk.shimmer.ShimmerBounds
import com.valentinilk.shimmer.rememberShimmer
import com.valentinilk.shimmer.shimmer
import com.valentinilk.shimmer.shimmerSpec
import top.fatweb.oxygen.toolbox.R
import top.fatweb.oxygen.toolbox.icon.OxygenIcons
import top.fatweb.oxygen.toolbox.model.tool.ToolEntity
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun ToolCard(
modifier: Modifier = Modifier,
tool: ToolEntity,
specifyVer: String? = null,
actionIcon: ImageVector? = null,
actionIconContentDescription: String = "",
onAction: () -> Unit = {},
onClick: () -> Unit = {},
onLongClick: () -> Unit = {}
) {
Card(
modifier = modifier
.clip(RoundedCornerShape(8.dp))
.combinedClickable(
onClick = onClick,
onLongClick = onLongClick
),
shape = RoundedCornerShape(8.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface)
) {
Column(
modifier = Modifier.padding(16.dp)
) {
ToolHeader(
ver = specifyVer ?: tool.ver,
actionIcon = actionIcon,
actionIconContentDescription = actionIconContentDescription,
onAction = onAction
)
Spacer(Modifier.height(16.dp))
ToolIcon(icon = tool.icon)
Spacer(Modifier.height(16.dp))
ToolInfo(
toolName = tool.name,
toolId = tool.toolId,
toolDesc = tool.description
)
Spacer(Modifier.height(16.dp))
AuthorInfo(
avatar = tool.authorAvatar,
nickname = tool.authorNickname
)
}
}
}
@Composable
private fun ToolHeader(
modifier: Modifier = Modifier,
ver: String,
actionIcon: ImageVector?,
actionIconContentDescription: String,
onAction: () -> Unit
) {
Row(
modifier = modifier
.height(28.dp)
) {
ToolVer(ver = ver)
Spacer(Modifier.weight(1f))
actionIcon?.let {
ToolAction(
actionIcon = actionIcon,
actionIconContentDescription = actionIconContentDescription,
onAction = onAction
)
}
}
}
@Composable
private fun ToolVer(
modifier: Modifier = Modifier,
ver: String
) {
Card(
modifier = modifier
.fillMaxHeight(),
shape = RoundedCornerShape(8.dp),
colors = CardDefaults.cardColors(contentColor = MaterialTheme.colorScheme.onSecondaryContainer)
) {
Column(
modifier = Modifier
.fillMaxHeight()
.background(color = MaterialTheme.colorScheme.surfaceContainer)
.padding(horizontal = 8.dp, vertical = 4.dp),
Arrangement.Center,
Alignment.CenterHorizontally
) {
Text(
style = MaterialTheme.typography.bodyMedium,
text = ver
)
}
}
}
@Composable
private fun ToolAction(
modifier: Modifier = Modifier,
actionIcon: ImageVector,
actionIconContentDescription: String,
onAction: () -> Unit
) {
Card(
modifier = modifier
.fillMaxHeight()
.clip(RoundedCornerShape(8.dp))
.clickable(
onClick = onAction
),
shape = RoundedCornerShape(8.dp),
colors = CardDefaults.cardColors(contentColor = MaterialTheme.colorScheme.onSecondaryContainer)
) {
Box(
modifier = Modifier
.fillMaxHeight()
.background(color = MaterialTheme.colorScheme.surfaceContainer)
.padding(horizontal = 6.dp, vertical = 6.dp)
) {
Icon(
imageVector = actionIcon,
contentDescription = actionIconContentDescription
)
}
}
}
@Composable
private fun ToolIcon(
modifier: Modifier = Modifier,
icon: String
) {
Box(
modifier = modifier
.fillMaxWidth(),
contentAlignment = Alignment.Center
) {
Image(
modifier = Modifier.size(80.dp),
bitmap = OxygenIcons.fromSvgBase64(icon),
contentDescription = ""
)
}
}
@Composable
private fun ToolInfo(
modifier: Modifier = Modifier,
toolName: String,
toolId: String,
toolDesc: String?
) {
Column(
modifier = modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
Text(
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.ExtraBold,
textAlign = TextAlign.Center,
text = toolName
)
Text(
style = MaterialTheme.typography.bodyMedium,
textAlign = TextAlign.Center,
text = "ID: $toolId"
)
toolDesc?.let {
Text(
modifier = Modifier.padding(horizontal = 16.dp, vertical = 8.dp),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.outline,
text = "${stringResource(R.string.feature_tools_description)}: $it"
)
}
}
}
@Composable
private fun AuthorInfo(
modifier: Modifier = Modifier,
avatar: String,
nickname: String
) {
Row(
modifier = modifier
.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally)
) {
Surface(
modifier = Modifier
.size(24.dp),
shape = RoundedCornerShape(12.dp)
) {
Image(
modifier = Modifier
.background(MaterialTheme.colorScheme.surfaceContainer),
bitmap = OxygenIcons.fromPngBase64(avatar), contentDescription = "Avatar"
)
}
Text(
style = MaterialTheme.typography.bodyMedium,
text = nickname
)
}
}
@Composable
fun ToolCardSkeleton(
modifier: Modifier = Modifier
) {
val shimmer = rememberShimmer(
shimmerBounds = ShimmerBounds.Window,
theme = LocalShimmerTheme.current.copy(
animationSpec = infiniteRepeatable(
animation = shimmerSpec(
durationMillis = 1_500,
easing = LinearEasing,
delayMillis = 200
),
repeatMode = RepeatMode.Restart
)
)
)
Card(
modifier = modifier
.clip(RoundedCornerShape(8.dp)),
shape = RoundedCornerShape(8.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface)
) {
Column(
modifier = Modifier.padding(16.dp)
) {
Row(
modifier = Modifier
.height(28.dp)
) {
Surface(
modifier = modifier
.fillMaxHeight()
.width(64.dp)
.shimmer(shimmer),
shape = RoundedCornerShape(8.dp),
color = MaterialTheme.colorScheme.surfaceContainer
) {}
}
Spacer(Modifier.height(16.dp))
Box(
modifier = Modifier
.fillMaxWidth(),
contentAlignment = Alignment.Center
) {
Surface(
modifier = Modifier
.size(80.dp)
.shimmer(shimmer),
shape = RoundedCornerShape(8.dp),
color = MaterialTheme.colorScheme.surfaceContainer
) {}
}
Spacer(Modifier.height(16.dp))
Column(
modifier = modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(6.dp)
) {
Surface(
modifier = Modifier
.size(width = 40.dp, height = 22.dp)
.shimmer(shimmer),
shape = RoundedCornerShape(4.dp),
color = MaterialTheme.colorScheme.surfaceContainer
) {}
Surface(
modifier = Modifier
.size(width = 60.dp, height = 20.dp)
.shimmer(shimmer),
shape = RoundedCornerShape(4.dp),
color = MaterialTheme.colorScheme.surfaceContainer
) {}
Column(
verticalArrangement = Arrangement.spacedBy(2.dp)
) {
Surface(
modifier = Modifier
.size(width = 120.dp, height = 12.dp)
.shimmer(shimmer),
shape = RoundedCornerShape(4.dp),
color = MaterialTheme.colorScheme.surfaceContainer
) {}
Surface(
modifier = Modifier
.size(width = 120.dp, height = 12.dp)
.shimmer(shimmer),
shape = RoundedCornerShape(4.dp),
color = MaterialTheme.colorScheme.surfaceContainer
) {}
Surface(
modifier = Modifier
.size(width = 80.dp, height = 12.dp)
.shimmer(shimmer),
shape = RoundedCornerShape(4.dp),
color = MaterialTheme.colorScheme.surfaceContainer
) {}
}
}
Spacer(Modifier.height(16.dp))
Row(
modifier = Modifier
.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally)
) {
Surface(
modifier = Modifier
.size(24.dp)
.shimmer(shimmer),
shape = RoundedCornerShape(12.dp),
color = MaterialTheme.colorScheme.surfaceContainer
) {}
Surface(
modifier = Modifier
.size(width = 120.dp, height = 12.dp)
.shimmer(shimmer),
shape = RoundedCornerShape(4.dp),
color = MaterialTheme.colorScheme.surfaceContainer
) {}
}
}
}
}
const val DEFAULT_TOOL_CARD_SKELETON_COUNT = 20

View File

@@ -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()
})
}
}*/

View File

@@ -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,
}

Some files were not shown because too many files have changed in this diff Show More