Added hiyobi.io
This commit is contained in:
1
.idea/misc.xml
generated
1
.idea/misc.xml
generated
@@ -41,6 +41,7 @@
|
||||
<entry key="app/src/main/res/layout/reader_item.xml" value="0.1" />
|
||||
<entry key="app/src/main/res/layout/search_result_item.xml" value="0.2489868287740628" />
|
||||
<entry key="app/src/main/res/layout/source_select_dialog_item.xml" value="0.5119791666666667" />
|
||||
<entry key="app/src/main/res/xml/hitomi_preferences.xml" value="0.5119791666666667" />
|
||||
</map>
|
||||
</option>
|
||||
</component>
|
||||
|
||||
@@ -58,6 +58,14 @@ android {
|
||||
jvmTarget = "1.8"
|
||||
freeCompilerArgs += "-Xopt-in=kotlin.RequiresOptIn"
|
||||
}
|
||||
packagingOptions {
|
||||
resources.excludes.addAll(
|
||||
listOf(
|
||||
"META-INF/AL2.0",
|
||||
"META-INF/LGPL2.1"
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
|
||||
@@ -23,6 +23,15 @@ package xyz.quaver.pupil
|
||||
import android.util.Log
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import com.google.api.Http
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.call.*
|
||||
import io.ktor.client.engine.okhttp.*
|
||||
import io.ktor.client.request.*
|
||||
import io.ktor.client.statement.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
|
||||
|
||||
@@ -28,7 +28,6 @@ import org.kodein.di.*
|
||||
import xyz.quaver.floatingsearchview.databinding.SearchSuggestionItemBinding
|
||||
import xyz.quaver.floatingsearchview.suggestions.model.SearchSuggestion
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.sources.hitomi.Hitomi
|
||||
|
||||
interface ItemInfo : Parcelable {
|
||||
val source: String
|
||||
@@ -39,7 +38,7 @@ interface ItemInfo : Parcelable {
|
||||
@Parcelize
|
||||
class DefaultSearchSuggestion(override val body: String) : SearchSuggestion
|
||||
|
||||
data class SearchResultEvent(val type: Type, val payload: String) {
|
||||
data class SearchResultEvent(val type: Type, val itemID: String, val payload: Parcelable? = null) {
|
||||
enum class Type {
|
||||
OPEN_READER,
|
||||
OPEN_DETAILS,
|
||||
@@ -75,7 +74,8 @@ val sourceModule = DI.Module(name = "source") {
|
||||
bindSet<SourceEntry>()
|
||||
|
||||
listOf<(Application) -> (Source)>(
|
||||
{ Hitomi(it) }
|
||||
{ Hitomi(it) },
|
||||
{ Hiyobi_io(it) }
|
||||
).forEach { source ->
|
||||
inSet { singleton { source.invoke(instance()).let { it.name to it } } }
|
||||
}
|
||||
|
||||
@@ -26,11 +26,8 @@ import kotlinx.coroutines.launch
|
||||
import org.kodein.di.DI
|
||||
import org.kodein.di.DIAware
|
||||
import org.kodein.di.direct
|
||||
import org.kodein.di.instance
|
||||
import xyz.quaver.floatingsearchview.suggestions.model.SearchSuggestion
|
||||
import xyz.quaver.pupil.db.AppDatabase
|
||||
import xyz.quaver.pupil.util.database
|
||||
import xyz.quaver.pupil.util.source
|
||||
|
||||
class History(override val di: DI) : Source(), DIAware {
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.quaver.pupil.sources.hitomi
|
||||
package xyz.quaver.pupil.sources
|
||||
|
||||
import android.app.Application
|
||||
import android.view.LayoutInflater
|
||||
@@ -40,12 +40,9 @@ 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.colorResource
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.room.*
|
||||
import coil.annotation.ExperimentalCoilApi
|
||||
import coil.compose.rememberImagePainter
|
||||
import com.google.accompanist.flowlayout.FlowRow
|
||||
@@ -57,7 +54,6 @@ import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.parcelize.IgnoredOnParcel
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import kotlinx.serialization.Serializable
|
||||
import org.kodein.di.DI
|
||||
import org.kodein.di.DIAware
|
||||
import org.kodein.di.android.closestDI
|
||||
import org.kodein.di.instance
|
||||
@@ -69,9 +65,9 @@ import xyz.quaver.hitomi.*
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.db.AppDatabase
|
||||
import xyz.quaver.pupil.db.Bookmark
|
||||
import xyz.quaver.pupil.sources.ItemInfo
|
||||
import xyz.quaver.pupil.sources.SearchResultEvent
|
||||
import xyz.quaver.pupil.sources.Source
|
||||
import xyz.quaver.pupil.ui.theme.Blue700
|
||||
import xyz.quaver.pupil.ui.theme.Orange500
|
||||
import xyz.quaver.pupil.ui.theme.Pink600
|
||||
import xyz.quaver.pupil.util.Preferences
|
||||
import xyz.quaver.pupil.util.wordCapitalize
|
||||
import kotlin.math.max
|
||||
@@ -152,7 +148,7 @@ class Hitomi(app: Application) : Source(), DIAware {
|
||||
var cachedSortMode: Int = -1
|
||||
private val cache = mutableListOf<Int>()
|
||||
|
||||
override suspend fun search(query: String, range: IntRange, sortMode: Int): Pair<Channel<ItemInfo>, Int> = coroutineScope { withContext(Dispatchers.IO) {
|
||||
override suspend fun search(query: String, range: IntRange, sortMode: Int): Pair<Channel<ItemInfo>, Int> = withContext(Dispatchers.IO) {
|
||||
if (cachedQuery != query || cachedSortMode != sortMode || cache.isEmpty()) {
|
||||
cachedQuery = null
|
||||
cache.clear()
|
||||
@@ -179,8 +175,8 @@ class Hitomi(app: Application) : Source(), DIAware {
|
||||
channel.close()
|
||||
}
|
||||
|
||||
Pair(channel, cache.size)
|
||||
} }
|
||||
channel to cache.size
|
||||
}
|
||||
|
||||
override suspend fun suggestion(query: String) : List<TagSuggestion> {
|
||||
return getSuggestionsForQuery(query.takeLastWhile { !it.isWhitespace() }).map {
|
||||
@@ -336,10 +332,10 @@ class Hitomi(app: Application) : Source(), DIAware {
|
||||
}
|
||||
|
||||
val (surfaceColor, textTint) = when {
|
||||
isFavorite -> Pair(colorResource(id = R.color.material_orange_500), Color.White)
|
||||
isFavorite -> Pair(Orange500, Color.White)
|
||||
else -> when (tagParts[0]) {
|
||||
"male" -> Pair(colorResource(id = R.color.material_blue_700), Color.White)
|
||||
"female" -> Pair(colorResource(id = R.color.material_pink_600), Color.White)
|
||||
"male" -> Pair(Blue700, Color.White)
|
||||
"female" -> Pair(Pink600, Color.White)
|
||||
else -> Pair(MaterialTheme.colors.background, MaterialTheme.colors.onBackground)
|
||||
}
|
||||
}
|
||||
@@ -394,7 +390,7 @@ class Hitomi(app: Application) : Source(), DIAware {
|
||||
var isFolded by remember { mutableStateOf(true) }
|
||||
val bookmarkedTags by bookmarkDao.getAll(name).observeAsState(emptyList())
|
||||
|
||||
val bookmarkedTagsInList = bookmarkedTags.toSet() intersect tags
|
||||
val bookmarkedTagsInList = bookmarkedTags.toSet() intersect tags.toSet()
|
||||
|
||||
FlowRow(Modifier.padding(0.dp, 16.dp)) {
|
||||
tags.sortedBy { if (bookmarkedTagsInList.contains(it)) 0 else 1 }.let { (if (isFolded) it.take(10) else it) }.forEach { tag ->
|
||||
@@ -454,7 +450,9 @@ class Hitomi(app: Application) : Source(), DIAware {
|
||||
|
||||
val painter = rememberImagePainter(itemInfo.thumbnail)
|
||||
|
||||
Column {
|
||||
Column(
|
||||
modifier = Modifier.clickable { onEvent(SearchResultEvent(SearchResultEvent.Type.OPEN_READER, itemInfo.itemID, itemInfo)) }
|
||||
) {
|
||||
Row {
|
||||
Image(
|
||||
painter = painter,
|
||||
|
||||
441
app/src/main/java/xyz/quaver/pupil/sources/Hiyobi_io.kt
Normal file
441
app/src/main/java/xyz/quaver/pupil/sources/Hiyobi_io.kt
Normal file
@@ -0,0 +1,441 @@
|
||||
/*
|
||||
* 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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.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.floatingsearchview.suggestions.model.SearchSuggestion
|
||||
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<String>,
|
||||
val series: List<String>,
|
||||
val type: String,
|
||||
val date: String,
|
||||
val bookmark: Unit?,
|
||||
val tags: List<Tag>,
|
||||
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<String>,
|
||||
val thumbnail: String,
|
||||
val series: List<String>,
|
||||
val type: String,
|
||||
val date: String,
|
||||
val bookmark: Unit?,
|
||||
val tags: List<Tag>,
|
||||
val commentCount: Int,
|
||||
val pageCount: Int
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class QueryManga(
|
||||
val nowPage: Int,
|
||||
val maxPage: Int,
|
||||
val manga: List<Manga>
|
||||
)
|
||||
|
||||
@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 preferenceID = 0
|
||||
override val availableSortMode = emptyList<String>()
|
||||
|
||||
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<JsonObject>(
|
||||
"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<JsonObject>(
|
||||
"https://api.hiyobi.io/api?query=$lastPageQuery"
|
||||
)["data"]!!["queryManga"]!!["manga"]!!.jsonArray.size
|
||||
|
||||
return (maxPage-1)*25+lastPageCount
|
||||
}
|
||||
|
||||
override suspend fun search(
|
||||
query: String,
|
||||
range: IntRange,
|
||||
sortMode: Int
|
||||
): Pair<Channel<ItemInfo>, Int> = withContext(Dispatchers.IO) {
|
||||
val channel = Channel<ItemInfo>()
|
||||
|
||||
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 suggestion(query: String): List<SearchSuggestion> {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
override suspend fun images(itemID: String): List<String> = withContext(Dispatchers.IO) {
|
||||
val query = "{getManga(mangaId:$itemID){urls}}"
|
||||
|
||||
client.post<JsonObject>("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<Tag>) {
|
||||
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 painter = rememberImagePainter(itemInfo.thumbnail)
|
||||
|
||||
Row(
|
||||
modifier = Modifier.clickable {
|
||||
onEvent(SearchResultEvent(SearchResultEvent.Type.OPEN_READER, itemInfo.itemID, itemInfo))
|
||||
}
|
||||
) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
/*
|
||||
* Pupil, Hitomi.la viewer for Android
|
||||
* Copyright (C) 2020 tom5079
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.quaver.pupil.ui
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.os.PersistableBundle
|
||||
import android.view.WindowManager
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.annotation.CallSuper
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.util.LockManager
|
||||
import xyz.quaver.pupil.util.Preferences
|
||||
import xyz.quaver.pupil.util.normalizeID
|
||||
|
||||
open class BaseActivity : AppCompatActivity() {
|
||||
|
||||
private var locked: Boolean = true
|
||||
|
||||
private val lockLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
if (it.resultCode == Activity.RESULT_OK)
|
||||
locked = false
|
||||
else
|
||||
finish()
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
|
||||
super.onCreate(savedInstanceState, persistentState)
|
||||
|
||||
locked = !LockManager(this).locks.isNullOrEmpty()
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
if (Preferences["security_mode"])
|
||||
window.setFlags(
|
||||
WindowManager.LayoutParams.FLAG_SECURE,
|
||||
WindowManager.LayoutParams.FLAG_SECURE)
|
||||
else
|
||||
window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
|
||||
|
||||
if (locked)
|
||||
lockLauncher.launch(Intent(this, LockActivity::class.java))
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,280 +0,0 @@
|
||||
/*
|
||||
* Pupil, Hitomi.la viewer for Android
|
||||
* Copyright (C) 2019 tom5079
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.quaver.pupil.ui
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.AlertDialog
|
||||
import android.os.Bundle
|
||||
import android.view.animation.Animation
|
||||
import android.view.animation.AnimationUtils
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.biometric.BiometricManager
|
||||
import androidx.biometric.BiometricPrompt
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.andrognito.patternlockview.PatternLockView
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.databinding.LockActivityBinding
|
||||
import xyz.quaver.pupil.ui.fragment.PINLockFragment
|
||||
import xyz.quaver.pupil.ui.fragment.PatternLockFragment
|
||||
import xyz.quaver.pupil.util.Lock
|
||||
import xyz.quaver.pupil.util.LockManager
|
||||
import xyz.quaver.pupil.util.Preferences
|
||||
|
||||
private var lastUnlocked = 0L
|
||||
class LockActivity : AppCompatActivity() {
|
||||
|
||||
private lateinit var lockManager: LockManager
|
||||
private var mode: String? = null
|
||||
|
||||
private lateinit var binding: LockActivityBinding
|
||||
|
||||
private val patternLockFragment = PatternLockFragment().apply {
|
||||
var lastPass = ""
|
||||
onPatternDrawn = {
|
||||
when(mode) {
|
||||
null -> {
|
||||
val result = lockManager.check(it)
|
||||
|
||||
if (result == true) {
|
||||
lastUnlocked = System.currentTimeMillis()
|
||||
setResult(Activity.RESULT_OK)
|
||||
finish()
|
||||
} else
|
||||
binding.patternLockView.setViewMode(PatternLockView.PatternViewMode.WRONG)
|
||||
}
|
||||
"add_lock" -> {
|
||||
if (lastPass.isEmpty()) {
|
||||
lastPass = it
|
||||
|
||||
Snackbar.make(view!!, R.string.settings_lock_confirm, Snackbar.LENGTH_LONG).show()
|
||||
} else {
|
||||
if (lastPass == it) {
|
||||
LockManager(context!!).add(Lock.generate(Lock.Type.PATTERN, it))
|
||||
finish()
|
||||
} else {
|
||||
binding.patternLockView.setViewMode(PatternLockView.PatternViewMode.WRONG)
|
||||
lastPass = ""
|
||||
|
||||
Snackbar.make(view!!, R.string.settings_lock_wrong_confirm, Snackbar.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val pinLockFragment = PINLockFragment().apply {
|
||||
var lastPass = ""
|
||||
onPINEntered = {
|
||||
when(mode) {
|
||||
null -> {
|
||||
val result = lockManager.check(it)
|
||||
|
||||
if (result == true) {
|
||||
lastUnlocked = System.currentTimeMillis()
|
||||
setResult(Activity.RESULT_OK)
|
||||
finish()
|
||||
} else {
|
||||
binding.indicatorDots.startAnimation(AnimationUtils.loadAnimation(context, R.anim.shake).apply {
|
||||
setAnimationListener(object: Animation.AnimationListener {
|
||||
override fun onAnimationEnd(animation: Animation?) {
|
||||
binding.pinLockView.resetPinLockView()
|
||||
binding.pinLockView.isEnabled = true
|
||||
}
|
||||
|
||||
override fun onAnimationStart(animation: Animation?) {
|
||||
binding.pinLockView.isEnabled = false
|
||||
}
|
||||
|
||||
override fun onAnimationRepeat(animation: Animation?) {
|
||||
// Do Nothing
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
"add_lock" -> {
|
||||
if (lastPass.isEmpty()) {
|
||||
lastPass = it
|
||||
|
||||
binding.pinLockView.resetPinLockView()
|
||||
Snackbar.make(view!!, R.string.settings_lock_confirm, Snackbar.LENGTH_LONG).show()
|
||||
} else {
|
||||
if (lastPass == it) {
|
||||
LockManager(context!!).add(Lock.generate(Lock.Type.PIN, it))
|
||||
finish()
|
||||
} else {
|
||||
binding.indicatorDots.startAnimation(AnimationUtils.loadAnimation(context, R.anim.shake).apply {
|
||||
setAnimationListener(object: Animation.AnimationListener {
|
||||
override fun onAnimationEnd(animation: Animation?) {
|
||||
binding.pinLockView.resetPinLockView()
|
||||
binding.pinLockView.isEnabled = true
|
||||
}
|
||||
|
||||
override fun onAnimationStart(animation: Animation?) {
|
||||
binding.pinLockView.isEnabled = false
|
||||
}
|
||||
|
||||
override fun onAnimationRepeat(animation: Animation?) {
|
||||
// Do Nothing
|
||||
}
|
||||
})
|
||||
})
|
||||
lastPass = ""
|
||||
|
||||
Snackbar.make(view!!, R.string.settings_lock_wrong_confirm, Snackbar.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showBiometricPrompt() {
|
||||
val promptInfo = BiometricPrompt.PromptInfo.Builder()
|
||||
.setTitle(getText(R.string.settings_lock_fingerprint_prompt))
|
||||
.setSubtitle(getText(R.string.settings_lock_fingerprint_prompt_subtitle))
|
||||
.setNegativeButtonText(getText(android.R.string.cancel))
|
||||
.setConfirmationRequired(false)
|
||||
.build()
|
||||
|
||||
val biometricPrompt = BiometricPrompt(this, ContextCompat.getMainExecutor(this),
|
||||
object : BiometricPrompt.AuthenticationCallback() {
|
||||
override fun onAuthenticationSucceeded(
|
||||
result: BiometricPrompt.AuthenticationResult) {
|
||||
super.onAuthenticationSucceeded(result)
|
||||
lastUnlocked = System.currentTimeMillis()
|
||||
setResult(RESULT_OK)
|
||||
finish()
|
||||
return
|
||||
}
|
||||
})
|
||||
|
||||
// Displays the "log in" prompt.
|
||||
biometricPrompt.authenticate(promptInfo)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
binding = LockActivityBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
lockManager = try {
|
||||
LockManager(this)
|
||||
} catch (e: Exception) {
|
||||
AlertDialog.Builder(this).apply {
|
||||
setTitle(R.string.warning)
|
||||
setMessage(R.string.lock_corrupted)
|
||||
setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
finish()
|
||||
}
|
||||
}.show()
|
||||
return
|
||||
}
|
||||
|
||||
mode = intent.getStringExtra("mode")
|
||||
val force = intent.getBooleanExtra("force", false)
|
||||
|
||||
when(mode) {
|
||||
null -> {
|
||||
if (lockManager.isEmpty()) {
|
||||
setResult(RESULT_OK)
|
||||
finish()
|
||||
return
|
||||
}
|
||||
|
||||
if (System.currentTimeMillis() - lastUnlocked < 5*60*1000 && !force) {
|
||||
lastUnlocked = System.currentTimeMillis()
|
||||
setResult(RESULT_OK)
|
||||
finish()
|
||||
return
|
||||
}
|
||||
|
||||
if (
|
||||
Preferences["lock_fingerprint"]
|
||||
&& BiometricManager.from(this).canAuthenticate() == BiometricManager.BIOMETRIC_SUCCESS
|
||||
) {
|
||||
binding.fingerprintBtn.apply {
|
||||
isEnabled = true
|
||||
setOnClickListener {
|
||||
showBiometricPrompt()
|
||||
}
|
||||
}
|
||||
showBiometricPrompt()
|
||||
}
|
||||
|
||||
binding.patternBtn.apply {
|
||||
isEnabled = lockManager.contains(Lock.Type.PATTERN)
|
||||
setOnClickListener {
|
||||
supportFragmentManager.beginTransaction().replace(
|
||||
R.id.lock_content, patternLockFragment
|
||||
).commit()
|
||||
}
|
||||
}
|
||||
binding.pinBtn.apply {
|
||||
isEnabled = lockManager.contains(Lock.Type.PIN)
|
||||
setOnClickListener {
|
||||
supportFragmentManager.beginTransaction().replace(
|
||||
R.id.lock_content, pinLockFragment
|
||||
).commit()
|
||||
}
|
||||
}
|
||||
binding.passwordBtn.isEnabled = false
|
||||
|
||||
when (lockManager.locks!!.first().type) {
|
||||
Lock.Type.PIN -> {
|
||||
|
||||
supportFragmentManager.beginTransaction().add(
|
||||
R.id.lock_content, pinLockFragment
|
||||
).commit()
|
||||
}
|
||||
Lock.Type.PATTERN -> {
|
||||
supportFragmentManager.beginTransaction().add(
|
||||
R.id.lock_content, patternLockFragment
|
||||
).commit()
|
||||
}
|
||||
else -> return
|
||||
}
|
||||
}
|
||||
"add_lock" -> {
|
||||
binding.patternBtn.isEnabled = false
|
||||
binding.pinBtn.isEnabled = false
|
||||
binding.fingerprintBtn.isEnabled = false
|
||||
binding.passwordBtn.isEnabled = false
|
||||
|
||||
when(intent.getStringExtra("type")!!) {
|
||||
"pattern" -> {
|
||||
binding.patternBtn.isEnabled = true
|
||||
supportFragmentManager.beginTransaction().add(
|
||||
R.id.lock_content, patternLockFragment
|
||||
).commit()
|
||||
}
|
||||
"pin" -> {
|
||||
binding.pinBtn.isEnabled = true
|
||||
supportFragmentManager.beginTransaction().add(
|
||||
R.id.lock_content, pinLockFragment
|
||||
).commit()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -53,7 +53,6 @@ import com.google.accompanist.drawablepainter.rememberDrawablePainter
|
||||
import kotlinx.coroutines.*
|
||||
import org.kodein.di.DIAware
|
||||
import org.kodein.di.android.closestDI
|
||||
import org.kodein.di.compose.withDI
|
||||
import org.kodein.log.LoggerFactory
|
||||
import org.kodein.log.newLogger
|
||||
import xyz.quaver.pupil.*
|
||||
@@ -65,7 +64,6 @@ import xyz.quaver.pupil.ui.composable.FloatingSearchBar
|
||||
import xyz.quaver.pupil.ui.composable.MultipleFloatingActionButton
|
||||
import xyz.quaver.pupil.ui.composable.SubFabItem
|
||||
import xyz.quaver.pupil.ui.dialog.SourceSelectDialog
|
||||
import xyz.quaver.pupil.ui.dialog.SourceSelectDialogItem
|
||||
import xyz.quaver.pupil.ui.theme.PupilTheme
|
||||
import xyz.quaver.pupil.ui.view.ProgressCardView
|
||||
import xyz.quaver.pupil.ui.viewmodel.MainViewModel
|
||||
@@ -117,8 +115,12 @@ class MainActivity : ComponentActivity(), DIAware {
|
||||
}
|
||||
|
||||
if (openSourceSelectDialog)
|
||||
SourceSelectDialog {
|
||||
SourceSelectDialog(
|
||||
currentSource = model.source.name,
|
||||
onDismissRequest = { openSourceSelectDialog = false }
|
||||
) { source ->
|
||||
openSourceSelectDialog = false
|
||||
model.setSourceAndReset(source.name)
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
@@ -185,7 +187,8 @@ class MainActivity : ComponentActivity(), DIAware {
|
||||
ReaderActivity::class.java
|
||||
).apply {
|
||||
putExtra("source", model.source.name)
|
||||
putExtra("id", itemInfo.itemID)
|
||||
putExtra("id", event.itemID)
|
||||
putExtra("payload", event.payload)
|
||||
})
|
||||
}
|
||||
else -> TODO("")
|
||||
|
||||
@@ -81,8 +81,6 @@ class ReaderActivity : ComponentActivity(), DIAware {
|
||||
setContent {
|
||||
var isFABExpanded by remember { mutableStateOf(FloatingActionButtonState.COLLAPSED) }
|
||||
val isFullscreen by model.isFullscreen.observeAsState(false)
|
||||
val title by model.title.observeAsState(stringResource(R.string.reader_loading))
|
||||
val sourceIcon by model.sourceIcon.observeAsState()
|
||||
val imageSources = remember { mutableStateListOf<ImageSource?>() }
|
||||
val imageHeights = remember { mutableStateListOf<Float?>() }
|
||||
val states = remember { mutableStateListOf<SubSampledImageState>() }
|
||||
@@ -131,12 +129,12 @@ class ReaderActivity : ComponentActivity(), DIAware {
|
||||
TopAppBar(
|
||||
title = {
|
||||
Text(
|
||||
title,
|
||||
model.title ?: stringResource(R.string.reader_loading),
|
||||
color = MaterialTheme.colors.onSecondary
|
||||
)
|
||||
},
|
||||
actions = {
|
||||
sourceIcon?.let { sourceIcon ->
|
||||
model.sourceIcon?.let { sourceIcon ->
|
||||
Image(
|
||||
modifier = Modifier.size(36.dp),
|
||||
painter = painterResource(id = sourceIcon),
|
||||
|
||||
@@ -20,11 +20,12 @@ package xyz.quaver.pupil.ui
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.MenuItem
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.ui.fragment.SettingsFragment
|
||||
import xyz.quaver.pupil.ui.fragment.SourceSettingsFragment
|
||||
|
||||
class SettingsActivity : BaseActivity() {
|
||||
class SettingsActivity : AppCompatActivity() {
|
||||
|
||||
companion object {
|
||||
const val SETTINGS_EXTRA = "xyz.quaver.pupil.ui.SettingsActivity.SETTINGS_EXTRA"
|
||||
|
||||
@@ -27,7 +27,7 @@ import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.databinding.DefaultQueryDialogBinding
|
||||
import xyz.quaver.pupil.sources.hitomi.Hitomi
|
||||
import xyz.quaver.pupil.sources.Hitomi
|
||||
import xyz.quaver.pupil.types.Tags
|
||||
import xyz.quaver.pupil.util.Preferences
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
package xyz.quaver.pupil.ui.dialog
|
||||
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.*
|
||||
@@ -36,7 +37,7 @@ import xyz.quaver.pupil.sources.Source
|
||||
import xyz.quaver.pupil.sources.SourceEntries
|
||||
|
||||
@Composable
|
||||
fun SourceSelectDialogItem(source: Source) {
|
||||
fun SourceSelectDialogItem(source: Source, isSelected: Boolean, onSelected: (Source) -> Unit = { }) {
|
||||
Row(
|
||||
modifier = Modifier.padding(16.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
@@ -59,16 +60,20 @@ fun SourceSelectDialogItem(source: Source) {
|
||||
tint = MaterialTheme.colors.onSurface.copy(alpha = 0.5f)
|
||||
)
|
||||
|
||||
Button(onClick = { /*TODO*/ }) {
|
||||
Button(
|
||||
enabled = !isSelected,
|
||||
onClick = {
|
||||
onSelected(source)
|
||||
}
|
||||
) {
|
||||
Text("GO")
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Preview
|
||||
@Composable
|
||||
fun SourceSelectDialog(onDismissRequest: () -> Unit = { }) {
|
||||
fun SourceSelectDialog(currentSource: String, onDismissRequest: () -> Unit = { }, onSelected: (Source) -> Unit = { }) {
|
||||
val sourceEntries: SourceEntries by rememberInstance()
|
||||
|
||||
Dialog(onDismissRequest = onDismissRequest) {
|
||||
@@ -77,7 +82,7 @@ fun SourceSelectDialog(onDismissRequest: () -> Unit = { }) {
|
||||
shape = RoundedCornerShape(12.dp)
|
||||
) {
|
||||
Column() {
|
||||
sourceEntries.forEach { SourceSelectDialogItem(it.second) }
|
||||
sourceEntries.forEach { SourceSelectDialogItem(it.second, it.first == currentSource, onSelected) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,146 +0,0 @@
|
||||
/*
|
||||
* Pupil, Hitomi.la viewer for Android
|
||||
* Copyright (C) 2020 tom5079
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.quaver.pupil.ui.fragment
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.preference.Preference
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import androidx.preference.SwitchPreferenceCompat
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.ui.LockActivity
|
||||
import xyz.quaver.pupil.util.Lock
|
||||
import xyz.quaver.pupil.util.LockManager
|
||||
import xyz.quaver.pupil.util.Preferences
|
||||
|
||||
class LockSettingsFragment : PreferenceFragmentCompat() {
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
val lockManager = LockManager(requireContext())
|
||||
|
||||
findPreference<Preference>("lock_pattern")?.summary =
|
||||
if (lockManager.contains(Lock.Type.PATTERN))
|
||||
getString(R.string.settings_lock_enabled)
|
||||
else
|
||||
""
|
||||
|
||||
findPreference<Preference>("lock_pin")?.summary =
|
||||
if (lockManager.contains(Lock.Type.PIN))
|
||||
getString(R.string.settings_lock_enabled)
|
||||
else
|
||||
""
|
||||
|
||||
if (lockManager.isEmpty()) {
|
||||
(findPreference<Preference>("lock_fingerprint") as SwitchPreferenceCompat).isChecked = false
|
||||
|
||||
Preferences["lock_fingerprint"] = false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
setPreferencesFromResource(R.xml.lock_preferences, rootKey)
|
||||
|
||||
with (findPreference<Preference>("lock_pattern")) {
|
||||
this!!
|
||||
|
||||
if (LockManager(requireContext()).contains(Lock.Type.PATTERN))
|
||||
summary = getString(R.string.settings_lock_enabled)
|
||||
|
||||
onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||
val lockManager = LockManager(requireContext())
|
||||
|
||||
if (lockManager.contains(Lock.Type.PATTERN)) {
|
||||
AlertDialog.Builder(requireContext()).apply {
|
||||
setTitle(R.string.warning)
|
||||
setMessage(R.string.settings_lock_remove_message)
|
||||
|
||||
setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
lockManager.remove(Lock.Type.PATTERN)
|
||||
onResume()
|
||||
}
|
||||
setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
}.show()
|
||||
} else {
|
||||
val intent = Intent(requireContext(), LockActivity::class.java).apply {
|
||||
putExtra("mode", "add_lock")
|
||||
putExtra("type", "pattern")
|
||||
}
|
||||
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
with (findPreference<Preference>("lock_pin")) {
|
||||
this!!
|
||||
|
||||
if (LockManager(requireContext()).contains(Lock.Type.PIN))
|
||||
summary = getString(R.string.settings_lock_enabled)
|
||||
|
||||
onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||
val lockManager = LockManager(requireContext())
|
||||
|
||||
if (lockManager.contains(Lock.Type.PIN)) {
|
||||
AlertDialog.Builder(requireContext()).apply {
|
||||
setTitle(R.string.warning)
|
||||
setMessage(R.string.settings_lock_remove_message)
|
||||
|
||||
setPositiveButton(android.R.string.ok) { _, _ ->
|
||||
lockManager.remove(Lock.Type.PIN)
|
||||
onResume()
|
||||
}
|
||||
setNegativeButton(android.R.string.cancel) { _, _ -> }
|
||||
}.show()
|
||||
} else {
|
||||
val intent = Intent(requireContext(), LockActivity::class.java).apply {
|
||||
putExtra("mode", "add_lock")
|
||||
putExtra("type", "pin")
|
||||
}
|
||||
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
with (findPreference<Preference>("lock_fingerprint")) {
|
||||
this!!
|
||||
|
||||
setOnPreferenceChangeListener { _, newValue ->
|
||||
this as SwitchPreferenceCompat
|
||||
|
||||
if (newValue == true && LockManager(requireContext()).isEmpty()) {
|
||||
isChecked = false
|
||||
|
||||
Toast.makeText(requireContext(), R.string.settings_lock_fingerprint_without_lock, Toast.LENGTH_SHORT).show()
|
||||
} else
|
||||
isChecked = newValue as Boolean
|
||||
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,7 +32,6 @@ import org.kodein.di.instance
|
||||
import xyz.quaver.io.FileX
|
||||
import xyz.quaver.io.util.getChild
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.ui.LockActivity
|
||||
import xyz.quaver.pupil.ui.SettingsActivity
|
||||
import xyz.quaver.pupil.ui.dialog.*
|
||||
import xyz.quaver.pupil.util.*
|
||||
@@ -49,16 +48,6 @@ class SettingsFragment :
|
||||
|
||||
private val downloadManager: DownloadManager by instance()
|
||||
|
||||
private val lockLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
if (it.resultCode == Activity.RESULT_OK) {
|
||||
parentFragmentManager
|
||||
.beginTransaction()
|
||||
.replace(R.id.settings, LockSettingsFragment())
|
||||
.addToBackStack("Lock")
|
||||
.commitAllowingStateLoss()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
@@ -88,12 +77,6 @@ class SettingsFragment :
|
||||
"download_folder" -> {
|
||||
DownloadLocationDialogFragment().show(parentFragmentManager, "Download Location Dialog")
|
||||
}
|
||||
"app_lock" -> {
|
||||
val intent = Intent(requireContext(), LockActivity::class.java).apply {
|
||||
putExtra("force", true)
|
||||
}
|
||||
lockLauncher.launch(intent)
|
||||
}
|
||||
"proxy" -> {
|
||||
ProxyDialogFragment().show(parentFragmentManager, "Proxy Dialog")
|
||||
}
|
||||
|
||||
@@ -33,7 +33,7 @@ import xyz.quaver.floatingsearchview.databinding.SearchSuggestionItemBinding
|
||||
import xyz.quaver.floatingsearchview.suggestions.model.SearchSuggestion
|
||||
import xyz.quaver.floatingsearchview.util.view.SearchInputView
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.sources.hitomi.Hitomi
|
||||
import xyz.quaver.pupil.sources.Hitomi
|
||||
import xyz.quaver.pupil.types.FavoriteHistorySwitch
|
||||
import xyz.quaver.pupil.types.HistorySuggestion
|
||||
import xyz.quaver.pupil.types.LoadingSuggestion
|
||||
|
||||
@@ -1,27 +1,12 @@
|
||||
package xyz.quaver.pupil.ui.view
|
||||
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.cardview.widget.CardView
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.drawable.DrawableCompat
|
||||
import xyz.quaver.pupil.R
|
||||
import xyz.quaver.pupil.databinding.ProgressCardViewBinding
|
||||
import xyz.quaver.pupil.sources.ItemInfo
|
||||
import xyz.quaver.pupil.sources.Source
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
|
||||
@@ -1,59 +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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package xyz.quaver.pupil.ui.viewmodel
|
||||
|
||||
import android.app.Application
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.*
|
||||
import org.kodein.di.DIAware
|
||||
import org.kodein.di.android.x.closestDI
|
||||
import xyz.quaver.pupil.sources.ItemInfo
|
||||
import xyz.quaver.pupil.sources.Source
|
||||
import xyz.quaver.pupil.util.source
|
||||
|
||||
class GalleryDialogViewModel(app: Application) : AndroidViewModel(app), DIAware {
|
||||
|
||||
override val di by closestDI()
|
||||
|
||||
private val _info = MutableLiveData<ItemInfo>()
|
||||
val info: LiveData<ItemInfo> = _info
|
||||
|
||||
private val _related = MutableLiveData<List<ItemInfo>>()
|
||||
val related: LiveData<List<ItemInfo>> = _related
|
||||
|
||||
fun load(source: String, itemID: String) {/*
|
||||
val source: Source by source(source)
|
||||
|
||||
viewModelScope.launch {
|
||||
_info.value = withContext(Dispatchers.IO) {
|
||||
source.info(itemID)
|
||||
}.also {
|
||||
_related.value = it.extra[ItemInfo.ExtraType.RELATED_ITEM]?.await()?.split(", ")?.map { related ->
|
||||
async(Dispatchers.IO) {
|
||||
source.info(related)
|
||||
}
|
||||
}?.awaitAll()
|
||||
}
|
||||
}*/
|
||||
}
|
||||
|
||||
}
|
||||
@@ -31,7 +31,6 @@ import org.kodein.log.LoggerFactory
|
||||
import org.kodein.log.newLogger
|
||||
import xyz.quaver.floatingsearchview.suggestions.model.SearchSuggestion
|
||||
import xyz.quaver.pupil.sources.*
|
||||
import xyz.quaver.pupil.sources.hitomi.Hitomi
|
||||
import xyz.quaver.pupil.util.Preferences
|
||||
import xyz.quaver.pupil.util.source
|
||||
import kotlin.math.ceil
|
||||
@@ -59,7 +58,7 @@ class MainViewModel(app: Application) : AndroidViewModel(app), DIAware {
|
||||
direct.source(it)
|
||||
}
|
||||
private var sourceFactory: (String) -> Source = defaultSourceFactory
|
||||
var source by mutableStateOf(sourceFactory("hitomi.la"))
|
||||
var source by mutableStateOf(sourceFactory("hiyobi.io"))
|
||||
private set
|
||||
|
||||
var sortModeIndex by mutableStateOf(0)
|
||||
@@ -125,13 +124,12 @@ class MainViewModel(app: Application) : AndroidViewModel(app), DIAware {
|
||||
sortModeIndex
|
||||
)
|
||||
|
||||
logger.info { count.toString() }
|
||||
|
||||
totalItems.postValue(count)
|
||||
|
||||
for (result in channel) {
|
||||
yield()
|
||||
searchResults.add(result)
|
||||
logger.info { result.toString() }
|
||||
}
|
||||
|
||||
loading = false
|
||||
|
||||
@@ -22,10 +22,7 @@ package xyz.quaver.pupil.ui.viewmodel
|
||||
import android.app.Application
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.lifecycle.*
|
||||
import io.ktor.client.request.*
|
||||
import io.ktor.http.*
|
||||
@@ -36,11 +33,13 @@ import org.kodein.di.DIAware
|
||||
import org.kodein.di.android.x.closestDI
|
||||
import org.kodein.di.direct
|
||||
import org.kodein.di.instance
|
||||
import org.kodein.di.instanceOrNull
|
||||
import org.kodein.log.LoggerFactory
|
||||
import org.kodein.log.newLogger
|
||||
import xyz.quaver.pupil.db.AppDatabase
|
||||
import xyz.quaver.pupil.db.Bookmark
|
||||
import xyz.quaver.pupil.db.History
|
||||
import xyz.quaver.pupil.sources.ItemInfo
|
||||
import xyz.quaver.pupil.sources.Source
|
||||
import xyz.quaver.pupil.util.NetworkCache
|
||||
import xyz.quaver.pupil.util.source
|
||||
@@ -61,14 +60,12 @@ class ReaderViewModel(app: Application) : AndroidViewModel(app), DIAware {
|
||||
private val historyDao = database.historyDao()
|
||||
private val bookmarkDao = database.bookmarkDao()
|
||||
|
||||
private val _source = MutableLiveData<String>()
|
||||
val source = _source as LiveData<String>
|
||||
|
||||
private val _itemID = MutableLiveData<String>()
|
||||
val itemID = _itemID as LiveData<String>
|
||||
|
||||
private val _title = MutableLiveData<String>()
|
||||
val title = _title as LiveData<String>
|
||||
var source by mutableStateOf<Source?>(null)
|
||||
private set
|
||||
var itemID by mutableStateOf<String?>(null)
|
||||
private set
|
||||
var title by mutableStateOf<String?>(null)
|
||||
private set
|
||||
|
||||
private val totalProgressMutex = Mutex()
|
||||
var totalProgress by mutableStateOf(0)
|
||||
@@ -80,19 +77,8 @@ class ReaderViewModel(app: Application) : AndroidViewModel(app), DIAware {
|
||||
val imageList = mutableStateListOf<Uri?>()
|
||||
val progressList = mutableStateListOf<Float>()
|
||||
|
||||
val isBookmarked = Transformations.switchMap(MediatorLiveData<Pair<Source, String>>().apply {
|
||||
addSource(source) { source -> itemID.value?.let { itemID -> source to itemID } }
|
||||
addSource(itemID) { itemID -> source.value?.let { source -> source to itemID } }
|
||||
}) { (source, itemID) ->
|
||||
bookmarkDao.contains(source.name, itemID)
|
||||
}
|
||||
|
||||
val sourceInstance = Transformations.map(source) {
|
||||
direct.source(it)
|
||||
}
|
||||
|
||||
val sourceIcon = Transformations.map(sourceInstance) {
|
||||
it.iconResID
|
||||
val sourceIcon by derivedStateOf {
|
||||
source?.iconResID
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -105,8 +91,8 @@ class ReaderViewModel(app: Application) : AndroidViewModel(app), DIAware {
|
||||
val uri = intent.data
|
||||
val lastPathSegment = uri?.lastPathSegment
|
||||
if (uri != null && lastPathSegment != null) {
|
||||
_source.value = uri.host ?: error("Source cannot be null")
|
||||
_itemID.value = when (uri.host) {
|
||||
source = uri.host?.let { direct.source(it) } ?: error("Invalid host")
|
||||
itemID = when (uri.host) {
|
||||
"hitomi.la" ->
|
||||
Regex("([0-9]+).html").find(lastPathSegment)?.groupValues?.get(1) ?: error("Invalid itemID")
|
||||
"hiyobi.me" -> lastPathSegment
|
||||
@@ -115,15 +101,16 @@ class ReaderViewModel(app: Application) : AndroidViewModel(app), DIAware {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
_source.value = intent.getStringExtra("source") ?: error("Invalid source")
|
||||
_itemID.value = intent.getStringExtra("id") ?: error("Invalid itemID")
|
||||
source = intent.getStringExtra("source")?.let { direct.source(it) } ?: error("Invalid source")
|
||||
itemID = intent.getStringExtra("id") ?: error("Invalid itemID")
|
||||
title = intent.getParcelableExtra<ItemInfo>("payload")?.title
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun load() {
|
||||
val source: Source by source(source.value ?: return)
|
||||
val itemID = itemID.value ?: return
|
||||
val source = source ?: return
|
||||
val itemID = itemID ?: return
|
||||
|
||||
viewModelScope.launch {
|
||||
launch(Dispatchers.IO) {
|
||||
@@ -132,9 +119,10 @@ class ReaderViewModel(app: Application) : AndroidViewModel(app), DIAware {
|
||||
}
|
||||
|
||||
viewModelScope.launch {
|
||||
_title.value = withContext(Dispatchers.IO) {
|
||||
source.info(itemID)
|
||||
}.title
|
||||
if (title == null)
|
||||
title = withContext(Dispatchers.IO) {
|
||||
source.info(itemID)
|
||||
}.title
|
||||
}
|
||||
|
||||
viewModelScope.launch {
|
||||
@@ -152,6 +140,9 @@ class ReaderViewModel(app: Application) : AndroidViewModel(app), DIAware {
|
||||
}
|
||||
|
||||
images.forEachIndexed { index, image ->
|
||||
logger.info {
|
||||
progressList.toList().toString()
|
||||
}
|
||||
when (val scheme = image.takeWhile { it != ':' }) {
|
||||
"http", "https" -> {
|
||||
val (channel, file) = cache.load {
|
||||
@@ -203,7 +194,7 @@ class ReaderViewModel(app: Application) : AndroidViewModel(app), DIAware {
|
||||
}
|
||||
|
||||
fun toggleBookmark() {
|
||||
val bookmark = source.value?.let { source -> itemID.value?.let { itemID -> Bookmark(source, itemID) } } ?: return
|
||||
val bookmark = source?.let { source -> itemID?.let { itemID -> Bookmark(source.name, itemID) } } ?: return
|
||||
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
if (bookmarkDao.contains(bookmark).value ?: return@launch)
|
||||
|
||||
@@ -31,7 +31,6 @@ import org.kodein.di.DIAware
|
||||
import org.kodein.di.android.closestDI
|
||||
import xyz.quaver.io.FileX
|
||||
import xyz.quaver.io.util.*
|
||||
import xyz.quaver.pupil.sources.ItemInfo
|
||||
import xyz.quaver.pupil.sources.Source
|
||||
|
||||
class DownloadManager constructor(context: Context) : ContextWrapper(context), DIAware {
|
||||
|
||||
@@ -19,24 +19,16 @@
|
||||
package xyz.quaver.pupil.util
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.res.Resources
|
||||
import android.graphics.BitmapFactory
|
||||
import android.graphics.BitmapRegionDecoder
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.composed
|
||||
import androidx.compose.ui.focus.onFocusEvent
|
||||
import androidx.compose.ui.geometry.Rect
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.graphics.toAndroidRect
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import com.google.accompanist.insets.LocalWindowInsets
|
||||
import kotlinx.serialization.json.*
|
||||
import org.kodein.di.DIAware
|
||||
import org.kodein.di.DirectDIAware
|
||||
@@ -49,7 +41,6 @@ import xyz.quaver.io.util.inputStream
|
||||
import xyz.quaver.pupil.db.AppDatabase
|
||||
import xyz.quaver.pupil.sources.ItemInfo
|
||||
import xyz.quaver.pupil.sources.SourceEntries
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.util.*
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!--
|
||||
~ Pupil, Hitomi.la viewer for Android
|
||||
~ Copyright (C) 2019 tom5079
|
||||
~
|
||||
~ This program is free software: you can redistribute it and/or modify
|
||||
~ it under the terms of the GNU General Public License as published by
|
||||
~ the Free Software Foundation, either version 3 of the License, or
|
||||
~ (at your option) any later version.
|
||||
~
|
||||
~ This program is distributed in the hope that it will be useful,
|
||||
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
~ GNU General Public License for more details.
|
||||
~
|
||||
~ You should have received a copy of the GNU General Public License
|
||||
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<xyz.quaver.pupil.ui.view.ProgressCardView
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:clipChildren="true"
|
||||
app:cardCornerRadius="4dp"
|
||||
android:layout_marginLeft="8dp"
|
||||
android:layout_marginRight="8dp"
|
||||
app:cardUseCompatPadding="true"
|
||||
tools:ignore="RtlHardcoded">
|
||||
|
||||
<androidx.compose.ui.platform.ComposeView
|
||||
android:id="@+id/compose_view"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</xyz.quaver.pupil.ui.view.ProgressCardView>
|
||||
@@ -33,6 +33,7 @@ import kotlinx.serialization.json.Json
|
||||
import org.junit.Test
|
||||
import xyz.quaver.hitomi.getGalleryInfo
|
||||
import xyz.quaver.hitomi.imageUrlFromImage
|
||||
import xyz.quaver.pupil.sources.Hiyobi_io
|
||||
import java.lang.reflect.ParameterizedType
|
||||
import kotlin.reflect.KClass
|
||||
import kotlin.reflect.KType
|
||||
@@ -50,4 +51,9 @@ class ExampleUnitTest {
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun test2() {
|
||||
print(Hiyobi_io.parseQuery("female:loli female:big_breast tag:group"))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user