OpenWithIDDialog
This commit is contained in:
tom5079
2021-12-17 20:39:30 +09:00
parent 62d0de3ef6
commit e7debfec46
19 changed files with 677 additions and 266 deletions

View File

@@ -43,7 +43,6 @@ data class SearchResultEvent(val type: Type, val itemID: String, val payload: Pa
abstract class Source {
abstract val name: String
abstract val iconResID: Int
abstract val preferenceID: Int
abstract val availableSortMode: List<String>
abstract suspend fun search(query: String, range: IntRange, sortMode: Int): Pair<Channel<ItemInfo>, Int>

View File

@@ -36,8 +36,6 @@ class History(override val di: DI) : Source(), DIAware {
get() = "history"
override val iconResID: Int
get() = 0 //TODO
override val preferenceID: Int
get() = 0 //TODO
override val availableSortMode: List<String> = emptyList()
private val history = direct.database().historyDao()

View File

@@ -122,7 +122,6 @@ class Hitomi(app: Application) : Source(), DIAware {
override val name: String = "hitomi.la"
override val iconResID: Int = R.drawable.hitomi
override val preferenceID: Int = R.xml.hitomi_preferences
override val availableSortMode: List<String> = app.resources.getStringArray(R.array.hitomi_sort_mode).toList()
var cachedQuery: String? = null

View File

@@ -153,7 +153,6 @@ class Hiyobi_io(app: Application): Source(), DIAware {
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()

View File

@@ -1,111 +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.types
import kotlinx.serialization.Serializable
@Serializable
data class Tag(val area: String?, val tag: String, val isNegative: Boolean = false) {
companion object {
fun parse(tag: String) : Tag {
if (tag.firstOrNull() == '-') {
tag.substring(1).split(Regex(":"), 2).let {
return when(it.size) {
2 -> Tag(it[0], it[1], true)
else -> Tag(null, tag, true)
}
}
}
tag.split(Regex(":"), 2).let {
return when(it.size) {
2 -> Tag(it[0], it[1])
else -> Tag(null, tag)
}
}
}
}
override fun toString(): String {
return (if (isNegative) "-" else "") + when(area) {
null -> tag
else -> "$area:$tag"
}
}
fun toQuery(): String {
return toString().replace(' ', '_')
}
override fun equals(other: Any?): Boolean {
if (other !is Tag)
return false
if (other.area == area && other.tag == tag)
return true
return false
}
override fun hashCode() = toString().hashCode()
}
class Tags(val tags: MutableSet<Tag> = mutableSetOf()) : MutableSet<Tag> by tags {
companion object {
fun parse(tags: String) : Tags {
return Tags(
tags.split(' ').mapNotNull {
if (it.isNotEmpty())
Tag.parse(it)
else
null
}.toMutableSet()
)
}
}
fun contains(element: String): Boolean {
tags.forEach {
if (it.toString() == element)
return true
}
return false
}
fun add(element: String): Boolean {
return tags.add(Tag.parse(element))
}
fun remove(element: String) {
tags.filter { it.toString() == element }.forEach {
tags.remove(it)
}
}
fun removeByArea(area: String, isNegative: Boolean? = null) {
tags.filter { it.area == area && (if(isNegative == null) true else (it.isNegative == isNegative)) }.forEach {
tags.remove(it)
}
}
override fun toString(): String {
return tags.joinToString(" ") { it.toString() }
}
}

View File

@@ -58,7 +58,6 @@ import org.kodein.log.newLogger
import xyz.quaver.pupil.*
import xyz.quaver.pupil.R
import xyz.quaver.pupil.sources.SearchResultEvent
import xyz.quaver.pupil.types.*
import xyz.quaver.pupil.ui.composable.FloatingActionButtonState
import xyz.quaver.pupil.ui.composable.FloatingSearchBar
import xyz.quaver.pupil.ui.composable.MultipleFloatingActionButton
@@ -66,6 +65,7 @@ import xyz.quaver.pupil.ui.composable.SubFabItem
import xyz.quaver.pupil.ui.dialog.SourceSelectDialog
import xyz.quaver.pupil.ui.theme.PupilTheme
import xyz.quaver.pupil.ui.composable.ProgressCard
import xyz.quaver.pupil.ui.dialog.OpenWithItemIDDialog
import xyz.quaver.pupil.ui.viewmodel.MainViewModel
import xyz.quaver.pupil.util.*
import kotlin.math.*
@@ -108,21 +108,51 @@ class MainActivity : ComponentActivity(), DIAware {
}
}
var openSourceSelectDialog by remember { mutableStateOf(false) }
val onSearchResultEvent: (SearchResultEvent) -> Unit = { event ->
when (event.type) {
SearchResultEvent.Type.OPEN_READER -> {
startActivity(
Intent(
this@MainActivity,
ReaderActivity::class.java
).apply {
putExtra("source", model.source.name)
putExtra("id", event.itemID)
putExtra("payload", event.payload)
})
}
else -> TODO("")
}
}
var sourceSelectDialog by remember { mutableStateOf(false) }
var openWithItemIDDialog by remember { mutableStateOf(false) }
LaunchedEffect(navigationIconProgress) {
navigationIcon.progress = navigationIconProgress
}
if (openSourceSelectDialog)
if (sourceSelectDialog)
SourceSelectDialog(
currentSource = model.source.name,
onDismissRequest = { openSourceSelectDialog = false }
onDismissRequest = { sourceSelectDialog = false }
) { source ->
openSourceSelectDialog = false
sourceSelectDialog = false
model.setSourceAndReset(source.name)
}
if (openWithItemIDDialog)
OpenWithItemIDDialog {
openWithItemIDDialog = false
it?.let {
onSearchResultEvent(SearchResultEvent(
SearchResultEvent.Type.OPEN_READER,
it
))
}
}
Scaffold(
floatingActionButton = {
MultipleFloatingActionButton(
@@ -142,7 +172,9 @@ class MainActivity : ComponentActivity(), DIAware {
SubFabItem(
painterResource(R.drawable.numeric),
stringResource(R.string.main_open_gallery_by_id)
),
) {
openWithItemIDDialog = true
}
),
visible = isFabVisible,
targetState = isFabExpanded,
@@ -178,22 +210,7 @@ class MainActivity : ComponentActivity(), DIAware {
ProgressCard(
progress = 0.5f
) {
model.source.SearchResult(itemInfo = itemInfo) { event ->
when (event.type) {
SearchResultEvent.Type.OPEN_READER -> {
startActivity(
Intent(
this@MainActivity,
ReaderActivity::class.java
).apply {
putExtra("source", model.source.name)
putExtra("id", event.itemID)
putExtra("payload", event.payload)
})
}
else -> TODO("")
}
}
model.source.SearchResult(itemInfo = itemInfo, onEvent = onSearchResultEvent)
}
}
}
@@ -224,7 +241,7 @@ class MainActivity : ComponentActivity(), DIAware {
painterResource(model.source.iconResID),
contentDescription = null,
modifier = Modifier.size(24.dp).clickable {
openSourceSelectDialog = true
sourceSelectDialog = true
}
)
Icon(

View File

@@ -19,12 +19,16 @@
package xyz.quaver.pupil.ui
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.viewModels
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.Image
import androidx.compose.foundation.border
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
@@ -33,21 +37,18 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.BrokenImage
import androidx.compose.material.icons.filled.Fullscreen
import androidx.compose.runtime.*
import androidx.compose.runtime.livedata.observeAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import coil.annotation.ExperimentalCoilApi
import com.google.accompanist.appcompattheme.AppCompatTheme
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.kodein.di.DIAware
import org.kodein.di.android.closestDI
@@ -71,7 +72,7 @@ class ReaderActivity : ComponentActivity(), DIAware {
private val logger = newLogger(LoggerFactory.default)
@OptIn(ExperimentalCoilApi::class)
@OptIn(ExperimentalCoilApi::class, ExperimentalFoundationApi::class)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -80,24 +81,20 @@ class ReaderActivity : ComponentActivity(), DIAware {
setContent {
var isFABExpanded by remember { mutableStateOf(FloatingActionButtonState.COLLAPSED) }
val isFullscreen by model.isFullscreen.observeAsState(false)
val imageSources = remember { mutableStateListOf<ImageSource?>() }
val imageHeights = remember { mutableStateListOf<Float?>() }
val states = remember { mutableStateListOf<SubSampledImageState>() }
val scaffoldState = rememberScaffoldState()
val snackbarCoroutineScope = rememberCoroutineScope()
LaunchedEffect(model.imageList.count { it != null }) {
if (imageSources.isEmpty() && model.imageList.isNotEmpty())
imageSources.addAll(List(model.imageList.size) { null })
if (states.isEmpty() && model.imageList.isNotEmpty())
states.addAll(List(model.imageList.size) { SubSampledImageState(ScaleTypes.FIT_WIDTH, Bounds.FORCE_OVERLAP_OR_CENTER) })
if (imageHeights.isEmpty() && model.imageList.isNotEmpty())
imageHeights.addAll(List(model.imageList.size) { null })
logger.info {
"${model.imageList.count { it == null }} nulls"
}
states.addAll(List(model.imageList.size) { SubSampledImageState(ScaleTypes.FIT_WIDTH, Bounds.FORCE_OVERLAP_OR_CENTER).apply {
isGestureEnabled = true
} })
model.imageList.forEachIndexed { i, image ->
if (imageSources[i] == null && image != null)
@@ -108,24 +105,27 @@ class ReaderActivity : ComponentActivity(), DIAware {
model.error(i)
}.getOrNull()
}
logger.info {
"${imageSources.count { it == null }} nulls"
}
}
WindowInsetsControllerCompat(window, window.decorView).run {
if (isFullscreen) {
if (model.isFullscreen) {
hide(WindowInsetsCompat.Type.systemBars())
systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
} else
show(WindowInsetsCompat.Type.systemBars())
}
if (model.error)
stringResource(R.string.reader_failed_to_find_gallery).let {
snackbarCoroutineScope.launch {
scaffoldState.snackbarHostState.showSnackbar(it, duration = SnackbarDuration.Indefinite)
}
}
PupilTheme {
Scaffold(
topBar = {
if (!isFullscreen)
if (!model.isFullscreen)
TopAppBar(
title = {
Text(
@@ -145,14 +145,14 @@ class ReaderActivity : ComponentActivity(), DIAware {
)
},
floatingActionButton = {
if (!isFullscreen)
if (!model.isFullscreen)
MultipleFloatingActionButton(
items = listOf(
SubFabItem(
icon = Icons.Default.Fullscreen,
label = stringResource(id = R.string.reader_fab_fullscreen)
) {
model.isFullscreen.postValue(true)
model.isFullscreen = true
}
),
targetState = isFABExpanded,
@@ -160,61 +160,76 @@ class ReaderActivity : ComponentActivity(), DIAware {
isFABExpanded = it
}
)
}
},
scaffoldState = scaffoldState,
snackbarHost = { scaffoldState.snackbarHostState }
) {
LazyColumn(
Modifier.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
itemsIndexed(imageSources) { i, imageSource ->
LaunchedEffect(states[i].canvasSize, states[i].imageSize) {
if (imageHeights.isNotEmpty() && imageHeights[i] == null)
states[i].canvasSize?.let { canvasSize ->
states[i].imageSize?.let { imageSize ->
imageHeights[i] = imageSize.height * canvasSize.width / imageSize.width
} }
}
Box {
LazyColumn(
Modifier.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
itemsIndexed(imageSources) { i, imageSource ->
Box(
Modifier
.wrapContentHeight(states[i], 500.dp)
.fillMaxWidth()
.border(1.dp, Color.Gray),
contentAlignment = Alignment.Center
) {
if (imageSource == null)
model.progressList.getOrNull(i)?.let { progress ->
if (progress < 0f)
Icon(Icons.Filled.BrokenImage, null)
else
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
LinearProgressIndicator(progress)
Text((i + 1).toString())
}
}
else {
val haptic = LocalHapticFeedback.current
Box(
Modifier
.height(
imageHeights
.getOrNull(i)
?.let { with(LocalDensity.current) { it.toDp() } }
?: 500.dp)
.fillMaxWidth()
.border(1.dp, Color.Gray),
contentAlignment = Alignment.Center
) {
if (imageSource == null)
model.progressList.getOrNull(i)?.let { progress ->
if (progress < 0f)
Icon(Icons.Filled.BrokenImage, null)
else
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
LinearProgressIndicator(progress)
Text((i + 1).toString())
}
SubSampledImage(
modifier = Modifier
.fillMaxSize()
.run {
if (model.isFullscreen)
doubleClickCycleZoom(states[i], 2f)
else
combinedClickable(
onLongClick = {
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
}
) {
model.isFullscreen = true
}
},
imageSource = imageSource,
state = states[i]
)
}
else {
SubSampledImage(
modifier = Modifier.fillMaxSize(),
imageSource = imageSource,
state = states[i]
)
}
}
}
}
if (model.totalProgress != model.imageCount)
LinearProgressIndicator(
modifier = Modifier.fillMaxWidth(),
progress = model.progressList.map { abs(it) }.sum() / model.progressList.size,
color = colorResource(id = R.color.colorAccent)
if (model.totalProgress != model.imageCount)
LinearProgressIndicator(
modifier = Modifier
.fillMaxWidth()
.align(Alignment.TopCenter),
progress = model.progressList.map { abs(it) }
.sum() / model.progressList.size,
color = MaterialTheme.colors.secondary
)
SnackbarHost(
scaffoldState.snackbarHostState,
modifier = Modifier.align(Alignment.BottomCenter)
)
}
}
}
}
@@ -227,7 +242,7 @@ class ReaderActivity : ComponentActivity(), DIAware {
override fun onBackPressed() {
when {
model.isFullscreen.value == true -> model.isFullscreen.postValue(false)
model.isFullscreen -> model.isFullscreen = false
else -> super.onBackPressed()
}
}

View File

@@ -0,0 +1,74 @@
/*
* 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.ui.dialog
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import xyz.quaver.pupil.R
@Composable
fun OpenWithItemIDDialog(onDismissRequest: (String?) -> Unit = { }) {
var itemID by remember { mutableStateOf("") }
Dialog(onDismissRequest = { onDismissRequest(null) }) {
Card(
elevation = 8.dp,
shape = RoundedCornerShape(12.dp)
) {
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
Text(
stringResource(R.string.main_open_gallery_by_id),
style = MaterialTheme.typography.h6
)
TextField(
modifier = Modifier.fillMaxWidth(),
value = itemID,
onValueChange = {
itemID = it
},
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Go),
keyboardActions = KeyboardActions(
onGo = { onDismissRequest(itemID) }
)
)
Button(
modifier = Modifier.fillMaxWidth(),
onClick = { onDismissRequest(itemID) }
) {
Text(stringResource(android.R.string.ok))
}
}
}
}
}

View File

@@ -26,6 +26,8 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
private val DarkColorPalette = darkColors(
primary = LightBlue300,
primaryVariant = LightBlue700,
secondary = Pink600,
onSecondary = Color.White
)

View File

@@ -53,13 +53,16 @@ class ReaderViewModel(app: Application) : AndroidViewModel(app), DIAware {
private val logger = newLogger(LoggerFactory.default)
val isFullscreen = MutableLiveData(false)
var isFullscreen by mutableStateOf(false)
private val database: AppDatabase by instance()
private val historyDao = database.historyDao()
private val bookmarkDao = database.bookmarkDao()
var error by mutableStateOf(false)
private set
var source by mutableStateOf<Source?>(null)
private set
var itemID by mutableStateOf<String?>(null)
@@ -121,14 +124,20 @@ class ReaderViewModel(app: Application) : AndroidViewModel(app), DIAware {
viewModelScope.launch {
if (title == null)
title = withContext(Dispatchers.IO) {
source.info(itemID)
}.title
kotlin.runCatching {
source.info(itemID)
}.getOrNull()
}?.title
}
viewModelScope.launch {
withContext(Dispatchers.IO) {
source.images(itemID)
}.let { images ->
kotlin.runCatching {
source.images(itemID)
}.onFailure {
error = true
}.getOrNull()
}?.let { images ->
this@ReaderViewModel.images = images
imageCount = images.size

View File

@@ -149,7 +149,7 @@ fun View.show() {
visibility = View.VISIBLE
}
class FileXImageSource(file: FileX): ImageSource {
class FileXImageSource(val file: FileX): ImageSource {
private val decoder = newBitmapRegionDecoder(file.inputStream()!!)
override val imageSize by lazy { Size(decoder.width.toFloat(), decoder.height.toFloat()) }

View File

@@ -150,7 +150,7 @@ fun checkUpdate(context: Context, force: Boolean = false) {
Preferences["update_download_id"] = it
}
}
setNegativeButton(if (force) android.R.string.cancel else R.string.ignore_update) { _, _ ->
setNegativeButton(if (force) android.R.string.cancel else R.string.ignore) { _, _ ->
if (!force)
preferences.edit()
.putLong("ignore_update_until", System.currentTimeMillis() + 604800000)