diff --git a/.idea/deploymentTargetDropDown.xml b/.idea/deploymentTargetDropDown.xml
index 45c52bb6..fdd365df 100644
--- a/.idea/deploymentTargetDropDown.xml
+++ b/.idea/deploymentTargetDropDown.xml
@@ -12,6 +12,6 @@
-
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
index 94226a6e..ea0fb3d8 100644
--- a/.idea/misc.xml
+++ b/.idea/misc.xml
@@ -30,6 +30,7 @@
+
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index e7a4600d..3f11696b 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -83,7 +83,7 @@ dependencies {
implementation("androidx.compose.runtime:runtime-livedata:1.0.5")
implementation("androidx.compose.ui:ui-util:1.0.5")
implementation("androidx.activity:activity-compose:1.4.0")
- implementation("androidx.navigation:navigation-compose:2.4.0-beta02")
+ implementation("androidx.navigation:navigation-compose:2.4.0-rc01")
implementation("com.google.accompanist:accompanist-flowlayout:0.20.3")
implementation("com.google.accompanist:accompanist-appcompat-theme:0.20.3")
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 3e36b72d..15ee3fb5 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -46,166 +46,6 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
fun contains(bookmark: Bookmark) = contains(bookmark.source, bookmark.itemID)
- fun contains(itemInfo: ItemInfo) = contains(itemInfo.source, itemInfo.itemID)
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(bookmark: Bookmark)
suspend fun insert(source: String, itemID: String) = insert(Bookmark(source, itemID))
- suspend fun insert(itemInfo: ItemInfo) = insert(Bookmark(itemInfo.source, itemInfo.itemID))
@Delete
suspend fun delete(bookmark: Bookmark)
suspend fun delete(source: String, itemID: String) = delete(Bookmark(source, itemID))
- suspend fun delete(itemInfo: ItemInfo) = delete(Bookmark(itemInfo.source, itemInfo.itemID))
}
\ No newline at end of file
diff --git a/app/src/main/java/xyz/quaver/pupil/sources/Common.kt b/app/src/main/java/xyz/quaver/pupil/sources/Common.kt
index f8d9b5a9..40c015f1 100644
--- a/app/src/main/java/xyz/quaver/pupil/sources/Common.kt
+++ b/app/src/main/java/xyz/quaver/pupil/sources/Common.kt
@@ -19,40 +19,23 @@
package xyz.quaver.pupil.sources
import android.app.Application
-import android.os.Parcelable
import androidx.compose.runtime.Composable
-import io.ktor.http.*
-import kotlinx.coroutines.channels.Channel
+import androidx.navigation.NavController
import org.kodein.di.*
-import xyz.quaver.pupil.sources.manatoki.Manatoki
-
-interface ItemInfo : Parcelable {
- val source: String
- val itemID: String
- val title: String
-}
-
-data class SearchResultEvent(val type: Type, val itemID: String, val payload: Parcelable? = null) {
- enum class Type {
- OPEN_READER,
- OPEN_DETAILS,
- NEW_QUERY
- }
-}
+import xyz.quaver.pupil.sources.hitomi.Hitomi
abstract class Source {
abstract val name: String
abstract val iconResID: Int
- abstract val availableSortMode: List
-
- abstract suspend fun search(query: String, range: IntRange, sortMode: Int): Pair, Int>
- abstract suspend fun images(itemID: String): List
- abstract suspend fun info(itemID: String): ItemInfo
@Composable
- open fun SearchResult(itemInfo: ItemInfo, onEvent: (SearchResultEvent) -> Unit = { }) { }
+ open fun MainScreen(navController: NavController) { }
- open fun getHeadersBuilderForImage(itemID: String, url: String): HeadersBuilder.() -> Unit = { }
+ @Composable
+ open fun Search(navController: NavController) { }
+
+ @Composable
+ open fun Reader(navController: NavController) { }
}
typealias SourceEntry = Pair
@@ -62,12 +45,12 @@ val sourceModule = DI.Module(name = "source") {
listOf<(Application) -> (Source)>(
{ Hitomi(it) },
- { Hiyobi_io(it) },
- { Manatoki(it) }
+ //{ Hiyobi_io(it) },
+ //{ Manatoki(it) }
).forEach { source ->
inSet { singleton { source(instance()).let { it.name to it } } }
}
- bind { singleton { History(di) } }
+ //bind { singleton { History(di) } }
// inSet { singleton { Downloads(di).let { it.name to it as Source } } }
}
\ No newline at end of file
diff --git a/app/src/main/java/xyz/quaver/pupil/sources/Downloads.kt b/app/src/main/java/xyz/quaver/pupil/sources/Downloads.kt
index 9180a3c3..efebcc1f 100644
--- a/app/src/main/java/xyz/quaver/pupil/sources/Downloads.kt
+++ b/app/src/main/java/xyz/quaver/pupil/sources/Downloads.kt
@@ -18,20 +18,6 @@
package xyz.quaver.pupil.sources
-import androidx.compose.runtime.Composable
-import kotlinx.coroutines.*
-import kotlinx.coroutines.channels.Channel
-import kotlinx.serialization.decodeFromString
-import kotlinx.serialization.json.Json
-import org.kodein.di.DI
-import org.kodein.di.DIAware
-import org.kodein.di.instance
-import xyz.quaver.io.FileX
-import xyz.quaver.io.util.getChild
-import xyz.quaver.pupil.R
-import xyz.quaver.pupil.util.DownloadManager
-import kotlin.math.max
-import kotlin.math.min
/*
class Downloads(override val di: DI) : Source(), DIAware {
diff --git a/app/src/main/java/xyz/quaver/pupil/sources/History.kt b/app/src/main/java/xyz/quaver/pupil/sources/History.kt
index ab0590c3..f443ed3e 100644
--- a/app/src/main/java/xyz/quaver/pupil/sources/History.kt
+++ b/app/src/main/java/xyz/quaver/pupil/sources/History.kt
@@ -18,53 +18,43 @@
package xyz.quaver.pupil.sources
-import androidx.compose.runtime.Composable
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.channels.Channel
-import kotlinx.coroutines.launch
-import org.kodein.di.DI
-import org.kodein.di.DIAware
-import org.kodein.di.direct
-import xyz.quaver.pupil.util.database
-
-class History(override val di: DI) : Source(), DIAware {
-
- private val historyDao = direct.database().historyDao()
-
- override val name: String
- get() = "history"
- override val iconResID: Int
- get() = 0 //TODO
- override val availableSortMode: List = emptyList()
-
- private val history = direct.database().historyDao()
-
- override suspend fun search(query: String, range: IntRange, sortMode: Int): Pair, Int> {
- val channel = Channel()
-
- CoroutineScope(Dispatchers.IO).launch {
-
-
- channel.close()
- }
-
- throw NotImplementedError("")
- //return Pair(channel, histories.map.size)
- }
-
- override suspend fun images(itemID: String): List {
- throw NotImplementedError("")
- }
-
- override suspend fun info(itemID: String): ItemInfo {
- throw NotImplementedError("")
- }
-
-
- @Composable
- override fun SearchResult(itemInfo: ItemInfo, onEvent: (SearchResultEvent) -> Unit) {
-
- }
-
-}
\ No newline at end of file
+//
+//class History(override val di: DI) : Source(), DIAware {
+// private val historyDao = direct.database().historyDao()
+//
+// override val name: String
+// get() = "history"
+// override val iconResID: Int
+// get() = 0 //TODO
+// override val availableSortMode: List = emptyList()
+//
+// private val history = direct.database().historyDao()
+//
+// override suspend fun search(query: String, range: IntRange, sortMode: Int): Pair, Int> {
+// val channel = Channel()
+//
+// CoroutineScope(Dispatchers.IO).launch {
+//
+//
+// channel.close()
+// }
+//
+// throw NotImplementedError("")
+// //return Pair(channel, histories.map.size)
+// }
+//
+// override suspend fun images(itemID: String): List {
+// throw NotImplementedError("")
+// }
+//
+// override suspend fun info(itemID: String): ItemInfo {
+// throw NotImplementedError("")
+// }
+//
+//
+// @Composable
+// override fun SearchResult(itemInfo: ItemInfo, onEvent: (SearchResultEvent) -> Unit) {
+//
+// }
+//
+//}
\ No newline at end of file
diff --git a/app/src/main/java/xyz/quaver/pupil/sources/Hitomi.kt b/app/src/main/java/xyz/quaver/pupil/sources/Hitomi.kt
deleted file mode 100644
index 6f64556b..00000000
--- a/app/src/main/java/xyz/quaver/pupil/sources/Hitomi.kt
+++ /dev/null
@@ -1,506 +0,0 @@
-/*
- * Pupil, Hitomi.la viewer for Android
- * Copyright (C) 2020 tom5079
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-
-package xyz.quaver.pupil.sources
-
-import android.app.Application
-import androidx.compose.foundation.Image
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.*
-import androidx.compose.foundation.shape.CircleShape
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material.*
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.Female
-import androidx.compose.material.icons.filled.Male
-import androidx.compose.material.icons.filled.Star
-import androidx.compose.material.icons.filled.StarOutline
-import androidx.compose.material.icons.outlined.Star
-import androidx.compose.material.icons.outlined.StarOutline
-import androidx.compose.runtime.*
-import androidx.compose.runtime.livedata.observeAsState
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.clip
-import androidx.compose.ui.geometry.Size
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.layout.ContentScale
-import androidx.compose.ui.res.painterResource
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.unit.dp
-import coil.annotation.ExperimentalCoilApi
-import coil.compose.rememberImagePainter
-import com.google.accompanist.flowlayout.FlowRow
-import io.ktor.http.*
-import kotlinx.coroutines.*
-import kotlinx.coroutines.channels.Channel
-import kotlinx.coroutines.sync.Mutex
-import kotlinx.coroutines.sync.withLock
-import kotlinx.parcelize.IgnoredOnParcel
-import kotlinx.parcelize.Parcelize
-import kotlinx.serialization.Serializable
-import org.kodein.di.DIAware
-import org.kodein.di.android.closestDI
-import org.kodein.di.instance
-import org.kodein.log.LoggerFactory
-import org.kodein.log.newLogger
-import xyz.quaver.hitomi.*
-import xyz.quaver.pupil.R
-import xyz.quaver.pupil.db.AppDatabase
-import xyz.quaver.pupil.db.Bookmark
-import xyz.quaver.pupil.ui.theme.Blue700
-import xyz.quaver.pupil.ui.theme.Orange500
-import xyz.quaver.pupil.ui.theme.Pink600
-import xyz.quaver.pupil.util.Preferences
-import xyz.quaver.pupil.util.wordCapitalize
-import kotlin.math.max
-import kotlin.math.min
-
-@Serializable
-@Parcelize
-data class HitomiItemInfo(
- override val itemID: String,
- override val title: String,
- val thumbnail: String,
- val artists: List,
- val series: List,
- val type: String,
- val language: String,
- val tags: List,
- private var groups: List? = null,
- private var pageCount: Int? = null,
- val characters: List? = null,
- val preview: List? = null,
- val relatedItem: List? = null
-): ItemInfo {
-
- override val source: String
- get() = "hitomi.la"
-
- @IgnoredOnParcel
- private val groupMutex = Mutex()
- suspend fun getGroups() = withContext(Dispatchers.IO) {
- if (groups != null) groups
- else groupMutex.withLock { runCatching {
- getGallery(itemID.toInt()).groups
- }.getOrNull() }
- }
-
- @IgnoredOnParcel
- private val pageCountMutex = Mutex()
- suspend fun getPageCount() = withContext(Dispatchers.IO) {
- if (pageCount != null) pageCount
-
- else pageCountMutex.withLock { runCatching {
- getGalleryInfo(itemID.toInt()).files.size.also { pageCount = it }
- }.getOrNull() }
- }
-}
-
-class Hitomi(app: Application) : Source(), DIAware {
-
- override val di by closestDI(app)
-
- private val logger = newLogger(LoggerFactory.default)
-
- private val database: AppDatabase by instance()
-
- private val bookmarkDao = database.bookmarkDao()
-
- override val name: String = "hitomi.la"
- override val iconResID: Int = R.drawable.hitomi
- override val availableSortMode: List = app.resources.getStringArray(R.array.hitomi_sort_mode).toList()
-
- var cachedQuery: String? = null
- var cachedSortMode: Int = -1
- private val cache = mutableListOf()
-
- override suspend fun search(query: String, range: IntRange, sortMode: Int): Pair, Int> = withContext(Dispatchers.IO) {
- if (cachedQuery != query || cachedSortMode != sortMode || cache.isEmpty()) {
- cachedQuery = null
- cache.clear()
- yield()
- doSearch("$query ${Preferences["hitomi.default_query", ""]}", sortMode == 1).let {
- yield()
- cache.addAll(it)
- }
- cachedQuery = query
- }
-
- val channel = Channel()
- val sanitizedRange = max(0, range.first) .. min(range.last, cache.size-1)
-
- CoroutineScope(Dispatchers.IO).launch {
- cache.slice(sanitizedRange).map {
- async {
- getGalleryBlock(it)
- }
- }.forEach {
- channel.send(transform(it.await()))
- }
-
- channel.close()
- }
-
- channel to cache.size
- }
-
- override suspend fun images(itemID: String): List {
- val galleryID = itemID.toInt()
-
- val reader = getGalleryInfo(galleryID)
-
- return reader.files.map {
- imageUrlFromImage(galleryID, it, false)
- }
- }
-
- override suspend fun info(itemID: String): HitomiItemInfo = withContext(Dispatchers.IO) {
- kotlin.runCatching {
- getGallery(itemID.toInt()).let {
- HitomiItemInfo(
- itemID,
- it.title,
- it.cover,
- it.artists,
- it.series,
- it.type,
- it.language,
- it.tags,
- it.groups,
- it.thumbnails.size,
- it.characters,
- it.thumbnails,
- it.related.map { it.toString() }
- )
- }
- }.getOrElse {
- transform(getGalleryBlock(itemID.toInt()))
- }
- }
-
- @Composable
- override fun SearchResult(itemInfo: ItemInfo, onEvent: (SearchResultEvent) -> Unit) {
- itemInfo as HitomiItemInfo
-
- FullSearchResult(itemInfo = itemInfo, onEvent = onEvent)
- }
-
- override fun getHeadersBuilderForImage(itemID: String, url: String): HeadersBuilder.() -> Unit = {
- append("Referer", getReferer(itemID.toInt()))
- }
-
- companion object {
- val languageMap = mapOf(
- "indonesian" to "Bahasa Indonesia",
- "catalan" to "català",
- "cebuano" to "Cebuano",
- "czech" to "Čeština",
- "danish" to "Dansk",
- "german" to "Deutsch",
- "estonian" to "eesti",
- "english" to "English",
- "spanish" to "Español",
- "esperanto" to "Esperanto",
- "french" to "Français",
- "italian" to "Italiano",
- "latin" to "Latina",
- "hungarian" to "magyar",
- "dutch" to "Nederlands",
- "norwegian" to "norsk",
- "polish" to "polski",
- "portuguese" to "Português",
- "romanian" to "română",
- "albanian" to "shqip",
- "slovak" to "Slovenčina",
- "finnish" to "Suomi",
- "swedish" to "Svenska",
- "tagalog" to "Tagalog",
- "vietnamese" to "tiếng việt",
- "turkish" to "Türkçe",
- "greek" to "Ελληνικά",
- "mongolian" to "Монгол",
- "russian" to "Русский",
- "ukrainian" to "Українська",
- "hebrew" to "עברית",
- "arabic" to "العربية",
- "persian" to "فارسی",
- "thai" to "ไทย",
- "korean" to "한국어",
- "chinese" to "中文",
- "japanese" to "日本語"
- )
-
- fun transform(galleryBlock: GalleryBlock) =
- HitomiItemInfo(
- galleryBlock.id.toString(),
- galleryBlock.title,
- galleryBlock.thumbnails.first(),
- galleryBlock.artists,
- galleryBlock.series,
- galleryBlock.type,
- galleryBlock.language,
- galleryBlock.relatedTags
- )
- }
-
- @OptIn(ExperimentalMaterialApi::class)
- @Composable
- fun TagChip(tag: String, isFavorite: Boolean, onClick: ((String) -> Unit)? = null, onFavoriteClick: ((String) -> Unit)? = null) {
- val tagParts = tag.split(":", limit = 2).let {
- if (it.size == 1) listOf("", it.first())
- else it
- }
-
- val icon = when (tagParts[0]) {
- "male" -> Icons.Filled.Male
- "female" -> Icons.Filled.Female
- else -> null
- }
-
- val (surfaceColor, textTint) = when {
- isFavorite -> Pair(Orange500, Color.White)
- else -> when (tagParts[0]) {
- "male" -> Pair(Blue700, Color.White)
- "female" -> Pair(Pink600, Color.White)
- else -> Pair(MaterialTheme.colors.background, MaterialTheme.colors.onBackground)
- }
- }
-
- val starIcon = if (isFavorite) Icons.Filled.Star else Icons.Outlined.StarOutline
-
- Surface(
- modifier = Modifier.padding(2.dp),
- onClick = { onClick?.invoke(tag) },
- shape = RoundedCornerShape(16.dp),
- color = surfaceColor,
- elevation = 2.dp
- ) {
- Row(
- verticalAlignment = Alignment.CenterVertically
- ) {
- if (icon != null)
- Icon(
- icon,
- contentDescription = "Icon",
- modifier = Modifier
- .padding(4.dp)
- .size(24.dp),
- tint = Color.White
- )
- else
- Box(Modifier.size(16.dp))
-
- Text(
- tagParts[1],
- color = textTint,
- style = MaterialTheme.typography.body2
- )
-
- Icon(
- starIcon,
- contentDescription = "Favorites",
- modifier = Modifier
- .padding(8.dp)
- .size(16.dp)
- .clip(CircleShape)
- .clickable { onFavoriteClick?.invoke(tag) },
- tint = textTint
- )
- }
- }
- }
-
- @OptIn(ExperimentalMaterialApi::class)
- @Composable
- fun TagGroup(tags: List) {
- var isFolded by remember { mutableStateOf(true) }
- val bookmarkedTags by bookmarkDao.getAll(name).observeAsState(emptyList())
-
- val bookmarkedTagsInList = bookmarkedTags.toSet() intersect tags.toSet()
-
- FlowRow(Modifier.padding(0.dp, 16.dp)) {
- tags.sortedBy { if (bookmarkedTagsInList.contains(it)) 0 else 1 }.let { (if (isFolded) it.take(10) else it) }.forEach { tag ->
- TagChip(
- tag = tag,
- isFavorite = bookmarkedTagsInList.contains(tag),
- onFavoriteClick = { tag ->
- val bookmarkTag = Bookmark(name, tag)
-
- CoroutineScope(Dispatchers.IO).launch {
- if (bookmarkedTagsInList.contains(tag))
- bookmarkDao.delete(bookmarkTag)
- else
- bookmarkDao.insert(bookmarkTag)
- }
- }
- )
- }
-
- if (isFolded && tags.size > 10)
- Surface(
- modifier = Modifier.padding(2.dp),
- color = MaterialTheme.colors.background,
- shape = RoundedCornerShape(16.dp),
- elevation = 2.dp,
- onClick = { isFolded = false }
- ) {
- Text(
- "…",
- modifier = Modifier.padding(16.dp, 8.dp),
- color = MaterialTheme.colors.onBackground,
- style = MaterialTheme.typography.body2
- )
- }
- }
- }
-
- @OptIn(ExperimentalCoilApi::class)
- @Composable
- fun FullSearchResult(itemInfo: HitomiItemInfo, onEvent: (SearchResultEvent) -> Unit) {
- var group by remember { mutableStateOf(emptyList()) }
- var pageCount by remember { mutableStateOf("-") }
-
- val bookmark by bookmarkDao.contains(itemInfo).observeAsState(false)
-
- LaunchedEffect(itemInfo) {
- launch(Dispatchers.Default) {
- itemInfo.getPageCount()?.let {
- pageCount = "${it}P"
- }
- }
-
- launch(Dispatchers.Default) {
- itemInfo.getGroups()?.run {
- group = this
- }
- }
- }
-
- val painter = rememberImagePainter(itemInfo.thumbnail)
-
- Column(
- modifier = Modifier.clickable { onEvent(SearchResultEvent(SearchResultEvent.Type.OPEN_READER, itemInfo.itemID, itemInfo)) }
- ) {
- Row {
- Image(
- painter = painter,
- contentDescription = null,
- modifier = Modifier
- .requiredWidth(150.dp)
- .aspectRatio(
- with(painter.intrinsicSize) { if (this == Size.Companion.Unspecified) 1f else width / height },
- true
- )
- .padding(0.dp, 0.dp, 8.dp, 0.dp)
- .align(Alignment.CenterVertically),
- contentScale = ContentScale.FillWidth
- )
- Column {
- Text(
- itemInfo.title,
- style = MaterialTheme.typography.h6,
- color = MaterialTheme.colors.onSurface
- )
-
- val artistStringBuilder = StringBuilder()
-
- with (itemInfo.artists) {
- if (this.isNotEmpty())
- artistStringBuilder.append(this.joinToString(", ") { it.wordCapitalize() })
- }
-
- if (group.isNotEmpty()) {
- if (artistStringBuilder.isNotEmpty()) artistStringBuilder.append(" ")
-
- artistStringBuilder.append("(")
- artistStringBuilder.append(group.joinToString(", ") { it.wordCapitalize() })
- artistStringBuilder.append(")")
- }
-
- if (artistStringBuilder.isNotEmpty())
- Text(
- artistStringBuilder.toString(),
- style = MaterialTheme.typography.subtitle1,
- color = MaterialTheme.colors.onSurface.copy(alpha = 0.6F)
- )
-
- if (itemInfo.series.isNotEmpty())
- Text(
- stringResource(
- id = R.string.galleryblock_series,
- itemInfo.series.joinToString { it.wordCapitalize() }
- ),
- style = MaterialTheme.typography.body2,
- color = MaterialTheme.colors.onSurface.copy(alpha = 0.6F)
- )
-
- Text(
- stringResource(id = R.string.galleryblock_type, itemInfo.type),
- style = MaterialTheme.typography.body2,
- color = MaterialTheme.colors.onSurface.copy(alpha = 0.6F)
- )
-
- languageMap[itemInfo.language]?.run {
- Text(
- stringResource(id = R.string.galleryblock_language, this),
- style = MaterialTheme.typography.body2,
- color = MaterialTheme.colors.onSurface.copy(alpha = 0.6F)
- )
- }
-
- key(itemInfo.tags) {
- TagGroup(tags = itemInfo.tags)
- }
- }
- }
-
- Divider(
- thickness = 1.dp,
- modifier = Modifier.padding(0.dp, 8.dp, 0.dp, 0.dp)
- )
-
- Row(
- modifier = Modifier
- .padding(8.dp)
- .fillMaxWidth(),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.SpaceBetween
- ) {
- Text(itemInfo.itemID)
-
- Text(pageCount)
-
- Icon(
- if (bookmark) Icons.Default.Star else Icons.Default.StarOutline,
- contentDescription = null,
- tint = Orange500,
- modifier = Modifier
- .size(32.dp)
- .clickable {
- CoroutineScope(Dispatchers.IO).launch {
- if (bookmark) bookmarkDao.delete(itemInfo)
- else bookmarkDao.insert(itemInfo)
- }
- }
- )
- }
- }
- }
-
-}
\ No newline at end of file
diff --git a/app/src/main/java/xyz/quaver/pupil/sources/Hiyobi_io.kt b/app/src/main/java/xyz/quaver/pupil/sources/Hiyobi_io.kt
index 9eefef78..a21669f2 100644
--- a/app/src/main/java/xyz/quaver/pupil/sources/Hiyobi_io.kt
+++ b/app/src/main/java/xyz/quaver/pupil/sources/Hiyobi_io.kt
@@ -1,469 +1,465 @@
-/*
- * Pupil, Hitomi.la viewer for Android
- * Copyright (C) 2021 tom5079
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-
-package xyz.quaver.pupil.sources
-
-import android.app.Application
-import android.os.Parcelable
-import androidx.compose.foundation.Image
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.*
-import androidx.compose.foundation.shape.CircleShape
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material.*
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.Female
-import androidx.compose.material.icons.filled.Male
-import androidx.compose.material.icons.filled.Star
-import androidx.compose.material.icons.filled.StarOutline
-import androidx.compose.material.icons.outlined.StarOutline
-import androidx.compose.runtime.*
-import androidx.compose.runtime.livedata.observeAsState
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.clip
-import androidx.compose.ui.geometry.Size
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.layout.ContentScale
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.unit.dp
-import coil.annotation.ExperimentalCoilApi
-import coil.compose.rememberImagePainter
-import com.google.accompanist.flowlayout.FlowRow
-import io.ktor.client.*
-import io.ktor.client.request.*
-import io.ktor.http.*
-import kotlinx.coroutines.*
-import kotlinx.coroutines.channels.Channel
-import kotlinx.parcelize.Parcelize
-import kotlinx.serialization.Serializable
-import kotlinx.serialization.json.JsonObject
-import kotlinx.serialization.json.int
-import kotlinx.serialization.json.jsonArray
-import kotlinx.serialization.json.jsonPrimitive
-import org.kodein.di.DIAware
-import org.kodein.di.android.closestDI
-import org.kodein.di.instance
-import org.kodein.log.LoggerFactory
-import org.kodein.log.newLogger
-import xyz.quaver.pupil.R
-import xyz.quaver.pupil.db.AppDatabase
-import xyz.quaver.pupil.db.Bookmark
-import xyz.quaver.pupil.ui.theme.Blue700
-import xyz.quaver.pupil.ui.theme.Orange500
-import xyz.quaver.pupil.ui.theme.Pink600
-import xyz.quaver.pupil.util.content
-import xyz.quaver.pupil.util.get
-import xyz.quaver.pupil.util.wordCapitalize
-
-@Serializable
-@Parcelize
-data class Tag(
- val male: Int?,
- val female: Int?,
- val tag: String
-) : Parcelable {
- override fun toString(): String {
- val stringBuilder = StringBuilder()
-
- stringBuilder.append(when {
- male != null -> "male"
- female != null -> "female"
- else -> "tag"
- })
- stringBuilder.append(':')
- stringBuilder.append(tag)
-
- return stringBuilder.toString()
- }
-}
-
-@Serializable
-@Parcelize
-data class HiyobiItemInfo(
- override val itemID: String,
- override val title: String,
- val thumbnail: String,
- val artists: List,
- val series: List,
- val type: String,
- val date: String,
- val bookmark: Unit?,
- val tags: List,
- val commentCount: Int,
- val pageCount: Int
-): ItemInfo {
- override val source: String
- get() = "hiyobi.io"
-}
-
-@Serializable
-data class Manga(
- val mangaId: Int,
- val title: String,
- val artist: List,
- val thumbnail: String,
- val series: List,
- val type: String,
- val date: String,
- val bookmark: Unit?,
- val tags: List,
- val commentCount: Int,
- val pageCount: Int
-)
-
-@Serializable
-data class QueryManga(
- val nowPage: Int,
- val maxPage: Int,
- val manga: List
-)
-
-@Serializable
-data class SearchResultData(
- val queryManga: QueryManga
-)
-
-@Serializable
-data class SearchResult(
- val data: SearchResultData
-)
-
-class Hiyobi_io(app: Application): Source(), DIAware {
- override val di by closestDI(app)
-
- private val logger = newLogger(LoggerFactory.default)
-
- private val database: AppDatabase by instance()
- private val bookmarkDao = database.bookmarkDao()
-
- override val name = "hiyobi.io"
- override val iconResID = R.drawable.hitomi
- override val availableSortMode = emptyList()
-
- private val client: HttpClient by instance()
-
- private suspend fun query(page: Int, tags: String): SearchResult {
- val query = "{queryManga(page:$page,tags:$tags){nowPage maxPage manga{mangaId title artist thumbnail series type date bookmark tags{male female tag} commentCount pageCount}}}"
-
- return client.get("https://api.hiyobi.io/api?query=$query")
- }
-
- private suspend fun totalCount(tags: String): Int {
- val firstPageQuery = "{queryManga(page:1,tags:$tags){maxPage}}"
- val maxPage = client.get(
- "https://api.hiyobi.io/api?query=$firstPageQuery"
- )["data"]!!["queryManga"]!!["maxPage"]!!.jsonPrimitive.int
-
- val lastPageQuery = "{queryManga(page:$maxPage,tags:$tags){manga{mangaId}}}"
- val lastPageCount = client.get(
- "https://api.hiyobi.io/api?query=$lastPageQuery"
- )["data"]!!["queryManga"]!!["manga"]!!.jsonArray.size
-
- return (maxPage-1)*25+lastPageCount
- }
-
- override suspend fun search(
- query: String,
- range: IntRange,
- sortMode: Int
- ): Pair, Int> = withContext(Dispatchers.IO) {
- val channel = Channel()
-
- val tags = parseQuery(query)
-
- logger.info {
- tags
- }
-
- CoroutineScope(Dispatchers.IO).launch {
- (range.first/25+1 .. range.last/25+1).map { page ->
- page to async { query(page, tags) }
- }.forEach { (page, result) ->
- result.await().data.queryManga.manga.forEachIndexed { index, manga ->
- if ((page-1)*25+index in range) channel.send(transform(manga))
- }
- }
-
- channel.close()
- }
-
- channel to totalCount(tags)
- }
-
- override suspend fun images(itemID: String): List = withContext(Dispatchers.IO) {
- val query = "{getManga(mangaId:$itemID){urls}}"
-
- client.post("https://api.hiyobi.io/api") {
- contentType(ContentType.Application.Json)
- body = mapOf("query" to query)
- }["data"]!!["getManga"]!!["urls"]!!.jsonArray.map { "https://api.hiyobi.io/${it.content!!}" }
- }
-
- override suspend fun info(itemID: String): ItemInfo {
- TODO("Not yet implemented")
- }
-
- @OptIn(ExperimentalMaterialApi::class)
- @Composable
- fun TagChip(tag: Tag, isFavorite: Boolean, onClick: ((Tag) -> Unit)? = null, onFavoriteClick: ((Tag) -> Unit)? = null) {
- val icon = when {
- tag.male != null -> Icons.Filled.Male
- tag.female != null -> Icons.Filled.Female
- else -> null
- }
-
- val (surfaceColor, textTint) = when {
- isFavorite -> Pair(Orange500, Color.White)
- else -> when {
- tag.male != null -> Pair(Blue700, Color.White)
- tag.female != null -> Pair(Pink600, Color.White)
- else -> Pair(MaterialTheme.colors.background, MaterialTheme.colors.onBackground)
- }
- }
-
- val starIcon = if (isFavorite) Icons.Filled.Star else Icons.Outlined.StarOutline
-
- Surface(
- modifier = Modifier.padding(2.dp),
- onClick = { onClick?.invoke(tag) },
- shape = RoundedCornerShape(16.dp),
- color = surfaceColor,
- elevation = 2.dp
- ) {
- Row(
- verticalAlignment = Alignment.CenterVertically
- ) {
- if (icon != null)
- Icon(
- icon,
- contentDescription = "Icon",
- modifier = Modifier
- .padding(4.dp)
- .size(24.dp),
- tint = Color.White
- )
- else
- Box(Modifier.size(16.dp))
-
- Text(
- tag.tag,
- color = textTint,
- style = MaterialTheme.typography.body2
- )
-
- Icon(
- starIcon,
- contentDescription = "Favorites",
- modifier = Modifier
- .padding(8.dp)
- .size(16.dp)
- .clip(CircleShape)
- .clickable { onFavoriteClick?.invoke(tag) },
- tint = textTint
- )
- }
- }
- }
-
- @OptIn(ExperimentalMaterialApi::class)
- @Composable
- fun TagGroup(tags: List) {
- var isFolded by remember { mutableStateOf(true) }
- val bookmarkedTags by bookmarkDao.getAll(name).observeAsState(emptyList())
-
- val bookmarkedTagsInList = tags.filter { it.toString() in bookmarkedTags }
-
- FlowRow(Modifier.padding(0.dp, 16.dp)) {
- tags.sortedBy { if (bookmarkedTagsInList.contains(it)) 0 else 1 }.let { (if (isFolded) it.take(10) else it) }.forEach { tag ->
- TagChip(
- tag = tag,
- isFavorite = bookmarkedTagsInList.contains(tag),
- onFavoriteClick = {
- val bookmarkTag = Bookmark(name, it.toString())
-
- CoroutineScope(Dispatchers.IO).launch {
- if (bookmarkedTagsInList.contains(it))
- bookmarkDao.delete(bookmarkTag)
- else
- bookmarkDao.insert(bookmarkTag)
- }
- }
- )
- }
-
- if (isFolded && tags.size > 10)
- Surface(
- modifier = Modifier.padding(2.dp),
- color = MaterialTheme.colors.background,
- shape = RoundedCornerShape(16.dp),
- elevation = 2.dp,
- onClick = { isFolded = false }
- ) {
- Text(
- "…",
- modifier = Modifier.padding(16.dp, 8.dp),
- color = MaterialTheme.colors.onBackground,
- style = MaterialTheme.typography.body2
- )
- }
- }
- }
-
- @OptIn(ExperimentalCoilApi::class)
- @Composable
- override fun SearchResult(itemInfo: ItemInfo, onEvent: (SearchResultEvent) -> Unit) {
- itemInfo as HiyobiItemInfo
-
- val bookmark by bookmarkDao.contains(itemInfo).observeAsState(false)
-
- val painter = rememberImagePainter(itemInfo.thumbnail)
-
- Column(
- modifier = Modifier.clickable {
- onEvent(SearchResultEvent(SearchResultEvent.Type.OPEN_READER, itemInfo.itemID, itemInfo))
- }
- ) {
- Row {
- Image(
- painter = painter,
- contentDescription = null,
- modifier = Modifier
- .requiredWidth(150.dp)
- .aspectRatio(
- with(painter.intrinsicSize) { if (this == Size.Unspecified) 1f else width / height },
- true
- )
- .padding(0.dp, 0.dp, 8.dp, 0.dp)
- .align(Alignment.CenterVertically),
- contentScale = ContentScale.FillWidth
- )
-
- Column {
- Text(
- itemInfo.title,
- style = MaterialTheme.typography.h6,
- color = MaterialTheme.colors.onSurface
- )
-
- val artistStringBuilder = StringBuilder()
-
- with(itemInfo.artists) {
- if (this.isNotEmpty())
- artistStringBuilder.append(this.joinToString(", ") { it.wordCapitalize() })
- }
-
- if (artistStringBuilder.isNotEmpty())
- Text(
- artistStringBuilder.toString(),
- style = MaterialTheme.typography.subtitle1,
- color = MaterialTheme.colors.onSurface.copy(alpha = 0.6F)
- )
-
- if (itemInfo.series.isNotEmpty())
- Text(
- stringResource(
- id = R.string.galleryblock_series,
- itemInfo.series.joinToString { it.wordCapitalize() }
- ),
- style = MaterialTheme.typography.body2,
- color = MaterialTheme.colors.onSurface.copy(alpha = 0.6F)
- )
-
- Text(
- stringResource(id = R.string.galleryblock_type, itemInfo.type),
- style = MaterialTheme.typography.body2,
- color = MaterialTheme.colors.onSurface.copy(alpha = 0.6F)
- )
-
- key(itemInfo.tags) {
- TagGroup(tags = itemInfo.tags)
- }
- }
- }
-
- Divider(
- thickness = 1.dp,
- modifier = Modifier.padding(0.dp, 8.dp, 0.dp, 0.dp)
- )
-
- Row(
- modifier = Modifier.padding(8.dp).fillMaxWidth(),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.SpaceBetween
- ) {
- Text(itemInfo.itemID)
-
- Text("${itemInfo.pageCount}P")
-
- Icon(
- if (bookmark) Icons.Default.Star else Icons.Default.StarOutline,
- contentDescription = null,
- tint = Orange500,
- modifier = Modifier
- .size(32.dp)
- .clickable {
- CoroutineScope(Dispatchers.IO).launch {
- if (bookmark) bookmarkDao.delete(itemInfo)
- else bookmarkDao.insert(itemInfo)
- }
- }
- )
- }
- }
- }
-
- companion object {
- private fun transform(manga: Manga) = HiyobiItemInfo(
- manga.mangaId.toString(),
- manga.title,
- "https://api.hiyobi.io/${manga.thumbnail}",
- manga.artist,
- manga.series,
- manga.type,
- manga.date,
- manga.bookmark,
- manga.tags,
- manga.commentCount,
- manga.pageCount
- )
-
- fun parseQuery(query: String): String {
- val queryBuilder = StringBuilder("[")
-
- if (query.isNotBlank())
- query.split(' ').filter { it.isNotBlank() }.forEach {
- val tags = it.replace('_', ' ').split(':', limit = 2)
-
- if (queryBuilder.length != 1) queryBuilder.append(',')
-
- queryBuilder.append(
- when {
- tags.size == 1 -> "{tag:\"${tags[0]}\"}"
- tags[0] == "male" -> "{male:1,tag:\"${tags[1]}\"}"
- tags[0] == "female" -> "{female:1,tag:\"${tags[1]}\"}"
- else -> "{tag:\"${tags[1]}\"}"
- }
- )
- }
-
- return queryBuilder.append(']').toString()
- }
- }
-
-}
\ No newline at end of file
+///*
+// * Pupil, Hitomi.la viewer for Android
+// * Copyright (C) 2021 tom5079
+// *
+// * This program is free software: you can redistribute it and/or modify
+// * it under the terms of the GNU General Public License as published by
+// * the Free Software Foundation, either version 3 of the License, or
+// * (at your option) any later version.
+// *
+// * This program is distributed in the hope that it will be useful,
+// * but WITHOUT ANY WARRANTY; without even the implied warranty of
+// * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// * GNU General Public License for more details.
+// *
+// * You should have received a copy of the GNU General Public License
+// * along with this program. If not, see .
+// */
+//
+//package xyz.quaver.pupil.sources
+//
+//import android.app.Application
+//import android.os.Parcelable
+//import androidx.compose.foundation.Image
+//import androidx.compose.foundation.clickable
+//import androidx.compose.foundation.layout.*
+//import androidx.compose.foundation.shape.CircleShape
+//import androidx.compose.foundation.shape.RoundedCornerShape
+//import androidx.compose.material.*
+//import androidx.compose.material.icons.Icons
+//import androidx.compose.material.icons.filled.Female
+//import androidx.compose.material.icons.filled.Male
+//import androidx.compose.material.icons.filled.Star
+//import androidx.compose.material.icons.filled.StarOutline
+//import androidx.compose.material.icons.outlined.StarOutline
+//import androidx.compose.runtime.*
+//import androidx.compose.runtime.livedata.observeAsState
+//import androidx.compose.ui.Alignment
+//import androidx.compose.ui.Modifier
+//import androidx.compose.ui.draw.clip
+//import androidx.compose.ui.geometry.Size
+//import androidx.compose.ui.graphics.Color
+//import androidx.compose.ui.layout.ContentScale
+//import androidx.compose.ui.res.stringResource
+//import androidx.compose.ui.unit.dp
+//import coil.annotation.ExperimentalCoilApi
+//import coil.compose.rememberImagePainter
+//import com.google.accompanist.flowlayout.FlowRow
+//import io.ktor.client.*
+//import io.ktor.client.request.*
+//import io.ktor.http.*
+//import kotlinx.coroutines.*
+//import kotlinx.coroutines.channels.Channel
+//import kotlinx.parcelize.Parcelize
+//import kotlinx.serialization.Serializable
+//import kotlinx.serialization.json.JsonObject
+//import kotlinx.serialization.json.int
+//import kotlinx.serialization.json.jsonArray
+//import kotlinx.serialization.json.jsonPrimitive
+//import org.kodein.di.DIAware
+//import org.kodein.di.android.closestDI
+//import org.kodein.di.instance
+//import org.kodein.log.LoggerFactory
+//import org.kodein.log.newLogger
+//import xyz.quaver.pupil.R
+//import xyz.quaver.pupil.db.AppDatabase
+//import xyz.quaver.pupil.db.Bookmark
+//import xyz.quaver.pupil.ui.theme.Blue700
+//import xyz.quaver.pupil.ui.theme.Orange500
+//import xyz.quaver.pupil.ui.theme.Pink600
+//import xyz.quaver.pupil.util.content
+//import xyz.quaver.pupil.util.get
+//import xyz.quaver.pupil.util.wordCapitalize
+//
+//@Serializable
+//@Parcelize
+//data class Tag(
+// val male: Int?,
+// val female: Int?,
+// val tag: String
+//) : Parcelable {
+// override fun toString(): String {
+// val stringBuilder = StringBuilder()
+//
+// stringBuilder.append(when {
+// male != null -> "male"
+// female != null -> "female"
+// else -> "tag"
+// })
+// stringBuilder.append(':')
+// stringBuilder.append(tag)
+//
+// return stringBuilder.toString()
+// }
+//}
+//
+//@Serializable
+//@Parcelize
+//data class HiyobiItemInfo(
+// override val itemID: String,
+// override val title: String,
+// val thumbnail: String,
+// val artists: List,
+// val series: List,
+// val type: String,
+// val date: String,
+// val bookmark: Unit?,
+// val tags: List,
+// val commentCount: Int,
+// val pageCount: Int
+//): ItemInfo {
+// override val source: String
+// get() = "hiyobi.io"
+//}
+//
+//@Serializable
+//data class Manga(
+// val mangaId: Int,
+// val title: String,
+// val artist: List,
+// val thumbnail: String,
+// val series: List,
+// val type: String,
+// val date: String,
+// val bookmark: Unit?,
+// val tags: List,
+// val commentCount: Int,
+// val pageCount: Int
+//)
+//
+//@Serializable
+//data class QueryManga(
+// val nowPage: Int,
+// val maxPage: Int,
+// val manga: List
+//)
+//
+//@Serializable
+//data class SearchResultData(
+// val queryManga: QueryManga
+//)
+//
+//@Serializable
+//data class SearchResult(
+// val data: SearchResultData
+//)
+//
+//class Hiyobi_io(app: Application): Source(), DIAware {
+// override val di by closestDI(app)
+//
+// private val logger = newLogger(LoggerFactory.default)
+//
+// private val database: AppDatabase by instance()
+// private val bookmarkDao = database.bookmarkDao()
+//
+// override val name = "hiyobi.io"
+// override val iconResID = R.drawable.hitomi
+// override val availableSortMode = emptyList()
+//
+// private val client: HttpClient by instance()
+//
+// private suspend fun query(page: Int, tags: String): SearchResult {
+// val query = "{queryManga(page:$page,tags:$tags){nowPage maxPage manga{mangaId title artist thumbnail series type date bookmark tags{male female tag} commentCount pageCount}}}"
+//
+// return client.get("https://api.hiyobi.io/api?query=$query")
+// }
+//
+// private suspend fun totalCount(tags: String): Int {
+// val firstPageQuery = "{queryManga(page:1,tags:$tags){maxPage}}"
+// val maxPage = client.get(
+// "https://api.hiyobi.io/api?query=$firstPageQuery"
+// )["data"]!!["queryManga"]!!["maxPage"]!!.jsonPrimitive.int
+//
+// val lastPageQuery = "{queryManga(page:$maxPage,tags:$tags){manga{mangaId}}}"
+// val lastPageCount = client.get(
+// "https://api.hiyobi.io/api?query=$lastPageQuery"
+// )["data"]!!["queryManga"]!!["manga"]!!.jsonArray.size
+//
+// return (maxPage-1)*25+lastPageCount
+// }
+//
+// override suspend fun search(query: String, page: Int, sortMode: Int): Pair, Int> = withContext(Dispatchers.IO) {
+// val channel = Channel()
+//
+// val tags = parseQuery(query)
+//
+// logger.info {
+// tags
+// }
+//
+// CoroutineScope(Dispatchers.IO).launch {
+// (range.first/25+1 .. range.last/25+1).map { page ->
+// page to async { query(page, tags) }
+// }.forEach { (page, result) ->
+// result.await().data.queryManga.manga.forEachIndexed { index, manga ->
+// if ((page-1)*25+index in range) channel.send(transform(manga))
+// }
+// }
+//
+// channel.close()
+// }
+//
+// channel to totalCount(tags)
+// }
+//
+// override suspend fun images(itemID: String): List = withContext(Dispatchers.IO) {
+// val query = "{getManga(mangaId:$itemID){urls}}"
+//
+// client.post("https://api.hiyobi.io/api") {
+// contentType(ContentType.Application.Json)
+// body = mapOf("query" to query)
+// }["data"]!!["getManga"]!!["urls"]!!.jsonArray.map { "https://api.hiyobi.io/${it.content!!}" }
+// }
+//
+// override suspend fun info(itemID: String): ItemInfo {
+// TODO("Not yet implemented")
+// }
+//
+// @OptIn(ExperimentalMaterialApi::class)
+// @Composable
+// fun TagChip(tag: Tag, isFavorite: Boolean, onClick: ((Tag) -> Unit)? = null, onFavoriteClick: ((Tag) -> Unit)? = null) {
+// val icon = when {
+// tag.male != null -> Icons.Filled.Male
+// tag.female != null -> Icons.Filled.Female
+// else -> null
+// }
+//
+// val (surfaceColor, textTint) = when {
+// isFavorite -> Pair(Orange500, Color.White)
+// else -> when {
+// tag.male != null -> Pair(Blue700, Color.White)
+// tag.female != null -> Pair(Pink600, Color.White)
+// else -> Pair(MaterialTheme.colors.background, MaterialTheme.colors.onBackground)
+// }
+// }
+//
+// val starIcon = if (isFavorite) Icons.Filled.Star else Icons.Outlined.StarOutline
+//
+// Surface(
+// modifier = Modifier.padding(2.dp),
+// onClick = { onClick?.invoke(tag) },
+// shape = RoundedCornerShape(16.dp),
+// color = surfaceColor,
+// elevation = 2.dp
+// ) {
+// Row(
+// verticalAlignment = Alignment.CenterVertically
+// ) {
+// if (icon != null)
+// Icon(
+// icon,
+// contentDescription = "Icon",
+// modifier = Modifier
+// .padding(4.dp)
+// .size(24.dp),
+// tint = Color.White
+// )
+// else
+// Box(Modifier.size(16.dp))
+//
+// Text(
+// tag.tag,
+// color = textTint,
+// style = MaterialTheme.typography.body2
+// )
+//
+// Icon(
+// starIcon,
+// contentDescription = "Favorites",
+// modifier = Modifier
+// .padding(8.dp)
+// .size(16.dp)
+// .clip(CircleShape)
+// .clickable { onFavoriteClick?.invoke(tag) },
+// tint = textTint
+// )
+// }
+// }
+// }
+//
+// @OptIn(ExperimentalMaterialApi::class)
+// @Composable
+// fun TagGroup(tags: List) {
+// var isFolded by remember { mutableStateOf(true) }
+// val bookmarkedTags by bookmarkDao.getAll(name).observeAsState(emptyList())
+//
+// val bookmarkedTagsInList = tags.filter { it.toString() in bookmarkedTags }
+//
+// FlowRow(Modifier.padding(0.dp, 16.dp)) {
+// tags.sortedBy { if (bookmarkedTagsInList.contains(it)) 0 else 1 }.let { (if (isFolded) it.take(10) else it) }.forEach { tag ->
+// TagChip(
+// tag = tag,
+// isFavorite = bookmarkedTagsInList.contains(tag),
+// onFavoriteClick = {
+// val bookmarkTag = Bookmark(name, it.toString())
+//
+// CoroutineScope(Dispatchers.IO).launch {
+// if (bookmarkedTagsInList.contains(it))
+// bookmarkDao.delete(bookmarkTag)
+// else
+// bookmarkDao.insert(bookmarkTag)
+// }
+// }
+// )
+// }
+//
+// if (isFolded && tags.size > 10)
+// Surface(
+// modifier = Modifier.padding(2.dp),
+// color = MaterialTheme.colors.background,
+// shape = RoundedCornerShape(16.dp),
+// elevation = 2.dp,
+// onClick = { isFolded = false }
+// ) {
+// Text(
+// "…",
+// modifier = Modifier.padding(16.dp, 8.dp),
+// color = MaterialTheme.colors.onBackground,
+// style = MaterialTheme.typography.body2
+// )
+// }
+// }
+// }
+//
+// @OptIn(ExperimentalCoilApi::class)
+// @Composable
+// override fun SearchResult(itemInfo: ItemInfo, onEvent: (SearchResultEvent) -> Unit) {
+// itemInfo as HiyobiItemInfo
+//
+// val bookmark by bookmarkDao.contains(itemInfo).observeAsState(false)
+//
+// val painter = rememberImagePainter(itemInfo.thumbnail)
+//
+// Column(
+// modifier = Modifier.clickable {
+// onEvent(SearchResultEvent(SearchResultEvent.Type.OPEN_READER, itemInfo.itemID, itemInfo))
+// }
+// ) {
+// Row {
+// Image(
+// painter = painter,
+// contentDescription = null,
+// modifier = Modifier
+// .requiredWidth(150.dp)
+// .aspectRatio(
+// with(painter.intrinsicSize) { if (this == Size.Unspecified) 1f else width / height },
+// true
+// )
+// .padding(0.dp, 0.dp, 8.dp, 0.dp)
+// .align(Alignment.CenterVertically),
+// contentScale = ContentScale.FillWidth
+// )
+//
+// Column {
+// Text(
+// itemInfo.title,
+// style = MaterialTheme.typography.h6,
+// color = MaterialTheme.colors.onSurface
+// )
+//
+// val artistStringBuilder = StringBuilder()
+//
+// with(itemInfo.artists) {
+// if (this.isNotEmpty())
+// artistStringBuilder.append(this.joinToString(", ") { it.wordCapitalize() })
+// }
+//
+// if (artistStringBuilder.isNotEmpty())
+// Text(
+// artistStringBuilder.toString(),
+// style = MaterialTheme.typography.subtitle1,
+// color = MaterialTheme.colors.onSurface.copy(alpha = 0.6F)
+// )
+//
+// if (itemInfo.series.isNotEmpty())
+// Text(
+// stringResource(
+// id = R.string.galleryblock_series,
+// itemInfo.series.joinToString { it.wordCapitalize() }
+// ),
+// style = MaterialTheme.typography.body2,
+// color = MaterialTheme.colors.onSurface.copy(alpha = 0.6F)
+// )
+//
+// Text(
+// stringResource(id = R.string.galleryblock_type, itemInfo.type),
+// style = MaterialTheme.typography.body2,
+// color = MaterialTheme.colors.onSurface.copy(alpha = 0.6F)
+// )
+//
+// key(itemInfo.tags) {
+// TagGroup(tags = itemInfo.tags)
+// }
+// }
+// }
+//
+// Divider(
+// thickness = 1.dp,
+// modifier = Modifier.padding(0.dp, 8.dp, 0.dp, 0.dp)
+// )
+//
+// Row(
+// modifier = Modifier.padding(8.dp).fillMaxWidth(),
+// verticalAlignment = Alignment.CenterVertically,
+// horizontalArrangement = Arrangement.SpaceBetween
+// ) {
+// Text(itemInfo.itemID)
+//
+// Text("${itemInfo.pageCount}P")
+//
+// Icon(
+// if (bookmark) Icons.Default.Star else Icons.Default.StarOutline,
+// contentDescription = null,
+// tint = Orange500,
+// modifier = Modifier
+// .size(32.dp)
+// .clickable {
+// CoroutineScope(Dispatchers.IO).launch {
+// if (bookmark) bookmarkDao.delete(itemInfo)
+// else bookmarkDao.insert(itemInfo)
+// }
+// }
+// )
+// }
+// }
+// }
+//
+// companion object {
+// private fun transform(manga: Manga) = HiyobiItemInfo(
+// manga.mangaId.toString(),
+// manga.title,
+// "https://api.hiyobi.io/${manga.thumbnail}",
+// manga.artist,
+// manga.series,
+// manga.type,
+// manga.date,
+// manga.bookmark,
+// manga.tags,
+// manga.commentCount,
+// manga.pageCount
+// )
+//
+// fun parseQuery(query: String): String {
+// val queryBuilder = StringBuilder("[")
+//
+// if (query.isNotBlank())
+// query.split(' ').filter { it.isNotBlank() }.forEach {
+// val tags = it.replace('_', ' ').split(':', limit = 2)
+//
+// if (queryBuilder.length != 1) queryBuilder.append(',')
+//
+// queryBuilder.append(
+// when {
+// tags.size == 1 -> "{tag:\"${tags[0]}\"}"
+// tags[0] == "male" -> "{male:1,tag:\"${tags[1]}\"}"
+// tags[0] == "female" -> "{female:1,tag:\"${tags[1]}\"}"
+// else -> "{tag:\"${tags[1]}\"}"
+// }
+// )
+// }
+//
+// return queryBuilder.append(']').toString()
+// }
+// }
+//
+//}
\ No newline at end of file
diff --git a/app/src/main/java/xyz/quaver/pupil/ui/composable/FloatingSearchBar.kt b/app/src/main/java/xyz/quaver/pupil/sources/composable/FloatingSearchBar.kt
similarity index 99%
rename from app/src/main/java/xyz/quaver/pupil/ui/composable/FloatingSearchBar.kt
rename to app/src/main/java/xyz/quaver/pupil/sources/composable/FloatingSearchBar.kt
index 8fbad34b..ded774bd 100644
--- a/app/src/main/java/xyz/quaver/pupil/ui/composable/FloatingSearchBar.kt
+++ b/app/src/main/java/xyz/quaver/pupil/sources/composable/FloatingSearchBar.kt
@@ -16,7 +16,7 @@
* along with this program. If not, see .
*/
-package xyz.quaver.pupil.ui.composable
+package xyz.quaver.pupil.sources.composable
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
diff --git a/app/src/main/java/xyz/quaver/pupil/sources/composable/ListSearchResult.kt b/app/src/main/java/xyz/quaver/pupil/sources/composable/ListSearchResult.kt
new file mode 100644
index 00000000..78af1e79
--- /dev/null
+++ b/app/src/main/java/xyz/quaver/pupil/sources/composable/ListSearchResult.kt
@@ -0,0 +1,41 @@
+/*
+ * Pupil, Hitomi.la viewer for Android
+ * Copyright (C) 2021 tom5079
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package xyz.quaver.pupil.sources.composable
+
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.items
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+
+@Composable
+fun ListSearchResult(searchResults: List, content: @Composable (T) -> Unit) {
+ LazyColumn(
+ Modifier.fillMaxSize(),
+ contentPadding = PaddingValues(0.dp, 64.dp, 0.dp, 0.dp),
+ verticalArrangement = Arrangement.spacedBy(8.dp)
+ ) {
+ items(searchResults) { itemInfo ->
+ content(itemInfo)
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/xyz/quaver/pupil/ui/composable/MultipleFloatingActionButton.kt b/app/src/main/java/xyz/quaver/pupil/sources/composable/MultipleFloatingActionButton.kt
similarity index 90%
rename from app/src/main/java/xyz/quaver/pupil/ui/composable/MultipleFloatingActionButton.kt
rename to app/src/main/java/xyz/quaver/pupil/sources/composable/MultipleFloatingActionButton.kt
index 58f36f27..203adf21 100644
--- a/app/src/main/java/xyz/quaver/pupil/ui/composable/MultipleFloatingActionButton.kt
+++ b/app/src/main/java/xyz/quaver/pupil/sources/composable/MultipleFloatingActionButton.kt
@@ -1,8 +1,24 @@
-package xyz.quaver.pupil.ui.composable
+/*
+ * Pupil, Hitomi.la viewer for Android
+ * Copyright (C) 2021 tom5079
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package xyz.quaver.pupil.sources.composable
import androidx.compose.animation.core.*
-import androidx.compose.foundation.background
-import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
@@ -16,11 +32,9 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.Modifier.Companion.any
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.draw.scale
-import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.graphics.vector.ImageVector
@@ -29,7 +43,6 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.compose.ui.tooling.preview.PreviewParameterProvider
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
-import androidx.compose.ui.util.fastAll
enum class FloatingActionButtonState(private val isExpanded: Boolean) {
COLLAPSED(false), EXPANDED(true);
diff --git a/app/src/main/java/xyz/quaver/pupil/sources/composable/ReaderBase.kt b/app/src/main/java/xyz/quaver/pupil/sources/composable/ReaderBase.kt
new file mode 100644
index 00000000..4fb4a514
--- /dev/null
+++ b/app/src/main/java/xyz/quaver/pupil/sources/composable/ReaderBase.kt
@@ -0,0 +1,311 @@
+/*
+ * Pupil, Hitomi.la viewer for Android
+ * Copyright (C) 2021 tom5079
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package xyz.quaver.pupil.sources.composable
+
+import android.app.Application
+import android.content.Intent
+import android.net.Uri
+import androidx.compose.foundation.ExperimentalFoundationApi
+import androidx.compose.foundation.border
+import androidx.compose.foundation.combinedClickable
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.itemsIndexed
+import androidx.compose.material.*
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.BrokenImage
+import androidx.compose.material.icons.filled.Fullscreen
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.hapticfeedback.HapticFeedbackType
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalHapticFeedback
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.dp
+import androidx.core.content.FileProvider
+import androidx.lifecycle.AndroidViewModel
+import androidx.lifecycle.viewModelScope
+import io.ktor.client.request.*
+import io.ktor.client.utils.*
+import io.ktor.http.*
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.launch
+import org.kodein.di.DIAware
+import org.kodein.di.android.closestDI
+import org.kodein.di.instance
+import xyz.quaver.graphics.subsampledimage.*
+import xyz.quaver.io.FileX
+import xyz.quaver.pupil.R
+import xyz.quaver.pupil.db.AppDatabase
+import xyz.quaver.pupil.util.FileXImageSource
+import xyz.quaver.pupil.util.NetworkCache
+import kotlin.math.abs
+
+open class ReaderBaseViewModel(app: Application) : AndroidViewModel(app), DIAware {
+ override val di by closestDI(app)
+
+ private val cache: NetworkCache by instance()
+
+ var isFullscreen by mutableStateOf(false)
+
+ private val database: AppDatabase by instance()
+
+ private val historyDao = database.historyDao()
+ private val bookmarkDao = database.bookmarkDao()
+
+ var error by mutableStateOf(false)
+
+ var title by mutableStateOf(null)
+
+ var imageCount by mutableStateOf(0)
+
+ private var images: List? = null
+ val imageList = mutableStateListOf()
+ val progressList = mutableStateListOf()
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ fun load(urls: List, headerBuilder: HeadersBuilder.() -> Unit = { }) {
+ viewModelScope.launch {
+ imageCount = urls.size
+
+ progressList.addAll(List(imageCount) { 0f })
+ imageList.addAll(List(imageCount) { null })
+
+ urls.forEachIndexed { index, url ->
+ when (val scheme = url.takeWhile { it != ':' }) {
+ "http", "https" -> {
+ val (channel, file) = cache.load {
+ url(url)
+ buildHeaders(headerBuilder)
+ }
+
+ if (channel.isClosedForReceive) {
+ imageList[index] = Uri.fromFile(file)
+ } else {
+ channel.invokeOnClose { e ->
+ viewModelScope.launch {
+ if (e == null) {
+ imageList[index] = Uri.fromFile(file)
+ } else {
+ error(index)
+ }
+ }
+ }
+
+ launch {
+ kotlin.runCatching {
+ for (progress in channel) {
+ progressList[index] = progress
+ }
+ }
+ }
+ }
+ }
+ "content" -> {
+ imageList[index] = Uri.parse(url)
+ progressList[index] = 1f
+ }
+ else -> throw IllegalArgumentException("Expected URL scheme 'http(s)' or 'content' but was '$scheme'")
+ }
+ }
+ }
+ }
+
+ fun error(index: Int) {
+ progressList[index] = -1f
+ }
+}
+
+@OptIn(ExperimentalFoundationApi::class)
+@Composable
+fun ReaderBase(
+ model: ReaderBaseViewModel,
+ bookmark: Boolean = false,
+ onToggleBookmark: () -> Unit = { }
+) {
+ val context = LocalContext.current
+
+ var isFABExpanded by remember { mutableStateOf(FloatingActionButtonState.COLLAPSED) }
+ val imageSources = remember { mutableStateListOf() }
+ val states = remember { mutableStateListOf() }
+
+ val scaffoldState = rememberScaffoldState()
+ val snackbarCoroutineScope = rememberCoroutineScope()
+
+ LaunchedEffect(model.imageList.count { it != null }) {
+ if (imageSources.isEmpty() && model.imageList.isNotEmpty())
+ imageSources.addAll(List(model.imageList.size) { null })
+
+ if (states.isEmpty() && model.imageList.isNotEmpty())
+ states.addAll(List(model.imageList.size) {
+ SubSampledImageState(ScaleTypes.FIT_WIDTH, Bounds.FORCE_OVERLAP_OR_CENTER).apply {
+ isGestureEnabled = true
+ }
+ })
+
+ model.imageList.forEachIndexed { i, image ->
+ if (imageSources[i] == null && image != null)
+ imageSources[i] = kotlin.runCatching {
+ FileXImageSource(FileX(context, image))
+ }.onFailure {
+ model.error(i)
+ }.getOrNull()
+ }
+ }
+
+ if (model.error)
+ stringResource(R.string.reader_failed_to_find_gallery).let {
+ snackbarCoroutineScope.launch {
+ scaffoldState.snackbarHostState.showSnackbar(
+ it,
+ duration = SnackbarDuration.Indefinite
+ )
+ }
+ }
+
+ Scaffold(
+ topBar = {
+ if (!model.isFullscreen)
+ TopAppBar(
+ title = {
+ Text(
+ model.title ?: stringResource(R.string.reader_loading),
+ color = MaterialTheme.colors.onSecondary,
+ maxLines = 1,
+ overflow = TextOverflow.Ellipsis
+ )
+ },
+ actions = {
+ //TODO
+ }
+ )
+ },
+ floatingActionButton = {
+ if (!model.isFullscreen)
+ MultipleFloatingActionButton(
+ items = listOf(
+ SubFabItem(
+ icon = Icons.Default.Fullscreen,
+ label = stringResource(id = R.string.reader_fab_fullscreen)
+ ) {
+ model.isFullscreen = true
+ }
+ ),
+ targetState = isFABExpanded,
+ onStateChanged = {
+ isFABExpanded = it
+ }
+ )
+ },
+ scaffoldState = scaffoldState,
+ snackbarHost = { scaffoldState.snackbarHostState }
+ ) {
+ Box {
+ LazyColumn(
+ Modifier.fillMaxSize(),
+ verticalArrangement = Arrangement.spacedBy(4.dp)
+ ) {
+ itemsIndexed(imageSources) { i, imageSource ->
+ Box(
+ Modifier
+ .wrapContentHeight(states[i], 500.dp)
+ .fillMaxWidth()
+ .border(1.dp, Color.Gray),
+ contentAlignment = Alignment.Center
+ ) {
+ if (imageSource == null)
+ model.progressList.getOrNull(i)?.let { progress ->
+ if (progress < 0f)
+ Icon(Icons.Filled.BrokenImage, null)
+ else
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally
+ ) {
+ LinearProgressIndicator(progress)
+ Text((i + 1).toString())
+ }
+ }
+ else {
+ val haptic = LocalHapticFeedback.current
+
+ SubSampledImage(
+ modifier = Modifier
+ .fillMaxSize()
+ .run {
+ if (model.isFullscreen)
+ doubleClickCycleZoom(states[i], 2f)
+ else
+ combinedClickable(
+ onLongClick = {
+ haptic.performHapticFeedback(
+ HapticFeedbackType.LongPress
+ )
+
+ // TODO
+ val uri = FileProvider.getUriForFile(
+ context,
+ "xyz.quaver.pupil.fileprovider",
+ (imageSource as FileXImageSource).file
+ )
+ context.startActivity(
+ Intent.createChooser(
+ Intent(
+ Intent.ACTION_SEND
+ ).apply {
+ type = "image/*"
+ putExtra(
+ Intent.EXTRA_STREAM,
+ uri
+ )
+ addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
+ }, "Share image"
+ )
+ )
+ }
+ ) {
+ model.isFullscreen = true
+ }
+ },
+ imageSource = imageSource,
+ state = states[i]
+ )
+ }
+ }
+ }
+ }
+
+ if (model.progressList.any { abs(it) != 1f })
+ LinearProgressIndicator(
+ modifier = Modifier
+ .fillMaxWidth()
+ .align(Alignment.TopCenter),
+ progress = model.progressList.map { abs(it) }.sum() / model.progressList.size,
+ color = MaterialTheme.colors.secondary
+ )
+
+ SnackbarHost(
+ scaffoldState.snackbarHostState,
+ modifier = Modifier.align(Alignment.BottomCenter)
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/xyz/quaver/pupil/sources/composable/SearchBase.kt b/app/src/main/java/xyz/quaver/pupil/sources/composable/SearchBase.kt
new file mode 100644
index 00000000..850d2562
--- /dev/null
+++ b/app/src/main/java/xyz/quaver/pupil/sources/composable/SearchBase.kt
@@ -0,0 +1,242 @@
+/*
+ * Pupil, Hitomi.la viewer for Android
+ * Copyright (C) 2021 tom5079
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package xyz.quaver.pupil.sources.composable
+
+import android.app.Application
+import androidx.appcompat.graphics.drawable.DrawerArrowDrawable
+import androidx.compose.animation.core.animateFloat
+import androidx.compose.animation.core.updateTransition
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.gestures.awaitFirstDown
+import androidx.compose.foundation.gestures.forEachGesture
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.*
+import androidx.compose.material.CircularProgressIndicator
+import androidx.compose.material.Icon
+import androidx.compose.material.Scaffold
+import androidx.compose.material.ripple.rememberRipple
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
+import androidx.compose.ui.input.nestedscroll.NestedScrollSource
+import androidx.compose.ui.input.nestedscroll.nestedScroll
+import androidx.compose.ui.input.pointer.changedToUpIgnoreConsumed
+import androidx.compose.ui.input.pointer.consumePositionChange
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.platform.LocalFocusManager
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.util.fastFirstOrNull
+import androidx.lifecycle.AndroidViewModel
+import androidx.lifecycle.viewmodel.compose.viewModel
+import com.google.accompanist.drawablepainter.rememberDrawablePainter
+import kotlin.math.max
+import kotlin.math.min
+import kotlin.math.roundToInt
+
+private enum class NavigationIconState {
+ MENU,
+ ARROW
+}
+
+open class SearchBaseViewModel(app: Application) : AndroidViewModel(app) {
+ val searchResults = mutableStateListOf()
+
+ var sortModeIndex by mutableStateOf(0)
+ private set
+
+ var currentPage by mutableStateOf(1)
+
+ var totalItems by mutableStateOf(0)
+ private set
+
+ var maxPage by mutableStateOf(0)
+ private set
+
+ val prevPageAvailable by derivedStateOf { currentPage > 1 }
+ val nextPageAvailable by derivedStateOf { currentPage <= maxPage }
+
+ var query by mutableStateOf("")
+
+ var loading by mutableStateOf(false)
+ private set
+
+ //region UI
+ var isFabVisible by mutableStateOf(true)
+ var searchBarOffset by mutableStateOf(0)
+ //endregion
+}
+
+@Composable
+fun SearchBase(
+ model: SearchBaseViewModel = viewModel(),
+ fabSubMenu: List = emptyList(),
+ actions: @Composable RowScope.() -> Unit = { },
+ onSearch: () -> Unit = { },
+ content: @Composable BoxScope.() -> Unit
+) {
+ val context = LocalContext.current
+ val focusManager = LocalFocusManager.current
+
+ var isFabExpanded by remember { mutableStateOf(FloatingActionButtonState.COLLAPSED) }
+
+ val navigationIcon = remember { DrawerArrowDrawable(context) }
+ var navigationIconState by remember { mutableStateOf(NavigationIconState.MENU) }
+ val navigationIconTransition = updateTransition(navigationIconState, label = "navigationIconTransition")
+ val navigationIconProgress by navigationIconTransition.animateFloat(
+ label = "navigationIconProgress"
+ ) { state ->
+ when (state) {
+ NavigationIconState.MENU -> 0f
+ NavigationIconState.ARROW -> 1f
+ }
+ }
+
+ val pageTurnIndicatorHeight = LocalDensity.current.run { 64.dp.toPx() }
+ val searchBarHeight = LocalDensity.current.run { 64.dp.roundToPx() }
+
+ var overscroll: Float? by remember { mutableStateOf(null) }
+
+ LaunchedEffect(navigationIconProgress) {
+ navigationIcon.progress = navigationIconProgress
+ }
+
+ Scaffold(
+ floatingActionButton = {
+ MultipleFloatingActionButton(
+ items = fabSubMenu,
+ visible = model.isFabVisible,
+ targetState = isFabExpanded,
+ onStateChanged = {
+ isFabExpanded = it
+ }
+ )
+ }
+ ) {
+ Box(Modifier.fillMaxSize()) {
+ Box(
+ modifier = Modifier
+ .offset(
+ 0.dp,
+ overscroll?.let { overscroll -> LocalDensity.current.run { overscroll.toDp() } }
+ ?: 0.dp)
+ .nestedScroll(object : NestedScrollConnection {
+ override fun onPreScroll(
+ available: Offset,
+ source: NestedScrollSource
+ ): Offset {
+ val overscrollSnapshot = overscroll
+
+ if (overscrollSnapshot == null || overscrollSnapshot == 0f) {
+ model.searchBarOffset = (model.searchBarOffset + available.y.roundToInt()).coerceIn(-searchBarHeight, 0)
+
+ model.isFabVisible = available.y > 0f
+
+ return Offset.Zero
+ } else {
+ val newOverscroll =
+ if (overscrollSnapshot > 0f && available.y < 0f)
+ max(overscrollSnapshot + available.y, 0f)
+ else if (overscrollSnapshot < 0f && available.y > 0f)
+ min(overscrollSnapshot + available.y, 0f)
+ else
+ overscrollSnapshot
+
+ return Offset(0f, newOverscroll - overscrollSnapshot).also {
+ overscroll = newOverscroll
+ }
+ }
+ }
+
+ override fun onPostScroll(
+ consumed: Offset,
+ available: Offset,
+ source: NestedScrollSource
+ ): Offset {
+ if (available.y == 0f || source == NestedScrollSource.Fling) return Offset.Zero
+
+ return overscroll?.let {
+ val newOverscroll = (it + available.y).coerceIn(
+ -pageTurnIndicatorHeight,
+ pageTurnIndicatorHeight
+ )
+
+ Offset(0f, newOverscroll - it).also {
+ overscroll = newOverscroll
+ }
+ } ?: Offset.Zero
+ }
+ }).pointerInput(Unit) {
+ forEachGesture {
+ awaitPointerEventScope {
+ val down = awaitFirstDown(requireUnconsumed = false)
+ var pointer = down.id
+ overscroll = 0f
+
+ while (true) {
+ val event = awaitPointerEvent()
+ val dragEvent = event.changes.fastFirstOrNull { it.id == pointer }!!
+
+ if (dragEvent.changedToUpIgnoreConsumed()) {
+ val otherDown = event.changes.fastFirstOrNull { it.pressed }
+ if (otherDown == null) {
+ dragEvent.consumePositionChange()
+ overscroll = null
+ break
+ } else
+ pointer = otherDown.id
+ }
+ }
+ }
+ }
+ },
+ content = content
+ )
+
+ if (model.loading)
+ CircularProgressIndicator(Modifier.align(Alignment.Center))
+
+ FloatingSearchBar(
+ modifier = Modifier.offset(0.dp, LocalDensity.current.run { model.searchBarOffset.toDp() }),
+ query = model.query,
+ onQueryChange = { model.query = it },
+ navigationIcon = {
+ Icon(
+ painter = rememberDrawablePainter(navigationIcon),
+ contentDescription = null,
+ modifier = Modifier
+ .size(24.dp)
+ .clickable(
+ interactionSource = remember { MutableInteractionSource() },
+ indication = rememberRipple(bounded = false)
+ ) {
+ focusManager.clearFocus()
+ }
+ )
+ },
+ actions = actions,
+ onTextFieldFocused = { navigationIconState = NavigationIconState.ARROW },
+ onTextFieldUnfocused = { navigationIconState = NavigationIconState.MENU; onSearch() }
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/xyz/quaver/pupil/sources/hitomi/Hitomi.kt b/app/src/main/java/xyz/quaver/pupil/sources/hitomi/Hitomi.kt
new file mode 100644
index 00000000..e6c88ac2
--- /dev/null
+++ b/app/src/main/java/xyz/quaver/pupil/sources/hitomi/Hitomi.kt
@@ -0,0 +1,160 @@
+/*
+ * Pupil, Hitomi.la viewer for Android
+ * Copyright (C) 2021 tom5079
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package xyz.quaver.pupil.sources.hitomi
+
+import android.app.Application
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Shuffle
+import androidx.compose.runtime.*
+import androidx.compose.runtime.livedata.observeAsState
+import androidx.compose.ui.res.painterResource
+import androidx.compose.ui.res.stringResource
+import androidx.lifecycle.viewmodel.compose.viewModel
+import androidx.navigation.NavController
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import org.kodein.di.DIAware
+import org.kodein.di.android.closestDI
+import org.kodein.di.compose.rememberInstance
+import org.kodein.di.instance
+import org.kodein.log.LoggerFactory
+import org.kodein.log.newLogger
+import xyz.quaver.hitomi.getGalleryInfo
+import xyz.quaver.hitomi.getReferer
+import xyz.quaver.hitomi.imageUrlFromImage
+import xyz.quaver.pupil.R
+import xyz.quaver.pupil.db.AppDatabase
+import xyz.quaver.pupil.sources.Source
+import xyz.quaver.pupil.sources.composable.*
+import xyz.quaver.pupil.sources.hitomi.composable.DetailedSearchResult
+
+class Hitomi(app: Application) : Source(), DIAware {
+ override val di by closestDI(app)
+
+ private val logger = newLogger(LoggerFactory.default)
+
+ private val database: AppDatabase by instance()
+ private val bookmarkDao = database.bookmarkDao()
+
+ override val name: String = "hitomi.la"
+ override val iconResID: Int = R.drawable.hitomi
+
+ @Composable
+ override fun MainScreen(navController: NavController) {
+ navController.navigate("search/hitomi.la") {
+ launchSingleTop = true
+ popUpTo("main") { inclusive = true }
+ }
+ }
+
+ @Composable
+ override fun Search(navController: NavController) {
+ val model: HitomiSearchResultViewModel = viewModel()
+ val database: AppDatabase by rememberInstance()
+ val bookmarkDao = remember { database.bookmarkDao() }
+ val coroutineScope = rememberCoroutineScope()
+
+ val bookmarks by bookmarkDao.getAll(name).observeAsState()
+ val bookmarkSet by derivedStateOf {
+ bookmarks?.toSet() ?: emptySet()
+ }
+
+ SearchBase(
+ model,
+ fabSubMenu = listOf(
+ SubFabItem(
+ painterResource(R.drawable.ic_jump),
+ stringResource(R.string.main_jump_title)
+ ),
+ SubFabItem(
+ Icons.Default.Shuffle,
+ stringResource(R.string.main_fab_random)
+ ),
+ SubFabItem(
+ painterResource(R.drawable.numeric),
+ stringResource(R.string.main_open_gallery_by_id)
+ )
+ ),
+ actions = {
+
+ },
+ onSearch = { model.search() }
+ ) {
+ ListSearchResult(model.searchResults) {
+ DetailedSearchResult(
+ it,
+ bookmarks = bookmarkSet,
+ onBookmarkToggle = {
+ coroutineScope.launch {
+ if (it in bookmarkSet) bookmarkDao.delete(name, it)
+ else bookmarkDao.insert(name, it)
+ }
+ }
+ ) { result ->
+ navController.navigate("reader/$name/${result.itemID}")
+ }
+ }
+ }
+ }
+
+ @Composable
+ override fun Reader(navController: NavController) {
+ val model: ReaderBaseViewModel = viewModel()
+
+ val database: AppDatabase by rememberInstance()
+ val bookmarkDao = database.bookmarkDao()
+
+ val coroutineScope = rememberCoroutineScope()
+
+ val itemID = navController.currentBackStackEntry?.arguments?.getString("itemID") ?: ""
+
+ if (itemID.isEmpty()) model.error = true
+
+ val bookmark by bookmarkDao.contains(name, itemID).observeAsState(false)
+
+ LaunchedEffect(model) {
+ launch(Dispatchers.IO) {
+ kotlin.runCatching {
+ val galleryID = itemID.toInt()
+
+ val galleryInfo = getGalleryInfo(galleryID)
+
+ model.title = galleryInfo.title
+
+ model.load(galleryInfo.files.map { imageUrlFromImage(galleryID, it, false) }) {
+ append("Referer", getReferer(galleryID))
+ }
+ }.onFailure {
+ model.error = true
+ }
+ }
+ }
+
+ ReaderBase(
+ model,
+ bookmark = bookmark,
+ onToggleBookmark = {
+ coroutineScope.launch {
+ if (itemID.isEmpty() || bookmark) bookmarkDao.delete(name, itemID)
+ else bookmarkDao.insert(name, itemID)
+ }
+ }
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/xyz/quaver/pupil/sources/hitomi/HitomiSearchResult.kt b/app/src/main/java/xyz/quaver/pupil/sources/hitomi/HitomiSearchResult.kt
new file mode 100644
index 00000000..68da8b44
--- /dev/null
+++ b/app/src/main/java/xyz/quaver/pupil/sources/hitomi/HitomiSearchResult.kt
@@ -0,0 +1,33 @@
+/*
+ * Pupil, Hitomi.la viewer for Android
+ * Copyright (C) 2021 tom5079
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package xyz.quaver.pupil.sources.hitomi
+
+import kotlinx.serialization.Serializable
+
+@Serializable
+data class HitomiSearchResult(
+ val itemID: String,
+ val title: String,
+ val thumbnail: String,
+ val artists: List,
+ val series: List,
+ val type: String,
+ val language: String,
+ val tags: List
+)
diff --git a/app/src/main/java/xyz/quaver/pupil/sources/hitomi/HitomiSearchResultViewModel.kt b/app/src/main/java/xyz/quaver/pupil/sources/hitomi/HitomiSearchResultViewModel.kt
new file mode 100644
index 00000000..0e72eaa0
--- /dev/null
+++ b/app/src/main/java/xyz/quaver/pupil/sources/hitomi/HitomiSearchResultViewModel.kt
@@ -0,0 +1,71 @@
+/*
+ * Pupil, Hitomi.la viewer for Android
+ * Copyright (C) 2021 tom5079
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package xyz.quaver.pupil.sources.hitomi
+
+import android.app.Application
+import kotlinx.coroutines.*
+import org.kodein.di.DIAware
+import org.kodein.di.android.closestDI
+import org.kodein.di.instance
+import xyz.quaver.hitomi.GalleryBlock
+import xyz.quaver.hitomi.doSearch
+import xyz.quaver.hitomi.getGalleryBlock
+import xyz.quaver.pupil.db.AppDatabase
+import xyz.quaver.pupil.sources.composable.SearchBaseViewModel
+
+class HitomiSearchResultViewModel(app: Application) : SearchBaseViewModel(app), DIAware {
+ override val di by closestDI(app)
+
+ private val database: AppDatabase by instance()
+ private val bookmarkDao = database.bookmarkDao()
+
+ init {
+ search()
+ }
+
+ private var searchJob: Job? = null
+ fun search() {
+ searchJob?.cancel()
+ searchResults.clear()
+ searchJob = CoroutineScope(Dispatchers.IO).launch {
+ val result = doSearch("female:loli")
+
+ yield()
+
+ result.take(25).forEach {
+ yield()
+ searchResults.add(transform(getGalleryBlock(it)))
+ }
+ }
+ }
+
+ companion object {
+ fun transform(galleryBlock: GalleryBlock) =
+ HitomiSearchResult(
+ galleryBlock.id.toString(),
+ galleryBlock.title,
+ galleryBlock.thumbnails.first(),
+ galleryBlock.artists,
+ galleryBlock.series,
+ galleryBlock.type,
+ galleryBlock.language,
+ galleryBlock.relatedTags
+ )
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/xyz/quaver/pupil/sources/hitomi/composable/SearchResultEntry.kt b/app/src/main/java/xyz/quaver/pupil/sources/hitomi/composable/SearchResultEntry.kt
new file mode 100644
index 00000000..64e3a909
--- /dev/null
+++ b/app/src/main/java/xyz/quaver/pupil/sources/hitomi/composable/SearchResultEntry.kt
@@ -0,0 +1,311 @@
+/*
+ * Pupil, Hitomi.la viewer for Android
+ * Copyright (C) 2021 tom5079
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ */
+
+package xyz.quaver.pupil.sources.hitomi.composable
+
+import androidx.compose.foundation.Image
+import androidx.compose.foundation.clickable
+import androidx.compose.foundation.layout.*
+import androidx.compose.foundation.shape.CircleShape
+import androidx.compose.foundation.shape.RoundedCornerShape
+import androidx.compose.material.*
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.Female
+import androidx.compose.material.icons.filled.Male
+import androidx.compose.material.icons.filled.Star
+import androidx.compose.material.icons.filled.StarOutline
+import androidx.compose.material.icons.outlined.StarOutline
+import androidx.compose.runtime.*
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.clip
+import androidx.compose.ui.geometry.Size
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.layout.ContentScale
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import coil.compose.rememberImagePainter
+import com.google.accompanist.flowlayout.FlowRow
+import xyz.quaver.pupil.R
+import xyz.quaver.pupil.sources.hitomi.HitomiSearchResult
+import xyz.quaver.pupil.ui.theme.Blue700
+import xyz.quaver.pupil.ui.theme.Orange500
+import xyz.quaver.pupil.ui.theme.Pink600
+
+private val languageMap = mapOf(
+ "indonesian" to "Bahasa Indonesia",
+ "catalan" to "català",
+ "cebuano" to "Cebuano",
+ "czech" to "Čeština",
+ "danish" to "Dansk",
+ "german" to "Deutsch",
+ "estonian" to "eesti",
+ "english" to "English",
+ "spanish" to "Español",
+ "esperanto" to "Esperanto",
+ "french" to "Français",
+ "italian" to "Italiano",
+ "latin" to "Latina",
+ "hungarian" to "magyar",
+ "dutch" to "Nederlands",
+ "norwegian" to "norsk",
+ "polish" to "polski",
+ "portuguese" to "Português",
+ "romanian" to "română",
+ "albanian" to "shqip",
+ "slovak" to "Slovenčina",
+ "finnish" to "Suomi",
+ "swedish" to "Svenska",
+ "tagalog" to "Tagalog",
+ "vietnamese" to "tiếng việt",
+ "turkish" to "Türkçe",
+ "greek" to "Ελληνικά",
+ "mongolian" to "Монгол",
+ "russian" to "Русский",
+ "ukrainian" to "Українська",
+ "hebrew" to "עברית",
+ "arabic" to "العربية",
+ "persian" to "فارسی",
+ "thai" to "ไทย",
+ "korean" to "한국어",
+ "chinese" to "中文",
+ "japanese" to "日本語"
+)
+
+private fun String.wordCapitalize() : String {
+ val result = ArrayList()
+
+ for (word in this.split(" "))
+ result.add(word.replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() })
+
+ return result.joinToString(" ")
+}
+
+@Composable
+fun DetailedSearchResult(
+ result: HitomiSearchResult,
+ bookmarks: Set,
+ onBookmarkToggle: (String) -> Unit = { },
+ onClick: (HitomiSearchResult) -> Unit = { }
+) {
+ val painter = rememberImagePainter(result.thumbnail)
+
+ Card(
+ modifier = Modifier
+ .padding(8.dp, 0.dp)
+ .fillMaxWidth()
+ .clickable { onClick(result) },
+ elevation = 4.dp
+ ) {
+ Column {
+ Row {
+ Image(
+ painter = painter,
+ contentDescription = null,
+ modifier = Modifier
+ .width(150.dp)
+ .aspectRatio(
+ with(painter.intrinsicSize) { if (this == Size.Unspecified) 1f else width / height },
+ true
+ )
+ .padding(0.dp, 0.dp, 8.dp, 0.dp)
+ .align(Alignment.CenterVertically),
+ contentScale = ContentScale.FillWidth
+ )
+ Column {
+ Text(
+ result.title,
+ style = MaterialTheme.typography.h6,
+ color = MaterialTheme.colors.onSurface
+ )
+
+ Text(
+ result.artists.joinToString { it.wordCapitalize() },
+ style = MaterialTheme.typography.subtitle1,
+ color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f)
+ )
+
+ if (result.series.isNotEmpty())
+ Text(
+ stringResource(
+ id = R.string.galleryblock_series,
+ result.series.joinToString { it.wordCapitalize() }
+ ),
+ style = MaterialTheme.typography.body2,
+ color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f)
+ )
+
+ Text(
+ stringResource(id = R.string.galleryblock_type, result.type),
+ style = MaterialTheme.typography.body2,
+ color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f)
+ )
+
+ languageMap[result.language]?.run {
+ Text(
+ stringResource(id = R.string.galleryblock_language, this),
+ style = MaterialTheme.typography.body2,
+ color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f)
+ )
+ }
+
+ key(result.tags) {
+ TagGroup(
+ tags = result.tags,
+ bookmarks,
+ onBookmarkToggle = onBookmarkToggle
+ )
+ }
+ }
+ }
+
+ Divider()
+
+ Row(
+ modifier = Modifier.fillMaxWidth().padding(8.dp),
+ verticalAlignment = Alignment.CenterVertically,
+ horizontalArrangement = Arrangement.SpaceBetween
+ ) {
+ Text(
+ result.itemID,
+ style = MaterialTheme.typography.body2,
+ color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f)
+ )
+
+ Icon(
+ if (result.itemID in bookmarks) Icons.Default.Star else Icons.Default.StarOutline,
+ contentDescription = null,
+ tint = Orange500,
+ modifier = Modifier.size(24.dp).clickable {
+ onBookmarkToggle(result.itemID)
+ }
+ )
+ }
+ }
+ }
+}
+
+@OptIn(ExperimentalMaterialApi::class)
+@Composable
+fun TagGroup(
+ tags: List,
+ bookmarks: Set,
+ onBookmarkToggle: (String) -> Unit = { }
+) {
+ var isFolded by remember { mutableStateOf(true) }
+
+ val bookmarkedTagsInList = bookmarks intersect tags.toSet()
+
+ FlowRow(Modifier.padding(0.dp, 16.dp)) {
+ tags.sortedBy { if (bookmarkedTagsInList.contains(it)) 0 else 1 }.let { (if (isFolded) it.take(10) else it) }.forEach { tag ->
+ TagChip(
+ tag = tag,
+ isFavorite = bookmarkedTagsInList.contains(tag),
+ onFavoriteClick = onBookmarkToggle
+ )
+ }
+
+ if (isFolded && tags.size > 10)
+ Surface(
+ modifier = Modifier.padding(2.dp),
+ color = MaterialTheme.colors.background,
+ shape = RoundedCornerShape(16.dp),
+ elevation = 2.dp,
+ onClick = { isFolded = false }
+ ) {
+ Text(
+ "…",
+ modifier = Modifier.padding(16.dp, 8.dp),
+ color = MaterialTheme.colors.onBackground,
+ style = MaterialTheme.typography.body2
+ )
+ }
+ }
+}
+
+@OptIn(ExperimentalMaterialApi::class)
+@Composable
+fun TagChip(
+ tag: String,
+ isFavorite: Boolean,
+ onClick: (String) -> Unit = { },
+ onFavoriteClick: (String) -> Unit = { }
+) {
+ val tagParts = tag.split(":", limit = 2).let {
+ if (it.size == 1) listOf("", it.first())
+ else it
+ }
+
+ val icon = when (tagParts[0]) {
+ "male" -> Icons.Filled.Male
+ "female" -> Icons.Filled.Female
+ else -> null
+ }
+
+ val (surfaceColor, textTint) = when {
+ isFavorite -> Pair(Orange500, Color.White)
+ else -> when (tagParts[0]) {
+ "male" -> Pair(Blue700, Color.White)
+ "female" -> Pair(Pink600, Color.White)
+ else -> Pair(MaterialTheme.colors.background, MaterialTheme.colors.onBackground)
+ }
+ }
+
+ val starIcon = if (isFavorite) Icons.Filled.Star else Icons.Outlined.StarOutline
+
+ Surface(
+ modifier = Modifier.padding(2.dp),
+ onClick = { onClick(tag) },
+ shape = RoundedCornerShape(16.dp),
+ color = surfaceColor,
+ elevation = 2.dp
+ ) {
+ Row(
+ verticalAlignment = Alignment.CenterVertically
+ ) {
+ if (icon != null)
+ Icon(
+ icon,
+ contentDescription = "Icon",
+ modifier = Modifier
+ .padding(4.dp)
+ .size(24.dp),
+ tint = Color.White
+ )
+ else
+ Box(Modifier.size(16.dp))
+
+ Text(
+ tagParts[1],
+ color = textTint,
+ style = MaterialTheme.typography.body2
+ )
+
+ Icon(
+ starIcon,
+ contentDescription = "Favorites",
+ modifier = Modifier
+ .padding(8.dp)
+ .size(16.dp)
+ .clip(CircleShape)
+ .clickable { onFavoriteClick(tag) },
+ tint = textTint
+ )
+ }
+ }
+}
diff --git a/app/src/main/java/xyz/quaver/pupil/sources/manatoki/Manatoki.kt b/app/src/main/java/xyz/quaver/pupil/sources/manatoki/Manatoki.kt
index 72473d7e..1d9c8eac 100644
--- a/app/src/main/java/xyz/quaver/pupil/sources/manatoki/Manatoki.kt
+++ b/app/src/main/java/xyz/quaver/pupil/sources/manatoki/Manatoki.kt
@@ -1,101 +1,101 @@
-/*
- * Pupil, Hitomi.la viewer for Android
- * Copyright (C) 2021 tom5079
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-
-package xyz.quaver.pupil.sources.manatoki
-
-import android.app.Application
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.channels.Channel
-import kotlinx.coroutines.coroutineScope
-import kotlinx.coroutines.withContext
-import kotlinx.parcelize.Parcelize
-import org.jsoup.Jsoup
-import org.kodein.di.DIAware
-import org.kodein.di.android.closestDI
-import org.kodein.log.LoggerFactory
-import org.kodein.log.newLogger
-import xyz.quaver.pupil.R
-import xyz.quaver.pupil.sources.ItemInfo
-import xyz.quaver.pupil.sources.Source
-
-@Parcelize
-class ManatokiItemInfo(
- override val itemID: String,
- override val title: String
-) : ItemInfo {
- override val source: String = "manatoki.net"
-}
-
-class Manatoki(app: Application) : Source(), DIAware {
- override val di by closestDI(app)
-
- private val logger = newLogger(LoggerFactory.default)
-
- override val name = "manatoki.net"
- override val availableSortMode: List = emptyList()
- override val iconResID: Int = R.drawable.manatoki
-
- override suspend fun search(
- query: String,
- range: IntRange,
- sortMode: Int
- ): Pair, Int> {
- TODO("Not yet implemented")
- }
-
- override suspend fun images(itemID: String): List = coroutineScope {
- val jsoup = withContext(Dispatchers.IO) {
- Jsoup.connect("https://manatoki116.net/comic/$itemID").get()
- }
-
- val htmlData = jsoup
- .selectFirst(".view-padding > script")!!
- .data()
- .splitToSequence('\n')
- .fold(StringBuilder()) { sb, line ->
- if (!line.startsWith("html_data")) return@fold sb
-
- line.drop(12).dropLast(2).split('.').forEach {
- if (it.isNotBlank()) sb.appendCodePoint(it.toInt(16))
- }
- sb
- }.toString()
-
- Jsoup.parse(htmlData)
- .select("img[^data-]:not([style])")
- .map {
- it.attributes()
- .first { it.key.startsWith("data-") }
- .value
- }
- }
-
- override suspend fun info(itemID: String): ItemInfo = coroutineScope {
- val jsoup = withContext(Dispatchers.IO) {
- Jsoup.connect("https://manatoki116.net/comic/$itemID").get()
- }
-
- val title = jsoup.selectFirst(".toon-title")!!.ownText()
-
- ManatokiItemInfo(
- itemID,
- title
- )
- }
-
-}
\ No newline at end of file
+///*
+// * Pupil, Hitomi.la viewer for Android
+// * Copyright (C) 2021 tom5079
+// *
+// * This program is free software: you can redistribute it and/or modify
+// * it under the terms of the GNU General Public License as published by
+// * the Free Software Foundation, either version 3 of the License, or
+// * (at your option) any later version.
+// *
+// * This program is distributed in the hope that it will be useful,
+// * but WITHOUT ANY WARRANTY; without even the implied warranty of
+// * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+// * GNU General Public License for more details.
+// *
+// * You should have received a copy of the GNU General Public License
+// * along with this program. If not, see .
+// */
+//
+//package xyz.quaver.pupil.sources.manatoki
+//
+//import android.app.Application
+//import kotlinx.coroutines.Dispatchers
+//import kotlinx.coroutines.channels.Channel
+//import kotlinx.coroutines.coroutineScope
+//import kotlinx.coroutines.withContext
+//import kotlinx.parcelize.Parcelize
+//import org.jsoup.Jsoup
+//import org.kodein.di.DIAware
+//import org.kodein.di.android.closestDI
+//import org.kodein.log.LoggerFactory
+//import org.kodein.log.newLogger
+//import xyz.quaver.pupil.R
+//import xyz.quaver.pupil.sources.ItemInfo
+//import xyz.quaver.pupil.sources.Source
+//
+//@Parcelize
+//class ManatokiItemInfo(
+// override val itemID: String,
+// override val title: String
+//) : ItemInfo {
+// override val source: String = "manatoki.net"
+//}
+//
+//class Manatoki(app: Application) : Source(), DIAware {
+// override val di by closestDI(app)
+//
+// private val logger = newLogger(LoggerFactory.default)
+//
+// override val name = "manatoki.net"
+// override val availableSortMode: List = emptyList()
+// override val iconResID: Int = R.drawable.manatoki
+//
+// override suspend fun search(
+// query: String,
+// range: IntRange,
+// sortMode: Int
+// ): Pair, Int> {
+// TODO("Not yet implemented")
+// }
+//
+// override suspend fun images(itemID: String): List = coroutineScope {
+// val jsoup = withContext(Dispatchers.IO) {
+// Jsoup.connect("https://manatoki116.net/comic/$itemID").get()
+// }
+//
+// val htmlData = jsoup
+// .selectFirst(".view-padding > script")!!
+// .data()
+// .splitToSequence('\n')
+// .fold(StringBuilder()) { sb, line ->
+// if (!line.startsWith("html_data")) return@fold sb
+//
+// line.drop(12).dropLast(2).split('.').forEach {
+// if (it.isNotBlank()) sb.appendCodePoint(it.toInt(16))
+// }
+// sb
+// }.toString()
+//
+// Jsoup.parse(htmlData)
+// .select("img[^data-]:not([style])")
+// .map {
+// it.attributes()
+// .first { it.key.startsWith("data-") }
+// .value
+// }
+// }
+//
+// override suspend fun info(itemID: String): ItemInfo = coroutineScope {
+// val jsoup = withContext(Dispatchers.IO) {
+// Jsoup.connect("https://manatoki116.net/comic/$itemID").get()
+// }
+//
+// val title = jsoup.selectFirst(".toon-title")!!.ownText()
+//
+// ManatokiItemInfo(
+// itemID,
+// title
+// )
+// }
+//
+//}
\ No newline at end of file
diff --git a/app/src/main/java/xyz/quaver/pupil/ui/MainActivity.kt b/app/src/main/java/xyz/quaver/pupil/ui/MainActivity.kt
index a16f26ab..9a4c3384 100644
--- a/app/src/main/java/xyz/quaver/pupil/ui/MainActivity.kt
+++ b/app/src/main/java/xyz/quaver/pupil/ui/MainActivity.kt
@@ -18,60 +18,23 @@
package xyz.quaver.pupil.ui
-import android.content.Intent
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.viewModels
-import androidx.appcompat.graphics.drawable.DrawerArrowDrawable
import androidx.compose.animation.ExperimentalAnimationApi
-import androidx.compose.animation.core.animateFloat
-import androidx.compose.animation.core.updateTransition
-import androidx.compose.foundation.*
-import androidx.compose.foundation.gestures.*
-import androidx.compose.foundation.interaction.MutableInteractionSource
-import androidx.compose.foundation.layout.*
-import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.lazy.items
-import androidx.compose.material.*
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.*
-import androidx.compose.material.ripple.rememberRipple
-import androidx.compose.runtime.*
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
-import androidx.compose.ui.input.nestedscroll.NestedScrollSource
-import androidx.compose.ui.input.nestedscroll.nestedScroll
-import androidx.compose.ui.input.pointer.*
-import androidx.compose.ui.platform.LocalDensity
-import androidx.compose.ui.platform.LocalFocusManager
-import androidx.compose.ui.res.painterResource
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.util.fastFirstOrNull
-import com.google.accompanist.drawablepainter.rememberDrawablePainter
-import kotlinx.coroutines.*
+import androidx.navigation.compose.NavHost
+import androidx.navigation.compose.composable
+import androidx.navigation.compose.rememberNavController
import org.kodein.di.DIAware
import org.kodein.di.android.closestDI
+import org.kodein.di.direct
import org.kodein.log.LoggerFactory
import org.kodein.log.newLogger
-import xyz.quaver.pupil.*
-import xyz.quaver.pupil.R
-import xyz.quaver.pupil.sources.SearchResultEvent
-import xyz.quaver.pupil.ui.composable.*
-import xyz.quaver.pupil.ui.dialog.OpenWithItemIDDialog
-import xyz.quaver.pupil.ui.dialog.SourceSelectDialog
import xyz.quaver.pupil.ui.theme.PupilTheme
import xyz.quaver.pupil.ui.viewmodel.MainViewModel
-import xyz.quaver.pupil.util.*
-import kotlin.math.*
+import xyz.quaver.pupil.util.source
-private enum class NavigationIconState {
- MENU,
- ARROW
-}
class MainActivity : ComponentActivity(), DIAware {
override val di by closestDI()
@@ -86,251 +49,22 @@ class MainActivity : ComponentActivity(), DIAware {
setContent {
PupilTheme {
- val focusManager = LocalFocusManager.current
+ val navController = rememberNavController()
- val maxPage by model.maxPage.collectAsState(0)
-
- val pageTurnIndicatorHeight = LocalDensity.current.run { 64.dp.toPx() }
-
- val prevPageAvailable by derivedStateOf {
- model.currentPage > 1
- }
-
- val nextPageAvailable by derivedStateOf {
- model.currentPage <= maxPage
- }
-
- var overscroll: Float? by remember { mutableStateOf(null) }
-
- var isFabExpanded by remember { mutableStateOf(FloatingActionButtonState.COLLAPSED) }
- var isFabVisible by remember { mutableStateOf(true) }
-
- val searchBarHeight = LocalDensity.current.run { 56.dp.roundToPx() }
- var searchBarOffset by remember { mutableStateOf(0) }
-
- val navigationIcon = remember { DrawerArrowDrawable(this) }
- var navigationIconState by remember { mutableStateOf(NavigationIconState.MENU) }
- val navigationIconTransition = updateTransition(navigationIconState, label = "navigationIconTransition")
- val navigationIconProgress by navigationIconTransition.animateFloat(
- label = "navigationIconProgress"
- ) { state ->
- when (state) {
- NavigationIconState.MENU -> 0f
- NavigationIconState.ARROW -> 1f
- }
- }
-
- val onSearchResultEvent: (SearchResultEvent) -> Unit = { event ->
- when (event.type) {
- SearchResultEvent.Type.OPEN_READER -> {
- startActivity(
- Intent(
- this@MainActivity,
- ReaderActivity::class.java
- ).apply {
- putExtra("source", model.source.name)
- putExtra("id", event.itemID)
- putExtra("payload", event.payload)
- })
- }
- else -> TODO("")
- }
- }
-
- var sourceSelectDialog by remember { mutableStateOf(false) }
- var openWithItemIDDialog by remember { mutableStateOf(false) }
-
- LaunchedEffect(navigationIconProgress) {
- navigationIcon.progress = navigationIconProgress
- }
-
- if (sourceSelectDialog)
- SourceSelectDialog(
- currentSource = model.source.name,
- onDismissRequest = { sourceSelectDialog = false }
- ) { source ->
- sourceSelectDialog = false
- model.setSourceAndReset(source.name)
+ NavHost(navController, startDestination = "main/{source}") {
+ composable("main/{source}") {
+ direct.source(it.arguments?.getString("source") ?: "hitomi.la")
+ .MainScreen(navController)
}
- if (openWithItemIDDialog)
- OpenWithItemIDDialog {
- openWithItemIDDialog = false
-
- it?.let {
- onSearchResultEvent(SearchResultEvent(
- SearchResultEvent.Type.OPEN_READER,
- it
- ))
- }
+ composable("search/{source}") {
+ direct.source(it.arguments?.getString("source") ?: "hitomi.la")
+ .Search(navController)
}
- Scaffold(
- floatingActionButton = {
- MultipleFloatingActionButton(
- listOf(
- SubFabItem(
- Icons.Default.Block,
- stringResource(R.string.main_fab_cancel)
- ),
- SubFabItem(
- painterResource(R.drawable.ic_jump),
- stringResource(R.string.main_jump_title)
- ),
- SubFabItem(
- Icons.Default.Shuffle,
- stringResource(R.string.main_fab_random)
- ),
- SubFabItem(
- painterResource(R.drawable.numeric),
- stringResource(R.string.main_open_gallery_by_id)
- ) {
- openWithItemIDDialog = true
- }
- ),
- visible = isFabVisible,
- targetState = isFabExpanded,
- onStateChanged = {
- isFabExpanded = it
- }
- )
- }
- ) {
- Box(Modifier.fillMaxSize()) {
- LazyColumn(
- Modifier
- .fillMaxSize()
- .offset(0.dp, overscroll?.let { overscroll -> LocalDensity.current.run { overscroll.toDp() } } ?: 0.dp)
- .nestedScroll(object : NestedScrollConnection {
- override fun onPreScroll(
- available: Offset,
- source: NestedScrollSource
- ): Offset {
- val overscrollSnapshot = overscroll
-
- if (overscrollSnapshot == null || overscrollSnapshot == 0f) {
- searchBarOffset =
- (searchBarOffset + available.y.roundToInt()).coerceIn(
- -searchBarHeight,
- 0
- )
-
- isFabVisible = available.y > 0f
-
- return Offset.Zero
- } else {
- val newOverscroll =
- if (overscrollSnapshot > 0f && available.y < 0f)
- max(overscrollSnapshot + available.y, 0f)
- else if (overscrollSnapshot < 0f && available.y > 0f)
- min(overscrollSnapshot + available.y, 0f)
- else
- overscrollSnapshot
-
- return Offset(0f, newOverscroll - overscrollSnapshot).also {
- overscroll = newOverscroll
- }
- }
- }
-
- override fun onPostScroll(
- consumed: Offset,
- available: Offset,
- source: NestedScrollSource
- ): Offset {
- if (available.y == 0f || source == NestedScrollSource.Fling) return Offset.Zero
-
- return overscroll?.let {
- val newOverscroll = (it + available.y).coerceIn(-pageTurnIndicatorHeight, pageTurnIndicatorHeight)
-
- Offset(0f, newOverscroll - it).also {
- overscroll = newOverscroll
- }
- } ?: Offset.Zero
- }
- }).pointerInput(Unit) {
- forEachGesture {
- awaitPointerEventScope {
- val down = awaitFirstDown(requireUnconsumed = false)
- var pointer = down.id
- overscroll = 0f
-
- while (true) {
- val event = awaitPointerEvent()
- val dragEvent = event.changes.fastFirstOrNull { it.id == pointer }!!
-
- if (dragEvent.changedToUpIgnoreConsumed()) {
- val otherDown = event.changes.fastFirstOrNull { it.pressed }
- if (otherDown == null) {
- dragEvent.consumePositionChange()
- overscroll = null
- break
- }
- else
- pointer = otherDown.id
- }
- }
- }
- }
- },
- contentPadding = PaddingValues(0.dp, 56.dp, 0.dp, 0.dp)
- ) {
- items(model.searchResults, key = { it.itemID }) { itemInfo ->
- ProgressCard(
- progress = 0.5f
- ) {
- model.source.SearchResult(itemInfo = itemInfo, onEvent = onSearchResultEvent)
- }
- }
- }
-
- if (model.loading)
- CircularProgressIndicator(Modifier.align(Alignment.Center))
-
- FloatingSearchBar(
- modifier = Modifier.offset(0.dp, LocalDensity.current.run { searchBarOffset.toDp() }),
- query = model.query,
- onQueryChange = { model.query = it },
- navigationIcon = {
- Icon(
- painter = rememberDrawablePainter(navigationIcon),
- contentDescription = null,
- modifier = Modifier
- .size(24.dp)
- .clickable(
- interactionSource = remember { MutableInteractionSource() },
- indication = rememberRipple(bounded = false)
- ) {
- focusManager.clearFocus()
- }
- )
- },
- actions = {
- Image(
- painterResource(model.source.iconResID),
- contentDescription = null,
- modifier = Modifier
- .size(24.dp)
- .clickable {
- sourceSelectDialog = true
- }
- )
- Icon(
- Icons.Default.Sort,
- contentDescription = null,
- modifier = Modifier.size(24.dp),
- tint = MaterialTheme.colors.onSurface.copy(alpha = 0.5f)
- )
- Icon(
- Icons.Default.Settings,
- contentDescription = null,
- modifier = Modifier.size(24.dp),
- tint = MaterialTheme.colors.onSurface.copy(alpha = 0.5f)
- )
- },
- onTextFieldFocused = { navigationIconState = NavigationIconState.ARROW },
- onTextFieldUnfocused = { navigationIconState = NavigationIconState.MENU; model.resetAndQuery() }
- )
+ composable("reader/{source}/{itemID}") {
+ direct.source(it.arguments?.getString("source") ?: "hitomi.la")
+ .Reader(navController)
}
}
}
diff --git a/app/src/main/java/xyz/quaver/pupil/ui/ReaderActivity.kt b/app/src/main/java/xyz/quaver/pupil/ui/ReaderActivity.kt
deleted file mode 100644
index 0b73e7e9..00000000
--- a/app/src/main/java/xyz/quaver/pupil/ui/ReaderActivity.kt
+++ /dev/null
@@ -1,278 +0,0 @@
-/*
- * Pupil, Hitomi.la viewer for Android
- * Copyright (C) 2019 tom5079
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-
-package xyz.quaver.pupil.ui
-
-import android.content.ClipData
-import android.content.Intent
-import android.net.Uri
-import android.os.Bundle
-import androidx.activity.ComponentActivity
-import androidx.activity.compose.setContent
-import androidx.activity.viewModels
-import androidx.compose.foundation.*
-import androidx.compose.foundation.gestures.detectTapGestures
-import androidx.compose.foundation.layout.*
-import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.lazy.itemsIndexed
-import androidx.compose.material.*
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.BrokenImage
-import androidx.compose.material.icons.filled.Fullscreen
-import androidx.compose.material.icons.filled.Star
-import androidx.compose.material.icons.filled.StarOutline
-import androidx.compose.runtime.*
-import androidx.compose.runtime.livedata.observeAsState
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.hapticfeedback.HapticFeedbackType
-import androidx.compose.ui.input.pointer.pointerInput
-import androidx.compose.ui.platform.LocalHapticFeedback
-import androidx.compose.ui.res.painterResource
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.style.TextOverflow
-import androidx.compose.ui.unit.dp
-import androidx.core.content.FileProvider
-import androidx.core.view.WindowInsetsCompat
-import androidx.core.view.WindowInsetsControllerCompat
-import coil.annotation.ExperimentalCoilApi
-import kotlinx.coroutines.launch
-import org.kodein.di.DIAware
-import org.kodein.di.android.closestDI
-import org.kodein.log.LoggerFactory
-import org.kodein.log.newLogger
-import xyz.quaver.graphics.subsampledimage.*
-import xyz.quaver.io.FileX
-import xyz.quaver.pupil.R
-import xyz.quaver.pupil.ui.composable.FloatingActionButtonState
-import xyz.quaver.pupil.ui.composable.MultipleFloatingActionButton
-import xyz.quaver.pupil.ui.composable.SubFabItem
-import xyz.quaver.pupil.ui.theme.Orange500
-import xyz.quaver.pupil.ui.theme.PupilTheme
-import xyz.quaver.pupil.ui.viewmodel.ReaderViewModel
-import xyz.quaver.pupil.util.FileXImageSource
-import kotlin.math.abs
-
-class ReaderActivity : ComponentActivity(), DIAware {
- override val di by closestDI()
-
- private val model: ReaderViewModel by viewModels()
-
- private val logger = newLogger(LoggerFactory.default)
-
- @OptIn(ExperimentalCoilApi::class, ExperimentalFoundationApi::class)
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
-
- model.handleIntent(intent)
- model.load()
-
- setContent {
- var isFABExpanded by remember { mutableStateOf(FloatingActionButtonState.COLLAPSED) }
- val imageSources = remember { mutableStateListOf() }
- val states = remember { mutableStateListOf() }
- val bookmark by model.bookmark.observeAsState(false)
-
- val scaffoldState = rememberScaffoldState()
- val snackbarCoroutineScope = rememberCoroutineScope()
-
- LaunchedEffect(model.imageList.count { it != null }) {
- if (imageSources.isEmpty() && model.imageList.isNotEmpty())
- imageSources.addAll(List(model.imageList.size) { null })
-
- if (states.isEmpty() && model.imageList.isNotEmpty())
- states.addAll(List(model.imageList.size) { SubSampledImageState(ScaleTypes.FIT_WIDTH, Bounds.FORCE_OVERLAP_OR_CENTER).apply {
- isGestureEnabled = true
- } })
-
- model.imageList.forEachIndexed { i, image ->
- if (imageSources[i] == null && image != null)
- imageSources[i] = kotlin.runCatching {
- FileXImageSource(FileX(this@ReaderActivity, image))
- }.onFailure {
- logger.warning(it)
- model.error(i)
- }.getOrNull()
- }
- }
-
- WindowInsetsControllerCompat(window, window.decorView).run {
- if (model.isFullscreen) {
- hide(WindowInsetsCompat.Type.systemBars())
- systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
- } else
- show(WindowInsetsCompat.Type.systemBars())
- }
-
- if (model.error)
- stringResource(R.string.reader_failed_to_find_gallery).let {
- snackbarCoroutineScope.launch {
- scaffoldState.snackbarHostState.showSnackbar(it, duration = SnackbarDuration.Indefinite)
- }
- }
-
- PupilTheme {
- Scaffold(
- topBar = {
- if (!model.isFullscreen)
- TopAppBar(
- title = {
- Text(
- model.title ?: stringResource(R.string.reader_loading),
- color = MaterialTheme.colors.onSecondary,
- maxLines = 1,
- overflow = TextOverflow.Ellipsis
- )
- },
- actions = {
- Row(
- modifier = Modifier.padding(16.dp, 0.dp),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.spacedBy(24.dp)
- ) {
- Icon(
- if (bookmark) Icons.Default.Star else Icons.Default.StarOutline,
- contentDescription = null,
- tint = Orange500,
- modifier = Modifier.size(24.dp).clickable {
- model.toggleBookmark()
- }
- )
- model.sourceIcon?.let { sourceIcon ->
- Image(
- modifier = Modifier.size(24.dp),
- painter = painterResource(id = sourceIcon),
- contentDescription = null
- )
- }
- }
- }
- )
- },
- floatingActionButton = {
- if (!model.isFullscreen)
- MultipleFloatingActionButton(
- items = listOf(
- SubFabItem(
- icon = Icons.Default.Fullscreen,
- label = stringResource(id = R.string.reader_fab_fullscreen)
- ) {
- model.isFullscreen = true
- }
- ),
- targetState = isFABExpanded,
- onStateChanged = {
- isFABExpanded = it
- }
- )
- },
- scaffoldState = scaffoldState,
- snackbarHost = { scaffoldState.snackbarHostState }
- ) {
- Box {
- LazyColumn(
- Modifier.fillMaxSize(),
- verticalArrangement = Arrangement.spacedBy(4.dp)
- ) {
- itemsIndexed(imageSources) { i, imageSource ->
- Box(
- Modifier
- .wrapContentHeight(states[i], 500.dp)
- .fillMaxWidth()
- .border(1.dp, Color.Gray),
- contentAlignment = Alignment.Center
- ) {
- if (imageSource == null)
- model.progressList.getOrNull(i)?.let { progress ->
- if (progress < 0f)
- Icon(Icons.Filled.BrokenImage, null)
- else
- Column(
- horizontalAlignment = Alignment.CenterHorizontally
- ) {
- LinearProgressIndicator(progress)
- Text((i + 1).toString())
- }
- }
- else {
- val haptic = LocalHapticFeedback.current
-
- SubSampledImage(
- modifier = Modifier
- .fillMaxSize()
- .run {
- if (model.isFullscreen)
- doubleClickCycleZoom(states[i], 2f)
- else
- combinedClickable(
- onLongClick = {
- haptic.performHapticFeedback(HapticFeedbackType.LongPress)
-
- // TODO
- val uri = FileProvider.getUriForFile(this@ReaderActivity, "xyz.quaver.pupil.fileprovider", (imageSource as FileXImageSource).file)
- startActivity(Intent.createChooser(Intent(Intent.ACTION_SEND).apply {
- type = "image/*"
- putExtra(Intent.EXTRA_STREAM, uri)
- addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
- }, "Share image"))
- }
- ) {
- model.isFullscreen = true
- }
- },
- imageSource = imageSource,
- state = states[i]
- )
- }
- }
- }
- }
-
- if (model.totalProgress != model.imageCount)
- LinearProgressIndicator(
- modifier = Modifier
- .fillMaxWidth()
- .align(Alignment.TopCenter),
- progress = model.progressList.map { abs(it) }
- .sum() / model.progressList.size,
- color = MaterialTheme.colors.secondary
- )
-
- SnackbarHost(
- scaffoldState.snackbarHostState,
- modifier = Modifier.align(Alignment.BottomCenter)
- )
- }
- }
- }
- }
- }
-
- override fun onNewIntent(intent: Intent) {
- super.onNewIntent(intent)
- model.handleIntent(intent)
- }
-
- override fun onBackPressed() {
- when {
- model.isFullscreen -> model.isFullscreen = false
- else -> super.onBackPressed()
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/xyz/quaver/pupil/ui/composable/ProgressCard.kt b/app/src/main/java/xyz/quaver/pupil/ui/composable/ProgressCard.kt
deleted file mode 100644
index d713cbf0..00000000
--- a/app/src/main/java/xyz/quaver/pupil/ui/composable/ProgressCard.kt
+++ /dev/null
@@ -1,28 +0,0 @@
-package xyz.quaver.pupil.ui.composable
-
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material.Card
-import androidx.compose.material.LinearProgressIndicator
-import androidx.compose.material.MaterialTheme
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.unit.dp
-
-@OptIn(ExperimentalFoundationApi::class)
-@Composable
-fun ProgressCard(progress: Float? = null, content: @Composable () -> Unit) {
- Card(
- modifier = Modifier.padding(8.dp),
- shape = RoundedCornerShape(4.dp),
- elevation = 4.dp
- ) {
- Column {
- progress?.run { LinearProgressIndicator(progress = progress, modifier = Modifier.fillMaxWidth(), color = MaterialTheme.colors.secondary) }
- content()
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/xyz/quaver/pupil/ui/viewmodel/MainViewModel.kt b/app/src/main/java/xyz/quaver/pupil/ui/viewmodel/MainViewModel.kt
index 2d9afb69..6a0fd9bb 100644
--- a/app/src/main/java/xyz/quaver/pupil/ui/viewmodel/MainViewModel.kt
+++ b/app/src/main/java/xyz/quaver/pupil/ui/viewmodel/MainViewModel.kt
@@ -19,26 +19,17 @@
package xyz.quaver.pupil.ui.viewmodel
import android.app.Application
-import androidx.compose.runtime.*
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.setValue
import androidx.lifecycle.AndroidViewModel
-import androidx.lifecycle.asLiveData
-import androidx.lifecycle.viewModelScope
-import kotlinx.coroutines.*
-import kotlinx.coroutines.flow.first
-import kotlinx.coroutines.flow.map
import org.kodein.di.DIAware
import org.kodein.di.android.x.closestDI
import org.kodein.di.direct
-import org.kodein.di.instance
import org.kodein.log.LoggerFactory
import org.kodein.log.newLogger
-import xyz.quaver.pupil.proto.settingsDataStore
-import xyz.quaver.pupil.sources.History
-import xyz.quaver.pupil.sources.ItemInfo
import xyz.quaver.pupil.sources.Source
import xyz.quaver.pupil.util.source
-import kotlin.math.ceil
-import kotlin.random.Random
@Suppress("UNCHECKED_CAST")
class MainViewModel(app: Application) : AndroidViewModel(app), DIAware {
@@ -46,138 +37,9 @@ class MainViewModel(app: Application) : AndroidViewModel(app), DIAware {
private val logger = newLogger(LoggerFactory.default)
- val searchResults = mutableStateListOf()
-
- private val resultsPerPage = app.settingsDataStore.data.map {
- it.resultsPerPage
- }
-
- var loading by mutableStateOf(false)
- private set
-
- private var queryJob: Job? = null
- private var suggestionJob: Job? = null
-
- var query by mutableStateOf("")
- private val queryStack = mutableListOf()
-
private val defaultSourceFactory: (String) -> Source = {
direct.source(it)
}
private var sourceFactory: (String) -> Source = defaultSourceFactory
var source by mutableStateOf(sourceFactory("hitomi.la"))
- private set
-
- var sortModeIndex by mutableStateOf(0)
- private set
-
- var currentPage by mutableStateOf(1)
-
- var totalItems by mutableStateOf(0)
- private set
-
- val maxPage by derivedStateOf {
- resultsPerPage.map {
- ceil(totalItems / it.toDouble()).toInt()
- }
- }
-
- fun setSourceAndReset(sourceName: String) {
- source = sourceFactory(sourceName)
- sortModeIndex = 0
-
- query = ""
- resetAndQuery()
- }
-
- fun resetAndQuery() {
- queryStack.add(query)
- currentPage = 1
-
- query()
- }
-
- fun setModeAndReset(mode: MainMode) {
- sourceFactory = when (mode) {
- MainMode.SEARCH, MainMode.DOWNLOADS -> defaultSourceFactory
- MainMode.HISTORY -> { { direct.instance(arg = it) } }
- else -> return
- }
-
- setSourceAndReset(
- when {
- mode == MainMode.DOWNLOADS -> "downloads"
- //source.value is Downloads -> "hitomi.la"
- else -> source.name
- }
- )
- }
-
- fun query() {
- suggestionJob?.cancel()
- queryJob?.cancel()
-
- loading = true
- searchResults.clear()
-
- queryJob = viewModelScope.launch {
- val resultsPerPage = resultsPerPage.first()
-
- logger.info {
- resultsPerPage.toString()
- }
-
- val (channel, count) = source.search(
- query,
- (currentPage - 1) * resultsPerPage until currentPage * resultsPerPage,
- sortModeIndex
- )
-
- totalItems = count
-
- for (result in channel) {
- yield()
- searchResults.add(result)
- }
-
- loading = false
- }
- }
-
- fun random(callback: (ItemInfo) -> Unit) {
- if (totalItems == 0)
- return
-
- val random = Random.Default.nextInt(totalItems)
-
- viewModelScope.launch {
- withContext(Dispatchers.IO) {
- source.search(
- query,
- random .. random,
- sortModeIndex
- ).first.receive()
- }.let(callback)
- }
- }
-
- /**
- * @return true if backpress is consumed, false otherwise
- */
- fun onBackPressed(): Boolean {
- if (queryStack.removeLastOrNull() == null || queryStack.isEmpty())
- return false
-
- query = queryStack.removeLast()
- resetAndQuery()
- return true
- }
-
- enum class MainMode {
- SEARCH,
- HISTORY,
- DOWNLOADS,
- FAVORITES
- }
-
}
\ No newline at end of file
diff --git a/app/src/main/java/xyz/quaver/pupil/ui/viewmodel/ReaderViewModel.kt b/app/src/main/java/xyz/quaver/pupil/ui/viewmodel/ReaderViewModel.kt
deleted file mode 100644
index a6fe92e5..00000000
--- a/app/src/main/java/xyz/quaver/pupil/ui/viewmodel/ReaderViewModel.kt
+++ /dev/null
@@ -1,224 +0,0 @@
-/*
- * Pupil, Hitomi.la viewer for Android
- * Copyright (C) 2020 tom5079
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-
-
-package xyz.quaver.pupil.ui.viewmodel
-
-import android.app.Application
-import android.content.Intent
-import android.net.Uri
-import androidx.compose.runtime.*
-import androidx.lifecycle.*
-import io.ktor.client.request.*
-import io.ktor.http.*
-import kotlinx.coroutines.*
-import kotlinx.coroutines.sync.Mutex
-import kotlinx.coroutines.sync.withLock
-import org.kodein.di.DIAware
-import org.kodein.di.android.x.closestDI
-import org.kodein.di.direct
-import org.kodein.di.instance
-import org.kodein.log.LoggerFactory
-import org.kodein.log.newLogger
-import xyz.quaver.pupil.db.AppDatabase
-import xyz.quaver.pupil.db.History
-import xyz.quaver.pupil.sources.ItemInfo
-import xyz.quaver.pupil.sources.Source
-import xyz.quaver.pupil.util.NetworkCache
-import xyz.quaver.pupil.util.source
-
-@Suppress("UNCHECKED_CAST")
-class ReaderViewModel(app: Application) : AndroidViewModel(app), DIAware {
-
- override val di by closestDI()
-
- private val cache: NetworkCache by instance()
-
- private val logger = newLogger(LoggerFactory.default)
-
- var isFullscreen by mutableStateOf(false)
-
- private val database: AppDatabase by instance()
-
- private val historyDao = database.historyDao()
- private val bookmarkDao = database.bookmarkDao()
-
- lateinit var bookmark: LiveData
- private set
-
- var error by mutableStateOf(false)
- private set
-
- var source by mutableStateOf(null)
- private set
- var itemID by mutableStateOf(null)
- private set
- var title by mutableStateOf(null)
- private set
-
- private val totalProgressMutex = Mutex()
- var totalProgress by mutableStateOf(0)
- private set
- var imageCount by mutableStateOf(0)
- private set
-
- private var images: List? = null
- val imageList = mutableStateListOf()
- val progressList = mutableStateListOf()
-
- val sourceIcon by derivedStateOf {
- source?.iconResID
- }
-
- /**
- * Parses source and itemID from the intent
- *
- * @throws IllegalStateException when the intent has no recognizable source and/or itemID
- */
- fun handleIntent(intent: Intent) {
- if (intent.action == Intent.ACTION_VIEW) {
- val uri = intent.data
- val lastPathSegment = uri?.lastPathSegment
- if (uri != null && lastPathSegment != null) {
- source = uri.host?.let { direct.source(it) } ?: error("Invalid host")
- itemID = when (uri.host) {
- "hitomi.la" ->
- Regex("([0-9]+).html").find(lastPathSegment)?.groupValues?.get(1) ?: error("Invalid itemID")
- "hiyobi.me" -> lastPathSegment
- "e-hentai.org" -> uri.pathSegments[1]
- else -> error("Invalid host")
- }
- }
- } else {
- source = intent.getStringExtra("source")?.let { direct.source(it) } ?: error("Invalid source")
- itemID = intent.getStringExtra("id") ?: error("Invalid itemID")
- title = intent.getParcelableExtra("payload")?.title
- }
-
- bookmark = bookmarkDao.contains(source!!.name, itemID!!)
- }
-
- @OptIn(ExperimentalCoroutinesApi::class)
- fun load() {
- val source = source ?: return
- val itemID = itemID ?: return
-
- viewModelScope.launch {
- launch(Dispatchers.IO) {
- historyDao.insert(History(source.name, itemID))
- }
- }
-
- viewModelScope.launch {
- if (title == null)
- title = withContext(Dispatchers.IO) {
- kotlin.runCatching {
- source.info(itemID)
- }.getOrNull()
- }?.title
- }
-
- viewModelScope.launch {
- withContext(Dispatchers.IO) {
- kotlin.runCatching {
- source.images(itemID)
- }.onFailure {
- error = true
- }.getOrNull()
- }?.let { images ->
- this@ReaderViewModel.images = images
-
- imageCount = images.size
-
- progressList.addAll(List(imageCount) { 0f })
- imageList.addAll(List(imageCount) { null })
- totalProgressMutex.withLock {
- totalProgress = 0
- }
-
- images.forEachIndexed { index, image ->
- logger.info {
- progressList.toList().toString()
- }
- when (val scheme = image.takeWhile { it != ':' }) {
- "http", "https" -> {
- val (channel, file) = cache.load {
- url(image)
- headers(source.getHeadersBuilderForImage(itemID, image))
- }
-
- if (channel.isClosedForReceive) {
- imageList[index] = Uri.fromFile(file)
- totalProgressMutex.withLock {
- totalProgress++
- }
- } else {
- channel.invokeOnClose { e ->
- viewModelScope.launch {
- if (e == null) {
- imageList[index] = Uri.fromFile(file)
- totalProgressMutex.withLock {
- totalProgress++
- }
- } else {
- error(index)
- }
- }
- }
-
- launch {
- kotlin.runCatching {
- for (progress in channel) {
- progressList[index] = progress
- }
- }
- }
- }
- }
- "content" -> {
- imageList[index] = Uri.parse(image)
- progressList[index] = 1f
- }
- else -> throw IllegalArgumentException("Expected URL scheme 'http(s)' or 'content' but was '$scheme'")
- }
- }
- }
- }
- }
-
- fun error(index: Int) {
- progressList[index] = -1f
- }
-
- fun toggleBookmark() {
- source?.name?.let { source ->
- itemID?.let { itemID ->
- bookmark.value?.let { bookmark ->
- CoroutineScope(Dispatchers.IO).launch {
- if (bookmark) bookmarkDao.delete(source, itemID)
- else bookmarkDao.insert(source, itemID)
- }
- } } }
- }
-
- override fun onCleared() {
- cache.cleanup()
- images?.let { cache.free(it) }
- }
-
-}
\ No newline at end of file
diff --git a/app/src/main/java/xyz/quaver/pupil/util/DownloadManager.kt b/app/src/main/java/xyz/quaver/pupil/util/DownloadManager.kt
deleted file mode 100644
index 6ba525d2..00000000
--- a/app/src/main/java/xyz/quaver/pupil/util/DownloadManager.kt
+++ /dev/null
@@ -1,115 +0,0 @@
-/*
- * Pupil, Hitomi.la viewer for Android
- * Copyright (C) 2021 tom5079
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- */
-
-package xyz.quaver.pupil.util
-
-import android.content.Context
-import android.content.ContextWrapper
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.async
-import kotlinx.coroutines.launch
-import kotlinx.serialization.decodeFromString
-import kotlinx.serialization.encodeToString
-import kotlinx.serialization.json.Json
-import org.kodein.di.DIAware
-import org.kodein.di.android.closestDI
-import xyz.quaver.io.FileX
-import xyz.quaver.io.util.*
-import xyz.quaver.pupil.sources.Source
-
-class DownloadManager constructor(context: Context) : ContextWrapper(context), DIAware {
-
- override val di by closestDI(context)
-
- private val defaultDownloadFolder = FileX(this, getExternalFilesDir(null)!!)
-
- val downloadFolder: FileX
- get() = kotlin.runCatching {
- FileX(this, Preferences.get("download_folder"))
- }.getOrElse {
- Preferences["download_folder"] = defaultDownloadFolder.uri.toString()
- defaultDownloadFolder
- }
-
- private var prevDownloadFolder: FileX? = null
- private var downloadFolderMapInstance: MutableMap? = null
- private val downloadFolderMap: MutableMap
- @Synchronized
- get() {
- if (prevDownloadFolder != downloadFolder) {
- prevDownloadFolder = downloadFolder
- downloadFolderMapInstance = run {
- val file = downloadFolder.getChild(".download")
- val data = if (file.exists())
- kotlin.runCatching {
- file.readText()?.let> { Json.decodeFromString(it) }
- }.onFailure { file.delete() }.getOrNull()
- else
- null
- data ?: run {
- file.createNewFile()
- mutableMapOf()
- }
- }
- }
-
- return downloadFolderMapInstance ?: mutableMapOf()
- }
-
- val downloads: Map
- get() = downloadFolderMap
-
- @Synchronized
- fun getDownloadFolder(source: String, itemID: String): FileX? =
- downloadFolderMap["$source-$itemID"]?.let { downloadFolder.getChild(it) }
-
- @Synchronized
- fun download(source: String, itemID: String) = CoroutineScope(Dispatchers.IO).launch {
- val source: Source by source(source)
- val info = async { source.info(itemID) }
- val images = async { source.images(itemID) }
-
- val name = info.await().formatDownloadFolder()
-
- val folder = downloadFolder.getChild("$source/$name")
-
- if (folder.exists())
- return@launch
-
- folder.mkdir()
-
- downloadFolderMap["$source/$itemID"] = folder.name
-
- downloadFolder.getChild(".download").let { if (!it.exists()) it.createNewFile() }
- downloadFolder.getChild(".download").writeText(Json.encodeToString(downloadFolderMap))
- }
-
- @Synchronized
- fun delete(source: String, itemID: String) {
- downloadFolderMap["$source/$itemID"]?.let {
- kotlin.runCatching {
- downloadFolder.getChild(it).deleteRecursively()
- downloadFolderMap.remove("$source/$itemID")
-
- downloadFolder.getChild(".download").let { if (!it.exists()) it.createNewFile() }
- downloadFolder.getChild(".download").writeText(Json.encodeToString(downloadFolderMap))
- }
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/xyz/quaver/pupil/util/misc.kt b/app/src/main/java/xyz/quaver/pupil/util/misc.kt
index 0a53cded..639ea60d 100644
--- a/app/src/main/java/xyz/quaver/pupil/util/misc.kt
+++ b/app/src/main/java/xyz/quaver/pupil/util/misc.kt
@@ -18,18 +18,15 @@
package xyz.quaver.pupil.util
-import android.annotation.SuppressLint
import android.graphics.BitmapFactory
-import android.view.MenuItem
import android.view.View
-import androidx.compose.runtime.*
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.toAndroidRect
-import androidx.lifecycle.MutableLiveData
-import kotlinx.coroutines.flow.Flow
import kotlinx.serialization.json.*
import org.kodein.di.DIAware
import org.kodein.di.DirectDIAware
@@ -40,71 +37,7 @@ import xyz.quaver.graphics.subsampledimage.newBitmapRegionDecoder
import xyz.quaver.io.FileX
import xyz.quaver.io.util.inputStream
import xyz.quaver.pupil.db.AppDatabase
-import xyz.quaver.pupil.sources.ItemInfo
import xyz.quaver.pupil.sources.SourceEntries
-import java.io.InputStream
-import java.io.OutputStream
-import java.util.*
-import kotlin.collections.ArrayList
-
-@OptIn(ExperimentalStdlibApi::class)
-fun String.wordCapitalize() : String {
- val result = ArrayList()
-
- @SuppressLint("DefaultLocale")
- for (word in this.split(" "))
- result.add(word.replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() })
-
- return result.joinToString(" ")
-}
-
-private val suffix = listOf(
- "B",
- "kB",
- "MB",
- "GB",
- "TB" //really?
-)
-
-fun byteToString(byte: Long, precision : Int = 1) : String {
- var size = byte.toDouble(); var suffixIndex = 0
-
- while (size >= 1024) {
- size /= 1024
- suffixIndex++
- }
-
- return "%.${precision}f ${suffix[suffixIndex]}".format(size)
-}
-
-/**
- * Convert android generated ID to requestCode
- * to prevent java.lang.IllegalArgumentException: Can only use lower 16 bits for requestCode
- *
- * https://stackoverflow.com/questions/38072322/generate-16-bit-unique-ids-in-android-for-startactivityforresult
- */
-fun Int.normalizeID() = this.and(0xFFFF)
-
-val formatMap = mapOf (String)>(
- "-id-" to { itemID },
- "-title-" to { title },
- // TODO
-)
-/**
- * Formats download folder name with given Metadata
- */
-fun ItemInfo.formatDownloadFolder(format: String = Preferences["download_folder_name", "[-id-] -title-"]): String =
- format.let {
- formatMap.entries.fold(it) { str, (k, v) ->
- str.replace(k, v.invoke(this), true)
- }
- }.replace(Regex("""[*\\|"?><:/]"""), "").ellipsize(127)
-
-fun String.ellipsize(n: Int): String =
- if (this.length > n)
- this.slice(0 until n) + "…"
- else
- this
operator fun JsonElement.get(index: Int) =
this.jsonArray[index]
@@ -115,27 +48,6 @@ operator fun JsonElement.get(tag: String) =
val JsonElement.content
get() = this.jsonPrimitive.contentOrNull
-fun List