Added hiyobi.io
This commit is contained in:
1
.idea/misc.xml
generated
1
.idea/misc.xml
generated
@@ -41,6 +41,7 @@
|
|||||||
<entry key="app/src/main/res/layout/reader_item.xml" value="0.1" />
|
<entry key="app/src/main/res/layout/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>
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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 } } }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
441
app/src/main/java/xyz/quaver/pupil/sources/Hiyobi_io.kt
Normal file
441
app/src/main/java/xyz/quaver/pupil/sources/Hiyobi_io.kt
Normal file
@@ -0,0 +1,441 @@
|
|||||||
|
/*
|
||||||
|
* Pupil, Hitomi.la viewer for Android
|
||||||
|
* Copyright (C) 2021 tom5079
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* (at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package xyz.quaver.pupil.sources
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.os.Parcelable
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.*
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.*
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Female
|
||||||
|
import androidx.compose.material.icons.filled.Male
|
||||||
|
import androidx.compose.material.icons.filled.Star
|
||||||
|
import androidx.compose.material.icons.outlined.StarOutline
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.runtime.livedata.observeAsState
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.geometry.Size
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import coil.annotation.ExperimentalCoilApi
|
||||||
|
import coil.compose.rememberImagePainter
|
||||||
|
import com.google.accompanist.flowlayout.FlowRow
|
||||||
|
import io.ktor.client.*
|
||||||
|
import io.ktor.client.request.*
|
||||||
|
import io.ktor.http.*
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.coroutines.channels.Channel
|
||||||
|
import kotlinx.parcelize.Parcelize
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.json.JsonObject
|
||||||
|
import kotlinx.serialization.json.int
|
||||||
|
import kotlinx.serialization.json.jsonArray
|
||||||
|
import kotlinx.serialization.json.jsonPrimitive
|
||||||
|
import org.kodein.di.DIAware
|
||||||
|
import org.kodein.di.android.closestDI
|
||||||
|
import org.kodein.di.instance
|
||||||
|
import org.kodein.log.LoggerFactory
|
||||||
|
import org.kodein.log.newLogger
|
||||||
|
import xyz.quaver.floatingsearchview.suggestions.model.SearchSuggestion
|
||||||
|
import xyz.quaver.pupil.R
|
||||||
|
import xyz.quaver.pupil.db.AppDatabase
|
||||||
|
import xyz.quaver.pupil.db.Bookmark
|
||||||
|
import xyz.quaver.pupil.ui.theme.Blue700
|
||||||
|
import xyz.quaver.pupil.ui.theme.Orange500
|
||||||
|
import xyz.quaver.pupil.ui.theme.Pink600
|
||||||
|
import xyz.quaver.pupil.util.content
|
||||||
|
import xyz.quaver.pupil.util.get
|
||||||
|
import xyz.quaver.pupil.util.wordCapitalize
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
@Parcelize
|
||||||
|
data class Tag(
|
||||||
|
val male: Int?,
|
||||||
|
val female: Int?,
|
||||||
|
val tag: String
|
||||||
|
) : Parcelable {
|
||||||
|
override fun toString(): String {
|
||||||
|
val stringBuilder = StringBuilder()
|
||||||
|
|
||||||
|
stringBuilder.append(when {
|
||||||
|
male != null -> "male"
|
||||||
|
female != null -> "female"
|
||||||
|
else -> "tag"
|
||||||
|
})
|
||||||
|
stringBuilder.append(':')
|
||||||
|
stringBuilder.append(tag)
|
||||||
|
|
||||||
|
return stringBuilder.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
@Parcelize
|
||||||
|
data class HiyobiItemInfo(
|
||||||
|
override val itemID: String,
|
||||||
|
override val title: String,
|
||||||
|
val thumbnail: String,
|
||||||
|
val artists: List<String>,
|
||||||
|
val series: List<String>,
|
||||||
|
val type: String,
|
||||||
|
val date: String,
|
||||||
|
val bookmark: Unit?,
|
||||||
|
val tags: List<Tag>,
|
||||||
|
val commentCount: Int,
|
||||||
|
val pageCount: Int
|
||||||
|
): ItemInfo {
|
||||||
|
override val source: String
|
||||||
|
get() = "hiyobi.io"
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Manga(
|
||||||
|
val mangaId: Int,
|
||||||
|
val title: String,
|
||||||
|
val artist: List<String>,
|
||||||
|
val thumbnail: String,
|
||||||
|
val series: List<String>,
|
||||||
|
val type: String,
|
||||||
|
val date: String,
|
||||||
|
val bookmark: Unit?,
|
||||||
|
val tags: List<Tag>,
|
||||||
|
val commentCount: Int,
|
||||||
|
val pageCount: Int
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class QueryManga(
|
||||||
|
val nowPage: Int,
|
||||||
|
val maxPage: Int,
|
||||||
|
val manga: List<Manga>
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class SearchResultData(
|
||||||
|
val queryManga: QueryManga
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class SearchResult(
|
||||||
|
val data: SearchResultData
|
||||||
|
)
|
||||||
|
|
||||||
|
class Hiyobi_io(app: Application): Source(), DIAware {
|
||||||
|
override val di by closestDI(app)
|
||||||
|
|
||||||
|
private val logger = newLogger(LoggerFactory.default)
|
||||||
|
|
||||||
|
private val database: AppDatabase by instance()
|
||||||
|
private val bookmarkDao = database.bookmarkDao()
|
||||||
|
|
||||||
|
override val name = "hiyobi.io"
|
||||||
|
override val iconResID = R.drawable.hitomi
|
||||||
|
override val preferenceID = 0
|
||||||
|
override val availableSortMode = emptyList<String>()
|
||||||
|
|
||||||
|
private val client: HttpClient by instance()
|
||||||
|
|
||||||
|
private suspend fun query(page: Int, tags: String): SearchResult {
|
||||||
|
val query = "{queryManga(page:$page,tags:$tags){nowPage maxPage manga{mangaId title artist thumbnail series type date bookmark tags{male female tag} commentCount pageCount}}}"
|
||||||
|
|
||||||
|
return client.get("https://api.hiyobi.io/api?query=$query")
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun totalCount(tags: String): Int {
|
||||||
|
val firstPageQuery = "{queryManga(page:1,tags:$tags){maxPage}}"
|
||||||
|
val maxPage = client.get<JsonObject>(
|
||||||
|
"https://api.hiyobi.io/api?query=$firstPageQuery"
|
||||||
|
)["data"]!!["queryManga"]!!["maxPage"]!!.jsonPrimitive.int
|
||||||
|
|
||||||
|
val lastPageQuery = "{queryManga(page:$maxPage,tags:$tags){manga{mangaId}}}"
|
||||||
|
val lastPageCount = client.get<JsonObject>(
|
||||||
|
"https://api.hiyobi.io/api?query=$lastPageQuery"
|
||||||
|
)["data"]!!["queryManga"]!!["manga"]!!.jsonArray.size
|
||||||
|
|
||||||
|
return (maxPage-1)*25+lastPageCount
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun search(
|
||||||
|
query: String,
|
||||||
|
range: IntRange,
|
||||||
|
sortMode: Int
|
||||||
|
): Pair<Channel<ItemInfo>, Int> = withContext(Dispatchers.IO) {
|
||||||
|
val channel = Channel<ItemInfo>()
|
||||||
|
|
||||||
|
val tags = parseQuery(query)
|
||||||
|
|
||||||
|
logger.info {
|
||||||
|
tags
|
||||||
|
}
|
||||||
|
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
(range.first/25+1 .. range.last/25+1).map { page ->
|
||||||
|
page to async { query(page, tags) }
|
||||||
|
}.forEach { (page, result) ->
|
||||||
|
result.await().data.queryManga.manga.forEachIndexed { index, manga ->
|
||||||
|
if ((page-1)*25+index in range) channel.send(transform(manga))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
channel.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
channel to totalCount(tags)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun suggestion(query: String): List<SearchSuggestion> {
|
||||||
|
return emptyList()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun images(itemID: String): List<String> = withContext(Dispatchers.IO) {
|
||||||
|
val query = "{getManga(mangaId:$itemID){urls}}"
|
||||||
|
|
||||||
|
client.post<JsonObject>("https://api.hiyobi.io/api") {
|
||||||
|
contentType(ContentType.Application.Json)
|
||||||
|
body = mapOf("query" to query)
|
||||||
|
}["data"]!!["getManga"]!!["urls"]!!.jsonArray.map { "https://api.hiyobi.io/${it.content!!}" }
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun info(itemID: String): ItemInfo {
|
||||||
|
TODO("Not yet implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterialApi::class)
|
||||||
|
@Composable
|
||||||
|
fun TagChip(tag: Tag, isFavorite: Boolean, onClick: ((Tag) -> Unit)? = null, onFavoriteClick: ((Tag) -> Unit)? = null) {
|
||||||
|
val icon = when {
|
||||||
|
tag.male != null -> Icons.Filled.Male
|
||||||
|
tag.female != null -> Icons.Filled.Female
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
|
||||||
|
val (surfaceColor, textTint) = when {
|
||||||
|
isFavorite -> Pair(Orange500, Color.White)
|
||||||
|
else -> when {
|
||||||
|
tag.male != null -> Pair(Blue700, Color.White)
|
||||||
|
tag.female != null -> Pair(Pink600, Color.White)
|
||||||
|
else -> Pair(MaterialTheme.colors.background, MaterialTheme.colors.onBackground)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val starIcon = if (isFavorite) Icons.Filled.Star else Icons.Outlined.StarOutline
|
||||||
|
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier.padding(2.dp),
|
||||||
|
onClick = { onClick?.invoke(tag) },
|
||||||
|
shape = RoundedCornerShape(16.dp),
|
||||||
|
color = surfaceColor,
|
||||||
|
elevation = 2.dp
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
if (icon != null)
|
||||||
|
Icon(
|
||||||
|
icon,
|
||||||
|
contentDescription = "Icon",
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(4.dp)
|
||||||
|
.size(24.dp),
|
||||||
|
tint = Color.White
|
||||||
|
)
|
||||||
|
else
|
||||||
|
Box(Modifier.size(16.dp))
|
||||||
|
|
||||||
|
Text(
|
||||||
|
tag.tag,
|
||||||
|
color = textTint,
|
||||||
|
style = MaterialTheme.typography.body2
|
||||||
|
)
|
||||||
|
|
||||||
|
Icon(
|
||||||
|
starIcon,
|
||||||
|
contentDescription = "Favorites",
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(8.dp)
|
||||||
|
.size(16.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.clickable { onFavoriteClick?.invoke(tag) },
|
||||||
|
tint = textTint
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterialApi::class)
|
||||||
|
@Composable
|
||||||
|
fun TagGroup(tags: List<Tag>) {
|
||||||
|
var isFolded by remember { mutableStateOf(true) }
|
||||||
|
val bookmarkedTags by bookmarkDao.getAll(name).observeAsState(emptyList())
|
||||||
|
|
||||||
|
val bookmarkedTagsInList = tags.filter { it.toString() in bookmarkedTags }
|
||||||
|
|
||||||
|
FlowRow(Modifier.padding(0.dp, 16.dp)) {
|
||||||
|
tags.sortedBy { if (bookmarkedTagsInList.contains(it)) 0 else 1 }.let { (if (isFolded) it.take(10) else it) }.forEach { tag ->
|
||||||
|
TagChip(
|
||||||
|
tag = tag,
|
||||||
|
isFavorite = bookmarkedTagsInList.contains(tag),
|
||||||
|
onFavoriteClick = {
|
||||||
|
val bookmarkTag = Bookmark(name, it.toString())
|
||||||
|
|
||||||
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
|
if (bookmarkedTagsInList.contains(it))
|
||||||
|
bookmarkDao.delete(bookmarkTag)
|
||||||
|
else
|
||||||
|
bookmarkDao.insert(bookmarkTag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isFolded && tags.size > 10)
|
||||||
|
Surface(
|
||||||
|
modifier = Modifier.padding(2.dp),
|
||||||
|
color = MaterialTheme.colors.background,
|
||||||
|
shape = RoundedCornerShape(16.dp),
|
||||||
|
elevation = 2.dp,
|
||||||
|
onClick = { isFolded = false }
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
"…",
|
||||||
|
modifier = Modifier.padding(16.dp, 8.dp),
|
||||||
|
color = MaterialTheme.colors.onBackground,
|
||||||
|
style = MaterialTheme.typography.body2
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoilApi::class)
|
||||||
|
@Composable
|
||||||
|
override fun SearchResult(itemInfo: ItemInfo, onEvent: (SearchResultEvent) -> Unit) {
|
||||||
|
itemInfo as HiyobiItemInfo
|
||||||
|
|
||||||
|
val painter = rememberImagePainter(itemInfo.thumbnail)
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.clickable {
|
||||||
|
onEvent(SearchResultEvent(SearchResultEvent.Type.OPEN_READER, itemInfo.itemID, itemInfo))
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Image(
|
||||||
|
painter = painter,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier
|
||||||
|
.requiredWidth(150.dp)
|
||||||
|
.aspectRatio(
|
||||||
|
with(painter.intrinsicSize) { if (this == Size.Unspecified) 1f else width / height },
|
||||||
|
true
|
||||||
|
)
|
||||||
|
.padding(0.dp, 0.dp, 8.dp, 0.dp)
|
||||||
|
.align(Alignment.CenterVertically),
|
||||||
|
contentScale = ContentScale.FillWidth
|
||||||
|
)
|
||||||
|
|
||||||
|
Column {
|
||||||
|
Text(
|
||||||
|
itemInfo.title,
|
||||||
|
style = MaterialTheme.typography.h6,
|
||||||
|
color = MaterialTheme.colors.onSurface
|
||||||
|
)
|
||||||
|
|
||||||
|
val artistStringBuilder = StringBuilder()
|
||||||
|
|
||||||
|
with (itemInfo.artists) {
|
||||||
|
if (this.isNotEmpty())
|
||||||
|
artistStringBuilder.append(this.joinToString(", ") { it.wordCapitalize() })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (artistStringBuilder.isNotEmpty())
|
||||||
|
Text(
|
||||||
|
artistStringBuilder.toString(),
|
||||||
|
style = MaterialTheme.typography.subtitle1,
|
||||||
|
color = MaterialTheme.colors.onSurface.copy(alpha = 0.6F)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (itemInfo.series.isNotEmpty())
|
||||||
|
Text(
|
||||||
|
stringResource(
|
||||||
|
id = R.string.galleryblock_series,
|
||||||
|
itemInfo.series.joinToString { it.wordCapitalize() }
|
||||||
|
),
|
||||||
|
style = MaterialTheme.typography.body2,
|
||||||
|
color = MaterialTheme.colors.onSurface.copy(alpha = 0.6F)
|
||||||
|
)
|
||||||
|
|
||||||
|
Text(
|
||||||
|
stringResource(id = R.string.galleryblock_type, itemInfo.type),
|
||||||
|
style = MaterialTheme.typography.body2,
|
||||||
|
color = MaterialTheme.colors.onSurface.copy(alpha = 0.6F)
|
||||||
|
)
|
||||||
|
|
||||||
|
key(itemInfo.tags) {
|
||||||
|
TagGroup(tags = itemInfo.tags)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private fun transform(manga: Manga) = HiyobiItemInfo(
|
||||||
|
manga.mangaId.toString(),
|
||||||
|
manga.title,
|
||||||
|
"https://api.hiyobi.io/${manga.thumbnail}",
|
||||||
|
manga.artist,
|
||||||
|
manga.series,
|
||||||
|
manga.type,
|
||||||
|
manga.date,
|
||||||
|
manga.bookmark,
|
||||||
|
manga.tags,
|
||||||
|
manga.commentCount,
|
||||||
|
manga.pageCount
|
||||||
|
)
|
||||||
|
|
||||||
|
fun parseQuery(query: String): String {
|
||||||
|
val queryBuilder = StringBuilder("[")
|
||||||
|
|
||||||
|
if (query.isNotBlank())
|
||||||
|
query.split(' ').filter { it.isNotBlank() }.forEach {
|
||||||
|
val tags = it.replace('_', ' ').split(':', limit = 2)
|
||||||
|
|
||||||
|
if (queryBuilder.length != 1) queryBuilder.append(',')
|
||||||
|
|
||||||
|
queryBuilder.append(
|
||||||
|
when {
|
||||||
|
tags.size == 1 -> "{tag:\"${tags[0]}\"}"
|
||||||
|
tags[0] == "male" -> "{male:1,tag:\"${tags[1]}\"}"
|
||||||
|
tags[0] == "female" -> "{female:1,tag:\"${tags[1]}\"}"
|
||||||
|
else -> "{tag:\"${tags[1]}\"}"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return queryBuilder.append(']').toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
/*
|
|
||||||
* Pupil, Hitomi.la viewer for Android
|
|
||||||
* Copyright (C) 2020 tom5079
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package xyz.quaver.pupil.ui
|
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.os.PersistableBundle
|
|
||||||
import android.view.WindowManager
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
|
||||||
import androidx.annotation.CallSuper
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import xyz.quaver.pupil.R
|
|
||||||
import xyz.quaver.pupil.util.LockManager
|
|
||||||
import xyz.quaver.pupil.util.Preferences
|
|
||||||
import xyz.quaver.pupil.util.normalizeID
|
|
||||||
|
|
||||||
open class BaseActivity : AppCompatActivity() {
|
|
||||||
|
|
||||||
private var locked: Boolean = true
|
|
||||||
|
|
||||||
private val lockLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
|
||||||
if (it.resultCode == Activity.RESULT_OK)
|
|
||||||
locked = false
|
|
||||||
else
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
|
|
||||||
@CallSuper
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?, persistentState: PersistableBundle?) {
|
|
||||||
super.onCreate(savedInstanceState, persistentState)
|
|
||||||
|
|
||||||
locked = !LockManager(this).locks.isNullOrEmpty()
|
|
||||||
}
|
|
||||||
|
|
||||||
@CallSuper
|
|
||||||
override fun onResume() {
|
|
||||||
super.onResume()
|
|
||||||
|
|
||||||
if (Preferences["security_mode"])
|
|
||||||
window.setFlags(
|
|
||||||
WindowManager.LayoutParams.FLAG_SECURE,
|
|
||||||
WindowManager.LayoutParams.FLAG_SECURE)
|
|
||||||
else
|
|
||||||
window.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
|
|
||||||
|
|
||||||
if (locked)
|
|
||||||
lockLauncher.launch(Intent(this, LockActivity::class.java))
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,280 +0,0 @@
|
|||||||
/*
|
|
||||||
* Pupil, Hitomi.la viewer for Android
|
|
||||||
* Copyright (C) 2019 tom5079
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package xyz.quaver.pupil.ui
|
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.app.AlertDialog
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.animation.Animation
|
|
||||||
import android.view.animation.AnimationUtils
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import androidx.biometric.BiometricManager
|
|
||||||
import androidx.biometric.BiometricPrompt
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import com.andrognito.patternlockview.PatternLockView
|
|
||||||
import com.google.android.material.snackbar.Snackbar
|
|
||||||
import xyz.quaver.pupil.R
|
|
||||||
import xyz.quaver.pupil.databinding.LockActivityBinding
|
|
||||||
import xyz.quaver.pupil.ui.fragment.PINLockFragment
|
|
||||||
import xyz.quaver.pupil.ui.fragment.PatternLockFragment
|
|
||||||
import xyz.quaver.pupil.util.Lock
|
|
||||||
import xyz.quaver.pupil.util.LockManager
|
|
||||||
import xyz.quaver.pupil.util.Preferences
|
|
||||||
|
|
||||||
private var lastUnlocked = 0L
|
|
||||||
class LockActivity : AppCompatActivity() {
|
|
||||||
|
|
||||||
private lateinit var lockManager: LockManager
|
|
||||||
private var mode: String? = null
|
|
||||||
|
|
||||||
private lateinit var binding: LockActivityBinding
|
|
||||||
|
|
||||||
private val patternLockFragment = PatternLockFragment().apply {
|
|
||||||
var lastPass = ""
|
|
||||||
onPatternDrawn = {
|
|
||||||
when(mode) {
|
|
||||||
null -> {
|
|
||||||
val result = lockManager.check(it)
|
|
||||||
|
|
||||||
if (result == true) {
|
|
||||||
lastUnlocked = System.currentTimeMillis()
|
|
||||||
setResult(Activity.RESULT_OK)
|
|
||||||
finish()
|
|
||||||
} else
|
|
||||||
binding.patternLockView.setViewMode(PatternLockView.PatternViewMode.WRONG)
|
|
||||||
}
|
|
||||||
"add_lock" -> {
|
|
||||||
if (lastPass.isEmpty()) {
|
|
||||||
lastPass = it
|
|
||||||
|
|
||||||
Snackbar.make(view!!, R.string.settings_lock_confirm, Snackbar.LENGTH_LONG).show()
|
|
||||||
} else {
|
|
||||||
if (lastPass == it) {
|
|
||||||
LockManager(context!!).add(Lock.generate(Lock.Type.PATTERN, it))
|
|
||||||
finish()
|
|
||||||
} else {
|
|
||||||
binding.patternLockView.setViewMode(PatternLockView.PatternViewMode.WRONG)
|
|
||||||
lastPass = ""
|
|
||||||
|
|
||||||
Snackbar.make(view!!, R.string.settings_lock_wrong_confirm, Snackbar.LENGTH_LONG).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val pinLockFragment = PINLockFragment().apply {
|
|
||||||
var lastPass = ""
|
|
||||||
onPINEntered = {
|
|
||||||
when(mode) {
|
|
||||||
null -> {
|
|
||||||
val result = lockManager.check(it)
|
|
||||||
|
|
||||||
if (result == true) {
|
|
||||||
lastUnlocked = System.currentTimeMillis()
|
|
||||||
setResult(Activity.RESULT_OK)
|
|
||||||
finish()
|
|
||||||
} else {
|
|
||||||
binding.indicatorDots.startAnimation(AnimationUtils.loadAnimation(context, R.anim.shake).apply {
|
|
||||||
setAnimationListener(object: Animation.AnimationListener {
|
|
||||||
override fun onAnimationEnd(animation: Animation?) {
|
|
||||||
binding.pinLockView.resetPinLockView()
|
|
||||||
binding.pinLockView.isEnabled = true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onAnimationStart(animation: Animation?) {
|
|
||||||
binding.pinLockView.isEnabled = false
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onAnimationRepeat(animation: Animation?) {
|
|
||||||
// Do Nothing
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"add_lock" -> {
|
|
||||||
if (lastPass.isEmpty()) {
|
|
||||||
lastPass = it
|
|
||||||
|
|
||||||
binding.pinLockView.resetPinLockView()
|
|
||||||
Snackbar.make(view!!, R.string.settings_lock_confirm, Snackbar.LENGTH_LONG).show()
|
|
||||||
} else {
|
|
||||||
if (lastPass == it) {
|
|
||||||
LockManager(context!!).add(Lock.generate(Lock.Type.PIN, it))
|
|
||||||
finish()
|
|
||||||
} else {
|
|
||||||
binding.indicatorDots.startAnimation(AnimationUtils.loadAnimation(context, R.anim.shake).apply {
|
|
||||||
setAnimationListener(object: Animation.AnimationListener {
|
|
||||||
override fun onAnimationEnd(animation: Animation?) {
|
|
||||||
binding.pinLockView.resetPinLockView()
|
|
||||||
binding.pinLockView.isEnabled = true
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onAnimationStart(animation: Animation?) {
|
|
||||||
binding.pinLockView.isEnabled = false
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onAnimationRepeat(animation: Animation?) {
|
|
||||||
// Do Nothing
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
lastPass = ""
|
|
||||||
|
|
||||||
Snackbar.make(view!!, R.string.settings_lock_wrong_confirm, Snackbar.LENGTH_LONG).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showBiometricPrompt() {
|
|
||||||
val promptInfo = BiometricPrompt.PromptInfo.Builder()
|
|
||||||
.setTitle(getText(R.string.settings_lock_fingerprint_prompt))
|
|
||||||
.setSubtitle(getText(R.string.settings_lock_fingerprint_prompt_subtitle))
|
|
||||||
.setNegativeButtonText(getText(android.R.string.cancel))
|
|
||||||
.setConfirmationRequired(false)
|
|
||||||
.build()
|
|
||||||
|
|
||||||
val biometricPrompt = BiometricPrompt(this, ContextCompat.getMainExecutor(this),
|
|
||||||
object : BiometricPrompt.AuthenticationCallback() {
|
|
||||||
override fun onAuthenticationSucceeded(
|
|
||||||
result: BiometricPrompt.AuthenticationResult) {
|
|
||||||
super.onAuthenticationSucceeded(result)
|
|
||||||
lastUnlocked = System.currentTimeMillis()
|
|
||||||
setResult(RESULT_OK)
|
|
||||||
finish()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// Displays the "log in" prompt.
|
|
||||||
biometricPrompt.authenticate(promptInfo)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
binding = LockActivityBinding.inflate(layoutInflater)
|
|
||||||
setContentView(binding.root)
|
|
||||||
|
|
||||||
lockManager = try {
|
|
||||||
LockManager(this)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
AlertDialog.Builder(this).apply {
|
|
||||||
setTitle(R.string.warning)
|
|
||||||
setMessage(R.string.lock_corrupted)
|
|
||||||
setPositiveButton(android.R.string.ok) { _, _ ->
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
}.show()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
mode = intent.getStringExtra("mode")
|
|
||||||
val force = intent.getBooleanExtra("force", false)
|
|
||||||
|
|
||||||
when(mode) {
|
|
||||||
null -> {
|
|
||||||
if (lockManager.isEmpty()) {
|
|
||||||
setResult(RESULT_OK)
|
|
||||||
finish()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (System.currentTimeMillis() - lastUnlocked < 5*60*1000 && !force) {
|
|
||||||
lastUnlocked = System.currentTimeMillis()
|
|
||||||
setResult(RESULT_OK)
|
|
||||||
finish()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
Preferences["lock_fingerprint"]
|
|
||||||
&& BiometricManager.from(this).canAuthenticate() == BiometricManager.BIOMETRIC_SUCCESS
|
|
||||||
) {
|
|
||||||
binding.fingerprintBtn.apply {
|
|
||||||
isEnabled = true
|
|
||||||
setOnClickListener {
|
|
||||||
showBiometricPrompt()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
showBiometricPrompt()
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.patternBtn.apply {
|
|
||||||
isEnabled = lockManager.contains(Lock.Type.PATTERN)
|
|
||||||
setOnClickListener {
|
|
||||||
supportFragmentManager.beginTransaction().replace(
|
|
||||||
R.id.lock_content, patternLockFragment
|
|
||||||
).commit()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
binding.pinBtn.apply {
|
|
||||||
isEnabled = lockManager.contains(Lock.Type.PIN)
|
|
||||||
setOnClickListener {
|
|
||||||
supportFragmentManager.beginTransaction().replace(
|
|
||||||
R.id.lock_content, pinLockFragment
|
|
||||||
).commit()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
binding.passwordBtn.isEnabled = false
|
|
||||||
|
|
||||||
when (lockManager.locks!!.first().type) {
|
|
||||||
Lock.Type.PIN -> {
|
|
||||||
|
|
||||||
supportFragmentManager.beginTransaction().add(
|
|
||||||
R.id.lock_content, pinLockFragment
|
|
||||||
).commit()
|
|
||||||
}
|
|
||||||
Lock.Type.PATTERN -> {
|
|
||||||
supportFragmentManager.beginTransaction().add(
|
|
||||||
R.id.lock_content, patternLockFragment
|
|
||||||
).commit()
|
|
||||||
}
|
|
||||||
else -> return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"add_lock" -> {
|
|
||||||
binding.patternBtn.isEnabled = false
|
|
||||||
binding.pinBtn.isEnabled = false
|
|
||||||
binding.fingerprintBtn.isEnabled = false
|
|
||||||
binding.passwordBtn.isEnabled = false
|
|
||||||
|
|
||||||
when(intent.getStringExtra("type")!!) {
|
|
||||||
"pattern" -> {
|
|
||||||
binding.patternBtn.isEnabled = true
|
|
||||||
supportFragmentManager.beginTransaction().add(
|
|
||||||
R.id.lock_content, patternLockFragment
|
|
||||||
).commit()
|
|
||||||
}
|
|
||||||
"pin" -> {
|
|
||||||
binding.pinBtn.isEnabled = true
|
|
||||||
supportFragmentManager.beginTransaction().add(
|
|
||||||
R.id.lock_content, pinLockFragment
|
|
||||||
).commit()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -53,7 +53,6 @@ import com.google.accompanist.drawablepainter.rememberDrawablePainter
|
|||||||
import kotlinx.coroutines.*
|
import 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("")
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,146 +0,0 @@
|
|||||||
/*
|
|
||||||
* Pupil, Hitomi.la viewer for Android
|
|
||||||
* Copyright (C) 2020 tom5079
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package xyz.quaver.pupil.ui.fragment
|
|
||||||
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import androidx.preference.Preference
|
|
||||||
import androidx.preference.PreferenceFragmentCompat
|
|
||||||
import androidx.preference.SwitchPreferenceCompat
|
|
||||||
import xyz.quaver.pupil.R
|
|
||||||
import xyz.quaver.pupil.ui.LockActivity
|
|
||||||
import xyz.quaver.pupil.util.Lock
|
|
||||||
import xyz.quaver.pupil.util.LockManager
|
|
||||||
import xyz.quaver.pupil.util.Preferences
|
|
||||||
|
|
||||||
class LockSettingsFragment : PreferenceFragmentCompat() {
|
|
||||||
|
|
||||||
override fun onResume() {
|
|
||||||
super.onResume()
|
|
||||||
|
|
||||||
val lockManager = LockManager(requireContext())
|
|
||||||
|
|
||||||
findPreference<Preference>("lock_pattern")?.summary =
|
|
||||||
if (lockManager.contains(Lock.Type.PATTERN))
|
|
||||||
getString(R.string.settings_lock_enabled)
|
|
||||||
else
|
|
||||||
""
|
|
||||||
|
|
||||||
findPreference<Preference>("lock_pin")?.summary =
|
|
||||||
if (lockManager.contains(Lock.Type.PIN))
|
|
||||||
getString(R.string.settings_lock_enabled)
|
|
||||||
else
|
|
||||||
""
|
|
||||||
|
|
||||||
if (lockManager.isEmpty()) {
|
|
||||||
(findPreference<Preference>("lock_fingerprint") as SwitchPreferenceCompat).isChecked = false
|
|
||||||
|
|
||||||
Preferences["lock_fingerprint"] = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
|
||||||
setPreferencesFromResource(R.xml.lock_preferences, rootKey)
|
|
||||||
|
|
||||||
with (findPreference<Preference>("lock_pattern")) {
|
|
||||||
this!!
|
|
||||||
|
|
||||||
if (LockManager(requireContext()).contains(Lock.Type.PATTERN))
|
|
||||||
summary = getString(R.string.settings_lock_enabled)
|
|
||||||
|
|
||||||
onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
|
||||||
val lockManager = LockManager(requireContext())
|
|
||||||
|
|
||||||
if (lockManager.contains(Lock.Type.PATTERN)) {
|
|
||||||
AlertDialog.Builder(requireContext()).apply {
|
|
||||||
setTitle(R.string.warning)
|
|
||||||
setMessage(R.string.settings_lock_remove_message)
|
|
||||||
|
|
||||||
setPositiveButton(android.R.string.ok) { _, _ ->
|
|
||||||
lockManager.remove(Lock.Type.PATTERN)
|
|
||||||
onResume()
|
|
||||||
}
|
|
||||||
setNegativeButton(android.R.string.cancel) { _, _ -> }
|
|
||||||
}.show()
|
|
||||||
} else {
|
|
||||||
val intent = Intent(requireContext(), LockActivity::class.java).apply {
|
|
||||||
putExtra("mode", "add_lock")
|
|
||||||
putExtra("type", "pattern")
|
|
||||||
}
|
|
||||||
|
|
||||||
startActivity(intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
with (findPreference<Preference>("lock_pin")) {
|
|
||||||
this!!
|
|
||||||
|
|
||||||
if (LockManager(requireContext()).contains(Lock.Type.PIN))
|
|
||||||
summary = getString(R.string.settings_lock_enabled)
|
|
||||||
|
|
||||||
onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
|
||||||
val lockManager = LockManager(requireContext())
|
|
||||||
|
|
||||||
if (lockManager.contains(Lock.Type.PIN)) {
|
|
||||||
AlertDialog.Builder(requireContext()).apply {
|
|
||||||
setTitle(R.string.warning)
|
|
||||||
setMessage(R.string.settings_lock_remove_message)
|
|
||||||
|
|
||||||
setPositiveButton(android.R.string.ok) { _, _ ->
|
|
||||||
lockManager.remove(Lock.Type.PIN)
|
|
||||||
onResume()
|
|
||||||
}
|
|
||||||
setNegativeButton(android.R.string.cancel) { _, _ -> }
|
|
||||||
}.show()
|
|
||||||
} else {
|
|
||||||
val intent = Intent(requireContext(), LockActivity::class.java).apply {
|
|
||||||
putExtra("mode", "add_lock")
|
|
||||||
putExtra("type", "pin")
|
|
||||||
}
|
|
||||||
|
|
||||||
startActivity(intent)
|
|
||||||
}
|
|
||||||
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
with (findPreference<Preference>("lock_fingerprint")) {
|
|
||||||
this!!
|
|
||||||
|
|
||||||
setOnPreferenceChangeListener { _, newValue ->
|
|
||||||
this as SwitchPreferenceCompat
|
|
||||||
|
|
||||||
if (newValue == true && LockManager(requireContext()).isEmpty()) {
|
|
||||||
isChecked = false
|
|
||||||
|
|
||||||
Toast.makeText(requireContext(), R.string.settings_lock_fingerprint_without_lock, Toast.LENGTH_SHORT).show()
|
|
||||||
} else
|
|
||||||
isChecked = newValue as Boolean
|
|
||||||
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -32,7 +32,6 @@ import org.kodein.di.instance
|
|||||||
import xyz.quaver.io.FileX
|
import xyz.quaver.io.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")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -1,59 +0,0 @@
|
|||||||
/*
|
|
||||||
* Pupil, Hitomi.la viewer for Android
|
|
||||||
* Copyright (C) 2021 tom5079
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* (at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package xyz.quaver.pupil.ui.viewmodel
|
|
||||||
|
|
||||||
import android.app.Application
|
|
||||||
import androidx.lifecycle.AndroidViewModel
|
|
||||||
import androidx.lifecycle.LiveData
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import androidx.lifecycle.viewModelScope
|
|
||||||
import kotlinx.coroutines.*
|
|
||||||
import org.kodein.di.DIAware
|
|
||||||
import org.kodein.di.android.x.closestDI
|
|
||||||
import xyz.quaver.pupil.sources.ItemInfo
|
|
||||||
import xyz.quaver.pupil.sources.Source
|
|
||||||
import xyz.quaver.pupil.util.source
|
|
||||||
|
|
||||||
class GalleryDialogViewModel(app: Application) : AndroidViewModel(app), DIAware {
|
|
||||||
|
|
||||||
override val di by closestDI()
|
|
||||||
|
|
||||||
private val _info = MutableLiveData<ItemInfo>()
|
|
||||||
val info: LiveData<ItemInfo> = _info
|
|
||||||
|
|
||||||
private val _related = MutableLiveData<List<ItemInfo>>()
|
|
||||||
val related: LiveData<List<ItemInfo>> = _related
|
|
||||||
|
|
||||||
fun load(source: String, itemID: String) {/*
|
|
||||||
val source: Source by source(source)
|
|
||||||
|
|
||||||
viewModelScope.launch {
|
|
||||||
_info.value = withContext(Dispatchers.IO) {
|
|
||||||
source.info(itemID)
|
|
||||||
}.also {
|
|
||||||
_related.value = it.extra[ItemInfo.ExtraType.RELATED_ITEM]?.await()?.split(", ")?.map { related ->
|
|
||||||
async(Dispatchers.IO) {
|
|
||||||
source.info(related)
|
|
||||||
}
|
|
||||||
}?.awaitAll()
|
|
||||||
}
|
|
||||||
}*/
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -31,7 +31,6 @@ import org.kodein.log.LoggerFactory
|
|||||||
import org.kodein.log.newLogger
|
import 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
|
||||||
|
|||||||
@@ -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,9 +119,10 @@ class ReaderViewModel(app: Application) : AndroidViewModel(app), DIAware {
|
|||||||
}
|
}
|
||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
_title.value = withContext(Dispatchers.IO) {
|
if (title == null)
|
||||||
source.info(itemID)
|
title = withContext(Dispatchers.IO) {
|
||||||
}.title
|
source.info(itemID)
|
||||||
|
}.title
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModelScope.launch {
|
viewModelScope.launch {
|
||||||
@@ -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)
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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.*
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<!--
|
|
||||||
~ Pupil, Hitomi.la viewer for Android
|
|
||||||
~ Copyright (C) 2019 tom5079
|
|
||||||
~
|
|
||||||
~ This program is free software: you can redistribute it and/or modify
|
|
||||||
~ it under the terms of the GNU General Public License as published by
|
|
||||||
~ the Free Software Foundation, either version 3 of the License, or
|
|
||||||
~ (at your option) any later version.
|
|
||||||
~
|
|
||||||
~ This program is distributed in the hope that it will be useful,
|
|
||||||
~ but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
~ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
~ GNU General Public License for more details.
|
|
||||||
~
|
|
||||||
~ You should have received a copy of the GNU General Public License
|
|
||||||
~ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
-->
|
|
||||||
|
|
||||||
<xyz.quaver.pupil.ui.view.ProgressCardView
|
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:clipChildren="true"
|
|
||||||
app:cardCornerRadius="4dp"
|
|
||||||
android:layout_marginLeft="8dp"
|
|
||||||
android:layout_marginRight="8dp"
|
|
||||||
app:cardUseCompatPadding="true"
|
|
||||||
tools:ignore="RtlHardcoded">
|
|
||||||
|
|
||||||
<androidx.compose.ui.platform.ComposeView
|
|
||||||
android:id="@+id/compose_view"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content" />
|
|
||||||
|
|
||||||
</xyz.quaver.pupil.ui.view.ProgressCardView>
|
|
||||||
@@ -33,6 +33,7 @@ import kotlinx.serialization.json.Json
|
|||||||
import org.junit.Test
|
import 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"))
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user