Added hiyobi.io

This commit is contained in:
tom5079
2021-12-16 22:51:17 +09:00
parent 077d9b976c
commit 052990c4ef
25 changed files with 533 additions and 709 deletions

1
.idea/misc.xml generated
View File

@@ -41,6 +41,7 @@
<entry key="app/src/main/res/layout/reader_item.xml" value="0.1" /> <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/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/layout/source_select_dialog_item.xml" value="0.5119791666666667" />
<entry key="app/src/main/res/xml/hitomi_preferences.xml" value="0.5119791666666667" />
</map> </map>
</option> </option>
</component> </component>

View File

@@ -58,6 +58,14 @@ android {
jvmTarget = "1.8" jvmTarget = "1.8"
freeCompilerArgs += "-Xopt-in=kotlin.RequiresOptIn" freeCompilerArgs += "-Xopt-in=kotlin.RequiresOptIn"
} }
packagingOptions {
resources.excludes.addAll(
listOf(
"META-INF/AL2.0",
"META-INF/LGPL2.1"
)
)
}
} }
dependencies { dependencies {

View File

@@ -23,6 +23,15 @@ package xyz.quaver.pupil
import android.util.Log import android.util.Log
import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry 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.Test
import org.junit.runner.RunWith import org.junit.runner.RunWith

View File

@@ -28,7 +28,6 @@ import org.kodein.di.*
import xyz.quaver.floatingsearchview.databinding.SearchSuggestionItemBinding import xyz.quaver.floatingsearchview.databinding.SearchSuggestionItemBinding
import xyz.quaver.floatingsearchview.suggestions.model.SearchSuggestion import xyz.quaver.floatingsearchview.suggestions.model.SearchSuggestion
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.sources.hitomi.Hitomi
interface ItemInfo : Parcelable { interface ItemInfo : Parcelable {
val source: String val source: String
@@ -39,7 +38,7 @@ interface ItemInfo : Parcelable {
@Parcelize @Parcelize
class DefaultSearchSuggestion(override val body: String) : SearchSuggestion 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 { enum class Type {
OPEN_READER, OPEN_READER,
OPEN_DETAILS, OPEN_DETAILS,
@@ -75,7 +74,8 @@ val sourceModule = DI.Module(name = "source") {
bindSet<SourceEntry>() bindSet<SourceEntry>()
listOf<(Application) -> (Source)>( listOf<(Application) -> (Source)>(
{ Hitomi(it) } { Hitomi(it) },
{ Hiyobi_io(it) }
).forEach { source -> ).forEach { source ->
inSet { singleton { source.invoke(instance()).let { it.name to it } } } inSet { singleton { source.invoke(instance()).let { it.name to it } } }
} }

View File

@@ -26,11 +26,8 @@ import kotlinx.coroutines.launch
import org.kodein.di.DI import org.kodein.di.DI
import org.kodein.di.DIAware import org.kodein.di.DIAware
import org.kodein.di.direct import org.kodein.di.direct
import org.kodein.di.instance
import xyz.quaver.floatingsearchview.suggestions.model.SearchSuggestion import xyz.quaver.floatingsearchview.suggestions.model.SearchSuggestion
import xyz.quaver.pupil.db.AppDatabase
import xyz.quaver.pupil.util.database import xyz.quaver.pupil.util.database
import xyz.quaver.pupil.util.source
class History(override val di: DI) : Source(), DIAware { class History(override val di: DI) : Source(), DIAware {

View File

@@ -16,7 +16,7 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * 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.app.Application
import android.view.LayoutInflater import android.view.LayoutInflater
@@ -40,12 +40,9 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.geometry.Size import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.LiveData
import androidx.room.*
import coil.annotation.ExperimentalCoilApi import coil.annotation.ExperimentalCoilApi
import coil.compose.rememberImagePainter import coil.compose.rememberImagePainter
import com.google.accompanist.flowlayout.FlowRow import com.google.accompanist.flowlayout.FlowRow
@@ -57,7 +54,6 @@ import kotlinx.coroutines.sync.withLock
import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import org.kodein.di.DI
import org.kodein.di.DIAware import org.kodein.di.DIAware
import org.kodein.di.android.closestDI import org.kodein.di.android.closestDI
import org.kodein.di.instance import org.kodein.di.instance
@@ -69,9 +65,9 @@ import xyz.quaver.hitomi.*
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.db.AppDatabase import xyz.quaver.pupil.db.AppDatabase
import xyz.quaver.pupil.db.Bookmark import xyz.quaver.pupil.db.Bookmark
import xyz.quaver.pupil.sources.ItemInfo import xyz.quaver.pupil.ui.theme.Blue700
import xyz.quaver.pupil.sources.SearchResultEvent import xyz.quaver.pupil.ui.theme.Orange500
import xyz.quaver.pupil.sources.Source import xyz.quaver.pupil.ui.theme.Pink600
import xyz.quaver.pupil.util.Preferences import xyz.quaver.pupil.util.Preferences
import xyz.quaver.pupil.util.wordCapitalize import xyz.quaver.pupil.util.wordCapitalize
import kotlin.math.max import kotlin.math.max
@@ -152,7 +148,7 @@ class Hitomi(app: Application) : Source(), DIAware {
var cachedSortMode: Int = -1 var cachedSortMode: Int = -1
private val cache = mutableListOf<Int>() 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()) { if (cachedQuery != query || cachedSortMode != sortMode || cache.isEmpty()) {
cachedQuery = null cachedQuery = null
cache.clear() cache.clear()
@@ -179,8 +175,8 @@ class Hitomi(app: Application) : Source(), DIAware {
channel.close() channel.close()
} }
Pair(channel, cache.size) channel to cache.size
} } }
override suspend fun suggestion(query: String) : List<TagSuggestion> { override suspend fun suggestion(query: String) : List<TagSuggestion> {
return getSuggestionsForQuery(query.takeLastWhile { !it.isWhitespace() }).map { return getSuggestionsForQuery(query.takeLastWhile { !it.isWhitespace() }).map {
@@ -336,10 +332,10 @@ class Hitomi(app: Application) : Source(), DIAware {
} }
val (surfaceColor, textTint) = when { val (surfaceColor, textTint) = when {
isFavorite -> Pair(colorResource(id = R.color.material_orange_500), Color.White) isFavorite -> Pair(Orange500, Color.White)
else -> when (tagParts[0]) { else -> when (tagParts[0]) {
"male" -> Pair(colorResource(id = R.color.material_blue_700), Color.White) "male" -> Pair(Blue700, Color.White)
"female" -> Pair(colorResource(id = R.color.material_pink_600), Color.White) "female" -> Pair(Pink600, Color.White)
else -> Pair(MaterialTheme.colors.background, MaterialTheme.colors.onBackground) else -> Pair(MaterialTheme.colors.background, MaterialTheme.colors.onBackground)
} }
} }
@@ -394,7 +390,7 @@ class Hitomi(app: Application) : Source(), DIAware {
var isFolded by remember { mutableStateOf(true) } var isFolded by remember { mutableStateOf(true) }
val bookmarkedTags by bookmarkDao.getAll(name).observeAsState(emptyList()) 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)) { 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 -> 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) val painter = rememberImagePainter(itemInfo.thumbnail)
Column { Column(
modifier = Modifier.clickable { onEvent(SearchResultEvent(SearchResultEvent.Type.OPEN_READER, itemInfo.itemID, itemInfo)) }
) {
Row { Row {
Image( Image(
painter = painter, painter = painter,

View 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()
}
}
}

View File

@@ -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))
}
}

View File

@@ -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()
}
}
}
}
}
}

View File

@@ -53,7 +53,6 @@ import com.google.accompanist.drawablepainter.rememberDrawablePainter
import kotlinx.coroutines.* import kotlinx.coroutines.*
import org.kodein.di.DIAware import org.kodein.di.DIAware
import org.kodein.di.android.closestDI import org.kodein.di.android.closestDI
import org.kodein.di.compose.withDI
import org.kodein.log.LoggerFactory import org.kodein.log.LoggerFactory
import org.kodein.log.newLogger import org.kodein.log.newLogger
import xyz.quaver.pupil.* 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.MultipleFloatingActionButton
import xyz.quaver.pupil.ui.composable.SubFabItem import xyz.quaver.pupil.ui.composable.SubFabItem
import xyz.quaver.pupil.ui.dialog.SourceSelectDialog 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.theme.PupilTheme
import xyz.quaver.pupil.ui.view.ProgressCardView import xyz.quaver.pupil.ui.view.ProgressCardView
import xyz.quaver.pupil.ui.viewmodel.MainViewModel import xyz.quaver.pupil.ui.viewmodel.MainViewModel
@@ -117,8 +115,12 @@ class MainActivity : ComponentActivity(), DIAware {
} }
if (openSourceSelectDialog) if (openSourceSelectDialog)
SourceSelectDialog { SourceSelectDialog(
currentSource = model.source.name,
onDismissRequest = { openSourceSelectDialog = false }
) { source ->
openSourceSelectDialog = false openSourceSelectDialog = false
model.setSourceAndReset(source.name)
} }
Scaffold( Scaffold(
@@ -185,7 +187,8 @@ class MainActivity : ComponentActivity(), DIAware {
ReaderActivity::class.java ReaderActivity::class.java
).apply { ).apply {
putExtra("source", model.source.name) putExtra("source", model.source.name)
putExtra("id", itemInfo.itemID) putExtra("id", event.itemID)
putExtra("payload", event.payload)
}) })
} }
else -> TODO("") else -> TODO("")

View File

@@ -81,8 +81,6 @@ class ReaderActivity : ComponentActivity(), DIAware {
setContent { setContent {
var isFABExpanded by remember { mutableStateOf(FloatingActionButtonState.COLLAPSED) } var isFABExpanded by remember { mutableStateOf(FloatingActionButtonState.COLLAPSED) }
val isFullscreen by model.isFullscreen.observeAsState(false) 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 imageSources = remember { mutableStateListOf<ImageSource?>() }
val imageHeights = remember { mutableStateListOf<Float?>() } val imageHeights = remember { mutableStateListOf<Float?>() }
val states = remember { mutableStateListOf<SubSampledImageState>() } val states = remember { mutableStateListOf<SubSampledImageState>() }
@@ -131,12 +129,12 @@ class ReaderActivity : ComponentActivity(), DIAware {
TopAppBar( TopAppBar(
title = { title = {
Text( Text(
title, model.title ?: stringResource(R.string.reader_loading),
color = MaterialTheme.colors.onSecondary color = MaterialTheme.colors.onSecondary
) )
}, },
actions = { actions = {
sourceIcon?.let { sourceIcon -> model.sourceIcon?.let { sourceIcon ->
Image( Image(
modifier = Modifier.size(36.dp), modifier = Modifier.size(36.dp),
painter = painterResource(id = sourceIcon), painter = painterResource(id = sourceIcon),

View File

@@ -20,11 +20,12 @@ package xyz.quaver.pupil.ui
import android.os.Bundle import android.os.Bundle
import android.view.MenuItem import android.view.MenuItem
import androidx.appcompat.app.AppCompatActivity
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.ui.fragment.SettingsFragment import xyz.quaver.pupil.ui.fragment.SettingsFragment
import xyz.quaver.pupil.ui.fragment.SourceSettingsFragment import xyz.quaver.pupil.ui.fragment.SourceSettingsFragment
class SettingsActivity : BaseActivity() { class SettingsActivity : AppCompatActivity() {
companion object { companion object {
const val SETTINGS_EXTRA = "xyz.quaver.pupil.ui.SettingsActivity.SETTINGS_EXTRA" const val SETTINGS_EXTRA = "xyz.quaver.pupil.ui.SettingsActivity.SETTINGS_EXTRA"

View File

@@ -27,7 +27,7 @@ import androidx.appcompat.app.AlertDialog
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.databinding.DefaultQueryDialogBinding 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.types.Tags
import xyz.quaver.pupil.util.Preferences import xyz.quaver.pupil.util.Preferences

View File

@@ -19,6 +19,7 @@
package xyz.quaver.pupil.ui.dialog package xyz.quaver.pupil.ui.dialog
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.* import androidx.compose.material.*
@@ -36,7 +37,7 @@ import xyz.quaver.pupil.sources.Source
import xyz.quaver.pupil.sources.SourceEntries import xyz.quaver.pupil.sources.SourceEntries
@Composable @Composable
fun SourceSelectDialogItem(source: Source) { fun SourceSelectDialogItem(source: Source, isSelected: Boolean, onSelected: (Source) -> Unit = { }) {
Row( Row(
modifier = Modifier.padding(16.dp), modifier = Modifier.padding(16.dp),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
@@ -59,16 +60,20 @@ fun SourceSelectDialogItem(source: Source) {
tint = MaterialTheme.colors.onSurface.copy(alpha = 0.5f) tint = MaterialTheme.colors.onSurface.copy(alpha = 0.5f)
) )
Button(onClick = { /*TODO*/ }) { Button(
enabled = !isSelected,
onClick = {
onSelected(source)
}
) {
Text("GO") Text("GO")
} }
} }
} }
@Preview
@Composable @Composable
fun SourceSelectDialog(onDismissRequest: () -> Unit = { }) { fun SourceSelectDialog(currentSource: String, onDismissRequest: () -> Unit = { }, onSelected: (Source) -> Unit = { }) {
val sourceEntries: SourceEntries by rememberInstance() val sourceEntries: SourceEntries by rememberInstance()
Dialog(onDismissRequest = onDismissRequest) { Dialog(onDismissRequest = onDismissRequest) {
@@ -77,7 +82,7 @@ fun SourceSelectDialog(onDismissRequest: () -> Unit = { }) {
shape = RoundedCornerShape(12.dp) shape = RoundedCornerShape(12.dp)
) { ) {
Column() { Column() {
sourceEntries.forEach { SourceSelectDialogItem(it.second) } sourceEntries.forEach { SourceSelectDialogItem(it.second, it.first == currentSource, onSelected) }
} }
} }
} }

View File

@@ -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
}
}
}
}

View File

@@ -32,7 +32,6 @@ import org.kodein.di.instance
import xyz.quaver.io.FileX import xyz.quaver.io.FileX
import xyz.quaver.io.util.getChild import xyz.quaver.io.util.getChild
import xyz.quaver.pupil.R import xyz.quaver.pupil.R
import xyz.quaver.pupil.ui.LockActivity
import xyz.quaver.pupil.ui.SettingsActivity import xyz.quaver.pupil.ui.SettingsActivity
import xyz.quaver.pupil.ui.dialog.* import xyz.quaver.pupil.ui.dialog.*
import xyz.quaver.pupil.util.* import xyz.quaver.pupil.util.*
@@ -49,16 +48,6 @@ class SettingsFragment :
private val downloadManager: DownloadManager by instance() 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() { override fun onResume() {
super.onResume() super.onResume()
@@ -88,12 +77,6 @@ class SettingsFragment :
"download_folder" -> { "download_folder" -> {
DownloadLocationDialogFragment().show(parentFragmentManager, "Download Location Dialog") DownloadLocationDialogFragment().show(parentFragmentManager, "Download Location Dialog")
} }
"app_lock" -> {
val intent = Intent(requireContext(), LockActivity::class.java).apply {
putExtra("force", true)
}
lockLauncher.launch(intent)
}
"proxy" -> { "proxy" -> {
ProxyDialogFragment().show(parentFragmentManager, "Proxy Dialog") ProxyDialogFragment().show(parentFragmentManager, "Proxy Dialog")
} }

View File

@@ -33,7 +33,7 @@ import xyz.quaver.floatingsearchview.databinding.SearchSuggestionItemBinding
import xyz.quaver.floatingsearchview.suggestions.model.SearchSuggestion import xyz.quaver.floatingsearchview.suggestions.model.SearchSuggestion
import xyz.quaver.floatingsearchview.util.view.SearchInputView import xyz.quaver.floatingsearchview.util.view.SearchInputView
import xyz.quaver.pupil.R 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.FavoriteHistorySwitch
import xyz.quaver.pupil.types.HistorySuggestion import xyz.quaver.pupil.types.HistorySuggestion
import xyz.quaver.pupil.types.LoadingSuggestion import xyz.quaver.pupil.types.LoadingSuggestion

View File

@@ -1,27 +1,12 @@
package xyz.quaver.pupil.ui.view 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.ExperimentalFoundationApi
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.* import androidx.compose.material.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp 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) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable

View File

@@ -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()
}
}*/
}
}

View File

@@ -31,7 +31,6 @@ import org.kodein.log.LoggerFactory
import org.kodein.log.newLogger import org.kodein.log.newLogger
import xyz.quaver.floatingsearchview.suggestions.model.SearchSuggestion import xyz.quaver.floatingsearchview.suggestions.model.SearchSuggestion
import xyz.quaver.pupil.sources.* import xyz.quaver.pupil.sources.*
import xyz.quaver.pupil.sources.hitomi.Hitomi
import xyz.quaver.pupil.util.Preferences import xyz.quaver.pupil.util.Preferences
import xyz.quaver.pupil.util.source import xyz.quaver.pupil.util.source
import kotlin.math.ceil import kotlin.math.ceil
@@ -59,7 +58,7 @@ class MainViewModel(app: Application) : AndroidViewModel(app), DIAware {
direct.source(it) direct.source(it)
} }
private var sourceFactory: (String) -> Source = defaultSourceFactory private var sourceFactory: (String) -> Source = defaultSourceFactory
var source by mutableStateOf(sourceFactory("hitomi.la")) var source by mutableStateOf(sourceFactory("hiyobi.io"))
private set private set
var sortModeIndex by mutableStateOf(0) var sortModeIndex by mutableStateOf(0)
@@ -125,13 +124,12 @@ class MainViewModel(app: Application) : AndroidViewModel(app), DIAware {
sortModeIndex sortModeIndex
) )
logger.info { count.toString() }
totalItems.postValue(count) totalItems.postValue(count)
for (result in channel) { for (result in channel) {
yield() yield()
searchResults.add(result) searchResults.add(result)
logger.info { result.toString() }
} }
loading = false loading = false

View File

@@ -22,10 +22,7 @@ package xyz.quaver.pupil.ui.viewmodel
import android.app.Application import android.app.Application
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import androidx.compose.runtime.getValue import androidx.compose.runtime.*
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.* import androidx.lifecycle.*
import io.ktor.client.request.* import io.ktor.client.request.*
import io.ktor.http.* import io.ktor.http.*
@@ -36,11 +33,13 @@ import org.kodein.di.DIAware
import org.kodein.di.android.x.closestDI import org.kodein.di.android.x.closestDI
import org.kodein.di.direct import org.kodein.di.direct
import org.kodein.di.instance import org.kodein.di.instance
import org.kodein.di.instanceOrNull
import org.kodein.log.LoggerFactory import org.kodein.log.LoggerFactory
import org.kodein.log.newLogger import org.kodein.log.newLogger
import xyz.quaver.pupil.db.AppDatabase import xyz.quaver.pupil.db.AppDatabase
import xyz.quaver.pupil.db.Bookmark import xyz.quaver.pupil.db.Bookmark
import xyz.quaver.pupil.db.History import xyz.quaver.pupil.db.History
import xyz.quaver.pupil.sources.ItemInfo
import xyz.quaver.pupil.sources.Source import xyz.quaver.pupil.sources.Source
import xyz.quaver.pupil.util.NetworkCache import xyz.quaver.pupil.util.NetworkCache
import xyz.quaver.pupil.util.source import xyz.quaver.pupil.util.source
@@ -61,14 +60,12 @@ class ReaderViewModel(app: Application) : AndroidViewModel(app), DIAware {
private val historyDao = database.historyDao() private val historyDao = database.historyDao()
private val bookmarkDao = database.bookmarkDao() private val bookmarkDao = database.bookmarkDao()
private val _source = MutableLiveData<String>() var source by mutableStateOf<Source?>(null)
val source = _source as LiveData<String> private set
var itemID by mutableStateOf<String?>(null)
private val _itemID = MutableLiveData<String>() private set
val itemID = _itemID as LiveData<String> var title by mutableStateOf<String?>(null)
private set
private val _title = MutableLiveData<String>()
val title = _title as LiveData<String>
private val totalProgressMutex = Mutex() private val totalProgressMutex = Mutex()
var totalProgress by mutableStateOf(0) var totalProgress by mutableStateOf(0)
@@ -80,19 +77,8 @@ class ReaderViewModel(app: Application) : AndroidViewModel(app), DIAware {
val imageList = mutableStateListOf<Uri?>() val imageList = mutableStateListOf<Uri?>()
val progressList = mutableStateListOf<Float>() val progressList = mutableStateListOf<Float>()
val isBookmarked = Transformations.switchMap(MediatorLiveData<Pair<Source, String>>().apply { val sourceIcon by derivedStateOf {
addSource(source) { source -> itemID.value?.let { itemID -> source to itemID } } source?.iconResID
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
} }
/** /**
@@ -105,8 +91,8 @@ class ReaderViewModel(app: Application) : AndroidViewModel(app), DIAware {
val uri = intent.data val uri = intent.data
val lastPathSegment = uri?.lastPathSegment val lastPathSegment = uri?.lastPathSegment
if (uri != null && lastPathSegment != null) { if (uri != null && lastPathSegment != null) {
_source.value = uri.host ?: error("Source cannot be null") source = uri.host?.let { direct.source(it) } ?: error("Invalid host")
_itemID.value = when (uri.host) { itemID = when (uri.host) {
"hitomi.la" -> "hitomi.la" ->
Regex("([0-9]+).html").find(lastPathSegment)?.groupValues?.get(1) ?: error("Invalid itemID") Regex("([0-9]+).html").find(lastPathSegment)?.groupValues?.get(1) ?: error("Invalid itemID")
"hiyobi.me" -> lastPathSegment "hiyobi.me" -> lastPathSegment
@@ -115,15 +101,16 @@ class ReaderViewModel(app: Application) : AndroidViewModel(app), DIAware {
} }
} }
} else { } else {
_source.value = intent.getStringExtra("source") ?: error("Invalid source") source = intent.getStringExtra("source")?.let { direct.source(it) } ?: error("Invalid source")
_itemID.value = intent.getStringExtra("id") ?: error("Invalid itemID") itemID = intent.getStringExtra("id") ?: error("Invalid itemID")
title = intent.getParcelableExtra<ItemInfo>("payload")?.title
} }
} }
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
fun load() { fun load() {
val source: Source by source(source.value ?: return) val source = source ?: return
val itemID = itemID.value ?: return val itemID = itemID ?: return
viewModelScope.launch { viewModelScope.launch {
launch(Dispatchers.IO) { launch(Dispatchers.IO) {
@@ -132,7 +119,8 @@ class ReaderViewModel(app: Application) : AndroidViewModel(app), DIAware {
} }
viewModelScope.launch { viewModelScope.launch {
_title.value = withContext(Dispatchers.IO) { if (title == null)
title = withContext(Dispatchers.IO) {
source.info(itemID) source.info(itemID)
}.title }.title
} }
@@ -152,6 +140,9 @@ class ReaderViewModel(app: Application) : AndroidViewModel(app), DIAware {
} }
images.forEachIndexed { index, image -> images.forEachIndexed { index, image ->
logger.info {
progressList.toList().toString()
}
when (val scheme = image.takeWhile { it != ':' }) { when (val scheme = image.takeWhile { it != ':' }) {
"http", "https" -> { "http", "https" -> {
val (channel, file) = cache.load { val (channel, file) = cache.load {
@@ -203,7 +194,7 @@ class ReaderViewModel(app: Application) : AndroidViewModel(app), DIAware {
} }
fun toggleBookmark() { 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 { CoroutineScope(Dispatchers.IO).launch {
if (bookmarkDao.contains(bookmark).value ?: return@launch) if (bookmarkDao.contains(bookmark).value ?: return@launch)

View File

@@ -31,7 +31,6 @@ import org.kodein.di.DIAware
import org.kodein.di.android.closestDI import org.kodein.di.android.closestDI
import xyz.quaver.io.FileX import xyz.quaver.io.FileX
import xyz.quaver.io.util.* import xyz.quaver.io.util.*
import xyz.quaver.pupil.sources.ItemInfo
import xyz.quaver.pupil.sources.Source import xyz.quaver.pupil.sources.Source
class DownloadManager constructor(context: Context) : ContextWrapper(context), DIAware { class DownloadManager constructor(context: Context) : ContextWrapper(context), DIAware {

View File

@@ -19,24 +19,16 @@
package xyz.quaver.pupil.util package xyz.quaver.pupil.util
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context
import android.content.res.Resources
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import android.graphics.BitmapRegionDecoder
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import androidx.compose.runtime.* 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.Rect
import androidx.compose.ui.geometry.Size import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.toAndroidRect import androidx.compose.ui.graphics.toAndroidRect
import androidx.compose.ui.platform.LocalFocusManager
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
import com.google.accompanist.insets.LocalWindowInsets
import kotlinx.serialization.json.* import kotlinx.serialization.json.*
import org.kodein.di.DIAware import org.kodein.di.DIAware
import org.kodein.di.DirectDIAware 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.db.AppDatabase
import xyz.quaver.pupil.sources.ItemInfo import xyz.quaver.pupil.sources.ItemInfo
import xyz.quaver.pupil.sources.SourceEntries import xyz.quaver.pupil.sources.SourceEntries
import java.io.IOException
import java.io.InputStream import java.io.InputStream
import java.io.OutputStream import java.io.OutputStream
import java.util.* import java.util.*

View File

@@ -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>

View File

@@ -33,6 +33,7 @@ import kotlinx.serialization.json.Json
import org.junit.Test import org.junit.Test
import xyz.quaver.hitomi.getGalleryInfo import xyz.quaver.hitomi.getGalleryInfo
import xyz.quaver.hitomi.imageUrlFromImage import xyz.quaver.hitomi.imageUrlFromImage
import xyz.quaver.pupil.sources.Hiyobi_io
import java.lang.reflect.ParameterizedType import java.lang.reflect.ParameterizedType
import kotlin.reflect.KClass import kotlin.reflect.KClass
import kotlin.reflect.KType import kotlin.reflect.KType
@@ -50,4 +51,9 @@ class ExampleUnitTest {
} }
} }
@Test
fun test2() {
print(Hiyobi_io.parseQuery("female:loli female:big_breast tag:group"))
}
} }