Initial downloads menu implementation

This commit is contained in:
Syer10
2021-06-15 16:54:13 -04:00
parent 28c8de99bc
commit 51e8718c29
14 changed files with 567 additions and 179 deletions

View File

@@ -16,8 +16,10 @@ import ca.gosyer.data.server.Http
import ca.gosyer.data.server.HttpProvider
import ca.gosyer.data.server.ServerPreferences
import ca.gosyer.data.server.ServerService
import ca.gosyer.data.server.interactions.BackupInteractionHandler
import ca.gosyer.data.server.interactions.CategoryInteractionHandler
import ca.gosyer.data.server.interactions.ChapterInteractionHandler
import ca.gosyer.data.server.interactions.DownloadInteractionHandler
import ca.gosyer.data.server.interactions.ExtensionInteractionHandler
import ca.gosyer.data.server.interactions.LibraryInteractionHandler
import ca.gosyer.data.server.interactions.MangaInteractionHandler
@@ -58,10 +60,14 @@ val DataModule = module {
.toProvider(HttpProvider::class)
.providesSingleton()
bind<BackupInteractionHandler>()
.toClass<BackupInteractionHandler>()
bind<CategoryInteractionHandler>()
.toClass<CategoryInteractionHandler>()
bind<ChapterInteractionHandler>()
.toClass<ChapterInteractionHandler>()
bind<DownloadInteractionHandler>()
.toClass<DownloadInteractionHandler>()
bind<ExtensionInteractionHandler>()
.toClass<ExtensionInteractionHandler>()
bind<LibraryInteractionHandler>()

View File

@@ -40,6 +40,10 @@ class DownloadService @Inject constructor(
private val serverUrl = serverPreferences.serverUrl().stateIn(GlobalScope)
private val _downloaderStatus = MutableStateFlow(DownloaderStatus.Stopped)
val downloaderStatus = _downloaderStatus.asStateFlow()
private val _downloadQueue = MutableStateFlow(emptyList<DownloadChapter>())
val downloadQueue = _downloadQueue.asStateFlow()
private val watching = mutableMapOf<Long, MutableSharedFlow<List<DownloadChapter>>>()
init {
@@ -58,6 +62,7 @@ class DownloadService @Inject constructor(
frame as Frame.Text
val status = json.decodeFromString<DownloadStatus>(frame.readText())
_downloaderStatus.value = status.status
_downloadQueue.value = status.queue
val queue = status.queue.groupBy { it.mangaId }
watching.forEach { (mangaId, flow) ->
flow.emit(queue[mangaId].orEmpty())

View File

@@ -13,8 +13,8 @@ import kotlinx.serialization.Serializable
data class DownloadChapter(
val chapterIndex: Int,
val mangaId: Long,
var state: DownloadState = DownloadState.Queued,
var progress: Float = 0f,
var tries: Int = 0,
var chapter: Chapter? = null,
val state: DownloadState = DownloadState.Queued,
val progress: Float = 0f,
val tries: Int = 0,
val chapter: Chapter? = null,
)

View File

@@ -0,0 +1,46 @@
/*
* 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.foundation.BoxWithTooltip
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Surface
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.unit.DpOffset
import androidx.compose.ui.unit.dp
@Composable
fun BoxWithTooltipSurface(
tooltip: @Composable () -> Unit,
modifier: Modifier = Modifier,
contentAlignment: Alignment = Alignment.TopStart,
propagateMinConstraints: Boolean = false,
delay: Int = 500,
offset: DpOffset = DpOffset.Zero,
content: @Composable BoxScope.() -> Unit
) {
BoxWithTooltip(
{
Surface(
modifier = Modifier.shadow(4.dp),
shape = RoundedCornerShape(4.dp),
elevation = 4.dp,
content = tooltip
)
},
modifier,
contentAlignment,
propagateMinConstraints,
delay,
offset,
content
)
}

View File

@@ -26,7 +26,7 @@ import androidx.compose.ui.semantics.Role
import androidx.compose.ui.unit.IntOffset
import java.awt.event.MouseEvent
suspend fun AwaitPointerEventScope.awaitEventFirstDown(): PointerEvent {
internal suspend fun AwaitPointerEventScope.awaitEventFirstDown(): PointerEvent {
var event: PointerEvent
do {
event = awaitPointerEvent()

View File

@@ -0,0 +1,67 @@
/*
* 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.foundation.clickable
import androidx.compose.foundation.gestures.forEachGesture
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.size
import androidx.compose.material.DropdownMenu
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp
@Composable
fun DropdownIconButton(
key: Any? = Unit,
dropdownItems: @Composable ColumnScope.() -> Unit,
content: @Composable BoxScope.() -> Unit
) {
var showMenu by remember(key) { mutableStateOf(false) }
var offset by remember(key) { mutableStateOf(DpOffset(0.dp, 0.dp)) }
DropdownMenu(
expanded = showMenu,
onDismissRequest = { showMenu = false },
offset = offset,
content = dropdownItems
)
Box(
modifier = Modifier.fillMaxHeight()
.size(48.dp)
.clickable(
remember { MutableInteractionSource() },
role = Role.Button,
indication = rememberRipple(bounded = false, radius = 24.dp)
) {
showMenu = true
}
.pointerInput(Unit) {
forEachGesture {
awaitPointerEventScope {
awaitEventFirstDown().mouseEvent?.let {
offset = DpOffset(it.x.dp, it.y.dp)
}
}
}
},
contentAlignment = Alignment.Center,
content = content
)
}

View File

@@ -36,6 +36,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
@@ -94,13 +95,22 @@ fun Toolbar(
Row {
actions()
if (closable) {
IconButton(
onClick = onClose
ActionIcon(onClick = onClose, "Close", Icons.Default.Close)
}
}
}
}
}
@Composable
fun ActionIcon(onClick: () -> Unit, contentDescription: String, icon: ImageVector) {
BoxWithTooltipSurface(
{
Text(contentDescription, modifier = Modifier.padding(10.dp))
}
) {
Icon(Icons.Default.Close, "close", Modifier.size(52.dp))
}
}
}
IconButton(onClick = onClick) {
Icon(icon, contentDescription, Modifier.size(52.dp))
}
}
}

View File

@@ -0,0 +1,144 @@
/*
* 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.downloads
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredWidth
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.ContentAlpha
import androidx.compose.material.DropdownMenuItem
import androidx.compose.material.Icon
import androidx.compose.material.LinearProgressIndicator
import androidx.compose.material.LocalContentColor
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ClearAll
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.Pause
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import ca.gosyer.BuildConfig
import ca.gosyer.data.download.model.DownloadChapter
import ca.gosyer.data.download.model.DownloaderStatus
import ca.gosyer.data.models.Chapter
import ca.gosyer.ui.base.components.ActionIcon
import ca.gosyer.ui.base.components.DropdownIconButton
import ca.gosyer.ui.base.components.Toolbar
import ca.gosyer.ui.base.vm.viewModel
import ca.gosyer.util.compose.ThemedWindow
fun openDownloadsMenu() {
ThemedWindow(BuildConfig.NAME) {
DownloadsMenu()
}
}
@Composable
fun DownloadsMenu() {
val vm = viewModel<DownloadsMenuViewModel>()
val downloadQueue by vm.downloadQueue.collectAsState()
Surface {
Column {
Toolbar(
"Downloads",
closable = false,
actions = {
val downloadStatus by vm.downloaderStatus.collectAsState()
if (downloadStatus == DownloaderStatus.Started) {
ActionIcon(onClick = vm::pause, "Pause", Icons.Default.Pause)
} else {
ActionIcon(onClick = vm::start, "Continue", Icons.Default.PlayArrow)
}
ActionIcon(onClick = vm::clear, "Clear queue", Icons.Default.ClearAll)
}
)
LazyColumn(Modifier.fillMaxSize()) {
items(downloadQueue) {
downloadsItem(
it,
vm::stopDownload,
vm::moveToBottom
)
}
}
}
}
}
@Composable
private fun downloadsItem(
chapter: DownloadChapter,
onDownloadCancel: (Chapter?) -> Unit,
onMoveDownloadToBottom: (Chapter?) -> Unit
) {
Row(
modifier = Modifier.fillMaxWidth()
.height(56.dp)
.padding(vertical = 8.dp),
horizontalArrangement = Arrangement.SpaceAround
) {
Column(Modifier.fillMaxHeight(), verticalArrangement = Arrangement.SpaceAround) {
Row(Modifier.fillMaxWidth().padding(horizontal = 32.dp), horizontalArrangement = Arrangement.SpaceBetween) {
Text(chapter.chapter?.name.toString(), maxLines = 1, overflow = TextOverflow.Ellipsis)
// Spacer(Modifier.width(16.dp))
if (chapter.chapter?.pageCount != null && chapter.chapter.pageCount != -1) {
Text(
"${(chapter.chapter.pageCount * chapter.progress).toInt()}/${chapter.chapter.pageCount}",
Modifier.padding(start = 16.dp).requiredWidth(IntrinsicSize.Max),
style = MaterialTheme.typography.body2,
color = LocalContentColor.current.copy(alpha = ContentAlpha.disabled),
maxLines = 1,
overflow = TextOverflow.Visible
)
} else {
Spacer(Modifier.width(32.dp))
}
}
Spacer(Modifier.height(4.dp))
LinearProgressIndicator(
chapter.progress,
Modifier.fillMaxWidth()
.padding(start = 32.dp, end = 16.dp, bottom = 8.dp)
)
}
DropdownIconButton(
chapter.mangaId to chapter.chapterIndex,
{
DropdownMenuItem(onClick = { onDownloadCancel(chapter.chapter) }) {
Text("Cancel")
}
DropdownMenuItem(onClick = { onMoveDownloadToBottom(chapter.chapter) }) {
Text("Move to bottom")
}
}
) {
Icon(
Icons.Default.MoreVert,
null
)
}
}
}

View File

@@ -0,0 +1,57 @@
/*
* 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.downloads
import ca.gosyer.data.download.DownloadService
import ca.gosyer.data.models.Chapter
import ca.gosyer.data.server.interactions.ChapterInteractionHandler
import ca.gosyer.data.server.interactions.DownloadInteractionHandler
import ca.gosyer.ui.base.vm.ViewModel
import kotlinx.coroutines.launch
import javax.inject.Inject
class DownloadsMenuViewModel @Inject constructor(
private val downloadService: DownloadService,
private val downloadsHandler: DownloadInteractionHandler,
private val chapterHandler: ChapterInteractionHandler
) : ViewModel() {
val downloaderStatus get() = downloadService.downloaderStatus
val downloadQueue get() = downloadService.downloadQueue
fun start() {
scope.launch {
downloadsHandler.startDownloading()
}
}
fun pause() {
scope.launch {
downloadsHandler.stopDownloading()
}
}
fun clear() {
scope.launch {
downloadsHandler.clearDownloadQueue()
}
}
fun stopDownload(chapter: Chapter?) {
chapter ?: return
scope.launch {
chapterHandler.deleteChapterDownload(chapter)
}
}
fun moveToBottom(chapter: Chapter?) {
chapter ?: return
scope.launch {
chapterHandler.deleteChapterDownload(chapter)
chapterHandler.queueChapterDownload(chapter)
}
}
}

View File

@@ -6,7 +6,6 @@
package ca.gosyer.ui.extensions
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.VerticalScrollbar
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
@@ -26,6 +25,7 @@ import androidx.compose.foundation.rememberScrollbarAdapter
import androidx.compose.material.Button
import androidx.compose.material.ContentAlpha
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
@@ -39,6 +39,7 @@ import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import ca.gosyer.BuildConfig
import ca.gosyer.data.models.Extension
import ca.gosyer.ui.base.components.KtorImage
import ca.gosyer.ui.base.components.LoadingScreen
@@ -49,12 +50,11 @@ import ca.gosyer.util.compose.persistentLazyListState
import java.util.Locale
fun openExtensionsMenu() {
ThemedWindow(title = "TachideskJUI - Extensions", size = IntSize(550, 700)) {
ThemedWindow(BuildConfig.NAME, size = IntSize(550, 700)) {
ExtensionsMenu()
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun ExtensionsMenu() {
val vm = viewModel<ExtensionsMenuViewModel>()
@@ -63,7 +63,7 @@ fun ExtensionsMenu() {
val serverUrl by vm.serverUrl.collectAsState()
val search by vm.searchQuery.collectAsState()
Box(Modifier.fillMaxSize().background(MaterialTheme.colors.background)) {
Surface(Modifier.fillMaxSize()) {
if (isLoading) {
LoadingScreen(isLoading)
} else {

View File

@@ -13,6 +13,7 @@ import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.material.ScrollableTabRow
import androidx.compose.material.Surface
import androidx.compose.material.Tab
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
@@ -24,6 +25,7 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.util.fastForEachIndexed
import ca.gosyer.BuildConfig
import ca.gosyer.data.library.model.DisplayMode
import ca.gosyer.data.models.Category
import ca.gosyer.data.models.Manga
@@ -35,7 +37,7 @@ import com.google.accompanist.pager.HorizontalPager
import com.google.accompanist.pager.PagerState
fun openLibraryMenu() {
ThemedWindow {
ThemedWindow(BuildConfig.NAME) {
LibraryScreen()
}
}
@@ -50,6 +52,7 @@ fun LibraryScreen(onClickManga: (Long) -> Unit = { openMangaMenu(it) }) {
val serverUrl by vm.serverUrl.collectAsState()
// val sheetState = rememberModalBottomSheetState(ModalBottomSheetValue.Hidden)
Surface {
if (categories.isEmpty()) {
LoadingScreen(isLoading)
} else {
@@ -92,6 +95,7 @@ fun LibraryScreen(onClickManga: (Long) -> Unit = { openMangaMenu(it) }) {
// }
}
}
}
@Composable
private fun LibraryTabs(

View File

@@ -15,23 +15,30 @@ import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Card
import androidx.compose.material.ContentAlpha
import androidx.compose.material.LocalContentColor
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Surface
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Book
import androidx.compose.material.icons.filled.Download
import androidx.compose.material.icons.filled.Explore
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.filled.Store
import androidx.compose.material.icons.outlined.Book
import androidx.compose.material.icons.outlined.Download
import androidx.compose.material.icons.outlined.Explore
import androidx.compose.material.icons.outlined.Settings
import androidx.compose.material.icons.outlined.Store
import androidx.compose.runtime.Composable
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
@@ -42,9 +49,14 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import ca.gosyer.BuildConfig
import ca.gosyer.data.ui.model.StartScreen
import ca.gosyer.ui.base.components.combinedMouseClickable
import ca.gosyer.ui.base.vm.viewModel
import ca.gosyer.ui.downloads.DownloadsMenu
import ca.gosyer.ui.downloads.DownloadsMenuViewModel
import ca.gosyer.ui.extensions.ExtensionsMenu
import ca.gosyer.ui.extensions.openExtensionsMenu
import ca.gosyer.ui.library.LibraryScreen
import ca.gosyer.ui.library.openLibraryMenu
import ca.gosyer.ui.manga.MangaMenu
import ca.gosyer.ui.settings.SettingsAdvancedScreen
import ca.gosyer.ui.settings.SettingsAppearance
@@ -56,6 +68,8 @@ import ca.gosyer.ui.settings.SettingsReaderScreen
import ca.gosyer.ui.settings.SettingsScreen
import ca.gosyer.ui.settings.SettingsServerScreen
import ca.gosyer.ui.sources.SourcesMenu
import ca.gosyer.ui.sources.openSourcesMenu
import com.github.zsoltk.compose.router.BackStack
import com.github.zsoltk.compose.router.Router
import com.github.zsoltk.compose.savedinstancestate.Bundle
import com.github.zsoltk.compose.savedinstancestate.BundleScope
@@ -66,8 +80,18 @@ fun MainMenu(rootBundle: Bundle) {
Surface {
Router("TopLevel", vm.startScreen.toRoute()) { backStack ->
Row {
Surface(elevation = 2.dp) {
Column(Modifier.width(200.dp).fillMaxHeight(),) {
SideMenu(backStack)
MainWindow(rootBundle, backStack)
}
}
}
}
@Composable
fun SideMenu(backStack: BackStack<Route>) {
Surface(Modifier.width(200.dp).fillMaxHeight(), elevation = 2.dp) {
Box(Modifier.fillMaxSize()) {
Column(Modifier.fillMaxSize()) {
Box(Modifier.fillMaxWidth().height(60.dp)) {
Text(
BuildConfig.NAME,
@@ -76,16 +100,44 @@ fun MainMenu(rootBundle: Bundle) {
)
}
Spacer(Modifier.height(20.dp))
remember { TopLevelMenus.values() }.forEach { topLevelMenu ->
MainMenuItem(
remember { TopLevelMenus.values().filter { it.top } }.forEach { topLevelMenu ->
SideMenuItem(
topLevelMenu,
backStack.elements.first() == topLevelMenu.menu
backStack
)
}
Box(Modifier.fillMaxSize()) {
Column(Modifier.align(Alignment.BottomStart).padding(bottom = 8.dp)) {
remember { TopLevelMenus.values().filterNot { it.top } }.forEach { topLevelMenu ->
SideMenuItem(
topLevelMenu,
backStack
)
}
}
}
}
}
}
}
@Composable
fun SideMenuItem(topLevelMenu: TopLevelMenus, backStack: BackStack<Route>) {
MainMenuItem(
backStack.elements.first() == topLevelMenu.menu,
topLevelMenu.text,
topLevelMenu.menu,
topLevelMenu.selectedIcon,
topLevelMenu.unselectedIcon,
topLevelMenu.openInNewWindow,
topLevelMenu.extraInfo
) {
backStack.newRoot(it)
}
}
}
}
@Composable
fun MainWindow(rootBundle: Bundle, backStack: BackStack<Route>) {
Column(Modifier.fillMaxSize()) {
BundleScope("K${backStack.lastIndex}", rootBundle, false) {
when (val routing = backStack.last()) {
@@ -97,6 +149,7 @@ fun MainMenu(rootBundle: Bundle) {
}
is Route.Extensions -> ExtensionsMenu()
is Route.Manga -> MangaMenu(routing.mangaId, backStack)
is Route.Downloads -> DownloadsMenu()
is Route.Settings -> SettingsScreen(backStack)
is Route.SettingsGeneral -> SettingsGeneralScreen(backStack)
@@ -115,38 +168,55 @@ fun MainMenu(rootBundle: Bundle) {
}
}
}
}
}
}
@Composable
fun MainMenuItem(menu: TopLevelMenus, selected: Boolean, onClick: (Route) -> Unit) {
fun MainMenuItem(
selected: Boolean,
text: String,
menu: Route,
selectedIcon: ImageVector,
unselectedIcon: ImageVector,
onMiddleClick: () -> Unit,
extraInfo: (@Composable () -> Unit)? = null,
onClick: (Route) -> Unit
) {
Card(
{ onClick(menu.menu) },
Modifier.fillMaxWidth().height(40.dp),
Modifier.fillMaxWidth(),
backgroundColor = if (!selected) {
Color.Transparent
} else {
MaterialTheme.colors.primary.copy(0.30F)
},
contentColor = Color.Transparent,
elevation = 0.dp,
shape = RoundedCornerShape(8.dp)
) {
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxSize()) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth()
.height(40.dp)
.combinedMouseClickable(
onClick = { onClick(menu) },
onMiddleClick = { onMiddleClick() }
)
) {
Spacer(Modifier.width(16.dp))
Image(
if (selected) {
menu.selectedIcon
selectedIcon
} else {
menu.unselectedIcon
unselectedIcon
},
menu.text,
modifier = Modifier.size(20.dp),
text,
Modifier.size(20.dp),
colorFilter = ColorFilter.tint(MaterialTheme.colors.onSurface)
)
Spacer(Modifier.width(16.dp))
Text(menu.text, color = MaterialTheme.colors.onSurface)
Column {
Text(text, color = MaterialTheme.colors.onSurface)
if (extraInfo != null) {
extraInfo()
}
}
}
}
}
@@ -157,11 +227,33 @@ fun StartScreen.toRoute() = when (this) {
StartScreen.Extensions -> Route.Extensions
}
enum class TopLevelMenus(val text: String, val unselectedIcon: ImageVector, val selectedIcon: ImageVector, val menu: Route) {
Library("Library", Icons.Outlined.Book, Icons.Filled.Book, Route.Library),
Sources("Sources", Icons.Outlined.Explore, Icons.Filled.Explore, Route.Sources),
Extensions("Extensions", Icons.Outlined.Store, Icons.Filled.Store, Route.Extensions),
Settings("Settings", Icons.Outlined.Settings, Icons.Filled.Settings, Route.Settings)
@Composable
fun DownloadsExtraInfo() {
val vm = viewModel<DownloadsMenuViewModel>()
val list by vm.downloadQueue.collectAsState()
if (list.isNotEmpty()) {
Text(
"${list.size} remaining",
style = MaterialTheme.typography.body2,
color = LocalContentColor.current.copy(alpha = ContentAlpha.disabled)
)
}
}
enum class TopLevelMenus(
val text: String,
val unselectedIcon: ImageVector,
val selectedIcon: ImageVector,
val menu: Route,
val top: Boolean,
val openInNewWindow: () -> Unit = {},
val extraInfo: (@Composable () -> Unit)? = null
) {
Library("Library", Icons.Outlined.Book, Icons.Filled.Book, Route.Library, true, ::openLibraryMenu),
Sources("Sources", Icons.Outlined.Explore, Icons.Filled.Explore, Route.Sources, true, ::openSourcesMenu),
Extensions("Extensions", Icons.Outlined.Store, Icons.Filled.Store, Route.Extensions, true, ::openExtensionsMenu),
Downloads("Downloads", Icons.Outlined.Download, Icons.Filled.Download, Route.Downloads, false, extraInfo = { DownloadsExtraInfo() }),
Settings("Settings", Icons.Outlined.Settings, Icons.Filled.Settings, Route.Settings, false)
}
sealed class Route {
@@ -169,6 +261,7 @@ sealed class Route {
object Sources : Route()
object Extensions : Route()
data class Manga(val mangaId: Long) : Route()
object Downloads : Route()
object Settings : Route()
object SettingsGeneral : Route()

View File

@@ -7,26 +7,19 @@
package ca.gosyer.ui.manga
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.forEachGesture
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.requiredSize
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Card
import androidx.compose.material.CircularProgressIndicator
import androidx.compose.material.ContentAlpha
import androidx.compose.material.DropdownMenu
import androidx.compose.material.DropdownMenuItem
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
@@ -38,26 +31,18 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowDownward
import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.Error
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp
import ca.gosyer.data.download.model.DownloadChapter
import ca.gosyer.data.download.model.DownloadState
import ca.gosyer.ui.base.components.awaitEventFirstDown
import ca.gosyer.ui.base.components.DropdownIconButton
import ca.gosyer.ui.base.components.combinedMouseClickable
import ca.gosyer.util.compose.contextMenu
import java.time.Instant
@@ -140,7 +125,7 @@ fun ChapterItem(
when (downloadState) {
MangaMenuViewModel.DownloadState.Downloaded -> {
DownloadedIconButton(onClick = { deleteDownload(chapter.index) })
DownloadedIconButton(chapter.mangaId to chapter.index, onClick = { deleteDownload(chapter.index) })
}
MangaMenuViewModel.DownloadState.Downloading -> {
DownloadingIconButton(downloadChapter, onClick = { stopDownload(chapter.index) })
@@ -167,7 +152,7 @@ private fun DownloadIconButton(onClick: () -> Unit) {
Icons.Default.ArrowDownward,
null,
Modifier
.size(22.dp)
.requiredSize(22.dp)
.padding(2.dp),
LocalContentColor.current.copy(alpha = ContentAlpha.disabled)
)
@@ -178,6 +163,7 @@ private fun DownloadIconButton(onClick: () -> Unit) {
@Composable
private fun DownloadingIconButton(downloadChapter: DownloadChapter?, onClick: () -> Unit) {
DropdownIconButton(
downloadChapter?.mangaId to downloadChapter?.chapterIndex,
{
DropdownMenuItem(onClick = onClick) {
Text("Cancel")
@@ -187,7 +173,7 @@ private fun DownloadingIconButton(downloadChapter: DownloadChapter?, onClick: ()
when (downloadChapter?.state) {
null, DownloadState.Queued -> CircularProgressIndicator(
Modifier
.size(26.dp)
.requiredSize(26.dp)
.padding(2.dp),
LocalContentColor.current.copy(alpha = ContentAlpha.disabled),
2.dp
@@ -196,7 +182,7 @@ private fun DownloadingIconButton(downloadChapter: DownloadChapter?, onClick: ()
CircularProgressIndicator(
downloadChapter.progress,
Modifier
.size(26.dp)
.requiredSize(26.dp)
.padding(2.dp),
LocalContentColor.current.copy(alpha = ContentAlpha.disabled),
2.dp
@@ -205,14 +191,14 @@ private fun DownloadingIconButton(downloadChapter: DownloadChapter?, onClick: ()
Icons.Default.ArrowDownward,
null,
Modifier
.size(22.dp)
.requiredSize(22.dp)
.padding(2.dp),
LocalContentColor.current.copy(alpha = ContentAlpha.disabled)
)
} else {
CircularProgressIndicator(
Modifier
.size(26.dp)
.requiredSize(26.dp)
.padding(2.dp),
LocalContentColor.current.copy(alpha = ContentAlpha.disabled),
2.dp
@@ -223,7 +209,7 @@ private fun DownloadingIconButton(downloadChapter: DownloadChapter?, onClick: ()
Icons.Default.Error,
null,
Modifier
.size(22.dp)
.requiredSize(22.dp)
.padding(2.dp),
Color.Red
)
@@ -233,7 +219,7 @@ private fun DownloadingIconButton(downloadChapter: DownloadChapter?, onClick: ()
Icons.Default.Check,
null,
Modifier
.size(22.dp)
.requiredSize(22.dp)
.padding(2.dp),
MaterialTheme.colors.surface
)
@@ -243,8 +229,9 @@ private fun DownloadingIconButton(downloadChapter: DownloadChapter?, onClick: ()
}
@Composable
private fun DownloadedIconButton(onClick: () -> Unit) {
private fun DownloadedIconButton(chapter: Pair<Long, Int?>, onClick: () -> Unit) {
DropdownIconButton(
chapter,
{
DropdownMenuItem(onClick = onClick) {
Text("Delete")
@@ -256,47 +243,10 @@ private fun DownloadedIconButton(onClick: () -> Unit) {
Icons.Default.Check,
null,
Modifier
.size(22.dp)
.requiredSize(22.dp)
.padding(2.dp),
MaterialTheme.colors.surface
)
}
}
}
@Composable
fun DropdownIconButton(
dropdownItems: @Composable ColumnScope.() -> Unit,
content: @Composable BoxScope.() -> Unit
) {
var showMenu by remember { mutableStateOf(false) }
var offset by remember { mutableStateOf(DpOffset(0.dp, 0.dp)) }
DropdownMenu(
expanded = showMenu,
onDismissRequest = { showMenu = false },
offset = offset,
content = dropdownItems
)
Box(
modifier = Modifier.fillMaxHeight()
.size(48.dp)
.clickable(
remember { MutableInteractionSource() },
role = Role.Button,
indication = rememberRipple(bounded = false, radius = 24.dp)
) {
showMenu = true
}
.pointerInput(Unit) {
forEachGesture {
awaitPointerEventScope {
awaitEventFirstDown().mouseEvent?.let {
offset = DpOffset(it.x.dp, it.y.dp)
}
}
}
},
contentAlignment = Alignment.Center,
content = content
)
}

View File

@@ -22,11 +22,13 @@ import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Home
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.unit.dp
import ca.gosyer.BuildConfig
import ca.gosyer.data.models.Source
import ca.gosyer.ui.base.components.KtorImage
import ca.gosyer.ui.base.components.Toolbar
@@ -41,12 +43,16 @@ import com.github.zsoltk.compose.savedinstancestate.BundleScope
import com.github.zsoltk.compose.savedinstancestate.LocalSavedInstanceState
fun openSourcesMenu() {
ThemedWindow(title = "TachideskJUI - Sources") {
ThemedWindow(BuildConfig.NAME) {
CompositionLocalProvider(
LocalSavedInstanceState provides Bundle()
) {
SourcesMenu {
openMangaMenu(it)
}
}
}
}
@Composable
fun SourcesMenu(onMangaClick: (Long) -> Unit) {