diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index eaeaa25b..f07a75fd 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -135,6 +135,8 @@ dependencies {
implementation("ru.noties.markwon:core:3.1.0")
+ implementation("xyz.quaver.pupil.sources:core:0.0.1-alpha01-DEV09")
+
implementation("xyz.quaver:documentfilex:0.7.1")
implementation("xyz.quaver:subsampledimage:0.0.1-alpha18-SNAPSHOT")
diff --git a/app/src/androidTest/java/xyz/quaver/pupil/SourceLoaderInstrumentedTest.kt b/app/src/androidTest/java/xyz/quaver/pupil/SourceLoaderInstrumentedTest.kt
new file mode 100644
index 00000000..094248a0
--- /dev/null
+++ b/app/src/androidTest/java/xyz/quaver/pupil/SourceLoaderInstrumentedTest.kt
@@ -0,0 +1,52 @@
+/*
+ * 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
+
+import android.app.Application
+import android.content.pm.PackageManager
+import android.util.Log
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.platform.app.InstrumentationRegistry
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.Assert.*
+import xyz.quaver.pupil.sources.isSourceFeatureEnabled
+import xyz.quaver.pupil.sources.loadSource
+
+@RunWith(AndroidJUnit4::class)
+class SourceLoaderInstrumentedTest {
+
+ @Test
+ fun getPackages() {
+ val appContext = InstrumentationRegistry.getInstrumentation().targetContext
+ val application: Application = appContext.applicationContext as Application
+
+ val packageManager = appContext.packageManager
+
+ val packages = packageManager.getInstalledPackages(
+ PackageManager.GET_CONFIGURATIONS or
+ PackageManager.GET_META_DATA
+ )
+
+ val sources = packages.filter { it.isSourceFeatureEnabled }
+
+ assertEquals(1, sources.size)
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index a55ffe31..c643728e 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -12,6 +12,8 @@
+
+
{
override suspend fun writeTo(t: Settings, output: OutputStream) = t.writeTo(output)
}
-val Context.settingsDataStore: DataStore by dataStore(
+val Application.settingsDataStore: DataStore by dataStore(
fileName = "settings.proto",
serializer = SettingsSerializer
)
\ 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
deleted file mode 100644
index efebcc1f..00000000
--- a/app/src/main/java/xyz/quaver/pupil/sources/Downloads.kt
+++ /dev/null
@@ -1,100 +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.sources
-
-/*
-class Downloads(override val di: DI) : Source(), DIAware {
-
- override val name: String
- get() = "downloads"
- override val iconResID: Int
- get() = R.drawable.ic_download
- override val preferenceID: Int
- get() = R.xml.download_preferences
- override val availableSortMode: List = emptyList()
-
- private val downloadManager: DownloadManager by instance()
-
- override suspend fun search(query: String, range: IntRange, sortMode: Int): Pair, Int> {
- TODO()
- /*
- val downloads = downloadManager.downloads.toList()
-
- val channel = Channel()
- val sanitizedRange = max(0, range.first) .. min(range.last, downloads.size - 1)
-
- CoroutineScope(Dispatchers.IO).launch {
- downloads.slice(sanitizedRange).map { (_, folderName) ->
- transform(downloadManager.downloadFolder.getChild(folderName))
- }.forEach {
- channel.send(it)
- }
-
- channel.close()
- }
-
- return Pair(channel, downloads.size)*/
- }
-
- override suspend fun suggestion(query: String): List {
- return emptyList()
- }
-
- override suspend fun images(itemID: String): List {
- return downloadManager.downloadFolder.getChild(itemID).let {
- if (!it.exists()) null else images(it)
- }!!
- }
-
- override suspend fun info(itemID: String): ItemInfo {
- TODO("Not yet implemented")
-/* return transform(downloadManager.downloadFolder.getChild(itemID))*/
- }
-
- companion object {
- private fun images(folder: FileX): List? =
- folder.list { _, name ->
- name.takeLastWhile { it != '.' } in listOf("jpg", "png", "gif", "webp")
- }?.toList()
-/*
- suspend fun transform(folder: FileX): ItemInfo = withContext(Dispatchers.Unconfined) {
- kotlin.runCatching {
- Json.decodeFromString(folder.getChild(".metadata").readText())
- }.getOrNull() ?: run {
- val images = images(folder)
- ItemInfo(
- "Downloads",
- folder.name,
- folder.name,
- images?.firstOrNull() ?: "",
- "",
- mapOf(
- ItemInfo.ExtraType.PAGECOUNT to async { images?.size?.toString() }
- )
- )
- }
- }*/
- }
-
- @Composable
- override fun compose(itemInfo: ItemInfo) {
- TODO("Not yet implemented")
- }
-
-}*/
\ No newline at end of file
diff --git a/app/src/main/java/xyz/quaver/pupil/sources/History.kt b/app/src/main/java/xyz/quaver/pupil/sources/History.kt
deleted file mode 100644
index f443ed3e..00000000
--- a/app/src/main/java/xyz/quaver/pupil/sources/History.kt
+++ /dev/null
@@ -1,60 +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.sources
-
-//
-//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/Hiyobi_io.kt b/app/src/main/java/xyz/quaver/pupil/sources/Hiyobi_io.kt
deleted file mode 100644
index a21669f2..00000000
--- a/app/src/main/java/xyz/quaver/pupil/sources/Hiyobi_io.kt
+++ /dev/null
@@ -1,465 +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.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/sources/SourceLoader.kt b/app/src/main/java/xyz/quaver/pupil/sources/SourceLoader.kt
index 33975834..957105a8 100644
--- a/app/src/main/java/xyz/quaver/pupil/sources/SourceLoader.kt
+++ b/app/src/main/java/xyz/quaver/pupil/sources/SourceLoader.kt
@@ -19,32 +19,76 @@
package xyz.quaver.pupil.sources
import android.app.Application
-import androidx.navigation.NavController
-import androidx.navigation.NavGraphBuilder
+import android.content.Context
+import android.content.pm.PackageInfo
+import android.content.pm.PackageManager
+import android.graphics.drawable.Drawable
+import android.util.Log
+import dalvik.system.DexClassLoader
+import dalvik.system.PathClassLoader
import org.kodein.di.*
-import xyz.quaver.pupil.sources.hitomi.Hitomi
-import xyz.quaver.pupil.sources.manatoki.Manatoki
+import org.kodein.di.bindings.NoArgBindingDI
+import org.kodein.di.bindings.NoArgDIBinding
+import java.util.*
-abstract class Source {
- abstract val name: String
- abstract val iconResID: Int
+private const val SOURCES_FEATURE = "pupil.sources"
+private const val SOURCES_PACKAGE_PREFIX = "xyz.quaver.pupil.sources"
+private const val SOURCES_PATH = "pupil.sources.path"
- open fun NavGraphBuilder.navGraph(navController: NavController) { }
+data class SourceEntry(
+ val name: String,
+ val source: Source,
+ val icon: Drawable
+)
+typealias SourceEntries = Map
+
+private val sources = mutableMapOf()
+
+val PackageInfo.isSourceFeatureEnabled
+ get() = this.reqFeatures.orEmpty().any { it.name == SOURCES_FEATURE }
+
+fun loadSource(app: Application, packageInfo: PackageInfo) {
+ val packageManager = app.packageManager
+ val applicationInfo = packageInfo.applicationInfo
+
+ val classLoader = PathClassLoader(applicationInfo.sourceDir, null, app.classLoader)
+ val packageName = packageInfo.packageName
+
+ val sourceName = packageManager.getApplicationLabel(applicationInfo).toString().substringAfter("[Pupil] ")
+
+ val icon = packageManager.getApplicationIcon(applicationInfo)
+
+ packageInfo
+ .applicationInfo
+ .metaData
+ .getString(SOURCES_PATH)
+ ?.split(';')
+ .orEmpty()
+ .forEach { sourcePath ->
+ sources[sourceName] = SourceEntry(
+ sourceName,
+ Class.forName("$packageName$sourcePath", false, classLoader)
+ .getConstructor(Application::class.java)
+ .newInstance(app) as Source,
+ icon
+ )
+ }
}
-typealias SourceEntry = Pair
-typealias SourceEntries = Set
-val sourceModule = DI.Module(name = "source") {
- bindSet()
+fun loadSources(app: Application) {
+ val packageManager = app.packageManager
- listOf<(Application) -> (Source)>(
- { Hitomi(it) },
- //{ Hiyobi_io(it) },
- { Manatoki(it) }
- ).forEach { source ->
- inSet { singleton { source(instance()).let { it.name to it } } }
- }
+ val packages = packageManager.getInstalledPackages(
+ PackageManager.GET_CONFIGURATIONS or
+ PackageManager.GET_META_DATA
+ )
- //bind { singleton { History(di) } }
- // inSet { singleton { Downloads(di).let { it.name to it as Source } } }
+ val sources = packages.filter { it.isSourceFeatureEnabled }
+
+ sources.forEach { loadSource(app, it) }
+}
+
+fun sourceModule(app: Application) = DI.Module(name = "source") {
+ loadSources(app)
+ bindInstance { Collections.unmodifiableMap(sources) }
}
\ No newline at end of file
diff --git a/app/src/main/java/xyz/quaver/pupil/sources/composable/SourceSelectDialog.kt b/app/src/main/java/xyz/quaver/pupil/sources/SourceSelectDialog.kt
similarity index 80%
rename from app/src/main/java/xyz/quaver/pupil/sources/composable/SourceSelectDialog.kt
rename to app/src/main/java/xyz/quaver/pupil/sources/SourceSelectDialog.kt
index 648e341a..434333eb 100644
--- a/app/src/main/java/xyz/quaver/pupil/sources/composable/SourceSelectDialog.kt
+++ b/app/src/main/java/xyz/quaver/pupil/sources/SourceSelectDialog.kt
@@ -16,7 +16,7 @@
* along with this program. If not, see .
*/
-package xyz.quaver.pupil.sources.composable
+package xyz.quaver.pupil.sources
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.*
@@ -25,42 +25,39 @@ import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Settings
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
-import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.navigation.NavController
+import com.google.accompanist.drawablepainter.rememberDrawablePainter
import org.kodein.di.compose.rememberInstance
-import xyz.quaver.pupil.sources.Source
-import xyz.quaver.pupil.sources.SourceEntries
@Composable
fun SourceSelectDialog(navController: NavController, currentSource: String? = null, onDismissRequest: () -> Unit = { }) {
SourceSelectDialog(currentSource = currentSource, onDismissRequest = onDismissRequest) {
onDismissRequest()
navController.navigate(it.name) {
- currentSource?.let { popUpTo(currentSource) { inclusive = true } }
+ currentSource?.let { popUpTo("main") }
}
}
}
@Composable
-fun SourceSelectDialogItem(source: Source, isSelected: Boolean, onSelected: (Source) -> Unit = { }) {
+fun SourceSelectDialogItem(sourceEntry: SourceEntry, isSelected: Boolean, onSelected: (Source) -> Unit = { }) {
Row(
modifier = Modifier.padding(16.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp)
) {
Image(
- painter = painterResource(source.iconResID),
+ painter = rememberDrawablePainter(sourceEntry.icon),
contentDescription = null,
modifier = Modifier.size(24.dp)
)
Text(
- source.name,
+ sourceEntry.name,
modifier = Modifier.weight(1f)
)
@@ -73,7 +70,7 @@ fun SourceSelectDialogItem(source: Source, isSelected: Boolean, onSelected: (Sou
Button(
enabled = !isSelected,
onClick = {
- onSelected(source)
+ onSelected(sourceEntry.source)
}
) {
Text("GO")
@@ -92,8 +89,8 @@ fun SourceSelectDialog(currentSource: String? = null, onDismissRequest: () -> Un
shape = RoundedCornerShape(12.dp)
) {
Column() {
- sourceEntries.forEach { SourceSelectDialogItem(it.second, it.first == currentSource, onSelected) }
+ sourceEntries.values.forEach { SourceSelectDialogItem(it, it.name == currentSource, onSelected) }
}
}
}
-}
+}
\ No newline at end of file
diff --git a/app/src/main/java/xyz/quaver/pupil/sources/composable/FloatingSearchBar.kt b/app/src/main/java/xyz/quaver/pupil/sources/composable/FloatingSearchBar.kt
deleted file mode 100644
index 2eee3d82..00000000
--- a/app/src/main/java/xyz/quaver/pupil/sources/composable/FloatingSearchBar.kt
+++ /dev/null
@@ -1,140 +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.sources.composable
-
-import androidx.compose.foundation.background
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.*
-import androidx.compose.foundation.text.BasicTextField
-import androidx.compose.foundation.text.KeyboardActions
-import androidx.compose.foundation.text.KeyboardOptions
-import androidx.compose.material.*
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.Close
-import androidx.compose.runtime.*
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.focus.onFocusChanged
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.SolidColor
-import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.platform.LocalFocusManager
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.text.input.ImeAction
-import androidx.compose.ui.tooling.preview.Preview
-import androidx.compose.ui.unit.dp
-import xyz.quaver.pupil.R
-import xyz.quaver.pupil.util.KeyboardManager
-
-@Preview
-@Composable
-fun FloatingSearchBar(
- modifier: Modifier = Modifier,
- query: String = "",
- onQueryChange: (String) -> Unit = { },
- navigationIcon: @Composable () -> Unit = { },
- actions: @Composable RowScope.() -> Unit = { },
- onSearch: () -> Unit = { },
- onTextFieldFocused: () -> Unit = { },
- onTextFieldUnfocused: () -> Unit = { }
-) {
- val context = LocalContext.current
- val focusManager = LocalFocusManager.current
-
- var isFocused by remember { mutableStateOf(false) }
-
- DisposableEffect(context) {
- val keyboardManager = KeyboardManager(context)
- keyboardManager.attachKeyboardDismissListener {
- focusManager.clearFocus()
- }
- onDispose {
- keyboardManager.release()
- }
- }
-
- Card(
- modifier = modifier
- .fillMaxWidth()
- .height(64.dp)
- .padding(8.dp, 8.dp)
- .background(Color.Transparent),
- elevation = 8.dp
- ) {
- Row(
- modifier = Modifier
- .fillMaxSize(),
- verticalAlignment = Alignment.CenterVertically
- ) {
- navigationIcon()
-
- BasicTextField(
- modifier = Modifier
- .weight(1f)
- .padding(16.dp, 0.dp)
- .onFocusChanged {
- if (it.isFocused) onTextFieldFocused()
- else onTextFieldUnfocused()
-
- isFocused = it.isFocused
- },
- value = query,
- onValueChange = onQueryChange,
- singleLine = true,
- textStyle = LocalTextStyle.current.copy(color = MaterialTheme.colors.onSurface),
- cursorBrush = SolidColor(MaterialTheme.colors.secondary),
- keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
- keyboardActions = KeyboardActions(
- onSearch = { onSearch() }
- ),
- decorationBox = { innerTextField ->
- Row(
- verticalAlignment = Alignment.CenterVertically
- ) {
- Box(Modifier.weight(1f)) {
- if (query.isEmpty())
- Text(
- stringResource(R.string.search_hint),
- color = MaterialTheme.colors.onSurface.copy(alpha = 0.5f)
- )
- innerTextField()
- }
-
- if (query.isNotEmpty() && isFocused)
- Icon(
- Icons.Default.Close,
- contentDescription = null,
- tint = MaterialTheme.colors.onSurface.copy(alpha = 0.5f),
- modifier = Modifier.clickable { onQueryChange("") }
- )
- }
- }
- )
-
- CompositionLocalProvider(LocalContentAlpha provides ContentAlpha.medium) {
- Row(
- Modifier.fillMaxHeight(),
- horizontalArrangement = Arrangement.End,
- verticalAlignment = Alignment.CenterVertically,
- content = actions
- )
- }
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/xyz/quaver/pupil/sources/composable/ModalTopSheetLayout.kt b/app/src/main/java/xyz/quaver/pupil/sources/composable/ModalTopSheetLayout.kt
deleted file mode 100644
index e3580786..00000000
--- a/app/src/main/java/xyz/quaver/pupil/sources/composable/ModalTopSheetLayout.kt
+++ /dev/null
@@ -1,287 +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.sources.composable
-
-import android.util.Log
-import androidx.compose.animation.core.TweenSpec
-import androidx.compose.animation.core.animateFloatAsState
-import androidx.compose.foundation.Canvas
-import androidx.compose.foundation.gestures.Orientation
-import androidx.compose.foundation.gestures.detectTapGestures
-import androidx.compose.foundation.layout.*
-import androidx.compose.material.*
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.ArrowDropDown
-import androidx.compose.runtime.*
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.geometry.Rect
-import androidx.compose.ui.geometry.Size
-import androidx.compose.ui.graphics.*
-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.pointerInput
-import androidx.compose.ui.layout.onGloballyPositioned
-import androidx.compose.ui.unit.*
-import kotlinx.coroutines.launch
-import xyz.quaver.pupil.sources.composable.ModalTopSheetState.Expanded
-import xyz.quaver.pupil.sources.composable.ModalTopSheetState.Hidden
-import kotlin.math.roundToInt
-
-class ModalTopSheetLayoutShape(
- private val cornerRadius: Dp,
- private val handleRadius: Dp
-): Shape {
-
- private fun drawDrawerPath(
- size: Size,
- cornerRadius: Float,
- handleRadius: Float
- ) = Path().apply {
- reset()
-
- lineTo(x = size.width, y = 0f)
-
- lineTo(x = size.width, y = size.height - cornerRadius)
-
- arcTo(
- Rect(
- left = size.width - 2*cornerRadius,
- top = size.height - 2*cornerRadius,
- right = size.width,
- bottom = size.height
- ),
- startAngleDegrees = 0f,
- sweepAngleDegrees = 90f,
- forceMoveTo = false
- )
-
- lineTo(x = size.width / 2 + handleRadius, y = size.height)
-
- arcTo(
- Rect(
- left = size.width/2 - handleRadius,
- top = size.height - handleRadius,
- right = size.width/2 + handleRadius,
- bottom = size.height + handleRadius
- ),
- startAngleDegrees = 0f,
- sweepAngleDegrees = 180f,
- forceMoveTo = false
- )
-
- lineTo(x = cornerRadius, y = size.height)
-
- arcTo(
- Rect(
- left = 0f,
- top = size.height - 2*cornerRadius,
- right = 2*cornerRadius,
- bottom = size.height
- ),
- startAngleDegrees = 90f,
- sweepAngleDegrees = 90f,
- forceMoveTo = false
- )
-
- close()
- }
-
- override fun createOutline(
- size: Size,
- layoutDirection: LayoutDirection,
- density: Density
- ): Outline = Outline.Generic(
- path = drawDrawerPath(
- size,
- density.run { cornerRadius.toPx() },
- density.run { handleRadius.toPx() }
- )
- )
-
-}
-
-enum class ModalTopSheetState {
- Hidden,
- Expanded
-}
-
-@Composable
-private fun Scrim(
- color: Color,
- onDismiss: () -> Unit,
- visible: Boolean
-) {
- if (color.isSpecified) {
- val alpha by animateFloatAsState(
- targetValue = if (visible) 1f else 0f,
- animationSpec = TweenSpec()
- )
- val dismissModifier = if (visible) {
- Modifier.pointerInput(onDismiss) { detectTapGestures { onDismiss() } }
- } else {
- Modifier
- }
-
- Canvas(
- Modifier
- .fillMaxSize()
- .then(dismissModifier)
- ) {
- drawRect(color = color, alpha = alpha)
- }
- }
-}
-
-@Composable
-@ExperimentalMaterialApi
-fun ModalTopSheetLayout(
- drawerContent: @Composable ColumnScope.() -> Unit,
- modifier: Modifier = Modifier,
- drawerCornerRadius: Dp = SearchOptionDrawerDefaults.CornerRadius,
- drawerHandleRadius: Dp = SearchOptionDrawerDefaults.HandleRadius,
- drawerState: SwipeableState = rememberSwipeableState(Hidden),
- drawerElevation: Dp = SearchOptionDrawerDefaults.Elevation,
- drawerBackgroundColor: Color = MaterialTheme.colors.surface,
- drawerContentColor: Color = contentColorFor(drawerBackgroundColor),
- scrimColor: Color = SearchOptionDrawerDefaults.scrimColor,
- content: @Composable () -> Unit
-) {
- val coroutineScope = rememberCoroutineScope()
-
- val nestedScrollConnection = remember {
- object: NestedScrollConnection {
- override fun onPreScroll(
- available: Offset,
- source: NestedScrollSource
- ): Offset {
- val delta = available.y
- return if (delta > 0 && source == NestedScrollSource.Drag)
- Offset(0f, drawerState.performDrag(delta))
- else
- Offset.Zero
- }
-
- override fun onPostScroll(
- consumed: Offset,
- available: Offset,
- source: NestedScrollSource
- ): Offset {
- return if (source == NestedScrollSource.Drag)
- Offset(0f, drawerState.performDrag(available.y))
- else
- Offset.Zero
- }
-
- override suspend fun onPreFling(available: Velocity): Velocity = Velocity.Zero
-
- override suspend fun onPostFling(consumed: Velocity, available: Velocity): Velocity {
- drawerState.performFling(available.y)
- return available
- }
- }
- }
-
- BoxWithConstraints {
- var sheetHeight by remember { mutableStateOf(null) }
-
- Box(Modifier.fillMaxSize()) {
- content()
- Scrim(
- color = scrimColor,
- onDismiss = {
- coroutineScope.launch { drawerState.animateTo(Hidden) }
- },
- visible = drawerState.targetValue != Hidden
- )
- }
-
- Surface(
- modifier
- .fillMaxWidth()
- .nestedScroll(nestedScrollConnection)
- .offset {
- IntOffset(0, drawerState.offset.value.roundToInt())
- }
- .drawerSwipeable(drawerState, sheetHeight)
- .onGloballyPositioned {
- sheetHeight = it.size.height.toFloat()
- },
- shape = ModalTopSheetLayoutShape(drawerCornerRadius, drawerHandleRadius),
- elevation = drawerElevation,
- color = drawerBackgroundColor,
- contentColor = drawerContentColor
- ) {
- Column(content = drawerContent)
- Icon(
- Icons.Default.ArrowDropDown,
- contentDescription = null,
- modifier = Modifier
- .size(32.dp)
- .align(Alignment.BottomCenter)
- .offset(0.dp, drawerHandleRadius)
- )
- }
-
- Box(
- modifier = Modifier
- .size(2*drawerHandleRadius, drawerHandleRadius)
- .align(Alignment.TopCenter)
- .pointerInput(drawerState) {
- detectTapGestures {
- coroutineScope.launch {
- drawerState.animateTo(Expanded)
- }
- }
- }
- ) { }
- }
-}
-
-@ExperimentalMaterialApi
-private fun Modifier.drawerSwipeable(
- drawerState: SwipeableState,
- sheetHeight: Float?
-) = this.then(
- if (sheetHeight != null) {
- val anchors = mapOf(
- -sheetHeight to Hidden,
- 0f to Expanded
- )
-
- Modifier.swipeable(
- state = drawerState,
- anchors = anchors,
- orientation = Orientation.Vertical,
- enabled = drawerState.currentValue != Hidden,
- resistance = null
- )
- } else Modifier
-)
-
-object SearchOptionDrawerDefaults {
- val Elevation = 16.dp
- val scrimColor: Color
- @Composable
- get() = MaterialTheme.colors.onSurface.copy(alpha = 0.32f)
- val CornerRadius = 32.dp
- val HandleRadius = 32.dp
-}
\ No newline at end of file
diff --git a/app/src/main/java/xyz/quaver/pupil/sources/composable/MultipleFloatingActionButton.kt b/app/src/main/java/xyz/quaver/pupil/sources/composable/MultipleFloatingActionButton.kt
deleted file mode 100644
index ee6c1d17..00000000
--- a/app/src/main/java/xyz/quaver/pupil/sources/composable/MultipleFloatingActionButton.kt
+++ /dev/null
@@ -1,229 +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.sources.composable
-
-import androidx.compose.animation.core.*
-import androidx.compose.foundation.interaction.MutableInteractionSource
-import androidx.compose.foundation.layout.*
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material.*
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.Add
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.remember
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.alpha
-import androidx.compose.ui.draw.rotate
-import androidx.compose.ui.draw.scale
-import androidx.compose.ui.graphics.vector.ImageVector
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.dp
-
-enum class FloatingActionButtonState(private val isExpanded: Boolean) {
- COLLAPSED(false), EXPANDED(true);
-
- operator fun not() = lookupTable[!this.isExpanded]!!
-
- companion object {
- private val lookupTable = mapOf(
- false to COLLAPSED,
- true to EXPANDED
- )
- }
-}
-
-data class SubFabItem(
- val label: String? = null,
- val onClick: ((SubFabItem) -> Unit)? = null,
- val icon: @Composable () -> Unit
-)
-
-@Composable
-fun MiniFloatingActionButton(
- modifier: Modifier = Modifier,
- item: SubFabItem,
- buttonScale: Float = 1f,
- labelAlpha: Float = 1f,
- labelOffset: Dp = 0.dp,
- onClick: ((SubFabItem) -> Unit)? = null
-) {
- Row(
- modifier = modifier,
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.spacedBy(16.dp)
- ) {
- val elevation = FloatingActionButtonDefaults.elevation()
- val interactionSource = remember { MutableInteractionSource() }
-
- item.label?.let { label ->
- Surface(
- modifier = Modifier
- .alpha(labelAlpha)
- .offset(x = labelOffset),
- shape = RoundedCornerShape(4.dp),
- elevation = elevation.elevation(interactionSource).value
- ) {
- Text(modifier = Modifier.padding(8.dp, 4.dp), text = label)
- }
- }
-
- if (buttonScale > 0f)
- FloatingActionButton(
- modifier = Modifier
- .size(40.dp)
- .scale(buttonScale),
- onClick = { onClick?.invoke(item) },
- elevation = elevation,
- interactionSource = interactionSource,
- content = item.icon
- )
- }
-}
-
-@Composable
-fun MultipleFloatingActionButton(
- items: List,
- modifier: Modifier = Modifier,
- fabIcon: ImageVector = Icons.Default.Add,
- visible: Boolean = true,
- targetState: FloatingActionButtonState = FloatingActionButtonState.COLLAPSED,
- onStateChanged: ((FloatingActionButtonState) -> Unit)? = null
-) {
- val transition = updateTransition(targetState = targetState, label = "expand")
-
- val rotation by transition.animateFloat(
- label = "FABRotation",
- transitionSpec = {
- spring(
- dampingRatio = Spring.DampingRatioMediumBouncy,
- stiffness = Spring.StiffnessMedium
- )
- }) { state ->
- when (state) {
- FloatingActionButtonState.COLLAPSED -> 0f
- FloatingActionButtonState.EXPANDED -> 45f
- }
- }
-
- if (!visible) onStateChanged?.invoke(FloatingActionButtonState.COLLAPSED)
-
- Column(
- modifier = modifier,
- horizontalAlignment = Alignment.End,
- verticalArrangement = Arrangement.spacedBy(12.dp)
- ) {
- var allCollapsed = true
-
- items.forEachIndexed { index, item ->
- val delay = when (targetState) {
- FloatingActionButtonState.COLLAPSED -> index
- FloatingActionButtonState.EXPANDED -> (items.size - index)
- } * 50
-
- val buttonScale by transition.animateFloat(
- label = "miniFAB scale",
- transitionSpec = {
- tween(
- durationMillis = 100,
- delayMillis = delay
- )
- }
- ) { state ->
- when (state) {
- FloatingActionButtonState.COLLAPSED -> 0f
- FloatingActionButtonState.EXPANDED -> 1f
- }
- }
-
- val labelAlpha by transition.animateFloat(
- label = "miniFAB alpha",
- transitionSpec = {
- tween(
- durationMillis = 150,
- delayMillis = delay,
- )
- }
- ) { state ->
- when (state) {
- FloatingActionButtonState.COLLAPSED -> 0f
- FloatingActionButtonState.EXPANDED -> 1f
- }
- }
-
- val labelOffset by transition.animateDp(
- label = "miniFAB offset",
- transitionSpec = {
- keyframes {
- durationMillis = 200
- delayMillis = delay
-
- when (targetState) {
- FloatingActionButtonState.COLLAPSED -> {
- 0.dp at 0
- 64.dp at 200
- }
- FloatingActionButtonState.EXPANDED -> {
- 64.dp at 0
- (-4).dp at 150 with LinearEasing
- 0.dp at 200 with FastOutLinearInEasing
- }
- }
- }
- }
- ) { state ->
- when (state) {
- FloatingActionButtonState.COLLAPSED -> 64.dp
- FloatingActionButtonState.EXPANDED -> 0.dp
- }
- }
-
- MiniFloatingActionButton(
- modifier = Modifier.padding(end = 8.dp),
- item = item,
- buttonScale = buttonScale,
- labelAlpha = labelAlpha,
- labelOffset = labelOffset
- ) {
- item.onClick?.invoke(it)
- }
-
- if (buttonScale != 0f) allCollapsed = false
- }
-
- val visibilityTransition = updateTransition(targetState = visible || !allCollapsed, label = "visible")
-
- val scale by visibilityTransition.animateFloat(
- label = "main FAB scale"
- ) { state ->
- if (state) 1f else 0f
- }
-
- if (scale > 0f)
- FloatingActionButton(
- modifier = Modifier.scale(scale),
- onClick = {
- onStateChanged?.invoke(!targetState)
- }
- ) {
- Icon(modifier = Modifier.rotate(rotation), imageVector = fabIcon, contentDescription = null)
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/xyz/quaver/pupil/sources/composable/OverscrollPager.kt b/app/src/main/java/xyz/quaver/pupil/sources/composable/OverscrollPager.kt
deleted file mode 100644
index 8e554a17..00000000
--- a/app/src/main/java/xyz/quaver/pupil/sources/composable/OverscrollPager.kt
+++ /dev/null
@@ -1,224 +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.sources.composable
-
-import androidx.compose.animation.core.animateFloatAsState
-import androidx.compose.foundation.Canvas
-import androidx.compose.foundation.gestures.awaitFirstDown
-import androidx.compose.foundation.gestures.forEachGesture
-import androidx.compose.foundation.layout.*
-import androidx.compose.material.Icon
-import androidx.compose.material.MaterialTheme
-import androidx.compose.material.Text
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.NavigateBefore
-import androidx.compose.material.icons.filled.NavigateNext
-import androidx.compose.runtime.*
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.geometry.Size
-import androidx.compose.ui.hapticfeedback.HapticFeedbackType
-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.layout.onGloballyPositioned
-import androidx.compose.ui.platform.LocalConfiguration
-import androidx.compose.ui.platform.LocalDensity
-import androidx.compose.ui.platform.LocalHapticFeedback
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.unit.Dp
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.toSize
-import androidx.compose.ui.util.fastFirstOrNull
-import xyz.quaver.pupil.R
-import xyz.quaver.pupil.ui.theme.LightBlue300
-import kotlin.math.abs
-import kotlin.math.max
-import kotlin.math.min
-import kotlin.math.sign
-
-@Composable
-fun OverscrollPager(
- currentPage: Int,
- prevPageAvailable: Boolean,
- nextPageAvailable: Boolean,
- onPageTurn: (Int) -> Unit,
- prevPageTurnIndicatorOffset: Dp = 0.dp,
- nextPageTurnIndicatorOffset: Dp = 0.dp,
- content: @Composable () -> Unit
-) {
- val haptic = LocalHapticFeedback.current
- val pageTurnIndicatorHeight = LocalDensity.current.run { 64.dp.toPx() }
-
- var overscroll: Float? by remember { mutableStateOf(null) }
-
- var size: Size? by remember { mutableStateOf(null) }
- val circleRadius = (size?.width ?: 0f) / 2
-
- val topCircleRadius by animateFloatAsState(if (overscroll?.let { it >= pageTurnIndicatorHeight } == true) circleRadius else 0f)
- val bottomCircleRadius by animateFloatAsState(if (overscroll?.let { it <= -pageTurnIndicatorHeight } == true) circleRadius else 0f)
-
- val prevPageTurnIndicatorOffsetPx = LocalDensity.current.run { prevPageTurnIndicatorOffset.toPx() }
- val nextPageTurnIndicatorOffsetPx = LocalDensity.current.run { nextPageTurnIndicatorOffset.toPx() }
-
- if (topCircleRadius != 0f || bottomCircleRadius != 0f)
- Canvas(modifier = Modifier.fillMaxSize()) {
- drawCircle(
- LightBlue300,
- center = Offset(this.center.x, prevPageTurnIndicatorOffsetPx),
- radius = topCircleRadius
- )
- drawCircle(
- LightBlue300,
- center = Offset(this.center.x, this.size.height-pageTurnIndicatorHeight-nextPageTurnIndicatorOffsetPx),
- radius = bottomCircleRadius
- )
- }
-
- val isOverscrollOverHeight = overscroll?.let { abs(it) >= pageTurnIndicatorHeight } == true
- LaunchedEffect(isOverscrollOverHeight) {
- if (isOverscrollOverHeight) haptic.performHapticFeedback(HapticFeedbackType.LongPress)
- }
-
- Box(
- Modifier.onGloballyPositioned {
- size = it.size.toSize()
- }
- ) {
- overscroll?.let { overscroll ->
- if (overscroll > 0f)
- Row(
- modifier = Modifier
- .align(Alignment.TopCenter)
- .offset(0.dp, prevPageTurnIndicatorOffset),
- verticalAlignment = Alignment.CenterVertically
- ) {
- Icon(
- Icons.Default.NavigateBefore,
- contentDescription = null,
- tint = MaterialTheme.colors.secondary,
- modifier = Modifier.size(48.dp)
- )
- Text(stringResource(R.string.main_move_to_page, currentPage - 1))
- }
-
- if (overscroll < 0f)
- Row(
- modifier = Modifier
- .align(Alignment.BottomCenter)
- .offset(0.dp, -nextPageTurnIndicatorOffset),
- verticalAlignment = Alignment.CenterVertically
- ) {
- Text(stringResource(R.string.main_move_to_page, currentPage + 1))
- Icon(
- Icons.Default.NavigateNext,
- contentDescription = null,
- tint = MaterialTheme.colors.secondary,
- modifier = Modifier.size(48.dp)
- )
- }
- }
-
- Box(
- modifier = Modifier
- .offset(
- 0.dp,
- overscroll
- ?.coerceIn(-pageTurnIndicatorHeight, pageTurnIndicatorHeight)
- ?.let { overscroll -> LocalDensity.current.run { overscroll.toDp() } }
- ?: 0.dp)
- .nestedScroll(object : NestedScrollConnection {
- override fun onPreScroll(
- available: Offset,
- source: NestedScrollSource
- ): Offset {
- val overscrollSnapshot = overscroll
-
- return if (overscrollSnapshot == null || overscrollSnapshot == 0f) {
- 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
-
- Offset(0f, newOverscroll - overscrollSnapshot).also {
- overscroll = newOverscroll
- }
- }
- }
-
- override fun onPostScroll(
- consumed: Offset,
- available: Offset,
- source: NestedScrollSource
- ): Offset {
- if (
- available.y == 0f ||
- !prevPageAvailable && available.y > 0f ||
- !nextPageAvailable && available.y < 0f
- ) return Offset.Zero
-
- return overscroll?.let {
- overscroll = it + available.y
- Offset(0f, available.y)
- } ?: Offset.Zero
- }
- })
- .pointerInput(currentPage) {
- 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?.let {
- if (abs(it) > pageTurnIndicatorHeight)
- onPageTurn(currentPage - it.sign.toInt())
- }
- overscroll = null
- break
- } else
- pointer = otherDown.id
- }
- }
- }
- }
- }
- ) {
- content()
- }
- }
-}
\ No newline at end of file
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
deleted file mode 100644
index 2d144ad4..00000000
--- a/app/src/main/java/xyz/quaver/pupil/sources/composable/ReaderBase.kt
+++ /dev/null
@@ -1,823 +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.sources.composable
-
-import android.app.Application
-import android.net.Uri
-import androidx.compose.animation.core.*
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.combinedClickable
-import androidx.compose.foundation.gestures.detectTapGestures
-import androidx.compose.foundation.layout.*
-import androidx.compose.foundation.lazy.*
-import androidx.compose.material.*
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.AutoFixHigh
-import androidx.compose.material.icons.filled.BrokenImage
-import androidx.compose.material.icons.materialIcon
-import androidx.compose.material.icons.materialPath
-import androidx.compose.runtime.*
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.ExperimentalComposeUiApi
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.composed
-import androidx.compose.ui.draw.clipToBounds
-import androidx.compose.ui.draw.rotate
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.geometry.Rect
-import androidx.compose.ui.geometry.Size
-import androidx.compose.ui.graphics.vector.ImageVector
-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.pointerInput
-import androidx.compose.ui.layout.onGloballyPositioned
-import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.platform.LocalDensity
-import androidx.compose.ui.platform.LocalHapticFeedback
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.unit.Velocity
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.toSize
-import androidx.core.view.ViewCompat
-import androidx.core.view.WindowInsetsCompat
-import androidx.core.view.WindowInsetsControllerCompat
-import androidx.lifecycle.AndroidViewModel
-import androidx.lifecycle.viewModelScope
-import com.google.accompanist.insets.LocalWindowInsets
-import com.google.accompanist.insets.rememberInsetsPaddingValues
-import io.ktor.client.request.*
-import io.ktor.http.*
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.cancelAndJoin
-import kotlinx.coroutines.flow.collect
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.takeWhile
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.sync.Mutex
-import kotlinx.coroutines.sync.withLock
-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.graphics.subsampledimage.ImageSource
-import xyz.quaver.graphics.subsampledimage.SubSampledImage
-import xyz.quaver.graphics.subsampledimage.SubSampledImageState
-import xyz.quaver.graphics.subsampledimage.rememberSubSampledImageState
-import xyz.quaver.io.FileX
-import xyz.quaver.pupil.R
-import xyz.quaver.pupil.proto.ReaderOptions
-import xyz.quaver.pupil.proto.settingsDataStore
-import xyz.quaver.pupil.util.FileXImageSource
-import xyz.quaver.pupil.util.NetworkCache
-import xyz.quaver.pupil.util.activity
-import java.util.concurrent.ConcurrentHashMap
-import kotlin.math.abs
-import kotlin.math.sign
-
-private var _singleImage: ImageVector? = null
-val SingleImage: ImageVector
- get() {
- if (_singleImage != null) {
- return _singleImage!!
- }
-
- _singleImage = materialIcon(name = "ReaderBase.SingleImage") {
- materialPath {
- moveTo(17.0f, 3.0f)
- lineTo(7.0f, 3.0f)
- curveToRelative(-1.1f, 0.0f, -2.0f, 0.9f, -2.0f, 2.0f)
- verticalLineToRelative(14.0f)
- curveToRelative(0.0f, 1.1f, 0.9f, 2.0f, 2.0f, 2.0f)
- horizontalLineToRelative(10.0f)
- curveToRelative(1.1f, 0.0f, 2.0f, -0.9f, 2.0f, -2.0f)
- lineTo(19.0f, 5.0f)
- curveToRelative(0.0f, -1.1f, -0.9f, -2.0f, -2.0f, -2.0f)
- close()
- moveTo(17.0f, 19.0f)
- lineTo(7.0f, 19.0f)
- lineTo(7.0f, 5.0f)
- horizontalLineToRelative(10.0f)
- verticalLineToRelative(14.0f)
- close()
- }
- }
-
- return _singleImage!!
- }
-
-private var _doubleImage: ImageVector? = null
-val DoubleImage: ImageVector
- get() {
- if (_doubleImage != null) {
- return _doubleImage!!
- }
-
- _doubleImage = materialIcon(name = "ReaderBase.DoubleImage") {
- materialPath {
- moveTo(9.0f, 3.0f)
- lineTo(2.0f, 3.0f)
- curveToRelative(-1.1f, 0.0f, -2.0f, 0.9f, -2.0f, 2.0f)
- verticalLineToRelative(14.0f)
- curveToRelative(0.0f, 1.1f, 0.9f, 2.0f, 2.0f, 2.0f)
- horizontalLineToRelative(7.0f)
- curveToRelative(1.1f, 0.0f, 2.0f, -0.9f, 2.0f, -2.0f)
- lineTo(11.0f, 5.0f)
- curveToRelative(0.0f, -1.1f, -0.9f, -2.0f, -2.0f, -2.0f)
- close()
- moveTo(9.0f, 19.0f)
- lineTo(2.0f, 19.0f)
- lineTo(2.0f, 5.0f)
- horizontalLineToRelative(7.0f)
- verticalLineToRelative(14.0f)
- close()
- moveTo(21.0f, 3.0f)
- lineTo(14.0f, 3.0f)
- curveToRelative(-1.1f, 0.0f, -2.0f, 0.9f, -2.0f, 2.0f)
- verticalLineToRelative(14.0f)
- curveToRelative(0.0f, 1.1f, 0.9f, 2.0f, 2.0f, 2.0f)
- horizontalLineToRelative(7.0f)
- curveToRelative(1.1f, 0.0f, 2.0f, -0.9f, 2.0f, -2.0f)
- lineTo(23.0f, 5.0f)
- curveToRelative(0.0f, -1.1f, -0.9f, -2.0f, -2.0f, -2.0f)
- close()
- moveTo(21.0f, 19.0f)
- lineTo(14.0f, 19.0f)
- lineTo(14.0f, 5.0f)
- horizontalLineToRelative(7.0f)
- verticalLineToRelative(14.0f)
- close()
- }
- }
-
- return _doubleImage!!
- }
-
-open class ReaderBaseViewModel(app: Application) : AndroidViewModel(app), DIAware {
- override val di by closestDI(app)
-
- private val logger = newLogger(LoggerFactory.default)
-
- private val cache: NetworkCache by instance()
-
- var fullscreen by mutableStateOf(false)
-
- var error by mutableStateOf(false)
-
- var imageCount by mutableStateOf(0)
-
- val imageList = mutableStateListOf()
- val progressList = mutableStateListOf()
-
- private val progressCollectJobs = ConcurrentHashMap()
-
- private val totalProgressMutex = Mutex()
- var totalProgress by mutableStateOf(0)
- private set
-
- private var urls: List? = null
-
- var loadJob: Job? = null
- @OptIn(ExperimentalCoroutinesApi::class)
- fun load(urls: List, headerBuilder: HeadersBuilder.() -> Unit = { }) {
- this.urls = urls
- viewModelScope.launch {
- loadJob?.cancelAndJoin()
- progressList.clear()
- imageList.clear()
- totalProgressMutex.withLock {
- totalProgress = 0
- }
-
- imageCount = urls.size
-
- progressList.addAll(List(imageCount) { 0f })
- imageList.addAll(List(imageCount) { null })
- totalProgressMutex.withLock {
- totalProgress = 0
- }
-
- loadJob = launch {
- urls.forEachIndexed { index, url ->
- when (val scheme = url.takeWhile { it != ':' }) {
- "http", "https" -> {
- val (flow, file) = cache.load {
- url(url)
- headers(headerBuilder)
- }
-
- imageList[index] = Uri.fromFile(file)
- progressCollectJobs[index] = launch {
- flow.takeWhile { it.isFinite() }.collect {
- progressList[index] = it
- }
-
- progressList[index] = flow.value
- totalProgressMutex.withLock {
- totalProgress++
- }
- }
- }
- "content" -> {
- imageList[index] = Uri.parse(url)
- progressList[index] = Float.POSITIVE_INFINITY
- totalProgressMutex.withLock {
- totalProgress++
- }
- }
- else -> {
- logger.warning(IllegalArgumentException("Expected URL scheme 'http(s)' or 'content' but was '$scheme'"))
- progressList[index] = Float.NEGATIVE_INFINITY
- }
- }
- }
- }
- }
- }
-
- fun error(index: Int) {
- progressList[index] = Float.NEGATIVE_INFINITY
- }
-
- override fun onCleared() {
- urls?.let { cache.free(it) }
- cache.cleanup()
- }
-}
-
-val ReaderOptions.Orientation.isVertical: Boolean
- get() =
- this == ReaderOptions.Orientation.VERTICAL_DOWN ||
- this == ReaderOptions.Orientation.VERTICAL_UP
-val ReaderOptions.Orientation.isReverse: Boolean
- get() =
- this == ReaderOptions.Orientation.VERTICAL_UP ||
- this == ReaderOptions.Orientation.HORIZONTAL_LEFT
-
-@Composable
-fun ReaderOptionsSheet(readerOptions: ReaderOptions, onOptionsChange: (ReaderOptions.Builder.() -> Unit) -> Unit) {
- CompositionLocalProvider(LocalTextStyle provides MaterialTheme.typography.h6) {
- Column(Modifier.padding(16.dp, 0.dp)) {
- val layout = readerOptions.layout
- val snap = readerOptions.snap
- val orientation = readerOptions.orientation
- val padding = readerOptions.padding
-
- Row(
- modifier = Modifier.fillMaxWidth(),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.SpaceBetween
- ) {
- Text("Layout")
-
- Row {
- listOf(
- ReaderOptions.Layout.SINGLE_PAGE to SingleImage,
- ReaderOptions.Layout.DOUBLE_PAGE to DoubleImage,
- ReaderOptions.Layout.AUTO to Icons.Default.AutoFixHigh
- ).forEach { (option, icon) ->
- IconButton(onClick = {
- onOptionsChange {
- setLayout(option)
- }
- }) {
- Icon(
- icon,
- contentDescription = null,
- tint =
- if (layout == option) MaterialTheme.colors.secondary
- else LocalContentColor.current
- )
- }
- }
- }
- }
-
- val infiniteTransition = rememberInfiniteTransition()
-
- val isReverse = orientation.isReverse
- val isVertical = orientation.isVertical
-
- val animationOrientation = if (isReverse) -1f else 1f
- val animationSpacing by animateFloatAsState(if (padding) 48f else 32f)
- val animationOffset by infiniteTransition.animateFloat(
- initialValue = animationOrientation * (if (snap) 0f else animationSpacing/2),
- targetValue = animationOrientation * (if (snap) -animationSpacing else -animationSpacing/2),
- animationSpec = infiniteRepeatable(
- animation = tween(
- durationMillis = 1000,
- easing = if(snap) FastOutSlowInEasing else LinearEasing
- ),
- repeatMode = RepeatMode.Restart
- )
- )
- val animationRotation by animateFloatAsState(if (isVertical) 90f else 0f)
-
- val setOrientation: (Boolean, Boolean) -> Unit = { isVertical, isReverse ->
- val orientation = when {
- isVertical && !isReverse -> ReaderOptions.Orientation.VERTICAL_DOWN
- isVertical && isReverse -> ReaderOptions.Orientation.VERTICAL_UP
- !isVertical && !isReverse -> ReaderOptions.Orientation.HORIZONTAL_RIGHT
- !isVertical && isReverse -> ReaderOptions.Orientation.HORIZONTAL_LEFT
- else -> error("Invalid value")
- }
-
- onOptionsChange {
- setOrientation(orientation)
- }
- }
-
- Box(
- modifier = Modifier
- .size(48.dp)
- .clipToBounds()
- .rotate(animationRotation)
- .align(Alignment.CenterHorizontally)
- ) {
- for (i in 0..4)
- Icon(
- SingleImage,
- contentDescription = null,
- modifier = Modifier
- .size(48.dp)
- .align(Alignment.CenterStart)
- .offset((animationOffset + animationSpacing * (i - 2)).dp, 0.dp)
- )
- }
-
- Row(
- modifier = Modifier.fillMaxWidth(),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.SpaceBetween
- ) {
- Text("Orientation")
-
- CompositionLocalProvider(LocalTextStyle provides MaterialTheme.typography.caption) {
- Row(verticalAlignment = Alignment.CenterVertically) {
- Text("H")
- Switch(checked = isVertical, onCheckedChange = {
- setOrientation(!isVertical, isReverse)
- })
- Text("V")
- }
- }
- }
-
- Row(
- modifier = Modifier.fillMaxWidth(),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.SpaceBetween
- ) {
- Text("Reverse")
- Switch(checked = isReverse, onCheckedChange = {
- setOrientation(isVertical, !isReverse)
- })
- }
-
- Row(
- modifier = Modifier.fillMaxWidth(),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.SpaceBetween
- ) {
- Text("Snap")
-
- Switch(checked = snap, onCheckedChange = {
- onOptionsChange {
- setSnap(!snap)
- }
- })
- }
-
- Row(
- modifier = Modifier.fillMaxWidth(),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.SpaceBetween
- ) {
- Text("Padding")
-
- Switch(checked = padding, onCheckedChange = {
- onOptionsChange {
- setPadding(!padding)
- }
- })
- }
-
- Box(
- Modifier
- .fillMaxWidth()
- .height(8.dp))
- }
- }
-}
-
-@Composable
-fun BoxScope.ReaderLazyList(
- modifier: Modifier = Modifier,
- state: LazyListState = rememberLazyListState(),
- orientation: ReaderOptions.Orientation,
- onScroll: (direction: Float) -> Unit,
- content: LazyListScope.() -> Unit
-) {
- val isReverse = orientation.isReverse
-
- val nestedScrollConnection = remember(orientation) { object: NestedScrollConnection {
- override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
- onScroll(
- when (orientation) {
- ReaderOptions.Orientation.VERTICAL_DOWN -> available.y.sign
- ReaderOptions.Orientation.VERTICAL_UP -> -(available.y.sign)
- ReaderOptions.Orientation.HORIZONTAL_RIGHT -> available.x.sign
- ReaderOptions.Orientation.HORIZONTAL_LEFT -> -(available.x.sign)
- }
- )
-
- return Offset.Zero
- }
- } }
-
- when (orientation) {
- ReaderOptions.Orientation.VERTICAL_DOWN,
- ReaderOptions.Orientation.VERTICAL_UP ->
- LazyColumn(
- modifier = modifier
- .fillMaxSize()
- .align(Alignment.TopStart)
- .nestedScroll(nestedScrollConnection),
- state = state,
- contentPadding = rememberInsetsPaddingValues(LocalWindowInsets.current.navigationBars),
- reverseLayout = isReverse,
- content = content
- )
- ReaderOptions.Orientation.HORIZONTAL_RIGHT,
- ReaderOptions.Orientation.HORIZONTAL_LEFT ->
- LazyRow(
- modifier = modifier
- .fillMaxSize()
- .align(Alignment.CenterStart)
- .nestedScroll(nestedScrollConnection),
- state = state,
- reverseLayout = isReverse,
- content = content
- )
- }
-}
-
-data class ReaderItemData(
- val index: Int,
- val size: Size?,
- val imageSource: ImageSource?
-)
-
-@ExperimentalFoundationApi
-@Composable
-fun ReaderItem(
- model: ReaderBaseViewModel,
- readerOptions: ReaderOptions,
- listSize: Size,
- images: List,
- onTap: () -> Unit = { }
-) {
- val (widthDp, heightDp) = LocalDensity.current.run { listSize.width.toDp() to listSize.height.toDp() }
-
- Row(
- modifier = when {
- readerOptions.padding -> Modifier.size(widthDp, heightDp)
- readerOptions.orientation.isVertical -> Modifier.fillMaxWidth()
- else -> Modifier.fillMaxHeight()
- },
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.Center
- ) {
- images.let { if (readerOptions.orientation.isReverse) it.reversed() else it }.forEach { (index, imageSize, imageSource) ->
- val state = rememberSubSampledImageState().apply {
- isGestureEnabled = true
- }
-
- val modifier = when {
- imageSize == null -> Modifier
- .weight(1f)
- .height(heightDp)
- readerOptions.padding -> Modifier
- .fillMaxHeight()
- .widthIn(0.dp, widthDp / images.size)
- .aspectRatio(imageSize.width / imageSize.height)
- readerOptions.orientation.isVertical -> Modifier
- .weight(1f)
- .aspectRatio(imageSize.width / imageSize.height)
- else -> Modifier.aspectRatio(imageSize.width/imageSize.height)
- }
-
-
- Box(
- modifier,
- contentAlignment = Alignment.Center
- ) {
- val progress = model.progressList.getOrNull(index) ?: 0f
-
- if (progress == Float.NEGATIVE_INFINITY)
- Icon(Icons.Filled.BrokenImage, null)
- else if (progress.isFinite())
- Column(
- horizontalAlignment = Alignment.CenterHorizontally
- ) {
- LinearProgressIndicator(progress)
- Text((index + 1).toString())
- }
- else if (progress == Float.POSITIVE_INFINITY) {
- SubSampledImage(
- modifier = Modifier
- .fillMaxSize()
- .run {
- if (model.fullscreen)
- doubleClickCycleZoom(state, 2f, onTap = onTap)
- else
- combinedClickable(
- onLongClick = {
-
- }
- ) {
- model.fullscreen = true
- }
- },
- imageSource = imageSource,
- state = state,
- onError = {
- model.error(index)
- }
- )
- }
- }
- }
- }
-}
-
-@ExperimentalFoundationApi
-fun LazyListScope.ReaderLazyListContent(
- model: ReaderBaseViewModel,
- listSize: Size,
- imageSources: List,
- imageSizes: List,
- readerOptions: ReaderOptions,
- onTap: () -> Unit = { }
-) {
- when (readerOptions.layout) {
- ReaderOptions.Layout.SINGLE_PAGE ->
- itemsIndexed(imageSources) { index, source ->
- ReaderItem(model, readerOptions, listSize, listOf(ReaderItemData(index, imageSizes[index], source)))
- }
- ReaderOptions.Layout.DOUBLE_PAGE ->
- itemsIndexed(imageSources.chunked(2), key = { i, _ -> i*2 }) { chunkIndex, sourceList ->
- ReaderItem(model, readerOptions, listSize, sourceList.mapIndexed { i, it ->
- val index = chunkIndex*2+i
- ReaderItemData(index, imageSizes[index], it)
- }, onTap)
- }
- ReaderOptions.Layout.AUTO -> {
- val images = mutableListOf>()
-
- var i = 0
- while (i < imageSizes.size) {
- val list = mutableListOf(i)
-
- if (
- imageSizes[i] != null &&
- imageSizes.getOrNull(i+1) != null &&
- listSize != Size.Zero &&
- imageSizes[i]!!.width*listSize.height/imageSizes[i]!!.height +
- imageSizes[i+1]!!.width*listSize.height/imageSizes[i+1]!!.height < listSize.width
- ) list.add(++i)
-
- images.add(list)
- i++
- }
-
- items(images, key = { it.first() }) { images ->
- ReaderItem(model, readerOptions, listSize, images.map { ReaderItemData(it, imageSizes[it], imageSources[it]) }, onTap)
- }
- }
- else -> itemsIndexed(imageSources) { index, source ->
- ReaderItem(model, readerOptions, listSize, listOf(ReaderItemData(index, imageSizes[index], source)), onTap)
- }
- }
-}
-
-@ExperimentalComposeUiApi
-@ExperimentalMaterialApi
-@ExperimentalFoundationApi
-@Composable
-fun ReaderBase(
- modifier: Modifier = Modifier,
- model: ReaderBaseViewModel,
- listState: LazyListState = rememberLazyListState(),
- onScroll: (direction: Float) -> Unit = { }
-) {
- val context = LocalContext.current
- val haptic = LocalHapticFeedback.current
-
- val coroutineScope = rememberCoroutineScope()
-
- val scaffoldState = rememberScaffoldState()
- val snackbarCoroutineScope = rememberCoroutineScope()
-
- var scrollDirection by remember { mutableStateOf(0f) }
- val handleOffset by animateDpAsState(if (model.fullscreen || scrollDirection < 0f) (-36).dp else 0.dp)
-
- val mainReaderOptions by remember {
- context.settingsDataStore.data.map { it.mainReaderOption }
- }.collectAsState(ReaderOptions.getDefaultInstance())
-
- LaunchedEffect(scrollDirection) {
- onScroll(scrollDirection)
- }
-
- LaunchedEffect(model.fullscreen) {
- context.activity?.window?.let { window ->
- ViewCompat.getWindowInsetsController(window.decorView)?.let {
- if (model.fullscreen) {
- it.systemBarsBehavior =
- WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
- it.hide(WindowInsetsCompat.Type.systemBars())
- } else
- it.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
- )
- }
- }
-
- Box(modifier) {
- ModalTopSheetLayout(
- modifier = Modifier.offset(0.dp, handleOffset),
- drawerContent = {
- ReaderOptionsSheet(mainReaderOptions) { readerOptionsBlock ->
- coroutineScope.launch {
- context.settingsDataStore.updateData {
- it.toBuilder().setMainReaderOption(
- mainReaderOptions.toBuilder().apply(readerOptionsBlock).build()
- ).build()
- }
- }
- }
- }
- ) {
- var listSize: Size? by remember { mutableStateOf(null) }
-
- val nestedScrollConnection = remember { object: NestedScrollConnection {
- override suspend fun onPreFling(available: Velocity): Velocity {
- return if (
- mainReaderOptions.snap &&
- listState.layoutInfo.visibleItemsInfo.size > 1
- ) {
- val velocity = when (mainReaderOptions.orientation) {
- ReaderOptions.Orientation.VERTICAL_DOWN -> available.y
- ReaderOptions.Orientation.VERTICAL_UP -> -(available.y)
- ReaderOptions.Orientation.HORIZONTAL_RIGHT -> available.x
- ReaderOptions.Orientation.HORIZONTAL_LEFT -> -(available.x)
- }
-
- val index = listState.firstVisibleItemIndex
-
- coroutineScope.launch {
- when {
- velocity < 0f -> listState.animateScrollToItem(index+1)
- else -> listState.animateScrollToItem(index)
- }
- }
-
- available
- } else Velocity.Zero
-
- }
- } }
-
- val imageSources = remember { mutableStateListOf() }
- val imageSizes = remember { mutableStateListOf() }
-
- LaunchedEffect(model.totalProgress) {
- val size = model.progressList.size
-
- if (imageSources.size != size)
- imageSources.addAll(List (size-imageSources.size) { null })
-
- if (imageSizes.size != size)
- imageSizes.addAll(List (size-imageSizes.size) { null })
-
- coroutineScope.launch {
- repeat(size) { i ->
- val uri = model.imageList[i]
-
- if (imageSources[i] == null && uri != null)
- imageSources[i] = FileXImageSource(FileX(context, uri))
-
- if (imageSizes[i] == null && model.progressList[i] == Float.POSITIVE_INFINITY)
- imageSources[i]?.let {
- imageSizes[i] = runCatching { it.imageSize }.getOrNull()
- }
- }
- }
- }
-
- ReaderLazyList(
- Modifier
- .onGloballyPositioned { listSize = it.size.toSize() }
- .nestedScroll(nestedScrollConnection),
- listState,
- mainReaderOptions.orientation,
- onScroll = { scrollDirection = it },
- ) {
- ReaderLazyListContent(
- model,
- listSize ?: Size.Zero,
- imageSources,
- imageSizes,
- mainReaderOptions
- ) {
- coroutineScope.launch {
- listState.scrollToItem(listState.firstVisibleItemIndex + 1)
- }
- }
- }
-
- if (model.progressList.any { it.isFinite() })
- LinearProgressIndicator(
- modifier = Modifier
- .fillMaxWidth()
- .align(Alignment.TopCenter),
- progress = model.progressList.map { if (it.isInfinite()) 1f else abs(it) }
- .sum() / model.progressList.size,
- color = MaterialTheme.colors.secondary
- )
-
- SnackbarHost(
- scaffoldState.snackbarHostState,
- modifier = Modifier.align(Alignment.BottomCenter)
- )
- }
- }
-}
-
-fun Modifier.doubleClickCycleZoom(
- state: SubSampledImageState,
- scale: Float = 2f,
- animationSpec: AnimationSpec = spring(),
- onTap: () -> Unit = { },
-) = composed {
- val initialImageRect by produceState(null, state.canvasSize, state.imageSize) {
- state.canvasSize?.let { canvasSize ->
- state.imageSize?.let { imageSize ->
- value = state.bound(state.scaleType(canvasSize, imageSize), canvasSize)
- } }
- }
-
- val coroutineScope = rememberCoroutineScope()
-
- pointerInput(Unit) {
- detectTapGestures(
- onTap = { onTap() },
- onDoubleTap = { centroid ->
- val imageRect = state.imageRect
- coroutineScope.launch {
- if (imageRect == null || imageRect != initialImageRect)
- state.resetImageRect(animationSpec)
- else {
- state.setImageRectWithBound(
- Rect(
- Offset(
- centroid.x - (centroid.x - imageRect.left) * scale,
- centroid.y - (centroid.y - imageRect.top) * scale
- ),
- imageRect.size * scale
- ), animationSpec
- )
- }
- }
- }
- )
- }
-}
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
deleted file mode 100644
index 78ac47e0..00000000
--- a/app/src/main/java/xyz/quaver/pupil/sources/composable/SearchBase.kt
+++ /dev/null
@@ -1,202 +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.sources.composable
-
-import android.app.Application
-import androidx.appcompat.graphics.drawable.DrawerArrowDrawable
-import androidx.compose.animation.core.animateFloat
-import androidx.compose.animation.core.animateFloatAsState
-import androidx.compose.animation.core.updateTransition
-import androidx.compose.foundation.Canvas
-import androidx.compose.foundation.gestures.awaitFirstDown
-import androidx.compose.foundation.gestures.forEachGesture
-import androidx.compose.foundation.layout.*
-import androidx.compose.material.*
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.NavigateBefore
-import androidx.compose.material.icons.filled.NavigateNext
-import androidx.compose.runtime.*
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.hapticfeedback.HapticFeedbackType
-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.platform.LocalHapticFeedback
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.unit.Velocity
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.util.fastFirstOrNull
-import androidx.lifecycle.AndroidViewModel
-import androidx.lifecycle.ViewModel
-import androidx.lifecycle.viewmodel.compose.viewModel
-import com.google.accompanist.drawablepainter.rememberDrawablePainter
-import com.google.accompanist.insets.LocalWindowInsets
-import com.google.accompanist.insets.navigationBarsPadding
-import com.google.accompanist.insets.rememberInsetsPaddingValues
-import com.google.accompanist.insets.statusBarsPadding
-import com.google.accompanist.insets.ui.Scaffold
-import xyz.quaver.pupil.R
-import xyz.quaver.pupil.ui.theme.LightBlue300
-import kotlin.math.*
-
-private enum class NavigationIconState {
- MENU,
- ARROW
-}
-
-open class SearchBaseViewModel : ViewModel() {
- val searchResults = mutableStateListOf()
-
- var sortModeIndex by mutableStateOf(0)
- private set
-
- var currentPage by mutableStateOf(1)
-
- var totalItems by mutableStateOf(0)
-
- var maxPage by mutableStateOf(0)
-
- val prevPageAvailable by derivedStateOf { currentPage > 1 }
- val nextPageAvailable by derivedStateOf { currentPage <= maxPage }
-
- var query by mutableStateOf("")
-
- var loading by mutableStateOf(false)
- var error by mutableStateOf(false)
-
- //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.(contentPadding: PaddingValues) -> 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 statusBarsPaddingValues = rememberInsetsPaddingValues(insets = LocalWindowInsets.current.statusBars)
-
- val searchBarDefaultOffset = statusBarsPaddingValues.calculateTopPadding() + 64.dp
- val searchBarDefaultOffsetPx = LocalDensity.current.run { searchBarDefaultOffset.roundToPx() }
-
- LaunchedEffect(navigationIconProgress) {
- navigationIcon.progress = navigationIconProgress
- }
-
- Scaffold(
- floatingActionButton = {
- MultipleFloatingActionButton(
- modifier = Modifier.navigationBarsPadding(),
- items = fabSubMenu,
- visible = model.isFabVisible,
- targetState = isFabExpanded,
- onStateChanged = {
- isFabExpanded = it
- }
- )
- }
- ) { contentPadding ->
- Box(Modifier.padding(contentPadding).fillMaxSize()) {
- OverscrollPager(
- currentPage = model.currentPage,
- prevPageAvailable = model.prevPageAvailable,
- nextPageAvailable = model.nextPageAvailable,
- onPageTurn = { model.currentPage = it },
- prevPageTurnIndicatorOffset = searchBarDefaultOffset,
- nextPageTurnIndicatorOffset = rememberInsetsPaddingValues(LocalWindowInsets.current.navigationBars).calculateBottomPadding()
- ) {
- Box(
- Modifier
- .nestedScroll(object: NestedScrollConnection {
- override fun onPreScroll(
- available: Offset,
- source: NestedScrollSource
- ): Offset {
- model.searchBarOffset =
- (model.searchBarOffset + available.y.roundToInt()).coerceIn(
- -searchBarDefaultOffsetPx,
- 0
- )
-
- model.isFabVisible = available.y > 0f
-
- return Offset.Zero
- }
- })
- ) {
- content(PaddingValues(0.dp, searchBarDefaultOffset, 0.dp, rememberInsetsPaddingValues(
- insets = LocalWindowInsets.current.navigationBars
- ).calculateBottomPadding()))
- }
- }
-
- if (model.loading)
- CircularProgressIndicator(Modifier.align(Alignment.Center))
-
- FloatingSearchBar(
- modifier = Modifier
- .statusBarsPadding()
- .offset(0.dp, LocalDensity.current.run { model.searchBarOffset.toDp() }),
- query = model.query,
- onQueryChange = { model.query = it },
- navigationIcon = {
- IconButton(onClick = { focusManager.clearFocus() }) {
- Icon(
- painter = rememberDrawablePainter(navigationIcon),
- contentDescription = null
- )
- }
- },
- actions = actions,
- onSearch = { onSearch(); focusManager.clearFocus() },
- onTextFieldFocused = { navigationIconState = NavigationIconState.ARROW },
- onTextFieldUnfocused = { navigationIconState = NavigationIconState.MENU }
- )
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/xyz/quaver/pupil/sources/hitomi/Database.kt b/app/src/main/java/xyz/quaver/pupil/sources/hitomi/Database.kt
deleted file mode 100644
index 43990fd0..00000000
--- a/app/src/main/java/xyz/quaver/pupil/sources/hitomi/Database.kt
+++ /dev/null
@@ -1,50 +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.sources.hitomi
-
-import androidx.room.*
-import kotlinx.coroutines.flow.Flow
-
-@Entity
-data class Favorite(
- @PrimaryKey val item: String,
- val timestamp: Long = System.currentTimeMillis()
-)
-
-@Dao
-interface FavoritesDao {
- @Query("SELECT * FROM favorite")
- fun getAll(): Flow>
-
- @Query("SELECT EXISTS(SELECT * FROM favorite WHERE item = :item)")
- fun contains(item: String): Flow
-
- @Insert(onConflict = OnConflictStrategy.REPLACE)
- suspend fun insert(favorite: Favorite)
- suspend fun insert(item: String) = insert(Favorite(item))
-
- @Delete
- suspend fun delete(favorite: Favorite)
- suspend fun delete(item: String) = delete(Favorite(item))
-}
-
-@Database(entities = [Favorite::class], version = 1, exportSchema = false)
-abstract class HitomiDatabase : RoomDatabase() {
- abstract fun favoritesDao(): FavoritesDao
-}
\ 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
deleted file mode 100644
index 674d7f6d..00000000
--- a/app/src/main/java/xyz/quaver/pupil/sources/hitomi/Hitomi.kt
+++ /dev/null
@@ -1,304 +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.sources.hitomi
-
-import android.app.Application
-import androidx.activity.compose.BackHandler
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.Image
-import androidx.compose.foundation.layout.Arrangement
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.lazy.GridCells
-import androidx.compose.foundation.lazy.LazyVerticalGrid
-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.runtime.*
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.ExperimentalComposeUiApi
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.LocalContext
-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.lifecycle.viewmodel.compose.viewModel
-import androidx.navigation.NavController
-import androidx.navigation.NavGraphBuilder
-import androidx.navigation.compose.composable
-import androidx.navigation.compose.navigation
-import androidx.room.Room
-import com.google.accompanist.insets.LocalWindowInsets
-import com.google.accompanist.insets.rememberInsetsPaddingValues
-import com.google.accompanist.insets.ui.Scaffold
-import com.google.accompanist.insets.ui.TopAppBar
-import io.ktor.client.*
-import kotlinx.coroutines.launch
-import org.kodein.di.*
-import org.kodein.di.android.closestDI
-import org.kodein.di.android.subDI
-import org.kodein.di.compose.rememberInstance
-import org.kodein.di.compose.rememberViewModel
-import org.kodein.di.compose.withDI
-import org.kodein.log.LoggerFactory
-import org.kodein.log.newLogger
-import xyz.quaver.pupil.R
-import xyz.quaver.pupil.proto.settingsDataStore
-import xyz.quaver.pupil.sources.Source
-import xyz.quaver.pupil.sources.composable.*
-import xyz.quaver.pupil.sources.hitomi.composable.DetailedSearchResult
-import xyz.quaver.pupil.sources.hitomi.lib.GalleryInfo
-import xyz.quaver.pupil.sources.hitomi.lib.getGalleryInfo
-import xyz.quaver.pupil.sources.hitomi.lib.getReferer
-import xyz.quaver.pupil.sources.hitomi.lib.imageUrlFromImage
-import xyz.quaver.pupil.ui.theme.Orange500
-import java.util.*
-
-@OptIn(ExperimentalMaterialApi::class, ExperimentalFoundationApi::class, ExperimentalComposeUiApi::class)
-class Hitomi(app: Application) : Source(), DIAware {
- override val di by subDI(closestDI(app)) {
- bindSingleton {
- Room.databaseBuilder(app, HitomiDatabase::class.java, name).build()
- }
-
- bindProvider { HitomiSearchResultViewModel(instance()) }
- }
-
- private val client: HttpClient by instance()
-
- private val logger = newLogger(LoggerFactory.default)
-
- override val name: String = "hitomi.la"
- override val iconResID: Int = R.drawable.hitomi
-
- override fun NavGraphBuilder.navGraph(navController: NavController) {
- navigation(startDestination = "hitomi.la/search", route = name) {
- composable("hitomi.la/search") { withDI(di) { Search(navController) } }
- composable("hitomi.la/reader/{itemID}") { withDI(di) { Reader(navController) } }
- }
- }
-
- @Composable
- fun Search(navController: NavController) {
- val model: HitomiSearchResultViewModel by rememberViewModel()
- val database: HitomiDatabase by rememberInstance()
- val favoritesDao = remember { database.favoritesDao() }
- val coroutineScope = rememberCoroutineScope()
-
- val favorites by favoritesDao.getAll().collectAsState(emptyList())
- val favoritesSet by derivedStateOf {
- Collections.unmodifiableSet(favorites.mapTo(mutableSetOf()) { it.item })
- }
-
- val context = LocalContext.current
- LaunchedEffect(Unit) {
- context.settingsDataStore.updateData {
- it.toBuilder()
- .setRecentSource(name)
- .build()
- }
- }
-
- var sourceSelectDialog by remember { mutableStateOf(false) }
-
- if (sourceSelectDialog)
- SourceSelectDialog(navController, name) { sourceSelectDialog = false }
-
- LaunchedEffect(model.currentPage, model.sortByPopularity) {
- model.search()
- }
-
- SearchBase(
- model,
- fabSubMenu = listOf(
- SubFabItem(
- stringResource(R.string.main_jump_title)
- ) {
- Icon(painterResource(R.drawable.ic_jump), contentDescription = null)
- },
- SubFabItem(
- stringResource(R.string.main_fab_random)
- ) {
- Icon(Icons.Default.Shuffle, contentDescription = null)
- },
- SubFabItem(
- stringResource(R.string.main_open_gallery_by_id)
- ) {
- Icon(painterResource(R.drawable.numeric), contentDescription = null)
- }
- ),
- actions = {
- var expanded by remember { mutableStateOf(false) }
-
- IconButton(onClick = { sourceSelectDialog = true }) {
- Image(
- painter = painterResource(id = R.drawable.hitomi),
- contentDescription = null,
- modifier = Modifier.size(24.dp)
- )
- }
-
- IconButton(onClick = { expanded = true }) {
- Icon(Icons.Default.Sort, contentDescription = null)
- }
-
- IconButton(onClick = { navController.navigate("settings") }) {
- Icon(Icons.Default.Settings, contentDescription = null)
- }
-
- val onClick: (Boolean?) -> Unit = {
- expanded = false
-
- it?.let {
- model.sortByPopularity = it
- }
- }
- DropdownMenu(expanded, onDismissRequest = { onClick(null) }) {
- DropdownMenuItem(onClick = { onClick(false) }) {
- Row(
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.spacedBy(16.dp)
- ) {
- Text(stringResource(R.string.main_menu_sort_newest))
- RadioButton(selected = !model.sortByPopularity, onClick = { onClick(false) })
- }
- }
-
- Divider()
-
- DropdownMenuItem(onClick = { onClick(true) }){
- Row(
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.spacedBy(16.dp)
- ) {
- Text(stringResource(R.string.main_menu_sort_popular))
- RadioButton(selected = model.sortByPopularity, onClick = { onClick(true) })
- }
- }
- }
- },
- onSearch = { model.search() }
- ) { contentPadding ->
- LazyVerticalGrid(
- cells = GridCells.Adaptive(minSize = 500.dp),
- contentPadding = contentPadding
- ) {
- items(model.searchResults) {
- DetailedSearchResult(
- it,
- favorites = favoritesSet,
- onFavoriteToggle = {
- coroutineScope.launch {
- if (it in favoritesSet) favoritesDao.delete(it)
- else favoritesDao.insert(it)
- }
- }
- ) { result ->
- navController.navigate("hitomi.la/reader/${result.itemID}")
- }
- }
- }
- }
- }
-
- @Composable
- fun Reader(navController: NavController) {
- val model: ReaderBaseViewModel = viewModel()
-
- val database: HitomiDatabase by rememberInstance()
- val favoritesDao = remember { database.favoritesDao() }
-
- val coroutineScope = rememberCoroutineScope()
-
- val itemID = navController.currentBackStackEntry?.arguments?.getString("itemID")
-
- if (itemID == null) model.error = true
-
- val isFavorite by favoritesDao.contains(itemID ?: "").collectAsState(false)
- val galleryInfo by produceState(null) {
- runCatching {
- val galleryID = itemID!!.toInt()
-
- value = getGalleryInfo(client, galleryID).also {
- model.load(it.files.map { imageUrlFromImage(galleryID, it, false) }) {
- append("Referer", getReferer(galleryID))
- }
- }
- }.onFailure {
- model.error = true
- }
- }
-
- BackHandler {
- if (model.fullscreen) model.fullscreen = false
- else navController.popBackStack()
- }
-
- Scaffold(
- topBar = {
- if (!model.fullscreen)
- TopAppBar(
- title = {
- Text(
- galleryInfo?.title ?: stringResource(R.string.reader_loading),
- maxLines = 1,
- overflow = TextOverflow.Ellipsis
- )
- },
- actions = {
- IconButton({ }) {
- Image(
- painter = painterResource(R.drawable.hitomi),
- contentDescription = null,
- modifier = Modifier.size(24.dp)
- )
- }
-
- IconButton(onClick = {
- itemID?.let {
- coroutineScope.launch {
- if (isFavorite) favoritesDao.delete(it)
- else favoritesDao.insert(it)
- }
- }
- }) {
- Icon(
- if (isFavorite) Icons.Default.Star else Icons.Default.StarOutline,
- contentDescription = null,
- tint = Orange500
- )
- }
- },
- contentPadding = rememberInsetsPaddingValues(
- LocalWindowInsets.current.statusBars,
- applyBottom = false
- )
- )
- }
- ) { contentPadding ->
- ReaderBase(
- Modifier.padding(contentPadding),
- model
- )
- }
- }
-}
\ 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
deleted file mode 100644
index 68da8b44..00000000
--- a/app/src/main/java/xyz/quaver/pupil/sources/hitomi/HitomiSearchResult.kt
+++ /dev/null
@@ -1,33 +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.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
deleted file mode 100644
index bf63237a..00000000
--- a/app/src/main/java/xyz/quaver/pupil/sources/hitomi/HitomiSearchResultViewModel.kt
+++ /dev/null
@@ -1,126 +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.sources.hitomi
-
-import android.util.LruCache
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.setValue
-import androidx.lifecycle.viewModelScope
-import io.ktor.client.*
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.cancelAndJoin
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.yield
-import xyz.quaver.pupil.sources.composable.SearchBaseViewModel
-import xyz.quaver.pupil.sources.hitomi.lib.GalleryBlock
-import xyz.quaver.pupil.sources.hitomi.lib.doSearch
-import xyz.quaver.pupil.sources.hitomi.lib.getGalleryBlock
-import kotlin.math.ceil
-import kotlin.math.max
-import kotlin.math.min
-
-class HitomiSearchResultViewModel(
- private val client: HttpClient
-) : SearchBaseViewModel() {
- private var cachedQuery: String? = null
- private var cachedSortByPopularity: Boolean? = null
- private val cache = mutableListOf()
-
- private val galleryBlockCache = LruCache(100)
-
- var sortByPopularity by mutableStateOf(false)
-
- private var searchJob: Job? = null
- fun search() {
- val resultsPerPage = 25
-
- viewModelScope.launch {
- searchJob?.cancelAndJoin()
-
- searchResults.clear()
- searchBarOffset = 0
- loading = true
- error = false
-
- searchJob = launch {
- if (cachedQuery != query || cachedSortByPopularity != sortByPopularity || cache.isEmpty()) {
- cachedQuery = null
- cache.clear()
-
- yield()
-
- val result = runCatching {
- doSearch(client, query, sortByPopularity)
- }.onFailure {
- error = true
- }.getOrNull()
-
- yield()
-
- result?.let { cache.addAll(result) }
- cachedQuery = query
- totalItems = result?.size ?: 0
- maxPage =
- result?.let { ceil(result.size / resultsPerPage.toDouble()).toInt() }
- ?: 0
- }
-
- yield()
-
- val range = max((currentPage-1)*resultsPerPage, 0) until min(currentPage*resultsPerPage, totalItems)
-
- cache.slice(range)
- .forEach { galleryID ->
- yield()
- loading = false
- kotlin.runCatching {
- galleryBlockCache.get(galleryID) ?: getGalleryBlock(client, galleryID).also {
- galleryBlockCache.put(galleryID, it)
- }
- }.onFailure {
- error = true
- }.getOrNull()?.let {
- searchResults.add(transform(it))
- }
-
- }
- }
-
- viewModelScope.launch {
- searchJob?.join()
- loading = false
- }
- }
- }
-
- 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
deleted file mode 100644
index 99b7667c..00000000
--- a/app/src/main/java/xyz/quaver/pupil/sources/hitomi/composable/SearchResultEntry.kt
+++ /dev/null
@@ -1,317 +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.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(" ")
-}
-
-@ExperimentalMaterialApi
-@Composable
-fun DetailedSearchResult(
- result: HitomiSearchResult,
- favorites: Set,
- onFavoriteToggle: (String) -> Unit = { },
- onClick: (HitomiSearchResult) -> Unit = { }
-) {
- val painter = rememberImagePainter(result.thumbnail)
-
- Card(
- modifier = Modifier
- .padding(8.dp, 4.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,
- favorites,
- onFavoriteToggle = onFavoriteToggle
- )
- }
- }
- }
-
- 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 favorites) Icons.Default.Star else Icons.Default.StarOutline,
- contentDescription = null,
- tint = Orange500,
- modifier = Modifier
- .size(24.dp)
- .clickable {
- onFavoriteToggle(result.itemID)
- }
- )
- }
- }
- }
-}
-
-@ExperimentalMaterialApi
-@Composable
-fun TagGroup(
- tags: List,
- favorites: Set,
- onFavoriteToggle: (String) -> Unit = { }
-) {
- var isFolded by remember { mutableStateOf(true) }
-
- val favoriteTagsInList = favorites intersect tags.toSet()
-
- FlowRow(Modifier.padding(0.dp, 16.dp)) {
- tags.sortedBy { if (favoriteTagsInList.contains(it)) 0 else 1 }
- .let { (if (isFolded) it.take(10) else it) }.forEach { tag ->
- TagChip(
- tag = tag,
- isFavorite = favoriteTagsInList.contains(tag),
- onFavoriteClick = onFavoriteToggle
- )
- }
-
- 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/hitomi/lib/common.kt b/app/src/main/java/xyz/quaver/pupil/sources/hitomi/lib/common.kt
deleted file mode 100644
index 19708ae3..00000000
--- a/app/src/main/java/xyz/quaver/pupil/sources/hitomi/lib/common.kt
+++ /dev/null
@@ -1,121 +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.sources.hitomi.lib
-
-import io.ktor.client.*
-import io.ktor.client.request.*
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.withContext
-import kotlinx.serialization.decodeFromString
-import kotlinx.serialization.json.Json
-
-const val protocol = "https:"
-
-private val json = Json {
- isLenient = true
- ignoreUnknownKeys = true
- allowSpecialFloatingPointValues = true
- useArrayPolymorphism = true
-}
-
-suspend fun getGalleryInfo(client: HttpClient, galleryID: Int): GalleryInfo = withContext(Dispatchers.IO) {
- json.decodeFromString(
- client.get("$protocol//$domain/galleries/$galleryID.js")
- .replace("var galleryinfo = ", "")
- )
-}
-
-//common.js
-const val domain = "ltn.hitomi.la"
-const val galleryblockextension = ".html"
-const val galleryblockdir = "galleryblock"
-const val nozomiextension = ".nozomi"
-
-fun subdomainFromGalleryID(g: Int, numberOfFrontends: Int) : String {
- val o = g % numberOfFrontends
-
- return (97+o).toChar().toString()
-}
-
-fun subdomainFromURL(url: String, base: String? = null) : String {
- var retval = "b"
-
- if (!base.isNullOrBlank())
- retval = base
-
- var numberOfFrontends = 2
- val b = 16
-
- val r = Regex("""/[0-9a-f]/([0-9a-f]{2})/""")
- val m = r.find(url) ?: return "a"
-
- val g = m.groupValues[1].toIntOrNull(b)
-
- if (g != null) {
- val o = when {
- g < 0x7c -> 1
- else -> 0
- }
-
- // retval = subdomainFromGalleryID(g, numberOfFrontends) + retval
- retval = (97+o).toChar().toString() + retval
- }
-
- return retval
-}
-
-fun urlFromURL(url: String, base: String? = null) : String {
- return url.replace(Regex("""//..?\.hitomi\.la/"""), "//${subdomainFromURL(url, base)}.hitomi.la/")
-}
-
-
-fun fullPathFromHash(hash: String?) : String? {
- return when {
- (hash?.length ?: 0) < 3 -> hash
- else -> hash!!.replace(Regex("^.*(..)(.)$"), "$2/$1/$hash")
- }
-}
-
-@Suppress("NAME_SHADOWING", "UNUSED_PARAMETER")
-fun urlFromHash(galleryID: Int, image: GalleryFiles, dir: String? = null, ext: String? = null) : String {
- val ext = ext ?: dir ?: image.name.split('.').last()
- val dir = dir ?: "images"
- return "$protocol//a.hitomi.la/$dir/${fullPathFromHash(image.hash)}.$ext"
-}
-
-fun urlFromUrlFromHash(galleryID: Int, image: GalleryFiles, dir: String? = null, ext: String? = null, base: String? = null) =
- urlFromURL(urlFromHash(galleryID, image, dir, ext), base)
-
-fun rewriteTnPaths(html: String) =
- html.replace(Regex("""//tn\.hitomi\.la/[^/]+/[0-9a-f]/[0-9a-f]{2}/""")) { url ->
- urlFromURL(url.value, "tn")
- }
-
-fun imageUrlFromImage(galleryID: Int, image: GalleryFiles, noWebp: Boolean) : String {
- return when {
- noWebp ->
- urlFromUrlFromHash(galleryID, image)
- //image.hasavif != 0 ->
- // urlFromUrlFromHash(galleryID, image, "avif", null, "a")
- image.haswebp != 0 ->
- urlFromUrlFromHash(galleryID, image, "webp", null, "a")
- else ->
- urlFromUrlFromHash(galleryID, image)
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/xyz/quaver/pupil/sources/hitomi/lib/galleries.kt b/app/src/main/java/xyz/quaver/pupil/sources/hitomi/lib/galleries.kt
deleted file mode 100644
index 7ffe0d58..00000000
--- a/app/src/main/java/xyz/quaver/pupil/sources/hitomi/lib/galleries.kt
+++ /dev/null
@@ -1,84 +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.sources.hitomi.lib
-
-import io.ktor.client.*
-import io.ktor.client.request.*
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.withContext
-import kotlinx.serialization.Serializable
-import org.jsoup.Jsoup
-import java.net.URLDecoder
-
-@Serializable
-data class Gallery(
- val related: List,
- val langList: List>,
- val cover: String,
- val title: String,
- val artists: List,
- val groups: List,
- val type: String,
- val language: String,
- val series: List,
- val characters: List,
- val tags: List,
- val thumbnails: List
-)
-suspend fun getGallery(client: HttpClient, galleryID: Int) : Gallery = withContext(Dispatchers.IO) {
- val url = Jsoup.parse(client.get("https://hitomi.la/galleries/$galleryID.html"))
- .select("link").attr("href")
-
- val doc = Jsoup.parse(client.get(url))
-
- val related = Regex("\\d+")
- .findAll(doc.select("script").first()!!.html())
- .map {
- it.value.toInt()
- }.toList()
-
- val langList = doc.select("#lang-list a").map {
- Pair(it.text(), "$protocol//hitomi.la${it.attr("href")}")
- }
-
- val cover = protocol + doc.selectFirst(".cover img")!!.attr("src")
- val title = doc.selectFirst(".gallery h1 a")!!.text()
- val artists = doc.select(".gallery h2 a").map { it.text() }
- val groups = doc.select(".gallery-info a[href~=^/group/]").map { it.text() }
- val type = doc.selectFirst(".gallery-info a[href~=^/type/]")!!.text()
-
- val language = run {
- val href = doc.select(".gallery-info a[href~=^/index.+\\.html\$]").attr("href")
- Regex("""index-([^-]+)(-.+)?\.html""").find(href)?.groupValues?.getOrNull(1) ?: ""
- }
-
- val series = doc.select(".gallery-info a[href~=^/series/]").map { it.text() }
- val characters = doc.select(".gallery-info a[href~=^/character/]").map { it.text() }
-
- val tags = doc.select(".gallery-info a[href~=^/tag/]").map {
- val href = URLDecoder.decode(it.attr("href"), "UTF-8")
- href.slice(5 until href.indexOf('-'))
- }
-
- val thumbnails = getGalleryInfo(client, galleryID).files.map { galleryInfo ->
- urlFromUrlFromHash(galleryID, galleryInfo, "smalltn", "jpg", "tn")
- }
-
- Gallery(related, langList, cover, title, artists, groups, type, language, series, characters, tags, thumbnails)
-}
\ No newline at end of file
diff --git a/app/src/main/java/xyz/quaver/pupil/sources/hitomi/lib/galleryblock.kt b/app/src/main/java/xyz/quaver/pupil/sources/hitomi/lib/galleryblock.kt
deleted file mode 100644
index bc4e1a9a..00000000
--- a/app/src/main/java/xyz/quaver/pupil/sources/hitomi/lib/galleryblock.kt
+++ /dev/null
@@ -1,105 +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.sources.hitomi.lib
-
-import io.ktor.client.*
-import io.ktor.client.request.*
-import io.ktor.client.statement.*
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.withContext
-import kotlinx.serialization.Serializable
-import org.jsoup.Jsoup
-import java.net.URLDecoder
-
-//galleryblock.js
-suspend fun fetchNozomi(
- client: HttpClient,
- area: String? = null,
- tag: String = "index",
- language: String = "all",
- start: Int = -1,
- count: Int = -1
-) : Pair, Int> = withContext(Dispatchers.IO) {
- val url =
- when(area) {
- null -> "$protocol//$domain/$tag-$language$nozomiextension"
- else -> "$protocol//$domain/$area/$tag-$language$nozomiextension"
- }
-
- val response: HttpResponse = client.get(url) {
- headers {
- if (start != -1 && count != -1) {
- val startByte = start*4
- val endByte = (start+count)*4-1
-
- set("Range", "bytes=$startByte-$endByte")
- }
- }
- }
-
- val totalItems = response.headers["Content-Range"]!!
- .replace(Regex("^[Bb]ytes \\d+-\\d+/"), "").toInt() / 4
-
- response.readBytes().asIterable().chunked(4) {
- (it[0].toInt() and 0xFF) or
- ((it[1].toInt() and 0xFF) shl 8) or
- ((it[2].toInt() and 0xFF) shl 16) or
- ((it[3].toInt() and 0xFF) shl 24)
- } to totalItems
-}
-
-@Serializable
-data class GalleryBlock(
- val id: Int,
- val galleryUrl: String,
- val thumbnails: List,
- val title: String,
- val artists: List,
- val series: List,
- val type: String,
- val language: String,
- val relatedTags: List
-)
-
-suspend fun getGalleryBlock(client: HttpClient, galleryID: Int) : GalleryBlock = withContext(Dispatchers.IO) {
- val url = "$protocol//$domain/$galleryblockdir/$galleryID$extension"
-
- val doc = Jsoup.parse(rewriteTnPaths(client.get(url)))
-
- val galleryUrl = doc.selectFirst("h1 > a")!!.attr("href")
-
- val thumbnails = doc.select(".dj-img-cont img").map { protocol + it.attr("src") }
-
- val title = doc.selectFirst("h1 > a")!!.text()
- val artists = doc.select(".artist-list a").map{ it.text() }
- val series = doc.select(".dj-content a[href~=^/series/]").map { it.text() }
- val type = doc.selectFirst("a[href~=^/type/]")!!.text()
-
- val language = run {
- val href = doc.select("a[href~=^/index.+\\.html\$]").attr("href")
- Regex("""index-([^-]+)(-.+)?\.html""").find(href)?.groupValues?.getOrNull(1) ?: ""
- }
-
- val relatedTags = doc.select(".relatedtags a").map {
- val href = URLDecoder.decode(it.attr("href"), "UTF-8")
- href.slice(5 until href.indexOf("-all"))
- }
-
- GalleryBlock(galleryID, galleryUrl, thumbnails, title, artists, series, type, language, relatedTags)
-}
diff --git a/app/src/main/java/xyz/quaver/pupil/sources/hitomi/lib/reader.kt b/app/src/main/java/xyz/quaver/pupil/sources/hitomi/lib/reader.kt
deleted file mode 100644
index 70e4d833..00000000
--- a/app/src/main/java/xyz/quaver/pupil/sources/hitomi/lib/reader.kt
+++ /dev/null
@@ -1,55 +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.sources.hitomi.lib
-
-import kotlinx.serialization.Serializable
-
-fun getReferer(galleryID: Int) = "https://hitomi.la/reader/$galleryID.html"
-
-@Serializable
-data class Tag(
- val male: String? = null,
- val female: String? = null,
- val url: String,
- val tag: String
-)
-
-@Serializable
-data class GalleryInfo(
- val id: Int? = null,
- val language_localname: String? = null,
- val tags: List = emptyList(),
- val title: String? = null,
- val files: List,
- val date: String? = null,
- val type: String? = null,
- val language: String? = null,
- val japanese_title: String? = null
-)
-
-@Serializable
-data class GalleryFiles(
- val width: Int,
- val hash: String? = null,
- val haswebp: Int = 0,
- val name: String,
- val height: Int,
- val hasavif: Int = 0,
- val hasavifsmalltn: Int? = 0
-)
\ No newline at end of file
diff --git a/app/src/main/java/xyz/quaver/pupil/sources/hitomi/lib/results.kt b/app/src/main/java/xyz/quaver/pupil/sources/hitomi/lib/results.kt
deleted file mode 100644
index ea8bb28a..00000000
--- a/app/src/main/java/xyz/quaver/pupil/sources/hitomi/lib/results.kt
+++ /dev/null
@@ -1,76 +0,0 @@
-package xyz.quaver.pupil.sources.hitomi.lib
-
-import io.ktor.client.*
-import kotlinx.coroutines.async
-import kotlinx.coroutines.coroutineScope
-import java.util.*
-
-suspend fun doSearch(
- client: HttpClient,
- query: String,
- sortByPopularity: Boolean = false
-) : Set = coroutineScope {
- val terms = query
- .trim()
- .replace(Regex("""^\?"""), "")
- .lowercase()
- .split(Regex("\\s+"))
- .map {
- it.replace('_', ' ')
- }
-
- val positiveTerms = LinkedList()
- val negativeTerms = LinkedList()
-
- for (term in terms) {
- if (term.matches(Regex("^-.+")))
- negativeTerms.push(term.replace(Regex("^-"), ""))
- else if (term.isNotBlank())
- positiveTerms.push(term)
- }
-
- val positiveResults = positiveTerms.map {
- async {
- runCatching {
- getGalleryIDsForQuery(client, it)
- }.getOrElse { emptySet() }
- }
- }
-
- val negativeResults = negativeTerms.map {
- async {
- runCatching {
- getGalleryIDsForQuery(client, it)
- }.getOrElse { emptySet() }
- }
- }
-
- var results = when {
- sortByPopularity -> getGalleryIDsFromNozomi(client, null, "popular", "all")
- positiveTerms.isEmpty() -> getGalleryIDsFromNozomi(client, null, "index", "all")
- else -> emptySet()
- }
-
- fun filterPositive(newResults: Set) {
- results = when {
- results.isEmpty() -> newResults
- else -> results intersect newResults
- }
- }
-
- fun filterNegative(newResults: Set) {
- results = results subtract newResults
- }
-
- //positive results
- positiveResults.forEach {
- filterPositive(it.await())
- }
-
- //negative results
- negativeResults.forEach {
- filterNegative(it.await())
- }
-
- results
-}
\ No newline at end of file
diff --git a/app/src/main/java/xyz/quaver/pupil/sources/hitomi/lib/search.kt b/app/src/main/java/xyz/quaver/pupil/sources/hitomi/lib/search.kt
deleted file mode 100644
index 07f836db..00000000
--- a/app/src/main/java/xyz/quaver/pupil/sources/hitomi/lib/search.kt
+++ /dev/null
@@ -1,341 +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.sources.hitomi.lib
-
-import android.util.Log
-import io.ktor.client.*
-import io.ktor.client.call.*
-import io.ktor.client.request.*
-import io.ktor.client.statement.*
-import io.ktor.util.*
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.withContext
-import java.nio.ByteBuffer
-import java.nio.ByteOrder
-import java.security.MessageDigest
-import kotlin.math.min
-
-//searchlib.js
-const val separator = "-"
-const val extension = ".html"
-const val index_dir = "tagindex"
-const val galleries_index_dir = "galleriesindex"
-const val max_node_size = 464
-const val B = 16
-const val compressed_nozomi_prefix = "n"
-
-var _tag_index_version: String? = null
-suspend fun getTagIndexVersion(client: HttpClient): String = _tag_index_version ?: getIndexVersion(client, "tagindex").also {
- _tag_index_version = it
-}
-
-var _galleries_index_version: String? = null
-suspend fun getGalleriesIndexVersion(client: HttpClient): String = _galleries_index_version ?: getIndexVersion(client, "galleriesindex").also {
- _galleries_index_version = it
-}
-
-fun sha256(data: ByteArray) : ByteArray {
- return MessageDigest.getInstance("SHA-256").digest(data)
-}
-
-@OptIn(ExperimentalUnsignedTypes::class)
-fun hashTerm(term: String) : UByteArray {
- return sha256(term.toByteArray()).toUByteArray().sliceArray(0 until 4)
-}
-
-fun sanitize(input: String) : String {
- return input.replace(Regex("[/#]"), "")
-}
-
-suspend fun getIndexVersion(client: HttpClient, name: String): String =
- client.get("$protocol//$domain/$name/version?_=${System.currentTimeMillis()}")
-
-//search.js
-suspend fun getGalleryIDsForQuery(client: HttpClient, query: String) : Set {
- query.replace("_", " ").let {
- if (it.indexOf(':') > -1) {
- val sides = it.split(":")
- val ns = sides[0]
- var tag = sides[1]
-
- var area : String? = ns
- var language = "all"
- when (ns) {
- "female", "male" -> {
- area = "tag"
- tag = it
- }
- "language" -> {
- area = null
- language = tag
- tag = "index"
- }
- }
-
- return getGalleryIDsFromNozomi(client, area, tag, language)
- }
-
- val key = hashTerm(it)
- val field = "galleries"
-
- val node = getNodeAtAddress(client, field, 0) ?: return emptySet()
-
- val data = bSearch(client, field, key, node)
-
- if (data != null)
- return getGalleryIDsFromData(client, data)
-
- return emptySet()
- }
-}
-
-suspend fun getSuggestionsForQuery(client: HttpClient, query: String) : List {
- query.replace('_', ' ').let {
- var field = "global"
- var term = it
-
- if (term.indexOf(':') > -1) {
- val sides = it.split(':')
- field = sides[0]
- term = sides[1]
- }
-
- val key = hashTerm(term)
- val node = getNodeAtAddress(client, field, 0) ?: return emptyList()
- val data = bSearch(client, field, key, node)
-
- if (data != null)
- return getSuggestionsFromData(client, field, data)
-
- return emptyList()
- }
-}
-
-data class Suggestion(val s: String, val t: Int, val u: String, val n: String)
-suspend fun getSuggestionsFromData(client: HttpClient, field: String, data: Pair) : List {
- val url = "$protocol//$domain/$index_dir/$field.${getTagIndexVersion(client)}.data"
- val (offset, length) = data
- if (length > 10000 || length <= 0)
- throw Exception("length $length is too long")
-
- val inbuf = getURLAtRange(client, url, offset.until(offset+length))
-
- val suggestions = ArrayList()
-
- val buffer = ByteBuffer
- .wrap(inbuf)
- .order(ByteOrder.BIG_ENDIAN)
- val numberOfSuggestions = buffer.int
-
- if (numberOfSuggestions > 100 || numberOfSuggestions <= 0)
- throw Exception("number of suggestions $numberOfSuggestions is too long")
-
- for (i in 0.until(numberOfSuggestions)) {
- var top = buffer.int
-
- val ns = inbuf.sliceArray(buffer.position().until(buffer.position()+top)).toString(charset("UTF-8"))
- buffer.position(buffer.position()+top)
-
- top = buffer.int
-
- val tag = inbuf.sliceArray(buffer.position().until(buffer.position()+top)).toString(charset("UTF-8"))
- buffer.position(buffer.position()+top)
-
- val count = buffer.int
-
- val tagname = sanitize(tag)
- val u =
- when(ns) {
- "female", "male" -> "/tag/$ns:$tagname${separator}1$extension"
- "language" -> "/index-$tagname${separator}1$extension"
- else -> "/$ns/$tagname${separator}all${separator}1$extension"
- }
-
- suggestions.add(Suggestion(tag, count, u, ns))
- }
-
- return suggestions
-}
-
-suspend fun getGalleryIDsFromNozomi(client: HttpClient, area: String?, tag: String, language: String) : Set = withContext(Dispatchers.IO) {
- val nozomiAddress =
- when(area) {
- null -> "$protocol//$domain/$compressed_nozomi_prefix/$tag-$language$nozomiextension"
- else -> "$protocol//$domain/$compressed_nozomi_prefix/$area/$tag-$language$nozomiextension"
- }
-
- val bytes: ByteArray = try {
- client.get(nozomiAddress)
- } catch (e: Exception) {
- return@withContext emptySet()
- }
-
- val nozomi = mutableSetOf()
-
- val arrayBuffer = ByteBuffer
- .wrap(bytes)
- .order(ByteOrder.BIG_ENDIAN)
-
- while (arrayBuffer.hasRemaining())
- nozomi.add(arrayBuffer.int)
-
- nozomi
-}
-
-suspend fun getGalleryIDsFromData(client: HttpClient, data: Pair) : Set {
- val url = "$protocol//$domain/$galleries_index_dir/galleries.${getGalleriesIndexVersion(client)}.data"
- val (offset, length) = data
- if (length > 100000000 || length <= 0)
- throw Exception("length $length is too long")
-
- val inbuf = getURLAtRange(client, url, offset.until(offset+length))
-
- val galleryIDs = mutableSetOf()
-
- val buffer = ByteBuffer
- .wrap(inbuf)
- .order(ByteOrder.BIG_ENDIAN)
-
- val numberOfGalleryIDs = buffer.int
-
- val expectedLength = numberOfGalleryIDs*4+4
-
- if (numberOfGalleryIDs > 10000000 || numberOfGalleryIDs <= 0)
- throw Exception("number_of_galleryids $numberOfGalleryIDs is too long")
- else if (inbuf.size != expectedLength)
- throw Exception("inbuf.byteLength ${inbuf.size} != expected_length $expectedLength")
-
- for (i in 0.until(numberOfGalleryIDs))
- galleryIDs.add(buffer.int)
-
- return galleryIDs
-}
-
-suspend fun getNodeAtAddress(client: HttpClient, field: String, address: Long) : Node? {
- val url =
- when(field) {
- "galleries" -> "$protocol//$domain/$galleries_index_dir/galleries.${getGalleriesIndexVersion(client)}.index"
- "languages" -> "$protocol//$domain/$galleries_index_dir/languages.${getGalleriesIndexVersion(client)}.index"
- "nozomiurl" -> "$protocol//$domain/$galleries_index_dir/nozomiurl.${getGalleriesIndexVersion(client)}.index"
- else -> "$protocol//$domain/$index_dir/$field.${getTagIndexVersion(client)}.index"
- }
-
- val nodedata = getURLAtRange(client, url, address.until(address+max_node_size))
-
- return decodeNode(nodedata)
-}
-
-suspend fun getURLAtRange(client: HttpClient, url: String, range: LongRange) : ByteArray = withContext(Dispatchers.IO) {
- client.get(url) {
- headers {
- set("Range", "bytes=${range.first}-${range.last}")
- }
- }
-}
-
-@OptIn(ExperimentalUnsignedTypes::class)
-data class Node(val keys: List, val datas: List>, val subNodeAddresses: List)
-@OptIn(ExperimentalUnsignedTypes::class)
-fun decodeNode(data: ByteArray) : Node {
- val buffer = ByteBuffer
- .wrap(data)
- .order(ByteOrder.BIG_ENDIAN)
-
- val uData = data.toUByteArray()
-
- val numberOfKeys = buffer.int
- val keys = ArrayList()
-
- for (i in 0.until(numberOfKeys)) {
- val keySize = buffer.int
-
- if (keySize == 0 || keySize > 32)
- throw Exception("fatal: !keySize || keySize > 32")
-
- keys.add(uData.sliceArray(buffer.position().until(buffer.position()+keySize)))
- buffer.position(buffer.position()+keySize)
- }
-
- val numberOfDatas = buffer.int
- val datas = ArrayList>()
-
- for (i in 0.until(numberOfDatas)) {
- val offset = buffer.long
- val length = buffer.int
-
- datas.add(Pair(offset, length))
- }
-
- val numberOfSubNodeAddresses = B+1
- val subNodeAddresses = ArrayList()
-
- for (i in 0.until(numberOfSubNodeAddresses)) {
- val subNodeAddress = buffer.long
- subNodeAddresses.add(subNodeAddress)
- }
-
- return Node(keys, datas, subNodeAddresses)
-}
-
-@OptIn(ExperimentalUnsignedTypes::class)
-suspend fun bSearch(client: HttpClient, field: String, key: UByteArray, node: Node) : Pair? {
- fun compareArrayBuffers(dv1: UByteArray, dv2: UByteArray) : Int {
- val top = min(dv1.size, dv2.size)
-
- for (i in 0.until(top)) {
- if (dv1[i] < dv2[i])
- return -1
- else if (dv1[i] > dv2[i])
- return 1
- }
-
- return 0
- }
-
- fun locateKey(key: UByteArray, node: Node) : Pair {
- for (i in node.keys.indices) {
- val cmpResult = compareArrayBuffers(key, node.keys[i])
-
- if (cmpResult <= 0)
- return Pair(cmpResult==0, i)
- }
-
- return Pair(false, node.keys.size)
- }
-
- fun isLeaf(node: Node) : Boolean {
- for (subnode in node.subNodeAddresses)
- if (subnode != 0L)
- return false
-
- return true
- }
-
- if (node.keys.isEmpty())
- return null
-
- val (there, where) = locateKey(key, node)
- if (there)
- return node.datas[where]
- else if (isLeaf(node))
- return null
-
- val nextNode = getNodeAtAddress(client, field, node.subNodeAddresses[where]) ?: return null
- return bSearch(client, field, key, nextNode)
-}
\ No newline at end of file
diff --git a/app/src/main/java/xyz/quaver/pupil/sources/manatoki/Database.kt b/app/src/main/java/xyz/quaver/pupil/sources/manatoki/Database.kt
deleted file mode 100644
index 2280970d..00000000
--- a/app/src/main/java/xyz/quaver/pupil/sources/manatoki/Database.kt
+++ /dev/null
@@ -1,84 +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.sources.manatoki
-
-import androidx.room.*
-import kotlinx.coroutines.flow.Flow
-import java.sql.Timestamp
-
-@Entity
-data class Favorite(
- @PrimaryKey val itemID: String
-)
-
-@Entity
-data class Bookmark(
- @PrimaryKey val itemID: String,
- val page: Int
-)
-
-@Entity
-data class History(
- @PrimaryKey val itemID: String,
- val parent: String,
- val page: Int,
- val timestamp: Long = System.currentTimeMillis()
-)
-
-@Dao
-interface FavoriteDao {
- @Query("SELECT EXISTS(SELECT * FROM favorite WHERE itemID = :itemID)")
- fun contains(itemID: String): Flow
-
- @Insert(onConflict = OnConflictStrategy.REPLACE)
- suspend fun insert(favorite: Favorite)
- suspend fun insert(itemID: String) = insert(Favorite(itemID))
-
- @Delete
- suspend fun delete(favorite: Favorite)
- suspend fun delete(itemID: String) = delete(Favorite(itemID))
-}
-
-@Dao
-interface BookmarkDao {
-
-}
-
-@Dao
-interface HistoryDao {
- @Insert(onConflict = OnConflictStrategy.REPLACE)
- suspend fun insert(history: History)
- suspend fun insert(itemID: String, parent: String, page: Int) = insert(History(itemID, parent, page))
-
- @Query("DELETE FROM history WHERE itemID = :itemID")
- suspend fun delete(itemID: String)
-
- @Query("SELECT parent FROM (SELECT parent, max(timestamp) as t FROM history GROUP BY parent) ORDER BY t DESC")
- fun getRecentManga(): Flow>
-
- @Query("SELECT itemID FROM history WHERE parent = :parent ORDER BY timestamp DESC")
- suspend fun getAll(parent: String): List
-}
-
-@Database(entities = [Favorite::class, Bookmark::class, History::class], version = 1, exportSchema = false)
-abstract class ManatokiDatabase: RoomDatabase() {
- abstract fun favoriteDao(): FavoriteDao
- abstract fun bookmarkDao(): BookmarkDao
- abstract fun historyDao(): HistoryDao
-}
\ No newline at end of file
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
deleted file mode 100644
index d0871c17..00000000
--- a/app/src/main/java/xyz/quaver/pupil/sources/manatoki/Manatoki.kt
+++ /dev/null
@@ -1,88 +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.sources.manatoki
-
-import android.app.Application
-import androidx.compose.animation.ExperimentalAnimationApi
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.lazy.items
-import androidx.compose.material.Button
-import androidx.compose.material.ExperimentalMaterialApi
-import androidx.compose.material.Text
-import androidx.compose.runtime.*
-import androidx.compose.ui.ExperimentalComposeUiApi
-import androidx.navigation.NavController
-import androidx.navigation.NavGraphBuilder
-import androidx.navigation.compose.composable
-import androidx.navigation.navigation
-import androidx.room.Room
-import org.kodein.di.DIAware
-import org.kodein.di.android.closestDI
-import org.kodein.di.android.subDI
-import org.kodein.di.bindProvider
-import org.kodein.di.bindSingleton
-import org.kodein.di.compose.withDI
-import org.kodein.di.instance
-import org.kodein.log.LoggerFactory
-import org.kodein.log.frontend.defaultLogFrontend
-import org.kodein.log.newLogger
-import org.kodein.log.withShortPackageKeepLast
-import xyz.quaver.pupil.R
-import xyz.quaver.pupil.sources.Source
-import xyz.quaver.pupil.sources.manatoki.composable.Main
-import xyz.quaver.pupil.sources.manatoki.composable.Reader
-import xyz.quaver.pupil.sources.manatoki.composable.Recent
-import xyz.quaver.pupil.sources.manatoki.composable.Search
-import xyz.quaver.pupil.sources.manatoki.viewmodel.MainViewModel
-import xyz.quaver.pupil.sources.manatoki.viewmodel.RecentViewModel
-import xyz.quaver.pupil.sources.manatoki.viewmodel.SearchViewModel
-
-@OptIn(
- ExperimentalMaterialApi::class,
- ExperimentalFoundationApi::class,
- ExperimentalAnimationApi::class,
- ExperimentalComposeUiApi::class
-)
-class Manatoki(app: Application) : Source(), DIAware {
- override val di by subDI(closestDI(app)) {
- bindSingleton {
- Room.databaseBuilder(
- app, ManatokiDatabase::class.java, name
- ).build()
- }
-
- bindProvider { MainViewModel(instance()) }
- bindProvider { RecentViewModel(instance()) }
- bindProvider { SearchViewModel(instance()) }
- }
-
- override val name = "manatoki.net"
- override val iconResID = R.drawable.manatoki
-
- override fun NavGraphBuilder.navGraph(navController: NavController) {
- navigation(route = name, startDestination = "manatoki.net/") {
- composable("manatoki.net/") { withDI(di) { Main(navController) } }
- composable("manatoki.net/reader/{itemID}") { withDI(di) { Reader(navController) } }
- composable("manatoki.net/search") { withDI(di) { Search(navController) } }
- composable("manatoki.net/recent") { withDI(di) { Recent(navController) } }
- }
- }
-
-}
\ No newline at end of file
diff --git a/app/src/main/java/xyz/quaver/pupil/sources/manatoki/composable/BoardButton.kt b/app/src/main/java/xyz/quaver/pupil/sources/manatoki/composable/BoardButton.kt
deleted file mode 100644
index 628fa27e..00000000
--- a/app/src/main/java/xyz/quaver/pupil/sources/manatoki/composable/BoardButton.kt
+++ /dev/null
@@ -1,66 +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.sources.manatoki.composable
-
-import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.*
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material.Card
-import androidx.compose.material.Icon
-import androidx.compose.material.MaterialTheme
-import androidx.compose.material.Text
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.ArrowForward
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.unit.dp
-
-@Composable
-fun RowScope.BoardButton(
- text: String,
- color: Color
-) {
- Card(
- modifier = Modifier.height(64.dp).weight(1f),
- shape = RoundedCornerShape(12.dp),
- elevation = 8.dp
- ) {
- Row(
- verticalAlignment = Alignment.CenterVertically
- ) {
- Text(
- text,
- modifier = Modifier.padding(8.dp, 0.dp).weight(1f),
- style = MaterialTheme.typography.h6
- )
-
- Icon(
- Icons.Default.ArrowForward,
- contentDescription = null,
- tint = Color.White,
- modifier = Modifier
- .width(48.dp)
- .fillMaxHeight()
- .background(color)
- )
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/xyz/quaver/pupil/sources/manatoki/composable/Main.kt b/app/src/main/java/xyz/quaver/pupil/sources/manatoki/composable/Main.kt
deleted file mode 100644
index bc1d188d..00000000
--- a/app/src/main/java/xyz/quaver/pupil/sources/manatoki/composable/Main.kt
+++ /dev/null
@@ -1,461 +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.sources.manatoki.composable
-
-import androidx.activity.compose.BackHandler
-import androidx.compose.foundation.*
-import androidx.compose.foundation.gestures.animateScrollBy
-import androidx.compose.foundation.interaction.MutableInteractionSource
-import androidx.compose.foundation.interaction.PressInteraction
-import androidx.compose.foundation.layout.*
-import androidx.compose.foundation.lazy.LazyRow
-import androidx.compose.foundation.lazy.items
-import androidx.compose.foundation.lazy.rememberLazyListState
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material.*
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.Add
-import androidx.compose.material.icons.filled.Search
-import androidx.compose.material.icons.filled.Settings
-import androidx.compose.runtime.*
-import androidx.compose.runtime.saveable.rememberSaveable
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.geometry.Size
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.platform.LocalDensity
-import androidx.compose.ui.res.painterResource
-import androidx.compose.ui.text.style.TextAlign
-import androidx.compose.ui.text.style.TextOverflow
-import androidx.compose.ui.unit.dp
-import androidx.navigation.NavController
-import com.google.accompanist.insets.LocalWindowInsets
-import com.google.accompanist.insets.navigationBarsPadding
-import com.google.accompanist.insets.rememberInsetsPaddingValues
-import com.google.accompanist.insets.ui.Scaffold
-import com.google.accompanist.insets.ui.TopAppBar
-import io.ktor.client.*
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.isActive
-import kotlinx.coroutines.launch
-import org.kodein.di.compose.rememberInstance
-import org.kodein.di.compose.rememberViewModel
-import xyz.quaver.pupil.R
-import xyz.quaver.pupil.proto.settingsDataStore
-import xyz.quaver.pupil.sources.composable.SourceSelectDialog
-import xyz.quaver.pupil.sources.manatoki.ManatokiDatabase
-import xyz.quaver.pupil.sources.manatoki.MangaListing
-import xyz.quaver.pupil.sources.manatoki.ReaderInfo
-import xyz.quaver.pupil.sources.manatoki.getItem
-import xyz.quaver.pupil.sources.manatoki.viewmodel.MainViewModel
-
-@ExperimentalMaterialApi
-@Composable
-fun Main(navController: NavController) {
- val model: MainViewModel by rememberViewModel()
-
- val client: HttpClient by rememberInstance()
-
- val database: ManatokiDatabase by rememberInstance()
- val historyDao = remember { database.historyDao() }
- val recent by remember { historyDao.getRecentManga() }.collectAsState(emptyList())
- val recentManga = remember { mutableStateListOf() }
-
- LaunchedEffect(recent) {
- recentManga.clear()
-
- recent.forEach {
- if (isActive)
- client.getItem(it, onListing = {
- recentManga.add(
- Thumbnail(it.itemID, it.title, it.thumbnail)
- )
- })
- }
- }
-
- val sheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)
-
- val coroutineScope = rememberCoroutineScope()
-
- val context = LocalContext.current
- LaunchedEffect(Unit) {
- context.settingsDataStore.updateData {
- it.toBuilder()
- .setRecentSource("manatoki.net")
- .build()
- }
- }
-
- var sourceSelectDialog by remember { mutableStateOf(false) }
-
- if (sourceSelectDialog)
- SourceSelectDialog(navController, "manatoki.net") { sourceSelectDialog = false }
-
- LaunchedEffect(Unit) {
- model.load()
- }
-
- BackHandler {
- if (sheetState.currentValue == ModalBottomSheetValue.Hidden)
- navController.popBackStack()
- else
- coroutineScope.launch {
- sheetState.hide()
- }
- }
-
- var mangaListing: MangaListing? by rememberSaveable { mutableStateOf(null) }
- var recentItem: String? by rememberSaveable { mutableStateOf(null) }
- val mangaListingListState = rememberLazyListState()
- var mangaListingListSize: Size? by remember { mutableStateOf(null) }
- val mangaListingInteractionSource = remember { mutableStateMapOf() }
- val navigationBarsPadding = LocalDensity.current.run {
- rememberInsetsPaddingValues(
- LocalWindowInsets.current.navigationBars
- ).calculateBottomPadding().toPx()
- }
-
- val onListing: (MangaListing) -> Unit = {
- mangaListing = it
-
- coroutineScope.launch {
- val recentItemID = historyDao.getAll(it.itemID).firstOrNull() ?: return@launch
- recentItem = recentItemID
-
- while (mangaListingListState.layoutInfo.totalItemsCount != it.entries.size) {
- delay(100)
- }
-
- val interactionSource = mangaListingInteractionSource.getOrPut(recentItemID) {
- MutableInteractionSource()
- }
-
- val targetIndex =
- it.entries.indexOfFirst { entry -> entry.itemID == recentItemID }
-
- mangaListingListState.scrollToItem(targetIndex)
-
- mangaListingListSize?.let { sheetSize ->
- val targetItem =
- mangaListingListState.layoutInfo.visibleItemsInfo.first {
- it.key == recentItemID
- }
-
- if (targetItem.offset == 0) {
- mangaListingListState.animateScrollBy(
- -(sheetSize.height - navigationBarsPadding - targetItem.size)
- )
- }
-
- delay(200)
-
- with(interactionSource) {
- val interaction =
- PressInteraction.Press(
- Offset(
- sheetSize.width / 2,
- targetItem.size / 2f
- )
- )
-
-
- emit(interaction)
- emit(PressInteraction.Release(interaction))
- }
- }
- }
- }
-
- val onReader: (ReaderInfo) -> Unit = { readerInfo ->
- coroutineScope.launch {
- sheetState.snapTo(ModalBottomSheetValue.Hidden)
- navController.navigate("manatoki.net/reader/${readerInfo.itemID}")
- }
- }
-
- ModalBottomSheetLayout(
- sheetState = sheetState,
- sheetShape = RoundedCornerShape(32.dp, 32.dp, 0.dp, 0.dp),
- sheetContent = {
- MangaListingBottomSheet(
- mangaListing,
- onListSize = { mangaListingListSize = it },
- rippleInteractionSource = mangaListingInteractionSource,
- listState = mangaListingListState,
- recentItem = recentItem
- ) {
- coroutineScope.launch {
- client.getItem(it, onListing, onReader)
- }
- }
- }
- ) {
- Scaffold(
- topBar = {
- TopAppBar(
- title = {
- Text("마나토끼")
- },
- actions = {
- IconButton(onClick = { sourceSelectDialog = true }) {
- Image(
- painter = painterResource(id = R.drawable.manatoki),
- contentDescription = null,
- modifier = Modifier.size(24.dp)
- )
- }
-
- IconButton(onClick = { navController.navigate("settings") }) {
- Icon(Icons.Default.Settings, contentDescription = null)
- }
- },
- contentPadding = rememberInsetsPaddingValues(
- insets = LocalWindowInsets.current.statusBars,
- applyBottom = false
- )
- )
- },
- floatingActionButton = {
- FloatingActionButton(
- modifier = Modifier.navigationBarsPadding(),
- onClick = {
- navController.navigate("manatoki.net/search")
- }
- ) {
- Icon(
- Icons.Default.Search,
- contentDescription = null
- )
- }
- }
- ) { contentPadding ->
- Box(Modifier.padding(contentPadding)) {
- Column(
- Modifier
- .padding(8.dp, 0.dp)
- .verticalScroll(rememberScrollState()),
- verticalArrangement = Arrangement.spacedBy(16.dp)
- ) {
- if (recentManga.isNotEmpty()) {
- Row(
- modifier = Modifier.fillMaxWidth(),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.SpaceBetween
- ) {
- Text(
- "이어 보기",
- style = MaterialTheme.typography.h5
- )
-
- IconButton(onClick = { navController.navigate("manatoki.net/recent") }) {
- Icon(
- Icons.Default.Add,
- contentDescription = null
- )
- }
- }
-
- LazyRow(
- modifier = Modifier
- .fillMaxWidth()
- .height(210.dp),
- horizontalArrangement = Arrangement.spacedBy(16.dp)
- ) {
- items(recentManga) { item ->
- Thumbnail(
- item,
- Modifier
- .width(180.dp)
- .aspectRatio(6 / 7f)
- ) {
- coroutineScope.launch {
- mangaListing = null
- sheetState.animateTo(ModalBottomSheetValue.Expanded)
- }
- coroutineScope.launch {
- client.getItem(it, onListing, onReader)
- }
- }
- }
- }
- }
-
- Row(
- modifier = Modifier.fillMaxWidth(),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.SpaceBetween
- ) {
- Text(
- "최신화",
- style = MaterialTheme.typography.h5
- )
-
- IconButton(onClick = { navController.navigate("manatoki.net/recent") }) {
- Icon(
- Icons.Default.Add,
- contentDescription = null
- )
- }
- }
-
- LazyRow(
- modifier = Modifier
- .fillMaxWidth()
- .height(210.dp),
- horizontalArrangement = Arrangement.spacedBy(16.dp)
- ) {
- items(model.recentUpload) { item ->
- Thumbnail(item,
- Modifier
- .width(180.dp)
- .aspectRatio(6 / 7f)) {
- coroutineScope.launch {
- mangaListing = null
- sheetState.animateTo(ModalBottomSheetValue.Expanded)
- }
- coroutineScope.launch {
- client.getItem(it, onListing, onReader)
- }
- }
- }
- }
-
- Divider()
-
- Column(
- modifier = Modifier.fillMaxWidth(),
- verticalArrangement = Arrangement.spacedBy(16.dp)
- ) {
- Row(
- modifier = Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.spacedBy(16.dp)
- ) {
- BoardButton("마나게시판", Color(0xFF007DB4))
- BoardButton("유머/가십", Color(0xFFF09614))
- }
-
- Row(
- modifier = Modifier.fillMaxWidth(),
- horizontalArrangement = Arrangement.spacedBy(16.dp)
- ) {
- BoardButton("역식자게시판", Color(0xFFA0C850))
- BoardButton("원본게시판", Color(0xFFFF4500))
- }
- }
-
- Row(
- modifier = Modifier.fillMaxWidth(),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.SpaceBetween
- ) {
- Text("만화 목록", style = MaterialTheme.typography.h5)
-
- IconButton(onClick = { navController.navigate("manatoki.net/search") }) {
- Icon(
- Icons.Default.Add,
- contentDescription = null
- )
- }
- }
- LazyRow(
- modifier = Modifier
- .fillMaxWidth()
- .height(210.dp),
- horizontalArrangement = Arrangement.spacedBy(16.dp)
- ) {
- items(model.mangaList) { item ->
- Thumbnail(item,
- Modifier
- .width(180.dp)
- .aspectRatio(6f / 7)) {
- coroutineScope.launch {
- mangaListing = null
- sheetState.animateTo(ModalBottomSheetValue.Expanded)
- }
- coroutineScope.launch {
- client.getItem(it, onListing, onReader)
- }
- }
- }
- }
-
- Text("주간 베스트", style = MaterialTheme.typography.h5)
- Column(
- modifier = Modifier.fillMaxWidth(),
- verticalArrangement = Arrangement.spacedBy(8.dp)
- ) {
- model.topWeekly.forEachIndexed { index, item ->
- Card(
- modifier = Modifier.clickable {
- coroutineScope.launch {
- mangaListing = null
- sheetState.animateTo(ModalBottomSheetValue.Expanded)
- }
-
- coroutineScope.launch {
- client.getItem(item.itemID, onListing, onReader)
- }
- }
- ) {
- Row(
- modifier = Modifier.height(IntrinsicSize.Min),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.spacedBy(8.dp)
- ) {
- Box(
- modifier = Modifier
- .background(Color(0xFF64C3F5))
- .width(24.dp)
- .fillMaxHeight(),
- contentAlignment = Alignment.Center
- ) {
- Text(
- (index + 1).toString(),
- color = Color.White,
- textAlign = TextAlign.Center
- )
- }
-
- Text(
- item.title,
- modifier = Modifier
- .weight(1f)
- .padding(0.dp, 4.dp),
- maxLines = 1,
- overflow = TextOverflow.Ellipsis
- )
-
- Text(
- item.count,
- color = Color(0xFFFF4500)
- )
- }
- }
- }
- }
-
- Box(Modifier.navigationBarsPadding())
- }
- }
- }
- }
-}
diff --git a/app/src/main/java/xyz/quaver/pupil/sources/manatoki/composable/MangaListingBottomSheet.kt b/app/src/main/java/xyz/quaver/pupil/sources/manatoki/composable/MangaListingBottomSheet.kt
deleted file mode 100644
index 494695d1..00000000
--- a/app/src/main/java/xyz/quaver/pupil/sources/manatoki/composable/MangaListingBottomSheet.kt
+++ /dev/null
@@ -1,261 +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.sources.manatoki.composable
-
-import androidx.compose.foundation.Image
-import androidx.compose.foundation.background
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.indication
-import androidx.compose.foundation.interaction.MutableInteractionSource
-import androidx.compose.foundation.layout.*
-import androidx.compose.foundation.lazy.LazyColumn
-import androidx.compose.foundation.lazy.LazyListState
-import androidx.compose.foundation.lazy.itemsIndexed
-import androidx.compose.foundation.lazy.rememberLazyListState
-import androidx.compose.material.*
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.ArrowRight
-import androidx.compose.material.ripple.rememberRipple
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.runtime.rememberCoroutineScope
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.geometry.Size
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.layout.SubcomposeLayout
-import androidx.compose.ui.layout.onGloballyPositioned
-import androidx.compose.ui.unit.dp
-import androidx.compose.ui.unit.toSize
-import coil.compose.rememberImagePainter
-import com.google.accompanist.flowlayout.FlowRow
-import com.google.accompanist.insets.LocalWindowInsets
-import com.google.accompanist.insets.navigationBarsPadding
-import com.google.accompanist.insets.rememberInsetsPaddingValues
-import xyz.quaver.pupil.sources.manatoki.MangaListing
-
-private val FabSpacing = 8.dp
-private val HeightPercentage = 75 // take 75% of the available space
-private enum class MangaListingBottomSheetLayoutContent { Top, Bottom, Fab }
-
-@Composable
-fun MangaListingBottomSheetLayout(
- floatingActionButton: @Composable () -> Unit,
- top: @Composable () -> Unit,
- bottom: @Composable () -> Unit
-) {
- SubcomposeLayout { constraints ->
- val layoutWidth = constraints.maxWidth
- val layoutHeight = constraints.maxHeight * HeightPercentage / 100
-
- layout(layoutWidth, layoutHeight) {
- val topPlaceables = subcompose(MangaListingBottomSheetLayoutContent.Top, top).map {
- it.measure(constraints)
- }
-
- val topPlaceableHeight = topPlaceables.maxOfOrNull { it.height } ?: 0
-
- val bottomConstraints = constraints.copy(
- maxHeight = layoutHeight - topPlaceableHeight
- )
-
- val bottomPlaceables = subcompose(MangaListingBottomSheetLayoutContent.Bottom, bottom).map {
- it.measure(bottomConstraints)
- }
-
- val fabPlaceables = subcompose(MangaListingBottomSheetLayoutContent.Fab, floatingActionButton).mapNotNull {
- it.measure(constraints).takeIf { it.height != 0 && it.width != 0 }
- }
-
- topPlaceables.forEach { it.place(0, 0) }
- bottomPlaceables.forEach { it.place(0, topPlaceableHeight) }
-
- if (fabPlaceables.isNotEmpty()) {
- val fabWidth = fabPlaceables.maxOf { it.width }
- val fabHeight = fabPlaceables.maxOf { it.height }
-
- fabPlaceables.forEach {
- it.place(
- layoutWidth - fabWidth - FabSpacing.roundToPx(),
- topPlaceableHeight - fabHeight / 2
- )
- }
- }
- }
- }
-}
-
-@ExperimentalMaterialApi
-@Composable
-fun MangaListingBottomSheet(
- mangaListing: MangaListing? = null,
- currentItemID: String? = null,
- onListSize: (Size) -> Unit = { },
- listState: LazyListState = rememberLazyListState(),
- rippleInteractionSource: Map = emptyMap(),
- recentItem: String? = null,
- nextItem: String? = null,
- onOpenItem: (String) -> Unit = { },
-) {
- val coroutineScope = rememberCoroutineScope()
-
- Box(
- modifier = Modifier.fillMaxWidth()
- ) {
- if (mangaListing == null)
- CircularProgressIndicator(
- Modifier
- .navigationBarsPadding()
- .padding(16.dp)
- .align(Alignment.Center))
- else
- MangaListingBottomSheetLayout(
- floatingActionButton = {
- ExtendedFloatingActionButton(
- text = { Text(
- when {
- mangaListing.entries.any { it.itemID == recentItem } -> "이어보기"
- mangaListing.entries.any { it.itemID == nextItem } -> "다음화보기"
- else -> "첫화보기"
- }
- ) },
- onClick = {
- when {
- mangaListing.entries.any { it.itemID == recentItem } -> onOpenItem(recentItem!!)
- mangaListing.entries.any { it.itemID == nextItem } -> onOpenItem(nextItem!!)
- else -> mangaListing.entries.lastOrNull()?.let { onOpenItem(it.itemID) }
- }
- }
- )
- },
- top = {
- Row(
- modifier = Modifier
- .height(IntrinsicSize.Min)
- .background(MaterialTheme.colors.primary)
- .padding(0.dp, 0.dp, 0.dp, 4.dp),
- horizontalArrangement = Arrangement.spacedBy(8.dp)
- ) {
- val painter = rememberImagePainter(mangaListing.thumbnail)
-
- Box(Modifier.fillMaxHeight()) {
- Image(
- modifier = Modifier
- .width(150.dp)
- .aspectRatio(
- with(painter.intrinsicSize) { if (this == Size.Unspecified) 1f else width / height }
- ).align(Alignment.Center),
- painter = painter,
- contentDescription = null
- )
- }
-
- Column(
- modifier = Modifier
- .weight(1f)
- .padding(0.dp, 8.dp)
- .fillMaxHeight(),
- verticalArrangement = Arrangement.spacedBy(4.dp)
- ) {
- Text(
- mangaListing.title,
- style = MaterialTheme.typography.h5,
- modifier = Modifier.weight(1f)
- )
-
- CompositionLocalProvider(LocalContentAlpha provides 0.7f) {
- Text("작가: ${mangaListing.author}")
-
- Row(verticalAlignment = Alignment.CenterVertically) {
- Text("분류: ")
-
- CompositionLocalProvider(LocalContentAlpha provides 1f) {
- FlowRow(
- modifier = Modifier.weight(1f),
- mainAxisSpacing = 8.dp
- ) {
- mangaListing.tags.forEach {
- Card(
- elevation = 4.dp,
- backgroundColor = Color.White
- ) {
- Text(
- it,
- style = MaterialTheme.typography.caption,
- modifier = Modifier.padding(4.dp),
- color = Color.Black
- )
- }
- }
- }
- }
- }
-
- Text("발행구분: ${mangaListing.type}")
- }
- }
- }
- },
- bottom = {
- LazyColumn(
- modifier = Modifier
- .fillMaxWidth()
- .onGloballyPositioned {
- onListSize(it.size.toSize())
- },
- state = listState,
- contentPadding = rememberInsetsPaddingValues(LocalWindowInsets.current.navigationBars)
- ) {
- itemsIndexed(mangaListing.entries, key = { _, entry -> entry.itemID }) { index, entry ->
- Row(
- modifier = Modifier
- .clickable {
- onOpenItem(entry.itemID)
- }
- .run {
- rippleInteractionSource[entry.itemID]?.let {
- indication(it, rememberRipple())
- } ?: this
- }
- .padding(16.dp),
- verticalAlignment = Alignment.CenterVertically,
- horizontalArrangement = Arrangement.spacedBy(8.dp)
- ) {
- if (entry.itemID == currentItemID)
- Icon(
- Icons.Default.ArrowRight,
- contentDescription = null,
- tint = MaterialTheme.colors.secondary
- )
-
- Text(
- entry.title,
- style = MaterialTheme.typography.h6,
- modifier = Modifier.weight(1f)
- )
-
- Text("★ ${entry.starRating}")
- }
- Divider()
- }
- }
- }
- )
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/xyz/quaver/pupil/sources/manatoki/composable/Reader.kt b/app/src/main/java/xyz/quaver/pupil/sources/manatoki/composable/Reader.kt
deleted file mode 100644
index e37cc522..00000000
--- a/app/src/main/java/xyz/quaver/pupil/sources/manatoki/composable/Reader.kt
+++ /dev/null
@@ -1,303 +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.sources.manatoki.composable
-
-import android.util.Log
-import androidx.activity.compose.BackHandler
-import androidx.compose.animation.ExperimentalAnimationApi
-import androidx.compose.animation.core.animateFloatAsState
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.Image
-import androidx.compose.foundation.gestures.animateScrollBy
-import androidx.compose.foundation.interaction.MutableInteractionSource
-import androidx.compose.foundation.interaction.PressInteraction
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.size
-import androidx.compose.foundation.lazy.rememberLazyListState
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material.*
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.*
-import androidx.compose.runtime.*
-import androidx.compose.runtime.saveable.rememberSaveable
-import androidx.compose.ui.ExperimentalComposeUiApi
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.scale
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.geometry.Size
-import androidx.compose.ui.platform.LocalDensity
-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.lifecycle.viewmodel.compose.viewModel
-import androidx.navigation.NavController
-import com.google.accompanist.insets.LocalWindowInsets
-import com.google.accompanist.insets.navigationBarsPadding
-import com.google.accompanist.insets.rememberInsetsPaddingValues
-import com.google.accompanist.insets.ui.Scaffold
-import com.google.accompanist.insets.ui.TopAppBar
-import io.ktor.client.*
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.launch
-import org.kodein.di.compose.rememberInstance
-import org.kodein.di.compose.rememberViewModel
-import xyz.quaver.pupil.R
-import xyz.quaver.pupil.sources.composable.ReaderBase
-import xyz.quaver.pupil.sources.composable.ReaderBaseViewModel
-import xyz.quaver.pupil.sources.manatoki.ManatokiDatabase
-import xyz.quaver.pupil.sources.manatoki.MangaListing
-import xyz.quaver.pupil.sources.manatoki.ReaderInfo
-import xyz.quaver.pupil.sources.manatoki.getItem
-import xyz.quaver.pupil.ui.theme.Orange500
-import kotlin.math.max
-
-private val imageUserAgent = "Mozilla/5.0 (Linux; Android 6.0; Nexus 5 Build/MRA58N) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/96.0.4664.45 Mobile Safari/537.36"
-
-@ExperimentalAnimationApi
-@ExperimentalFoundationApi
-@ExperimentalMaterialApi
-@ExperimentalComposeUiApi
-@Composable
-fun Reader(navController: NavController) {
- val model: ReaderBaseViewModel = viewModel()
-
- val client: HttpClient by rememberInstance()
-
- val database: ManatokiDatabase by rememberInstance()
- val favoriteDao = remember { database.favoriteDao() }
- val bookmarkDao = remember { database.bookmarkDao() }
- val historyDao = remember { database.historyDao() }
-
- val coroutineScope = rememberCoroutineScope()
-
- val itemID = navController.currentBackStackEntry?.arguments?.getString("itemID")
- var readerInfo: ReaderInfo? by rememberSaveable { mutableStateOf(null) }
-
- LaunchedEffect(Unit) {
- if (itemID != null)
- client.getItem(itemID, onReader = {
- coroutineScope.launch {
- historyDao.insert(it.itemID, it.listingItemID, 1)
- }
- readerInfo = it
- model.load(it.urls) {
- set("User-Agent", imageUserAgent)
- }
- })
- else model.error = true
- }
-
- val isFavorite by favoriteDao.contains(itemID ?: "").collectAsState(false)
-
- val sheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)
- val navigationBarsPadding = LocalDensity.current.run {
- rememberInsetsPaddingValues(
- LocalWindowInsets.current.navigationBars
- ).calculateBottomPadding().toPx()
- }
-
- val readerListState = rememberLazyListState()
-
- LaunchedEffect(readerListState.firstVisibleItemIndex) {
- readerInfo?.let { readerInfo ->
- historyDao.insert(
- readerInfo.itemID,
- readerInfo.listingItemID,
- readerListState.firstVisibleItemIndex
- )
- }
- }
-
- var scrollDirection by remember { mutableStateOf(0f) }
-
- BackHandler {
- when {
- sheetState.isVisible -> coroutineScope.launch { sheetState.hide() }
- model.fullscreen -> model.fullscreen = false
- else -> navController.popBackStack()
- }
- }
-
- var mangaListing: MangaListing? by rememberSaveable { mutableStateOf(null) }
- val mangaListingListState = rememberLazyListState()
- var mangaListingListSize: Size? by remember { mutableStateOf(null) }
- val mangaListingRippleInteractionSource = remember { MutableInteractionSource() }
-
- ModalBottomSheetLayout(
- sheetState = sheetState,
- sheetShape = RoundedCornerShape(32.dp, 32.dp, 0.dp, 0.dp),
- sheetContent = {
- MangaListingBottomSheet(
- mangaListing,
- currentItemID = itemID,
- onListSize = { mangaListingListSize = it },
- rippleInteractionSource = if (itemID == null) emptyMap() else mapOf(itemID to mangaListingRippleInteractionSource),
- listState = mangaListingListState,
- nextItem = readerInfo?.nextItemID
- ) {
- navController.navigate("manatoki.net/reader/$it") {
- popUpTo("manatoki.net/")
- }
- }
- }
- ) {
- Scaffold(
- topBar = {
- if (!model.fullscreen)
- TopAppBar(
- title = {
- Text(
- readerInfo?.title ?: stringResource(R.string.reader_loading),
- maxLines = 1,
- overflow = TextOverflow.Ellipsis
- )
- },
- navigationIcon = {
- IconButton(onClick = { navController.navigateUp() }) {
- Icon(
- Icons.Default.NavigateBefore,
- contentDescription = null
- )
- }
- },
- actions = {
- IconButton({ }) {
- Image(
- painter = painterResource(R.drawable.manatoki),
- contentDescription = null,
- modifier = Modifier.size(24.dp)
- )
- }
-
- IconButton(onClick = {
- itemID?.let {
- coroutineScope.launch {
- if (isFavorite) favoriteDao.delete(it)
- else favoriteDao.insert(it)
- }
- }
- }) {
- Icon(
- if (isFavorite) Icons.Default.Star else Icons.Default.StarOutline,
- contentDescription = null,
- tint = Orange500
- )
- }
- },
- contentPadding = rememberInsetsPaddingValues(
- LocalWindowInsets.current.statusBars,
- applyBottom = false
- )
- )
- },
- floatingActionButton = {
- val showNextButton by derivedStateOf {
- (readerInfo?.nextItemID?.isNotEmpty() == true) && with (readerListState.layoutInfo) {
- visibleItemsInfo.lastOrNull()?.index == totalItemsCount-1
- }
- }
- val scale by animateFloatAsState(if (!showNextButton && (model.fullscreen || scrollDirection < 0f)) 0f else 1f)
-
- if (scale > 0f)
- FloatingActionButton(
- modifier = Modifier
- .navigationBarsPadding()
- .scale(scale),
- onClick = {
- readerInfo?.let {
- if (showNextButton) {
- navController.navigate("manatoki.net/reader/${it.nextItemID}") {
- popUpTo("manatoki.net/")
- }
- } else {
- coroutineScope.launch {
- sheetState.animateTo(ModalBottomSheetValue.Expanded)
- }
-
- coroutineScope.launch {
- if (mangaListing?.itemID != it.listingItemID)
- client.getItem(it.listingItemID, onListing = {
- mangaListing = it
-
- coroutineScope.launch {
- while (mangaListingListState.layoutInfo.totalItemsCount != it.entries.size) {
- delay(100)
- }
-
- val targetIndex =
- it.entries.indexOfFirst { it.itemID == itemID }
-
- mangaListingListState.scrollToItem(targetIndex)
-
- mangaListingListSize?.let { sheetSize ->
- val targetItem =
- mangaListingListState.layoutInfo.visibleItemsInfo.first {
- it.key == itemID
- }
-
- if (targetItem.offset == 0) {
- mangaListingListState.animateScrollBy(
- -(sheetSize.height - navigationBarsPadding - targetItem.size)
- )
- }
-
- delay(200)
-
- with(mangaListingRippleInteractionSource) {
- val interaction =
- PressInteraction.Press(
- Offset(
- sheetSize.width / 2,
- targetItem.size / 2f
- )
- )
-
-
- emit(interaction)
- emit(
- PressInteraction.Release(
- interaction
- )
- )
- }
- }
- }
- })
- }
- }
- }
- }
- ) {
- Icon(
- if (showNextButton) Icons.Default.NavigateNext else Icons.Default.List,
- contentDescription = null
- )
- }
- }
- ) { contentPadding ->
- ReaderBase(
- Modifier.padding(contentPadding),
- model = model,
- listState = readerListState,
- onScroll = { scrollDirection = it }
- )
- }
- }
-}
diff --git a/app/src/main/java/xyz/quaver/pupil/sources/manatoki/composable/Recent.kt b/app/src/main/java/xyz/quaver/pupil/sources/manatoki/composable/Recent.kt
deleted file mode 100644
index 7be0e7d7..00000000
--- a/app/src/main/java/xyz/quaver/pupil/sources/manatoki/composable/Recent.kt
+++ /dev/null
@@ -1,131 +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.sources.manatoki.composable
-
-import androidx.activity.compose.BackHandler
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.layout.*
-import androidx.compose.foundation.lazy.GridCells
-import androidx.compose.foundation.lazy.LazyVerticalGrid
-import androidx.compose.foundation.lazy.items
-import androidx.compose.material.*
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.NavigateBefore
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.rememberCoroutineScope
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.unit.dp
-import androidx.navigation.NavController
-import com.google.accompanist.insets.LocalWindowInsets
-import com.google.accompanist.insets.rememberInsetsPaddingValues
-import com.google.accompanist.insets.ui.Scaffold
-import com.google.accompanist.insets.ui.TopAppBar
-import io.ktor.client.*
-import kotlinx.coroutines.launch
-import org.kodein.di.compose.rememberInstance
-import org.kodein.di.compose.rememberViewModel
-import xyz.quaver.pupil.sources.composable.OverscrollPager
-import xyz.quaver.pupil.sources.manatoki.getItem
-import xyz.quaver.pupil.sources.manatoki.viewmodel.RecentViewModel
-
-@ExperimentalFoundationApi
-@ExperimentalMaterialApi
-@Composable
-fun Recent(navController: NavController) {
- val model: RecentViewModel by rememberViewModel()
-
- val client: HttpClient by rememberInstance()
-
- val coroutineScope = rememberCoroutineScope()
-
- LaunchedEffect(Unit) {
- model.load()
- }
-
- BackHandler {
- navController.popBackStack()
- }
-
- Scaffold(
- topBar = {
- TopAppBar(
- title = {
- Text("최신 업데이트")
- },
- navigationIcon = {
- IconButton(onClick = { navController.navigateUp() }) {
- Icon(
- Icons.Default.NavigateBefore,
- contentDescription = null
- )
- }
- },
- contentPadding = rememberInsetsPaddingValues(
- LocalWindowInsets.current.statusBars,
- applyBottom = false
- )
- )
- }
- ) { contentPadding ->
- Box(Modifier.padding(contentPadding)) {
- OverscrollPager(
- currentPage = model.page,
- prevPageAvailable = model.page > 1,
- nextPageAvailable = model.page < 10,
- nextPageTurnIndicatorOffset = rememberInsetsPaddingValues(
- LocalWindowInsets.current.navigationBars
- ).calculateBottomPadding(),
- onPageTurn = {
- model.page = it
- model.load()
- }
- ) {
- Box(Modifier.fillMaxSize()) {
- LazyVerticalGrid(
- GridCells.Adaptive(minSize = 200.dp),
- contentPadding = rememberInsetsPaddingValues(
- LocalWindowInsets.current.navigationBars
- )
- ) {
- items(model.result) {
- Thumbnail(
- it,
- modifier = Modifier
- .fillMaxWidth()
- .aspectRatio(3f / 4)
- .padding(8.dp)
- ) {
- coroutineScope.launch {
- client.getItem(it, onReader = {
- navController.navigate("manatoki.net/reader/${it.itemID}")
- })
- }
- }
- }
- }
-
- if (model.loading)
- CircularProgressIndicator(Modifier.align(Alignment.Center))
- }
- }
- }
- }
-}
diff --git a/app/src/main/java/xyz/quaver/pupil/sources/manatoki/composable/Search.kt b/app/src/main/java/xyz/quaver/pupil/sources/manatoki/composable/Search.kt
deleted file mode 100644
index e990f7f2..00000000
--- a/app/src/main/java/xyz/quaver/pupil/sources/manatoki/composable/Search.kt
+++ /dev/null
@@ -1,382 +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.sources.manatoki.composable
-
-import android.util.Log
-import androidx.activity.compose.BackHandler
-import androidx.compose.animation.core.animateDpAsState
-import androidx.compose.foundation.ExperimentalFoundationApi
-import androidx.compose.foundation.gestures.animateScrollBy
-import androidx.compose.foundation.gestures.detectTapGestures
-import androidx.compose.foundation.interaction.MutableInteractionSource
-import androidx.compose.foundation.interaction.PressInteraction
-import androidx.compose.foundation.layout.*
-import androidx.compose.foundation.lazy.*
-import androidx.compose.foundation.rememberScrollState
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.foundation.text.KeyboardActions
-import androidx.compose.foundation.text.KeyboardOptions
-import androidx.compose.foundation.verticalScroll
-import androidx.compose.material.*
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.filled.Close
-import androidx.compose.material.icons.filled.NavigateBefore
-import androidx.compose.runtime.*
-import androidx.compose.runtime.saveable.rememberSaveable
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.focus.onFocusChanged
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.geometry.Size
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.input.pointer.pointerInput
-import androidx.compose.ui.platform.LocalDensity
-import androidx.compose.ui.platform.LocalFocusManager
-import androidx.compose.ui.text.input.ImeAction
-import androidx.compose.ui.unit.dp
-import androidx.lifecycle.viewmodel.compose.viewModel
-import androidx.navigation.NavController
-import com.google.accompanist.flowlayout.FlowRow
-import com.google.accompanist.insets.LocalWindowInsets
-import com.google.accompanist.insets.navigationBarsWithImePadding
-import com.google.accompanist.insets.rememberInsetsPaddingValues
-import com.google.accompanist.insets.ui.Scaffold
-import com.google.accompanist.insets.ui.TopAppBar
-import io.ktor.client.*
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.launch
-import org.kodein.di.compose.rememberInstance
-import org.kodein.di.compose.rememberViewModel
-import xyz.quaver.pupil.sources.composable.ModalTopSheetLayout
-import xyz.quaver.pupil.sources.composable.ModalTopSheetState
-import xyz.quaver.pupil.sources.composable.OverscrollPager
-import xyz.quaver.pupil.sources.manatoki.*
-import xyz.quaver.pupil.sources.manatoki.viewmodel.*
-
-@ExperimentalFoundationApi
-@ExperimentalMaterialApi
-@Composable
-fun Search(navController: NavController) {
- val model: SearchViewModel by rememberViewModel()
-
- val client: HttpClient by rememberInstance()
-
- val database: ManatokiDatabase by rememberInstance()
- val historyDao = remember { database.historyDao() }
-
- var searchFocused by remember { mutableStateOf(false) }
- val handleOffset by animateDpAsState(if (searchFocused) 0.dp else (-36).dp)
-
- val drawerState = rememberSwipeableState(ModalTopSheetState.Hidden)
- val sheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)
-
- val coroutineScope = rememberCoroutineScope()
-
- val focusManager = LocalFocusManager.current
-
- LaunchedEffect(Unit) {
- model.search()
- }
-
- BackHandler {
- when {
- sheetState.isVisible -> coroutineScope.launch { sheetState.hide() }
- drawerState.currentValue != ModalTopSheetState.Hidden ->
- coroutineScope.launch { drawerState.animateTo(ModalTopSheetState.Hidden) }
- else -> navController.popBackStack()
- }
- }
-
- var mangaListing: MangaListing? by rememberSaveable { mutableStateOf(null) }
- var recentItem: String? by rememberSaveable { mutableStateOf(null) }
- val mangaListingListState = rememberLazyListState()
- var mangaListingListSize: Size? by remember { mutableStateOf(null) }
- val mangaListingInteractionSource = remember { mutableStateMapOf() }
- val navigationBarsPadding = LocalDensity.current.run {
- rememberInsetsPaddingValues(
- LocalWindowInsets.current.navigationBars
- ).calculateBottomPadding().toPx()
- }
-
- ModalBottomSheetLayout(
- sheetState = sheetState,
- sheetShape = RoundedCornerShape(32.dp, 32.dp, 0.dp, 0.dp),
- sheetContent = {
- MangaListingBottomSheet(
- mangaListing,
- onListSize = { mangaListingListSize = it },
- rippleInteractionSource = mangaListingInteractionSource,
- listState = mangaListingListState,
- recentItem = recentItem
- ) {
- coroutineScope.launch {
- client.getItem(it, onReader = {
- launch {
- sheetState.snapTo(ModalBottomSheetValue.Hidden)
- navController.navigate("manatoki.net/reader/${it.itemID}")
- }
- })
- }
- }
- }
- ) {
- Scaffold(
- modifier = Modifier
- .pointerInput(Unit) {
- detectTapGestures { focusManager.clearFocus() }
- },
- topBar = {
- TopAppBar(
- title = {
- TextField(
- model.stx,
- modifier = Modifier
- .onFocusChanged {
- searchFocused = it.isFocused
- }
- .fillMaxWidth(),
- onValueChange = { model.stx = it },
- placeholder = { Text("제목") },
- textStyle = MaterialTheme.typography.subtitle1,
- singleLine = true,
- trailingIcon = {
- if (model.stx != "" && searchFocused)
- IconButton(onClick = { model.stx = "" }) {
- Icon(
- Icons.Default.Close,
- contentDescription = null,
- tint = contentColorFor(MaterialTheme.colors.primarySurface)
- )
- }
- },
- keyboardOptions = KeyboardOptions(imeAction = ImeAction.Search),
- keyboardActions = KeyboardActions(
- onSearch = {
- focusManager.clearFocus()
- coroutineScope.launch {
- drawerState.animateTo(ModalTopSheetState.Hidden)
- }
- coroutineScope.launch {
- model.search()
- }
- }
- ),
- colors = TextFieldDefaults.textFieldColors(
- textColor = contentColorFor(MaterialTheme.colors.primarySurface),
- placeholderColor = contentColorFor(MaterialTheme.colors.primarySurface).copy(alpha = 0.75f),
- backgroundColor = Color.Transparent,
- cursorColor = MaterialTheme.colors.secondary,
- disabledIndicatorColor = Color.Transparent,
- focusedIndicatorColor = Color.Transparent,
- errorIndicatorColor = Color.Transparent,
- unfocusedIndicatorColor = Color.Transparent
- )
- )
- },
- navigationIcon = {
- IconButton(onClick = { navController.navigateUp() }) {
- Icon(
- Icons.Default.NavigateBefore,
- contentDescription = null
- )
- }
- },
- contentPadding = rememberInsetsPaddingValues(
- LocalWindowInsets.current.statusBars,
- applyBottom = false
- )
- )
- }
- ) { contentPadding ->
- Box(Modifier.padding(contentPadding)) {
- ModalTopSheetLayout(
- modifier = Modifier.run {
- if (drawerState.currentValue == ModalTopSheetState.Hidden)
- offset(0.dp, handleOffset)
- else
- navigationBarsWithImePadding()
- },
- drawerState = drawerState,
- drawerContent = {
- Column(
- modifier = Modifier
- .fillMaxWidth()
- .padding(16.dp, 0.dp)
- .verticalScroll(rememberScrollState()),
- verticalArrangement = Arrangement.spacedBy(8.dp)
- ) {
- Text("작가")
- TextField(model.artist, onValueChange = { model.artist = it })
-
- Text("발행")
- FlowRow(mainAxisSpacing = 4.dp, crossAxisSpacing = 4.dp) {
- Chip("전체", model.publish.isEmpty()) {
- model.publish = ""
- }
- availablePublish.forEach {
- Chip(it, model.publish == it) {
- model.publish = it
- }
- }
- }
-
- Text("초성")
- FlowRow(mainAxisSpacing = 4.dp, crossAxisSpacing = 4.dp) {
- Chip("전체", model.jaum.isEmpty()) {
- model.jaum = ""
- }
- availableJaum.forEach {
- Chip(it, model.jaum == it) {
- model.jaum = it
- }
- }
- }
-
- Text("장르")
- FlowRow(mainAxisSpacing = 4.dp, crossAxisSpacing = 4.dp) {
- Chip("전체", model.tag.isEmpty()) {
- model.tag.clear()
- }
- availableTag.forEach {
- Chip(it, model.tag.contains(it)) {
- if (model.tag.contains(it))
- model.tag.remove(it)
- else
- model.tag[it] = it
- }
- }
- }
-
- Text("정렬")
- FlowRow(mainAxisSpacing = 4.dp, crossAxisSpacing = 4.dp) {
- Chip("기본", model.sst.isEmpty()) {
- model.sst = ""
- }
- availableSst.entries.forEach { (k, v) ->
- Chip(v, model.sst == k) {
- model.sst = k
- }
- }
- }
-
- Box(
- Modifier
- .fillMaxWidth()
- .height(8.dp))
- }
- }
- ) {
- OverscrollPager(
- currentPage = model.page,
- prevPageAvailable = model.page > 1,
- nextPageAvailable = model.page < model.maxPage,
- nextPageTurnIndicatorOffset = rememberInsetsPaddingValues(
- LocalWindowInsets.current.navigationBars
- ).calculateBottomPadding(),
- onPageTurn = {
- model.page = it
- coroutineScope.launch {
- model.search(resetPage = false)
- }
- }
- ) {
- Box(Modifier.fillMaxSize()) {
- LazyVerticalGrid(
- GridCells.Adaptive(minSize = 200.dp),
- contentPadding = rememberInsetsPaddingValues(
- LocalWindowInsets.current.navigationBars
- )
- ) {
- items(model.result) { item ->
- Thumbnail(
- Thumbnail(item.itemID, item.title, item.thumbnail),
- modifier = Modifier
- .fillMaxWidth()
- .aspectRatio(3f / 4)
- .padding(8.dp)
- ) {
- coroutineScope.launch {
- mangaListing = null
- sheetState.animateTo(ModalBottomSheetValue.Expanded)
- }
- coroutineScope.launch {
- client.getItem(it, onListing = {
- mangaListing = it
-
- coroutineScope.launch {
- val recentItemID = historyDao.getAll(it.itemID).firstOrNull() ?: return@launch
- recentItem = recentItemID
-
- while (mangaListingListState.layoutInfo.totalItemsCount != it.entries.size) {
- delay(100)
- }
-
- val interactionSource = mangaListingInteractionSource.getOrPut(recentItemID) {
- MutableInteractionSource()
- }
-
- val targetIndex =
- it.entries.indexOfFirst { entry -> entry.itemID == recentItemID }
-
- mangaListingListState.scrollToItem(targetIndex)
-
- mangaListingListSize?.let { sheetSize ->
- val targetItem =
- mangaListingListState.layoutInfo.visibleItemsInfo.first {
- it.key == recentItemID
- }
-
- if (targetItem.offset == 0) {
- mangaListingListState.animateScrollBy(
- -(sheetSize.height - navigationBarsPadding - targetItem.size)
- )
- }
-
- delay(200)
-
- with(interactionSource) {
- val interaction =
- PressInteraction.Press(
- Offset(
- sheetSize.width / 2,
- targetItem.size / 2f
- )
- )
-
-
- emit(interaction)
- emit(PressInteraction.Release(interaction))
- }
- }
- }
- })
- }
- }
- }
- }
-
- if (model.loading)
- CircularProgressIndicator(Modifier.align(Alignment.Center))
- }
- }
- }
- }
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/xyz/quaver/pupil/sources/manatoki/composable/Thumbnail.kt b/app/src/main/java/xyz/quaver/pupil/sources/manatoki/composable/Thumbnail.kt
deleted file mode 100644
index 5194f571..00000000
--- a/app/src/main/java/xyz/quaver/pupil/sources/manatoki/composable/Thumbnail.kt
+++ /dev/null
@@ -1,80 +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.sources.manatoki.composable
-
-import android.os.Parcelable
-import androidx.compose.foundation.Image
-import androidx.compose.foundation.background
-import androidx.compose.foundation.clickable
-import androidx.compose.foundation.layout.*
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material.Card
-import androidx.compose.material.MaterialTheme
-import androidx.compose.material.Text
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.unit.dp
-import coil.compose.rememberImagePainter
-import kotlinx.parcelize.Parcelize
-import kotlinx.serialization.Serializable
-
-@Parcelize
-@Serializable
-data class Thumbnail(
- val itemID: String,
- val title: String,
- val thumbnail: String
-): Parcelable
-
-@Composable
-fun Thumbnail(
- thumbnail: Thumbnail,
- modifier: Modifier = Modifier,
- onClick: (String) -> Unit = { }
-) {
- Card(
- shape = RoundedCornerShape(12.dp),
- elevation = 8.dp,
- modifier = modifier.clickable { onClick(thumbnail.itemID) }
- ) {
- Box(
- modifier = Modifier.width(IntrinsicSize.Min)
- ) {
- Image(
- modifier = Modifier.fillMaxSize(),
- painter = rememberImagePainter(thumbnail.thumbnail),
- contentDescription = null
- )
-
- Text(
- thumbnail.title,
- color = Color.White,
- modifier = Modifier
- .align(Alignment.BottomStart)
- .fillMaxWidth()
- .background(Color.Black.copy(alpha = 0.7f))
- .padding(8.dp),
- softWrap = true,
- style = MaterialTheme.typography.subtitle1
- )
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/xyz/quaver/pupil/sources/manatoki/util.kt b/app/src/main/java/xyz/quaver/pupil/sources/manatoki/util.kt
deleted file mode 100644
index f698bc68..00000000
--- a/app/src/main/java/xyz/quaver/pupil/sources/manatoki/util.kt
+++ /dev/null
@@ -1,271 +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.sources.manatoki
-
-import android.os.Parcelable
-import android.util.Log
-import androidx.collection.LruCache
-import androidx.compose.foundation.background
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.shape.RoundedCornerShape
-import androidx.compose.material.*
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.CompositionLocalProvider
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.unit.dp
-import com.google.common.util.concurrent.RateLimiter
-import io.ktor.client.*
-import io.ktor.client.request.*
-import kotlinx.coroutines.asCoroutineDispatcher
-import kotlinx.coroutines.coroutineScope
-import kotlinx.coroutines.withContext
-import kotlinx.coroutines.yield
-import kotlinx.parcelize.Parcelize
-import kotlinx.serialization.Serializable
-import org.jsoup.Jsoup
-import java.util.concurrent.Executors
-
-val manatokiUrl = "https://manatoki118.net"
-
-private val rateLimitCoroutineDispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
-private val rateLimiter = RateLimiter.create(10.0)
-
-suspend fun waitForRateLimit() {
- withContext(rateLimitCoroutineDispatcher) {
- rateLimiter.acquire()
- }
- yield()
-}
-
-@Parcelize
-@Serializable
-data class MangaListingEntry(
- val itemID: String,
- val episode: Int,
- val title: String,
- val starRating: Float,
- val date: String,
- val viewCount: Int,
- val thumbsUpCount: Int
-): Parcelable
-
-@Parcelize
-@Serializable
-data class MangaListing(
- val itemID: String,
- val title: String,
- val thumbnail: String,
- val author: String,
- val tags: List,
- val type: String,
- val thumbsUpCount: Int,
- val entries: List
-): Parcelable
-
-@Parcelize
-@Serializable
-data class ReaderInfo(
- val itemID: String,
- val title: String,
- val urls: List,
- val listingItemID: String,
- val prevItemID: String,
- val nextItemID: String
-): Parcelable
-
-@ExperimentalMaterialApi
-@Composable
-fun Chip(text: String, selected: Boolean = false, onClick: () -> Unit = { }) {
- Card(
- onClick = onClick,
- backgroundColor = if (selected) MaterialTheme.colors.secondary else MaterialTheme.colors.surface,
- shape = RoundedCornerShape(8.dp),
- elevation = 4.dp
- ) {
- Text(text, modifier = Modifier.padding(4.dp))
- }
-}
-
-private val cache = LruCache(100)
-suspend fun HttpClient.getItem(
- itemID: String,
- onListing: (MangaListing) -> Unit = { },
- onReader: (ReaderInfo) -> Unit = { },
- onError: (Throwable) -> Unit = { throw it }
-) = coroutineScope {
- val cachedValue = synchronized(cache) {
- cache.get(itemID)
- }
-
- if (cachedValue != null) {
- when (cachedValue) {
- is MangaListing -> onListing(cachedValue)
- is ReaderInfo -> onReader(cachedValue)
- else -> onError(IllegalStateException("Cached value is not MangaListing nor ReaderInfo"))
- }
- } else {
- runCatching {
- waitForRateLimit()
- val content: String = get("$manatokiUrl/comic/$itemID")
-
- val doc = Jsoup.parse(content)
-
- yield()
-
- if (doc.getElementsByClass("serial-list").size == 0) {
- val htmlData = doc
- .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()
-
- val urls = Jsoup.parse(htmlData)
- .select("img[^data-]:not([style])")
- .map {
- it.attributes()
- .first { it.key.startsWith("data-") }
- .value
- }
-
- val title = doc.getElementsByClass("toon-title").first()!!.ownText()
-
- val listingItemID = doc.select("a:contains(전체목록)").first()!!.attr("href")
- .takeLastWhile { it != '/' }
-
- val prevItemID = doc.getElementById("goPrevBtn")!!.attr("href")
- .let {
- if (it.contains('?'))
- it.dropLastWhile { it != '?' }.drop(1)
- else it
- }
- .takeLastWhile { it != '/' }
-
- val nextItemID = doc.getElementById("goNextBtn")!!.attr("href")
- .let {
- if (it.contains('?'))
- it.dropLastWhile { it != '?' }.drop(1)
- else it
- }
- .takeLastWhile { it != '/' }
-
- val readerInfo = ReaderInfo(
- itemID,
- title,
- urls,
- listingItemID,
- prevItemID,
- nextItemID
- )
-
- synchronized(cache) {
- cache.put(itemID, readerInfo)
- }
-
- onReader(readerInfo)
- } else {
- val titleBlock = doc.selectFirst("div.view-title")!!
-
- val title = titleBlock.select("div.view-content:not([itemprop])").first()!!.text()
-
- val author =
- titleBlock
- .select("div.view-content:not([itemprop]):contains(작가)")
- .first()!!
- .getElementsByTag("a")
- .first()!!
- .text()
-
- val tags =
- titleBlock
- .select("div.view-content:not([itemprop]):contains(분류)")
- .first()!!
- .getElementsByTag("a")
- .map { it.text() }
-
- val type =
- titleBlock
- .select("div.view-content:not([itemprop]):contains(발행구분)")
- .first()!!
- .getElementsByTag("a")
- .first()!!
- .text()
-
- val thumbnail =
- titleBlock.getElementsByTag("img").first()!!.attr("src")
-
- val thumbsUpCount =
- titleBlock.select("i.fa-thumbs-up + b").text().toInt()
-
- val entries =
- doc.select("div.serial-list .list-item").map {
- val episode = it.getElementsByClass("wr-num").first()!!.text().toInt()
- val (itemID, title) = it.getElementsByClass("item-subject").first()!!
- .let { subject ->
- subject.attr("href").dropLastWhile { it != '?' }.dropLast(1)
- .takeLastWhile { it != '/' } to subject.ownText()
- }
- val starRating = it.getElementsByClass("wr-star").first()!!.text().drop(1)
- .takeWhile { it != ')' }.toFloat()
- val date = it.getElementsByClass("wr-date").first()!!.text()
- val viewCount =
- it.getElementsByClass("wr-hit").first()!!.text().replace(",", "")
- .toInt()
- val thumbsUpCount =
- it.getElementsByClass("wr-good").first()!!.text().replace(",", "")
- .toInt()
-
- MangaListingEntry(
- itemID,
- episode,
- title,
- starRating,
- date,
- viewCount,
- thumbsUpCount
- )
- }
-
- val mangaListing = MangaListing(
- itemID,
- title,
- thumbnail,
- author,
- tags,
- type,
- thumbsUpCount,
- entries
- )
-
- synchronized(cache) {
- cache.put(itemID, mangaListing)
- }
-
- onListing(mangaListing)
- }
- }.onFailure(onError)
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/xyz/quaver/pupil/sources/manatoki/viewmodel/MainViewModel.kt b/app/src/main/java/xyz/quaver/pupil/sources/manatoki/viewmodel/MainViewModel.kt
deleted file mode 100644
index c6c64cc7..00000000
--- a/app/src/main/java/xyz/quaver/pupil/sources/manatoki/viewmodel/MainViewModel.kt
+++ /dev/null
@@ -1,116 +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.sources.manatoki.viewmodel
-
-import android.app.Application
-import androidx.compose.runtime.collectAsState
-import androidx.compose.runtime.mutableStateListOf
-import androidx.lifecycle.AndroidViewModel
-import androidx.lifecycle.ViewModel
-import androidx.lifecycle.viewModelScope
-import io.ktor.client.*
-import io.ktor.client.request.*
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.cancelAndJoin
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.yield
-import kotlinx.serialization.Serializable
-import org.jsoup.Jsoup
-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.sources.manatoki.HistoryDao
-import xyz.quaver.pupil.sources.manatoki.composable.Thumbnail
-import xyz.quaver.pupil.sources.manatoki.manatokiUrl
-import xyz.quaver.pupil.sources.manatoki.waitForRateLimit
-
-@Serializable
-data class TopWeekly(
- val itemID: String,
- val title: String,
- val count: String
-)
-
-class MainViewModel(
- private val client: HttpClient
-) : ViewModel() {
- val recentUpload = mutableStateListOf()
- val mangaList = mutableStateListOf()
- val topWeekly = mutableStateListOf()
-
- private var loadJob: Job? = null
-
- fun load() {
- viewModelScope.launch {
- loadJob?.cancelAndJoin()
- recentUpload.clear()
- mangaList.clear()
- topWeekly.clear()
-
- loadJob = launch {
- runCatching {
- waitForRateLimit()
- val doc = Jsoup.parse(client.get(manatokiUrl))
-
- yield()
-
- val misoPostGallery = doc.select(".miso-post-gallery")
-
- misoPostGallery[0]
- .select(".post-image > a")
- .forEach { entry ->
- val itemID = entry.attr("href").takeLastWhile { it != '/' }
- val title = entry.selectFirst("div.in-subject > b")!!.ownText()
- val thumbnail = entry.selectFirst("img")!!.attr("src")
-
- yield()
- recentUpload.add(Thumbnail(itemID, title, thumbnail))
- }
-
- misoPostGallery[1]
- .select(".post-image > a")
- .forEach { entry ->
- val itemID = entry.attr("href").takeLastWhile { it != '/' }
- val title = entry.selectFirst("div.in-subject")!!.ownText()
- val thumbnail = entry.selectFirst("img")!!.attr("src")
-
- yield()
- mangaList.add(Thumbnail(itemID, title, thumbnail))
- }
-
- val misoPostList = doc.select(".miso-post-list")
-
- misoPostList[4]
- .select(".post-row > a")
- .forEach { entry ->
- yield()
- val itemID = entry.attr("href").takeLastWhile { it != '/' }
- val title = entry.ownText()
- val count = entry.selectFirst("span.count")!!.text()
- topWeekly.add(TopWeekly(itemID, title, count))
- }
- }.onFailure {
- TODO()
- }
- }
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/xyz/quaver/pupil/sources/manatoki/viewmodel/RecentViewModel.kt b/app/src/main/java/xyz/quaver/pupil/sources/manatoki/viewmodel/RecentViewModel.kt
deleted file mode 100644
index 5dc42d49..00000000
--- a/app/src/main/java/xyz/quaver/pupil/sources/manatoki/viewmodel/RecentViewModel.kt
+++ /dev/null
@@ -1,80 +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.sources.manatoki.viewmodel
-
-import android.app.Application
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateListOf
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.setValue
-import androidx.lifecycle.AndroidViewModel
-import androidx.lifecycle.ViewModel
-import androidx.lifecycle.viewModelScope
-import io.ktor.client.*
-import io.ktor.client.request.*
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.cancelAndJoin
-import kotlinx.coroutines.launch
-import org.jsoup.Jsoup
-import org.kodein.di.DIAware
-import org.kodein.di.android.closestDI
-import org.kodein.di.instance
-import xyz.quaver.pupil.sources.manatoki.composable.Thumbnail
-import xyz.quaver.pupil.sources.manatoki.manatokiUrl
-
-class RecentViewModel(
- private val client: HttpClient
-): ViewModel() {
- var page by mutableStateOf(1)
-
- var loading by mutableStateOf(false)
- private set
- var error by mutableStateOf(false)
- private set
-
- val result = mutableStateListOf()
-
- private var loadJob: Job? = null
- fun load() {
- viewModelScope.launch {
- loadJob?.cancelAndJoin()
- result.clear()
- loading = true
-
- loadJob = launch {
- runCatching {
- val doc = Jsoup.parse(client.get("$manatokiUrl/bbs/page.php?hid=update&page=$page"))
-
- doc.getElementsByClass("post-list").forEach {
- val (itemID, title) = it.selectFirst(".post-subject > a")!!.let {
- it.attr("href").takeLastWhile { it != '/' } to it.ownText()
- }
- val thumbnail = it.getElementsByTag("img").attr("src")
-
- loading = false
- result.add(Thumbnail(itemID, title, thumbnail))
- }
- }.onFailure {
- loading = false
- error = true
- }
- }
- }
- }
-}
\ No newline at end of file
diff --git a/app/src/main/java/xyz/quaver/pupil/sources/manatoki/viewmodel/SearchViewModel.kt b/app/src/main/java/xyz/quaver/pupil/sources/manatoki/viewmodel/SearchViewModel.kt
deleted file mode 100644
index d7d40246..00000000
--- a/app/src/main/java/xyz/quaver/pupil/sources/manatoki/viewmodel/SearchViewModel.kt
+++ /dev/null
@@ -1,218 +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.sources.manatoki.viewmodel
-
-import android.app.Application
-import android.os.Parcelable
-import android.util.Log
-import androidx.compose.runtime.*
-import androidx.lifecycle.AndroidViewModel
-import androidx.lifecycle.ViewModel
-import io.ktor.client.*
-import io.ktor.client.request.*
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.cancelAndJoin
-import kotlinx.coroutines.coroutineScope
-import kotlinx.coroutines.launch
-import kotlinx.parcelize.Parcelize
-import kotlinx.serialization.Serializable
-import org.jsoup.Jsoup
-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.sources.manatoki.manatokiUrl
-
-@Parcelize
-@Serializable
-data class SearchResult(
- val itemID: String,
- val title: String,
- val thumbnail: String,
- val artist: String,
- val type: String,
- val lastUpdate: String
-): Parcelable
-
-val availablePublish = listOf(
- "주간",
- "격주",
- "월간",
- "단편",
- "단행본",
- "완결"
-)
-
-val availableJaum = listOf(
- "ㄱ",
- "ㄴ",
- "ㄷ",
- "ㄹ",
- "ㅁ",
- "ㅂ",
- "ㅅ",
- "ㅇ",
- "ㅈ",
- "ㅊ",
- "ㅋ",
- "ㅌ",
- "ㅍ",
- "ㅎ",
- "0-9",
- "a-z"
-)
-
-val availableTag = listOf(
- "17",
- "BL",
- "SF",
- "TS",
- "개그",
- "게임",
- "도박",
- "드라마",
- "라노벨",
- "러브코미디",
- "먹방",
- "백합",
- "붕탁",
- "순정",
- "스릴러",
- "스포츠",
- "시대",
- "애니화",
- "액션",
- "음악",
- "이세계",
- "일상",
- "전생",
- "추리",
- "판타지",
- "학원",
- "호러"
-)
-
-val availableSst = mapOf(
- "as_view" to "인기순",
- "as_good" to "추천순",
- "as_comment" to "댓글순",
- "as_bookmark" to "북마크순"
-)
-
-class SearchViewModel(
- private val client: HttpClient
-) : ViewModel() {
- private val logger = newLogger(LoggerFactory.default)
-
- // 발행
- var publish by mutableStateOf("")
- // 초성
- var jaum by mutableStateOf("")
- // 장르
- val tag = mutableStateMapOf()
- // 정렬
- var sst by mutableStateOf("")
- // 오름/내림
- var sod by mutableStateOf("")
- // 제목
- var stx by mutableStateOf("")
- // 작가
- var artist by mutableStateOf("")
-
- var page by mutableStateOf(1)
- var maxPage by mutableStateOf(0)
-
- val availableArtists = mutableStateListOf()
-
- var loading by mutableStateOf(false)
- private set
-
- var error by mutableStateOf(false)
- private set
-
- val result = mutableStateListOf()
-
- private var searchJob: Job? = null
- suspend fun search(resetPage: Boolean = true) = coroutineScope {
- searchJob?.cancelAndJoin()
-
- loading = true
- result.clear()
- availableArtists.clear()
- if (resetPage) page = 1
-
- searchJob = launch {
- runCatching {
- val urlBuilder = StringBuilder("$manatokiUrl/comic")
-
- if (page != 1) urlBuilder.append("/p$page")
-
- val args = mutableListOf()
-
- if (publish.isNotEmpty()) args.add("publish=$publish")
- if (jaum.isNotEmpty()) args.add("jaum=$jaum")
- if (tag.isNotEmpty()) args.add("tag=${tag.keys.joinToString(",")}")
- if (sst.isNotEmpty()) args.add("sst=$sst&sod=desc")
- if (stx.isNotEmpty()) args.add("stx=$stx")
- if (artist.isNotEmpty()) args.add("artist=$artist")
-
- if (args.isNotEmpty()) urlBuilder.append('?')
-
- urlBuilder.append(args.joinToString("&"))
-
- val doc = Jsoup.parse(client.get(urlBuilder.toString()))
-
- maxPage = doc.getElementsByClass("pagination").first()!!.getElementsByTag("a").maxOf { it.text().toIntOrNull() ?: 0 }
-
- doc.select("select > option").forEach {
- val value = it.ownText()
-
- if (value.isNotEmpty())
- availableArtists.add(value)
- }
-
- doc.getElementsByClass("list-item").forEach {
- val itemID =
- it.selectFirst(".img-item > a")!!.attr("href").takeLastWhile { it != '/' }
- val title = it.getElementsByClass("title").first()!!.text()
- val thumbnail = it.getElementsByTag("img").first()!!.attr("src")
- val artist = it.getElementsByClass("list-artist").first()!!.text()
- val type = it.getElementsByClass("list-publish").first()!!.text()
- val lastUpdate = it.getElementsByClass("list-date").first()!!.text()
-
- loading = false
- result.add(
- SearchResult(
- itemID,
- title,
- thumbnail,
- artist,
- type,
- lastUpdate
- )
- )
- }
- }.onFailure {
- loading = false
- error = true
- }
- }
- }
-}
\ 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 f0be83fa..5068117b 100644
--- a/app/src/main/java/xyz/quaver/pupil/ui/MainActivity.kt
+++ b/app/src/main/java/xyz/quaver/pupil/ui/MainActivity.kt
@@ -27,8 +27,8 @@ import androidx.compose.material.MaterialTheme
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.platform.LocalContext
import androidx.core.view.WindowCompat
+import androidx.datastore.core.DataStore
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController
@@ -36,15 +36,15 @@ import com.google.accompanist.insets.ProvideWindowInsets
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.runBlocking
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.pupil.proto.settingsDataStore
+import xyz.quaver.pupil.proto.Settings
import xyz.quaver.pupil.sources.SourceEntries
-import xyz.quaver.pupil.sources.composable.SourceSelectDialog
+import xyz.quaver.pupil.sources.SourceSelectDialog
import xyz.quaver.pupil.ui.theme.PupilTheme
@OptIn(ExperimentalAnimationApi::class, ExperimentalMaterialApi::class)
@@ -78,7 +78,7 @@ class MainActivity : ComponentActivity(), DIAware {
NavHost(navController, startDestination = "main") {
composable("main") {
var launched by rememberSaveable { mutableStateOf(false) }
- val context = LocalContext.current
+ val settingsDataStore: DataStore by rememberInstance()
var sourceSelectDialog by remember { mutableStateOf(false) }
@@ -86,7 +86,9 @@ class MainActivity : ComponentActivity(), DIAware {
SourceSelectDialog(navController, null)
LaunchedEffect(Unit) {
- val recentSource = context.settingsDataStore.data.map { it.recentSource }.first()
+ val recentSource =
+ settingsDataStore.data.map { it.recentSource }
+ .first()
if (recentSource.isEmpty()) {
sourceSelectDialog = true
@@ -104,8 +106,8 @@ class MainActivity : ComponentActivity(), DIAware {
composable("settings") {
}
- sources.forEach {
- it.second.run {
+ sources.values.forEach {
+ it.source.run {
navGraph(navController)
}
}
diff --git a/app/src/main/java/xyz/quaver/pupil/util/NetworkCache.kt b/app/src/main/java/xyz/quaver/pupil/util/NetworkCache.kt
deleted file mode 100644
index 2c2ca63d..00000000
--- a/app/src/main/java/xyz/quaver/pupil/util/NetworkCache.kt
+++ /dev/null
@@ -1,180 +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.util.Log
-import com.google.firebase.crashlytics.FirebaseCrashlytics
-import io.ktor.client.*
-import io.ktor.client.call.*
-import io.ktor.client.request.*
-import io.ktor.client.statement.*
-import io.ktor.http.*
-import io.ktor.util.*
-import io.ktor.util.collections.*
-import io.ktor.utils.io.*
-import io.ktor.utils.io.core.*
-import kotlinx.coroutines.*
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.sync.Mutex
-import kotlinx.coroutines.sync.withLock
-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 java.io.File
-import java.io.IOException
-import java.util.*
-import java.util.concurrent.ConcurrentHashMap
-import java.util.concurrent.Executors
-import kotlin.text.toByteArray
-
-private const val CACHE_LIMIT = 100*1024*1024 // 100M
-
-class NetworkCache(context: Context) : DIAware {
- override val di by closestDI(context)
-
- private val logger = newLogger(LoggerFactory.default)
-
- private val client: HttpClient by instance()
- private val networkScope = CoroutineScope(Executors.newFixedThreadPool(4).asCoroutineDispatcher())
-
- private val cacheDir = File(context.cacheDir, "networkcache")
-
- private val flowMutex = Mutex()
- private val flow = ConcurrentHashMap>()
-
- private val requestsMutex = Mutex()
- private val requests = ConcurrentHashMap()
-
- private val activeFilesMutex = Mutex()
- private val activeFiles = Collections.newSetFromMap(ConcurrentHashMap())
-
- private fun urlToFilename(url: String): String =
- sha256(url.toByteArray()).joinToString("") { "%02x".format(it) }
-
- fun cleanup() = CoroutineScope(Dispatchers.IO).launch {
- if (cacheDir.size() > CACHE_LIMIT)
- cacheDir.listFiles { file -> file.name !in activeFiles }?.forEach { it.delete() }
- }
-
- fun free(urls: List) = CoroutineScope(Dispatchers.IO).launch {
- requestsMutex.withLock {
- urls.forEach {
- requests[it]?.cancel()
- }
- }
- flowMutex.withLock {
- urls.forEach {
- flow.remove(it)
- }
- }
- activeFilesMutex.withLock {
- urls.forEach {
- activeFiles.remove(urlToFilename(it))
- }
- }
- }
-
- fun clear() = CoroutineScope(Dispatchers.IO).launch {
- requests.values.forEach { it.cancel() }
- flow.clear()
- activeFiles.clear()
- cacheDir.listFiles()?.forEach { it.delete() }
- }
-
- @OptIn(ExperimentalCoroutinesApi::class)
- suspend fun load(force: Boolean = false, requestBuilder: HttpRequestBuilder.() -> Unit): Pair, File> = coroutineScope {
- val request = HttpRequestBuilder().apply(requestBuilder)
-
- val url = request.url.buildString()
-
- val fileName = urlToFilename(url)
- val file = File(cacheDir, fileName)
- activeFiles.add(fileName)
-
- val progressFlow = flowMutex.withLock {
- if (flow.contains(url)) {
- flow[url]!!
- } else MutableStateFlow(0f).also { flow[url] = it }
- }
-
- requestsMutex.withLock {
- if (!requests.contains(url) || force) {
- if (force) requests[url]?.cancelAndJoin()
-
- requests[url] = networkScope.launch {
- runCatching {
- if (!force && file.exists()) {
- progressFlow.emit(Float.POSITIVE_INFINITY)
- } else {
- cacheDir.mkdirs()
- file.createNewFile()
-
- client.request(request).execute { httpResponse ->
- if (!httpResponse.status.isSuccess()) throw IOException("${request.url} failed with code ${httpResponse.status.value}")
- val responseChannel: ByteReadChannel = httpResponse.receive()
- val contentLength = httpResponse.contentLength() ?: -1
- var readBytes = 0f
-
- file.outputStream().use { outputStream ->
- outputStream.channel.truncate(0)
- while (!responseChannel.isClosedForRead) {
- if (!isActive) {
- file.delete()
- break
- }
-
- val packet =
- responseChannel.readRemaining(DEFAULT_BUFFER_SIZE.toLong())
- while (!packet.isEmpty) {
- if (!isActive) {
- file.delete()
- break
- }
-
- val bytes = packet.readBytes()
- outputStream.write(bytes)
-
- readBytes += bytes.size
- progressFlow.emit(readBytes / contentLength)
- }
- }
- }
- progressFlow.emit(Float.POSITIVE_INFINITY)
- }
- }
- }.onFailure {
- logger.warning(it)
- file.delete()
- FirebaseCrashlytics.getInstance().recordException(it)
- progressFlow.emit(Float.NEGATIVE_INFINITY)
- requestsMutex.withLock {
- requests.remove(url)
- }
- }
- }
- }
- }
-
- return@coroutineScope progressFlow to file
- }
-}
\ 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 c7759a49..7450876f 100644
--- a/app/src/main/java/xyz/quaver/pupil/util/misc.kt
+++ b/app/src/main/java/xyz/quaver/pupil/util/misc.kt
@@ -52,7 +52,7 @@ val JsonElement.content
get() = this.jsonPrimitive.contentOrNull
fun DIAware.source(source: String) = lazy { direct.source(source) }
-fun DirectDIAware.source(source: String) = instance().toMap()[source]!!
+fun DirectDIAware.source(source: String) = instance()[source]!!
class FileXImageSource(val file: FileX): ImageSource {
private val decoder by lazy {