diff --git a/.idea/other.xml b/.idea/other.xml
index d95a0f7..e7925b8 100644
--- a/.idea/other.xml
+++ b/.idea/other.xml
@@ -4,279 +4,6 @@
-
-
+
\ No newline at end of file
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 56035c9..0a6ab55 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -173,7 +173,7 @@ dependencies {
kapt("com.google.dagger:hilt-compiler:2.51.1")
implementation("androidx.hilt:hilt-work: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")
}
implementation("com.mikepenz:aboutlibraries-compose-m3:10.10.0")
diff --git a/app/schemas/org.xtimms.shirizu.core.database.ShirizuDatabase/4.json b/app/schemas/org.xtimms.shirizu.core.database.ShirizuDatabase/4.json
index 60e68c2..4956198 100644
--- a/app/schemas/org.xtimms.shirizu.core.database.ShirizuDatabase/4.json
+++ b/app/schemas/org.xtimms.shirizu.core.database.ShirizuDatabase/4.json
@@ -2,7 +2,7 @@
"formatVersion": 1,
"database": {
"version": 4,
- "identityHash": "dbe1dcac0f49c5ae2ac88d88aa280081",
+ "identityHash": "90b73386d5c61c2ddf46d6354ca2f1b6",
"entities": [
{
"tableName": "manga",
@@ -199,7 +199,7 @@
},
{
"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": [
{
"fieldPath": "source",
@@ -218,6 +218,24 @@
"columnName": "sort_key",
"affinity": "INTEGER",
"notNull": true
+ },
+ {
+ "fieldPath": "addedIn",
+ "columnName": "added_in",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "lastUsedAt",
+ "columnName": "used_at",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isPinned",
+ "columnName": "pinned",
+ "affinity": "INTEGER",
+ "notNull": true
}
],
"primaryKey": {
@@ -851,7 +869,7 @@
"views": [],
"setupQueries": [
"CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
- "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'dbe1dcac0f49c5ae2ac88d88aa280081')"
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '90b73386d5c61c2ddf46d6354ca2f1b6')"
]
}
}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 7f8e92a..7a2a547 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -15,6 +15,10 @@
+
+
diff --git a/app/src/main/java/org/xtimms/shirizu/MainActivity.kt b/app/src/main/java/org/xtimms/shirizu/MainActivity.kt
index 2df5ef6..7b26bc4 100644
--- a/app/src/main/java/org/xtimms/shirizu/MainActivity.kt
+++ b/app/src/main/java/org/xtimms/shirizu/MainActivity.kt
@@ -86,6 +86,7 @@ import org.xtimms.shirizu.sections.feed.FeedScreen
import org.xtimms.shirizu.sections.history.HistoryTab
import org.xtimms.shirizu.sections.library.LibraryTab
import org.xtimms.shirizu.sections.onboarding.OnboardingScreen
+import org.xtimms.shirizu.sections.profile.ProfileTab
import org.xtimms.shirizu.sections.search.SearchTab
import org.xtimms.shirizu.sections.settings.SettingsScreen
import org.xtimms.shirizu.sections.shelf.ShelfTab
@@ -301,7 +302,6 @@ class MainActivity : ComponentActivity() {
@OptIn(ExperimentalMaterial3Api::class)
object MainScreen : Screen() {
- private val librarySearchEvent = Channel()
private val openTabEvent = Channel()
private val showBottomNavEvent = Channel()
@@ -312,7 +312,8 @@ object MainScreen : Screen() {
// ShelfTab,
// HistoryTab,
ExploreTab(),
- SearchTab
+ SearchTab,
+ // ProfileTab
)
@Composable
@@ -331,13 +332,6 @@ object MainScreen : Screen() {
actions = {
AppBarActions(
persistentListOf(
- AppBar.Action(
- title = stringResource(R.string.suggestions),
- icon = Icons.Outlined.Creation,
- onClick = {
- navigator.push(SuggestionsScreen)
- },
- ),
AppBar.Action(
title = stringResource(R.string.feed),
icon = Icons.Outlined.RssFeed,
@@ -425,6 +419,7 @@ object MainScreen : Screen() {
// is Tab.History -> HistoryTab
is Tab.Explore -> ExploreTab()
is Tab.Search -> SearchTab
+ // is Tab.Profile -> ProfileTab
}
}
}
@@ -508,5 +503,6 @@ object MainScreen : Screen() {
// data object History : Tab
data object Explore : Tab
data object Search : Tab
+ // data object Profile : Tab
}
}
\ No newline at end of file
diff --git a/app/src/main/java/org/xtimms/shirizu/core/cache/ContentCache.kt b/app/src/main/java/org/xtimms/shirizu/core/cache/ContentCache.kt
deleted file mode 100644
index 21d0a91..0000000
--- a/app/src/main/java/org/xtimms/shirizu/core/cache/ContentCache.kt
+++ /dev/null
@@ -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)
-
- suspend fun getPages(source: MangaSource, url: String): List?
-
- fun putPages(source: MangaSource, url: String, pages: SafeDeferred>)
-
- suspend fun getRelatedManga(source: MangaSource, url: String): List?
-
- fun putRelatedManga(source: MangaSource, url: String, related: SafeDeferred>)
-
- data class Key(
- val source: MangaSource,
- val url: String,
- )
-}
\ No newline at end of file
diff --git a/app/src/main/java/org/xtimms/shirizu/core/cache/ExpiringLruCache.kt b/app/src/main/java/org/xtimms/shirizu/core/cache/ExpiringLruCache.kt
index 8d1f768..92cedbe 100644
--- a/app/src/main/java/org/xtimms/shirizu/core/cache/ExpiringLruCache.kt
+++ b/app/src/main/java/org/xtimms/shirizu/core/cache/ExpiringLruCache.kt
@@ -2,16 +2,19 @@ package org.xtimms.shirizu.core.cache
import androidx.collection.LruCache
import java.util.concurrent.TimeUnit
+import org.xtimms.shirizu.core.cache.MemoryContentCache.Key as CacheKey
class ExpiringLruCache(
val maxSize: Int,
private val lifetime: Long,
private val timeUnit: TimeUnit,
-) {
+) : Iterable {
- private val cache = LruCache>(maxSize)
+ private val cache = LruCache>(maxSize)
- operator fun get(key: ContentCache.Key): T? {
+ override fun iterator(): Iterator = cache.snapshot().keys.iterator()
+
+ operator fun get(key: CacheKey): T? {
val value = cache[key] ?: return null
if (value.isExpired) {
cache.remove(key)
@@ -19,7 +22,7 @@ class ExpiringLruCache(
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))
}
@@ -30,4 +33,8 @@ class ExpiringLruCache(
fun trimToSize(size: Int) {
cache.trimToSize(size)
}
+
+ fun remove(key: CacheKey) {
+ cache.remove(key)
+ }
}
diff --git a/app/src/main/java/org/xtimms/shirizu/core/cache/MemoryContentCache.kt b/app/src/main/java/org/xtimms/shirizu/core/cache/MemoryContentCache.kt
index bec6d8f..211db01 100644
--- a/app/src/main/java/org/xtimms/shirizu/core/cache/MemoryContentCache.kt
+++ b/app/src/main/java/org/xtimms/shirizu/core/cache/MemoryContentCache.kt
@@ -6,42 +6,54 @@ import android.content.res.Configuration
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.model.MangaSource
+import org.xtimms.shirizu.utils.system.isLowRamDevice
import java.util.concurrent.TimeUnit
+import javax.inject.Inject
+import javax.inject.Singleton
-class MemoryContentCache(application: Application) : ContentCache, ComponentCallbacks2 {
+@Singleton
+class MemoryContentCache @Inject constructor(application: Application) : ComponentCallbacks2 {
+
+ private val isLowRam = application.isLowRamDevice()
+
+ private val detailsCache = ExpiringLruCache>(if (isLowRam) 1 else 4, 5, TimeUnit.MINUTES)
+ private val pagesCache =
+ ExpiringLruCache>>(if (isLowRam) 1 else 4, 10, TimeUnit.MINUTES)
+ private val relatedMangaCache =
+ ExpiringLruCache>>(if (isLowRam) 1 else 3, 10, TimeUnit.MINUTES)
init {
application.registerComponentCallbacks(this)
}
- private val detailsCache = ExpiringLruCache>(4, 5, TimeUnit.MINUTES)
- private val pagesCache = ExpiringLruCache>>(4, 10, TimeUnit.MINUTES)
- private val relatedMangaCache = ExpiringLruCache>>(4, 10, TimeUnit.MINUTES)
-
- override val isCachingEnabled: Boolean = true
+ suspend fun getDetails(source: MangaSource, url: String): Manga? {
+ return detailsCache[Key(source, url)]?.awaitOrNull()
+ }
- override suspend fun getDetails(source: MangaSource, url: String): Manga? {
- return detailsCache[ContentCache.Key(source, url)]?.awaitOrNull()
+ fun putDetails(source: MangaSource, url: String, details: SafeDeferred) {
+ detailsCache[Key(source, url)] = details
}
- override fun putDetails(source: MangaSource, url: String, details: SafeDeferred) {
- detailsCache[ContentCache.Key(source, url)] = details
+ suspend fun getPages(source: MangaSource, url: String): List? {
+ return pagesCache[Key(source, url)]?.awaitOrNull()
}
- override suspend fun getPages(source: MangaSource, url: String): List? {
- return pagesCache[ContentCache.Key(source, url)]?.awaitOrNull()
+ fun putPages(source: MangaSource, url: String, pages: SafeDeferred>) {
+ pagesCache[Key(source, url)] = pages
}
- override fun putPages(source: MangaSource, url: String, pages: SafeDeferred>) {
- pagesCache[ContentCache.Key(source, url)] = pages
+ suspend fun getRelatedManga(source: MangaSource, url: String): List? {
+ return relatedMangaCache[Key(source, url)]?.awaitOrNull()
}
- override suspend fun getRelatedManga(source: MangaSource, url: String): List? {
- return relatedMangaCache[ContentCache.Key(source, url)]?.awaitOrNull()
+ fun putRelatedManga(source: MangaSource, url: String, related: SafeDeferred>) {
+ relatedMangaCache[Key(source, url)] = related
}
- override fun putRelatedManga(source: MangaSource, url: String, related: SafeDeferred>) {
- relatedMangaCache[ContentCache.Key(source, url)] = related
+ fun clear(source: MangaSource) {
+ clearCache(detailsCache, source)
+ clearCache(pagesCache, source)
+ clearCache(relatedMangaCache, source)
}
override fun onConfigurationChanged(newConfig: Configuration) = Unit
@@ -67,4 +79,17 @@ class MemoryContentCache(application: Application) : ContentCache, ComponentCall
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,
+ )
}
\ No newline at end of file
diff --git a/app/src/main/java/org/xtimms/shirizu/core/cache/StubContentCache.kt b/app/src/main/java/org/xtimms/shirizu/core/cache/StubContentCache.kt
deleted file mode 100644
index f2ad5da..0000000
--- a/app/src/main/java/org/xtimms/shirizu/core/cache/StubContentCache.kt
+++ /dev/null
@@ -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) = Unit
-
- override suspend fun getPages(source: MangaSource, url: String): List? = null
-
- override fun putPages(source: MangaSource, url: String, pages: SafeDeferred>) = Unit
-
- override suspend fun getRelatedManga(source: MangaSource, url: String): List? = null
-
- override fun putRelatedManga(source: MangaSource, url: String, related: SafeDeferred>) = Unit
-}
\ No newline at end of file
diff --git a/app/src/main/java/org/xtimms/shirizu/core/components/ReadButton.kt b/app/src/main/java/org/xtimms/shirizu/core/components/ReadButton.kt
index ce66056..b02eaa7 100644
--- a/app/src/main/java/org/xtimms/shirizu/core/components/ReadButton.kt
+++ b/app/src/main/java/org/xtimms/shirizu/core/components/ReadButton.kt
@@ -54,7 +54,7 @@ import java.time.Instant
@Composable
fun RowScope.ReadButton(
- info: HistoryInfo,
+ info: HistoryInfo?,
estimatedReadTime: String
) {
@@ -63,13 +63,13 @@ fun RowScope.ReadButton(
val animatedCardContainerColor = animateColorAsState(
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)
).value
val animatedCardContentColor = animateColorAsState(
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)
).value
@@ -105,9 +105,9 @@ fun RowScope.ReadButton(
contentAlignment = Alignment.CenterEnd,
) {
BackgroundProgress(
- if (info.totalChapters == 0) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.primary,
- if (!info.isValid) 0.1f else 0.33f,
- info.history?.percent?.coerceIn(0f, 1f) ?: 0f
+ if (info?.totalChapters == 0) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.primary,
+ if (info?.isValid == true) 0.1f else 0.33f,
+ info?.history?.percent?.coerceIn(0f, 1f) ?: 0f
)
Column(
modifier = Modifier
@@ -143,7 +143,8 @@ fun RowScope.ReadButton(
animationSpec = infiniteRepeatable(tween(15000), RepeatMode.Restart)
)
val chaptersSubtitle = when {
- !info.isValid -> stringResource(R.string.loading_)
+ info == null -> "null"
+ info.isValid -> stringResource(R.string.loading_)
info.currentChapter >= 0 -> when (infiniteTransition) {
1 -> stringResource(
R.string.chapter_d_of_d,
@@ -162,7 +163,7 @@ fun RowScope.ReadButton(
)
}
Text(
- text = if (info.history != null) {
+ text = if (info?.history != null) {
stringResource(R.string.continue_reading)
} else {
stringResource(R.string.read)
diff --git a/app/src/main/java/org/xtimms/shirizu/core/components/SortChip.kt b/app/src/main/java/org/xtimms/shirizu/core/components/SortChip.kt
index a3c4e13..7b60220 100644
--- a/app/src/main/java/org/xtimms/shirizu/core/components/SortChip.kt
+++ b/app/src/main/java/org/xtimms/shirizu/core/components/SortChip.kt
@@ -1,11 +1,12 @@
package org.xtimms.shirizu.core.components
+import androidx.compose.animation.AnimatedContent
import androidx.compose.animation.animateContentSize
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.size
import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.automirrored.filled.Sort
-import androidx.compose.material.icons.filled.ArrowDropDown
+import androidx.compose.material.icons.outlined.ArrowDropDown
+import androidx.compose.material.icons.outlined.ArrowDropUp
import androidx.compose.material3.DropdownMenu
import androidx.compose.material3.FilterChip
import androidx.compose.material3.Icon
@@ -30,28 +31,33 @@ fun SortChip(
Box(modifier) {
var expanded by remember { mutableStateOf(false) }
+ val arrowDrop = if (expanded) Icons.Outlined.ArrowDropUp else Icons.Outlined.ArrowDropDown
+
FilterChip(
selected = true,
onClick = { expanded = true },
label = {
- Text(
- text = currentSortOption.label(LocalContext.current.resources),
- modifier = Modifier.animateContentSize(),
- )
+ AnimatedContent(targetState = currentSortOption.label(LocalContext.current.resources), label = "Text") {
+ Text(text = it)
+ }
},
leadingIcon = {
- Icon(
- imageVector = Icons.AutoMirrored.Filled.Sort,
- contentDescription = null, // decorative
- modifier = Modifier.size(16.dp),
- )
+ AnimatedContent(targetState = currentSortOption.icon(), label = "Icon") {
+ Icon(
+ imageVector = it,
+ contentDescription = null, // decorative
+ modifier = Modifier.size(16.dp),
+ )
+ }
},
trailingIcon = {
- Icon(
- imageVector = Icons.Default.ArrowDropDown,
- contentDescription = null, // decorative
- modifier = Modifier.size(16.dp),
- )
+ AnimatedContent(targetState = arrowDrop, label = "Arrow drop") {
+ Icon(
+ imageVector = it,
+ contentDescription = null, // decorative
+ modifier = Modifier.size(16.dp),
+ )
+ }
},
)
diff --git a/app/src/main/java/org/xtimms/shirizu/core/components/SortMenuPopup.kt b/app/src/main/java/org/xtimms/shirizu/core/components/SortMenuPopup.kt
index d69dcb5..9872c78 100644
--- a/app/src/main/java/org/xtimms/shirizu/core/components/SortMenuPopup.kt
+++ b/app/src/main/java/org/xtimms/shirizu/core/components/SortMenuPopup.kt
@@ -2,10 +2,15 @@ package org.xtimms.shirizu.core.components
import android.content.res.Resources
import androidx.compose.foundation.layout.ColumnScope
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.ArrowOutward
+import androidx.compose.material.icons.outlined.DateRange
+import androidx.compose.material.icons.outlined.SortByAlpha
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontWeight
import org.xtimms.shirizu.R
@@ -35,5 +40,12 @@ internal fun ColumnScope.SortDropdownMenuContent(
internal fun SortOption.label(resources: Resources): String = when (this) {
SortOption.ALPHABETICAL -> resources.getString(R.string.sort_alphabetically)
+ SortOption.PROGRESS -> resources.getString(R.string.progress)
SortOption.DATE_ADDED -> resources.getString(R.string.sort_date_added)
+}
+
+internal fun SortOption.icon(): ImageVector = when (this) {
+ SortOption.ALPHABETICAL -> Icons.Outlined.SortByAlpha
+ SortOption.PROGRESS -> Icons.Outlined.ArrowOutward
+ SortOption.DATE_ADDED -> Icons.Outlined.DateRange
}
\ No newline at end of file
diff --git a/app/src/main/java/org/xtimms/shirizu/core/database/dao/MangaSourcesDao.kt b/app/src/main/java/org/xtimms/shirizu/core/database/dao/MangaSourcesDao.kt
index 09ce76c..9a1d170 100644
--- a/app/src/main/java/org/xtimms/shirizu/core/database/dao/MangaSourcesDao.kt
+++ b/app/src/main/java/org/xtimms/shirizu/core/database/dao/MangaSourcesDao.kt
@@ -11,6 +11,7 @@ import androidx.sqlite.db.SimpleSQLiteQuery
import androidx.sqlite.db.SupportSQLiteQuery
import kotlinx.coroutines.flow.Flow
import org.intellij.lang.annotations.Language
+import org.xtimms.shirizu.BuildConfig
import org.xtimms.shirizu.core.database.entity.MangaSourceEntity
import org.xtimms.shirizu.sections.explore.data.SourcesSortOrder
@@ -65,6 +66,9 @@ abstract class MangaSourcesDao {
source = source,
isEnabled = isEnabled,
sortKey = getMaxSortKey() + 1,
+ addedIn = BuildConfig.VERSION_CODE,
+ lastUsedAt = 0,
+ isPinned = false,
)
upsert(entity)
}
diff --git a/app/src/main/java/org/xtimms/shirizu/core/database/entity/MangaSourceEntity.kt b/app/src/main/java/org/xtimms/shirizu/core/database/entity/MangaSourceEntity.kt
index b2afa5b..586f65f 100644
--- a/app/src/main/java/org/xtimms/shirizu/core/database/entity/MangaSourceEntity.kt
+++ b/app/src/main/java/org/xtimms/shirizu/core/database/entity/MangaSourceEntity.kt
@@ -12,4 +12,7 @@ data class MangaSourceEntity(
val source: String,
@ColumnInfo(name = "enabled") val isEnabled: Boolean,
@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,
)
\ No newline at end of file
diff --git a/app/src/main/java/org/xtimms/shirizu/core/exceptions/UnsupportedSourceException.kt b/app/src/main/java/org/xtimms/shirizu/core/exceptions/UnsupportedSourceException.kt
new file mode 100644
index 0000000..abb2e76
--- /dev/null
+++ b/app/src/main/java/org/xtimms/shirizu/core/exceptions/UnsupportedSourceException.kt
@@ -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)
\ No newline at end of file
diff --git a/app/src/main/java/org/xtimms/shirizu/core/model/Manga.kt b/app/src/main/java/org/xtimms/shirizu/core/model/Manga.kt
index 66e6000..d0fb8a4 100644
--- a/app/src/main/java/org/xtimms/shirizu/core/model/Manga.kt
+++ b/app/src/main/java/org/xtimms/shirizu/core/model/Manga.kt
@@ -3,6 +3,7 @@ package org.xtimms.shirizu.core.model
import androidx.core.os.LocaleListCompat
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaChapter
+import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.xtimms.shirizu.utils.system.iterator
import java.text.DecimalFormat
@@ -61,4 +62,4 @@ fun MangaChapter.formatNumber(): String? {
}
val Manga.isLocal: Boolean
- get() = source == MangaSource.LOCAL
\ No newline at end of file
+ get() = source == LocalMangaSource
\ No newline at end of file
diff --git a/app/src/main/java/org/xtimms/shirizu/core/model/MangaSource.kt b/app/src/main/java/org/xtimms/shirizu/core/model/MangaSource.kt
index 228bda3..9079b81 100644
--- a/app/src/main/java/org/xtimms/shirizu/core/model/MangaSource.kt
+++ b/app/src/main/java/org/xtimms/shirizu/core/model/MangaSource.kt
@@ -1,13 +1,72 @@
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.MangaParserSource
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 {
- MangaSource.entries.forEach {
+data object LocalMangaSource : MangaSource {
+ 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
}
- return MangaSource.DUMMY
+ return UnknownMangaSource
+}
+
+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.isNsfw() = contentType == ContentType.HENTAI
\ No newline at end of file
+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
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/xtimms/shirizu/core/model/MangaSourceInfo.kt b/app/src/main/java/org/xtimms/shirizu/core/model/MangaSourceInfo.kt
new file mode 100644
index 0000000..5814b1d
--- /dev/null
+++ b/app/src/main/java/org/xtimms/shirizu/core/model/MangaSourceInfo.kt
@@ -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
\ No newline at end of file
diff --git a/app/src/main/java/org/xtimms/shirizu/core/model/parcelable/MangaSourceParceler.kt b/app/src/main/java/org/xtimms/shirizu/core/model/parcelable/MangaSourceParceler.kt
new file mode 100644
index 0000000..6cc3eea
--- /dev/null
+++ b/app/src/main/java/org/xtimms/shirizu/core/model/parcelable/MangaSourceParceler.kt
@@ -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 {
+
+ override fun create(parcel: Parcel): MangaSource = MangaSource(parcel.readString())
+
+ override fun MangaSource.write(parcel: Parcel, flags: Int) {
+ parcel.writeString(name)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/xtimms/shirizu/core/model/parcelable/ParcelableMangaTags.kt b/app/src/main/java/org/xtimms/shirizu/core/model/parcelable/ParcelableMangaTags.kt
index 70ec851..c737ae5 100644
--- a/app/src/main/java/org/xtimms/shirizu/core/model/parcelable/ParcelableMangaTags.kt
+++ b/app/src/main/java/org/xtimms/shirizu/core/model/parcelable/ParcelableMangaTags.kt
@@ -18,7 +18,7 @@ object MangaTagParceler : Parceler {
override fun MangaTag.write(parcel: Parcel, flags: Int) {
parcel.writeString(title)
parcel.writeString(key)
- parcel.writeSerializable(source)
+ parcel.writeString(source.name)
}
}
diff --git a/app/src/main/java/org/xtimms/shirizu/core/model/parcelable/ParcerableManga.kt b/app/src/main/java/org/xtimms/shirizu/core/model/parcelable/ParcerableManga.kt
index c5ff57c..d617a31 100644
--- a/app/src/main/java/org/xtimms/shirizu/core/model/parcelable/ParcerableManga.kt
+++ b/app/src/main/java/org/xtimms/shirizu/core/model/parcelable/ParcerableManga.kt
@@ -30,7 +30,7 @@ data class ParcelableManga(
parcel.writeParcelable(ParcelableMangaTags(tags), flags)
parcel.writeSerializable(state)
parcel.writeString(author)
- parcel.writeSerializable(source)
+ parcel.writeString(source.name)
}
override fun create(parcel: Parcel) = ParcelableManga(
diff --git a/app/src/main/java/org/xtimms/shirizu/core/network/interceptors/CommonHeadersInterceptor.kt b/app/src/main/java/org/xtimms/shirizu/core/network/interceptors/CommonHeadersInterceptor.kt
index 8afb7de..115950a 100644
--- a/app/src/main/java/org/xtimms/shirizu/core/network/interceptors/CommonHeadersInterceptor.kt
+++ b/app/src/main/java/org/xtimms/shirizu/core/network/interceptors/CommonHeadersInterceptor.kt
@@ -12,7 +12,7 @@ import org.koitharu.kotatsu.parsers.util.mergeWith
import org.xtimms.shirizu.BuildConfig
import org.xtimms.shirizu.core.network.CommonHeaders
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 javax.inject.Inject
import javax.inject.Singleton
@@ -26,7 +26,7 @@ class CommonHeadersInterceptor @Inject constructor(
val request = chain.request()
val source = request.tag(MangaSource::class.java)
val repository = if (source != null) {
- mangaRepositoryFactoryLazy.get().create(source) as? RemoteMangaRepository
+ mangaRepositoryFactoryLazy.get().create(source) as? ParserMangaRepository
} else {
if (BuildConfig.DEBUG) {
Log.w("Http", "Request without source tag: ${request.url}")
diff --git a/app/src/main/java/org/xtimms/shirizu/core/parser/BitmapWrapper.kt b/app/src/main/java/org/xtimms/shirizu/core/parser/BitmapWrapper.kt
new file mode 100644
index 0000000..435530a
--- /dev/null
+++ b/app/src/main/java/org/xtimms/shirizu/core/parser/BitmapWrapper.kt
@@ -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)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/xtimms/shirizu/core/parser/CachingMangaRepository.kt b/app/src/main/java/org/xtimms/shirizu/core/parser/CachingMangaRepository.kt
new file mode 100644
index 0000000..c25c119
--- /dev/null
+++ b/app/src/main/java/org/xtimms/shirizu/core/parser/CachingMangaRepository.kt
@@ -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()
+ private val relatedMangaMutex = MultiMutex()
+ private val pagesMutex = MultiMutex()
+
+ final override suspend fun getDetails(manga: Manga): Manga = getDetails(manga, CachePolicy.ENABLED)
+
+ final override suspend fun getPages(chapter: MangaChapter): List = 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 = 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
+
+ protected abstract suspend fun getPagesImpl(chapter: MangaChapter): List
+
+ @OptIn(ExperimentalStdlibApi::class)
+ private suspend fun asyncSafe(block: suspend CoroutineScope.() -> T): SafeDeferred {
+ var dispatcher = currentCoroutineContext()[CoroutineDispatcher.Key]
+ if (dispatcher == null || dispatcher is MainCoroutineDispatcher) {
+ dispatcher = Dispatchers.Default
+ }
+ return SafeDeferred(
+ processLifecycleScope.async(dispatcher) {
+ runCatchingCancellable { block() }
+ },
+ )
+ }
+
+ private fun List.distinctById(): List {
+ if (isEmpty()) {
+ return emptyList()
+ }
+ val result = ArrayList(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
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/xtimms/shirizu/core/parser/EmptyMangaRepository.kt b/app/src/main/java/org/xtimms/shirizu/core/parser/EmptyMangaRepository.kt
new file mode 100644
index 0000000..0f27f83
--- /dev/null
+++ b/app/src/main/java/org/xtimms/shirizu/core/parser/EmptyMangaRepository.kt
@@ -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
+ get() = EnumSet.allOf(SortOrder::class.java)
+ override val states: Set
+ get() = emptySet()
+ override val contentRatings: Set
+ 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 = stub(null)
+
+ override suspend fun getDetails(manga: Manga): Manga = stub(manga)
+
+ override suspend fun getPages(chapter: MangaChapter): List = stub(null)
+
+ override suspend fun getPageUrl(page: MangaPage): String = stub(null)
+
+ override suspend fun getTags(): Set = stub(null)
+
+ override suspend fun getLocales(): Set = stub(null)
+
+ override suspend fun getRelated(seed: Manga): List = stub(seed)
+
+ private fun stub(manga: Manga?): Nothing {
+ throw UnsupportedSourceException("This manga source is not supported", manga)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/xtimms/shirizu/core/parser/MangaDataRepository.kt b/app/src/main/java/org/xtimms/shirizu/core/parser/MangaDataRepository.kt
index b0b80d8..cfa029a 100644
--- a/app/src/main/java/org/xtimms/shirizu/core/parser/MangaDataRepository.kt
+++ b/app/src/main/java/org/xtimms/shirizu/core/parser/MangaDataRepository.kt
@@ -13,6 +13,7 @@ import javax.inject.Provider
@Reusable
class MangaDataRepository @Inject constructor(
private val db: ShirizuDatabase,
+ private val resolverProvider: Provider,
) {
suspend fun findMangaById(mangaId: Long): Manga? {
@@ -23,6 +24,13 @@ class MangaDataRepository @Inject constructor(
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) {
db.withTransaction {
val tags = manga.tags.toEntities()
diff --git a/app/src/main/java/org/xtimms/shirizu/core/parser/MangaLinkResolver.kt b/app/src/main/java/org/xtimms/shirizu/core/parser/MangaLinkResolver.kt
new file mode 100644
index 0000000..e909c55
--- /dev/null
+++ b/app/src/main/java/org/xtimms/shirizu/core/parser/MangaLinkResolver.kt
@@ -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,
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/xtimms/shirizu/core/parser/MangaLoaderContextImpl.kt b/app/src/main/java/org/xtimms/shirizu/core/parser/MangaLoaderContextImpl.kt
index 21022ec..2a55324 100644
--- a/app/src/main/java/org/xtimms/shirizu/core/parser/MangaLoaderContextImpl.kt
+++ b/app/src/main/java/org/xtimms/shirizu/core/parser/MangaLoaderContextImpl.kt
@@ -2,6 +2,7 @@ package org.xtimms.shirizu.core.parser
import android.annotation.SuppressLint
import android.content.Context
+import android.graphics.BitmapFactory
import android.util.Base64
import android.webkit.WebView
import androidx.annotation.MainThread
@@ -10,8 +11,13 @@ import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
+import okhttp3.MediaType.Companion.toMediaType
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.bitmap.Bitmap
import org.koitharu.kotatsu.parsers.config.MangaSourceConfig
import org.koitharu.kotatsu.parsers.model.MangaSource
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.prefs.SourceSettings
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.toList
import java.lang.ref.WeakReference
@@ -72,6 +79,30 @@ class MangaLoaderContextImpl @Inject constructor(
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
private fun obtainWebView(): WebView {
return webViewCached?.get() ?: WebView(androidContext).also {
diff --git a/app/src/main/java/org/xtimms/shirizu/core/parser/MangaParser.kt b/app/src/main/java/org/xtimms/shirizu/core/parser/MangaParser.kt
index 810e254..e5658da 100644
--- a/app/src/main/java/org/xtimms/shirizu/core/parser/MangaParser.kt
+++ b/app/src/main/java/org/xtimms/shirizu/core/parser/MangaParser.kt
@@ -2,8 +2,8 @@ package org.xtimms.shirizu.core.parser
import org.koitharu.kotatsu.parsers.MangaLoaderContext
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)
}
\ No newline at end of file
diff --git a/app/src/main/java/org/xtimms/shirizu/core/parser/MangaRepository.kt b/app/src/main/java/org/xtimms/shirizu/core/parser/MangaRepository.kt
index cfa92d4..510f063 100644
--- a/app/src/main/java/org/xtimms/shirizu/core/parser/MangaRepository.kt
+++ b/app/src/main/java/org/xtimms/shirizu/core/parser/MangaRepository.kt
@@ -1,21 +1,28 @@
package org.xtimms.shirizu.core.parser
+import android.content.Context
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.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.MangaParserSource
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.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 java.lang.ref.WeakReference
-import java.util.EnumMap
import java.util.Locale
import javax.inject.Inject
import javax.inject.Singleton
@@ -30,6 +37,8 @@ interface MangaRepository {
val contentRatings: Set
+ var defaultSortOrder: SortOrder
+
val isMultipleTagsSupported: Boolean
val isTagsExclusionSupported: Boolean
@@ -50,30 +59,58 @@ interface MangaRepository {
suspend fun getRelated(seed: Manga): List
+ suspend fun find(manga: Manga): Manga? {
+ val list = getList(0, MangaListFilter.Search(manga.title))
+ return list.find { x -> x.id == manga.id }
+ }
+
@Singleton
class Factory @Inject constructor(
+ @ApplicationContext private val context: Context,
private val localMangaRepository: LocalMangaRepository,
private val loaderContext: MangaLoaderContext,
- private val contentCache: ContentCache,
+ private val contentCache: MemoryContentCache,
) {
- private val cache = EnumMap>(MangaSource::class.java)
+ private val cache = ArrayMap>()
@AnyThread
fun create(source: MangaSource): MangaRepository {
- if (source == MangaSource.LOCAL) {
- return localMangaRepository
+ when (source) {
+ is MangaSourceInfo -> return create(source.mangaSource)
+ LocalMangaSource -> return localMangaRepository
+ UnknownMangaSource -> return EmptyMangaRepository(source)
}
cache[source]?.get()?.let { return it }
return synchronized(cache) {
cache[source]?.get()?.let { return it }
- val repository = RemoteMangaRepository(
- parser = MangaParser(source, loaderContext),
+ 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),
+ cache = contentCache,
+ )
+
+ is ExternalMangaSource -> if (source.isAvailable(context)) {
+ ExternalMangaRepository(
+ contentResolver = context.contentResolver,
+ source = source,
cache = contentCache,
)
- cache[source] = WeakReference(repository)
- repository
+ } else {
+ EmptyMangaRepository(source)
}
+
+ else -> null
}
}
}
\ No newline at end of file
diff --git a/app/src/main/java/org/xtimms/shirizu/core/parser/RemoteMangaRepository.kt b/app/src/main/java/org/xtimms/shirizu/core/parser/ParserMangaRepository.kt
similarity index 55%
rename from app/src/main/java/org/xtimms/shirizu/core/parser/RemoteMangaRepository.kt
rename to app/src/main/java/org/xtimms/shirizu/core/parser/ParserMangaRepository.kt
index c4a476e..d57dcce 100644
--- a/app/src/main/java/org/xtimms/shirizu/core/parser/RemoteMangaRepository.kt
+++ b/app/src/main/java/org/xtimms/shirizu/core/parser/ParserMangaRepository.kt
@@ -1,7 +1,6 @@
package org.xtimms.shirizu.core.parser
import android.util.Log
-import coil.request.CachePolicy
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
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.runCatchingCancellable
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.prefs.SourceSettings
import org.xtimms.shirizu.utils.lang.processLifecycleScope
import java.util.Locale
@OptIn(InternalParsersApi::class)
-class RemoteMangaRepository(
+class ParserMangaRepository(
private val parser: MangaParser,
- private val cache: ContentCache,
-) : MangaRepository, Interceptor {
+ private val cache: MemoryContentCache,
+) : CachingMangaRepository(cache), Interceptor {
override val source: MangaSource
get() = parser.source
@@ -51,6 +50,12 @@ class RemoteMangaRepository(
override val contentRatings: Set
get() = parser.availableContentRating
+ override var defaultSortOrder: SortOrder
+ get() = getConfig().defaultSortOrder ?: sortOrders.first()
+ set(value) {
+ getConfig().defaultSortOrder = value
+ }
+
override val isMultipleTagsSupported: Boolean
get() = parser.isMultipleTagsSupported
@@ -70,7 +75,7 @@ class RemoteMangaRepository(
get() = parser.configKeyDomain.presetValues
val headers: Headers
- get() = parser.headers
+ get() = parser.getRequestHeaders()
override fun intercept(chain: Interceptor.Chain): Response {
return if (parser is Interceptor) {
@@ -84,16 +89,9 @@ class RemoteMangaRepository(
return parser.getList(offset, filter)
}
- override suspend fun getDetails(manga: Manga): Manga = getDetails(manga, CachePolicy.ENABLED)
-
- override suspend fun getPages(chapter: MangaChapter): List {
- 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 getPagesImpl(
+ chapter: MangaChapter
+ ): List = parser.getPages(chapter)
override suspend fun getPageUrl(page: MangaPage): String = parser.getPageUrl(page)
@@ -105,59 +103,19 @@ class RemoteMangaRepository(
suspend fun getFavicons(): Favicons = parser.getFavicons()
- override suspend fun getRelated(seed: Manga): List {
- cache.getRelatedManga(source, seed.url)?.let { return it }
- val related = asyncSafe {
- parser.getRelatedManga(seed).filterNot { it.id == seed.id }
- }
- cache.putRelatedManga(source, seed.url, related)
- return related.await()
- }
+ override suspend fun getRelatedMangaImpl(seed: Manga): List = parser.getRelatedManga(seed)
- suspend fun getDetails(manga: Manga, cachePolicy: CachePolicy): Manga {
- if (cachePolicy.readEnabled) {
- 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()
- }
+ override suspend fun getDetailsImpl(manga: Manga): Manga = parser.getDetails(manga)
- private fun getConfig() = parser.config as SourceSettings
-
- @OptIn(ExperimentalStdlibApi::class)
- private suspend fun asyncSafe(block: suspend CoroutineScope.() -> T): SafeDeferred {
- var dispatcher = currentCoroutineContext()[CoroutineDispatcher.Key]
- if (dispatcher == null || dispatcher is MainCoroutineDispatcher) {
- dispatcher = Dispatchers.Default
- }
- return SafeDeferred(
- processLifecycleScope.async(dispatcher) {
- runCatchingCancellable { block() }
- },
- )
+ fun getAvailableMirrors(): List {
+ return parser.configKeyDomain.presetValues.toList()
}
- private fun List.distinctById(): List {
- if (isEmpty()) {
- return emptyList()
- }
- val result = ArrayList(size)
- val set = HashSet(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
+ fun isSlowdownEnabled(): Boolean {
+ return getConfig().isSlowdownEnabled
}
- private fun Result<*>.isValidResult() = exceptionOrNull() !is ParseException
- && (getOrNull() as? Collection<*>)?.isEmpty() != true
+ private fun getConfig() = parser.config as SourceSettings
+
+ private fun Result<*>.isValidResult() = isSuccess && (getOrNull() as? Collection<*>)?.isEmpty() != true
}
\ No newline at end of file
diff --git a/app/src/main/java/org/xtimms/shirizu/core/parser/external/ExternalMangaRepository.kt b/app/src/main/java/org/xtimms/shirizu/core/parser/external/ExternalMangaRepository.kt
new file mode 100644
index 0000000..1acf826
--- /dev/null
+++ b/app/src/main/java/org/xtimms/shirizu/core/parser/external/ExternalMangaRepository.kt
@@ -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
+ get() = capabilities?.availableSortOrders ?: EnumSet.of(SortOrder.ALPHABETICAL)
+ override val states: Set
+ get() = capabilities?.availableStates.orEmpty()
+ override val contentRatings: Set
+ 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 =
+ 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(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 = 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(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 = runInterruptible(Dispatchers.Default) {
+ val uri = "content://${source.authority}/tags".toUri()
+ contentResolver.query(uri, null, null, null, null)?.use { cursor ->
+ val result = ArraySet(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 = emptySet()
+
+ override suspend fun getRelatedMangaImpl(seed: Manga): List = 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? = 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(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,
+ val availableStates: Set,
+ val availableContentRating: Set,
+ val isMultipleTagsSupported: Boolean,
+ val isTagsExclusionSupported: Boolean,
+ val isSearchSupported: Boolean,
+ val contentType: ContentType,
+ val defaultSortOrder: SortOrder,
+ val sourceLocale: Locale,
+ )
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/xtimms/shirizu/core/parser/external/ExternalMangaSource.kt b/app/src/main/java/org/xtimms/shirizu/core/parser/external/ExternalMangaSource.kt
new file mode 100644
index 0000000..4c266cc
--- /dev/null
+++ b/app/src/main/java/org/xtimms/shirizu/core/parser/external/ExternalMangaSource.kt
@@ -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
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/xtimms/shirizu/core/parser/favicon/FaviconFetcher.kt b/app/src/main/java/org/xtimms/shirizu/core/parser/favicon/FaviconFetcher.kt
index e9d8193..4de0721 100644
--- a/app/src/main/java/org/xtimms/shirizu/core/parser/favicon/FaviconFetcher.kt
+++ b/app/src/main/java/org/xtimms/shirizu/core/parser/favicon/FaviconFetcher.kt
@@ -1,13 +1,20 @@
package org.xtimms.shirizu.core.parser.favicon
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.os.Build
import android.webkit.MimeTypeMap
import coil.ImageLoader
import coil.annotation.ExperimentalCoilApi
import coil.decode.DataSource
import coil.decode.ImageSource
import coil.disk.DiskCache
+import coil.fetch.DrawableResult
import coil.fetch.FetchResult
import coil.fetch.Fetcher
import coil.fetch.SourceResult
@@ -15,7 +22,9 @@ import coil.network.HttpException
import coil.request.Options
import coil.size.Size
import coil.size.pxOrElse
+import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ensureActive
+import kotlinx.coroutines.runInterruptible
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
@@ -27,8 +36,10 @@ import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.await
import org.xtimms.shirizu.core.cache.CacheDir
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.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.withExtraCloseable
import java.net.HttpURLConnection
@@ -46,14 +57,27 @@ class FaviconFetcher(
) : Fetcher {
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
get() = checkNotNull(diskCache.value).fileSystem
override suspend fun fetch(): FetchResult {
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(
options.size.width.pxOrElse { FALLBACK_SIZE },
options.size.height.pxOrElse { FALLBACK_SIZE },
@@ -83,6 +107,20 @@ class FaviconFetcher(
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 {
val request = Request.Builder()
.url(url)
@@ -167,12 +205,20 @@ class FaviconFetcher(
}
}
+ private fun Drawable.nonAdaptive() =
+ if (this is AdaptiveIconDrawable) {
+ LayerDrawable(arrayOf(background, foreground))
+ } else {
+ this
+ }
+
class Factory(
context: Context,
- private val okHttpClient: OkHttpClient,
+ okHttpClientLazy: Lazy,
private val mangaRepositoryFactory: MangaRepository.Factory,
) : Fetcher.Factory {
+ private val okHttpClient by okHttpClientLazy
private val diskCache = lazy {
val rootDir = context.externalCacheDir ?: context.cacheDir
DiskCache.Builder()
diff --git a/app/src/main/java/org/xtimms/shirizu/core/parser/local/LocalMangaRepository.kt b/app/src/main/java/org/xtimms/shirizu/core/parser/local/LocalMangaRepository.kt
index 25feaa4..15a806b 100644
--- a/app/src/main/java/org/xtimms/shirizu/core/parser/local/LocalMangaRepository.kt
+++ b/app/src/main/java/org/xtimms/shirizu/core/parser/local/LocalMangaRepository.kt
@@ -23,6 +23,7 @@ import org.koitharu.kotatsu.parsers.model.MangaTag
import org.koitharu.kotatsu.parsers.model.SortOrder
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
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.parser.MangaRepository
import org.xtimms.shirizu.core.parser.local.input.LocalMangaInput
@@ -49,7 +50,7 @@ class LocalMangaRepository @Inject constructor(
@LocalStorageChanges private val localStorageChanges: MutableSharedFlow,
) : MangaRepository {
- override val source = MangaSource.LOCAL
+ override val source = LocalMangaSource
private val locks = MultiMutex()
private val localMappingCache = LocalMangaMappingCache()
@@ -60,6 +61,10 @@ class LocalMangaRepository @Inject constructor(
override val states = emptySet()
override val contentRatings = emptySet()
+ override var defaultSortOrder: SortOrder
+ get() = SortOrder.NEWEST // TODO
+ set(value) {}
+
override suspend fun getList(offset: Int, filter: MangaListFilter?): List {
if (offset > 0) {
return emptyList()
@@ -94,7 +99,7 @@ class LocalMangaRepository @Inject constructor(
}
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"
}
diff --git a/app/src/main/java/org/xtimms/shirizu/core/parser/local/MangaIndex.kt b/app/src/main/java/org/xtimms/shirizu/core/parser/local/MangaIndex.kt
index bed58d9..6baa6bb 100644
--- a/app/src/main/java/org/xtimms/shirizu/core/parser/local/MangaIndex.kt
+++ b/app/src/main/java/org/xtimms/shirizu/core/parser/local/MangaIndex.kt
@@ -5,6 +5,7 @@ import org.json.JSONArray
import org.json.JSONObject
import org.koitharu.kotatsu.parsers.model.Manga
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.MangaState
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 {
- val source = MangaSource.valueOf(json.getString("source"))
+ val source = MangaParserSource.valueOf(json.getString("source"))
Manga(
id = json.getLong("id"),
title = json.getString("title"),
diff --git a/app/src/main/java/org/xtimms/shirizu/core/parser/local/input/LocalMangaDirInput.kt b/app/src/main/java/org/xtimms/shirizu/core/parser/local/input/LocalMangaDirInput.kt
index c5c1161..cb5643a 100644
--- a/app/src/main/java/org/xtimms/shirizu/core/parser/local/input/LocalMangaDirInput.kt
+++ b/app/src/main/java/org/xtimms/shirizu/core/parser/local/input/LocalMangaDirInput.kt
@@ -10,6 +10,7 @@ import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.toCamelCase
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.hasCbzExtension
import org.xtimms.shirizu.core.parser.local.output.LocalMangaOutput
@@ -47,7 +48,7 @@ class LocalMangaDirInput(root: File) : LocalMangaInput(root) {
index?.getCoverEntry() ?: findFirstImageEntry().orEmpty(),
)
val manga = info?.copy2(
- source = MangaSource.LOCAL,
+ source = LocalMangaSource,
url = mangaUri,
coverUrl = cover,
largeCoverUrl = cover,
@@ -59,14 +60,14 @@ class LocalMangaDirInput(root: File) : LocalMangaInput(root) {
// old downloads
chapterFiles.values.elementAtOrNull(i)
} ?: return@mapIndexedNotNull null
- c.copy(url = file.toUri().toString(), source = MangaSource.LOCAL)
+ c.copy(url = file.toUri().toString(), source = LocalMangaSource)
},
) ?: Manga(
id = root.absolutePath.longHashCode(),
title = root.name.toHumanReadable(),
url = mangaUri,
publicUrl = mangaUri,
- source = MangaSource.LOCAL,
+ source = LocalMangaSource,
coverUrl = findFirstImageEntry().orEmpty(),
chapters = chapterFiles.values.mapIndexed { i, f ->
MangaChapter(
@@ -74,7 +75,7 @@ class LocalMangaDirInput(root: File) : LocalMangaInput(root) {
name = f.nameWithoutExtension.toHumanReadable(),
number = 0f,
volume = 0,
- source = MangaSource.LOCAL,
+ source = LocalMangaSource,
uploadDate = f.creationTime,
url = f.toUri().toString(),
scanlator = null,
@@ -106,7 +107,7 @@ class LocalMangaDirInput(root: File) : LocalMangaInput(root) {
.toListSorted(compareBy(AlphanumComparator()) { x -> x.name })
.map {
val pageUri = it.toUri().toString()
- MangaPage(pageUri.longHashCode(), pageUri, null, MangaSource.LOCAL)
+ MangaPage(pageUri.longHashCode(), pageUri, null, LocalMangaSource)
}
} else {
ZipFile(file).use { zip ->
@@ -121,7 +122,7 @@ class LocalMangaDirInput(root: File) : LocalMangaInput(root) {
id = pageUri.longHashCode(),
url = pageUri,
preview = null,
- source = MangaSource.LOCAL,
+ source = LocalMangaSource,
)
}
}
diff --git a/app/src/main/java/org/xtimms/shirizu/core/parser/local/input/LocalMangaZipInput.kt b/app/src/main/java/org/xtimms/shirizu/core/parser/local/input/LocalMangaZipInput.kt
index c00f031..25bf60c 100644
--- a/app/src/main/java/org/xtimms/shirizu/core/parser/local/input/LocalMangaZipInput.kt
+++ b/app/src/main/java/org/xtimms/shirizu/core/parser/local/input/LocalMangaZipInput.kt
@@ -13,6 +13,7 @@ import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.util.toCamelCase
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.output.LocalMangaOutput
import org.xtimms.shirizu.utils.AlphanumComparator
@@ -47,12 +48,12 @@ class LocalMangaZipInput(root: File) : LocalMangaInput(root) {
entryName = index.getCoverEntry() ?: findFirstImageEntry(zip.entries())?.name.orEmpty(),
)
return@use info.copy2(
- source = MangaSource.LOCAL,
+ source = LocalMangaSource,
url = fileUri,
coverUrl = cover,
largeCoverUrl = cover,
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,
url = fileUri,
publicUrl = fileUri,
- source = MangaSource.LOCAL,
+ source = LocalMangaSource,
coverUrl = zipUri(root, findFirstImageEntry(zip.entries())?.name.orEmpty()),
chapters = chapters.sortedWith(AlphanumComparator())
.mapIndexed { i, s ->
@@ -79,7 +80,7 @@ class LocalMangaZipInput(root: File) : LocalMangaInput(root) {
name = s.ifEmpty { title },
number = 0f,
volume = 0,
- source = MangaSource.LOCAL,
+ source = LocalMangaSource,
uploadDate = 0L,
url = uriBuilder.fragment(s).build().toString(),
scanlator = null,
@@ -135,7 +136,7 @@ class LocalMangaZipInput(root: File) : LocalMangaInput(root) {
id = entryUri.longHashCode(),
url = entryUri,
preview = null,
- source = MangaSource.LOCAL,
+ source = LocalMangaSource,
)
}
}
diff --git a/app/src/main/java/org/xtimms/shirizu/core/parser/local/output/LocalMangaUtil.kt b/app/src/main/java/org/xtimms/shirizu/core/parser/local/output/LocalMangaUtil.kt
index de3e224..959ef21 100644
--- a/app/src/main/java/org/xtimms/shirizu/core/parser/local/output/LocalMangaUtil.kt
+++ b/app/src/main/java/org/xtimms/shirizu/core/parser/local/output/LocalMangaUtil.kt
@@ -6,13 +6,14 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runInterruptible
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaSource
+import org.xtimms.shirizu.core.model.LocalMangaSource
class LocalMangaUtil(
private val manga: Manga,
) {
init {
- require(manga.source == MangaSource.LOCAL) {
+ require(manga.source == LocalMangaSource) {
"Expected LOCAL source but ${manga.source} found"
}
}
diff --git a/app/src/main/java/org/xtimms/shirizu/core/prefs/AppSettings.kt b/app/src/main/java/org/xtimms/shirizu/core/prefs/AppSettings.kt
index 27905b8..b125551 100644
--- a/app/src/main/java/org/xtimms/shirizu/core/prefs/AppSettings.kt
+++ b/app/src/main/java/org/xtimms/shirizu/core/prefs/AppSettings.kt
@@ -17,6 +17,7 @@ import org.xtimms.shirizu.App
import org.xtimms.shirizu.ui.theme.SEED
import org.xtimms.shirizu.R
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.utils.lang.processLifecycleScope
import org.xtimms.shirizu.utils.system.LocaleLanguageCodeMap
@@ -36,6 +37,8 @@ const val CONFIGURE = "configure"
const val NOTIFICATION = "notification"
const val READING_TIME = "reading_time"
const val GRID_COLUMNS = "grid_columns"
+const val SORT_OPTION = "sort_option"
+
const val DOH = "doh"
const val SYSTEM_DEFAULT = 0
diff --git a/app/src/main/java/org/xtimms/shirizu/core/prefs/SourceSettings.kt b/app/src/main/java/org/xtimms/shirizu/core/prefs/SourceSettings.kt
index 29826fd..b6f881b 100644
--- a/app/src/main/java/org/xtimms/shirizu/core/prefs/SourceSettings.kt
+++ b/app/src/main/java/org/xtimms/shirizu/core/prefs/SourceSettings.kt
@@ -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.ShowSuspiciousContent -> 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
}
@@ -40,6 +41,7 @@ class SourceSettings(context: Context, source: MangaSource) : MangaSourceConfig
is ConfigKey.ShowSuspiciousContent -> putBoolean(key.key, value as Boolean)
is ConfigKey.UserAgent -> putString(key.key, value as String?)
is ConfigKey.SplitByTranslations -> putBoolean(key.key, value as Boolean)
+ is ConfigKey.PreferredImageServer -> putString(key.key, value as String? ?: "")
}
}
}
\ No newline at end of file
diff --git a/app/src/main/java/org/xtimms/shirizu/core/tracker/Tracker.kt b/app/src/main/java/org/xtimms/shirizu/core/tracker/Tracker.kt
index a9cb571..c163d35 100644
--- a/app/src/main/java/org/xtimms/shirizu/core/tracker/Tracker.kt
+++ b/app/src/main/java/org/xtimms/shirizu/core/tracker/Tracker.kt
@@ -6,7 +6,7 @@ import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import org.xtimms.shirizu.core.model.getPreferredBranch
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.MangaUpdates
import org.xtimms.shirizu.data.repository.HistoryRepository
@@ -51,7 +51,7 @@ class Tracker @Inject constructor(
): MangaUpdates = withMangaLock(track.manga.id) {
val updates = runCatchingCancellable {
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)
compare(track, manga, getBranch(manga))
}.getOrElse { error ->
diff --git a/app/src/main/java/org/xtimms/shirizu/data/repository/MangaSearchRepository.kt b/app/src/main/java/org/xtimms/shirizu/data/repository/MangaSearchRepository.kt
index a3be5f7..4af8761 100644
--- a/app/src/main/java/org/xtimms/shirizu/data/repository/MangaSearchRepository.kt
+++ b/app/src/main/java/org/xtimms/shirizu/data/repository/MangaSearchRepository.kt
@@ -9,7 +9,6 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.currentCoroutineContext
import kotlinx.coroutines.isActive
import kotlinx.coroutines.withContext
-import org.koitharu.kotatsu.parsers.model.ContentType
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaTag
@@ -123,22 +122,6 @@ class MangaSearchRepository @Inject constructor(
return db.getTagsDao().findRareTags(source.name, limit).toMangaTagsList()
}
- fun getSourcesSuggestion(query: String, limit: Int): List {
- 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) {
recentSuggestions.saveRecentQuery(query, null)
}
diff --git a/app/src/main/java/org/xtimms/shirizu/data/repository/MangaSourcesRepository.kt b/app/src/main/java/org/xtimms/shirizu/data/repository/MangaSourcesRepository.kt
index 3054fd7..5157125 100644
--- a/app/src/main/java/org/xtimms/shirizu/data/repository/MangaSourcesRepository.kt
+++ b/app/src/main/java/org/xtimms/shirizu/data/repository/MangaSourcesRepository.kt
@@ -1,23 +1,38 @@
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.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.channels.trySendBlocking
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onStart
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.util.mapToSet
+import org.xtimms.shirizu.BuildConfig
import org.xtimms.shirizu.core.database.ShirizuDatabase
import org.xtimms.shirizu.core.database.dao.MangaSourcesDao
import org.xtimms.shirizu.core.database.entity.MangaSourceEntity
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.parser.external.ExternalMangaSource
import org.xtimms.shirizu.core.prefs.AppSettings
import org.xtimms.shirizu.core.prefs.KotatsuAppSettings
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 java.util.Collections
import java.util.EnumSet
+import java.util.concurrent.atomic.AtomicBoolean
import javax.inject.Inject
+import javax.inject.Singleton
-@OptIn(ExperimentalCoroutinesApi::class)
-@Reusable
+@Singleton
class MangaSourcesRepository @Inject constructor(
+ @ApplicationContext private val context: Context,
private val db: ShirizuDatabase,
private val settings: KotatsuAppSettings,
) {
+ private val isNewSourcesAssimilated = AtomicBoolean(false)
private val dao: MangaSourcesDao
get() = db.getSourcesDao()
- private val remoteSources = EnumSet.allOf(MangaSource::class.java).apply {
- remove(MangaSource.LOCAL)
- remove(MangaSource.DUMMY)
+ private val remoteSources = EnumSet.allOf(MangaParserSource::class.java).apply {
+ if (!BuildConfig.DEBUG) {
+ remove(MangaParserSource.DUMMY)
+ }
}
- val allMangaSources: Set
+ val allMangaSources: Set
get() = Collections.unmodifiableSet(remoteSources)
suspend fun getEnabledSources(): List {
@@ -54,7 +73,8 @@ class MangaSourcesRepository @Inject constructor(
return dao.findAllDisabled().toSources(settings.isNsfwContentDisabled)
}
- fun observeDisabledSources(): Flow> = combine(
+ @OptIn(ExperimentalCoroutinesApi::class)
+ fun observeDisabledSources(): Flow> = combine(
observeIsNsfwDisabled(),
observeSortOrder(),
) { skipNsfw, _ ->
@@ -84,7 +104,7 @@ class MangaSourcesRepository @Inject constructor(
}.distinctUntilChanged()
}
- fun observeEnabledSources(): Flow> = combine(
+ fun observeEnabledSources(): Flow> = combine(
observeIsNsfwDisabled(),
observeSortOrder(),
) { skipNsfw, order ->
@@ -120,10 +140,13 @@ class MangaSourcesRepository @Inject constructor(
}
}
- suspend fun assimilateNewSources(): Set {
+ suspend fun assimilateNewSources(): Boolean {
+ if (isNewSourcesAssimilated.getAndSet(true)) {
+ return false
+ }
val new = getNewSources()
if (new.isEmpty()) {
- return emptySet()
+ return false
}
var maxSortKey = dao.getMaxSortKey()
val entities = new.map { x ->
@@ -131,20 +154,20 @@ class MangaSourcesRepository @Inject constructor(
source = x.name,
isEnabled = false,
sortKey = ++maxSortKey,
+ addedIn = BuildConfig.VERSION_CODE,
+ lastUsedAt = 0,
+ isPinned = false,
)
}
dao.insertIfAbsent(entities)
- if (settings.isNsfwContentDisabled) {
- new.removeAll { x -> x.isNsfw() }
- }
- return new
+ return true
}
suspend fun isSetupRequired(): Boolean {
return dao.findAll().isEmpty()
}
- private suspend fun getNewSources(): MutableSet {
+ private suspend fun getNewSources(): MutableSet {
val entities = dao.findAll()
val result = EnumSet.copyOf(remoteSources)
for (e in entities) {
@@ -155,11 +178,11 @@ class MangaSourcesRepository @Inject constructor(
private fun List.toSources(
skipNsfwSources: Boolean,
- ): List {
- val result = ArrayList(size)
+ ): List {
+ val result = ArrayList(size)
for (entity in this) {
- val source = MangaSource(entity.source)
- if (skipNsfwSources && source.contentType == ContentType.HENTAI) {
+ val source = entity.source.toMangaSourceOrNull() ?: continue
+ if (skipNsfwSources && source.isNsfw()) {
continue
}
if (source in remoteSources) {
@@ -169,6 +192,41 @@ class MangaSourcesRepository @Inject constructor(
return result
}
+ private fun observeExternalSources(): Flow> {
+ 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 observeIsNewSourcesEnabled() = settings.observeAsFlow(KotatsuAppSettings.KEY_SOURCES_NEW) {
@@ -178,4 +236,6 @@ class MangaSourcesRepository @Inject constructor(
private fun observeSortOrder() = settings.observeAsFlow(KotatsuAppSettings.KEY_SOURCES_ORDER) {
sourcesSortOrder
}
+
+ private fun String.toMangaSourceOrNull(): MangaParserSource? = MangaParserSource.entries.find { it.name == this }
}
\ No newline at end of file
diff --git a/app/src/main/java/org/xtimms/shirizu/data/repository/backup/JsonDeserializer.kt b/app/src/main/java/org/xtimms/shirizu/data/repository/backup/JsonDeserializer.kt
index 07144e6..0704417 100644
--- a/app/src/main/java/org/xtimms/shirizu/data/repository/backup/JsonDeserializer.kt
+++ b/app/src/main/java/org/xtimms/shirizu/data/repository/backup/JsonDeserializer.kt
@@ -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.getFloatOrDefault
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.xtimms.shirizu.core.database.entity.BookmarkEntity
import org.xtimms.shirizu.core.database.entity.FavouriteCategoryEntity
@@ -83,6 +84,9 @@ class JsonDeserializer(private val json: JSONObject) {
source = json.getString("source"),
isEnabled = json.getBoolean("enabled"),
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 {
diff --git a/app/src/main/java/org/xtimms/shirizu/di/ShirizuModule.kt b/app/src/main/java/org/xtimms/shirizu/di/ShirizuModule.kt
index 60f8e4e..f88eefa 100644
--- a/app/src/main/java/org/xtimms/shirizu/di/ShirizuModule.kt
+++ b/app/src/main/java/org/xtimms/shirizu/di/ShirizuModule.kt
@@ -1,6 +1,5 @@
package org.xtimms.shirizu.di
-import android.app.Application
import android.content.Context
import android.text.Html
import androidx.work.WorkManager
@@ -22,9 +21,6 @@ import okhttp3.OkHttpClient
import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.xtimms.shirizu.BuildConfig
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.model.LocalManga
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.system.connectivityManager
import org.xtimms.shirizu.utils.system.isLowRamDevice
+import javax.inject.Provider
import javax.inject.Singleton
@Module
@@ -70,7 +67,7 @@ interface ShirizuModule {
@Singleton
fun provideCoil(
@ApplicationContext context: Context,
- @MangaHttpClient okHttpClient: OkHttpClient,
+ @MangaHttpClient okHttpClientProvider: Provider,
mangaRepositoryFactory: MangaRepository.Factory,
imageProxyInterceptor: ImageProxyInterceptor,
pageFetcherFactory: MangaPageFetcher.Factory,
@@ -81,37 +78,30 @@ interface ShirizuModule {
.directory(rootDir.resolve(CacheDir.THUMBS.dir))
.build()
}
+ val okHttpClientLazy = lazy {
+ okHttpClientProvider.get().newBuilder().cache(null).build()
+ }
return ImageLoader.Builder(context)
.crossfade(500)
- .okHttpClient(okHttpClient.newBuilder().cache(null).build())
+ .okHttpClient { okHttpClientLazy.value }
.interceptorDispatcher(Dispatchers.Default)
.fetcherDispatcher(Dispatchers.IO)
.decoderDispatcher(Dispatchers.Default)
.transformationDispatcher(Dispatchers.Default)
.diskCache(diskCacheFactory)
+ .respectCacheHeaders(false)
+ .networkObserverEnabled(false)
.logger(if (BuildConfig.DEBUG) DebugLogger() else null)
.allowRgb565(context.isLowRamDevice())
.components(
ComponentRegistry.Builder()
- .add(FaviconFetcher.Factory(context, okHttpClient, mangaRepositoryFactory))
+ .add(FaviconFetcher.Factory(context, okHttpClientLazy, mangaRepositoryFactory))
.add(pageFetcherFactory)
.add(imageProxyInterceptor)
.build(),
).build()
}
- @Provides
- @Singleton
- fun provideContentCache(
- application: Application,
- ): ContentCache {
- return if (application.isLowRamDevice()) {
- StubContentCache()
- } else {
- MemoryContentCache(application)
- }
- }
-
@Provides
@Singleton
@LocalStorageChanges
diff --git a/app/src/main/java/org/xtimms/shirizu/sections/details/ClassicDetailsInfoBox.kt b/app/src/main/java/org/xtimms/shirizu/sections/details/ClassicDetailsInfoBox.kt
index 0fd8164..813c274 100644
--- a/app/src/main/java/org/xtimms/shirizu/sections/details/ClassicDetailsInfoBox.kt
+++ b/app/src/main/java/org/xtimms/shirizu/sections/details/ClassicDetailsInfoBox.kt
@@ -32,11 +32,13 @@ import androidx.compose.ui.unit.dp
import coil.ImageLoader
import coil.compose.AsyncImage
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.MangaState
import org.xtimms.shirizu.R
import org.xtimms.shirizu.core.ShirizuAsyncImage
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.model.HistoryInfo
@@ -45,13 +47,13 @@ fun ClassicDetailsInfoBox(
imageUrl: String,
favicon: Uri,
title: String,
- altTitle: String,
- author: String,
+ altTitle: String?,
+ author: String?,
isNsfw: Boolean,
state: MangaState?,
source: MangaSource,
- historyInfo: HistoryInfo,
- readingTime: ReadingTime,
+ historyInfo: HistoryInfo?,
+ readingTime: ReadingTime?,
isTabletUi: Boolean,
appBarPadding: Dp,
modifier: Modifier = Modifier,
@@ -133,8 +135,8 @@ fun MangaInfoLarge(
imageUrl: String,
favicon: Uri,
title: String,
- altTitle: String,
- author: String,
+ altTitle: String?,
+ author: String?,
source: MangaSource,
state: MangaState?,
historyInfo: HistoryInfo?,
@@ -168,7 +170,7 @@ fun MangaInfoLarge(
altTitle = altTitle,
author = author,
state = state,
- source = source.title,
+ source = source.name,
isInShelf = isInShelf,
onAddToShelfClicked = onAddToShelfClicked,
onSourceClicked = onSourceClicked,
@@ -185,12 +187,12 @@ fun MangaInfoSmall(
imageUrl: String,
favicon: Uri,
title: String,
- altTitle: String,
- author: String,
+ altTitle: String?,
+ author: String?,
state: MangaState?,
source: MangaSource,
- historyInfo: HistoryInfo,
- readingTime: ReadingTime,
+ historyInfo: HistoryInfo?,
+ readingTime: ReadingTime?,
isInShelf: Boolean,
onAddToShelfClicked: () -> Unit,
onCoverClick: () -> Unit,
@@ -225,7 +227,7 @@ fun MangaInfoSmall(
altTitle = altTitle,
author = author,
state = state,
- source = source.title,
+ source = source.name,
isInShelf = isInShelf,
onAddToShelfClicked = onAddToShelfClicked,
onSourceClicked = onSourceClicked,
diff --git a/app/src/main/java/org/xtimms/shirizu/sections/details/DetailsInfoHeader.kt b/app/src/main/java/org/xtimms/shirizu/sections/details/DetailsInfoHeader.kt
index e44c7bb..5b6c7e5 100644
--- a/app/src/main/java/org/xtimms/shirizu/sections/details/DetailsInfoHeader.kt
+++ b/app/src/main/java/org/xtimms/shirizu/sections/details/DetailsInfoHeader.kt
@@ -49,6 +49,7 @@ import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon
import androidx.compose.material3.InputChip
import androidx.compose.material3.LocalMinimumInteractiveComponentEnforcement
+import androidx.compose.material3.LocalMinimumInteractiveComponentSize
import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedIconButton
@@ -82,6 +83,7 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import coil.ImageLoader
import coil.compose.AsyncImage
+import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaState
import org.koitharu.kotatsu.parsers.model.MangaTag
@@ -108,7 +110,7 @@ fun MangaAndSourceTitlesLarge(
title: String,
altTitle: String,
author: String,
- source: MangaSource,
+ source: MangaParserSource,
state: MangaState?,
historyInfo: HistoryInfo,
readingTime: ReadingTime?,
@@ -153,7 +155,7 @@ fun MangaAndSourceTitlesSmall(
altTitle: String,
author: String,
state: MangaState?,
- source: MangaSource,
+ source: MangaParserSource,
historyInfo: HistoryInfo,
readingTime: ReadingTime?,
isInShelf: Boolean,
@@ -188,19 +190,16 @@ fun MangaAndSourceTitlesSmall(
}
}
-@OptIn(
- ExperimentalLayoutApi::class,
- ExperimentalMaterial3Api::class
-)
+@OptIn(ExperimentalLayoutApi::class)
@Composable
fun DetailsContentInfo(
favicon: Uri,
title: String,
- altTitle: String,
- author: String,
+ altTitle: String?,
+ author: String?,
state: MangaState?,
source: String?,
- historyInfo: HistoryInfo,
+ historyInfo: HistoryInfo?,
readingTime: ReadingTime?,
isInShelf: Boolean,
onAddToShelfClicked: () -> Unit,
@@ -224,7 +223,7 @@ fun DetailsContentInfo(
maxLines = 3
)
- if (altTitle.isNotBlank()) {
+ if (!altTitle.isNullOrBlank()) {
Text(
text = altTitle,
style = MaterialTheme.typography.headlineSmall,
@@ -235,7 +234,7 @@ fun DetailsContentInfo(
Spacer(modifier = Modifier.height(4.dp))
}
- if (author.isNotEmpty()) {
+ if (!author.isNullOrBlank()) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
@@ -294,7 +293,7 @@ fun DetailsContentInfo(
verticalArrangement = Arrangement.spacedBy(8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
- CompositionLocalProvider(LocalMinimumInteractiveComponentEnforcement provides false) {
+ CompositionLocalProvider(LocalMinimumInteractiveComponentSize provides 0.dp) {
InputChip(
selected = false,
onClick = { onAddToShelfClicked() },
@@ -354,7 +353,7 @@ fun DetailsContentInfo(
modifier = Modifier
.height(32.dp)
.width(56.dp),
- onClick = { onAddToShelfClicked() /*TODO*/ },
+ onClick = { /*TODO*/ },
shape = MaterialTheme.shapes.small,
border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline)
) {
@@ -579,14 +578,13 @@ private fun MangaSummary(
}
}
-@OptIn(ExperimentalMaterial3Api::class)
@Composable
private fun TagsChip(
tag: MangaTag,
modifier: Modifier = Modifier,
onClick: () -> Unit,
) {
- CompositionLocalProvider(LocalMinimumInteractiveComponentEnforcement provides false) {
+ CompositionLocalProvider(LocalMinimumInteractiveComponentSize provides 0.dp) {
SuggestionChip(
modifier = modifier,
onClick = onClick,
diff --git a/app/src/main/java/org/xtimms/shirizu/sections/details/DetailsScreen.kt b/app/src/main/java/org/xtimms/shirizu/sections/details/DetailsScreen.kt
index 1b72882..9c01c3c 100644
--- a/app/src/main/java/org/xtimms/shirizu/sections/details/DetailsScreen.kt
+++ b/app/src/main/java/org/xtimms/shirizu/sections/details/DetailsScreen.kt
@@ -18,6 +18,7 @@ import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.parsers.MangaParser
import org.koitharu.kotatsu.parsers.model.Manga
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.utils.lang.AssistContentScreen
import org.xtimms.shirizu.utils.lang.Screen
@@ -26,13 +27,9 @@ import javax.inject.Inject
import javax.inject.Singleton
class DetailsScreen(
- private val manga: Manga,
+ private val mangaId: Long,
val fromSource: Boolean = false,
-) : Screen(), AssistContentScreen {
-
- private var assistUrl: String? = null
-
- override fun onProvideAssistUrl() = assistUrl
+) : Screen() {
@Composable
override fun Content() {
@@ -43,7 +40,7 @@ class DetailsScreen(
val screenModel =
getScreenModel { factory ->
- factory.create(context, manga, SnackbarHostState())
+ factory.create(context, mangaId, SnackbarHostState())
}
val state by screenModel.state.collectAsState()
@@ -54,13 +51,13 @@ class DetailsScreen(
}
val successState = state as DetailsScreenModel.State.Success
- val isOnlineSource = remember { successState.source != MangaSource.DUMMY && successState.source != MangaSource.LOCAL }
MangaScreen(
state = successState,
snackbarHostState = screenModel.snackbarHostState,
isTabletUi = isTabletUi(),
onBackClicked = navigator::pop,
+ onMangaClicked = { },
onWebViewClicked = {
},
@@ -77,15 +74,4 @@ class DetailsScreen(
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
- }
- }
}
\ No newline at end of file
diff --git a/app/src/main/java/org/xtimms/shirizu/sections/details/DetailsScreenModel.kt b/app/src/main/java/org/xtimms/shirizu/sections/details/DetailsScreenModel.kt
index ffbe67b..d59c9ae 100644
--- a/app/src/main/java/org/xtimms/shirizu/sections/details/DetailsScreenModel.kt
+++ b/app/src/main/java/org/xtimms/shirizu/sections/details/DetailsScreenModel.kt
@@ -3,53 +3,40 @@ package org.xtimms.shirizu.sections.details
import android.content.Context
import androidx.compose.material3.SnackbarHostState
import androidx.compose.runtime.Immutable
+import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.screenModelScope
import cafe.adriel.voyager.hilt.ScreenModelFactory
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.channels.Channel
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.async
+import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.StateFlow
-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.collectLatest
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
+import kotlinx.coroutines.isActive
+import kotlinx.coroutines.launch
import kotlinx.coroutines.plus
-import org.koitharu.kotatsu.parsers.model.Manga
-import org.koitharu.kotatsu.parsers.model.MangaSource
-import org.xtimms.shirizu.core.base.viewmodel.BaseStateScreenModel
-import org.xtimms.shirizu.core.model.findById
-import org.xtimms.shirizu.core.model.getPreferredBranch
+import kotlinx.coroutines.withContext
+import org.xtimms.shirizu.core.parser.MangaDataRepository
import org.xtimms.shirizu.data.repository.BookmarksRepository
import org.xtimms.shirizu.data.repository.FavouritesRepository
import org.xtimms.shirizu.data.repository.HistoryRepository
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.DetailsLoadUseCase
import org.xtimms.shirizu.sections.details.domain.ReadingTimeUseCase
import org.xtimms.shirizu.sections.details.domain.RelatedMangaUseCase
import org.xtimms.shirizu.sections.details.model.ChapterItem
-import org.xtimms.shirizu.sections.details.model.HistoryInfo
-import org.xtimms.shirizu.sections.details.model.MangaBranch
-import org.xtimms.shirizu.utils.lang.onEachWhile
+import org.xtimms.shirizu.utils.system.getDisplayMessage
class DetailsScreenModel @AssistedInject constructor(
@Assisted val context: Context,
- @Assisted val manga: Manga,
+ @Assisted val mangaId: Long,
private val interactor: DetailsInteractor,
+ private val mangaDataRepository: MangaDataRepository,
private val historyRepository: HistoryRepository,
private val bookmarksRepository: BookmarksRepository,
private val favouritesRepository: FavouritesRepository,
@@ -57,13 +44,16 @@ class DetailsScreenModel @AssistedInject constructor(
private val readingTimeUseCase: ReadingTimeUseCase,
private val relatedMangaUseCase: RelatedMangaUseCase,
@Assisted val snackbarHostState: SnackbarHostState = SnackbarHostState(),
-) : BaseStateScreenModel(State.Loading) {
+) : StateScreenModel(State.Loading) {
private val successState: State.Success?
get() = state.value as? State.Success
- private val _events: Channel = Channel(Channel.UNLIMITED)
- val events: Flow = _events.receiveAsFlow()
+ val details: MangaDetails?
+ get() = successState?.details
+
+ val history = historyRepository.observeOne(mangaId)
+ .stateIn(screenModelScope + Dispatchers.Default, SharingStarted.Eagerly, null)
private inline fun updateSuccessState(func: (State.Success) -> State.Success) {
mutableState.update {
@@ -74,143 +64,56 @@ class DetailsScreenModel @AssistedInject constructor(
}
}
- private var loadingJob: Job
-
- 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(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(null)
-
- val historyInfo: StateFlow = 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> = mangaImpl.mapLatest {
- if (it != null) {
- relatedMangaUseCase.invoke(it).orEmpty()
- } else {
- emptyList()
- }
- }.stateIn(screenModelScope, SharingStarted.Lazily, emptyList())
+ private val selectedPositions: Array = arrayOf(-1, -1) // first and last selected index in list
+ private val selectedChapterIds: HashSet = HashSet()
- val branches: StateFlow> = combine(
- details,
- selectedBranch,
- history,
- ) { m, b, h ->
- val c = m.chapters
- if (c.isEmpty()) {
- return@combine emptyList()
+ init {
+ screenModelScope.launch(Dispatchers.IO) {
+ detailsLoadUseCase.invoke(mangaId)
+ .collectLatest { details ->
+ updateSuccessState {
+ it.copy(
+ details = details
+ )
+ }
+ }
}
- 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 = details.map {
- it.isLoaded && it.allChapters.isEmpty()
- }.stateIn(screenModelScope, SharingStarted.WhileSubscribed(), false)
+ screenModelScope.launch(Dispatchers.IO) {
+ val manga = requireNotNull(mangaDataRepository.findMangaById(mangaId))
+ val details = MangaDetails(manga, null, null, 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 needRefreshInfo = !details.isLoaded
- val readingTime = combine(
- details,
- selectedBranch,
- history,
- ) { m, b, h ->
- readingTimeUseCase.invoke(m, b, h)
- }.stateIn(screenModelScope, SharingStarted.Lazily, null)
+ mutableState.update {
+ State.Success(
+ details = details
+ )
+ }
- val selectedBranchValue: String?
- get() = selectedBranch.value
+ if (screenModelScope.isActive) {
+ val fetchFromSourceTasks = listOf(
+ async { if (needRefreshInfo) fetchMangaFromSource() },
+ )
+ fetchFromSourceTasks.awaitAll()
+ }
- init {
- loadingJob = doLoad(manga.id)
- updateSuccessState { it.copy(isRefreshingData = false) }
+ updateSuccessState { it.copy(isRefreshingData = false) }
+ }
}
- private fun doLoad(mangaId: Long) = launchLoadingJob(Dispatchers.Default) {
- detailsLoadUseCase.invoke(mangaId)
- .onEachWhile {
- if (it.allChapters.isEmpty()) {
- return@onEachWhile false
- }
- 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 {
- State.Success(
- manga = details.value.toManga(),
- source = details.value.toManga().source,
- readingTime = checkNotNull(readingTime.value),
- historyInfo = historyInfo.value,
- availableScanlators = setOf(),
- excludedScanlators = setOf(),
- 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) })
+ }
+ }
}
private fun List.filterSearch(query: String): List {
@@ -222,12 +125,6 @@ class DetailsScreenModel @AssistedInject constructor(
}
}
- fun removeFromHistory() {
- launchJob(Dispatchers.Default) {
- historyRepository.delete(setOf(manga.id))
- }
- }
-
sealed interface Event {
data object InternalError : Event
}
@@ -238,12 +135,7 @@ class DetailsScreenModel @AssistedInject constructor(
@Immutable
data class Success(
- val manga: Manga,
- val source: MangaSource,
- val historyInfo: HistoryInfo,
- val readingTime: ReadingTime,
- val availableScanlators: Set,
- val excludedScanlators: Set,
+ val details: MangaDetails,
val isRefreshingData: Boolean = false,
) : State
}
@@ -252,7 +144,7 @@ class DetailsScreenModel @AssistedInject constructor(
interface Factory : ScreenModelFactory {
fun create(
context: Context,
- manga: Manga,
+ mangaId: Long,
snackbarHostState: SnackbarHostState
): DetailsScreenModel
}
diff --git a/app/src/main/java/org/xtimms/shirizu/sections/details/DetailsUiState.kt b/app/src/main/java/org/xtimms/shirizu/sections/details/DetailsUiState.kt
deleted file mode 100644
index de4fdf8..0000000
--- a/app/src/main/java/org/xtimms/shirizu/sections/details/DetailsUiState.kt
+++ /dev/null
@@ -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)
-}
\ No newline at end of file
diff --git a/app/src/main/java/org/xtimms/shirizu/sections/details/MangaScreen.kt b/app/src/main/java/org/xtimms/shirizu/sections/details/MangaScreen.kt
index 95dea64..1e1a3ad 100644
--- a/app/src/main/java/org/xtimms/shirizu/sections/details/MangaScreen.kt
+++ b/app/src/main/java/org/xtimms/shirizu/sections/details/MangaScreen.kt
@@ -1,16 +1,28 @@
package org.xtimms.shirizu.sections.details
import androidx.activity.compose.BackHandler
+import androidx.compose.animation.AnimatedVisibility
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.calculateEndPadding
import androidx.compose.foundation.layout.calculateStartPadding
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.LazyRow
+import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.ExperimentalMaterial3Api
+import androidx.compose.material3.HorizontalDivider
+import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
+import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.derivedStateOf
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.LocalLayoutDirection
import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
import coil.ImageLoader
+import org.koitharu.kotatsu.parsers.model.Manga
import org.xtimms.shirizu.R
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.VerticalFastScroller
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.prefs.AppSettings
import org.xtimms.shirizu.sections.details.data.ReadingTime
import org.xtimms.shirizu.sections.details.model.HistoryInfo
import java.time.Instant
@@ -36,6 +53,7 @@ fun MangaScreen(
snackbarHostState: SnackbarHostState,
isTabletUi: Boolean,
onBackClicked: () -> Unit,
+ onMangaClicked: (Manga) -> Unit,
onWebViewClicked: (() -> Unit)?,
onWebViewLongClicked: (() -> Unit)?,
onTrackingClicked: () -> Unit,
@@ -56,6 +74,7 @@ fun MangaScreen(
snackbarHostState = snackbarHostState,
onBackClicked = onBackClicked,
onTagSearch = onTagSearch,
+ onMangaClicked = onMangaClicked,
onRefresh = onRefresh,
)
}
@@ -68,6 +87,7 @@ private fun MangaScreenSmallImpl(
snackbarHostState: SnackbarHostState,
onBackClicked: () -> Unit,
onTagSearch: (String) -> Unit,
+ onMangaClicked: (Manga) -> Unit,
onRefresh: () -> Unit,
) {
val chapterListState = rememberLazyListState()
@@ -91,7 +111,7 @@ private fun MangaScreenSmallImpl(
label = "Top Bar Background",
)
ClassicDetailsToolbar(
- title = state.manga?.title ?: "",
+ title = state.details.toManga().title,
titleAlphaProvider = { animatedTitleAlpha },
backgroundAlphaProvider = { animatedBgAlpha },
navigateBack = { onBackClicked() },
@@ -102,6 +122,8 @@ private fun MangaScreenSmallImpl(
val topPadding = contentPadding.calculateTopPadding()
val layoutDirection = LocalLayoutDirection.current
+ val relatedMangaListState = rememberLazyListState()
+
VerticalFastScroller(
listState = chapterListState,
topContentPadding = topPadding,
@@ -121,16 +143,16 @@ private fun MangaScreenSmallImpl(
contentType = DetailsScreenItem.INFO_BOX,
) {
ClassicDetailsInfoBox(
- imageUrl = state.manga.largeCoverUrl ?: state.manga.coverUrl,
- favicon = state.manga.source.faviconUri(),
- title = state.manga.title,
- altTitle = state.manga.altTitle ?: stringResource(id = R.string.unknown),
- author = state.manga.author ?: stringResource(id = R.string.unknown),
- isNsfw = state.manga.isNsfw,
- state = state.manga.state,
- source = state.manga.source,
- historyInfo = state.historyInfo,
- readingTime = state.readingTime,
+ imageUrl = state.details.toManga().largeCoverUrl ?: state.details.toManga().coverUrl,
+ favicon = state.details.toManga().source.faviconUri(),
+ title = state.details.toManga().title,
+ altTitle = state.details.toManga().altTitle,
+ author = state.details.toManga().author,
+ isNsfw = state.details.toManga().isNsfw,
+ state = state.details.toManga().state,
+ source = state.details.toManga().source,
+ historyInfo = null,
+ readingTime = null,
isTabletUi = false,
appBarPadding = topPadding,
onCoverClick = { },
@@ -147,12 +169,71 @@ private fun MangaScreenSmallImpl(
) {
ExpandableMangaDescription(
defaultExpandState = false,
- description = state.manga?.description,
- tagsProvider = { state.manga?.tags },
+ description = state.details.toManga().description,
+ tagsProvider = { state.details.toManga().tags },
onTagSearch = onTagSearch,
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*//* }
+ )
+ }*/
}
}
}
diff --git a/app/src/main/java/org/xtimms/shirizu/sections/details/ModernDetailsInfoBox.kt b/app/src/main/java/org/xtimms/shirizu/sections/details/ModernDetailsInfoBox.kt
index cf8a14b..71cfa51 100644
--- a/app/src/main/java/org/xtimms/shirizu/sections/details/ModernDetailsInfoBox.kt
+++ b/app/src/main/java/org/xtimms/shirizu/sections/details/ModernDetailsInfoBox.kt
@@ -23,6 +23,7 @@ import androidx.compose.ui.semantics.Role
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import coil.ImageLoader
+import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.koitharu.kotatsu.parsers.model.MangaState
import org.xtimms.shirizu.core.ShirizuAsyncImage
@@ -38,7 +39,7 @@ fun ModernDetailsInfoBox(
author: String,
isNsfw: Boolean,
state: MangaState?,
- source: MangaSource,
+ source: MangaParserSource,
historyInfo: HistoryInfo,
readingTime: ReadingTime?,
isTabletUi: Boolean,
diff --git a/app/src/main/java/org/xtimms/shirizu/sections/details/domain/DetailsLoadUseCase.kt b/app/src/main/java/org/xtimms/shirizu/sections/details/domain/DetailsLoadUseCase.kt
index 0584581..65fbb06 100644
--- a/app/src/main/java/org/xtimms/shirizu/sections/details/domain/DetailsLoadUseCase.kt
+++ b/app/src/main/java/org/xtimms/shirizu/sections/details/domain/DetailsLoadUseCase.kt
@@ -32,7 +32,7 @@ class DetailsLoadUseCase @Inject constructor(
) {
operator fun invoke(mangaId: Long): Flow = channelFlow {
- val manga = requireNotNull(mangaDataRepository.findMangaById(mangaId)) {
+ val manga = requireNotNull(mangaDataRepository.findMangaById(mangaId)) { // wrong method
"Cannot resolve id $mangaId"
}
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)
repository.getDetails(seed)
}.getOrThrow()
diff --git a/app/src/main/java/org/xtimms/shirizu/sections/explore/ExploreTab.kt b/app/src/main/java/org/xtimms/shirizu/sections/explore/ExploreTab.kt
index ed4bd61..fcea139 100644
--- a/app/src/main/java/org/xtimms/shirizu/sections/explore/ExploreTab.kt
+++ b/app/src/main/java/org/xtimms/shirizu/sections/explore/ExploreTab.kt
@@ -24,7 +24,7 @@ data class ExploreTab(
get() {
val image = Icons.Outlined.Explore
return TabOptions(
- index = 3u,
+ index = 1u,
title = stringResource(R.string.nav_explore),
icon = rememberVectorPainter(image),
)
diff --git a/app/src/main/java/org/xtimms/shirizu/sections/explore/catalog/CatalogScreen.kt b/app/src/main/java/org/xtimms/shirizu/sections/explore/catalog/CatalogScreen.kt
index f51bdd1..b8c79ac 100644
--- a/app/src/main/java/org/xtimms/shirizu/sections/explore/catalog/CatalogScreen.kt
+++ b/app/src/main/java/org/xtimms/shirizu/sections/explore/catalog/CatalogScreen.kt
@@ -29,6 +29,7 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.dp
import org.koitharu.kotatsu.parsers.model.ContentType
+import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.xtimms.shirizu.R
import org.xtimms.shirizu.core.components.FastScrollLazyColumn
@@ -234,7 +235,7 @@ fun CatalogScreen(
@Composable
fun SourceItem(
- source: MangaSource,
+ source: MangaParserSource,
onClickItem: (MangaSource) -> Unit,
onLongClickItem: (MangaSource) -> Unit,
onClickMenu: (MangaSource) -> Unit,
diff --git a/app/src/main/java/org/xtimms/shirizu/sections/explore/catalog/CatalogScreenModel.kt b/app/src/main/java/org/xtimms/shirizu/sections/explore/catalog/CatalogScreenModel.kt
index 4c080f5..cd323db 100644
--- a/app/src/main/java/org/xtimms/shirizu/sections/explore/catalog/CatalogScreenModel.kt
+++ b/app/src/main/java/org/xtimms/shirizu/sections/explore/catalog/CatalogScreenModel.kt
@@ -17,7 +17,9 @@ import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import org.koitharu.kotatsu.parsers.model.ContentType
+import org.koitharu.kotatsu.parsers.model.MangaParserSource
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.data.repository.MangaSourcesRepository
import org.xtimms.shirizu.sections.explore.sources.SourceUiModel
@@ -34,7 +36,7 @@ class CatalogScreenModel @Inject constructor(
val events = _events.receiveAsFlow()
init {
- val queryFilter: (String) -> ((MangaSource) -> Boolean) = { query ->
+ val queryFilter: (String) -> ((MangaParserSource) -> Boolean) = { query ->
filter@{ source ->
if (query.isEmpty()) return@filter true
query.split(",").any { _input ->
diff --git a/app/src/main/java/org/xtimms/shirizu/sections/explore/sources/BaseSourceItem.kt b/app/src/main/java/org/xtimms/shirizu/sections/explore/sources/BaseSourceItem.kt
index b968750..2705808 100644
--- a/app/src/main/java/org/xtimms/shirizu/sections/explore/sources/BaseSourceItem.kt
+++ b/app/src/main/java/org/xtimms/shirizu/sections/explore/sources/BaseSourceItem.kt
@@ -13,6 +13,7 @@ import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.core.os.LocaleListCompat
import org.koitharu.kotatsu.parsers.model.ContentType
+import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.xtimms.shirizu.R
import org.xtimms.shirizu.utils.LocaleHelper
@@ -21,14 +22,14 @@ import java.util.Locale
@Composable
fun BaseSourceItem(
- source: MangaSource,
+ source: MangaParserSource,
modifier: Modifier = Modifier,
showTypeInContent: Boolean = true,
onClickItem: () -> Unit = {},
onLongClickItem: () -> Unit = {},
icon: @Composable RowScope.(MangaSource) -> Unit = defaultIcon,
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 {
if (type == null) {
@@ -60,7 +61,7 @@ private val defaultIcon: @Composable RowScope.(MangaSource) -> Unit = { 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(
modifier = Modifier
.padding(horizontal = 24.dp)
diff --git a/app/src/main/java/org/xtimms/shirizu/sections/explore/sources/SourcesScreen.kt b/app/src/main/java/org/xtimms/shirizu/sections/explore/sources/SourcesScreen.kt
index fb7aa80..6763e1f 100644
--- a/app/src/main/java/org/xtimms/shirizu/sections/explore/sources/SourcesScreen.kt
+++ b/app/src/main/java/org/xtimms/shirizu/sections/explore/sources/SourcesScreen.kt
@@ -20,6 +20,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
+import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.xtimms.shirizu.R
import org.xtimms.shirizu.core.components.ScrollbarLazyColumn
@@ -105,7 +106,7 @@ fun SourceHeader(
@Composable
fun SourceItem(
- source: MangaSource,
+ source: MangaParserSource,
onClickItem: (MangaSource) -> Unit,
onLongClickItem: (MangaSource) -> Unit,
onClickMenu: (MangaSource) -> Unit,
@@ -172,6 +173,6 @@ private fun SourcePinButton(
}
sealed interface SourceUiModel {
- data class Item(val source: MangaSource) : SourceUiModel
+ data class Item(val source: MangaParserSource) : SourceUiModel
data class Header(val language: String?) : SourceUiModel
}
\ No newline at end of file
diff --git a/app/src/main/java/org/xtimms/shirizu/sections/explore/sources/SourcesScreenModel.kt b/app/src/main/java/org/xtimms/shirizu/sections/explore/sources/SourcesScreenModel.kt
index 495cd7d..c2084f1 100644
--- a/app/src/main/java/org/xtimms/shirizu/sections/explore/sources/SourcesScreenModel.kt
+++ b/app/src/main/java/org/xtimms/shirizu/sections/explore/sources/SourcesScreenModel.kt
@@ -1,8 +1,10 @@
package org.xtimms.shirizu.sections.explore.sources
+import android.content.Context
import androidx.compose.runtime.Immutable
import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.screenModelScope
+import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList
@@ -13,12 +15,17 @@ import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.receiveAsFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
+import org.koitharu.kotatsu.parsers.model.MangaParserSource
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.utils.LocaleHelper
import javax.inject.Inject
class SourcesScreenModel @Inject constructor(
+ @ApplicationContext context: Context,
private val mangaSourcesRepository: MangaSourcesRepository,
) : StateScreenModel(State()) {
@@ -36,7 +43,7 @@ class SourcesScreenModel @Inject constructor(
}
}
- private fun collectEnabledSources(sources: List) {
+ private fun collectEnabledSources(sources: List) {
mutableState.update { state ->
state.copy(
isLoading = false,
diff --git a/app/src/main/java/org/xtimms/shirizu/sections/library/history/HistoryScreenModel.kt b/app/src/main/java/org/xtimms/shirizu/sections/library/history/HistoryScreenModel.kt
index dc17502..9d757e9 100644
--- a/app/src/main/java/org/xtimms/shirizu/sections/library/history/HistoryScreenModel.kt
+++ b/app/src/main/java/org/xtimms/shirizu/sections/library/history/HistoryScreenModel.kt
@@ -73,12 +73,7 @@ class HistoryScreenModel @Inject constructor(
val searchQuery = query ?: ""
history.asSequence().map { it }
.filter { it.manga.isNsfw == nsfw }
- .sortedByDescending {
- when (sort) {
- SortOption.DATE_ADDED -> it.history.updatedAt
- SortOption.ALPHABETICAL -> it.manga.title.lowercase()
- }.toString()
- }
+ .sortedWith(MangaComparator(sort))
.filter(queryFilter(searchQuery)).toList()
.toImmutableList()
}.collectLatest {
@@ -222,10 +217,7 @@ class HistoryScreenModel @Inject constructor(
val searchQuery: String? = null,
val selection: PersistentList = persistentListOf(),
val showNsfw: Boolean = AppSettings.showNsfwInHistory(),
- val availableSorts: List = listOf(
- SortOption.DATE_ADDED,
- SortOption.ALPHABETICAL
- ),
+ val availableSorts: List = SortOption.entries,
val sort: SortOption = SortOption.ALPHABETICAL,
val list: PersistentList = persistentListOf(),
val dialog: Dialog? = null,
@@ -264,4 +256,14 @@ class HistoryScreenModel @Inject constructor(
data object HistoryCleared : Event
}
+}
+
+private class MangaComparator(private val sort: SortOption) : Comparator {
+ 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)
+ }
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/org/xtimms/shirizu/sections/library/history/HistoryTab.kt b/app/src/main/java/org/xtimms/shirizu/sections/library/history/HistoryTab.kt
index a984bb6..ca7eb53 100644
--- a/app/src/main/java/org/xtimms/shirizu/sections/library/history/HistoryTab.kt
+++ b/app/src/main/java/org/xtimms/shirizu/sections/library/history/HistoryTab.kt
@@ -36,6 +36,7 @@ import org.xtimms.shirizu.core.components.DialogCheckBoxItem
import org.xtimms.shirizu.core.components.Scaffold
import org.xtimms.shirizu.core.components.LibraryBottomActionMenu
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.sections.details.DetailsScreen
@@ -80,7 +81,7 @@ fun Screen.historyTab(): TabContent {
onToggleEnableNsfw = { screenModel.filterNsfw(it) },
onFilterChanged = { screenModel.search(it) },
onSortSelected = { screenModel.sort(it) },
- onClick = { navigator.push(DetailsScreen(it.manga)) },
+ onClick = { navigator.push(DetailsScreen(it.manga.id)) },
onHistorySelected = screenModel::toggleSelection
)
}
diff --git a/app/src/main/java/org/xtimms/shirizu/sections/library/history/SortOption.kt b/app/src/main/java/org/xtimms/shirizu/sections/library/history/SortOption.kt
index 17552f1..27adbf3 100644
--- a/app/src/main/java/org/xtimms/shirizu/sections/library/history/SortOption.kt
+++ b/app/src/main/java/org/xtimms/shirizu/sections/library/history/SortOption.kt
@@ -1,6 +1,7 @@
package org.xtimms.shirizu.sections.library.history
-enum class SortOption {
- ALPHABETICAL,
- DATE_ADDED,
+enum class SortOption(id: Int) {
+ ALPHABETICAL(0),
+ DATE_ADDED(1),
+ PROGRESS(2)
}
\ No newline at end of file
diff --git a/app/src/main/java/org/xtimms/shirizu/sections/list/MangaListScreen.kt b/app/src/main/java/org/xtimms/shirizu/sections/list/MangaListScreen.kt
index 6067d79..102adc5 100644
--- a/app/src/main/java/org/xtimms/shirizu/sections/list/MangaListScreen.kt
+++ b/app/src/main/java/org/xtimms/shirizu/sections/list/MangaListScreen.kt
@@ -26,6 +26,7 @@ import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import org.koitharu.kotatsu.parsers.model.MangaSource
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.utils.lang.Screen
@@ -75,7 +76,7 @@ data class MangaListScreen(private val source: MangaSource) : Screen() {
columns = screenModel.getColumnsPreference(LocalConfiguration.current.orientation),
snackbarHostState = snackbarHostState,
contentPadding = paddingValues,
- onMangaClick = { navigator.push((DetailsScreen(it, true))) },
+ onMangaClick = { navigator.push((DetailsScreen(it.id, true))) },
onMangaLongClick = { manga -> },
)
}
diff --git a/app/src/main/java/org/xtimms/shirizu/sections/list/MangaListScreenModel.kt b/app/src/main/java/org/xtimms/shirizu/sections/list/MangaListScreenModel.kt
index 4fa06e9..f4a9765 100644
--- a/app/src/main/java/org/xtimms/shirizu/sections/list/MangaListScreenModel.kt
+++ b/app/src/main/java/org/xtimms/shirizu/sections/list/MangaListScreenModel.kt
@@ -17,6 +17,7 @@ import kotlinx.coroutines.flow.getAndUpdate
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import org.koitharu.kotatsu.parsers.model.Manga
+import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.xtimms.shirizu.core.parser.MangaRepository
@@ -25,7 +26,7 @@ class MangaListScreenModel @AssistedInject constructor(
mangaRepositoryFactory: MangaRepository.Factory,
) : StateScreenModel(State()) {
- val source = MangaSource.valueOf(sourceName)
+ val source = MangaParserSource.valueOf(sourceName)
private val repository = mangaRepositoryFactory.create(source)
private val hasNextPage = MutableStateFlow(false)
private val mangaList = MutableStateFlow?>(null)
diff --git a/app/src/main/java/org/xtimms/shirizu/sections/list/MangaListToolbar.kt b/app/src/main/java/org/xtimms/shirizu/sections/list/MangaListToolbar.kt
index ab7c225..0e30134 100644
--- a/app/src/main/java/org/xtimms/shirizu/sections/list/MangaListToolbar.kt
+++ b/app/src/main/java/org/xtimms/shirizu/sections/list/MangaListToolbar.kt
@@ -9,6 +9,7 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.res.stringResource
import kotlinx.collections.immutable.persistentListOf
+import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.xtimms.shirizu.R
import org.xtimms.shirizu.core.components.AppBar
@@ -21,7 +22,7 @@ import org.xtimms.shirizu.core.components.SearchToolbar
fun BrowseSourceToolbar(
searchQuery: String?,
onSearchQueryChange: (String?) -> Unit,
- source: MangaSource?,
+ source: MangaParserSource?,
navigateUp: () -> Unit,
onWebViewClick: () -> Unit,
onSearch: (String) -> Unit,
diff --git a/app/src/main/java/org/xtimms/shirizu/sections/profile/ProfileScreen.kt b/app/src/main/java/org/xtimms/shirizu/sections/profile/ProfileScreen.kt
new file mode 100644
index 0000000..93cb0f6
--- /dev/null
+++ b/app/src/main/java/org/xtimms/shirizu/sections/profile/ProfileScreen.kt
@@ -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()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/xtimms/shirizu/sections/profile/ProfileTab.kt b/app/src/main/java/org/xtimms/shirizu/sections/profile/ProfileTab.kt
new file mode 100644
index 0000000..a640474
--- /dev/null
+++ b/app/src/main/java/org/xtimms/shirizu/sections/profile/ProfileTab.kt
@@ -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()
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/xtimms/shirizu/sections/reader/domain/PageLoader.kt b/app/src/main/java/org/xtimms/shirizu/sections/reader/domain/PageLoader.kt
index 63e47db..1279cbf 100644
--- a/app/src/main/java/org/xtimms/shirizu/sections/reader/domain/PageLoader.kt
+++ b/app/src/main/java/org/xtimms/shirizu/sections/reader/domain/PageLoader.kt
@@ -32,10 +32,9 @@ import org.xtimms.shirizu.core.network.CommonHeaders
import org.xtimms.shirizu.core.network.MangaHttpClient
import org.xtimms.shirizu.core.network.interceptors.ImageProxyInterceptor
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.isZipUri
-import org.xtimms.shirizu.core.prefs.AppSettings
import org.xtimms.shirizu.sections.reader.pager.ReaderPage
import org.xtimms.shirizu.utils.FileSize
import org.xtimms.shirizu.utils.RetainedLifecycleCoroutineScope
@@ -81,7 +80,7 @@ class PageLoader @Inject constructor(
private var prefetchQueueLimit = PREFETCH_LIMIT_DEFAULT // TODO adaptive
fun isPrefetchApplicable(): Boolean {
- return repository is RemoteMangaRepository
+ return repository is ParserMangaRepository
// && settings.isPagesPreloadEnabled
&& !context.isPowerSaveMode()
&& !isLowRam()
diff --git a/app/src/main/java/org/xtimms/shirizu/sections/reader/pager/ReaderPage.kt b/app/src/main/java/org/xtimms/shirizu/sections/reader/pager/ReaderPage.kt
index 351e452..385859d 100644
--- a/app/src/main/java/org/xtimms/shirizu/sections/reader/pager/ReaderPage.kt
+++ b/app/src/main/java/org/xtimms/shirizu/sections/reader/pager/ReaderPage.kt
@@ -2,10 +2,13 @@ package org.xtimms.shirizu.sections.reader.pager
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
+import kotlinx.parcelize.TypeParceler
import org.koitharu.kotatsu.parsers.model.MangaPage
import org.koitharu.kotatsu.parsers.model.MangaSource
+import org.xtimms.shirizu.core.model.parcelable.MangaSourceParceler
@Parcelize
+@TypeParceler
data class ReaderPage(
val id: Long,
val url: String,
diff --git a/app/src/main/java/org/xtimms/shirizu/sections/search/SearchTab.kt b/app/src/main/java/org/xtimms/shirizu/sections/search/SearchTab.kt
index e18dcd1..15d161f 100644
--- a/app/src/main/java/org/xtimms/shirizu/sections/search/SearchTab.kt
+++ b/app/src/main/java/org/xtimms/shirizu/sections/search/SearchTab.kt
@@ -1,8 +1,6 @@
package org.xtimms.shirizu.sections.search
-import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
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.Scaffold
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.search.global.GlobalSearchScreen
import org.xtimms.shirizu.sections.suggestions.SuggestionsScreen
@@ -57,13 +56,13 @@ object SearchTab : Tab {
get() {
val image = Icons.Outlined.Search
return TabOptions(
- index = 4u,
+ index = 2u,
title = stringResource(R.string.search),
icon = rememberVectorPainter(image),
)
}
- @OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)
+ @OptIn(ExperimentalMaterial3Api::class)
@Composable
override fun Content() {
val navigator = LocalNavigator.currentOrThrow
@@ -135,7 +134,7 @@ object SearchTab : Tab {
MangaCarouselWithHeader(
items = state.list,
title = stringResource(id = R.string.suggestions),
- onItemClick = { navigator.push(DetailsScreen(it)) },
+ onItemClick = { navigator.push(DetailsScreen(it.id)) },
onMoreClick = { navigator.push(SuggestionsScreen) },
refreshing = state.isLoading,
modifier = Modifier.animateItem(),
diff --git a/app/src/main/java/org/xtimms/shirizu/sections/search/global/GlobalSearchScreenModel.kt b/app/src/main/java/org/xtimms/shirizu/sections/search/global/GlobalSearchScreenModel.kt
index aa47be3..b84d688 100644
--- a/app/src/main/java/org/xtimms/shirizu/sections/search/global/GlobalSearchScreenModel.kt
+++ b/app/src/main/java/org/xtimms/shirizu/sections/search/global/GlobalSearchScreenModel.kt
@@ -81,11 +81,6 @@ class GlobalSearchScreenModel @Inject constructor(
} else {
null
}
- val sources = if (SearchSuggestionType.SOURCES in types) {
- repository.getSourcesSuggestion(searchQuery, MAX_SOURCES_ITEMS)
- } else {
- null
- }
val tags = tagsDeferred?.await()
val mangaList = mangaDeferred?.await()
@@ -93,11 +88,10 @@ class GlobalSearchScreenModel @Inject constructor(
val hints = hintsDeferred?.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()) {
add(SearchSuggestionItem.MangaList(mangaList))
}
- sources?.mapTo(this) { SearchSuggestionItem.Source(it, it in enabledSources) }
queries?.mapTo(this) { SearchSuggestionItem.RecentQuery(it) }
authors?.mapTo(this) { SearchSuggestionItem.Author(it) }
hints?.mapTo(this) { SearchSuggestionItem.Hint(it) }
diff --git a/app/src/main/java/org/xtimms/shirizu/sections/search/global/model/SearchSuggestionItem.kt b/app/src/main/java/org/xtimms/shirizu/sections/search/global/model/SearchSuggestionItem.kt
index ddeb930..6b2a614 100644
--- a/app/src/main/java/org/xtimms/shirizu/sections/search/global/model/SearchSuggestionItem.kt
+++ b/app/src/main/java/org/xtimms/shirizu/sections/search/global/model/SearchSuggestionItem.kt
@@ -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.Manga
+import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.xtimms.shirizu.core.model.ListModel
@@ -44,7 +45,7 @@ sealed interface SearchSuggestionItem : ListModel {
}
data class Source(
- val source: MangaSource,
+ val source: MangaParserSource,
val isEnabled: Boolean,
) : SearchSuggestionItem {
diff --git a/app/src/main/java/org/xtimms/shirizu/sections/settings/sources/SourcesSettingsView.kt b/app/src/main/java/org/xtimms/shirizu/sections/settings/sources/SourcesSettingsView.kt
deleted file mode 100644
index 273618b..0000000
--- a/app/src/main/java/org/xtimms/shirizu/sections/settings/sources/SourcesSettingsView.kt
+++ /dev/null
@@ -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)
- }
- }
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/org/xtimms/shirizu/sections/settings/sources/catalog/SourceCatalogItem.kt b/app/src/main/java/org/xtimms/shirizu/sections/settings/sources/catalog/SourceCatalogItem.kt
index b4a9f57..bf4fa00 100644
--- a/app/src/main/java/org/xtimms/shirizu/sections/settings/sources/catalog/SourceCatalogItem.kt
+++ b/app/src/main/java/org/xtimms/shirizu/sections/settings/sources/catalog/SourceCatalogItem.kt
@@ -16,6 +16,7 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import coil.ImageLoader
+import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.xtimms.shirizu.core.ShirizuAsyncImage
import org.xtimms.shirizu.core.parser.favicon.faviconUri
@@ -23,7 +24,7 @@ import org.xtimms.shirizu.ui.theme.ShirizuTheme
@Composable
fun SourceCatalogItem(
- source: MangaSource,
+ source: MangaParserSource,
) {
Row(
@@ -53,6 +54,6 @@ fun SourceCatalogItem(
@Composable
fun SourceCatalogItemPreview() {
ShirizuTheme {
- SourceCatalogItem(source = MangaSource.MANGADEX)
+ SourceCatalogItem(source = MangaParserSource.MANGADEX)
}
}
\ No newline at end of file
diff --git a/app/src/main/java/org/xtimms/shirizu/sections/settings/sources/catalog/SourcesCatalogListProducer.kt b/app/src/main/java/org/xtimms/shirizu/sections/settings/sources/catalog/SourcesCatalogListProducer.kt
deleted file mode 100644
index f61ebe4..0000000
--- a/app/src/main/java/org/xtimms/shirizu/sections/settings/sources/catalog/SourcesCatalogListProducer.kt
+++ /dev/null
@@ -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())
-
- 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) {
- 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 {
- 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
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/org/xtimms/shirizu/sections/settings/sources/catalog/SourcesCatalogPager.kt b/app/src/main/java/org/xtimms/shirizu/sections/settings/sources/catalog/SourcesCatalogPager.kt
index dc20b5b..5507f79 100644
--- a/app/src/main/java/org/xtimms/shirizu/sections/settings/sources/catalog/SourcesCatalogPager.kt
+++ b/app/src/main/java/org/xtimms/shirizu/sections/settings/sources/catalog/SourcesCatalogPager.kt
@@ -57,11 +57,11 @@ fun SourcesCatalogPager(
items(
items = sources,
) { item ->
- item.items.forEach { source ->
+ /*item.items.forEach { source ->
SourceCatalogItem(
source = source.source,
)
- }
+ }*/
}
}
}
diff --git a/app/src/main/java/org/xtimms/shirizu/sections/settings/sources/catalog/SourcesCatalogView.kt b/app/src/main/java/org/xtimms/shirizu/sections/settings/sources/catalog/SourcesCatalogView.kt
deleted file mode 100644
index 21594b4..0000000
--- a/app/src/main/java/org/xtimms/shirizu/sections/settings/sources/catalog/SourcesCatalogView.kt
+++ /dev/null
@@ -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 }
- )
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/org/xtimms/shirizu/sections/settings/sources/catalog/SourcesCatalogViewModel.kt b/app/src/main/java/org/xtimms/shirizu/sections/settings/sources/catalog/SourcesCatalogViewModel.kt
deleted file mode 100644
index ac221d3..0000000
--- a/app/src/main/java/org/xtimms/shirizu/sections/settings/sources/catalog/SourcesCatalogViewModel.kt
+++ /dev/null
@@ -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()
- 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> = listProducers.flatMapLatest {
- val flows = it.entries.map { (type, producer) -> producer.list.map { x -> SourceCatalogPage(type, x) } }
- combine>(flows, Array::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 {
- 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)
- }
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/org/xtimms/shirizu/sections/shelf/ShelfTab.kt b/app/src/main/java/org/xtimms/shirizu/sections/shelf/ShelfTab.kt
index c0679a0..4b9e045 100644
--- a/app/src/main/java/org/xtimms/shirizu/sections/shelf/ShelfTab.kt
+++ b/app/src/main/java/org/xtimms/shirizu/sections/shelf/ShelfTab.kt
@@ -25,6 +25,7 @@ import cafe.adriel.voyager.navigator.tab.TabOptions
import org.xtimms.shirizu.R
import org.xtimms.shirizu.core.components.Scaffold
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.LoadingScreen
import org.xtimms.shirizu.sections.details.DetailsScreen
@@ -85,7 +86,7 @@ object ShelfTab : Tab, NoLiftingAppBarScreen {
currentPage = { screenModel.activeCategoryIndex },
hasActiveFilters = state.hasActiveFilters,
onChangeCurrentPage = { },
- onMangaClicked = { navigator.push(DetailsScreen(it)) },
+ onMangaClicked = { navigator.push(DetailsScreen(it.id)) },
onToggleSelection = { },
onToggleRangeSelection = {
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
diff --git a/app/src/main/java/org/xtimms/shirizu/sections/stats/ChaptersChart.kt b/app/src/main/java/org/xtimms/shirizu/sections/stats/ChaptersChart.kt
index 367abdf..99ab301 100644
--- a/app/src/main/java/org/xtimms/shirizu/sections/stats/ChaptersChart.kt
+++ b/app/src/main/java/org/xtimms/shirizu/sections/stats/ChaptersChart.kt
@@ -18,6 +18,7 @@ import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import org.koitharu.kotatsu.parsers.model.MangaChapter
+import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.xtimms.shirizu.ui.theme.colorMax
import org.xtimms.shirizu.ui.theme.colorMin
@@ -158,12 +159,12 @@ private fun PreviewChart() {
ChaptersChart(
modifier = Modifier.size(100.dp),
chapters = listOf(
- MangaChapter(id = 1, name = "", number = 1, "", "", 0L, "", MangaSource.DUMMY),
- MangaChapter(id = 1, name = "", number = 1, "", "", 0L, "", MangaSource.DUMMY),
- MangaChapter(id = 1, name = "", number = 1, "", "", 0L, "", MangaSource.DUMMY),
- MangaChapter(id = 1, name = "", number = 1, "", "", 0L, "", MangaSource.DUMMY),
- MangaChapter(id = 1, name = "", number = 1, "", "", 0L, "", MangaSource.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, "", MangaParserSource.DUMMY),
+ MangaChapter(id = 1, name = "", number = 1, "", "", 0L, "", MangaParserSource.DUMMY),
+ MangaChapter(id = 1, name = "", number = 1, "", "", 0L, "", MangaParserSource.DUMMY),
+ MangaChapter(id = 1, name = "", number = 1, "", "", 0L, "", MangaParserSource.DUMMY),
+ MangaChapter(id = 1, name = "", number = 1, "", "", 0L, "", MangaParserSource.DUMMY)
),
chartPadding = PaddingValues(vertical = 16.dp)
)
diff --git a/app/src/main/java/org/xtimms/shirizu/sections/stats/MinMaxReadCard.kt b/app/src/main/java/org/xtimms/shirizu/sections/stats/MinMaxReadCard.kt
index bd25c17..5429c38 100644
--- a/app/src/main/java/org/xtimms/shirizu/sections/stats/MinMaxReadCard.kt
+++ b/app/src/main/java/org/xtimms/shirizu/sections/stats/MinMaxReadCard.kt
@@ -20,6 +20,7 @@ import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import org.koitharu.kotatsu.parsers.model.MangaChapter
+import org.koitharu.kotatsu.parsers.model.MangaParserSource
import org.koitharu.kotatsu.parsers.model.MangaSource
import org.xtimms.shirizu.ui.theme.colorMax
import org.xtimms.shirizu.ui.theme.colorMin
@@ -98,12 +99,12 @@ fun MinMaxReadCard(
.fillMaxHeight()
.fillMaxWidth(),
chapters = listOf(
- MangaChapter(id = 1, name = "", number = 1, "", "", 0L, "", MangaSource.DUMMY),
- MangaChapter(id = 1, name = "", number = 1, "", "", 0L, "", MangaSource.DUMMY),
- MangaChapter(id = 1, name = "", number = 1, "", "", 0L, "", MangaSource.DUMMY),
- MangaChapter(id = 1, name = "", number = 1, "", "", 0L, "", MangaSource.DUMMY),
- MangaChapter(id = 1, name = "", number = 1, "", "", 0L, "", MangaSource.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, "", MangaParserSource.DUMMY),
+ MangaChapter(id = 1, name = "", number = 1, "", "", 0L, "", MangaParserSource.DUMMY),
+ MangaChapter(id = 1, name = "", number = 1, "", "", 0L, "", MangaParserSource.DUMMY),
+ MangaChapter(id = 1, name = "", number = 1, "", "", 0L, "", MangaParserSource.DUMMY),
+ MangaChapter(id = 1, name = "", number = 1, "", "", 0L, "", MangaParserSource.DUMMY)
)
)
}
diff --git a/app/src/main/java/org/xtimms/shirizu/sections/stats/categories/CategoriesChartCard.kt b/app/src/main/java/org/xtimms/shirizu/sections/stats/categories/CategoriesChartCard.kt
index b174cf5..654e06e 100644
--- a/app/src/main/java/org/xtimms/shirizu/sections/stats/categories/CategoriesChartCard.kt
+++ b/app/src/main/java/org/xtimms/shirizu/sections/stats/categories/CategoriesChartCard.kt
@@ -14,13 +14,28 @@ import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.MaterialTheme
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.Modifier
import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.res.stringResource
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.harmonize
import org.xtimms.shirizu.utils.material.harmonizeWithColor
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(
Color(0xFFF86BAE),
@@ -35,10 +50,13 @@ var baseColors = listOf(
@OptIn(ExperimentalLayoutApi::class)
@Composable
fun CategoriesChart(
- modifier: Modifier = Modifier
+ modifier: Modifier = Modifier,
+ categories: List
) {
val isNightMode = isSystemInDarkTheme()
+ val labelWithoutTag = stringResource(R.string.progress)
+ val maxDisplay = 7
val colors = baseColors.map {
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.. acc + next },
+ color = restColor,
+ )
+ }
+
+ mutableStateOf(result)
+ }
Card(
- modifier = modifier,
+ modifier = modifier.fillMaxWidth(),
shape = RoundedCornerShape(22.dp),
colors = CardDefaults.cardColors(
containerColor = combineColors(
@@ -60,25 +130,21 @@ fun CategoriesChart(
),
)
) {
- Box {
- Column(
- modifier = Modifier
- .fillMaxWidth()
- .padding(16.dp),
- verticalArrangement = Arrangement.Center,
- horizontalAlignment = Alignment.CenterHorizontally,
- ) {
- DonutChart(
- modifier = Modifier
- .padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 8.dp)
- .size(64.dp),
- items = emptyList(),
+ DonutChart(
+ modifier = Modifier
+ .padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 8.dp)
+ .size(64.dp),
+ items = tags,
+ )
+ 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
)
- FlowRow(Modifier.padding(4.dp, 4.dp)) {
-
- }
}
}
}
-
}
\ No newline at end of file
diff --git a/app/src/main/java/org/xtimms/shirizu/sections/stats/categories/DonutChart.kt b/app/src/main/java/org/xtimms/shirizu/sections/stats/categories/DonutChart.kt
index 8efbaf3..f7e6038 100644
--- a/app/src/main/java/org/xtimms/shirizu/sections/stats/categories/DonutChart.kt
+++ b/app/src/main/java/org/xtimms/shirizu/sections/stats/categories/DonutChart.kt
@@ -16,11 +16,12 @@ import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import org.xtimms.shirizu.sections.shelf.ShelfCategory
+import java.math.RoundingMode
@Composable
fun DonutChart(
modifier: Modifier = Modifier,
- items: List,
+ items: List,
chartPadding: PaddingValues = PaddingValues(0.dp),
) {
val localDensity = LocalDensity.current
@@ -42,7 +43,7 @@ fun DonutChart(
val heightWithPaddings = height - topOffset - bottomOffset
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
val gap = 0f
@@ -52,11 +53,37 @@ fun DonutChart(
val minSweepAngle = 28f
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 ->
+ val sweepAngle = itemAngles[index]
drawArc(
- Color.Black,
+ tag.color?.main ?: Color.Black,
startAngle = offset + halfGap + offsetAngle,
- sweepAngle = 36 - gap,
+ sweepAngle = sweepAngle - gap,
useCenter = false,
topLeft = Offset(startOffset + halfStrokeWidth, topOffset + halfStrokeWidth),
size = Size(widthWithPaddings - strokeWidth, heightWithPaddings - strokeWidth),
@@ -66,7 +93,7 @@ fun DonutChart(
),
)
- offset += 50
+ offset += sweepAngle
}
}
}
\ No newline at end of file
diff --git a/app/src/main/java/org/xtimms/shirizu/sections/stats/categories/TagAmount.kt b/app/src/main/java/org/xtimms/shirizu/sections/stats/categories/TagAmount.kt
index 7b2af54..e0c27b2 100644
--- a/app/src/main/java/org/xtimms/shirizu/sections/stats/categories/TagAmount.kt
+++ b/app/src/main/java/org/xtimms/shirizu/sections/stats/categories/TagAmount.kt
@@ -49,7 +49,7 @@ fun TagAmount(
)
Spacer(modifier = Modifier.width(8.dp))
Text(
- text = "56",
+ text = amount.toString(),
softWrap = false,
style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.W900),
)
diff --git a/app/src/main/java/org/xtimms/shirizu/sections/suggestions/SuggestionsScreen.kt b/app/src/main/java/org/xtimms/shirizu/sections/suggestions/SuggestionsScreen.kt
index 355a555..8b8b018 100644
--- a/app/src/main/java/org/xtimms/shirizu/sections/suggestions/SuggestionsScreen.kt
+++ b/app/src/main/java/org/xtimms/shirizu/sections/suggestions/SuggestionsScreen.kt
@@ -41,6 +41,7 @@ import org.xtimms.shirizu.R
import org.xtimms.shirizu.core.components.MangaGridItem
import org.xtimms.shirizu.core.components.ScaffoldWithTopAppBar
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.LoadingScreen
import org.xtimms.shirizu.sections.details.DetailsScreen
@@ -84,7 +85,7 @@ object SuggestionsScreen : Screen() {
SuggestionsScreenContent(
suggestions = it,
contentPadding = padding,
- onClick = { suggestion -> navigator.push(DetailsScreen(suggestion.manga)) }
+ onClick = { suggestion -> navigator.push(DetailsScreen(suggestion.manga.id)) }
)
}
}
diff --git a/app/src/main/java/org/xtimms/shirizu/utils/MultiMutex.kt b/app/src/main/java/org/xtimms/shirizu/utils/MultiMutex.kt
index f270d9d..3cea827 100644
--- a/app/src/main/java/org/xtimms/shirizu/utils/MultiMutex.kt
+++ b/app/src/main/java/org/xtimms/shirizu/utils/MultiMutex.kt
@@ -2,6 +2,9 @@ package org.xtimms.shirizu.utils
import androidx.collection.ArrayMap
import kotlinx.coroutines.sync.Mutex
+import kotlin.contracts.ExperimentalContracts
+import kotlin.contracts.InvocationKind
+import kotlin.contracts.contract
class MultiMutex : Set {
@@ -40,4 +43,17 @@ class MultiMutex : Set {
delegates.remove(element)?.unlock()
}
}
+
+ @OptIn(ExperimentalContracts::class)
+ suspend inline fun withLock(element: T, block: () -> R): R {
+ contract {
+ callsInPlace(block, InvocationKind.EXACTLY_ONCE)
+ }
+ return try {
+ lock(element)
+ block()
+ } finally {
+ unlock(element)
+ }
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/org/xtimms/shirizu/utils/system/Http.kt b/app/src/main/java/org/xtimms/shirizu/utils/system/Http.kt
index 178f5fc..eae7ae3 100644
--- a/app/src/main/java/org/xtimms/shirizu/utils/system/Http.kt
+++ b/app/src/main/java/org/xtimms/shirizu/utils/system/Http.kt
@@ -5,6 +5,7 @@ import okhttp3.HttpUrl
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
+import okhttp3.ResponseBody
import okhttp3.internal.closeQuietly
import okio.IOException
import org.json.JSONObject
@@ -71,4 +72,6 @@ fun String.sanitizeHeaderValue(): String {
private fun Char.isValidForHeaderValue(): Boolean {
// from okhttp3.Headers$Companion.checkValue
return this == '\t' || this in '\u0020'..'\u007e'
-}
\ No newline at end of file
+}
+
+fun Response.requireBody(): ResponseBody = checkNotNull(body) { "Response body is null" }
\ No newline at end of file
diff --git a/app/src/main/java/org/xtimms/shirizu/utils/system/Locale.kt b/app/src/main/java/org/xtimms/shirizu/utils/system/Locale.kt
index dccca40..0bc13fa 100644
--- a/app/src/main/java/org/xtimms/shirizu/utils/system/Locale.kt
+++ b/app/src/main/java/org/xtimms/shirizu/utils/system/Locale.kt
@@ -1,12 +1,22 @@
package org.xtimms.shirizu.utils.system
+import android.content.Context
import androidx.appcompat.app.AppCompatDelegate
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.stringResource
import androidx.core.os.LocaleListCompat
+import org.koitharu.kotatsu.parsers.util.toTitleCase
import org.xtimms.shirizu.R
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 = List(size()) { i -> getOrThrow(i) }
fun LocaleListCompat.getOrThrow(index: Int) = get(index) ?: throw NoSuchElementException()
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 12a2430..e680562 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -356,9 +356,16 @@
Shelves
History cleared
Search by reading history
- A-Z
+ Name
Date added
Show NSFW
Save
Mark as completed
+ Z-A
+ Profile
+ Menu
+ Help centre
+ %1$s, %2$s
+ External/plugin
+ Various languages
\ No newline at end of file