Compare commits
60 Commits
af01156bdc
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
4f4a9049f8
|
|||
|
4acbe85ea1
|
|||
|
47fffb5541
|
|||
|
210da5db8a
|
|||
|
b8c2446335
|
|||
|
bb21f9cfad
|
|||
|
df60f2e84a
|
|||
|
774bb84f7f
|
|||
|
0245c04122
|
|||
|
df158db1e9
|
|||
|
44b6f1b52f
|
|||
|
746934d421
|
|||
|
f2e1b26a59
|
|||
|
c9b9fabf86
|
|||
|
ac3d1efc60
|
|||
|
c51fe59b4d
|
|||
|
5bfb0dcfcb
|
|||
|
3e12247600
|
|||
|
a73e6ad767
|
|||
|
02cc6c8bf2
|
|||
|
f0f25a4d48
|
|||
|
d947e8234d
|
|||
|
ff021c6eec
|
|||
|
91a3ddff4e
|
|||
|
8b278ba0d7
|
|||
|
d3e10583d3
|
|||
|
ac9626616d
|
|||
|
57e490fe28
|
|||
|
9e269a9e03
|
|||
|
2746b71575
|
|||
|
c905548f37
|
|||
|
97c256ec06
|
|||
|
1c54c4dc59
|
|||
|
47321c7ae6
|
|||
|
dea934155d
|
|||
|
fb629fa909
|
|||
|
7c55ecf419
|
|||
|
248250dea3
|
|||
| 7d3a257794 | |||
|
89e3c510b3
|
|||
|
ddf84e2028
|
|||
|
1415a940f4
|
|||
|
acdce5c3f7
|
|||
|
529bdcd82f
|
|||
|
11022d0d04
|
|||
|
859ebd208e
|
|||
|
6e009da8cc
|
|||
|
8655961e46
|
|||
|
9f13d1e888
|
|||
|
cdd4ff4eb4
|
|||
|
2a6edad196
|
|||
|
98451e55e2
|
|||
|
e2b860c4da
|
|||
|
9436b36697
|
|||
|
4dbbd11bb9
|
|||
|
2aa6732e1d
|
|||
|
1c24b7d0b2
|
|||
|
b68b9d0b4b
|
|||
|
b786eb3182
|
|||
|
c6268d92d1
|
2
.idea/.name
generated
2
.idea/.name
generated
@@ -1 +1 @@
|
|||||||
Tokusho
|
Shirizu
|
||||||
48
.idea/androidTestResultsUserPreferences.xml
generated
Normal file
48
.idea/androidTestResultsUserPreferences.xml
generated
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="AndroidTestResultsUserPreferences">
|
||||||
|
<option name="androidTestResultsTableState">
|
||||||
|
<map>
|
||||||
|
<entry key="-639820289">
|
||||||
|
<value>
|
||||||
|
<AndroidTestResultsTableState>
|
||||||
|
<option name="preferredColumnWidths">
|
||||||
|
<map>
|
||||||
|
<entry key="Duration" value="90" />
|
||||||
|
<entry key="Pixel_API_34" value="120" />
|
||||||
|
<entry key="Tests" value="360" />
|
||||||
|
</map>
|
||||||
|
</option>
|
||||||
|
</AndroidTestResultsTableState>
|
||||||
|
</value>
|
||||||
|
</entry>
|
||||||
|
<entry key="83150218">
|
||||||
|
<value>
|
||||||
|
<AndroidTestResultsTableState>
|
||||||
|
<option name="preferredColumnWidths">
|
||||||
|
<map>
|
||||||
|
<entry key="Duration" value="90" />
|
||||||
|
<entry key="Pixel_5_API_34" value="120" />
|
||||||
|
<entry key="Tests" value="360" />
|
||||||
|
</map>
|
||||||
|
</option>
|
||||||
|
</AndroidTestResultsTableState>
|
||||||
|
</value>
|
||||||
|
</entry>
|
||||||
|
<entry key="1878637654">
|
||||||
|
<value>
|
||||||
|
<AndroidTestResultsTableState>
|
||||||
|
<option name="preferredColumnWidths">
|
||||||
|
<map>
|
||||||
|
<entry key="Duration" value="90" />
|
||||||
|
<entry key="Pixel_5_API_34" value="120" />
|
||||||
|
<entry key="Tests" value="360" />
|
||||||
|
</map>
|
||||||
|
</option>
|
||||||
|
</AndroidTestResultsTableState>
|
||||||
|
</value>
|
||||||
|
</entry>
|
||||||
|
</map>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
41
.idea/appInsightsSettings.xml
generated
Normal file
41
.idea/appInsightsSettings.xml
generated
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="AppInsightsSettings">
|
||||||
|
<option name="selectedTabId" value="Android Vitals" />
|
||||||
|
<option name="tabSettings">
|
||||||
|
<map>
|
||||||
|
<entry key="Android Vitals">
|
||||||
|
<value>
|
||||||
|
<InsightsFilterSettings>
|
||||||
|
<option name="connection">
|
||||||
|
<ConnectionSetting>
|
||||||
|
<option name="appId" value="org.spray.qmanga" />
|
||||||
|
</ConnectionSetting>
|
||||||
|
</option>
|
||||||
|
<option name="signal" value="SIGNAL_UNSPECIFIED" />
|
||||||
|
<option name="timeIntervalDays" value="SEVEN_DAYS" />
|
||||||
|
<option name="visibilityType" value="ALL" />
|
||||||
|
</InsightsFilterSettings>
|
||||||
|
</value>
|
||||||
|
</entry>
|
||||||
|
<entry key="Firebase Crashlytics">
|
||||||
|
<value>
|
||||||
|
<InsightsFilterSettings>
|
||||||
|
<option name="connection">
|
||||||
|
<ConnectionSetting>
|
||||||
|
<option name="appId" value="PLACEHOLDER" />
|
||||||
|
<option name="mobileSdkAppId" value="" />
|
||||||
|
<option name="projectId" value="" />
|
||||||
|
<option name="projectNumber" value="" />
|
||||||
|
</ConnectionSetting>
|
||||||
|
</option>
|
||||||
|
<option name="signal" value="SIGNAL_UNSPECIFIED" />
|
||||||
|
<option name="timeIntervalDays" value="THIRTY_DAYS" />
|
||||||
|
<option name="visibilityType" value="ALL" />
|
||||||
|
</InsightsFilterSettings>
|
||||||
|
</value>
|
||||||
|
</entry>
|
||||||
|
</map>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
15
.idea/deploymentTargetDropDown.xml
generated
15
.idea/deploymentTargetDropDown.xml
generated
@@ -3,20 +3,7 @@
|
|||||||
<component name="deploymentTargetDropDown">
|
<component name="deploymentTargetDropDown">
|
||||||
<value>
|
<value>
|
||||||
<entry key="app">
|
<entry key="app">
|
||||||
<State>
|
<State />
|
||||||
<targetSelectedWithDropDown>
|
|
||||||
<Target>
|
|
||||||
<type value="QUICK_BOOT_TARGET" />
|
|
||||||
<deviceKey>
|
|
||||||
<Key>
|
|
||||||
<type value="VIRTUAL_DEVICE_PATH" />
|
|
||||||
<value value="C:\Users\xtimms\.android\avd\Pixel_API_34.avd" />
|
|
||||||
</Key>
|
|
||||||
</deviceKey>
|
|
||||||
</Target>
|
|
||||||
</targetSelectedWithDropDown>
|
|
||||||
<timeTargetWasSelectedWithDropDown value="2024-01-29T14:26:34.807344800Z" />
|
|
||||||
</State>
|
|
||||||
</entry>
|
</entry>
|
||||||
</value>
|
</value>
|
||||||
</component>
|
</component>
|
||||||
|
|||||||
9
.idea/other.xml
generated
Normal file
9
.idea/other.xml
generated
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ScreenshotViewer">
|
||||||
|
<option name="frameScreenshot" value="true" />
|
||||||
|
</component>
|
||||||
|
<component name="direct_access_persist.xml">
|
||||||
|
<option name="selectedCloudProject" />
|
||||||
|
</component>
|
||||||
|
</project>
|
||||||
23
README.md
23
README.md
@@ -1,20 +1,35 @@
|
|||||||
# Tokusho
|
<div align="center">
|
||||||
|
|
||||||
An attempt to write an Android manga reading application on Jetpack Compose using the [Kotatsu parser library](https://github.com/KotatsuApp/kotatsu-parsers).
|
<a>
|
||||||
|
<img src="./app/src/main/res/mipmap-xxxhdpi/ic_launcher.png" alt="Shirizu logo" title="Shirizu logo" width="80"/>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
# Shirizu
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
Shirizu (シリーズ, from Japanese - series) - An attempt to write an Android manga reading application on Jetpack Compose using the [Kotatsu parser library](https://github.com/KotatsuApp/kotatsu-parsers).
|
||||||
|
|
||||||
## Is it possible to use it now?
|
## Is it possible to use it now?
|
||||||
|
|
||||||
No, nothing works.
|
No, nothing works.
|
||||||
|
|
||||||
|
## Screens
|
||||||
|
| Library | Explore | Search |
|
||||||
|
|:------------------------------------------:|:--------------------------------------------:|:-------------------------------------------:|
|
||||||
|
|  |  |  |
|
||||||
|
|
||||||
## Acknowledgements
|
## Acknowledgements
|
||||||
|
|
||||||
- [Kotatsu](https://github.com/KotatsuApp/Kotatsu) - UI and parsers
|
- [Kotatsu](https://github.com/KotatsuApp/Kotatsu) - UI, parsers, under the hood
|
||||||
|
- [Mihon](https://github.com/mihonapp/mihon) - UI, under the hood
|
||||||
- [Seal](https://github.com/JunkFood02/Seal) - UI
|
- [Seal](https://github.com/JunkFood02/Seal) - UI
|
||||||
|
- [Tivi](https://github.com/chrisbanes/tivi) - UI
|
||||||
|
- [Buckwheat](https://github.com/danilkinkin/buckwheat) - UI
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
You may copy, distribute and modify the software as long as you track changes/dates in source files.
|
You may copy, distribute and modify the software as long as you track changes/dates in source files.
|
||||||
Any modifications to or software including (via compiler) GPL-licensed code must also be made available under the
|
Any modifications to or software including (via compiler) GPL-licensed code must also be made available under the
|
||||||
GPL along with build & install instructions.
|
GPL along with build & install instructions.
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,46 +1,98 @@
|
|||||||
|
import com.android.build.gradle.internal.cxx.configure.gradleLocalProperties
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.Date
|
||||||
|
import java.util.TimeZone
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("com.android.application")
|
id("com.android.application")
|
||||||
id("org.jetbrains.kotlin.android")
|
id("org.jetbrains.kotlin.android")
|
||||||
id("org.jetbrains.kotlin.plugin.serialization")
|
id("org.jetbrains.kotlin.plugin.serialization")
|
||||||
id("org.jetbrains.kotlin.kapt")
|
id("org.jetbrains.kotlin.kapt")
|
||||||
|
id("org.jetbrains.kotlin.plugin.parcelize")
|
||||||
id("com.google.devtools.ksp")
|
id("com.google.devtools.ksp")
|
||||||
|
id("com.mikepenz.aboutlibraries.plugin")
|
||||||
id("dagger.hilt.android.plugin")
|
id("dagger.hilt.android.plugin")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val acraAuthLogin: String =
|
||||||
|
gradleLocalProperties(rootDir).getProperty("authLogin") ?: "\"acra_login\""
|
||||||
|
val acraAuthPassword: String =
|
||||||
|
gradleLocalProperties(rootDir).getProperty("authPassword") ?: "\"acra_password\""
|
||||||
|
|
||||||
|
val shikimoriClientId: String =
|
||||||
|
gradleLocalProperties(rootDir).getProperty("shikimoriClientId") ?: "\"shikimori\""
|
||||||
|
val shikimoriClientSecret: String =
|
||||||
|
gradleLocalProperties(rootDir).getProperty("shikimoriClientSecret") ?: "\"shikimori\""
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "org.xtimms.tokusho"
|
namespace = "org.xtimms.shirizu"
|
||||||
compileSdk = 34
|
compileSdk = 34
|
||||||
|
|
||||||
defaultConfig {
|
defaultConfig {
|
||||||
applicationId = "org.xtimms.tokusho"
|
applicationId = "org.xtimms.shirizu"
|
||||||
minSdk = 26
|
minSdk = 26
|
||||||
targetSdk = 34
|
targetSdk = 34
|
||||||
versionCode = 1
|
versionCode = 1
|
||||||
versionName = "1.0"
|
versionName = "1.0"
|
||||||
|
|
||||||
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
|
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
||||||
|
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
|
||||||
|
buildConfigField("String", "BUILD_TIME", "\"${getBuildTime()}\"")
|
||||||
|
|
||||||
|
buildConfigField("String", "ACRA_URI", "\"https://bugs.kotatsu.app/report\"")
|
||||||
|
buildConfigField("String", "ACRA_AUTH_LOGIN", acraAuthLogin)
|
||||||
|
buildConfigField("String", "ACRA_AUTH_PASSWORD", acraAuthPassword)
|
||||||
|
|
||||||
|
buildConfigField("String", "SHIKIMORI_CLIENT_ID", shikimoriClientId)
|
||||||
|
buildConfigField("String", "SHIKIMORI_CLIENT_SECRET", shikimoriClientSecret)
|
||||||
|
|
||||||
|
testInstrumentationRunner = "org.xtimms.shirizu.HiltTestRunner"
|
||||||
vectorDrawables {
|
vectorDrawables {
|
||||||
useSupportLibrary = true
|
useSupportLibrary = true
|
||||||
}
|
}
|
||||||
javaCompileOptions {
|
ksp {
|
||||||
annotationProcessorOptions {
|
arg("room.schemaLocation", "$projectDir/schemas")
|
||||||
arguments += mapOf(
|
|
||||||
"room.generateKotlin" to "true"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
release {
|
named("debug") {
|
||||||
|
versionNameSuffix = "-${getCommitCount()}"
|
||||||
|
applicationIdSuffix = ".debug"
|
||||||
|
}
|
||||||
|
named("release") {
|
||||||
|
isShrinkResources = true
|
||||||
isMinifyEnabled = true
|
isMinifyEnabled = true
|
||||||
proguardFiles(
|
proguardFiles(
|
||||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||||
"proguard-rules.pro"
|
"proguard-rules.pro"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
create("benchmark") {
|
||||||
|
initWith(getByName("release"))
|
||||||
|
signingConfig = signingConfigs.getByName("debug")
|
||||||
|
matchingFallbacks.add("release")
|
||||||
|
isDebuggable = false
|
||||||
|
isProfileable = true
|
||||||
|
versionNameSuffix = "-benchmark"
|
||||||
|
applicationIdSuffix = ".benchmark"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sourceSets {
|
||||||
|
getByName("benchmark").res.srcDirs("src/debug/res")
|
||||||
|
}
|
||||||
|
flavorDimensions.add("default")
|
||||||
|
productFlavors {
|
||||||
|
create("standard") {
|
||||||
|
dimension = "default"
|
||||||
|
}
|
||||||
|
create("dev") {
|
||||||
|
dimension = "default"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
compileOptions {
|
compileOptions {
|
||||||
|
isCoreLibraryDesugaringEnabled = true
|
||||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||||
targetCompatibility = JavaVersion.VERSION_1_8
|
targetCompatibility = JavaVersion.VERSION_1_8
|
||||||
}
|
}
|
||||||
@@ -49,6 +101,7 @@ android {
|
|||||||
}
|
}
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
compose = true
|
compose = true
|
||||||
|
viewBinding = true
|
||||||
buildConfig = true
|
buildConfig = true
|
||||||
}
|
}
|
||||||
composeOptions {
|
composeOptions {
|
||||||
@@ -59,48 +112,131 @@ android {
|
|||||||
excludes += "/META-INF/{AL2.0,LGPL2.1}"
|
excludes += "/META-INF/{AL2.0,LGPL2.1}"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
androidResources {
|
||||||
|
generateLocaleConfig = true
|
||||||
|
}
|
||||||
|
kapt {
|
||||||
|
correctErrorTypes = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
|
||||||
|
// AndroidX
|
||||||
implementation("androidx.core:core-ktx:1.12.0")
|
implementation("androidx.core:core-ktx:1.12.0")
|
||||||
|
implementation("androidx.core:core-splashscreen:1.0.1")
|
||||||
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0")
|
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0")
|
||||||
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.7.0")
|
implementation("androidx.lifecycle:lifecycle-runtime-compose:2.7.0")
|
||||||
implementation("androidx.lifecycle:lifecycle-process:2.7.0")
|
implementation("androidx.lifecycle:lifecycle-process:2.7.0")
|
||||||
implementation("androidx.activity:activity-compose:1.8.2")
|
implementation("androidx.activity:activity-compose:1.8.2")
|
||||||
implementation(platform("androidx.compose:compose-bom:2024.01.00"))
|
implementation(platform("dev.chrisbanes.compose:compose-bom:2024.05.00-alpha02"))
|
||||||
|
implementation("androidx.compose.animation:animation-graphics")
|
||||||
implementation("androidx.compose.ui:ui")
|
implementation("androidx.compose.ui:ui")
|
||||||
implementation("androidx.compose.ui:ui-graphics")
|
implementation("androidx.compose.ui:ui-graphics")
|
||||||
|
implementation("androidx.compose.ui:ui-text-google-fonts")
|
||||||
implementation("androidx.compose.ui:ui-tooling-preview")
|
implementation("androidx.compose.ui:ui-tooling-preview")
|
||||||
implementation("androidx.compose.material:material-icons-extended:1.6.0")
|
implementation("androidx.compose.material:material-icons-extended:1.6.5")
|
||||||
implementation("androidx.compose.material3:material3-android:1.2.0-rc01")
|
implementation("androidx.compose.material3:material3-android:1.2.1")
|
||||||
implementation("androidx.compose.material3:material3-window-size-class:1.2.0-rc01")
|
implementation("androidx.compose.material3:material3-window-size-class:1.2.1")
|
||||||
implementation("androidx.hilt:hilt-navigation-compose:1.1.0")
|
implementation("androidx.hilt:hilt-navigation-compose:1.2.0")
|
||||||
implementation("androidx.navigation:navigation-compose:2.7.6")
|
implementation("androidx.navigation:navigation-compose:2.7.7")
|
||||||
|
implementation("androidx.paging:paging-runtime-ktx:3.2.1")
|
||||||
|
implementation("androidx.paging:paging-compose:3.2.1")
|
||||||
|
implementation("androidx.preference:preference-ktx:1.2.1")
|
||||||
|
implementation("androidx.profileinstaller:profileinstaller:1.3.1")
|
||||||
implementation("androidx.room:room-runtime:2.6.1")
|
implementation("androidx.room:room-runtime:2.6.1")
|
||||||
implementation("androidx.room:room-ktx:2.6.1")
|
implementation("androidx.room:room-ktx:2.6.1")
|
||||||
implementation("androidx.work:work-runtime-ktx:2.9.0")
|
implementation("androidx.work:work-runtime-ktx:2.9.0")
|
||||||
|
implementation("androidx.room:room-testing:2.6.1")
|
||||||
|
implementation("androidx.webkit:webkit:1.11.0")
|
||||||
ksp("androidx.room:room-compiler:2.6.1")
|
ksp("androidx.room:room-compiler:2.6.1")
|
||||||
|
|
||||||
|
// Navigation
|
||||||
|
implementation("cafe.adriel.voyager:voyager-navigator:1.1.0-alpha04")
|
||||||
|
implementation("cafe.adriel.voyager:voyager-hilt:1.1.0-alpha04")
|
||||||
|
implementation("cafe.adriel.voyager:voyager-screenmodel:1.1.0-alpha04")
|
||||||
|
implementation("cafe.adriel.voyager:voyager-tab-navigator:1.1.0-alpha04")
|
||||||
|
implementation("cafe.adriel.voyager:voyager-transitions:1.1.0-alpha04")
|
||||||
|
|
||||||
|
// Motion
|
||||||
|
implementation("io.github.fornewid:material-motion-compose-core:1.2.0")
|
||||||
|
|
||||||
|
implementation("ch.acra:acra-http:5.9.7")
|
||||||
|
implementation("com.github.solkin:disk-lru-cache:1.4")
|
||||||
implementation("com.google.android.material:material:1.11.0")
|
implementation("com.google.android.material:material:1.11.0")
|
||||||
|
implementation("com.google.accompanist:accompanist-flowlayout:0.32.0")
|
||||||
implementation("com.google.accompanist:accompanist-systemuicontroller:0.32.0")
|
implementation("com.google.accompanist:accompanist-systemuicontroller:0.32.0")
|
||||||
implementation("com.google.accompanist:accompanist-pager:0.32.0")
|
implementation("com.google.accompanist:accompanist-pager:0.32.0")
|
||||||
implementation("com.google.accompanist:accompanist-pager-indicators:0.32.0")
|
implementation("com.google.accompanist:accompanist-pager-indicators:0.32.0")
|
||||||
implementation("com.google.dagger:hilt-android:2.50")
|
implementation("com.google.accompanist:accompanist-permissions:0.32.0")
|
||||||
kapt("com.google.dagger:hilt-compiler:2.50")
|
implementation("com.google.dagger:hilt-android:2.51.1")
|
||||||
implementation("androidx.hilt:hilt-work:1.1.0")
|
kapt("com.google.dagger:hilt-compiler:2.51.1")
|
||||||
kapt("androidx.hilt:hilt-compiler:1.1.0")
|
implementation("androidx.hilt:hilt-work:1.2.0")
|
||||||
implementation("com.github.KotatsuApp:kotatsu-parsers:a8f9423307")
|
kapt("androidx.hilt:hilt-compiler:1.2.0")
|
||||||
|
implementation("com.github.KotatsuApp:kotatsu-parsers:b404b44008") {
|
||||||
|
exclude(group = "org.json", module = "json")
|
||||||
|
}
|
||||||
|
implementation("com.mikepenz:aboutlibraries-compose-m3:10.10.0")
|
||||||
implementation("com.squareup.okhttp3:okhttp:4.12.0")
|
implementation("com.squareup.okhttp3:okhttp:4.12.0")
|
||||||
implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:4.12.0")
|
implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:4.12.0")
|
||||||
implementation("com.squareup.okio:okio:3.7.0")
|
implementation("com.squareup.okio:okio:3.8.0")
|
||||||
implementation("com.tencent:mmkv:1.3.2")
|
implementation("com.tencent:mmkv:1.3.2")
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.7")
|
implementation("org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.7")
|
||||||
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-guava:1.7.3")
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2")
|
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2")
|
||||||
implementation("io.coil-kt:coil-compose:2.5.0")
|
implementation("io.coil-kt:coil-compose:2.5.0")
|
||||||
|
implementation("me.saket.telephoto:zoomable-image-coil:0.8.0")
|
||||||
testImplementation("junit:junit:4.13.2")
|
testImplementation("junit:junit:4.13.2")
|
||||||
androidTestImplementation("androidx.test.ext:junit:1.1.5")
|
androidTestImplementation("androidx.test.ext:junit:1.1.5")
|
||||||
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
|
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
|
||||||
androidTestImplementation(platform("androidx.compose:compose-bom:2023.08.00"))
|
androidTestImplementation(platform("androidx.compose:compose-bom:2023.08.00"))
|
||||||
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
|
androidTestImplementation("androidx.compose.ui:ui-test-junit4")
|
||||||
|
androidTestImplementation("com.squareup.moshi:moshi-kotlin:1.15.1")
|
||||||
|
androidTestImplementation("com.google.dagger:hilt-android-testing:2.51.1")
|
||||||
|
kaptAndroidTest("com.google.dagger:hilt-android-compiler:2.51.1")
|
||||||
debugImplementation("androidx.compose.ui:ui-tooling")
|
debugImplementation("androidx.compose.ui:ui-tooling")
|
||||||
debugImplementation("androidx.compose.ui:ui-test-manifest")
|
debugImplementation("androidx.compose.ui:ui-test-manifest")
|
||||||
|
debugImplementation("com.github.koitharu:workinspector:5778dd1747")
|
||||||
|
coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.4")
|
||||||
|
}
|
||||||
|
|
||||||
|
androidComponents {
|
||||||
|
beforeVariants { variantBuilder ->
|
||||||
|
// Disables standardBenchmark
|
||||||
|
if (variantBuilder.buildType == "benchmark") {
|
||||||
|
variantBuilder.enable = variantBuilder.productFlavors.containsAll(listOf("default" to "dev"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
onVariants(selector().withFlavor("default" to "standard")) {
|
||||||
|
// Only excluding in standard flavor because this breaks
|
||||||
|
// Layout Inspector's Compose tree
|
||||||
|
it.packaging.resources.excludes.add("META-INF/*.version")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Git is needed in your system PATH for these commands to work.
|
||||||
|
// If it's not installed, you can return a random value as a workaround
|
||||||
|
fun Project.getCommitCount(): String {
|
||||||
|
return runCommand("git rev-list --count HEAD")
|
||||||
|
// return "1"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Project.getGitSha(): String {
|
||||||
|
return runCommand("git rev-parse --short HEAD")
|
||||||
|
// return "1"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Project.getBuildTime(): String {
|
||||||
|
val df = SimpleDateFormat("yyyy-MM-dd'T'HH:mm'Z'")
|
||||||
|
df.timeZone = TimeZone.getTimeZone("UTC")
|
||||||
|
return df.format(Date())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Project.runCommand(command: String): String {
|
||||||
|
val byteOut = ByteArrayOutputStream()
|
||||||
|
project.exec {
|
||||||
|
commandLine = command.split(" ")
|
||||||
|
standardOutput = byteOut
|
||||||
|
}
|
||||||
|
return String(byteOut.toByteArray()).trim()
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,793 @@
|
|||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 2,
|
||||||
|
"identityHash": "37a2caa74779de4fe989f19a8cb1eaf6",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "manga",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`manga_id` INTEGER NOT NULL, `title` TEXT NOT NULL, `alt_title` TEXT, `url` TEXT NOT NULL, `public_url` TEXT NOT NULL, `rating` REAL NOT NULL, `nsfw` INTEGER NOT NULL, `cover_url` TEXT NOT NULL, `large_cover_url` TEXT, `state` TEXT, `author` TEXT, `source` TEXT NOT NULL, PRIMARY KEY(`manga_id`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "manga_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "title",
|
||||||
|
"columnName": "title",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "altTitle",
|
||||||
|
"columnName": "alt_title",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "publicUrl",
|
||||||
|
"columnName": "public_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "rating",
|
||||||
|
"columnName": "rating",
|
||||||
|
"affinity": "REAL",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isNsfw",
|
||||||
|
"columnName": "nsfw",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "coverUrl",
|
||||||
|
"columnName": "cover_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "largeCoverUrl",
|
||||||
|
"columnName": "large_cover_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "state",
|
||||||
|
"columnName": "state",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "author",
|
||||||
|
"columnName": "author",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "source",
|
||||||
|
"columnName": "source",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"manga_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "tags",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tag_id` INTEGER NOT NULL, `title` TEXT NOT NULL, `key` TEXT NOT NULL, `source` TEXT NOT NULL, PRIMARY KEY(`tag_id`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "tag_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "title",
|
||||||
|
"columnName": "title",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "key",
|
||||||
|
"columnName": "key",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "source",
|
||||||
|
"columnName": "source",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"tag_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "manga_tags",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`manga_id` INTEGER NOT NULL, `tag_id` INTEGER NOT NULL, PRIMARY KEY(`manga_id`, `tag_id`), FOREIGN KEY(`manga_id`) REFERENCES `manga`(`manga_id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`tag_id`) REFERENCES `tags`(`tag_id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "mangaId",
|
||||||
|
"columnName": "manga_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "tagId",
|
||||||
|
"columnName": "tag_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"manga_id",
|
||||||
|
"tag_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_manga_tags_manga_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"manga_id"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_manga_tags_manga_id` ON `${TABLE_NAME}` (`manga_id`)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "index_manga_tags_tag_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"tag_id"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_manga_tags_tag_id` ON `${TABLE_NAME}` (`tag_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "manga",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"manga_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"manga_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"table": "tags",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"tag_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"tag_id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "sources",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`source` TEXT NOT NULL, `enabled` INTEGER NOT NULL, `sort_key` INTEGER NOT NULL, PRIMARY KEY(`source`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "source",
|
||||||
|
"columnName": "source",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isEnabled",
|
||||||
|
"columnName": "enabled",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "sortKey",
|
||||||
|
"columnName": "sort_key",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"source"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_sources_sort_key",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"sort_key"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_sources_sort_key` ON `${TABLE_NAME}` (`sort_key`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "history",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`manga_id` INTEGER NOT NULL, `created_at` INTEGER NOT NULL, `updated_at` INTEGER NOT NULL, `chapter_id` INTEGER NOT NULL, `page` INTEGER NOT NULL, `scroll` REAL NOT NULL, `percent` REAL NOT NULL, `deleted_at` INTEGER NOT NULL, PRIMARY KEY(`manga_id`), FOREIGN KEY(`manga_id`) REFERENCES `manga`(`manga_id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "mangaId",
|
||||||
|
"columnName": "manga_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "createdAt",
|
||||||
|
"columnName": "created_at",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "updatedAt",
|
||||||
|
"columnName": "updated_at",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "chapterId",
|
||||||
|
"columnName": "chapter_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "page",
|
||||||
|
"columnName": "page",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "scroll",
|
||||||
|
"columnName": "scroll",
|
||||||
|
"affinity": "REAL",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "percent",
|
||||||
|
"columnName": "percent",
|
||||||
|
"affinity": "REAL",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "deletedAt",
|
||||||
|
"columnName": "deleted_at",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"manga_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "manga",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"manga_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"manga_id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "favourites",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`manga_id` INTEGER NOT NULL, `category_id` INTEGER NOT NULL, `sort_key` INTEGER NOT NULL, `created_at` INTEGER NOT NULL, `deleted_at` INTEGER NOT NULL, PRIMARY KEY(`manga_id`, `category_id`), FOREIGN KEY(`manga_id`) REFERENCES `manga`(`manga_id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`category_id`) REFERENCES `favourite_categories`(`category_id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "mangaId",
|
||||||
|
"columnName": "manga_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "categoryId",
|
||||||
|
"columnName": "category_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "sortKey",
|
||||||
|
"columnName": "sort_key",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "createdAt",
|
||||||
|
"columnName": "created_at",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "deletedAt",
|
||||||
|
"columnName": "deleted_at",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"manga_id",
|
||||||
|
"category_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_favourites_manga_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"manga_id"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_favourites_manga_id` ON `${TABLE_NAME}` (`manga_id`)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "index_favourites_category_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"category_id"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_favourites_category_id` ON `${TABLE_NAME}` (`category_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "manga",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"manga_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"manga_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"table": "favourite_categories",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"category_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"category_id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "favourite_categories",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`category_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `created_at` INTEGER NOT NULL, `sort_key` INTEGER NOT NULL, `title` TEXT NOT NULL, `order` TEXT NOT NULL, `track` INTEGER NOT NULL, `show_in_lib` INTEGER NOT NULL, `deleted_at` INTEGER NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "categoryId",
|
||||||
|
"columnName": "category_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "createdAt",
|
||||||
|
"columnName": "created_at",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "sortKey",
|
||||||
|
"columnName": "sort_key",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "title",
|
||||||
|
"columnName": "title",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "order",
|
||||||
|
"columnName": "order",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "track",
|
||||||
|
"columnName": "track",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isVisibleInLibrary",
|
||||||
|
"columnName": "show_in_lib",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "deletedAt",
|
||||||
|
"columnName": "deleted_at",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"category_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "bookmarks",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`manga_id` INTEGER NOT NULL, `page_id` INTEGER NOT NULL, `chapter_id` INTEGER NOT NULL, `page` INTEGER NOT NULL, `scroll` INTEGER NOT NULL, `image` TEXT NOT NULL, `created_at` INTEGER NOT NULL, `percent` REAL NOT NULL, PRIMARY KEY(`manga_id`, `page_id`), FOREIGN KEY(`manga_id`) REFERENCES `manga`(`manga_id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "mangaId",
|
||||||
|
"columnName": "manga_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "pageId",
|
||||||
|
"columnName": "page_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "chapterId",
|
||||||
|
"columnName": "chapter_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "page",
|
||||||
|
"columnName": "page",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "scroll",
|
||||||
|
"columnName": "scroll",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "imageUrl",
|
||||||
|
"columnName": "image",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "createdAt",
|
||||||
|
"columnName": "created_at",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "percent",
|
||||||
|
"columnName": "percent",
|
||||||
|
"affinity": "REAL",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"manga_id",
|
||||||
|
"page_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_bookmarks_manga_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"manga_id"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_bookmarks_manga_id` ON `${TABLE_NAME}` (`manga_id`)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "index_bookmarks_page_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"page_id"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_bookmarks_page_id` ON `${TABLE_NAME}` (`page_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "manga",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"manga_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"manga_id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "suggestions",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`manga_id` INTEGER NOT NULL, `relevance` REAL NOT NULL, `created_at` INTEGER NOT NULL, PRIMARY KEY(`manga_id`), FOREIGN KEY(`manga_id`) REFERENCES `manga`(`manga_id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "mangaId",
|
||||||
|
"columnName": "manga_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "relevance",
|
||||||
|
"columnName": "relevance",
|
||||||
|
"affinity": "REAL",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "createdAt",
|
||||||
|
"columnName": "created_at",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"manga_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_suggestions_manga_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"manga_id"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_suggestions_manga_id` ON `${TABLE_NAME}` (`manga_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "manga",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"manga_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"manga_id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "tracks",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`manga_id` INTEGER NOT NULL, `chapters_total` INTEGER NOT NULL, `last_chapter_id` INTEGER NOT NULL, `chapters_new` INTEGER NOT NULL, `last_check` INTEGER NOT NULL, `last_notified_id` INTEGER NOT NULL, PRIMARY KEY(`manga_id`), FOREIGN KEY(`manga_id`) REFERENCES `manga`(`manga_id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "mangaId",
|
||||||
|
"columnName": "manga_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "totalChapters",
|
||||||
|
"columnName": "chapters_total",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastChapterId",
|
||||||
|
"columnName": "last_chapter_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "newChapters",
|
||||||
|
"columnName": "chapters_new",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastCheck",
|
||||||
|
"columnName": "last_check",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastNotifiedChapterId",
|
||||||
|
"columnName": "last_notified_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"manga_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "manga",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"manga_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"manga_id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "track_logs",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `manga_id` INTEGER NOT NULL, `chapters` TEXT NOT NULL, `created_at` INTEGER NOT NULL, FOREIGN KEY(`manga_id`) REFERENCES `manga`(`manga_id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "mangaId",
|
||||||
|
"columnName": "manga_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "chapters",
|
||||||
|
"columnName": "chapters",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "createdAt",
|
||||||
|
"columnName": "created_at",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_track_logs_manga_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"manga_id"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_track_logs_manga_id` ON `${TABLE_NAME}` (`manga_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "manga",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"manga_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"manga_id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "stats",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`manga_id` INTEGER NOT NULL, `started_at` INTEGER NOT NULL, `duration` INTEGER NOT NULL, `pages` INTEGER NOT NULL, PRIMARY KEY(`manga_id`, `started_at`), FOREIGN KEY(`manga_id`) REFERENCES `history`(`manga_id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "mangaId",
|
||||||
|
"columnName": "manga_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "startedAt",
|
||||||
|
"columnName": "started_at",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "duration",
|
||||||
|
"columnName": "duration",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "pages",
|
||||||
|
"columnName": "pages",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"manga_id",
|
||||||
|
"started_at"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "history",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"manga_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"manga_id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"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, '37a2caa74779de4fe989f19a8cb1eaf6')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,857 @@
|
|||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 3,
|
||||||
|
"identityHash": "dbe1dcac0f49c5ae2ac88d88aa280081",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "manga",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`manga_id` INTEGER NOT NULL, `title` TEXT NOT NULL, `alt_title` TEXT, `url` TEXT NOT NULL, `public_url` TEXT NOT NULL, `rating` REAL NOT NULL, `nsfw` INTEGER NOT NULL, `cover_url` TEXT NOT NULL, `large_cover_url` TEXT, `state` TEXT, `author` TEXT, `source` TEXT NOT NULL, PRIMARY KEY(`manga_id`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "manga_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "title",
|
||||||
|
"columnName": "title",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "altTitle",
|
||||||
|
"columnName": "alt_title",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "publicUrl",
|
||||||
|
"columnName": "public_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "rating",
|
||||||
|
"columnName": "rating",
|
||||||
|
"affinity": "REAL",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isNsfw",
|
||||||
|
"columnName": "nsfw",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "coverUrl",
|
||||||
|
"columnName": "cover_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "largeCoverUrl",
|
||||||
|
"columnName": "large_cover_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "state",
|
||||||
|
"columnName": "state",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "author",
|
||||||
|
"columnName": "author",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "source",
|
||||||
|
"columnName": "source",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"manga_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "tags",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tag_id` INTEGER NOT NULL, `title` TEXT NOT NULL, `key` TEXT NOT NULL, `source` TEXT NOT NULL, PRIMARY KEY(`tag_id`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "tag_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "title",
|
||||||
|
"columnName": "title",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "key",
|
||||||
|
"columnName": "key",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "source",
|
||||||
|
"columnName": "source",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"tag_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "manga_tags",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`manga_id` INTEGER NOT NULL, `tag_id` INTEGER NOT NULL, PRIMARY KEY(`manga_id`, `tag_id`), FOREIGN KEY(`manga_id`) REFERENCES `manga`(`manga_id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`tag_id`) REFERENCES `tags`(`tag_id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "mangaId",
|
||||||
|
"columnName": "manga_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "tagId",
|
||||||
|
"columnName": "tag_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"manga_id",
|
||||||
|
"tag_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_manga_tags_manga_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"manga_id"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_manga_tags_manga_id` ON `${TABLE_NAME}` (`manga_id`)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "index_manga_tags_tag_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"tag_id"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_manga_tags_tag_id` ON `${TABLE_NAME}` (`tag_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "manga",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"manga_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"manga_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"table": "tags",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"tag_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"tag_id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "sources",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`source` TEXT NOT NULL, `enabled` INTEGER NOT NULL, `sort_key` INTEGER NOT NULL, PRIMARY KEY(`source`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "source",
|
||||||
|
"columnName": "source",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isEnabled",
|
||||||
|
"columnName": "enabled",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "sortKey",
|
||||||
|
"columnName": "sort_key",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"source"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_sources_sort_key",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"sort_key"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_sources_sort_key` ON `${TABLE_NAME}` (`sort_key`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "history",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`manga_id` INTEGER NOT NULL, `created_at` INTEGER NOT NULL, `updated_at` INTEGER NOT NULL, `chapter_id` INTEGER NOT NULL, `page` INTEGER NOT NULL, `scroll` REAL NOT NULL, `percent` REAL NOT NULL, `deleted_at` INTEGER NOT NULL, PRIMARY KEY(`manga_id`), FOREIGN KEY(`manga_id`) REFERENCES `manga`(`manga_id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "mangaId",
|
||||||
|
"columnName": "manga_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "createdAt",
|
||||||
|
"columnName": "created_at",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "updatedAt",
|
||||||
|
"columnName": "updated_at",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "chapterId",
|
||||||
|
"columnName": "chapter_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "page",
|
||||||
|
"columnName": "page",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "scroll",
|
||||||
|
"columnName": "scroll",
|
||||||
|
"affinity": "REAL",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "percent",
|
||||||
|
"columnName": "percent",
|
||||||
|
"affinity": "REAL",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "deletedAt",
|
||||||
|
"columnName": "deleted_at",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"manga_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "manga",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"manga_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"manga_id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "favourites",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`manga_id` INTEGER NOT NULL, `category_id` INTEGER NOT NULL, `sort_key` INTEGER NOT NULL, `created_at` INTEGER NOT NULL, `deleted_at` INTEGER NOT NULL, PRIMARY KEY(`manga_id`, `category_id`), FOREIGN KEY(`manga_id`) REFERENCES `manga`(`manga_id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`category_id`) REFERENCES `favourite_categories`(`category_id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "mangaId",
|
||||||
|
"columnName": "manga_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "categoryId",
|
||||||
|
"columnName": "category_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "sortKey",
|
||||||
|
"columnName": "sort_key",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "createdAt",
|
||||||
|
"columnName": "created_at",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "deletedAt",
|
||||||
|
"columnName": "deleted_at",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"manga_id",
|
||||||
|
"category_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_favourites_manga_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"manga_id"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_favourites_manga_id` ON `${TABLE_NAME}` (`manga_id`)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "index_favourites_category_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"category_id"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_favourites_category_id` ON `${TABLE_NAME}` (`category_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "manga",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"manga_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"manga_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"table": "favourite_categories",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"category_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"category_id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "favourite_categories",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`category_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `created_at` INTEGER NOT NULL, `sort_key` INTEGER NOT NULL, `title` TEXT NOT NULL, `order` TEXT NOT NULL, `track` INTEGER NOT NULL, `show_in_lib` INTEGER NOT NULL, `deleted_at` INTEGER NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "categoryId",
|
||||||
|
"columnName": "category_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "createdAt",
|
||||||
|
"columnName": "created_at",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "sortKey",
|
||||||
|
"columnName": "sort_key",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "title",
|
||||||
|
"columnName": "title",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "order",
|
||||||
|
"columnName": "order",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "track",
|
||||||
|
"columnName": "track",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isVisibleInLibrary",
|
||||||
|
"columnName": "show_in_lib",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "deletedAt",
|
||||||
|
"columnName": "deleted_at",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"category_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "bookmarks",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`manga_id` INTEGER NOT NULL, `page_id` INTEGER NOT NULL, `chapter_id` INTEGER NOT NULL, `page` INTEGER NOT NULL, `scroll` INTEGER NOT NULL, `image` TEXT NOT NULL, `created_at` INTEGER NOT NULL, `percent` REAL NOT NULL, PRIMARY KEY(`manga_id`, `page_id`), FOREIGN KEY(`manga_id`) REFERENCES `manga`(`manga_id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "mangaId",
|
||||||
|
"columnName": "manga_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "pageId",
|
||||||
|
"columnName": "page_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "chapterId",
|
||||||
|
"columnName": "chapter_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "page",
|
||||||
|
"columnName": "page",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "scroll",
|
||||||
|
"columnName": "scroll",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "imageUrl",
|
||||||
|
"columnName": "image",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "createdAt",
|
||||||
|
"columnName": "created_at",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "percent",
|
||||||
|
"columnName": "percent",
|
||||||
|
"affinity": "REAL",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"manga_id",
|
||||||
|
"page_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_bookmarks_manga_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"manga_id"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_bookmarks_manga_id` ON `${TABLE_NAME}` (`manga_id`)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "index_bookmarks_page_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"page_id"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_bookmarks_page_id` ON `${TABLE_NAME}` (`page_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "manga",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"manga_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"manga_id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "suggestions",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`manga_id` INTEGER NOT NULL, `relevance` REAL NOT NULL, `created_at` INTEGER NOT NULL, PRIMARY KEY(`manga_id`), FOREIGN KEY(`manga_id`) REFERENCES `manga`(`manga_id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "mangaId",
|
||||||
|
"columnName": "manga_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "relevance",
|
||||||
|
"columnName": "relevance",
|
||||||
|
"affinity": "REAL",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "createdAt",
|
||||||
|
"columnName": "created_at",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"manga_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_suggestions_manga_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"manga_id"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_suggestions_manga_id` ON `${TABLE_NAME}` (`manga_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "manga",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"manga_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"manga_id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "tracks",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`manga_id` INTEGER NOT NULL, `last_chapter_id` INTEGER NOT NULL, `chapters_new` INTEGER NOT NULL, `last_check_time` INTEGER NOT NULL, `last_chapter_date` INTEGER NOT NULL, `last_result` INTEGER NOT NULL, PRIMARY KEY(`manga_id`), FOREIGN KEY(`manga_id`) REFERENCES `manga`(`manga_id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "mangaId",
|
||||||
|
"columnName": "manga_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastChapterId",
|
||||||
|
"columnName": "last_chapter_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "newChapters",
|
||||||
|
"columnName": "chapters_new",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastCheckTime",
|
||||||
|
"columnName": "last_check_time",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastChapterDate",
|
||||||
|
"columnName": "last_chapter_date",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastResult",
|
||||||
|
"columnName": "last_result",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"manga_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "manga",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"manga_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"manga_id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "track_logs",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `manga_id` INTEGER NOT NULL, `chapters` TEXT NOT NULL, `created_at` INTEGER NOT NULL, FOREIGN KEY(`manga_id`) REFERENCES `manga`(`manga_id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "mangaId",
|
||||||
|
"columnName": "manga_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "chapters",
|
||||||
|
"columnName": "chapters",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "createdAt",
|
||||||
|
"columnName": "created_at",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_track_logs_manga_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"manga_id"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_track_logs_manga_id` ON `${TABLE_NAME}` (`manga_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "manga",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"manga_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"manga_id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "stats",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`manga_id` INTEGER NOT NULL, `started_at` INTEGER NOT NULL, `duration` INTEGER NOT NULL, `pages` INTEGER NOT NULL, PRIMARY KEY(`manga_id`, `started_at`), FOREIGN KEY(`manga_id`) REFERENCES `history`(`manga_id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "mangaId",
|
||||||
|
"columnName": "manga_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "startedAt",
|
||||||
|
"columnName": "started_at",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "duration",
|
||||||
|
"columnName": "duration",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "pages",
|
||||||
|
"columnName": "pages",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"manga_id",
|
||||||
|
"started_at"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "history",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"manga_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"manga_id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "scrobblings",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`scrobbler` INTEGER NOT NULL, `id` INTEGER NOT NULL, `manga_id` INTEGER NOT NULL, `target_id` INTEGER NOT NULL, `status` TEXT, `chapter` INTEGER NOT NULL, `comment` TEXT, `rating` REAL NOT NULL, PRIMARY KEY(`scrobbler`, `id`, `manga_id`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "scrobbler",
|
||||||
|
"columnName": "scrobbler",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "mangaId",
|
||||||
|
"columnName": "manga_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "targetId",
|
||||||
|
"columnName": "target_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "status",
|
||||||
|
"columnName": "status",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "chapter",
|
||||||
|
"columnName": "chapter",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "comment",
|
||||||
|
"columnName": "comment",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "rating",
|
||||||
|
"columnName": "rating",
|
||||||
|
"affinity": "REAL",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"scrobbler",
|
||||||
|
"id",
|
||||||
|
"manga_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, 'dbe1dcac0f49c5ae2ac88d88aa280081')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,875 @@
|
|||||||
|
{
|
||||||
|
"formatVersion": 1,
|
||||||
|
"database": {
|
||||||
|
"version": 4,
|
||||||
|
"identityHash": "90b73386d5c61c2ddf46d6354ca2f1b6",
|
||||||
|
"entities": [
|
||||||
|
{
|
||||||
|
"tableName": "manga",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`manga_id` INTEGER NOT NULL, `title` TEXT NOT NULL, `alt_title` TEXT, `url` TEXT NOT NULL, `public_url` TEXT NOT NULL, `rating` REAL NOT NULL, `nsfw` INTEGER NOT NULL, `cover_url` TEXT NOT NULL, `large_cover_url` TEXT, `state` TEXT, `author` TEXT, `source` TEXT NOT NULL, PRIMARY KEY(`manga_id`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "manga_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "title",
|
||||||
|
"columnName": "title",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "altTitle",
|
||||||
|
"columnName": "alt_title",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "url",
|
||||||
|
"columnName": "url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "publicUrl",
|
||||||
|
"columnName": "public_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "rating",
|
||||||
|
"columnName": "rating",
|
||||||
|
"affinity": "REAL",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isNsfw",
|
||||||
|
"columnName": "nsfw",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "coverUrl",
|
||||||
|
"columnName": "cover_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "largeCoverUrl",
|
||||||
|
"columnName": "large_cover_url",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "state",
|
||||||
|
"columnName": "state",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "author",
|
||||||
|
"columnName": "author",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "source",
|
||||||
|
"columnName": "source",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"manga_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "tags",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tag_id` INTEGER NOT NULL, `title` TEXT NOT NULL, `key` TEXT NOT NULL, `source` TEXT NOT NULL, PRIMARY KEY(`tag_id`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "tag_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "title",
|
||||||
|
"columnName": "title",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "key",
|
||||||
|
"columnName": "key",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "source",
|
||||||
|
"columnName": "source",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"tag_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "manga_tags",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`manga_id` INTEGER NOT NULL, `tag_id` INTEGER NOT NULL, PRIMARY KEY(`manga_id`, `tag_id`), FOREIGN KEY(`manga_id`) REFERENCES `manga`(`manga_id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`tag_id`) REFERENCES `tags`(`tag_id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "mangaId",
|
||||||
|
"columnName": "manga_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "tagId",
|
||||||
|
"columnName": "tag_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"manga_id",
|
||||||
|
"tag_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_manga_tags_manga_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"manga_id"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_manga_tags_manga_id` ON `${TABLE_NAME}` (`manga_id`)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "index_manga_tags_tag_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"tag_id"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_manga_tags_tag_id` ON `${TABLE_NAME}` (`tag_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "manga",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"manga_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"manga_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"table": "tags",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"tag_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"tag_id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "sources",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`source` TEXT NOT NULL, `enabled` INTEGER NOT NULL, `sort_key` INTEGER NOT NULL, `added_in` INTEGER NOT NULL, `used_at` INTEGER NOT NULL, `pinned` INTEGER NOT NULL, PRIMARY KEY(`source`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "source",
|
||||||
|
"columnName": "source",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isEnabled",
|
||||||
|
"columnName": "enabled",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "sortKey",
|
||||||
|
"columnName": "sort_key",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "addedIn",
|
||||||
|
"columnName": "added_in",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastUsedAt",
|
||||||
|
"columnName": "used_at",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isPinned",
|
||||||
|
"columnName": "pinned",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"source"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_sources_sort_key",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"sort_key"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_sources_sort_key` ON `${TABLE_NAME}` (`sort_key`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "history",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`manga_id` INTEGER NOT NULL, `created_at` INTEGER NOT NULL, `updated_at` INTEGER NOT NULL, `chapter_id` INTEGER NOT NULL, `page` INTEGER NOT NULL, `scroll` REAL NOT NULL, `percent` REAL NOT NULL, `deleted_at` INTEGER NOT NULL, PRIMARY KEY(`manga_id`), FOREIGN KEY(`manga_id`) REFERENCES `manga`(`manga_id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "mangaId",
|
||||||
|
"columnName": "manga_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "createdAt",
|
||||||
|
"columnName": "created_at",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "updatedAt",
|
||||||
|
"columnName": "updated_at",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "chapterId",
|
||||||
|
"columnName": "chapter_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "page",
|
||||||
|
"columnName": "page",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "scroll",
|
||||||
|
"columnName": "scroll",
|
||||||
|
"affinity": "REAL",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "percent",
|
||||||
|
"columnName": "percent",
|
||||||
|
"affinity": "REAL",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "deletedAt",
|
||||||
|
"columnName": "deleted_at",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"manga_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "manga",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"manga_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"manga_id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "favourites",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`manga_id` INTEGER NOT NULL, `category_id` INTEGER NOT NULL, `sort_key` INTEGER NOT NULL, `created_at` INTEGER NOT NULL, `deleted_at` INTEGER NOT NULL, PRIMARY KEY(`manga_id`, `category_id`), FOREIGN KEY(`manga_id`) REFERENCES `manga`(`manga_id`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`category_id`) REFERENCES `favourite_categories`(`category_id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "mangaId",
|
||||||
|
"columnName": "manga_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "categoryId",
|
||||||
|
"columnName": "category_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "sortKey",
|
||||||
|
"columnName": "sort_key",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "createdAt",
|
||||||
|
"columnName": "created_at",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "deletedAt",
|
||||||
|
"columnName": "deleted_at",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"manga_id",
|
||||||
|
"category_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_favourites_manga_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"manga_id"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_favourites_manga_id` ON `${TABLE_NAME}` (`manga_id`)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "index_favourites_category_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"category_id"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_favourites_category_id` ON `${TABLE_NAME}` (`category_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "manga",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"manga_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"manga_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"table": "favourite_categories",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"category_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"category_id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "favourite_categories",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`category_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `created_at` INTEGER NOT NULL, `sort_key` INTEGER NOT NULL, `title` TEXT NOT NULL, `order` TEXT NOT NULL, `track` INTEGER NOT NULL, `show_in_lib` INTEGER NOT NULL, `deleted_at` INTEGER NOT NULL)",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "categoryId",
|
||||||
|
"columnName": "category_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "createdAt",
|
||||||
|
"columnName": "created_at",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "sortKey",
|
||||||
|
"columnName": "sort_key",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "title",
|
||||||
|
"columnName": "title",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "order",
|
||||||
|
"columnName": "order",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "track",
|
||||||
|
"columnName": "track",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "isVisibleInLibrary",
|
||||||
|
"columnName": "show_in_lib",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "deletedAt",
|
||||||
|
"columnName": "deleted_at",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"category_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "bookmarks",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`manga_id` INTEGER NOT NULL, `page_id` INTEGER NOT NULL, `chapter_id` INTEGER NOT NULL, `page` INTEGER NOT NULL, `scroll` INTEGER NOT NULL, `image` TEXT NOT NULL, `created_at` INTEGER NOT NULL, `percent` REAL NOT NULL, PRIMARY KEY(`manga_id`, `page_id`), FOREIGN KEY(`manga_id`) REFERENCES `manga`(`manga_id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "mangaId",
|
||||||
|
"columnName": "manga_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "pageId",
|
||||||
|
"columnName": "page_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "chapterId",
|
||||||
|
"columnName": "chapter_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "page",
|
||||||
|
"columnName": "page",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "scroll",
|
||||||
|
"columnName": "scroll",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "imageUrl",
|
||||||
|
"columnName": "image",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "createdAt",
|
||||||
|
"columnName": "created_at",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "percent",
|
||||||
|
"columnName": "percent",
|
||||||
|
"affinity": "REAL",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"manga_id",
|
||||||
|
"page_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_bookmarks_manga_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"manga_id"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_bookmarks_manga_id` ON `${TABLE_NAME}` (`manga_id`)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "index_bookmarks_page_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"page_id"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_bookmarks_page_id` ON `${TABLE_NAME}` (`page_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "manga",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"manga_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"manga_id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "suggestions",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`manga_id` INTEGER NOT NULL, `relevance` REAL NOT NULL, `created_at` INTEGER NOT NULL, PRIMARY KEY(`manga_id`), FOREIGN KEY(`manga_id`) REFERENCES `manga`(`manga_id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "mangaId",
|
||||||
|
"columnName": "manga_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "relevance",
|
||||||
|
"columnName": "relevance",
|
||||||
|
"affinity": "REAL",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "createdAt",
|
||||||
|
"columnName": "created_at",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"manga_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_suggestions_manga_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"manga_id"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_suggestions_manga_id` ON `${TABLE_NAME}` (`manga_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "manga",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"manga_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"manga_id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "tracks",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`manga_id` INTEGER NOT NULL, `last_chapter_id` INTEGER NOT NULL, `chapters_new` INTEGER NOT NULL, `last_check_time` INTEGER NOT NULL, `last_chapter_date` INTEGER NOT NULL, `last_result` INTEGER NOT NULL, PRIMARY KEY(`manga_id`), FOREIGN KEY(`manga_id`) REFERENCES `manga`(`manga_id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "mangaId",
|
||||||
|
"columnName": "manga_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastChapterId",
|
||||||
|
"columnName": "last_chapter_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "newChapters",
|
||||||
|
"columnName": "chapters_new",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastCheckTime",
|
||||||
|
"columnName": "last_check_time",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastChapterDate",
|
||||||
|
"columnName": "last_chapter_date",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "lastResult",
|
||||||
|
"columnName": "last_result",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"manga_id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "manga",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"manga_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"manga_id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "track_logs",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `manga_id` INTEGER NOT NULL, `chapters` TEXT NOT NULL, `created_at` INTEGER NOT NULL, FOREIGN KEY(`manga_id`) REFERENCES `manga`(`manga_id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "mangaId",
|
||||||
|
"columnName": "manga_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "chapters",
|
||||||
|
"columnName": "chapters",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "createdAt",
|
||||||
|
"columnName": "created_at",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": true,
|
||||||
|
"columnNames": [
|
||||||
|
"id"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [
|
||||||
|
{
|
||||||
|
"name": "index_track_logs_manga_id",
|
||||||
|
"unique": false,
|
||||||
|
"columnNames": [
|
||||||
|
"manga_id"
|
||||||
|
],
|
||||||
|
"orders": [],
|
||||||
|
"createSql": "CREATE INDEX IF NOT EXISTS `index_track_logs_manga_id` ON `${TABLE_NAME}` (`manga_id`)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "manga",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"manga_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"manga_id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "stats",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`manga_id` INTEGER NOT NULL, `started_at` INTEGER NOT NULL, `duration` INTEGER NOT NULL, `pages` INTEGER NOT NULL, PRIMARY KEY(`manga_id`, `started_at`), FOREIGN KEY(`manga_id`) REFERENCES `history`(`manga_id`) ON UPDATE NO ACTION ON DELETE CASCADE )",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "mangaId",
|
||||||
|
"columnName": "manga_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "startedAt",
|
||||||
|
"columnName": "started_at",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "duration",
|
||||||
|
"columnName": "duration",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "pages",
|
||||||
|
"columnName": "pages",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"manga_id",
|
||||||
|
"started_at"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"indices": [],
|
||||||
|
"foreignKeys": [
|
||||||
|
{
|
||||||
|
"table": "history",
|
||||||
|
"onDelete": "CASCADE",
|
||||||
|
"onUpdate": "NO ACTION",
|
||||||
|
"columns": [
|
||||||
|
"manga_id"
|
||||||
|
],
|
||||||
|
"referencedColumns": [
|
||||||
|
"manga_id"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tableName": "scrobblings",
|
||||||
|
"createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`scrobbler` INTEGER NOT NULL, `id` INTEGER NOT NULL, `manga_id` INTEGER NOT NULL, `target_id` INTEGER NOT NULL, `status` TEXT, `chapter` INTEGER NOT NULL, `comment` TEXT, `rating` REAL NOT NULL, PRIMARY KEY(`scrobbler`, `id`, `manga_id`))",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldPath": "scrobbler",
|
||||||
|
"columnName": "scrobbler",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "id",
|
||||||
|
"columnName": "id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "mangaId",
|
||||||
|
"columnName": "manga_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "targetId",
|
||||||
|
"columnName": "target_id",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "status",
|
||||||
|
"columnName": "status",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "chapter",
|
||||||
|
"columnName": "chapter",
|
||||||
|
"affinity": "INTEGER",
|
||||||
|
"notNull": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "comment",
|
||||||
|
"columnName": "comment",
|
||||||
|
"affinity": "TEXT",
|
||||||
|
"notNull": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldPath": "rating",
|
||||||
|
"columnName": "rating",
|
||||||
|
"affinity": "REAL",
|
||||||
|
"notNull": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryKey": {
|
||||||
|
"autoGenerate": false,
|
||||||
|
"columnNames": [
|
||||||
|
"scrobbler",
|
||||||
|
"id",
|
||||||
|
"manga_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, '90b73386d5c61c2ddf46d6354ca2f1b6')"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
9
app/src/androidTest/assets/categories/simple.json
Normal file
9
app/src/androidTest/assets/categories/simple.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"id": 4,
|
||||||
|
"title": "Read later",
|
||||||
|
"sortKey": 1,
|
||||||
|
"order": "NEWEST",
|
||||||
|
"createdAt": 1335906000000,
|
||||||
|
"isTrackingEnabled": true,
|
||||||
|
"isVisibleInLibrary": true
|
||||||
|
}
|
||||||
BIN
app/src/androidTest/assets/kotatsu_test.bak
Normal file
BIN
app/src/androidTest/assets/kotatsu_test.bak
Normal file
Binary file not shown.
163
app/src/androidTest/assets/manga/bad_ids.json
Normal file
163
app/src/androidTest/assets/manga/bad_ids.json
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
{
|
||||||
|
"id": -2096681732556647985,
|
||||||
|
"title": "Странствия Эманон",
|
||||||
|
"url": "/stranstviia_emanon",
|
||||||
|
"publicUrl": "https://readmanga.io/stranstviia_emanon",
|
||||||
|
"rating": 0.9400894,
|
||||||
|
"isNsfw": true,
|
||||||
|
"coverUrl": "https://staticrm.rmr.rocks/uploads/pics/01/12/559_p.jpg",
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"title": "Сверхъестественное",
|
||||||
|
"key": "supernatural",
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Сэйнэн",
|
||||||
|
"key": "seinen",
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Повседневность",
|
||||||
|
"key": "slice_of_life",
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Приключения",
|
||||||
|
"key": "adventure",
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"state": "FINISHED",
|
||||||
|
"largeCoverUrl": "https://staticrm.rmr.rocks/uploads/pics/01/12/559_o.jpg",
|
||||||
|
"description": "Продолжение истории о загадочной девушке по имени Эманон, которая помнит всё, что происходило на Земле за последние три миллиарда лет. \n<br>Начало истории читайте в \"Воспоминаниях Эманон\". \n<div class=\"clearfix\"></div>",
|
||||||
|
"chapters": [
|
||||||
|
{
|
||||||
|
"id": 1552943969433540704,
|
||||||
|
"name": "1 - 1",
|
||||||
|
"number": 1,
|
||||||
|
"url": "/stranstviia_emanon/vol1/1",
|
||||||
|
"scanlator": "Sad-Robot",
|
||||||
|
"uploadDate": 1342731600000,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1552943969433540705,
|
||||||
|
"name": "1 - 2",
|
||||||
|
"number": 2,
|
||||||
|
"url": "/stranstviia_emanon/vol1/2",
|
||||||
|
"scanlator": "Sad-Robot",
|
||||||
|
"uploadDate": 1342731600000,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1552943969433540706,
|
||||||
|
"name": "1 - 3",
|
||||||
|
"number": 3,
|
||||||
|
"url": "/stranstviia_emanon/vol1/3",
|
||||||
|
"scanlator": "Sad-Robot",
|
||||||
|
"uploadDate": 1342731600000,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1552943969433540707,
|
||||||
|
"name": "1 - 4",
|
||||||
|
"number": 4,
|
||||||
|
"url": "/stranstviia_emanon/vol1/4",
|
||||||
|
"scanlator": "Sad-Robot",
|
||||||
|
"uploadDate": 1342731600000,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1552943969433540708,
|
||||||
|
"name": "1 - 5",
|
||||||
|
"number": 5,
|
||||||
|
"url": "/stranstviia_emanon/vol1/5",
|
||||||
|
"scanlator": "Sad-Robot",
|
||||||
|
"uploadDate": 1342731600000,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1552943969433541665,
|
||||||
|
"name": "2 - 1",
|
||||||
|
"number": 6,
|
||||||
|
"url": "/stranstviia_emanon/vol2/1",
|
||||||
|
"scanlator": "Sup!",
|
||||||
|
"uploadDate": 1415570400000,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1552943969433541666,
|
||||||
|
"name": "2 - 2",
|
||||||
|
"number": 7,
|
||||||
|
"url": "/stranstviia_emanon/vol2/2",
|
||||||
|
"scanlator": "Sup!",
|
||||||
|
"uploadDate": 1419976800000,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1552943969433541667,
|
||||||
|
"name": "2 - 3",
|
||||||
|
"number": 8,
|
||||||
|
"url": "/stranstviia_emanon/vol2/3",
|
||||||
|
"scanlator": "Sup!",
|
||||||
|
"uploadDate": 1427922000000,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1552943969433541668,
|
||||||
|
"name": "2 - 4",
|
||||||
|
"number": 9,
|
||||||
|
"url": "/stranstviia_emanon/vol2/4",
|
||||||
|
"scanlator": "Sup!",
|
||||||
|
"uploadDate": 1436907600000,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1552943969433541669,
|
||||||
|
"name": "2 - 5",
|
||||||
|
"number": 10,
|
||||||
|
"url": "/stranstviia_emanon/vol2/5",
|
||||||
|
"scanlator": "Sup!",
|
||||||
|
"uploadDate": 1446674400000,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1552943969433541670,
|
||||||
|
"name": "2 - 6",
|
||||||
|
"number": 11,
|
||||||
|
"url": "/stranstviia_emanon/vol2/6",
|
||||||
|
"scanlator": "Sup!",
|
||||||
|
"uploadDate": 1451512800000,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1552943969433542626,
|
||||||
|
"name": "3 - 1",
|
||||||
|
"number": 12,
|
||||||
|
"url": "/stranstviia_emanon/vol3/1",
|
||||||
|
"scanlator": "Sup!",
|
||||||
|
"uploadDate": 1461618000000,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1552943969433542627,
|
||||||
|
"name": "3 - 2",
|
||||||
|
"number": 13,
|
||||||
|
"url": "/stranstviia_emanon/vol3/2",
|
||||||
|
"scanlator": "Sup!",
|
||||||
|
"uploadDate": 1461618000000,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1552943969433542628,
|
||||||
|
"name": "3 - 3",
|
||||||
|
"number": 14,
|
||||||
|
"url": "/stranstviia_emanon/vol3/3",
|
||||||
|
"scanlator": "",
|
||||||
|
"uploadDate": 1465851600000,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
}
|
||||||
36
app/src/androidTest/assets/manga/empty.json
Normal file
36
app/src/androidTest/assets/manga/empty.json
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
{
|
||||||
|
"id": -2096681732556647985,
|
||||||
|
"title": "Странствия Эманон",
|
||||||
|
"url": "/stranstviia_emanon",
|
||||||
|
"publicUrl": "https://readmanga.io/stranstviia_emanon",
|
||||||
|
"rating": 0.9400894,
|
||||||
|
"isNsfw": true,
|
||||||
|
"coverUrl": "https://staticrm.rmr.rocks/uploads/pics/01/12/559_p.jpg",
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"title": "Сверхъестественное",
|
||||||
|
"key": "supernatural",
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Сэйнэн",
|
||||||
|
"key": "seinen",
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Повседневность",
|
||||||
|
"key": "slice_of_life",
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Приключения",
|
||||||
|
"key": "adventure",
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"state": "FINISHED",
|
||||||
|
"largeCoverUrl": "https://staticrm.rmr.rocks/uploads/pics/01/12/559_o.jpg",
|
||||||
|
"description": "Продолжение истории о загадочной девушке по имени Эманон, которая помнит всё, что происходило на Земле за последние три миллиарда лет. \n<br>Начало истории читайте в \"Воспоминаниях Эманон\". \n<div class=\"clearfix\"></div>",
|
||||||
|
"chapters": [],
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
}
|
||||||
136
app/src/androidTest/assets/manga/first_chapters.json
Normal file
136
app/src/androidTest/assets/manga/first_chapters.json
Normal file
@@ -0,0 +1,136 @@
|
|||||||
|
{
|
||||||
|
"id": -2096681732556647985,
|
||||||
|
"title": "Странствия Эманон",
|
||||||
|
"url": "/stranstviia_emanon",
|
||||||
|
"publicUrl": "https://readmanga.io/stranstviia_emanon",
|
||||||
|
"rating": 0.9400894,
|
||||||
|
"isNsfw": true,
|
||||||
|
"coverUrl": "https://staticrm.rmr.rocks/uploads/pics/01/12/559_p.jpg",
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"title": "Сверхъестественное",
|
||||||
|
"key": "supernatural",
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Сэйнэн",
|
||||||
|
"key": "seinen",
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Повседневность",
|
||||||
|
"key": "slice_of_life",
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Приключения",
|
||||||
|
"key": "adventure",
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"state": "FINISHED",
|
||||||
|
"largeCoverUrl": "https://staticrm.rmr.rocks/uploads/pics/01/12/559_o.jpg",
|
||||||
|
"description": "Продолжение истории о загадочной девушке по имени Эманон, которая помнит всё, что происходило на Земле за последние три миллиарда лет. \n<br>Начало истории читайте в \"Воспоминаниях Эманон\". \n<div class=\"clearfix\"></div>",
|
||||||
|
"chapters": [
|
||||||
|
{
|
||||||
|
"id": 3552943969433540704,
|
||||||
|
"name": "1 - 1",
|
||||||
|
"number": 1,
|
||||||
|
"url": "/stranstviia_emanon/vol1/1",
|
||||||
|
"scanlator": "Sad-Robot",
|
||||||
|
"uploadDate": 1342731600000,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3552943969433540705,
|
||||||
|
"name": "1 - 2",
|
||||||
|
"number": 2,
|
||||||
|
"url": "/stranstviia_emanon/vol1/2",
|
||||||
|
"scanlator": "Sad-Robot",
|
||||||
|
"uploadDate": 1342731600000,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3552943969433540706,
|
||||||
|
"name": "1 - 3",
|
||||||
|
"number": 3,
|
||||||
|
"url": "/stranstviia_emanon/vol1/3",
|
||||||
|
"scanlator": "Sad-Robot",
|
||||||
|
"uploadDate": 1342731600000,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3552943969433540707,
|
||||||
|
"name": "1 - 4",
|
||||||
|
"number": 4,
|
||||||
|
"url": "/stranstviia_emanon/vol1/4",
|
||||||
|
"scanlator": "Sad-Robot",
|
||||||
|
"uploadDate": 1342731600000,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3552943969433540708,
|
||||||
|
"name": "1 - 5",
|
||||||
|
"number": 5,
|
||||||
|
"url": "/stranstviia_emanon/vol1/5",
|
||||||
|
"scanlator": "Sad-Robot",
|
||||||
|
"uploadDate": 1342731600000,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3552943969433541665,
|
||||||
|
"name": "2 - 1",
|
||||||
|
"number": 6,
|
||||||
|
"url": "/stranstviia_emanon/vol2/1",
|
||||||
|
"scanlator": "Sup!",
|
||||||
|
"uploadDate": 1415570400000,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3552943969433541666,
|
||||||
|
"name": "2 - 2",
|
||||||
|
"number": 7,
|
||||||
|
"url": "/stranstviia_emanon/vol2/2",
|
||||||
|
"scanlator": "Sup!",
|
||||||
|
"uploadDate": 1419976800000,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3552943969433541667,
|
||||||
|
"name": "2 - 3",
|
||||||
|
"number": 8,
|
||||||
|
"url": "/stranstviia_emanon/vol2/3",
|
||||||
|
"scanlator": "Sup!",
|
||||||
|
"uploadDate": 1427922000000,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3552943969433541668,
|
||||||
|
"name": "2 - 4",
|
||||||
|
"number": 9,
|
||||||
|
"url": "/stranstviia_emanon/vol2/4",
|
||||||
|
"scanlator": "Sup!",
|
||||||
|
"uploadDate": 1436907600000,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3552943969433541669,
|
||||||
|
"name": "2 - 5",
|
||||||
|
"number": 10,
|
||||||
|
"url": "/stranstviia_emanon/vol2/5",
|
||||||
|
"scanlator": "Sup!",
|
||||||
|
"uploadDate": 1446674400000,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3552943969433541670,
|
||||||
|
"name": "2 - 6",
|
||||||
|
"number": 11,
|
||||||
|
"url": "/stranstviia_emanon/vol2/6",
|
||||||
|
"scanlator": "Sup!",
|
||||||
|
"uploadDate": 1451512800000,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
}
|
||||||
163
app/src/androidTest/assets/manga/full.json
Normal file
163
app/src/androidTest/assets/manga/full.json
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
{
|
||||||
|
"id": -2096681732556647985,
|
||||||
|
"title": "Странствия Эманон",
|
||||||
|
"url": "/stranstviia_emanon",
|
||||||
|
"publicUrl": "https://readmanga.io/stranstviia_emanon",
|
||||||
|
"rating": 0.9400894,
|
||||||
|
"isNsfw": true,
|
||||||
|
"coverUrl": "https://staticrm.rmr.rocks/uploads/pics/01/12/559_p.jpg",
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"title": "Сверхъестественное",
|
||||||
|
"key": "supernatural",
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Сэйнэн",
|
||||||
|
"key": "seinen",
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Повседневность",
|
||||||
|
"key": "slice_of_life",
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Приключения",
|
||||||
|
"key": "adventure",
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"state": "FINISHED",
|
||||||
|
"largeCoverUrl": "https://staticrm.rmr.rocks/uploads/pics/01/12/559_o.jpg",
|
||||||
|
"description": "Продолжение истории о загадочной девушке по имени Эманон, которая помнит всё, что происходило на Земле за последние три миллиарда лет. \n<br>Начало истории читайте в \"Воспоминаниях Эманон\". \n<div class=\"clearfix\"></div>",
|
||||||
|
"chapters": [
|
||||||
|
{
|
||||||
|
"id": 3552943969433540704,
|
||||||
|
"name": "1 - 1",
|
||||||
|
"number": 1,
|
||||||
|
"url": "/stranstviia_emanon/vol1/1",
|
||||||
|
"scanlator": "Sad-Robot",
|
||||||
|
"uploadDate": 1342731600000,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3552943969433540705,
|
||||||
|
"name": "1 - 2",
|
||||||
|
"number": 2,
|
||||||
|
"url": "/stranstviia_emanon/vol1/2",
|
||||||
|
"scanlator": "Sad-Robot",
|
||||||
|
"uploadDate": 1342731600000,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3552943969433540706,
|
||||||
|
"name": "1 - 3",
|
||||||
|
"number": 3,
|
||||||
|
"url": "/stranstviia_emanon/vol1/3",
|
||||||
|
"scanlator": "Sad-Robot",
|
||||||
|
"uploadDate": 1342731600000,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3552943969433540707,
|
||||||
|
"name": "1 - 4",
|
||||||
|
"number": 4,
|
||||||
|
"url": "/stranstviia_emanon/vol1/4",
|
||||||
|
"scanlator": "Sad-Robot",
|
||||||
|
"uploadDate": 1342731600000,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3552943969433540708,
|
||||||
|
"name": "1 - 5",
|
||||||
|
"number": 5,
|
||||||
|
"url": "/stranstviia_emanon/vol1/5",
|
||||||
|
"scanlator": "Sad-Robot",
|
||||||
|
"uploadDate": 1342731600000,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3552943969433541665,
|
||||||
|
"name": "2 - 1",
|
||||||
|
"number": 6,
|
||||||
|
"url": "/stranstviia_emanon/vol2/1",
|
||||||
|
"scanlator": "Sup!",
|
||||||
|
"uploadDate": 1415570400000,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3552943969433541666,
|
||||||
|
"name": "2 - 2",
|
||||||
|
"number": 7,
|
||||||
|
"url": "/stranstviia_emanon/vol2/2",
|
||||||
|
"scanlator": "Sup!",
|
||||||
|
"uploadDate": 1419976800000,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3552943969433541667,
|
||||||
|
"name": "2 - 3",
|
||||||
|
"number": 8,
|
||||||
|
"url": "/stranstviia_emanon/vol2/3",
|
||||||
|
"scanlator": "Sup!",
|
||||||
|
"uploadDate": 1427922000000,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3552943969433541668,
|
||||||
|
"name": "2 - 4",
|
||||||
|
"number": 9,
|
||||||
|
"url": "/stranstviia_emanon/vol2/4",
|
||||||
|
"scanlator": "Sup!",
|
||||||
|
"uploadDate": 1436907600000,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3552943969433541669,
|
||||||
|
"name": "2 - 5",
|
||||||
|
"number": 10,
|
||||||
|
"url": "/stranstviia_emanon/vol2/5",
|
||||||
|
"scanlator": "Sup!",
|
||||||
|
"uploadDate": 1446674400000,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3552943969433541670,
|
||||||
|
"name": "2 - 6",
|
||||||
|
"number": 11,
|
||||||
|
"url": "/stranstviia_emanon/vol2/6",
|
||||||
|
"scanlator": "Sup!",
|
||||||
|
"uploadDate": 1451512800000,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3552943969433542626,
|
||||||
|
"name": "3 - 1",
|
||||||
|
"number": 12,
|
||||||
|
"url": "/stranstviia_emanon/vol3/1",
|
||||||
|
"scanlator": "Sup!",
|
||||||
|
"uploadDate": 1461618000000,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3552943969433542627,
|
||||||
|
"name": "3 - 2",
|
||||||
|
"number": 13,
|
||||||
|
"url": "/stranstviia_emanon/vol3/2",
|
||||||
|
"scanlator": "Sup!",
|
||||||
|
"uploadDate": 1461618000000,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3552943969433542628,
|
||||||
|
"name": "3 - 3",
|
||||||
|
"number": 14,
|
||||||
|
"url": "/stranstviia_emanon/vol3/3",
|
||||||
|
"scanlator": "",
|
||||||
|
"uploadDate": 1465851600000,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
}
|
||||||
35
app/src/androidTest/assets/manga/header.json
Normal file
35
app/src/androidTest/assets/manga/header.json
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
"id": -2096681732556647985,
|
||||||
|
"title": "Странствия Эманон",
|
||||||
|
"url": "/stranstviia_emanon",
|
||||||
|
"publicUrl": "https://readmanga.io/stranstviia_emanon",
|
||||||
|
"rating": 0.9400894,
|
||||||
|
"isNsfw": true,
|
||||||
|
"coverUrl": "https://staticrm.rmr.rocks/uploads/pics/01/12/559_p.jpg",
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"title": "Сверхъестественное",
|
||||||
|
"key": "supernatural",
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Сэйнэн",
|
||||||
|
"key": "seinen",
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Повседневность",
|
||||||
|
"key": "slice_of_life",
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Приключения",
|
||||||
|
"key": "adventure",
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"state": "FINISHED",
|
||||||
|
"largeCoverUrl": "https://staticrm.rmr.rocks/uploads/pics/01/12/559_o.jpg",
|
||||||
|
"description": null,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
}
|
||||||
154
app/src/androidTest/assets/manga/without_middle_chapter.json
Normal file
154
app/src/androidTest/assets/manga/without_middle_chapter.json
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
{
|
||||||
|
"id": -2096681732556647985,
|
||||||
|
"title": "Странствия Эманон",
|
||||||
|
"url": "/stranstviia_emanon",
|
||||||
|
"publicUrl": "https://readmanga.io/stranstviia_emanon",
|
||||||
|
"rating": 0.9400894,
|
||||||
|
"isNsfw": true,
|
||||||
|
"coverUrl": "https://staticrm.rmr.rocks/uploads/pics/01/12/559_p.jpg",
|
||||||
|
"tags": [
|
||||||
|
{
|
||||||
|
"title": "Сверхъестественное",
|
||||||
|
"key": "supernatural",
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Сэйнэн",
|
||||||
|
"key": "seinen",
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Повседневность",
|
||||||
|
"key": "slice_of_life",
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Приключения",
|
||||||
|
"key": "adventure",
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"state": "FINISHED",
|
||||||
|
"largeCoverUrl": "https://staticrm.rmr.rocks/uploads/pics/01/12/559_o.jpg",
|
||||||
|
"description": "Продолжение истории о загадочной девушке по имени Эманон, которая помнит всё, что происходило на Земле за последние три миллиарда лет. \n<br>Начало истории читайте в \"Воспоминаниях Эманон\". \n<div class=\"clearfix\"></div>",
|
||||||
|
"chapters": [
|
||||||
|
{
|
||||||
|
"id": 3552943969433540704,
|
||||||
|
"name": "1 - 1",
|
||||||
|
"number": 1,
|
||||||
|
"url": "/stranstviia_emanon/vol1/1",
|
||||||
|
"scanlator": "Sad-Robot",
|
||||||
|
"uploadDate": 1342731600000,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3552943969433540705,
|
||||||
|
"name": "1 - 2",
|
||||||
|
"number": 2,
|
||||||
|
"url": "/stranstviia_emanon/vol1/2",
|
||||||
|
"scanlator": "Sad-Robot",
|
||||||
|
"uploadDate": 1342731600000,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3552943969433540706,
|
||||||
|
"name": "1 - 3",
|
||||||
|
"number": 3,
|
||||||
|
"url": "/stranstviia_emanon/vol1/3",
|
||||||
|
"scanlator": "Sad-Robot",
|
||||||
|
"uploadDate": 1342731600000,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3552943969433540707,
|
||||||
|
"name": "1 - 4",
|
||||||
|
"number": 4,
|
||||||
|
"url": "/stranstviia_emanon/vol1/4",
|
||||||
|
"scanlator": "Sad-Robot",
|
||||||
|
"uploadDate": 1342731600000,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3552943969433540708,
|
||||||
|
"name": "1 - 5",
|
||||||
|
"number": 5,
|
||||||
|
"url": "/stranstviia_emanon/vol1/5",
|
||||||
|
"scanlator": "Sad-Robot",
|
||||||
|
"uploadDate": 1342731600000,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3552943969433541666,
|
||||||
|
"name": "2 - 2",
|
||||||
|
"number": 7,
|
||||||
|
"url": "/stranstviia_emanon/vol2/2",
|
||||||
|
"scanlator": "Sup!",
|
||||||
|
"uploadDate": 1419976800000,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3552943969433541667,
|
||||||
|
"name": "2 - 3",
|
||||||
|
"number": 8,
|
||||||
|
"url": "/stranstviia_emanon/vol2/3",
|
||||||
|
"scanlator": "Sup!",
|
||||||
|
"uploadDate": 1427922000000,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3552943969433541668,
|
||||||
|
"name": "2 - 4",
|
||||||
|
"number": 9,
|
||||||
|
"url": "/stranstviia_emanon/vol2/4",
|
||||||
|
"scanlator": "Sup!",
|
||||||
|
"uploadDate": 1436907600000,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3552943969433541669,
|
||||||
|
"name": "2 - 5",
|
||||||
|
"number": 10,
|
||||||
|
"url": "/stranstviia_emanon/vol2/5",
|
||||||
|
"scanlator": "Sup!",
|
||||||
|
"uploadDate": 1446674400000,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3552943969433541670,
|
||||||
|
"name": "2 - 6",
|
||||||
|
"number": 11,
|
||||||
|
"url": "/stranstviia_emanon/vol2/6",
|
||||||
|
"scanlator": "Sup!",
|
||||||
|
"uploadDate": 1451512800000,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3552943969433542626,
|
||||||
|
"name": "3 - 1",
|
||||||
|
"number": 12,
|
||||||
|
"url": "/stranstviia_emanon/vol3/1",
|
||||||
|
"scanlator": "Sup!",
|
||||||
|
"uploadDate": 1461618000000,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3552943969433542627,
|
||||||
|
"name": "3 - 2",
|
||||||
|
"number": 13,
|
||||||
|
"url": "/stranstviia_emanon/vol3/2",
|
||||||
|
"scanlator": "Sup!",
|
||||||
|
"uploadDate": 1461618000000,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 3552943969433542628,
|
||||||
|
"name": "3 - 3",
|
||||||
|
"number": 14,
|
||||||
|
"url": "/stranstviia_emanon/vol3/3",
|
||||||
|
"scanlator": "",
|
||||||
|
"uploadDate": 1465851600000,
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"source": "READMANGA_RU"
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package org.xtimms.shirizu
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.test.runner.AndroidJUnitRunner
|
||||||
|
import dagger.hilt.android.testing.HiltTestApplication
|
||||||
|
|
||||||
|
class HiltTestRunner : AndroidJUnitRunner() {
|
||||||
|
|
||||||
|
override fun newApplication(cl: ClassLoader?, name: String?, context: Context?): Application {
|
||||||
|
return super.newApplication(cl, HiltTestApplication::class.java.name, context)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package org.xtimms.shirizu
|
||||||
|
|
||||||
|
import android.app.Instrumentation
|
||||||
|
import kotlin.coroutines.resume
|
||||||
|
import kotlin.coroutines.suspendCoroutine
|
||||||
|
|
||||||
|
suspend fun Instrumentation.awaitForIdle() = suspendCoroutine<Unit> { cont ->
|
||||||
|
waitForIdle { cont.resume(Unit) }
|
||||||
|
}
|
||||||
59
app/src/androidTest/java/org/xtimms/shirizu/SampleData.kt
Normal file
59
app/src/androidTest/java/org/xtimms/shirizu/SampleData.kt
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
package org.xtimms.shirizu
|
||||||
|
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
|
import com.squareup.moshi.FromJson
|
||||||
|
import com.squareup.moshi.JsonAdapter
|
||||||
|
import com.squareup.moshi.JsonReader
|
||||||
|
import com.squareup.moshi.JsonWriter
|
||||||
|
import com.squareup.moshi.Moshi
|
||||||
|
import com.squareup.moshi.ToJson
|
||||||
|
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
|
||||||
|
import okio.buffer
|
||||||
|
import okio.source
|
||||||
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
|
import org.xtimms.shirizu.core.model.FavouriteCategory
|
||||||
|
import java.util.Date
|
||||||
|
import kotlin.reflect.KClass
|
||||||
|
|
||||||
|
object SampleData {
|
||||||
|
|
||||||
|
private val moshi = Moshi.Builder()
|
||||||
|
.add(DateAdapter())
|
||||||
|
.add(KotlinJsonAdapterFactory())
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val manga: Manga = loadAsset("manga/header.json", Manga::class)
|
||||||
|
|
||||||
|
val mangaDetails: Manga = loadAsset("manga/full.json", Manga::class)
|
||||||
|
|
||||||
|
val tag = mangaDetails.tags.elementAt(2)
|
||||||
|
|
||||||
|
val chapter = checkNotNull(mangaDetails.chapters)[2]
|
||||||
|
|
||||||
|
val favouriteCategory: FavouriteCategory = loadAsset("categories/simple.json", FavouriteCategory::class)
|
||||||
|
|
||||||
|
fun <T : Any> loadAsset(name: String, cls: KClass<T>): T {
|
||||||
|
val assets = InstrumentationRegistry.getInstrumentation().context.assets
|
||||||
|
return assets.open(name).use {
|
||||||
|
moshi.adapter(cls.java).fromJson(it.source().buffer())
|
||||||
|
} ?: throw RuntimeException("Cannot read asset from json \"$name\"")
|
||||||
|
}
|
||||||
|
|
||||||
|
private class DateAdapter : JsonAdapter<Date>() {
|
||||||
|
|
||||||
|
@FromJson
|
||||||
|
override fun fromJson(reader: JsonReader): Date? {
|
||||||
|
val ms = reader.nextLong()
|
||||||
|
return if (ms == 0L) {
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
Date(ms)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@ToJson
|
||||||
|
override fun toJson(writer: JsonWriter, value: Date?) {
|
||||||
|
writer.value(value?.time ?: 0L)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package org.xtimms.shirizu.core.database
|
||||||
|
|
||||||
|
import androidx.room.testing.MigrationTestHelper
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
|
import org.junit.Assert.assertEquals
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class ShirizuDatabaseTest {
|
||||||
|
|
||||||
|
@get:Rule
|
||||||
|
val helper: MigrationTestHelper = MigrationTestHelper(
|
||||||
|
InstrumentationRegistry.getInstrumentation(),
|
||||||
|
ShirizuDatabase::class.java,
|
||||||
|
)
|
||||||
|
|
||||||
|
private val migrations = getDatabaseMigrations(InstrumentationRegistry.getInstrumentation().targetContext)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun versions() {
|
||||||
|
assertEquals(1, migrations.first().startVersion)
|
||||||
|
repeat(migrations.size) { i ->
|
||||||
|
assertEquals(i + 1, migrations[i].startVersion)
|
||||||
|
assertEquals(i + 2, migrations[i].endVersion)
|
||||||
|
}
|
||||||
|
assertEquals(DATABASE_VERSION, migrations.last().endVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun migrateAll() {
|
||||||
|
helper.createDatabase(TEST_DB, 1).close()
|
||||||
|
for (migration in migrations) {
|
||||||
|
helper.runMigrationsAndValidate(
|
||||||
|
TEST_DB,
|
||||||
|
migration.endVersion,
|
||||||
|
true,
|
||||||
|
migration,
|
||||||
|
).close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun prePopulate() {
|
||||||
|
val resources = InstrumentationRegistry.getInstrumentation().targetContext.resources
|
||||||
|
helper.createDatabase(TEST_DB, DATABASE_VERSION).use {
|
||||||
|
DatabasePrePopulateCallback(resources).onCreate(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
|
||||||
|
const val TEST_DB = "test-db"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
package org.xtimms.shirizu.sections.settings.backup
|
||||||
|
|
||||||
|
import android.content.res.AssetManager
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import androidx.test.platform.app.InstrumentationRegistry
|
||||||
|
import dagger.hilt.android.testing.HiltAndroidRule
|
||||||
|
import dagger.hilt.android.testing.HiltAndroidTest
|
||||||
|
import kotlinx.coroutines.flow.first
|
||||||
|
import kotlinx.coroutines.test.runTest
|
||||||
|
import org.junit.Assert.*
|
||||||
|
import org.junit.Before
|
||||||
|
import org.junit.Rule
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.xtimms.shirizu.SampleData
|
||||||
|
import org.xtimms.shirizu.core.database.ShirizuDatabase
|
||||||
|
import org.xtimms.shirizu.core.database.entity.toMangaTags
|
||||||
|
import org.xtimms.shirizu.data.repository.FavouritesRepository
|
||||||
|
import org.xtimms.shirizu.data.repository.HistoryRepository
|
||||||
|
import org.xtimms.shirizu.data.repository.backup.BackupRepository
|
||||||
|
import java.io.File
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@HiltAndroidTest
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
class AppBackupAgentTest {
|
||||||
|
|
||||||
|
@get:Rule
|
||||||
|
var hiltRule = HiltAndroidRule(this)
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var historyRepository: HistoryRepository
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var favouritesRepository: FavouritesRepository
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var backupRepository: BackupRepository
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var database: ShirizuDatabase
|
||||||
|
|
||||||
|
@Before
|
||||||
|
fun setUp() {
|
||||||
|
hiltRule.inject()
|
||||||
|
database.clearAllTables()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun backupAndRestore() = runTest {
|
||||||
|
val category = favouritesRepository.createCategory(
|
||||||
|
title = SampleData.favouriteCategory.title,
|
||||||
|
sortOrder = SampleData.favouriteCategory.order,
|
||||||
|
isTrackerEnabled = SampleData.favouriteCategory.isTrackingEnabled,
|
||||||
|
isVisibleOnShelf = SampleData.favouriteCategory.isVisibleInLibrary,
|
||||||
|
)
|
||||||
|
favouritesRepository.addToCategory(categoryId = category.id, mangas = listOf(SampleData.manga))
|
||||||
|
historyRepository.addOrUpdate(
|
||||||
|
manga = SampleData.mangaDetails,
|
||||||
|
chapterId = SampleData.mangaDetails.chapters!![2].id,
|
||||||
|
page = 3,
|
||||||
|
scroll = 40,
|
||||||
|
percent = 0.2f,
|
||||||
|
)
|
||||||
|
val history = checkNotNull(historyRepository.getOne(SampleData.manga))
|
||||||
|
|
||||||
|
val agent = AppBackupAgent()
|
||||||
|
val backup = agent.createBackupFile(
|
||||||
|
context = InstrumentationRegistry.getInstrumentation().targetContext,
|
||||||
|
repository = backupRepository,
|
||||||
|
)
|
||||||
|
|
||||||
|
database.clearAllTables()
|
||||||
|
assertTrue(favouritesRepository.getAllManga().isEmpty())
|
||||||
|
assertNull(historyRepository.getLastOrNull())
|
||||||
|
|
||||||
|
backup.inputStream().use {
|
||||||
|
agent.restoreBackupFile(it.fd, backup.length(), backupRepository)
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEquals(category, favouritesRepository.getCategory(category.id))
|
||||||
|
assertEquals(history, historyRepository.getOne(SampleData.manga))
|
||||||
|
assertEquals(listOf(SampleData.manga), favouritesRepository.getManga(category.id))
|
||||||
|
|
||||||
|
val allTags = database.getTagsDao().findTags(SampleData.tag.source.name).toMangaTags()
|
||||||
|
assertTrue(SampleData.tag in allTags)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun restoreOldBackup() {
|
||||||
|
val agent = AppBackupAgent()
|
||||||
|
val backup = File.createTempFile("backup_", ".tmp")
|
||||||
|
InstrumentationRegistry.getInstrumentation().context.assets
|
||||||
|
.open("kotatsu_test.bak", AssetManager.ACCESS_STREAMING)
|
||||||
|
.use { input ->
|
||||||
|
backup.outputStream().use { output ->
|
||||||
|
input.copyTo(output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
backup.inputStream().use {
|
||||||
|
agent.restoreBackupFile(it.fd, backup.length(), backupRepository)
|
||||||
|
}
|
||||||
|
runTest {
|
||||||
|
assertEquals(6, historyRepository.observeAll().first().size)
|
||||||
|
assertEquals(2, favouritesRepository.observeCategories().first().size)
|
||||||
|
assertEquals(15, favouritesRepository.getAllManga().size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
package org.xtimms.tokusho
|
|
||||||
|
|
||||||
import androidx.test.platform.app.InstrumentationRegistry
|
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
|
||||||
|
|
||||||
import org.junit.Test
|
|
||||||
import org.junit.runner.RunWith
|
|
||||||
|
|
||||||
import org.junit.Assert.*
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Instrumented test, which will execute on an Android device.
|
|
||||||
*
|
|
||||||
* See [testing documentation](http://d.android.com/tools/testing).
|
|
||||||
*/
|
|
||||||
@RunWith(AndroidJUnit4::class)
|
|
||||||
class ExampleInstrumentedTest {
|
|
||||||
@Test
|
|
||||||
fun useAppContext() {
|
|
||||||
// Context of the app under test.
|
|
||||||
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
|
|
||||||
assertEquals("org.xtimms.tokusho", appContext.packageName)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,27 +4,52 @@
|
|||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
|
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
||||||
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
|
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
|
||||||
|
<uses-permission
|
||||||
|
android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||||
|
android:maxSdkVersion="29" />
|
||||||
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||||
|
<uses-permission
|
||||||
|
android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
|
||||||
|
tools:ignore="ScopedStorage" />
|
||||||
|
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES"/>
|
||||||
|
<uses-permission
|
||||||
|
android:name="android.permission.QUERY_ALL_PACKAGES"
|
||||||
|
tools:ignore="QueryAllPackagesPermission" />
|
||||||
|
|
||||||
|
<queries>
|
||||||
|
<intent>
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
</intent>
|
||||||
|
</queries>
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name=".App"
|
android:name=".App"
|
||||||
android:allowBackup="false"
|
android:allowBackup="false"
|
||||||
android:enableOnBackInvokedCallback="true"
|
android:backupAgent=".sections.settings.backup.AppBackupAgent"
|
||||||
android:hardwareAccelerated="true"
|
|
||||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||||
|
android:enableOnBackInvokedCallback="true"
|
||||||
android:fullBackupContent="@xml/backup_rules"
|
android:fullBackupContent="@xml/backup_rules"
|
||||||
|
android:hardwareAccelerated="true"
|
||||||
android:icon="@mipmap/ic_launcher"
|
android:icon="@mipmap/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:largeHeap="true"
|
android:largeHeap="true"
|
||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:networkSecurityConfig="@xml/network_security_config"
|
||||||
|
android:requestLegacyExternalStorage="true"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.Tokusho"
|
android:theme="@style/Theme.Shirizu"
|
||||||
tools:targetApi="tiramisu">
|
tools:targetApi="tiramisu">
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:launchMode="singleTop"
|
android:launchMode="singleTop"
|
||||||
android:theme="@style/Theme.Tokusho">
|
android:theme="@style/Theme.Shirizu.SplashTheme">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|
||||||
@@ -37,6 +62,22 @@
|
|||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:process=":error_handler" />
|
android:process=":error_handler" />
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".core.scrobbling.ScrobblingLoginActivity"
|
||||||
|
android:exported="true"
|
||||||
|
android:label="@string/scrobbling">
|
||||||
|
<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="shrz" />
|
||||||
|
|
||||||
|
<data android:host="shikimori-auth" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name="androidx.work.impl.foreground.SystemForegroundService"
|
android:name="androidx.work.impl.foreground.SystemForegroundService"
|
||||||
android:foregroundServiceType="dataSync"
|
android:foregroundServiceType="dataSync"
|
||||||
@@ -44,14 +85,33 @@
|
|||||||
|
|
||||||
<provider
|
<provider
|
||||||
android:name="androidx.core.content.FileProvider"
|
android:name="androidx.core.content.FileProvider"
|
||||||
android:authorities="org.xtimms.tokusho.provider"
|
android:authorities="${applicationId}.files"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:grantUriPermissions="true">
|
android:grantUriPermissions="true">
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="android.support.FILE_PROVIDER_PATHS"
|
android:name="android.support.FILE_PROVIDER_PATHS"
|
||||||
android:resource="@xml/provider_paths" />
|
android:resource="@xml/provider_paths" />
|
||||||
</provider>
|
</provider>
|
||||||
|
<provider
|
||||||
|
android:name="androidx.startup.InitializationProvider"
|
||||||
|
android:authorities="${applicationId}.androidx-startup"
|
||||||
|
android:exported="false"
|
||||||
|
tools:node="remove">
|
||||||
|
<meta-data
|
||||||
|
android:name="androidx.work.WorkManagerInitializer"
|
||||||
|
android:value="androidx.startup"
|
||||||
|
tools:node="remove" />
|
||||||
|
</provider>
|
||||||
|
|
||||||
|
<meta-data
|
||||||
|
android:name="android.webkit.WebView.EnableSafeBrowsing"
|
||||||
|
android:value="false" />
|
||||||
|
<meta-data
|
||||||
|
android:name="android.webkit.WebView.MetricsOptOut"
|
||||||
|
android:value="true" />
|
||||||
|
<meta-data
|
||||||
|
android:name="com.samsung.android.icon_container.has_icon_container"
|
||||||
|
android:value="@bool/com_samsung_android_icon_container_has_icon_container" />
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
||||||
BIN
app/src/main/assets/font/manrope_variable.ttf
Normal file
BIN
app/src/main/assets/font/manrope_variable.ttf
Normal file
Binary file not shown.
17808
app/src/main/baseline-prof.txt
Normal file
17808
app/src/main/baseline-prof.txt
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,54 +1,107 @@
|
|||||||
package org.xtimms.tokusho
|
package org.xtimms.shirizu
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.pm.PackageInfo
|
import android.content.pm.PackageInfo
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
|
import android.net.ConnectivityManager
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.StrictMode
|
import android.os.StrictMode
|
||||||
|
import androidx.core.content.getSystemService
|
||||||
|
import androidx.hilt.work.HiltWorkerFactory
|
||||||
|
import androidx.work.Configuration
|
||||||
|
import androidx.work.WorkManager
|
||||||
|
import coil.ImageLoader
|
||||||
import com.google.android.material.color.DynamicColors
|
import com.google.android.material.color.DynamicColors
|
||||||
import com.tencent.mmkv.MMKV
|
import com.tencent.mmkv.MMKV
|
||||||
import dagger.hilt.android.HiltAndroidApp
|
import dagger.hilt.android.HiltAndroidApp
|
||||||
import kotlinx.coroutines.CoroutineScope
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.SupervisorJob
|
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import org.acra.ReportField
|
||||||
|
import org.acra.config.httpSender
|
||||||
|
import org.acra.data.StringFormat
|
||||||
|
import org.acra.ktx.initAcra
|
||||||
|
import org.acra.sender.HttpSender
|
||||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||||
import org.xtimms.tokusho.core.database.MangaDatabase
|
import org.xtimms.shirizu.core.database.ShirizuDatabase
|
||||||
import org.xtimms.tokusho.core.updates.Updater
|
import org.xtimms.shirizu.core.prefs.KotatsuAppSettings
|
||||||
import org.xtimms.tokusho.crash.CrashActivity
|
import org.xtimms.shirizu.core.updates.Updater
|
||||||
import org.xtimms.tokusho.crash.GlobalExceptionHandler
|
import org.xtimms.shirizu.crash.CrashActivity
|
||||||
|
import org.xtimms.shirizu.crash.GlobalExceptionHandler
|
||||||
|
import org.xtimms.shirizu.utils.lang.processLifecycleScope
|
||||||
|
import org.xtimms.shirizu.work.WorkScheduleManager
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Provider
|
import javax.inject.Provider
|
||||||
|
|
||||||
@HiltAndroidApp
|
@HiltAndroidApp
|
||||||
class App : Application() {
|
class App : Application(), Configuration.Provider {
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var database: Provider<MangaDatabase>
|
lateinit var database: Provider<ShirizuDatabase>
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var settings: KotatsuAppSettings
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var workerFactory: HiltWorkerFactory
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var workScheduleManager: WorkScheduleManager
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var workManagerProvider: Provider<WorkManager>
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var imageLoader: ImageLoader
|
||||||
|
|
||||||
|
override val workManagerConfiguration: Configuration
|
||||||
|
get() = Configuration.Builder()
|
||||||
|
.setWorkerFactory(workerFactory)
|
||||||
|
.build()
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
MMKV.initialize(this)
|
MMKV.initialize(this)
|
||||||
context = applicationContext
|
|
||||||
packageInfo = packageManager.run {
|
packageInfo = packageManager.run {
|
||||||
if (Build.VERSION.SDK_INT >= 33) getPackageInfo(
|
if (Build.VERSION.SDK_INT >= 33) getPackageInfo(
|
||||||
packageName, PackageManager.PackageInfoFlags.of(0)
|
packageName, PackageManager.PackageInfoFlags.of(0)
|
||||||
) else getPackageInfo(packageName, 0)
|
) else getPackageInfo(packageName, 0)
|
||||||
}
|
}
|
||||||
applicationScope = CoroutineScope(SupervisorJob())
|
|
||||||
DynamicColors.applyToActivitiesIfAvailable(this)
|
DynamicColors.applyToActivitiesIfAvailable(this)
|
||||||
|
connectivityManager = getSystemService()!!
|
||||||
|
|
||||||
applicationScope.launch((Dispatchers.IO)) {
|
processLifecycleScope.launch(Dispatchers.IO) {
|
||||||
try {
|
try {
|
||||||
Updater.deleteOutdatedApk()
|
Updater.deleteOutdatedApk(this@App)
|
||||||
} catch (_: Throwable) {
|
} catch (_: Throwable) {
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
initAcra {
|
||||||
|
buildConfigClass = BuildConfig::class.java
|
||||||
|
reportFormat = StringFormat.JSON
|
||||||
|
httpSender {
|
||||||
|
uri = BuildConfig.ACRA_URI
|
||||||
|
basicAuthLogin = BuildConfig.ACRA_AUTH_LOGIN
|
||||||
|
basicAuthPassword = BuildConfig.ACRA_AUTH_PASSWORD
|
||||||
|
httpMethod = HttpSender.Method.POST
|
||||||
|
}
|
||||||
|
reportContent = listOf(
|
||||||
|
ReportField.PACKAGE_NAME,
|
||||||
|
ReportField.INSTALLATION_ID,
|
||||||
|
ReportField.APP_VERSION_CODE,
|
||||||
|
ReportField.APP_VERSION_NAME,
|
||||||
|
ReportField.ANDROID_VERSION,
|
||||||
|
ReportField.PHONE_MODEL,
|
||||||
|
ReportField.STACK_TRACE,
|
||||||
|
ReportField.CRASH_CONFIGURATION,
|
||||||
|
ReportField.CUSTOM_DATA,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
GlobalExceptionHandler.initialize(applicationContext, CrashActivity::class.java)
|
GlobalExceptionHandler.initialize(applicationContext, CrashActivity::class.java)
|
||||||
|
workScheduleManager.init()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun attachBaseContext(base: Context?) {
|
override fun attachBaseContext(base: Context?) {
|
||||||
@@ -76,8 +129,8 @@ class App : Application() {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
lateinit var applicationScope: CoroutineScope
|
|
||||||
lateinit var packageInfo: PackageInfo
|
lateinit var packageInfo: PackageInfo
|
||||||
|
lateinit var connectivityManager: ConnectivityManager
|
||||||
|
|
||||||
fun getVersionReport(): String {
|
fun getVersionReport(): String {
|
||||||
val versionName = packageInfo.versionName
|
val versionName = packageInfo.versionName
|
||||||
@@ -95,9 +148,6 @@ class App : Application() {
|
|||||||
.append("Device information: Android $release (API ${Build.VERSION.SDK_INT})\n")
|
.append("Device information: Android $release (API ${Build.VERSION.SDK_INT})\n")
|
||||||
.append("Supported ABIs: ${Build.SUPPORTED_ABIS.contentToString()}\n").toString()
|
.append("Supported ABIs: ${Build.SUPPORTED_ABIS.contentToString()}\n").toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("StaticFieldLeak")
|
|
||||||
lateinit var context: Context
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
62
app/src/main/java/org/xtimms/shirizu/CompositionLocals.kt
Normal file
62
app/src/main/java/org/xtimms/shirizu/CompositionLocals.kt
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
package org.xtimms.shirizu
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.material3.dynamicDarkColorScheme
|
||||||
|
import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.compositionLocalOf
|
||||||
|
import androidx.compose.runtime.staticCompositionLocalOf
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import coil.ImageLoader
|
||||||
|
import org.xtimms.shirizu.core.logs.FileLogger
|
||||||
|
import org.xtimms.shirizu.ui.theme.SEED
|
||||||
|
import org.xtimms.shirizu.core.prefs.AppSettings
|
||||||
|
import org.xtimms.shirizu.core.prefs.DarkThemePreference
|
||||||
|
import org.xtimms.shirizu.core.prefs.paletteStyles
|
||||||
|
import org.xtimms.shirizu.core.scrobbling.services.kitsu.data.KitsuRepository
|
||||||
|
import org.xtimms.shirizu.core.scrobbling.services.shikimori.data.ShikimoriRepository
|
||||||
|
import org.xtimms.shirizu.ui.monet.LocalTonalPalettes
|
||||||
|
import org.xtimms.shirizu.ui.monet.PaletteStyle
|
||||||
|
import org.xtimms.shirizu.ui.monet.TonalPalettes.Companion.toTonalPalettes
|
||||||
|
|
||||||
|
data class BottomSheetScrollState(
|
||||||
|
val topPadding: Dp,
|
||||||
|
)
|
||||||
|
|
||||||
|
val LocalBottomSheetScrollState = compositionLocalOf { BottomSheetScrollState(0.dp) }
|
||||||
|
val LocalDarkTheme = compositionLocalOf { DarkThemePreference() }
|
||||||
|
val LocalSeedColor = compositionLocalOf { SEED }
|
||||||
|
val LocalDynamicColorSwitch = compositionLocalOf { false }
|
||||||
|
val LocalPaletteStyleIndex = compositionLocalOf { 0 }
|
||||||
|
val LocalWindowInsets = compositionLocalOf { PaddingValues(0.dp) }
|
||||||
|
val LocalWindowWidthState = staticCompositionLocalOf { WindowWidthSizeClass.Compact }
|
||||||
|
val LocalImageLoader = compositionLocalOf<ImageLoader> { error("No ImageLoader provided") }
|
||||||
|
val LocalLoggers = compositionLocalOf<Set<@JvmSuppressWildcards FileLogger>> { error("No file loggers provided") }
|
||||||
|
|
||||||
|
val LocalKitsuRepository = compositionLocalOf<KitsuRepository> { error("No KitsuRepository provided") }
|
||||||
|
val LocalShikimoriRepository = compositionLocalOf<ShikimoriRepository> { error("No ShikimoriRepository provided") }
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SettingsProvider(content: @Composable () -> Unit) {
|
||||||
|
AppSettings.AppSettingsStateFlow.collectAsState().value.run {
|
||||||
|
CompositionLocalProvider(
|
||||||
|
LocalDarkTheme provides darkTheme,
|
||||||
|
LocalSeedColor provides seedColor,
|
||||||
|
LocalPaletteStyleIndex provides paletteStyleIndex,
|
||||||
|
LocalTonalPalettes provides if (isDynamicColorEnabled && Build.VERSION.SDK_INT >= 31) dynamicDarkColorScheme(
|
||||||
|
LocalContext.current
|
||||||
|
).toTonalPalettes()
|
||||||
|
else Color(seedColor).toTonalPalettes(
|
||||||
|
paletteStyles.getOrElse(paletteStyleIndex) { PaletteStyle.TonalSpot }
|
||||||
|
),
|
||||||
|
LocalDynamicColorSwitch provides isDynamicColorEnabled,
|
||||||
|
content = content
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
508
app/src/main/java/org/xtimms/shirizu/MainActivity.kt
Normal file
508
app/src/main/java/org/xtimms/shirizu/MainActivity.kt
Normal file
@@ -0,0 +1,508 @@
|
|||||||
|
package org.xtimms.shirizu
|
||||||
|
|
||||||
|
import android.Manifest
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.provider.Settings
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.activity.compose.BackHandler
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.activity.compose.setContent
|
||||||
|
import androidx.activity.enableEdgeToEdge
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.compose.animation.AnimatedContent
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.expandVertically
|
||||||
|
import androidx.compose.animation.shrinkVertically
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.RowScope
|
||||||
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
|
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||||
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
|
import androidx.compose.foundation.layout.navigationBars
|
||||||
|
import androidx.compose.foundation.layout.only
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.RssFeed
|
||||||
|
import androidx.compose.material.icons.outlined.Settings
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.LocalContentColor
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.NavigationBar
|
||||||
|
import androidx.compose.material3.NavigationBarItem
|
||||||
|
import androidx.compose.material3.NavigationRail
|
||||||
|
import androidx.compose.material3.NavigationRailItem
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.MutableState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.produceState
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.util.fastForEach
|
||||||
|
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||||
|
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||||
|
import cafe.adriel.voyager.navigator.Navigator
|
||||||
|
import cafe.adriel.voyager.navigator.NavigatorDisposeBehavior
|
||||||
|
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||||
|
import cafe.adriel.voyager.navigator.tab.LocalTabNavigator
|
||||||
|
import cafe.adriel.voyager.navigator.tab.TabNavigator
|
||||||
|
import coil.ImageLoader
|
||||||
|
import dagger.hilt.android.AndroidEntryPoint
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.channels.Channel
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
import kotlinx.coroutines.flow.receiveAsFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import org.xtimms.shirizu.core.components.AppBar
|
||||||
|
import org.xtimms.shirizu.core.components.AppBarActions
|
||||||
|
import org.xtimms.shirizu.core.components.AppToolbar
|
||||||
|
import org.xtimms.shirizu.core.components.Scaffold
|
||||||
|
import org.xtimms.shirizu.core.components.icons.Creation
|
||||||
|
import org.xtimms.shirizu.core.logs.FileLogger
|
||||||
|
import org.xtimms.shirizu.core.onboarding.OnboardingScreen
|
||||||
|
import org.xtimms.shirizu.core.prefs.AppSettings
|
||||||
|
import org.xtimms.shirizu.core.scrobbling.services.kitsu.data.KitsuRepository
|
||||||
|
import org.xtimms.shirizu.core.scrobbling.services.shikimori.data.ShikimoriRepository
|
||||||
|
import org.xtimms.shirizu.core.ui.dialogs.UpdateDialogImpl
|
||||||
|
import org.xtimms.shirizu.core.updates.Updater
|
||||||
|
import org.xtimms.shirizu.sections.explore.ExploreTab
|
||||||
|
import org.xtimms.shirizu.sections.feed.FeedScreen
|
||||||
|
import org.xtimms.shirizu.sections.history.HistoryTab
|
||||||
|
import org.xtimms.shirizu.sections.library.LibraryTab
|
||||||
|
import org.xtimms.shirizu.sections.onboarding.OnboardingScreen
|
||||||
|
import org.xtimms.shirizu.sections.profile.ProfileTab
|
||||||
|
import org.xtimms.shirizu.sections.search.SearchTab
|
||||||
|
import org.xtimms.shirizu.sections.settings.SettingsScreen
|
||||||
|
import org.xtimms.shirizu.sections.shelf.ShelfTab
|
||||||
|
import org.xtimms.shirizu.sections.suggestions.SuggestionsScreen
|
||||||
|
import org.xtimms.shirizu.ui.theme.ShirizuTheme
|
||||||
|
import org.xtimms.shirizu.utils.lang.DefaultNavigatorScreenTransition
|
||||||
|
import org.xtimms.shirizu.utils.lang.NoLiftingAppBarScreen
|
||||||
|
import org.xtimms.shirizu.utils.lang.Screen
|
||||||
|
import org.xtimms.shirizu.utils.lang.isTabletUi
|
||||||
|
import org.xtimms.shirizu.utils.lang.materialSharedAxisX
|
||||||
|
import org.xtimms.shirizu.utils.system.setLanguage
|
||||||
|
import org.xtimms.shirizu.utils.system.suspendToast
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@AndroidEntryPoint
|
||||||
|
class MainActivity : ComponentActivity() {
|
||||||
|
|
||||||
|
private val isReady: MutableState<Boolean> = mutableStateOf(false)
|
||||||
|
private val isDone: MutableState<Boolean> = mutableStateOf(false)
|
||||||
|
|
||||||
|
private var navigator: Navigator? = null
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var coil: ImageLoader
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var loggers: Set<@JvmSuppressWildcards FileLogger>
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var shikimoriRepository: ShikimoriRepository
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var kitsuRepository: KitsuRepository
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
installSplashScreen().setKeepOnScreenCondition { !isDone.value }
|
||||||
|
enableEdgeToEdge()
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
|
if (!isTaskRoot) {
|
||||||
|
finish()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
runBlocking {
|
||||||
|
if (Build.VERSION.SDK_INT < 33) {
|
||||||
|
setLanguage(AppSettings.getLocaleFromPreference())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setContent {
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
var updateJob: Job? = null
|
||||||
|
var latestRelease by remember { mutableStateOf(Updater.LatestRelease()) }
|
||||||
|
var showUpdateDialog by rememberSaveable { mutableStateOf(false) }
|
||||||
|
var currentDownloadStatus by remember { mutableStateOf(Updater.DownloadStatus.NotYet as Updater.DownloadStatus) }
|
||||||
|
|
||||||
|
val settings =
|
||||||
|
rememberLauncherForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||||
|
Updater.installLatestApk(context)
|
||||||
|
}
|
||||||
|
|
||||||
|
val launcher = rememberLauncherForActivityResult(
|
||||||
|
ActivityResultContracts.RequestPermission()
|
||||||
|
) { result ->
|
||||||
|
if (result) {
|
||||||
|
Updater.installLatestApk(context)
|
||||||
|
} else {
|
||||||
|
if (!context.packageManager.canRequestPackageInstalls())
|
||||||
|
settings.launch(
|
||||||
|
Intent(
|
||||||
|
Settings.ACTION_MANAGE_UNKNOWN_APP_SOURCES,
|
||||||
|
Uri.parse("package:${context.packageName}"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else
|
||||||
|
Updater.installLatestApk(context)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
isReady.value = true
|
||||||
|
}
|
||||||
|
if (isReady.value) {
|
||||||
|
CompositionLocalProvider(
|
||||||
|
LocalImageLoader provides coil,
|
||||||
|
LocalLoggers provides loggers,
|
||||||
|
LocalShikimoriRepository provides shikimoriRepository,
|
||||||
|
LocalKitsuRepository provides kitsuRepository
|
||||||
|
) {
|
||||||
|
SettingsProvider {
|
||||||
|
ShirizuTheme(
|
||||||
|
darkTheme = LocalDarkTheme.current.isDarkTheme(),
|
||||||
|
isDynamicColorEnabled = LocalDynamicColorSwitch.current,
|
||||||
|
isHighContrastModeEnabled = LocalDarkTheme.current.isHighContrastModeEnabled,
|
||||||
|
) {
|
||||||
|
Navigator(
|
||||||
|
screen = MainScreen,
|
||||||
|
disposeBehavior = NavigatorDisposeBehavior(
|
||||||
|
disposeNestedNavigators = false,
|
||||||
|
disposeSteps = true
|
||||||
|
),
|
||||||
|
) { navigator ->
|
||||||
|
LaunchedEffect(navigator) {
|
||||||
|
this@MainActivity.navigator = navigator
|
||||||
|
}
|
||||||
|
|
||||||
|
val scaffoldInsets =
|
||||||
|
WindowInsets.navigationBars.only(WindowInsetsSides.Horizontal)
|
||||||
|
Scaffold(
|
||||||
|
contentWindowInsets = scaffoldInsets,
|
||||||
|
) { contentPadding ->
|
||||||
|
// Consume insets already used by app state banners
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(contentPadding)
|
||||||
|
.consumeWindowInsets(contentPadding),
|
||||||
|
) {
|
||||||
|
// Shows current screen
|
||||||
|
DefaultNavigatorScreenTransition(navigator = navigator)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShowOnboarding()
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
isDone.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
if (!AppSettings.isAutoUpdateEnabled())
|
||||||
|
return@LaunchedEffect
|
||||||
|
launch(Dispatchers.IO) {
|
||||||
|
runCatching {
|
||||||
|
Updater.checkForUpdate(context)?.let {
|
||||||
|
latestRelease = it
|
||||||
|
showUpdateDialog = true
|
||||||
|
}
|
||||||
|
}.onFailure {
|
||||||
|
it.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showUpdateDialog) {
|
||||||
|
UpdateDialogImpl(
|
||||||
|
onDismissRequest = {
|
||||||
|
showUpdateDialog = false
|
||||||
|
updateJob?.cancel()
|
||||||
|
},
|
||||||
|
title = latestRelease.name.toString(),
|
||||||
|
onConfirmUpdate = {
|
||||||
|
updateJob = scope.launch(Dispatchers.IO) {
|
||||||
|
runCatching {
|
||||||
|
Updater.downloadApk(context, latestRelease)
|
||||||
|
.collect { downloadStatus ->
|
||||||
|
currentDownloadStatus = downloadStatus
|
||||||
|
if (downloadStatus is Updater.DownloadStatus.Finished) {
|
||||||
|
launcher.launch(Manifest.permission.REQUEST_INSTALL_PACKAGES)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.onFailure {
|
||||||
|
it.printStackTrace()
|
||||||
|
currentDownloadStatus =
|
||||||
|
Updater.DownloadStatus.NotYet
|
||||||
|
context.suspendToast(R.string.app_update_failed)
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
releaseNote = latestRelease.body.toString(),
|
||||||
|
downloadStatus = currentDownloadStatus
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
putDataToExtras(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ShowOnboarding() {
|
||||||
|
val navigator = LocalNavigator.currentOrThrow
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
if (navigator.lastItem !is OnboardingScreen) {
|
||||||
|
navigator.push(OnboardingScreen())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onNewIntent(intent: Intent) {
|
||||||
|
putDataToExtras(intent)
|
||||||
|
super.onNewIntent(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun putDataToExtras(intent: Intent?) {
|
||||||
|
intent?.putExtra(EXTRA_DATA, intent.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val TAG = "MainActivity"
|
||||||
|
const val EXTRA_DATA = "data"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
object MainScreen : Screen() {
|
||||||
|
|
||||||
|
private val openTabEvent = Channel<Tab>()
|
||||||
|
private val showBottomNavEvent = Channel<Boolean>()
|
||||||
|
|
||||||
|
private const val TabNavigatorKey = "HomeTabs"
|
||||||
|
|
||||||
|
private val tabs = listOf(
|
||||||
|
LibraryTab(),
|
||||||
|
// ShelfTab,
|
||||||
|
// HistoryTab,
|
||||||
|
ExploreTab(),
|
||||||
|
SearchTab,
|
||||||
|
// ProfileTab
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun Content() {
|
||||||
|
val navigator = LocalNavigator.currentOrThrow
|
||||||
|
TabNavigator(
|
||||||
|
tab = LibraryTab(),
|
||||||
|
key = TabNavigatorKey,
|
||||||
|
) { tabNavigator ->
|
||||||
|
// Provide usable navigator to content screen
|
||||||
|
CompositionLocalProvider(LocalNavigator provides navigator) {
|
||||||
|
Scaffold(
|
||||||
|
topBar = { scrollBehavior ->
|
||||||
|
if (!isTabletUi()) {
|
||||||
|
AppToolbar(
|
||||||
|
actions = {
|
||||||
|
AppBarActions(
|
||||||
|
persistentListOf(
|
||||||
|
AppBar.Action(
|
||||||
|
title = stringResource(R.string.feed),
|
||||||
|
icon = Icons.Outlined.RssFeed,
|
||||||
|
onClick = {
|
||||||
|
navigator.push(FeedScreen)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
AppBar.Action(
|
||||||
|
title = stringResource(R.string.settings),
|
||||||
|
icon = Icons.Outlined.Settings,
|
||||||
|
onClick = {
|
||||||
|
navigator.push(SettingsScreen)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
scrollBehavior = if (tabNavigator.current is NoLiftingAppBarScreen) {
|
||||||
|
null
|
||||||
|
} else scrollBehavior
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
startBar = {
|
||||||
|
if (isTabletUi()) {
|
||||||
|
NavigationRail {
|
||||||
|
tabs.fastForEach {
|
||||||
|
NavigationRailItem(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
bottomBar = {
|
||||||
|
if (!isTabletUi()) {
|
||||||
|
val bottomNavVisible by produceState(initialValue = true) {
|
||||||
|
showBottomNavEvent.receiveAsFlow().collectLatest { value = it }
|
||||||
|
}
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = bottomNavVisible,
|
||||||
|
enter = expandVertically(),
|
||||||
|
exit = shrinkVertically(),
|
||||||
|
) {
|
||||||
|
NavigationBar {
|
||||||
|
tabs.fastForEach {
|
||||||
|
NavigationBarItem(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
contentWindowInsets = WindowInsets(0),
|
||||||
|
) { contentPadding ->
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(contentPadding)
|
||||||
|
.consumeWindowInsets(contentPadding),
|
||||||
|
) {
|
||||||
|
AnimatedContent(
|
||||||
|
targetState = tabNavigator.current,
|
||||||
|
transitionSpec = {
|
||||||
|
materialSharedAxisX(forward = true)
|
||||||
|
},
|
||||||
|
label = "tabContent",
|
||||||
|
) {
|
||||||
|
tabNavigator.saveableState(key = "currentTab", it) {
|
||||||
|
it.Content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val goToLibraryTab = { tabNavigator.current = LibraryTab() }
|
||||||
|
BackHandler(
|
||||||
|
enabled = tabNavigator.current != LibraryTab(),
|
||||||
|
onBack = goToLibraryTab,
|
||||||
|
)
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
launch {
|
||||||
|
openTabEvent.receiveAsFlow().collectLatest {
|
||||||
|
tabNavigator.current = when (it) {
|
||||||
|
is Tab.Library -> LibraryTab()
|
||||||
|
// is Tab.Shelf -> ShelfTab
|
||||||
|
// is Tab.History -> HistoryTab
|
||||||
|
is Tab.Explore -> ExploreTab()
|
||||||
|
is Tab.Search -> SearchTab
|
||||||
|
// is Tab.Profile -> ProfileTab
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun RowScope.NavigationBarItem(tab: org.xtimms.shirizu.utils.lang.Tab) {
|
||||||
|
val tabNavigator = LocalTabNavigator.current
|
||||||
|
val navigator = LocalNavigator.currentOrThrow
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
val selected = tabNavigator.current::class == tab::class
|
||||||
|
NavigationBarItem(
|
||||||
|
selected = selected,
|
||||||
|
onClick = {
|
||||||
|
if (!selected) {
|
||||||
|
tabNavigator.current = tab
|
||||||
|
} else {
|
||||||
|
scope.launch { tab.onReselect(navigator) }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
icon = { NavigationIconItem(tab) },
|
||||||
|
label = {
|
||||||
|
Text(
|
||||||
|
text = tab.options.title,
|
||||||
|
style = MaterialTheme.typography.labelLarge,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
alwaysShowLabel = true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun NavigationRailItem(tab: org.xtimms.shirizu.utils.lang.Tab) {
|
||||||
|
val tabNavigator = LocalTabNavigator.current
|
||||||
|
val navigator = LocalNavigator.currentOrThrow
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
val selected = tabNavigator.current::class == tab::class
|
||||||
|
NavigationRailItem(
|
||||||
|
selected = selected,
|
||||||
|
onClick = {
|
||||||
|
if (!selected) {
|
||||||
|
tabNavigator.current = tab
|
||||||
|
} else {
|
||||||
|
scope.launch { tab.onReselect(navigator) }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
icon = { NavigationIconItem(tab) },
|
||||||
|
label = {
|
||||||
|
Text(
|
||||||
|
text = tab.options.title,
|
||||||
|
style = MaterialTheme.typography.labelLarge,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
alwaysShowLabel = true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun NavigationIconItem(tab: org.xtimms.shirizu.utils.lang.Tab) {
|
||||||
|
Icon(
|
||||||
|
painter = tab.options.icon!!,
|
||||||
|
contentDescription = tab.options.title,
|
||||||
|
// TODO: https://issuetracker.google.com/u/0/issues/316327367
|
||||||
|
tint = LocalContentColor.current,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun showBottomNav(show: Boolean) {
|
||||||
|
showBottomNavEvent.send(show)
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed interface Tab {
|
||||||
|
data object Library : Tab
|
||||||
|
// data object Shelf : Tab
|
||||||
|
// data object History : Tab
|
||||||
|
data object Explore : Tab
|
||||||
|
data object Search : Tab
|
||||||
|
// data object Profile : Tab
|
||||||
|
}
|
||||||
|
}
|
||||||
12
app/src/main/java/org/xtimms/shirizu/core/HapticFeedback.kt
Normal file
12
app/src/main/java/org/xtimms/shirizu/core/HapticFeedback.kt
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
package org.xtimms.shirizu.core
|
||||||
|
|
||||||
|
import android.view.HapticFeedbackConstants
|
||||||
|
import android.view.View
|
||||||
|
|
||||||
|
object HapticFeedback {
|
||||||
|
fun View.slightHapticFeedback() =
|
||||||
|
this.performHapticFeedback(HapticFeedbackConstants.CLOCK_TICK)
|
||||||
|
|
||||||
|
fun View.longPressHapticFeedback() =
|
||||||
|
this.performHapticFeedback(HapticFeedbackConstants.LONG_PRESS)
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package org.xtimms.shirizu.core
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.painter.ColorPainter
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import coil.compose.AsyncImage
|
||||||
|
import org.xtimms.shirizu.LocalImageLoader
|
||||||
|
import org.xtimms.shirizu.R
|
||||||
|
import org.xtimms.shirizu.utils.composable.rememberResourceBitmapPainter
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ShirizuAsyncImage(
|
||||||
|
model: Any? = null,
|
||||||
|
contentDescription: String?,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
contentScale: ContentScale = ContentScale.Crop,
|
||||||
|
) {
|
||||||
|
AsyncImage(
|
||||||
|
imageLoader = LocalImageLoader.current,
|
||||||
|
model = model,
|
||||||
|
placeholder = ColorPainter(Color(0x1F888888)),
|
||||||
|
error = rememberResourceBitmapPainter(id = R.drawable.cover_error),
|
||||||
|
fallback = rememberResourceBitmapPainter(id = R.drawable.cover_loading),
|
||||||
|
modifier = modifier,
|
||||||
|
contentScale = contentScale,
|
||||||
|
contentDescription = contentDescription
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
package org.xtimms.shirizu.core.base
|
||||||
|
|
||||||
|
import android.app.Dialog
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.annotation.CallSuper
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.fragment.app.DialogFragment
|
||||||
|
import androidx.viewbinding.ViewBinding
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
|
|
||||||
|
abstract class AlertDialogFragment<B : ViewBinding> : DialogFragment() {
|
||||||
|
|
||||||
|
var viewBinding: B? = null
|
||||||
|
private set
|
||||||
|
|
||||||
|
@Deprecated("", ReplaceWith("requireViewBinding()"))
|
||||||
|
protected val binding: B
|
||||||
|
get() = requireViewBinding()
|
||||||
|
|
||||||
|
final override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
|
val binding = onCreateViewBinding(layoutInflater, null)
|
||||||
|
viewBinding = binding
|
||||||
|
return MaterialAlertDialogBuilder(requireContext(), theme)
|
||||||
|
.setView(binding.root)
|
||||||
|
.run(::onBuildDialog)
|
||||||
|
.create()
|
||||||
|
.also(::onDialogCreated)
|
||||||
|
}
|
||||||
|
|
||||||
|
final override fun onCreateView(
|
||||||
|
inflater: LayoutInflater,
|
||||||
|
container: ViewGroup?,
|
||||||
|
savedInstanceState: Bundle?,
|
||||||
|
) = viewBinding?.root
|
||||||
|
|
||||||
|
final override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedInstanceState)
|
||||||
|
onViewBindingCreated(requireViewBinding(), savedInstanceState)
|
||||||
|
}
|
||||||
|
|
||||||
|
@CallSuper
|
||||||
|
override fun onDestroyView() {
|
||||||
|
viewBinding = null
|
||||||
|
super.onDestroyView()
|
||||||
|
}
|
||||||
|
|
||||||
|
open fun onBuildDialog(builder: MaterialAlertDialogBuilder): MaterialAlertDialogBuilder = builder
|
||||||
|
|
||||||
|
open fun onDialogCreated(dialog: AlertDialog) = Unit
|
||||||
|
|
||||||
|
@Deprecated("", ReplaceWith("viewBinding"))
|
||||||
|
protected fun bindingOrNull() = viewBinding
|
||||||
|
|
||||||
|
fun requireViewBinding(): B = checkNotNull(viewBinding) {
|
||||||
|
"Fragment $this did not return a ViewBinding from onCreateView() or this was called before onCreateView()."
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract fun onCreateViewBinding(inflater: LayoutInflater, container: ViewGroup?): B
|
||||||
|
|
||||||
|
protected open fun onViewBindingCreated(binding: B, savedInstanceState: Bundle?) = Unit
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
package org.xtimms.shirizu.core.base
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.res.Configuration
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.core.view.WindowCompat
|
||||||
|
import androidx.viewbinding.ViewBinding
|
||||||
|
|
||||||
|
@Suppress("LeakingThis")
|
||||||
|
abstract class BaseActivity<B : ViewBinding> :
|
||||||
|
AppCompatActivity() {
|
||||||
|
|
||||||
|
lateinit var viewBinding: B
|
||||||
|
private set
|
||||||
|
|
||||||
|
private var defaultStatusBarColor = Color.TRANSPARENT
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
putDataToExtras(intent)
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onNewIntent(intent: Intent) {
|
||||||
|
putDataToExtras(intent)
|
||||||
|
super.onNewIntent(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deprecated("Use ViewBinding", level = DeprecationLevel.ERROR)
|
||||||
|
override fun setContentView(layoutResID: Int) {
|
||||||
|
super.setContentView(layoutResID)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Deprecated("Use ViewBinding", level = DeprecationLevel.ERROR)
|
||||||
|
override fun setContentView(view: View?) {
|
||||||
|
super.setContentView(view)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun setContentView(binding: B) {
|
||||||
|
this.viewBinding = binding
|
||||||
|
super.setContentView(binding.root)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSupportNavigateUp(): Boolean {
|
||||||
|
dispatchNavigateUp()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun isDarkAmoledTheme(): Boolean {
|
||||||
|
val uiMode = resources.configuration.uiMode
|
||||||
|
return uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES
|
||||||
|
}
|
||||||
|
|
||||||
|
protected open fun dispatchNavigateUp() {
|
||||||
|
val upIntent = parentActivityIntent
|
||||||
|
if (upIntent != null) {
|
||||||
|
if (!navigateUpTo(upIntent)) {
|
||||||
|
startActivity(upIntent)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
finishAfterTransition()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun putDataToExtras(intent: Intent?) {
|
||||||
|
intent?.putExtra(EXTRA_DATA, intent.data)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
const val EXTRA_DATA = "data"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package org.xtimms.shirizu.core.base.event
|
||||||
|
|
||||||
|
interface PagedUiEvent : UiEvent {
|
||||||
|
fun loadMore()
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package org.xtimms.tokusho.core.base.event
|
package org.xtimms.shirizu.core.base.event
|
||||||
|
|
||||||
interface UiEvent {
|
interface UiEvent {
|
||||||
fun showMessage(message: String?)
|
fun showMessage(message: String?)
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package org.xtimms.shirizu.core.base.state
|
||||||
|
|
||||||
|
abstract class PagedUiState : UiState() {
|
||||||
|
|
||||||
|
abstract val nextPage: String?
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Trigger variable to load more items, be careful to set it to false after loading more
|
||||||
|
*/
|
||||||
|
abstract val loadMore: Boolean
|
||||||
|
|
||||||
|
val canLoadMore get() = nextPage != null && !isLoading
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package org.xtimms.tokusho.core.base.state
|
package org.xtimms.shirizu.core.base.state
|
||||||
|
|
||||||
abstract class UiState {
|
abstract class UiState {
|
||||||
abstract val isLoading: Boolean
|
abstract val isLoading: Boolean
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
package org.xtimms.shirizu.core.base.viewmodel
|
||||||
|
|
||||||
|
import cafe.adriel.voyager.core.model.ScreenModel
|
||||||
|
import cafe.adriel.voyager.core.model.screenModelScope
|
||||||
|
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.CoroutineStart
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.catch
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.onCompletion
|
||||||
|
import kotlinx.coroutines.flow.onStart
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.xtimms.shirizu.utils.lang.EventFlow
|
||||||
|
import org.xtimms.shirizu.utils.lang.MutableEventFlow
|
||||||
|
import org.xtimms.shirizu.utils.lang.call
|
||||||
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
import kotlin.coroutines.EmptyCoroutineContext
|
||||||
|
import kotlin.coroutines.cancellation.CancellationException
|
||||||
|
|
||||||
|
abstract class BaseStateScreenModel<S>(initialState: S) : ScreenModel {
|
||||||
|
|
||||||
|
protected val mutableState: MutableStateFlow<S> = MutableStateFlow(initialState)
|
||||||
|
public val state: StateFlow<S> = mutableState.asStateFlow()
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
protected val loadingCounter = MutableStateFlow(0)
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
protected val errorEvent = MutableEventFlow<Throwable>()
|
||||||
|
|
||||||
|
val onError: EventFlow<Throwable>
|
||||||
|
get() = errorEvent
|
||||||
|
|
||||||
|
val isLoading: StateFlow<Boolean> = loadingCounter.map { it > 0 }
|
||||||
|
.stateIn(screenModelScope, SharingStarted.Lazily, loadingCounter.value > 0)
|
||||||
|
|
||||||
|
protected fun launchJob(
|
||||||
|
context: CoroutineContext = EmptyCoroutineContext,
|
||||||
|
start: CoroutineStart = CoroutineStart.DEFAULT,
|
||||||
|
block: suspend CoroutineScope.() -> Unit
|
||||||
|
): Job = screenModelScope.launch(context + createErrorHandler(), start, block)
|
||||||
|
|
||||||
|
protected fun launchLoadingJob(
|
||||||
|
context: CoroutineContext = EmptyCoroutineContext,
|
||||||
|
start: CoroutineStart = CoroutineStart.DEFAULT,
|
||||||
|
block: suspend CoroutineScope.() -> Unit
|
||||||
|
): Job = screenModelScope.launch(context + createErrorHandler(), start) {
|
||||||
|
loadingCounter.increment()
|
||||||
|
try {
|
||||||
|
block()
|
||||||
|
} finally {
|
||||||
|
loadingCounter.decrement()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun <T> Flow<T>.withLoading() = onStart {
|
||||||
|
loadingCounter.increment()
|
||||||
|
}.onCompletion {
|
||||||
|
loadingCounter.decrement()
|
||||||
|
}
|
||||||
|
|
||||||
|
protected suspend inline fun <T> withLoading(block: () -> T): T = try {
|
||||||
|
loadingCounter.increment()
|
||||||
|
block()
|
||||||
|
} finally {
|
||||||
|
loadingCounter.decrement()
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun <T> Flow<T>.withErrorHandling() = catch { error ->
|
||||||
|
errorEvent.call(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun MutableStateFlow<Int>.increment() = update { it + 1 }
|
||||||
|
|
||||||
|
protected fun MutableStateFlow<Int>.decrement() = update { it - 1 }
|
||||||
|
|
||||||
|
private fun createErrorHandler() = CoroutineExceptionHandler { _, throwable ->
|
||||||
|
if (throwable !is CancellationException) {
|
||||||
|
errorEvent.call(throwable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
package org.xtimms.shirizu.core.base.viewmodel
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.CoroutineStart
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.xtimms.shirizu.core.base.event.UiEvent
|
||||||
|
import org.xtimms.shirizu.core.base.state.UiState
|
||||||
|
import org.xtimms.shirizu.utils.lang.EventFlow
|
||||||
|
import org.xtimms.shirizu.utils.lang.MutableEventFlow
|
||||||
|
import org.xtimms.shirizu.utils.lang.call
|
||||||
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
import kotlin.coroutines.EmptyCoroutineContext
|
||||||
|
import kotlin.coroutines.cancellation.CancellationException
|
||||||
|
|
||||||
|
abstract class BaseViewModel<S : UiState> : ViewModel(), UiEvent {
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
protected val loadingCounter = MutableStateFlow(0)
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
protected val errorEvent = MutableEventFlow<Throwable>()
|
||||||
|
|
||||||
|
val onError: EventFlow<Throwable>
|
||||||
|
get() = errorEvent
|
||||||
|
|
||||||
|
protected abstract val mutableUiState: MutableStateFlow<S>
|
||||||
|
val uiState: StateFlow<S> by lazy { mutableUiState.asStateFlow() }
|
||||||
|
|
||||||
|
protected fun launchJob(
|
||||||
|
context: CoroutineContext = EmptyCoroutineContext,
|
||||||
|
start: CoroutineStart = CoroutineStart.DEFAULT,
|
||||||
|
block: suspend CoroutineScope.() -> Unit
|
||||||
|
): Job = viewModelScope.launch(context + createErrorHandler(), start, block)
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
fun setLoading(value: Boolean) {
|
||||||
|
mutableUiState.update { it.setLoading(value) as S }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
override fun showMessage(message: String?) {
|
||||||
|
mutableUiState.update { it.setMessage(message ?: GENERIC_ERROR) as S }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
override fun onMessageDisplayed() {
|
||||||
|
mutableUiState.update { it.setMessage(null) as S }
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun launchLoadingJob(
|
||||||
|
context: CoroutineContext = EmptyCoroutineContext,
|
||||||
|
start: CoroutineStart = CoroutineStart.DEFAULT,
|
||||||
|
block: suspend CoroutineScope.() -> Unit
|
||||||
|
): Job = viewModelScope.launch(context + createErrorHandler(), start) {
|
||||||
|
loadingCounter.increment()
|
||||||
|
try {
|
||||||
|
block()
|
||||||
|
} finally {
|
||||||
|
loadingCounter.decrement()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun MutableStateFlow<Int>.increment() = update { it + 1 }
|
||||||
|
|
||||||
|
protected fun MutableStateFlow<Int>.decrement() = update { it - 1 }
|
||||||
|
|
||||||
|
private fun createErrorHandler() = CoroutineExceptionHandler { _, throwable ->
|
||||||
|
if (throwable !is CancellationException) {
|
||||||
|
errorEvent.call(throwable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val GENERIC_ERROR = "Generic Error"
|
||||||
|
const val FLOW_TIMEOUT = 5_000L
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
package org.xtimms.shirizu.core.base.viewmodel
|
||||||
|
|
||||||
|
import androidx.lifecycle.ViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.CoroutineStart
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.catch
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.onCompletion
|
||||||
|
import kotlinx.coroutines.flow.onStart
|
||||||
|
import kotlinx.coroutines.flow.stateIn
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.xtimms.shirizu.utils.lang.EventFlow
|
||||||
|
import org.xtimms.shirizu.utils.lang.MutableEventFlow
|
||||||
|
import org.xtimms.shirizu.utils.lang.call
|
||||||
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
import kotlin.coroutines.EmptyCoroutineContext
|
||||||
|
import kotlin.coroutines.cancellation.CancellationException
|
||||||
|
|
||||||
|
abstract class KotatsuBaseViewModel : ViewModel() {
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
protected val loadingCounter = MutableStateFlow(0)
|
||||||
|
|
||||||
|
@JvmField
|
||||||
|
protected val errorEvent = MutableEventFlow<Throwable>()
|
||||||
|
|
||||||
|
val onError: EventFlow<Throwable>
|
||||||
|
get() = errorEvent
|
||||||
|
|
||||||
|
val isLoading: StateFlow<Boolean> = loadingCounter.map { it > 0 }
|
||||||
|
.stateIn(viewModelScope, SharingStarted.Lazily, loadingCounter.value > 0)
|
||||||
|
|
||||||
|
protected fun launchJob(
|
||||||
|
context: CoroutineContext = EmptyCoroutineContext,
|
||||||
|
start: CoroutineStart = CoroutineStart.DEFAULT,
|
||||||
|
block: suspend CoroutineScope.() -> Unit
|
||||||
|
): Job = viewModelScope.launch(context + createErrorHandler(), start, block)
|
||||||
|
|
||||||
|
protected fun launchLoadingJob(
|
||||||
|
context: CoroutineContext = EmptyCoroutineContext,
|
||||||
|
start: CoroutineStart = CoroutineStart.DEFAULT,
|
||||||
|
block: suspend CoroutineScope.() -> Unit
|
||||||
|
): Job = viewModelScope.launch(context + createErrorHandler(), start) {
|
||||||
|
loadingCounter.increment()
|
||||||
|
try {
|
||||||
|
block()
|
||||||
|
} finally {
|
||||||
|
loadingCounter.decrement()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun <T> Flow<T>.withLoading() = onStart {
|
||||||
|
loadingCounter.increment()
|
||||||
|
}.onCompletion {
|
||||||
|
loadingCounter.decrement()
|
||||||
|
}
|
||||||
|
|
||||||
|
protected suspend inline fun <T> withLoading(block: () -> T): T = try {
|
||||||
|
loadingCounter.increment()
|
||||||
|
block()
|
||||||
|
} finally {
|
||||||
|
loadingCounter.decrement()
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun <T> Flow<T>.withErrorHandling() = catch { error ->
|
||||||
|
errorEvent.call(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fun MutableStateFlow<Int>.increment() = update { it + 1 }
|
||||||
|
|
||||||
|
protected fun MutableStateFlow<Int>.decrement() = update { it - 1 }
|
||||||
|
|
||||||
|
private fun createErrorHandler() = CoroutineExceptionHandler { _, throwable ->
|
||||||
|
if (throwable !is CancellationException) {
|
||||||
|
errorEvent.call(throwable)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package org.xtimms.tokusho.core.cache
|
package org.xtimms.shirizu.core.cache
|
||||||
|
|
||||||
enum class CacheDir(val dir: String) {
|
enum class CacheDir(val dir: String) {
|
||||||
|
|
||||||
@@ -1,17 +1,20 @@
|
|||||||
package org.xtimms.tokusho.core.cache
|
package org.xtimms.shirizu.core.cache
|
||||||
|
|
||||||
import androidx.collection.LruCache
|
import androidx.collection.LruCache
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
import org.xtimms.shirizu.core.cache.MemoryContentCache.Key as CacheKey
|
||||||
|
|
||||||
class ExpiringLruCache<T>(
|
class ExpiringLruCache<T>(
|
||||||
val maxSize: Int,
|
val maxSize: Int,
|
||||||
private val lifetime: Long,
|
private val lifetime: Long,
|
||||||
private val timeUnit: TimeUnit,
|
private val timeUnit: TimeUnit,
|
||||||
) {
|
) : Iterable<CacheKey> {
|
||||||
|
|
||||||
private val cache = LruCache<ContentCache.Key, ExpiringValue<T>>(maxSize)
|
private val cache = LruCache<CacheKey, ExpiringValue<T>>(maxSize)
|
||||||
|
|
||||||
operator fun get(key: ContentCache.Key): T? {
|
override fun iterator(): Iterator<CacheKey> = cache.snapshot().keys.iterator()
|
||||||
|
|
||||||
|
operator fun get(key: CacheKey): T? {
|
||||||
val value = cache[key] ?: return null
|
val value = cache[key] ?: return null
|
||||||
if (value.isExpired) {
|
if (value.isExpired) {
|
||||||
cache.remove(key)
|
cache.remove(key)
|
||||||
@@ -19,7 +22,7 @@ class ExpiringLruCache<T>(
|
|||||||
return value.get()
|
return value.get()
|
||||||
}
|
}
|
||||||
|
|
||||||
operator fun set(key: ContentCache.Key, value: T) {
|
operator fun set(key: CacheKey, value: T) {
|
||||||
cache.put(key, ExpiringValue(value, lifetime, timeUnit))
|
cache.put(key, ExpiringValue(value, lifetime, timeUnit))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,4 +33,8 @@ class ExpiringLruCache<T>(
|
|||||||
fun trimToSize(size: Int) {
|
fun trimToSize(size: Int) {
|
||||||
cache.trimToSize(size)
|
cache.trimToSize(size)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun remove(key: CacheKey) {
|
||||||
|
cache.remove(key)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package org.xtimms.tokusho.core.cache
|
package org.xtimms.shirizu.core.cache
|
||||||
|
|
||||||
import android.os.SystemClock
|
import android.os.SystemClock
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
95
app/src/main/java/org/xtimms/shirizu/core/cache/MemoryContentCache.kt
vendored
Normal file
95
app/src/main/java/org/xtimms/shirizu/core/cache/MemoryContentCache.kt
vendored
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
package org.xtimms.shirizu.core.cache
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.ComponentCallbacks2
|
||||||
|
import android.content.res.Configuration
|
||||||
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
|
import org.xtimms.shirizu.utils.system.isLowRamDevice
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
class MemoryContentCache @Inject constructor(application: Application) : ComponentCallbacks2 {
|
||||||
|
|
||||||
|
private val isLowRam = application.isLowRamDevice()
|
||||||
|
|
||||||
|
private val detailsCache = ExpiringLruCache<SafeDeferred<Manga>>(if (isLowRam) 1 else 4, 5, TimeUnit.MINUTES)
|
||||||
|
private val pagesCache =
|
||||||
|
ExpiringLruCache<SafeDeferred<List<MangaPage>>>(if (isLowRam) 1 else 4, 10, TimeUnit.MINUTES)
|
||||||
|
private val relatedMangaCache =
|
||||||
|
ExpiringLruCache<SafeDeferred<List<Manga>>>(if (isLowRam) 1 else 3, 10, TimeUnit.MINUTES)
|
||||||
|
|
||||||
|
init {
|
||||||
|
application.registerComponentCallbacks(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getDetails(source: MangaSource, url: String): Manga? {
|
||||||
|
return detailsCache[Key(source, url)]?.awaitOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun putDetails(source: MangaSource, url: String, details: SafeDeferred<Manga>) {
|
||||||
|
detailsCache[Key(source, url)] = details
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getPages(source: MangaSource, url: String): List<MangaPage>? {
|
||||||
|
return pagesCache[Key(source, url)]?.awaitOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun putPages(source: MangaSource, url: String, pages: SafeDeferred<List<MangaPage>>) {
|
||||||
|
pagesCache[Key(source, url)] = pages
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun getRelatedManga(source: MangaSource, url: String): List<Manga>? {
|
||||||
|
return relatedMangaCache[Key(source, url)]?.awaitOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun putRelatedManga(source: MangaSource, url: String, related: SafeDeferred<List<Manga>>) {
|
||||||
|
relatedMangaCache[Key(source, url)] = related
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clear(source: MangaSource) {
|
||||||
|
clearCache(detailsCache, source)
|
||||||
|
clearCache(pagesCache, source)
|
||||||
|
clearCache(relatedMangaCache, source)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onConfigurationChanged(newConfig: Configuration) = Unit
|
||||||
|
|
||||||
|
override fun onLowMemory() = Unit
|
||||||
|
|
||||||
|
override fun onTrimMemory(level: Int) {
|
||||||
|
trimCache(detailsCache, level)
|
||||||
|
trimCache(pagesCache, level)
|
||||||
|
trimCache(relatedMangaCache, level)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun trimCache(cache: ExpiringLruCache<*>, level: Int) {
|
||||||
|
when (level) {
|
||||||
|
ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL,
|
||||||
|
ComponentCallbacks2.TRIM_MEMORY_COMPLETE,
|
||||||
|
ComponentCallbacks2.TRIM_MEMORY_MODERATE -> cache.clear()
|
||||||
|
|
||||||
|
ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN,
|
||||||
|
ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW,
|
||||||
|
ComponentCallbacks2.TRIM_MEMORY_BACKGROUND -> cache.trimToSize(1)
|
||||||
|
|
||||||
|
else -> cache.trimToSize(cache.maxSize / 2)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun clearCache(cache: ExpiringLruCache<*>, source: MangaSource) {
|
||||||
|
cache.forEach { key ->
|
||||||
|
if (key.source == source) {
|
||||||
|
cache.remove(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Key(
|
||||||
|
val source: MangaSource,
|
||||||
|
val url: String,
|
||||||
|
)
|
||||||
|
}
|
||||||
95
app/src/main/java/org/xtimms/shirizu/core/cache/PagesCache.kt
vendored
Normal file
95
app/src/main/java/org/xtimms/shirizu/core/cache/PagesCache.kt
vendored
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
package org.xtimms.shirizu.core.cache
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.os.StatFs
|
||||||
|
import com.tomclaw.cache.DiskLruCache
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.runInterruptible
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import okio.Source
|
||||||
|
import okio.buffer
|
||||||
|
import okio.sink
|
||||||
|
import org.koitharu.kotatsu.parsers.util.SuspendLazy
|
||||||
|
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||||
|
import org.xtimms.shirizu.utils.FileSize
|
||||||
|
import org.xtimms.shirizu.utils.lang.longHashCode
|
||||||
|
import org.xtimms.shirizu.utils.lang.writeAllCancellable
|
||||||
|
import org.xtimms.shirizu.utils.system.compressToPNG
|
||||||
|
import org.xtimms.shirizu.utils.system.subdir
|
||||||
|
import org.xtimms.shirizu.utils.system.takeIfReadable
|
||||||
|
import org.xtimms.shirizu.utils.system.takeIfWriteable
|
||||||
|
import java.io.File
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
|
@Singleton
|
||||||
|
class PagesCache @Inject constructor(@ApplicationContext context: Context) {
|
||||||
|
|
||||||
|
private val cacheDir = SuspendLazy {
|
||||||
|
val dirs = context.externalCacheDirs + context.cacheDir
|
||||||
|
dirs.firstNotNullOf {
|
||||||
|
it?.subdir(CacheDir.PAGES.dir)?.takeIfWriteable()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private val lruCache = SuspendLazy {
|
||||||
|
val dir = cacheDir.get()
|
||||||
|
val availableSize = (getAvailableSize() * 0.8).toLong()
|
||||||
|
val size = SIZE_DEFAULT.coerceAtMost(availableSize).coerceAtLeast(SIZE_MIN)
|
||||||
|
runCatchingCancellable {
|
||||||
|
DiskLruCache.create(dir, size)
|
||||||
|
}.recoverCatching { error ->
|
||||||
|
error.printStackTrace()
|
||||||
|
dir.deleteRecursively()
|
||||||
|
dir.mkdir()
|
||||||
|
DiskLruCache.create(dir, size)
|
||||||
|
}.getOrThrow()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun get(url: String): File? {
|
||||||
|
val cache = lruCache.get()
|
||||||
|
return runInterruptible(Dispatchers.IO) {
|
||||||
|
cache.get(url)?.takeIfReadable()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun put(url: String, source: Source): File = withContext(Dispatchers.IO) {
|
||||||
|
val file = File(cacheDir.get().parentFile, url.longHashCode().toString())
|
||||||
|
try {
|
||||||
|
val bytes = file.sink(append = false).buffer().use {
|
||||||
|
it.writeAllCancellable(source)
|
||||||
|
}
|
||||||
|
check(bytes != 0L) { "No data has been written" }
|
||||||
|
lruCache.get().put(url, file)
|
||||||
|
} finally {
|
||||||
|
file.delete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun put(url: String, bitmap: Bitmap): File = withContext(Dispatchers.IO) {
|
||||||
|
val file = File(cacheDir.get().parentFile, url.longHashCode().toString())
|
||||||
|
try {
|
||||||
|
bitmap.compressToPNG(file)
|
||||||
|
lruCache.get().put(url, file)
|
||||||
|
} finally {
|
||||||
|
file.delete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun getAvailableSize(): Long = runCatchingCancellable {
|
||||||
|
val statFs = StatFs(cacheDir.get().absolutePath)
|
||||||
|
statFs.availableBytes
|
||||||
|
}.onFailure {
|
||||||
|
it.printStackTrace()
|
||||||
|
}.getOrDefault(SIZE_DEFAULT)
|
||||||
|
|
||||||
|
private companion object {
|
||||||
|
|
||||||
|
val SIZE_MIN
|
||||||
|
get() = FileSize.MEGABYTES.convert(20, FileSize.BYTES)
|
||||||
|
|
||||||
|
val SIZE_DEFAULT
|
||||||
|
get() = FileSize.MEGABYTES.convert(200, FileSize.BYTES)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package org.xtimms.tokusho.core.cache
|
package org.xtimms.shirizu.core.cache
|
||||||
|
|
||||||
import kotlinx.coroutines.Deferred
|
import kotlinx.coroutines.Deferred
|
||||||
|
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
package org.xtimms.shirizu.core.components
|
||||||
|
|
||||||
|
import androidx.compose.animation.core.animateDpAsState
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.combinedClickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.interaction.collectIsPressedAsState
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.Edit
|
||||||
|
import androidx.compose.material.ripple.rememberRipple
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.ripple
|
||||||
|
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.clip
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.layout.onGloballyPositioned
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.min
|
||||||
|
import org.xtimms.shirizu.ui.theme.ShirizuTheme
|
||||||
|
import java.lang.Integer.MAX_VALUE
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
|
enum class ButtonType { PRIMARY, SECONDARY, TERTIARY, DELETE }
|
||||||
|
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
|
@Composable
|
||||||
|
fun AnimatedButton(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
type: ButtonType,
|
||||||
|
icon: ImageVector? = null,
|
||||||
|
onClick: (() -> Unit) = {},
|
||||||
|
onLongClick: (() -> Unit) = {},
|
||||||
|
) {
|
||||||
|
val localDensity = LocalDensity.current
|
||||||
|
var minSize by remember { mutableStateOf(MAX_VALUE.dp) }
|
||||||
|
var minSizeFloat by remember { mutableStateOf(MAX_VALUE.toFloat()) }
|
||||||
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
|
val isPressed = interactionSource.collectIsPressedAsState()
|
||||||
|
val radius = animateDpAsState(targetValue = if (isPressed.value) 12.dp else minSize / 2)
|
||||||
|
|
||||||
|
val color = when (type) {
|
||||||
|
ButtonType.PRIMARY -> MaterialTheme.colorScheme.primaryContainer
|
||||||
|
ButtonType.SECONDARY -> MaterialTheme.colorScheme.secondaryContainer
|
||||||
|
ButtonType.TERTIARY -> MaterialTheme.colorScheme.tertiaryContainer
|
||||||
|
ButtonType.DELETE -> MaterialTheme.colorScheme.errorContainer
|
||||||
|
}
|
||||||
|
|
||||||
|
val contentColor = when (type) {
|
||||||
|
ButtonType.PRIMARY -> MaterialTheme.colorScheme.onPrimaryContainer
|
||||||
|
ButtonType.SECONDARY -> MaterialTheme.colorScheme.onSecondaryContainer
|
||||||
|
ButtonType.TERTIARY -> MaterialTheme.colorScheme.onTertiaryContainer
|
||||||
|
ButtonType.DELETE -> MaterialTheme.colorScheme.onErrorContainer
|
||||||
|
}
|
||||||
|
|
||||||
|
Surface(
|
||||||
|
tonalElevation = 10.dp,
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.onGloballyPositioned {
|
||||||
|
minSize = with(localDensity) { min(it.size.height, it.size.width).toDp() }
|
||||||
|
minSizeFloat = min(it.size.height, it.size.width).toFloat()
|
||||||
|
}
|
||||||
|
.clip(RoundedCornerShape(radius.value))
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.background(color = color)
|
||||||
|
.fillMaxSize()
|
||||||
|
.clip(RoundedCornerShape(radius.value))
|
||||||
|
.combinedClickable(
|
||||||
|
interactionSource = interactionSource,
|
||||||
|
indication = ripple(),
|
||||||
|
onClick = { onClick.invoke() },
|
||||||
|
onLongClick = { onLongClick.invoke() },
|
||||||
|
),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
if (icon !== null) {
|
||||||
|
Icon(
|
||||||
|
imageVector = icon,
|
||||||
|
tint = contentColor,
|
||||||
|
modifier = Modifier.size(min(minSize * 0.5f, 154.dp)),
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(name = "Icon")
|
||||||
|
@Composable
|
||||||
|
private fun PreviewWithIcon() {
|
||||||
|
ShirizuTheme {
|
||||||
|
AnimatedButton(
|
||||||
|
type = ButtonType.PRIMARY,
|
||||||
|
icon = Icons.Outlined.Edit
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
package org.xtimms.shirizu.core.components
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedContent
|
||||||
|
import androidx.compose.animation.SizeTransform
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.animation.fadeIn
|
||||||
|
import androidx.compose.animation.fadeOut
|
||||||
|
import androidx.compose.animation.slideInVertically
|
||||||
|
import androidx.compose.animation.slideOutVertically
|
||||||
|
import androidx.compose.animation.togetherWith
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import java.text.BreakIterator
|
||||||
|
|
||||||
|
data class CharState(val preview: String, val current: String)
|
||||||
|
|
||||||
|
// https://github.com/danilkinkin/buckwheat/blob/master/app/src/main/java/com/danilkinkin/buckwheat/base/AnimatedNumber.kt
|
||||||
|
@Composable
|
||||||
|
fun AnimatedNumber(
|
||||||
|
value: String = "",
|
||||||
|
style: TextStyle = MaterialTheme.typography.displayLarge,
|
||||||
|
) {
|
||||||
|
|
||||||
|
var previewsValue by remember { mutableStateOf<List<String>>(emptyList()) }
|
||||||
|
var blocks by remember { mutableStateOf<List<CharState>>(emptyList()) }
|
||||||
|
|
||||||
|
DisposableEffect(value) {
|
||||||
|
var splittedValue = emptyList<String>().toMutableList()
|
||||||
|
val it = BreakIterator.getCharacterInstance()
|
||||||
|
it.setText(value)
|
||||||
|
var count = 0
|
||||||
|
|
||||||
|
var start = 0
|
||||||
|
var end = it.next()
|
||||||
|
while (end != BreakIterator.DONE) {
|
||||||
|
splittedValue.add(value.substring(start, end))
|
||||||
|
|
||||||
|
start = end
|
||||||
|
end = it.next()
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
|
||||||
|
val length = splittedValue.size.coerceAtLeast(previewsValue.size)
|
||||||
|
|
||||||
|
var newBlocks: MutableList<CharState> = emptyList<CharState>().toMutableList()
|
||||||
|
|
||||||
|
for (i in 0..length) {
|
||||||
|
newBlocks.add(
|
||||||
|
CharState(
|
||||||
|
preview = previewsValue.getOrElse(previewsValue.size - i) { "" },
|
||||||
|
current = splittedValue.getOrElse(splittedValue.size - i) { "" },
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
newBlocks = newBlocks.asReversed()
|
||||||
|
|
||||||
|
blocks = newBlocks
|
||||||
|
previewsValue = splittedValue
|
||||||
|
|
||||||
|
onDispose { }
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
blocks.forEach {
|
||||||
|
AnimatedContent(
|
||||||
|
targetState = it.current,
|
||||||
|
transitionSpec = {
|
||||||
|
if (targetState > initialState) {
|
||||||
|
(slideInVertically(tween(durationMillis = 300)) { height -> height } + fadeIn(
|
||||||
|
tween(durationMillis = 300)
|
||||||
|
)).togetherWith(
|
||||||
|
slideOutVertically(tween(durationMillis = 300)) { height -> -height } + fadeOut(
|
||||||
|
tween(durationMillis = 300)
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
(slideInVertically(tween(durationMillis = 300)) { height -> -height } + fadeIn(
|
||||||
|
tween(durationMillis = 300)
|
||||||
|
)).togetherWith(
|
||||||
|
slideOutVertically(tween(durationMillis = 300)) { height -> height } + fadeOut(
|
||||||
|
tween(durationMillis = 300)
|
||||||
|
))
|
||||||
|
}.using(
|
||||||
|
SizeTransform(clip = false)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
label = ""
|
||||||
|
) { targetCount ->
|
||||||
|
Text(text = targetCount, style = style)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package org.xtimms.shirizu.core.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.min
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AutoSizedCircularProgressIndicator(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
color: Color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
) {
|
||||||
|
BoxWithConstraints(modifier) {
|
||||||
|
val diameter = with(LocalDensity.current) {
|
||||||
|
// We need to minus the padding added within CircularProgressIndicator
|
||||||
|
min(constraints.maxWidth.toDp(), constraints.maxHeight.toDp()) - InternalPadding
|
||||||
|
}
|
||||||
|
CircularProgressIndicator(
|
||||||
|
strokeWidth = (diameter.value * StrokeDiameterFraction)
|
||||||
|
.roundToInt().dp
|
||||||
|
.coerceAtLeast(2.dp),
|
||||||
|
color = color,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default stroke size
|
||||||
|
private val DefaultStrokeWidth = 4.dp
|
||||||
|
|
||||||
|
// Preferred diameter for CircularProgressIndicator
|
||||||
|
private val DefaultDiameter = 40.dp
|
||||||
|
|
||||||
|
// Internal padding added by CircularProgressIndicator
|
||||||
|
private val InternalPadding = 4.dp
|
||||||
|
|
||||||
|
private val StrokeDiameterFraction = DefaultStrokeWidth / DefaultDiameter
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package org.xtimms.shirizu.core.components
|
||||||
|
|
||||||
|
import androidx.compose.animation.core.Animatable
|
||||||
|
import androidx.compose.animation.core.TweenSpec
|
||||||
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.times
|
||||||
|
import org.xtimms.shirizu.core.components.shape.WavyShape
|
||||||
|
import org.xtimms.shirizu.utils.lang.clamp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun BackgroundProgress(
|
||||||
|
color: Color,
|
||||||
|
alpha: Float,
|
||||||
|
percent: Float,
|
||||||
|
) {
|
||||||
|
|
||||||
|
val animatedAlpha = animateFloatAsState(
|
||||||
|
label = "alpha",
|
||||||
|
targetValue = alpha,
|
||||||
|
animationSpec = TweenSpec(500)
|
||||||
|
).value
|
||||||
|
|
||||||
|
val percentAnimated = animateFloatAsState(
|
||||||
|
label = "percentAnimated",
|
||||||
|
targetValue = percent,
|
||||||
|
animationSpec = TweenSpec(300),
|
||||||
|
).value
|
||||||
|
|
||||||
|
val shift = remember { Animatable(0f) }
|
||||||
|
|
||||||
|
Box(Modifier.fillMaxSize()) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.background(
|
||||||
|
color.copy(alpha = animatedAlpha),
|
||||||
|
shape = WavyShape(
|
||||||
|
period = 30.dp,
|
||||||
|
amplitude = percentAnimated.clamp(0.96f, 1f) * 2.dp,
|
||||||
|
shift = shift.value,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.fillMaxHeight()
|
||||||
|
.fillMaxWidth(percentAnimated),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,183 @@
|
|||||||
|
package org.xtimms.shirizu.core.components
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.animation.expandVertically
|
||||||
|
import androidx.compose.animation.fadeIn
|
||||||
|
import androidx.compose.animation.fadeOut
|
||||||
|
import androidx.compose.animation.shrinkVertically
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.combinedClickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.RowScope
|
||||||
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
|
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||||
|
import androidx.compose.foundation.layout.navigationBars
|
||||||
|
import androidx.compose.foundation.layout.only
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||||
|
import androidx.compose.foundation.shape.ZeroCornerSize
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.outlined.Label
|
||||||
|
import androidx.compose.material.icons.outlined.Delete
|
||||||
|
import androidx.compose.material.icons.outlined.DoneAll
|
||||||
|
import androidx.compose.material.icons.outlined.Download
|
||||||
|
import androidx.compose.material.icons.outlined.Favorite
|
||||||
|
import androidx.compose.material.icons.outlined.RemoveDone
|
||||||
|
import androidx.compose.material.icons.outlined.Save
|
||||||
|
import androidx.compose.material.icons.outlined.Share
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.ripple
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateListOf
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||||
|
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
import kotlinx.coroutines.isActive
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.xtimms.shirizu.R
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
|
@Composable
|
||||||
|
private fun RowScope.Button(
|
||||||
|
title: String,
|
||||||
|
icon: ImageVector,
|
||||||
|
toConfirm: Boolean,
|
||||||
|
onLongClick: () -> Unit,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
content: (@Composable () -> Unit)? = null,
|
||||||
|
) {
|
||||||
|
val animatedWeight by animateFloatAsState(
|
||||||
|
targetValue = if (toConfirm) 2f else 1f,
|
||||||
|
label = "weight",
|
||||||
|
)
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(48.dp)
|
||||||
|
.weight(animatedWeight)
|
||||||
|
.combinedClickable(
|
||||||
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
|
indication = ripple(bounded = false),
|
||||||
|
onLongClick = onLongClick,
|
||||||
|
onClick = onClick,
|
||||||
|
),
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = icon,
|
||||||
|
contentDescription = title,
|
||||||
|
)
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = toConfirm,
|
||||||
|
enter = expandVertically(expandFrom = Alignment.Top) + fadeIn(),
|
||||||
|
exit = shrinkVertically(shrinkTowards = Alignment.Top) + fadeOut(),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
overflow = TextOverflow.Visible,
|
||||||
|
maxLines = 1,
|
||||||
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
content?.invoke()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LibraryBottomActionMenu(
|
||||||
|
visible: Boolean,
|
||||||
|
onDeleteClicked: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = visible,
|
||||||
|
enter = expandVertically(animationSpec = tween(delayMillis = 300)),
|
||||||
|
exit = shrinkVertically(animationSpec = tween()),
|
||||||
|
) {
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
Surface(
|
||||||
|
modifier = modifier,
|
||||||
|
shape = MaterialTheme.shapes.large.copy(bottomEnd = ZeroCornerSize, bottomStart = ZeroCornerSize),
|
||||||
|
tonalElevation = 3.dp,
|
||||||
|
) {
|
||||||
|
val haptic = LocalHapticFeedback.current
|
||||||
|
val confirm = remember { mutableStateListOf(false, false, false, false, false) }
|
||||||
|
var resetJob: Job? = remember { null }
|
||||||
|
val onLongClickItem: (Int) -> Unit = { toConfirmIndex ->
|
||||||
|
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||||
|
(0..<5).forEach { i -> confirm[i] = i == toConfirmIndex }
|
||||||
|
resetJob?.cancel()
|
||||||
|
resetJob = scope.launch {
|
||||||
|
delay(1.seconds)
|
||||||
|
if (isActive) confirm[toConfirmIndex] = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.windowInsetsPadding(
|
||||||
|
WindowInsets.navigationBars
|
||||||
|
.only(WindowInsetsSides.Bottom),
|
||||||
|
)
|
||||||
|
.padding(horizontal = 8.dp, vertical = 12.dp),
|
||||||
|
) {
|
||||||
|
Button(
|
||||||
|
title = stringResource(R.string.action_share),
|
||||||
|
icon = Icons.Outlined.Share,
|
||||||
|
toConfirm = confirm[0],
|
||||||
|
onLongClick = { onLongClickItem(0) },
|
||||||
|
onClick = { },
|
||||||
|
)
|
||||||
|
Button(
|
||||||
|
title = stringResource(R.string.action_delete),
|
||||||
|
icon = Icons.Outlined.Delete,
|
||||||
|
toConfirm = confirm[1],
|
||||||
|
onLongClick = { onLongClickItem(1) },
|
||||||
|
onClick = onDeleteClicked,
|
||||||
|
)
|
||||||
|
Button(
|
||||||
|
title = stringResource(R.string.action_save),
|
||||||
|
icon = Icons.Outlined.Save,
|
||||||
|
toConfirm = confirm[2],
|
||||||
|
onLongClick = { onLongClickItem(2) },
|
||||||
|
onClick = { },
|
||||||
|
)
|
||||||
|
Button(
|
||||||
|
title = stringResource(R.string.add_to_shelf),
|
||||||
|
icon = Icons.Outlined.Favorite,
|
||||||
|
toConfirm = confirm[3],
|
||||||
|
onLongClick = { onLongClickItem(3) },
|
||||||
|
onClick = { },
|
||||||
|
)
|
||||||
|
Button(
|
||||||
|
title = stringResource(R.string.action_mark_as_completed),
|
||||||
|
icon = Icons.Outlined.DoneAll,
|
||||||
|
toConfirm = confirm[4],
|
||||||
|
onLongClick = { onLongClickItem(4) },
|
||||||
|
onClick = { },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
181
app/src/main/java/org/xtimms/shirizu/core/components/Buttons.kt
Normal file
181
app/src/main/java/org/xtimms/shirizu/core/components/Buttons.kt
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
package org.xtimms.shirizu.core.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.ButtonColors
|
||||||
|
import androidx.compose.material3.ButtonDefaults
|
||||||
|
import androidx.compose.material3.FilledTonalButton
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.OutlinedButton
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import org.xtimms.shirizu.R
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ConfirmButton(
|
||||||
|
text: String = stringResource(R.string.confirm),
|
||||||
|
enabled: Boolean = true,
|
||||||
|
onClick: () -> Unit
|
||||||
|
) {
|
||||||
|
TextButton(onClick = onClick, enabled = enabled) {
|
||||||
|
Text(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DismissButton(text: String = stringResource(R.string.dismiss), onClick: () -> Unit) {
|
||||||
|
TextButton(onClick = onClick) {
|
||||||
|
Text(text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ActionButton(
|
||||||
|
title: String,
|
||||||
|
icon: ImageVector,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
TextButton(
|
||||||
|
modifier = modifier,
|
||||||
|
onClick = onClick,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = icon,
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
textAlign = TextAlign.Center,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun OutlinedButtonWithIcon(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
icon: ImageVector,
|
||||||
|
text: String,
|
||||||
|
contentColor: Color = MaterialTheme.colorScheme.primary
|
||||||
|
) {
|
||||||
|
OutlinedButton(
|
||||||
|
modifier = modifier,
|
||||||
|
onClick = onClick,
|
||||||
|
contentPadding = ButtonDefaults.ButtonWithIconContentPadding,
|
||||||
|
colors = ButtonDefaults.outlinedButtonColors(contentColor = contentColor)
|
||||||
|
)
|
||||||
|
{
|
||||||
|
Icon(
|
||||||
|
modifier = Modifier.size(ButtonDefaults.IconSize),
|
||||||
|
imageVector = icon,
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.padding(start = 8.dp),
|
||||||
|
text = text
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun TextButtonWithIcon(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
icon: ImageVector,
|
||||||
|
text: String,
|
||||||
|
contentColor: Color = MaterialTheme.colorScheme.primary,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
) {
|
||||||
|
TextButton(
|
||||||
|
modifier = modifier,
|
||||||
|
onClick = onClick,
|
||||||
|
contentPadding = ButtonDefaults.ButtonWithIconContentPadding,
|
||||||
|
colors = ButtonDefaults.textButtonColors(contentColor = contentColor)
|
||||||
|
)
|
||||||
|
{
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
Icon(
|
||||||
|
modifier = Modifier.size(18.dp),
|
||||||
|
imageVector = icon,
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.padding(start = 8.dp),
|
||||||
|
text = text
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun FilledTonalButtonWithIcon(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
icon: ImageVector,
|
||||||
|
text: String,
|
||||||
|
colors: ButtonColors = ButtonDefaults.filledTonalButtonColors(),
|
||||||
|
) {
|
||||||
|
FilledTonalButton(
|
||||||
|
modifier = modifier,
|
||||||
|
onClick = onClick,
|
||||||
|
contentPadding = ButtonDefaults.ButtonWithIconContentPadding,
|
||||||
|
colors = colors
|
||||||
|
)
|
||||||
|
{
|
||||||
|
Icon(
|
||||||
|
modifier = Modifier.size(18.dp),
|
||||||
|
imageVector = icon,
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.padding(start = 8.dp),
|
||||||
|
text = text
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun FilledButtonWithIcon(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
icon: ImageVector,
|
||||||
|
text: String,
|
||||||
|
enabled: Boolean = true,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
) {
|
||||||
|
Button(
|
||||||
|
modifier = modifier,
|
||||||
|
onClick = onClick,
|
||||||
|
contentPadding = ButtonDefaults.ButtonWithIconContentPadding,
|
||||||
|
enabled = enabled
|
||||||
|
)
|
||||||
|
{
|
||||||
|
Icon(
|
||||||
|
modifier = Modifier.size(18.dp),
|
||||||
|
imageVector = icon,
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.padding(start = 6.dp),
|
||||||
|
text = text
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
282
app/src/main/java/org/xtimms/shirizu/core/components/Carousel.kt
Normal file
282
app/src/main/java/org/xtimms/shirizu/core/components/Carousel.kt
Normal file
@@ -0,0 +1,282 @@
|
|||||||
|
package org.xtimms.shirizu.core.components
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.gestures.snapping.SnapLayoutInfoProvider
|
||||||
|
import androidx.compose.foundation.gestures.snapping.SnapPosition
|
||||||
|
import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.IntrinsicSize
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.RowScope
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.aspectRatio
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material3.ButtonDefaults
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.Stable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.composed
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.draw.drawWithContent
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.layout.FirstBaseline
|
||||||
|
import androidx.compose.ui.platform.testTag
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.IntOffset
|
||||||
|
import androidx.compose.ui.unit.IntSize
|
||||||
|
import androidx.compose.ui.unit.LayoutDirection
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import coil.compose.AsyncImage
|
||||||
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
|
import org.xtimms.shirizu.LocalImageLoader
|
||||||
|
import org.xtimms.shirizu.R
|
||||||
|
import org.xtimms.shirizu.core.components.icons.Creation
|
||||||
|
import org.xtimms.shirizu.core.ui.screens.EmptyScreen
|
||||||
|
import org.xtimms.shirizu.ui.theme.ShirizuTheme
|
||||||
|
import kotlin.math.pow
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MangaCarouselWithHeader(
|
||||||
|
items: List<Manga>,
|
||||||
|
title: String,
|
||||||
|
refreshing: Boolean,
|
||||||
|
onItemClick: (Manga) -> Unit,
|
||||||
|
onMoreClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Column(modifier = modifier) {
|
||||||
|
if (refreshing || items.isNotEmpty()) {
|
||||||
|
Header(
|
||||||
|
title = title,
|
||||||
|
loading = refreshing,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
) {
|
||||||
|
TextButton(
|
||||||
|
onClick = onMoreClick,
|
||||||
|
colors = ButtonDefaults.textButtonColors(
|
||||||
|
contentColor = MaterialTheme.colorScheme.secondary,
|
||||||
|
),
|
||||||
|
modifier = Modifier.alignBy(FirstBaseline),
|
||||||
|
) {
|
||||||
|
Text(text = stringResource(id = R.string.more))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (items.isNotEmpty()) {
|
||||||
|
MangaCarousel(
|
||||||
|
items = items,
|
||||||
|
onItemClick = onItemClick,
|
||||||
|
modifier = Modifier
|
||||||
|
.testTag("search_carousel")
|
||||||
|
.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clip(MaterialTheme.shapes.extraLarge)
|
||||||
|
) {
|
||||||
|
EmptyScreen(
|
||||||
|
icon = Icons.Outlined.Creation,
|
||||||
|
title = R.string.nothing_here,
|
||||||
|
description = R.string.empty_carousel_hint,
|
||||||
|
modifier = Modifier.height(IntrinsicSize.Min)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
|
@Composable
|
||||||
|
fun MangaCarousel(
|
||||||
|
items: List<Manga>,
|
||||||
|
onItemClick: (Manga) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val lazyListState = rememberLazyListState()
|
||||||
|
|
||||||
|
LazyRow(
|
||||||
|
state = lazyListState,
|
||||||
|
modifier = modifier
|
||||||
|
.padding(horizontal = 16.dp, vertical = 8.dp)
|
||||||
|
.clip(MaterialTheme.shapes.extraLarge),
|
||||||
|
flingBehavior = rememberSnapFlingBehavior(
|
||||||
|
snapLayoutInfoProvider = remember(lazyListState) {
|
||||||
|
SnapLayoutInfoProvider(
|
||||||
|
lazyListState = lazyListState,
|
||||||
|
snapPosition = SnapPosition.Start,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
) {
|
||||||
|
items(
|
||||||
|
items = items,
|
||||||
|
key = { it.id },
|
||||||
|
) { item ->
|
||||||
|
BackdropCard(
|
||||||
|
manga = item,
|
||||||
|
onClick = { onItemClick(item) },
|
||||||
|
alignment = remember {
|
||||||
|
ParallaxAlignment(
|
||||||
|
horizontalBias = {
|
||||||
|
val layoutInfo = lazyListState.layoutInfo
|
||||||
|
val itemInfo = layoutInfo.visibleItemsInfo.first {
|
||||||
|
it.key == item.id
|
||||||
|
}
|
||||||
|
|
||||||
|
val adjustedOffset = itemInfo.offset - layoutInfo.viewportStartOffset
|
||||||
|
(adjustedOffset / itemInfo.size.toFloat()).coerceIn(-1f, 1f)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.testTag("search_carousel_item")
|
||||||
|
.animateItem()
|
||||||
|
.width(156.dp)
|
||||||
|
.aspectRatio(2 / 3f),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun BackdropCard(
|
||||||
|
manga: Manga,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
alignment: Alignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
Card(
|
||||||
|
onClick = onClick,
|
||||||
|
shape = MaterialTheme.shapes.extraLarge,
|
||||||
|
modifier = modifier,
|
||||||
|
) {
|
||||||
|
BackdropCardContent(
|
||||||
|
manga = manga,
|
||||||
|
alignment = alignment,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun BackdropCardContent(
|
||||||
|
manga: Manga,
|
||||||
|
alignment: Alignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
|
AsyncImage(
|
||||||
|
imageLoader = LocalImageLoader.current,
|
||||||
|
model = manga.largeCoverUrl ?: manga.coverUrl,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.matchParentSize(),
|
||||||
|
contentScale = ContentScale.Crop,
|
||||||
|
alignment = alignment,
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(
|
||||||
|
Modifier
|
||||||
|
.matchParentSize()
|
||||||
|
.drawForegroundGradientScrim(MaterialTheme.colorScheme.surfaceDim),
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = manga.title,
|
||||||
|
style = MaterialTheme.typography.labelLarge,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
|
maxLines = 3,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(16.dp)
|
||||||
|
.align(Alignment.BottomStart),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Stable
|
||||||
|
class ParallaxAlignment(
|
||||||
|
private val horizontalBias: () -> Float = { 0f },
|
||||||
|
private val verticalBias: () -> Float = { 0f },
|
||||||
|
) : Alignment {
|
||||||
|
override fun align(
|
||||||
|
size: IntSize,
|
||||||
|
space: IntSize,
|
||||||
|
layoutDirection: LayoutDirection,
|
||||||
|
): IntOffset {
|
||||||
|
// Convert to Px first and only round at the end, to avoid rounding twice while calculating
|
||||||
|
// the new positions
|
||||||
|
val centerX = (space.width - size.width).toFloat() / 2f
|
||||||
|
val centerY = (space.height - size.height).toFloat() / 2f
|
||||||
|
val resolvedHorizontalBias = if (layoutDirection == LayoutDirection.Ltr) {
|
||||||
|
horizontalBias()
|
||||||
|
} else {
|
||||||
|
-1 * horizontalBias()
|
||||||
|
}
|
||||||
|
|
||||||
|
val x = centerX * (1 + resolvedHorizontalBias)
|
||||||
|
val y = centerY * (1 + verticalBias())
|
||||||
|
return IntOffset(x.roundToInt(), y.roundToInt())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws a vertical gradient scrim in the foreground.
|
||||||
|
*
|
||||||
|
* @param color The color of the gradient scrim.
|
||||||
|
* @param decay The exponential decay to apply to the gradient. Defaults to `3.0f` which is
|
||||||
|
* a cubic decay.
|
||||||
|
* @param numStops The number of color stops to draw in the gradient. Higher numbers result in
|
||||||
|
* the higher visual quality at the cost of draw performance. Defaults to `16`.
|
||||||
|
*/
|
||||||
|
fun Modifier.drawForegroundGradientScrim(
|
||||||
|
color: Color,
|
||||||
|
decay: Float = 1.0f,
|
||||||
|
numStops: Int = 16,
|
||||||
|
startY: Float = 0f,
|
||||||
|
endY: Float = 1f,
|
||||||
|
): Modifier = composed {
|
||||||
|
val colors = remember(color, numStops) {
|
||||||
|
val baseAlpha = color.alpha
|
||||||
|
List(numStops) { i ->
|
||||||
|
val x = i * 1f / (numStops - 1)
|
||||||
|
val opacity = x.pow(decay)
|
||||||
|
color.copy(alpha = baseAlpha * opacity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
drawWithContent {
|
||||||
|
drawContent()
|
||||||
|
drawRect(
|
||||||
|
topLeft = Offset(x = 0f, y = startY * size.height),
|
||||||
|
size = size.copy(height = (endY - startY) * size.height),
|
||||||
|
brush = Brush.verticalGradient(colors = colors),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package org.xtimms.shirizu.core.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.selection.toggleable
|
||||||
|
import androidx.compose.material3.Checkbox
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.semantics.clearAndSetSemantics
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DialogCheckBoxItem(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
text: String,
|
||||||
|
checked: Boolean,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.toggleable(
|
||||||
|
value = checked,
|
||||||
|
enabled = true,
|
||||||
|
onValueChange = { onClick() },
|
||||||
|
)
|
||||||
|
.padding(horizontal = 12.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Checkbox(
|
||||||
|
modifier = Modifier.clearAndSetSemantics { },
|
||||||
|
checked = checked, onCheckedChange = { onClick() },
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
text = text,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package org.xtimms.shirizu.core.components
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.Check
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.FilterChip
|
||||||
|
import androidx.compose.material3.FilterChipDefaults
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SingleChoiceChip(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
selected: Boolean,
|
||||||
|
enabled: Boolean = true,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
label: String,
|
||||||
|
leadingIcon: ImageVector = Icons.Outlined.Check
|
||||||
|
) {
|
||||||
|
FilterChip(
|
||||||
|
modifier = modifier.padding(horizontal = 4.dp),
|
||||||
|
selected = selected,
|
||||||
|
onClick = onClick,
|
||||||
|
enabled = enabled,
|
||||||
|
shape = MaterialTheme.shapes.large,
|
||||||
|
label = {
|
||||||
|
Text(text = label)
|
||||||
|
},
|
||||||
|
leadingIcon = {
|
||||||
|
Row {
|
||||||
|
AnimatedVisibility(visible = selected) {
|
||||||
|
Icon(
|
||||||
|
imageVector = leadingIcon,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(FilterChipDefaults.IconSize)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,176 @@
|
|||||||
|
package org.xtimms.shirizu.core.components
|
||||||
|
|
||||||
|
import androidx.compose.animation.core.animateDpAsState
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.MoreVert
|
||||||
|
import androidx.compose.material.icons.filled.Share
|
||||||
|
import androidx.compose.material.icons.outlined.Download
|
||||||
|
import androidx.compose.material.icons.outlined.Language
|
||||||
|
import androidx.compose.material3.DropdownMenu
|
||||||
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.FilledTonalIconButton
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.IconButtonColors
|
||||||
|
import androidx.compose.material3.LocalContentColor
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TopAppBar
|
||||||
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
|
import androidx.compose.material3.surfaceColorAtElevation
|
||||||
|
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.Modifier
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun ModernDetailsToolbar(
|
||||||
|
title: String,
|
||||||
|
titleAlphaProvider: () -> Float,
|
||||||
|
navigateBack: () -> Unit,
|
||||||
|
navigateToWebBrowser: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
backgroundAlphaProvider: () -> Float = titleAlphaProvider
|
||||||
|
) {
|
||||||
|
|
||||||
|
var expanded by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
val padding by animateDpAsState(
|
||||||
|
targetValue = if (backgroundAlphaProvider() == 1f) 0.dp else 16.dp,
|
||||||
|
label = "padding",
|
||||||
|
)
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = modifier
|
||||||
|
) {
|
||||||
|
TopAppBar(
|
||||||
|
navigationIcon = {
|
||||||
|
CircleBackIconButton(
|
||||||
|
modifier = Modifier.padding(start = padding),
|
||||||
|
onClick = navigateBack
|
||||||
|
)
|
||||||
|
},
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
color = LocalContentColor.current.copy(alpha = titleAlphaProvider()),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
actions = {
|
||||||
|
FilledTonalIconButton(
|
||||||
|
modifier = Modifier.padding(end = padding),
|
||||||
|
onClick = { expanded = true }, colors = IconButtonColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp),
|
||||||
|
contentColor = MaterialTheme.colorScheme.onSurface,
|
||||||
|
disabledContainerColor = MaterialTheme.colorScheme.outline,
|
||||||
|
disabledContentColor = MaterialTheme.colorScheme.outlineVariant
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Icon(imageVector = Icons.Default.MoreVert, contentDescription = null)
|
||||||
|
}
|
||||||
|
DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text("Share") },
|
||||||
|
onClick = { /*TODO*/ },
|
||||||
|
leadingIcon = {
|
||||||
|
Icon(imageVector = Icons.Default.Share, contentDescription = null)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text("Download") },
|
||||||
|
onClick = { /*TODO*/ },
|
||||||
|
leadingIcon = {
|
||||||
|
Icon(imageVector = Icons.Outlined.Download, contentDescription = null)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text("Open in web browser") },
|
||||||
|
onClick = {
|
||||||
|
navigateToWebBrowser()
|
||||||
|
expanded = false
|
||||||
|
},
|
||||||
|
leadingIcon = {
|
||||||
|
Icon(imageVector = Icons.Outlined.Language, contentDescription = null)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
colors = TopAppBarDefaults.topAppBarColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme
|
||||||
|
.surfaceColorAtElevation(3.dp)
|
||||||
|
.copy(alpha = backgroundAlphaProvider())
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun ClassicDetailsToolbar(
|
||||||
|
title: String,
|
||||||
|
titleAlphaProvider: () -> Float,
|
||||||
|
navigateBack: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
backgroundAlphaProvider: () -> Float = titleAlphaProvider
|
||||||
|
) {
|
||||||
|
|
||||||
|
var expanded by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = modifier
|
||||||
|
) {
|
||||||
|
TopAppBar(
|
||||||
|
navigationIcon = {
|
||||||
|
BackIconButton(
|
||||||
|
onClick = navigateBack
|
||||||
|
)
|
||||||
|
},
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
color = LocalContentColor.current.copy(alpha = titleAlphaProvider()),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
actions = {
|
||||||
|
IconButton(
|
||||||
|
onClick = { expanded = true }
|
||||||
|
) {
|
||||||
|
Icon(imageVector = Icons.Default.MoreVert, contentDescription = null)
|
||||||
|
}
|
||||||
|
DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text("Share") },
|
||||||
|
onClick = { /*TODO*/ },
|
||||||
|
leadingIcon = {
|
||||||
|
Icon(imageVector = Icons.Default.Share, contentDescription = null)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = { Text("Download") },
|
||||||
|
onClick = { /*TODO*/ },
|
||||||
|
leadingIcon = {
|
||||||
|
Icon(imageVector = Icons.Outlined.Download, contentDescription = null)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
colors = TopAppBarDefaults.topAppBarColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme
|
||||||
|
.surfaceColorAtElevation(3.dp)
|
||||||
|
.copy(alpha = backgroundAlphaProvider())
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,222 @@
|
|||||||
|
package org.xtimms.shirizu.core.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.selection.selectable
|
||||||
|
import androidx.compose.foundation.selection.toggleable
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.Check
|
||||||
|
import androidx.compose.material3.Checkbox
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.LocalTextStyle
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.RadioButton
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Switch
|
||||||
|
import androidx.compose.material3.SwitchDefaults
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
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.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.semantics.clearAndSetSemantics
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.Density
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DialogSingleChoiceItem(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
text: String,
|
||||||
|
selected: Boolean,
|
||||||
|
onClick: () -> Unit
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = modifier
|
||||||
|
.selectable(
|
||||||
|
selected = selected,
|
||||||
|
enabled = true,
|
||||||
|
onClick = onClick,
|
||||||
|
)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 12.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.Start
|
||||||
|
) {
|
||||||
|
RadioButton(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(end = 8.dp)
|
||||||
|
.clearAndSetSemantics { },
|
||||||
|
selected = selected,
|
||||||
|
onClick = onClick
|
||||||
|
)
|
||||||
|
Text(text = text, style = LocalTextStyle.current.copy(fontSize = 16.sp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun SingleChoiceItemPreview() {
|
||||||
|
Surface {
|
||||||
|
Column {
|
||||||
|
DialogSingleChoiceItemWithLabel(
|
||||||
|
text = "Better compatibility", label = "For sharing to other apps", selected = false
|
||||||
|
) {
|
||||||
|
|
||||||
|
}
|
||||||
|
DialogSingleChoiceItemWithLabel(
|
||||||
|
text = "Better quality", label = "For watching in compatible apps", selected = true
|
||||||
|
) {
|
||||||
|
|
||||||
|
}
|
||||||
|
DialogSingleChoiceItem(text = "Preview", selected = true) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DialogSingleChoiceItemWithLabel(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
text: String,
|
||||||
|
label: String?,
|
||||||
|
selected: Boolean,
|
||||||
|
onClick: () -> Unit
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = modifier
|
||||||
|
.selectable(
|
||||||
|
selected = selected,
|
||||||
|
enabled = true,
|
||||||
|
onClick = onClick,
|
||||||
|
)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(start = 8.dp, end = 16.dp)
|
||||||
|
.padding(vertical = 8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.Start
|
||||||
|
) {
|
||||||
|
RadioButton(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(end = 8.dp)
|
||||||
|
.clearAndSetSemantics { },
|
||||||
|
selected = selected,
|
||||||
|
onClick = onClick
|
||||||
|
)
|
||||||
|
Column {
|
||||||
|
Text(text = text, style = MaterialTheme.typography.bodyLarge)
|
||||||
|
label?.let {
|
||||||
|
Text(
|
||||||
|
text = it,
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
modifier = Modifier.padding()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun CheckBoxItem(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
text: String,
|
||||||
|
checked: Boolean,
|
||||||
|
onValueChange: (Boolean) -> Unit,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(top = 12.dp)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.toggleable(
|
||||||
|
value = checked, enabled = true, onValueChange = onValueChange
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = modifier, verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Checkbox(
|
||||||
|
modifier = Modifier.clearAndSetSemantics { },
|
||||||
|
checked = checked, onCheckedChange = onValueChange,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
modifier = Modifier, text = text, style = MaterialTheme.typography.bodyMedium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DialogSwitchItem(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
text: String,
|
||||||
|
value: Boolean,
|
||||||
|
onValueChange: (Boolean) -> Unit
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.toggleable(value = value, onValueChange = onValueChange)
|
||||||
|
.padding(horizontal = 24.dp, vertical = 8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
style = MaterialTheme.typography.labelLarge,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
val thumbContent: (@Composable () -> Unit)? = remember(value) {
|
||||||
|
if (value) {
|
||||||
|
{
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.Check,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(SwitchDefaults.IconSize)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val density = LocalDensity.current
|
||||||
|
CompositionLocalProvider(
|
||||||
|
LocalDensity provides Density(
|
||||||
|
density.density * 0.8f,
|
||||||
|
density.fontScale
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Switch(
|
||||||
|
checked = value,
|
||||||
|
onCheckedChange = onValueChange,
|
||||||
|
modifier = Modifier.clearAndSetSemantics { },
|
||||||
|
thumbContent = thumbContent
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
private fun SwitchItemPrev() {
|
||||||
|
var value by remember { mutableStateOf(false) }
|
||||||
|
Surface {
|
||||||
|
DialogSwitchItem(text = "Use cookies", value = value) {
|
||||||
|
value = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
328
app/src/main/java/org/xtimms/shirizu/core/components/Dialogs.kt
Normal file
328
app/src/main/java/org/xtimms/shirizu/core/components/Dialogs.kt
Normal file
@@ -0,0 +1,328 @@
|
|||||||
|
package org.xtimms.shirizu.core.components
|
||||||
|
|
||||||
|
import android.content.res.Configuration
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
|
import androidx.compose.foundation.layout.FlowRow
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.SignalCellularConnectedNoInternet4Bar
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.AlertDialogDefaults
|
||||||
|
import androidx.compose.material3.BasicAlertDialog
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.LocalContentColor
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.ProvideTextStyle
|
||||||
|
import androidx.compose.material3.Surface
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.Shape
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.window.DialogProperties
|
||||||
|
import org.xtimms.shirizu.R
|
||||||
|
import org.xtimms.shirizu.ui.theme.FixedAccentColors
|
||||||
|
import org.xtimms.shirizu.ui.theme.ShirizuTheme
|
||||||
|
|
||||||
|
private val DialogVerticalPadding = PaddingValues(vertical = 24.dp)
|
||||||
|
private val IconPadding = PaddingValues(bottom = 16.dp)
|
||||||
|
private val DialogHorizontalPadding = PaddingValues(horizontal = 24.dp)
|
||||||
|
private val TitlePadding = PaddingValues(bottom = 16.dp)
|
||||||
|
private val TextPadding = PaddingValues(bottom = 24.dp)
|
||||||
|
private val ButtonsMainAxisSpacing = Arrangement.spacedBy(8.dp, Alignment.Start)
|
||||||
|
private val ButtonsCrossAxisSpacing = Arrangement.spacedBy(12.dp, Alignment.Top)
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalLayoutApi::class)
|
||||||
|
@Composable
|
||||||
|
fun ShirizuDialog(
|
||||||
|
onDismissRequest: () -> Unit,
|
||||||
|
confirmButton: @Composable () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
dismissButton: @Composable (() -> Unit)? = null,
|
||||||
|
icon: @Composable (() -> Unit)? = null,
|
||||||
|
title: @Composable (() -> Unit)? = null,
|
||||||
|
text: @Composable (() -> Unit)? = null,
|
||||||
|
shape: Shape = AlertDialogDefaults.shape,
|
||||||
|
containerColor: Color = AlertDialogDefaults.containerColor,
|
||||||
|
iconContentColor: Color = AlertDialogDefaults.iconContentColor,
|
||||||
|
titleContentColor: Color = AlertDialogDefaults.titleContentColor,
|
||||||
|
textContentColor: Color = AlertDialogDefaults.textContentColor,
|
||||||
|
tonalElevation: Dp = AlertDialogDefaults.TonalElevation,
|
||||||
|
properties: DialogProperties = DialogProperties()
|
||||||
|
) {
|
||||||
|
BasicAlertDialog(
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
modifier = modifier,
|
||||||
|
properties = properties
|
||||||
|
) {
|
||||||
|
Surface(
|
||||||
|
modifier = modifier,
|
||||||
|
shape = shape,
|
||||||
|
color = containerColor,
|
||||||
|
tonalElevation = tonalElevation,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(DialogVerticalPadding)
|
||||||
|
) {
|
||||||
|
icon?.let {
|
||||||
|
CompositionLocalProvider(LocalContentColor provides iconContentColor) {
|
||||||
|
Box(
|
||||||
|
Modifier
|
||||||
|
.padding(IconPadding)
|
||||||
|
.padding(DialogHorizontalPadding)
|
||||||
|
.align(Alignment.CenterHorizontally)
|
||||||
|
) {
|
||||||
|
icon()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
title?.let {
|
||||||
|
CompositionLocalProvider(LocalContentColor provides titleContentColor) {
|
||||||
|
val textStyle = MaterialTheme.typography.headlineSmall
|
||||||
|
ProvideTextStyle(textStyle) {
|
||||||
|
Box(
|
||||||
|
// Align the title to the center when an icon is present.
|
||||||
|
Modifier
|
||||||
|
.padding(TitlePadding)
|
||||||
|
.padding(DialogHorizontalPadding)
|
||||||
|
.align(
|
||||||
|
if (icon == null) {
|
||||||
|
Alignment.Start
|
||||||
|
} else {
|
||||||
|
Alignment.CenterHorizontally
|
||||||
|
}
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
title()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
text?.let {
|
||||||
|
CompositionLocalProvider(LocalContentColor provides textContentColor) {
|
||||||
|
val textStyle =
|
||||||
|
MaterialTheme.typography.bodyMedium
|
||||||
|
ProvideTextStyle(textStyle) {
|
||||||
|
Box(
|
||||||
|
Modifier
|
||||||
|
.weight(weight = 1f, fill = false)
|
||||||
|
.padding(TextPadding)
|
||||||
|
.align(Alignment.Start)
|
||||||
|
) {
|
||||||
|
text()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.End)
|
||||||
|
.padding(DialogHorizontalPadding)
|
||||||
|
) {
|
||||||
|
val textStyle =
|
||||||
|
MaterialTheme.typography.labelLarge
|
||||||
|
ProvideTextStyle(value = textStyle) {
|
||||||
|
FlowRow(
|
||||||
|
horizontalArrangement = ButtonsMainAxisSpacing,
|
||||||
|
verticalArrangement = ButtonsCrossAxisSpacing
|
||||||
|
) {
|
||||||
|
dismissButton?.invoke()
|
||||||
|
confirmButton()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ShirizuDialogButtonVariant(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
shape: Shape = MiddleButtonShape,
|
||||||
|
text: String,
|
||||||
|
onClick: () -> Unit
|
||||||
|
) {
|
||||||
|
Box {
|
||||||
|
Surface(
|
||||||
|
modifier = modifier
|
||||||
|
.clickable(onClick = onClick)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(48.dp),
|
||||||
|
color = FixedAccentColors.secondaryFixed,
|
||||||
|
shape = shape
|
||||||
|
) {
|
||||||
|
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
style = MaterialTheme.typography.labelLarge,
|
||||||
|
color = FixedAccentColors.onSecondaryFixed,
|
||||||
|
modifier = Modifier.align(Alignment.Center)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(name = "dark", uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||||
|
@Preview(name = "light", uiMode = Configuration.UI_MODE_NIGHT_NO)
|
||||||
|
@Composable
|
||||||
|
private fun ButtonVariantPreview() {
|
||||||
|
ShirizuTheme {
|
||||||
|
ShirizuDialogVariant(
|
||||||
|
onDismissRequest = {}, modifier = Modifier,
|
||||||
|
icon = {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.SignalCellularConnectedNoInternet4Bar,
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
},
|
||||||
|
title = {
|
||||||
|
Text(
|
||||||
|
text = "Download with cellular network?",
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
textAlign = TextAlign.Center
|
||||||
|
)
|
||||||
|
},
|
||||||
|
buttons = {
|
||||||
|
ShirizuDialogButtonVariant(
|
||||||
|
text = stringResource(R.string.allow_always),
|
||||||
|
shape = TopButtonShape
|
||||||
|
) {}
|
||||||
|
ShirizuDialogButtonVariant(
|
||||||
|
text = stringResource(id = R.string.allow_once),
|
||||||
|
shape = MiddleButtonShape
|
||||||
|
) {}
|
||||||
|
ShirizuDialogButtonVariant(
|
||||||
|
text = stringResource(R.string.dont_allow),
|
||||||
|
shape = BottomButtonShape
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val TopButtonShape = RoundedCornerShape(
|
||||||
|
topStart = 12.dp,
|
||||||
|
topEnd = 12.dp,
|
||||||
|
bottomStart = 4.dp,
|
||||||
|
bottomEnd = 4.dp
|
||||||
|
)
|
||||||
|
|
||||||
|
val MiddleButtonShape = RoundedCornerShape(4.dp)
|
||||||
|
|
||||||
|
val BottomButtonShape = RoundedCornerShape(
|
||||||
|
topStart = 4.dp,
|
||||||
|
topEnd = 4.dp,
|
||||||
|
bottomStart = 12.dp,
|
||||||
|
bottomEnd = 12.dp
|
||||||
|
)
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun ShirizuDialogVariant(
|
||||||
|
onDismissRequest: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
buttons: @Composable (() -> Unit)? = null,
|
||||||
|
icon: @Composable (() -> Unit)? = null,
|
||||||
|
title: @Composable (() -> Unit)? = null,
|
||||||
|
text: @Composable (() -> Unit)? = null,
|
||||||
|
shape: Shape = AlertDialogDefaults.shape,
|
||||||
|
containerColor: Color = MaterialTheme.colorScheme.surfaceContainer,
|
||||||
|
iconContentColor: Color = AlertDialogDefaults.iconContentColor,
|
||||||
|
titleContentColor: Color = AlertDialogDefaults.titleContentColor,
|
||||||
|
textContentColor: Color = AlertDialogDefaults.textContentColor,
|
||||||
|
tonalElevation: Dp = AlertDialogDefaults.TonalElevation,
|
||||||
|
properties: DialogProperties = DialogProperties()
|
||||||
|
) {
|
||||||
|
BasicAlertDialog(
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
modifier = modifier,
|
||||||
|
properties = properties
|
||||||
|
) {
|
||||||
|
Surface(
|
||||||
|
modifier = modifier,
|
||||||
|
shape = shape,
|
||||||
|
color = containerColor,
|
||||||
|
tonalElevation = tonalElevation,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.padding(DialogVerticalPadding)
|
||||||
|
) {
|
||||||
|
icon?.let {
|
||||||
|
CompositionLocalProvider(LocalContentColor provides iconContentColor) {
|
||||||
|
Box(
|
||||||
|
Modifier
|
||||||
|
.padding(IconPadding)
|
||||||
|
.padding(DialogHorizontalPadding)
|
||||||
|
.align(Alignment.CenterHorizontally)
|
||||||
|
) {
|
||||||
|
icon()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
title?.let {
|
||||||
|
CompositionLocalProvider(LocalContentColor provides titleContentColor) {
|
||||||
|
val textStyle = MaterialTheme.typography.headlineSmall
|
||||||
|
ProvideTextStyle(textStyle.copy(textAlign = TextAlign.Center)) {
|
||||||
|
Box(
|
||||||
|
// Align the title to the center when an icon is present.
|
||||||
|
Modifier
|
||||||
|
.padding(TitlePadding)
|
||||||
|
.padding(DialogHorizontalPadding)
|
||||||
|
.align(
|
||||||
|
if (icon == null) {
|
||||||
|
Alignment.Start
|
||||||
|
} else {
|
||||||
|
Alignment.CenterHorizontally
|
||||||
|
}
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
title()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
text?.let {
|
||||||
|
CompositionLocalProvider(LocalContentColor provides textContentColor) {
|
||||||
|
val textStyle =
|
||||||
|
MaterialTheme.typography.bodyMedium
|
||||||
|
ProvideTextStyle(textStyle) {
|
||||||
|
Box(
|
||||||
|
Modifier
|
||||||
|
.weight(weight = 1f, fill = false)
|
||||||
|
.padding(TextPadding)
|
||||||
|
.align(Alignment.Start)
|
||||||
|
) {
|
||||||
|
text()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Column(
|
||||||
|
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
modifier = Modifier.padding(DialogHorizontalPadding)
|
||||||
|
) {
|
||||||
|
buttons?.invoke()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package org.xtimms.shirizu.core.components
|
||||||
|
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DotSeparatorText(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = " • ",
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DotSeparatorNoSpaceText(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "•",
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package org.xtimms.tokusho.core.components
|
package org.xtimms.shirizu.core.components
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
@@ -15,23 +15,24 @@ import androidx.compose.material3.surfaceColorAtElevation
|
|||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.painter.Painter
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
import org.xtimms.shirizu.ui.theme.applyOpacity
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ExploreButton(
|
fun ExploreButton(
|
||||||
text: String,
|
text: String,
|
||||||
icon: ImageVector,
|
icon: Any? = null,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
) {
|
) {
|
||||||
Card(
|
Card(
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
modifier = modifier.padding(start = 8.dp, end = 8.dp),
|
modifier = modifier.padding(start = 8.dp, end = 8.dp),
|
||||||
shape = RoundedCornerShape(50),
|
shape = RoundedCornerShape(25),
|
||||||
colors = CardDefaults.cardColors(
|
colors = CardDefaults.cardColors(
|
||||||
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(4.dp)
|
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(4.dp)
|
||||||
)
|
)
|
||||||
@@ -42,14 +43,29 @@ fun ExploreButton(
|
|||||||
.height(40.dp),
|
.height(40.dp),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Icon(
|
when (icon) {
|
||||||
imageVector = icon,
|
is ImageVector -> {
|
||||||
contentDescription = text,
|
Icon(
|
||||||
modifier = Modifier
|
imageVector = icon,
|
||||||
.padding(end = 16.dp)
|
contentDescription = text,
|
||||||
.size(24.dp),
|
modifier = Modifier
|
||||||
tint = MaterialTheme.colorScheme.primary
|
.padding(end = 16.dp)
|
||||||
)
|
.size(24.dp),
|
||||||
|
tint = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is Painter -> {
|
||||||
|
Icon(
|
||||||
|
painter = icon,
|
||||||
|
contentDescription = text,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(end = 16.dp)
|
||||||
|
.size(24.dp),
|
||||||
|
tint = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
Text(
|
Text(
|
||||||
text = text,
|
text = text,
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package org.xtimms.shirizu.core.components
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.foundation.horizontalScroll
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.RowScope
|
||||||
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun FilterSortPanel(
|
||||||
|
filterExpanded: Boolean,
|
||||||
|
filterIcon: @Composable () -> Unit,
|
||||||
|
filterTextField: @Composable () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
content: @Composable RowScope.() -> Unit,
|
||||||
|
) {
|
||||||
|
Column(modifier = modifier) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.horizontalScroll(rememberScrollState()),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
) {
|
||||||
|
AnimatedVisibility(!filterExpanded) {
|
||||||
|
filterIcon()
|
||||||
|
}
|
||||||
|
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
|
||||||
|
AnimatedVisibility(visible = filterExpanded) {
|
||||||
|
filterTextField()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
package org.xtimms.shirizu.core.components
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.core.CubicBezierEasing
|
||||||
|
import androidx.compose.animation.core.animateDpAsState
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.animation.expandHorizontally
|
||||||
|
import androidx.compose.animation.fadeIn
|
||||||
|
import androidx.compose.animation.fadeOut
|
||||||
|
import androidx.compose.animation.shrinkHorizontally
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.sizeIn
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.material3.FloatingActionButton
|
||||||
|
import androidx.compose.material3.FloatingActionButtonDefaults
|
||||||
|
import androidx.compose.material3.FloatingActionButtonElevation
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.contentColorFor
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.Shape
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ExtendedFloatingActionButton with custom transition between collapsed/expanded state.
|
||||||
|
*
|
||||||
|
* @see androidx.compose.material3.ExtendedFloatingActionButton
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun ExtendedFloatingActionButton(
|
||||||
|
text: @Composable () -> Unit,
|
||||||
|
icon: @Composable () -> Unit,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
expanded: Boolean = true,
|
||||||
|
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||||
|
shape: Shape = MaterialTheme.shapes.large,
|
||||||
|
containerColor: Color = MaterialTheme.colorScheme.primaryContainer,
|
||||||
|
contentColor: Color = contentColorFor(containerColor),
|
||||||
|
elevation: FloatingActionButtonElevation = FloatingActionButtonDefaults.elevation(),
|
||||||
|
) {
|
||||||
|
val minWidth by animateDpAsState(
|
||||||
|
targetValue = if (expanded) ExtendedFabMinimumWidth else FabContainerWidth,
|
||||||
|
label = "minWidth",
|
||||||
|
)
|
||||||
|
FloatingActionButton(
|
||||||
|
modifier = modifier.sizeIn(minWidth = minWidth),
|
||||||
|
onClick = onClick,
|
||||||
|
interactionSource = interactionSource,
|
||||||
|
shape = shape,
|
||||||
|
containerColor = containerColor,
|
||||||
|
contentColor = contentColor,
|
||||||
|
elevation = elevation,
|
||||||
|
) {
|
||||||
|
val startPadding by animateDpAsState(
|
||||||
|
targetValue = if (expanded) ExtendedFabIconSize / 2 else 0.dp,
|
||||||
|
label = "startPadding",
|
||||||
|
)
|
||||||
|
val endPadding by animateDpAsState(
|
||||||
|
targetValue = if (expanded) ExtendedFabTextPadding else 0.dp,
|
||||||
|
label = "endPadding",
|
||||||
|
)
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.padding(start = startPadding, end = endPadding),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
icon()
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = expanded,
|
||||||
|
enter = ExtendedFabExpandAnimation,
|
||||||
|
exit = ExtendedFabCollapseAnimation,
|
||||||
|
) {
|
||||||
|
Row {
|
||||||
|
Spacer(Modifier.width(ExtendedFabIconPadding))
|
||||||
|
text()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val EasingLinearCubicBezier = CubicBezierEasing(0.0f, 0.0f, 1.0f, 1.0f)
|
||||||
|
private val EasingEmphasizedCubicBezier = CubicBezierEasing(0.2f, 0.0f, 0.0f, 1.0f)
|
||||||
|
|
||||||
|
private val ExtendedFabMinimumWidth = 80.dp
|
||||||
|
private val ExtendedFabIconSize = 24.0.dp
|
||||||
|
private val ExtendedFabIconPadding = 12.dp
|
||||||
|
private val ExtendedFabTextPadding = 20.dp
|
||||||
|
|
||||||
|
private val ExtendedFabCollapseAnimation = fadeOut(
|
||||||
|
animationSpec = tween(
|
||||||
|
durationMillis = 100,
|
||||||
|
easing = EasingLinearCubicBezier,
|
||||||
|
),
|
||||||
|
) + shrinkHorizontally(
|
||||||
|
animationSpec = tween(
|
||||||
|
durationMillis = 500,
|
||||||
|
easing = EasingEmphasizedCubicBezier,
|
||||||
|
),
|
||||||
|
shrinkTowards = Alignment.Start,
|
||||||
|
)
|
||||||
|
|
||||||
|
private val ExtendedFabExpandAnimation = fadeIn(
|
||||||
|
animationSpec = tween(
|
||||||
|
durationMillis = 200,
|
||||||
|
delayMillis = 100,
|
||||||
|
easing = EasingLinearCubicBezier,
|
||||||
|
),
|
||||||
|
) + expandHorizontally(
|
||||||
|
animationSpec = tween(
|
||||||
|
durationMillis = 500,
|
||||||
|
easing = EasingEmphasizedCubicBezier,
|
||||||
|
),
|
||||||
|
expandFrom = Alignment.Start,
|
||||||
|
)
|
||||||
|
|
||||||
|
private val FabContainerWidth = 56.0.dp
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
package org.xtimms.shirizu.core.components
|
||||||
|
|
||||||
|
import android.text.Spanned
|
||||||
|
import android.text.style.BulletSpan
|
||||||
|
import android.text.style.ForegroundColorSpan
|
||||||
|
import android.text.style.RelativeSizeSpan
|
||||||
|
import android.text.style.StrikethroughSpan
|
||||||
|
import android.text.style.StyleSpan
|
||||||
|
import android.text.style.SubscriptSpan
|
||||||
|
import android.text.style.SuperscriptSpan
|
||||||
|
import android.text.style.TypefaceSpan
|
||||||
|
import android.text.style.URLSpan
|
||||||
|
import android.text.style.UnderlineSpan
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.compose.foundation.text.ClickableText
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
|
import androidx.compose.ui.text.SpanStyle
|
||||||
|
import androidx.compose.ui.text.TextLayoutResult
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.buildAnnotatedString
|
||||||
|
import androidx.compose.ui.text.style.TextDecoration
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.TextUnit
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.core.text.HtmlCompat
|
||||||
|
import org.xtimms.shirizu.utils.spanStyle
|
||||||
|
|
||||||
|
private const val URL_TAG = "url_tag"
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun HtmlTextField(
|
||||||
|
text: String,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
style: TextStyle = TextStyle.Default,
|
||||||
|
softWrap: Boolean = true,
|
||||||
|
overflow: TextOverflow = TextOverflow.Clip,
|
||||||
|
maxLines: Int = Int.MAX_VALUE,
|
||||||
|
onTextLayout: (TextLayoutResult) -> Unit = {},
|
||||||
|
linkClicked: ((String) -> Unit)? = null,
|
||||||
|
fontSize: TextUnit = 14.sp,
|
||||||
|
flags: Int = HtmlCompat.FROM_HTML_MODE_COMPACT,
|
||||||
|
urlSpanStyle: SpanStyle = SpanStyle(
|
||||||
|
color = linkTextColor(),
|
||||||
|
textDecoration = TextDecoration.Underline
|
||||||
|
),
|
||||||
|
customSpannedHandler: ((Spanned) -> AnnotatedString)? = null
|
||||||
|
) {
|
||||||
|
val content = text.asHTML(fontSize, flags, urlSpanStyle, customSpannedHandler)
|
||||||
|
if (linkClicked != null) {
|
||||||
|
ClickableText(
|
||||||
|
modifier = modifier,
|
||||||
|
text = content,
|
||||||
|
style = style,
|
||||||
|
softWrap = softWrap,
|
||||||
|
overflow = overflow,
|
||||||
|
maxLines = maxLines,
|
||||||
|
onTextLayout = onTextLayout,
|
||||||
|
onClick = {
|
||||||
|
content
|
||||||
|
.getStringAnnotations(URL_TAG, it, it)
|
||||||
|
.firstOrNull()
|
||||||
|
?.let { stringAnnotation -> linkClicked(stringAnnotation.item) }
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Text(
|
||||||
|
modifier = modifier,
|
||||||
|
text = content,
|
||||||
|
style = style,
|
||||||
|
softWrap = softWrap,
|
||||||
|
overflow = overflow,
|
||||||
|
maxLines = maxLines,
|
||||||
|
onTextLayout = onTextLayout
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun linkTextColor() = Color(
|
||||||
|
TextView(LocalContext.current).linkTextColors.defaultColor
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun String.asHTML(
|
||||||
|
fontSize: TextUnit,
|
||||||
|
flags: Int,
|
||||||
|
URLSpanStyle: SpanStyle,
|
||||||
|
customSpannedHandler: ((Spanned) -> AnnotatedString)? = null
|
||||||
|
) = buildAnnotatedString {
|
||||||
|
val spanned = HtmlCompat.fromHtml(this@asHTML, flags)
|
||||||
|
val spans = spanned.getSpans(0, spanned.length, Any::class.java)
|
||||||
|
|
||||||
|
if (customSpannedHandler != null) {
|
||||||
|
append(customSpannedHandler(spanned))
|
||||||
|
} else {
|
||||||
|
append(spanned.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
spans
|
||||||
|
.filter { it !is BulletSpan }
|
||||||
|
.forEach { span ->
|
||||||
|
val start = spanned.getSpanStart(span)
|
||||||
|
val end = spanned.getSpanEnd(span)
|
||||||
|
when (span) {
|
||||||
|
is RelativeSizeSpan -> span.spanStyle(fontSize)
|
||||||
|
is StyleSpan -> span.spanStyle()
|
||||||
|
is UnderlineSpan -> span.spanStyle()
|
||||||
|
is ForegroundColorSpan -> span.spanStyle()
|
||||||
|
is TypefaceSpan -> span.spanStyle()
|
||||||
|
is StrikethroughSpan -> span.spanStyle()
|
||||||
|
is SuperscriptSpan -> span.spanStyle()
|
||||||
|
is SubscriptSpan -> span.spanStyle()
|
||||||
|
is URLSpan -> {
|
||||||
|
addStringAnnotation(
|
||||||
|
tag = URL_TAG,
|
||||||
|
annotation = span.url,
|
||||||
|
start = start,
|
||||||
|
end = end
|
||||||
|
)
|
||||||
|
URLSpanStyle
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}?.let { spanStyle ->
|
||||||
|
addStyle(spanStyle, start, end)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun UnderlineSpan.spanStyle(): SpanStyle =
|
||||||
|
SpanStyle(textDecoration = TextDecoration.Underline)
|
||||||
|
|
||||||
|
internal fun ForegroundColorSpan.spanStyle(): SpanStyle =
|
||||||
|
SpanStyle(color = Color(foregroundColor))
|
||||||
|
|
||||||
|
internal fun StrikethroughSpan.spanStyle(): SpanStyle =
|
||||||
|
SpanStyle(textDecoration = TextDecoration.LineThrough)
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package org.xtimms.shirizu.core.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.outlined.ArrowBack
|
||||||
|
import androidx.compose.material.icons.outlined.OpenInBrowser
|
||||||
|
import androidx.compose.material3.FilledTonalIconButton
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.IconButtonColors
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.surfaceColorAtElevation
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import org.xtimms.shirizu.R
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun BackIconButton(
|
||||||
|
onClick: () -> Unit
|
||||||
|
) {
|
||||||
|
IconButton(onClick = onClick) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.AutoMirrored.Outlined.ArrowBack,
|
||||||
|
contentDescription = "arrow_back"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun CircleBackIconButton(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onClick: () -> Unit
|
||||||
|
) {
|
||||||
|
FilledTonalIconButton(
|
||||||
|
modifier = modifier,
|
||||||
|
onClick = onClick,
|
||||||
|
colors = IconButtonColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(3.dp),
|
||||||
|
contentColor = MaterialTheme.colorScheme.onSurface,
|
||||||
|
disabledContainerColor = MaterialTheme.colorScheme.outline,
|
||||||
|
disabledContentColor = MaterialTheme.colorScheme.outlineVariant
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.AutoMirrored.Outlined.ArrowBack,
|
||||||
|
contentDescription = "arrow_back"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ViewInBrowserButton(
|
||||||
|
onClick: () -> Unit
|
||||||
|
) {
|
||||||
|
IconButton(onClick = onClick) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.OpenInBrowser,
|
||||||
|
contentDescription = stringResource(R.string.open_in_browser)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package org.xtimms.shirizu.core.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.lazy.grid.GridCells
|
||||||
|
import androidx.compose.foundation.lazy.grid.LazyGridScope
|
||||||
|
import androidx.compose.foundation.lazy.grid.LazyGridState
|
||||||
|
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||||
|
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun FastScrollLazyVerticalGrid(
|
||||||
|
columns: GridCells,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
state: LazyGridState = rememberLazyGridState(),
|
||||||
|
thumbAllowed: () -> Boolean = { true },
|
||||||
|
thumbColor: Color = MaterialTheme.colorScheme.primary,
|
||||||
|
contentPadding: PaddingValues = PaddingValues(0.dp),
|
||||||
|
topContentPadding: Dp = Dp.Hairline,
|
||||||
|
bottomContentPadding: Dp = Dp.Hairline,
|
||||||
|
endContentPadding: Dp = Dp.Hairline,
|
||||||
|
reverseLayout: Boolean = false,
|
||||||
|
verticalArrangement: Arrangement.Vertical =
|
||||||
|
if (!reverseLayout) Arrangement.Top else Arrangement.Bottom,
|
||||||
|
horizontalArrangement: Arrangement.Horizontal = Arrangement.Start,
|
||||||
|
userScrollEnabled: Boolean = true,
|
||||||
|
content: LazyGridScope.() -> Unit,
|
||||||
|
) {
|
||||||
|
VerticalGridFastScroller(
|
||||||
|
state = state,
|
||||||
|
columns = columns,
|
||||||
|
arrangement = horizontalArrangement,
|
||||||
|
contentPadding = contentPadding,
|
||||||
|
modifier = modifier,
|
||||||
|
thumbAllowed = thumbAllowed,
|
||||||
|
thumbColor = thumbColor,
|
||||||
|
topContentPadding = topContentPadding,
|
||||||
|
bottomContentPadding = bottomContentPadding,
|
||||||
|
endContentPadding = endContentPadding,
|
||||||
|
) {
|
||||||
|
LazyVerticalGrid(
|
||||||
|
columns = columns,
|
||||||
|
state = state,
|
||||||
|
contentPadding = contentPadding,
|
||||||
|
reverseLayout = reverseLayout,
|
||||||
|
verticalArrangement = verticalArrangement,
|
||||||
|
horizontalArrangement = horizontalArrangement,
|
||||||
|
userScrollEnabled = userScrollEnabled,
|
||||||
|
content = content,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
package org.xtimms.shirizu.core.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.calculateEndPadding
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.LazyListScope
|
||||||
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import org.xtimms.shirizu.utils.composable.drawVerticalScrollbar
|
||||||
|
|
||||||
|
/**
|
||||||
|
* LazyColumn with scrollbar.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun ScrollbarLazyColumn(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
state: LazyListState = rememberLazyListState(),
|
||||||
|
contentPadding: PaddingValues = PaddingValues(0.dp),
|
||||||
|
reverseLayout: Boolean = false,
|
||||||
|
verticalArrangement: Arrangement.Vertical =
|
||||||
|
if (!reverseLayout) Arrangement.Top else Arrangement.Bottom,
|
||||||
|
horizontalAlignment: Alignment.Horizontal = Alignment.Start,
|
||||||
|
userScrollEnabled: Boolean = true,
|
||||||
|
content: LazyListScope.() -> Unit,
|
||||||
|
) {
|
||||||
|
val direction = LocalLayoutDirection.current
|
||||||
|
val density = LocalDensity.current
|
||||||
|
val positionOffset = remember(contentPadding) {
|
||||||
|
with(density) { contentPadding.calculateEndPadding(direction).toPx() }
|
||||||
|
}
|
||||||
|
LazyColumn(
|
||||||
|
modifier = modifier
|
||||||
|
.drawVerticalScrollbar(
|
||||||
|
state = state,
|
||||||
|
reverseScrolling = reverseLayout,
|
||||||
|
positionOffsetPx = positionOffset,
|
||||||
|
),
|
||||||
|
state = state,
|
||||||
|
contentPadding = contentPadding,
|
||||||
|
reverseLayout = reverseLayout,
|
||||||
|
verticalArrangement = verticalArrangement,
|
||||||
|
horizontalAlignment = horizontalAlignment,
|
||||||
|
userScrollEnabled = userScrollEnabled,
|
||||||
|
content = content,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* LazyColumn with fast scroller.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun FastScrollLazyColumn(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
state: LazyListState = rememberLazyListState(),
|
||||||
|
contentPadding: PaddingValues = PaddingValues(0.dp),
|
||||||
|
reverseLayout: Boolean = false,
|
||||||
|
verticalArrangement: Arrangement.Vertical =
|
||||||
|
if (!reverseLayout) Arrangement.Top else Arrangement.Bottom,
|
||||||
|
horizontalAlignment: Alignment.Horizontal = Alignment.Start,
|
||||||
|
userScrollEnabled: Boolean = true,
|
||||||
|
content: LazyListScope.() -> Unit,
|
||||||
|
) {
|
||||||
|
VerticalFastScroller(
|
||||||
|
listState = state,
|
||||||
|
modifier = modifier,
|
||||||
|
topContentPadding = contentPadding.calculateTopPadding(),
|
||||||
|
endContentPadding = contentPadding.calculateEndPadding(LocalLayoutDirection.current),
|
||||||
|
) {
|
||||||
|
LazyColumn(
|
||||||
|
state = state,
|
||||||
|
contentPadding = contentPadding,
|
||||||
|
reverseLayout = reverseLayout,
|
||||||
|
verticalArrangement = verticalArrangement,
|
||||||
|
horizontalAlignment = horizontalAlignment,
|
||||||
|
userScrollEnabled = userScrollEnabled,
|
||||||
|
content = content,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package org.xtimms.shirizu.core.components
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.RowScope
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun Header(
|
||||||
|
title: String,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
loading: Boolean = false,
|
||||||
|
content: @Composable RowScope.() -> Unit = {},
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
modifier = modifier,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = title,
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
)
|
||||||
|
|
||||||
|
Spacer(Modifier.weight(1f))
|
||||||
|
|
||||||
|
AnimatedVisibility(visible = loading) {
|
||||||
|
AutoSizedCircularProgressIndicator(
|
||||||
|
color = MaterialTheme.colorScheme.secondary,
|
||||||
|
modifier = Modifier.size(16.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ListGroupHeader(
|
||||||
|
text: String,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
modifier = modifier
|
||||||
|
.padding(
|
||||||
|
horizontal = 16.dp,
|
||||||
|
vertical = 8.dp,
|
||||||
|
),
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
package org.xtimms.shirizu.core.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.aspectRatio
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.Shape
|
||||||
|
import androidx.compose.ui.graphics.painter.ColorPainter
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.semantics.Role
|
||||||
|
import coil.ImageLoader
|
||||||
|
import coil.compose.AsyncImage
|
||||||
|
import coil.request.ImageRequest
|
||||||
|
import org.xtimms.shirizu.LocalImageLoader
|
||||||
|
import org.xtimms.shirizu.R
|
||||||
|
import org.xtimms.shirizu.utils.composable.rememberResourceBitmapPainter
|
||||||
|
|
||||||
|
enum class MangaCover(val ratio: Float) {
|
||||||
|
Square(1f / 1f),
|
||||||
|
Book(2f / 3f),
|
||||||
|
;
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
operator fun invoke(
|
||||||
|
data: Any?,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
contentDescription: String = "",
|
||||||
|
shape: Shape = MaterialTheme.shapes.small,
|
||||||
|
onClick: (() -> Unit)? = null,
|
||||||
|
) {
|
||||||
|
AsyncImage(
|
||||||
|
imageLoader = LocalImageLoader.current,
|
||||||
|
model = data,
|
||||||
|
placeholder = ColorPainter(CoverPlaceholderColor),
|
||||||
|
error = rememberResourceBitmapPainter(id = R.drawable.cover_error),
|
||||||
|
contentDescription = contentDescription,
|
||||||
|
modifier = modifier
|
||||||
|
.aspectRatio(ratio)
|
||||||
|
.clip(shape)
|
||||||
|
.then(
|
||||||
|
if (onClick != null) {
|
||||||
|
Modifier.clickable(
|
||||||
|
role = Role.Button,
|
||||||
|
onClick = onClick,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Modifier
|
||||||
|
},
|
||||||
|
),
|
||||||
|
contentScale = ContentScale.Crop,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val CoverPlaceholderColor = Color(0x1F888888)
|
||||||
@@ -0,0 +1,236 @@
|
|||||||
|
package org.xtimms.shirizu.core.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.combinedClickable
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.BoxScope
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.IntrinsicSize
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.RowScope
|
||||||
|
import androidx.compose.foundation.layout.aspectRatio
|
||||||
|
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.width
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.LocalContentColor
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
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.alpha
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.draw.drawBehind
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.Shadow
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import coil.ImageLoader
|
||||||
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
|
import org.xtimms.shirizu.core.ShirizuAsyncImage
|
||||||
|
|
||||||
|
private const val GridSelectedCoverAlpha = 0.76f
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MangaGridItem(
|
||||||
|
manga: Manga,
|
||||||
|
title: String,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
onLongClick: () -> Unit,
|
||||||
|
isSelected: Boolean = false,
|
||||||
|
titleMaxLines: Int = 2,
|
||||||
|
coverAlpha: Float = 1f,
|
||||||
|
) {
|
||||||
|
GridItemSelectable(
|
||||||
|
isSelected = isSelected,
|
||||||
|
onClick = onClick,
|
||||||
|
onLongClick = onLongClick,
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
|
MangaGridCover(
|
||||||
|
cover = {
|
||||||
|
MangaCover.Book(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(4.dp)
|
||||||
|
.clip(MaterialTheme.shapes.medium)
|
||||||
|
.alpha(if (isSelected) GridSelectedCoverAlpha else coverAlpha),
|
||||||
|
data = manga.largeCoverUrl ?: manga.coverUrl,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
GridItemTitle(
|
||||||
|
modifier = Modifier.padding(4.dp),
|
||||||
|
title = title,
|
||||||
|
style = MaterialTheme.typography.titleSmall,
|
||||||
|
minLines = 2,
|
||||||
|
maxLines = titleMaxLines,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun MangaHorizontalItem(
|
||||||
|
manga: Manga,
|
||||||
|
onClick: (Manga) -> Unit,
|
||||||
|
onLongClick: () -> Unit,
|
||||||
|
isSelected: Boolean = false,
|
||||||
|
) {
|
||||||
|
GridItemSelectable(
|
||||||
|
isSelected = isSelected,
|
||||||
|
onClick = { onClick(manga) },
|
||||||
|
onLongClick = onLongClick,
|
||||||
|
modifier = Modifier.width(IntrinsicSize.Min)
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.Start
|
||||||
|
) {
|
||||||
|
Box {
|
||||||
|
ShirizuAsyncImage(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(4.dp)
|
||||||
|
.clip(MaterialTheme.shapes.medium)
|
||||||
|
.aspectRatio(10F / 16F)
|
||||||
|
.height(156.dp),
|
||||||
|
model = manga.largeCoverUrl ?: manga.coverUrl,
|
||||||
|
contentDescription = null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
text = manga.title,
|
||||||
|
modifier = Modifier.padding(4.dp),
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
maxLines = 2,
|
||||||
|
style = MaterialTheme.typography.titleSmall,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Common cover layout to add contents to be drawn on top of the cover.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
private fun MangaGridCover(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
cover: @Composable BoxScope.() -> Unit = {},
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.aspectRatio(MangaCover.Book.ratio),
|
||||||
|
) {
|
||||||
|
cover()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Title overlay for [MangaCompactGridItem]
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
private fun BoxScope.CoverTextOverlay(
|
||||||
|
title: String,
|
||||||
|
onClickContinueReading: (() -> Unit)? = null,
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.clip(RoundedCornerShape(bottomStart = 4.dp, bottomEnd = 4.dp))
|
||||||
|
.background(
|
||||||
|
Brush.verticalGradient(
|
||||||
|
0f to Color.Transparent,
|
||||||
|
1f to Color(0xAA000000),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.fillMaxHeight(0.33f)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.align(Alignment.BottomCenter),
|
||||||
|
)
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.align(Alignment.BottomStart),
|
||||||
|
verticalAlignment = Alignment.Bottom,
|
||||||
|
) {
|
||||||
|
GridItemTitle(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.padding(8.dp),
|
||||||
|
title = title,
|
||||||
|
style = MaterialTheme.typography.titleSmall.copy(
|
||||||
|
color = Color.White,
|
||||||
|
shadow = Shadow(
|
||||||
|
color = Color.Black,
|
||||||
|
blurRadius = 4f,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
minLines = 1,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun GridItemTitle(
|
||||||
|
title: String,
|
||||||
|
style: TextStyle,
|
||||||
|
minLines: Int,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
maxLines: Int = 2,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
modifier = modifier,
|
||||||
|
text = title,
|
||||||
|
minLines = minLines,
|
||||||
|
maxLines = maxLines,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
style = style,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapper for grid items to handle selection state, click and long click.
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
|
@Composable
|
||||||
|
private fun GridItemSelectable(
|
||||||
|
isSelected: Boolean,
|
||||||
|
onClick: () -> Unit,
|
||||||
|
onLongClick: () -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
content: @Composable () -> Unit,
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.clip(MaterialTheme.shapes.small)
|
||||||
|
.combinedClickable(
|
||||||
|
onClick = onClick,
|
||||||
|
onLongClick = onLongClick,
|
||||||
|
)
|
||||||
|
.selectedOutline(isSelected = isSelected, color = MaterialTheme.colorScheme.secondary)
|
||||||
|
.padding(4.dp),
|
||||||
|
) {
|
||||||
|
val contentColor = if (isSelected) {
|
||||||
|
MaterialTheme.colorScheme.onSecondary
|
||||||
|
} else {
|
||||||
|
LocalContentColor.current
|
||||||
|
}
|
||||||
|
CompositionLocalProvider(LocalContentColor provides contentColor) {
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @see GridItemSelectable
|
||||||
|
*/
|
||||||
|
private fun Modifier.selectedOutline(
|
||||||
|
isSelected: Boolean,
|
||||||
|
color: Color,
|
||||||
|
) = this then drawBehind { if (isSelected) drawRect(color = color) }
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
package org.xtimms.shirizu.core.components
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
|
import androidx.compose.foundation.layout.asPaddingValues
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.navigationBars
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.ModalBottomSheet
|
||||||
|
import androidx.compose.material3.SheetState
|
||||||
|
import androidx.compose.material3.SheetValue
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun ShirizuModalBottomSheet(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
sheetState: SheetState = SheetState(
|
||||||
|
skipPartiallyExpanded = true,
|
||||||
|
density = LocalDensity.current,
|
||||||
|
initialValue = SheetValue.Hidden
|
||||||
|
),
|
||||||
|
onDismissRequest: () -> Unit,
|
||||||
|
horizontalPadding: PaddingValues = PaddingValues(horizontal = 28.dp),
|
||||||
|
content: @Composable ColumnScope.() -> Unit = {},
|
||||||
|
) {
|
||||||
|
ModalBottomSheet(
|
||||||
|
modifier = modifier,
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
sheetState = sheetState,
|
||||||
|
contentWindowInsets = { WindowInsets(0.dp, 0.dp, 0.dp, 0.dp) },
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.padding(paddingValues = horizontalPadding)) {
|
||||||
|
content()
|
||||||
|
Spacer(modifier = Modifier.height(28.dp))
|
||||||
|
}
|
||||||
|
Spacer(
|
||||||
|
modifier = Modifier
|
||||||
|
.background(MaterialTheme.colorScheme.surfaceContainerHigh)
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(
|
||||||
|
with(
|
||||||
|
WindowInsets.navigationBars
|
||||||
|
.asPaddingValues()
|
||||||
|
.calculateBottomPadding()
|
||||||
|
) {
|
||||||
|
when {
|
||||||
|
this.value > 30f -> {
|
||||||
|
this
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: https://issuetracker.google.com/issues/290798798
|
||||||
|
Build.VERSION.SDK_INT < 30 -> {
|
||||||
|
48.dp
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
0.dp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DrawerSheetSubtitle(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
text: String,
|
||||||
|
color: Color = MaterialTheme.colorScheme.primary,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = text,
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(start = 4.dp, top = 16.dp, bottom = 8.dp),
|
||||||
|
color = color,
|
||||||
|
style = MaterialTheme.typography.labelLarge
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package org.xtimms.tokusho.core.components
|
package org.xtimms.shirizu.core.components
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
@@ -1,5 +1,13 @@
|
|||||||
package org.xtimms.tokusho.core.components
|
package org.xtimms.shirizu.core.components
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedContent
|
||||||
|
import androidx.compose.animation.SizeTransform
|
||||||
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
|
import androidx.compose.animation.fadeIn
|
||||||
|
import androidx.compose.animation.fadeOut
|
||||||
|
import androidx.compose.animation.slideInVertically
|
||||||
|
import androidx.compose.animation.slideOutVertically
|
||||||
|
import androidx.compose.animation.togetherWith
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
@@ -15,16 +23,20 @@ import androidx.compose.foundation.layout.size
|
|||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.selection.selectable
|
import androidx.compose.foundation.selection.selectable
|
||||||
import androidx.compose.foundation.selection.toggleable
|
import androidx.compose.foundation.selection.toggleable
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.outlined.Check
|
|
||||||
import androidx.compose.material.icons.outlined.TipsAndUpdates
|
|
||||||
import androidx.compose.material.icons.outlined.Call
|
import androidx.compose.material.icons.outlined.Call
|
||||||
|
import androidx.compose.material.icons.outlined.Check
|
||||||
import androidx.compose.material.icons.outlined.Info
|
import androidx.compose.material.icons.outlined.Info
|
||||||
|
import androidx.compose.material.icons.outlined.Save
|
||||||
|
import androidx.compose.material.icons.outlined.TipsAndUpdates
|
||||||
import androidx.compose.material.icons.outlined.ToggleOn
|
import androidx.compose.material.icons.outlined.ToggleOn
|
||||||
import androidx.compose.material.icons.outlined.Translate
|
import androidx.compose.material.icons.outlined.Translate
|
||||||
import androidx.compose.material.icons.outlined.Update
|
import androidx.compose.material.icons.outlined.Update
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.LinearProgressIndicator
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.ProgressIndicatorDefaults
|
||||||
import androidx.compose.material3.RadioButton
|
import androidx.compose.material3.RadioButton
|
||||||
import androidx.compose.material3.Surface
|
import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.Switch
|
import androidx.compose.material3.Switch
|
||||||
@@ -41,8 +53,10 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.StrokeCap
|
||||||
import androidx.compose.ui.graphics.painter.Painter
|
import androidx.compose.ui.graphics.painter.Painter
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.semantics.clearAndSetSemantics
|
import androidx.compose.ui.semantics.clearAndSetSemantics
|
||||||
import androidx.compose.ui.semantics.contentDescription
|
import androidx.compose.ui.semantics.contentDescription
|
||||||
@@ -52,13 +66,15 @@ import androidx.compose.ui.text.style.TextOverflow
|
|||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import org.xtimms.shiki.ui.theme.FixedAccentColors
|
import org.xtimms.shirizu.ui.theme.FixedAccentColors
|
||||||
import org.xtimms.tokusho.R
|
import org.xtimms.shirizu.R
|
||||||
import org.xtimms.tokusho.ui.monet.LocalTonalPalettes
|
import org.xtimms.shirizu.core.ShirizuAsyncImage
|
||||||
import org.xtimms.tokusho.ui.monet.TonalPalettes.Companion.toTonalPalettes
|
import org.xtimms.shirizu.ui.monet.LocalTonalPalettes
|
||||||
import org.xtimms.tokusho.ui.theme.PreviewThemeLight
|
import org.xtimms.shirizu.ui.monet.TonalPalettes.Companion.toTonalPalettes
|
||||||
import org.xtimms.tokusho.ui.theme.applyOpacity
|
import org.xtimms.shirizu.ui.theme.PreviewThemeLight
|
||||||
import org.xtimms.tokusho.ui.theme.preferenceTitle
|
import org.xtimms.shirizu.ui.theme.ShirizuTheme
|
||||||
|
import org.xtimms.shirizu.ui.theme.applyOpacity
|
||||||
|
import org.xtimms.shirizu.utils.FileSize
|
||||||
|
|
||||||
private const val horizontal = 8
|
private const val horizontal = 8
|
||||||
private const val vertical = 16
|
private const val vertical = 16
|
||||||
@@ -121,8 +137,13 @@ fun PreferenceItem(
|
|||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.weight(1f)
|
.weight(1f)
|
||||||
.padding(horizontal = 16.dp)
|
.then(
|
||||||
.padding(end = 8.dp)
|
if (icon != null)
|
||||||
|
Modifier
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
.padding(end = 8.dp)
|
||||||
|
else Modifier.padding(horizontal = 8.dp)
|
||||||
|
)
|
||||||
) {
|
) {
|
||||||
PreferenceItemTitle(text = title, enabled = enabled)
|
PreferenceItemTitle(text = title, enabled = enabled)
|
||||||
if (!description.isNullOrEmpty()) PreferenceItemDescription(
|
if (!description.isNullOrEmpty()) PreferenceItemDescription(
|
||||||
@@ -150,7 +171,6 @@ internal fun PreferenceItemTitle(
|
|||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
text: String,
|
text: String,
|
||||||
maxLines: Int = 2,
|
maxLines: Int = 2,
|
||||||
style: TextStyle = preferenceTitle,
|
|
||||||
enabled: Boolean,
|
enabled: Boolean,
|
||||||
color: Color = MaterialTheme.colorScheme.onBackground,
|
color: Color = MaterialTheme.colorScheme.onBackground,
|
||||||
overflow: TextOverflow = TextOverflow.Ellipsis
|
overflow: TextOverflow = TextOverflow.Ellipsis
|
||||||
@@ -159,7 +179,6 @@ internal fun PreferenceItemTitle(
|
|||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
text = text,
|
text = text,
|
||||||
maxLines = maxLines,
|
maxLines = maxLines,
|
||||||
style = style,
|
|
||||||
color = color.applyOpacity(enabled),
|
color = color.applyOpacity(enabled),
|
||||||
overflow = overflow
|
overflow = overflow
|
||||||
)
|
)
|
||||||
@@ -175,14 +194,28 @@ internal fun PreferenceItemDescription(
|
|||||||
color: Color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color: Color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
overflow: TextOverflow = TextOverflow.Ellipsis
|
overflow: TextOverflow = TextOverflow.Ellipsis
|
||||||
) {
|
) {
|
||||||
Text(
|
AnimatedContent(
|
||||||
|
targetState = text,
|
||||||
|
transitionSpec = {
|
||||||
|
if (targetState > initialState) {
|
||||||
|
(slideInVertically { height -> -height } + fadeIn()).togetherWith(
|
||||||
|
slideOutVertically { height -> height } + fadeOut())
|
||||||
|
} else {
|
||||||
|
(slideInVertically { height -> height } + fadeIn()).togetherWith(
|
||||||
|
slideOutVertically { height -> -height } + fadeOut())
|
||||||
|
}.using(SizeTransform(clip = false))
|
||||||
|
},
|
||||||
modifier = modifier.padding(top = 2.dp),
|
modifier = modifier.padding(top = 2.dp),
|
||||||
text = text,
|
label = "Preference desc"
|
||||||
maxLines = maxLines,
|
) { targetText ->
|
||||||
style = style,
|
Text(
|
||||||
color = color.applyOpacity(enabled),
|
text = targetText,
|
||||||
overflow = overflow
|
maxLines = maxLines,
|
||||||
)
|
style = style,
|
||||||
|
color = color.applyOpacity(enabled),
|
||||||
|
overflow = overflow
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -315,7 +348,6 @@ fun PreferenceSwitchWithContainer(
|
|||||||
Text(
|
Text(
|
||||||
text = title,
|
text = title,
|
||||||
maxLines = 2,
|
maxLines = 2,
|
||||||
style = preferenceTitle,
|
|
||||||
color = if (isChecked) FixedAccentColors.onPrimaryFixed else colorScheme.surface
|
color = if (isChecked) FixedAccentColors.onPrimaryFixed else colorScheme.surface
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -465,7 +497,8 @@ fun PreferenceSwitch(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
.padding(horizontal = 16.dp)
|
.padding(horizontal = 16.dp)
|
||||||
) {
|
) {
|
||||||
PreferenceItemTitle(
|
PreferenceItemTitle(
|
||||||
@@ -510,7 +543,7 @@ fun PreferencesHintCard(
|
|||||||
imageVector = icon,
|
imageVector = icon,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(start = 8.dp, end = 16.dp)
|
.padding(start = 8.dp, end = 24.dp)
|
||||||
.size(24.dp),
|
.size(24.dp),
|
||||||
tint = contentColor
|
tint = contentColor
|
||||||
)
|
)
|
||||||
@@ -538,11 +571,209 @@ fun PreferencesHintCard(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PreferenceStorageHeader(
|
||||||
|
used: Float = 40F,
|
||||||
|
total: Float = 128F
|
||||||
|
) {
|
||||||
|
|
||||||
|
val animatedProgress = animateFloatAsState(
|
||||||
|
targetValue = 1 - ((total - used) / total),
|
||||||
|
animationSpec = ProgressIndicatorDefaults.ProgressAnimationSpec,
|
||||||
|
label = "Progress"
|
||||||
|
).value
|
||||||
|
|
||||||
|
Column {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp, vertical = 12.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
AnimatedNumber(
|
||||||
|
value = FileSize.BYTES.formatWithoutUnits(used)
|
||||||
|
)
|
||||||
|
AnimatedContent(
|
||||||
|
targetState = used,
|
||||||
|
transitionSpec = {
|
||||||
|
if (targetState > initialState) {
|
||||||
|
(fadeIn()).togetherWith(fadeOut())
|
||||||
|
} else {
|
||||||
|
(fadeIn()).togetherWith(fadeOut())
|
||||||
|
}.using(SizeTransform(clip = false))
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.align(Alignment.Bottom)
|
||||||
|
.padding(PaddingValues(start = 4.dp, bottom = 12.dp)),
|
||||||
|
label = "Unit"
|
||||||
|
) { targetUsed ->
|
||||||
|
Text(text = FileSize.BYTES.showUnit(LocalContext.current, targetUsed))
|
||||||
|
}
|
||||||
|
AnimatedContent(
|
||||||
|
targetState = total,
|
||||||
|
transitionSpec = {
|
||||||
|
if (targetState > initialState) {
|
||||||
|
(fadeIn()).togetherWith(fadeOut())
|
||||||
|
} else {
|
||||||
|
(fadeIn()).togetherWith(fadeOut())
|
||||||
|
}.using(SizeTransform(clip = false))
|
||||||
|
},
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.Bottom)
|
||||||
|
.padding(PaddingValues(bottom = 12.dp)),
|
||||||
|
label = "Total used"
|
||||||
|
) { targetTotal ->
|
||||||
|
Text(
|
||||||
|
text = FileSize.BYTES.totalFormat(LocalContext.current, targetTotal),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LinearProgressIndicator(
|
||||||
|
progress = { animatedProgress },
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(PaddingValues(start = 16.dp, end = 16.dp, bottom = 16.dp))
|
||||||
|
.height(16.dp)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
trackColor = MaterialTheme.colorScheme.primaryContainer,
|
||||||
|
strokeCap = StrokeCap.Round,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
|
@Composable
|
||||||
|
fun PreferenceStorageItem(
|
||||||
|
title: String,
|
||||||
|
used: Float = 0F,
|
||||||
|
total: Float = 0F,
|
||||||
|
icon: Any? = null,
|
||||||
|
leadingIcon: (@Composable () -> Unit)? = null,
|
||||||
|
trailingIcon: (@Composable () -> Unit)? = null,
|
||||||
|
onClick: () -> Unit = {},
|
||||||
|
) {
|
||||||
|
|
||||||
|
val animatedProgress = animateFloatAsState(
|
||||||
|
targetValue = 1 - ((total - used) / total),
|
||||||
|
animationSpec = ProgressIndicatorDefaults.ProgressAnimationSpec,
|
||||||
|
label = "Progress"
|
||||||
|
).value
|
||||||
|
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier.combinedClickable(
|
||||||
|
onClick = onClick,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal.dp, vertical.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
leadingIcon?.invoke()
|
||||||
|
|
||||||
|
when (icon) {
|
||||||
|
is ImageVector -> {
|
||||||
|
Icon(
|
||||||
|
imageVector = icon,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(start = 8.dp, end = 16.dp)
|
||||||
|
.size(24.dp),
|
||||||
|
tint = MaterialTheme.colorScheme.onSurfaceVariant.applyOpacity(true),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is Painter -> {
|
||||||
|
Icon(
|
||||||
|
painter = icon,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(start = 8.dp, end = 16.dp)
|
||||||
|
.size(24.dp),
|
||||||
|
tint = MaterialTheme.colorScheme.onSurfaceVariant.applyOpacity(true),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.then(
|
||||||
|
if (icon != null)
|
||||||
|
Modifier
|
||||||
|
.padding(start = 16.dp, end = 8.dp)
|
||||||
|
else Modifier.padding(horizontal = 8.dp)
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
Row {
|
||||||
|
PreferenceItemTitle(
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
text = title,
|
||||||
|
enabled = true
|
||||||
|
)
|
||||||
|
AnimatedContent(
|
||||||
|
targetState = used,
|
||||||
|
transitionSpec = {
|
||||||
|
if (targetState > initialState) {
|
||||||
|
(fadeIn()).togetherWith(fadeOut())
|
||||||
|
} else {
|
||||||
|
(fadeIn()).togetherWith(fadeOut())
|
||||||
|
}.using(SizeTransform(clip = false))
|
||||||
|
},
|
||||||
|
label = "Total used"
|
||||||
|
) { targetTotal ->
|
||||||
|
Text(text = FileSize.BYTES.format(LocalContext.current, targetTotal))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LinearProgressIndicator(
|
||||||
|
progress = { animatedProgress },
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(PaddingValues(top = 12.dp))
|
||||||
|
.height(5.dp)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
trackColor = MaterialTheme.colorScheme.primaryContainer,
|
||||||
|
strokeCap = StrokeCap.Round,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
trailingIcon?.let {
|
||||||
|
VerticalDivider(
|
||||||
|
modifier = Modifier
|
||||||
|
.height(32.dp)
|
||||||
|
.padding(horizontal = 8.dp)
|
||||||
|
.align(Alignment.CenterVertically),
|
||||||
|
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.3f),
|
||||||
|
thickness = 1.dp
|
||||||
|
)
|
||||||
|
trailingIcon.invoke()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
fun PreferenceStorageHeaderPreview() {
|
||||||
|
ShirizuTheme {
|
||||||
|
PreferenceStorageHeader()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
fun PreferenceStorageItemPreview() {
|
||||||
|
ShirizuTheme {
|
||||||
|
PreferenceStorageItem(title = "Saved manga", icon = Icons.Outlined.Save, total = 0F)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@Preview
|
@Preview
|
||||||
fun PreferenceItemPreview() {
|
fun PreferenceItemPreview() {
|
||||||
Column {
|
Column {
|
||||||
PreferenceItem(title = "title", description = "description", icon = 0)
|
PreferenceItem(title = "title", description = "description")
|
||||||
PreferenceItem(title = "title", description = "description", icon = Icons.Outlined.Update)
|
PreferenceItem(title = "title", description = "description", icon = Icons.Outlined.Update)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,245 @@
|
|||||||
|
package org.xtimms.shirizu.core.components
|
||||||
|
|
||||||
|
import android.content.res.Configuration.UI_MODE_NIGHT_YES
|
||||||
|
import androidx.compose.animation.AnimatedContent
|
||||||
|
import androidx.compose.animation.animateColorAsState
|
||||||
|
import androidx.compose.animation.core.Animatable
|
||||||
|
import androidx.compose.animation.core.FloatTweenSpec
|
||||||
|
import androidx.compose.animation.core.LinearEasing
|
||||||
|
import androidx.compose.animation.core.RepeatMode
|
||||||
|
import androidx.compose.animation.core.TweenSpec
|
||||||
|
import androidx.compose.animation.core.VectorConverter
|
||||||
|
import androidx.compose.animation.core.animateValue
|
||||||
|
import androidx.compose.animation.core.infiniteRepeatable
|
||||||
|
import androidx.compose.animation.core.rememberInfiniteTransition
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
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.RowScope
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.CardDefaults
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.drawWithContent
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.geometry.Size
|
||||||
|
import androidx.compose.ui.graphics.BlendMode
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.drawscope.ContentDrawScope
|
||||||
|
import androidx.compose.ui.graphics.nativeCanvas
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.xtimms.shirizu.R
|
||||||
|
import org.xtimms.shirizu.core.model.MangaHistory
|
||||||
|
import org.xtimms.shirizu.sections.details.model.HistoryInfo
|
||||||
|
import org.xtimms.shirizu.ui.theme.ShirizuTheme
|
||||||
|
import java.time.Instant
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun RowScope.ReadButton(
|
||||||
|
info: HistoryInfo?,
|
||||||
|
estimatedReadTime: String
|
||||||
|
) {
|
||||||
|
|
||||||
|
val shift = remember { Animatable(0f) }
|
||||||
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
|
||||||
|
val animatedCardContainerColor = animateColorAsState(
|
||||||
|
label = "animatedCardContainerColor",
|
||||||
|
targetValue = if (info?.totalChapters == 0) MaterialTheme.colorScheme.errorContainer else MaterialTheme.colorScheme.primaryContainer,
|
||||||
|
animationSpec = TweenSpec(500)
|
||||||
|
).value
|
||||||
|
|
||||||
|
val animatedCardContentColor = animateColorAsState(
|
||||||
|
label = "animatedCardContentColor",
|
||||||
|
targetValue = if (info?.totalChapters == 0) MaterialTheme.colorScheme.onErrorContainer else MaterialTheme.colorScheme.onPrimaryContainer,
|
||||||
|
animationSpec = TweenSpec(500)
|
||||||
|
).value
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
fun anim() {
|
||||||
|
coroutineScope.launch {
|
||||||
|
shift.animateTo(
|
||||||
|
1f,
|
||||||
|
animationSpec = FloatTweenSpec(4000, 0, LinearEasing)
|
||||||
|
)
|
||||||
|
shift.snapTo(0f)
|
||||||
|
anim()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
anim()
|
||||||
|
}
|
||||||
|
|
||||||
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1F)
|
||||||
|
.height(54.dp),
|
||||||
|
shape = CircleShape,
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = animatedCardContainerColor,
|
||||||
|
contentColor = animatedCardContentColor,
|
||||||
|
),
|
||||||
|
onClick = {
|
||||||
|
// todo
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxHeight(),
|
||||||
|
contentAlignment = Alignment.CenterEnd,
|
||||||
|
) {
|
||||||
|
BackgroundProgress(
|
||||||
|
if (info?.totalChapters == 0) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.primary,
|
||||||
|
if (info?.isValid == true) 0.1f else 0.33f,
|
||||||
|
info?.history?.percent?.coerceIn(0f, 1f) ?: 0f
|
||||||
|
)
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.drawWithLayer {
|
||||||
|
drawContent()
|
||||||
|
val leftOffset = size.width - 20.dp.toPx()
|
||||||
|
drawRect(
|
||||||
|
topLeft = Offset(leftOffset, 0f),
|
||||||
|
size = Size(
|
||||||
|
20.dp.toPx(),
|
||||||
|
size.height,
|
||||||
|
),
|
||||||
|
blendMode = BlendMode.SrcIn,
|
||||||
|
brush = Brush.horizontalGradient(
|
||||||
|
colors = listOf(
|
||||||
|
Color.Black,
|
||||||
|
Color.Black.copy(alpha = 0f),
|
||||||
|
),
|
||||||
|
startX = leftOffset,
|
||||||
|
endX = leftOffset + 14.dp.toPx()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
) {
|
||||||
|
val infiniteTransition by rememberInfiniteTransition("infiniteTransition").animateValue(
|
||||||
|
label = "infiniteTransition",
|
||||||
|
initialValue = 0,
|
||||||
|
targetValue = 2,
|
||||||
|
typeConverter = Int.VectorConverter,
|
||||||
|
animationSpec = infiniteRepeatable(tween(15000), RepeatMode.Restart)
|
||||||
|
)
|
||||||
|
val chaptersSubtitle = when {
|
||||||
|
info == null -> "null"
|
||||||
|
info.isValid -> stringResource(R.string.loading_)
|
||||||
|
info.currentChapter >= 0 -> when (infiniteTransition) {
|
||||||
|
1 -> stringResource(
|
||||||
|
R.string.chapter_d_of_d,
|
||||||
|
info.currentChapter + 1,
|
||||||
|
info.totalChapters,
|
||||||
|
)
|
||||||
|
0 -> estimatedReadTime
|
||||||
|
else -> estimatedReadTime
|
||||||
|
}
|
||||||
|
|
||||||
|
info.totalChapters == 0 -> stringResource(R.string.no_chapters)
|
||||||
|
else -> LocalContext.current.resources.getQuantityString(
|
||||||
|
R.plurals.chapters,
|
||||||
|
info.totalChapters,
|
||||||
|
info.totalChapters,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
text = if (info?.history != null) {
|
||||||
|
stringResource(R.string.continue_reading)
|
||||||
|
} else {
|
||||||
|
stringResource(R.string.read)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
AnimatedContent(targetState = chaptersSubtitle, label = "Subtitle animation") {
|
||||||
|
Text(
|
||||||
|
text = it,
|
||||||
|
style = MaterialTheme.typography.labelMedium,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun ContentDrawScope.drawWithLayer(block: ContentDrawScope.() -> Unit) {
|
||||||
|
with(drawContext.canvas.nativeCanvas) {
|
||||||
|
val checkPoint = saveLayer(null, null)
|
||||||
|
block()
|
||||||
|
restoreToCount(checkPoint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Modifier.drawWithLayer(block: ContentDrawScope.() -> Unit) = this.then(
|
||||||
|
Modifier.drawWithContent {
|
||||||
|
drawWithLayer {
|
||||||
|
block()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
private fun Preview() {
|
||||||
|
ShirizuTheme {
|
||||||
|
Row {
|
||||||
|
ReadButton(
|
||||||
|
HistoryInfo(
|
||||||
|
100,
|
||||||
|
33,
|
||||||
|
MangaHistory(
|
||||||
|
Instant.now(),
|
||||||
|
Instant.now(),
|
||||||
|
1,
|
||||||
|
3,
|
||||||
|
1,
|
||||||
|
50f
|
||||||
|
)
|
||||||
|
),
|
||||||
|
"123"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(name = "Night mode", uiMode = UI_MODE_NIGHT_YES)
|
||||||
|
@Composable
|
||||||
|
private fun PreviewNightMode() {
|
||||||
|
ShirizuTheme {
|
||||||
|
Row {
|
||||||
|
ReadButton(
|
||||||
|
HistoryInfo(
|
||||||
|
100,
|
||||||
|
33,
|
||||||
|
MangaHistory(
|
||||||
|
Instant.now(),
|
||||||
|
Instant.now(),
|
||||||
|
1,
|
||||||
|
3,
|
||||||
|
1,
|
||||||
|
50f
|
||||||
|
)
|
||||||
|
),
|
||||||
|
"123"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
333
app/src/main/java/org/xtimms/shirizu/core/components/Scaffold.kt
Normal file
333
app/src/main/java/org/xtimms/shirizu/core/components/Scaffold.kt
Normal file
@@ -0,0 +1,333 @@
|
|||||||
|
package org.xtimms.shirizu.core.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
|
import androidx.compose.foundation.layout.MutableWindowInsets
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
|
import androidx.compose.foundation.layout.asPaddingValues
|
||||||
|
import androidx.compose.foundation.layout.calculateEndPadding
|
||||||
|
import androidx.compose.foundation.layout.calculateStartPadding
|
||||||
|
import androidx.compose.foundation.layout.exclude
|
||||||
|
import androidx.compose.foundation.layout.onConsumedWindowInsetsChanged
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.FabPosition
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.ScaffoldDefaults
|
||||||
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
|
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||||
|
import androidx.compose.material3.contentColorFor
|
||||||
|
import androidx.compose.material3.rememberTopAppBarState
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
|
import androidx.compose.ui.layout.SubcomposeLayout
|
||||||
|
import androidx.compose.ui.unit.Constraints
|
||||||
|
import androidx.compose.ui.unit.LayoutDirection
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.max
|
||||||
|
import androidx.compose.ui.util.fastForEach
|
||||||
|
import androidx.compose.ui.util.fastMap
|
||||||
|
import androidx.compose.ui.util.fastMaxBy
|
||||||
|
import kotlin.math.max
|
||||||
|
|
||||||
|
/**
|
||||||
|
* From Mihon
|
||||||
|
*
|
||||||
|
* <a href="https://material.io/design/layout/understanding-layout.html" class="external" target="_blank">Material Design layout</a>.
|
||||||
|
*
|
||||||
|
* Scaffold implements the basic material design visual layout structure.
|
||||||
|
*
|
||||||
|
* This component provides API to put together several material components to construct your
|
||||||
|
* screen, by ensuring proper layout strategy for them and collecting necessary data so these
|
||||||
|
* components will work together correctly.
|
||||||
|
*
|
||||||
|
* Simple example of a Scaffold with [SmallTopAppBar], [FloatingActionButton]:
|
||||||
|
*
|
||||||
|
* @sample androidx.compose.material3.samples.SimpleScaffoldWithTopBar
|
||||||
|
*
|
||||||
|
* To show a [Snackbar], use [SnackbarHostState.showSnackbar].
|
||||||
|
*
|
||||||
|
* @sample androidx.compose.material3.samples.ScaffoldWithSimpleSnackbar
|
||||||
|
*
|
||||||
|
* @param modifier the [Modifier] to be applied to this scaffold
|
||||||
|
* @param topBar top app bar of the screen, typically a [SmallTopAppBar]
|
||||||
|
* @param startBar side bar on the start of the screen, typically a [NavigationRail]
|
||||||
|
* @param bottomBar bottom bar of the screen, typically a [NavigationBar]
|
||||||
|
* @param snackbarHost component to host [Snackbar]s that are pushed to be shown via
|
||||||
|
* [SnackbarHostState.showSnackbar], typically a [SnackbarHost]
|
||||||
|
* @param floatingActionButton Main action button of the screen, typically a [FloatingActionButton]
|
||||||
|
* @param floatingActionButtonPosition position of the FAB on the screen. See [FabPosition].
|
||||||
|
* @param containerColor the color used for the background of this scaffold. Use [Color.Transparent]
|
||||||
|
* to have no color.
|
||||||
|
* @param contentColor the preferred color for content inside this scaffold. Defaults to either the
|
||||||
|
* matching content color for [containerColor], or to the current [LocalContentColor] if
|
||||||
|
* [containerColor] is not a color from the theme.
|
||||||
|
* @param contentWindowInsets window insets to be passed to content slot via PaddingValues params.
|
||||||
|
* Scaffold will take the insets into account from the top/bottom only if the topBar/ bottomBar
|
||||||
|
* are not present, as the scaffold expect topBar/bottomBar to handle insets instead
|
||||||
|
* @param content content of the screen. The lambda receives a [PaddingValues] that should be
|
||||||
|
* applied to the content root via [Modifier.padding] and [Modifier.consumeWindowInsets] to
|
||||||
|
* properly offset top and bottom bars. If using [Modifier.verticalScroll], apply this modifier to
|
||||||
|
* the child of the scroll, and not on the scroll itself.
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalLayoutApi::class)
|
||||||
|
@ExperimentalMaterial3Api
|
||||||
|
@Composable
|
||||||
|
fun Scaffold(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
topBarScrollBehavior: TopAppBarScrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(
|
||||||
|
rememberTopAppBarState(),
|
||||||
|
),
|
||||||
|
topBar: @Composable (TopAppBarScrollBehavior) -> Unit = {},
|
||||||
|
bottomBar: @Composable () -> Unit = {},
|
||||||
|
startBar: @Composable () -> Unit = {},
|
||||||
|
snackbarHost: @Composable () -> Unit = {},
|
||||||
|
floatingActionButton: @Composable () -> Unit = {},
|
||||||
|
floatingActionButtonPosition: FabPosition = FabPosition.End,
|
||||||
|
containerColor: Color = MaterialTheme.colorScheme.background,
|
||||||
|
contentColor: Color = contentColorFor(containerColor),
|
||||||
|
contentWindowInsets: WindowInsets = ScaffoldDefaults.contentWindowInsets,
|
||||||
|
content: @Composable (PaddingValues) -> Unit,
|
||||||
|
) {
|
||||||
|
val remainingWindowInsets = remember { MutableWindowInsets() }
|
||||||
|
androidx.compose.material3.Surface(
|
||||||
|
modifier = Modifier
|
||||||
|
.nestedScroll(topBarScrollBehavior.nestedScrollConnection)
|
||||||
|
.onConsumedWindowInsetsChanged {
|
||||||
|
remainingWindowInsets.insets = contentWindowInsets.exclude(
|
||||||
|
it,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.then(modifier),
|
||||||
|
color = containerColor,
|
||||||
|
contentColor = contentColor,
|
||||||
|
) {
|
||||||
|
ScaffoldLayout(
|
||||||
|
fabPosition = floatingActionButtonPosition,
|
||||||
|
topBar = { topBar(topBarScrollBehavior) },
|
||||||
|
startBar = startBar,
|
||||||
|
bottomBar = bottomBar,
|
||||||
|
content = content,
|
||||||
|
snackbar = snackbarHost,
|
||||||
|
contentWindowInsets = remainingWindowInsets,
|
||||||
|
fab = floatingActionButton,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Layout for a [Scaffold]'s content.
|
||||||
|
*
|
||||||
|
* @param fabPosition [FabPosition] for the FAB (if present)
|
||||||
|
* @param topBar the content to place at the top of the [Scaffold], typically a [SmallTopAppBar]
|
||||||
|
* @param content the main 'body' of the [Scaffold]
|
||||||
|
* @param snackbar the [Snackbar] displayed on top of the [content]
|
||||||
|
* @param fab the [FloatingActionButton] displayed on top of the [content], below the [snackbar]
|
||||||
|
* and above the [bottomBar]
|
||||||
|
* @param bottomBar the content to place at the bottom of the [Scaffold], on top of the
|
||||||
|
* [content], typically a [NavigationBar].
|
||||||
|
*/
|
||||||
|
@Suppress("CyclomaticComplexMethod")
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
private fun ScaffoldLayout(
|
||||||
|
fabPosition: FabPosition,
|
||||||
|
topBar: @Composable () -> Unit,
|
||||||
|
startBar: @Composable () -> Unit,
|
||||||
|
content: @Composable (PaddingValues) -> Unit,
|
||||||
|
snackbar: @Composable () -> Unit,
|
||||||
|
fab: @Composable () -> Unit,
|
||||||
|
contentWindowInsets: WindowInsets,
|
||||||
|
bottomBar: @Composable () -> Unit,
|
||||||
|
) {
|
||||||
|
SubcomposeLayout { constraints ->
|
||||||
|
val layoutWidth = constraints.maxWidth
|
||||||
|
val layoutHeight = constraints.maxHeight
|
||||||
|
|
||||||
|
val looseConstraints = constraints.copy(minWidth = 0, minHeight = 0)
|
||||||
|
|
||||||
|
val topBarConstraints = looseConstraints.copy(maxHeight = Constraints.Infinity)
|
||||||
|
|
||||||
|
layout(layoutWidth, layoutHeight) {
|
||||||
|
val leftInset = contentWindowInsets.getLeft(this@SubcomposeLayout, layoutDirection)
|
||||||
|
val rightInset = contentWindowInsets.getRight(this@SubcomposeLayout, layoutDirection)
|
||||||
|
val bottomInset = contentWindowInsets.getBottom(this@SubcomposeLayout)
|
||||||
|
|
||||||
|
val startBarPlaceables = subcompose(ScaffoldLayoutContent.StartBar, startBar).fastMap {
|
||||||
|
it.measure(looseConstraints)
|
||||||
|
}
|
||||||
|
val startBarWidth = startBarPlaceables.fastMaxBy { it.width }?.width ?: 0
|
||||||
|
|
||||||
|
val insetLayoutWidth = layoutWidth - leftInset - rightInset - startBarWidth
|
||||||
|
|
||||||
|
val topBarPlaceables = subcompose(ScaffoldLayoutContent.TopBar, topBar).fastMap {
|
||||||
|
it.measure(topBarConstraints)
|
||||||
|
}
|
||||||
|
|
||||||
|
val topBarHeight = topBarPlaceables.fastMaxBy { it.height }?.height ?: 0
|
||||||
|
|
||||||
|
val snackbarPlaceables = subcompose(ScaffoldLayoutContent.Snackbar, snackbar).fastMap {
|
||||||
|
it.measure(looseConstraints)
|
||||||
|
}
|
||||||
|
|
||||||
|
val snackbarHeight = snackbarPlaceables.fastMaxBy { it.height }?.height ?: 0
|
||||||
|
val snackbarWidth = snackbarPlaceables.fastMaxBy { it.width }?.width ?: 0
|
||||||
|
|
||||||
|
val snackbarLeft = if (snackbarPlaceables.isNotEmpty()) {
|
||||||
|
(insetLayoutWidth - snackbarWidth) / 2 + leftInset
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
|
||||||
|
val fabPlaceables =
|
||||||
|
subcompose(ScaffoldLayoutContent.Fab, fab).fastMap { measurable ->
|
||||||
|
measurable.measure(looseConstraints)
|
||||||
|
}
|
||||||
|
|
||||||
|
val fabWidth = fabPlaceables.fastMaxBy { it.width }?.width ?: 0
|
||||||
|
val fabHeight = fabPlaceables.fastMaxBy { it.height }?.height ?: 0
|
||||||
|
|
||||||
|
val fabPlacement = if (fabPlaceables.isNotEmpty() && fabWidth != 0 && fabHeight != 0) {
|
||||||
|
// FAB distance from the left of the layout, taking into account LTR / RTL
|
||||||
|
val fabLeftOffset = if (fabPosition == FabPosition.End) {
|
||||||
|
if (layoutDirection == LayoutDirection.Ltr) {
|
||||||
|
layoutWidth - FabSpacing.roundToPx() - fabWidth - rightInset
|
||||||
|
} else {
|
||||||
|
FabSpacing.roundToPx() + leftInset
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
leftInset + ((insetLayoutWidth - fabWidth) / 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
FabPlacement(
|
||||||
|
left = fabLeftOffset,
|
||||||
|
width = fabWidth,
|
||||||
|
height = fabHeight,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
val bottomBarPlaceables = subcompose(ScaffoldLayoutContent.BottomBar) {
|
||||||
|
bottomBar()
|
||||||
|
}.fastMap { it.measure(looseConstraints) }
|
||||||
|
|
||||||
|
val bottomBarHeight = bottomBarPlaceables
|
||||||
|
.fastMaxBy { it.height }
|
||||||
|
?.height
|
||||||
|
?.takeIf { it != 0 }
|
||||||
|
val fabOffsetFromBottom = fabPlacement?.let {
|
||||||
|
max(bottomBarHeight ?: 0, bottomInset) + it.height + FabSpacing.roundToPx()
|
||||||
|
}
|
||||||
|
|
||||||
|
val snackbarOffsetFromBottom = if (snackbarHeight != 0) {
|
||||||
|
snackbarHeight + (fabOffsetFromBottom ?: max(bottomBarHeight ?: 0, bottomInset))
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
|
||||||
|
val bodyContentPlaceables = subcompose(ScaffoldLayoutContent.MainContent) {
|
||||||
|
val insets = contentWindowInsets.asPaddingValues(this@SubcomposeLayout)
|
||||||
|
val fabOffsetDp = fabOffsetFromBottom?.toDp() ?: 0.dp
|
||||||
|
val bottomBarHeightPx = bottomBarHeight ?: 0
|
||||||
|
val innerPadding = PaddingValues(
|
||||||
|
top =
|
||||||
|
if (topBarPlaceables.isEmpty()) {
|
||||||
|
insets.calculateTopPadding()
|
||||||
|
} else {
|
||||||
|
topBarHeight.toDp()
|
||||||
|
},
|
||||||
|
bottom =
|
||||||
|
if (bottomBarPlaceables.isEmpty() || bottomBarHeightPx == 0) {
|
||||||
|
max(insets.calculateBottomPadding(), fabOffsetDp)
|
||||||
|
} else {
|
||||||
|
max(bottomBarHeightPx.toDp(), fabOffsetDp)
|
||||||
|
},
|
||||||
|
start = max(
|
||||||
|
insets.calculateStartPadding((this@SubcomposeLayout).layoutDirection),
|
||||||
|
startBarWidth.toDp(),
|
||||||
|
),
|
||||||
|
end = insets.calculateEndPadding((this@SubcomposeLayout).layoutDirection),
|
||||||
|
)
|
||||||
|
content(innerPadding)
|
||||||
|
}.fastMap { it.measure(looseConstraints) }
|
||||||
|
|
||||||
|
// Placing to control drawing order to match default elevation of each placeable
|
||||||
|
|
||||||
|
bodyContentPlaceables.fastForEach {
|
||||||
|
it.place(0, 0)
|
||||||
|
}
|
||||||
|
startBarPlaceables.fastForEach {
|
||||||
|
it.placeRelative(0, 0)
|
||||||
|
}
|
||||||
|
topBarPlaceables.fastForEach {
|
||||||
|
it.place(0, 0)
|
||||||
|
}
|
||||||
|
snackbarPlaceables.fastForEach {
|
||||||
|
it.place(
|
||||||
|
snackbarLeft,
|
||||||
|
layoutHeight - snackbarOffsetFromBottom,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// The bottom bar is always at the bottom of the layout
|
||||||
|
bottomBarPlaceables.fastForEach {
|
||||||
|
it.place(0, layoutHeight - (bottomBarHeight ?: 0))
|
||||||
|
}
|
||||||
|
// Explicitly not using placeRelative here as `leftOffset` already accounts for RTL
|
||||||
|
fabPlaceables.fastForEach {
|
||||||
|
it.place(fabPlacement?.left ?: 0, layoutHeight - (fabOffsetFromBottom ?: 0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The possible positions for a [FloatingActionButton] attached to a [Scaffold].
|
||||||
|
*/
|
||||||
|
@ExperimentalMaterial3Api
|
||||||
|
@JvmInline
|
||||||
|
value class FabPosition internal constructor(@Suppress("unused") private val value: Int) {
|
||||||
|
companion object {
|
||||||
|
/**
|
||||||
|
* Position FAB at the bottom of the screen in the center, above the [NavigationBar] (if it
|
||||||
|
* exists)
|
||||||
|
*/
|
||||||
|
val Center = FabPosition(0)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Position FAB at the bottom of the screen at the end, above the [NavigationBar] (if it
|
||||||
|
* exists)
|
||||||
|
*/
|
||||||
|
val End = FabPosition(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return when (this) {
|
||||||
|
Center -> "FabPosition.Center"
|
||||||
|
else -> "FabPosition.End"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Placement information for a [FloatingActionButton] inside a [Scaffold].
|
||||||
|
*
|
||||||
|
* @property left the FAB's offset from the left edge of the bottom bar, already adjusted for RTL
|
||||||
|
* support
|
||||||
|
* @property width the width of the FAB
|
||||||
|
* @property height the height of the FAB
|
||||||
|
*/
|
||||||
|
@Immutable
|
||||||
|
internal class FabPlacement(
|
||||||
|
val left: Int,
|
||||||
|
val width: Int,
|
||||||
|
val height: Int,
|
||||||
|
)
|
||||||
|
|
||||||
|
// FAB spacing above the bottom bar / bottom of the Scaffold
|
||||||
|
private val FabSpacing = 16.dp
|
||||||
|
|
||||||
|
private enum class ScaffoldLayoutContent { TopBar, MainContent, Snackbar, Fab, BottomBar, StartBar }
|
||||||
@@ -1,8 +1,13 @@
|
|||||||
package org.xtimms.tokusho.core.components
|
package org.xtimms.shirizu.core.components
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.RowScope
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
|
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||||
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.navigationBars
|
||||||
|
import androidx.compose.foundation.layout.only
|
||||||
import androidx.compose.foundation.layout.systemBars
|
import androidx.compose.foundation.layout.systemBars
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
@@ -17,8 +22,9 @@ import androidx.compose.ui.input.nestedscroll.nestedScroll
|
|||||||
fun ScaffoldWithTopAppBar(
|
fun ScaffoldWithTopAppBar(
|
||||||
title: String,
|
title: String,
|
||||||
navigateBack: () -> Unit,
|
navigateBack: () -> Unit,
|
||||||
floatingActionButton: @Composable (() -> Unit) = {},
|
snackbarHost: @Composable () -> Unit = {},
|
||||||
contentWindowInsets: WindowInsets = WindowInsets.systemBars,
|
floatingActionButton: @Composable () -> Unit = {},
|
||||||
|
actions: @Composable (RowScope.() -> Unit) = {},
|
||||||
content: @Composable (PaddingValues) -> Unit
|
content: @Composable (PaddingValues) -> Unit
|
||||||
) {
|
) {
|
||||||
val topAppBarScrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(
|
val topAppBarScrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(
|
||||||
@@ -28,24 +34,28 @@ fun ScaffoldWithTopAppBar(
|
|||||||
Scaffold(
|
Scaffold(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.nestedScroll(topAppBarScrollBehavior.nestedScrollConnection),
|
.nestedScroll(topAppBarScrollBehavior.nestedScrollConnection)
|
||||||
|
.consumeWindowInsets(WindowInsets.navigationBars.only(WindowInsetsSides.Horizontal)),
|
||||||
topBar = {
|
topBar = {
|
||||||
DefaultTopAppBar(
|
DefaultTopAppBar(
|
||||||
title = title,
|
title = title,
|
||||||
scrollBehavior = topAppBarScrollBehavior,
|
scrollBehavior = topAppBarScrollBehavior,
|
||||||
|
actions = actions,
|
||||||
navigateBack = navigateBack
|
navigateBack = navigateBack
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
snackbarHost = snackbarHost,
|
||||||
floatingActionButton = floatingActionButton,
|
floatingActionButton = floatingActionButton,
|
||||||
contentWindowInsets = contentWindowInsets,
|
contentWindowInsets = WindowInsets.navigationBars.only(WindowInsetsSides.Horizontal),
|
||||||
content = content
|
content = content
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun ScaffoldWithSmallTopAppBar(
|
fun ScaffoldWithSmallTopAppBarWithChips(
|
||||||
title: String,
|
title: String,
|
||||||
|
chips: List<String>,
|
||||||
navigateBack: () -> Unit,
|
navigateBack: () -> Unit,
|
||||||
floatingActionButton: @Composable (() -> Unit) = {},
|
floatingActionButton: @Composable (() -> Unit) = {},
|
||||||
contentWindowInsets: WindowInsets = WindowInsets.systemBars,
|
contentWindowInsets: WindowInsets = WindowInsets.systemBars,
|
||||||
@@ -60,9 +70,10 @@ fun ScaffoldWithSmallTopAppBar(
|
|||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.nestedScroll(topAppBarScrollBehavior.nestedScrollConnection),
|
.nestedScroll(topAppBarScrollBehavior.nestedScrollConnection),
|
||||||
topBar = {
|
topBar = {
|
||||||
SmallTopAppBar(
|
SmallTopAppBarWithChips(
|
||||||
title = title,
|
title = title,
|
||||||
scrollBehavior = topAppBarScrollBehavior,
|
scrollBehavior = topAppBarScrollBehavior,
|
||||||
|
chips = chips,
|
||||||
navigateBack = navigateBack
|
navigateBack = navigateBack
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -78,21 +89,16 @@ fun ScaffoldWithClassicTopAppBar(
|
|||||||
title: String,
|
title: String,
|
||||||
navigateBack: () -> Unit,
|
navigateBack: () -> Unit,
|
||||||
floatingActionButton: @Composable (() -> Unit) = {},
|
floatingActionButton: @Composable (() -> Unit) = {},
|
||||||
|
actions: @Composable (RowScope.() -> Unit) = {},
|
||||||
contentWindowInsets: WindowInsets = WindowInsets.systemBars,
|
contentWindowInsets: WindowInsets = WindowInsets.systemBars,
|
||||||
content: @Composable (PaddingValues) -> Unit
|
content: @Composable (PaddingValues) -> Unit
|
||||||
) {
|
) {
|
||||||
val topAppBarScrollBehavior = TopAppBarDefaults.exitUntilCollapsedScrollBehavior(
|
org.xtimms.shirizu.core.components.Scaffold(
|
||||||
rememberTopAppBarState(),
|
|
||||||
canScroll = { true }
|
|
||||||
)
|
|
||||||
Scaffold(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxSize()
|
|
||||||
.nestedScroll(topAppBarScrollBehavior.nestedScrollConnection),
|
|
||||||
topBar = {
|
topBar = {
|
||||||
ClassicTopAppBar(
|
ClassicTopAppBar(
|
||||||
title = title,
|
title = title,
|
||||||
scrollBehavior = topAppBarScrollBehavior,
|
scrollBehavior = it,
|
||||||
|
actions = actions,
|
||||||
navigateBack = navigateBack
|
navigateBack = navigateBack
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package org.xtimms.shirizu.core.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.StarOutline
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.TextUnit
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import org.xtimms.shirizu.R
|
||||||
|
import org.xtimms.shirizu.ui.theme.ShirizuTheme
|
||||||
|
import org.xtimms.shirizu.utils.lang.toStringPositiveValueOrUnknown
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SmallScoreIndicator(
|
||||||
|
score: Float?,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
fontSize: TextUnit = 16.sp,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = modifier,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.StarOutline,
|
||||||
|
contentDescription = stringResource(R.string.mean_score),
|
||||||
|
tint = MaterialTheme.colorScheme.outline
|
||||||
|
)
|
||||||
|
if (score != null) {
|
||||||
|
Text(
|
||||||
|
text = (score.times(5.0F)).toStringPositiveValueOrUnknown(),
|
||||||
|
modifier = Modifier.padding(horizontal = 4.dp),
|
||||||
|
color = MaterialTheme.colorScheme.outline,
|
||||||
|
fontSize = fontSize
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
fun SmallScoreIndicatorPreview() {
|
||||||
|
ShirizuTheme {
|
||||||
|
SmallScoreIndicator(score = 1f)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
package org.xtimms.shirizu.core.components
|
||||||
|
|
||||||
|
import androidx.compose.animation.animateContentSize
|
||||||
|
import androidx.compose.animation.core.Spring
|
||||||
|
import androidx.compose.animation.core.spring
|
||||||
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.rounded.ArrowBack
|
||||||
|
import androidx.compose.material.icons.outlined.Close
|
||||||
|
import androidx.compose.material.icons.outlined.Search
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.SearchBar
|
||||||
|
import androidx.compose.material3.SearchBarDefaults
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.saveable.rememberSaveable
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import org.xtimms.shirizu.R
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
fun EmbeddedSearchBar(
|
||||||
|
onQueryChange: (String) -> Unit,
|
||||||
|
isSearchActive: Boolean,
|
||||||
|
onActiveChanged: (Boolean) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onSearch: ((String) -> Unit)? = null,
|
||||||
|
) {
|
||||||
|
var searchQuery by rememberSaveable { mutableStateOf("") }
|
||||||
|
val activeChanged: (Boolean) -> Unit = { active ->
|
||||||
|
searchQuery = ""
|
||||||
|
onQueryChange("")
|
||||||
|
onActiveChanged(active)
|
||||||
|
}
|
||||||
|
SearchBar(
|
||||||
|
query = searchQuery,
|
||||||
|
onQueryChange = { query ->
|
||||||
|
searchQuery = query
|
||||||
|
onQueryChange(query)
|
||||||
|
},
|
||||||
|
onSearch = onSearch ?: { activeChanged(false) },
|
||||||
|
active = isSearchActive,
|
||||||
|
onActiveChange = activeChanged,
|
||||||
|
modifier = if (isSearchActive) {
|
||||||
|
modifier
|
||||||
|
.animateContentSize(spring(stiffness = Spring.StiffnessHigh))
|
||||||
|
} else {
|
||||||
|
modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.animateContentSize(spring(stiffness = Spring.StiffnessHigh))
|
||||||
|
},
|
||||||
|
placeholder = { Text("Search") },
|
||||||
|
leadingIcon = {
|
||||||
|
if (isSearchActive) {
|
||||||
|
IconButton(
|
||||||
|
onClick = { activeChanged(false) },
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.AutoMirrored.Rounded.ArrowBack,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.Search,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
trailingIcon = if (isSearchActive && searchQuery.isNotEmpty()) {
|
||||||
|
{
|
||||||
|
IconButton(
|
||||||
|
onClick = {
|
||||||
|
searchQuery = ""
|
||||||
|
onQueryChange("")
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.Close,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
},
|
||||||
|
colors = SearchBarDefaults.colors(
|
||||||
|
containerColor = if (isSearchActive) {
|
||||||
|
MaterialTheme.colorScheme.background
|
||||||
|
} else {
|
||||||
|
MaterialTheme.colorScheme.surfaceContainerLow
|
||||||
|
},
|
||||||
|
),
|
||||||
|
tonalElevation = 0.dp,
|
||||||
|
windowInsets = if (isSearchActive) {
|
||||||
|
SearchBarDefaults.windowInsets
|
||||||
|
} else {
|
||||||
|
WindowInsets(0.dp)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
// Search suggestions or results
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package org.xtimms.shirizu.core.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Clear
|
||||||
|
import androidx.compose.material.icons.filled.Search
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.OutlinedTextField
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||||
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalComposeUiApi::class)
|
||||||
|
@Composable
|
||||||
|
fun SearchTextField(
|
||||||
|
value: TextFieldValue,
|
||||||
|
onValueChange: (TextFieldValue) -> Unit,
|
||||||
|
hint: String,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
keyboardOptions: KeyboardOptions = KeyboardOptions.Default,
|
||||||
|
keyboardActions: KeyboardActions = KeyboardActions(),
|
||||||
|
onCleared: (() -> Unit) = { onValueChange(TextFieldValue()) },
|
||||||
|
) {
|
||||||
|
val keyboardController = LocalSoftwareKeyboardController.current
|
||||||
|
|
||||||
|
OutlinedTextField(
|
||||||
|
value = value,
|
||||||
|
onValueChange = onValueChange,
|
||||||
|
leadingIcon = {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Search,
|
||||||
|
contentDescription = null, // decorative
|
||||||
|
)
|
||||||
|
},
|
||||||
|
trailingIcon = {
|
||||||
|
IconButton(
|
||||||
|
onClick = {
|
||||||
|
onCleared()
|
||||||
|
// This is mostly for iOS, otherwise there is no way to dismiss the iOS
|
||||||
|
// keyboard once opened.
|
||||||
|
keyboardController?.hide()
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Clear,
|
||||||
|
contentDescription = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
placeholder = { Text(text = hint) },
|
||||||
|
keyboardOptions = keyboardOptions,
|
||||||
|
keyboardActions = keyboardActions,
|
||||||
|
maxLines = 1,
|
||||||
|
singleLine = true,
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,5 +1,10 @@
|
|||||||
package org.xtimms.tokusho.core.components
|
package org.xtimms.shirizu.core.components
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedContent
|
||||||
|
import androidx.compose.animation.SizeTransform
|
||||||
|
import androidx.compose.animation.fadeIn
|
||||||
|
import androidx.compose.animation.fadeOut
|
||||||
|
import androidx.compose.animation.togetherWith
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
@@ -15,16 +20,16 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import org.xtimms.tokusho.ui.theme.applyOpacity
|
import org.xtimms.shirizu.ui.theme.applyOpacity
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SettingTitle(text: String) {
|
fun SettingTitle(text: String) {
|
||||||
Text(
|
Text(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(top = 32.dp)
|
.padding(top = 32.dp)
|
||||||
.padding(horizontal = 20.dp, vertical = 16.dp),
|
.padding(horizontal = 16.dp, vertical = 16.dp),
|
||||||
text = text,
|
text = text,
|
||||||
style = MaterialTheme.typography.displaySmall
|
style = MaterialTheme.typography.headlineLarge
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,12 +65,24 @@ fun SettingItem(title: String, description: String, icon: ImageVector?, onClick:
|
|||||||
style = MaterialTheme.typography.titleLarge,
|
style = MaterialTheme.typography.titleLarge,
|
||||||
color = MaterialTheme.colorScheme.onSurface
|
color = MaterialTheme.colorScheme.onSurface
|
||||||
)
|
)
|
||||||
Text(
|
AnimatedContent(
|
||||||
text = description,
|
targetState = description,
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
transitionSpec = {
|
||||||
maxLines = 1,
|
if (targetState > initialState) {
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
(fadeIn()).togetherWith(fadeOut())
|
||||||
)
|
} else {
|
||||||
|
(fadeIn()).togetherWith(fadeOut())
|
||||||
|
}.using(SizeTransform(clip = false))
|
||||||
|
},
|
||||||
|
label = "Total used"
|
||||||
|
) { targetDescription ->
|
||||||
|
Text(
|
||||||
|
text = targetDescription,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
maxLines = 1,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
package org.xtimms.shirizu.core.components
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedContent
|
||||||
|
import androidx.compose.animation.animateContentSize
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.ArrowDropDown
|
||||||
|
import androidx.compose.material.icons.outlined.ArrowDropUp
|
||||||
|
import androidx.compose.material3.DropdownMenu
|
||||||
|
import androidx.compose.material3.FilterChip
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
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.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import org.xtimms.shirizu.sections.library.history.SortOption
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SortChip(
|
||||||
|
sortOptions: List<SortOption>,
|
||||||
|
currentSortOption: SortOption,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onSortSelected: (SortOption) -> Unit,
|
||||||
|
) {
|
||||||
|
Box(modifier) {
|
||||||
|
var expanded by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
val arrowDrop = if (expanded) Icons.Outlined.ArrowDropUp else Icons.Outlined.ArrowDropDown
|
||||||
|
|
||||||
|
FilterChip(
|
||||||
|
selected = true,
|
||||||
|
onClick = { expanded = true },
|
||||||
|
label = {
|
||||||
|
AnimatedContent(targetState = currentSortOption.label(LocalContext.current.resources), label = "Text") {
|
||||||
|
Text(text = it)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
leadingIcon = {
|
||||||
|
AnimatedContent(targetState = currentSortOption.icon(), label = "Icon") {
|
||||||
|
Icon(
|
||||||
|
imageVector = it,
|
||||||
|
contentDescription = null, // decorative
|
||||||
|
modifier = Modifier.size(16.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
trailingIcon = {
|
||||||
|
AnimatedContent(targetState = arrowDrop, label = "Arrow drop") {
|
||||||
|
Icon(
|
||||||
|
imageVector = it,
|
||||||
|
contentDescription = null, // decorative
|
||||||
|
modifier = Modifier.size(16.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
DropdownMenu(
|
||||||
|
expanded = expanded,
|
||||||
|
onDismissRequest = { expanded = false },
|
||||||
|
) {
|
||||||
|
SortDropdownMenuContent(
|
||||||
|
sortOptions = sortOptions,
|
||||||
|
currentSortOption = currentSortOption,
|
||||||
|
onItemClick = {
|
||||||
|
onSortSelected(it)
|
||||||
|
expanded = false
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package org.xtimms.shirizu.core.components
|
||||||
|
|
||||||
|
import android.content.res.Resources
|
||||||
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.ArrowOutward
|
||||||
|
import androidx.compose.material.icons.outlined.DateRange
|
||||||
|
import androidx.compose.material.icons.outlined.SortByAlpha
|
||||||
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import org.xtimms.shirizu.R
|
||||||
|
import org.xtimms.shirizu.sections.library.history.SortOption
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun ColumnScope.SortDropdownMenuContent(
|
||||||
|
sortOptions: List<SortOption>,
|
||||||
|
onItemClick: (SortOption) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
currentSortOption: SortOption? = null,
|
||||||
|
) {
|
||||||
|
val resources = LocalContext.current.resources
|
||||||
|
for (sort in sortOptions) {
|
||||||
|
DropdownMenuItem(
|
||||||
|
text = {
|
||||||
|
Text(
|
||||||
|
text = sort.label(resources),
|
||||||
|
fontWeight = if (sort == currentSortOption) FontWeight.Bold else null,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onClick = { onItemClick(sort) },
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun SortOption.label(resources: Resources): String = when (this) {
|
||||||
|
SortOption.ALPHABETICAL -> resources.getString(R.string.sort_alphabetically)
|
||||||
|
SortOption.PROGRESS -> resources.getString(R.string.progress)
|
||||||
|
SortOption.DATE_ADDED -> resources.getString(R.string.sort_date_added)
|
||||||
|
}
|
||||||
|
|
||||||
|
internal fun SortOption.icon(): ImageVector = when (this) {
|
||||||
|
SortOption.ALPHABETICAL -> Icons.Outlined.SortByAlpha
|
||||||
|
SortOption.PROGRESS -> Icons.Outlined.ArrowOutward
|
||||||
|
SortOption.DATE_ADDED -> Icons.Outlined.DateRange
|
||||||
|
}
|
||||||
@@ -1,13 +1,15 @@
|
|||||||
package org.xtimms.tokusho.core.components
|
package org.xtimms.shirizu.core.components
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.aspectRatio
|
import androidx.compose.foundation.layout.aspectRatio
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
@@ -15,15 +17,18 @@ import androidx.compose.ui.Alignment
|
|||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
|
import androidx.core.net.toUri
|
||||||
import coil.ImageLoader
|
import coil.ImageLoader
|
||||||
import org.xtimms.tokusho.core.AsyncImageImpl
|
import org.xtimms.shirizu.core.ShirizuAsyncImage
|
||||||
|
import org.xtimms.shirizu.ui.theme.ShirizuTheme
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SourceItem(
|
fun SourceItem(
|
||||||
coil: ImageLoader,
|
|
||||||
faviconUrl: Uri,
|
faviconUrl: Uri,
|
||||||
title: String,
|
title: String,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
@@ -32,22 +37,27 @@ fun SourceItem(
|
|||||||
) {
|
) {
|
||||||
Column(
|
Column(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.width(96.dp)
|
.width(88.dp)
|
||||||
.clip(RoundedCornerShape(8.dp))
|
.clip(RoundedCornerShape(8.dp))
|
||||||
.clickable(onClick = onClick)
|
.clickable(onClick = onClick)
|
||||||
.padding(start = 8.dp, end = 8.dp),
|
.padding(start = 8.dp, end = 8.dp),
|
||||||
horizontalAlignment = Alignment.Start
|
horizontalAlignment = Alignment.Start
|
||||||
) {
|
) {
|
||||||
AsyncImageImpl(
|
Card(
|
||||||
coil = coil,
|
modifier = Modifier
|
||||||
model = faviconUrl,
|
.size(88.dp)
|
||||||
contentDescription = "favicon",
|
.clip(MaterialTheme.shapes.large)
|
||||||
contentScale = ContentScale.Crop,
|
|
||||||
modifier = modifier
|
|
||||||
.size(96.dp)
|
|
||||||
.clip(RoundedCornerShape(8.dp))
|
|
||||||
.aspectRatio(1f)
|
.aspectRatio(1f)
|
||||||
)
|
) {
|
||||||
|
ShirizuAsyncImage(
|
||||||
|
model = faviconUrl,
|
||||||
|
contentDescription = "favicon",
|
||||||
|
contentScale = ContentScale.Crop,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.clip(MaterialTheme.shapes.large)
|
||||||
|
)
|
||||||
|
}
|
||||||
Text(
|
Text(
|
||||||
text = title,
|
text = title,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -60,4 +70,16 @@ fun SourceItem(
|
|||||||
maxLines = maxLines
|
maxLines = maxLines
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun SourceItemPreview() {
|
||||||
|
ShirizuTheme {
|
||||||
|
SourceItem(
|
||||||
|
faviconUrl = "".toUri(),
|
||||||
|
title = "Test",
|
||||||
|
onClick = { }
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package org.xtimms.tokusho.core.components
|
package org.xtimms.shirizu.core.components
|
||||||
|
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
@@ -0,0 +1,594 @@
|
|||||||
|
package org.xtimms.shirizu.core.components
|
||||||
|
|
||||||
|
import android.graphics.Path
|
||||||
|
import android.view.animation.PathInterpolator
|
||||||
|
import androidx.compose.foundation.basicMarquee
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.RowScope
|
||||||
|
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.lazy.LazyRow
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.text.BasicTextField
|
||||||
|
import androidx.compose.foundation.text.KeyboardActions
|
||||||
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
|
import androidx.compose.material.ExperimentalMaterialApi
|
||||||
|
import androidx.compose.material.TextFieldDefaults
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.outlined.ArrowBack
|
||||||
|
import androidx.compose.material.icons.outlined.Close
|
||||||
|
import androidx.compose.material.icons.outlined.MoreVert
|
||||||
|
import androidx.compose.material.icons.outlined.QueryStats
|
||||||
|
import androidx.compose.material.icons.outlined.RssFeed
|
||||||
|
import androidx.compose.material.icons.outlined.Search
|
||||||
|
import androidx.compose.material.icons.outlined.SentimentSatisfiedAlt
|
||||||
|
import androidx.compose.material.icons.outlined.Settings
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.CardDefaults
|
||||||
|
import androidx.compose.material3.DropdownMenu
|
||||||
|
import androidx.compose.material3.DropdownMenuItem
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.LargeTopAppBar
|
||||||
|
import androidx.compose.material3.LocalContentColor
|
||||||
|
import androidx.compose.material3.LocalMinimumInteractiveComponentEnforcement
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.MediumTopAppBar
|
||||||
|
import androidx.compose.material3.PlainTooltip
|
||||||
|
import androidx.compose.material3.SuggestionChip
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TooltipBox
|
||||||
|
import androidx.compose.material3.TooltipDefaults
|
||||||
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
|
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||||
|
import androidx.compose.material3.rememberTooltipState
|
||||||
|
import androidx.compose.material3.surfaceColorAtElevation
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
import androidx.compose.runtime.derivedStateOf
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.key
|
||||||
|
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.focus.FocusRequester
|
||||||
|
import androidx.compose.ui.focus.focusRequester
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.SolidColor
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.platform.LocalFocusManager
|
||||||
|
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.input.ImeAction
|
||||||
|
import androidx.compose.ui.text.input.VisualTransformation
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import org.xtimms.shirizu.R
|
||||||
|
import org.xtimms.shirizu.core.components.icons.Shirizu
|
||||||
|
import org.xtimms.shirizu.core.prefs.AppSettings
|
||||||
|
import org.xtimms.shirizu.ui.theme.ShirizuTheme
|
||||||
|
import org.xtimms.shirizu.utils.composable.secondaryItemAlpha
|
||||||
|
import java.time.LocalDate
|
||||||
|
import java.time.format.DateTimeFormatter
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun AppBar(
|
||||||
|
title: String?,
|
||||||
|
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
backgroundColor: Color? = null,
|
||||||
|
// Text
|
||||||
|
subtitle: String? = null,
|
||||||
|
// Up button
|
||||||
|
navigateUp: (() -> Unit)? = null,
|
||||||
|
navigationIcon: ImageVector? = null,
|
||||||
|
// Menu
|
||||||
|
actions: @Composable RowScope.() -> Unit = {},
|
||||||
|
// Action mode
|
||||||
|
actionModeCounter: Int = 0,
|
||||||
|
onCancelActionMode: () -> Unit = {},
|
||||||
|
actionModeActions: @Composable RowScope.() -> Unit = {},
|
||||||
|
|
||||||
|
scrollBehavior: TopAppBarScrollBehavior? = null,
|
||||||
|
) {
|
||||||
|
val isActionMode by remember(actionModeCounter) {
|
||||||
|
derivedStateOf { actionModeCounter > 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
AppBar(
|
||||||
|
modifier = modifier,
|
||||||
|
backgroundColor = backgroundColor,
|
||||||
|
titleContent = {
|
||||||
|
if (isActionMode) {
|
||||||
|
AppBarTitle(actionModeCounter.toString())
|
||||||
|
} else {
|
||||||
|
AppBarTitle(title, subtitle = subtitle)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
navigateUp = navigateUp,
|
||||||
|
navigationIcon = navigationIcon,
|
||||||
|
actions = {
|
||||||
|
if (isActionMode) {
|
||||||
|
actionModeActions()
|
||||||
|
} else {
|
||||||
|
actions()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
isActionMode = isActionMode,
|
||||||
|
onCancelActionMode = onCancelActionMode,
|
||||||
|
scrollBehavior = scrollBehavior,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun AppBar(
|
||||||
|
// Title
|
||||||
|
titleContent: @Composable () -> Unit,
|
||||||
|
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
backgroundColor: Color? = null,
|
||||||
|
// Up button
|
||||||
|
navigateUp: (() -> Unit)? = null,
|
||||||
|
navigationIcon: ImageVector? = null,
|
||||||
|
// Menu
|
||||||
|
actions: @Composable RowScope.() -> Unit = {},
|
||||||
|
// Action mode
|
||||||
|
isActionMode: Boolean = false,
|
||||||
|
onCancelActionMode: () -> Unit = {},
|
||||||
|
|
||||||
|
scrollBehavior: TopAppBarScrollBehavior? = null,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = modifier,
|
||||||
|
) {
|
||||||
|
androidx.compose.material3.TopAppBar(
|
||||||
|
navigationIcon = {
|
||||||
|
if (isActionMode) {
|
||||||
|
IconButton(onClick = onCancelActionMode) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.Close,
|
||||||
|
contentDescription = stringResource(R.string.cancel),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
navigateUp?.let {
|
||||||
|
IconButton(onClick = it) {
|
||||||
|
UpIcon(navigationIcon = navigationIcon)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
title = titleContent,
|
||||||
|
actions = actions,
|
||||||
|
colors = TopAppBarDefaults.topAppBarColors(
|
||||||
|
containerColor = backgroundColor
|
||||||
|
?: MaterialTheme.colorScheme.surfaceColorAtElevation(
|
||||||
|
elevation = if (isActionMode) 3.dp else 0.dp,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
scrollBehavior = scrollBehavior,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun AppToolbar(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
actions: @Composable RowScope.() -> Unit = {},
|
||||||
|
scrollBehavior: TopAppBarScrollBehavior? = null,
|
||||||
|
) {
|
||||||
|
AppBar(
|
||||||
|
modifier = modifier,
|
||||||
|
titleContent = {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
modifier = Modifier.size(22.dp),
|
||||||
|
imageVector = Icons.Filled.Shirizu,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = stringResource(id = R.string.app_name),
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
style = MaterialTheme.typography.titleLarge,
|
||||||
|
fontWeight = FontWeight(900)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
actions = { key("actions") { actions() } },
|
||||||
|
isActionMode = false,
|
||||||
|
scrollBehavior = scrollBehavior,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun AppBarActions(
|
||||||
|
actions: ImmutableList<AppBar.AppBarAction>,
|
||||||
|
) {
|
||||||
|
var showMenu by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
actions.filterIsInstance<AppBar.Action>().map {
|
||||||
|
TooltipBox(
|
||||||
|
positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
|
||||||
|
tooltip = {
|
||||||
|
PlainTooltip {
|
||||||
|
Text(it.title)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
state = rememberTooltipState(),
|
||||||
|
) {
|
||||||
|
IconButton(
|
||||||
|
onClick = it.onClick,
|
||||||
|
enabled = it.enabled,
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = it.icon,
|
||||||
|
tint = it.iconTint ?: LocalContentColor.current,
|
||||||
|
contentDescription = it.title,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val overflowActions = actions.filterIsInstance<AppBar.OverflowAction>()
|
||||||
|
if (overflowActions.isNotEmpty()) {
|
||||||
|
TooltipBox(
|
||||||
|
positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
|
||||||
|
tooltip = {
|
||||||
|
PlainTooltip {
|
||||||
|
Text(stringResource(R.string.action_menu_overflow_description))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
state = rememberTooltipState(),
|
||||||
|
) {
|
||||||
|
IconButton(
|
||||||
|
onClick = { showMenu = !showMenu },
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Outlined.MoreVert,
|
||||||
|
contentDescription = stringResource(R.string.action_menu_overflow_description),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DropdownMenu(
|
||||||
|
expanded = showMenu,
|
||||||
|
onDismissRequest = { showMenu = false },
|
||||||
|
) {
|
||||||
|
overflowActions.map {
|
||||||
|
DropdownMenuItem(
|
||||||
|
onClick = {
|
||||||
|
it.onClick()
|
||||||
|
showMenu = false
|
||||||
|
},
|
||||||
|
text = { Text(it.title, fontWeight = FontWeight.Normal) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun DefaultTopAppBar(
|
||||||
|
title: String,
|
||||||
|
actions: @Composable (RowScope.() -> Unit),
|
||||||
|
scrollBehavior: TopAppBarScrollBehavior? = null,
|
||||||
|
navigateBack: () -> Unit,
|
||||||
|
) {
|
||||||
|
LargeTopAppBar(
|
||||||
|
title = { Text(text = title) },
|
||||||
|
navigationIcon = {
|
||||||
|
BackIconButton(onClick = navigateBack)
|
||||||
|
},
|
||||||
|
actions = actions,
|
||||||
|
scrollBehavior = scrollBehavior
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun SmallTopAppBarWithChips(
|
||||||
|
title: String,
|
||||||
|
scrollBehavior: TopAppBarScrollBehavior? = null,
|
||||||
|
navigateBack: () -> Unit,
|
||||||
|
chips: List<String>
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
|
MediumTopAppBar(
|
||||||
|
title = { Text(text = title) },
|
||||||
|
navigationIcon = {
|
||||||
|
BackIconButton(onClick = navigateBack)
|
||||||
|
},
|
||||||
|
scrollBehavior = scrollBehavior
|
||||||
|
)
|
||||||
|
LazyRow(
|
||||||
|
modifier = Modifier.padding(horizontal = 16.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
) {
|
||||||
|
items(items = chips) {
|
||||||
|
CompositionLocalProvider(LocalMinimumInteractiveComponentEnforcement provides false) {
|
||||||
|
SuggestionChip(
|
||||||
|
modifier = Modifier.padding(vertical = 4.dp),
|
||||||
|
onClick = { },
|
||||||
|
label = { Text(text = it) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val path = Path().apply {
|
||||||
|
moveTo(0f, 0f)
|
||||||
|
lineTo(0.7f, 0.1f)
|
||||||
|
cubicTo(0.7f, 0.1f, .95F, .5F, 1F, 1F)
|
||||||
|
moveTo(1f, 1f)
|
||||||
|
}
|
||||||
|
|
||||||
|
val fraction: (Float) -> Float = { PathInterpolator(path).getInterpolation(it) }
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun SmallTopAppBar(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
titleText: String = "",
|
||||||
|
scrollBehavior: TopAppBarScrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(),
|
||||||
|
title: @Composable () -> Unit = {
|
||||||
|
Text(
|
||||||
|
text = titleText,
|
||||||
|
color = MaterialTheme.colorScheme.onSurface.copy(alpha = fraction(scrollBehavior.state.overlappedFraction)),
|
||||||
|
maxLines = 1
|
||||||
|
)
|
||||||
|
},
|
||||||
|
navigationIcon: @Composable () -> Unit = {},
|
||||||
|
actions: @Composable RowScope.() -> Unit = {},
|
||||||
|
) {
|
||||||
|
androidx.compose.material3.TopAppBar(
|
||||||
|
modifier = modifier,
|
||||||
|
title = title,
|
||||||
|
navigationIcon = navigationIcon,
|
||||||
|
actions = actions,
|
||||||
|
scrollBehavior = scrollBehavior
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun ClassicTopAppBar(
|
||||||
|
title: String,
|
||||||
|
scrollBehavior: TopAppBarScrollBehavior? = null,
|
||||||
|
actions: @Composable (RowScope.() -> Unit),
|
||||||
|
navigateBack: () -> Unit,
|
||||||
|
) {
|
||||||
|
androidx.compose.material3.TopAppBar(
|
||||||
|
title = { Text(text = title) },
|
||||||
|
navigationIcon = {
|
||||||
|
BackIconButton(onClick = navigateBack)
|
||||||
|
},
|
||||||
|
actions = actions,
|
||||||
|
scrollBehavior = scrollBehavior
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AppBarTitle(
|
||||||
|
title: String?,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
subtitle: String? = null,
|
||||||
|
) {
|
||||||
|
Column(modifier = modifier) {
|
||||||
|
title?.let {
|
||||||
|
Text(
|
||||||
|
text = it,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
subtitle?.let {
|
||||||
|
Text(
|
||||||
|
text = it,
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
modifier = Modifier.basicMarquee(
|
||||||
|
delayMillis = 2_000,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class, ExperimentalMaterialApi::class)
|
||||||
|
@Composable
|
||||||
|
fun SearchToolbar(
|
||||||
|
searchQuery: String?,
|
||||||
|
onChangeSearchQuery: (String?) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
titleContent: @Composable () -> Unit = {},
|
||||||
|
navigateUp: (() -> Unit)? = null,
|
||||||
|
searchEnabled: Boolean = true,
|
||||||
|
placeholderText: String? = null,
|
||||||
|
onSearch: (String) -> Unit = {},
|
||||||
|
onClickCloseSearch: () -> Unit = { onChangeSearchQuery(null) },
|
||||||
|
actions: @Composable RowScope.() -> Unit = {},
|
||||||
|
scrollBehavior: TopAppBarScrollBehavior? = null,
|
||||||
|
visualTransformation: VisualTransformation = VisualTransformation.None,
|
||||||
|
interactionSource: MutableInteractionSource = remember { MutableInteractionSource() },
|
||||||
|
) {
|
||||||
|
val focusRequester = remember { FocusRequester() }
|
||||||
|
|
||||||
|
AppBar(
|
||||||
|
modifier = modifier,
|
||||||
|
titleContent = {
|
||||||
|
if (searchQuery == null) return@AppBar titleContent()
|
||||||
|
|
||||||
|
val keyboardController = LocalSoftwareKeyboardController.current
|
||||||
|
val focusManager = LocalFocusManager.current
|
||||||
|
|
||||||
|
val searchAndClearFocus: () -> Unit = f@{
|
||||||
|
if (searchQuery.isBlank()) return@f
|
||||||
|
onSearch(searchQuery)
|
||||||
|
focusManager.clearFocus()
|
||||||
|
keyboardController?.hide()
|
||||||
|
}
|
||||||
|
|
||||||
|
BasicTextField(
|
||||||
|
value = searchQuery,
|
||||||
|
onValueChange = onChangeSearchQuery,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.focusRequester(focusRequester),
|
||||||
|
textStyle = MaterialTheme.typography.titleMedium.copy(
|
||||||
|
color = MaterialTheme.colorScheme.onBackground,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
fontSize = 18.sp,
|
||||||
|
),
|
||||||
|
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
|
||||||
|
keyboardActions = KeyboardActions(onSearch = { searchAndClearFocus() }),
|
||||||
|
singleLine = true,
|
||||||
|
cursorBrush = SolidColor(MaterialTheme.colorScheme.onBackground),
|
||||||
|
visualTransformation = visualTransformation,
|
||||||
|
interactionSource = interactionSource,
|
||||||
|
decorationBox = { innerTextField ->
|
||||||
|
TextFieldDefaults.TextFieldDecorationBox(
|
||||||
|
value = searchQuery,
|
||||||
|
innerTextField = innerTextField,
|
||||||
|
enabled = true,
|
||||||
|
singleLine = true,
|
||||||
|
visualTransformation = visualTransformation,
|
||||||
|
interactionSource = interactionSource,
|
||||||
|
placeholder = {
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.secondaryItemAlpha(),
|
||||||
|
text = (placeholderText ?: stringResource(R.string.search)),
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
style = MaterialTheme.typography.titleMedium.copy(
|
||||||
|
fontSize = 18.sp,
|
||||||
|
fontWeight = FontWeight.Normal,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
},
|
||||||
|
navigateUp = if (searchQuery == null) navigateUp else onClickCloseSearch,
|
||||||
|
actions = {
|
||||||
|
key("search") {
|
||||||
|
val onClick = { onChangeSearchQuery("") }
|
||||||
|
|
||||||
|
if (!searchEnabled) {
|
||||||
|
// Don't show search action
|
||||||
|
} else if (searchQuery == null) {
|
||||||
|
TooltipBox(
|
||||||
|
positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
|
||||||
|
tooltip = {
|
||||||
|
PlainTooltip {
|
||||||
|
Text(stringResource(R.string.search))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
state = rememberTooltipState(),
|
||||||
|
) {
|
||||||
|
IconButton(
|
||||||
|
onClick = onClick,
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Outlined.Search,
|
||||||
|
contentDescription = stringResource(R.string.search),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (searchQuery.isNotEmpty()) {
|
||||||
|
TooltipBox(
|
||||||
|
positionProvider = TooltipDefaults.rememberPlainTooltipPositionProvider(),
|
||||||
|
tooltip = {
|
||||||
|
PlainTooltip {
|
||||||
|
Text(stringResource(R.string.action_reset))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
state = rememberTooltipState(),
|
||||||
|
) {
|
||||||
|
IconButton(
|
||||||
|
onClick = {
|
||||||
|
onClick()
|
||||||
|
focusRequester.requestFocus()
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Outlined.Close,
|
||||||
|
contentDescription = stringResource(R.string.action_reset),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
key("actions") { actions() }
|
||||||
|
},
|
||||||
|
isActionMode = false,
|
||||||
|
scrollBehavior = scrollBehavior,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun UpIcon(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
navigationIcon: ImageVector? = null,
|
||||||
|
) {
|
||||||
|
val icon = navigationIcon
|
||||||
|
?: Icons.AutoMirrored.Outlined.ArrowBack
|
||||||
|
Icon(
|
||||||
|
imageVector = icon,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed interface AppBar {
|
||||||
|
sealed interface AppBarAction
|
||||||
|
|
||||||
|
data class Action(
|
||||||
|
val title: String,
|
||||||
|
val icon: ImageVector,
|
||||||
|
val iconTint: Color? = null,
|
||||||
|
val onClick: () -> Unit,
|
||||||
|
val enabled: Boolean = true,
|
||||||
|
) : AppBarAction
|
||||||
|
|
||||||
|
data class OverflowAction(
|
||||||
|
val title: String,
|
||||||
|
val onClick: () -> Unit,
|
||||||
|
) : AppBarAction
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun SearchToolbarPreview() {
|
||||||
|
ShirizuTheme {
|
||||||
|
AppToolbar()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,451 @@
|
|||||||
|
package org.xtimms.shirizu.core.components
|
||||||
|
|
||||||
|
import android.view.ViewConfiguration
|
||||||
|
import androidx.compose.animation.core.Animatable
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.gestures.Orientation
|
||||||
|
import androidx.compose.foundation.gestures.draggable
|
||||||
|
import androidx.compose.foundation.gestures.rememberDraggableState
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.interaction.collectIsDraggedAsState
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.calculateEndPadding
|
||||||
|
import androidx.compose.foundation.layout.calculateStartPadding
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.offset
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.lazy.LazyListItemInfo
|
||||||
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
|
import androidx.compose.foundation.lazy.grid.GridCells
|
||||||
|
import androidx.compose.foundation.lazy.grid.LazyGridState
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.foundation.systemGestureExclusion
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableFloatStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.alpha
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.layout.SubcomposeLayout
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
|
import androidx.compose.ui.unit.Constraints
|
||||||
|
import androidx.compose.ui.unit.Density
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.IntOffset
|
||||||
|
import androidx.compose.ui.unit.LayoutDirection
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.util.fastFirstOrNull
|
||||||
|
import androidx.compose.ui.util.fastForEach
|
||||||
|
import androidx.compose.ui.util.fastMaxBy
|
||||||
|
import kotlinx.coroutines.FlowPreview
|
||||||
|
import kotlinx.coroutines.channels.BufferOverflow
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
import kotlinx.coroutines.flow.sample
|
||||||
|
import org.xtimms.shirizu.core.components.Scroller.STICKY_HEADER_KEY_PREFIX
|
||||||
|
import kotlin.math.abs
|
||||||
|
import kotlin.math.max
|
||||||
|
import kotlin.math.min
|
||||||
|
import kotlin.math.roundToInt
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws vertical fast scroller to a lazy list
|
||||||
|
*
|
||||||
|
* Set key with [STICKY_HEADER_KEY_PREFIX] prefix to any sticky header item in the list.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun VerticalFastScroller(
|
||||||
|
listState: LazyListState,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
thumbAllowed: () -> Boolean = { true },
|
||||||
|
thumbColor: Color = MaterialTheme.colorScheme.primary,
|
||||||
|
topContentPadding: Dp = Dp.Hairline,
|
||||||
|
bottomContentPadding: Dp = Dp.Hairline,
|
||||||
|
endContentPadding: Dp = Dp.Hairline,
|
||||||
|
content: @Composable () -> Unit,
|
||||||
|
) {
|
||||||
|
SubcomposeLayout(modifier = modifier) { constraints ->
|
||||||
|
val contentPlaceable = subcompose("content", content).map { it.measure(constraints) }
|
||||||
|
val contentHeight = contentPlaceable.fastMaxBy { it.height }?.height ?: 0
|
||||||
|
val contentWidth = contentPlaceable.fastMaxBy { it.width }?.width ?: 0
|
||||||
|
|
||||||
|
val scrollerConstraints = constraints.copy(minWidth = 0, minHeight = 0)
|
||||||
|
val scrollerPlaceable = subcompose("scroller") {
|
||||||
|
val layoutInfo = listState.layoutInfo
|
||||||
|
val showScroller = layoutInfo.visibleItemsInfo.size < layoutInfo.totalItemsCount
|
||||||
|
if (!showScroller) return@subcompose
|
||||||
|
|
||||||
|
val thumbTopPadding = with(LocalDensity.current) { topContentPadding.toPx() }
|
||||||
|
var thumbOffsetY by remember(thumbTopPadding) { mutableFloatStateOf(thumbTopPadding) }
|
||||||
|
|
||||||
|
val dragInteractionSource = remember { MutableInteractionSource() }
|
||||||
|
val isThumbDragged by dragInteractionSource.collectIsDraggedAsState()
|
||||||
|
val scrolled = remember {
|
||||||
|
MutableSharedFlow<Unit>(
|
||||||
|
extraBufferCapacity = 1,
|
||||||
|
onBufferOverflow = BufferOverflow.DROP_OLDEST,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val thumbBottomPadding = with(LocalDensity.current) { bottomContentPadding.toPx() }
|
||||||
|
val heightPx = contentHeight.toFloat() -
|
||||||
|
thumbTopPadding -
|
||||||
|
thumbBottomPadding -
|
||||||
|
listState.layoutInfo.afterContentPadding
|
||||||
|
val thumbHeightPx = with(LocalDensity.current) { ThumbLength.toPx() }
|
||||||
|
val trackHeightPx = heightPx - thumbHeightPx
|
||||||
|
|
||||||
|
// When thumb dragged
|
||||||
|
LaunchedEffect(thumbOffsetY) {
|
||||||
|
if (layoutInfo.totalItemsCount == 0 || !isThumbDragged) return@LaunchedEffect
|
||||||
|
val scrollRatio = (thumbOffsetY - thumbTopPadding) / trackHeightPx
|
||||||
|
val scrollItem = layoutInfo.totalItemsCount * scrollRatio
|
||||||
|
val scrollItemRounded = scrollItem.roundToInt()
|
||||||
|
val scrollItemSize = layoutInfo.visibleItemsInfo.find { it.index == scrollItemRounded }?.size ?: 0
|
||||||
|
val scrollItemOffset = scrollItemSize * (scrollItem - scrollItemRounded)
|
||||||
|
listState.scrollToItem(index = scrollItemRounded, scrollOffset = scrollItemOffset.roundToInt())
|
||||||
|
scrolled.tryEmit(Unit)
|
||||||
|
}
|
||||||
|
|
||||||
|
// When list scrolled
|
||||||
|
LaunchedEffect(listState.firstVisibleItemScrollOffset) {
|
||||||
|
if (listState.layoutInfo.totalItemsCount == 0 || isThumbDragged) return@LaunchedEffect
|
||||||
|
val scrollOffset = computeScrollOffset(state = listState)
|
||||||
|
val scrollRange = computeScrollRange(state = listState)
|
||||||
|
val proportion = scrollOffset.toFloat() / (scrollRange.toFloat() - heightPx)
|
||||||
|
thumbOffsetY = trackHeightPx * proportion + thumbTopPadding
|
||||||
|
scrolled.tryEmit(Unit)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Thumb alpha
|
||||||
|
val alpha = remember { Animatable(0f) }
|
||||||
|
val isThumbVisible = alpha.value > 0f
|
||||||
|
LaunchedEffect(scrolled, alpha) {
|
||||||
|
scrolled
|
||||||
|
.sample(100)
|
||||||
|
.collectLatest {
|
||||||
|
if (thumbAllowed()) {
|
||||||
|
alpha.snapTo(1f)
|
||||||
|
alpha.animateTo(0f, animationSpec = FadeOutAnimationSpec)
|
||||||
|
} else {
|
||||||
|
alpha.animateTo(0f, animationSpec = ImmediateFadeOutAnimationSpec)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.offset { IntOffset(0, thumbOffsetY.roundToInt()) }
|
||||||
|
.then(
|
||||||
|
// Recompose opts
|
||||||
|
if (isThumbVisible && !listState.isScrollInProgress) {
|
||||||
|
Modifier.draggable(
|
||||||
|
interactionSource = dragInteractionSource,
|
||||||
|
orientation = Orientation.Vertical,
|
||||||
|
state = rememberDraggableState { delta ->
|
||||||
|
val newOffsetY = thumbOffsetY + delta
|
||||||
|
thumbOffsetY = newOffsetY.coerceIn(
|
||||||
|
thumbTopPadding,
|
||||||
|
thumbTopPadding + trackHeightPx,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Modifier
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.then(
|
||||||
|
// Exclude thumb from gesture area only when needed
|
||||||
|
if (isThumbVisible && !isThumbDragged && !listState.isScrollInProgress) {
|
||||||
|
Modifier.systemGestureExclusion()
|
||||||
|
} else {
|
||||||
|
Modifier
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.height(ThumbLength)
|
||||||
|
.padding(horizontal = 8.dp)
|
||||||
|
.padding(end = endContentPadding)
|
||||||
|
.width(ThumbThickness)
|
||||||
|
.alpha(alpha.value)
|
||||||
|
.background(color = thumbColor, shape = ThumbShape),
|
||||||
|
)
|
||||||
|
}.map { it.measure(scrollerConstraints) }
|
||||||
|
val scrollerWidth = scrollerPlaceable.fastMaxBy { it.width }?.width ?: 0
|
||||||
|
|
||||||
|
layout(contentWidth, contentHeight) {
|
||||||
|
contentPlaceable.fastForEach {
|
||||||
|
it.place(0, 0)
|
||||||
|
}
|
||||||
|
scrollerPlaceable.fastForEach {
|
||||||
|
it.placeRelative(contentWidth - scrollerWidth, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun rememberColumnWidthSums(
|
||||||
|
columns: GridCells,
|
||||||
|
horizontalArrangement: Arrangement.Horizontal,
|
||||||
|
contentPadding: PaddingValues,
|
||||||
|
) = remember<Density.(Constraints) -> List<Int>>(
|
||||||
|
columns,
|
||||||
|
horizontalArrangement,
|
||||||
|
contentPadding,
|
||||||
|
) {
|
||||||
|
{
|
||||||
|
constraints ->
|
||||||
|
require(constraints.maxWidth != Constraints.Infinity) {
|
||||||
|
"LazyVerticalGrid's width should be bound by parent"
|
||||||
|
}
|
||||||
|
val horizontalPadding = contentPadding.calculateStartPadding(LayoutDirection.Ltr) +
|
||||||
|
contentPadding.calculateEndPadding(LayoutDirection.Ltr)
|
||||||
|
val gridWidth = constraints.maxWidth - horizontalPadding.roundToPx()
|
||||||
|
with(columns) {
|
||||||
|
calculateCrossAxisCellSizes(
|
||||||
|
gridWidth,
|
||||||
|
horizontalArrangement.spacing.roundToPx(),
|
||||||
|
).toMutableList().apply {
|
||||||
|
for (i in 1..<size) {
|
||||||
|
this[i] += this[i - 1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(FlowPreview::class)
|
||||||
|
@Composable
|
||||||
|
fun VerticalGridFastScroller(
|
||||||
|
state: LazyGridState,
|
||||||
|
columns: GridCells,
|
||||||
|
arrangement: Arrangement.Horizontal,
|
||||||
|
contentPadding: PaddingValues,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
thumbAllowed: () -> Boolean = { true },
|
||||||
|
thumbColor: Color = MaterialTheme.colorScheme.primary,
|
||||||
|
topContentPadding: Dp = Dp.Hairline,
|
||||||
|
bottomContentPadding: Dp = Dp.Hairline,
|
||||||
|
endContentPadding: Dp = Dp.Hairline,
|
||||||
|
content: @Composable () -> Unit,
|
||||||
|
) {
|
||||||
|
val slotSizesSums = rememberColumnWidthSums(
|
||||||
|
columns = columns,
|
||||||
|
horizontalArrangement = arrangement,
|
||||||
|
contentPadding = contentPadding,
|
||||||
|
)
|
||||||
|
|
||||||
|
SubcomposeLayout(modifier = modifier) { constraints ->
|
||||||
|
val contentPlaceable = subcompose("content", content).map { it.measure(constraints) }
|
||||||
|
val contentHeight = contentPlaceable.fastMaxBy { it.height }?.height ?: 0
|
||||||
|
val contentWidth = contentPlaceable.fastMaxBy { it.width }?.width ?: 0
|
||||||
|
|
||||||
|
val scrollerConstraints = constraints.copy(minWidth = 0, minHeight = 0)
|
||||||
|
val scrollerPlaceable = subcompose("scroller") {
|
||||||
|
val layoutInfo = state.layoutInfo
|
||||||
|
val showScroller = layoutInfo.visibleItemsInfo.size < layoutInfo.totalItemsCount
|
||||||
|
if (!showScroller) return@subcompose
|
||||||
|
val thumbTopPadding = with(LocalDensity.current) { topContentPadding.toPx() }
|
||||||
|
var thumbOffsetY by remember(thumbTopPadding) { mutableFloatStateOf(thumbTopPadding) }
|
||||||
|
|
||||||
|
val dragInteractionSource = remember { MutableInteractionSource() }
|
||||||
|
val isThumbDragged by dragInteractionSource.collectIsDraggedAsState()
|
||||||
|
val scrolled = remember {
|
||||||
|
MutableSharedFlow<Unit>(
|
||||||
|
extraBufferCapacity = 1,
|
||||||
|
onBufferOverflow = BufferOverflow.DROP_OLDEST,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val thumbBottomPadding = with(LocalDensity.current) { bottomContentPadding.toPx() }
|
||||||
|
val heightPx = contentHeight.toFloat() -
|
||||||
|
thumbTopPadding -
|
||||||
|
thumbBottomPadding -
|
||||||
|
state.layoutInfo.afterContentPadding
|
||||||
|
val thumbHeightPx = with(LocalDensity.current) { ThumbLength.toPx() }
|
||||||
|
val trackHeightPx = heightPx - thumbHeightPx
|
||||||
|
|
||||||
|
val columnCount = remember { slotSizesSums(constraints).size }
|
||||||
|
|
||||||
|
// When thumb dragged
|
||||||
|
LaunchedEffect(thumbOffsetY) {
|
||||||
|
if (layoutInfo.totalItemsCount == 0 || !isThumbDragged) return@LaunchedEffect
|
||||||
|
val scrollRatio = (thumbOffsetY - thumbTopPadding) / trackHeightPx
|
||||||
|
val scrollItem = layoutInfo.totalItemsCount * scrollRatio
|
||||||
|
// I can't think of anything else rn but this'll do
|
||||||
|
val scrollItemWhole = scrollItem.toInt()
|
||||||
|
val columnNum = ((scrollItemWhole + 1) % columnCount).takeIf { it != 0 } ?: columnCount
|
||||||
|
val scrollItemFraction = if (scrollItemWhole == 0) scrollItem else scrollItem % scrollItemWhole
|
||||||
|
val offsetPerItem = 1f / columnCount
|
||||||
|
val offsetRatio = (offsetPerItem * scrollItemFraction) + (offsetPerItem * (columnNum - 1))
|
||||||
|
|
||||||
|
// TODO: Sometimes item height is not available when scrolling up
|
||||||
|
val scrollItemSize = (1..columnCount).maxOf { num ->
|
||||||
|
val actualIndex = if (num != columnNum) {
|
||||||
|
scrollItemWhole + num - columnCount
|
||||||
|
} else {
|
||||||
|
scrollItemWhole
|
||||||
|
}
|
||||||
|
layoutInfo.visibleItemsInfo.find { it.index == actualIndex }?.size?.height ?: 0
|
||||||
|
}
|
||||||
|
val scrollItemOffset = scrollItemSize * offsetRatio
|
||||||
|
|
||||||
|
state.scrollToItem(index = scrollItemWhole, scrollOffset = scrollItemOffset.roundToInt())
|
||||||
|
scrolled.tryEmit(Unit)
|
||||||
|
}
|
||||||
|
|
||||||
|
// When list scrolled
|
||||||
|
LaunchedEffect(state.firstVisibleItemScrollOffset) {
|
||||||
|
if (state.layoutInfo.totalItemsCount == 0 || isThumbDragged) return@LaunchedEffect
|
||||||
|
val scrollOffset = computeScrollOffset(state = state)
|
||||||
|
val scrollRange = computeScrollRange(state = state)
|
||||||
|
val proportion = scrollOffset.toFloat() / (scrollRange.toFloat() - heightPx)
|
||||||
|
thumbOffsetY = trackHeightPx * proportion + thumbTopPadding
|
||||||
|
scrolled.tryEmit(Unit)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Thumb alpha
|
||||||
|
val alpha = remember { Animatable(0f) }
|
||||||
|
val isThumbVisible = alpha.value > 0f
|
||||||
|
LaunchedEffect(scrolled, alpha) {
|
||||||
|
scrolled
|
||||||
|
.sample(100)
|
||||||
|
.collectLatest {
|
||||||
|
if (thumbAllowed()) {
|
||||||
|
alpha.snapTo(1f)
|
||||||
|
alpha.animateTo(0f, animationSpec = FadeOutAnimationSpec)
|
||||||
|
} else {
|
||||||
|
alpha.animateTo(0f, animationSpec = ImmediateFadeOutAnimationSpec)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.offset { IntOffset(0, thumbOffsetY.roundToInt()) }
|
||||||
|
.then(
|
||||||
|
// Recompose opts
|
||||||
|
if (isThumbVisible && !state.isScrollInProgress) {
|
||||||
|
Modifier.draggable(
|
||||||
|
interactionSource = dragInteractionSource,
|
||||||
|
orientation = Orientation.Vertical,
|
||||||
|
state = rememberDraggableState { delta ->
|
||||||
|
val newOffsetY = thumbOffsetY + delta
|
||||||
|
thumbOffsetY = newOffsetY.coerceIn(
|
||||||
|
thumbTopPadding,
|
||||||
|
thumbTopPadding + trackHeightPx,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Modifier
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.then(
|
||||||
|
// Exclude thumb from gesture area only when needed
|
||||||
|
if (isThumbVisible && !isThumbDragged && !state.isScrollInProgress) {
|
||||||
|
Modifier.systemGestureExclusion()
|
||||||
|
} else {
|
||||||
|
Modifier
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.height(ThumbLength)
|
||||||
|
.padding(end = endContentPadding)
|
||||||
|
.width(ThumbThickness)
|
||||||
|
.alpha(alpha.value)
|
||||||
|
.background(color = thumbColor, shape = ThumbShape),
|
||||||
|
)
|
||||||
|
}.map { it.measure(scrollerConstraints) }
|
||||||
|
val scrollerWidth = scrollerPlaceable.fastMaxBy { it.width }?.width ?: 0
|
||||||
|
|
||||||
|
layout(contentWidth, contentHeight) {
|
||||||
|
contentPlaceable.fastForEach {
|
||||||
|
it.place(0, 0)
|
||||||
|
}
|
||||||
|
scrollerPlaceable.fastForEach {
|
||||||
|
it.placeRelative(contentWidth - scrollerWidth, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun computeScrollOffset(state: LazyGridState): Int {
|
||||||
|
if (state.layoutInfo.totalItemsCount == 0) return 0
|
||||||
|
val visibleItems = state.layoutInfo.visibleItemsInfo
|
||||||
|
val startChild = visibleItems.first()
|
||||||
|
val endChild = visibleItems.last()
|
||||||
|
val minPosition = min(startChild.index, endChild.index)
|
||||||
|
val maxPosition = max(startChild.index, endChild.index)
|
||||||
|
val itemsBefore = minPosition.coerceAtLeast(0)
|
||||||
|
val startDecoratedTop = startChild.offset.y
|
||||||
|
val laidOutArea = abs((endChild.offset.y + endChild.size.height) - startDecoratedTop)
|
||||||
|
val itemRange = abs(minPosition - maxPosition) + 1
|
||||||
|
val avgSizePerRow = laidOutArea.toFloat() / itemRange
|
||||||
|
return (itemsBefore * avgSizePerRow + (0 - startDecoratedTop)).roundToInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun computeScrollRange(state: LazyGridState): Int {
|
||||||
|
if (state.layoutInfo.totalItemsCount == 0) return 0
|
||||||
|
val visibleItems = state.layoutInfo.visibleItemsInfo
|
||||||
|
val startChild = visibleItems.first()
|
||||||
|
val endChild = visibleItems.last()
|
||||||
|
val laidOutArea = (endChild.offset.y + endChild.size.height) - startChild.offset.y
|
||||||
|
val laidOutRange = abs(startChild.index - endChild.index) + 1
|
||||||
|
return (laidOutArea.toFloat() / laidOutRange * state.layoutInfo.totalItemsCount).roundToInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun computeScrollOffset(state: LazyListState): Int {
|
||||||
|
if (state.layoutInfo.totalItemsCount == 0) return 0
|
||||||
|
val visibleItems = state.layoutInfo.visibleItemsInfo
|
||||||
|
val startChild = visibleItems
|
||||||
|
.fastFirstOrNull { (it.key as? String)?.startsWith(STICKY_HEADER_KEY_PREFIX)?.not() ?: true }!!
|
||||||
|
val endChild = visibleItems.last()
|
||||||
|
val minPosition = min(startChild.index, endChild.index)
|
||||||
|
val maxPosition = max(startChild.index, endChild.index)
|
||||||
|
val itemsBefore = minPosition.coerceAtLeast(0)
|
||||||
|
val startDecoratedTop = startChild.top
|
||||||
|
val laidOutArea = abs(endChild.bottom - startDecoratedTop)
|
||||||
|
val itemRange = abs(minPosition - maxPosition) + 1
|
||||||
|
val avgSizePerRow = laidOutArea.toFloat() / itemRange
|
||||||
|
return (itemsBefore * avgSizePerRow + (0 - startDecoratedTop)).roundToInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun computeScrollRange(state: LazyListState): Int {
|
||||||
|
if (state.layoutInfo.totalItemsCount == 0) return 0
|
||||||
|
val visibleItems = state.layoutInfo.visibleItemsInfo
|
||||||
|
val startChild = visibleItems
|
||||||
|
.fastFirstOrNull { (it.key as? String)?.startsWith(STICKY_HEADER_KEY_PREFIX)?.not() ?: true }!!
|
||||||
|
val endChild = visibleItems.last()
|
||||||
|
val laidOutArea = endChild.bottom - startChild.top
|
||||||
|
val laidOutRange = abs(startChild.index - endChild.index) + 1
|
||||||
|
return (laidOutArea.toFloat() / laidOutRange * state.layoutInfo.totalItemsCount).roundToInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
object Scroller {
|
||||||
|
const val STICKY_HEADER_KEY_PREFIX = "sticky:"
|
||||||
|
}
|
||||||
|
|
||||||
|
private val ThumbLength = 48.dp
|
||||||
|
private val ThumbThickness = 6.dp
|
||||||
|
private val ThumbShape = RoundedCornerShape(ThumbThickness / 2)
|
||||||
|
private val FadeOutAnimationSpec = tween<Float>(
|
||||||
|
durationMillis = ViewConfiguration.getScrollBarFadeDuration(),
|
||||||
|
delayMillis = 2000,
|
||||||
|
)
|
||||||
|
private val ImmediateFadeOutAnimationSpec = tween<Float>(
|
||||||
|
durationMillis = ViewConfiguration.getScrollBarFadeDuration(),
|
||||||
|
)
|
||||||
|
|
||||||
|
private val LazyListItemInfo.top: Int
|
||||||
|
get() = offset
|
||||||
|
|
||||||
|
private val LazyListItemInfo.bottom: Int
|
||||||
|
get() = offset + size
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
package org.xtimms.shirizu.core.components.effects
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.EnterTransition
|
||||||
|
import androidx.compose.animation.ExitTransition
|
||||||
|
import androidx.compose.animation.core.MutableTransitionState
|
||||||
|
import androidx.compose.animation.expandVertically
|
||||||
|
import androidx.compose.animation.fadeIn
|
||||||
|
import androidx.compose.animation.fadeOut
|
||||||
|
import androidx.compose.animation.shrinkVertically
|
||||||
|
import androidx.compose.foundation.lazy.LazyItemScope
|
||||||
|
import androidx.compose.foundation.lazy.LazyListScope
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.State
|
||||||
|
import androidx.compose.runtime.key
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.recyclerview.widget.DiffUtil
|
||||||
|
import androidx.recyclerview.widget.ListUpdateCallback
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.xtimms.shirizu.core.model.ListModel
|
||||||
|
import java.time.Instant
|
||||||
|
|
||||||
|
enum class RowEntityType { Header, Item }
|
||||||
|
|
||||||
|
data class RowEntity(
|
||||||
|
val type: RowEntityType,
|
||||||
|
val key: String,
|
||||||
|
var contentHash: String? = null,
|
||||||
|
val day: Instant,
|
||||||
|
var itemModel: ListModel?,
|
||||||
|
)
|
||||||
|
|
||||||
|
@SuppressLint("ComposableNaming", "UnusedTransitionTargetStateParameter")
|
||||||
|
/**
|
||||||
|
* @param state Use [updateAnimatedItemsState].
|
||||||
|
*/
|
||||||
|
inline fun LazyListScope.animatedItemsIndexed(
|
||||||
|
state: List<AnimatedItem<RowEntity>>,
|
||||||
|
enterTransition: EnterTransition = expandVertically() + fadeIn(),
|
||||||
|
exitTransition: ExitTransition = shrinkVertically() + fadeOut(),
|
||||||
|
noinline key: ((item: RowEntity) -> Any)? = null,
|
||||||
|
crossinline itemContent: @Composable LazyItemScope.(index: Int, item: RowEntity) -> Unit
|
||||||
|
) {
|
||||||
|
items(
|
||||||
|
state.size,
|
||||||
|
if (key != null) { keyIndex: Int -> key(state[keyIndex].item) } else null
|
||||||
|
) { index ->
|
||||||
|
|
||||||
|
val item = state[index]
|
||||||
|
|
||||||
|
key(key?.invoke(item.item)) {
|
||||||
|
AnimatedVisibility(
|
||||||
|
visibleState = item.visibility,
|
||||||
|
enter = enterTransition,
|
||||||
|
exit = exitTransition
|
||||||
|
) {
|
||||||
|
itemContent(index, item.item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun updateAnimatedItemsState(
|
||||||
|
newList: List<RowEntity>
|
||||||
|
): State<List<AnimatedItem<RowEntity>>> {
|
||||||
|
|
||||||
|
val state = remember { mutableStateOf(emptyList<AnimatedItem<RowEntity>>()) }
|
||||||
|
val firstInject = remember { mutableStateOf(true) }
|
||||||
|
|
||||||
|
DisposableEffect(Unit) {
|
||||||
|
state.value = emptyList()
|
||||||
|
onDispose {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LaunchedEffect(newList) {
|
||||||
|
if (state.value == newList) {
|
||||||
|
return@LaunchedEffect
|
||||||
|
}
|
||||||
|
val oldList = state.value.toList()
|
||||||
|
|
||||||
|
val diffCb = object : DiffUtil.Callback() {
|
||||||
|
override fun getOldListSize(): Int = oldList.size
|
||||||
|
override fun getNewListSize(): Int = newList.size
|
||||||
|
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean =
|
||||||
|
oldList[oldItemPosition].item.key == newList[newItemPosition].key
|
||||||
|
|
||||||
|
override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean =
|
||||||
|
(oldList[oldItemPosition].item.contentHash
|
||||||
|
?: oldList[oldItemPosition].item.key) == (newList[newItemPosition].contentHash
|
||||||
|
?: newList[newItemPosition].key)
|
||||||
|
|
||||||
|
override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): RowEntity =
|
||||||
|
newList[newItemPosition]
|
||||||
|
}
|
||||||
|
val diffResult = calculateDiff(false, diffCb)
|
||||||
|
val compositeList = oldList.toMutableList()
|
||||||
|
|
||||||
|
diffResult.dispatchUpdatesTo(object : ListUpdateCallback {
|
||||||
|
override fun onInserted(position: Int, count: Int) {
|
||||||
|
for (i in 0 until count) {
|
||||||
|
val newItem = AnimatedItem(
|
||||||
|
visibility = MutableTransitionState(firstInject.value),
|
||||||
|
newList[position + i]
|
||||||
|
)
|
||||||
|
newItem.visibility.targetState = true
|
||||||
|
compositeList.add(position + i, newItem)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onRemoved(position: Int, count: Int) {
|
||||||
|
for (i in 0 until count) {
|
||||||
|
compositeList[position + i].visibility.targetState = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onMoved(fromPosition: Int, toPosition: Int) {
|
||||||
|
// not detecting moves.
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onChanged(position: Int, count: Int, payload: Any?) {
|
||||||
|
for (i in 0 until count) {
|
||||||
|
compositeList[position + i].item.itemModel = (payload as RowEntity).itemModel
|
||||||
|
compositeList[position + i].item.contentHash = payload.contentHash
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (state.value != compositeList) {
|
||||||
|
state.value = compositeList
|
||||||
|
}
|
||||||
|
firstInject.value = false
|
||||||
|
val initialAnimation = androidx.compose.animation.core.Animatable(1.0f)
|
||||||
|
initialAnimation.animateTo(0f)
|
||||||
|
state.value = state.value.filter { it.visibility.targetState }
|
||||||
|
}
|
||||||
|
|
||||||
|
return state
|
||||||
|
}
|
||||||
|
|
||||||
|
data class AnimatedItem<T>(
|
||||||
|
val visibility: MutableTransitionState<Boolean>,
|
||||||
|
val item: T,
|
||||||
|
) {
|
||||||
|
|
||||||
|
override fun hashCode(): Int {
|
||||||
|
return item?.hashCode() ?: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun equals(other: Any?): Boolean {
|
||||||
|
if (this === other) return true
|
||||||
|
if (javaClass != other?.javaClass) return false
|
||||||
|
|
||||||
|
other as AnimatedItem<*>
|
||||||
|
|
||||||
|
return item == other.item
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun calculateDiff(
|
||||||
|
detectMoves: Boolean = true,
|
||||||
|
diffCb: DiffUtil.Callback
|
||||||
|
): DiffUtil.DiffResult {
|
||||||
|
return withContext(Dispatchers.Unconfined) {
|
||||||
|
DiffUtil.calculateDiff(diffCb, detectMoves)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package org.xtimms.shirizu.core.components.effects
|
||||||
|
|
||||||
|
import androidx.compose.animation.core.LinearEasing
|
||||||
|
import androidx.compose.animation.core.RepeatMode
|
||||||
|
import androidx.compose.animation.core.animateFloat
|
||||||
|
import androidx.compose.animation.core.infiniteRepeatable
|
||||||
|
import androidx.compose.animation.core.rememberInfiniteTransition
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.foundation.Canvas
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||||
|
import kotlin.random.Random
|
||||||
|
|
||||||
|
data class Snowflake(
|
||||||
|
var x: Float,
|
||||||
|
var y: Float,
|
||||||
|
var radius: Float,
|
||||||
|
var speed: Float
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SnowfallEffect() {
|
||||||
|
val snowflakes = remember { List(100) { generateRandomSnowflake() } }
|
||||||
|
val infiniteTransition = rememberInfiniteTransition(label = "")
|
||||||
|
|
||||||
|
val offsetY by infiniteTransition.animateFloat(
|
||||||
|
initialValue = 0f,
|
||||||
|
targetValue = 100f,
|
||||||
|
animationSpec = infiniteRepeatable(
|
||||||
|
animation = tween(durationMillis = 10000, easing = LinearEasing)
|
||||||
|
), label = ""
|
||||||
|
)
|
||||||
|
|
||||||
|
Canvas(modifier = Modifier.fillMaxSize().background(Color.Transparent)) {
|
||||||
|
snowflakes.forEach { snowflake ->
|
||||||
|
drawSnowflake(snowflake, offsetY % size.height)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun generateRandomSnowflake(): Snowflake {
|
||||||
|
return Snowflake(
|
||||||
|
x = Random.nextFloat(),
|
||||||
|
y = Random.nextFloat() * 1000f,
|
||||||
|
radius = Random.nextFloat() * 2f + 2f, // Snowflake size
|
||||||
|
speed = Random.nextFloat() * 0.5f + 1f // Falling speed
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun DrawScope.drawSnowflake(snowflake: Snowflake, offsetY: Float) {
|
||||||
|
val newY = (snowflake.y + offsetY * snowflake.speed) % size.height
|
||||||
|
drawCircle(Color.White, radius = snowflake.radius, center = Offset(snowflake.x * size.width, newY))
|
||||||
|
}
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package org.xtimms.shirizu.core.components.icons
|
||||||
|
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.materialIcon
|
||||||
|
import androidx.compose.material.icons.materialPath
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
|
||||||
|
val Icons.Outlined.ArrowDecisionOutline: ImageVector
|
||||||
|
get() {
|
||||||
|
if (_arrow_decision_outline != null) {
|
||||||
|
return _arrow_decision_outline!!
|
||||||
|
}
|
||||||
|
_arrow_decision_outline = materialIcon(name = "Outlined.ArrowDecisionOutline") {
|
||||||
|
materialPath {
|
||||||
|
moveTo(9.64f, 13.4f)
|
||||||
|
curveTo(8.63f, 12.5f, 7.34f, 12.03f, 6.0f, 12.0f)
|
||||||
|
verticalLineTo(15.0f)
|
||||||
|
lineTo(2.0f, 11.0f)
|
||||||
|
lineTo(6.0f, 7.0f)
|
||||||
|
verticalLineTo(10.0f)
|
||||||
|
curveTo(7.67f, 10.0f, 9.3f, 10.57f, 10.63f, 11.59f)
|
||||||
|
curveTo(10.22f, 12.15f, 9.89f, 12.76f, 9.64f, 13.4f)
|
||||||
|
moveTo(18.0f, 15.0f)
|
||||||
|
verticalLineTo(12.0f)
|
||||||
|
curveTo(17.5f, 12.0f, 13.5f, 12.16f, 13.05f, 16.2f)
|
||||||
|
curveTo(14.61f, 16.75f, 15.43f, 18.47f, 14.88f, 20.03f)
|
||||||
|
curveTo(14.33f, 21.59f, 12.61f, 22.41f, 11.05f, 21.86f)
|
||||||
|
curveTo(9.5f, 21.3f, 8.67f, 19.59f, 9.22f, 18.03f)
|
||||||
|
curveTo(9.5f, 17.17f, 10.2f, 16.5f, 11.05f, 16.2f)
|
||||||
|
curveTo(11.34f, 12.61f, 14.4f, 9.88f, 18.0f, 10.0f)
|
||||||
|
verticalLineTo(7.0f)
|
||||||
|
lineTo(22.0f, 11.0f)
|
||||||
|
lineTo(18.0f, 15.0f)
|
||||||
|
moveTo(13.0f, 19.0f)
|
||||||
|
arcTo(1.0f, 1.0f, 0.0f, false, false, 12.0f, 18.0f)
|
||||||
|
arcTo(1.0f, 1.0f, 0.0f, false, false, 11.0f, 19.0f)
|
||||||
|
arcTo(1.0f, 1.0f, 0.0f, false, false, 12.0f, 20.0f)
|
||||||
|
arcTo(1.0f, 1.0f, 0.0f, false, false, 13.0f, 19.0f)
|
||||||
|
moveTo(11.0f, 11.12f)
|
||||||
|
curveTo(11.58f, 10.46f, 12.25f, 9.89f, 13.0f, 9.43f)
|
||||||
|
verticalLineTo(5.0f)
|
||||||
|
horizontalLineTo(16.0f)
|
||||||
|
lineTo(12.0f, 1.0f)
|
||||||
|
lineTo(8.0f, 5.0f)
|
||||||
|
horizontalLineTo(11.0f)
|
||||||
|
verticalLineTo(11.12f)
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return _arrow_decision_outline!!
|
||||||
|
}
|
||||||
|
|
||||||
|
private var _arrow_decision_outline: ImageVector? = null
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
package org.xtimms.shirizu.core.components.icons
|
||||||
|
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.PathFillType.Companion.NonZero
|
||||||
|
import androidx.compose.ui.graphics.SolidColor
|
||||||
|
import androidx.compose.ui.graphics.StrokeCap.Companion.Butt
|
||||||
|
import androidx.compose.ui.graphics.StrokeJoin.Companion.Miter
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector.Builder
|
||||||
|
import androidx.compose.ui.graphics.vector.path
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
val Icons.Outlined.Creation: ImageVector
|
||||||
|
get() {
|
||||||
|
if (_creation != null) {
|
||||||
|
return _creation!!
|
||||||
|
}
|
||||||
|
_creation = Builder(name = "Creation", defaultWidth = 24.0.dp, defaultHeight = 24.0.dp,
|
||||||
|
viewportWidth = 24.0f, viewportHeight = 24.0f).apply {
|
||||||
|
path(fill = SolidColor(Color(0xFF000000)), stroke = null, strokeLineWidth = 0.0f,
|
||||||
|
strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f,
|
||||||
|
pathFillType = NonZero) {
|
||||||
|
moveTo(9.0f, 4.0f)
|
||||||
|
lineTo(11.5f, 9.5f)
|
||||||
|
lineTo(17.0f, 12.0f)
|
||||||
|
lineTo(11.5f, 14.5f)
|
||||||
|
lineTo(9.0f, 20.0f)
|
||||||
|
lineTo(6.5f, 14.5f)
|
||||||
|
lineTo(1.0f, 12.0f)
|
||||||
|
lineTo(6.5f, 9.5f)
|
||||||
|
lineTo(9.0f, 4.0f)
|
||||||
|
moveTo(9.0f, 8.83f)
|
||||||
|
lineTo(8.0f, 11.0f)
|
||||||
|
lineTo(5.83f, 12.0f)
|
||||||
|
lineTo(8.0f, 13.0f)
|
||||||
|
lineTo(9.0f, 15.17f)
|
||||||
|
lineTo(10.0f, 13.0f)
|
||||||
|
lineTo(12.17f, 12.0f)
|
||||||
|
lineTo(10.0f, 11.0f)
|
||||||
|
lineTo(9.0f, 8.83f)
|
||||||
|
moveTo(19.0f, 9.0f)
|
||||||
|
lineTo(17.74f, 6.26f)
|
||||||
|
lineTo(15.0f, 5.0f)
|
||||||
|
lineTo(17.74f, 3.75f)
|
||||||
|
lineTo(19.0f, 1.0f)
|
||||||
|
lineTo(20.25f, 3.75f)
|
||||||
|
lineTo(23.0f, 5.0f)
|
||||||
|
lineTo(20.25f, 6.26f)
|
||||||
|
lineTo(19.0f, 9.0f)
|
||||||
|
moveTo(19.0f, 23.0f)
|
||||||
|
lineTo(17.74f, 20.26f)
|
||||||
|
lineTo(15.0f, 19.0f)
|
||||||
|
lineTo(17.74f, 17.75f)
|
||||||
|
lineTo(19.0f, 15.0f)
|
||||||
|
lineTo(20.25f, 17.75f)
|
||||||
|
lineTo(23.0f, 19.0f)
|
||||||
|
lineTo(20.25f, 20.26f)
|
||||||
|
lineTo(19.0f, 23.0f)
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.build()
|
||||||
|
return _creation!!
|
||||||
|
}
|
||||||
|
|
||||||
|
private var _creation: ImageVector? = null
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
package org.xtimms.tokusho.core.components.icons
|
package org.xtimms.shirizu.core.components.icons
|
||||||
|
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.materialIcon
|
import androidx.compose.material.icons.materialIcon
|
||||||
import androidx.compose.material.icons.materialPath
|
import androidx.compose.material.icons.materialPath
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
|
||||||
public val Icons.Outlined.Dice: ImageVector
|
val Icons.Outlined.Dice: ImageVector
|
||||||
get() {
|
get() {
|
||||||
if (_dice != null) {
|
if (_dice != null) {
|
||||||
return _dice!!
|
return _dice!!
|
||||||
@@ -0,0 +1,293 @@
|
|||||||
|
package org.xtimms.shirizu.core.components.icons
|
||||||
|
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.PathFillType.Companion.NonZero
|
||||||
|
import androidx.compose.ui.graphics.SolidColor
|
||||||
|
import androidx.compose.ui.graphics.StrokeCap.Companion.Butt
|
||||||
|
import androidx.compose.ui.graphics.StrokeJoin.Companion.Miter
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector.Builder
|
||||||
|
import androidx.compose.ui.graphics.vector.path
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
val Icons.Filled.Kotatsu: ImageVector
|
||||||
|
get() {
|
||||||
|
if (_kotatsu != null) {
|
||||||
|
return _kotatsu!!
|
||||||
|
}
|
||||||
|
_kotatsu = Builder(name = "Kotatsu", defaultWidth = 1406.2.dp, defaultHeight = 1406.2.dp,
|
||||||
|
viewportWidth = 1406.2f, viewportHeight = 1406.2f).apply {
|
||||||
|
path(fill = SolidColor(Color(0xFF0058C9)), stroke = null, strokeLineWidth = 0.0f,
|
||||||
|
strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f,
|
||||||
|
pathFillType = NonZero) {
|
||||||
|
moveTo(391.7f, 270.7f)
|
||||||
|
curveToRelative(-51.6f, 18.6f, -96.2f, 88.4f, -117.9f, 183.6f)
|
||||||
|
curveToRelative(-7.8f, 34.8f, -15.1f, 93.5f, -15.1f, 121.7f)
|
||||||
|
verticalLineToRelative(19.7f)
|
||||||
|
lineToRelative(-23.3f, 36.6f)
|
||||||
|
curveToRelative(-65.0f, 101.3f, -124.6f, 206.8f, -180.5f, 319.2f)
|
||||||
|
curveTo(5.1f, 1051.0f, 0.0f, 1063.2f, 0.0f, 1080.9f)
|
||||||
|
curveToRelative(0.0f, 7.8f, 2.0f, 18.0f, 4.0f, 22.2f)
|
||||||
|
curveToRelative(6.6f, 12.0f, 22.2f, 24.4f, 39.7f, 31.0f)
|
||||||
|
lineToRelative(16.0f, 6.2f)
|
||||||
|
lineToRelative(651.1f, -0.2f)
|
||||||
|
curveToRelative(633.1f, 0.0f, 651.1f, -0.4f, 661.2f, -5.3f)
|
||||||
|
curveToRelative(32.6f, -16.9f, 43.0f, -51.7f, 26.2f, -88.0f)
|
||||||
|
curveToRelative(-63.8f, -139.0f, -150.5f, -296.8f, -229.6f, -418.5f)
|
||||||
|
lineToRelative(-19.1f, -29.5f)
|
||||||
|
lineToRelative(-2.2f, -33.7f)
|
||||||
|
curveToRelative(-8.7f, -129.4f, -36.1f, -208.6f, -92.0f, -266.5f)
|
||||||
|
curveToRelative(-24.2f, -24.8f, -33.5f, -30.4f, -50.6f, -30.6f)
|
||||||
|
curveToRelative(-23.9f, 0.0f, -39.9f, 10.9f, -75.6f, 52.1f)
|
||||||
|
curveToRelative(-35.2f, 40.4f, -42.4f, 50.1f, -66.9f, 86.4f)
|
||||||
|
curveToRelative(-12.0f, 17.7f, -27.0f, 38.3f, -33.2f, 45.5f)
|
||||||
|
lineToRelative(-11.3f, 13.5f)
|
||||||
|
horizontalLineToRelative(-117.0f)
|
||||||
|
lineToRelative(-117.0f, -0.2f)
|
||||||
|
lineTo(560.2f, 429.0f)
|
||||||
|
curveTo(515.0f, 359.2f, 440.7f, 274.5f, 419.6f, 268.7f)
|
||||||
|
curveTo(406.8f, 264.9f, 409.0f, 264.5f, 391.7f, 270.7f)
|
||||||
|
close()
|
||||||
|
moveTo(466.2f, 666.4f)
|
||||||
|
curveToRelative(8.9f, 6.2f, 11.3f, 11.8f, 14.4f, 37.7f)
|
||||||
|
curveToRelative(4.0f, 30.6f, 7.7f, 34.8f, 27.5f, 32.4f)
|
||||||
|
curveToRelative(18.0f, -2.2f, 32.6f, 3.6f, 40.8f, 16.6f)
|
||||||
|
curveToRelative(16.0f, 25.9f, -11.5f, 80.2f, -50.6f, 99.3f)
|
||||||
|
curveToRelative(-14.0f, 7.1f, -19.1f, 7.8f, -42.8f, 7.8f)
|
||||||
|
curveToRelative(-22.8f, 0.0f, -28.8f, -1.1f, -39.4f, -6.7f)
|
||||||
|
curveToRelative(-31.2f, -16.4f, -50.3f, -40.3f, -58.3f, -71.8f)
|
||||||
|
curveToRelative(-4.0f, -16.6f, -4.2f, -21.7f, -1.1f, -36.3f)
|
||||||
|
curveToRelative(4.2f, -21.1f, 11.5f, -35.2f, 24.8f, -50.1f)
|
||||||
|
curveTo(404.8f, 669.5f, 449.1f, 654.4f, 466.2f, 666.4f)
|
||||||
|
close()
|
||||||
|
moveTo(964.0f, 669.0f)
|
||||||
|
curveToRelative(8.7f, 7.3f, 9.3f, 9.7f, 13.5f, 43.4f)
|
||||||
|
curveToRelative(2.6f, 20.6f, 8.7f, 26.8f, 25.3f, 24.2f)
|
||||||
|
curveToRelative(16.0f, -2.2f, 29.9f, 2.2f, 39.2f, 12.9f)
|
||||||
|
curveToRelative(15.1f, 18.2f, 6.2f, 53.4f, -20.8f, 82.2f)
|
||||||
|
curveToRelative(-21.7f, 23.1f, -35.2f, 28.4f, -69.2f, 28.8f)
|
||||||
|
curveToRelative(-25.9f, 0.0f, -29.1f, -0.5f, -42.8f, -8.2f)
|
||||||
|
curveToRelative(-20.2f, -11.3f, -38.4f, -29.9f, -47.7f, -49.0f)
|
||||||
|
curveToRelative(-6.2f, -12.8f, -8.2f, -20.8f, -8.9f, -39.2f)
|
||||||
|
curveToRelative(-0.9f, -21.1f, 0.0f, -25.0f, 7.7f, -41.0f)
|
||||||
|
curveToRelative(14.0f, -30.4f, 35.5f, -49.2f, 65.0f, -57.0f)
|
||||||
|
curveTo(945.2f, 660.2f, 954.7f, 661.1f, 964.0f, 669.0f)
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
path(fill = SolidColor(Color(0xFF000000)), stroke = null, strokeLineWidth = 0.0f,
|
||||||
|
strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f,
|
||||||
|
pathFillType = NonZero) {
|
||||||
|
moveTo(1806.5f, 939.2f)
|
||||||
|
verticalLineTo(464.0f)
|
||||||
|
horizontalLineToRelative(88.3f)
|
||||||
|
verticalLineToRelative(475.3f)
|
||||||
|
lineTo(1806.5f, 939.2f)
|
||||||
|
lineTo(1806.5f, 939.2f)
|
||||||
|
close()
|
||||||
|
moveTo(1885.3f, 827.2f)
|
||||||
|
lineToRelative(-4.8f, -104.5f)
|
||||||
|
lineTo(2129.7f, 464.0f)
|
||||||
|
horizontalLineToRelative(99.1f)
|
||||||
|
lineToRelative(-207.1f, 220.0f)
|
||||||
|
lineToRelative(-48.9f, 53.6f)
|
||||||
|
lineTo(1885.3f, 827.2f)
|
||||||
|
close()
|
||||||
|
moveTo(2137.8f, 939.2f)
|
||||||
|
lineToRelative(-181.9f, -216.6f)
|
||||||
|
lineToRelative(58.4f, -64.5f)
|
||||||
|
lineTo(2241.0f, 939.2f)
|
||||||
|
horizontalLineTo(2137.8f)
|
||||||
|
lineTo(2137.8f, 939.2f)
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
path(fill = SolidColor(Color(0xFF000000)), stroke = null, strokeLineWidth = 0.0f,
|
||||||
|
strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f,
|
||||||
|
pathFillType = NonZero) {
|
||||||
|
moveTo(2440.6f, 944.0f)
|
||||||
|
curveToRelative(-37.1f, 0.0f, -70.2f, -8.0f, -99.1f, -24.1f)
|
||||||
|
curveToRelative(-29.0f, -16.1f, -51.8f, -38.1f, -68.6f, -66.2f)
|
||||||
|
curveToRelative(-16.8f, -28.1f, -25.1f, -60.0f, -25.1f, -95.7f)
|
||||||
|
curveToRelative(0.0f, -36.2f, 8.4f, -68.2f, 25.1f, -96.1f)
|
||||||
|
curveToRelative(16.7f, -27.8f, 39.6f, -49.7f, 68.6f, -65.5f)
|
||||||
|
reflectiveCurveToRelative(62.0f, -23.8f, 99.1f, -23.8f)
|
||||||
|
curveToRelative(37.6f, 0.0f, 70.9f, 7.9f, 100.1f, 23.8f)
|
||||||
|
curveToRelative(29.2f, 15.9f, 52.1f, 37.6f, 68.6f, 65.2f)
|
||||||
|
curveToRelative(16.5f, 27.6f, 24.8f, 59.8f, 24.8f, 96.4f)
|
||||||
|
curveToRelative(0.0f, 35.8f, -8.3f, 67.7f, -24.8f, 95.7f)
|
||||||
|
curveToRelative(-16.5f, 28.1f, -39.4f, 50.1f, -68.6f, 66.2f)
|
||||||
|
curveTo(2511.6f, 936.0f, 2478.2f, 944.0f, 2440.6f, 944.0f)
|
||||||
|
close()
|
||||||
|
moveTo(2440.6f, 871.3f)
|
||||||
|
curveToRelative(20.8f, 0.0f, 39.4f, -4.5f, 55.7f, -13.6f)
|
||||||
|
curveToRelative(16.3f, -9.0f, 29.1f, -22.2f, 38.4f, -39.4f)
|
||||||
|
curveToRelative(9.3f, -17.2f, 13.9f, -37.3f, 13.9f, -60.4f)
|
||||||
|
curveToRelative(0.0f, -23.5f, -4.6f, -43.8f, -13.9f, -60.8f)
|
||||||
|
curveToRelative(-9.3f, -17.0f, -22.1f, -30.0f, -38.4f, -39.0f)
|
||||||
|
curveToRelative(-16.3f, -9.0f, -34.6f, -13.6f, -55.0f, -13.6f)
|
||||||
|
curveToRelative(-20.8f, 0.0f, -39.3f, 4.5f, -55.3f, 13.6f)
|
||||||
|
curveToRelative(-16.1f, 9.0f, -28.8f, 22.1f, -38.3f, 39.0f)
|
||||||
|
curveToRelative(-9.5f, 17.0f, -14.3f, 37.2f, -14.3f, 60.8f)
|
||||||
|
curveToRelative(0.0f, 23.1f, 4.8f, 43.2f, 14.3f, 60.4f)
|
||||||
|
reflectiveCurveToRelative(22.3f, 30.3f, 38.3f, 39.4f)
|
||||||
|
curveTo(2402.0f, 866.8f, 2420.3f, 871.3f, 2440.6f, 871.3f)
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
path(fill = SolidColor(Color(0xFF000000)), stroke = null, strokeLineWidth = 0.0f,
|
||||||
|
strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f,
|
||||||
|
pathFillType = NonZero) {
|
||||||
|
moveTo(2667.4f, 647.3f)
|
||||||
|
verticalLineToRelative(-67.9f)
|
||||||
|
horizontalLineToRelative(241.7f)
|
||||||
|
verticalLineToRelative(67.9f)
|
||||||
|
horizontalLineTo(2667.4f)
|
||||||
|
close()
|
||||||
|
moveTo(2852.1f, 944.0f)
|
||||||
|
curveToRelative(-39.8f, 0.0f, -70.6f, -10.3f, -92.3f, -30.9f)
|
||||||
|
curveToRelative(-21.7f, -20.6f, -32.6f, -51.0f, -32.6f, -91.3f)
|
||||||
|
verticalLineTo(496.6f)
|
||||||
|
horizontalLineToRelative(84.9f)
|
||||||
|
verticalLineToRelative(323.2f)
|
||||||
|
curveToRelative(0.0f, 17.2f, 4.4f, 30.5f, 13.2f, 40.1f)
|
||||||
|
curveToRelative(8.8f, 9.5f, 21.1f, 14.3f, 37.0f, 14.3f)
|
||||||
|
curveToRelative(19.0f, 0.0f, 34.9f, -5.0f, 47.5f, -14.9f)
|
||||||
|
lineToRelative(23.8f, 60.4f)
|
||||||
|
curveToRelative(-9.9f, 8.2f, -22.2f, 14.3f, -36.7f, 18.3f)
|
||||||
|
curveTo(2882.4f, 941.9f, 2867.4f, 944.0f, 2852.1f, 944.0f)
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
path(fill = SolidColor(Color(0xFF000000)), stroke = null, strokeLineWidth = 0.0f,
|
||||||
|
strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f,
|
||||||
|
pathFillType = NonZero) {
|
||||||
|
moveTo(3112.8f, 944.0f)
|
||||||
|
curveToRelative(-27.2f, 0.0f, -50.9f, -4.6f, -71.3f, -13.9f)
|
||||||
|
curveToRelative(-20.4f, -9.3f, -36.1f, -22.2f, -47.2f, -38.7f)
|
||||||
|
curveToRelative(-11.1f, -16.5f, -16.6f, -35.2f, -16.6f, -56.0f)
|
||||||
|
curveToRelative(0.0f, -20.4f, 4.9f, -38.7f, 14.6f, -55.0f)
|
||||||
|
curveToRelative(9.7f, -16.3f, 25.7f, -29.2f, 47.9f, -38.7f)
|
||||||
|
reflectiveCurveToRelative(51.6f, -14.3f, 88.3f, -14.3f)
|
||||||
|
horizontalLineToRelative(105.2f)
|
||||||
|
verticalLineToRelative(56.3f)
|
||||||
|
horizontalLineToRelative(-99.1f)
|
||||||
|
curveToRelative(-29.0f, 0.0f, -48.4f, 4.7f, -58.4f, 13.9f)
|
||||||
|
curveToRelative(-10.0f, 9.3f, -14.9f, 20.7f, -14.9f, 34.3f)
|
||||||
|
curveToRelative(0.0f, 15.4f, 6.1f, 27.6f, 18.3f, 36.7f)
|
||||||
|
curveToRelative(12.2f, 9.1f, 29.2f, 13.6f, 50.9f, 13.6f)
|
||||||
|
curveToRelative(20.8f, 0.0f, 39.5f, -4.8f, 56.0f, -14.3f)
|
||||||
|
reflectiveCurveToRelative(28.4f, -23.5f, 35.6f, -42.1f)
|
||||||
|
lineToRelative(14.3f, 50.9f)
|
||||||
|
curveToRelative(-8.1f, 21.3f, -22.7f, 37.8f, -43.8f, 49.6f)
|
||||||
|
reflectiveCurveTo(3144.9f, 944.0f, 3112.8f, 944.0f)
|
||||||
|
close()
|
||||||
|
moveTo(3226.8f, 939.2f)
|
||||||
|
verticalLineToRelative(-73.3f)
|
||||||
|
lineToRelative(-4.8f, -15.6f)
|
||||||
|
verticalLineTo(722.0f)
|
||||||
|
curveToRelative(0.0f, -24.9f, -7.5f, -44.2f, -22.4f, -58.0f)
|
||||||
|
reflectiveCurveToRelative(-37.6f, -20.7f, -67.9f, -20.7f)
|
||||||
|
curveToRelative(-20.4f, 0.0f, -40.4f, 3.2f, -60.1f, 9.5f)
|
||||||
|
curveToRelative(-19.7f, 6.3f, -36.3f, 15.2f, -49.9f, 26.5f)
|
||||||
|
lineToRelative(-33.3f, -61.8f)
|
||||||
|
curveToRelative(19.4f, -14.9f, 42.6f, -26.1f, 69.6f, -33.6f)
|
||||||
|
curveToRelative(26.9f, -7.5f, 54.9f, -11.2f, 83.8f, -11.2f)
|
||||||
|
curveToRelative(52.5f, 0.0f, 93.1f, 12.6f, 121.9f, 37.7f)
|
||||||
|
curveToRelative(28.7f, 25.1f, 43.1f, 63.9f, 43.1f, 116.4f)
|
||||||
|
verticalLineToRelative(212.5f)
|
||||||
|
lineTo(3226.8f, 939.2f)
|
||||||
|
lineTo(3226.8f, 939.2f)
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
path(fill = SolidColor(Color(0xFF000000)), stroke = null, strokeLineWidth = 0.0f,
|
||||||
|
strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f,
|
||||||
|
pathFillType = NonZero) {
|
||||||
|
moveTo(3367.4f, 647.3f)
|
||||||
|
verticalLineToRelative(-67.9f)
|
||||||
|
horizontalLineToRelative(241.7f)
|
||||||
|
verticalLineToRelative(67.9f)
|
||||||
|
horizontalLineTo(3367.4f)
|
||||||
|
close()
|
||||||
|
moveTo(3552.0f, 944.0f)
|
||||||
|
curveToRelative(-39.8f, 0.0f, -70.6f, -10.3f, -92.3f, -30.9f)
|
||||||
|
curveToRelative(-21.7f, -20.6f, -32.6f, -51.0f, -32.6f, -91.3f)
|
||||||
|
verticalLineTo(496.6f)
|
||||||
|
horizontalLineToRelative(84.9f)
|
||||||
|
verticalLineToRelative(323.2f)
|
||||||
|
curveToRelative(0.0f, 17.2f, 4.4f, 30.5f, 13.2f, 40.1f)
|
||||||
|
curveToRelative(8.8f, 9.5f, 21.1f, 14.3f, 37.0f, 14.3f)
|
||||||
|
curveToRelative(19.0f, 0.0f, 34.9f, -5.0f, 47.5f, -14.9f)
|
||||||
|
lineToRelative(23.8f, 60.4f)
|
||||||
|
curveToRelative(-9.9f, 8.2f, -22.2f, 14.3f, -36.7f, 18.3f)
|
||||||
|
curveTo(3582.3f, 941.9f, 3567.4f, 944.0f, 3552.0f, 944.0f)
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
path(fill = SolidColor(Color(0xFF000000)), stroke = null, strokeLineWidth = 0.0f,
|
||||||
|
strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f,
|
||||||
|
pathFillType = NonZero) {
|
||||||
|
moveTo(3815.5f, 944.0f)
|
||||||
|
curveToRelative(-30.3f, 0.0f, -59.4f, -4.0f, -87.2f, -11.9f)
|
||||||
|
reflectiveCurveToRelative(-49.9f, -17.5f, -66.2f, -28.9f)
|
||||||
|
lineToRelative(32.6f, -64.5f)
|
||||||
|
curveToRelative(16.3f, 10.4f, 35.8f, 19.0f, 58.4f, 25.8f)
|
||||||
|
curveToRelative(22.6f, 6.8f, 45.3f, 10.2f, 67.9f, 10.2f)
|
||||||
|
curveToRelative(26.7f, 0.0f, 46.0f, -3.6f, 58.0f, -10.9f)
|
||||||
|
curveToRelative(12.0f, -7.2f, 18.0f, -17.0f, 18.0f, -29.2f)
|
||||||
|
curveToRelative(0.0f, -10.0f, -4.1f, -17.5f, -12.2f, -22.8f)
|
||||||
|
curveToRelative(-8.1f, -5.2f, -18.8f, -9.2f, -31.9f, -11.9f)
|
||||||
|
curveToRelative(-13.1f, -2.7f, -27.7f, -5.2f, -43.8f, -7.5f)
|
||||||
|
reflectiveCurveToRelative(-32.1f, -5.3f, -48.2f, -9.2f)
|
||||||
|
curveToRelative(-16.1f, -3.8f, -30.7f, -9.5f, -43.8f, -17.0f)
|
||||||
|
reflectiveCurveToRelative(-23.8f, -17.5f, -31.9f, -30.2f)
|
||||||
|
curveToRelative(-8.1f, -12.7f, -12.2f, -29.4f, -12.2f, -50.2f)
|
||||||
|
curveToRelative(0.0f, -23.1f, 6.6f, -43.1f, 19.7f, -60.1f)
|
||||||
|
curveToRelative(13.1f, -17.0f, 31.6f, -30.1f, 55.3f, -39.4f)
|
||||||
|
curveToRelative(23.8f, -9.3f, 51.9f, -13.9f, 84.5f, -13.9f)
|
||||||
|
curveToRelative(24.4f, 0.0f, 49.1f, 2.7f, 74.0f, 8.1f)
|
||||||
|
curveToRelative(24.9f, 5.4f, 45.5f, 13.1f, 61.8f, 23.1f)
|
||||||
|
lineToRelative(-32.6f, 64.5f)
|
||||||
|
curveToRelative(-17.2f, -10.4f, -34.5f, -17.5f, -51.9f, -21.4f)
|
||||||
|
curveToRelative(-17.4f, -3.8f, -34.7f, -5.8f, -51.9f, -5.8f)
|
||||||
|
curveToRelative(-25.8f, 0.0f, -44.9f, 3.9f, -57.4f, 11.5f)
|
||||||
|
curveToRelative(-12.4f, 7.7f, -18.7f, 17.4f, -18.7f, 29.2f)
|
||||||
|
curveToRelative(0.0f, 10.9f, 4.1f, 19.0f, 12.2f, 24.4f)
|
||||||
|
curveToRelative(8.1f, 5.4f, 18.8f, 9.7f, 31.9f, 12.9f)
|
||||||
|
curveToRelative(13.1f, 3.2f, 27.7f, 5.8f, 43.8f, 7.8f)
|
||||||
|
reflectiveCurveToRelative(32.0f, 5.1f, 47.9f, 9.2f)
|
||||||
|
curveToRelative(15.8f, 4.1f, 30.4f, 9.6f, 43.8f, 16.6f)
|
||||||
|
curveToRelative(13.3f, 7.0f, 24.1f, 16.9f, 32.3f, 29.5f)
|
||||||
|
curveToRelative(8.1f, 12.7f, 12.2f, 29.2f, 12.2f, 49.6f)
|
||||||
|
curveToRelative(0.0f, 22.6f, -6.7f, 42.3f, -20.0f, 59.1f)
|
||||||
|
curveToRelative(-13.4f, 16.8f, -32.3f, 29.8f, -56.7f, 39.0f)
|
||||||
|
curveTo(3878.6f, 939.3f, 3849.4f, 944.0f, 3815.5f, 944.0f)
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
path(fill = SolidColor(Color(0xFF000000)), stroke = null, strokeLineWidth = 0.0f,
|
||||||
|
strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f,
|
||||||
|
pathFillType = NonZero) {
|
||||||
|
moveTo(4206.5f, 944.0f)
|
||||||
|
curveToRelative(-30.8f, 0.0f, -57.9f, -5.8f, -81.5f, -17.3f)
|
||||||
|
curveToRelative(-23.5f, -11.5f, -41.9f, -29.2f, -55.0f, -53.0f)
|
||||||
|
reflectiveCurveToRelative(-19.7f, -53.7f, -19.7f, -90.0f)
|
||||||
|
verticalLineToRelative(-207.0f)
|
||||||
|
horizontalLineToRelative(84.9f)
|
||||||
|
verticalLineToRelative(195.5f)
|
||||||
|
curveToRelative(0.0f, 32.6f, 7.4f, 56.9f, 22.1f, 73.0f)
|
||||||
|
reflectiveCurveToRelative(35.6f, 24.1f, 62.8f, 24.1f)
|
||||||
|
curveToRelative(19.9f, 0.0f, 37.2f, -4.1f, 51.9f, -12.2f)
|
||||||
|
curveToRelative(14.7f, -8.1f, 26.2f, -20.4f, 34.6f, -36.7f)
|
||||||
|
reflectiveCurveToRelative(12.6f, -36.4f, 12.6f, -60.4f)
|
||||||
|
verticalLineTo(576.7f)
|
||||||
|
horizontalLineToRelative(84.9f)
|
||||||
|
verticalLineToRelative(362.5f)
|
||||||
|
horizontalLineToRelative(-80.8f)
|
||||||
|
verticalLineToRelative(-97.8f)
|
||||||
|
lineToRelative(14.3f, 29.9f)
|
||||||
|
curveToRelative(-12.2f, 23.5f, -30.1f, 41.5f, -53.6f, 54.0f)
|
||||||
|
curveTo(4260.4f, 937.8f, 4234.6f, 944.0f, 4206.5f, 944.0f)
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.build()
|
||||||
|
return _kotatsu!!
|
||||||
|
}
|
||||||
|
|
||||||
|
private var _kotatsu: ImageVector? = null
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
package org.xtimms.shirizu.core.components.icons
|
||||||
|
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.PathFillType.Companion.NonZero
|
||||||
|
import androidx.compose.ui.graphics.SolidColor
|
||||||
|
import androidx.compose.ui.graphics.StrokeCap.Companion.Butt
|
||||||
|
import androidx.compose.ui.graphics.StrokeJoin.Companion.Miter
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector.Builder
|
||||||
|
import androidx.compose.ui.graphics.vector.path
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
val Icons.Filled.Shirizu: ImageVector
|
||||||
|
get() {
|
||||||
|
if (_shirizu != null) {
|
||||||
|
return _shirizu!!
|
||||||
|
}
|
||||||
|
_shirizu = Builder(name = "Shirizu", defaultWidth = 30.0.dp, defaultHeight = 30.0.dp,
|
||||||
|
viewportWidth = 30.0f, viewportHeight = 30.0f).apply {
|
||||||
|
path(fill = SolidColor(Color(0xFF000000)), stroke = null, strokeLineWidth = 0.0f,
|
||||||
|
strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f,
|
||||||
|
pathFillType = NonZero) {
|
||||||
|
moveTo(9.1f, 17.6f)
|
||||||
|
curveToRelative(-0.8f, -0.4f, -1.7f, -0.9f, -2.8f, -1.3f)
|
||||||
|
curveToRelative(-1.1f, -0.4f, -2.2f, -0.8f, -3.3f, -1.2f)
|
||||||
|
curveToRelative(-1.1f, -0.3f, -2.1f, -0.6f, -2.9f, -0.8f)
|
||||||
|
lineToRelative(1.6f, -5.3f)
|
||||||
|
curveToRelative(1.0f, 0.2f, 2.1f, 0.5f, 3.2f, 0.8f)
|
||||||
|
curveToRelative(1.1f, 0.4f, 2.3f, 0.8f, 3.4f, 1.2f)
|
||||||
|
curveToRelative(1.1f, 0.4f, 2.1f, 0.9f, 3.1f, 1.3f)
|
||||||
|
lineTo(9.1f, 17.6f)
|
||||||
|
close()
|
||||||
|
moveTo(30.0f, 15.7f)
|
||||||
|
curveToRelative(-1.0f, 1.5f, -2.2f, 3.0f, -3.6f, 4.4f)
|
||||||
|
curveToRelative(-1.4f, 1.4f, -2.9f, 2.7f, -4.5f, 3.9f)
|
||||||
|
curveToRelative(-1.6f, 1.2f, -3.2f, 2.3f, -4.9f, 3.2f)
|
||||||
|
curveToRelative(-1.7f, 0.9f, -3.3f, 1.6f, -4.9f, 2.1f)
|
||||||
|
curveTo(10.6f, 29.7f, 9.1f, 30.0f, 7.8f, 30.0f)
|
||||||
|
curveToRelative(-1.8f, 0.0f, -3.4f, -0.6f, -4.8f, -1.7f)
|
||||||
|
curveToRelative(-1.4f, -1.1f, -2.3f, -3.0f, -2.9f, -5.6f)
|
||||||
|
lineTo(5.0f, 20.5f)
|
||||||
|
curveToRelative(0.3f, 1.4f, 0.8f, 2.4f, 1.4f, 3.0f)
|
||||||
|
curveToRelative(0.6f, 0.6f, 1.4f, 0.9f, 2.3f, 0.9f)
|
||||||
|
curveToRelative(0.6f, 0.0f, 1.4f, -0.2f, 2.5f, -0.6f)
|
||||||
|
curveToRelative(1.1f, -0.4f, 2.2f, -0.9f, 3.5f, -1.7f)
|
||||||
|
curveToRelative(1.3f, -0.7f, 2.6f, -1.6f, 4.0f, -2.7f)
|
||||||
|
curveToRelative(1.4f, -1.1f, 2.7f, -2.3f, 4.0f, -3.7f)
|
||||||
|
curveToRelative(1.3f, -1.4f, 2.4f, -2.9f, 3.4f, -4.6f)
|
||||||
|
lineTo(30.0f, 15.7f)
|
||||||
|
close()
|
||||||
|
moveTo(12.9f, 10.3f)
|
||||||
|
curveToRelative(-1.0f, -0.9f, -2.3f, -1.9f, -4.0f, -2.8f)
|
||||||
|
curveTo(7.3f, 6.6f, 5.6f, 5.8f, 3.8f, 5.0f)
|
||||||
|
lineToRelative(1.9f, -5.0f)
|
||||||
|
curveTo(7.0f, 0.4f, 8.2f, 1.0f, 9.5f, 1.6f)
|
||||||
|
curveToRelative(1.2f, 0.6f, 2.4f, 1.3f, 3.5f, 2.0f)
|
||||||
|
curveToRelative(1.1f, 0.7f, 2.0f, 1.3f, 2.7f, 2.0f)
|
||||||
|
lineTo(12.9f, 10.3f)
|
||||||
|
close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.build()
|
||||||
|
return _shirizu!!
|
||||||
|
}
|
||||||
|
|
||||||
|
private var _shirizu: ImageVector? = null
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package org.xtimms.shirizu.core.components.shape
|
||||||
|
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.geometry.Rect
|
||||||
|
import androidx.compose.ui.geometry.Size
|
||||||
|
import androidx.compose.ui.graphics.Outline
|
||||||
|
import androidx.compose.ui.graphics.Path
|
||||||
|
import androidx.compose.ui.graphics.PathOperation
|
||||||
|
import androidx.compose.ui.graphics.Shape
|
||||||
|
import androidx.compose.ui.unit.Density
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.LayoutDirection
|
||||||
|
import kotlin.math.ceil
|
||||||
|
|
||||||
|
class WavyShape(
|
||||||
|
private val period: Dp,
|
||||||
|
private val amplitude: Dp,
|
||||||
|
private val shift: Float,
|
||||||
|
) : Shape {
|
||||||
|
override fun createOutline(
|
||||||
|
size: Size,
|
||||||
|
layoutDirection: LayoutDirection,
|
||||||
|
density: Density,
|
||||||
|
) = Outline.Generic(Path().apply {
|
||||||
|
val halfPeriod = with(density) { period.toPx() } / 2
|
||||||
|
val amplitude = with(density) { amplitude.toPx() }
|
||||||
|
|
||||||
|
val wavyPath = Path().apply {
|
||||||
|
moveTo(x = 0f, y = 0f)
|
||||||
|
lineTo(size.width - amplitude, -halfPeriod * 2.5f + halfPeriod * 2 * shift)
|
||||||
|
repeat(ceil(size.height / halfPeriod + 3).toInt()) { i ->
|
||||||
|
relativeQuadraticTo(
|
||||||
|
dx1 = 2 * amplitude * (if (i % 2 == 0) 1 else -1),
|
||||||
|
dy1 = halfPeriod / 2,
|
||||||
|
dx2 = 0f,
|
||||||
|
dy2 = halfPeriod
|
||||||
|
)
|
||||||
|
}
|
||||||
|
lineTo(0f, size.height)
|
||||||
|
}
|
||||||
|
val boundsPath = Path().apply {
|
||||||
|
addRect(Rect(offset = Offset.Zero, size = size))
|
||||||
|
}
|
||||||
|
op(wavyPath, boundsPath, PathOperation.Intersect)
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package org.xtimms.shirizu.core.database
|
||||||
|
|
||||||
|
import android.content.res.Resources
|
||||||
|
import androidx.room.RoomDatabase
|
||||||
|
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||||
|
import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||||
|
import org.xtimms.shirizu.R
|
||||||
|
|
||||||
|
class DatabasePrePopulateCallback(private val resources: Resources) : RoomDatabase.Callback() {
|
||||||
|
|
||||||
|
override fun onCreate(db: SupportSQLiteDatabase) {
|
||||||
|
db.execSQL(
|
||||||
|
"INSERT INTO favourite_categories (created_at, sort_key, title, `order`, track, show_in_lib, `deleted_at`) VALUES (?,?,?,?,?,?,?)",
|
||||||
|
arrayOf(
|
||||||
|
System.currentTimeMillis(),
|
||||||
|
1,
|
||||||
|
resources.getString(R.string.read_later),
|
||||||
|
SortOrder.NEWEST.name,
|
||||||
|
1,
|
||||||
|
1,
|
||||||
|
0L,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
package org.xtimms.shirizu.core.database
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.room.Database
|
||||||
|
import androidx.room.InvalidationTracker
|
||||||
|
import androidx.room.Room
|
||||||
|
import androidx.room.RoomDatabase
|
||||||
|
import androidx.room.migration.Migration
|
||||||
|
import kotlinx.coroutines.CoroutineStart
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.isActive
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import org.xtimms.shirizu.core.database.dao.BookmarksDao
|
||||||
|
import org.xtimms.shirizu.core.database.dao.FavouriteCategoriesDao
|
||||||
|
import org.xtimms.shirizu.core.database.dao.FavouritesDao
|
||||||
|
import org.xtimms.shirizu.core.database.dao.HistoryDao
|
||||||
|
import org.xtimms.shirizu.core.database.dao.MangaDao
|
||||||
|
import org.xtimms.shirizu.core.database.dao.MangaSourcesDao
|
||||||
|
import org.xtimms.shirizu.core.database.dao.StatsDao
|
||||||
|
import org.xtimms.shirizu.core.database.dao.SuggestionDao
|
||||||
|
import org.xtimms.shirizu.core.database.dao.TagsDao
|
||||||
|
import org.xtimms.shirizu.core.database.dao.TrackLogsDao
|
||||||
|
import org.xtimms.shirizu.core.database.dao.TracksDao
|
||||||
|
import org.xtimms.shirizu.core.database.entity.BookmarkEntity
|
||||||
|
import org.xtimms.shirizu.core.database.entity.FavouriteCategoryEntity
|
||||||
|
import org.xtimms.shirizu.core.database.entity.FavouriteEntity
|
||||||
|
import org.xtimms.shirizu.core.database.entity.HistoryEntity
|
||||||
|
import org.xtimms.shirizu.core.database.entity.MangaEntity
|
||||||
|
import org.xtimms.shirizu.core.database.entity.MangaSourceEntity
|
||||||
|
import org.xtimms.shirizu.core.database.entity.MangaTagsEntity
|
||||||
|
import org.xtimms.shirizu.core.database.entity.StatsEntity
|
||||||
|
import org.xtimms.shirizu.core.database.entity.SuggestionEntity
|
||||||
|
import org.xtimms.shirizu.core.database.entity.TagEntity
|
||||||
|
import org.xtimms.shirizu.core.database.entity.TrackEntity
|
||||||
|
import org.xtimms.shirizu.core.database.entity.TrackLogEntity
|
||||||
|
import org.xtimms.shirizu.core.database.migrations.Migration1To2
|
||||||
|
import org.xtimms.shirizu.core.database.migrations.Migration2To3
|
||||||
|
import org.xtimms.shirizu.core.database.migrations.Migration3To4
|
||||||
|
import org.xtimms.shirizu.core.scrobbling.data.ScrobblingDao
|
||||||
|
import org.xtimms.shirizu.core.scrobbling.data.ScrobblingEntity
|
||||||
|
import org.xtimms.shirizu.utils.lang.processLifecycleScope
|
||||||
|
|
||||||
|
const val DATABASE_VERSION = 4
|
||||||
|
|
||||||
|
@Database(
|
||||||
|
entities = [
|
||||||
|
MangaEntity::class,
|
||||||
|
TagEntity::class,
|
||||||
|
MangaTagsEntity::class,
|
||||||
|
MangaSourceEntity::class,
|
||||||
|
HistoryEntity::class,
|
||||||
|
FavouriteEntity::class,
|
||||||
|
FavouriteCategoryEntity::class,
|
||||||
|
BookmarkEntity::class,
|
||||||
|
SuggestionEntity::class,
|
||||||
|
TrackEntity::class,
|
||||||
|
TrackLogEntity::class,
|
||||||
|
StatsEntity::class,
|
||||||
|
ScrobblingEntity::class,
|
||||||
|
],
|
||||||
|
version = DATABASE_VERSION
|
||||||
|
)
|
||||||
|
abstract class ShirizuDatabase : RoomDatabase() {
|
||||||
|
|
||||||
|
abstract fun getTagsDao(): TagsDao
|
||||||
|
|
||||||
|
abstract fun getHistoryDao(): HistoryDao
|
||||||
|
|
||||||
|
abstract fun getMangaDao(): MangaDao
|
||||||
|
|
||||||
|
abstract fun getSourcesDao(): MangaSourcesDao
|
||||||
|
|
||||||
|
abstract fun getFavouritesDao(): FavouritesDao
|
||||||
|
|
||||||
|
abstract fun getFavouriteCategoriesDao(): FavouriteCategoriesDao
|
||||||
|
|
||||||
|
abstract fun getBookmarksDao(): BookmarksDao
|
||||||
|
|
||||||
|
abstract fun getSuggestionDao(): SuggestionDao
|
||||||
|
|
||||||
|
abstract fun getTracksDao(): TracksDao
|
||||||
|
|
||||||
|
abstract fun getTrackLogsDao(): TrackLogsDao
|
||||||
|
|
||||||
|
abstract fun getStatsDao(): StatsDao
|
||||||
|
|
||||||
|
abstract fun getScrobblingDao(): ScrobblingDao
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getDatabaseMigrations(context: Context): Array<Migration> = arrayOf(
|
||||||
|
Migration1To2(),
|
||||||
|
Migration2To3(),
|
||||||
|
Migration3To4()
|
||||||
|
)
|
||||||
|
|
||||||
|
fun ShirizuDatabase(context: Context): ShirizuDatabase = Room
|
||||||
|
.databaseBuilder(context, ShirizuDatabase::class.java, "shirizu-db")
|
||||||
|
.addMigrations(*getDatabaseMigrations(context))
|
||||||
|
.addCallback(DatabasePrePopulateCallback(context.resources))
|
||||||
|
.build()
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
fun InvalidationTracker.removeObserverAsync(observer: InvalidationTracker.Observer) {
|
||||||
|
val scope = processLifecycleScope
|
||||||
|
if (scope.isActive) {
|
||||||
|
processLifecycleScope.launch(Dispatchers.Default, CoroutineStart.ATOMIC) {
|
||||||
|
removeObserver(observer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package org.xtimms.shirizu.core.database
|
||||||
|
|
||||||
|
const val TABLE_MANGA = "manga"
|
||||||
|
const val TABLE_TAGS = "tags"
|
||||||
|
const val TABLE_MANGA_TAGS = "manga_tags"
|
||||||
|
const val TABLE_SOURCES = "sources"
|
||||||
|
const val TABLE_HISTORY = "history"
|
||||||
|
const val TABLE_FAVOURITES = "favourites"
|
||||||
|
const val TABLE_FAVOURITE_CATEGORIES = "favourite_categories"
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package org.xtimms.shirizu.core.database.dao
|
||||||
|
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Delete
|
||||||
|
import androidx.room.Insert
|
||||||
|
import androidx.room.Query
|
||||||
|
import androidx.room.Transaction
|
||||||
|
import androidx.room.Upsert
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import org.xtimms.shirizu.core.database.entity.BookmarkEntity
|
||||||
|
import org.xtimms.shirizu.core.database.entity.MangaWithTags
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
abstract class BookmarksDao {
|
||||||
|
|
||||||
|
@Query("SELECT * FROM bookmarks WHERE manga_id = :mangaId AND page_id = :pageId")
|
||||||
|
abstract suspend fun find(mangaId: Long, pageId: Long): BookmarkEntity?
|
||||||
|
|
||||||
|
@Query("SELECT * FROM bookmarks WHERE page_id = :pageId")
|
||||||
|
abstract suspend fun find(pageId: Long): BookmarkEntity?
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
@Query(
|
||||||
|
"SELECT * FROM manga JOIN bookmarks ON bookmarks.manga_id = manga.manga_id ORDER BY percent",
|
||||||
|
)
|
||||||
|
abstract suspend fun findAll(): Map<MangaWithTags, List<BookmarkEntity>>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM bookmarks WHERE manga_id = :mangaId AND chapter_id = :chapterId AND page = :page ORDER BY percent")
|
||||||
|
abstract fun observe(mangaId: Long, chapterId: Long, page: Int): Flow<BookmarkEntity?>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM bookmarks WHERE manga_id = :mangaId ORDER BY percent")
|
||||||
|
abstract fun observe(mangaId: Long): Flow<List<BookmarkEntity>>
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
@Query(
|
||||||
|
"SELECT * FROM manga JOIN bookmarks ON bookmarks.manga_id = manga.manga_id ORDER BY percent",
|
||||||
|
)
|
||||||
|
abstract fun observe(): Flow<Map<MangaWithTags, List<BookmarkEntity>>>
|
||||||
|
|
||||||
|
@Insert
|
||||||
|
abstract suspend fun insert(entity: BookmarkEntity)
|
||||||
|
|
||||||
|
@Delete
|
||||||
|
abstract suspend fun delete(entity: BookmarkEntity)
|
||||||
|
|
||||||
|
@Query("DELETE FROM bookmarks WHERE manga_id = :mangaId AND page_id = :pageId")
|
||||||
|
abstract suspend fun delete(mangaId: Long, pageId: Long): Int
|
||||||
|
|
||||||
|
@Query("DELETE FROM bookmarks WHERE page_id = :pageId")
|
||||||
|
abstract suspend fun delete(pageId: Long): Int
|
||||||
|
|
||||||
|
@Query("DELETE FROM bookmarks WHERE manga_id = :mangaId AND chapter_id = :chapterId AND page = :page")
|
||||||
|
abstract suspend fun delete(mangaId: Long, chapterId: Long, page: Int): Int
|
||||||
|
|
||||||
|
@Upsert
|
||||||
|
abstract suspend fun upsert(bookmarks: Collection<BookmarkEntity>)
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
package org.xtimms.shirizu.core.database.dao
|
||||||
|
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Insert
|
||||||
|
import androidx.room.OnConflictStrategy
|
||||||
|
import androidx.room.Query
|
||||||
|
import androidx.room.Upsert
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import org.xtimms.shirizu.core.database.entity.FavouriteCategoryEntity
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
abstract class FavouriteCategoriesDao {
|
||||||
|
|
||||||
|
@Query("SELECT * FROM favourite_categories WHERE category_id = :id AND deleted_at = 0")
|
||||||
|
abstract suspend fun find(id: Int): FavouriteCategoryEntity
|
||||||
|
|
||||||
|
@Query("SELECT * FROM favourite_categories WHERE deleted_at = 0 ORDER BY sort_key")
|
||||||
|
abstract suspend fun findAll(): List<FavouriteCategoryEntity>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM favourite_categories WHERE deleted_at = 0 ORDER BY sort_key")
|
||||||
|
abstract fun observeAll(): Flow<List<FavouriteCategoryEntity>>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM favourite_categories WHERE deleted_at = 0 AND show_in_lib = 1 ORDER BY sort_key")
|
||||||
|
abstract fun observeAllForLibrary(): Flow<List<FavouriteCategoryEntity>>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM favourite_categories WHERE category_id = :id AND deleted_at = 0")
|
||||||
|
abstract fun observe(id: Long): Flow<FavouriteCategoryEntity?>
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.ABORT)
|
||||||
|
abstract suspend fun insert(category: FavouriteCategoryEntity): Long
|
||||||
|
|
||||||
|
suspend fun delete(id: Long) = setDeletedAt(id, System.currentTimeMillis())
|
||||||
|
|
||||||
|
@Query("UPDATE favourite_categories SET title = :title, `order` = :order, `track` = :tracker, `show_in_lib` = :onShelf WHERE category_id = :id")
|
||||||
|
abstract suspend fun update(id: Long, title: String, order: String, tracker: Boolean, onShelf: Boolean)
|
||||||
|
|
||||||
|
@Query("UPDATE favourite_categories SET `order` = :order WHERE category_id = :id")
|
||||||
|
abstract suspend fun updateOrder(id: Long, order: String)
|
||||||
|
|
||||||
|
@Query("UPDATE favourite_categories SET `track` = :isEnabled WHERE category_id = :id")
|
||||||
|
abstract suspend fun updateTracking(id: Long, isEnabled: Boolean)
|
||||||
|
|
||||||
|
@Query("UPDATE favourite_categories SET `show_in_lib` = :isEnabled WHERE category_id = :id")
|
||||||
|
abstract suspend fun updateLibVisibility(id: Long, isEnabled: Boolean)
|
||||||
|
|
||||||
|
@Query("UPDATE favourite_categories SET sort_key = :sortKey WHERE category_id = :id")
|
||||||
|
abstract suspend fun updateSortKey(id: Long, sortKey: Int)
|
||||||
|
|
||||||
|
@Query("DELETE FROM favourite_categories WHERE deleted_at != 0 AND deleted_at < :maxDeletionTime")
|
||||||
|
abstract suspend fun gc(maxDeletionTime: Long)
|
||||||
|
|
||||||
|
@Query("SELECT MAX(sort_key) FROM favourite_categories WHERE deleted_at = 0")
|
||||||
|
protected abstract suspend fun getMaxSortKey(): Int?
|
||||||
|
|
||||||
|
suspend fun getNextSortKey(): Int {
|
||||||
|
return (getMaxSortKey() ?: 0) + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
@Upsert
|
||||||
|
abstract suspend fun upsert(entity: FavouriteCategoryEntity)
|
||||||
|
|
||||||
|
@Query("UPDATE favourite_categories SET deleted_at = :deletedAt WHERE category_id = :id")
|
||||||
|
protected abstract suspend fun setDeletedAt(id: Long, deletedAt: Long)
|
||||||
|
}
|
||||||
@@ -0,0 +1,180 @@
|
|||||||
|
package org.xtimms.shirizu.core.database.dao
|
||||||
|
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Insert
|
||||||
|
import androidx.room.OnConflictStrategy
|
||||||
|
import androidx.room.Query
|
||||||
|
import androidx.room.RawQuery
|
||||||
|
import androidx.room.Transaction
|
||||||
|
import androidx.room.Upsert
|
||||||
|
import androidx.sqlite.db.SimpleSQLiteQuery
|
||||||
|
import androidx.sqlite.db.SupportSQLiteQuery
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import org.intellij.lang.annotations.Language
|
||||||
|
import org.xtimms.shirizu.core.database.entity.FavouriteCategoryEntity
|
||||||
|
import org.xtimms.shirizu.core.database.entity.FavouriteEntity
|
||||||
|
import org.xtimms.shirizu.core.database.entity.MangaEntity
|
||||||
|
import org.xtimms.shirizu.core.model.ListSortOrder
|
||||||
|
import org.xtimms.shirizu.sections.shelf.FavouriteManga
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
abstract class FavouritesDao {
|
||||||
|
|
||||||
|
/** SELECT **/
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
@Query("SELECT * FROM favourites WHERE deleted_at = 0 GROUP BY manga_id ORDER BY created_at DESC")
|
||||||
|
abstract suspend fun findAll(): List<FavouriteManga>
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
@Query("SELECT * FROM favourites WHERE deleted_at = 0 GROUP BY manga_id ORDER BY created_at DESC LIMIT :limit")
|
||||||
|
abstract suspend fun findLast(limit: Int): List<FavouriteManga>
|
||||||
|
|
||||||
|
fun observeAll(order: ListSortOrder): Flow<List<FavouriteManga>> {
|
||||||
|
val orderBy = getOrderBy(order)
|
||||||
|
|
||||||
|
@Language("RoomSql")
|
||||||
|
val query = SimpleSQLiteQuery(
|
||||||
|
"SELECT * FROM favourites LEFT JOIN manga ON favourites.manga_id = manga.manga_id " +
|
||||||
|
"WHERE favourites.deleted_at = 0 GROUP BY favourites.manga_id ORDER BY $orderBy",
|
||||||
|
)
|
||||||
|
return observeAllImpl(query)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
@Query(
|
||||||
|
"SELECT * FROM favourites WHERE deleted_at = 0 " +
|
||||||
|
"GROUP BY manga_id ORDER BY created_at DESC LIMIT :limit OFFSET :offset",
|
||||||
|
)
|
||||||
|
abstract suspend fun findAll(offset: Int, limit: Int): List<FavouriteManga>
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
@Query("SELECT * FROM favourites WHERE deleted_at = 0 ORDER BY created_at DESC LIMIT :limit OFFSET :offset")
|
||||||
|
abstract suspend fun findAllRaw(offset: Int, limit: Int): List<FavouriteManga>
|
||||||
|
|
||||||
|
@Query("SELECT DISTINCT manga_id FROM favourites WHERE deleted_at = 0 AND category_id IN (SELECT category_id FROM favourite_categories WHERE track = 1)")
|
||||||
|
abstract suspend fun findIdsWithTrack(): LongArray
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
@Query(
|
||||||
|
"SELECT * FROM favourites WHERE category_id = :categoryId AND deleted_at = 0 " +
|
||||||
|
"GROUP BY manga_id ORDER BY created_at DESC",
|
||||||
|
)
|
||||||
|
abstract suspend fun findAll(categoryId: Long): List<FavouriteManga>
|
||||||
|
|
||||||
|
fun observeAll(categoryId: Long, order: ListSortOrder): Flow<List<FavouriteManga>> {
|
||||||
|
val orderBy = getOrderBy(order)
|
||||||
|
|
||||||
|
@Language("RoomSql")
|
||||||
|
val query = SimpleSQLiteQuery(
|
||||||
|
"SELECT * FROM favourites LEFT JOIN manga ON favourites.manga_id = manga.manga_id " +
|
||||||
|
"WHERE category_id = ? AND deleted_at = 0 GROUP BY favourites.manga_id ORDER BY $orderBy",
|
||||||
|
arrayOf<Any>(categoryId),
|
||||||
|
)
|
||||||
|
return observeAllImpl(query)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
@Query(
|
||||||
|
"SELECT * FROM favourites WHERE category_id = :categoryId AND deleted_at = 0 " +
|
||||||
|
"GROUP BY manga_id ORDER BY created_at DESC LIMIT :limit OFFSET :offset",
|
||||||
|
)
|
||||||
|
abstract suspend fun findAll(categoryId: Long, offset: Int, limit: Int): List<FavouriteManga>
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"SELECT * FROM manga WHERE manga_id IN " +
|
||||||
|
"(SELECT manga_id FROM favourites WHERE category_id = :categoryId AND deleted_at = 0)",
|
||||||
|
)
|
||||||
|
abstract suspend fun findAllManga(categoryId: Int): List<MangaEntity>
|
||||||
|
|
||||||
|
@Query("SELECT COUNT(DISTINCT manga_id) FROM favourites WHERE deleted_at = 0")
|
||||||
|
abstract fun observeMangaCount(): Flow<Int>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM manga WHERE manga_id IN (SELECT manga_id FROM favourites WHERE deleted_at = 0)")
|
||||||
|
abstract suspend fun findAllManga(): List<MangaEntity>
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
@Query("SELECT * FROM favourites WHERE manga_id = :id AND deleted_at = 0 GROUP BY manga_id")
|
||||||
|
abstract suspend fun find(id: Long): FavouriteManga?
|
||||||
|
|
||||||
|
@Query("SELECT * FROM favourites WHERE manga_id = :mangaId AND deleted_at = 0")
|
||||||
|
abstract suspend fun findAllRaw(mangaId: Long): List<FavouriteEntity>
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
@Deprecated("Ignores order")
|
||||||
|
@Query("SELECT * FROM favourites WHERE manga_id = :id AND deleted_at = 0 GROUP BY manga_id")
|
||||||
|
abstract fun observe(id: Long): Flow<FavouriteManga?>
|
||||||
|
|
||||||
|
@Query("SELECT DISTINCT category_id FROM favourites WHERE manga_id = :id AND deleted_at = 0")
|
||||||
|
abstract fun observeIds(id: Long): Flow<List<Long>>
|
||||||
|
|
||||||
|
@Query("SELECT favourite_categories.* FROM favourites LEFT JOIN favourite_categories ON favourite_categories.category_id = favourites.category_id WHERE favourites.manga_id = :mangaId AND favourites.deleted_at = 0")
|
||||||
|
abstract fun observeCategories(mangaId: Long): Flow<List<FavouriteCategoryEntity>>
|
||||||
|
|
||||||
|
@Query("SELECT DISTINCT category_id FROM favourites WHERE manga_id IN (:mangaIds) AND deleted_at = 0 ORDER BY favourites.created_at ASC")
|
||||||
|
abstract suspend fun findCategoriesIds(mangaIds: Collection<Long>): List<Long>
|
||||||
|
|
||||||
|
@Query("SELECT DISTINCT favourite_categories.category_id FROM favourites LEFT JOIN favourite_categories ON favourites.category_id = favourite_categories.category_id WHERE manga_id = :mangaId AND favourites.deleted_at = 0 AND favourite_categories.deleted_at = 0 AND favourite_categories.track = 1")
|
||||||
|
abstract suspend fun findCategoriesIdsWithTrack(mangaId: Long): List<Long>
|
||||||
|
|
||||||
|
/** INSERT **/
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
abstract suspend fun insert(favourite: FavouriteEntity)
|
||||||
|
|
||||||
|
/** DELETE **/
|
||||||
|
|
||||||
|
suspend fun delete(mangaId: Long) = setDeletedAt(
|
||||||
|
mangaId = mangaId,
|
||||||
|
deletedAt = System.currentTimeMillis(),
|
||||||
|
)
|
||||||
|
|
||||||
|
suspend fun delete(mangaId: Long, categoryId: Long) = setDeletedAt(
|
||||||
|
categoryId = categoryId,
|
||||||
|
mangaId = mangaId,
|
||||||
|
deletedAt = System.currentTimeMillis(),
|
||||||
|
)
|
||||||
|
|
||||||
|
suspend fun deleteAll(categoryId: Long) = setDeletedAtAll(
|
||||||
|
categoryId = categoryId,
|
||||||
|
deletedAt = System.currentTimeMillis(),
|
||||||
|
)
|
||||||
|
|
||||||
|
suspend fun recover(mangaId: Long) = setDeletedAt(
|
||||||
|
mangaId = mangaId,
|
||||||
|
deletedAt = 0L,
|
||||||
|
)
|
||||||
|
|
||||||
|
suspend fun recover(categoryId: Long, mangaId: Long) = setDeletedAt(
|
||||||
|
categoryId = categoryId,
|
||||||
|
mangaId = mangaId,
|
||||||
|
deletedAt = 0L,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Query("DELETE FROM favourites WHERE deleted_at != 0 AND deleted_at < :maxDeletionTime")
|
||||||
|
abstract suspend fun gc(maxDeletionTime: Long)
|
||||||
|
|
||||||
|
/** TOOLS **/
|
||||||
|
|
||||||
|
@Upsert
|
||||||
|
abstract suspend fun upsert(entity: FavouriteEntity)
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
@RawQuery(observedEntities = [FavouriteEntity::class])
|
||||||
|
protected abstract fun observeAllImpl(query: SupportSQLiteQuery): Flow<List<FavouriteManga>>
|
||||||
|
|
||||||
|
@Query("UPDATE favourites SET deleted_at = :deletedAt WHERE manga_id = :mangaId")
|
||||||
|
protected abstract suspend fun setDeletedAt(mangaId: Long, deletedAt: Long)
|
||||||
|
|
||||||
|
@Query("UPDATE favourites SET deleted_at = :deletedAt WHERE manga_id = :mangaId AND category_id = :categoryId")
|
||||||
|
abstract suspend fun setDeletedAt(categoryId: Long, mangaId: Long, deletedAt: Long)
|
||||||
|
|
||||||
|
@Query("UPDATE favourites SET deleted_at = :deletedAt WHERE category_id = :categoryId AND deleted_at = 0")
|
||||||
|
protected abstract suspend fun setDeletedAtAll(categoryId: Long, deletedAt: Long)
|
||||||
|
|
||||||
|
private fun getOrderBy(sortOrder: ListSortOrder) = when (sortOrder) {
|
||||||
|
ListSortOrder.NEWEST -> "favourites.created_at DESC"
|
||||||
|
ListSortOrder.ALPHABETIC -> "manga.title ASC"
|
||||||
|
ListSortOrder.PROGRESS -> "IFNULL((SELECT percent FROM history WHERE history.manga_id = manga.manga_id), 0) DESC"
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user