diff --git a/app/src/main/java/eu/kanade/data/chapter/ChapterRepositoryImpl.kt b/app/src/main/java/eu/kanade/data/chapter/ChapterRepositoryImpl.kt index cfa52dc56c..fe9eafc156 100644 --- a/app/src/main/java/eu/kanade/data/chapter/ChapterRepositoryImpl.kt +++ b/app/src/main/java/eu/kanade/data/chapter/ChapterRepositoryImpl.kt @@ -92,6 +92,10 @@ class ChapterRepositoryImpl( return handler.awaitList { chaptersQueries.getChaptersByMangaId(mangaId, chapterMapper) } } + override suspend fun getChapterById(id: Long): Chapter? { + return handler.awaitOneOrNull { chaptersQueries.getChapterById(id, chapterMapper) } + } + override suspend fun getChapterByMangaIdAsFlow(mangaId: Long): Flow> { return handler.subscribeToList { chaptersQueries.getChaptersByMangaId(mangaId, chapterMapper) } } diff --git a/app/src/main/java/eu/kanade/domain/DomainModule.kt b/app/src/main/java/eu/kanade/domain/DomainModule.kt index 3cb07b9fc1..bcd296777b 100644 --- a/app/src/main/java/eu/kanade/domain/DomainModule.kt +++ b/app/src/main/java/eu/kanade/domain/DomainModule.kt @@ -12,6 +12,7 @@ import eu.kanade.domain.category.interactor.InsertCategory import eu.kanade.domain.category.interactor.MoveMangaToCategories import eu.kanade.domain.category.interactor.UpdateCategory import eu.kanade.domain.category.repository.CategoryRepository +import eu.kanade.domain.chapter.interactor.GetChapter import eu.kanade.domain.chapter.interactor.GetChapterByMangaId import eu.kanade.domain.chapter.interactor.ShouldUpdateDbChapter import eu.kanade.domain.chapter.interactor.SyncChaptersWithSource @@ -84,6 +85,7 @@ class DomainModule : InjektModule { addFactory { InsertTrack(get()) } addSingletonFactory { ChapterRepositoryImpl(get()) } + addFactory { GetChapter(get()) } addFactory { GetChapterByMangaId(get()) } addFactory { UpdateChapter(get()) } addFactory { ShouldUpdateDbChapter() } diff --git a/app/src/main/java/eu/kanade/domain/chapter/interactor/GetChapter.kt b/app/src/main/java/eu/kanade/domain/chapter/interactor/GetChapter.kt new file mode 100644 index 0000000000..28724ef589 --- /dev/null +++ b/app/src/main/java/eu/kanade/domain/chapter/interactor/GetChapter.kt @@ -0,0 +1,20 @@ +package eu.kanade.domain.chapter.interactor + +import eu.kanade.domain.chapter.model.Chapter +import eu.kanade.domain.chapter.repository.ChapterRepository +import eu.kanade.tachiyomi.util.system.logcat +import logcat.LogPriority + +class GetChapter( + private val chapterRepository: ChapterRepository, +) { + + suspend fun await(id: Long): Chapter? { + return try { + chapterRepository.getChapterById(id) + } catch (e: Exception) { + logcat(LogPriority.ERROR, e) + null + } + } +} diff --git a/app/src/main/java/eu/kanade/domain/chapter/repository/ChapterRepository.kt b/app/src/main/java/eu/kanade/domain/chapter/repository/ChapterRepository.kt index 7d9c47afc6..2a2abd5f3f 100644 --- a/app/src/main/java/eu/kanade/domain/chapter/repository/ChapterRepository.kt +++ b/app/src/main/java/eu/kanade/domain/chapter/repository/ChapterRepository.kt @@ -16,5 +16,7 @@ interface ChapterRepository { suspend fun getChapterByMangaId(mangaId: Long): List + suspend fun getChapterById(id: Long): Chapter? + suspend fun getChapterByMangaIdAsFlow(mangaId: Long): Flow> } diff --git a/app/src/main/java/eu/kanade/presentation/components/ChapterDownloadIndicator.kt b/app/src/main/java/eu/kanade/presentation/components/ChapterDownloadIndicator.kt index 693f8dee90..b3474b4520 100644 --- a/app/src/main/java/eu/kanade/presentation/components/ChapterDownloadIndicator.kt +++ b/app/src/main/java/eu/kanade/presentation/components/ChapterDownloadIndicator.kt @@ -10,7 +10,6 @@ import androidx.compose.material.icons.filled.CheckCircle import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.Icon -import androidx.compose.material3.IconButton import androidx.compose.material3.LocalMinimumTouchTargetEnforcement import androidx.compose.material3.MaterialTheme import androidx.compose.material3.ProgressIndicatorDefaults @@ -51,6 +50,14 @@ fun ChapterDownloadIndicator( onClick(ChapterDownloadAction.START) } }, + onLongClick = { + val chapterDownloadAction = when { + isDownloaded -> ChapterDownloadAction.DELETE + isDownloading -> ChapterDownloadAction.CANCEL + else -> ChapterDownloadAction.START_NOW + } + onClick(chapterDownloadAction) + }, ) { if (isDownloaded) { Icon( diff --git a/app/src/main/java/eu/kanade/presentation/components/IconButton.kt b/app/src/main/java/eu/kanade/presentation/components/IconButton.kt new file mode 100644 index 0000000000..618da2b5c3 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/components/IconButton.kt @@ -0,0 +1,105 @@ +/* + * Copyright 2021 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package eu.kanade.presentation.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.interaction.Interaction +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.size +import androidx.compose.material.ripple.rememberRipple +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButtonColors +import androidx.compose.material3.IconButtonDefaults +import androidx.compose.material3.LocalContentColor +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.semantics.Role +import androidx.compose.ui.unit.dp +import eu.kanade.presentation.util.minimumTouchTargetSize + +/** + * Material Design standard icon button. + * + * Icon buttons help people take supplementary actions with a single tap. They’re used when a + * compact button is required, such as in a toolbar or image list. + * + * ![Standard icon button image](https://developer.android.com/images/reference/androidx/compose/material3/standard-icon-button.png) + * + * [content] should typically be an [Icon] (see [androidx.compose.material.icons.Icons]). If using a + * custom icon, note that the typical size for the internal icon is 24 x 24 dp. + * This icon button has an overall minimum touch target size of 48 x 48dp, to meet accessibility + * guidelines. + * + * @sample androidx.compose.material3.samples.IconButtonSample + * + * Tachiyomi changes: + * * Add on long click + * + * @param onClick called when this icon button is clicked + * @param modifier the [Modifier] to be applied to this icon button + * @param enabled controls the enabled state of this icon button. When `false`, this component will + * not respond to user input, and it will appear visually disabled and disabled to accessibility + * services. + * @param interactionSource the [MutableInteractionSource] representing the stream of [Interaction]s + * for this icon button. You can create and pass in your own `remember`ed instance to observe + * [Interaction]s and customize the appearance / behavior of this icon button in different states. + * @param colors [IconButtonColors] that will be used to resolve the colors used for this icon + * button in different states. See [IconButtonDefaults.iconButtonColors]. + * @param content the content of this icon button, typically an [Icon] + */ +@Composable +fun IconButton( + onClick: () -> Unit, + onLongClick: () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + interactionSource: MutableInteractionSource = remember { MutableInteractionSource() }, + colors: IconButtonColors = IconButtonDefaults.iconButtonColors(), + content: @Composable () -> Unit, +) { + Box( + modifier = + modifier + .minimumTouchTargetSize() + .size(IconButtonTokens.StateLayerSize) + .background(color = colors.containerColor(enabled).value) + .combinedClickable( + onClick = onClick, + onLongClick = onLongClick, + enabled = enabled, + role = Role.Button, + interactionSource = interactionSource, + indication = rememberRipple( + bounded = false, + radius = IconButtonTokens.StateLayerSize / 2, + ), + ), + contentAlignment = Alignment.Center, + ) { + val contentColor = colors.contentColor(enabled).value + CompositionLocalProvider(LocalContentColor provides contentColor, content = content) + } +} + +object IconButtonTokens { + val StateLayerSize = 40.0.dp +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadManager.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadManager.kt index c6706398f8..66e4bd9b64 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/DownloadManager.kt @@ -15,6 +15,7 @@ import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.util.lang.launchIO import eu.kanade.tachiyomi.util.system.logcat +import kotlinx.coroutines.runBlocking import logcat.LogPriority import rx.Observable import uy.kohesive.injekt.Injekt @@ -104,10 +105,12 @@ class DownloadManager( fun startDownloadNow(chapterId: Long?) { if (chapterId == null) return - val download = downloader.queue.find { it.chapter.id == chapterId } ?: return + val download = downloader.queue.find { it.chapter.id == chapterId } + // If not in queue try to start a new download + val toAdd = download ?: runBlocking { Download.fromChapterId(chapterId) } ?: return val queue = downloader.queue.toMutableList() - queue.remove(download) - queue.add(0, download) + download?.let { queue.remove(it) } + queue.add(0, toAdd) reorderQueue(queue) if (isPaused()) { if (DownloadService.isRunning(context)) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/model/Download.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/model/Download.kt index a1438b9949..2b8a384515 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/model/Download.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/model/Download.kt @@ -1,10 +1,17 @@ package eu.kanade.tachiyomi.data.download.model +import eu.kanade.domain.chapter.interactor.GetChapter +import eu.kanade.domain.chapter.model.toDbChapter +import eu.kanade.domain.manga.interactor.GetMangaById +import eu.kanade.domain.manga.model.toDbManga import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.online.HttpSource import rx.subjects.PublishSubject +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get data class Download( val source: HttpSource, @@ -57,4 +64,19 @@ data class Download( DOWNLOADED(3), ERROR(4), } + + companion object { + suspend fun fromChapterId( + chapterId: Long, + getChapter: GetChapter = Injekt.get(), + getMangaById: GetMangaById = Injekt.get(), + sourceManager: SourceManager = Injekt.get(), + ): Download? { + val chapter = getChapter.await(chapterId) ?: return null + val manga = getMangaById.await(chapter.mangaId) ?: return null + val source = sourceManager.get(manga.source) as? HttpSource ?: return null + + return Download(source, manga.toDbManga(), chapter.toDbChapter()) + } + } }