Update Kotatsu parsers lib
This commit is contained in:
275
.idea/other.xml
generated
275
.idea/other.xml
generated
@@ -4,279 +4,6 @@
|
|||||||
<option name="frameScreenshot" value="true" />
|
<option name="frameScreenshot" value="true" />
|
||||||
</component>
|
</component>
|
||||||
<component name="direct_access_persist.xml">
|
<component name="direct_access_persist.xml">
|
||||||
<option name="deviceSelectionList">
|
<option name="selectedCloudProject" />
|
||||||
<list>
|
|
||||||
<PersistentDeviceSelectionData>
|
|
||||||
<option name="api" value="33" />
|
|
||||||
<option name="brand" value="samsung" />
|
|
||||||
<option name="codename" value="b0q" />
|
|
||||||
<option name="id" value="b0q" />
|
|
||||||
<option name="manufacturer" value="Samsung" />
|
|
||||||
<option name="name" value="Galaxy S22 Ultra" />
|
|
||||||
<option name="screenDensity" value="600" />
|
|
||||||
<option name="screenX" value="1440" />
|
|
||||||
<option name="screenY" value="3088" />
|
|
||||||
<option name="selected" value="true" />
|
|
||||||
</PersistentDeviceSelectionData>
|
|
||||||
<PersistentDeviceSelectionData>
|
|
||||||
<option name="api" value="33" />
|
|
||||||
<option name="brand" value="google" />
|
|
||||||
<option name="codename" value="felix" />
|
|
||||||
<option name="id" value="felix" />
|
|
||||||
<option name="manufacturer" value="Google" />
|
|
||||||
<option name="name" value="Pixel Fold" />
|
|
||||||
<option name="screenDensity" value="420" />
|
|
||||||
<option name="screenX" value="2208" />
|
|
||||||
<option name="screenY" value="1840" />
|
|
||||||
<option name="selected" value="true" />
|
|
||||||
</PersistentDeviceSelectionData>
|
|
||||||
<PersistentDeviceSelectionData>
|
|
||||||
<option name="api" value="33" />
|
|
||||||
<option name="brand" value="samsung" />
|
|
||||||
<option name="codename" value="gts8uwifi" />
|
|
||||||
<option name="id" value="gts8uwifi" />
|
|
||||||
<option name="manufacturer" value="Samsung" />
|
|
||||||
<option name="name" value="Galaxy Tab S8 Ultra" />
|
|
||||||
<option name="screenDensity" value="320" />
|
|
||||||
<option name="screenX" value="1848" />
|
|
||||||
<option name="screenY" value="2960" />
|
|
||||||
<option name="selected" value="true" />
|
|
||||||
</PersistentDeviceSelectionData>
|
|
||||||
<PersistentDeviceSelectionData>
|
|
||||||
<option name="api" value="34" />
|
|
||||||
<option name="brand" value="google" />
|
|
||||||
<option name="codename" value="shiba" />
|
|
||||||
<option name="id" value="shiba" />
|
|
||||||
<option name="manufacturer" value="Google" />
|
|
||||||
<option name="name" value="Pixel 8" />
|
|
||||||
<option name="screenDensity" value="420" />
|
|
||||||
<option name="screenX" value="1080" />
|
|
||||||
<option name="screenY" value="2400" />
|
|
||||||
<option name="selected" value="true" />
|
|
||||||
</PersistentDeviceSelectionData>
|
|
||||||
<PersistentDeviceSelectionData>
|
|
||||||
<option name="api" value="27" />
|
|
||||||
<option name="brand" value="DOCOMO" />
|
|
||||||
<option name="codename" value="F01L" />
|
|
||||||
<option name="id" value="F01L" />
|
|
||||||
<option name="manufacturer" value="FUJITSU" />
|
|
||||||
<option name="name" value="F-01L" />
|
|
||||||
<option name="screenDensity" value="360" />
|
|
||||||
<option name="screenX" value="720" />
|
|
||||||
<option name="screenY" value="1280" />
|
|
||||||
</PersistentDeviceSelectionData>
|
|
||||||
<PersistentDeviceSelectionData>
|
|
||||||
<option name="api" value="28" />
|
|
||||||
<option name="brand" value="DOCOMO" />
|
|
||||||
<option name="codename" value="SH-01L" />
|
|
||||||
<option name="id" value="SH-01L" />
|
|
||||||
<option name="manufacturer" value="SHARP" />
|
|
||||||
<option name="name" value="AQUOS sense2 SH-01L" />
|
|
||||||
<option name="screenDensity" value="480" />
|
|
||||||
<option name="screenX" value="1080" />
|
|
||||||
<option name="screenY" value="2160" />
|
|
||||||
</PersistentDeviceSelectionData>
|
|
||||||
<PersistentDeviceSelectionData>
|
|
||||||
<option name="api" value="31" />
|
|
||||||
<option name="brand" value="samsung" />
|
|
||||||
<option name="codename" value="a51" />
|
|
||||||
<option name="id" value="a51" />
|
|
||||||
<option name="manufacturer" value="Samsung" />
|
|
||||||
<option name="name" value="Galaxy A51" />
|
|
||||||
<option name="screenDensity" value="420" />
|
|
||||||
<option name="screenX" value="1080" />
|
|
||||||
<option name="screenY" value="2400" />
|
|
||||||
</PersistentDeviceSelectionData>
|
|
||||||
<PersistentDeviceSelectionData>
|
|
||||||
<option name="api" value="34" />
|
|
||||||
<option name="brand" value="google" />
|
|
||||||
<option name="codename" value="akita" />
|
|
||||||
<option name="id" value="akita" />
|
|
||||||
<option name="manufacturer" value="Google" />
|
|
||||||
<option name="name" value="Pixel 8a" />
|
|
||||||
<option name="screenDensity" value="420" />
|
|
||||||
<option name="screenX" value="1080" />
|
|
||||||
<option name="screenY" value="2400" />
|
|
||||||
</PersistentDeviceSelectionData>
|
|
||||||
<PersistentDeviceSelectionData>
|
|
||||||
<option name="api" value="32" />
|
|
||||||
<option name="brand" value="google" />
|
|
||||||
<option name="codename" value="bluejay" />
|
|
||||||
<option name="id" value="bluejay" />
|
|
||||||
<option name="manufacturer" value="Google" />
|
|
||||||
<option name="name" value="Pixel 6a" />
|
|
||||||
<option name="screenDensity" value="420" />
|
|
||||||
<option name="screenX" value="1080" />
|
|
||||||
<option name="screenY" value="2400" />
|
|
||||||
</PersistentDeviceSelectionData>
|
|
||||||
<PersistentDeviceSelectionData>
|
|
||||||
<option name="api" value="29" />
|
|
||||||
<option name="brand" value="samsung" />
|
|
||||||
<option name="codename" value="crownqlteue" />
|
|
||||||
<option name="id" value="crownqlteue" />
|
|
||||||
<option name="manufacturer" value="Samsung" />
|
|
||||||
<option name="name" value="Galaxy Note9" />
|
|
||||||
<option name="screenDensity" value="420" />
|
|
||||||
<option name="screenX" value="2220" />
|
|
||||||
<option name="screenY" value="1080" />
|
|
||||||
</PersistentDeviceSelectionData>
|
|
||||||
<PersistentDeviceSelectionData>
|
|
||||||
<option name="api" value="34" />
|
|
||||||
<option name="brand" value="samsung" />
|
|
||||||
<option name="codename" value="dm3q" />
|
|
||||||
<option name="id" value="dm3q" />
|
|
||||||
<option name="manufacturer" value="Samsung" />
|
|
||||||
<option name="name" value="Galaxy S23 Ultra" />
|
|
||||||
<option name="screenDensity" value="600" />
|
|
||||||
<option name="screenX" value="1440" />
|
|
||||||
<option name="screenY" value="3088" />
|
|
||||||
</PersistentDeviceSelectionData>
|
|
||||||
<PersistentDeviceSelectionData>
|
|
||||||
<option name="api" value="33" />
|
|
||||||
<option name="brand" value="google" />
|
|
||||||
<option name="codename" value="felix_camera" />
|
|
||||||
<option name="id" value="felix_camera" />
|
|
||||||
<option name="manufacturer" value="Google" />
|
|
||||||
<option name="name" value="Pixel Fold (Camera-enabled)" />
|
|
||||||
<option name="screenDensity" value="420" />
|
|
||||||
<option name="screenX" value="2208" />
|
|
||||||
<option name="screenY" value="1840" />
|
|
||||||
</PersistentDeviceSelectionData>
|
|
||||||
<PersistentDeviceSelectionData>
|
|
||||||
<option name="api" value="34" />
|
|
||||||
<option name="brand" value="google" />
|
|
||||||
<option name="codename" value="husky" />
|
|
||||||
<option name="id" value="husky" />
|
|
||||||
<option name="manufacturer" value="Google" />
|
|
||||||
<option name="name" value="Pixel 8 Pro" />
|
|
||||||
<option name="screenDensity" value="390" />
|
|
||||||
<option name="screenX" value="1008" />
|
|
||||||
<option name="screenY" value="2244" />
|
|
||||||
</PersistentDeviceSelectionData>
|
|
||||||
<PersistentDeviceSelectionData>
|
|
||||||
<option name="api" value="30" />
|
|
||||||
<option name="brand" value="motorola" />
|
|
||||||
<option name="codename" value="java" />
|
|
||||||
<option name="id" value="java" />
|
|
||||||
<option name="manufacturer" value="Motorola" />
|
|
||||||
<option name="name" value="G20" />
|
|
||||||
<option name="screenDensity" value="280" />
|
|
||||||
<option name="screenX" value="720" />
|
|
||||||
<option name="screenY" value="1600" />
|
|
||||||
</PersistentDeviceSelectionData>
|
|
||||||
<PersistentDeviceSelectionData>
|
|
||||||
<option name="api" value="33" />
|
|
||||||
<option name="brand" value="google" />
|
|
||||||
<option name="codename" value="lynx" />
|
|
||||||
<option name="id" value="lynx" />
|
|
||||||
<option name="manufacturer" value="Google" />
|
|
||||||
<option name="name" value="Pixel 7a" />
|
|
||||||
<option name="screenDensity" value="420" />
|
|
||||||
<option name="screenX" value="1080" />
|
|
||||||
<option name="screenY" value="2400" />
|
|
||||||
</PersistentDeviceSelectionData>
|
|
||||||
<PersistentDeviceSelectionData>
|
|
||||||
<option name="api" value="31" />
|
|
||||||
<option name="brand" value="google" />
|
|
||||||
<option name="codename" value="oriole" />
|
|
||||||
<option name="id" value="oriole" />
|
|
||||||
<option name="manufacturer" value="Google" />
|
|
||||||
<option name="name" value="Pixel 6" />
|
|
||||||
<option name="screenDensity" value="420" />
|
|
||||||
<option name="screenX" value="1080" />
|
|
||||||
<option name="screenY" value="2400" />
|
|
||||||
</PersistentDeviceSelectionData>
|
|
||||||
<PersistentDeviceSelectionData>
|
|
||||||
<option name="api" value="33" />
|
|
||||||
<option name="brand" value="google" />
|
|
||||||
<option name="codename" value="panther" />
|
|
||||||
<option name="id" value="panther" />
|
|
||||||
<option name="manufacturer" value="Google" />
|
|
||||||
<option name="name" value="Pixel 7" />
|
|
||||||
<option name="screenDensity" value="420" />
|
|
||||||
<option name="screenX" value="1080" />
|
|
||||||
<option name="screenY" value="2400" />
|
|
||||||
</PersistentDeviceSelectionData>
|
|
||||||
<PersistentDeviceSelectionData>
|
|
||||||
<option name="api" value="31" />
|
|
||||||
<option name="brand" value="samsung" />
|
|
||||||
<option name="codename" value="q2q" />
|
|
||||||
<option name="id" value="q2q" />
|
|
||||||
<option name="manufacturer" value="Samsung" />
|
|
||||||
<option name="name" value="Galaxy Z Fold3" />
|
|
||||||
<option name="screenDensity" value="420" />
|
|
||||||
<option name="screenX" value="1768" />
|
|
||||||
<option name="screenY" value="2208" />
|
|
||||||
</PersistentDeviceSelectionData>
|
|
||||||
<PersistentDeviceSelectionData>
|
|
||||||
<option name="api" value="34" />
|
|
||||||
<option name="brand" value="samsung" />
|
|
||||||
<option name="codename" value="q5q" />
|
|
||||||
<option name="id" value="q5q" />
|
|
||||||
<option name="manufacturer" value="Samsung" />
|
|
||||||
<option name="name" value="Galaxy Z Fold5" />
|
|
||||||
<option name="screenDensity" value="420" />
|
|
||||||
<option name="screenX" value="1812" />
|
|
||||||
<option name="screenY" value="2176" />
|
|
||||||
</PersistentDeviceSelectionData>
|
|
||||||
<PersistentDeviceSelectionData>
|
|
||||||
<option name="api" value="30" />
|
|
||||||
<option name="brand" value="google" />
|
|
||||||
<option name="codename" value="r11" />
|
|
||||||
<option name="id" value="r11" />
|
|
||||||
<option name="manufacturer" value="Google" />
|
|
||||||
<option name="name" value="Pixel Watch" />
|
|
||||||
<option name="screenDensity" value="320" />
|
|
||||||
<option name="screenX" value="384" />
|
|
||||||
<option name="screenY" value="384" />
|
|
||||||
<option name="type" value="WEAR_OS" />
|
|
||||||
</PersistentDeviceSelectionData>
|
|
||||||
<PersistentDeviceSelectionData>
|
|
||||||
<option name="api" value="30" />
|
|
||||||
<option name="brand" value="google" />
|
|
||||||
<option name="codename" value="redfin" />
|
|
||||||
<option name="id" value="redfin" />
|
|
||||||
<option name="manufacturer" value="Google" />
|
|
||||||
<option name="name" value="Pixel 5" />
|
|
||||||
<option name="screenDensity" value="440" />
|
|
||||||
<option name="screenX" value="1080" />
|
|
||||||
<option name="screenY" value="2340" />
|
|
||||||
</PersistentDeviceSelectionData>
|
|
||||||
<PersistentDeviceSelectionData>
|
|
||||||
<option name="api" value="35" />
|
|
||||||
<option name="brand" value="google" />
|
|
||||||
<option name="codename" value="shiba_beta" />
|
|
||||||
<option name="id" value="shiba_beta" />
|
|
||||||
<option name="manufacturer" value="Google" />
|
|
||||||
<option name="name" value="Pixel 8" />
|
|
||||||
<option name="screenDensity" value="420" />
|
|
||||||
<option name="screenX" value="1080" />
|
|
||||||
<option name="screenY" value="2400" />
|
|
||||||
</PersistentDeviceSelectionData>
|
|
||||||
<PersistentDeviceSelectionData>
|
|
||||||
<option name="api" value="33" />
|
|
||||||
<option name="brand" value="google" />
|
|
||||||
<option name="codename" value="tangorpro" />
|
|
||||||
<option name="id" value="tangorpro" />
|
|
||||||
<option name="manufacturer" value="Google" />
|
|
||||||
<option name="name" value="Pixel Tablet" />
|
|
||||||
<option name="screenDensity" value="320" />
|
|
||||||
<option name="screenX" value="1600" />
|
|
||||||
<option name="screenY" value="2560" />
|
|
||||||
</PersistentDeviceSelectionData>
|
|
||||||
<PersistentDeviceSelectionData>
|
|
||||||
<option name="api" value="29" />
|
|
||||||
<option name="brand" value="samsung" />
|
|
||||||
<option name="codename" value="x1q" />
|
|
||||||
<option name="id" value="x1q" />
|
|
||||||
<option name="manufacturer" value="Samsung" />
|
|
||||||
<option name="name" value="Galaxy S20" />
|
|
||||||
<option name="screenDensity" value="480" />
|
|
||||||
<option name="screenX" value="1440" />
|
|
||||||
<option name="screenY" value="3200" />
|
|
||||||
</PersistentDeviceSelectionData>
|
|
||||||
</list>
|
|
||||||
</option>
|
|
||||||
<option name="selectedCloudProject" value="api-7108673381507456403-50668" />
|
|
||||||
</component>
|
</component>
|
||||||
</project>
|
</project>
|
||||||
@@ -173,7 +173,7 @@ dependencies {
|
|||||||
kapt("com.google.dagger:hilt-compiler:2.51.1")
|
kapt("com.google.dagger:hilt-compiler:2.51.1")
|
||||||
implementation("androidx.hilt:hilt-work:1.2.0")
|
implementation("androidx.hilt:hilt-work:1.2.0")
|
||||||
kapt("androidx.hilt:hilt-compiler:1.2.0")
|
kapt("androidx.hilt:hilt-compiler:1.2.0")
|
||||||
implementation("com.github.KotatsuApp:kotatsu-parsers:7d2f5696f5") {
|
implementation("com.github.KotatsuApp:kotatsu-parsers:b404b44008") {
|
||||||
exclude(group = "org.json", module = "json")
|
exclude(group = "org.json", module = "json")
|
||||||
}
|
}
|
||||||
implementation("com.mikepenz:aboutlibraries-compose-m3:10.10.0")
|
implementation("com.mikepenz:aboutlibraries-compose-m3:10.10.0")
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
"formatVersion": 1,
|
"formatVersion": 1,
|
||||||
"database": {
|
"database": {
|
||||||
"version": 4,
|
"version": 4,
|
||||||
"identityHash": "dbe1dcac0f49c5ae2ac88d88aa280081",
|
"identityHash": "90b73386d5c61c2ddf46d6354ca2f1b6",
|
||||||
"entities": [
|
"entities": [
|
||||||
{
|
{
|
||||||
"tableName": "manga",
|
"tableName": "manga",
|
||||||
@@ -199,7 +199,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"tableName": "sources",
|
"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`))",
|
"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": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"fieldPath": "source",
|
"fieldPath": "source",
|
||||||
@@ -218,6 +218,24 @@
|
|||||||
"columnName": "sort_key",
|
"columnName": "sort_key",
|
||||||
"affinity": "INTEGER",
|
"affinity": "INTEGER",
|
||||||
"notNull": true
|
"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": {
|
"primaryKey": {
|
||||||
@@ -851,7 +869,7 @@
|
|||||||
"views": [],
|
"views": [],
|
||||||
"setupQueries": [
|
"setupQueries": [
|
||||||
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
|
"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')"
|
"INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '90b73386d5c61c2ddf46d6354ca2f1b6')"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -15,6 +15,10 @@
|
|||||||
<uses-permission
|
<uses-permission
|
||||||
android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
|
android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
|
||||||
tools:ignore="ScopedStorage" />
|
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>
|
<queries>
|
||||||
<intent>
|
<intent>
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ import org.xtimms.shirizu.sections.feed.FeedScreen
|
|||||||
import org.xtimms.shirizu.sections.history.HistoryTab
|
import org.xtimms.shirizu.sections.history.HistoryTab
|
||||||
import org.xtimms.shirizu.sections.library.LibraryTab
|
import org.xtimms.shirizu.sections.library.LibraryTab
|
||||||
import org.xtimms.shirizu.sections.onboarding.OnboardingScreen
|
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.search.SearchTab
|
||||||
import org.xtimms.shirizu.sections.settings.SettingsScreen
|
import org.xtimms.shirizu.sections.settings.SettingsScreen
|
||||||
import org.xtimms.shirizu.sections.shelf.ShelfTab
|
import org.xtimms.shirizu.sections.shelf.ShelfTab
|
||||||
@@ -301,7 +302,6 @@ class MainActivity : ComponentActivity() {
|
|||||||
@OptIn(ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
object MainScreen : Screen() {
|
object MainScreen : Screen() {
|
||||||
|
|
||||||
private val librarySearchEvent = Channel<String>()
|
|
||||||
private val openTabEvent = Channel<Tab>()
|
private val openTabEvent = Channel<Tab>()
|
||||||
private val showBottomNavEvent = Channel<Boolean>()
|
private val showBottomNavEvent = Channel<Boolean>()
|
||||||
|
|
||||||
@@ -312,7 +312,8 @@ object MainScreen : Screen() {
|
|||||||
// ShelfTab,
|
// ShelfTab,
|
||||||
// HistoryTab,
|
// HistoryTab,
|
||||||
ExploreTab(),
|
ExploreTab(),
|
||||||
SearchTab
|
SearchTab,
|
||||||
|
// ProfileTab
|
||||||
)
|
)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -331,13 +332,6 @@ object MainScreen : Screen() {
|
|||||||
actions = {
|
actions = {
|
||||||
AppBarActions(
|
AppBarActions(
|
||||||
persistentListOf(
|
persistentListOf(
|
||||||
AppBar.Action(
|
|
||||||
title = stringResource(R.string.suggestions),
|
|
||||||
icon = Icons.Outlined.Creation,
|
|
||||||
onClick = {
|
|
||||||
navigator.push(SuggestionsScreen)
|
|
||||||
},
|
|
||||||
),
|
|
||||||
AppBar.Action(
|
AppBar.Action(
|
||||||
title = stringResource(R.string.feed),
|
title = stringResource(R.string.feed),
|
||||||
icon = Icons.Outlined.RssFeed,
|
icon = Icons.Outlined.RssFeed,
|
||||||
@@ -425,6 +419,7 @@ object MainScreen : Screen() {
|
|||||||
// is Tab.History -> HistoryTab
|
// is Tab.History -> HistoryTab
|
||||||
is Tab.Explore -> ExploreTab()
|
is Tab.Explore -> ExploreTab()
|
||||||
is Tab.Search -> SearchTab
|
is Tab.Search -> SearchTab
|
||||||
|
// is Tab.Profile -> ProfileTab
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -508,5 +503,6 @@ object MainScreen : Screen() {
|
|||||||
// data object History : Tab
|
// data object History : Tab
|
||||||
data object Explore : Tab
|
data object Explore : Tab
|
||||||
data object Search : Tab
|
data object Search : Tab
|
||||||
|
// data object Profile : Tab
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
package org.xtimms.shirizu.core.cache
|
|
||||||
|
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
|
||||||
|
|
||||||
interface ContentCache {
|
|
||||||
|
|
||||||
val isCachingEnabled: Boolean
|
|
||||||
|
|
||||||
suspend fun getDetails(source: MangaSource, url: String): Manga?
|
|
||||||
|
|
||||||
fun putDetails(source: MangaSource, url: String, details: SafeDeferred<Manga>)
|
|
||||||
|
|
||||||
suspend fun getPages(source: MangaSource, url: String): List<MangaPage>?
|
|
||||||
|
|
||||||
fun putPages(source: MangaSource, url: String, pages: SafeDeferred<List<MangaPage>>)
|
|
||||||
|
|
||||||
suspend fun getRelatedManga(source: MangaSource, url: String): List<Manga>?
|
|
||||||
|
|
||||||
fun putRelatedManga(source: MangaSource, url: String, related: SafeDeferred<List<Manga>>)
|
|
||||||
|
|
||||||
data class Key(
|
|
||||||
val source: MangaSource,
|
|
||||||
val url: String,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -2,16 +2,19 @@ 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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,42 +6,54 @@ import android.content.res.Configuration
|
|||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
|
import org.xtimms.shirizu.utils.system.isLowRamDevice
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
class MemoryContentCache(application: Application) : ContentCache, ComponentCallbacks2 {
|
@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 {
|
init {
|
||||||
application.registerComponentCallbacks(this)
|
application.registerComponentCallbacks(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val detailsCache = ExpiringLruCache<SafeDeferred<Manga>>(4, 5, TimeUnit.MINUTES)
|
suspend fun getDetails(source: MangaSource, url: String): Manga? {
|
||||||
private val pagesCache = ExpiringLruCache<SafeDeferred<List<MangaPage>>>(4, 10, TimeUnit.MINUTES)
|
return detailsCache[Key(source, url)]?.awaitOrNull()
|
||||||
private val relatedMangaCache = ExpiringLruCache<SafeDeferred<List<Manga>>>(4, 10, TimeUnit.MINUTES)
|
|
||||||
|
|
||||||
override val isCachingEnabled: Boolean = true
|
|
||||||
|
|
||||||
override suspend fun getDetails(source: MangaSource, url: String): Manga? {
|
|
||||||
return detailsCache[ContentCache.Key(source, url)]?.awaitOrNull()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun putDetails(source: MangaSource, url: String, details: SafeDeferred<Manga>) {
|
fun putDetails(source: MangaSource, url: String, details: SafeDeferred<Manga>) {
|
||||||
detailsCache[ContentCache.Key(source, url)] = details
|
detailsCache[Key(source, url)] = details
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getPages(source: MangaSource, url: String): List<MangaPage>? {
|
suspend fun getPages(source: MangaSource, url: String): List<MangaPage>? {
|
||||||
return pagesCache[ContentCache.Key(source, url)]?.awaitOrNull()
|
return pagesCache[Key(source, url)]?.awaitOrNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun putPages(source: MangaSource, url: String, pages: SafeDeferred<List<MangaPage>>) {
|
fun putPages(source: MangaSource, url: String, pages: SafeDeferred<List<MangaPage>>) {
|
||||||
pagesCache[ContentCache.Key(source, url)] = pages
|
pagesCache[Key(source, url)] = pages
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getRelatedManga(source: MangaSource, url: String): List<Manga>? {
|
suspend fun getRelatedManga(source: MangaSource, url: String): List<Manga>? {
|
||||||
return relatedMangaCache[ContentCache.Key(source, url)]?.awaitOrNull()
|
return relatedMangaCache[Key(source, url)]?.awaitOrNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun putRelatedManga(source: MangaSource, url: String, related: SafeDeferred<List<Manga>>) {
|
fun putRelatedManga(source: MangaSource, url: String, related: SafeDeferred<List<Manga>>) {
|
||||||
relatedMangaCache[ContentCache.Key(source, url)] = related
|
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 onConfigurationChanged(newConfig: Configuration) = Unit
|
||||||
@@ -67,4 +79,17 @@ class MemoryContentCache(application: Application) : ContentCache, ComponentCall
|
|||||||
else -> cache.trimToSize(cache.maxSize / 2)
|
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,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
package org.xtimms.shirizu.core.cache
|
|
||||||
|
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
|
||||||
|
|
||||||
class StubContentCache : ContentCache {
|
|
||||||
|
|
||||||
override val isCachingEnabled: Boolean = false
|
|
||||||
|
|
||||||
override suspend fun getDetails(source: MangaSource, url: String): Manga? = null
|
|
||||||
|
|
||||||
override fun putDetails(source: MangaSource, url: String, details: SafeDeferred<Manga>) = Unit
|
|
||||||
|
|
||||||
override suspend fun getPages(source: MangaSource, url: String): List<MangaPage>? = null
|
|
||||||
|
|
||||||
override fun putPages(source: MangaSource, url: String, pages: SafeDeferred<List<MangaPage>>) = Unit
|
|
||||||
|
|
||||||
override suspend fun getRelatedManga(source: MangaSource, url: String): List<Manga>? = null
|
|
||||||
|
|
||||||
override fun putRelatedManga(source: MangaSource, url: String, related: SafeDeferred<List<Manga>>) = Unit
|
|
||||||
}
|
|
||||||
@@ -54,7 +54,7 @@ import java.time.Instant
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun RowScope.ReadButton(
|
fun RowScope.ReadButton(
|
||||||
info: HistoryInfo,
|
info: HistoryInfo?,
|
||||||
estimatedReadTime: String
|
estimatedReadTime: String
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@@ -63,13 +63,13 @@ fun RowScope.ReadButton(
|
|||||||
|
|
||||||
val animatedCardContainerColor = animateColorAsState(
|
val animatedCardContainerColor = animateColorAsState(
|
||||||
label = "animatedCardContainerColor",
|
label = "animatedCardContainerColor",
|
||||||
targetValue = if (info.totalChapters == 0) MaterialTheme.colorScheme.errorContainer else MaterialTheme.colorScheme.primaryContainer,
|
targetValue = if (info?.totalChapters == 0) MaterialTheme.colorScheme.errorContainer else MaterialTheme.colorScheme.primaryContainer,
|
||||||
animationSpec = TweenSpec(500)
|
animationSpec = TweenSpec(500)
|
||||||
).value
|
).value
|
||||||
|
|
||||||
val animatedCardContentColor = animateColorAsState(
|
val animatedCardContentColor = animateColorAsState(
|
||||||
label = "animatedCardContentColor",
|
label = "animatedCardContentColor",
|
||||||
targetValue = if (info.totalChapters == 0) MaterialTheme.colorScheme.onErrorContainer else MaterialTheme.colorScheme.onPrimaryContainer,
|
targetValue = if (info?.totalChapters == 0) MaterialTheme.colorScheme.onErrorContainer else MaterialTheme.colorScheme.onPrimaryContainer,
|
||||||
animationSpec = TweenSpec(500)
|
animationSpec = TweenSpec(500)
|
||||||
).value
|
).value
|
||||||
|
|
||||||
@@ -105,9 +105,9 @@ fun RowScope.ReadButton(
|
|||||||
contentAlignment = Alignment.CenterEnd,
|
contentAlignment = Alignment.CenterEnd,
|
||||||
) {
|
) {
|
||||||
BackgroundProgress(
|
BackgroundProgress(
|
||||||
if (info.totalChapters == 0) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.primary,
|
if (info?.totalChapters == 0) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.primary,
|
||||||
if (!info.isValid) 0.1f else 0.33f,
|
if (info?.isValid == true) 0.1f else 0.33f,
|
||||||
info.history?.percent?.coerceIn(0f, 1f) ?: 0f
|
info?.history?.percent?.coerceIn(0f, 1f) ?: 0f
|
||||||
)
|
)
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -143,7 +143,8 @@ fun RowScope.ReadButton(
|
|||||||
animationSpec = infiniteRepeatable(tween(15000), RepeatMode.Restart)
|
animationSpec = infiniteRepeatable(tween(15000), RepeatMode.Restart)
|
||||||
)
|
)
|
||||||
val chaptersSubtitle = when {
|
val chaptersSubtitle = when {
|
||||||
!info.isValid -> stringResource(R.string.loading_)
|
info == null -> "null"
|
||||||
|
info.isValid -> stringResource(R.string.loading_)
|
||||||
info.currentChapter >= 0 -> when (infiniteTransition) {
|
info.currentChapter >= 0 -> when (infiniteTransition) {
|
||||||
1 -> stringResource(
|
1 -> stringResource(
|
||||||
R.string.chapter_d_of_d,
|
R.string.chapter_d_of_d,
|
||||||
@@ -162,7 +163,7 @@ fun RowScope.ReadButton(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
Text(
|
Text(
|
||||||
text = if (info.history != null) {
|
text = if (info?.history != null) {
|
||||||
stringResource(R.string.continue_reading)
|
stringResource(R.string.continue_reading)
|
||||||
} else {
|
} else {
|
||||||
stringResource(R.string.read)
|
stringResource(R.string.read)
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
package org.xtimms.shirizu.core.components
|
package org.xtimms.shirizu.core.components
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedContent
|
||||||
import androidx.compose.animation.animateContentSize
|
import androidx.compose.animation.animateContentSize
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.automirrored.filled.Sort
|
import androidx.compose.material.icons.outlined.ArrowDropDown
|
||||||
import androidx.compose.material.icons.filled.ArrowDropDown
|
import androidx.compose.material.icons.outlined.ArrowDropUp
|
||||||
import androidx.compose.material3.DropdownMenu
|
import androidx.compose.material3.DropdownMenu
|
||||||
import androidx.compose.material3.FilterChip
|
import androidx.compose.material3.FilterChip
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
@@ -30,28 +31,33 @@ fun SortChip(
|
|||||||
Box(modifier) {
|
Box(modifier) {
|
||||||
var expanded by remember { mutableStateOf(false) }
|
var expanded by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
val arrowDrop = if (expanded) Icons.Outlined.ArrowDropUp else Icons.Outlined.ArrowDropDown
|
||||||
|
|
||||||
FilterChip(
|
FilterChip(
|
||||||
selected = true,
|
selected = true,
|
||||||
onClick = { expanded = true },
|
onClick = { expanded = true },
|
||||||
label = {
|
label = {
|
||||||
Text(
|
AnimatedContent(targetState = currentSortOption.label(LocalContext.current.resources), label = "Text") {
|
||||||
text = currentSortOption.label(LocalContext.current.resources),
|
Text(text = it)
|
||||||
modifier = Modifier.animateContentSize(),
|
}
|
||||||
)
|
|
||||||
},
|
},
|
||||||
leadingIcon = {
|
leadingIcon = {
|
||||||
|
AnimatedContent(targetState = currentSortOption.icon(), label = "Icon") {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.AutoMirrored.Filled.Sort,
|
imageVector = it,
|
||||||
contentDescription = null, // decorative
|
contentDescription = null, // decorative
|
||||||
modifier = Modifier.size(16.dp),
|
modifier = Modifier.size(16.dp),
|
||||||
)
|
)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
trailingIcon = {
|
trailingIcon = {
|
||||||
|
AnimatedContent(targetState = arrowDrop, label = "Arrow drop") {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.ArrowDropDown,
|
imageVector = it,
|
||||||
contentDescription = null, // decorative
|
contentDescription = null, // decorative
|
||||||
modifier = Modifier.size(16.dp),
|
modifier = Modifier.size(16.dp),
|
||||||
)
|
)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -2,10 +2,15 @@ package org.xtimms.shirizu.core.components
|
|||||||
|
|
||||||
import android.content.res.Resources
|
import android.content.res.Resources
|
||||||
import androidx.compose.foundation.layout.ColumnScope
|
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.DropdownMenuItem
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import org.xtimms.shirizu.R
|
import org.xtimms.shirizu.R
|
||||||
@@ -35,5 +40,12 @@ internal fun ColumnScope.SortDropdownMenuContent(
|
|||||||
|
|
||||||
internal fun SortOption.label(resources: Resources): String = when (this) {
|
internal fun SortOption.label(resources: Resources): String = when (this) {
|
||||||
SortOption.ALPHABETICAL -> resources.getString(R.string.sort_alphabetically)
|
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)
|
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
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ import androidx.sqlite.db.SimpleSQLiteQuery
|
|||||||
import androidx.sqlite.db.SupportSQLiteQuery
|
import androidx.sqlite.db.SupportSQLiteQuery
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import org.intellij.lang.annotations.Language
|
import org.intellij.lang.annotations.Language
|
||||||
|
import org.xtimms.shirizu.BuildConfig
|
||||||
import org.xtimms.shirizu.core.database.entity.MangaSourceEntity
|
import org.xtimms.shirizu.core.database.entity.MangaSourceEntity
|
||||||
import org.xtimms.shirizu.sections.explore.data.SourcesSortOrder
|
import org.xtimms.shirizu.sections.explore.data.SourcesSortOrder
|
||||||
|
|
||||||
@@ -65,6 +66,9 @@ abstract class MangaSourcesDao {
|
|||||||
source = source,
|
source = source,
|
||||||
isEnabled = isEnabled,
|
isEnabled = isEnabled,
|
||||||
sortKey = getMaxSortKey() + 1,
|
sortKey = getMaxSortKey() + 1,
|
||||||
|
addedIn = BuildConfig.VERSION_CODE,
|
||||||
|
lastUsedAt = 0,
|
||||||
|
isPinned = false,
|
||||||
)
|
)
|
||||||
upsert(entity)
|
upsert(entity)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,4 +12,7 @@ data class MangaSourceEntity(
|
|||||||
val source: String,
|
val source: String,
|
||||||
@ColumnInfo(name = "enabled") val isEnabled: Boolean,
|
@ColumnInfo(name = "enabled") val isEnabled: Boolean,
|
||||||
@ColumnInfo(name = "sort_key", index = true) val sortKey: Int,
|
@ColumnInfo(name = "sort_key", index = true) val sortKey: Int,
|
||||||
|
@ColumnInfo(name = "added_in") val addedIn: Int,
|
||||||
|
@ColumnInfo(name = "used_at") val lastUsedAt: Long,
|
||||||
|
@ColumnInfo(name = "pinned") val isPinned: Boolean,
|
||||||
)
|
)
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package org.xtimms.shirizu.core.exceptions
|
||||||
|
|
||||||
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
|
|
||||||
|
class UnsupportedSourceException(
|
||||||
|
message: String?,
|
||||||
|
val manga: Manga?,
|
||||||
|
) : IllegalArgumentException(message)
|
||||||
@@ -3,6 +3,7 @@ package org.xtimms.shirizu.core.model
|
|||||||
import androidx.core.os.LocaleListCompat
|
import androidx.core.os.LocaleListCompat
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaParserSource
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
import org.xtimms.shirizu.utils.system.iterator
|
import org.xtimms.shirizu.utils.system.iterator
|
||||||
import java.text.DecimalFormat
|
import java.text.DecimalFormat
|
||||||
@@ -61,4 +62,4 @@ fun MangaChapter.formatNumber(): String? {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val Manga.isLocal: Boolean
|
val Manga.isLocal: Boolean
|
||||||
get() = source == MangaSource.LOCAL
|
get() = source == LocalMangaSource
|
||||||
@@ -1,13 +1,72 @@
|
|||||||
package org.xtimms.shirizu.core.model
|
package org.xtimms.shirizu.core.model
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.annotation.StringRes
|
||||||
import org.koitharu.kotatsu.parsers.model.ContentType
|
import org.koitharu.kotatsu.parsers.model.ContentType
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaParserSource
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
|
import org.koitharu.kotatsu.parsers.util.splitTwoParts
|
||||||
|
import org.xtimms.shirizu.R
|
||||||
|
import org.xtimms.shirizu.core.parser.external.ExternalMangaSource
|
||||||
|
import org.xtimms.shirizu.utils.system.getDisplayName
|
||||||
|
import org.xtimms.shirizu.utils.system.toLocale
|
||||||
|
|
||||||
fun MangaSource(name: String): MangaSource {
|
data object LocalMangaSource : MangaSource {
|
||||||
MangaSource.entries.forEach {
|
override val name = "LOCAL"
|
||||||
|
}
|
||||||
|
|
||||||
|
data object UnknownMangaSource : MangaSource {
|
||||||
|
override val name = "UNKNOWN"
|
||||||
|
}
|
||||||
|
|
||||||
|
fun MangaSource(name: String?): MangaSource {
|
||||||
|
when (name ?: return UnknownMangaSource) {
|
||||||
|
UnknownMangaSource.name -> return UnknownMangaSource
|
||||||
|
|
||||||
|
LocalMangaSource.name -> return LocalMangaSource
|
||||||
|
}
|
||||||
|
if (name.startsWith("content:")) {
|
||||||
|
val parts = name.substringAfter(':').splitTwoParts('/') ?: return UnknownMangaSource
|
||||||
|
return ExternalMangaSource(packageName = parts.first, authority = parts.second)
|
||||||
|
}
|
||||||
|
MangaParserSource.entries.forEach {
|
||||||
if (it.name == name) return it
|
if (it.name == name) return it
|
||||||
}
|
}
|
||||||
return MangaSource.DUMMY
|
return UnknownMangaSource
|
||||||
}
|
}
|
||||||
|
|
||||||
fun MangaSource.isNsfw() = contentType == ContentType.HENTAI
|
fun MangaSource.isNsfw(): Boolean = when (this) {
|
||||||
|
is MangaSourceInfo -> mangaSource.isNsfw()
|
||||||
|
is MangaParserSource -> contentType == ContentType.HENTAI
|
||||||
|
else -> false
|
||||||
|
}
|
||||||
|
|
||||||
|
@get:StringRes
|
||||||
|
val ContentType.titleResId
|
||||||
|
get() = when (this) {
|
||||||
|
ContentType.MANGA -> R.string.content_type_manga
|
||||||
|
ContentType.HENTAI -> R.string.hentai
|
||||||
|
ContentType.COMICS -> R.string.comics
|
||||||
|
ContentType.OTHER -> R.string.other_source
|
||||||
|
}
|
||||||
|
|
||||||
|
fun MangaSource.getTitle(context: Context): String = when (this) {
|
||||||
|
is MangaSourceInfo -> mangaSource.getTitle(context)
|
||||||
|
is MangaParserSource -> title
|
||||||
|
LocalMangaSource -> context.getString(R.string.local_storage)
|
||||||
|
is ExternalMangaSource -> resolveName(context)
|
||||||
|
else -> context.getString(R.string.unknown)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun MangaSource.getSummary(context: Context): String? = when (this) {
|
||||||
|
is MangaSourceInfo -> mangaSource.getSummary(context)
|
||||||
|
is MangaParserSource -> {
|
||||||
|
val type = context.getString(contentType.titleResId)
|
||||||
|
val locale = locale.toLocale().getDisplayName(context)
|
||||||
|
context.getString(R.string.source_summary_pattern, type, locale)
|
||||||
|
}
|
||||||
|
|
||||||
|
is ExternalMangaSource -> context.getString(R.string.external_source)
|
||||||
|
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package org.xtimms.shirizu.core.model
|
||||||
|
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
|
|
||||||
|
data class MangaSourceInfo(
|
||||||
|
val mangaSource: MangaSource,
|
||||||
|
val isEnabled: Boolean,
|
||||||
|
val isPinned: Boolean,
|
||||||
|
) : MangaSource by mangaSource
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package org.xtimms.shirizu.core.model.parcelable
|
||||||
|
|
||||||
|
import android.os.Parcel
|
||||||
|
import kotlinx.parcelize.Parceler
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
|
import org.xtimms.shirizu.core.model.MangaSource
|
||||||
|
|
||||||
|
class MangaSourceParceler : Parceler<MangaSource> {
|
||||||
|
|
||||||
|
override fun create(parcel: Parcel): MangaSource = MangaSource(parcel.readString())
|
||||||
|
|
||||||
|
override fun MangaSource.write(parcel: Parcel, flags: Int) {
|
||||||
|
parcel.writeString(name)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,7 +18,7 @@ object MangaTagParceler : Parceler<MangaTag> {
|
|||||||
override fun MangaTag.write(parcel: Parcel, flags: Int) {
|
override fun MangaTag.write(parcel: Parcel, flags: Int) {
|
||||||
parcel.writeString(title)
|
parcel.writeString(title)
|
||||||
parcel.writeString(key)
|
parcel.writeString(key)
|
||||||
parcel.writeSerializable(source)
|
parcel.writeString(source.name)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ data class ParcelableManga(
|
|||||||
parcel.writeParcelable(ParcelableMangaTags(tags), flags)
|
parcel.writeParcelable(ParcelableMangaTags(tags), flags)
|
||||||
parcel.writeSerializable(state)
|
parcel.writeSerializable(state)
|
||||||
parcel.writeString(author)
|
parcel.writeString(author)
|
||||||
parcel.writeSerializable(source)
|
parcel.writeString(source.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun create(parcel: Parcel) = ParcelableManga(
|
override fun create(parcel: Parcel) = ParcelableManga(
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import org.koitharu.kotatsu.parsers.util.mergeWith
|
|||||||
import org.xtimms.shirizu.BuildConfig
|
import org.xtimms.shirizu.BuildConfig
|
||||||
import org.xtimms.shirizu.core.network.CommonHeaders
|
import org.xtimms.shirizu.core.network.CommonHeaders
|
||||||
import org.xtimms.shirizu.core.parser.MangaRepository
|
import org.xtimms.shirizu.core.parser.MangaRepository
|
||||||
import org.xtimms.shirizu.core.parser.RemoteMangaRepository
|
import org.xtimms.shirizu.core.parser.ParserMangaRepository
|
||||||
import java.net.IDN
|
import java.net.IDN
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
@@ -26,7 +26,7 @@ class CommonHeadersInterceptor @Inject constructor(
|
|||||||
val request = chain.request()
|
val request = chain.request()
|
||||||
val source = request.tag(MangaSource::class.java)
|
val source = request.tag(MangaSource::class.java)
|
||||||
val repository = if (source != null) {
|
val repository = if (source != null) {
|
||||||
mangaRepositoryFactoryLazy.get().create(source) as? RemoteMangaRepository
|
mangaRepositoryFactoryLazy.get().create(source) as? ParserMangaRepository
|
||||||
} else {
|
} else {
|
||||||
if (BuildConfig.DEBUG) {
|
if (BuildConfig.DEBUG) {
|
||||||
Log.w("Http", "Request without source tag: ${request.url}")
|
Log.w("Http", "Request without source tag: ${request.url}")
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package org.xtimms.shirizu.core.parser
|
||||||
|
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import org.koitharu.kotatsu.parsers.bitmap.Bitmap
|
||||||
|
import org.koitharu.kotatsu.parsers.bitmap.Rect
|
||||||
|
import java.io.OutputStream
|
||||||
|
import android.graphics.Bitmap as AndroidBitmap
|
||||||
|
import android.graphics.Rect as AndroidRect
|
||||||
|
|
||||||
|
class BitmapWrapper private constructor(
|
||||||
|
private val androidBitmap: AndroidBitmap
|
||||||
|
) : Bitmap {
|
||||||
|
|
||||||
|
private val canvas by lazy { Canvas(androidBitmap) }
|
||||||
|
|
||||||
|
override val height: Int
|
||||||
|
get() = androidBitmap.height
|
||||||
|
|
||||||
|
override val width: Int
|
||||||
|
get() = androidBitmap.width
|
||||||
|
|
||||||
|
override fun drawBitmap(sourceBitmap: Bitmap, src: Rect, dst: Rect) {
|
||||||
|
val androidSourceBitmap = (sourceBitmap as BitmapWrapper).androidBitmap
|
||||||
|
canvas.drawBitmap(androidSourceBitmap, src.toAndroidRect(), dst.toAndroidRect(), null)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun compressTo(output: OutputStream) {
|
||||||
|
androidBitmap.compress(AndroidBitmap.CompressFormat.PNG, 100, output)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun create(width: Int, height: Int): Bitmap = BitmapWrapper(
|
||||||
|
AndroidBitmap.createBitmap(width, height, AndroidBitmap.Config.ARGB_8888),
|
||||||
|
)
|
||||||
|
|
||||||
|
fun create(bitmap: AndroidBitmap): Bitmap = BitmapWrapper(
|
||||||
|
if (bitmap.isMutable) bitmap else bitmap.copy(AndroidBitmap.Config.ARGB_8888, true)
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun Rect.toAndroidRect() = AndroidRect(left, top, right, bottom)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
package org.xtimms.shirizu.core.parser
|
||||||
|
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.collection.MutableLongSet
|
||||||
|
import coil.request.CachePolicy
|
||||||
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.MainCoroutineDispatcher
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.currentCoroutineContext
|
||||||
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||||
|
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||||
|
import org.xtimms.shirizu.BuildConfig
|
||||||
|
import org.xtimms.shirizu.core.cache.MemoryContentCache
|
||||||
|
import org.xtimms.shirizu.core.cache.SafeDeferred
|
||||||
|
import org.xtimms.shirizu.utils.MultiMutex
|
||||||
|
import org.xtimms.shirizu.utils.lang.processLifecycleScope
|
||||||
|
|
||||||
|
abstract class CachingMangaRepository(
|
||||||
|
private val cache: MemoryContentCache,
|
||||||
|
) : MangaRepository {
|
||||||
|
|
||||||
|
private val detailsMutex = MultiMutex<Long>()
|
||||||
|
private val relatedMangaMutex = MultiMutex<Long>()
|
||||||
|
private val pagesMutex = MultiMutex<Long>()
|
||||||
|
|
||||||
|
final override suspend fun getDetails(manga: Manga): Manga = getDetails(manga, CachePolicy.ENABLED)
|
||||||
|
|
||||||
|
final override suspend fun getPages(chapter: MangaChapter): List<MangaPage> = pagesMutex.withLock(chapter.id) {
|
||||||
|
cache.getPages(source, chapter.url)?.let { return it }
|
||||||
|
val pages = asyncSafe {
|
||||||
|
getPagesImpl(chapter).distinctById()
|
||||||
|
}
|
||||||
|
cache.putPages(source, chapter.url, pages)
|
||||||
|
pages
|
||||||
|
}.await()
|
||||||
|
|
||||||
|
final override suspend fun getRelated(seed: Manga): List<Manga> = relatedMangaMutex.withLock(seed.id) {
|
||||||
|
cache.getRelatedManga(source, seed.url)?.let { return it }
|
||||||
|
val related = asyncSafe {
|
||||||
|
getRelatedMangaImpl(seed).filterNot { it.id == seed.id }
|
||||||
|
}
|
||||||
|
cache.putRelatedManga(source, seed.url, related)
|
||||||
|
related
|
||||||
|
}.await()
|
||||||
|
|
||||||
|
suspend fun getDetails(manga: Manga, cachePolicy: CachePolicy): Manga = detailsMutex.withLock(manga.id) {
|
||||||
|
if (cachePolicy.readEnabled) {
|
||||||
|
cache.getDetails(source, manga.url)?.let { return it }
|
||||||
|
}
|
||||||
|
val details = asyncSafe {
|
||||||
|
getDetailsImpl(manga)
|
||||||
|
}
|
||||||
|
if (cachePolicy.writeEnabled) {
|
||||||
|
cache.putDetails(source, manga.url, details)
|
||||||
|
}
|
||||||
|
details
|
||||||
|
}.await()
|
||||||
|
|
||||||
|
suspend fun peekDetails(manga: Manga): Manga? {
|
||||||
|
return cache.getDetails(source, manga.url)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun invalidateCache() {
|
||||||
|
cache.clear(source)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract suspend fun getDetailsImpl(manga: Manga): Manga
|
||||||
|
|
||||||
|
protected abstract suspend fun getRelatedMangaImpl(seed: Manga): List<Manga>
|
||||||
|
|
||||||
|
protected abstract suspend fun getPagesImpl(chapter: MangaChapter): List<MangaPage>
|
||||||
|
|
||||||
|
@OptIn(ExperimentalStdlibApi::class)
|
||||||
|
private suspend fun <T> asyncSafe(block: suspend CoroutineScope.() -> T): SafeDeferred<T> {
|
||||||
|
var dispatcher = currentCoroutineContext()[CoroutineDispatcher.Key]
|
||||||
|
if (dispatcher == null || dispatcher is MainCoroutineDispatcher) {
|
||||||
|
dispatcher = Dispatchers.Default
|
||||||
|
}
|
||||||
|
return SafeDeferred(
|
||||||
|
processLifecycleScope.async(dispatcher) {
|
||||||
|
runCatchingCancellable { block() }
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun List<MangaPage>.distinctById(): List<MangaPage> {
|
||||||
|
if (isEmpty()) {
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
val result = ArrayList<MangaPage>(size)
|
||||||
|
val set = MutableLongSet(size)
|
||||||
|
for (page in this) {
|
||||||
|
if (set.add(page.id)) {
|
||||||
|
result.add(page)
|
||||||
|
} else if (BuildConfig.DEBUG) {
|
||||||
|
Log.w(null, "Duplicate page: $page")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package org.xtimms.shirizu.core.parser
|
||||||
|
|
||||||
|
import org.koitharu.kotatsu.parsers.model.ContentRating
|
||||||
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaListFilter
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaState
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||||
|
import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||||
|
import org.xtimms.shirizu.core.exceptions.UnsupportedSourceException
|
||||||
|
import java.util.EnumSet
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
class EmptyMangaRepository(override val source: MangaSource) : MangaRepository {
|
||||||
|
|
||||||
|
override val sortOrders: Set<SortOrder>
|
||||||
|
get() = EnumSet.allOf(SortOrder::class.java)
|
||||||
|
override val states: Set<MangaState>
|
||||||
|
get() = emptySet()
|
||||||
|
override val contentRatings: Set<ContentRating>
|
||||||
|
get() = emptySet()
|
||||||
|
override var defaultSortOrder: SortOrder
|
||||||
|
get() = SortOrder.NEWEST
|
||||||
|
set(value) = Unit
|
||||||
|
override val isMultipleTagsSupported: Boolean
|
||||||
|
get() = false
|
||||||
|
override val isTagsExclusionSupported: Boolean
|
||||||
|
get() = false
|
||||||
|
override val isSearchSupported: Boolean
|
||||||
|
get() = false
|
||||||
|
|
||||||
|
override suspend fun getList(offset: Int, filter: MangaListFilter?): List<Manga> = stub(null)
|
||||||
|
|
||||||
|
override suspend fun getDetails(manga: Manga): Manga = stub(manga)
|
||||||
|
|
||||||
|
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> = stub(null)
|
||||||
|
|
||||||
|
override suspend fun getPageUrl(page: MangaPage): String = stub(null)
|
||||||
|
|
||||||
|
override suspend fun getTags(): Set<MangaTag> = stub(null)
|
||||||
|
|
||||||
|
override suspend fun getLocales(): Set<Locale> = stub(null)
|
||||||
|
|
||||||
|
override suspend fun getRelated(seed: Manga): List<Manga> = stub(seed)
|
||||||
|
|
||||||
|
private fun stub(manga: Manga?): Nothing {
|
||||||
|
throw UnsupportedSourceException("This manga source is not supported", manga)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,6 +13,7 @@ import javax.inject.Provider
|
|||||||
@Reusable
|
@Reusable
|
||||||
class MangaDataRepository @Inject constructor(
|
class MangaDataRepository @Inject constructor(
|
||||||
private val db: ShirizuDatabase,
|
private val db: ShirizuDatabase,
|
||||||
|
private val resolverProvider: Provider<MangaLinkResolver>,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
suspend fun findMangaById(mangaId: Long): Manga? {
|
suspend fun findMangaById(mangaId: Long): Manga? {
|
||||||
@@ -23,6 +24,13 @@ class MangaDataRepository @Inject constructor(
|
|||||||
return db.getMangaDao().findByPublicUrl(publicUrl)?.toManga()
|
return db.getMangaDao().findByPublicUrl(publicUrl)?.toManga()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
suspend fun resolveIntent(intent: MangaIntent): Manga? = when {
|
||||||
|
intent.manga != null -> intent.manga
|
||||||
|
intent.mangaId != 0L -> findMangaById(intent.mangaId)
|
||||||
|
intent.uri != null -> resolverProvider.get().resolve(intent.uri)
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun storeManga(manga: Manga) {
|
suspend fun storeManga(manga: Manga) {
|
||||||
db.withTransaction {
|
db.withTransaction {
|
||||||
val tags = manga.tags.toEntities()
|
val tags = manga.tags.toEntities()
|
||||||
|
|||||||
@@ -0,0 +1,124 @@
|
|||||||
|
package org.xtimms.shirizu.core.parser
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import coil.request.CachePolicy
|
||||||
|
import dagger.Reusable
|
||||||
|
import org.koitharu.kotatsu.parsers.exception.NotFoundException
|
||||||
|
import org.koitharu.kotatsu.parsers.model.ContentType
|
||||||
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaListFilter
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaParserSource
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
|
import org.koitharu.kotatsu.parsers.util.almostEquals
|
||||||
|
import org.koitharu.kotatsu.parsers.util.levenshteinDistance
|
||||||
|
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||||
|
import org.koitharu.kotatsu.parsers.util.toRelativeUrl
|
||||||
|
import org.xtimms.shirizu.core.model.MangaSource
|
||||||
|
import org.xtimms.shirizu.core.model.UnknownMangaSource
|
||||||
|
import org.xtimms.shirizu.core.model.isNsfw
|
||||||
|
import org.xtimms.shirizu.data.repository.MangaSourcesRepository
|
||||||
|
import org.xtimms.shirizu.utils.lang.ifNullOrEmpty
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@Reusable
|
||||||
|
class MangaLinkResolver @Inject constructor(
|
||||||
|
private val repositoryFactory: MangaRepository.Factory,
|
||||||
|
private val sourcesRepository: MangaSourcesRepository,
|
||||||
|
private val dataRepository: MangaDataRepository,
|
||||||
|
) {
|
||||||
|
|
||||||
|
suspend fun resolve(uri: Uri): Manga {
|
||||||
|
return if (uri.scheme == "kotatsu" || uri.host == "kotatsu.app") {
|
||||||
|
resolveAppLink(uri)
|
||||||
|
} else {
|
||||||
|
resolveExternalLink(uri)
|
||||||
|
} ?: throw NotFoundException("Cannot resolve link", uri.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun resolveAppLink(uri: Uri): Manga? {
|
||||||
|
require(uri.pathSegments.singleOrNull() == "manga") { "Invalid url" }
|
||||||
|
val sourceName = requireNotNull(uri.getQueryParameter("source")) { "Source is not specified" }
|
||||||
|
val source = MangaSource(sourceName)
|
||||||
|
require(source != UnknownMangaSource) { "Manga source $sourceName is not supported" }
|
||||||
|
val repo = repositoryFactory.create(source)
|
||||||
|
return repo.findExact(
|
||||||
|
url = uri.getQueryParameter("url"),
|
||||||
|
title = uri.getQueryParameter("name"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun resolveExternalLink(uri: Uri): Manga? {
|
||||||
|
dataRepository.findMangaByPublicUrl(uri.toString())?.let {
|
||||||
|
return it
|
||||||
|
}
|
||||||
|
val host = uri.host ?: return null
|
||||||
|
val repo = sourcesRepository.allMangaSources.asSequence()
|
||||||
|
.map { source ->
|
||||||
|
repositoryFactory.create(source) as ParserMangaRepository
|
||||||
|
}.find { repo ->
|
||||||
|
host in repo.domains
|
||||||
|
} ?: return null
|
||||||
|
return repo.findExact(uri.toString().toRelativeUrl(host), null)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun MangaRepository.findExact(url: String?, title: String?): Manga? {
|
||||||
|
if (!title.isNullOrEmpty()) {
|
||||||
|
val list = getList(0, MangaListFilter.Search(title))
|
||||||
|
if (url != null) {
|
||||||
|
list.find { it.url == url }?.let {
|
||||||
|
return it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
list.minByOrNull { it.title.levenshteinDistance(title) }
|
||||||
|
?.takeIf { it.title.almostEquals(title, 0.2f) }
|
||||||
|
?.let { return it }
|
||||||
|
}
|
||||||
|
val seed = getDetailsNoCache(
|
||||||
|
getSeedManga(source, url ?: return null, title),
|
||||||
|
)
|
||||||
|
return runCatchingCancellable {
|
||||||
|
val seedTitle = seed.title.ifEmpty {
|
||||||
|
seed.altTitle
|
||||||
|
}.ifNullOrEmpty {
|
||||||
|
seed.author
|
||||||
|
} ?: return@runCatchingCancellable null
|
||||||
|
val seedList = getList(0, MangaListFilter.Search(seedTitle))
|
||||||
|
seedList.first { x -> x.url == url }
|
||||||
|
}.getOrThrow()
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun MangaRepository.getDetailsNoCache(manga: Manga): Manga {
|
||||||
|
return if (this is ParserMangaRepository) {
|
||||||
|
getDetails(manga, CachePolicy.READ_ONLY)
|
||||||
|
} else {
|
||||||
|
getDetails(manga)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getSeedManga(source: MangaSource, url: String, title: String?) = Manga(
|
||||||
|
id = run {
|
||||||
|
var h = 1125899906842597L
|
||||||
|
source.name.forEach { c ->
|
||||||
|
h = 31 * h + c.code
|
||||||
|
}
|
||||||
|
url.forEach { c ->
|
||||||
|
h = 31 * h + c.code
|
||||||
|
}
|
||||||
|
h
|
||||||
|
},
|
||||||
|
title = title.orEmpty(),
|
||||||
|
altTitle = null,
|
||||||
|
url = url,
|
||||||
|
publicUrl = "",
|
||||||
|
rating = 0.0f,
|
||||||
|
isNsfw = source.isNsfw(),
|
||||||
|
coverUrl = "",
|
||||||
|
tags = emptySet(),
|
||||||
|
state = null,
|
||||||
|
author = null,
|
||||||
|
largeCoverUrl = null,
|
||||||
|
description = null,
|
||||||
|
chapters = null,
|
||||||
|
source = source,
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ package org.xtimms.shirizu.core.parser
|
|||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
import android.util.Base64
|
import android.util.Base64
|
||||||
import android.webkit.WebView
|
import android.webkit.WebView
|
||||||
import androidx.annotation.MainThread
|
import androidx.annotation.MainThread
|
||||||
@@ -10,8 +11,13 @@ import dagger.hilt.android.qualifiers.ApplicationContext
|
|||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Response
|
||||||
|
import okhttp3.ResponseBody.Companion.asResponseBody
|
||||||
|
import okio.Buffer
|
||||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||||
|
import org.koitharu.kotatsu.parsers.bitmap.Bitmap
|
||||||
import org.koitharu.kotatsu.parsers.config.MangaSourceConfig
|
import org.koitharu.kotatsu.parsers.config.MangaSourceConfig
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
import org.koitharu.kotatsu.parsers.network.UserAgents
|
import org.koitharu.kotatsu.parsers.network.UserAgents
|
||||||
@@ -20,6 +26,7 @@ import org.xtimms.shirizu.core.network.MangaHttpClient
|
|||||||
import org.xtimms.shirizu.core.network.cookies.MutableCookieJar
|
import org.xtimms.shirizu.core.network.cookies.MutableCookieJar
|
||||||
import org.xtimms.shirizu.core.prefs.SourceSettings
|
import org.xtimms.shirizu.core.prefs.SourceSettings
|
||||||
import org.xtimms.shirizu.utils.system.configureForParser
|
import org.xtimms.shirizu.utils.system.configureForParser
|
||||||
|
import org.xtimms.shirizu.utils.system.requireBody
|
||||||
import org.xtimms.shirizu.utils.system.sanitizeHeaderValue
|
import org.xtimms.shirizu.utils.system.sanitizeHeaderValue
|
||||||
import org.xtimms.shirizu.utils.system.toList
|
import org.xtimms.shirizu.utils.system.toList
|
||||||
import java.lang.ref.WeakReference
|
import java.lang.ref.WeakReference
|
||||||
@@ -72,6 +79,30 @@ class MangaLoaderContextImpl @Inject constructor(
|
|||||||
return LocaleListCompat.getAdjustedDefault().toList()
|
return LocaleListCompat.getAdjustedDefault().toList()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun createBitmap(width: Int, height: Int): Bitmap {
|
||||||
|
return BitmapWrapper.create(width, height)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun redrawImageResponse(
|
||||||
|
response: Response,
|
||||||
|
redraw: (image: Bitmap) -> Bitmap
|
||||||
|
): Response {
|
||||||
|
val image = response.requireBody().byteStream()
|
||||||
|
|
||||||
|
val opts = BitmapFactory.Options()
|
||||||
|
opts.inMutable = true
|
||||||
|
val bitmap = BitmapFactory.decodeStream(image, null, opts) ?: error("Cannot decode bitmap")
|
||||||
|
val result = redraw(BitmapWrapper.create(bitmap)) as BitmapWrapper
|
||||||
|
|
||||||
|
val body = Buffer().also {
|
||||||
|
result.compressTo(it.outputStream())
|
||||||
|
}.asResponseBody("image/jpeg".toMediaType())
|
||||||
|
|
||||||
|
return response.newBuilder()
|
||||||
|
.body(body)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
@MainThread
|
@MainThread
|
||||||
private fun obtainWebView(): WebView {
|
private fun obtainWebView(): WebView {
|
||||||
return webViewCached?.get() ?: WebView(androidContext).also {
|
return webViewCached?.get() ?: WebView(androidContext).also {
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ package org.xtimms.shirizu.core.parser
|
|||||||
|
|
||||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||||
import org.koitharu.kotatsu.parsers.MangaParser
|
import org.koitharu.kotatsu.parsers.MangaParser
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaParserSource
|
||||||
|
|
||||||
fun MangaParser(source: MangaSource, loaderContext: MangaLoaderContext): MangaParser {
|
fun MangaParser(source: MangaParserSource, loaderContext: MangaLoaderContext): MangaParser {
|
||||||
return loaderContext.newParserInstance(source)
|
return loaderContext.newParserInstance(source)
|
||||||
}
|
}
|
||||||
@@ -1,21 +1,28 @@
|
|||||||
package org.xtimms.shirizu.core.parser
|
package org.xtimms.shirizu.core.parser
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import androidx.annotation.AnyThread
|
import androidx.annotation.AnyThread
|
||||||
import androidx.paging.PagingSource
|
import androidx.collection.ArrayMap
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||||
import org.koitharu.kotatsu.parsers.model.ContentRating
|
import org.koitharu.kotatsu.parsers.model.ContentRating
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaListFilter
|
import org.koitharu.kotatsu.parsers.model.MangaListFilter
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaParserSource
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaState
|
import org.koitharu.kotatsu.parsers.model.MangaState
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||||
import org.koitharu.kotatsu.parsers.model.SortOrder
|
import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||||
import org.xtimms.shirizu.core.cache.ContentCache
|
import org.xtimms.shirizu.core.cache.MemoryContentCache
|
||||||
|
import org.xtimms.shirizu.core.model.LocalMangaSource
|
||||||
|
import org.xtimms.shirizu.core.model.MangaSourceInfo
|
||||||
|
import org.xtimms.shirizu.core.model.UnknownMangaSource
|
||||||
|
import org.xtimms.shirizu.core.parser.external.ExternalMangaRepository
|
||||||
|
import org.xtimms.shirizu.core.parser.external.ExternalMangaSource
|
||||||
import org.xtimms.shirizu.core.parser.local.LocalMangaRepository
|
import org.xtimms.shirizu.core.parser.local.LocalMangaRepository
|
||||||
import java.lang.ref.WeakReference
|
import java.lang.ref.WeakReference
|
||||||
import java.util.EnumMap
|
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
@@ -30,6 +37,8 @@ interface MangaRepository {
|
|||||||
|
|
||||||
val contentRatings: Set<ContentRating>
|
val contentRatings: Set<ContentRating>
|
||||||
|
|
||||||
|
var defaultSortOrder: SortOrder
|
||||||
|
|
||||||
val isMultipleTagsSupported: Boolean
|
val isMultipleTagsSupported: Boolean
|
||||||
|
|
||||||
val isTagsExclusionSupported: Boolean
|
val isTagsExclusionSupported: Boolean
|
||||||
@@ -50,30 +59,58 @@ interface MangaRepository {
|
|||||||
|
|
||||||
suspend fun getRelated(seed: Manga): List<Manga>
|
suspend fun getRelated(seed: Manga): List<Manga>
|
||||||
|
|
||||||
|
suspend fun find(manga: Manga): Manga? {
|
||||||
|
val list = getList(0, MangaListFilter.Search(manga.title))
|
||||||
|
return list.find { x -> x.id == manga.id }
|
||||||
|
}
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
class Factory @Inject constructor(
|
class Factory @Inject constructor(
|
||||||
|
@ApplicationContext private val context: Context,
|
||||||
private val localMangaRepository: LocalMangaRepository,
|
private val localMangaRepository: LocalMangaRepository,
|
||||||
private val loaderContext: MangaLoaderContext,
|
private val loaderContext: MangaLoaderContext,
|
||||||
private val contentCache: ContentCache,
|
private val contentCache: MemoryContentCache,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private val cache = EnumMap<MangaSource, WeakReference<RemoteMangaRepository>>(MangaSource::class.java)
|
private val cache = ArrayMap<MangaSource, WeakReference<MangaRepository>>()
|
||||||
|
|
||||||
@AnyThread
|
@AnyThread
|
||||||
fun create(source: MangaSource): MangaRepository {
|
fun create(source: MangaSource): MangaRepository {
|
||||||
if (source == MangaSource.LOCAL) {
|
when (source) {
|
||||||
return localMangaRepository
|
is MangaSourceInfo -> return create(source.mangaSource)
|
||||||
|
LocalMangaSource -> return localMangaRepository
|
||||||
|
UnknownMangaSource -> return EmptyMangaRepository(source)
|
||||||
}
|
}
|
||||||
cache[source]?.get()?.let { return it }
|
cache[source]?.get()?.let { return it }
|
||||||
return synchronized(cache) {
|
return synchronized(cache) {
|
||||||
cache[source]?.get()?.let { return it }
|
cache[source]?.get()?.let { return it }
|
||||||
val repository = RemoteMangaRepository(
|
val repository = createRepository(source)
|
||||||
|
if (repository != null) {
|
||||||
|
cache[source] = WeakReference(repository)
|
||||||
|
repository
|
||||||
|
} else {
|
||||||
|
EmptyMangaRepository(source)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createRepository(source: MangaSource): MangaRepository? = when (source) {
|
||||||
|
is MangaParserSource -> ParserMangaRepository(
|
||||||
parser = MangaParser(source, loaderContext),
|
parser = MangaParser(source, loaderContext),
|
||||||
cache = contentCache,
|
cache = contentCache,
|
||||||
)
|
)
|
||||||
cache[source] = WeakReference(repository)
|
|
||||||
repository
|
is ExternalMangaSource -> if (source.isAvailable(context)) {
|
||||||
|
ExternalMangaRepository(
|
||||||
|
contentResolver = context.contentResolver,
|
||||||
|
source = source,
|
||||||
|
cache = contentCache,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
EmptyMangaRepository(source)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
else -> null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
package org.xtimms.shirizu.core.parser
|
package org.xtimms.shirizu.core.parser
|
||||||
|
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import coil.request.CachePolicy
|
|
||||||
import kotlinx.coroutines.CoroutineDispatcher
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
@@ -27,17 +26,17 @@ import org.koitharu.kotatsu.parsers.model.SortOrder
|
|||||||
import org.koitharu.kotatsu.parsers.util.domain
|
import org.koitharu.kotatsu.parsers.util.domain
|
||||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||||
import org.xtimms.shirizu.BuildConfig
|
import org.xtimms.shirizu.BuildConfig
|
||||||
import org.xtimms.shirizu.core.cache.ContentCache
|
import org.xtimms.shirizu.core.cache.MemoryContentCache
|
||||||
import org.xtimms.shirizu.core.cache.SafeDeferred
|
import org.xtimms.shirizu.core.cache.SafeDeferred
|
||||||
import org.xtimms.shirizu.core.prefs.SourceSettings
|
import org.xtimms.shirizu.core.prefs.SourceSettings
|
||||||
import org.xtimms.shirizu.utils.lang.processLifecycleScope
|
import org.xtimms.shirizu.utils.lang.processLifecycleScope
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
@OptIn(InternalParsersApi::class)
|
@OptIn(InternalParsersApi::class)
|
||||||
class RemoteMangaRepository(
|
class ParserMangaRepository(
|
||||||
private val parser: MangaParser,
|
private val parser: MangaParser,
|
||||||
private val cache: ContentCache,
|
private val cache: MemoryContentCache,
|
||||||
) : MangaRepository, Interceptor {
|
) : CachingMangaRepository(cache), Interceptor {
|
||||||
|
|
||||||
override val source: MangaSource
|
override val source: MangaSource
|
||||||
get() = parser.source
|
get() = parser.source
|
||||||
@@ -51,6 +50,12 @@ class RemoteMangaRepository(
|
|||||||
override val contentRatings: Set<ContentRating>
|
override val contentRatings: Set<ContentRating>
|
||||||
get() = parser.availableContentRating
|
get() = parser.availableContentRating
|
||||||
|
|
||||||
|
override var defaultSortOrder: SortOrder
|
||||||
|
get() = getConfig().defaultSortOrder ?: sortOrders.first()
|
||||||
|
set(value) {
|
||||||
|
getConfig().defaultSortOrder = value
|
||||||
|
}
|
||||||
|
|
||||||
override val isMultipleTagsSupported: Boolean
|
override val isMultipleTagsSupported: Boolean
|
||||||
get() = parser.isMultipleTagsSupported
|
get() = parser.isMultipleTagsSupported
|
||||||
|
|
||||||
@@ -70,7 +75,7 @@ class RemoteMangaRepository(
|
|||||||
get() = parser.configKeyDomain.presetValues
|
get() = parser.configKeyDomain.presetValues
|
||||||
|
|
||||||
val headers: Headers
|
val headers: Headers
|
||||||
get() = parser.headers
|
get() = parser.getRequestHeaders()
|
||||||
|
|
||||||
override fun intercept(chain: Interceptor.Chain): Response {
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
return if (parser is Interceptor) {
|
return if (parser is Interceptor) {
|
||||||
@@ -84,16 +89,9 @@ class RemoteMangaRepository(
|
|||||||
return parser.getList(offset, filter)
|
return parser.getList(offset, filter)
|
||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getDetails(manga: Manga): Manga = getDetails(manga, CachePolicy.ENABLED)
|
override suspend fun getPagesImpl(
|
||||||
|
chapter: MangaChapter
|
||||||
override suspend fun getPages(chapter: MangaChapter): List<MangaPage> {
|
): List<MangaPage> = parser.getPages(chapter)
|
||||||
cache.getPages(source, chapter.url)?.let { return it }
|
|
||||||
val pages = asyncSafe {
|
|
||||||
parser.getPages(chapter).distinctById()
|
|
||||||
}
|
|
||||||
cache.putPages(source, chapter.url, pages)
|
|
||||||
return pages.await()
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getPageUrl(page: MangaPage): String = parser.getPageUrl(page)
|
override suspend fun getPageUrl(page: MangaPage): String = parser.getPageUrl(page)
|
||||||
|
|
||||||
@@ -105,59 +103,19 @@ class RemoteMangaRepository(
|
|||||||
|
|
||||||
suspend fun getFavicons(): Favicons = parser.getFavicons()
|
suspend fun getFavicons(): Favicons = parser.getFavicons()
|
||||||
|
|
||||||
override suspend fun getRelated(seed: Manga): List<Manga> {
|
override suspend fun getRelatedMangaImpl(seed: Manga): List<Manga> = parser.getRelatedManga(seed)
|
||||||
cache.getRelatedManga(source, seed.url)?.let { return it }
|
|
||||||
val related = asyncSafe {
|
override suspend fun getDetailsImpl(manga: Manga): Manga = parser.getDetails(manga)
|
||||||
parser.getRelatedManga(seed).filterNot { it.id == seed.id }
|
|
||||||
}
|
fun getAvailableMirrors(): List<String> {
|
||||||
cache.putRelatedManga(source, seed.url, related)
|
return parser.configKeyDomain.presetValues.toList()
|
||||||
return related.await()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getDetails(manga: Manga, cachePolicy: CachePolicy): Manga {
|
fun isSlowdownEnabled(): Boolean {
|
||||||
if (cachePolicy.readEnabled) {
|
return getConfig().isSlowdownEnabled
|
||||||
cache.getDetails(source, manga.url)?.let { return it }
|
|
||||||
}
|
|
||||||
val details = asyncSafe {
|
|
||||||
parser.getDetails(manga)
|
|
||||||
}
|
|
||||||
if (cachePolicy.writeEnabled) {
|
|
||||||
cache.putDetails(source, manga.url, details)
|
|
||||||
}
|
|
||||||
return details.await()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getConfig() = parser.config as SourceSettings
|
private fun getConfig() = parser.config as SourceSettings
|
||||||
|
|
||||||
@OptIn(ExperimentalStdlibApi::class)
|
private fun Result<*>.isValidResult() = isSuccess && (getOrNull() as? Collection<*>)?.isEmpty() != true
|
||||||
private suspend fun <T> asyncSafe(block: suspend CoroutineScope.() -> T): SafeDeferred<T> {
|
|
||||||
var dispatcher = currentCoroutineContext()[CoroutineDispatcher.Key]
|
|
||||||
if (dispatcher == null || dispatcher is MainCoroutineDispatcher) {
|
|
||||||
dispatcher = Dispatchers.Default
|
|
||||||
}
|
|
||||||
return SafeDeferred(
|
|
||||||
processLifecycleScope.async(dispatcher) {
|
|
||||||
runCatchingCancellable { block() }
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun List<MangaPage>.distinctById(): List<MangaPage> {
|
|
||||||
if (isEmpty()) {
|
|
||||||
return emptyList()
|
|
||||||
}
|
|
||||||
val result = ArrayList<MangaPage>(size)
|
|
||||||
val set = HashSet<Long>(size)
|
|
||||||
for (page in this) {
|
|
||||||
if (set.add(page.id)) {
|
|
||||||
result.add(page)
|
|
||||||
} else if (BuildConfig.DEBUG) {
|
|
||||||
Log.w(null, "Duplicate page: $page")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Result<*>.isValidResult() = exceptionOrNull() !is ParseException
|
|
||||||
&& (getOrNull() as? Collection<*>)?.isEmpty() != true
|
|
||||||
}
|
}
|
||||||
264
app/src/main/java/org/xtimms/shirizu/core/parser/external/ExternalMangaRepository.kt
vendored
Normal file
264
app/src/main/java/org/xtimms/shirizu/core/parser/external/ExternalMangaRepository.kt
vendored
Normal file
@@ -0,0 +1,264 @@
|
|||||||
|
package org.xtimms.shirizu.core.parser.external
|
||||||
|
|
||||||
|
import android.content.ContentResolver
|
||||||
|
import android.database.Cursor
|
||||||
|
import androidx.collection.ArraySet
|
||||||
|
import androidx.core.database.getStringOrNull
|
||||||
|
import androidx.core.net.toUri
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.coroutineScope
|
||||||
|
import kotlinx.coroutines.runInterruptible
|
||||||
|
import org.koitharu.kotatsu.parsers.model.ContentRating
|
||||||
|
import org.koitharu.kotatsu.parsers.model.ContentType
|
||||||
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaListFilter
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaState
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||||
|
import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||||
|
import org.koitharu.kotatsu.parsers.util.find
|
||||||
|
import org.koitharu.kotatsu.parsers.util.mapNotNullToSet
|
||||||
|
import org.koitharu.kotatsu.parsers.util.splitTwoParts
|
||||||
|
import org.xtimms.shirizu.core.cache.MemoryContentCache
|
||||||
|
import org.xtimms.shirizu.core.parser.CachingMangaRepository
|
||||||
|
import org.xtimms.shirizu.utils.lang.ifNullOrEmpty
|
||||||
|
import java.util.EnumSet
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
class ExternalMangaRepository(
|
||||||
|
private val contentResolver: ContentResolver,
|
||||||
|
override val source: ExternalMangaSource,
|
||||||
|
cache: MemoryContentCache,
|
||||||
|
) : CachingMangaRepository(cache) {
|
||||||
|
|
||||||
|
private val capabilities by lazy { queryCapabilities() }
|
||||||
|
|
||||||
|
override val sortOrders: Set<SortOrder>
|
||||||
|
get() = capabilities?.availableSortOrders ?: EnumSet.of(SortOrder.ALPHABETICAL)
|
||||||
|
override val states: Set<MangaState>
|
||||||
|
get() = capabilities?.availableStates.orEmpty()
|
||||||
|
override val contentRatings: Set<ContentRating>
|
||||||
|
get() = capabilities?.availableContentRating.orEmpty()
|
||||||
|
override var defaultSortOrder: SortOrder
|
||||||
|
get() = capabilities?.defaultSortOrder ?: SortOrder.ALPHABETICAL
|
||||||
|
set(value) = Unit
|
||||||
|
override val isMultipleTagsSupported: Boolean
|
||||||
|
get() = capabilities?.isMultipleTagsSupported ?: true
|
||||||
|
override val isTagsExclusionSupported: Boolean
|
||||||
|
get() = capabilities?.isTagsExclusionSupported ?: false
|
||||||
|
override val isSearchSupported: Boolean
|
||||||
|
get() = capabilities?.isSearchSupported ?: true
|
||||||
|
|
||||||
|
override suspend fun getList(offset: Int, filter: MangaListFilter?): List<Manga> =
|
||||||
|
runInterruptible(Dispatchers.Default) {
|
||||||
|
val uri = "content://${source.authority}/manga".toUri().buildUpon()
|
||||||
|
uri.appendQueryParameter("offset", offset.toString())
|
||||||
|
when (filter) {
|
||||||
|
is MangaListFilter.Advanced -> {
|
||||||
|
filter.tags.forEach { uri.appendQueryParameter("tag_include", it.key) }
|
||||||
|
filter.tagsExclude.forEach { uri.appendQueryParameter("tag_exclude", it.key) }
|
||||||
|
filter.states.forEach { uri.appendQueryParameter("state", it.name) }
|
||||||
|
filter.locale?.let { uri.appendQueryParameter("locale", it.language) }
|
||||||
|
filter.contentRating.forEach { uri.appendQueryParameter("content_rating", it.name) }
|
||||||
|
}
|
||||||
|
|
||||||
|
is MangaListFilter.Search -> {
|
||||||
|
uri.appendQueryParameter("query", filter.query)
|
||||||
|
}
|
||||||
|
|
||||||
|
null -> Unit
|
||||||
|
}
|
||||||
|
contentResolver.query(uri.build(), null, null, null, filter?.sortOrder?.name)?.use { cursor ->
|
||||||
|
val result = ArrayList<Manga>(cursor.count)
|
||||||
|
if (cursor.moveToFirst()) {
|
||||||
|
do {
|
||||||
|
result += cursor.getManga()
|
||||||
|
} while (cursor.moveToNext())
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}.orEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getDetailsImpl(manga: Manga): Manga = coroutineScope {
|
||||||
|
val chapters = async { queryChapters(manga.url) }
|
||||||
|
val details = queryDetails(manga.url)
|
||||||
|
Manga(
|
||||||
|
id = manga.id,
|
||||||
|
title = details.title.ifBlank { manga.title },
|
||||||
|
altTitle = details.altTitle.ifNullOrEmpty { manga.altTitle },
|
||||||
|
url = details.url.ifEmpty { manga.url },
|
||||||
|
publicUrl = details.publicUrl.ifEmpty { manga.publicUrl },
|
||||||
|
rating = maxOf(details.rating, manga.rating),
|
||||||
|
isNsfw = details.isNsfw,
|
||||||
|
coverUrl = details.coverUrl.ifEmpty { manga.coverUrl },
|
||||||
|
tags = details.tags + manga.tags,
|
||||||
|
state = details.state ?: manga.state,
|
||||||
|
author = details.author.ifNullOrEmpty { manga.author },
|
||||||
|
largeCoverUrl = details.largeCoverUrl.ifNullOrEmpty { manga.largeCoverUrl },
|
||||||
|
description = details.description.ifNullOrEmpty { manga.description },
|
||||||
|
chapters = chapters.await(),
|
||||||
|
source = source,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getPagesImpl(chapter: MangaChapter): List<MangaPage> = runInterruptible(Dispatchers.Default) {
|
||||||
|
val uri = "content://${source.authority}/chapters".toUri()
|
||||||
|
.buildUpon()
|
||||||
|
.appendPath(chapter.url)
|
||||||
|
.build()
|
||||||
|
contentResolver.query(uri, null, null, null, null)?.use { cursor ->
|
||||||
|
val result = ArrayList<MangaPage>(cursor.count)
|
||||||
|
if (cursor.moveToFirst()) {
|
||||||
|
do {
|
||||||
|
result += MangaPage(
|
||||||
|
id = cursor.getLong(0),
|
||||||
|
url = cursor.getString(1),
|
||||||
|
preview = cursor.getStringOrNull(2),
|
||||||
|
source = source,
|
||||||
|
)
|
||||||
|
} while (cursor.moveToNext())
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}.orEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getPageUrl(page: MangaPage): String = page.url
|
||||||
|
|
||||||
|
override suspend fun getTags(): Set<MangaTag> = runInterruptible(Dispatchers.Default) {
|
||||||
|
val uri = "content://${source.authority}/tags".toUri()
|
||||||
|
contentResolver.query(uri, null, null, null, null)?.use { cursor ->
|
||||||
|
val result = ArraySet<MangaTag>(cursor.count)
|
||||||
|
if (cursor.moveToFirst()) {
|
||||||
|
do {
|
||||||
|
result += MangaTag(
|
||||||
|
key = cursor.getString(0),
|
||||||
|
title = cursor.getString(1),
|
||||||
|
source = source,
|
||||||
|
)
|
||||||
|
} while (cursor.moveToNext())
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}.orEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getLocales(): Set<Locale> = emptySet()
|
||||||
|
|
||||||
|
override suspend fun getRelatedMangaImpl(seed: Manga): List<Manga> = emptyList() // TODO
|
||||||
|
|
||||||
|
private suspend fun queryDetails(url: String): Manga = runInterruptible(Dispatchers.Default) {
|
||||||
|
val uri = "content://${source.authority}/manga".toUri()
|
||||||
|
.buildUpon()
|
||||||
|
.appendPath(url)
|
||||||
|
.build()
|
||||||
|
checkNotNull(
|
||||||
|
contentResolver.query(uri, null, null, null, null)?.use { cursor ->
|
||||||
|
cursor.moveToFirst()
|
||||||
|
cursor.getManga()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun queryChapters(url: String): List<MangaChapter>? = runInterruptible(Dispatchers.Default) {
|
||||||
|
val uri = "content://${source.authority}/manga/chapters".toUri()
|
||||||
|
.buildUpon()
|
||||||
|
.appendPath(url)
|
||||||
|
.build()
|
||||||
|
contentResolver.query(uri, null, null, null, null)?.use { cursor ->
|
||||||
|
val result = ArrayList<MangaChapter>(cursor.count)
|
||||||
|
if (cursor.moveToFirst()) {
|
||||||
|
do {
|
||||||
|
result += MangaChapter(
|
||||||
|
id = cursor.getLong(0),
|
||||||
|
name = cursor.getString(1),
|
||||||
|
number = cursor.getFloat(2),
|
||||||
|
volume = cursor.getInt(3),
|
||||||
|
url = cursor.getString(4),
|
||||||
|
scanlator = cursor.getStringOrNull(5),
|
||||||
|
uploadDate = cursor.getLong(6),
|
||||||
|
branch = cursor.getStringOrNull(7),
|
||||||
|
source = source,
|
||||||
|
)
|
||||||
|
} while (cursor.moveToNext())
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Cursor.getManga() = Manga(
|
||||||
|
id = getLong(0),
|
||||||
|
title = getString(1),
|
||||||
|
altTitle = getStringOrNull(2),
|
||||||
|
url = getString(3),
|
||||||
|
publicUrl = getString(4),
|
||||||
|
rating = getFloat(5),
|
||||||
|
isNsfw = getInt(6) > 1,
|
||||||
|
coverUrl = getString(7),
|
||||||
|
tags = getStringOrNull(8)?.split(':')?.mapNotNullToSet {
|
||||||
|
val parts = it.splitTwoParts('=') ?: return@mapNotNullToSet null
|
||||||
|
MangaTag(key = parts.first, title = parts.second, source = source)
|
||||||
|
}.orEmpty(),
|
||||||
|
state = getStringOrNull(9)?.let { MangaState.entries.find(it) },
|
||||||
|
author = optString(10),
|
||||||
|
largeCoverUrl = optString(11),
|
||||||
|
description = optString(12),
|
||||||
|
chapters = emptyList(),
|
||||||
|
source = source,
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun Cursor.optString(columnIndex: Int): String? {
|
||||||
|
return if (isNull(columnIndex)) {
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
getString(columnIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun queryCapabilities(): MangaSourceCapabilities? {
|
||||||
|
val uri = "content://${source.authority}/capabilities".toUri()
|
||||||
|
return contentResolver.query(uri, null, null, null, null)?.use { cursor ->
|
||||||
|
if (cursor.moveToFirst()) {
|
||||||
|
MangaSourceCapabilities(
|
||||||
|
availableSortOrders = cursor.getStringOrNull(0)
|
||||||
|
?.split(',')
|
||||||
|
?.mapNotNullTo(EnumSet.noneOf(SortOrder::class.java)) {
|
||||||
|
SortOrder.entries.find(it)
|
||||||
|
}.orEmpty(),
|
||||||
|
availableStates = cursor.getStringOrNull(1)
|
||||||
|
?.split(',')
|
||||||
|
?.mapNotNullTo(EnumSet.noneOf(MangaState::class.java)) {
|
||||||
|
MangaState.entries.find(it)
|
||||||
|
}.orEmpty(),
|
||||||
|
availableContentRating = cursor.getStringOrNull(2)
|
||||||
|
?.split(',')
|
||||||
|
?.mapNotNullTo(EnumSet.noneOf(ContentRating::class.java)) {
|
||||||
|
ContentRating.entries.find(it)
|
||||||
|
}.orEmpty(),
|
||||||
|
isMultipleTagsSupported = cursor.getInt(3) > 1,
|
||||||
|
isTagsExclusionSupported = cursor.getInt(4) > 1,
|
||||||
|
isSearchSupported = cursor.getInt(5) > 1,
|
||||||
|
contentType = ContentType.entries.find(cursor.getString(6)) ?: ContentType.OTHER,
|
||||||
|
defaultSortOrder = cursor.getStringOrNull(7)?.let {
|
||||||
|
SortOrder.entries.find(it)
|
||||||
|
} ?: SortOrder.ALPHABETICAL,
|
||||||
|
sourceLocale = cursor.getStringOrNull(8)?.let { Locale(it) } ?: Locale.ROOT,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class MangaSourceCapabilities(
|
||||||
|
val availableSortOrders: Set<SortOrder>,
|
||||||
|
val availableStates: Set<MangaState>,
|
||||||
|
val availableContentRating: Set<ContentRating>,
|
||||||
|
val isMultipleTagsSupported: Boolean,
|
||||||
|
val isTagsExclusionSupported: Boolean,
|
||||||
|
val isSearchSupported: Boolean,
|
||||||
|
val contentType: ContentType,
|
||||||
|
val defaultSortOrder: SortOrder,
|
||||||
|
val sourceLocale: Locale,
|
||||||
|
)
|
||||||
|
}
|
||||||
30
app/src/main/java/org/xtimms/shirizu/core/parser/external/ExternalMangaSource.kt
vendored
Normal file
30
app/src/main/java/org/xtimms/shirizu/core/parser/external/ExternalMangaSource.kt
vendored
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
package org.xtimms.shirizu.core.parser.external
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
|
|
||||||
|
data class ExternalMangaSource(
|
||||||
|
val packageName: String,
|
||||||
|
val authority: String,
|
||||||
|
) : MangaSource {
|
||||||
|
|
||||||
|
override val name: String
|
||||||
|
get() = "content:$packageName/$authority"
|
||||||
|
|
||||||
|
private var cachedName: String? = null
|
||||||
|
|
||||||
|
fun isAvailable(context: Context): Boolean {
|
||||||
|
return context.packageManager.resolveContentProvider(authority, 0)?.isEnabled == true
|
||||||
|
}
|
||||||
|
|
||||||
|
fun resolveName(context: Context): String {
|
||||||
|
cachedName?.let {
|
||||||
|
return it
|
||||||
|
}
|
||||||
|
val pm = context.packageManager
|
||||||
|
val info = pm.resolveContentProvider(authority, 0)
|
||||||
|
return info?.loadLabel(pm)?.toString()?.also {
|
||||||
|
cachedName = it
|
||||||
|
} ?: authority
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,13 +1,20 @@
|
|||||||
package org.xtimms.shirizu.core.parser.favicon
|
package org.xtimms.shirizu.core.parser.favicon
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.graphics.drawable.AdaptiveIconDrawable
|
||||||
|
import android.graphics.drawable.ColorDrawable
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
|
import android.graphics.drawable.LayerDrawable
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
import android.webkit.MimeTypeMap
|
import android.webkit.MimeTypeMap
|
||||||
import coil.ImageLoader
|
import coil.ImageLoader
|
||||||
import coil.annotation.ExperimentalCoilApi
|
import coil.annotation.ExperimentalCoilApi
|
||||||
import coil.decode.DataSource
|
import coil.decode.DataSource
|
||||||
import coil.decode.ImageSource
|
import coil.decode.ImageSource
|
||||||
import coil.disk.DiskCache
|
import coil.disk.DiskCache
|
||||||
|
import coil.fetch.DrawableResult
|
||||||
import coil.fetch.FetchResult
|
import coil.fetch.FetchResult
|
||||||
import coil.fetch.Fetcher
|
import coil.fetch.Fetcher
|
||||||
import coil.fetch.SourceResult
|
import coil.fetch.SourceResult
|
||||||
@@ -15,7 +22,9 @@ import coil.network.HttpException
|
|||||||
import coil.request.Options
|
import coil.request.Options
|
||||||
import coil.size.Size
|
import coil.size.Size
|
||||||
import coil.size.pxOrElse
|
import coil.size.pxOrElse
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.ensureActive
|
import kotlinx.coroutines.ensureActive
|
||||||
|
import kotlinx.coroutines.runInterruptible
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
@@ -27,8 +36,10 @@ import org.koitharu.kotatsu.parsers.model.MangaSource
|
|||||||
import org.koitharu.kotatsu.parsers.util.await
|
import org.koitharu.kotatsu.parsers.util.await
|
||||||
import org.xtimms.shirizu.core.cache.CacheDir
|
import org.xtimms.shirizu.core.cache.CacheDir
|
||||||
import org.xtimms.shirizu.core.model.MangaSource
|
import org.xtimms.shirizu.core.model.MangaSource
|
||||||
|
import org.xtimms.shirizu.core.parser.EmptyMangaRepository
|
||||||
import org.xtimms.shirizu.core.parser.MangaRepository
|
import org.xtimms.shirizu.core.parser.MangaRepository
|
||||||
import org.xtimms.shirizu.core.parser.RemoteMangaRepository
|
import org.xtimms.shirizu.core.parser.ParserMangaRepository
|
||||||
|
import org.xtimms.shirizu.core.parser.external.ExternalMangaRepository
|
||||||
import org.xtimms.shirizu.utils.lang.writeAllCancellable
|
import org.xtimms.shirizu.utils.lang.writeAllCancellable
|
||||||
import org.xtimms.shirizu.utils.withExtraCloseable
|
import org.xtimms.shirizu.utils.withExtraCloseable
|
||||||
import java.net.HttpURLConnection
|
import java.net.HttpURLConnection
|
||||||
@@ -46,14 +57,27 @@ class FaviconFetcher(
|
|||||||
) : Fetcher {
|
) : Fetcher {
|
||||||
|
|
||||||
private val diskCacheKey
|
private val diskCacheKey
|
||||||
get() = options.diskCacheKey ?: "${mangaSource.name}[${mangaSource.ordinal}]x${options.size.toCacheKey()}"
|
get() = options.diskCacheKey ?: "${mangaSource.name}x${options.size.toCacheKey()}"
|
||||||
|
|
||||||
private val fileSystem
|
private val fileSystem
|
||||||
get() = checkNotNull(diskCache.value).fileSystem
|
get() = checkNotNull(diskCache.value).fileSystem
|
||||||
|
|
||||||
override suspend fun fetch(): FetchResult {
|
override suspend fun fetch(): FetchResult {
|
||||||
getCached(options)?.let { return it }
|
getCached(options)?.let { return it }
|
||||||
val repo = mangaRepositoryFactory.create(mangaSource) as RemoteMangaRepository
|
return when (val repo = mangaRepositoryFactory.create(mangaSource)) {
|
||||||
|
is ParserMangaRepository -> fetchParserFavicon(repo)
|
||||||
|
is ExternalMangaRepository -> fetchPluginIcon(repo)
|
||||||
|
is EmptyMangaRepository -> DrawableResult(
|
||||||
|
drawable = ColorDrawable(Color.WHITE),
|
||||||
|
isSampled = false,
|
||||||
|
dataSource = DataSource.MEMORY,
|
||||||
|
)
|
||||||
|
|
||||||
|
else -> throw IllegalArgumentException("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun fetchParserFavicon(repo: ParserMangaRepository): FetchResult {
|
||||||
val sizePx = maxOf(
|
val sizePx = maxOf(
|
||||||
options.size.width.pxOrElse { FALLBACK_SIZE },
|
options.size.width.pxOrElse { FALLBACK_SIZE },
|
||||||
options.size.height.pxOrElse { FALLBACK_SIZE },
|
options.size.height.pxOrElse { FALLBACK_SIZE },
|
||||||
@@ -83,6 +107,20 @@ class FaviconFetcher(
|
|||||||
throwNSEE(lastError)
|
throwNSEE(lastError)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun fetchPluginIcon(repository: ExternalMangaRepository): FetchResult {
|
||||||
|
val source = repository.source
|
||||||
|
val pm = options.context.packageManager
|
||||||
|
val icon = runInterruptible(Dispatchers.IO) {
|
||||||
|
val provider = pm.resolveContentProvider(source.authority, 0)
|
||||||
|
provider?.loadIcon(pm) ?: pm.getApplicationIcon(source.packageName)
|
||||||
|
}
|
||||||
|
return DrawableResult(
|
||||||
|
drawable = icon.nonAdaptive(),
|
||||||
|
isSampled = false,
|
||||||
|
dataSource = DataSource.DISK,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun loadIcon(url: String, source: MangaSource): Response {
|
private suspend fun loadIcon(url: String, source: MangaSource): Response {
|
||||||
val request = Request.Builder()
|
val request = Request.Builder()
|
||||||
.url(url)
|
.url(url)
|
||||||
@@ -167,12 +205,20 @@ class FaviconFetcher(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun Drawable.nonAdaptive() =
|
||||||
|
if (this is AdaptiveIconDrawable) {
|
||||||
|
LayerDrawable(arrayOf(background, foreground))
|
||||||
|
} else {
|
||||||
|
this
|
||||||
|
}
|
||||||
|
|
||||||
class Factory(
|
class Factory(
|
||||||
context: Context,
|
context: Context,
|
||||||
private val okHttpClient: OkHttpClient,
|
okHttpClientLazy: Lazy<OkHttpClient>,
|
||||||
private val mangaRepositoryFactory: MangaRepository.Factory,
|
private val mangaRepositoryFactory: MangaRepository.Factory,
|
||||||
) : Fetcher.Factory<Uri> {
|
) : Fetcher.Factory<Uri> {
|
||||||
|
|
||||||
|
private val okHttpClient by okHttpClientLazy
|
||||||
private val diskCache = lazy {
|
private val diskCache = lazy {
|
||||||
val rootDir = context.externalCacheDir ?: context.cacheDir
|
val rootDir = context.externalCacheDir ?: context.cacheDir
|
||||||
DiskCache.Builder()
|
DiskCache.Builder()
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import org.koitharu.kotatsu.parsers.model.MangaTag
|
|||||||
import org.koitharu.kotatsu.parsers.model.SortOrder
|
import org.koitharu.kotatsu.parsers.model.SortOrder
|
||||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||||
import org.xtimms.shirizu.core.model.LocalManga
|
import org.xtimms.shirizu.core.model.LocalManga
|
||||||
|
import org.xtimms.shirizu.core.model.LocalMangaSource
|
||||||
import org.xtimms.shirizu.core.model.isLocal
|
import org.xtimms.shirizu.core.model.isLocal
|
||||||
import org.xtimms.shirizu.core.parser.MangaRepository
|
import org.xtimms.shirizu.core.parser.MangaRepository
|
||||||
import org.xtimms.shirizu.core.parser.local.input.LocalMangaInput
|
import org.xtimms.shirizu.core.parser.local.input.LocalMangaInput
|
||||||
@@ -49,7 +50,7 @@ class LocalMangaRepository @Inject constructor(
|
|||||||
@LocalStorageChanges private val localStorageChanges: MutableSharedFlow<LocalManga?>,
|
@LocalStorageChanges private val localStorageChanges: MutableSharedFlow<LocalManga?>,
|
||||||
) : MangaRepository {
|
) : MangaRepository {
|
||||||
|
|
||||||
override val source = MangaSource.LOCAL
|
override val source = LocalMangaSource
|
||||||
private val locks = MultiMutex<Long>()
|
private val locks = MultiMutex<Long>()
|
||||||
private val localMappingCache = LocalMangaMappingCache()
|
private val localMappingCache = LocalMangaMappingCache()
|
||||||
|
|
||||||
@@ -60,6 +61,10 @@ class LocalMangaRepository @Inject constructor(
|
|||||||
override val states = emptySet<MangaState>()
|
override val states = emptySet<MangaState>()
|
||||||
override val contentRatings = emptySet<ContentRating>()
|
override val contentRatings = emptySet<ContentRating>()
|
||||||
|
|
||||||
|
override var defaultSortOrder: SortOrder
|
||||||
|
get() = SortOrder.NEWEST // TODO
|
||||||
|
set(value) {}
|
||||||
|
|
||||||
override suspend fun getList(offset: Int, filter: MangaListFilter?): List<Manga> {
|
override suspend fun getList(offset: Int, filter: MangaListFilter?): List<Manga> {
|
||||||
if (offset > 0) {
|
if (offset > 0) {
|
||||||
return emptyList()
|
return emptyList()
|
||||||
@@ -94,7 +99,7 @@ class LocalMangaRepository @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
override suspend fun getDetails(manga: Manga): Manga = when {
|
override suspend fun getDetails(manga: Manga): Manga = when {
|
||||||
manga.source != MangaSource.LOCAL -> requireNotNull(findSavedManga(manga)?.manga) {
|
manga.source != LocalMangaSource -> requireNotNull(findSavedManga(manga)?.manga) {
|
||||||
"Manga is not local or saved"
|
"Manga is not local or saved"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import org.json.JSONArray
|
|||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaParserSource
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaState
|
import org.koitharu.kotatsu.parsers.model.MangaState
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||||
@@ -59,7 +60,7 @@ class MangaIndex(source: String?) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getMangaInfo(): Manga? = if (json.length() == 0) null else runCatching {
|
fun getMangaInfo(): Manga? = if (json.length() == 0) null else runCatching {
|
||||||
val source = MangaSource.valueOf(json.getString("source"))
|
val source = MangaParserSource.valueOf(json.getString("source"))
|
||||||
Manga(
|
Manga(
|
||||||
id = json.getLong("id"),
|
id = json.getLong("id"),
|
||||||
title = json.getString("title"),
|
title = json.getString("title"),
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import org.koitharu.kotatsu.parsers.model.MangaPage
|
|||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
import org.koitharu.kotatsu.parsers.util.toCamelCase
|
import org.koitharu.kotatsu.parsers.util.toCamelCase
|
||||||
import org.xtimms.shirizu.core.model.LocalManga
|
import org.xtimms.shirizu.core.model.LocalManga
|
||||||
|
import org.xtimms.shirizu.core.model.LocalMangaSource
|
||||||
import org.xtimms.shirizu.core.parser.local.MangaIndex
|
import org.xtimms.shirizu.core.parser.local.MangaIndex
|
||||||
import org.xtimms.shirizu.core.parser.local.hasCbzExtension
|
import org.xtimms.shirizu.core.parser.local.hasCbzExtension
|
||||||
import org.xtimms.shirizu.core.parser.local.output.LocalMangaOutput
|
import org.xtimms.shirizu.core.parser.local.output.LocalMangaOutput
|
||||||
@@ -47,7 +48,7 @@ class LocalMangaDirInput(root: File) : LocalMangaInput(root) {
|
|||||||
index?.getCoverEntry() ?: findFirstImageEntry().orEmpty(),
|
index?.getCoverEntry() ?: findFirstImageEntry().orEmpty(),
|
||||||
)
|
)
|
||||||
val manga = info?.copy2(
|
val manga = info?.copy2(
|
||||||
source = MangaSource.LOCAL,
|
source = LocalMangaSource,
|
||||||
url = mangaUri,
|
url = mangaUri,
|
||||||
coverUrl = cover,
|
coverUrl = cover,
|
||||||
largeCoverUrl = cover,
|
largeCoverUrl = cover,
|
||||||
@@ -59,14 +60,14 @@ class LocalMangaDirInput(root: File) : LocalMangaInput(root) {
|
|||||||
// old downloads
|
// old downloads
|
||||||
chapterFiles.values.elementAtOrNull(i)
|
chapterFiles.values.elementAtOrNull(i)
|
||||||
} ?: return@mapIndexedNotNull null
|
} ?: return@mapIndexedNotNull null
|
||||||
c.copy(url = file.toUri().toString(), source = MangaSource.LOCAL)
|
c.copy(url = file.toUri().toString(), source = LocalMangaSource)
|
||||||
},
|
},
|
||||||
) ?: Manga(
|
) ?: Manga(
|
||||||
id = root.absolutePath.longHashCode(),
|
id = root.absolutePath.longHashCode(),
|
||||||
title = root.name.toHumanReadable(),
|
title = root.name.toHumanReadable(),
|
||||||
url = mangaUri,
|
url = mangaUri,
|
||||||
publicUrl = mangaUri,
|
publicUrl = mangaUri,
|
||||||
source = MangaSource.LOCAL,
|
source = LocalMangaSource,
|
||||||
coverUrl = findFirstImageEntry().orEmpty(),
|
coverUrl = findFirstImageEntry().orEmpty(),
|
||||||
chapters = chapterFiles.values.mapIndexed { i, f ->
|
chapters = chapterFiles.values.mapIndexed { i, f ->
|
||||||
MangaChapter(
|
MangaChapter(
|
||||||
@@ -74,7 +75,7 @@ class LocalMangaDirInput(root: File) : LocalMangaInput(root) {
|
|||||||
name = f.nameWithoutExtension.toHumanReadable(),
|
name = f.nameWithoutExtension.toHumanReadable(),
|
||||||
number = 0f,
|
number = 0f,
|
||||||
volume = 0,
|
volume = 0,
|
||||||
source = MangaSource.LOCAL,
|
source = LocalMangaSource,
|
||||||
uploadDate = f.creationTime,
|
uploadDate = f.creationTime,
|
||||||
url = f.toUri().toString(),
|
url = f.toUri().toString(),
|
||||||
scanlator = null,
|
scanlator = null,
|
||||||
@@ -106,7 +107,7 @@ class LocalMangaDirInput(root: File) : LocalMangaInput(root) {
|
|||||||
.toListSorted(compareBy(AlphanumComparator()) { x -> x.name })
|
.toListSorted(compareBy(AlphanumComparator()) { x -> x.name })
|
||||||
.map {
|
.map {
|
||||||
val pageUri = it.toUri().toString()
|
val pageUri = it.toUri().toString()
|
||||||
MangaPage(pageUri.longHashCode(), pageUri, null, MangaSource.LOCAL)
|
MangaPage(pageUri.longHashCode(), pageUri, null, LocalMangaSource)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ZipFile(file).use { zip ->
|
ZipFile(file).use { zip ->
|
||||||
@@ -121,7 +122,7 @@ class LocalMangaDirInput(root: File) : LocalMangaInput(root) {
|
|||||||
id = pageUri.longHashCode(),
|
id = pageUri.longHashCode(),
|
||||||
url = pageUri,
|
url = pageUri,
|
||||||
preview = null,
|
preview = null,
|
||||||
source = MangaSource.LOCAL,
|
source = LocalMangaSource,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import org.koitharu.kotatsu.parsers.model.MangaPage
|
|||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
import org.koitharu.kotatsu.parsers.util.toCamelCase
|
import org.koitharu.kotatsu.parsers.util.toCamelCase
|
||||||
import org.xtimms.shirizu.core.model.LocalManga
|
import org.xtimms.shirizu.core.model.LocalManga
|
||||||
|
import org.xtimms.shirizu.core.model.LocalMangaSource
|
||||||
import org.xtimms.shirizu.core.parser.local.MangaIndex
|
import org.xtimms.shirizu.core.parser.local.MangaIndex
|
||||||
import org.xtimms.shirizu.core.parser.local.output.LocalMangaOutput
|
import org.xtimms.shirizu.core.parser.local.output.LocalMangaOutput
|
||||||
import org.xtimms.shirizu.utils.AlphanumComparator
|
import org.xtimms.shirizu.utils.AlphanumComparator
|
||||||
@@ -47,12 +48,12 @@ class LocalMangaZipInput(root: File) : LocalMangaInput(root) {
|
|||||||
entryName = index.getCoverEntry() ?: findFirstImageEntry(zip.entries())?.name.orEmpty(),
|
entryName = index.getCoverEntry() ?: findFirstImageEntry(zip.entries())?.name.orEmpty(),
|
||||||
)
|
)
|
||||||
return@use info.copy2(
|
return@use info.copy2(
|
||||||
source = MangaSource.LOCAL,
|
source = LocalMangaSource,
|
||||||
url = fileUri,
|
url = fileUri,
|
||||||
coverUrl = cover,
|
coverUrl = cover,
|
||||||
largeCoverUrl = cover,
|
largeCoverUrl = cover,
|
||||||
chapters = info.chapters?.map { c ->
|
chapters = info.chapters?.map { c ->
|
||||||
c.copy(url = fileUri, source = MangaSource.LOCAL)
|
c.copy(url = fileUri, source = LocalMangaSource)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -70,7 +71,7 @@ class LocalMangaZipInput(root: File) : LocalMangaInput(root) {
|
|||||||
title = title,
|
title = title,
|
||||||
url = fileUri,
|
url = fileUri,
|
||||||
publicUrl = fileUri,
|
publicUrl = fileUri,
|
||||||
source = MangaSource.LOCAL,
|
source = LocalMangaSource,
|
||||||
coverUrl = zipUri(root, findFirstImageEntry(zip.entries())?.name.orEmpty()),
|
coverUrl = zipUri(root, findFirstImageEntry(zip.entries())?.name.orEmpty()),
|
||||||
chapters = chapters.sortedWith(AlphanumComparator())
|
chapters = chapters.sortedWith(AlphanumComparator())
|
||||||
.mapIndexed { i, s ->
|
.mapIndexed { i, s ->
|
||||||
@@ -79,7 +80,7 @@ class LocalMangaZipInput(root: File) : LocalMangaInput(root) {
|
|||||||
name = s.ifEmpty { title },
|
name = s.ifEmpty { title },
|
||||||
number = 0f,
|
number = 0f,
|
||||||
volume = 0,
|
volume = 0,
|
||||||
source = MangaSource.LOCAL,
|
source = LocalMangaSource,
|
||||||
uploadDate = 0L,
|
uploadDate = 0L,
|
||||||
url = uriBuilder.fragment(s).build().toString(),
|
url = uriBuilder.fragment(s).build().toString(),
|
||||||
scanlator = null,
|
scanlator = null,
|
||||||
@@ -135,7 +136,7 @@ class LocalMangaZipInput(root: File) : LocalMangaInput(root) {
|
|||||||
id = entryUri.longHashCode(),
|
id = entryUri.longHashCode(),
|
||||||
url = entryUri,
|
url = entryUri,
|
||||||
preview = null,
|
preview = null,
|
||||||
source = MangaSource.LOCAL,
|
source = LocalMangaSource,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,13 +6,14 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.runInterruptible
|
import kotlinx.coroutines.runInterruptible
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
|
import org.xtimms.shirizu.core.model.LocalMangaSource
|
||||||
|
|
||||||
class LocalMangaUtil(
|
class LocalMangaUtil(
|
||||||
private val manga: Manga,
|
private val manga: Manga,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
init {
|
init {
|
||||||
require(manga.source == MangaSource.LOCAL) {
|
require(manga.source == LocalMangaSource) {
|
||||||
"Expected LOCAL source but ${manga.source} found"
|
"Expected LOCAL source but ${manga.source} found"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import org.xtimms.shirizu.App
|
|||||||
import org.xtimms.shirizu.ui.theme.SEED
|
import org.xtimms.shirizu.ui.theme.SEED
|
||||||
import org.xtimms.shirizu.R
|
import org.xtimms.shirizu.R
|
||||||
import org.xtimms.shirizu.core.network.doh.DoHProvider
|
import org.xtimms.shirizu.core.network.doh.DoHProvider
|
||||||
|
import org.xtimms.shirizu.sections.library.history.SortOption
|
||||||
import org.xtimms.shirizu.ui.monet.PaletteStyle
|
import org.xtimms.shirizu.ui.monet.PaletteStyle
|
||||||
import org.xtimms.shirizu.utils.lang.processLifecycleScope
|
import org.xtimms.shirizu.utils.lang.processLifecycleScope
|
||||||
import org.xtimms.shirizu.utils.system.LocaleLanguageCodeMap
|
import org.xtimms.shirizu.utils.system.LocaleLanguageCodeMap
|
||||||
@@ -36,6 +37,8 @@ const val CONFIGURE = "configure"
|
|||||||
const val NOTIFICATION = "notification"
|
const val NOTIFICATION = "notification"
|
||||||
const val READING_TIME = "reading_time"
|
const val READING_TIME = "reading_time"
|
||||||
const val GRID_COLUMNS = "grid_columns"
|
const val GRID_COLUMNS = "grid_columns"
|
||||||
|
const val SORT_OPTION = "sort_option"
|
||||||
|
|
||||||
const val DOH = "doh"
|
const val DOH = "doh"
|
||||||
|
|
||||||
const val SYSTEM_DEFAULT = 0
|
const val SYSTEM_DEFAULT = 0
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ class SourceSettings(context: Context, source: MangaSource) : MangaSourceConfig
|
|||||||
is ConfigKey.Domain -> prefs.getString(key.key, key.defaultValue).ifNullOrEmpty { key.defaultValue }
|
is ConfigKey.Domain -> prefs.getString(key.key, key.defaultValue).ifNullOrEmpty { key.defaultValue }
|
||||||
is ConfigKey.ShowSuspiciousContent -> prefs.getBoolean(key.key, key.defaultValue)
|
is ConfigKey.ShowSuspiciousContent -> prefs.getBoolean(key.key, key.defaultValue)
|
||||||
is ConfigKey.SplitByTranslations -> prefs.getBoolean(key.key, key.defaultValue)
|
is ConfigKey.SplitByTranslations -> prefs.getBoolean(key.key, key.defaultValue)
|
||||||
|
is ConfigKey.PreferredImageServer -> prefs.getString(key.key, key.defaultValue)?.takeUnless(String::isEmpty)
|
||||||
} as T
|
} as T
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,6 +41,7 @@ class SourceSettings(context: Context, source: MangaSource) : MangaSourceConfig
|
|||||||
is ConfigKey.ShowSuspiciousContent -> putBoolean(key.key, value as Boolean)
|
is ConfigKey.ShowSuspiciousContent -> putBoolean(key.key, value as Boolean)
|
||||||
is ConfigKey.UserAgent -> putString(key.key, value as String?)
|
is ConfigKey.UserAgent -> putString(key.key, value as String?)
|
||||||
is ConfigKey.SplitByTranslations -> putBoolean(key.key, value as Boolean)
|
is ConfigKey.SplitByTranslations -> putBoolean(key.key, value as Boolean)
|
||||||
|
is ConfigKey.PreferredImageServer -> putString(key.key, value as String? ?: "")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6,7 +6,7 @@ import org.koitharu.kotatsu.parsers.model.Manga
|
|||||||
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
|
||||||
import org.xtimms.shirizu.core.model.getPreferredBranch
|
import org.xtimms.shirizu.core.model.getPreferredBranch
|
||||||
import org.xtimms.shirizu.core.parser.MangaRepository
|
import org.xtimms.shirizu.core.parser.MangaRepository
|
||||||
import org.xtimms.shirizu.core.parser.RemoteMangaRepository
|
import org.xtimms.shirizu.core.parser.ParserMangaRepository
|
||||||
import org.xtimms.shirizu.core.tracker.model.MangaTracking
|
import org.xtimms.shirizu.core.tracker.model.MangaTracking
|
||||||
import org.xtimms.shirizu.core.tracker.model.MangaUpdates
|
import org.xtimms.shirizu.core.tracker.model.MangaUpdates
|
||||||
import org.xtimms.shirizu.data.repository.HistoryRepository
|
import org.xtimms.shirizu.data.repository.HistoryRepository
|
||||||
@@ -51,7 +51,7 @@ class Tracker @Inject constructor(
|
|||||||
): MangaUpdates = withMangaLock(track.manga.id) {
|
): MangaUpdates = withMangaLock(track.manga.id) {
|
||||||
val updates = runCatchingCancellable {
|
val updates = runCatchingCancellable {
|
||||||
val repo = mangaRepositoryFactory.create(track.manga.source)
|
val repo = mangaRepositoryFactory.create(track.manga.source)
|
||||||
require(repo is RemoteMangaRepository) { "Repository ${repo.javaClass.simpleName} is not supported" }
|
require(repo is ParserMangaRepository) { "Repository ${repo.javaClass.simpleName} is not supported" }
|
||||||
val manga = repo.getDetails(track.manga, CachePolicy.WRITE_ONLY)
|
val manga = repo.getDetails(track.manga, CachePolicy.WRITE_ONLY)
|
||||||
compare(track, manga, getBranch(manga))
|
compare(track, manga, getBranch(manga))
|
||||||
}.getOrElse { error ->
|
}.getOrElse { error ->
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import kotlinx.coroutines.Dispatchers
|
|||||||
import kotlinx.coroutines.currentCoroutineContext
|
import kotlinx.coroutines.currentCoroutineContext
|
||||||
import kotlinx.coroutines.isActive
|
import kotlinx.coroutines.isActive
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import org.koitharu.kotatsu.parsers.model.ContentType
|
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||||
@@ -123,22 +122,6 @@ class MangaSearchRepository @Inject constructor(
|
|||||||
return db.getTagsDao().findRareTags(source.name, limit).toMangaTagsList()
|
return db.getTagsDao().findRareTags(source.name, limit).toMangaTagsList()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getSourcesSuggestion(query: String, limit: Int): List<MangaSource> {
|
|
||||||
if (query.length < 3) {
|
|
||||||
return emptyList()
|
|
||||||
}
|
|
||||||
val skipNsfw = !AppSettings.isNSFWEnabled()
|
|
||||||
val sources = sourcesRepository.allMangaSources
|
|
||||||
.filter { x ->
|
|
||||||
(x.contentType != ContentType.HENTAI || !skipNsfw) && x.title.contains(query, ignoreCase = true)
|
|
||||||
}
|
|
||||||
return if (limit == 0) {
|
|
||||||
sources
|
|
||||||
} else {
|
|
||||||
sources.take(limit)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun saveSearchQuery(query: String) {
|
fun saveSearchQuery(query: String) {
|
||||||
recentSuggestions.saveRecentQuery(query, null)
|
recentSuggestions.saveRecentQuery(query, null)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,38 @@
|
|||||||
package org.xtimms.shirizu.data.repository
|
package org.xtimms.shirizu.data.repository
|
||||||
|
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
import dagger.Reusable
|
import dagger.Reusable
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||||
|
import kotlinx.coroutines.channels.awaitClose
|
||||||
|
import kotlinx.coroutines.channels.trySendBlocking
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.callbackFlow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.flatMapLatest
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
import kotlinx.coroutines.flow.flowOf
|
import kotlinx.coroutines.flow.flowOf
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.onStart
|
||||||
import org.koitharu.kotatsu.parsers.model.ContentType
|
import org.koitharu.kotatsu.parsers.model.ContentType
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaParserSource
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
import org.koitharu.kotatsu.parsers.util.mapToSet
|
import org.koitharu.kotatsu.parsers.util.mapToSet
|
||||||
|
import org.xtimms.shirizu.BuildConfig
|
||||||
import org.xtimms.shirizu.core.database.ShirizuDatabase
|
import org.xtimms.shirizu.core.database.ShirizuDatabase
|
||||||
import org.xtimms.shirizu.core.database.dao.MangaSourcesDao
|
import org.xtimms.shirizu.core.database.dao.MangaSourcesDao
|
||||||
import org.xtimms.shirizu.core.database.entity.MangaSourceEntity
|
import org.xtimms.shirizu.core.database.entity.MangaSourceEntity
|
||||||
import org.xtimms.shirizu.core.model.MangaSource
|
import org.xtimms.shirizu.core.model.MangaSource
|
||||||
|
import org.xtimms.shirizu.core.model.MangaSourceInfo
|
||||||
|
import org.xtimms.shirizu.core.model.getTitle
|
||||||
import org.xtimms.shirizu.core.model.isNsfw
|
import org.xtimms.shirizu.core.model.isNsfw
|
||||||
|
import org.xtimms.shirizu.core.parser.external.ExternalMangaSource
|
||||||
import org.xtimms.shirizu.core.prefs.AppSettings
|
import org.xtimms.shirizu.core.prefs.AppSettings
|
||||||
import org.xtimms.shirizu.core.prefs.KotatsuAppSettings
|
import org.xtimms.shirizu.core.prefs.KotatsuAppSettings
|
||||||
import org.xtimms.shirizu.core.prefs.observeAsFlow
|
import org.xtimms.shirizu.core.prefs.observeAsFlow
|
||||||
@@ -25,24 +40,28 @@ import org.xtimms.shirizu.sections.explore.data.SourcesSortOrder
|
|||||||
import org.xtimms.shirizu.utils.ReversibleHandle
|
import org.xtimms.shirizu.utils.ReversibleHandle
|
||||||
import java.util.Collections
|
import java.util.Collections
|
||||||
import java.util.EnumSet
|
import java.util.EnumSet
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@Singleton
|
||||||
@Reusable
|
|
||||||
class MangaSourcesRepository @Inject constructor(
|
class MangaSourcesRepository @Inject constructor(
|
||||||
|
@ApplicationContext private val context: Context,
|
||||||
private val db: ShirizuDatabase,
|
private val db: ShirizuDatabase,
|
||||||
private val settings: KotatsuAppSettings,
|
private val settings: KotatsuAppSettings,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
private val isNewSourcesAssimilated = AtomicBoolean(false)
|
||||||
private val dao: MangaSourcesDao
|
private val dao: MangaSourcesDao
|
||||||
get() = db.getSourcesDao()
|
get() = db.getSourcesDao()
|
||||||
|
|
||||||
private val remoteSources = EnumSet.allOf(MangaSource::class.java).apply {
|
private val remoteSources = EnumSet.allOf(MangaParserSource::class.java).apply {
|
||||||
remove(MangaSource.LOCAL)
|
if (!BuildConfig.DEBUG) {
|
||||||
remove(MangaSource.DUMMY)
|
remove(MangaParserSource.DUMMY)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val allMangaSources: Set<MangaSource>
|
val allMangaSources: Set<MangaParserSource>
|
||||||
get() = Collections.unmodifiableSet(remoteSources)
|
get() = Collections.unmodifiableSet(remoteSources)
|
||||||
|
|
||||||
suspend fun getEnabledSources(): List<MangaSource> {
|
suspend fun getEnabledSources(): List<MangaSource> {
|
||||||
@@ -54,7 +73,8 @@ class MangaSourcesRepository @Inject constructor(
|
|||||||
return dao.findAllDisabled().toSources(settings.isNsfwContentDisabled)
|
return dao.findAllDisabled().toSources(settings.isNsfwContentDisabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun observeDisabledSources(): Flow<List<MangaSource>> = combine(
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
fun observeDisabledSources(): Flow<List<MangaParserSource>> = combine(
|
||||||
observeIsNsfwDisabled(),
|
observeIsNsfwDisabled(),
|
||||||
observeSortOrder(),
|
observeSortOrder(),
|
||||||
) { skipNsfw, _ ->
|
) { skipNsfw, _ ->
|
||||||
@@ -84,7 +104,7 @@ class MangaSourcesRepository @Inject constructor(
|
|||||||
}.distinctUntilChanged()
|
}.distinctUntilChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun observeEnabledSources(): Flow<List<MangaSource>> = combine(
|
fun observeEnabledSources(): Flow<List<MangaParserSource>> = combine(
|
||||||
observeIsNsfwDisabled(),
|
observeIsNsfwDisabled(),
|
||||||
observeSortOrder(),
|
observeSortOrder(),
|
||||||
) { skipNsfw, order ->
|
) { skipNsfw, order ->
|
||||||
@@ -120,10 +140,13 @@ class MangaSourcesRepository @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun assimilateNewSources(): Set<MangaSource> {
|
suspend fun assimilateNewSources(): Boolean {
|
||||||
|
if (isNewSourcesAssimilated.getAndSet(true)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
val new = getNewSources()
|
val new = getNewSources()
|
||||||
if (new.isEmpty()) {
|
if (new.isEmpty()) {
|
||||||
return emptySet()
|
return false
|
||||||
}
|
}
|
||||||
var maxSortKey = dao.getMaxSortKey()
|
var maxSortKey = dao.getMaxSortKey()
|
||||||
val entities = new.map { x ->
|
val entities = new.map { x ->
|
||||||
@@ -131,20 +154,20 @@ class MangaSourcesRepository @Inject constructor(
|
|||||||
source = x.name,
|
source = x.name,
|
||||||
isEnabled = false,
|
isEnabled = false,
|
||||||
sortKey = ++maxSortKey,
|
sortKey = ++maxSortKey,
|
||||||
|
addedIn = BuildConfig.VERSION_CODE,
|
||||||
|
lastUsedAt = 0,
|
||||||
|
isPinned = false,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
dao.insertIfAbsent(entities)
|
dao.insertIfAbsent(entities)
|
||||||
if (settings.isNsfwContentDisabled) {
|
return true
|
||||||
new.removeAll { x -> x.isNsfw() }
|
|
||||||
}
|
|
||||||
return new
|
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun isSetupRequired(): Boolean {
|
suspend fun isSetupRequired(): Boolean {
|
||||||
return dao.findAll().isEmpty()
|
return dao.findAll().isEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun getNewSources(): MutableSet<MangaSource> {
|
private suspend fun getNewSources(): MutableSet<out MangaSource> {
|
||||||
val entities = dao.findAll()
|
val entities = dao.findAll()
|
||||||
val result = EnumSet.copyOf(remoteSources)
|
val result = EnumSet.copyOf(remoteSources)
|
||||||
for (e in entities) {
|
for (e in entities) {
|
||||||
@@ -155,11 +178,11 @@ class MangaSourcesRepository @Inject constructor(
|
|||||||
|
|
||||||
private fun List<MangaSourceEntity>.toSources(
|
private fun List<MangaSourceEntity>.toSources(
|
||||||
skipNsfwSources: Boolean,
|
skipNsfwSources: Boolean,
|
||||||
): List<MangaSource> {
|
): List<MangaParserSource> {
|
||||||
val result = ArrayList<MangaSource>(size)
|
val result = ArrayList<MangaParserSource>(size)
|
||||||
for (entity in this) {
|
for (entity in this) {
|
||||||
val source = MangaSource(entity.source)
|
val source = entity.source.toMangaSourceOrNull() ?: continue
|
||||||
if (skipNsfwSources && source.contentType == ContentType.HENTAI) {
|
if (skipNsfwSources && source.isNsfw()) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if (source in remoteSources) {
|
if (source in remoteSources) {
|
||||||
@@ -169,6 +192,41 @@ class MangaSourcesRepository @Inject constructor(
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun observeExternalSources(): Flow<List<ExternalMangaSource>> {
|
||||||
|
val intent = Intent("app.kotatsu.parser.PROVIDE_MANGA")
|
||||||
|
val pm = context.packageManager
|
||||||
|
return callbackFlow {
|
||||||
|
val receiver = object : BroadcastReceiver() {
|
||||||
|
override fun onReceive(context: Context?, intent: Intent?) {
|
||||||
|
trySendBlocking(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ContextCompat.registerReceiver(
|
||||||
|
context,
|
||||||
|
receiver,
|
||||||
|
IntentFilter().apply {
|
||||||
|
addAction(Intent.ACTION_PACKAGE_ADDED)
|
||||||
|
addAction(Intent.ACTION_PACKAGE_VERIFIED)
|
||||||
|
addAction(Intent.ACTION_PACKAGE_REPLACED)
|
||||||
|
addAction(Intent.ACTION_PACKAGE_REMOVED)
|
||||||
|
addAction(Intent.ACTION_PACKAGE_FULLY_REMOVED)
|
||||||
|
addDataScheme("package")
|
||||||
|
},
|
||||||
|
ContextCompat.RECEIVER_EXPORTED,
|
||||||
|
)
|
||||||
|
awaitClose { context.unregisterReceiver(receiver) }
|
||||||
|
}.onStart {
|
||||||
|
emit(null)
|
||||||
|
}.map {
|
||||||
|
pm.queryIntentContentProviders(intent, 0).map { resolveInfo ->
|
||||||
|
ExternalMangaSource(
|
||||||
|
packageName = resolveInfo.providerInfo.packageName,
|
||||||
|
authority = resolveInfo.providerInfo.authority,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}.distinctUntilChanged()
|
||||||
|
}
|
||||||
|
|
||||||
private fun observeIsNsfwDisabled() = MutableStateFlow(AppSettings.isNSFWEnabled()).asStateFlow()
|
private fun observeIsNsfwDisabled() = MutableStateFlow(AppSettings.isNSFWEnabled()).asStateFlow()
|
||||||
|
|
||||||
private fun observeIsNewSourcesEnabled() = settings.observeAsFlow(KotatsuAppSettings.KEY_SOURCES_NEW) {
|
private fun observeIsNewSourcesEnabled() = settings.observeAsFlow(KotatsuAppSettings.KEY_SOURCES_NEW) {
|
||||||
@@ -178,4 +236,6 @@ class MangaSourcesRepository @Inject constructor(
|
|||||||
private fun observeSortOrder() = settings.observeAsFlow(KotatsuAppSettings.KEY_SOURCES_ORDER) {
|
private fun observeSortOrder() = settings.observeAsFlow(KotatsuAppSettings.KEY_SOURCES_ORDER) {
|
||||||
sourcesSortOrder
|
sourcesSortOrder
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun String.toMangaSourceOrNull(): MangaParserSource? = MangaParserSource.entries.find { it.name == this }
|
||||||
}
|
}
|
||||||
@@ -5,6 +5,7 @@ import org.koitharu.kotatsu.parsers.model.SortOrder
|
|||||||
import org.koitharu.kotatsu.parsers.util.json.getBooleanOrDefault
|
import org.koitharu.kotatsu.parsers.util.json.getBooleanOrDefault
|
||||||
import org.koitharu.kotatsu.parsers.util.json.getFloatOrDefault
|
import org.koitharu.kotatsu.parsers.util.json.getFloatOrDefault
|
||||||
import org.koitharu.kotatsu.parsers.util.json.getIntOrDefault
|
import org.koitharu.kotatsu.parsers.util.json.getIntOrDefault
|
||||||
|
import org.koitharu.kotatsu.parsers.util.json.getLongOrDefault
|
||||||
import org.koitharu.kotatsu.parsers.util.json.getStringOrNull
|
import org.koitharu.kotatsu.parsers.util.json.getStringOrNull
|
||||||
import org.xtimms.shirizu.core.database.entity.BookmarkEntity
|
import org.xtimms.shirizu.core.database.entity.BookmarkEntity
|
||||||
import org.xtimms.shirizu.core.database.entity.FavouriteCategoryEntity
|
import org.xtimms.shirizu.core.database.entity.FavouriteCategoryEntity
|
||||||
@@ -83,6 +84,9 @@ class JsonDeserializer(private val json: JSONObject) {
|
|||||||
source = json.getString("source"),
|
source = json.getString("source"),
|
||||||
isEnabled = json.getBoolean("enabled"),
|
isEnabled = json.getBoolean("enabled"),
|
||||||
sortKey = json.getInt("sort_key"),
|
sortKey = json.getInt("sort_key"),
|
||||||
|
addedIn = json.getIntOrDefault("added_in", 0),
|
||||||
|
lastUsedAt = json.getLongOrDefault("used_at", 0L),
|
||||||
|
isPinned = json.getBooleanOrDefault("pinned", false),
|
||||||
)
|
)
|
||||||
|
|
||||||
fun toMap(): Map<String, Any?> {
|
fun toMap(): Map<String, Any?> {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package org.xtimms.shirizu.di
|
package org.xtimms.shirizu.di
|
||||||
|
|
||||||
import android.app.Application
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.text.Html
|
import android.text.Html
|
||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
@@ -22,9 +21,6 @@ import okhttp3.OkHttpClient
|
|||||||
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
import org.koitharu.kotatsu.parsers.MangaLoaderContext
|
||||||
import org.xtimms.shirizu.BuildConfig
|
import org.xtimms.shirizu.BuildConfig
|
||||||
import org.xtimms.shirizu.core.cache.CacheDir
|
import org.xtimms.shirizu.core.cache.CacheDir
|
||||||
import org.xtimms.shirizu.core.cache.ContentCache
|
|
||||||
import org.xtimms.shirizu.core.cache.MemoryContentCache
|
|
||||||
import org.xtimms.shirizu.core.cache.StubContentCache
|
|
||||||
import org.xtimms.shirizu.core.database.ShirizuDatabase
|
import org.xtimms.shirizu.core.database.ShirizuDatabase
|
||||||
import org.xtimms.shirizu.core.model.LocalManga
|
import org.xtimms.shirizu.core.model.LocalManga
|
||||||
import org.xtimms.shirizu.core.network.MangaHttpClient
|
import org.xtimms.shirizu.core.network.MangaHttpClient
|
||||||
@@ -38,6 +34,7 @@ import org.xtimms.shirizu.sections.reader.thumbnails.MangaPageFetcher
|
|||||||
import org.xtimms.shirizu.utils.CoilImageGetter
|
import org.xtimms.shirizu.utils.CoilImageGetter
|
||||||
import org.xtimms.shirizu.utils.system.connectivityManager
|
import org.xtimms.shirizu.utils.system.connectivityManager
|
||||||
import org.xtimms.shirizu.utils.system.isLowRamDevice
|
import org.xtimms.shirizu.utils.system.isLowRamDevice
|
||||||
|
import javax.inject.Provider
|
||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
@Module
|
@Module
|
||||||
@@ -70,7 +67,7 @@ interface ShirizuModule {
|
|||||||
@Singleton
|
@Singleton
|
||||||
fun provideCoil(
|
fun provideCoil(
|
||||||
@ApplicationContext context: Context,
|
@ApplicationContext context: Context,
|
||||||
@MangaHttpClient okHttpClient: OkHttpClient,
|
@MangaHttpClient okHttpClientProvider: Provider<OkHttpClient>,
|
||||||
mangaRepositoryFactory: MangaRepository.Factory,
|
mangaRepositoryFactory: MangaRepository.Factory,
|
||||||
imageProxyInterceptor: ImageProxyInterceptor,
|
imageProxyInterceptor: ImageProxyInterceptor,
|
||||||
pageFetcherFactory: MangaPageFetcher.Factory,
|
pageFetcherFactory: MangaPageFetcher.Factory,
|
||||||
@@ -81,37 +78,30 @@ interface ShirizuModule {
|
|||||||
.directory(rootDir.resolve(CacheDir.THUMBS.dir))
|
.directory(rootDir.resolve(CacheDir.THUMBS.dir))
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
val okHttpClientLazy = lazy {
|
||||||
|
okHttpClientProvider.get().newBuilder().cache(null).build()
|
||||||
|
}
|
||||||
return ImageLoader.Builder(context)
|
return ImageLoader.Builder(context)
|
||||||
.crossfade(500)
|
.crossfade(500)
|
||||||
.okHttpClient(okHttpClient.newBuilder().cache(null).build())
|
.okHttpClient { okHttpClientLazy.value }
|
||||||
.interceptorDispatcher(Dispatchers.Default)
|
.interceptorDispatcher(Dispatchers.Default)
|
||||||
.fetcherDispatcher(Dispatchers.IO)
|
.fetcherDispatcher(Dispatchers.IO)
|
||||||
.decoderDispatcher(Dispatchers.Default)
|
.decoderDispatcher(Dispatchers.Default)
|
||||||
.transformationDispatcher(Dispatchers.Default)
|
.transformationDispatcher(Dispatchers.Default)
|
||||||
.diskCache(diskCacheFactory)
|
.diskCache(diskCacheFactory)
|
||||||
|
.respectCacheHeaders(false)
|
||||||
|
.networkObserverEnabled(false)
|
||||||
.logger(if (BuildConfig.DEBUG) DebugLogger() else null)
|
.logger(if (BuildConfig.DEBUG) DebugLogger() else null)
|
||||||
.allowRgb565(context.isLowRamDevice())
|
.allowRgb565(context.isLowRamDevice())
|
||||||
.components(
|
.components(
|
||||||
ComponentRegistry.Builder()
|
ComponentRegistry.Builder()
|
||||||
.add(FaviconFetcher.Factory(context, okHttpClient, mangaRepositoryFactory))
|
.add(FaviconFetcher.Factory(context, okHttpClientLazy, mangaRepositoryFactory))
|
||||||
.add(pageFetcherFactory)
|
.add(pageFetcherFactory)
|
||||||
.add(imageProxyInterceptor)
|
.add(imageProxyInterceptor)
|
||||||
.build(),
|
.build(),
|
||||||
).build()
|
).build()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Provides
|
|
||||||
@Singleton
|
|
||||||
fun provideContentCache(
|
|
||||||
application: Application,
|
|
||||||
): ContentCache {
|
|
||||||
return if (application.isLowRamDevice()) {
|
|
||||||
StubContentCache()
|
|
||||||
} else {
|
|
||||||
MemoryContentCache(application)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Provides
|
@Provides
|
||||||
@Singleton
|
@Singleton
|
||||||
@LocalStorageChanges
|
@LocalStorageChanges
|
||||||
|
|||||||
@@ -32,11 +32,13 @@ import androidx.compose.ui.unit.dp
|
|||||||
import coil.ImageLoader
|
import coil.ImageLoader
|
||||||
import coil.compose.AsyncImage
|
import coil.compose.AsyncImage
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaParserSource
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaState
|
import org.koitharu.kotatsu.parsers.model.MangaState
|
||||||
import org.xtimms.shirizu.R
|
import org.xtimms.shirizu.R
|
||||||
import org.xtimms.shirizu.core.ShirizuAsyncImage
|
import org.xtimms.shirizu.core.ShirizuAsyncImage
|
||||||
import org.xtimms.shirizu.core.components.MangaCover
|
import org.xtimms.shirizu.core.components.MangaCover
|
||||||
|
import org.xtimms.shirizu.core.model.getTitle
|
||||||
import org.xtimms.shirizu.sections.details.data.ReadingTime
|
import org.xtimms.shirizu.sections.details.data.ReadingTime
|
||||||
import org.xtimms.shirizu.sections.details.model.HistoryInfo
|
import org.xtimms.shirizu.sections.details.model.HistoryInfo
|
||||||
|
|
||||||
@@ -45,13 +47,13 @@ fun ClassicDetailsInfoBox(
|
|||||||
imageUrl: String,
|
imageUrl: String,
|
||||||
favicon: Uri,
|
favicon: Uri,
|
||||||
title: String,
|
title: String,
|
||||||
altTitle: String,
|
altTitle: String?,
|
||||||
author: String,
|
author: String?,
|
||||||
isNsfw: Boolean,
|
isNsfw: Boolean,
|
||||||
state: MangaState?,
|
state: MangaState?,
|
||||||
source: MangaSource,
|
source: MangaSource,
|
||||||
historyInfo: HistoryInfo,
|
historyInfo: HistoryInfo?,
|
||||||
readingTime: ReadingTime,
|
readingTime: ReadingTime?,
|
||||||
isTabletUi: Boolean,
|
isTabletUi: Boolean,
|
||||||
appBarPadding: Dp,
|
appBarPadding: Dp,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
@@ -133,8 +135,8 @@ fun MangaInfoLarge(
|
|||||||
imageUrl: String,
|
imageUrl: String,
|
||||||
favicon: Uri,
|
favicon: Uri,
|
||||||
title: String,
|
title: String,
|
||||||
altTitle: String,
|
altTitle: String?,
|
||||||
author: String,
|
author: String?,
|
||||||
source: MangaSource,
|
source: MangaSource,
|
||||||
state: MangaState?,
|
state: MangaState?,
|
||||||
historyInfo: HistoryInfo?,
|
historyInfo: HistoryInfo?,
|
||||||
@@ -168,7 +170,7 @@ fun MangaInfoLarge(
|
|||||||
altTitle = altTitle,
|
altTitle = altTitle,
|
||||||
author = author,
|
author = author,
|
||||||
state = state,
|
state = state,
|
||||||
source = source.title,
|
source = source.name,
|
||||||
isInShelf = isInShelf,
|
isInShelf = isInShelf,
|
||||||
onAddToShelfClicked = onAddToShelfClicked,
|
onAddToShelfClicked = onAddToShelfClicked,
|
||||||
onSourceClicked = onSourceClicked,
|
onSourceClicked = onSourceClicked,
|
||||||
@@ -185,12 +187,12 @@ fun MangaInfoSmall(
|
|||||||
imageUrl: String,
|
imageUrl: String,
|
||||||
favicon: Uri,
|
favicon: Uri,
|
||||||
title: String,
|
title: String,
|
||||||
altTitle: String,
|
altTitle: String?,
|
||||||
author: String,
|
author: String?,
|
||||||
state: MangaState?,
|
state: MangaState?,
|
||||||
source: MangaSource,
|
source: MangaSource,
|
||||||
historyInfo: HistoryInfo,
|
historyInfo: HistoryInfo?,
|
||||||
readingTime: ReadingTime,
|
readingTime: ReadingTime?,
|
||||||
isInShelf: Boolean,
|
isInShelf: Boolean,
|
||||||
onAddToShelfClicked: () -> Unit,
|
onAddToShelfClicked: () -> Unit,
|
||||||
onCoverClick: () -> Unit,
|
onCoverClick: () -> Unit,
|
||||||
@@ -225,7 +227,7 @@ fun MangaInfoSmall(
|
|||||||
altTitle = altTitle,
|
altTitle = altTitle,
|
||||||
author = author,
|
author = author,
|
||||||
state = state,
|
state = state,
|
||||||
source = source.title,
|
source = source.name,
|
||||||
isInShelf = isInShelf,
|
isInShelf = isInShelf,
|
||||||
onAddToShelfClicked = onAddToShelfClicked,
|
onAddToShelfClicked = onAddToShelfClicked,
|
||||||
onSourceClicked = onSourceClicked,
|
onSourceClicked = onSourceClicked,
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ import androidx.compose.material3.HorizontalDivider
|
|||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.InputChip
|
import androidx.compose.material3.InputChip
|
||||||
import androidx.compose.material3.LocalMinimumInteractiveComponentEnforcement
|
import androidx.compose.material3.LocalMinimumInteractiveComponentEnforcement
|
||||||
|
import androidx.compose.material3.LocalMinimumInteractiveComponentSize
|
||||||
import androidx.compose.material3.LocalTextStyle
|
import androidx.compose.material3.LocalTextStyle
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.OutlinedIconButton
|
import androidx.compose.material3.OutlinedIconButton
|
||||||
@@ -82,6 +83,7 @@ import androidx.compose.ui.unit.dp
|
|||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import coil.ImageLoader
|
import coil.ImageLoader
|
||||||
import coil.compose.AsyncImage
|
import coil.compose.AsyncImage
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaParserSource
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaState
|
import org.koitharu.kotatsu.parsers.model.MangaState
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaTag
|
import org.koitharu.kotatsu.parsers.model.MangaTag
|
||||||
@@ -108,7 +110,7 @@ fun MangaAndSourceTitlesLarge(
|
|||||||
title: String,
|
title: String,
|
||||||
altTitle: String,
|
altTitle: String,
|
||||||
author: String,
|
author: String,
|
||||||
source: MangaSource,
|
source: MangaParserSource,
|
||||||
state: MangaState?,
|
state: MangaState?,
|
||||||
historyInfo: HistoryInfo,
|
historyInfo: HistoryInfo,
|
||||||
readingTime: ReadingTime?,
|
readingTime: ReadingTime?,
|
||||||
@@ -153,7 +155,7 @@ fun MangaAndSourceTitlesSmall(
|
|||||||
altTitle: String,
|
altTitle: String,
|
||||||
author: String,
|
author: String,
|
||||||
state: MangaState?,
|
state: MangaState?,
|
||||||
source: MangaSource,
|
source: MangaParserSource,
|
||||||
historyInfo: HistoryInfo,
|
historyInfo: HistoryInfo,
|
||||||
readingTime: ReadingTime?,
|
readingTime: ReadingTime?,
|
||||||
isInShelf: Boolean,
|
isInShelf: Boolean,
|
||||||
@@ -188,19 +190,16 @@ fun MangaAndSourceTitlesSmall(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(
|
@OptIn(ExperimentalLayoutApi::class)
|
||||||
ExperimentalLayoutApi::class,
|
|
||||||
ExperimentalMaterial3Api::class
|
|
||||||
)
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DetailsContentInfo(
|
fun DetailsContentInfo(
|
||||||
favicon: Uri,
|
favicon: Uri,
|
||||||
title: String,
|
title: String,
|
||||||
altTitle: String,
|
altTitle: String?,
|
||||||
author: String,
|
author: String?,
|
||||||
state: MangaState?,
|
state: MangaState?,
|
||||||
source: String?,
|
source: String?,
|
||||||
historyInfo: HistoryInfo,
|
historyInfo: HistoryInfo?,
|
||||||
readingTime: ReadingTime?,
|
readingTime: ReadingTime?,
|
||||||
isInShelf: Boolean,
|
isInShelf: Boolean,
|
||||||
onAddToShelfClicked: () -> Unit,
|
onAddToShelfClicked: () -> Unit,
|
||||||
@@ -224,7 +223,7 @@ fun DetailsContentInfo(
|
|||||||
maxLines = 3
|
maxLines = 3
|
||||||
)
|
)
|
||||||
|
|
||||||
if (altTitle.isNotBlank()) {
|
if (!altTitle.isNullOrBlank()) {
|
||||||
Text(
|
Text(
|
||||||
text = altTitle,
|
text = altTitle,
|
||||||
style = MaterialTheme.typography.headlineSmall,
|
style = MaterialTheme.typography.headlineSmall,
|
||||||
@@ -235,7 +234,7 @@ fun DetailsContentInfo(
|
|||||||
Spacer(modifier = Modifier.height(4.dp))
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
}
|
}
|
||||||
|
|
||||||
if (author.isNotEmpty()) {
|
if (!author.isNullOrBlank()) {
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
@@ -294,7 +293,7 @@ fun DetailsContentInfo(
|
|||||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
) {
|
) {
|
||||||
CompositionLocalProvider(LocalMinimumInteractiveComponentEnforcement provides false) {
|
CompositionLocalProvider(LocalMinimumInteractiveComponentSize provides 0.dp) {
|
||||||
InputChip(
|
InputChip(
|
||||||
selected = false,
|
selected = false,
|
||||||
onClick = { onAddToShelfClicked() },
|
onClick = { onAddToShelfClicked() },
|
||||||
@@ -354,7 +353,7 @@ fun DetailsContentInfo(
|
|||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.height(32.dp)
|
.height(32.dp)
|
||||||
.width(56.dp),
|
.width(56.dp),
|
||||||
onClick = { onAddToShelfClicked() /*TODO*/ },
|
onClick = { /*TODO*/ },
|
||||||
shape = MaterialTheme.shapes.small,
|
shape = MaterialTheme.shapes.small,
|
||||||
border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline)
|
border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline)
|
||||||
) {
|
) {
|
||||||
@@ -579,14 +578,13 @@ private fun MangaSummary(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun TagsChip(
|
private fun TagsChip(
|
||||||
tag: MangaTag,
|
tag: MangaTag,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
) {
|
) {
|
||||||
CompositionLocalProvider(LocalMinimumInteractiveComponentEnforcement provides false) {
|
CompositionLocalProvider(LocalMinimumInteractiveComponentSize provides 0.dp) {
|
||||||
SuggestionChip(
|
SuggestionChip(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import kotlinx.coroutines.withContext
|
|||||||
import org.koitharu.kotatsu.parsers.MangaParser
|
import org.koitharu.kotatsu.parsers.MangaParser
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
|
import org.xtimms.shirizu.core.model.parcelable.ParcelableManga
|
||||||
import org.xtimms.shirizu.core.ui.screens.LoadingScreen
|
import org.xtimms.shirizu.core.ui.screens.LoadingScreen
|
||||||
import org.xtimms.shirizu.utils.lang.AssistContentScreen
|
import org.xtimms.shirizu.utils.lang.AssistContentScreen
|
||||||
import org.xtimms.shirizu.utils.lang.Screen
|
import org.xtimms.shirizu.utils.lang.Screen
|
||||||
@@ -26,13 +27,9 @@ import javax.inject.Inject
|
|||||||
import javax.inject.Singleton
|
import javax.inject.Singleton
|
||||||
|
|
||||||
class DetailsScreen(
|
class DetailsScreen(
|
||||||
private val manga: Manga,
|
private val mangaId: Long,
|
||||||
val fromSource: Boolean = false,
|
val fromSource: Boolean = false,
|
||||||
) : Screen(), AssistContentScreen {
|
) : Screen() {
|
||||||
|
|
||||||
private var assistUrl: String? = null
|
|
||||||
|
|
||||||
override fun onProvideAssistUrl() = assistUrl
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun Content() {
|
override fun Content() {
|
||||||
@@ -43,7 +40,7 @@ class DetailsScreen(
|
|||||||
|
|
||||||
val screenModel =
|
val screenModel =
|
||||||
getScreenModel<DetailsScreenModel, DetailsScreenModel.Factory> { factory ->
|
getScreenModel<DetailsScreenModel, DetailsScreenModel.Factory> { factory ->
|
||||||
factory.create(context, manga, SnackbarHostState())
|
factory.create(context, mangaId, SnackbarHostState())
|
||||||
}
|
}
|
||||||
|
|
||||||
val state by screenModel.state.collectAsState()
|
val state by screenModel.state.collectAsState()
|
||||||
@@ -54,13 +51,13 @@ class DetailsScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val successState = state as DetailsScreenModel.State.Success
|
val successState = state as DetailsScreenModel.State.Success
|
||||||
val isOnlineSource = remember { successState.source != MangaSource.DUMMY && successState.source != MangaSource.LOCAL }
|
|
||||||
|
|
||||||
MangaScreen(
|
MangaScreen(
|
||||||
state = successState,
|
state = successState,
|
||||||
snackbarHostState = screenModel.snackbarHostState,
|
snackbarHostState = screenModel.snackbarHostState,
|
||||||
isTabletUi = isTabletUi(),
|
isTabletUi = isTabletUi(),
|
||||||
onBackClicked = navigator::pop,
|
onBackClicked = navigator::pop,
|
||||||
|
onMangaClicked = { },
|
||||||
onWebViewClicked = {
|
onWebViewClicked = {
|
||||||
|
|
||||||
},
|
},
|
||||||
@@ -77,15 +74,4 @@ class DetailsScreen(
|
|||||||
onCoverClicked = { },
|
onCoverClicked = { },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun getMangaUrl(manga_: Manga?, parser_: MangaParser?): String? {
|
|
||||||
val manga = manga_ ?: return null
|
|
||||||
val source = parser_ ?: return null
|
|
||||||
|
|
||||||
return try {
|
|
||||||
source.getDetails(manga).publicUrl
|
|
||||||
} catch (e: Exception) {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -3,53 +3,40 @@ package org.xtimms.shirizu.sections.details
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import androidx.compose.material3.SnackbarHostState
|
import androidx.compose.material3.SnackbarHostState
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
|
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||||
import cafe.adriel.voyager.core.model.screenModelScope
|
import cafe.adriel.voyager.core.model.screenModelScope
|
||||||
import cafe.adriel.voyager.hilt.ScreenModelFactory
|
import cafe.adriel.voyager.hilt.ScreenModelFactory
|
||||||
import dagger.assisted.Assisted
|
import dagger.assisted.Assisted
|
||||||
import dagger.assisted.AssistedFactory
|
import dagger.assisted.AssistedFactory
|
||||||
import dagger.assisted.AssistedInject
|
import dagger.assisted.AssistedInject
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
import kotlinx.coroutines.async
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.awaitAll
|
||||||
import kotlinx.coroutines.channels.Channel
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
import kotlinx.coroutines.flow.SharingStarted
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.flow.catch
|
|
||||||
import kotlinx.coroutines.flow.combine
|
|
||||||
import kotlinx.coroutines.flow.flatMapLatest
|
|
||||||
import kotlinx.coroutines.flow.flowOf
|
|
||||||
import kotlinx.coroutines.flow.map
|
|
||||||
import kotlinx.coroutines.flow.mapLatest
|
|
||||||
import kotlinx.coroutines.flow.receiveAsFlow
|
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.isActive
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.plus
|
import kotlinx.coroutines.plus
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import kotlinx.coroutines.withContext
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.xtimms.shirizu.core.parser.MangaDataRepository
|
||||||
import org.xtimms.shirizu.core.base.viewmodel.BaseStateScreenModel
|
|
||||||
import org.xtimms.shirizu.core.model.findById
|
|
||||||
import org.xtimms.shirizu.core.model.getPreferredBranch
|
|
||||||
import org.xtimms.shirizu.data.repository.BookmarksRepository
|
import org.xtimms.shirizu.data.repository.BookmarksRepository
|
||||||
import org.xtimms.shirizu.data.repository.FavouritesRepository
|
import org.xtimms.shirizu.data.repository.FavouritesRepository
|
||||||
import org.xtimms.shirizu.data.repository.HistoryRepository
|
import org.xtimms.shirizu.data.repository.HistoryRepository
|
||||||
import org.xtimms.shirizu.sections.details.data.MangaDetails
|
import org.xtimms.shirizu.sections.details.data.MangaDetails
|
||||||
import org.xtimms.shirizu.sections.details.data.ReadingTime
|
|
||||||
import org.xtimms.shirizu.sections.details.domain.BranchComparator
|
|
||||||
import org.xtimms.shirizu.sections.details.domain.DetailsInteractor
|
import org.xtimms.shirizu.sections.details.domain.DetailsInteractor
|
||||||
import org.xtimms.shirizu.sections.details.domain.DetailsLoadUseCase
|
import org.xtimms.shirizu.sections.details.domain.DetailsLoadUseCase
|
||||||
import org.xtimms.shirizu.sections.details.domain.ReadingTimeUseCase
|
import org.xtimms.shirizu.sections.details.domain.ReadingTimeUseCase
|
||||||
import org.xtimms.shirizu.sections.details.domain.RelatedMangaUseCase
|
import org.xtimms.shirizu.sections.details.domain.RelatedMangaUseCase
|
||||||
import org.xtimms.shirizu.sections.details.model.ChapterItem
|
import org.xtimms.shirizu.sections.details.model.ChapterItem
|
||||||
import org.xtimms.shirizu.sections.details.model.HistoryInfo
|
import org.xtimms.shirizu.utils.system.getDisplayMessage
|
||||||
import org.xtimms.shirizu.sections.details.model.MangaBranch
|
|
||||||
import org.xtimms.shirizu.utils.lang.onEachWhile
|
|
||||||
|
|
||||||
class DetailsScreenModel @AssistedInject constructor(
|
class DetailsScreenModel @AssistedInject constructor(
|
||||||
@Assisted val context: Context,
|
@Assisted val context: Context,
|
||||||
@Assisted val manga: Manga,
|
@Assisted val mangaId: Long,
|
||||||
private val interactor: DetailsInteractor,
|
private val interactor: DetailsInteractor,
|
||||||
|
private val mangaDataRepository: MangaDataRepository,
|
||||||
private val historyRepository: HistoryRepository,
|
private val historyRepository: HistoryRepository,
|
||||||
private val bookmarksRepository: BookmarksRepository,
|
private val bookmarksRepository: BookmarksRepository,
|
||||||
private val favouritesRepository: FavouritesRepository,
|
private val favouritesRepository: FavouritesRepository,
|
||||||
@@ -57,13 +44,16 @@ class DetailsScreenModel @AssistedInject constructor(
|
|||||||
private val readingTimeUseCase: ReadingTimeUseCase,
|
private val readingTimeUseCase: ReadingTimeUseCase,
|
||||||
private val relatedMangaUseCase: RelatedMangaUseCase,
|
private val relatedMangaUseCase: RelatedMangaUseCase,
|
||||||
@Assisted val snackbarHostState: SnackbarHostState = SnackbarHostState(),
|
@Assisted val snackbarHostState: SnackbarHostState = SnackbarHostState(),
|
||||||
) : BaseStateScreenModel<DetailsScreenModel.State>(State.Loading) {
|
) : StateScreenModel<DetailsScreenModel.State>(State.Loading) {
|
||||||
|
|
||||||
private val successState: State.Success?
|
private val successState: State.Success?
|
||||||
get() = state.value as? State.Success
|
get() = state.value as? State.Success
|
||||||
|
|
||||||
private val _events: Channel<Event> = Channel(Channel.UNLIMITED)
|
val details: MangaDetails?
|
||||||
val events: Flow<Event> = _events.receiveAsFlow()
|
get() = successState?.details
|
||||||
|
|
||||||
|
val history = historyRepository.observeOne(mangaId)
|
||||||
|
.stateIn(screenModelScope + Dispatchers.Default, SharingStarted.Eagerly, null)
|
||||||
|
|
||||||
private inline fun updateSuccessState(func: (State.Success) -> State.Success) {
|
private inline fun updateSuccessState(func: (State.Success) -> State.Success) {
|
||||||
mutableState.update {
|
mutableState.update {
|
||||||
@@ -74,142 +64,55 @@ class DetailsScreenModel @AssistedInject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private var loadingJob: Job
|
private val selectedPositions: Array<Int> = arrayOf(-1, -1) // first and last selected index in list
|
||||||
|
private val selectedChapterIds: HashSet<Long> = HashSet()
|
||||||
var details = MutableStateFlow(MangaDetails(manga, null, null, false))
|
|
||||||
|
|
||||||
private val mangaImpl = details.map { x -> x.toManga() }
|
|
||||||
.stateIn(screenModelScope + Dispatchers.Default, SharingStarted.Eagerly, null)
|
|
||||||
|
|
||||||
val history = historyRepository.observeOne(manga.id)
|
|
||||||
.stateIn(screenModelScope + Dispatchers.Default, SharingStarted.Eagerly, null)
|
|
||||||
|
|
||||||
val favouriteCategories = interactor.observeIsFavourite(manga.id)
|
|
||||||
.stateIn(screenModelScope + Dispatchers.Default, SharingStarted.Eagerly, false)
|
|
||||||
|
|
||||||
val remoteManga = MutableStateFlow<Manga?>(null)
|
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
|
||||||
val newChaptersCount = details.flatMapLatest { d ->
|
|
||||||
flowOf(0)
|
|
||||||
}.stateIn(screenModelScope + Dispatchers.Default, SharingStarted.Eagerly, 0)
|
|
||||||
|
|
||||||
private val chaptersQuery = MutableStateFlow("")
|
|
||||||
val selectedBranch = MutableStateFlow<String?>(null)
|
|
||||||
|
|
||||||
val historyInfo: StateFlow<HistoryInfo> = combine(
|
|
||||||
mangaImpl,
|
|
||||||
selectedBranch,
|
|
||||||
history,
|
|
||||||
) { m, b, h ->
|
|
||||||
HistoryInfo(m, b, h)
|
|
||||||
}.stateIn(
|
|
||||||
scope = screenModelScope + Dispatchers.Default,
|
|
||||||
started = SharingStarted.Eagerly,
|
|
||||||
initialValue = HistoryInfo(null, null, null),
|
|
||||||
)
|
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
|
||||||
val bookmarks = mangaImpl.flatMapLatest {
|
|
||||||
if (it != null) bookmarksRepository.observeBookmarks(it) else flowOf(emptyList())
|
|
||||||
}.stateIn(screenModelScope + Dispatchers.Default, SharingStarted.Lazily, emptyList())
|
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
|
||||||
val relatedManga: StateFlow<List<Manga>> = mangaImpl.mapLatest {
|
|
||||||
if (it != null) {
|
|
||||||
relatedMangaUseCase.invoke(it).orEmpty()
|
|
||||||
} else {
|
|
||||||
emptyList()
|
|
||||||
}
|
|
||||||
}.stateIn(screenModelScope, SharingStarted.Lazily, emptyList())
|
|
||||||
|
|
||||||
val branches: StateFlow<List<MangaBranch>> = combine(
|
|
||||||
details,
|
|
||||||
selectedBranch,
|
|
||||||
history,
|
|
||||||
) { m, b, h ->
|
|
||||||
val c = m.chapters
|
|
||||||
if (c.isEmpty()) {
|
|
||||||
return@combine emptyList()
|
|
||||||
}
|
|
||||||
val currentBranch = h?.let { m.allChapters.findById(it.chapterId) }?.branch
|
|
||||||
c.map { x ->
|
|
||||||
MangaBranch(
|
|
||||||
name = x.key,
|
|
||||||
count = x.value.size,
|
|
||||||
isSelected = x.key == b,
|
|
||||||
isCurrent = h != null && x.key == currentBranch,
|
|
||||||
)
|
|
||||||
}.sortedWith(BranchComparator())
|
|
||||||
}.stateIn(screenModelScope + Dispatchers.Default, SharingStarted.Eagerly, emptyList())
|
|
||||||
|
|
||||||
val isChaptersEmpty: StateFlow<Boolean> = details.map {
|
|
||||||
it.isLoaded && it.allChapters.isEmpty()
|
|
||||||
}.stateIn(screenModelScope, SharingStarted.WhileSubscribed(), false)
|
|
||||||
|
|
||||||
val chapters = combine(
|
|
||||||
combine(
|
|
||||||
details,
|
|
||||||
history,
|
|
||||||
selectedBranch,
|
|
||||||
newChaptersCount,
|
|
||||||
bookmarks,
|
|
||||||
) { manga, history, branch, news, bookmarks ->
|
|
||||||
manga.mapChapters(
|
|
||||||
history,
|
|
||||||
news,
|
|
||||||
branch,
|
|
||||||
bookmarks,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
chaptersQuery,
|
|
||||||
) { list, query ->
|
|
||||||
list.filterSearch(query)
|
|
||||||
}.stateIn(screenModelScope, SharingStarted.Eagerly, emptyList())
|
|
||||||
|
|
||||||
val readingTime = combine(
|
|
||||||
details,
|
|
||||||
selectedBranch,
|
|
||||||
history,
|
|
||||||
) { m, b, h ->
|
|
||||||
readingTimeUseCase.invoke(m, b, h)
|
|
||||||
}.stateIn(screenModelScope, SharingStarted.Lazily, null)
|
|
||||||
|
|
||||||
val selectedBranchValue: String?
|
|
||||||
get() = selectedBranch.value
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
loadingJob = doLoad(manga.id)
|
screenModelScope.launch(Dispatchers.IO) {
|
||||||
updateSuccessState { it.copy(isRefreshingData = false) }
|
detailsLoadUseCase.invoke(mangaId)
|
||||||
|
.collectLatest { details ->
|
||||||
|
updateSuccessState {
|
||||||
|
it.copy(
|
||||||
|
details = details
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun doLoad(mangaId: Long) = launchLoadingJob(Dispatchers.Default) {
|
screenModelScope.launch(Dispatchers.IO) {
|
||||||
detailsLoadUseCase.invoke(mangaId)
|
val manga = requireNotNull(mangaDataRepository.findMangaById(mangaId))
|
||||||
.onEachWhile {
|
val details = MangaDetails(manga, null, null, false)
|
||||||
if (it.allChapters.isEmpty()) {
|
|
||||||
return@onEachWhile false
|
val needRefreshInfo = !details.isLoaded
|
||||||
}
|
|
||||||
val manga = it.toManga()
|
|
||||||
// find default branch
|
|
||||||
val hist = historyRepository.getOne(manga)
|
|
||||||
selectedBranch.value = manga.getPreferredBranch(hist)
|
|
||||||
true
|
|
||||||
}.catch { error ->
|
|
||||||
_events.send(Event.InternalError)
|
|
||||||
snackbarHostState.showSnackbar(error.message ?: error.stackTraceToString())
|
|
||||||
}.collect {
|
|
||||||
details.value = it
|
|
||||||
mutableState.update {
|
mutableState.update {
|
||||||
State.Success(
|
State.Success(
|
||||||
manga = details.value.toManga(),
|
details = details
|
||||||
source = details.value.toManga().source,
|
|
||||||
readingTime = checkNotNull(readingTime.value),
|
|
||||||
historyInfo = historyInfo.value,
|
|
||||||
availableScanlators = setOf(),
|
|
||||||
excludedScanlators = setOf(),
|
|
||||||
isRefreshingData = false
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (screenModelScope.isActive) {
|
||||||
|
val fetchFromSourceTasks = listOf(
|
||||||
|
async { if (needRefreshInfo) fetchMangaFromSource() },
|
||||||
|
)
|
||||||
|
fetchFromSourceTasks.awaitAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSuccessState { it.copy(isRefreshingData = false) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun fetchMangaFromSource(manualFetch: Boolean = false) {
|
||||||
|
val state = successState ?: return
|
||||||
|
try {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
val networkManga = state.details.toManga()
|
||||||
|
detailsLoadUseCase.getDetails(networkManga)
|
||||||
|
}
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
screenModelScope.launch {
|
||||||
|
snackbarHostState.showSnackbar(message = with(context) { e.getDisplayMessage(resources) })
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -222,12 +125,6 @@ class DetailsScreenModel @AssistedInject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeFromHistory() {
|
|
||||||
launchJob(Dispatchers.Default) {
|
|
||||||
historyRepository.delete(setOf(manga.id))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sealed interface Event {
|
sealed interface Event {
|
||||||
data object InternalError : Event
|
data object InternalError : Event
|
||||||
}
|
}
|
||||||
@@ -238,12 +135,7 @@ class DetailsScreenModel @AssistedInject constructor(
|
|||||||
|
|
||||||
@Immutable
|
@Immutable
|
||||||
data class Success(
|
data class Success(
|
||||||
val manga: Manga,
|
val details: MangaDetails,
|
||||||
val source: MangaSource,
|
|
||||||
val historyInfo: HistoryInfo,
|
|
||||||
val readingTime: ReadingTime,
|
|
||||||
val availableScanlators: Set<String>,
|
|
||||||
val excludedScanlators: Set<String>,
|
|
||||||
val isRefreshingData: Boolean = false,
|
val isRefreshingData: Boolean = false,
|
||||||
) : State
|
) : State
|
||||||
}
|
}
|
||||||
@@ -252,7 +144,7 @@ class DetailsScreenModel @AssistedInject constructor(
|
|||||||
interface Factory : ScreenModelFactory {
|
interface Factory : ScreenModelFactory {
|
||||||
fun create(
|
fun create(
|
||||||
context: Context,
|
context: Context,
|
||||||
manga: Manga,
|
mangaId: Long,
|
||||||
snackbarHostState: SnackbarHostState
|
snackbarHostState: SnackbarHostState
|
||||||
): DetailsScreenModel
|
): DetailsScreenModel
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +0,0 @@
|
|||||||
package org.xtimms.shirizu.sections.details
|
|
||||||
|
|
||||||
import org.xtimms.shirizu.core.base.state.UiState
|
|
||||||
import org.xtimms.shirizu.sections.details.data.MangaDetails
|
|
||||||
|
|
||||||
data class DetailsUiState(
|
|
||||||
val details: MangaDetails? = null,
|
|
||||||
override val isLoading: Boolean = false,
|
|
||||||
override val message: String? = null,
|
|
||||||
) : UiState() {
|
|
||||||
override fun setLoading(value: Boolean) = copy(isLoading = value)
|
|
||||||
override fun setMessage(value: String?) = copy(message = value)
|
|
||||||
}
|
|
||||||
@@ -1,16 +1,28 @@
|
|||||||
package org.xtimms.shirizu.sections.details
|
package org.xtimms.shirizu.sections.details
|
||||||
|
|
||||||
import androidx.activity.compose.BackHandler
|
import androidx.activity.compose.BackHandler
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.core.animateFloatAsState
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
|
import androidx.compose.animation.fadeIn
|
||||||
|
import androidx.compose.animation.fadeOut
|
||||||
|
import androidx.compose.foundation.gestures.snapping.rememberSnapFlingBehavior
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.calculateEndPadding
|
import androidx.compose.foundation.layout.calculateEndPadding
|
||||||
import androidx.compose.foundation.layout.calculateStartPadding
|
import androidx.compose.foundation.layout.calculateStartPadding
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.sizeIn
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.SnackbarHost
|
import androidx.compose.material3.SnackbarHost
|
||||||
import androidx.compose.material3.SnackbarHostState
|
import androidx.compose.material3.SnackbarHostState
|
||||||
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
@@ -19,13 +31,18 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
import coil.ImageLoader
|
import coil.ImageLoader
|
||||||
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
import org.xtimms.shirizu.R
|
import org.xtimms.shirizu.R
|
||||||
import org.xtimms.shirizu.core.components.ClassicDetailsToolbar
|
import org.xtimms.shirizu.core.components.ClassicDetailsToolbar
|
||||||
|
import org.xtimms.shirizu.core.components.MangaHorizontalItem
|
||||||
import org.xtimms.shirizu.core.components.Scaffold
|
import org.xtimms.shirizu.core.components.Scaffold
|
||||||
import org.xtimms.shirizu.core.components.VerticalFastScroller
|
import org.xtimms.shirizu.core.components.VerticalFastScroller
|
||||||
import org.xtimms.shirizu.core.model.MangaHistory
|
import org.xtimms.shirizu.core.model.MangaHistory
|
||||||
|
import org.xtimms.shirizu.core.model.parcelable.ParcelableManga
|
||||||
import org.xtimms.shirizu.core.parser.favicon.faviconUri
|
import org.xtimms.shirizu.core.parser.favicon.faviconUri
|
||||||
|
import org.xtimms.shirizu.core.prefs.AppSettings
|
||||||
import org.xtimms.shirizu.sections.details.data.ReadingTime
|
import org.xtimms.shirizu.sections.details.data.ReadingTime
|
||||||
import org.xtimms.shirizu.sections.details.model.HistoryInfo
|
import org.xtimms.shirizu.sections.details.model.HistoryInfo
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
@@ -36,6 +53,7 @@ fun MangaScreen(
|
|||||||
snackbarHostState: SnackbarHostState,
|
snackbarHostState: SnackbarHostState,
|
||||||
isTabletUi: Boolean,
|
isTabletUi: Boolean,
|
||||||
onBackClicked: () -> Unit,
|
onBackClicked: () -> Unit,
|
||||||
|
onMangaClicked: (Manga) -> Unit,
|
||||||
onWebViewClicked: (() -> Unit)?,
|
onWebViewClicked: (() -> Unit)?,
|
||||||
onWebViewLongClicked: (() -> Unit)?,
|
onWebViewLongClicked: (() -> Unit)?,
|
||||||
onTrackingClicked: () -> Unit,
|
onTrackingClicked: () -> Unit,
|
||||||
@@ -56,6 +74,7 @@ fun MangaScreen(
|
|||||||
snackbarHostState = snackbarHostState,
|
snackbarHostState = snackbarHostState,
|
||||||
onBackClicked = onBackClicked,
|
onBackClicked = onBackClicked,
|
||||||
onTagSearch = onTagSearch,
|
onTagSearch = onTagSearch,
|
||||||
|
onMangaClicked = onMangaClicked,
|
||||||
onRefresh = onRefresh,
|
onRefresh = onRefresh,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -68,6 +87,7 @@ private fun MangaScreenSmallImpl(
|
|||||||
snackbarHostState: SnackbarHostState,
|
snackbarHostState: SnackbarHostState,
|
||||||
onBackClicked: () -> Unit,
|
onBackClicked: () -> Unit,
|
||||||
onTagSearch: (String) -> Unit,
|
onTagSearch: (String) -> Unit,
|
||||||
|
onMangaClicked: (Manga) -> Unit,
|
||||||
onRefresh: () -> Unit,
|
onRefresh: () -> Unit,
|
||||||
) {
|
) {
|
||||||
val chapterListState = rememberLazyListState()
|
val chapterListState = rememberLazyListState()
|
||||||
@@ -91,7 +111,7 @@ private fun MangaScreenSmallImpl(
|
|||||||
label = "Top Bar Background",
|
label = "Top Bar Background",
|
||||||
)
|
)
|
||||||
ClassicDetailsToolbar(
|
ClassicDetailsToolbar(
|
||||||
title = state.manga?.title ?: "",
|
title = state.details.toManga().title,
|
||||||
titleAlphaProvider = { animatedTitleAlpha },
|
titleAlphaProvider = { animatedTitleAlpha },
|
||||||
backgroundAlphaProvider = { animatedBgAlpha },
|
backgroundAlphaProvider = { animatedBgAlpha },
|
||||||
navigateBack = { onBackClicked() },
|
navigateBack = { onBackClicked() },
|
||||||
@@ -102,6 +122,8 @@ private fun MangaScreenSmallImpl(
|
|||||||
val topPadding = contentPadding.calculateTopPadding()
|
val topPadding = contentPadding.calculateTopPadding()
|
||||||
|
|
||||||
val layoutDirection = LocalLayoutDirection.current
|
val layoutDirection = LocalLayoutDirection.current
|
||||||
|
val relatedMangaListState = rememberLazyListState()
|
||||||
|
|
||||||
VerticalFastScroller(
|
VerticalFastScroller(
|
||||||
listState = chapterListState,
|
listState = chapterListState,
|
||||||
topContentPadding = topPadding,
|
topContentPadding = topPadding,
|
||||||
@@ -121,16 +143,16 @@ private fun MangaScreenSmallImpl(
|
|||||||
contentType = DetailsScreenItem.INFO_BOX,
|
contentType = DetailsScreenItem.INFO_BOX,
|
||||||
) {
|
) {
|
||||||
ClassicDetailsInfoBox(
|
ClassicDetailsInfoBox(
|
||||||
imageUrl = state.manga.largeCoverUrl ?: state.manga.coverUrl,
|
imageUrl = state.details.toManga().largeCoverUrl ?: state.details.toManga().coverUrl,
|
||||||
favicon = state.manga.source.faviconUri(),
|
favicon = state.details.toManga().source.faviconUri(),
|
||||||
title = state.manga.title,
|
title = state.details.toManga().title,
|
||||||
altTitle = state.manga.altTitle ?: stringResource(id = R.string.unknown),
|
altTitle = state.details.toManga().altTitle,
|
||||||
author = state.manga.author ?: stringResource(id = R.string.unknown),
|
author = state.details.toManga().author,
|
||||||
isNsfw = state.manga.isNsfw,
|
isNsfw = state.details.toManga().isNsfw,
|
||||||
state = state.manga.state,
|
state = state.details.toManga().state,
|
||||||
source = state.manga.source,
|
source = state.details.toManga().source,
|
||||||
historyInfo = state.historyInfo,
|
historyInfo = null,
|
||||||
readingTime = state.readingTime,
|
readingTime = null,
|
||||||
isTabletUi = false,
|
isTabletUi = false,
|
||||||
appBarPadding = topPadding,
|
appBarPadding = topPadding,
|
||||||
onCoverClick = { },
|
onCoverClick = { },
|
||||||
@@ -147,12 +169,71 @@ private fun MangaScreenSmallImpl(
|
|||||||
) {
|
) {
|
||||||
ExpandableMangaDescription(
|
ExpandableMangaDescription(
|
||||||
defaultExpandState = false,
|
defaultExpandState = false,
|
||||||
description = state.manga?.description,
|
description = state.details.toManga().description,
|
||||||
tagsProvider = { state.manga?.tags },
|
tagsProvider = { state.details.toManga().tags },
|
||||||
onTagSearch = onTagSearch,
|
onTagSearch = onTagSearch,
|
||||||
onCopyTagToClipboard = { },
|
onCopyTagToClipboard = { },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*item {
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = state.relatedManga.isNotEmpty(),
|
||||||
|
enter = fadeIn(),
|
||||||
|
exit = fadeOut()
|
||||||
|
) {
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.padding(start = 16.dp, end = 8.dp),
|
||||||
|
text = stringResource(id = R.string.related_manga),
|
||||||
|
style = MaterialTheme.typography.titleLarge
|
||||||
|
)
|
||||||
|
LazyRow(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(top = 8.dp)
|
||||||
|
.sizeIn(minHeight = 100.dp),
|
||||||
|
state = relatedMangaListState,
|
||||||
|
contentPadding = PaddingValues(horizontal = 8.dp),
|
||||||
|
flingBehavior = rememberSnapFlingBehavior(lazyListState = relatedMangaListState)
|
||||||
|
) {
|
||||||
|
items(
|
||||||
|
items = state.relatedManga,
|
||||||
|
key = { it.id },
|
||||||
|
contentType = { it }
|
||||||
|
) {
|
||||||
|
MangaHorizontalItem(
|
||||||
|
manga = it,
|
||||||
|
onClick = { manga -> onMangaClicked(manga) },
|
||||||
|
onLongClick = { })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
HorizontalDivider(modifier = Modifier.padding(16.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}*/
|
||||||
|
|
||||||
|
item {
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.padding(start = 16.dp, end = 8.dp, bottom = 8.dp),
|
||||||
|
text = stringResource(id = R.string.chapters),
|
||||||
|
style = MaterialTheme.typography.titleLarge
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*items(
|
||||||
|
items = state.chapters
|
||||||
|
) {
|
||||||
|
ChapterListItem(
|
||||||
|
title = it.chapter.name,
|
||||||
|
date = it.chapter.uploadDate,
|
||||||
|
scanlator = it.chapter.scanlator,
|
||||||
|
read = !it.isUnread,
|
||||||
|
bookmark = false,
|
||||||
|
selected = false,
|
||||||
|
onLongClick = { *//*TODO*//* },
|
||||||
|
onClick = { *//*TODO*//* }
|
||||||
|
)
|
||||||
|
}*/
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import androidx.compose.ui.semantics.Role
|
|||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import coil.ImageLoader
|
import coil.ImageLoader
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaParserSource
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaState
|
import org.koitharu.kotatsu.parsers.model.MangaState
|
||||||
import org.xtimms.shirizu.core.ShirizuAsyncImage
|
import org.xtimms.shirizu.core.ShirizuAsyncImage
|
||||||
@@ -38,7 +39,7 @@ fun ModernDetailsInfoBox(
|
|||||||
author: String,
|
author: String,
|
||||||
isNsfw: Boolean,
|
isNsfw: Boolean,
|
||||||
state: MangaState?,
|
state: MangaState?,
|
||||||
source: MangaSource,
|
source: MangaParserSource,
|
||||||
historyInfo: HistoryInfo,
|
historyInfo: HistoryInfo,
|
||||||
readingTime: ReadingTime?,
|
readingTime: ReadingTime?,
|
||||||
isTabletUi: Boolean,
|
isTabletUi: Boolean,
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ class DetailsLoadUseCase @Inject constructor(
|
|||||||
) {
|
) {
|
||||||
|
|
||||||
operator fun invoke(mangaId: Long): Flow<MangaDetails> = channelFlow {
|
operator fun invoke(mangaId: Long): Flow<MangaDetails> = channelFlow {
|
||||||
val manga = requireNotNull(mangaDataRepository.findMangaById(mangaId)) {
|
val manga = requireNotNull(mangaDataRepository.findMangaById(mangaId)) { // wrong method
|
||||||
"Cannot resolve id $mangaId"
|
"Cannot resolve id $mangaId"
|
||||||
}
|
}
|
||||||
val local = if (!manga.isLocal) {
|
val local = if (!manga.isLocal) {
|
||||||
@@ -52,7 +52,7 @@ class DetailsLoadUseCase @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private suspend fun getDetails(seed: Manga) = runCatchingCancellable {
|
suspend fun getDetails(seed: Manga) = runCatchingCancellable {
|
||||||
val repository = mangaRepositoryFactory.create(seed.source)
|
val repository = mangaRepositoryFactory.create(seed.source)
|
||||||
repository.getDetails(seed)
|
repository.getDetails(seed)
|
||||||
}.getOrThrow()
|
}.getOrThrow()
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ data class ExploreTab(
|
|||||||
get() {
|
get() {
|
||||||
val image = Icons.Outlined.Explore
|
val image = Icons.Outlined.Explore
|
||||||
return TabOptions(
|
return TabOptions(
|
||||||
index = 3u,
|
index = 1u,
|
||||||
title = stringResource(R.string.nav_explore),
|
title = stringResource(R.string.nav_explore),
|
||||||
icon = rememberVectorPainter(image),
|
icon = rememberVectorPainter(image),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import androidx.compose.ui.res.stringResource
|
|||||||
import androidx.compose.ui.text.input.TextFieldValue
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import org.koitharu.kotatsu.parsers.model.ContentType
|
import org.koitharu.kotatsu.parsers.model.ContentType
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaParserSource
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
import org.xtimms.shirizu.R
|
import org.xtimms.shirizu.R
|
||||||
import org.xtimms.shirizu.core.components.FastScrollLazyColumn
|
import org.xtimms.shirizu.core.components.FastScrollLazyColumn
|
||||||
@@ -234,7 +235,7 @@ fun CatalogScreen(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SourceItem(
|
fun SourceItem(
|
||||||
source: MangaSource,
|
source: MangaParserSource,
|
||||||
onClickItem: (MangaSource) -> Unit,
|
onClickItem: (MangaSource) -> Unit,
|
||||||
onLongClickItem: (MangaSource) -> Unit,
|
onLongClickItem: (MangaSource) -> Unit,
|
||||||
onClickMenu: (MangaSource) -> Unit,
|
onClickMenu: (MangaSource) -> Unit,
|
||||||
|
|||||||
@@ -17,7 +17,9 @@ import kotlinx.coroutines.flow.receiveAsFlow
|
|||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.koitharu.kotatsu.parsers.model.ContentType
|
import org.koitharu.kotatsu.parsers.model.ContentType
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaParserSource
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
|
import org.xtimms.shirizu.core.model.getTitle
|
||||||
import org.xtimms.shirizu.core.prefs.AppSettings
|
import org.xtimms.shirizu.core.prefs.AppSettings
|
||||||
import org.xtimms.shirizu.data.repository.MangaSourcesRepository
|
import org.xtimms.shirizu.data.repository.MangaSourcesRepository
|
||||||
import org.xtimms.shirizu.sections.explore.sources.SourceUiModel
|
import org.xtimms.shirizu.sections.explore.sources.SourceUiModel
|
||||||
@@ -34,7 +36,7 @@ class CatalogScreenModel @Inject constructor(
|
|||||||
val events = _events.receiveAsFlow()
|
val events = _events.receiveAsFlow()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
val queryFilter: (String) -> ((MangaSource) -> Boolean) = { query ->
|
val queryFilter: (String) -> ((MangaParserSource) -> Boolean) = { query ->
|
||||||
filter@{ source ->
|
filter@{ source ->
|
||||||
if (query.isEmpty()) return@filter true
|
if (query.isEmpty()) return@filter true
|
||||||
query.split(",").any { _input ->
|
query.split(",").any { _input ->
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import androidx.compose.ui.text.style.TextOverflow
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.core.os.LocaleListCompat
|
import androidx.core.os.LocaleListCompat
|
||||||
import org.koitharu.kotatsu.parsers.model.ContentType
|
import org.koitharu.kotatsu.parsers.model.ContentType
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaParserSource
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
import org.xtimms.shirizu.R
|
import org.xtimms.shirizu.R
|
||||||
import org.xtimms.shirizu.utils.LocaleHelper
|
import org.xtimms.shirizu.utils.LocaleHelper
|
||||||
@@ -21,14 +22,14 @@ import java.util.Locale
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun BaseSourceItem(
|
fun BaseSourceItem(
|
||||||
source: MangaSource,
|
source: MangaParserSource,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
showTypeInContent: Boolean = true,
|
showTypeInContent: Boolean = true,
|
||||||
onClickItem: () -> Unit = {},
|
onClickItem: () -> Unit = {},
|
||||||
onLongClickItem: () -> Unit = {},
|
onLongClickItem: () -> Unit = {},
|
||||||
icon: @Composable RowScope.(MangaSource) -> Unit = defaultIcon,
|
icon: @Composable RowScope.(MangaSource) -> Unit = defaultIcon,
|
||||||
action: @Composable RowScope.(MangaSource) -> Unit = {},
|
action: @Composable RowScope.(MangaSource) -> Unit = {},
|
||||||
content: @Composable RowScope.(MangaSource, String?) -> Unit = defaultContent,
|
content: @Composable RowScope.(MangaParserSource, String?) -> Unit = defaultContent,
|
||||||
) {
|
) {
|
||||||
fun getPrettyContentTypeName(type: ContentType?, context: Context): String {
|
fun getPrettyContentTypeName(type: ContentType?, context: Context): String {
|
||||||
if (type == null) {
|
if (type == null) {
|
||||||
@@ -60,7 +61,7 @@ private val defaultIcon: @Composable RowScope.(MangaSource) -> Unit = { source -
|
|||||||
SourceIcon(source = source)
|
SourceIcon(source = source)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val defaultContent: @Composable RowScope.(MangaSource, String?) -> Unit = { source, sourceLangString ->
|
private val defaultContent: @Composable RowScope.(MangaParserSource, String?) -> Unit = { source, sourceLangString ->
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(horizontal = 24.dp)
|
.padding(horizontal = 24.dp)
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaParserSource
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
import org.xtimms.shirizu.R
|
import org.xtimms.shirizu.R
|
||||||
import org.xtimms.shirizu.core.components.ScrollbarLazyColumn
|
import org.xtimms.shirizu.core.components.ScrollbarLazyColumn
|
||||||
@@ -105,7 +106,7 @@ fun SourceHeader(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SourceItem(
|
fun SourceItem(
|
||||||
source: MangaSource,
|
source: MangaParserSource,
|
||||||
onClickItem: (MangaSource) -> Unit,
|
onClickItem: (MangaSource) -> Unit,
|
||||||
onLongClickItem: (MangaSource) -> Unit,
|
onLongClickItem: (MangaSource) -> Unit,
|
||||||
onClickMenu: (MangaSource) -> Unit,
|
onClickMenu: (MangaSource) -> Unit,
|
||||||
@@ -172,6 +173,6 @@ private fun SourcePinButton(
|
|||||||
}
|
}
|
||||||
|
|
||||||
sealed interface SourceUiModel {
|
sealed interface SourceUiModel {
|
||||||
data class Item(val source: MangaSource) : SourceUiModel
|
data class Item(val source: MangaParserSource) : SourceUiModel
|
||||||
data class Header(val language: String?) : SourceUiModel
|
data class Header(val language: String?) : SourceUiModel
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,10 @@
|
|||||||
package org.xtimms.shirizu.sections.explore.sources
|
package org.xtimms.shirizu.sections.explore.sources
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Immutable
|
||||||
import cafe.adriel.voyager.core.model.StateScreenModel
|
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||||
import cafe.adriel.voyager.core.model.screenModelScope
|
import cafe.adriel.voyager.core.model.screenModelScope
|
||||||
|
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
@@ -13,12 +15,17 @@ import kotlinx.coroutines.flow.collectLatest
|
|||||||
import kotlinx.coroutines.flow.receiveAsFlow
|
import kotlinx.coroutines.flow.receiveAsFlow
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaParserSource
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
|
import org.xtimms.shirizu.core.model.MangaSourceInfo
|
||||||
|
import org.xtimms.shirizu.core.model.getSummary
|
||||||
|
import org.xtimms.shirizu.core.model.getTitle
|
||||||
import org.xtimms.shirizu.data.repository.MangaSourcesRepository
|
import org.xtimms.shirizu.data.repository.MangaSourcesRepository
|
||||||
import org.xtimms.shirizu.utils.LocaleHelper
|
import org.xtimms.shirizu.utils.LocaleHelper
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
class SourcesScreenModel @Inject constructor(
|
class SourcesScreenModel @Inject constructor(
|
||||||
|
@ApplicationContext context: Context,
|
||||||
private val mangaSourcesRepository: MangaSourcesRepository,
|
private val mangaSourcesRepository: MangaSourcesRepository,
|
||||||
) : StateScreenModel<SourcesScreenModel.State>(State()) {
|
) : StateScreenModel<SourcesScreenModel.State>(State()) {
|
||||||
|
|
||||||
@@ -36,7 +43,7 @@ class SourcesScreenModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun collectEnabledSources(sources: List<MangaSource>) {
|
private fun collectEnabledSources(sources: List<MangaParserSource>) {
|
||||||
mutableState.update { state ->
|
mutableState.update { state ->
|
||||||
state.copy(
|
state.copy(
|
||||||
isLoading = false,
|
isLoading = false,
|
||||||
|
|||||||
@@ -73,12 +73,7 @@ class HistoryScreenModel @Inject constructor(
|
|||||||
val searchQuery = query ?: ""
|
val searchQuery = query ?: ""
|
||||||
history.asSequence().map { it }
|
history.asSequence().map { it }
|
||||||
.filter { it.manga.isNsfw == nsfw }
|
.filter { it.manga.isNsfw == nsfw }
|
||||||
.sortedByDescending {
|
.sortedWith(MangaComparator(sort))
|
||||||
when (sort) {
|
|
||||||
SortOption.DATE_ADDED -> it.history.updatedAt
|
|
||||||
SortOption.ALPHABETICAL -> it.manga.title.lowercase()
|
|
||||||
}.toString()
|
|
||||||
}
|
|
||||||
.filter(queryFilter(searchQuery)).toList()
|
.filter(queryFilter(searchQuery)).toList()
|
||||||
.toImmutableList()
|
.toImmutableList()
|
||||||
}.collectLatest {
|
}.collectLatest {
|
||||||
@@ -222,10 +217,7 @@ class HistoryScreenModel @Inject constructor(
|
|||||||
val searchQuery: String? = null,
|
val searchQuery: String? = null,
|
||||||
val selection: PersistentList<Manga> = persistentListOf(),
|
val selection: PersistentList<Manga> = persistentListOf(),
|
||||||
val showNsfw: Boolean = AppSettings.showNsfwInHistory(),
|
val showNsfw: Boolean = AppSettings.showNsfwInHistory(),
|
||||||
val availableSorts: List<SortOption> = listOf(
|
val availableSorts: List<SortOption> = SortOption.entries,
|
||||||
SortOption.DATE_ADDED,
|
|
||||||
SortOption.ALPHABETICAL
|
|
||||||
),
|
|
||||||
val sort: SortOption = SortOption.ALPHABETICAL,
|
val sort: SortOption = SortOption.ALPHABETICAL,
|
||||||
val list: PersistentList<HistoryItemModel> = persistentListOf(),
|
val list: PersistentList<HistoryItemModel> = persistentListOf(),
|
||||||
val dialog: Dialog? = null,
|
val dialog: Dialog? = null,
|
||||||
@@ -265,3 +257,13 @@ class HistoryScreenModel @Inject constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class MangaComparator(private val sort: SortOption) : Comparator<MangaWithHistory> {
|
||||||
|
override fun compare(o1: MangaWithHistory, o2: MangaWithHistory): Int {
|
||||||
|
return when (sort) {
|
||||||
|
SortOption.DATE_ADDED -> o2.history.updatedAt.compareTo(o1.history.updatedAt)
|
||||||
|
SortOption.ALPHABETICAL -> o1.manga.title.compareTo(o2.manga.title)
|
||||||
|
SortOption.PROGRESS -> o2.history.percent.compareTo(o1.history.percent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -36,6 +36,7 @@ import org.xtimms.shirizu.core.components.DialogCheckBoxItem
|
|||||||
import org.xtimms.shirizu.core.components.Scaffold
|
import org.xtimms.shirizu.core.components.Scaffold
|
||||||
import org.xtimms.shirizu.core.components.LibraryBottomActionMenu
|
import org.xtimms.shirizu.core.components.LibraryBottomActionMenu
|
||||||
import org.xtimms.shirizu.core.components.ShirizuDialog
|
import org.xtimms.shirizu.core.components.ShirizuDialog
|
||||||
|
import org.xtimms.shirizu.core.model.parcelable.ParcelableManga
|
||||||
import org.xtimms.shirizu.core.ui.screens.TabContent
|
import org.xtimms.shirizu.core.ui.screens.TabContent
|
||||||
import org.xtimms.shirizu.sections.details.DetailsScreen
|
import org.xtimms.shirizu.sections.details.DetailsScreen
|
||||||
|
|
||||||
@@ -80,7 +81,7 @@ fun Screen.historyTab(): TabContent {
|
|||||||
onToggleEnableNsfw = { screenModel.filterNsfw(it) },
|
onToggleEnableNsfw = { screenModel.filterNsfw(it) },
|
||||||
onFilterChanged = { screenModel.search(it) },
|
onFilterChanged = { screenModel.search(it) },
|
||||||
onSortSelected = { screenModel.sort(it) },
|
onSortSelected = { screenModel.sort(it) },
|
||||||
onClick = { navigator.push(DetailsScreen(it.manga)) },
|
onClick = { navigator.push(DetailsScreen(it.manga.id)) },
|
||||||
onHistorySelected = screenModel::toggleSelection
|
onHistorySelected = screenModel::toggleSelection
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package org.xtimms.shirizu.sections.library.history
|
package org.xtimms.shirizu.sections.library.history
|
||||||
|
|
||||||
enum class SortOption {
|
enum class SortOption(id: Int) {
|
||||||
ALPHABETICAL,
|
ALPHABETICAL(0),
|
||||||
DATE_ADDED,
|
DATE_ADDED(1),
|
||||||
|
PROGRESS(2)
|
||||||
}
|
}
|
||||||
@@ -26,6 +26,7 @@ import cafe.adriel.voyager.navigator.LocalNavigator
|
|||||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
import org.xtimms.shirizu.core.components.Scaffold
|
import org.xtimms.shirizu.core.components.Scaffold
|
||||||
|
import org.xtimms.shirizu.core.model.parcelable.ParcelableManga
|
||||||
import org.xtimms.shirizu.sections.details.DetailsScreen
|
import org.xtimms.shirizu.sections.details.DetailsScreen
|
||||||
import org.xtimms.shirizu.utils.lang.Screen
|
import org.xtimms.shirizu.utils.lang.Screen
|
||||||
|
|
||||||
@@ -75,7 +76,7 @@ data class MangaListScreen(private val source: MangaSource) : Screen() {
|
|||||||
columns = screenModel.getColumnsPreference(LocalConfiguration.current.orientation),
|
columns = screenModel.getColumnsPreference(LocalConfiguration.current.orientation),
|
||||||
snackbarHostState = snackbarHostState,
|
snackbarHostState = snackbarHostState,
|
||||||
contentPadding = paddingValues,
|
contentPadding = paddingValues,
|
||||||
onMangaClick = { navigator.push((DetailsScreen(it, true))) },
|
onMangaClick = { navigator.push((DetailsScreen(it.id, true))) },
|
||||||
onMangaLongClick = { manga -> },
|
onMangaLongClick = { manga -> },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import kotlinx.coroutines.flow.getAndUpdate
|
|||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaParserSource
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
import org.xtimms.shirizu.core.parser.MangaRepository
|
import org.xtimms.shirizu.core.parser.MangaRepository
|
||||||
|
|
||||||
@@ -25,7 +26,7 @@ class MangaListScreenModel @AssistedInject constructor(
|
|||||||
mangaRepositoryFactory: MangaRepository.Factory,
|
mangaRepositoryFactory: MangaRepository.Factory,
|
||||||
) : StateScreenModel<MangaListScreenModel.State>(State()) {
|
) : StateScreenModel<MangaListScreenModel.State>(State()) {
|
||||||
|
|
||||||
val source = MangaSource.valueOf(sourceName)
|
val source = MangaParserSource.valueOf(sourceName)
|
||||||
private val repository = mangaRepositoryFactory.create(source)
|
private val repository = mangaRepositoryFactory.create(source)
|
||||||
private val hasNextPage = MutableStateFlow(false)
|
private val hasNextPage = MutableStateFlow(false)
|
||||||
private val mangaList = MutableStateFlow<List<Manga>?>(null)
|
private val mangaList = MutableStateFlow<List<Manga>?>(null)
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import androidx.compose.runtime.mutableStateOf
|
|||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaParserSource
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
import org.xtimms.shirizu.R
|
import org.xtimms.shirizu.R
|
||||||
import org.xtimms.shirizu.core.components.AppBar
|
import org.xtimms.shirizu.core.components.AppBar
|
||||||
@@ -21,7 +22,7 @@ import org.xtimms.shirizu.core.components.SearchToolbar
|
|||||||
fun BrowseSourceToolbar(
|
fun BrowseSourceToolbar(
|
||||||
searchQuery: String?,
|
searchQuery: String?,
|
||||||
onSearchQueryChange: (String?) -> Unit,
|
onSearchQueryChange: (String?) -> Unit,
|
||||||
source: MangaSource?,
|
source: MangaParserSource?,
|
||||||
navigateUp: () -> Unit,
|
navigateUp: () -> Unit,
|
||||||
onWebViewClick: () -> Unit,
|
onWebViewClick: () -> Unit,
|
||||||
onSearch: (String) -> Unit,
|
onSearch: (String) -> Unit,
|
||||||
|
|||||||
@@ -0,0 +1,114 @@
|
|||||||
|
package org.xtimms.shirizu.sections.profile
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.IntrinsicSize
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.outlined.HelpOutline
|
||||||
|
import androidx.compose.material.icons.outlined.HelpOutline
|
||||||
|
import androidx.compose.material.icons.outlined.Settings
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
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.draw.clip
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import coil.compose.AsyncImage
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
|
import org.xtimms.shirizu.R
|
||||||
|
import org.xtimms.shirizu.core.ShirizuAsyncImage
|
||||||
|
import org.xtimms.shirizu.core.components.PreferenceItem
|
||||||
|
import org.xtimms.shirizu.core.components.PreferenceSubtitle
|
||||||
|
import org.xtimms.shirizu.sections.shelf.ShelfCategory
|
||||||
|
import org.xtimms.shirizu.sections.stats.ChaptersChart
|
||||||
|
import org.xtimms.shirizu.sections.stats.TimeCard
|
||||||
|
import org.xtimms.shirizu.sections.stats.categories.CategoriesChart
|
||||||
|
import org.xtimms.shirizu.ui.theme.ShirizuTheme
|
||||||
|
import org.xtimms.shirizu.utils.composable.bodyWidth
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ProfileScreen(
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
LazyColumn(
|
||||||
|
modifier = modifier.bodyWidth(),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
item {
|
||||||
|
AsyncImage(
|
||||||
|
model = "https://avatars.githubusercontent.com/u/61558546?v=4",
|
||||||
|
contentDescription = "profile",
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(16.dp)
|
||||||
|
.clip(RoundedCornerShape(100))
|
||||||
|
.size(100.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
Text(text = "Xtimms", style = MaterialTheme.typography.titleLarge)
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
Text(text = "My status", style = MaterialTheme.typography.bodyMedium)
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
HorizontalDivider(modifier = Modifier.padding(top = 16.dp, start = 16.dp, end = 16.dp))
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
PreferenceSubtitle(text = stringResource(id = R.string.statistics))
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
TimeCard(
|
||||||
|
modifier = Modifier
|
||||||
|
.height(IntrinsicSize.Min)
|
||||||
|
.padding(horizontal = 16.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
CategoriesChart(
|
||||||
|
modifier = Modifier.padding(16.dp),
|
||||||
|
categories = listOf(
|
||||||
|
ShelfCategory(1, "Test 1", 3),
|
||||||
|
ShelfCategory(2, "Test 2", 4),
|
||||||
|
ShelfCategory(3, "Test 3", 6),
|
||||||
|
ShelfCategory(4, "Test 4", 7),
|
||||||
|
ShelfCategory(5, "Test 5", 13),
|
||||||
|
ShelfCategory(6, "Test 6", 12),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
PreferenceSubtitle(text = stringResource(id = R.string.menu))
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
PreferenceItem(
|
||||||
|
icon = Icons.Outlined.Settings,
|
||||||
|
title = stringResource(id = R.string.settings)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
item {
|
||||||
|
PreferenceItem(
|
||||||
|
icon = Icons.AutoMirrored.Outlined.HelpOutline,
|
||||||
|
title = stringResource(id = R.string.help_centre)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = true)
|
||||||
|
@Composable
|
||||||
|
private fun ProfileScreenPreview() {
|
||||||
|
ShirizuTheme {
|
||||||
|
ProfileScreen()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package org.xtimms.shirizu.sections.profile
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.AccountCircle
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.SnackbarHostState
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import cafe.adriel.voyager.navigator.tab.TabOptions
|
||||||
|
import org.xtimms.shirizu.R
|
||||||
|
import org.xtimms.shirizu.core.ShirizuAsyncImage
|
||||||
|
import org.xtimms.shirizu.core.components.Scaffold
|
||||||
|
import org.xtimms.shirizu.utils.composable.bodyWidth
|
||||||
|
import org.xtimms.shirizu.utils.lang.Tab
|
||||||
|
|
||||||
|
object ProfileTab : Tab {
|
||||||
|
|
||||||
|
private val snackbarHostState = SnackbarHostState()
|
||||||
|
|
||||||
|
override val options: TabOptions
|
||||||
|
@Composable
|
||||||
|
get() {
|
||||||
|
val image = Icons.Outlined.AccountCircle
|
||||||
|
return TabOptions(
|
||||||
|
index = 3u,
|
||||||
|
title = stringResource(R.string.profile),
|
||||||
|
icon = rememberVectorPainter(image),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
override fun Content() {
|
||||||
|
Scaffold(
|
||||||
|
snackbarHost = { snackbarHostState }
|
||||||
|
) {
|
||||||
|
ProfileScreen()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -32,10 +32,9 @@ import org.xtimms.shirizu.core.network.CommonHeaders
|
|||||||
import org.xtimms.shirizu.core.network.MangaHttpClient
|
import org.xtimms.shirizu.core.network.MangaHttpClient
|
||||||
import org.xtimms.shirizu.core.network.interceptors.ImageProxyInterceptor
|
import org.xtimms.shirizu.core.network.interceptors.ImageProxyInterceptor
|
||||||
import org.xtimms.shirizu.core.parser.MangaRepository
|
import org.xtimms.shirizu.core.parser.MangaRepository
|
||||||
import org.xtimms.shirizu.core.parser.RemoteMangaRepository
|
import org.xtimms.shirizu.core.parser.ParserMangaRepository
|
||||||
import org.xtimms.shirizu.core.parser.local.isFileUri
|
import org.xtimms.shirizu.core.parser.local.isFileUri
|
||||||
import org.xtimms.shirizu.core.parser.local.isZipUri
|
import org.xtimms.shirizu.core.parser.local.isZipUri
|
||||||
import org.xtimms.shirizu.core.prefs.AppSettings
|
|
||||||
import org.xtimms.shirizu.sections.reader.pager.ReaderPage
|
import org.xtimms.shirizu.sections.reader.pager.ReaderPage
|
||||||
import org.xtimms.shirizu.utils.FileSize
|
import org.xtimms.shirizu.utils.FileSize
|
||||||
import org.xtimms.shirizu.utils.RetainedLifecycleCoroutineScope
|
import org.xtimms.shirizu.utils.RetainedLifecycleCoroutineScope
|
||||||
@@ -81,7 +80,7 @@ class PageLoader @Inject constructor(
|
|||||||
private var prefetchQueueLimit = PREFETCH_LIMIT_DEFAULT // TODO adaptive
|
private var prefetchQueueLimit = PREFETCH_LIMIT_DEFAULT // TODO adaptive
|
||||||
|
|
||||||
fun isPrefetchApplicable(): Boolean {
|
fun isPrefetchApplicable(): Boolean {
|
||||||
return repository is RemoteMangaRepository
|
return repository is ParserMangaRepository
|
||||||
// && settings.isPagesPreloadEnabled
|
// && settings.isPagesPreloadEnabled
|
||||||
&& !context.isPowerSaveMode()
|
&& !context.isPowerSaveMode()
|
||||||
&& !isLowRam()
|
&& !isLowRam()
|
||||||
|
|||||||
@@ -2,10 +2,13 @@ package org.xtimms.shirizu.sections.reader.pager
|
|||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
|
import kotlinx.parcelize.TypeParceler
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaPage
|
import org.koitharu.kotatsu.parsers.model.MangaPage
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
|
import org.xtimms.shirizu.core.model.parcelable.MangaSourceParceler
|
||||||
|
|
||||||
@Parcelize
|
@Parcelize
|
||||||
|
@TypeParceler<MangaSource, MangaSourceParceler>
|
||||||
data class ReaderPage(
|
data class ReaderPage(
|
||||||
val id: Long,
|
val id: Long,
|
||||||
val url: String,
|
val url: String,
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
package org.xtimms.shirizu.sections.search
|
package org.xtimms.shirizu.sections.search
|
||||||
|
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
@@ -42,6 +40,7 @@ import org.xtimms.shirizu.R
|
|||||||
import org.xtimms.shirizu.core.components.MangaCarouselWithHeader
|
import org.xtimms.shirizu.core.components.MangaCarouselWithHeader
|
||||||
import org.xtimms.shirizu.core.components.Scaffold
|
import org.xtimms.shirizu.core.components.Scaffold
|
||||||
import org.xtimms.shirizu.core.components.icons.Dice
|
import org.xtimms.shirizu.core.components.icons.Dice
|
||||||
|
import org.xtimms.shirizu.core.model.parcelable.ParcelableManga
|
||||||
import org.xtimms.shirizu.sections.details.DetailsScreen
|
import org.xtimms.shirizu.sections.details.DetailsScreen
|
||||||
import org.xtimms.shirizu.sections.search.global.GlobalSearchScreen
|
import org.xtimms.shirizu.sections.search.global.GlobalSearchScreen
|
||||||
import org.xtimms.shirizu.sections.suggestions.SuggestionsScreen
|
import org.xtimms.shirizu.sections.suggestions.SuggestionsScreen
|
||||||
@@ -57,13 +56,13 @@ object SearchTab : Tab {
|
|||||||
get() {
|
get() {
|
||||||
val image = Icons.Outlined.Search
|
val image = Icons.Outlined.Search
|
||||||
return TabOptions(
|
return TabOptions(
|
||||||
index = 4u,
|
index = 2u,
|
||||||
title = stringResource(R.string.search),
|
title = stringResource(R.string.search),
|
||||||
icon = rememberVectorPainter(image),
|
icon = rememberVectorPainter(image),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
@Composable
|
@Composable
|
||||||
override fun Content() {
|
override fun Content() {
|
||||||
val navigator = LocalNavigator.currentOrThrow
|
val navigator = LocalNavigator.currentOrThrow
|
||||||
@@ -135,7 +134,7 @@ object SearchTab : Tab {
|
|||||||
MangaCarouselWithHeader(
|
MangaCarouselWithHeader(
|
||||||
items = state.list,
|
items = state.list,
|
||||||
title = stringResource(id = R.string.suggestions),
|
title = stringResource(id = R.string.suggestions),
|
||||||
onItemClick = { navigator.push(DetailsScreen(it)) },
|
onItemClick = { navigator.push(DetailsScreen(it.id)) },
|
||||||
onMoreClick = { navigator.push(SuggestionsScreen) },
|
onMoreClick = { navigator.push(SuggestionsScreen) },
|
||||||
refreshing = state.isLoading,
|
refreshing = state.isLoading,
|
||||||
modifier = Modifier.animateItem(),
|
modifier = Modifier.animateItem(),
|
||||||
|
|||||||
@@ -81,11 +81,6 @@ class GlobalSearchScreenModel @Inject constructor(
|
|||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
val sources = if (SearchSuggestionType.SOURCES in types) {
|
|
||||||
repository.getSourcesSuggestion(searchQuery, MAX_SOURCES_ITEMS)
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
|
|
||||||
val tags = tagsDeferred?.await()
|
val tags = tagsDeferred?.await()
|
||||||
val mangaList = mangaDeferred?.await()
|
val mangaList = mangaDeferred?.await()
|
||||||
@@ -93,11 +88,10 @@ class GlobalSearchScreenModel @Inject constructor(
|
|||||||
val hints = hintsDeferred?.await()
|
val hints = hintsDeferred?.await()
|
||||||
val authors = authorsDeferred?.await()
|
val authors = authorsDeferred?.await()
|
||||||
|
|
||||||
buildList(queries.sizeOrZero() + sources.sizeOrZero() + authors.sizeOrZero() + hints.sizeOrZero() + 2) {
|
buildList(queries.sizeOrZero() + authors.sizeOrZero() + hints.sizeOrZero() + 2) {
|
||||||
if (!mangaList.isNullOrEmpty()) {
|
if (!mangaList.isNullOrEmpty()) {
|
||||||
add(SearchSuggestionItem.MangaList(mangaList))
|
add(SearchSuggestionItem.MangaList(mangaList))
|
||||||
}
|
}
|
||||||
sources?.mapTo(this) { SearchSuggestionItem.Source(it, it in enabledSources) }
|
|
||||||
queries?.mapTo(this) { SearchSuggestionItem.RecentQuery(it) }
|
queries?.mapTo(this) { SearchSuggestionItem.RecentQuery(it) }
|
||||||
authors?.mapTo(this) { SearchSuggestionItem.Author(it) }
|
authors?.mapTo(this) { SearchSuggestionItem.Author(it) }
|
||||||
hints?.mapTo(this) { SearchSuggestionItem.Hint(it) }
|
hints?.mapTo(this) { SearchSuggestionItem.Hint(it) }
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package org.xtimms.shirizu.sections.search.global.model
|
|||||||
|
|
||||||
import org.koitharu.kotatsu.parsers.model.ContentType
|
import org.koitharu.kotatsu.parsers.model.ContentType
|
||||||
import org.koitharu.kotatsu.parsers.model.Manga
|
import org.koitharu.kotatsu.parsers.model.Manga
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaParserSource
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
import org.xtimms.shirizu.core.model.ListModel
|
import org.xtimms.shirizu.core.model.ListModel
|
||||||
|
|
||||||
@@ -44,7 +45,7 @@ sealed interface SearchSuggestionItem : ListModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
data class Source(
|
data class Source(
|
||||||
val source: MangaSource,
|
val source: MangaParserSource,
|
||||||
val isEnabled: Boolean,
|
val isEnabled: Boolean,
|
||||||
) : SearchSuggestionItem {
|
) : SearchSuggestionItem {
|
||||||
|
|
||||||
|
|||||||
@@ -1,101 +0,0 @@
|
|||||||
package org.xtimms.shirizu.sections.settings.sources
|
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
|
||||||
import androidx.compose.foundation.layout.asPaddingValues
|
|
||||||
import androidx.compose.foundation.layout.navigationBars
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.outlined.Apps
|
|
||||||
import androidx.compose.material.icons.outlined.NoAdultContent
|
|
||||||
import androidx.compose.material.icons.outlined.SettingsApplications
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.collectAsState
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|
||||||
import org.xtimms.shirizu.R
|
|
||||||
import org.xtimms.shirizu.core.components.PreferenceItem
|
|
||||||
import org.xtimms.shirizu.core.components.PreferenceSwitch
|
|
||||||
import org.xtimms.shirizu.core.components.ScaffoldWithTopAppBar
|
|
||||||
import org.xtimms.shirizu.core.prefs.AppSettings
|
|
||||||
import org.xtimms.shirizu.core.prefs.NSFW
|
|
||||||
|
|
||||||
const val SOURCES_DESTINATION = "sources"
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun SourcesView(
|
|
||||||
viewModel: SourcesSettingsViewModel = hiltViewModel(),
|
|
||||||
navigateBack: () -> Unit,
|
|
||||||
navigateToSourcesCatalog: () -> Unit,
|
|
||||||
navigateToSourcesManagement: () -> Unit,
|
|
||||||
) {
|
|
||||||
|
|
||||||
val context = LocalContext.current
|
|
||||||
val availableSourcesCount = viewModel.availableSourcesCount.collectAsState(-1).value
|
|
||||||
val enabledSourcesCount = viewModel.enabledSourcesCount.collectAsState(-1).value
|
|
||||||
val state by viewModel.viewStateFlow.collectAsStateWithLifecycle()
|
|
||||||
|
|
||||||
var isNSFWEnabled by remember {
|
|
||||||
mutableStateOf(AppSettings.isNSFWEnabled())
|
|
||||||
}
|
|
||||||
|
|
||||||
ScaffoldWithTopAppBar(
|
|
||||||
title = stringResource(R.string.manga_sources),
|
|
||||||
navigateBack = navigateBack
|
|
||||||
) { padding ->
|
|
||||||
LazyColumn(
|
|
||||||
modifier = Modifier.padding(padding),
|
|
||||||
contentPadding = PaddingValues(
|
|
||||||
bottom = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
item {
|
|
||||||
PreferenceItem(
|
|
||||||
title = stringResource(id = R.string.manage_sources),
|
|
||||||
description = if (enabledSourcesCount >= 0) {
|
|
||||||
context.resources.getQuantityString(
|
|
||||||
R.plurals.items,
|
|
||||||
enabledSourcesCount,
|
|
||||||
enabledSourcesCount
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
},
|
|
||||||
icon = Icons.Outlined.SettingsApplications,
|
|
||||||
onClick = { navigateToSourcesManagement() }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
item {
|
|
||||||
PreferenceItem(
|
|
||||||
title = stringResource(id = R.string.sources_catalog),
|
|
||||||
description = if (availableSourcesCount >= 0) {
|
|
||||||
stringResource(R.string.available_d, availableSourcesCount)
|
|
||||||
} else {
|
|
||||||
null
|
|
||||||
},
|
|
||||||
icon = Icons.Outlined.Apps,
|
|
||||||
onClick = { navigateToSourcesCatalog() }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
item {
|
|
||||||
PreferenceSwitch(
|
|
||||||
title = stringResource(id = R.string.disable_nsfw),
|
|
||||||
description = stringResource(id = R.string.disable_nsfw_desc),
|
|
||||||
icon = Icons.Outlined.NoAdultContent,
|
|
||||||
isChecked = isNSFWEnabled
|
|
||||||
) {
|
|
||||||
isNSFWEnabled = !isNSFWEnabled
|
|
||||||
AppSettings.updateValue(NSFW, isNSFWEnabled)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -16,6 +16,7 @@ import androidx.compose.ui.platform.LocalContext
|
|||||||
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 coil.ImageLoader
|
import coil.ImageLoader
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaParserSource
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
import org.xtimms.shirizu.core.ShirizuAsyncImage
|
import org.xtimms.shirizu.core.ShirizuAsyncImage
|
||||||
import org.xtimms.shirizu.core.parser.favicon.faviconUri
|
import org.xtimms.shirizu.core.parser.favicon.faviconUri
|
||||||
@@ -23,7 +24,7 @@ import org.xtimms.shirizu.ui.theme.ShirizuTheme
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SourceCatalogItem(
|
fun SourceCatalogItem(
|
||||||
source: MangaSource,
|
source: MangaParserSource,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
@@ -53,6 +54,6 @@ fun SourceCatalogItem(
|
|||||||
@Composable
|
@Composable
|
||||||
fun SourceCatalogItemPreview() {
|
fun SourceCatalogItemPreview() {
|
||||||
ShirizuTheme {
|
ShirizuTheme {
|
||||||
SourceCatalogItem(source = MangaSource.MANGADEX)
|
SourceCatalogItem(source = MangaParserSource.MANGADEX)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
package org.xtimms.shirizu.sections.settings.sources.catalog
|
|
||||||
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.outlined.SearchOff
|
|
||||||
import androidx.room.InvalidationTracker
|
|
||||||
import dagger.assisted.Assisted
|
|
||||||
import dagger.assisted.AssistedFactory
|
|
||||||
import dagger.assisted.AssistedInject
|
|
||||||
import dagger.hilt.android.ViewModelLifecycle
|
|
||||||
import dagger.hilt.android.lifecycle.RetainedLifecycle
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.cancelAndJoin
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.update
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import org.koitharu.kotatsu.parsers.model.ContentType
|
|
||||||
import org.xtimms.shirizu.R
|
|
||||||
import org.xtimms.shirizu.core.database.TABLE_SOURCES
|
|
||||||
import org.xtimms.shirizu.core.database.ShirizuDatabase
|
|
||||||
import org.xtimms.shirizu.core.database.removeObserverAsync
|
|
||||||
import org.xtimms.shirizu.data.repository.MangaSourcesRepository
|
|
||||||
import org.xtimms.shirizu.utils.lang.lifecycleScope
|
|
||||||
|
|
||||||
class SourcesCatalogListProducer @AssistedInject constructor(
|
|
||||||
@Assisted private val locale: String?,
|
|
||||||
@Assisted private val contentType: ContentType,
|
|
||||||
@Assisted lifecycle: ViewModelLifecycle,
|
|
||||||
private val repository: MangaSourcesRepository,
|
|
||||||
private val database: ShirizuDatabase,
|
|
||||||
) : InvalidationTracker.Observer(TABLE_SOURCES), RetainedLifecycle.OnClearedListener {
|
|
||||||
|
|
||||||
private val scope = lifecycle.lifecycleScope
|
|
||||||
|
|
||||||
private var query: String? = null
|
|
||||||
val list = MutableStateFlow(emptyList<SourceCatalogItemModel>())
|
|
||||||
|
|
||||||
private var job = scope.launch(Dispatchers.Default) {
|
|
||||||
list.value = buildList()
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
|
||||||
scope.launch(Dispatchers.Default) {
|
|
||||||
database.invalidationTracker.addObserver(this@SourcesCatalogListProducer)
|
|
||||||
}
|
|
||||||
lifecycle.addOnClearedListener(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCleared() {
|
|
||||||
database.invalidationTracker.removeObserverAsync(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onInvalidated(tables: Set<String>) {
|
|
||||||
val prevJob = job
|
|
||||||
job = scope.launch(Dispatchers.Default) {
|
|
||||||
prevJob.cancelAndJoin()
|
|
||||||
list.update { buildList() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setQuery(value: String?) {
|
|
||||||
this.query = value
|
|
||||||
onInvalidated(emptySet())
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun buildList(): List<SourceCatalogItemModel> {
|
|
||||||
val sources = repository.getDisabledSources().toMutableList()
|
|
||||||
when (val q = query) {
|
|
||||||
null -> sources.retainAll { it.contentType == contentType && it.locale == locale }
|
|
||||||
"" -> return emptyList()
|
|
||||||
else -> sources.retainAll { it.title.contains(q, ignoreCase = true) }
|
|
||||||
}
|
|
||||||
sources.sortBy { it.title }
|
|
||||||
return sources.map {
|
|
||||||
SourceCatalogItemModel(
|
|
||||||
source = it,
|
|
||||||
showSummary = query != null,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@AssistedFactory
|
|
||||||
interface Factory {
|
|
||||||
|
|
||||||
fun create(
|
|
||||||
locale: String?,
|
|
||||||
contentType: ContentType,
|
|
||||||
lifecycle: ViewModelLifecycle,
|
|
||||||
): SourcesCatalogListProducer
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -57,11 +57,11 @@ fun SourcesCatalogPager(
|
|||||||
items(
|
items(
|
||||||
items = sources,
|
items = sources,
|
||||||
) { item ->
|
) { item ->
|
||||||
item.items.forEach { source ->
|
/*item.items.forEach { source ->
|
||||||
SourceCatalogItem(
|
SourceCatalogItem(
|
||||||
source = source.source,
|
source = source.source,
|
||||||
)
|
)
|
||||||
}
|
}*/
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,55 +0,0 @@
|
|||||||
package org.xtimms.shirizu.sections.settings.sources.catalog
|
|
||||||
|
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.pager.rememberPagerState
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.hilt.navigation.compose.hiltViewModel
|
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|
||||||
import coil.ImageLoader
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import org.xtimms.shirizu.R
|
|
||||||
import org.xtimms.shirizu.core.components.ScaffoldWithClassicTopAppBar
|
|
||||||
|
|
||||||
const val CATALOG_DESTINATION = "catalog"
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun SourcesCatalogView(
|
|
||||||
coil: ImageLoader,
|
|
||||||
sourcesCatalogViewModel: SourcesCatalogViewModel = hiltViewModel(),
|
|
||||||
navigateBack: () -> Unit,
|
|
||||||
) {
|
|
||||||
|
|
||||||
val categories by sourcesCatalogViewModel.content.collectAsStateWithLifecycle(emptyList())
|
|
||||||
|
|
||||||
ScaffoldWithClassicTopAppBar(
|
|
||||||
title = stringResource(R.string.sources_catalog),
|
|
||||||
navigateBack = navigateBack
|
|
||||||
) { padding ->
|
|
||||||
Column(
|
|
||||||
modifier = Modifier.padding(padding)
|
|
||||||
) {
|
|
||||||
val pagerState = rememberPagerState(0) { categories.size }
|
|
||||||
val scope = rememberCoroutineScope()
|
|
||||||
if (categories.isNotEmpty()) {
|
|
||||||
SourcesCatalogTabs(
|
|
||||||
categories = categories,
|
|
||||||
pagerState = pagerState,
|
|
||||||
) { scope.launch { pagerState.animateScrollToPage(it) } }
|
|
||||||
}
|
|
||||||
|
|
||||||
SourcesCatalogPager(
|
|
||||||
coil = coil,
|
|
||||||
state = pagerState,
|
|
||||||
contentPadding = padding,
|
|
||||||
searchQuery = null,
|
|
||||||
getSourcesForPage = { categories }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
package org.xtimms.shirizu.sections.settings.sources.catalog
|
|
||||||
|
|
||||||
import androidx.annotation.MainThread
|
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import dagger.hilt.android.internal.lifecycle.RetainedLifecycleImpl
|
|
||||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
|
||||||
import kotlinx.coroutines.flow.SharingStarted
|
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
|
||||||
import kotlinx.coroutines.flow.combine
|
|
||||||
import kotlinx.coroutines.flow.flatMapLatest
|
|
||||||
import kotlinx.coroutines.flow.map
|
|
||||||
import kotlinx.coroutines.flow.stateIn
|
|
||||||
import kotlinx.coroutines.plus
|
|
||||||
import org.koitharu.kotatsu.parsers.model.ContentType
|
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
|
||||||
import org.koitharu.kotatsu.parsers.util.mapToSet
|
|
||||||
import org.xtimms.shirizu.R
|
|
||||||
import org.xtimms.shirizu.core.base.viewmodel.KotatsuBaseViewModel
|
|
||||||
import org.xtimms.shirizu.core.prefs.AppSettings
|
|
||||||
import org.xtimms.shirizu.data.repository.MangaSourcesRepository
|
|
||||||
import org.xtimms.shirizu.utils.ReversibleAction
|
|
||||||
import org.xtimms.shirizu.utils.lang.MutableEventFlow
|
|
||||||
import org.xtimms.shirizu.utils.lang.call
|
|
||||||
import java.util.EnumMap
|
|
||||||
import java.util.EnumSet
|
|
||||||
import java.util.Locale
|
|
||||||
import javax.inject.Inject
|
|
||||||
|
|
||||||
@HiltViewModel
|
|
||||||
class SourcesCatalogViewModel @Inject constructor(
|
|
||||||
private val repository: MangaSourcesRepository,
|
|
||||||
private val listProducerFactory: SourcesCatalogListProducer.Factory,
|
|
||||||
) : KotatsuBaseViewModel() {
|
|
||||||
|
|
||||||
private val lifecycle = RetainedLifecycleImpl()
|
|
||||||
private var searchQuery: String? = null
|
|
||||||
val onActionDone = MutableEventFlow<ReversibleAction>()
|
|
||||||
val locales = repository.allMangaSources.mapToSet { it.locale }
|
|
||||||
val locale = MutableStateFlow(Locale.getDefault().language.takeIf { it in locales })
|
|
||||||
|
|
||||||
private val listProducers = locale.map { lc ->
|
|
||||||
createListProducers(lc)
|
|
||||||
}.stateIn(viewModelScope, SharingStarted.Eagerly, createListProducers(locale.value))
|
|
||||||
|
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
|
||||||
val content: StateFlow<List<SourceCatalogPage>> = listProducers.flatMapLatest {
|
|
||||||
val flows = it.entries.map { (type, producer) -> producer.list.map { x -> SourceCatalogPage(type, x) } }
|
|
||||||
combine<SourceCatalogPage, List<SourceCatalogPage>>(flows, Array<SourceCatalogPage>::toList)
|
|
||||||
}.stateIn(viewModelScope + Dispatchers.Default, SharingStarted.Eagerly, emptyList())
|
|
||||||
|
|
||||||
override fun onCleared() {
|
|
||||||
super.onCleared()
|
|
||||||
lifecycle.dispatchOnCleared()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun performSearch(query: String?) {
|
|
||||||
searchQuery = query
|
|
||||||
listProducers.value.forEach { (_, v) -> v.setQuery(query) }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setLocale(value: String?) {
|
|
||||||
locale.value = value
|
|
||||||
}
|
|
||||||
|
|
||||||
fun addSource(source: MangaSource) {
|
|
||||||
launchJob(Dispatchers.Default) {
|
|
||||||
val rollback = repository.setSourceEnabled(source, true)
|
|
||||||
onActionDone.call(ReversibleAction(R.string.source_enabled, rollback))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@MainThread
|
|
||||||
private fun createListProducers(lc: String?): Map<ContentType, SourcesCatalogListProducer> {
|
|
||||||
val types = EnumSet.allOf(ContentType::class.java)
|
|
||||||
if (AppSettings.isNSFWEnabled()) {
|
|
||||||
types.remove(ContentType.HENTAI)
|
|
||||||
}
|
|
||||||
return types.associateWithTo(EnumMap(ContentType::class.java)) { type ->
|
|
||||||
listProducerFactory.create(lc, type, lifecycle).also {
|
|
||||||
it.setQuery(searchQuery)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -25,6 +25,7 @@ import cafe.adriel.voyager.navigator.tab.TabOptions
|
|||||||
import org.xtimms.shirizu.R
|
import org.xtimms.shirizu.R
|
||||||
import org.xtimms.shirizu.core.components.Scaffold
|
import org.xtimms.shirizu.core.components.Scaffold
|
||||||
import org.xtimms.shirizu.core.components.LibraryBottomActionMenu
|
import org.xtimms.shirizu.core.components.LibraryBottomActionMenu
|
||||||
|
import org.xtimms.shirizu.core.model.parcelable.ParcelableManga
|
||||||
import org.xtimms.shirizu.core.ui.screens.EmptyScreen
|
import org.xtimms.shirizu.core.ui.screens.EmptyScreen
|
||||||
import org.xtimms.shirizu.core.ui.screens.LoadingScreen
|
import org.xtimms.shirizu.core.ui.screens.LoadingScreen
|
||||||
import org.xtimms.shirizu.sections.details.DetailsScreen
|
import org.xtimms.shirizu.sections.details.DetailsScreen
|
||||||
@@ -85,7 +86,7 @@ object ShelfTab : Tab, NoLiftingAppBarScreen {
|
|||||||
currentPage = { screenModel.activeCategoryIndex },
|
currentPage = { screenModel.activeCategoryIndex },
|
||||||
hasActiveFilters = state.hasActiveFilters,
|
hasActiveFilters = state.hasActiveFilters,
|
||||||
onChangeCurrentPage = { },
|
onChangeCurrentPage = { },
|
||||||
onMangaClicked = { navigator.push(DetailsScreen(it)) },
|
onMangaClicked = { navigator.push(DetailsScreen(it.id)) },
|
||||||
onToggleSelection = { },
|
onToggleSelection = { },
|
||||||
onToggleRangeSelection = {
|
onToggleRangeSelection = {
|
||||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import androidx.compose.ui.tooling.preview.Preview
|
|||||||
import androidx.compose.ui.unit.LayoutDirection
|
import androidx.compose.ui.unit.LayoutDirection
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaParserSource
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
import org.xtimms.shirizu.ui.theme.colorMax
|
import org.xtimms.shirizu.ui.theme.colorMax
|
||||||
import org.xtimms.shirizu.ui.theme.colorMin
|
import org.xtimms.shirizu.ui.theme.colorMin
|
||||||
@@ -158,12 +159,12 @@ private fun PreviewChart() {
|
|||||||
ChaptersChart(
|
ChaptersChart(
|
||||||
modifier = Modifier.size(100.dp),
|
modifier = Modifier.size(100.dp),
|
||||||
chapters = listOf(
|
chapters = listOf(
|
||||||
MangaChapter(id = 1, name = "", number = 1, "", "", 0L, "", MangaSource.DUMMY),
|
MangaChapter(id = 1, name = "", number = 1, "", "", 0L, "", MangaParserSource.DUMMY),
|
||||||
MangaChapter(id = 1, name = "", number = 1, "", "", 0L, "", MangaSource.DUMMY),
|
MangaChapter(id = 1, name = "", number = 1, "", "", 0L, "", MangaParserSource.DUMMY),
|
||||||
MangaChapter(id = 1, name = "", number = 1, "", "", 0L, "", MangaSource.DUMMY),
|
MangaChapter(id = 1, name = "", number = 1, "", "", 0L, "", MangaParserSource.DUMMY),
|
||||||
MangaChapter(id = 1, name = "", number = 1, "", "", 0L, "", MangaSource.DUMMY),
|
MangaChapter(id = 1, name = "", number = 1, "", "", 0L, "", MangaParserSource.DUMMY),
|
||||||
MangaChapter(id = 1, name = "", number = 1, "", "", 0L, "", MangaSource.DUMMY),
|
MangaChapter(id = 1, name = "", number = 1, "", "", 0L, "", MangaParserSource.DUMMY),
|
||||||
MangaChapter(id = 1, name = "", number = 1, "", "", 0L, "", MangaSource.DUMMY)
|
MangaChapter(id = 1, name = "", number = 1, "", "", 0L, "", MangaParserSource.DUMMY)
|
||||||
),
|
),
|
||||||
chartPadding = PaddingValues(vertical = 16.dp)
|
chartPadding = PaddingValues(vertical = 16.dp)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import androidx.compose.ui.platform.LocalContext
|
|||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
import org.koitharu.kotatsu.parsers.model.MangaChapter
|
||||||
|
import org.koitharu.kotatsu.parsers.model.MangaParserSource
|
||||||
import org.koitharu.kotatsu.parsers.model.MangaSource
|
import org.koitharu.kotatsu.parsers.model.MangaSource
|
||||||
import org.xtimms.shirizu.ui.theme.colorMax
|
import org.xtimms.shirizu.ui.theme.colorMax
|
||||||
import org.xtimms.shirizu.ui.theme.colorMin
|
import org.xtimms.shirizu.ui.theme.colorMin
|
||||||
@@ -98,12 +99,12 @@ fun MinMaxReadCard(
|
|||||||
.fillMaxHeight()
|
.fillMaxHeight()
|
||||||
.fillMaxWidth(),
|
.fillMaxWidth(),
|
||||||
chapters = listOf(
|
chapters = listOf(
|
||||||
MangaChapter(id = 1, name = "", number = 1, "", "", 0L, "", MangaSource.DUMMY),
|
MangaChapter(id = 1, name = "", number = 1, "", "", 0L, "", MangaParserSource.DUMMY),
|
||||||
MangaChapter(id = 1, name = "", number = 1, "", "", 0L, "", MangaSource.DUMMY),
|
MangaChapter(id = 1, name = "", number = 1, "", "", 0L, "", MangaParserSource.DUMMY),
|
||||||
MangaChapter(id = 1, name = "", number = 1, "", "", 0L, "", MangaSource.DUMMY),
|
MangaChapter(id = 1, name = "", number = 1, "", "", 0L, "", MangaParserSource.DUMMY),
|
||||||
MangaChapter(id = 1, name = "", number = 1, "", "", 0L, "", MangaSource.DUMMY),
|
MangaChapter(id = 1, name = "", number = 1, "", "", 0L, "", MangaParserSource.DUMMY),
|
||||||
MangaChapter(id = 1, name = "", number = 1, "", "", 0L, "", MangaSource.DUMMY),
|
MangaChapter(id = 1, name = "", number = 1, "", "", 0L, "", MangaParserSource.DUMMY),
|
||||||
MangaChapter(id = 1, name = "", number = 1, "", "", 0L, "", MangaSource.DUMMY)
|
MangaChapter(id = 1, name = "", number = 1, "", "", 0L, "", MangaParserSource.DUMMY)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,13 +14,28 @@ import androidx.compose.material3.Card
|
|||||||
import androidx.compose.material3.CardDefaults
|
import androidx.compose.material3.CardDefaults
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
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.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import org.xtimms.shirizu.R
|
||||||
|
import org.xtimms.shirizu.sections.shelf.ShelfCategory
|
||||||
|
import org.xtimms.shirizu.utils.material.HarmonizedColorPalette
|
||||||
import org.xtimms.shirizu.utils.material.combineColors
|
import org.xtimms.shirizu.utils.material.combineColors
|
||||||
|
import org.xtimms.shirizu.utils.material.harmonize
|
||||||
import org.xtimms.shirizu.utils.material.harmonizeWithColor
|
import org.xtimms.shirizu.utils.material.harmonizeWithColor
|
||||||
import org.xtimms.shirizu.utils.material.toPalette
|
import org.xtimms.shirizu.utils.material.toPalette
|
||||||
|
import java.math.BigDecimal
|
||||||
|
|
||||||
|
data class TagUsage(
|
||||||
|
val name: String,
|
||||||
|
val mangaCount: BigDecimal,
|
||||||
|
var color: HarmonizedColorPalette? = null
|
||||||
|
)
|
||||||
|
|
||||||
var baseColors = listOf(
|
var baseColors = listOf(
|
||||||
Color(0xFFF86BAE),
|
Color(0xFFF86BAE),
|
||||||
@@ -35,10 +50,13 @@ var baseColors = listOf(
|
|||||||
@OptIn(ExperimentalLayoutApi::class)
|
@OptIn(ExperimentalLayoutApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun CategoriesChart(
|
fun CategoriesChart(
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier,
|
||||||
|
categories: List<ShelfCategory>
|
||||||
) {
|
) {
|
||||||
|
|
||||||
val isNightMode = isSystemInDarkTheme()
|
val isNightMode = isSystemInDarkTheme()
|
||||||
|
val labelWithoutTag = stringResource(R.string.progress)
|
||||||
|
val maxDisplay = 7
|
||||||
|
|
||||||
val colors = baseColors.map {
|
val colors = baseColors.map {
|
||||||
toPalette(
|
toPalette(
|
||||||
@@ -48,9 +66,61 @@ fun CategoriesChart(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
val restColor = toPalette(
|
||||||
|
color = harmonize(
|
||||||
|
designColor = Color(0xFF222222),
|
||||||
|
sourceColor = MaterialTheme.colorScheme.primary
|
||||||
|
),
|
||||||
|
).copy(
|
||||||
|
main = if (isNightMode) Color(0xFFF0F0F0) else Color(0xFF222222),
|
||||||
|
onSurface = if (isNightMode) Color(0xFF1A1A1A) else Color(0xFFF4F4F4)
|
||||||
|
)
|
||||||
|
val stubColor = toPalette(
|
||||||
|
color = harmonize(
|
||||||
|
designColor = Color(0xFFCCCCCC),
|
||||||
|
sourceColor = MaterialTheme.colorScheme.primary
|
||||||
|
),
|
||||||
|
).copy(
|
||||||
|
main = if (isNightMode) MaterialTheme.colorScheme.surfaceVariant else Color(0xFFCCCCCC),
|
||||||
|
)
|
||||||
|
|
||||||
|
var offsetColor = 0
|
||||||
|
|
||||||
|
val tags by remember {
|
||||||
|
var result = categories
|
||||||
|
.groupBy { it.title.trim() }
|
||||||
|
.map { tag ->
|
||||||
|
TagUsage(
|
||||||
|
tag.key,
|
||||||
|
tag.value.map { it.mangaCount.toBigDecimal() }.reduce { acc, next -> acc + next },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.sortedBy { it.name }
|
||||||
|
.reversed()
|
||||||
|
.toList()
|
||||||
|
|
||||||
|
// Set colors
|
||||||
|
result.subList(0, result.size.coerceAtMost(maxDisplay)).forEachIndexed { index, tagUsage ->
|
||||||
|
tagUsage.color = colors.getOrNull(index - offsetColor) ?: colors.last()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Combine rest tags to one
|
||||||
|
if (result.size > maxDisplay) {
|
||||||
|
result = result.slice(0..<maxDisplay) + TagUsage(
|
||||||
|
name = labelWithoutTag,
|
||||||
|
mangaCount = result
|
||||||
|
.slice(maxDisplay until result.size)
|
||||||
|
.map { it.mangaCount }
|
||||||
|
.reduce { acc, next -> acc + next },
|
||||||
|
color = restColor,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
mutableStateOf(result)
|
||||||
|
}
|
||||||
|
|
||||||
Card(
|
Card(
|
||||||
modifier = modifier,
|
modifier = modifier.fillMaxWidth(),
|
||||||
shape = RoundedCornerShape(22.dp),
|
shape = RoundedCornerShape(22.dp),
|
||||||
colors = CardDefaults.cardColors(
|
colors = CardDefaults.cardColors(
|
||||||
containerColor = combineColors(
|
containerColor = combineColors(
|
||||||
@@ -59,26 +129,22 @@ fun CategoriesChart(
|
|||||||
angle = 0.3f,
|
angle = 0.3f,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
) {
|
|
||||||
Box {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(16.dp),
|
|
||||||
verticalArrangement = Arrangement.Center,
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
) {
|
) {
|
||||||
DonutChart(
|
DonutChart(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 8.dp)
|
.padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 8.dp)
|
||||||
.size(64.dp),
|
.size(64.dp),
|
||||||
items = emptyList(),
|
items = tags,
|
||||||
)
|
)
|
||||||
FlowRow(Modifier.padding(4.dp, 4.dp)) {
|
FlowRow(Modifier.padding(4.dp, 4.dp)) {
|
||||||
|
tags.forEach { tag ->
|
||||||
|
TagAmount(
|
||||||
|
modifier = Modifier.padding(4.dp, 4.dp),
|
||||||
|
value = tag.name,
|
||||||
|
palette = tag.color,
|
||||||
|
amount = tag.mangaCount
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
|
||||||
@@ -16,11 +16,12 @@ import androidx.compose.ui.platform.LocalDensity
|
|||||||
import androidx.compose.ui.unit.LayoutDirection
|
import androidx.compose.ui.unit.LayoutDirection
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import org.xtimms.shirizu.sections.shelf.ShelfCategory
|
import org.xtimms.shirizu.sections.shelf.ShelfCategory
|
||||||
|
import java.math.RoundingMode
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun DonutChart(
|
fun DonutChart(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
items: List<ShelfCategory>,
|
items: List<TagUsage>,
|
||||||
chartPadding: PaddingValues = PaddingValues(0.dp),
|
chartPadding: PaddingValues = PaddingValues(0.dp),
|
||||||
) {
|
) {
|
||||||
val localDensity = LocalDensity.current
|
val localDensity = LocalDensity.current
|
||||||
@@ -42,7 +43,7 @@ fun DonutChart(
|
|||||||
val heightWithPaddings = height - topOffset - bottomOffset
|
val heightWithPaddings = height - topOffset - bottomOffset
|
||||||
val widthWithPaddings = width - startOffset - endOffset
|
val widthWithPaddings = width - startOffset - endOffset
|
||||||
|
|
||||||
val total = items.map { it.id }.reduce { acc, next -> acc + next }
|
val total = items.map { it.mangaCount }.reduce { acc, next -> acc + next }
|
||||||
var offset = 0f
|
var offset = 0f
|
||||||
|
|
||||||
val gap = 0f
|
val gap = 0f
|
||||||
@@ -52,11 +53,37 @@ fun DonutChart(
|
|||||||
val minSweepAngle = 28f
|
val minSweepAngle = 28f
|
||||||
val offsetAngle = -90f
|
val offsetAngle = -90f
|
||||||
|
|
||||||
|
var itemAngles = items.map {
|
||||||
|
it.mangaCount
|
||||||
|
.divide(total, 5, RoundingMode.HALF_DOWN)
|
||||||
|
.multiply(360.toBigDecimal())
|
||||||
|
.toFloat()
|
||||||
|
}
|
||||||
|
|
||||||
|
val shareAngle = itemAngles
|
||||||
|
.filter { it < minSweepAngle }
|
||||||
|
.map { minSweepAngle - it }
|
||||||
|
.fold(0f) { acc, next -> acc + next }
|
||||||
|
val splitItems = itemAngles.filter { it > minSweepAngle }.toMutableList()
|
||||||
|
|
||||||
|
itemAngles = itemAngles.map { angle ->
|
||||||
|
if (angle < minSweepAngle) {
|
||||||
|
return@map minSweepAngle
|
||||||
|
}
|
||||||
|
|
||||||
|
if (angle > minSweepAngle) {
|
||||||
|
return@map angle - shareAngle / splitItems.size
|
||||||
|
}
|
||||||
|
|
||||||
|
angle
|
||||||
|
}
|
||||||
|
|
||||||
items.forEachIndexed { index, tag ->
|
items.forEachIndexed { index, tag ->
|
||||||
|
val sweepAngle = itemAngles[index]
|
||||||
drawArc(
|
drawArc(
|
||||||
Color.Black,
|
tag.color?.main ?: Color.Black,
|
||||||
startAngle = offset + halfGap + offsetAngle,
|
startAngle = offset + halfGap + offsetAngle,
|
||||||
sweepAngle = 36 - gap,
|
sweepAngle = sweepAngle - gap,
|
||||||
useCenter = false,
|
useCenter = false,
|
||||||
topLeft = Offset(startOffset + halfStrokeWidth, topOffset + halfStrokeWidth),
|
topLeft = Offset(startOffset + halfStrokeWidth, topOffset + halfStrokeWidth),
|
||||||
size = Size(widthWithPaddings - strokeWidth, heightWithPaddings - strokeWidth),
|
size = Size(widthWithPaddings - strokeWidth, heightWithPaddings - strokeWidth),
|
||||||
@@ -66,7 +93,7 @@ fun DonutChart(
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
offset += 50
|
offset += sweepAngle
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -49,7 +49,7 @@ fun TagAmount(
|
|||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.width(8.dp))
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
Text(
|
Text(
|
||||||
text = "56",
|
text = amount.toString(),
|
||||||
softWrap = false,
|
softWrap = false,
|
||||||
style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.W900),
|
style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.W900),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ import org.xtimms.shirizu.R
|
|||||||
import org.xtimms.shirizu.core.components.MangaGridItem
|
import org.xtimms.shirizu.core.components.MangaGridItem
|
||||||
import org.xtimms.shirizu.core.components.ScaffoldWithTopAppBar
|
import org.xtimms.shirizu.core.components.ScaffoldWithTopAppBar
|
||||||
import org.xtimms.shirizu.core.components.icons.Creation
|
import org.xtimms.shirizu.core.components.icons.Creation
|
||||||
|
import org.xtimms.shirizu.core.model.parcelable.ParcelableManga
|
||||||
import org.xtimms.shirizu.core.ui.screens.EmptyScreen
|
import org.xtimms.shirizu.core.ui.screens.EmptyScreen
|
||||||
import org.xtimms.shirizu.core.ui.screens.LoadingScreen
|
import org.xtimms.shirizu.core.ui.screens.LoadingScreen
|
||||||
import org.xtimms.shirizu.sections.details.DetailsScreen
|
import org.xtimms.shirizu.sections.details.DetailsScreen
|
||||||
@@ -84,7 +85,7 @@ object SuggestionsScreen : Screen() {
|
|||||||
SuggestionsScreenContent(
|
SuggestionsScreenContent(
|
||||||
suggestions = it,
|
suggestions = it,
|
||||||
contentPadding = padding,
|
contentPadding = padding,
|
||||||
onClick = { suggestion -> navigator.push(DetailsScreen(suggestion.manga)) }
|
onClick = { suggestion -> navigator.push(DetailsScreen(suggestion.manga.id)) }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ package org.xtimms.shirizu.utils
|
|||||||
|
|
||||||
import androidx.collection.ArrayMap
|
import androidx.collection.ArrayMap
|
||||||
import kotlinx.coroutines.sync.Mutex
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlin.contracts.ExperimentalContracts
|
||||||
|
import kotlin.contracts.InvocationKind
|
||||||
|
import kotlin.contracts.contract
|
||||||
|
|
||||||
class MultiMutex<T : Any> : Set<T> {
|
class MultiMutex<T : Any> : Set<T> {
|
||||||
|
|
||||||
@@ -40,4 +43,17 @@ class MultiMutex<T : Any> : Set<T> {
|
|||||||
delegates.remove(element)?.unlock()
|
delegates.remove(element)?.unlock()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalContracts::class)
|
||||||
|
suspend inline fun <R> withLock(element: T, block: () -> R): R {
|
||||||
|
contract {
|
||||||
|
callsInPlace(block, InvocationKind.EXACTLY_ONCE)
|
||||||
|
}
|
||||||
|
return try {
|
||||||
|
lock(element)
|
||||||
|
block()
|
||||||
|
} finally {
|
||||||
|
unlock(element)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -5,6 +5,7 @@ import okhttp3.HttpUrl
|
|||||||
import okhttp3.MediaType.Companion.toMediaType
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
|
import okhttp3.ResponseBody
|
||||||
import okhttp3.internal.closeQuietly
|
import okhttp3.internal.closeQuietly
|
||||||
import okio.IOException
|
import okio.IOException
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
@@ -72,3 +73,5 @@ private fun Char.isValidForHeaderValue(): Boolean {
|
|||||||
// from okhttp3.Headers$Companion.checkValue
|
// from okhttp3.Headers$Companion.checkValue
|
||||||
return this == '\t' || this in '\u0020'..'\u007e'
|
return this == '\t' || this in '\u0020'..'\u007e'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun Response.requireBody(): ResponseBody = checkNotNull(body) { "Response body is null" }
|
||||||
@@ -1,12 +1,22 @@
|
|||||||
package org.xtimms.shirizu.utils.system
|
package org.xtimms.shirizu.utils.system
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
import androidx.appcompat.app.AppCompatDelegate
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.core.os.LocaleListCompat
|
import androidx.core.os.LocaleListCompat
|
||||||
|
import org.koitharu.kotatsu.parsers.util.toTitleCase
|
||||||
import org.xtimms.shirizu.R
|
import org.xtimms.shirizu.R
|
||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
|
|
||||||
|
fun String.toLocale() = Locale(this)
|
||||||
|
|
||||||
|
fun Locale?.getDisplayName(context: Context): String = when (this) {
|
||||||
|
null -> context.getString(R.string.multi_lang)
|
||||||
|
Locale.ROOT -> context.getString(R.string.various_languages)
|
||||||
|
else -> getDisplayLanguage(this).toTitleCase(this)
|
||||||
|
}
|
||||||
|
|
||||||
fun LocaleListCompat.toList(): List<Locale> = List(size()) { i -> getOrThrow(i) }
|
fun LocaleListCompat.toList(): List<Locale> = List(size()) { i -> getOrThrow(i) }
|
||||||
|
|
||||||
fun LocaleListCompat.getOrThrow(index: Int) = get(index) ?: throw NoSuchElementException()
|
fun LocaleListCompat.getOrThrow(index: Int) = get(index) ?: throw NoSuchElementException()
|
||||||
|
|||||||
@@ -356,9 +356,16 @@
|
|||||||
<string name="shelves">Shelves</string>
|
<string name="shelves">Shelves</string>
|
||||||
<string name="history_cleared">History cleared</string>
|
<string name="history_cleared">History cleared</string>
|
||||||
<string name="search_by_reading_history">Search by reading history</string>
|
<string name="search_by_reading_history">Search by reading history</string>
|
||||||
<string name="sort_alphabetically">A-Z</string>
|
<string name="sort_alphabetically">Name</string>
|
||||||
<string name="sort_date_added">Date added</string>
|
<string name="sort_date_added">Date added</string>
|
||||||
<string name="show_nsfw">Show NSFW</string>
|
<string name="show_nsfw">Show NSFW</string>
|
||||||
<string name="action_save">Save</string>
|
<string name="action_save">Save</string>
|
||||||
<string name="action_mark_as_completed">Mark as completed</string>
|
<string name="action_mark_as_completed">Mark as completed</string>
|
||||||
|
<string name="sort_alphabetically_reversed">Z-A</string>
|
||||||
|
<string name="profile">Profile</string>
|
||||||
|
<string name="menu">Menu</string>
|
||||||
|
<string name="help_centre">Help centre</string>
|
||||||
|
<string name="source_summary_pattern">%1$s, %2$s</string>
|
||||||
|
<string name="external_source">External/plugin</string>
|
||||||
|
<string name="various_languages">Various languages</string>
|
||||||
</resources>
|
</resources>
|
||||||
Reference in New Issue
Block a user