Migrate image loading and display to Kamel

This commit is contained in:
Syer10
2021-10-24 17:44:20 -04:00
parent fdc72c5fbb
commit 1731e7c001
27 changed files with 221 additions and 203 deletions

View File

@@ -35,6 +35,7 @@ dependencies {
implementation("ca.gosyer:compose-router:0.24.2-jetbrains-2")
implementation("ca.gosyer:accompanist-pager:0.18.1")
implementation("ca.gosyer:accompanist-flowlayout:0.18.1")
implementation("com.alialbaali.kamel:kamel-image:0.3.0")
// UI (Swing)
implementation("com.github.weisj:darklaf-core:2.7.3")

View File

@@ -14,6 +14,7 @@ import ca.gosyer.data.library.LibraryPreferences
import ca.gosyer.data.reader.ReaderPreferences
import ca.gosyer.data.server.Http
import ca.gosyer.data.server.HttpProvider
import ca.gosyer.data.server.KamelConfigProvider
import ca.gosyer.data.server.ServerHostPreferences
import ca.gosyer.data.server.ServerPreferences
import ca.gosyer.data.server.ServerService
@@ -28,6 +29,7 @@ import ca.gosyer.data.server.interactions.SourceInteractionHandler
import ca.gosyer.data.translation.ResourceProvider
import ca.gosyer.data.translation.XmlResourceBundle
import ca.gosyer.data.ui.UiPreferences
import io.kamel.core.config.KamelConfig
import toothpick.ktp.binding.bind
import toothpick.ktp.binding.module
@@ -66,6 +68,10 @@ val DataModule = module {
.toProvider(HttpProvider::class)
.providesSingleton()
bind<KamelConfig>()
.toProvider(KamelConfigProvider::class)
.providesSingleton()
bind<XmlResourceBundle>()
.toProvider(ResourceProvider::class)
.providesSingleton()

View File

@@ -21,6 +21,4 @@ data class Extension(
val hasUpdate: Boolean,
val obsolete: Boolean,
val isNsfw: Boolean
) {
fun iconUrl(serverUrl: String) = serverUrl + iconUrl
}
)

View File

@@ -27,9 +27,7 @@ data class Manga(
val meta: MangaMeta,
val realUrl: String?,
val inLibraryAt: Long
) {
fun cover(serverUrl: String) = thumbnailUrl?.let { serverUrl + it }
}
)
@Serializable
data class MangaMeta(

View File

@@ -19,7 +19,6 @@ data class Source(
val isNsfw: Boolean,
val displayName: String
) {
fun iconUrl(serverUrl: String) = serverUrl + iconUrl
companion object {
const val LOCAL_SOURCE_LANG = "localsourcelang"
}

View File

@@ -0,0 +1,78 @@
/*
* 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.data.server
import ca.gosyer.data.models.Extension
import ca.gosyer.data.models.Manga
import ca.gosyer.data.models.Source
import ca.gosyer.ui.base.prefs.asStateIn
import io.kamel.core.config.DefaultCacheSize
import io.kamel.core.config.KamelConfig
import io.kamel.core.config.fileFetcher
import io.kamel.core.config.httpFetcher
import io.kamel.core.config.stringMapper
import io.kamel.core.config.uriMapper
import io.kamel.core.config.urlMapper
import io.kamel.core.mapper.Mapper
import io.kamel.image.config.imageBitmapDecoder
import io.kamel.image.config.resourcesFetcher
import io.ktor.http.Url
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import javax.inject.Inject
import javax.inject.Provider
class KamelConfigProvider @Inject constructor(
private val http: Http,
serverPreferences: ServerPreferences
) : Provider<KamelConfig> {
@OptIn(DelicateCoroutinesApi::class)
val serverUrl = serverPreferences.serverUrl().asStateIn(GlobalScope)
override fun get(): KamelConfig {
return KamelConfig {
// Default config
imageBitmapCacheSize = DefaultCacheSize
imageVectorCacheSize = DefaultCacheSize
imageBitmapDecoder()
stringMapper()
urlMapper()
uriMapper()
fileFetcher()
// JUI config
httpFetcher(http.engine) {
install(http)
}
resourcesFetcher()
val serverUrl = serverUrl.asStateFlow()
mapper(MangaCoverMapper(serverUrl))
mapper(ExtensionIconMapper(serverUrl))
mapper(SourceIconMapper(serverUrl))
}
}
class MangaCoverMapper(private val serverUrlStateFlow: StateFlow<String>) : Mapper<Manga, Url> {
override fun map(input: Manga): Url {
return Url(serverUrlStateFlow.value + input.thumbnailUrl)
}
}
class ExtensionIconMapper(private val serverUrlStateFlow: StateFlow<String>) : Mapper<Extension, Url> {
override fun map(input: Extension): Url {
return Url(serverUrlStateFlow.value + input.iconUrl)
}
}
class SourceIconMapper(private val serverUrlStateFlow: StateFlow<String>) : Mapper<Source, Url> {
override fun map(input: Source): Url {
return Url(serverUrlStateFlow.value + input.iconUrl)
}
}
}

View File

@@ -40,6 +40,8 @@ import ca.gosyer.ui.base.components.setIcon
import ca.gosyer.ui.base.resources.LocalResources
import ca.gosyer.ui.base.theme.AppTheme
import ca.gosyer.util.lang.launchApplication
import io.kamel.core.config.KamelConfig
import io.kamel.image.config.LocalKamelConfig
import kotlinx.coroutines.DelicateCoroutinesApi
@OptIn(DelicateCoroutinesApi::class)
@@ -69,6 +71,7 @@ fun WindowDialog(
}
val resources = remember { AppScope.getInstance<XmlResourceBundle>() }
val kamelConfig = remember { AppScope.getInstance<KamelConfig>() }
val windowState = rememberWindowState(size = size, position = WindowPosition(Alignment.Center))
Window(
@@ -95,7 +98,8 @@ fun WindowDialog(
) {
setIcon()
CompositionLocalProvider(
LocalResources provides resources
LocalResources provides resources,
LocalKamelConfig provides kamelConfig
) {
AppTheme {
Surface {
@@ -144,6 +148,7 @@ fun WindowDialog(
}
val resources = remember { AppScope.getInstance<XmlResourceBundle>() }
val kamelConfig = remember { AppScope.getInstance<KamelConfig>() }
val windowState = rememberWindowState(size = size, position = WindowPosition.Aligned(Alignment.Center))
Window(
@@ -162,7 +167,8 @@ fun WindowDialog(
) {
setIcon()
CompositionLocalProvider(
LocalResources provides resources
LocalResources provides resources,
LocalKamelConfig provides kamelConfig
) {
AppTheme {
Surface {

View File

@@ -0,0 +1,53 @@
/*
* 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.base.components
import androidx.compose.animation.core.FiniteAnimationSpec
import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.DefaultAlpha
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.layout.ContentScale
import io.kamel.core.Resource
import io.kamel.image.KamelImage as BaseKamelImage
@Composable
fun KamelImage(
resource: Resource<Painter>,
contentDescription: String?,
modifier: Modifier = Modifier,
alignment: Alignment = Alignment.Center,
contentScale: ContentScale = ContentScale.Fit,
alpha: Float = DefaultAlpha,
colorFilter: ColorFilter? = null,
onLoading: @Composable (Float) -> Unit = {
LoadingScreen(progress = it, modifier = modifier then Modifier.fillMaxSize())
},
onFailure: @Composable (Throwable) -> Unit = {
ErrorScreen(it.localizedMessage, modifier = modifier then Modifier.fillMaxSize())
},
crossfade: Boolean = true,
animationSpec: FiniteAnimationSpec<Float> = tween()
) {
BaseKamelImage(
resource,
contentDescription,
modifier,
alignment,
contentScale,
alpha,
colorFilter,
onLoading,
onFailure,
crossfade,
animationSpec
)
}

View File

@@ -1,100 +0,0 @@
/*
* 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.base.components
import androidx.compose.animation.Crossfade
import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.BoxWithConstraints
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.DefaultAlpha
import androidx.compose.ui.graphics.FilterQuality
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.layout.ContentScale
import ca.gosyer.common.di.AppScope
import ca.gosyer.data.server.Http
import ca.gosyer.util.compose.imageFromUrl
import ca.gosyer.util.system.kLogger
import io.ktor.client.features.onDownload
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit
private val logger = kLogger {}
private val semaphore = Semaphore(5)
@OptIn(DelicateCoroutinesApi::class)
@Composable
fun KtorImage(
imageUrl: String,
modifier: Modifier = Modifier.fillMaxSize(),
loadingModifier: Modifier = modifier,
contentDescription: String? = null,
alignment: Alignment = Alignment.Center,
contentScale: ContentScale = ContentScale.Fit,
alpha: Float = DefaultAlpha,
colorFilter: ColorFilter? = null,
filterQuality: FilterQuality = FilterQuality.Medium,
client: Http = remember { AppScope.getInstance() }
) {
BoxWithConstraints(modifier) {
val drawable = remember { mutableStateOf<ImageBitmap?>(null) }
val loading = remember { mutableStateOf(true) }
val progress = remember { mutableStateOf(0.0F) }
val error = remember { mutableStateOf<String?>(null) }
DisposableEffect(imageUrl) {
val handler = CoroutineExceptionHandler { _, throwable ->
logger.error(throwable) { "Error loading image $imageUrl" }
loading.value = false
error.value = throwable.message
}
val job = GlobalScope.launch(handler) {
if (drawable.value == null) {
semaphore.withPermit {
drawable.value = imageFromUrl(client, imageUrl) {
onDownload { bytesSentTotal, contentLength ->
progress.value = (bytesSentTotal.toFloat() / contentLength).coerceAtMost(1.0F)
}
}
}
}
loading.value = false
}
onDispose {
job.cancel()
drawable.value = null
}
}
Crossfade(drawable.value to loading.value) { (value, loading) ->
if (value != null) {
Image(
value,
modifier = Modifier.fillMaxSize(),
contentDescription = contentDescription,
alignment = alignment,
contentScale = contentScale,
alpha = alpha,
colorFilter = colorFilter,
filterQuality = filterQuality
)
} else {
LoadingScreen(loading, loadingModifier, progress.value, error.value)
}
}
}
}

View File

@@ -23,17 +23,19 @@ import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import io.kamel.core.Resource
@Composable
fun MangaGridItem(
title: String,
cover: String?,
cover: Resource<Painter>,
onClick: () -> Unit = {},
) {
val fontStyle = LocalTextStyle.current.merge(
@@ -50,9 +52,7 @@ fun MangaGridItem(
shape = RoundedCornerShape(4.dp)
) {
Box(modifier = Modifier.fillMaxSize()) {
if (cover != null) {
KtorImage(cover, contentScale = ContentScale.Crop)
}
KamelImage(cover, title, contentScale = ContentScale.Crop)
Box(modifier = Modifier.fillMaxSize().then(shadowGradient))
Text(
text = title,

View File

@@ -15,9 +15,11 @@ import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.painter.Painter
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import io.kamel.core.Resource
@Composable
fun MangaListItem(
@@ -35,18 +37,15 @@ fun MangaListItem(
@Composable
fun MangaListItemImage(
modifier: Modifier = Modifier,
imageUrl: String?
cover: Resource<Painter>,
contentDescription: String
) {
if (imageUrl != null) {
KtorImage(
imageUrl = imageUrl,
contentDescription = null,
modifier = modifier,
contentScale = ContentScale.Crop
)
} else {
ErrorScreen(modifier = modifier)
}
KamelImage(
cover,
contentDescription = contentDescription,
modifier = modifier,
contentScale = ContentScale.Crop
)
}
@Composable

View File

@@ -39,6 +39,7 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.FilterQuality
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.text.withStyle
@@ -50,7 +51,7 @@ import ca.gosyer.build.BuildConfig
import ca.gosyer.data.models.Extension
import ca.gosyer.ui.base.WindowDialog
import ca.gosyer.ui.base.components.ActionIcon
import ca.gosyer.ui.base.components.KtorImage
import ca.gosyer.ui.base.components.KamelImage
import ca.gosyer.ui.base.components.LoadingScreen
import ca.gosyer.ui.base.components.Toolbar
import ca.gosyer.ui.base.resources.stringResource
@@ -58,6 +59,7 @@ import ca.gosyer.ui.base.vm.viewModel
import ca.gosyer.util.compose.ThemedWindow
import ca.gosyer.util.compose.persistentLazyListState
import ca.gosyer.util.lang.launchApplication
import io.kamel.image.lazyPainterResource
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@@ -80,7 +82,6 @@ fun ExtensionsMenu() {
val vm = viewModel<ExtensionsMenuViewModel>()
val extensions by vm.extensions.collectAsState()
val isLoading by vm.isLoading.collectAsState()
val serverUrl by vm.serverUrl.collectAsState()
val search by vm.searchQuery.collectAsState()
if (isLoading) {
@@ -119,7 +120,6 @@ fun ExtensionsMenu() {
items(items) { extension ->
ExtensionItem(
extension,
serverUrl,
onInstallClicked = vm::install,
onUpdateClicked = vm::update,
onUninstallClicked = vm::uninstall
@@ -167,7 +167,6 @@ fun ExtensionsToolbar(
@Composable
fun ExtensionItem(
extension: Extension,
serverUrl: String,
onInstallClicked: (Extension) -> Unit,
onUpdateClicked: (Extension) -> Unit,
onUninstallClicked: (Extension) -> Unit
@@ -175,7 +174,7 @@ fun ExtensionItem(
Box(modifier = Modifier.fillMaxWidth().padding(end = 12.dp).height(50.dp).background(MaterialTheme.colors.background)) {
Row(verticalAlignment = Alignment.CenterVertically) {
Spacer(Modifier.width(4.dp))
KtorImage(extension.iconUrl(serverUrl), Modifier.size(50.dp))
KamelImage(lazyPainterResource(extension, filterQuality = FilterQuality.Medium), extension.name, Modifier.size(50.dp))
Spacer(Modifier.width(8.dp))
Column {
val title = buildAnnotatedString {

View File

@@ -8,7 +8,6 @@ package ca.gosyer.ui.extensions
import ca.gosyer.data.extension.ExtensionPreferences
import ca.gosyer.data.models.Extension
import ca.gosyer.data.server.ServerPreferences
import ca.gosyer.data.server.interactions.ExtensionInteractionHandler
import ca.gosyer.data.translation.XmlResourceBundle
import ca.gosyer.ui.base.vm.ViewModel
@@ -26,10 +25,8 @@ import javax.inject.Inject
class ExtensionsMenuViewModel @Inject constructor(
private val extensionHandler: ExtensionInteractionHandler,
private val resources: XmlResourceBundle,
serverPreferences: ServerPreferences,
extensionPreferences: ExtensionPreferences
) : ViewModel() {
val serverUrl = serverPreferences.serverUrl().stateIn(scope)
private val _enabledLangs = extensionPreferences.languages().asStateFlow()
val enabledLangs = _enabledLangs.asStateFlow()

View File

@@ -73,7 +73,6 @@ fun LibraryScreen(bundle: Bundle, onClickManga: (Long) -> Unit = { openMangaMenu
val displayMode by vm.displayMode.collectAsState()
val isLoading by vm.isLoading.collectAsState()
val error by vm.error.collectAsState()
val serverUrl by vm.serverUrl.collectAsState()
val query by vm.query.collectAsState()
// val sheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)
@@ -116,7 +115,6 @@ fun LibraryScreen(bundle: Bundle, onClickManga: (Long) -> Unit = { openMangaMenu
categories = categories,
displayMode = displayMode,
selectedPage = selectedCategoryIndex,
serverUrl = serverUrl,
getLibraryForPage = { vm.getLibraryForCategoryIndex(it).collectAsState() },
onPageChanged = vm::setSelectedPage,
onClickManga = onClickManga,
@@ -163,7 +161,6 @@ private fun LibraryPager(
categories: List<Category>,
displayMode: DisplayMode,
selectedPage: Int,
serverUrl: String,
getLibraryForPage: @Composable (Int) -> State<List<Manga>>,
onPageChanged: (Int) -> Unit,
onClickManga: (Long) -> Unit,
@@ -187,7 +184,6 @@ private fun LibraryPager(
when (displayMode) {
DisplayMode.CompactGrid -> LibraryMangaCompactGrid(
library = library,
serverUrl = serverUrl,
onClickManga = onClickManga,
onRemoveMangaClicked = onRemoveMangaClicked
)

View File

@@ -9,7 +9,6 @@ package ca.gosyer.ui.library
import ca.gosyer.data.library.LibraryPreferences
import ca.gosyer.data.models.Category
import ca.gosyer.data.models.Manga
import ca.gosyer.data.server.ServerPreferences
import ca.gosyer.data.server.interactions.CategoryInteractionHandler
import ca.gosyer.data.server.interactions.LibraryInteractionHandler
import ca.gosyer.ui.base.vm.ViewModel
@@ -73,11 +72,8 @@ class LibraryScreenViewModel @Inject constructor(
private val bundle: Bundle,
private val categoryHandler: CategoryInteractionHandler,
private val libraryHandler: LibraryInteractionHandler,
libraryPreferences: LibraryPreferences,
serverPreferences: ServerPreferences,
libraryPreferences: LibraryPreferences
) : ViewModel() {
val serverUrl = serverPreferences.serverUrl().stateIn(scope)
private val library = Library(MutableStateFlow(emptyList()), mutableMapOf())
val categories = library.categories.asStateFlow()

View File

@@ -19,7 +19,6 @@ import androidx.compose.material.LocalTextStyle
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@@ -27,19 +26,20 @@ import androidx.compose.ui.draw.drawWithCache
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.FilterQuality
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import ca.gosyer.data.models.Manga
import ca.gosyer.ui.base.components.KtorImage
import ca.gosyer.ui.base.components.KamelImage
import ca.gosyer.ui.base.components.contextMenuClickable
import io.kamel.image.lazyPainterResource
@Composable
fun LibraryMangaCompactGrid(
library: List<Manga>,
serverUrl: String,
onClickManga: (Long) -> Unit = {},
onRemoveMangaClicked: (Long) -> Unit = {}
) {
@@ -52,7 +52,6 @@ fun LibraryMangaCompactGrid(
manga = manga,
unread = null, // TODO
downloaded = null, // TODO
serverUrl = serverUrl,
onClick = { onClickManga(manga.id) }
) {
listOf(
@@ -68,11 +67,10 @@ private fun LibraryMangaCompactGridItem(
manga: Manga,
unread: Int?,
downloaded: Int?,
serverUrl: String,
onClick: () -> Unit = {},
contextMenuItems: () -> List<ContextMenuItem> = { emptyList() }
) {
val cover = remember(manga.id, serverUrl) { manga.cover(serverUrl) }
val cover = lazyPainterResource(manga, filterQuality = FilterQuality.Medium)
val fontStyle = LocalTextStyle.current.merge(
TextStyle(letterSpacing = 0.sp, fontFamily = FontFamily.SansSerif, fontSize = 14.sp)
)
@@ -87,9 +85,7 @@ private fun LibraryMangaCompactGridItem(
items = contextMenuItems
)
) {
if (cover != null) {
KtorImage(cover, contentScale = ContentScale.Crop)
}
KamelImage(cover, manga.title, contentScale = ContentScale.Crop)
Box(modifier = Modifier.fillMaxSize().then(shadowGradient))
Text(
text = manga.title,

View File

@@ -48,6 +48,8 @@ import com.github.weisj.darklaf.theme.IntelliJTheme
import com.github.zsoltk.compose.backpress.BackPressHandler
import com.github.zsoltk.compose.backpress.LocalBackPressHandler
import com.github.zsoltk.compose.savedinstancestate.Bundle
import io.kamel.core.config.KamelConfig
import io.kamel.image.config.LocalKamelConfig
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.flow.MutableStateFlow
@@ -94,6 +96,7 @@ suspend fun main() {
}
val resources = scope.getInstance<XmlResourceBundle>()
val kamelConfig = scope.getInstance<KamelConfig>()
// Set the Compose constants before any
// Swing functions are called
@@ -181,7 +184,8 @@ suspend fun main() {
CompositionLocalProvider(
LocalComposeWindow provides window,
LocalBackPressHandler provides backPressHandler,
LocalResources provides resources
LocalResources provides resources,
LocalKamelConfig provides kamelConfig
) {
Crossfade(serverService.initialized.collectAsState().value) { initialized ->
when (initialized) {

View File

@@ -40,6 +40,7 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.FilterQuality
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
@@ -50,7 +51,7 @@ import ca.gosyer.data.models.Manga
import ca.gosyer.ui.base.WindowDialog
import ca.gosyer.ui.base.components.ActionIcon
import ca.gosyer.ui.base.components.ErrorScreen
import ca.gosyer.ui.base.components.KtorImage
import ca.gosyer.ui.base.components.KamelImage
import ca.gosyer.ui.base.components.LoadingScreen
import ca.gosyer.ui.base.components.LocalMenuController
import ca.gosyer.ui.base.components.MenuController
@@ -61,6 +62,7 @@ import ca.gosyer.ui.reader.openReaderMenu
import ca.gosyer.util.compose.ThemedWindow
import ca.gosyer.util.lang.launchApplication
import com.google.accompanist.flowlayout.FlowRow
import io.kamel.image.lazyPainterResource
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.collect
@@ -84,7 +86,6 @@ fun MangaMenu(mangaId: Long, menuController: MenuController? = LocalMenuControll
val manga by vm.manga.collectAsState()
val chapters by vm.chapters.collectAsState()
val isLoading by vm.isLoading.collectAsState()
val serverUrl by vm.serverUrl.collectAsState()
val dateTimeFormatter by vm.dateTimeFormatter.collectAsState()
val categoriesExist by vm.categoriesExist.collectAsState()
@@ -128,7 +129,7 @@ fun MangaMenu(mangaId: Long, menuController: MenuController? = LocalMenuControll
val state = rememberLazyListState()
LazyColumn(state = state) {
item {
MangaItem(manga, serverUrl)
MangaItem(manga)
}
if (chapters.isNotEmpty()) {
items(chapters) { chapter ->
@@ -171,17 +172,17 @@ fun MangaMenu(mangaId: Long, menuController: MenuController? = LocalMenuControll
}
@Composable
fun MangaItem(manga: Manga, serverUrl: String) {
fun MangaItem(manga: Manga) {
BoxWithConstraints(Modifier.padding(8.dp)) {
if (maxWidth > 600.dp) {
Row {
Cover(manga, serverUrl, Modifier.width(300.dp))
Cover(manga, Modifier.width(300.dp))
Spacer(Modifier.width(16.dp))
MangaInfo(manga)
}
} else {
Column {
Cover(manga, serverUrl, Modifier.align(Alignment.CenterHorizontally))
Cover(manga, Modifier.align(Alignment.CenterHorizontally))
Spacer(Modifier.height(16.dp))
MangaInfo(manga)
}
@@ -190,17 +191,17 @@ fun MangaItem(manga: Manga, serverUrl: String) {
}
@Composable
private fun Cover(manga: Manga, serverUrl: String, modifier: Modifier = Modifier) {
private fun Cover(manga: Manga, modifier: Modifier = Modifier) {
Surface(
modifier = modifier then Modifier
.padding(4.dp),
shape = RoundedCornerShape(4.dp)
) {
Box(modifier = Modifier.fillMaxSize()) {
manga.cover(serverUrl)?.let {
KtorImage(it)
}
}
KamelImage(
lazyPainterResource(manga, filterQuality = FilterQuality.Medium),
manga.title,
Modifier.fillMaxSize()
)
}
}

View File

@@ -10,7 +10,6 @@ import ca.gosyer.data.download.DownloadService
import ca.gosyer.data.models.Category
import ca.gosyer.data.models.Chapter
import ca.gosyer.data.models.Manga
import ca.gosyer.data.server.ServerPreferences
import ca.gosyer.data.server.interactions.CategoryInteractionHandler
import ca.gosyer.data.server.interactions.ChapterInteractionHandler
import ca.gosyer.data.server.interactions.LibraryInteractionHandler
@@ -41,11 +40,8 @@ class MangaMenuViewModel @Inject constructor(
private val categoryHandler: CategoryInteractionHandler,
private val libraryHandler: LibraryInteractionHandler,
private val downloadService: DownloadService,
serverPreferences: ServerPreferences,
uiPreferences: UiPreferences,
) : ViewModel() {
val serverUrl = serverPreferences.serverUrl().stateIn(scope)
private val downloadingChapters = downloadService.registerWatch(params.mangaId)
private val _manga = MutableStateFlow<Manga?>(null)

View File

@@ -66,6 +66,8 @@ import ca.gosyer.ui.reader.navigation.navigationClickable
import ca.gosyer.ui.reader.viewer.ContinuousReader
import ca.gosyer.ui.reader.viewer.PagerReader
import ca.gosyer.util.lang.launchApplication
import io.kamel.core.config.KamelConfig
import io.kamel.image.config.LocalKamelConfig
import kotlinx.coroutines.DelicateCoroutinesApi
@OptIn(DelicateCoroutinesApi::class)
@@ -79,6 +81,7 @@ fun openReaderMenu(chapterIndex: Int, mangaId: Long) {
) = windowSettings.get().get()
val resources = AppScope.getInstance<XmlResourceBundle>()
val kamelConfig = AppScope.getInstance<KamelConfig>()
launchApplication {
var shortcuts by remember {
@@ -110,7 +113,8 @@ fun openReaderMenu(chapterIndex: Int, mangaId: Long) {
setIcon()
CompositionLocalProvider(
LocalComposeWindow provides window,
LocalResources provides resources
LocalResources provides resources,
LocalKamelConfig provides kamelConfig
) {
AppTheme {
ReaderMenu(chapterIndex, mangaId) { shortcuts = it }

View File

@@ -35,11 +35,12 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.FilterQuality
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp
import ca.gosyer.build.BuildConfig
import ca.gosyer.ui.base.components.ActionIcon
import ca.gosyer.ui.base.components.KtorImage
import ca.gosyer.ui.base.components.KamelImage
import ca.gosyer.ui.base.components.Toolbar
import ca.gosyer.ui.base.components.combinedMouseClickable
import ca.gosyer.ui.base.resources.stringResource
@@ -54,6 +55,7 @@ import ca.gosyer.util.lang.launchApplication
import com.github.zsoltk.compose.savedinstancestate.Bundle
import com.github.zsoltk.compose.savedinstancestate.BundleScope
import com.github.zsoltk.compose.savedinstancestate.LocalSavedInstanceState
import io.kamel.image.lazyPainterResource
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
@@ -91,7 +93,6 @@ fun SourcesMenu(bundle: Bundle, onSourceSettingsClick: (Long) -> Unit, onMangaCl
val selectedSourceTab by vm.selectedSourceTab.collectAsState()
val sourceSearchEnabled by vm.sourceSearchEnabled.collectAsState()
val sourceSearchQuery by vm.sourceSearchQuery.collectAsState()
val serverUrl by vm.serverUrl.collectAsState()
Column {
Toolbar(
selectedSourceTab?.name ?: stringResource("location_sources"),
@@ -165,7 +166,13 @@ fun SourcesMenu(bundle: Bundle, onSourceSettingsClick: (Long) -> Unit, onMangaCl
.requiredSize(50.dp)
.align(Alignment.Center)
if (source != null) {
KtorImage(source.iconUrl(serverUrl), modifier = modifier)
Box(Modifier.align(Alignment.Center)) {
KamelImage(
lazyPainterResource(source, filterQuality = FilterQuality.Medium),
source.displayName,
modifier
)
}
} else {
Icon(Icons.Rounded.Home, stringResource("sources_home"), modifier = modifier)
}
@@ -180,7 +187,7 @@ fun SourcesMenu(bundle: Bundle, onSourceSettingsClick: (Long) -> Unit, onMangaCl
if (selectedSource != null) {
SourceScreen(it, selectedSource, onMangaClick, vm::enableSearch, vm::setSearch)
} else {
SourceHomeScreen(isLoading, sources, serverUrl, vm::addTab)
SourceHomeScreen(isLoading, sources, vm::addTab)
}
}
}

View File

@@ -8,7 +8,6 @@ package ca.gosyer.ui.sources
import ca.gosyer.data.catalog.CatalogPreferences
import ca.gosyer.data.models.Source
import ca.gosyer.data.server.ServerPreferences
import ca.gosyer.data.server.interactions.SourceInteractionHandler
import ca.gosyer.ui.base.vm.ViewModel
import ca.gosyer.util.lang.throwIfCancellation
@@ -25,11 +24,8 @@ import javax.inject.Inject
class SourcesMenuViewModel @Inject constructor(
private val bundle: Bundle,
private val sourceHandler: SourceInteractionHandler,
serverPreferences: ServerPreferences,
catalogPreferences: CatalogPreferences
) : ViewModel() {
val serverUrl = serverPreferences.serverUrl().stateIn(scope)
private val _languages = catalogPreferences.languages().asStateFlow()
val languages = _languages.asStateFlow()

View File

@@ -33,17 +33,18 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.FilterQuality
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import ca.gosyer.data.models.Source
import ca.gosyer.ui.base.components.KtorImage
import ca.gosyer.ui.base.components.KamelImage
import ca.gosyer.ui.base.components.LoadingScreen
import io.kamel.image.lazyPainterResource
@Composable
fun SourceHomeScreen(
isLoading: Boolean,
sources: List<Source>,
serverUrl: String,
onSourceClicked: (Source) -> Unit
) {
if (sources.isEmpty()) {
@@ -51,7 +52,7 @@ fun SourceHomeScreen(
} else {
Box(Modifier.fillMaxSize(), Alignment.TopCenter) {
val state = rememberLazyListState()
SourceCategory(sources, serverUrl, onSourceClicked, state)
SourceCategory(sources, onSourceClicked, state)
/*val sourcesByLang = sources.groupBy { it.lang.toLowerCase() }.toList()
LazyColumn(state = state) {
items(sourcesByLang) { (lang, sources) ->
@@ -75,7 +76,6 @@ fun SourceHomeScreen(
@Composable
fun SourceCategory(
sources: List<Source>,
serverUrl: String,
onSourceClicked: (Source) -> Unit,
state: LazyListState
) {
@@ -83,7 +83,6 @@ fun SourceCategory(
items(sources) { source ->
SourceItem(
source,
serverUrl,
onSourceClicked = onSourceClicked
)
Spacer(Modifier.height(8.dp))
@@ -94,7 +93,6 @@ fun SourceCategory(
@Composable
fun SourceItem(
source: Source,
serverUrl: String,
onSourceClicked: (Source) -> Unit
) {
TooltipArea(
@@ -117,7 +115,7 @@ fun SourceItem(
},
horizontalAlignment = Alignment.CenterHorizontally
) {
KtorImage(source.iconUrl(serverUrl), Modifier.size(96.dp))
KamelImage(lazyPainterResource(source, filterQuality = FilterQuality.Medium), source.displayName, Modifier.size(96.dp))
Spacer(Modifier.height(4.dp))
Text(
"${source.name} (${source.lang.uppercase()})",

View File

@@ -21,6 +21,7 @@ import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.FilterQuality
import androidx.compose.ui.unit.dp
import ca.gosyer.data.models.Manga
import ca.gosyer.data.models.Source
@@ -30,6 +31,7 @@ import ca.gosyer.ui.base.resources.stringResource
import ca.gosyer.ui.base.vm.viewModel
import ca.gosyer.util.compose.persistentLazyListState
import com.github.zsoltk.compose.savedinstancestate.Bundle
import io.kamel.image.lazyPainterResource
@Composable
fun SourceScreen(
@@ -46,7 +48,6 @@ fun SourceScreen(
val hasNextPage by vm.hasNextPage.collectAsState()
val loading by vm.loading.collectAsState()
val isLatest by vm.isLatest.collectAsState()
val serverUrl by vm.serverUrl.collectAsState()
LaunchedEffect(Unit) {
setSearch(vm::search)
@@ -67,7 +68,6 @@ fun SourceScreen(
hasNextPage,
source.supportsLatest,
isLatest,
serverUrl,
onLoadNextPage = vm::loadNextPage,
onMangaClick = onMangaClick,
onClickMode = vm::setMode
@@ -82,7 +82,6 @@ private fun MangaTable(
hasNextPage: Boolean = false,
supportsLatest: Boolean,
isLatest: Boolean,
serverUrl: String,
onLoadNextPage: () -> Unit,
onMangaClick: (Long) -> Unit,
onClickMode: (Boolean) -> Unit
@@ -111,7 +110,7 @@ private fun MangaTable(
}
MangaGridItem(
title = manga.title,
cover = manga.cover(serverUrl),
cover = lazyPainterResource(manga, filterQuality = FilterQuality.Medium),
onClick = {
onMangaClick(manga.id)
}

View File

@@ -9,7 +9,6 @@ package ca.gosyer.ui.sources.components
import ca.gosyer.data.models.Manga
import ca.gosyer.data.models.MangaPage
import ca.gosyer.data.models.Source
import ca.gosyer.data.server.ServerPreferences
import ca.gosyer.data.server.interactions.SourceInteractionHandler
import ca.gosyer.ui.base.vm.ViewModel
import ca.gosyer.util.compose.saveBooleanInBundle
@@ -26,21 +25,17 @@ import javax.inject.Inject
class SourceScreenViewModel(
private val source: Source,
private val bundle: Bundle,
private val sourceHandler: SourceInteractionHandler,
serverPreferences: ServerPreferences
private val sourceHandler: SourceInteractionHandler
) : ViewModel() {
@Inject constructor(
params: Params,
sourceHandler: SourceInteractionHandler,
serverPreferences: ServerPreferences
sourceHandler: SourceInteractionHandler
) : this(
params.source,
params.bundle,
sourceHandler,
serverPreferences
sourceHandler
)
val serverUrl = serverPreferences.serverUrl().stateIn(scope)
private val _mangas = saveObjectInBundle(scope, bundle, MANGAS_KEY) { emptyList<Manga>() }
val mangas = _mangas.asStateFlow()

View File

@@ -22,6 +22,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.FilterQuality
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import ca.gosyer.data.models.Chapter
@@ -37,6 +38,7 @@ import ca.gosyer.ui.base.components.Toolbar
import ca.gosyer.ui.base.components.mangaAspectRatio
import ca.gosyer.ui.base.resources.stringResource
import ca.gosyer.ui.base.vm.viewModel
import io.kamel.image.lazyPainterResource
@Composable
fun UpdatesMenu(
@@ -44,7 +46,6 @@ fun UpdatesMenu(
openManga: (Long) -> Unit
) {
val vm = viewModel<UpdatesMenuViewModel>()
val serverUrl by vm.serverUrl.collectAsState()
val isLoading by vm.isLoading.collectAsState()
val updates by vm.updates.collectAsState()
Column {
@@ -57,7 +58,6 @@ fun UpdatesMenu(
val manga = it.manga!!
val chapter = it.chapter
UpdatesItem(
serverUrl,
it,
onClickItem = { openChapter(chapter.index, chapter.mangaId) },
onClickCover = { openManga(manga.id) },
@@ -73,7 +73,6 @@ fun UpdatesMenu(
@Composable
fun UpdatesItem(
serverUrl: String,
chapterDownloadItem: ChapterDownloadItem,
onClickItem: () -> Unit,
onClickCover: () -> Unit,
@@ -101,7 +100,8 @@ fun UpdatesItem(
.padding(start = 16.dp, top = 8.dp, bottom = 8.dp)
.clip(MaterialTheme.shapes.medium)
.clickable { onClickCover() },
imageUrl = manga.cover(serverUrl)
cover = lazyPainterResource(manga, filterQuality = FilterQuality.Medium),
contentDescription = manga.title
)
MangaListItemColumn(
modifier = Modifier

View File

@@ -8,11 +8,9 @@ package ca.gosyer.ui.updates
import ca.gosyer.data.download.DownloadService
import ca.gosyer.data.models.Chapter
import ca.gosyer.data.server.ServerPreferences
import ca.gosyer.data.server.interactions.ChapterInteractionHandler
import ca.gosyer.data.server.interactions.UpdatesInteractionHandler
import ca.gosyer.ui.base.components.ChapterDownloadItem
import ca.gosyer.ui.base.prefs.asStateIn
import ca.gosyer.ui.base.vm.ViewModel
import ca.gosyer.util.lang.throwIfCancellation
import kotlinx.coroutines.flow.MutableStateFlow
@@ -26,10 +24,8 @@ import javax.inject.Inject
class UpdatesMenuViewModel @Inject constructor(
private val chapterHandler: ChapterInteractionHandler,
private val updatesHandler: UpdatesInteractionHandler,
private val serverPreferences: ServerPreferences,
private val downloadService: DownloadService
) : ViewModel() {
val serverUrl = serverPreferences.serverUrl().asStateIn(scope).asStateFlow()
private val _isLoading = MutableStateFlow(true)
val isLoading = _isLoading.asStateFlow()