mirror of
https://github.com/Suwayomi/TachideskJUI.git
synced 2025-12-10 06:42:05 +01:00
Batch manga download chapters, bump minimum Tachidesk
This commit is contained in:
@@ -4,11 +4,11 @@ object Config {
|
||||
const val migrationCode = 2
|
||||
|
||||
// Tachidesk-Server version
|
||||
const val tachideskVersion = "v0.6.4"
|
||||
const val tachideskVersion = "v0.6.5"
|
||||
// Match this to the Tachidesk-Server commit count
|
||||
const val serverCode = 1118
|
||||
const val serverCode = 1143
|
||||
const val preview = true
|
||||
const val previewCommit = "d989940a4dcdf8d5cbdc2fdfdfc40849117dc85c"
|
||||
const val previewCommit = "2ac5c1362c0c5bb8f39d1049d6f72328102dd182"
|
||||
|
||||
val desktopJvmTarget = JavaVersion.VERSION_17
|
||||
val androidJvmTarget = JavaVersion.VERSION_11
|
||||
|
||||
@@ -32,9 +32,6 @@
|
||||
<string name="action_install">Install</string>
|
||||
<string name="action_uninstall">Uninstall</string>
|
||||
<string name="action_update">Update</string>
|
||||
<string name="action_toggle_read">Toggle read</string>
|
||||
<string name="action_mark_previous_read">Mark previous as read</string>
|
||||
<string name="action_toggle_bookmarked">Toggle bookmarked</string>
|
||||
<string name="action_favorite">Favorite</string>
|
||||
<string name="action_remove_favorite">Unfavorite</string>
|
||||
<string name="action_refresh_manga">Refresh</string>
|
||||
@@ -46,6 +43,15 @@
|
||||
<string name="action_ok">Ok</string>
|
||||
<string name="action_browser">Browser</string>
|
||||
<string name="action_filter">Filter</string>
|
||||
<string name="action_mark_as_read">Mark as read</string>
|
||||
<string name="action_mark_as_unread">Mark as unread</string>
|
||||
<string name="action_download">Download</string>
|
||||
<string name="action_bookmark">Bookmark chapter</string>
|
||||
<string name="action_remove_bookmark">Unbookmark chapter</string>
|
||||
<string name="action_mark_previous_read">Mark previous as read</string>
|
||||
<string name="action_filter_bookmarked">Bookmarked</string>
|
||||
<string name="action_select_all">Select all</string>
|
||||
<string name="action_select_inverse">Select inverse</string>
|
||||
|
||||
<!-- Locations -->
|
||||
<string name="location_library">Library</string>
|
||||
@@ -107,6 +113,11 @@
|
||||
<string name="failed_manga_fetch">Failed to load manga</string>
|
||||
<string name="edit_categories">Categories</string>
|
||||
<string name="select_categories">Select Categories</string>
|
||||
<string name="download_1">Next chapter</string>
|
||||
<string name="download_5">Next 5 chapters</string>
|
||||
<string name="download_10">Next 10 chapters</string>
|
||||
<string name="download_all">All</string>
|
||||
<string name="download_unread">Unread</string>
|
||||
|
||||
<string name="status_unknown">Unknown</string>
|
||||
<string name="status_ongoing">Ongoing</string>
|
||||
|
||||
@@ -7,49 +7,18 @@
|
||||
package ca.gosyer.jui.ui.manga.components
|
||||
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.material.DropdownMenu
|
||||
import androidx.compose.material.DropdownMenuItem
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.composed
|
||||
import ca.gosyer.jui.i18n.MR
|
||||
import ca.gosyer.jui.uicore.resources.stringResource
|
||||
|
||||
actual fun Modifier.chapterItemModifier(
|
||||
onClick: () -> Unit,
|
||||
toggleRead: () -> Unit,
|
||||
toggleBookmarked: () -> Unit,
|
||||
markPreviousAsRead: () -> Unit
|
||||
): Modifier = composed {
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
DropdownMenu(
|
||||
expanded,
|
||||
onDismissRequest = { expanded = false }
|
||||
) {
|
||||
listOf(
|
||||
stringResource(MR.strings.action_toggle_read) to toggleRead,
|
||||
stringResource(MR.strings.action_mark_previous_read) to markPreviousAsRead,
|
||||
stringResource(MR.strings.action_toggle_bookmarked) to toggleBookmarked
|
||||
).forEach { (label, onClick) ->
|
||||
DropdownMenuItem(
|
||||
onClick = {
|
||||
expanded = false
|
||||
onClick()
|
||||
}
|
||||
) {
|
||||
Text(text = label)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Modifier.combinedClickable(
|
||||
onClick = { onClick() },
|
||||
onLongClick = {
|
||||
expanded = true
|
||||
}
|
||||
)
|
||||
}
|
||||
markRead: (() -> Unit)?,
|
||||
markUnread: (() -> Unit)?,
|
||||
bookmarkChapter: (() -> Unit)?,
|
||||
unBookmarkChapter: (() -> Unit)?,
|
||||
markPreviousAsRead: () -> Unit,
|
||||
onSelectChapter: (() -> Unit)?,
|
||||
onUnselectChapter: (() -> Unit)?
|
||||
): Modifier = combinedClickable(
|
||||
onClick = onUnselectChapter ?: onClick,
|
||||
onLongClick = onSelectChapter
|
||||
)
|
||||
|
||||
@@ -49,16 +49,19 @@ import kotlinx.coroutines.flow.asStateFlow
|
||||
data class ChapterDownloadItem(
|
||||
val manga: Manga?,
|
||||
val chapter: Chapter,
|
||||
private val _downloadState: MutableStateFlow<ChapterDownloadState> = MutableStateFlow(
|
||||
if (chapter.downloaded) {
|
||||
ChapterDownloadState.Downloaded
|
||||
} else {
|
||||
ChapterDownloadState.NotDownloaded
|
||||
}
|
||||
),
|
||||
private val _downloadChapterFlow: MutableStateFlow<DownloadChapter?> = MutableStateFlow(null)
|
||||
) {
|
||||
private val _isSelected = MutableStateFlow(false)
|
||||
val isSelected = _isSelected.asStateFlow()
|
||||
|
||||
private val _downloadState: MutableStateFlow<ChapterDownloadState> = MutableStateFlow(
|
||||
when (chapter.downloaded) {
|
||||
true -> ChapterDownloadState.Downloaded
|
||||
false -> ChapterDownloadState.NotDownloaded
|
||||
}
|
||||
)
|
||||
val downloadState = _downloadState.asStateFlow()
|
||||
|
||||
private val _downloadChapterFlow: MutableStateFlow<DownloadChapter?> = MutableStateFlow(null)
|
||||
val downloadChapterFlow = _downloadChapterFlow.asStateFlow()
|
||||
|
||||
fun updateFrom(downloadingChapters: List<DownloadChapter>) {
|
||||
@@ -83,6 +86,10 @@ data class ChapterDownloadItem(
|
||||
stopChapterDownload.await(chapter)
|
||||
_downloadState.value = ChapterDownloadState.NotDownloaded
|
||||
}
|
||||
|
||||
fun isSelected(selectedItems: List<Long>): Boolean {
|
||||
return (chapter.id in selectedItems).also { _isSelected.value = it }
|
||||
}
|
||||
}
|
||||
|
||||
enum class ChapterDownloadState {
|
||||
|
||||
@@ -16,10 +16,14 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.MutableState
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.key
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.unit.DpOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.fastForEach
|
||||
import ca.gosyer.jui.i18n.MR
|
||||
import ca.gosyer.jui.uicore.components.DropdownMenu
|
||||
@@ -34,14 +38,27 @@ import kotlinx.collections.immutable.ImmutableList
|
||||
// As an item on the action bar, the action will be displayed with an IconButton
|
||||
// with the given icon, if not null. Otherwise, the string from the name resource is used.
|
||||
// In overflow menu, item will always be displayed as text.
|
||||
sealed class Action {
|
||||
abstract val name: String
|
||||
open val icon: ImageVector? = null
|
||||
open val overflowMode: OverflowMode = OverflowMode.IF_NECESSARY
|
||||
open val enabled: Boolean = true
|
||||
}
|
||||
|
||||
data class ActionGroup(
|
||||
override val name: String,
|
||||
override val icon: ImageVector? = null,
|
||||
val actions: ImmutableList<Action>
|
||||
) : Action()
|
||||
|
||||
@Stable
|
||||
data class ActionItem(
|
||||
val name: String,
|
||||
val icon: ImageVector? = null,
|
||||
val overflowMode: OverflowMode = OverflowMode.IF_NECESSARY,
|
||||
val enabled: Boolean = true,
|
||||
override val name: String,
|
||||
override val icon: ImageVector? = null,
|
||||
override val overflowMode: OverflowMode = OverflowMode.IF_NECESSARY,
|
||||
override val enabled: Boolean = true,
|
||||
val doAction: () -> Unit
|
||||
) {
|
||||
) : Action() {
|
||||
// allow 'calling' the action like a function
|
||||
operator fun invoke() = doAction()
|
||||
}
|
||||
@@ -54,7 +71,7 @@ enum class OverflowMode {
|
||||
// Note: should be used in a RowScope
|
||||
@Composable
|
||||
fun ActionMenu(
|
||||
items: ImmutableList<ActionItem>,
|
||||
items: ImmutableList<Action>,
|
||||
numIcons: Int = 3, // includes overflow menu icon; may be overridden by NEVER_OVERFLOW
|
||||
menuVisible: MutableState<Boolean> = remember { mutableStateOf(false) },
|
||||
iconItem: @Composable (onClick: () -> Unit, name: String, icon: ImageVector, enabled: Boolean) -> Unit
|
||||
@@ -67,12 +84,23 @@ fun ActionMenu(
|
||||
separateIntoIconAndOverflow(items, numIcons)
|
||||
}.value
|
||||
|
||||
var openGroup by remember { mutableStateOf<ActionGroup?>(null) }
|
||||
|
||||
appbarActions.fastForEach { item ->
|
||||
key(item.hashCode()) {
|
||||
if (item.icon != null) {
|
||||
iconItem(item.doAction, item.name, item.icon, item.enabled)
|
||||
when (item) {
|
||||
is ActionGroup -> iconItem({ openGroup = item }, item.name, item.icon!!, item.enabled)
|
||||
is ActionItem -> iconItem(item.doAction, item.name, item.icon!!, item.enabled)
|
||||
}
|
||||
} else {
|
||||
TextButton(onClick = item.doAction, enabled = item.enabled) {
|
||||
TextButton(
|
||||
onClick = when (item) {
|
||||
is ActionGroup -> { { openGroup = item } }
|
||||
is ActionItem -> item.doAction
|
||||
},
|
||||
enabled = item.enabled
|
||||
) {
|
||||
Text(
|
||||
text = item.name,
|
||||
color = MaterialTheme.colors.onPrimary.copy(alpha = LocalContentAlpha.current)
|
||||
@@ -91,14 +119,18 @@ fun ActionMenu(
|
||||
)
|
||||
DropdownMenu(
|
||||
expanded = menuVisible.value,
|
||||
onDismissRequest = { menuVisible.value = false }
|
||||
onDismissRequest = { menuVisible.value = false },
|
||||
offset = DpOffset(8.dp, (-56).dp),
|
||||
) {
|
||||
overflowActions.fastForEach { item ->
|
||||
key(item.hashCode()) {
|
||||
DropdownMenuItem(
|
||||
onClick = {
|
||||
menuVisible.value = false
|
||||
item()
|
||||
when (item) {
|
||||
is ActionGroup -> openGroup = item
|
||||
is ActionItem -> item()
|
||||
}
|
||||
},
|
||||
enabled = item.enabled
|
||||
) {
|
||||
@@ -109,12 +141,37 @@ fun ActionMenu(
|
||||
}
|
||||
}
|
||||
}
|
||||
DropdownMenu(
|
||||
openGroup != null,
|
||||
onDismissRequest = { openGroup = null },
|
||||
offset = DpOffset(8.dp, (-56).dp),
|
||||
) {
|
||||
openGroup?.actions?.fastForEach { item ->
|
||||
key(item.hashCode()) {
|
||||
DropdownMenuItem(
|
||||
onClick = {
|
||||
when (item) {
|
||||
is ActionGroup -> openGroup = item
|
||||
is ActionItem -> {
|
||||
openGroup = null
|
||||
item()
|
||||
}
|
||||
}
|
||||
},
|
||||
enabled = item.enabled
|
||||
) {
|
||||
// Icon(item.icon, item.name) just have text in the overflow menu
|
||||
Text(item.name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun separateIntoIconAndOverflow(
|
||||
items: ImmutableList<ActionItem>,
|
||||
items: ImmutableList<Action>,
|
||||
numIcons: Int
|
||||
): Pair<List<ActionItem>, List<ActionItem>> {
|
||||
): Pair<List<Action>, List<Action>> {
|
||||
var (iconCount, overflowCount, preferIconCount) = Triple(0, 0, 0)
|
||||
for (item in items) {
|
||||
when (item.overflowMode) {
|
||||
@@ -128,8 +185,8 @@ private fun separateIntoIconAndOverflow(
|
||||
val needsOverflow = iconCount + preferIconCount > numIcons || overflowCount > 0
|
||||
val actionIconSpace = numIcons - (if (needsOverflow) 1 else 0)
|
||||
|
||||
val iconActions = ArrayList<ActionItem>()
|
||||
val overflowActions = ArrayList<ActionItem>()
|
||||
val iconActions = ArrayList<Action>()
|
||||
val overflowActions = ArrayList<Action>()
|
||||
|
||||
var iconsAvailableBeforeOverflow = actionIconSpace - iconCount
|
||||
for (item in items) {
|
||||
|
||||
@@ -77,14 +77,17 @@ import cafe.adriel.voyager.navigator.Navigator
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
val ToolbarDefault = ImageVector.Builder(defaultWidth = 24.dp, defaultHeight = 24.dp, viewportWidth = 24f, viewportHeight = 24f).build()
|
||||
|
||||
@Composable
|
||||
fun Toolbar(
|
||||
name: String,
|
||||
navigator: Navigator? = LocalNavigator.current,
|
||||
closable: Boolean = (navigator?.size ?: 0) > 1,
|
||||
onClose: () -> Unit = { navigator?.pop() },
|
||||
closeIcon: ImageVector = ToolbarDefault,
|
||||
modifier: Modifier = Modifier,
|
||||
actions: @Composable () -> ImmutableList<ActionItem> = { remember { persistentListOf() } },
|
||||
actions: @Composable () -> ImmutableList<Action> = { remember { persistentListOf() } },
|
||||
backgroundColor: Color = MaterialTheme.colors.surface, // CustomColors.current.bars,
|
||||
contentColor: Color = contentColorFor(backgroundColor), // CustomColors.current.onBars,
|
||||
elevation: Dp = Dp.Hairline,
|
||||
@@ -98,6 +101,7 @@ fun Toolbar(
|
||||
name = name,
|
||||
closable = closable,
|
||||
onClose = onClose,
|
||||
closeIcon = closeIcon,
|
||||
modifier = modifier,
|
||||
actions = actions,
|
||||
backgroundColor = backgroundColor,
|
||||
@@ -112,6 +116,7 @@ fun Toolbar(
|
||||
name = name,
|
||||
closable = closable,
|
||||
onClose = onClose,
|
||||
closeIcon = closeIcon,
|
||||
modifier = modifier,
|
||||
actions = actions,
|
||||
backgroundColor = backgroundColor,
|
||||
@@ -130,8 +135,9 @@ private fun WideToolbar(
|
||||
name: String,
|
||||
closable: Boolean,
|
||||
onClose: () -> Unit,
|
||||
closeIcon: ImageVector,
|
||||
modifier: Modifier,
|
||||
actions: @Composable () -> ImmutableList<ActionItem> = { remember { persistentListOf() } },
|
||||
actions: @Composable () -> ImmutableList<Action> = { remember { persistentListOf() } },
|
||||
backgroundColor: Color,
|
||||
contentColor: Color,
|
||||
elevation: Dp,
|
||||
@@ -186,7 +192,7 @@ private fun WideToolbar(
|
||||
TextActionIcon(
|
||||
onClick = onClose,
|
||||
text = stringResource(MR.strings.action_close),
|
||||
icon = Icons.Rounded.Close
|
||||
icon = if (closeIcon === ToolbarDefault) Icons.Rounded.Close else closeIcon,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -199,8 +205,9 @@ private fun ThinToolbar(
|
||||
name: String,
|
||||
closable: Boolean,
|
||||
onClose: () -> Unit,
|
||||
closeIcon: ImageVector,
|
||||
modifier: Modifier,
|
||||
actions: @Composable () -> ImmutableList<ActionItem> = { remember { persistentListOf() } },
|
||||
actions: @Composable () -> ImmutableList<Action> = { remember { persistentListOf() } },
|
||||
backgroundColor: Color,
|
||||
contentColor: Color,
|
||||
elevation: Dp,
|
||||
@@ -247,7 +254,7 @@ private fun ThinToolbar(
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
Icons.Rounded.ArrowBack,
|
||||
if (closeIcon === ToolbarDefault) Icons.Rounded.ArrowBack else closeIcon,
|
||||
stringResource(MR.strings.action_close)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -33,16 +33,29 @@ class MangaScreen(private val mangaId: Long) : Screen {
|
||||
chooseCategoriesFlowHolder = vm.chooseCategoriesFlowHolder,
|
||||
availableCategories = vm.categories.collectAsState().value,
|
||||
mangaCategories = vm.mangaCategories.collectAsState().value,
|
||||
inActionMode = vm.inActionMode.collectAsState().value,
|
||||
selectedItems = vm.selectedItems.collectAsState().value,
|
||||
addFavorite = vm::addFavorite,
|
||||
setCategories = vm::setCategories,
|
||||
toggleFavorite = vm::toggleFavorite,
|
||||
refreshManga = vm::refreshManga,
|
||||
toggleRead = vm::toggleRead,
|
||||
toggleBookmarked = vm::toggleBookmarked,
|
||||
downloadNext = vm::downloadNext,
|
||||
downloadUnread = vm::downloadUnread,
|
||||
downloadAll = vm::downloadAll,
|
||||
markRead = vm::markRead,
|
||||
markUnread = vm::markUnread,
|
||||
bookmarkChapter = vm::bookmarkChapter,
|
||||
unBookmarkChapter = vm::unBookmarkChapter,
|
||||
markPreviousRead = vm::markPreviousRead,
|
||||
downloadChapter = vm::downloadChapter,
|
||||
deleteDownload = vm::deleteDownload,
|
||||
stopDownloadingChapter = vm::stopDownloadingChapter,
|
||||
onSelectChapter = vm::selectChapter,
|
||||
onUnselectChapter = vm::unselectChapter,
|
||||
selectAll = vm::selectAll,
|
||||
invertSelection = vm::invertSelection,
|
||||
clearSelection = vm::clearSelection,
|
||||
downloadChapters = vm::downloadChapters,
|
||||
loadChapters = vm::loadChapters,
|
||||
loadManga = vm::loadManga
|
||||
)
|
||||
|
||||
@@ -20,6 +20,7 @@ import ca.gosyer.jui.domain.chapter.interactor.UpdateChapterBookmarked
|
||||
import ca.gosyer.jui.domain.chapter.interactor.UpdateChapterMarkPreviousRead
|
||||
import ca.gosyer.jui.domain.chapter.interactor.UpdateChapterRead
|
||||
import ca.gosyer.jui.domain.chapter.model.Chapter
|
||||
import ca.gosyer.jui.domain.download.interactor.BatchChapterDownload
|
||||
import ca.gosyer.jui.domain.download.interactor.QueueChapterDownload
|
||||
import ca.gosyer.jui.domain.download.interactor.StopChapterDownload
|
||||
import ca.gosyer.jui.domain.download.service.DownloadService
|
||||
@@ -30,6 +31,7 @@ import ca.gosyer.jui.domain.manga.interactor.RefreshManga
|
||||
import ca.gosyer.jui.domain.manga.model.Manga
|
||||
import ca.gosyer.jui.domain.ui.service.UiPreferences
|
||||
import ca.gosyer.jui.ui.base.chapter.ChapterDownloadItem
|
||||
import ca.gosyer.jui.ui.base.chapter.ChapterDownloadState
|
||||
import ca.gosyer.jui.ui.base.model.StableHolder
|
||||
import ca.gosyer.jui.uicore.vm.ContextWrapper
|
||||
import ca.gosyer.jui.uicore.vm.ViewModel
|
||||
@@ -43,6 +45,7 @@ import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.mapLatest
|
||||
@@ -69,6 +72,7 @@ class MangaScreenViewModel @Inject constructor(
|
||||
private val removeMangaFromCategory: RemoveMangaFromCategory,
|
||||
private val addMangaToLibrary: AddMangaToLibrary,
|
||||
private val removeMangaFromLibrary: RemoveMangaFromLibrary,
|
||||
private val batchChapterDownload: BatchChapterDownload,
|
||||
uiPreferences: UiPreferences,
|
||||
contextWrapper: ContextWrapper,
|
||||
private val params: Params
|
||||
@@ -79,6 +83,11 @@ class MangaScreenViewModel @Inject constructor(
|
||||
private val _chapters = MutableStateFlow<ImmutableList<ChapterDownloadItem>>(persistentListOf())
|
||||
val chapters = _chapters.asStateFlow()
|
||||
|
||||
private val _selectedIds = MutableStateFlow<ImmutableList<Long>>(persistentListOf())
|
||||
val selectedItems = combine(chapters, _selectedIds) { chapters, selecteditems ->
|
||||
chapters.filter { it.isSelected(selecteditems) }.toImmutableList()
|
||||
}.stateIn(scope, SharingStarted.Eagerly, persistentListOf())
|
||||
|
||||
private val _isLoading = MutableStateFlow(true)
|
||||
val isLoading = _isLoading.asStateFlow()
|
||||
|
||||
@@ -96,6 +105,9 @@ class MangaScreenViewModel @Inject constructor(
|
||||
val categoriesExist = categories.map { it.isNotEmpty() }
|
||||
.stateIn(scope, SharingStarted.Eagerly, true)
|
||||
|
||||
val inActionMode = _selectedIds.map { it.isNotEmpty() }
|
||||
.stateIn(scope, SharingStarted.Eagerly, false)
|
||||
|
||||
private val chooseCategoriesFlow = MutableSharedFlow<Unit>()
|
||||
val chooseCategoriesFlowHolder = StableHolder(chooseCategoriesFlow.asSharedFlow())
|
||||
|
||||
@@ -118,6 +130,8 @@ class MangaScreenViewModel @Inject constructor(
|
||||
refreshMangaAsync(params.mangaId).await() to refreshChaptersAsync(params.mangaId).await()
|
||||
_isLoading.value = false
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
fun loadManga() {
|
||||
@@ -219,31 +233,40 @@ class MangaScreenViewModel @Inject constructor(
|
||||
|
||||
private fun findChapter(index: Int) = chapters.value.find { it.chapter.index == index }?.chapter
|
||||
|
||||
fun toggleRead(index: Int) {
|
||||
private fun setRead(index: Int, read: Boolean) {
|
||||
val chapter = findChapter(index) ?: return
|
||||
if (chapter.read == read) return
|
||||
scope.launch {
|
||||
manga.value?.let { manga ->
|
||||
updateChapterRead.await(manga, index, read = chapter.read.not(), onError = { toast(it.message.orEmpty()) })
|
||||
updateChapterRead.await(manga, index, read = read, onError = { toast(it.message.orEmpty()) })
|
||||
refreshChaptersAsync(manga.id).await()
|
||||
_selectedIds.value = _selectedIds.value.minus(chapter.id).toImmutableList()
|
||||
}
|
||||
}
|
||||
}
|
||||
fun markRead(index: Int) = setRead(index, true)
|
||||
fun markUnread(index: Int) = setRead(index, false)
|
||||
|
||||
fun toggleBookmarked(index: Int) {
|
||||
private fun setBookmarked(index: Int, bookmark: Boolean) {
|
||||
val chapter = findChapter(index) ?: return
|
||||
if (chapter.bookmarked == bookmark) return
|
||||
scope.launch {
|
||||
manga.value?.let { manga ->
|
||||
updateChapterBookmarked.await(manga, index, bookmarked = chapter.bookmarked.not(), onError = { toast(it.message.orEmpty()) })
|
||||
updateChapterBookmarked.await(manga, index, bookmarked = bookmark, onError = { toast(it.message.orEmpty()) })
|
||||
refreshChaptersAsync(manga.id).await()
|
||||
_selectedIds.value = _selectedIds.value.minus(chapter.id).toImmutableList()
|
||||
}
|
||||
}
|
||||
}
|
||||
fun bookmarkChapter(index: Int) = setBookmarked(index, true)
|
||||
fun unBookmarkChapter(index: Int) = setBookmarked(index, false)
|
||||
|
||||
fun markPreviousRead(index: Int) {
|
||||
scope.launch {
|
||||
manga.value?.let { manga ->
|
||||
updateChapterMarkPreviousRead.await(manga, index, onError = { toast(it.message.orEmpty()) })
|
||||
refreshChaptersAsync(manga.id).await()
|
||||
_selectedIds.value = persistentListOf()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -268,6 +291,73 @@ class MangaScreenViewModel @Inject constructor(
|
||||
}
|
||||
}
|
||||
|
||||
fun selectAll() {
|
||||
scope.launch {
|
||||
_selectedIds.value = chapters.value.map { it.chapter.id }.toImmutableList()
|
||||
}
|
||||
}
|
||||
|
||||
fun invertSelection() {
|
||||
scope.launch {
|
||||
_selectedIds.value = chapters.value.map { it.chapter.id }.minus(_selectedIds.value).toImmutableList()
|
||||
}
|
||||
}
|
||||
|
||||
fun selectChapter(index: Int) {
|
||||
scope.launch {
|
||||
chapters.value.find { it.chapter.index == index }
|
||||
?.let { _selectedIds.value = _selectedIds.value.plus(it.chapter.id).toImmutableList() }
|
||||
}
|
||||
}
|
||||
fun unselectChapter(index: Int) {
|
||||
scope.launch {
|
||||
chapters.value.find { it.chapter.index == index }
|
||||
?.let { _selectedIds.value = _selectedIds.value.minus(it.chapter.id).toImmutableList() }
|
||||
}
|
||||
}
|
||||
|
||||
fun clearSelection() {
|
||||
scope.launch {
|
||||
_selectedIds.value = persistentListOf()
|
||||
}
|
||||
}
|
||||
|
||||
fun downloadChapters() {
|
||||
scope.launch {
|
||||
batchChapterDownload.await(_selectedIds.value)
|
||||
_selectedIds.value = persistentListOf()
|
||||
}
|
||||
}
|
||||
|
||||
fun downloadNext(next: Int) {
|
||||
scope.launch {
|
||||
batchChapterDownload.await(
|
||||
_chapters.value.filter { !it.chapter.read && it.downloadState.value == ChapterDownloadState.NotDownloaded }
|
||||
.map { it.chapter.id }
|
||||
.takeLast(next)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun downloadUnread() {
|
||||
scope.launch {
|
||||
batchChapterDownload.await(
|
||||
_chapters.value.filter { !it.chapter.read && it.downloadState.value == ChapterDownloadState.NotDownloaded }
|
||||
.map { it.chapter.id }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun downloadAll() {
|
||||
scope.launch {
|
||||
batchChapterDownload.await(
|
||||
_chapters.value
|
||||
.filter { it.downloadState.value == ChapterDownloadState.NotDownloaded }
|
||||
.map { it.chapter.id }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun List<Chapter>.toDownloadChapters() = map {
|
||||
ChapterDownloadItem(null, it)
|
||||
}.toImmutableList()
|
||||
|
||||
@@ -10,19 +10,27 @@ import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.BoxWithConstraints
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.sizeIn
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.selection.SelectionContainer
|
||||
import androidx.compose.material.Card
|
||||
import androidx.compose.material.ContentAlpha
|
||||
import androidx.compose.material.LocalContentColor
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Bookmark
|
||||
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.platform.LocalDensity
|
||||
import androidx.compose.ui.text.AnnotatedString
|
||||
import androidx.compose.ui.text.SpanStyle
|
||||
import androidx.compose.ui.text.buildAnnotatedString
|
||||
@@ -31,14 +39,19 @@ import androidx.compose.ui.unit.dp
|
||||
import ca.gosyer.jui.i18n.MR
|
||||
import ca.gosyer.jui.ui.base.chapter.ChapterDownloadIcon
|
||||
import ca.gosyer.jui.ui.base.chapter.ChapterDownloadItem
|
||||
import ca.gosyer.jui.uicore.components.selectedBackground
|
||||
import ca.gosyer.jui.uicore.resources.stringResource
|
||||
import kotlinx.datetime.Instant
|
||||
|
||||
expect fun Modifier.chapterItemModifier(
|
||||
onClick: () -> Unit,
|
||||
toggleRead: () -> Unit,
|
||||
toggleBookmarked: () -> Unit,
|
||||
markPreviousAsRead: () -> Unit
|
||||
markRead: (() -> Unit)?,
|
||||
markUnread: (() -> Unit)?,
|
||||
bookmarkChapter: (() -> Unit)?,
|
||||
unBookmarkChapter: (() -> Unit)?,
|
||||
markPreviousAsRead: () -> Unit,
|
||||
onSelectChapter: (() -> Unit)?,
|
||||
onUnselectChapter: (() -> Unit)?
|
||||
): Modifier
|
||||
|
||||
@Composable
|
||||
@@ -46,80 +59,106 @@ fun ChapterItem(
|
||||
chapterDownload: ChapterDownloadItem,
|
||||
format: (Instant) -> String,
|
||||
onClick: (Int) -> Unit,
|
||||
toggleRead: (Int) -> Unit,
|
||||
toggleBookmarked: (Int) -> Unit,
|
||||
markRead: (Int) -> Unit,
|
||||
markUnread: (Int) -> Unit,
|
||||
bookmarkChapter: (Int) -> Unit,
|
||||
unBookmarkChapter: (Int) -> Unit,
|
||||
markPreviousAsRead: (Int) -> Unit,
|
||||
onClickDownload: (Int) -> Unit,
|
||||
onClickStopDownload: (Int) -> Unit,
|
||||
onClickDeleteChapter: (Int) -> Unit
|
||||
onClickDeleteChapter: (Int) -> Unit,
|
||||
onSelectChapter: (Int) -> Unit,
|
||||
onUnselectChapter: (Int) -> Unit
|
||||
) {
|
||||
val chapter = chapterDownload.chapter
|
||||
Card(
|
||||
modifier = Modifier.fillMaxWidth().height(70.dp).padding(4.dp),
|
||||
elevation = 1.dp,
|
||||
shape = RoundedCornerShape(4.dp)
|
||||
) {
|
||||
BoxWithConstraints(
|
||||
Modifier.chapterItemModifier(
|
||||
val isSelected by chapterDownload.isSelected.collectAsState()
|
||||
BoxWithConstraints(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.height(70.dp)
|
||||
.selectedBackground(isSelected)
|
||||
.chapterItemModifier(
|
||||
onClick = { onClick(chapter.index) },
|
||||
toggleRead = { toggleRead(chapter.index) },
|
||||
toggleBookmarked = { toggleBookmarked(chapter.index) },
|
||||
markPreviousAsRead = { markPreviousAsRead(chapter.index) }
|
||||
markRead = { markRead(chapter.index) }.takeUnless { chapter.read },
|
||||
markUnread = { markUnread(chapter.index) }.takeIf { chapter.read },
|
||||
bookmarkChapter = { bookmarkChapter(chapter.index) }.takeUnless { chapter.bookmarked },
|
||||
unBookmarkChapter = { unBookmarkChapter(chapter.index) }.takeIf { chapter.bookmarked },
|
||||
markPreviousAsRead = { markPreviousAsRead(chapter.index) },
|
||||
onSelectChapter = { onSelectChapter(chapter.index) }.takeUnless { chapterDownload.isSelected.value },
|
||||
onUnselectChapter = { onUnselectChapter(chapter.index) }.takeIf { chapterDownload.isSelected.value }
|
||||
)
|
||||
.padding(4.dp)
|
||||
) {
|
||||
val textColor = if (chapter.bookmarked && !chapter.read) {
|
||||
MaterialTheme.colors.primary
|
||||
} else {
|
||||
MaterialTheme.colors.onSurface
|
||||
}
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
Column(
|
||||
Modifier.padding(4.dp).width(this@BoxWithConstraints.maxWidth - 60.dp)
|
||||
) {
|
||||
Column(
|
||||
Modifier.padding(4.dp).width(this@BoxWithConstraints.maxWidth - 60.dp)
|
||||
) {
|
||||
SelectionContainer {
|
||||
Text(
|
||||
chapter.name,
|
||||
maxLines = 1,
|
||||
style = MaterialTheme.typography.h6,
|
||||
color = LocalContentColor.current.copy(
|
||||
alpha = if (chapter.read) ContentAlpha.disabled else ContentAlpha.high
|
||||
),
|
||||
overflow = TextOverflow.Ellipsis
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
var textHeight by remember { mutableStateOf(0) }
|
||||
if (chapter.bookmarked) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Bookmark,
|
||||
contentDescription = stringResource(MR.strings.action_filter_bookmarked),
|
||||
modifier = Modifier
|
||||
.sizeIn(maxHeight = with(LocalDensity.current) { textHeight.toDp() - 2.dp }),
|
||||
tint = MaterialTheme.colors.primary,
|
||||
)
|
||||
Spacer(modifier = Modifier.width(2.dp))
|
||||
}
|
||||
val subtitleStr = buildAnnotatedString {
|
||||
if (chapter.uploadDate > 0) {
|
||||
append(format(Instant.fromEpochMilliseconds(chapter.uploadDate)))
|
||||
}
|
||||
if (!chapter.read && chapter.lastPageRead > 0) {
|
||||
if (length > 0) append(" • ")
|
||||
append(
|
||||
AnnotatedString(
|
||||
stringResource(MR.strings.page_progress, (chapter.lastPageRead + 1)),
|
||||
SpanStyle(color = LocalContentColor.current.copy(alpha = ContentAlpha.disabled))
|
||||
)
|
||||
)
|
||||
}
|
||||
if (!chapter.scanlator.isNullOrBlank()) {
|
||||
if (length > 0) append(" • ")
|
||||
append(chapter.scanlator!!)
|
||||
Text(
|
||||
chapter.name,
|
||||
maxLines = 1,
|
||||
style = MaterialTheme.typography.h6,
|
||||
color = textColor.copy(
|
||||
alpha = if (chapter.read) ContentAlpha.disabled else ContentAlpha.high
|
||||
),
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
onTextLayout = {
|
||||
textHeight = it.size.height
|
||||
}
|
||||
)
|
||||
}
|
||||
val subtitleStr = buildAnnotatedString {
|
||||
if (chapter.uploadDate > 0) {
|
||||
append(format(Instant.fromEpochMilliseconds(chapter.uploadDate)))
|
||||
}
|
||||
SelectionContainer {
|
||||
Text(
|
||||
subtitleStr,
|
||||
style = MaterialTheme.typography.body2,
|
||||
color = LocalContentColor.current.copy(
|
||||
alpha = if (chapter.read) ContentAlpha.disabled else ContentAlpha.medium
|
||||
if (!chapter.read && chapter.lastPageRead > 0) {
|
||||
if (length > 0) append(" • ")
|
||||
append(
|
||||
AnnotatedString(
|
||||
stringResource(MR.strings.page_progress, (chapter.lastPageRead + 1)),
|
||||
SpanStyle(color = textColor.copy(alpha = ContentAlpha.disabled))
|
||||
)
|
||||
)
|
||||
}
|
||||
if (!chapter.scanlator.isNullOrBlank()) {
|
||||
if (length > 0) append(" • ")
|
||||
append(chapter.scanlator!!)
|
||||
}
|
||||
}
|
||||
|
||||
ChapterDownloadIcon(
|
||||
chapterDownload,
|
||||
{ onClickDownload(it.index) },
|
||||
{ onClickStopDownload(it.index) },
|
||||
{ onClickDeleteChapter(it.index) }
|
||||
Text(
|
||||
subtitleStr,
|
||||
style = MaterialTheme.typography.body2,
|
||||
color = textColor.copy(
|
||||
alpha = if (chapter.read) ContentAlpha.disabled else ContentAlpha.medium
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
ChapterDownloadIcon(
|
||||
chapterDownload,
|
||||
{ onClickDownload(it.index) },
|
||||
{ onClickStopDownload(it.index) },
|
||||
{ onClickDeleteChapter(it.index) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,11 +23,20 @@ import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material.Scaffold
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.rounded.BookmarkAdd
|
||||
import androidx.compose.material.icons.rounded.BookmarkRemove
|
||||
import androidx.compose.material.icons.rounded.Close
|
||||
import androidx.compose.material.icons.rounded.Delete
|
||||
import androidx.compose.material.icons.rounded.DoneAll
|
||||
import androidx.compose.material.icons.rounded.Download
|
||||
import androidx.compose.material.icons.rounded.Favorite
|
||||
import androidx.compose.material.icons.rounded.FavoriteBorder
|
||||
import androidx.compose.material.icons.rounded.FlipToBack
|
||||
import androidx.compose.material.icons.rounded.Label
|
||||
import androidx.compose.material.icons.rounded.OpenInBrowser
|
||||
import androidx.compose.material.icons.rounded.Refresh
|
||||
import androidx.compose.material.icons.rounded.RemoveDone
|
||||
import androidx.compose.material.icons.rounded.SelectAll
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.Stable
|
||||
@@ -35,23 +44,34 @@ import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.util.fastAny
|
||||
import ca.gosyer.jui.domain.category.model.Category
|
||||
import ca.gosyer.jui.domain.manga.model.Manga
|
||||
import ca.gosyer.jui.i18n.MR
|
||||
import ca.gosyer.jui.ui.base.chapter.ChapterDownloadItem
|
||||
import ca.gosyer.jui.ui.base.chapter.ChapterDownloadState
|
||||
import ca.gosyer.jui.ui.base.model.StableHolder
|
||||
import ca.gosyer.jui.ui.base.navigation.Action
|
||||
import ca.gosyer.jui.ui.base.navigation.ActionGroup
|
||||
import ca.gosyer.jui.ui.base.navigation.ActionItem
|
||||
import ca.gosyer.jui.ui.base.navigation.BackHandler
|
||||
import ca.gosyer.jui.ui.base.navigation.Toolbar
|
||||
import ca.gosyer.jui.ui.base.navigation.ToolbarDefault
|
||||
import ca.gosyer.jui.ui.main.components.bottomNav
|
||||
import ca.gosyer.jui.ui.reader.rememberReaderLauncher
|
||||
import ca.gosyer.jui.uicore.components.BottomActionItem
|
||||
import ca.gosyer.jui.uicore.components.BottomActionMenu
|
||||
import ca.gosyer.jui.uicore.components.ErrorScreen
|
||||
import ca.gosyer.jui.uicore.components.LoadingScreen
|
||||
import ca.gosyer.jui.uicore.components.VerticalScrollbar
|
||||
import ca.gosyer.jui.uicore.components.rememberScrollbarAdapter
|
||||
import ca.gosyer.jui.uicore.components.scrollbarPadding
|
||||
import ca.gosyer.jui.uicore.icons.JuiAssets
|
||||
import ca.gosyer.jui.uicore.icons.juiassets.DonePrev
|
||||
import ca.gosyer.jui.uicore.insets.navigationBars
|
||||
import ca.gosyer.jui.uicore.insets.statusBars
|
||||
import ca.gosyer.jui.uicore.resources.stringResource
|
||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||
import com.vanpra.composematerialdialogs.rememberMaterialDialogState
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
@@ -68,16 +88,29 @@ fun MangaScreenContent(
|
||||
chooseCategoriesFlowHolder: StableHolder<SharedFlow<Unit>>,
|
||||
availableCategories: ImmutableList<Category>,
|
||||
mangaCategories: ImmutableList<Category>,
|
||||
inActionMode: Boolean,
|
||||
selectedItems: ImmutableList<ChapterDownloadItem>,
|
||||
addFavorite: (List<Category>, List<Category>) -> Unit,
|
||||
setCategories: () -> Unit,
|
||||
toggleFavorite: () -> Unit,
|
||||
refreshManga: () -> Unit,
|
||||
toggleRead: (Int) -> Unit,
|
||||
toggleBookmarked: (Int) -> Unit,
|
||||
downloadNext: (Int) -> Unit,
|
||||
downloadUnread: () -> Unit,
|
||||
downloadAll: () -> Unit,
|
||||
markRead: (Int) -> Unit,
|
||||
markUnread: (Int) -> Unit,
|
||||
bookmarkChapter: (Int) -> Unit,
|
||||
unBookmarkChapter: (Int) -> Unit,
|
||||
markPreviousRead: (Int) -> Unit,
|
||||
downloadChapter: (Int) -> Unit,
|
||||
deleteDownload: (Int) -> Unit,
|
||||
stopDownloadingChapter: (Int) -> Unit,
|
||||
onSelectChapter: (Int) -> Unit,
|
||||
onUnselectChapter: (Int) -> Unit,
|
||||
selectAll: () -> Unit,
|
||||
invertSelection: () -> Unit,
|
||||
clearSelection: () -> Unit,
|
||||
downloadChapters: () -> Unit,
|
||||
loadChapters: () -> Unit,
|
||||
loadManga: () -> Unit
|
||||
) {
|
||||
@@ -90,6 +123,10 @@ fun MangaScreenContent(
|
||||
val readerLauncher = rememberReaderLauncher()
|
||||
readerLauncher.Reader()
|
||||
|
||||
BackHandler(inActionMode) {
|
||||
clearSelection()
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
modifier = Modifier.windowInsetsPadding(
|
||||
WindowInsets.statusBars.add(
|
||||
@@ -97,24 +134,58 @@ fun MangaScreenContent(
|
||||
)
|
||||
),
|
||||
topBar = {
|
||||
val navigator = LocalNavigator.current
|
||||
Toolbar(
|
||||
stringResource(MR.strings.location_manga),
|
||||
if (inActionMode) selectedItems.size.toString() else stringResource(MR.strings.location_manga),
|
||||
actions = {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
getActionItems(
|
||||
refreshManga = refreshManga,
|
||||
refreshMangaEnabled = !isLoading,
|
||||
categoryItemVisible = categoriesExist && manga?.inLibrary == true,
|
||||
setCategories = setCategories,
|
||||
inLibrary = manga?.inLibrary == true,
|
||||
toggleFavorite = toggleFavorite,
|
||||
favoritesButtonEnabled = manga != null,
|
||||
openInBrowserEnabled = manga?.realUrl != null,
|
||||
openInBrowser = {
|
||||
manga?.realUrl?.let { uriHandler.openUri(it) }
|
||||
}
|
||||
)
|
||||
}
|
||||
if (inActionMode) {
|
||||
getActionModeActionItems(
|
||||
selectAll = selectAll,
|
||||
invertSelection = invertSelection
|
||||
)
|
||||
} else {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
getActionItems(
|
||||
refreshManga = refreshManga,
|
||||
refreshMangaEnabled = !isLoading,
|
||||
categoryItemVisible = categoriesExist && manga?.inLibrary == true,
|
||||
setCategories = setCategories,
|
||||
inLibrary = manga?.inLibrary == true,
|
||||
toggleFavorite = toggleFavorite,
|
||||
favoritesButtonEnabled = manga != null,
|
||||
openInBrowserEnabled = manga?.realUrl != null,
|
||||
openInBrowser = {
|
||||
manga?.realUrl?.let { uriHandler.openUri(it) }
|
||||
},
|
||||
downloadNext = downloadNext,
|
||||
downloadUnread = downloadUnread,
|
||||
downloadAll = downloadAll
|
||||
)
|
||||
}
|
||||
},
|
||||
onClose = {
|
||||
if (inActionMode) {
|
||||
clearSelection()
|
||||
} else {
|
||||
navigator?.pop()
|
||||
}
|
||||
},
|
||||
closeIcon = if (inActionMode) Icons.Rounded.Close else ToolbarDefault
|
||||
)
|
||||
},
|
||||
bottomBar = {
|
||||
BottomActionMenu(
|
||||
visible = inActionMode,
|
||||
items = getBottomActionItems(
|
||||
selectedItems = selectedItems,
|
||||
markRead = markRead,
|
||||
markUnread = markUnread,
|
||||
bookmarkChapter = bookmarkChapter,
|
||||
unBookmarkChapter = unBookmarkChapter,
|
||||
markPreviousAsRead = markPreviousRead,
|
||||
deleteChapter = deleteDownload,
|
||||
downloadChapters = downloadChapters
|
||||
)
|
||||
)
|
||||
}
|
||||
) {
|
||||
@@ -141,13 +212,21 @@ fun MangaScreenContent(
|
||||
ChapterItem(
|
||||
chapter,
|
||||
dateTimeFormatter,
|
||||
onClick = { readerLauncher.launch(it, manga.id) },
|
||||
toggleRead = toggleRead,
|
||||
toggleBookmarked = toggleBookmarked,
|
||||
onClick = if (inActionMode) {
|
||||
{ if (chapter.isSelected.value) onUnselectChapter(chapter.chapter.index) else onSelectChapter(chapter.chapter.index) }
|
||||
} else {
|
||||
{ readerLauncher.launch(it, manga.id) }
|
||||
},
|
||||
markRead = markRead,
|
||||
markUnread = markUnread,
|
||||
bookmarkChapter = bookmarkChapter,
|
||||
unBookmarkChapter = unBookmarkChapter,
|
||||
markPreviousAsRead = markPreviousRead,
|
||||
onClickDownload = downloadChapter,
|
||||
onClickDeleteChapter = deleteDownload,
|
||||
onClickStopDownload = stopDownloadingChapter
|
||||
onClickStopDownload = stopDownloadingChapter,
|
||||
onSelectChapter = onSelectChapter,
|
||||
onUnselectChapter = onUnselectChapter
|
||||
)
|
||||
}
|
||||
} else if (!isLoading) {
|
||||
@@ -197,8 +276,11 @@ private fun getActionItems(
|
||||
toggleFavorite: () -> Unit,
|
||||
favoritesButtonEnabled: Boolean,
|
||||
openInBrowserEnabled: Boolean,
|
||||
openInBrowser: () -> Unit
|
||||
): ImmutableList<ActionItem> {
|
||||
openInBrowser: () -> Unit,
|
||||
downloadNext: (Int) -> Unit,
|
||||
downloadUnread: () -> Unit,
|
||||
downloadAll: () -> Unit
|
||||
): ImmutableList<Action> {
|
||||
return listOfNotNull(
|
||||
ActionItem(
|
||||
name = stringResource(MR.strings.action_refresh_manga),
|
||||
@@ -225,6 +307,32 @@ private fun getActionItems(
|
||||
doAction = toggleFavorite,
|
||||
enabled = favoritesButtonEnabled
|
||||
),
|
||||
ActionGroup(
|
||||
name = stringResource(MR.strings.action_download),
|
||||
icon = Icons.Rounded.Download,
|
||||
actions = listOf(
|
||||
ActionItem(
|
||||
name = stringResource(MR.strings.download_1),
|
||||
doAction = { downloadNext(1) }
|
||||
),
|
||||
ActionItem(
|
||||
name = stringResource(MR.strings.download_5),
|
||||
doAction = { downloadNext(5) }
|
||||
),
|
||||
ActionItem(
|
||||
name = stringResource(MR.strings.download_10),
|
||||
doAction = { downloadNext(10) }
|
||||
),
|
||||
ActionItem(
|
||||
name = stringResource(MR.strings.download_unread),
|
||||
doAction = downloadUnread
|
||||
),
|
||||
ActionItem(
|
||||
name = stringResource(MR.strings.download_all),
|
||||
doAction = downloadAll
|
||||
)
|
||||
).toImmutableList()
|
||||
),
|
||||
ActionItem(
|
||||
name = stringResource(MR.strings.action_browser),
|
||||
icon = Icons.Rounded.OpenInBrowser,
|
||||
@@ -233,3 +341,74 @@ private fun getActionItems(
|
||||
)
|
||||
).toImmutableList()
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Stable
|
||||
private fun getActionModeActionItems(
|
||||
selectAll: () -> Unit,
|
||||
invertSelection: () -> Unit
|
||||
): ImmutableList<ActionItem> {
|
||||
return listOf(
|
||||
ActionItem(
|
||||
name = stringResource(MR.strings.action_select_all),
|
||||
icon = Icons.Rounded.SelectAll,
|
||||
doAction = selectAll
|
||||
),
|
||||
ActionItem(
|
||||
name = stringResource(MR.strings.action_select_inverse),
|
||||
icon = Icons.Rounded.FlipToBack,
|
||||
doAction = invertSelection
|
||||
)
|
||||
).toImmutableList()
|
||||
}
|
||||
|
||||
@Composable
|
||||
@Stable
|
||||
private fun getBottomActionItems(
|
||||
selectedItems: ImmutableList<ChapterDownloadItem>,
|
||||
markRead: (Int) -> Unit,
|
||||
markUnread: (Int) -> Unit,
|
||||
bookmarkChapter: (Int) -> Unit,
|
||||
unBookmarkChapter: (Int) -> Unit,
|
||||
markPreviousAsRead: (Int) -> Unit,
|
||||
deleteChapter: (Int) -> Unit,
|
||||
downloadChapters: () -> Unit
|
||||
): ImmutableList<BottomActionItem> {
|
||||
return listOfNotNull(
|
||||
BottomActionItem(
|
||||
name = stringResource(MR.strings.action_bookmark),
|
||||
icon = Icons.Rounded.BookmarkAdd,
|
||||
onClick = { bookmarkChapter(selectedItems.first().chapter.index) },
|
||||
).takeIf { selectedItems.fastAny { !it.chapter.bookmarked } && selectedItems.size == 1 },
|
||||
BottomActionItem(
|
||||
name = stringResource(MR.strings.action_remove_bookmark),
|
||||
icon = Icons.Rounded.BookmarkRemove,
|
||||
onClick = { unBookmarkChapter(selectedItems.first().chapter.index) },
|
||||
).takeIf { selectedItems.fastAny { it.chapter.bookmarked } && selectedItems.size == 1 },
|
||||
BottomActionItem(
|
||||
name = stringResource(MR.strings.action_mark_as_read),
|
||||
icon = Icons.Rounded.DoneAll,
|
||||
onClick = { markRead(selectedItems.first().chapter.index) },
|
||||
).takeIf { selectedItems.fastAny { !it.chapter.read } && selectedItems.size == 1 },
|
||||
BottomActionItem(
|
||||
name = stringResource(MR.strings.action_mark_as_unread),
|
||||
icon = Icons.Rounded.RemoveDone,
|
||||
onClick = { markUnread(selectedItems.first().chapter.index) },
|
||||
).takeIf { selectedItems.fastAny { !it.chapter.read } && selectedItems.size == 1 },
|
||||
BottomActionItem(
|
||||
name = stringResource(MR.strings.action_mark_previous_read),
|
||||
icon = JuiAssets.DonePrev,
|
||||
onClick = { markPreviousAsRead(selectedItems.first().chapter.index) },
|
||||
).takeIf { selectedItems.size == 1 },
|
||||
BottomActionItem(
|
||||
name = stringResource(MR.strings.action_download),
|
||||
icon = Icons.Rounded.Download,
|
||||
onClick = downloadChapters
|
||||
).takeIf { selectedItems.fastAny { it.downloadState.value != ChapterDownloadState.Downloaded } },
|
||||
BottomActionItem(
|
||||
name = stringResource(MR.strings.action_delete),
|
||||
icon = Icons.Rounded.Delete,
|
||||
onClick = { deleteChapter(selectedItems.first().chapter.index) }
|
||||
).takeIf { selectedItems.fastAny { it.downloadState.value == ChapterDownloadState.Downloaded } && selectedItems.size == 1 }
|
||||
).toImmutableList()
|
||||
}
|
||||
|
||||
@@ -11,25 +11,37 @@ import androidx.compose.foundation.onClick
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.input.pointer.isCtrlPressed
|
||||
import ca.gosyer.jui.i18n.MR
|
||||
import ca.gosyer.jui.uicore.components.onRightClickContextMenu
|
||||
import ca.gosyer.jui.uicore.resources.stringResource
|
||||
|
||||
actual fun Modifier.chapterItemModifier(
|
||||
onClick: () -> Unit,
|
||||
toggleRead: () -> Unit,
|
||||
toggleBookmarked: () -> Unit,
|
||||
markPreviousAsRead: () -> Unit
|
||||
markRead: (() -> Unit)?,
|
||||
markUnread: (() -> Unit)?,
|
||||
bookmarkChapter: (() -> Unit)?,
|
||||
unBookmarkChapter: (() -> Unit)?,
|
||||
markPreviousAsRead: () -> Unit,
|
||||
onSelectChapter: (() -> Unit)?,
|
||||
onUnselectChapter: (() -> Unit)?
|
||||
): Modifier = this
|
||||
.onClick(
|
||||
onClick = onClick
|
||||
onClick = onClick,
|
||||
onLongClick = onSelectChapter
|
||||
)
|
||||
.onClick(
|
||||
onClick = onSelectChapter ?: onUnselectChapter ?: {},
|
||||
keyboardModifiers = { isCtrlPressed }
|
||||
)
|
||||
.onRightClickContextMenu(
|
||||
items = {
|
||||
getContextItems(
|
||||
toggleRead,
|
||||
toggleBookmarked,
|
||||
markPreviousAsRead
|
||||
markRead = markRead,
|
||||
markUnread = markUnread,
|
||||
bookmarkChapter = bookmarkChapter,
|
||||
unBookmarkChapter = unBookmarkChapter,
|
||||
markPreviousAsRead = markPreviousAsRead
|
||||
)
|
||||
}
|
||||
)
|
||||
@@ -37,13 +49,17 @@ actual fun Modifier.chapterItemModifier(
|
||||
@Composable
|
||||
@Stable
|
||||
private fun getContextItems(
|
||||
toggleRead: () -> Unit,
|
||||
toggleBookmarked: () -> Unit,
|
||||
markRead: (() -> Unit)?,
|
||||
markUnread: (() -> Unit)?,
|
||||
bookmarkChapter: (() -> Unit)?,
|
||||
unBookmarkChapter: (() -> Unit)?,
|
||||
markPreviousAsRead: () -> Unit
|
||||
): List<ContextMenuItem> {
|
||||
return listOf(
|
||||
ContextMenuItem(stringResource(MR.strings.action_toggle_read), toggleRead),
|
||||
return listOfNotNull(
|
||||
if (bookmarkChapter != null) ContextMenuItem(stringResource(MR.strings.action_bookmark), bookmarkChapter) else null,
|
||||
if (unBookmarkChapter != null) ContextMenuItem(stringResource(MR.strings.action_remove_bookmark), unBookmarkChapter) else null,
|
||||
if (markRead != null) ContextMenuItem(stringResource(MR.strings.action_mark_as_read), markRead) else null,
|
||||
if (markUnread != null) ContextMenuItem(stringResource(MR.strings.action_mark_as_unread), markUnread) else null,
|
||||
ContextMenuItem(stringResource(MR.strings.action_mark_previous_read), markPreviousAsRead),
|
||||
ContextMenuItem(stringResource(MR.strings.action_toggle_bookmarked), toggleBookmarked)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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.jui.uicore.components
|
||||
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.composed
|
||||
|
||||
actual fun Modifier.buttonModifier(
|
||||
onClick: () -> Unit,
|
||||
onHintClick: () -> Unit,
|
||||
): Modifier = composed {
|
||||
combinedClickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = rememberRipple(bounded = false),
|
||||
onLongClick = onHintClick,
|
||||
onClick = onClick,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
/*
|
||||
* 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.jui.uicore.components
|
||||
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.core.animateFloatAsState
|
||||
import androidx.compose.animation.expandVertically
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.shrinkVertically
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.WindowInsetsSides
|
||||
import androidx.compose.foundation.layout.asPaddingValues
|
||||
import androidx.compose.foundation.layout.only
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.ZeroCornerSize
|
||||
import androidx.compose.material.Icon
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Surface
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Stable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
|
||||
import androidx.compose.ui.platform.LocalHapticFeedback
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import ca.gosyer.jui.uicore.insets.navigationBars
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@Stable
|
||||
data class BottomActionItem(
|
||||
val name: String,
|
||||
val icon: ImageVector,
|
||||
val onClick: () -> Unit
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun BottomActionMenu(
|
||||
visible: Boolean,
|
||||
modifier: Modifier = Modifier,
|
||||
items: ImmutableList<BottomActionItem>,
|
||||
) {
|
||||
AnimatedVisibility(
|
||||
visible = visible,
|
||||
enter = expandVertically(expandFrom = Alignment.Bottom),
|
||||
exit = shrinkVertically(shrinkTowards = Alignment.Bottom),
|
||||
) {
|
||||
val scope = rememberCoroutineScope()
|
||||
Surface(
|
||||
modifier = modifier,
|
||||
shape = MaterialTheme.shapes.large.copy(bottomEnd = ZeroCornerSize, bottomStart = ZeroCornerSize),
|
||||
elevation = 3.dp,
|
||||
) {
|
||||
val haptic = LocalHapticFeedback.current
|
||||
var confirm by remember { mutableStateOf<Int?>(null) }
|
||||
var resetJob: Job? = remember { null }
|
||||
val onLongClickItem: (Int) -> Unit = { toConfirmIndex ->
|
||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
confirm = toConfirmIndex
|
||||
resetJob?.cancel()
|
||||
resetJob = scope.launch {
|
||||
delay(1.seconds)
|
||||
if (isActive) confirm = null
|
||||
}
|
||||
}
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(WindowInsets.navigationBars.only(WindowInsetsSides.Bottom).asPaddingValues())
|
||||
.padding(horizontal = 8.dp, vertical = 12.dp),
|
||||
) {
|
||||
items.forEachIndexed { index, item ->
|
||||
Button(
|
||||
title = item.name,
|
||||
icon = item.icon,
|
||||
toConfirm = confirm == index,
|
||||
onLongClick = { onLongClickItem(index) },
|
||||
onClick = item.onClick,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
expect fun Modifier.buttonModifier(
|
||||
onClick: () -> Unit,
|
||||
onHintClick: () -> Unit,
|
||||
): Modifier
|
||||
|
||||
@Composable
|
||||
private fun RowScope.Button(
|
||||
title: String,
|
||||
icon: ImageVector,
|
||||
toConfirm: Boolean,
|
||||
onLongClick: () -> Unit,
|
||||
onClick: () -> Unit,
|
||||
content: (@Composable () -> Unit)? = null,
|
||||
) {
|
||||
val animatedWeight by animateFloatAsState(if (toConfirm) 2f else 1f)
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.size(48.dp)
|
||||
.weight(animatedWeight)
|
||||
.buttonModifier(
|
||||
onHintClick = onLongClick,
|
||||
onClick = onClick,
|
||||
),
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = title,
|
||||
)
|
||||
AnimatedVisibility(
|
||||
visible = toConfirm,
|
||||
enter = expandVertically(expandFrom = Alignment.Top) + fadeIn(),
|
||||
exit = shrinkVertically(shrinkTowards = Alignment.Top) + fadeOut(),
|
||||
) {
|
||||
Text(
|
||||
text = title,
|
||||
overflow = TextOverflow.Visible,
|
||||
maxLines = 1,
|
||||
style = MaterialTheme.typography.overline,
|
||||
)
|
||||
}
|
||||
content?.invoke()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/*
|
||||
* 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.jui.uicore.components
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.composed
|
||||
|
||||
fun Modifier.selectedBackground(isSelected: Boolean): Modifier = composed {
|
||||
if (isSelected) {
|
||||
val alpha = if (isSystemInDarkTheme()) 0.08f else 0.22f
|
||||
background(MaterialTheme.colors.secondary.copy(alpha = alpha))
|
||||
} else {
|
||||
this
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package ca.gosyer.jui.uicore.icons
|
||||
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import ca.gosyer.jui.uicore.icons.juiassets.AllAssets
|
||||
import ca.gosyer.jui.uicore.icons.juiassets.Ca
|
||||
import ca.gosyer.jui.uicore.icons.juiassets.DonePrev
|
||||
import kotlin.collections.List as ____KtList
|
||||
|
||||
public object JuiAssets
|
||||
|
||||
private var __AllAssets: ____KtList<ImageVector>? = null
|
||||
|
||||
public val JuiAssets.AllAssets: ____KtList<ImageVector>
|
||||
get() {
|
||||
if (__AllAssets != null) {
|
||||
return __AllAssets!!
|
||||
}
|
||||
__AllAssets= Ca.AllAssets + listOf(DonePrev)
|
||||
return __AllAssets!!
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package ca.gosyer.jui.uicore.icons.juiassets
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.PathFillType
|
||||
import androidx.compose.ui.graphics.PathFillType.Companion.NonZero
|
||||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.graphics.StrokeCap
|
||||
import androidx.compose.ui.graphics.StrokeCap.Companion.Butt
|
||||
import androidx.compose.ui.graphics.StrokeJoin
|
||||
import androidx.compose.ui.graphics.StrokeJoin.Companion.Miter
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.graphics.vector.ImageVector.Builder
|
||||
import androidx.compose.ui.graphics.vector.path
|
||||
import androidx.compose.ui.unit.dp
|
||||
import ca.gosyer.jui.uicore.icons.JuiAssets
|
||||
|
||||
public val JuiAssets.DonePrev: ImageVector
|
||||
get() {
|
||||
if (_donePrev != null) {
|
||||
return _donePrev!!
|
||||
}
|
||||
_donePrev = Builder(name = "DonePrev", defaultWidth = 24.0.dp, defaultHeight = 24.0.dp,
|
||||
viewportWidth = 24.0f, viewportHeight = 24.0f).apply {
|
||||
path(fill = SolidColor(Color(0xFF000000)), stroke = null, strokeLineWidth = 0.0f,
|
||||
strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f,
|
||||
pathFillType = NonZero) {
|
||||
moveTo(9.0f, 16.2f)
|
||||
lineTo(4.8f, 12.0f)
|
||||
lineToRelative(-1.4f, 1.4f)
|
||||
lineTo(9.0f, 19.0f)
|
||||
lineTo(21.0f, 7.0f)
|
||||
lineToRelative(-1.4f, -1.4f)
|
||||
lineTo(9.0f, 16.2f)
|
||||
close()
|
||||
}
|
||||
path(fill = SolidColor(Color(0xFF000000)), stroke = null, strokeLineWidth = 0.0f,
|
||||
strokeLineCap = Butt, strokeLineJoin = Miter, strokeLineMiter = 4.0f,
|
||||
pathFillType = NonZero) {
|
||||
moveTo(22.0f, 18.0f)
|
||||
lineToRelative(-3.0f, 0.0f)
|
||||
lineToRelative(0.0f, -4.0f)
|
||||
lineToRelative(-2.0f, 0.0f)
|
||||
lineToRelative(0.0f, 4.0f)
|
||||
lineToRelative(-3.0f, 0.0f)
|
||||
lineToRelative(4.0f, 4.0f)
|
||||
close()
|
||||
}
|
||||
}
|
||||
.build()
|
||||
return _donePrev!!
|
||||
}
|
||||
|
||||
private var _donePrev: ImageVector? = null
|
||||
@@ -0,0 +1,23 @@
|
||||
package ca.gosyer.jui.uicore.icons.juiassets
|
||||
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import ca.gosyer.jui.uicore.icons.JuiAssets
|
||||
import ca.gosyer.jui.uicore.icons.juiassets.ca.AllAssets
|
||||
import ca.gosyer.jui.uicore.icons.juiassets.ca.Gosyer
|
||||
import kotlin.collections.List as ____KtList
|
||||
|
||||
public object CaGroup
|
||||
|
||||
public val JuiAssets.Ca: CaGroup
|
||||
get() = CaGroup
|
||||
|
||||
private var __AllAssets: ____KtList<ImageVector>? = null
|
||||
|
||||
public val CaGroup.AllAssets: ____KtList<ImageVector>
|
||||
get() {
|
||||
if (__AllAssets != null) {
|
||||
return __AllAssets!!
|
||||
}
|
||||
__AllAssets= Gosyer.AllAssets + listOf()
|
||||
return __AllAssets!!
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package ca.gosyer.jui.uicore.icons.juiassets.ca
|
||||
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import ca.gosyer.jui.uicore.icons.juiassets.CaGroup
|
||||
import ca.gosyer.jui.uicore.icons.juiassets.ca.gosyer.AllAssets
|
||||
import ca.gosyer.jui.uicore.icons.juiassets.ca.gosyer.Jui
|
||||
import kotlin.collections.List as ____KtList
|
||||
|
||||
public object GosyerGroup
|
||||
|
||||
public val CaGroup.Gosyer: GosyerGroup
|
||||
get() = GosyerGroup
|
||||
|
||||
private var __AllAssets: ____KtList<ImageVector>? = null
|
||||
|
||||
public val GosyerGroup.AllAssets: ____KtList<ImageVector>
|
||||
get() {
|
||||
if (__AllAssets != null) {
|
||||
return __AllAssets!!
|
||||
}
|
||||
__AllAssets= Jui.AllAssets + listOf()
|
||||
return __AllAssets!!
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package ca.gosyer.jui.uicore.icons.juiassets.ca.gosyer
|
||||
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import ca.gosyer.jui.uicore.icons.juiassets.ca.GosyerGroup
|
||||
import ca.gosyer.jui.uicore.icons.juiassets.ca.gosyer.jui.AllAssets
|
||||
import ca.gosyer.jui.uicore.icons.juiassets.ca.gosyer.jui.Uicore
|
||||
import kotlin.collections.List as ____KtList
|
||||
|
||||
public object JuiGroup
|
||||
|
||||
public val GosyerGroup.Jui: JuiGroup
|
||||
get() = JuiGroup
|
||||
|
||||
private var __AllAssets: ____KtList<ImageVector>? = null
|
||||
|
||||
public val JuiGroup.AllAssets: ____KtList<ImageVector>
|
||||
get() {
|
||||
if (__AllAssets != null) {
|
||||
return __AllAssets!!
|
||||
}
|
||||
__AllAssets= Uicore.AllAssets + listOf()
|
||||
return __AllAssets!!
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package ca.gosyer.jui.uicore.icons.juiassets.ca.gosyer.jui
|
||||
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import ca.gosyer.jui.uicore.icons.juiassets.ca.gosyer.JuiGroup
|
||||
import ca.gosyer.jui.uicore.icons.juiassets.ca.gosyer.jui.uicore.AllAssets
|
||||
import ca.gosyer.jui.uicore.icons.juiassets.ca.gosyer.jui.uicore.Icons
|
||||
import kotlin.collections.List as ____KtList
|
||||
|
||||
public object UicoreGroup
|
||||
|
||||
public val JuiGroup.Uicore: UicoreGroup
|
||||
get() = UicoreGroup
|
||||
|
||||
private var __AllAssets: ____KtList<ImageVector>? = null
|
||||
|
||||
public val UicoreGroup.AllAssets: ____KtList<ImageVector>
|
||||
get() {
|
||||
if (__AllAssets != null) {
|
||||
return __AllAssets!!
|
||||
}
|
||||
__AllAssets= Icons.AllAssets + listOf()
|
||||
return __AllAssets!!
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package ca.gosyer.jui.uicore.icons.juiassets.ca.gosyer.jui.uicore
|
||||
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import ca.gosyer.jui.uicore.icons.juiassets.ca.gosyer.jui.UicoreGroup
|
||||
import ca.gosyer.jui.uicore.icons.juiassets.ca.gosyer.jui.uicore.icons.AllAssets
|
||||
import ca.gosyer.jui.uicore.icons.juiassets.ca.gosyer.jui.uicore.icons.Juiassets
|
||||
import kotlin.collections.List as ____KtList
|
||||
|
||||
public object IconsGroup
|
||||
|
||||
public val UicoreGroup.Icons: IconsGroup
|
||||
get() = IconsGroup
|
||||
|
||||
private var __AllAssets: ____KtList<ImageVector>? = null
|
||||
|
||||
public val IconsGroup.AllAssets: ____KtList<ImageVector>
|
||||
get() {
|
||||
if (__AllAssets != null) {
|
||||
return __AllAssets!!
|
||||
}
|
||||
__AllAssets= Juiassets.AllAssets + listOf()
|
||||
return __AllAssets!!
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package ca.gosyer.jui.uicore.icons.juiassets.ca.gosyer.jui.uicore.icons
|
||||
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import ca.gosyer.jui.uicore.icons.juiassets.ca.gosyer.jui.uicore.IconsGroup
|
||||
import kotlin.collections.List as ____KtList
|
||||
|
||||
public object JuiassetsGroup
|
||||
|
||||
public val IconsGroup.Juiassets: JuiassetsGroup
|
||||
get() = JuiassetsGroup
|
||||
|
||||
private var __AllAssets: ____KtList<ImageVector>? = null
|
||||
|
||||
public val JuiassetsGroup.AllAssets: ____KtList<ImageVector>
|
||||
get() {
|
||||
if (__AllAssets != null) {
|
||||
return __AllAssets!!
|
||||
}
|
||||
__AllAssets= listOf()
|
||||
return __AllAssets!!
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* 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.jui.uicore.components
|
||||
|
||||
import androidx.compose.foundation.hoverable
|
||||
import androidx.compose.foundation.interaction.HoverInteraction
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.foundation.onClick
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.composed
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.mapLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
actual fun Modifier.buttonModifier(
|
||||
onClick: () -> Unit,
|
||||
onHintClick: () -> Unit,
|
||||
): Modifier = composed {
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
LaunchedEffect(interactionSource) {
|
||||
launch {
|
||||
interactionSource.interactions
|
||||
.mapLatest {
|
||||
if (it !is HoverInteraction.Enter) return@mapLatest
|
||||
delay(2.seconds)
|
||||
onHintClick()
|
||||
}
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
interactionSource.interactions
|
||||
onClick(onClick = onClick)
|
||||
.hoverable(interactionSource)
|
||||
}
|
||||
@@ -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.jui.uicore.components
|
||||
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||
import androidx.compose.material.ripple.rememberRipple
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.composed
|
||||
|
||||
actual fun Modifier.buttonModifier(
|
||||
onClick: () -> Unit,
|
||||
onHintClick: () -> Unit,
|
||||
): Modifier = composed {
|
||||
combinedClickable(
|
||||
interactionSource = remember { MutableInteractionSource() },
|
||||
indication = rememberRipple(bounded = false),
|
||||
onLongClick = onHintClick,
|
||||
onClick = onClick,
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user