Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(storage-viewer): added support for local anime and manga #1190

Closed
wants to merge 15 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -31,15 +31,14 @@ fun CumulativeStorage(
Canvas(
modifier = Modifier.aspectRatio(1f),
onDraw = {
// don't bother with drawing anything if there's no data
if (totalSize == 0f) return@Canvas

val totalAngle = 180f
var currentAngle = 0f
rotate(180f) {
for (item in items) {
val itemAngle = if (totalSize > 0f) {
(item.size / totalSize) * totalAngle
} else {
0f
}
val itemAngle = (item.size / totalSize) * totalAngle
drawArc(
color = item.color,
startAngle = currentAngle,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ data class StorageItem(
val thumbnail: String?,
val entriesCount: Int,
val color: Color,
val showDeleteButton: Boolean,
)

@Composable
Expand Down Expand Up @@ -110,17 +111,19 @@ fun StorageItem(
)
},
)
IconButton(
onClick = {
showDeleteDialog = true
},
content = {
Icon(
imageVector = Icons.Default.Delete,
contentDescription = stringResource(R.string.action_delete),
)
},
)
if (item.showDeleteButton) {
IconButton(
onClick = {
showDeleteDialog = true
},
content = {
Icon(
imageVector = Icons.Default.Delete,
contentDescription = stringResource(R.string.action_delete),
)
},
)
}
},
)

Expand Down Expand Up @@ -190,6 +193,7 @@ private fun StorageItemPreview() {
thumbnail = null,
entriesCount = 123,
color = Color.Red,
showDeleteButton = true,
),
isManga = true,
onDelete = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ private fun StorageScreenContentPreview() {
random.nextInt(255),
random.nextInt(255),
),
showDeleteButton = true,
)
},
categories = categories,
Expand Down Expand Up @@ -183,6 +184,7 @@ private fun StorageTabletUiScreenContentPreview() {
random.nextInt(255),
random.nextInt(255),
),
showDeleteButton = true,
)
},
categories = categories,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -254,6 +254,22 @@ class AnimeDownloadManager(
}
}

/**
* Returns the size of downloaded episodes.
*/
fun getDownloadSize(): Long {
return cache.getTotalDownloadSize()
}

/**
* Returns the size of downloaded episodes for an anime.
*
* @param anime the anime to check.
*/
fun getDownloadSize(anime: Anime): Long {
return cache.getDownloadSize(anime)
}

fun cancelQueuedDownloads(downloads: List<AnimeDownload>) {
removeFromDownloadQueue(downloads.map { it.episode })
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,22 @@ class MangaDownloadManager(
}
}

/**
* Returns the size of downloaded chapters.
*/
fun getDownloadSize(): Long {
return cache.getTotalDownloadSize()
}

/**
* Returns the size of downloaded chapters for a manga.
*
* @param manga the manga to check.
*/
fun getDownloadSize(manga: Manga): Long {
return cache.getDownloadSize(manga)
}

fun cancelQueuedDownloads(downloads: List<MangaDownload>) {
removeFromDownloadQueue(downloads.map { it.chapter })
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,14 @@ abstract class CommonStorageScreenModel<T>(
private val downloadCacheIsInitializing: StateFlow<Boolean>,
private val libraries: Flow<List<T>>,
private val categories: Flow<List<Category>>,
private val getTotalDownloadSize: () -> Long,
private val getDownloadSize: T.() -> Long,
private val getDownloadCount: T.() -> Int,
private val getId: T.() -> Long,
private val getCategoryId: T.() -> Long,
private val getTitle: T.() -> String,
private val getThumbnail: T.() -> String?,
private val isFromLocalSource: T.() -> Boolean,
) : StateScreenModel<StorageScreenState>(StorageScreenState.Loading) {

private val selectedCategory = MutableStateFlow(AllCategory)
Expand All @@ -46,13 +48,14 @@ abstract class CommonStorageScreenModel<T>(
}.filter {
selectedCategory == AllCategory || it.getCategoryId() == selectedCategory.id
}
val size = getTotalDownloadSize()
val random = Random(size + distinctLibraries.size)

mutableState.update {
StorageScreenState.Success(
selectedCategory = selectedCategory,
categories = listOf(AllCategory, *categories.toTypedArray()),
items = distinctLibraries.map {
val random = Random(it.getId())
StorageItem(
id = it.getId(),
title = it.getTitle(),
Expand All @@ -64,6 +67,7 @@ abstract class CommonStorageScreenModel<T>(
random.nextInt(255),
random.nextInt(255),
),
showDeleteButton = !it.isFromLocalSource(),
)
}.sortedByDescending { it.size },
)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
package eu.kanade.tachiyomi.ui.storage.anime

import cafe.adriel.voyager.core.model.coroutineScope
import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.data.download.anime.AnimeDownloadCache
import eu.kanade.tachiyomi.data.download.anime.AnimeDownloadManager
import eu.kanade.tachiyomi.ui.storage.CommonStorageScreenModel
import eu.kanade.tachiyomi.util.size
import tachiyomi.core.util.lang.launchNonCancellable
import tachiyomi.domain.category.anime.interactor.GetVisibleAnimeCategories
import tachiyomi.domain.entries.anime.interactor.GetLibraryAnime
import tachiyomi.domain.library.anime.LibraryAnime
import tachiyomi.domain.source.anime.service.AnimeSourceManager
import tachiyomi.source.local.entries.anime.isLocal
import tachiyomi.source.local.io.anime.LocalAnimeSourceFileSystem
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get

Expand All @@ -18,17 +22,34 @@ class AnimeStorageScreenModel(
getVisibleCategories: GetVisibleAnimeCategories = Injekt.get(),
private val downloadManager: AnimeDownloadManager = Injekt.get(),
private val sourceManager: AnimeSourceManager = Injekt.get(),
private val localManager: LocalAnimeSourceFileSystem = Injekt.get(),
) : CommonStorageScreenModel<LibraryAnime>(
downloadCacheChanges = downloadCache.changes,
downloadCacheIsInitializing = downloadCache.isInitializing,
libraries = getLibraries.subscribe(),
categories = getVisibleCategories.subscribe(),
getDownloadSize = { downloadManager.getDownloadSize(anime) },
getDownloadCount = { downloadManager.getDownloadCount(anime) },
getTotalDownloadSize = { downloadManager.getDownloadSize() },
getDownloadSize = {
if (sourceManager.getOrStub(anime.source).isLocal()) {
localManager.getEpisodesInAnimeDirectory(anime.title)
.map { UniFile.fromFile(it)?.size() ?: 0 }
.sum()
} else {
downloadManager.getDownloadSize(anime)
}
},
getDownloadCount = {
if (sourceManager.getOrStub(anime.source).isLocal()) {
localManager.getEpisodesInAnimeDirectory(anime.title).count()
} else {
downloadManager.getDownloadCount(anime)
}
},
getId = { id },
getCategoryId = { category },
getTitle = { anime.title },
getThumbnail = { anime.thumbnailUrl },
isFromLocalSource = { sourceManager.getOrStub(anime.source).isLocal() },
) {
override fun deleteEntry(id: Long) {
coroutineScope.launchNonCancellable {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
package eu.kanade.tachiyomi.ui.storage.manga

import cafe.adriel.voyager.core.model.coroutineScope
import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.data.download.manga.MangaDownloadCache
import eu.kanade.tachiyomi.data.download.manga.MangaDownloadManager
import eu.kanade.tachiyomi.ui.storage.CommonStorageScreenModel
import eu.kanade.tachiyomi.util.size
import tachiyomi.core.util.lang.launchNonCancellable
import tachiyomi.domain.category.manga.interactor.GetVisibleMangaCategories
import tachiyomi.domain.entries.manga.interactor.GetLibraryManga
import tachiyomi.domain.library.manga.LibraryManga
import tachiyomi.domain.source.manga.service.MangaSourceManager
import tachiyomi.source.local.entries.manga.isLocal
import tachiyomi.source.local.io.manga.LocalMangaSourceFileSystem
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get

Expand All @@ -18,17 +22,34 @@ class MangaStorageScreenModel(
getVisibleCategories: GetVisibleMangaCategories = Injekt.get(),
private val downloadManager: MangaDownloadManager = Injekt.get(),
private val sourceManager: MangaSourceManager = Injekt.get(),
private val localManager: LocalMangaSourceFileSystem = Injekt.get(),
) : CommonStorageScreenModel<LibraryManga>(
downloadCacheChanges = downloadCache.changes,
downloadCacheIsInitializing = downloadCache.isInitializing,
libraries = getLibraries.subscribe(),
categories = getVisibleCategories.subscribe(),
getDownloadSize = { downloadManager.getDownloadSize(manga) },
getDownloadCount = { downloadManager.getDownloadCount(manga) },
getTotalDownloadSize = { downloadManager.getDownloadSize() },
getDownloadSize = {
if (sourceManager.getOrStub(manga.source).isLocal()) {
localManager.getChaptersInMangaDirectory(manga.title)
.map { UniFile.fromFile(it)?.size() ?: 0 }
.sum()
} else {
downloadManager.getDownloadSize(manga)
}
},
getDownloadCount = {
if (sourceManager.getOrStub(manga.source).isLocal()) {
localManager.getChaptersInMangaDirectory(manga.title).count()
} else {
downloadManager.getDownloadCount(manga)
}
},
getId = { id },
getCategoryId = { category },
getTitle = { manga.title },
getThumbnail = { manga.thumbnailUrl },
isFromLocalSource = { sourceManager.getOrStub(manga.source).isLocal() },
) {
override fun deleteEntry(id: Long) {
coroutineScope.launchNonCancellable {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,6 @@ import tachiyomi.domain.items.episode.service.EpisodeRecognition
import tachiyomi.source.local.R
import tachiyomi.source.local.filter.anime.AnimeOrderBy
import tachiyomi.source.local.image.anime.LocalAnimeCoverManager
import tachiyomi.source.local.io.ArchiveAnime
import tachiyomi.source.local.io.anime.LocalAnimeSourceFileSystem
import uy.kohesive.injekt.injectLazy
import java.io.File
Expand Down Expand Up @@ -155,9 +154,7 @@ actual class LocalAnimeSource(

// Episodes
override suspend fun getEpisodeList(anime: SAnime): List<SEpisode> {
return fileSystem.getFilesInAnimeDirectory(anime.url)
// Only keep supported formats
.filter { it.isDirectory || ArchiveAnime.isSupported(it) }
return fileSystem.getEpisodesInAnimeDirectory(anime.url)
.map { episodeFile ->
SEpisode.create().apply {
url = episodeFile.absolutePath
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -256,9 +256,7 @@ actual class LocalMangaSource(

// Chapters
override suspend fun getChapterList(manga: SManga): List<SChapter> {
return fileSystem.getFilesInMangaDirectory(manga.url)
// Only keep supported formats
.filter { it.isDirectory || ArchiveManga.isSupported(it) }
return fileSystem.getChaptersInMangaDirectory(manga.url)
.map { chapterFile ->
SChapter.create().apply {
url = "${manga.url}/${chapterFile.name}"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package tachiyomi.source.local.io.anime
import android.content.Context
import eu.kanade.tachiyomi.util.storage.DiskUtil
import tachiyomi.source.local.R
import tachiyomi.source.local.io.ArchiveAnime
import java.io.File

actual class LocalAnimeSourceFileSystem(
Expand Down Expand Up @@ -36,4 +37,10 @@ actual class LocalAnimeSourceFileSystem(
// Get all the files inside the filtered folders
.flatMap { it.listFiles().orEmpty().toList() }
}

actual fun getEpisodesInAnimeDirectory(name: String): Sequence<File> {
return getFilesInAnimeDirectory(name).filter {
it.isDirectory || ArchiveAnime.isSupported(it)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package tachiyomi.source.local.io.manga
import android.content.Context
import eu.kanade.tachiyomi.util.storage.DiskUtil
import tachiyomi.source.local.R
import tachiyomi.source.local.io.ArchiveManga
import java.io.File

actual class LocalMangaSourceFileSystem(
Expand Down Expand Up @@ -36,4 +37,10 @@ actual class LocalMangaSourceFileSystem(
// Get all the files inside the filtered folders
.flatMap { it.listFiles().orEmpty().toList() }
}

actual fun getChaptersInMangaDirectory(name: String): Sequence<File> {
return getFilesInMangaDirectory(name).filter {
it.isDirectory || ArchiveManga.isSupported(it)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,6 @@ expect class LocalAnimeSourceFileSystem {
fun getAnimeDirectory(name: String): File?

fun getFilesInAnimeDirectory(name: String): Sequence<File>

fun getEpisodesInAnimeDirectory(name: String): Sequence<File>
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,6 @@ expect class LocalMangaSourceFileSystem {
fun getMangaDirectory(name: String): File?

fun getFilesInMangaDirectory(name: String): Sequence<File>

fun getChaptersInMangaDirectory(name: String): Sequence<File>
}