mirror of
https://github.com/Suwayomi/TachideskJUI.git
synced 2025-12-10 06:42:05 +01:00
Reader improvements
Switch reader pager to accompanist pager, may change to something else later as it doesn't have the needed dragging Implement proper page loading, add priority based queue, its based on the Tachi 0.x reader
This commit is contained in:
@@ -28,6 +28,7 @@ dependencies {
|
||||
implementation(compose.desktop.currentOs)
|
||||
implementation("br.com.devsrsouza.compose.icons.jetbrains:font-awesome:0.2.0")
|
||||
implementation("com.github.Syer10:compose-router:45a8c4fe83")
|
||||
implementation("ca.gosyer:accompanist-pager:0.8.1")
|
||||
|
||||
// UI (Swing)
|
||||
implementation("com.github.weisj:darklaf-core:2.5.5")
|
||||
@@ -82,7 +83,9 @@ tasks {
|
||||
"-Xopt-in=androidx.compose.foundation.ExperimentalFoundationApi",
|
||||
"-Xopt-in=kotlinx.serialization.ExperimentalSerializationApi",
|
||||
"-Xopt-in=com.russhwolf.settings.ExperimentalSettingsApi",
|
||||
"-Xopt-in=com.russhwolf.settings.ExperimentalSettingsImplementation"
|
||||
"-Xopt-in=com.russhwolf.settings.ExperimentalSettingsImplementation",
|
||||
"-Xopt-in=com.google.accompanist.pager.ExperimentalPagerApi",
|
||||
"-Xopt-in=androidx.compose.animation.ExperimentalAnimationApi"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,15 +25,12 @@ open class BaseInteractionHandler(
|
||||
private val _serverUrl = serverPreferences.server()
|
||||
val serverUrl get() = _serverUrl.get()
|
||||
|
||||
protected suspend inline fun <reified T> Http.getRepeat(
|
||||
urlString: String,
|
||||
block: HttpRequestBuilder.() -> Unit = {}
|
||||
): T {
|
||||
protected inline fun <T> repeat(block: () -> T): T {
|
||||
var attempt = 1
|
||||
var lastException: Exception
|
||||
do {
|
||||
try {
|
||||
return get(urlString, block)
|
||||
return block()
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
lastException = e
|
||||
@@ -43,58 +40,40 @@ open class BaseInteractionHandler(
|
||||
throw lastException
|
||||
}
|
||||
|
||||
protected suspend inline fun <reified T> Http.getRepeat(
|
||||
urlString: String,
|
||||
noinline block: HttpRequestBuilder.() -> Unit = {}
|
||||
): T {
|
||||
return repeat {
|
||||
get(urlString, block)
|
||||
}
|
||||
}
|
||||
|
||||
protected suspend inline fun <reified T> Http.deleteRepeat(
|
||||
urlString: String,
|
||||
block: HttpRequestBuilder.() -> Unit = {}
|
||||
noinline block: HttpRequestBuilder.() -> Unit = {}
|
||||
): T {
|
||||
var attempt = 1
|
||||
var lastException: Exception
|
||||
do {
|
||||
try {
|
||||
return delete(urlString, block)
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
lastException = e
|
||||
}
|
||||
attempt++
|
||||
} while (attempt <= 3)
|
||||
throw lastException
|
||||
return repeat {
|
||||
delete(urlString, block)
|
||||
}
|
||||
}
|
||||
|
||||
protected suspend inline fun <reified T> Http.patchRepeat(
|
||||
urlString: String,
|
||||
block: HttpRequestBuilder.() -> Unit = {}
|
||||
noinline block: HttpRequestBuilder.() -> Unit = {}
|
||||
): T {
|
||||
var attempt = 1
|
||||
var lastException: Exception
|
||||
do {
|
||||
try {
|
||||
return patch(urlString, block)
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
lastException = e
|
||||
}
|
||||
attempt++
|
||||
} while (attempt <= 3)
|
||||
throw lastException
|
||||
return repeat {
|
||||
patch(urlString, block)
|
||||
}
|
||||
}
|
||||
|
||||
protected suspend inline fun <reified T> Http.postRepeat(
|
||||
urlString: String,
|
||||
block: HttpRequestBuilder.() -> Unit = {}
|
||||
noinline block: HttpRequestBuilder.() -> Unit = {}
|
||||
): T {
|
||||
var attempt = 1
|
||||
var lastException: Exception
|
||||
do {
|
||||
try {
|
||||
return post(urlString, block)
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
lastException = e
|
||||
}
|
||||
attempt++
|
||||
} while (attempt <= 3)
|
||||
throw lastException
|
||||
return repeat {
|
||||
post(urlString, block)
|
||||
}
|
||||
}
|
||||
|
||||
protected suspend inline fun <reified T> Http.submitFormRepeat(
|
||||
@@ -103,32 +82,14 @@ open class BaseInteractionHandler(
|
||||
encodeInQuery: Boolean = false,
|
||||
block: HttpRequestBuilder.() -> Unit = {}
|
||||
): T {
|
||||
var attempt = 1
|
||||
var lastException: Exception
|
||||
do {
|
||||
try {
|
||||
return submitForm(urlString, formParameters, encodeInQuery, block)
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
lastException = e
|
||||
}
|
||||
attempt++
|
||||
} while (attempt <= 3)
|
||||
throw lastException
|
||||
return repeat {
|
||||
submitForm(urlString, formParameters, encodeInQuery, block)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun imageFromUrl(client: Http, imageUrl: String): ImageBitmap {
|
||||
var attempt = 1
|
||||
var lastException: Exception
|
||||
do {
|
||||
try {
|
||||
return ca.gosyer.util.compose.imageFromUrl(client, imageUrl)
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
lastException = e
|
||||
}
|
||||
attempt++
|
||||
} while (attempt <= 3)
|
||||
throw lastException
|
||||
return repeat {
|
||||
ca.gosyer.util.compose.imageFromUrl(client, imageUrl)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,11 @@ import androidx.compose.ui.unit.sp
|
||||
import kotlin.random.Random
|
||||
|
||||
@Composable
|
||||
fun ErrorScreen(errorMessage: String? = null, modifier: Modifier = Modifier, retry: (() -> Unit)? = null) {
|
||||
fun ErrorScreen(
|
||||
errorMessage: String? = null,
|
||||
modifier: Modifier = Modifier,
|
||||
retry: (() -> Unit)? = null
|
||||
) {
|
||||
Surface(modifier) {
|
||||
Box(Modifier.fillMaxSize()) {
|
||||
Column(modifier = Modifier.align(Alignment.Center)) {
|
||||
|
||||
@@ -7,13 +7,11 @@
|
||||
package ca.gosyer.ui.library
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.ExperimentalAnimationApi
|
||||
import androidx.compose.animation.expandVertically
|
||||
import androidx.compose.animation.shrinkVertically
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.material.ExperimentalMaterialApi
|
||||
import androidx.compose.material.ScrollableTabRow
|
||||
import androidx.compose.material.Tab
|
||||
import androidx.compose.material.Text
|
||||
@@ -29,11 +27,11 @@ import ca.gosyer.data.library.model.DisplayMode
|
||||
import ca.gosyer.data.models.Category
|
||||
import ca.gosyer.data.models.Manga
|
||||
import ca.gosyer.ui.base.components.LoadingScreen
|
||||
import ca.gosyer.ui.base.components.Pager
|
||||
import ca.gosyer.ui.base.components.PagerState
|
||||
import ca.gosyer.ui.base.vm.viewModel
|
||||
import ca.gosyer.ui.manga.openMangaMenu
|
||||
import ca.gosyer.util.compose.ThemedWindow
|
||||
import com.google.accompanist.pager.HorizontalPager
|
||||
import com.google.accompanist.pager.PagerState
|
||||
|
||||
fun openLibraryMenu() {
|
||||
ThemedWindow {
|
||||
@@ -41,7 +39,6 @@ fun openLibraryMenu() {
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
fun LibraryScreen(onClickManga: (Long) -> Unit = { openMangaMenu(it) }) {
|
||||
val vm = viewModel<LibraryScreenViewModel>()
|
||||
@@ -95,7 +92,6 @@ fun LibraryScreen(onClickManga: (Long) -> Unit = { openMangaMenu(it) }) {
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalAnimationApi::class)
|
||||
@Composable
|
||||
private fun LibraryTabs(
|
||||
visible: Boolean,
|
||||
@@ -142,8 +138,7 @@ private fun LibraryPager(
|
||||
val state = remember(categories.size, selectedPage) {
|
||||
PagerState(
|
||||
currentPage = selectedPage,
|
||||
minPage = 0,
|
||||
maxPage = categories.lastIndex
|
||||
pageCount = categories.lastIndex
|
||||
)
|
||||
}
|
||||
LaunchedEffect(state.currentPage) {
|
||||
@@ -151,8 +146,8 @@ private fun LibraryPager(
|
||||
onPageChanged(state.currentPage)
|
||||
}
|
||||
}
|
||||
Pager(state = state, offscreenLimit = 1) {
|
||||
val library by getLibraryForPage(page)
|
||||
HorizontalPager(state = state, offscreenLimit = 1) {
|
||||
val library by getLibraryForPage(it)
|
||||
when (displayMode) {
|
||||
DisplayMode.CompactGrid -> LibraryMangaCompactGrid(
|
||||
library = library,
|
||||
|
||||
59
src/main/kotlin/ca/gosyer/ui/reader/ChapterLoader.kt
Normal file
59
src/main/kotlin/ca/gosyer/ui/reader/ChapterLoader.kt
Normal file
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package ca.gosyer.ui.reader
|
||||
|
||||
import ca.gosyer.data.reader.ReaderPreferences
|
||||
import ca.gosyer.data.server.interactions.ChapterInteractionHandler
|
||||
import ca.gosyer.ui.reader.loader.TachideskPageLoader
|
||||
import ca.gosyer.ui.reader.model.ReaderChapter
|
||||
import ca.gosyer.ui.reader.model.ReaderPage
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.drop
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.take
|
||||
import mu.KotlinLogging
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
class ChapterLoader(
|
||||
val context: CoroutineContext,
|
||||
private val readerPreferences: ReaderPreferences,
|
||||
private val chapterHandler: ChapterInteractionHandler
|
||||
) {
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
fun loadChapter(chapter: ReaderChapter): StateFlow<List<ReaderPage>> {
|
||||
if (chapterIsReady(chapter)) {
|
||||
return (chapter.state as ReaderChapter.State.Loaded).pages
|
||||
} else {
|
||||
chapter.state = ReaderChapter.State.Loading
|
||||
logger.debug { "Loading pages for ${chapter.chapter.name}" }
|
||||
|
||||
val loader = TachideskPageLoader(context + Dispatchers.Default, chapter, readerPreferences, chapterHandler)
|
||||
|
||||
val pages = loader.getPages()
|
||||
|
||||
pages.drop(1).take(1).onEach { pages ->
|
||||
if (pages.isEmpty()) {
|
||||
chapter.state = ReaderChapter.State.Error(Exception("No pages found"))
|
||||
}
|
||||
}.launchIn(chapter.scope)
|
||||
|
||||
chapter.pageLoader = loader // Assign here to fix race with unref
|
||||
chapter.state = ReaderChapter.State.Loaded(pages)
|
||||
return pages
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks [chapter] to be loaded based on present pages and loader in addition to state.
|
||||
*/
|
||||
private fun chapterIsReady(chapter: ReaderChapter): Boolean {
|
||||
return chapter.state is ReaderChapter.State.Loaded && chapter.pageLoader != null
|
||||
}
|
||||
}
|
||||
@@ -6,65 +6,128 @@
|
||||
|
||||
package ca.gosyer.ui.reader
|
||||
|
||||
import androidx.compose.desktop.AppWindow
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
import androidx.compose.ui.input.key.Key
|
||||
import androidx.compose.ui.input.key.KeysSet
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import ca.gosyer.data.models.Chapter
|
||||
import androidx.compose.ui.unit.dp
|
||||
import ca.gosyer.data.reader.model.Direction
|
||||
import ca.gosyer.ui.base.KeyboardShortcut
|
||||
import ca.gosyer.ui.base.components.ErrorScreen
|
||||
import ca.gosyer.ui.base.components.LoadingScreen
|
||||
import ca.gosyer.ui.base.components.Pager
|
||||
import ca.gosyer.ui.base.components.PagerState
|
||||
import ca.gosyer.ui.base.components.mangaAspectRatio
|
||||
import ca.gosyer.ui.base.theme.AppTheme
|
||||
import ca.gosyer.ui.base.vm.viewModel
|
||||
import ca.gosyer.util.compose.ThemedWindow
|
||||
import ca.gosyer.ui.reader.model.ReaderChapter
|
||||
import ca.gosyer.ui.reader.model.ReaderPage
|
||||
import com.google.accompanist.pager.HorizontalPager
|
||||
import com.google.accompanist.pager.PagerScope
|
||||
import com.google.accompanist.pager.VerticalPager
|
||||
import com.google.accompanist.pager.rememberPagerState
|
||||
import javax.swing.SwingUtilities
|
||||
|
||||
fun openReaderMenu(chapterIndex: Int, mangaId: Long) {
|
||||
ThemedWindow("TachideskJUI - Reader") {
|
||||
ReaderMenu(chapterIndex, mangaId)
|
||||
SwingUtilities.invokeLater {
|
||||
val window = AppWindow(
|
||||
"TachideskJUI - Reader"
|
||||
)
|
||||
|
||||
val setHotkeys: (List<KeyboardShortcut>) -> Unit = { shortcuts ->
|
||||
shortcuts.forEach {
|
||||
window.keyboard.setShortcut(it.key) { it.shortcut(window) }
|
||||
}
|
||||
}
|
||||
|
||||
window.show {
|
||||
AppTheme {
|
||||
ReaderMenu(chapterIndex, mangaId, setHotkeys)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ReaderMenu(chapterIndex: Int, mangaId: Long) {
|
||||
fun ReaderMenu(chapterIndex: Int, mangaId: Long, setHotkeys: (List<KeyboardShortcut>) -> Unit) {
|
||||
val vm = viewModel<ReaderMenuViewModel> {
|
||||
ReaderMenuViewModel.Params(chapterIndex, mangaId)
|
||||
}
|
||||
val isLoading by vm.isLoading.collectAsState()
|
||||
|
||||
val state by vm.state.collectAsState()
|
||||
val previousChapter by vm.previousChapter.collectAsState()
|
||||
val chapter by vm.chapter.collectAsState()
|
||||
val nextChapter by vm.nextChapter.collectAsState()
|
||||
val pages by vm.pages.collectAsState()
|
||||
val continuous by vm.readerModeSettings.continuous.collectAsState()
|
||||
val direction by vm.readerModeSettings.direction.collectAsState()
|
||||
val padding by vm.readerModeSettings.padding.collectAsState()
|
||||
val currentPage by vm.currentPage.collectAsState()
|
||||
remember {
|
||||
setHotkeys(
|
||||
listOf(
|
||||
KeyboardShortcut(KeysSet(setOf(Key.W, Key.DirectionUp))) {
|
||||
vm.progress(currentPage + 1)
|
||||
},
|
||||
KeyboardShortcut(KeysSet(setOf(Key.S, Key.DirectionDown))) {
|
||||
vm.progress(currentPage - 1)
|
||||
}
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
Surface {
|
||||
if (!isLoading && chapter != null) {
|
||||
if (state is ReaderChapter.State.Loaded && chapter != null) {
|
||||
chapter?.let { chapter ->
|
||||
val pageModifier = Modifier.fillMaxWidth().aspectRatio(mangaAspectRatio)
|
||||
if (pages.isNotEmpty()) {
|
||||
if (continuous) {
|
||||
ContinuesReader(vm, pages, pageModifier)
|
||||
ContinuesReader(
|
||||
pages,
|
||||
pageModifier,
|
||||
vm::retry,
|
||||
vm::progress
|
||||
)
|
||||
} else {
|
||||
PagerReader(vm, chapter, currentPage, pages, pageModifier)
|
||||
PagerReader(
|
||||
direction,
|
||||
currentPage,
|
||||
pages,
|
||||
previousChapter,
|
||||
chapter,
|
||||
nextChapter,
|
||||
pageModifier,
|
||||
vm::retry,
|
||||
vm::progress
|
||||
)
|
||||
}
|
||||
} else {
|
||||
ErrorScreen("No pages found")
|
||||
}
|
||||
}
|
||||
} else {
|
||||
LoadingScreen(isLoading)
|
||||
LoadingScreen(
|
||||
state is ReaderChapter.State.Wait || state is ReaderChapter.State.Loading,
|
||||
errorMessage = (state as? ReaderChapter.State.Error)?.error?.message
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -73,7 +136,7 @@ fun ReaderMenu(chapterIndex: Int, mangaId: Long) {
|
||||
fun ReaderImage(
|
||||
imageIndex: Int,
|
||||
drawable: ImageBitmap?,
|
||||
loading: Boolean,
|
||||
status: ReaderPage.Status,
|
||||
error: String?,
|
||||
imageModifier: Modifier = Modifier.fillMaxSize(),
|
||||
loadingModifier: Modifier = imageModifier,
|
||||
@@ -88,51 +151,131 @@ fun ReaderImage(
|
||||
contentScale = contentScale
|
||||
)
|
||||
} else {
|
||||
LoadingScreen(loading, loadingModifier, error) { retry(imageIndex) }
|
||||
LoadingScreen(status == ReaderPage.Status.QUEUE, loadingModifier, error) { retry(imageIndex) }
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PagerReader(readerVM: ReaderMenuViewModel, chapter: Chapter, currentPage: Int, pages: List<ReaderImage>, pageModifier: Modifier) {
|
||||
val state = remember(chapter.pageCount!!, currentPage) {
|
||||
PagerState(
|
||||
currentPage = currentPage,
|
||||
minPage = 1,
|
||||
maxPage = chapter.pageCount - 1
|
||||
)
|
||||
}
|
||||
fun PagerReader(
|
||||
direction: Direction,
|
||||
currentPage: Int,
|
||||
pages: List<ReaderPage>,
|
||||
previousChapter: ReaderChapter?,
|
||||
currentChapter: ReaderChapter,
|
||||
nextChapter: ReaderChapter?,
|
||||
pageModifier: Modifier,
|
||||
retry: (ReaderPage) -> Unit,
|
||||
progress: (Int) -> Unit
|
||||
) {
|
||||
val state = rememberPagerState(pages.size + 1, initialPage = currentPage)
|
||||
|
||||
LaunchedEffect(state.currentPage) {
|
||||
if (state.currentPage != currentPage) {
|
||||
readerVM.progress(state.currentPage)
|
||||
progress(state.currentPage)
|
||||
}
|
||||
}
|
||||
Pager(state) {
|
||||
val image = pages[page - 1]
|
||||
ReaderImage(
|
||||
image.index,
|
||||
image.bitmap.collectAsState().value,
|
||||
image.loading.collectAsState().value,
|
||||
image.error.collectAsState().value,
|
||||
loadingModifier = pageModifier,
|
||||
retry = readerVM::retry
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ContinuesReader(readerVM: ReaderMenuViewModel, pages: List<ReaderImage>, pageModifier: Modifier) {
|
||||
LazyColumn {
|
||||
items(pages) { image ->
|
||||
LaunchedEffect(image.index) {
|
||||
readerVM.progress(image.index)
|
||||
}
|
||||
ReaderImage(
|
||||
image.index,
|
||||
image.bitmap.collectAsState().value,
|
||||
image.loading.collectAsState().value,
|
||||
image.error.collectAsState().value,
|
||||
loadingModifier = pageModifier,
|
||||
retry = readerVM::retry
|
||||
if (direction == Direction.Down || direction == Direction.Up) {
|
||||
VerticalPager(state, reverseLayout = direction == Direction.Up) {
|
||||
HandlePager(
|
||||
pages,
|
||||
it,
|
||||
previousChapter,
|
||||
currentChapter,
|
||||
nextChapter,
|
||||
pageModifier,
|
||||
retry
|
||||
)
|
||||
}
|
||||
} else {
|
||||
HorizontalPager(state, reverseLayout = direction == Direction.Left) {
|
||||
HandlePager(
|
||||
pages,
|
||||
it,
|
||||
previousChapter,
|
||||
currentChapter,
|
||||
nextChapter,
|
||||
pageModifier,
|
||||
retry
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun PagerScope.HandlePager(
|
||||
pages: List<ReaderPage>,
|
||||
page: Int,
|
||||
previousChapter: ReaderChapter?,
|
||||
currentChapter: ReaderChapter,
|
||||
nextChapter: ReaderChapter?,
|
||||
pageModifier: Modifier,
|
||||
retry: (ReaderPage) -> Unit,
|
||||
) {
|
||||
when (page) {
|
||||
0 -> ChapterSeperator(previousChapter, currentChapter)
|
||||
pages.size -> ChapterSeperator(currentChapter, nextChapter)
|
||||
else -> {
|
||||
val image = pages[page - 1]
|
||||
ReaderImage(
|
||||
image.index,
|
||||
image.bitmap.collectAsState().value,
|
||||
image.status.collectAsState().value,
|
||||
image.error.collectAsState().value,
|
||||
loadingModifier = pageModifier,
|
||||
retry = { pageIndex ->
|
||||
pages.find { it.index == pageIndex }?.let { retry(it) }
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ChapterSeperator(
|
||||
previousChapter: ReaderChapter?,
|
||||
nextChapter: ReaderChapter?
|
||||
) {
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
Column {
|
||||
when {
|
||||
previousChapter == null && nextChapter != null -> {
|
||||
Text("There is no previous chapter")
|
||||
}
|
||||
previousChapter != null && nextChapter != null -> {
|
||||
Text("Previous:\n ${previousChapter.chapter.name}")
|
||||
Spacer(Modifier.height(8.dp))
|
||||
Text("Next:\n ${nextChapter.chapter.name}")
|
||||
}
|
||||
previousChapter != null && nextChapter == null -> {
|
||||
Text("There is no next chapter")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ContinuesReader(
|
||||
pages: List<ReaderPage>,
|
||||
pageModifier: Modifier,
|
||||
retry: (ReaderPage) -> Unit,
|
||||
progress: (Int) -> Unit
|
||||
) {
|
||||
LazyColumn {
|
||||
items(pages) { image ->
|
||||
LaunchedEffect(image.index) {
|
||||
progress(image.index)
|
||||
}
|
||||
ReaderImage(
|
||||
image.index,
|
||||
image.bitmap.collectAsState().value,
|
||||
image.status.collectAsState().value,
|
||||
image.error.collectAsState().value,
|
||||
loadingModifier = pageModifier,
|
||||
retry = { pageIndex ->
|
||||
pages.find { it.index == pageIndex }?.let { retry(it) }
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,34 +6,42 @@
|
||||
|
||||
package ca.gosyer.ui.reader
|
||||
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
import ca.gosyer.data.models.Chapter
|
||||
import ca.gosyer.data.reader.ReaderModeWatch
|
||||
import ca.gosyer.data.reader.ReaderPreferences
|
||||
import ca.gosyer.data.server.interactions.ChapterInteractionHandler
|
||||
import ca.gosyer.ui.base.vm.ViewModel
|
||||
import ca.gosyer.ui.reader.model.ReaderChapter
|
||||
import ca.gosyer.ui.reader.model.ReaderPage
|
||||
import ca.gosyer.ui.reader.model.ViewerChapters
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.sync.Semaphore
|
||||
import kotlinx.coroutines.sync.withPermit
|
||||
import javax.inject.Inject
|
||||
|
||||
class ReaderMenuViewModel @Inject constructor(
|
||||
params: Params,
|
||||
readerPreferences: ReaderPreferences,
|
||||
chapterHandler: ChapterInteractionHandler
|
||||
private val readerPreferences: ReaderPreferences,
|
||||
private val chapterHandler: ChapterInteractionHandler
|
||||
) : ViewModel() {
|
||||
private val _chapter = MutableStateFlow<Chapter?>(null)
|
||||
val chapter = _chapter.asStateFlow()
|
||||
private val viewerChapters = ViewerChapters(
|
||||
MutableStateFlow(null),
|
||||
MutableStateFlow(null),
|
||||
MutableStateFlow(null)
|
||||
)
|
||||
val previousChapter = viewerChapters.prevChapter.asStateFlow()
|
||||
val chapter = viewerChapters.currChapter.asStateFlow()
|
||||
val nextChapter = viewerChapters.nextChapter.asStateFlow()
|
||||
|
||||
private val _isLoading = MutableStateFlow(true)
|
||||
val isLoading = _isLoading.asStateFlow()
|
||||
private val _state = MutableStateFlow<ReaderChapter.State>(ReaderChapter.State.Wait)
|
||||
val state = _state.asStateFlow()
|
||||
|
||||
private val _pages = MutableStateFlow(emptyList<ReaderImage>())
|
||||
private val _pages = MutableStateFlow(emptyList<ReaderPage>())
|
||||
val pages = _pages.asStateFlow()
|
||||
|
||||
private val _currentPage = MutableStateFlow(1)
|
||||
@@ -41,39 +49,11 @@ class ReaderMenuViewModel @Inject constructor(
|
||||
|
||||
val readerModeSettings = ReaderModeWatch(readerPreferences, scope)
|
||||
|
||||
private val loader = ChapterLoader(scope.coroutineContext, readerPreferences, chapterHandler)
|
||||
|
||||
init {
|
||||
scope.launch(Dispatchers.Default) {
|
||||
val chapter: Chapter
|
||||
_chapter.value = chapterHandler.getChapter(params.mangaId, params.chapterIndex).also { chapter = it }
|
||||
val pageRange = 1..(chapter.pageCount ?: 1)
|
||||
_pages.value = pageRange.map {
|
||||
ReaderImage(
|
||||
it,
|
||||
MutableStateFlow(null),
|
||||
MutableStateFlow(true),
|
||||
MutableStateFlow(null)
|
||||
)
|
||||
}
|
||||
|
||||
_isLoading.value = false
|
||||
|
||||
val semaphore = Semaphore(3)
|
||||
pageRange.map {
|
||||
async {
|
||||
semaphore.withPermit {
|
||||
val page = _pages.value[it - 1]
|
||||
try {
|
||||
page.bitmap.value = chapterHandler.getPage(chapter, it)
|
||||
page.loading.value = false
|
||||
page.error.value = null
|
||||
} catch (e: Exception) {
|
||||
page.bitmap.value = null
|
||||
page.loading.value = false
|
||||
page.error.value = e.message
|
||||
}
|
||||
}
|
||||
}
|
||||
}.awaitAll()
|
||||
init(params.mangaId, params.chapterIndex)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,15 +61,62 @@ class ReaderMenuViewModel @Inject constructor(
|
||||
_currentPage.value = index
|
||||
}
|
||||
|
||||
fun retry(index: Int) {
|
||||
fun retry(page: ReaderPage) {
|
||||
chapter.value?.pageLoader?.retryPage(page)
|
||||
}
|
||||
|
||||
private fun resetValues() {
|
||||
_pages.value = emptyList()
|
||||
_currentPage.value = 1
|
||||
_state.value = ReaderChapter.State.Wait
|
||||
viewerChapters.recycle()
|
||||
}
|
||||
|
||||
suspend fun init(mangaId: Long, chapterIndex: Int) {
|
||||
resetValues()
|
||||
val chapter = ReaderChapter(
|
||||
scope.coroutineContext + Dispatchers.Default,
|
||||
chapterHandler.getChapter(mangaId, chapterIndex)
|
||||
)
|
||||
val pages = loader.loadChapter(chapter)
|
||||
viewerChapters.currChapter.value = chapter
|
||||
scope.launch(Dispatchers.Default) {
|
||||
listOf(
|
||||
async {
|
||||
try {
|
||||
viewerChapters.nextChapter.value = ReaderChapter(
|
||||
scope.coroutineContext + Dispatchers.Default,
|
||||
chapterHandler.getChapter(mangaId, chapterIndex + 1)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
}
|
||||
},
|
||||
async {
|
||||
if (chapterIndex != 0) {
|
||||
try {
|
||||
viewerChapters.prevChapter.value = ReaderChapter(
|
||||
scope.coroutineContext + Dispatchers.Default,
|
||||
chapterHandler.getChapter(mangaId, chapterIndex - 1)
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
).awaitAll()
|
||||
}
|
||||
chapter.stateObserver.onEach {
|
||||
_state.value = it
|
||||
}.launchIn(chapter.scope)
|
||||
pages.onEach { pageList ->
|
||||
pageList.forEach { it.chapter = chapter }
|
||||
_pages.value = pageList
|
||||
}.launchIn(chapter.scope)
|
||||
_currentPage.onEach { index ->
|
||||
pages.value.getOrNull(index - 1)?.let { chapter.pageLoader?.loadPage(it) }
|
||||
}.launchIn(chapter.scope)
|
||||
}
|
||||
|
||||
data class Params(val chapterIndex: Int, val mangaId: Long)
|
||||
}
|
||||
|
||||
data class ReaderImage(
|
||||
val index: Int,
|
||||
val bitmap: MutableStateFlow<ImageBitmap?>,
|
||||
val loading: MutableStateFlow<Boolean>,
|
||||
val error: MutableStateFlow<String?>
|
||||
)
|
||||
|
||||
48
src/main/kotlin/ca/gosyer/ui/reader/loader/PageLoader.kt
Normal file
48
src/main/kotlin/ca/gosyer/ui/reader/loader/PageLoader.kt
Normal file
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package ca.gosyer.ui.reader.loader
|
||||
|
||||
import ca.gosyer.ui.reader.model.ReaderPage
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
|
||||
/**
|
||||
* A loader used to load pages into the reader. Any open resources must be cleaned up when the
|
||||
* method [recycle] is called.
|
||||
*/
|
||||
abstract class PageLoader {
|
||||
|
||||
/**
|
||||
* Whether this loader has been already recycled.
|
||||
*/
|
||||
var isRecycled = false
|
||||
private set
|
||||
|
||||
/**
|
||||
* Recycles this loader. Implementations must override this method to clean up any active
|
||||
* resources.
|
||||
*/
|
||||
open fun recycle() {
|
||||
isRecycled = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an [StateFlow] containing the list of pages of a chapter.
|
||||
*/
|
||||
abstract fun getPages(): StateFlow<List<ReaderPage>>
|
||||
|
||||
/**
|
||||
* Returns an [StateFlow] that should inform of the progress of the page (see the Page class
|
||||
* for the available states)
|
||||
*/
|
||||
abstract fun loadPage(page: ReaderPage)
|
||||
|
||||
/**
|
||||
* Retries the given [page] in case it failed to load. This method only makes sense when an
|
||||
* online source is used.
|
||||
*/
|
||||
open fun retryPage(page: ReaderPage) {}
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
/*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package ca.gosyer.ui.reader.loader
|
||||
|
||||
import ca.gosyer.data.reader.ReaderPreferences
|
||||
import ca.gosyer.data.server.interactions.ChapterInteractionHandler
|
||||
import ca.gosyer.ui.reader.model.ReaderChapter
|
||||
import ca.gosyer.ui.reader.model.ReaderPage
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onCompletion
|
||||
import kotlinx.coroutines.launch
|
||||
import java.util.concurrent.PriorityBlockingQueue
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
class TachideskPageLoader(
|
||||
context: CoroutineContext,
|
||||
val chapter: ReaderChapter,
|
||||
readerPreferences: ReaderPreferences,
|
||||
chapterHandler: ChapterInteractionHandler
|
||||
) : PageLoader() {
|
||||
/**
|
||||
* A queue used to manage requests one by one while allowing priorities.
|
||||
*/
|
||||
private val queue = PriorityBlockingQueue<PriorityPage>()
|
||||
val scope = CoroutineScope(SupervisorJob() + context)
|
||||
private val preloadSize = 3
|
||||
private val pagesFlow by lazy {
|
||||
MutableStateFlow<List<ReaderPage>>(emptyList())
|
||||
}
|
||||
|
||||
init {
|
||||
repeat(3) {
|
||||
scope.launch {
|
||||
while (true) {
|
||||
try {
|
||||
val page = queue.take().page
|
||||
if (page.status.value == ReaderPage.Status.QUEUE) {
|
||||
try {
|
||||
page.bitmap.value = chapterHandler.getPage(chapter.chapter, page.index)
|
||||
page.status.value = ReaderPage.Status.READY
|
||||
page.error.value = null
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
page.bitmap.value = null
|
||||
page.status.value = ReaderPage.Status.ERROR
|
||||
page.error.value = e.message
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) throw e
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Preloads the given [amount] of pages after the [currentPage] with a lower priority.
|
||||
* @return a list of [PriorityPage] that were added to the [queue]
|
||||
*/
|
||||
private fun preloadNextPages(currentPage: ReaderPage, amount: Int): List<PriorityPage> {
|
||||
val pageIndex = currentPage.index
|
||||
val pages = currentPage.chapter.pages ?: return emptyList()
|
||||
if (pageIndex == pages.value.lastIndex) return emptyList()
|
||||
|
||||
return pages.value
|
||||
.subList(pageIndex + 1, (pageIndex + 1 + amount).coerceAtMost(pages.value.size))
|
||||
.mapNotNull {
|
||||
if (it.status.value == ReaderPage.Status.QUEUE) {
|
||||
PriorityPage(it, 0).apply { queue.offer(this) }
|
||||
} else null
|
||||
}
|
||||
}
|
||||
|
||||
override fun getPages(): StateFlow<List<ReaderPage>> {
|
||||
scope.launch {
|
||||
if (pagesFlow.value.isNotEmpty()) return@launch
|
||||
val pageRange = 0..(chapter.chapter.pageCount?.minus(1) ?: 0)
|
||||
pagesFlow.value = pageRange.map {
|
||||
ReaderPage(
|
||||
it,
|
||||
MutableStateFlow(null),
|
||||
MutableStateFlow(ReaderPage.Status.QUEUE),
|
||||
MutableStateFlow(null)
|
||||
)
|
||||
}
|
||||
}
|
||||
return pagesFlow.asStateFlow()
|
||||
}
|
||||
|
||||
override fun loadPage(page: ReaderPage) {
|
||||
scope.launch {
|
||||
// Automatically retry failed pages when subscribed to this page
|
||||
if (page.status.value == ReaderPage.Status.ERROR) {
|
||||
page.status.value = ReaderPage.Status.QUEUE
|
||||
}
|
||||
|
||||
val queuedPages = mutableListOf<PriorityPage>()
|
||||
if (page.status.value == ReaderPage.Status.QUEUE) {
|
||||
queuedPages += PriorityPage(page, 1).also { queue.offer(it) }
|
||||
}
|
||||
queuedPages += preloadNextPages(page, preloadSize)
|
||||
|
||||
page.status.onCompletion {
|
||||
queuedPages.forEach {
|
||||
if (it.page.status.value == ReaderPage.Status.QUEUE) {
|
||||
queue.remove(it)
|
||||
}
|
||||
}
|
||||
}.launchIn(scope)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retries a page. This method is only called from user interaction on the viewer.
|
||||
*/
|
||||
override fun retryPage(page: ReaderPage) {
|
||||
if (page.status.value == ReaderPage.Status.ERROR) {
|
||||
page.status.value = ReaderPage.Status.QUEUE
|
||||
}
|
||||
queue.offer(PriorityPage(page, 2))
|
||||
}
|
||||
|
||||
/**
|
||||
* Data class used to keep ordering of pages in order to maintain priority.
|
||||
*/
|
||||
private class PriorityPage(
|
||||
val page: ReaderPage,
|
||||
val priority: Int
|
||||
) : Comparable<PriorityPage> {
|
||||
companion object {
|
||||
private val idGenerator = AtomicInteger()
|
||||
}
|
||||
|
||||
private val identifier = idGenerator.incrementAndGet()
|
||||
|
||||
override fun compareTo(other: PriorityPage): Int {
|
||||
val p = other.priority.compareTo(priority)
|
||||
return if (p != 0) p else identifier.compareTo(other.identifier)
|
||||
}
|
||||
}
|
||||
|
||||
override fun recycle() {
|
||||
super.recycle()
|
||||
scope.cancel()
|
||||
queue.clear()
|
||||
}
|
||||
}
|
||||
61
src/main/kotlin/ca/gosyer/ui/reader/model/ReaderChapter.kt
Normal file
61
src/main/kotlin/ca/gosyer/ui/reader/model/ReaderChapter.kt
Normal file
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package ca.gosyer.ui.reader.model
|
||||
|
||||
import ca.gosyer.data.models.Chapter
|
||||
import ca.gosyer.ui.reader.loader.PageLoader
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import mu.KotlinLogging
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
data class ReaderChapter(val context: CoroutineContext, val chapter: Chapter) {
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
var scope = CoroutineScope(context + Job())
|
||||
private set
|
||||
|
||||
var state: State =
|
||||
State.Wait
|
||||
set(value) {
|
||||
field = value
|
||||
stateRelay.value = value
|
||||
}
|
||||
|
||||
private val stateRelay by lazy { MutableStateFlow(state) }
|
||||
|
||||
val stateObserver by lazy { stateRelay.asStateFlow() }
|
||||
|
||||
val pages: StateFlow<List<ReaderPage>>?
|
||||
get() = (state as? State.Loaded)?.pages
|
||||
|
||||
var pageLoader: PageLoader? = null
|
||||
|
||||
var requestedPage: Int = 0
|
||||
|
||||
fun recycle() {
|
||||
if (pageLoader != null) {
|
||||
logger.debug { "Recycling chapter ${chapter.name}" }
|
||||
}
|
||||
pageLoader?.recycle()
|
||||
pageLoader = null
|
||||
state = State.Wait
|
||||
scope.cancel()
|
||||
scope = CoroutineScope(context + Job())
|
||||
}
|
||||
|
||||
sealed class State {
|
||||
object Wait : State()
|
||||
object Loading : State()
|
||||
class Error(val error: Throwable) : State()
|
||||
class Loaded(val pages: StateFlow<List<ReaderPage>>) : State()
|
||||
}
|
||||
}
|
||||
26
src/main/kotlin/ca/gosyer/ui/reader/model/ReaderPage.kt
Normal file
26
src/main/kotlin/ca/gosyer/ui/reader/model/ReaderPage.kt
Normal file
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package ca.gosyer.ui.reader.model
|
||||
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
data class ReaderPage(
|
||||
val index: Int,
|
||||
val bitmap: MutableStateFlow<ImageBitmap?>,
|
||||
val status: MutableStateFlow<Status>,
|
||||
val error: MutableStateFlow<String?>
|
||||
) {
|
||||
lateinit var chapter: ReaderChapter
|
||||
enum class Status {
|
||||
QUEUE,
|
||||
LOAD_PAGE,
|
||||
DOWNLOAD_IMAGE,
|
||||
READY,
|
||||
ERROR
|
||||
}
|
||||
}
|
||||
24
src/main/kotlin/ca/gosyer/ui/reader/model/ViewerChapters.kt
Normal file
24
src/main/kotlin/ca/gosyer/ui/reader/model/ViewerChapters.kt
Normal file
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package ca.gosyer.ui.reader.model
|
||||
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
||||
data class ViewerChapters(
|
||||
val currChapter: MutableStateFlow<ReaderChapter?>,
|
||||
val prevChapter: MutableStateFlow<ReaderChapter?>,
|
||||
val nextChapter: MutableStateFlow<ReaderChapter?>
|
||||
) {
|
||||
fun recycle() {
|
||||
currChapter.value?.recycle()
|
||||
prevChapter.value?.recycle()
|
||||
nextChapter.value?.recycle()
|
||||
currChapter.value = null
|
||||
prevChapter.value = null
|
||||
nextChapter.value = null
|
||||
}
|
||||
}
|
||||
@@ -10,14 +10,44 @@ import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.runtime.Composable
|
||||
import ca.gosyer.ui.base.components.Toolbar
|
||||
import ca.gosyer.ui.base.prefs.PreferenceRow
|
||||
import ca.gosyer.ui.base.vm.ViewModel
|
||||
import ca.gosyer.ui.base.vm.viewModel
|
||||
import ca.gosyer.ui.main.Route
|
||||
import ca.gosyer.util.system.filePicker
|
||||
import com.github.zsoltk.compose.router.BackStack
|
||||
import mu.KotlinLogging
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
|
||||
@Composable
|
||||
fun SettingsBackupScreen(navController: BackStack<Route>) {
|
||||
Column {
|
||||
Toolbar("Backup Settings", navController, true)
|
||||
LazyColumn {
|
||||
class SettingsBackupViewModel @Inject constructor() : ViewModel() {
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
fun setFile(file: File?) {
|
||||
if (file == null || !file.exists()) {
|
||||
logger.info { "Invalid file ${file?.absolutePath}" }
|
||||
} else {
|
||||
logger.info { file.absolutePath }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SettingsBackupScreen(navController: BackStack<Route>) {
|
||||
val vm = viewModel<SettingsBackupViewModel>()
|
||||
Column {
|
||||
Toolbar("Backup Settings", navController, true)
|
||||
LazyColumn {
|
||||
item {
|
||||
PreferenceRow(
|
||||
"Restore Backup",
|
||||
onClick = {
|
||||
filePicker {
|
||||
vm.setFile(it.selectedFile)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user