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

View File

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

View File

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

View File

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

View File

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

View File

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

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 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("")

View File

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

View File

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

View File

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

View File

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

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.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")
}

View File

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

View File

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

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

View File

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

View File

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

View File

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

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