/* * 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 . */ package xyz.quaver.pupil.ui import android.content.Intent import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.viewModels import androidx.compose.foundation.Image import androidx.compose.foundation.border import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.material.* 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.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 import org.kodein.log.LoggerFactory import org.kodein.log.newLogger import xyz.quaver.graphics.subsampledimage.* import xyz.quaver.io.FileX import xyz.quaver.pupil.R import xyz.quaver.pupil.ui.composable.FloatingActionButtonState import xyz.quaver.pupil.ui.composable.MultipleFloatingActionButton import xyz.quaver.pupil.ui.composable.SubFabItem import xyz.quaver.pupil.ui.theme.PupilTheme import xyz.quaver.pupil.ui.viewmodel.ReaderViewModel import xyz.quaver.pupil.util.FileXImageSource import kotlin.math.abs class ReaderActivity : ComponentActivity(), DIAware { override val di by closestDI() private val model: ReaderViewModel by viewModels() private val logger = newLogger(LoggerFactory.default) @OptIn(ExperimentalCoilApi::class) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) model.handleIntent(intent) model.load() setContent { var isFABExpanded by remember { mutableStateOf(FloatingActionButtonState.COLLAPSED) } val isFullscreen by model.isFullscreen.observeAsState(false) val imageSources = remember { mutableStateListOf() } val imageHeights = remember { mutableStateListOf() } val states = remember { mutableStateListOf() } 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" } model.imageList.forEachIndexed { i, image -> if (imageSources[i] == null && image != null) imageSources[i] = kotlin.runCatching { FileXImageSource(FileX(this@ReaderActivity, image)) }.onFailure { logger.warning(it) model.error(i) }.getOrNull() } logger.info { "${imageSources.count { it == null }} nulls" } } WindowInsetsControllerCompat(window, window.decorView).run { if (isFullscreen) { hide(WindowInsetsCompat.Type.systemBars()) systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE } else show(WindowInsetsCompat.Type.systemBars()) } PupilTheme { Scaffold( topBar = { if (!isFullscreen) TopAppBar( title = { Text( model.title ?: stringResource(R.string.reader_loading), color = MaterialTheme.colors.onSecondary ) }, actions = { model.sourceIcon?.let { sourceIcon -> Image( modifier = Modifier.size(36.dp), painter = painterResource(id = sourceIcon), contentDescription = null ) } } ) }, floatingActionButton = { if (!isFullscreen) MultipleFloatingActionButton( items = listOf( SubFabItem( icon = Icons.Default.Fullscreen, label = stringResource(id = R.string.reader_fab_fullscreen) ) { model.isFullscreen.postValue(true) } ), targetState = isFABExpanded, onStateChanged = { isFABExpanded = it } ) } ) { 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( 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()) } } 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) ) } } } } override fun onNewIntent(intent: Intent) { super.onNewIntent(intent) model.handleIntent(intent) } override fun onBackPressed() { when { model.isFullscreen.value == true -> model.isFullscreen.postValue(false) else -> super.onBackPressed() } } }