Last Commit Merged: 5b2e937d5f
This commit is contained in:
LuftVerbot 2023-06-06 12:55:36 +02:00
parent cd98fe13f7
commit 54ec5ac72c
123 changed files with 1500 additions and 727 deletions

View file

@ -141,7 +141,9 @@ android {
dependencies {
implementation(project(":i18n"))
implementation(project(":core"))
implementation(project(":core-metadata"))
implementation(project(":source-api"))
implementation(project(":source-local"))
implementation(project(":data"))
implementation(project(":domain"))
implementation(project(":presentation-core"))
@ -201,7 +203,7 @@ dependencies {
// TLS 1.3 support for Android < 10
implementation(libs.conscrypt.android)
// Data serialization (JSON, protobuf)
// Data serialization (JSON, protobuf, xml)
implementation(kotlinx.bundles.serialization)
// HTML parser
@ -225,9 +227,6 @@ dependencies {
}
implementation(libs.image.decoder)
// Sort
implementation(libs.natural.comparator)
// UI libraries
implementation(libs.material)
implementation(libs.flexible.adapter.core)

View file

@ -1,9 +1,9 @@
package eu.kanade.core.prefs
package eu.kanade.core.preference
import androidx.compose.ui.state.ToggleableState
import tachiyomi.core.preference.CheckboxState
fun <T> CheckboxState.TriState<T>.asState(): ToggleableState {
fun <T> CheckboxState.TriState<T>.asToggleableState(): ToggleableState {
return when (this) {
is CheckboxState.TriState.Exclude -> ToggleableState.Indeterminate
is CheckboxState.TriState.Include -> ToggleableState.On

View file

@ -1,4 +1,4 @@
package eu.kanade.core.prefs
package eu.kanade.core.preference
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableStateOf

View file

@ -1,16 +1,19 @@
package eu.kanade.data.source.anime
import eu.kanade.domain.source.anime.model.AnimeSourcePagingSourceType
import eu.kanade.domain.source.anime.repository.AnimeSourceRepository
import eu.kanade.tachiyomi.animesource.AnimeCatalogueSource
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
import eu.kanade.tachiyomi.source.anime.AnimeSourceManager
import eu.kanade.tachiyomi.source.anime.LocalAnimeSource
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import tachiyomi.data.handlers.anime.AnimeDatabaseHandler
import tachiyomi.data.source.anime.AnimeSourceLatestPagingSource
import tachiyomi.data.source.anime.AnimeSourcePagingSourceType
import tachiyomi.data.source.anime.AnimeSourcePopularPagingSource
import tachiyomi.data.source.anime.AnimeSourceSearchPagingSource
import tachiyomi.domain.source.anime.model.AnimeSource
import tachiyomi.domain.source.anime.model.AnimeSourceWithCount
import tachiyomi.source.local.entries.anime.LocalAnimeSource
class AnimeSourceRepositoryImpl(
private val sourceManager: AnimeSourceManager,
@ -35,9 +38,7 @@ class AnimeSourceRepositoryImpl(
sourceIdsWithCount
.filterNot { it.source == LocalAnimeSource.ID }
.map { (sourceId, count) ->
val source = sourceManager.getOrStub(sourceId).run {
animeSourceMapper(this)
}
val source = animeSourceMapper(sourceManager.getOrStub(sourceId))
source to count
}
}

View file

@ -1,16 +1,19 @@
package eu.kanade.data.source.manga
import eu.kanade.domain.source.manga.model.SourcePagingSourceType
import eu.kanade.domain.source.manga.repository.MangaSourceRepository
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.manga.LocalMangaSource
import eu.kanade.tachiyomi.source.manga.MangaSourceManager
import eu.kanade.tachiyomi.source.model.FilterList
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import tachiyomi.data.handlers.manga.MangaDatabaseHandler
import tachiyomi.data.source.manga.SourceLatestPagingSource
import tachiyomi.data.source.manga.SourcePagingSourceType
import tachiyomi.data.source.manga.SourcePopularPagingSource
import tachiyomi.data.source.manga.SourceSearchPagingSource
import tachiyomi.domain.source.manga.model.MangaSourceWithCount
import tachiyomi.domain.source.manga.model.Source
import tachiyomi.source.local.entries.manga.LocalMangaSource
class MangaSourceRepositoryImpl(
private val sourceManager: MangaSourceManager,
@ -35,9 +38,7 @@ class MangaSourceRepositoryImpl(
sourceIdsWithCount
.filterNot { it.source == LocalMangaSource.ID }
.map { (sourceId, count) ->
val source = sourceManager.getOrStub(sourceId).run {
mangaSourceMapper(this)
}
val source = mangaSourceMapper(sourceManager.getOrStub(sourceId))
source to count
}
}

View file

@ -2,12 +2,15 @@ package eu.kanade.domain.entries.manga.model
import eu.kanade.domain.base.BasePreferences
import eu.kanade.tachiyomi.data.cache.MangaCoverCache
import eu.kanade.tachiyomi.source.manga.LocalMangaSource
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
import tachiyomi.core.metadata.comicinfo.ComicInfo
import tachiyomi.core.metadata.comicinfo.ComicInfoPublishingStatus
import tachiyomi.domain.entries.TriStateFilter
import tachiyomi.domain.entries.manga.model.Manga
import tachiyomi.domain.items.chapter.model.Chapter
import tachiyomi.source.local.entries.manga.LocalMangaSource
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@ -91,3 +94,25 @@ fun Manga.isLocal(): Boolean = source == LocalMangaSource.ID
fun Manga.hasCustomCover(coverCache: MangaCoverCache = Injekt.get()): Boolean {
return coverCache.getCustomCoverFile(id).exists()
}
/**
* Creates a ComicInfo instance based on the manga and chapter metadata.
*/
fun getComicInfo(manga: Manga, chapter: Chapter, chapterUrl: String) = ComicInfo(
title = ComicInfo.Title(chapter.name),
series = ComicInfo.Series(manga.title),
web = ComicInfo.Web(chapterUrl),
summary = manga.description?.let { ComicInfo.Summary(it) },
writer = manga.author?.let { ComicInfo.Writer(it) },
penciller = manga.artist?.let { ComicInfo.Penciller(it) },
translator = chapter.scanlator?.let { ComicInfo.Translator(it) },
genre = manga.genre?.let { ComicInfo.Genre(it.joinToString()) },
publishingStatus = ComicInfo.PublishingStatusTachiyomi(
ComicInfoPublishingStatus.toComicInfoValue(manga.status),
),
inker = null,
colorist = null,
letterer = null,
coverArtist = null,
tags = null,
)

View file

@ -1,8 +1,8 @@
package eu.kanade.domain.source.anime.interactor
import eu.kanade.domain.source.anime.model.AnimeSourcePagingSourceType
import eu.kanade.domain.source.anime.repository.AnimeSourceRepository
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
import tachiyomi.data.source.anime.AnimeSourcePagingSourceType
class GetRemoteAnime(
private val repository: AnimeSourceRepository,

View file

@ -1,6 +0,0 @@
package eu.kanade.domain.source.anime.model
import androidx.paging.PagingSource
import eu.kanade.tachiyomi.animesource.model.SAnime
typealias AnimeSourcePagingSourceType = PagingSource<Long, SAnime>

View file

@ -1,8 +1,8 @@
package eu.kanade.domain.source.anime.repository
import eu.kanade.domain.source.anime.model.AnimeSourcePagingSourceType
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
import kotlinx.coroutines.flow.Flow
import tachiyomi.data.source.anime.AnimeSourcePagingSourceType
import tachiyomi.domain.source.anime.model.AnimeSource
import tachiyomi.domain.source.anime.model.AnimeSourceWithCount

View file

@ -2,13 +2,13 @@ package eu.kanade.domain.source.manga.interactor
import eu.kanade.domain.source.manga.repository.MangaSourceRepository
import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.tachiyomi.source.manga.LocalMangaSource
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import tachiyomi.domain.source.manga.model.Pin
import tachiyomi.domain.source.manga.model.Pins
import tachiyomi.domain.source.manga.model.Source
import tachiyomi.source.local.entries.manga.LocalMangaSource
class GetEnabledMangaSources(
private val repository: MangaSourceRepository,

View file

@ -1,8 +1,8 @@
package eu.kanade.domain.source.manga.interactor
import eu.kanade.domain.source.manga.model.SourcePagingSourceType
import eu.kanade.domain.source.manga.repository.MangaSourceRepository
import eu.kanade.tachiyomi.source.model.FilterList
import tachiyomi.data.source.manga.SourcePagingSourceType
class GetRemoteManga(
private val repository: MangaSourceRepository,

View file

@ -1,6 +0,0 @@
package eu.kanade.domain.source.manga.model
import androidx.paging.PagingSource
import eu.kanade.tachiyomi.source.model.SManga
typealias SourcePagingSourceType = PagingSource<Long, SManga>

View file

@ -1,8 +1,8 @@
package eu.kanade.domain.source.manga.repository
import eu.kanade.domain.source.manga.model.SourcePagingSourceType
import eu.kanade.tachiyomi.source.model.FilterList
import kotlinx.coroutines.flow.Flow
import tachiyomi.data.source.manga.SourcePagingSourceType
import tachiyomi.domain.source.manga.model.MangaSourceWithCount
import tachiyomi.domain.source.manga.model.Source

View file

@ -3,8 +3,6 @@ package eu.kanade.presentation.browse.anime.components
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ViewList
import androidx.compose.material.icons.filled.ViewModule
import androidx.compose.material.icons.outlined.Help
import androidx.compose.material.icons.outlined.Public
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.runtime.Composable
@ -21,6 +19,7 @@ import eu.kanade.presentation.components.RadioMenuItem
import eu.kanade.presentation.components.SearchToolbar
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.animesource.AnimeSource
import eu.kanade.tachiyomi.animesource.ConfigurableAnimeSource
import eu.kanade.tachiyomi.source.anime.LocalAnimeSource
import tachiyomi.domain.library.model.LibraryDisplayMode
@ -34,12 +33,16 @@ fun BrowseAnimeSourceToolbar(
navigateUp: () -> Unit,
onWebViewClick: () -> Unit,
onHelpClick: () -> Unit,
onSettingsClick: () -> Unit,
onSearch: (String) -> Unit,
scrollBehavior: TopAppBarScrollBehavior? = null,
) {
// Avoid capturing unstable source in actions lambda
val title = source?.name
val isLocalSource = source is LocalAnimeSource
val isConfigurableSource = source is ConfigurableAnimeSource
var selectingDisplayMode by remember { mutableStateOf(false) }
SearchToolbar(
navigateUp = navigateUp,
@ -49,29 +52,31 @@ fun BrowseAnimeSourceToolbar(
onSearch = onSearch,
onClickCloseSearch = navigateUp,
actions = {
var selectingDisplayMode by remember { mutableStateOf(false) }
AppBarActions(
actions = listOf(
actions = listOfNotNull(
AppBar.Action(
title = stringResource(R.string.action_display_mode),
icon = if (displayMode == LibraryDisplayMode.List) Icons.Filled.ViewList else Icons.Filled.ViewModule,
onClick = { selectingDisplayMode = true },
),
if (isLocalSource) {
AppBar.Action(
AppBar.OverflowAction(
title = stringResource(R.string.label_help),
icon = Icons.Outlined.Help,
onClick = onHelpClick,
)
} else {
AppBar.Action(
title = stringResource(R.string.action_web_view),
icon = Icons.Outlined.Public,
AppBar.OverflowAction(
title = stringResource(R.string.action_open_in_web_view),
onClick = onWebViewClick,
)
},
AppBar.OverflowAction(
title = stringResource(R.string.action_settings),
onClick = onSettingsClick,
).takeIf { isConfigurableSource },
),
)
DropdownMenu(
expanded = selectingDisplayMode,
onDismissRequest = { selectingDisplayMode = false },

View file

@ -22,7 +22,6 @@ import eu.kanade.presentation.browse.manga.components.BrowseMangaSourceList
import eu.kanade.presentation.components.AppBar
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.MangaSource
import eu.kanade.tachiyomi.source.manga.LocalMangaSource
import eu.kanade.tachiyomi.source.manga.MangaSourceManager
import kotlinx.coroutines.flow.StateFlow
import tachiyomi.domain.entries.manga.model.Manga
@ -32,6 +31,7 @@ import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.screens.EmptyScreen
import tachiyomi.presentation.core.screens.EmptyScreenAction
import tachiyomi.presentation.core.screens.LoadingScreen
import tachiyomi.source.local.entries.manga.LocalMangaSource
@Composable
fun BrowseSourceContent(

View file

@ -23,7 +23,6 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import eu.kanade.presentation.browse.manga.components.BaseMangaSourceItem
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.manga.LocalMangaSource
import eu.kanade.tachiyomi.ui.browse.manga.source.MangaSourcesState
import eu.kanade.tachiyomi.ui.browse.manga.source.browse.BrowseMangaSourceScreenModel.Listing
import eu.kanade.tachiyomi.util.system.LocaleHelper
@ -36,6 +35,7 @@ import tachiyomi.presentation.core.screens.EmptyScreen
import tachiyomi.presentation.core.screens.LoadingScreen
import tachiyomi.presentation.core.theme.header
import tachiyomi.presentation.core.util.plus
import tachiyomi.source.local.entries.manga.LocalMangaSource
@Composable
fun MangaSourcesScreen(

View file

@ -31,9 +31,9 @@ import eu.kanade.domain.source.manga.model.icon
import eu.kanade.presentation.util.rememberResourceBitmapPainter
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.extension.manga.model.MangaExtension
import eu.kanade.tachiyomi.source.manga.LocalMangaSource
import tachiyomi.core.util.lang.withIOContext
import tachiyomi.domain.source.manga.model.Source
import tachiyomi.source.local.entries.manga.LocalMangaSource
private val defaultModifier = Modifier
.height(40.dp)

View file

@ -3,8 +3,6 @@ package eu.kanade.presentation.browse.manga.components
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ViewList
import androidx.compose.material.icons.filled.ViewModule
import androidx.compose.material.icons.outlined.Help
import androidx.compose.material.icons.outlined.Public
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.runtime.Composable
@ -20,9 +18,10 @@ import eu.kanade.presentation.components.DropdownMenu
import eu.kanade.presentation.components.RadioMenuItem
import eu.kanade.presentation.components.SearchToolbar
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.MangaSource
import eu.kanade.tachiyomi.source.manga.LocalMangaSource
import tachiyomi.domain.library.model.LibraryDisplayMode
import tachiyomi.source.local.entries.manga.LocalMangaSource
@Composable
fun BrowseMangaSourceToolbar(
@ -34,12 +33,16 @@ fun BrowseMangaSourceToolbar(
navigateUp: () -> Unit,
onWebViewClick: () -> Unit,
onHelpClick: () -> Unit,
onSettingsClick: () -> Unit,
onSearch: (String) -> Unit,
scrollBehavior: TopAppBarScrollBehavior? = null,
) {
// Avoid capturing unstable source in actions lambda
val title = source?.name
val isLocalSource = source is LocalMangaSource
val isConfigurableSource = source is ConfigurableSource
var selectingDisplayMode by remember { mutableStateOf(false) }
SearchToolbar(
navigateUp = navigateUp,
@ -49,29 +52,31 @@ fun BrowseMangaSourceToolbar(
onSearch = onSearch,
onClickCloseSearch = navigateUp,
actions = {
var selectingDisplayMode by remember { mutableStateOf(false) }
AppBarActions(
actions = listOf(
actions = listOfNotNull(
AppBar.Action(
title = stringResource(R.string.action_display_mode),
icon = if (displayMode == LibraryDisplayMode.List) Icons.Filled.ViewList else Icons.Filled.ViewModule,
onClick = { selectingDisplayMode = true },
),
if (isLocalSource) {
AppBar.Action(
AppBar.OverflowAction(
title = stringResource(R.string.label_help),
icon = Icons.Outlined.Help,
onClick = onHelpClick,
)
} else {
AppBar.Action(
title = stringResource(R.string.action_web_view),
icon = Icons.Outlined.Public,
AppBar.OverflowAction(
title = stringResource(R.string.action_open_in_web_view),
onClick = onWebViewClick,
)
},
AppBar.OverflowAction(
title = stringResource(R.string.action_settings),
onClick = onSettingsClick,
).takeIf { isConfigurableSource },
),
)
DropdownMenu(
expanded = selectingDisplayMode,
onDismissRequest = { selectingDisplayMode = false },

View file

@ -21,7 +21,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import eu.kanade.core.prefs.asState
import eu.kanade.core.preference.asToggleableState
import eu.kanade.tachiyomi.R
import tachiyomi.core.preference.CheckboxState
import tachiyomi.domain.category.model.Category
@ -110,7 +110,7 @@ fun ChangeCategoryDialog(
when (checkbox) {
is CheckboxState.TriState -> {
TriStateCheckbox(
state = checkbox.asState(),
state = checkbox.asToggleableState(),
onClick = { onChange(checkbox) },
)
}

View file

@ -0,0 +1,18 @@
package eu.kanade.presentation.extensions
import android.Manifest
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import com.google.accompanist.permissions.rememberPermissionState
import eu.kanade.tachiyomi.util.storage.DiskUtil
/**
* Launches request for [Manifest.permission.WRITE_EXTERNAL_STORAGE] permission
*/
@Composable
fun DiskUtil.RequestStoragePermission() {
val permissionState = rememberPermissionState(permission = Manifest.permission.WRITE_EXTERNAL_STORAGE)
LaunchedEffect(Unit) {
permissionState.launchPermissionRequest()
}
}

View file

@ -67,10 +67,12 @@ fun DeleteLibraryEntryDialog(
list.forEach { state ->
val onCheck = {
val index = list.indexOf(state)
if (index != -1) {
val mutableList = list.toMutableList()
mutableList[index] = state.next() as CheckboxState.State<Int>
list = mutableList.toList()
}
}
Row(
modifier = Modifier

View file

@ -14,7 +14,7 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalLayoutDirection
import eu.kanade.core.prefs.PreferenceMutableState
import eu.kanade.core.preference.PreferenceMutableState
import eu.kanade.presentation.library.LibraryTabs
import eu.kanade.tachiyomi.ui.library.anime.AnimeLibraryItem
import kotlinx.coroutines.delay

View file

@ -10,7 +10,7 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalConfiguration
import eu.kanade.core.prefs.PreferenceMutableState
import eu.kanade.core.preference.PreferenceMutableState
import eu.kanade.presentation.library.manga.LibraryPagerEmptyScreen
import eu.kanade.tachiyomi.ui.library.anime.AnimeLibraryItem
import tachiyomi.domain.library.anime.LibraryAnime

View file

@ -14,7 +14,7 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalLayoutDirection
import eu.kanade.core.prefs.PreferenceMutableState
import eu.kanade.core.preference.PreferenceMutableState
import eu.kanade.presentation.library.LibraryTabs
import eu.kanade.tachiyomi.ui.library.manga.MangaLibraryItem
import kotlinx.coroutines.delay

View file

@ -16,7 +16,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.unit.dp
import eu.kanade.core.prefs.PreferenceMutableState
import eu.kanade.core.preference.PreferenceMutableState
import eu.kanade.presentation.animelib.components.GlobalSearchItem
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.library.manga.MangaLibraryItem

View file

@ -37,6 +37,7 @@ import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
import com.hippo.unifile.UniFile
import eu.kanade.domain.backup.service.BackupPreferences
import eu.kanade.presentation.extensions.RequestStoragePermission
import eu.kanade.presentation.more.settings.Preference
import eu.kanade.presentation.util.collectAsState
import eu.kanade.tachiyomi.R

View file

@ -62,6 +62,14 @@ import tachiyomi.data.handlers.manga.MangaDatabaseHandler
import tachiyomi.data.listOfStringsAdapter
import tachiyomi.data.updateStrategyAdapter
import tachiyomi.mi.data.AnimeDatabase
import tachiyomi.source.local.image.anime.AndroidLocalAnimeCoverManager
import tachiyomi.source.local.image.anime.LocalAnimeCoverManager
import tachiyomi.source.local.image.manga.AndroidLocalMangaCoverManager
import tachiyomi.source.local.image.manga.LocalMangaCoverManager
import tachiyomi.source.local.io.anime.AndroidLocalAnimeSourceFileSystem
import tachiyomi.source.local.io.anime.LocalAnimeSourceFileSystem
import tachiyomi.source.local.io.manga.AndroidLocalMangaSourceFileSystem
import tachiyomi.source.local.io.manga.LocalMangaSourceFileSystem
import uy.kohesive.injekt.api.InjektModule
import uy.kohesive.injekt.api.InjektRegistrar
import uy.kohesive.injekt.api.addSingleton
@ -198,6 +206,12 @@ class AppModule(val app: Application) : InjektModule {
addSingletonFactory { ImageSaver(app) }
addSingletonFactory<LocalMangaSourceFileSystem> { AndroidLocalMangaSourceFileSystem(app) }
addSingletonFactory<LocalMangaCoverManager> { AndroidLocalMangaCoverManager(app, get()) }
addSingletonFactory<LocalAnimeSourceFileSystem> { AndroidLocalAnimeSourceFileSystem(app) }
addSingletonFactory<LocalAnimeCoverManager> { AndroidLocalAnimeCoverManager(app, get()) }
addSingletonFactory { ExternalIntents() }
// Asynchronously init expensive components for a faster cold start

View file

@ -9,8 +9,8 @@ import coil.decode.ImageDecoderDecoder
import coil.decode.ImageSource
import coil.fetch.SourceResult
import coil.request.Options
import eu.kanade.tachiyomi.util.system.ImageUtil
import okio.BufferedSource
import tachiyomi.core.util.system.ImageUtil
import tachiyomi.decoder.ImageDecoder
/**

View file

@ -68,7 +68,7 @@ class AnimeDownloadManager(
*/
fun pauseDownloads() {
downloader.pause()
AnimeDownloadService.stop(context)
downloader.stop()
}
/**
@ -76,7 +76,7 @@ class AnimeDownloadManager(
*/
fun clearQueue() {
downloader.clearQueue()
AnimeDownloadService.stop(context)
downloader.stop()
}
/**
@ -117,8 +117,8 @@ class AnimeDownloadManager(
val wasRunning = downloader.isRunning
if (downloads.isEmpty()) {
AnimeDownloadService.stop(context)
queue.clear()
downloader.clearQueue()
downloader.stop()
return
}
@ -277,7 +277,6 @@ class AnimeDownloadManager(
if (wasRunning) {
if (queue.isEmpty()) {
AnimeDownloadService.stop(context)
downloader.stop()
} else if (queue.isNotEmpty()) {
downloader.start()

View file

@ -23,7 +23,6 @@ import eu.kanade.tachiyomi.animesource.online.fetchUrlFromVideo
import eu.kanade.tachiyomi.data.cache.EpisodeCache
import eu.kanade.tachiyomi.data.download.anime.model.AnimeDownload
import eu.kanade.tachiyomi.data.download.anime.model.AnimeDownloadQueue
import eu.kanade.tachiyomi.data.download.manga.MangaDownloader.Companion.WARNING_NOTIF_TIMEOUT_MS
import eu.kanade.tachiyomi.data.library.anime.AnimeLibraryUpdateNotifier
import eu.kanade.tachiyomi.data.notification.NotificationHandler
import eu.kanade.tachiyomi.source.UnmeteredSource
@ -31,7 +30,6 @@ import eu.kanade.tachiyomi.source.anime.AnimeSourceManager
import eu.kanade.tachiyomi.util.storage.DiskUtil
import eu.kanade.tachiyomi.util.storage.saveTo
import eu.kanade.tachiyomi.util.storage.toFFmpegString
import eu.kanade.tachiyomi.util.system.ImageUtil
import kotlinx.coroutines.async
import logcat.LogPriority
import okhttp3.HttpUrl.Companion.toHttpUrl
@ -42,6 +40,7 @@ import rx.schedulers.Schedulers
import tachiyomi.core.util.lang.launchIO
import tachiyomi.core.util.lang.launchNow
import tachiyomi.core.util.lang.withUIContext
import tachiyomi.core.util.system.ImageUtil
import tachiyomi.core.util.system.logcat
import tachiyomi.domain.entries.anime.model.Anime
import tachiyomi.domain.items.episode.model.Episode
@ -179,6 +178,11 @@ class AnimeDownloader(
}
isPaused = false
// Prevent recursion when DownloadService.onDestroy() calls downloader.stop()
if (AnimeDownloadService.isRunning.value) {
AnimeDownloadService.stop(context)
}
}
/**
@ -232,11 +236,11 @@ class AnimeDownloader(
completeAnimeDownload(completedDownload)
},
{ error ->
AnimeDownloadService.stop(context)
logcat(LogPriority.ERROR, error)
queue.state.value.forEach {
notifier.onError(it, error.message, it.episode.name, it.anime.title)
}
stop()
},
)
}
@ -749,7 +753,7 @@ class AnimeDownloader(
queue.remove(download)
}
if (areAllAnimeDownloadsFinished()) {
AnimeDownloadService.stop(context)
stop()
}
}

View file

@ -67,7 +67,7 @@ class MangaDownloadManager(
*/
fun pauseDownloads() {
downloader.pause()
MangaDownloadService.stop(context)
downloader.stop()
}
/**
@ -75,7 +75,7 @@ class MangaDownloadManager(
*/
fun clearQueue() {
downloader.clearQueue()
MangaDownloadService.stop(context)
downloader.stop()
}
/**
@ -115,8 +115,8 @@ class MangaDownloadManager(
val wasRunning = downloader.isRunning
if (downloads.isEmpty()) {
MangaDownloadService.stop(context)
queue.clear()
downloader.clearQueue()
downloader.stop()
return
}
@ -274,7 +274,6 @@ class MangaDownloadManager(
if (wasRunning) {
if (queue.isEmpty()) {
MangaDownloadService.stop(context)
downloader.stop()
} else if (queue.isNotEmpty()) {
downloader.start()

View file

@ -4,8 +4,6 @@ import android.content.Context
import com.hippo.unifile.UniFile
import com.jakewharton.rxrelay.PublishRelay
import eu.kanade.domain.download.service.DownloadPreferences
import eu.kanade.domain.entries.manga.model.COMIC_INFO_FILE
import eu.kanade.domain.entries.manga.model.ComicInfo
import eu.kanade.domain.entries.manga.model.getComicInfo
import eu.kanade.domain.items.chapter.model.toSChapter
import eu.kanade.tachiyomi.R
@ -21,7 +19,6 @@ import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.storage.DiskUtil
import eu.kanade.tachiyomi.util.storage.DiskUtil.NOMEDIA_FILE
import eu.kanade.tachiyomi.util.storage.saveTo
import eu.kanade.tachiyomi.util.system.ImageUtil
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
@ -40,11 +37,14 @@ import rx.Observable
import rx.Subscription
import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers
import tachiyomi.core.metadata.comicinfo.COMIC_INFO_FILE
import tachiyomi.core.metadata.comicinfo.ComicInfo
import tachiyomi.core.util.lang.awaitSingle
import tachiyomi.core.util.lang.launchIO
import tachiyomi.core.util.lang.launchNow
import tachiyomi.core.util.lang.withIOContext
import tachiyomi.core.util.lang.withUIContext
import tachiyomi.core.util.system.ImageUtil
import tachiyomi.core.util.system.logcat
import tachiyomi.domain.entries.manga.model.Manga
import tachiyomi.domain.items.chapter.model.Chapter
@ -167,6 +167,11 @@ class MangaDownloader(
}
isPaused = false
// Prevent recursion when DownloadService.onDestroy() calls downloader.stop()
if (MangaDownloadService.isRunning.value) {
MangaDownloadService.stop(context)
}
}
/**
@ -217,9 +222,9 @@ class MangaDownloader(
completeDownload(it)
},
{ error ->
MangaDownloadService.stop(context)
logcat(LogPriority.ERROR, error)
notifier.onError(error.message)
stop()
},
)
}
@ -634,7 +639,7 @@ class MangaDownloader(
queue.remove(download)
}
if (areAllDownloadsFinished()) {
MangaDownloadService.stop(context)
stop()
}
}

View file

@ -15,9 +15,9 @@ import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.storage.DiskUtil
import eu.kanade.tachiyomi.util.storage.cacheImageDir
import eu.kanade.tachiyomi.util.storage.getUriCompat
import eu.kanade.tachiyomi.util.system.ImageUtil
import logcat.LogPriority
import okio.IOException
import tachiyomi.core.util.system.ImageUtil
import tachiyomi.core.util.system.logcat
import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream

View file

@ -21,6 +21,9 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import tachiyomi.domain.source.anime.model.AnimeSourceData
import tachiyomi.domain.source.anime.repository.AnimeSourceDataRepository
import tachiyomi.source.local.entries.anime.LocalAnimeSource
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.util.concurrent.ConcurrentHashMap
@ -44,7 +47,15 @@ class AnimeSourceManager(
scope.launch {
extensionManager.installedExtensionsFlow
.collectLatest { extensions ->
val mutableMap = ConcurrentHashMap<Long, AnimeSource>(mapOf(LocalAnimeSource.ID to LocalAnimeSource(context)))
val mutableMap = ConcurrentHashMap<Long, AnimeSource>(
mapOf(
LocalAnimeSource.ID to LocalAnimeSource(
context,
Injekt.get(),
Injekt.get(),
),
),
)
extensions.forEach { extension ->
extension.sources.forEach {
mutableMap[it.id] = it

View file

@ -15,7 +15,6 @@ import eu.kanade.tachiyomi.source.UnmeteredSource
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
import eu.kanade.tachiyomi.util.storage.DiskUtil
import eu.kanade.tachiyomi.util.storage.toFFmpegString
import eu.kanade.tachiyomi.util.system.ImageUtil
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
@ -23,6 +22,7 @@ import kotlinx.serialization.json.decodeFromStream
import logcat.LogPriority
import rx.Observable
import tachiyomi.core.util.lang.withIOContext
import tachiyomi.core.util.system.ImageUtil
import tachiyomi.core.util.system.logcat
import tachiyomi.domain.items.episode.service.EpisodeRecognition
import uy.kohesive.injekt.injectLazy

View file

@ -1,462 +1,3 @@
package eu.kanade.tachiyomi.source.manga
import android.content.Context
import com.github.junrar.Archive
import com.hippo.unifile.UniFile
import eu.kanade.domain.entries.manga.model.COMIC_INFO_FILE
import eu.kanade.domain.entries.manga.model.ComicInfo
import eu.kanade.domain.entries.manga.model.copyFromComicInfo
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.UnmeteredSource
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
import eu.kanade.tachiyomi.util.storage.DiskUtil
import eu.kanade.tachiyomi.util.storage.EpubFile
import eu.kanade.tachiyomi.util.system.ImageUtil
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream
import logcat.LogPriority
import nl.adaptivity.xmlutil.AndroidXmlReader
import nl.adaptivity.xmlutil.serialization.XML
import rx.Observable
import tachiyomi.core.util.lang.withIOContext
import tachiyomi.core.util.system.logcat
import tachiyomi.domain.items.chapter.service.ChapterRecognition
import uy.kohesive.injekt.injectLazy
import java.io.File
import java.io.FileInputStream
import java.io.InputStream
import java.nio.charset.StandardCharsets
import java.util.concurrent.TimeUnit
import java.util.zip.ZipFile
class LocalMangaSource(
private val context: Context,
) : CatalogueSource, UnmeteredSource {
private val json: Json by injectLazy()
private val xml: XML by injectLazy()
override val name: String = context.getString(R.string.local_manga_source)
override val id: Long = ID
override val lang: String = "other"
override fun toString() = name
override val supportsLatest: Boolean = true
// Browse related
override fun fetchPopularManga(page: Int) = fetchSearchManga(page, "", POPULAR_FILTERS)
override fun fetchLatestUpdates(page: Int) = fetchSearchManga(page, "", LATEST_FILTERS)
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
val baseDirsFiles = getBaseDirectoriesFiles(context)
var mangaDirs = baseDirsFiles
// Filter out files that are hidden and is not a folder
.filter { it.isDirectory && !it.name.startsWith('.') }
.distinctBy { it.name }
val lastModifiedLimit = if (filters === LATEST_FILTERS) System.currentTimeMillis() - LATEST_THRESHOLD else 0L
// Filter by query or last modified
mangaDirs = mangaDirs.filter {
if (lastModifiedLimit == 0L) {
it.name.contains(query, ignoreCase = true)
} else {
it.lastModified() >= lastModifiedLimit
}
}
filters.forEach { filter ->
when (filter) {
is OrderBy -> {
when (filter.state!!.index) {
0 -> {
mangaDirs = if (filter.state!!.ascending) {
mangaDirs.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name })
} else {
mangaDirs.sortedWith(compareByDescending(String.CASE_INSENSITIVE_ORDER) { it.name })
}
}
1 -> {
mangaDirs = if (filter.state!!.ascending) {
mangaDirs.sortedBy(File::lastModified)
} else {
mangaDirs.sortedByDescending(File::lastModified)
}
}
}
}
else -> {
/* Do nothing */
}
}
}
// Transform mangaDirs to list of SManga
val mangas = mangaDirs.map { mangaDir ->
SManga.create().apply {
title = mangaDir.name
url = mangaDir.name
// Try to find the cover
val cover = getCoverFile(mangaDir.name, baseDirsFiles)
if (cover != null && cover.exists()) {
thumbnail_url = cover.absolutePath
}
}
}
// Fetch chapters of all the manga
mangas.forEach { manga ->
runBlocking {
val chapters = getChapterList(manga)
if (chapters.isNotEmpty()) {
val chapter = chapters.last()
val format = getFormat(chapter)
if (format is Format.Epub) {
EpubFile(format.file).use { epub ->
epub.fillMangaMetadata(manga)
}
}
// Copy the cover from the first chapter found if not available
if (manga.thumbnail_url == null) {
updateCover(chapter, manga)
}
}
}
}
return Observable.just(MangasPage(mangas.toList(), false))
}
// Manga details related
override suspend fun getMangaDetails(manga: SManga): SManga = withIOContext {
val baseDirsFile = getBaseDirectoriesFiles(context)
getCoverFile(manga.url, baseDirsFile)?.let {
manga.thumbnail_url = it.absolutePath
}
// Augment manga details based on metadata files
try {
val mangaDirFiles = getMangaDirsFiles(manga.url, baseDirsFile).toList()
val comicInfoFile = mangaDirFiles
.firstOrNull { it.name == COMIC_INFO_FILE }
val noXmlFile = mangaDirFiles
.firstOrNull { it.name == ".noxml" }
val legacyJsonDetailsFile = mangaDirFiles
.firstOrNull { it.extension == "json" }
when {
// Top level ComicInfo.xml
comicInfoFile != null -> {
noXmlFile?.delete()
setMangaDetailsFromComicInfoFile(comicInfoFile.inputStream(), manga)
}
// TODO: automatically convert these to ComicInfo.xml
legacyJsonDetailsFile != null -> {
json.decodeFromStream<MangaDetails>(legacyJsonDetailsFile.inputStream()).run {
title?.let { manga.title = it }
author?.let { manga.author = it }
artist?.let { manga.artist = it }
description?.let { manga.description = it }
genre?.let { manga.genre = it.joinToString() }
status?.let { manga.status = it }
}
}
// Copy ComicInfo.xml from chapter archive to top level if found
noXmlFile == null -> {
val chapterArchives = mangaDirFiles
.filter { isSupportedArchiveFile(it.extension) }
.toList()
val mangaDir = getMangaDir(manga.url, baseDirsFile)
val folderPath = mangaDir?.absolutePath
val copiedFile = copyComicInfoFileFromArchive(chapterArchives, folderPath)
if (copiedFile != null) {
setMangaDetailsFromComicInfoFile(copiedFile.inputStream(), manga)
} else {
// Avoid re-scanning
File("$folderPath/.noxml").createNewFile()
}
}
}
} catch (e: Throwable) {
logcat(LogPriority.ERROR, e) { "Error setting manga details from local metadata for ${manga.title}" }
}
return@withIOContext manga
}
private fun copyComicInfoFileFromArchive(chapterArchives: List<File>, folderPath: String?): File? {
for (chapter in chapterArchives) {
when (getFormat(chapter)) {
is Format.Zip -> {
ZipFile(chapter).use { zip: ZipFile ->
zip.getEntry(COMIC_INFO_FILE)?.let { comicInfoFile ->
zip.getInputStream(comicInfoFile).buffered().use { stream ->
return copyComicInfoFile(stream, folderPath)
}
}
}
}
is Format.Rar -> {
Archive(chapter).use { rar: Archive ->
rar.fileHeaders.firstOrNull { it.fileName == COMIC_INFO_FILE }?.let { comicInfoFile ->
rar.getInputStream(comicInfoFile).buffered().use { stream ->
return copyComicInfoFile(stream, folderPath)
}
}
}
}
else -> {}
}
}
return null
}
private fun copyComicInfoFile(comicInfoFileStream: InputStream, folderPath: String?): File {
return File("$folderPath/$COMIC_INFO_FILE").apply {
outputStream().use { outputStream ->
comicInfoFileStream.use { it.copyTo(outputStream) }
}
}
}
private fun setMangaDetailsFromComicInfoFile(stream: InputStream, manga: SManga) {
val comicInfo = AndroidXmlReader(stream, StandardCharsets.UTF_8.name()).use {
xml.decodeFromReader<ComicInfo>(it)
}
manga.copyFromComicInfo(comicInfo)
}
@Serializable
class MangaDetails(
val title: String? = null,
val author: String? = null,
val artist: String? = null,
val description: String? = null,
val genre: List<String>? = null,
val status: Int? = null,
)
// Chapters
override suspend fun getChapterList(manga: SManga): List<SChapter> {
val baseDirsFile = getBaseDirectoriesFiles(context)
return getMangaDirsFiles(manga.url, baseDirsFile)
// Only keep supported formats
.filter { it.isDirectory || isSupportedArchiveFile(it.extension) }
.map { chapterFile ->
SChapter.create().apply {
url = "${manga.url}/${chapterFile.name}"
name = if (chapterFile.isDirectory) {
chapterFile.name
} else {
chapterFile.nameWithoutExtension
}
date_upload = chapterFile.lastModified()
chapter_number = ChapterRecognition.parseChapterNumber(manga.title, this.name, this.chapter_number)
val format = getFormat(chapterFile)
if (format is Format.Epub) {
EpubFile(format.file).use { epub ->
epub.fillChapterMetadata(this)
}
}
}
}
.sortedWith { c1, c2 ->
val c = c2.chapter_number.compareTo(c1.chapter_number)
if (c == 0) c2.name.compareToCaseInsensitiveNaturalOrder(c1.name) else c
}
.toList()
}
// Filters
override fun getFilterList() = FilterList(OrderBy(context))
private val POPULAR_FILTERS = FilterList(OrderBy(context))
private val LATEST_FILTERS = FilterList(OrderBy(context).apply { state = Filter.Sort.Selection(1, false) })
private class OrderBy(context: Context) : Filter.Sort(
context.getString(R.string.local_filter_order_by),
arrayOf(context.getString(R.string.title), context.getString(R.string.date)),
Selection(0, true),
)
// Unused stuff
override suspend fun getPageList(chapter: SChapter) = throw UnsupportedOperationException("Unused")
// Miscellaneous
private fun isSupportedArchiveFile(extension: String): Boolean {
return extension.lowercase() in SUPPORTED_ARCHIVE_TYPES
}
fun getFormat(chapter: SChapter): Format {
val baseDirs = getBaseDirectories(context)
for (dir in baseDirs) {
val chapFile = File(dir, chapter.url)
if (!chapFile.exists()) continue
return getFormat(chapFile)
}
throw Exception(context.getString(R.string.chapter_not_found))
}
private fun getFormat(file: File) = with(file) {
when {
isDirectory -> Format.Directory(this)
extension.equals("zip", true) || extension.equals("cbz", true) -> Format.Zip(this)
extension.equals("rar", true) || extension.equals("cbr", true) -> Format.Rar(this)
extension.equals("epub", true) -> Format.Epub(this)
else -> throw Exception(context.getString(R.string.local_invalid_format))
}
}
private fun updateCover(chapter: SChapter, manga: SManga): File? {
return try {
when (val format = getFormat(chapter)) {
is Format.Directory -> {
val entry = format.file.listFiles()
?.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
?.find { !it.isDirectory && ImageUtil.isImage(it.name) { FileInputStream(it) } }
entry?.let { updateCover(context, manga, it.inputStream()) }
}
is Format.Zip -> {
ZipFile(format.file).use { zip ->
val entry = zip.entries().toList()
.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
.find { !it.isDirectory && ImageUtil.isImage(it.name) { zip.getInputStream(it) } }
entry?.let { updateCover(context, manga, zip.getInputStream(it)) }
}
}
is Format.Rar -> {
Archive(format.file).use { archive ->
val entry = archive.fileHeaders
.sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) }
.find { !it.isDirectory && ImageUtil.isImage(it.fileName) { archive.getInputStream(it) } }
entry?.let { updateCover(context, manga, archive.getInputStream(it)) }
}
}
is Format.Epub -> {
EpubFile(format.file).use { epub ->
val entry = epub.getImagesFromPages()
.firstOrNull()
?.let { epub.getEntry(it) }
entry?.let { updateCover(context, manga, epub.getInputStream(it)) }
}
}
}
} catch (e: Throwable) {
logcat(LogPriority.ERROR, e) { "Error updating cover for ${manga.title}" }
null
}
}
sealed class Format {
data class Directory(val file: File) : Format()
data class Zip(val file: File) : Format()
data class Rar(val file: File) : Format()
data class Epub(val file: File) : Format()
}
companion object {
const val ID = 0L
const val HELP_URL = "https://aniyomi.org/help/guides/local-manga/"
private const val DEFAULT_COVER_NAME = "cover.jpg"
private val LATEST_THRESHOLD = TimeUnit.MILLISECONDS.convert(7, TimeUnit.DAYS)
private fun getBaseDirectories(context: Context): Sequence<File> {
val localFolder = context.getString(R.string.app_name) + File.separator + "local"
return DiskUtil.getExternalStorages(context)
.map { File(it.absolutePath, localFolder) }
.asSequence()
}
private fun getBaseDirectoriesFiles(context: Context): Sequence<File> {
return getBaseDirectories(context)
// Get all the files inside all baseDir
.flatMap { it.listFiles().orEmpty().toList() }
}
private fun getMangaDir(mangaUrl: String, baseDirsFile: Sequence<File>): File? {
return baseDirsFile
// Get the first mangaDir or null
.firstOrNull { it.isDirectory && it.name == mangaUrl }
}
private fun getMangaDirsFiles(mangaUrl: String, baseDirsFile: Sequence<File>): Sequence<File> {
return baseDirsFile
// Filter out ones that are not related to the manga and is not a directory
.filter { it.isDirectory && it.name == mangaUrl }
// Get all the files inside the filtered folders
.flatMap { it.listFiles().orEmpty().toList() }
}
private fun getCoverFile(mangaUrl: String, baseDirsFile: Sequence<File>): File? {
return getMangaDirsFiles(mangaUrl, baseDirsFile)
// Get all file whose names start with 'cover'
.filter { it.isFile && it.nameWithoutExtension.equals("cover", ignoreCase = true) }
// Get the first actual image
.firstOrNull {
ImageUtil.isImage(it.name) { it.inputStream() }
}
}
fun updateCover(context: Context, manga: SManga, inputStream: InputStream): File? {
val baseDirsFiles = getBaseDirectoriesFiles(context)
val mangaDir = getMangaDir(manga.url, baseDirsFiles)
if (mangaDir == null) {
inputStream.close()
return null
}
var coverFile = getCoverFile(manga.url, baseDirsFiles)
if (coverFile == null) {
coverFile = File(mangaDir.absolutePath, DEFAULT_COVER_NAME)
coverFile.createNewFile()
}
// It might not exist at this point
coverFile.parentFile?.mkdirs()
inputStream.use { input ->
coverFile.outputStream().use { output ->
input.copyTo(output)
}
}
DiskUtil.createNoMediaFile(UniFile.fromFile(mangaDir), context)
manga.thumbnail_url = coverFile.absolutePath
return coverFile
}
}
}
private val SUPPORTED_ARCHIVE_TYPES = listOf("zip", "cbz", "rar", "cbr", "epub")

View file

@ -5,6 +5,7 @@ import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.tachiyomi.extension.manga.MangaExtensionManager
import eu.kanade.tachiyomi.source.MangaSource
import tachiyomi.domain.source.manga.model.MangaSourceData
import tachiyomi.source.local.entries.manga.LocalMangaSource
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get

View file

@ -22,6 +22,9 @@ import kotlinx.coroutines.runBlocking
import rx.Observable
import tachiyomi.domain.source.manga.model.MangaSourceData
import tachiyomi.domain.source.manga.repository.MangaSourceDataRepository
import tachiyomi.source.local.entries.manga.LocalMangaSource
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.util.concurrent.ConcurrentHashMap
@ -45,7 +48,15 @@ class MangaSourceManager(
scope.launch {
extensionManager.installedExtensionsFlow
.collectLatest { extensions ->
val mutableMap = ConcurrentHashMap<Long, MangaSource>(mapOf(LocalMangaSource.ID to LocalMangaSource(context)))
val mutableMap = ConcurrentHashMap<Long, MangaSource>(
mapOf(
LocalMangaSource.ID to LocalMangaSource(
context,
Injekt.get(),
Injekt.get(),
),
),
)
extensions.forEach { extension ->
extension.sources.forEach {
mutableMap[it.id] = it

View file

@ -14,6 +14,7 @@ import cafe.adriel.voyager.navigator.Navigator
import cafe.adriel.voyager.navigator.tab.LocalTabNavigator
import cafe.adriel.voyager.navigator.tab.TabOptions
import eu.kanade.presentation.components.TabbedScreen
import eu.kanade.presentation.extensions.RequestStoragePermission
import eu.kanade.presentation.util.Tab
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.browse.anime.extension.AnimeExtensionsScreenModel

View file

@ -58,7 +58,7 @@ class SourcePreferencesScreen(val sourceId: Long) : Screen() {
Scaffold(
topBar = {
TopAppBar(
title = { Text(text = Injekt.get<AnimeSourceManager>().get(sourceId)!!.toString()) },
title = { Text(text = Injekt.get<AnimeSourceManager>().getOrStub(sourceId).toString()) },
navigationIcon = {
IconButton(onClick = navigator::pop) {
Icon(

View file

@ -49,6 +49,7 @@ import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
import eu.kanade.tachiyomi.core.Constants
import eu.kanade.tachiyomi.source.anime.AnimeSourceManager
import eu.kanade.tachiyomi.source.anime.LocalAnimeSource
import eu.kanade.tachiyomi.ui.browse.anime.extension.details.SourcePreferencesScreen
import eu.kanade.tachiyomi.ui.browse.anime.source.browse.BrowseAnimeSourceScreenModel.Listing
import eu.kanade.tachiyomi.ui.category.CategoriesTab
import eu.kanade.tachiyomi.ui.entries.anime.AnimeScreen
@ -124,6 +125,7 @@ data class BrowseAnimeSourceScreen(
navigateUp = navigateUp,
onWebViewClick = onWebViewClick,
onHelpClick = onHelpClick,
onSettingsClick = { navigator.push(SourcePreferencesScreen(sourceId)) },
onSearch = { screenModel.search(it) },
)

View file

@ -13,7 +13,7 @@ import androidx.paging.filter
import androidx.paging.map
import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.coroutineScope
import eu.kanade.core.prefs.asState
import eu.kanade.core.preference.asState
import eu.kanade.domain.entries.anime.interactor.UpdateAnime
import eu.kanade.domain.entries.anime.model.copyFrom
import eu.kanade.domain.entries.anime.model.toDomainAnime

View file

@ -63,7 +63,10 @@ fun SourceFilterAnimeDialog(
Spacer(modifier = Modifier.weight(1f))
Button(onClick = onFilter) {
Button(onClick = {
onFilter()
onDismissRequest()
},) {
Text(stringResource(R.string.action_filter))
}
}

View file

@ -58,7 +58,7 @@ class MangaSourcePreferencesScreen(val sourceId: Long) : Screen() {
Scaffold(
topBar = {
TopAppBar(
title = { Text(text = Injekt.get<MangaSourceManager>().get(sourceId)!!.toString()) },
title = { Text(text = Injekt.get<MangaSourceManager>().getOrStub(sourceId).toString()) },
navigationIcon = {
IconButton(onClick = navigator::pop) {
Icon(

View file

@ -24,7 +24,6 @@ import eu.kanade.presentation.components.SearchToolbar
import eu.kanade.presentation.util.Screen
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.core.Constants
import eu.kanade.tachiyomi.source.manga.LocalMangaSource
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.ui.browse.manga.source.browse.BrowseMangaSourceScreenModel
import eu.kanade.tachiyomi.ui.entries.manga.MangaScreen
@ -34,6 +33,7 @@ import kotlinx.coroutines.launch
import tachiyomi.domain.entries.manga.model.Manga
import tachiyomi.presentation.core.components.material.ExtendedFloatingActionButton
import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.source.local.entries.manga.LocalMangaSource
data class MangaSourceSearchScreen(
private val oldManga: Manga,

View file

@ -46,9 +46,9 @@ import eu.kanade.presentation.util.Screen
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.core.Constants
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.manga.LocalMangaSource
import eu.kanade.tachiyomi.source.manga.MangaSourceManager
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.ui.browse.manga.extension.details.MangaSourcePreferencesScreen
import eu.kanade.tachiyomi.ui.browse.manga.source.browse.BrowseMangaSourceScreenModel.Listing
import eu.kanade.tachiyomi.ui.category.CategoriesTab
import eu.kanade.tachiyomi.ui.entries.manga.MangaScreen
@ -60,6 +60,7 @@ import tachiyomi.core.util.lang.launchIO
import tachiyomi.presentation.core.components.material.Divider
import tachiyomi.presentation.core.components.material.Scaffold
import tachiyomi.presentation.core.components.material.padding
import tachiyomi.source.local.entries.manga.LocalMangaSource
data class BrowseMangaSourceScreen(
private val sourceId: Long,
@ -124,6 +125,7 @@ data class BrowseMangaSourceScreen(
navigateUp = navigateUp,
onWebViewClick = onWebViewClick,
onHelpClick = onHelpClick,
onSettingsClick = { navigator.push(MangaSourcePreferencesScreen(sourceId)) },
onSearch = { screenModel.search(it) },
)

View file

@ -13,7 +13,7 @@ import androidx.paging.filter
import androidx.paging.map
import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.coroutineScope
import eu.kanade.core.prefs.asState
import eu.kanade.core.preference.asState
import eu.kanade.domain.entries.manga.interactor.UpdateManga
import eu.kanade.domain.entries.manga.model.copyFrom
import eu.kanade.domain.entries.manga.model.toDomainManga

View file

@ -63,7 +63,10 @@ fun SourceFilterMangaDialog(
Spacer(modifier = Modifier.weight(1f))
Button(onClick = onFilter) {
Button(onClick = {
onFilter()
onDismissRequest()
},) {
Text(stringResource(R.string.action_filter))
}
}

View file

@ -10,6 +10,7 @@ import androidx.compose.ui.res.stringResource
import cafe.adriel.voyager.navigator.tab.LocalTabNavigator
import cafe.adriel.voyager.navigator.tab.TabOptions
import eu.kanade.presentation.components.TabbedScreen
import eu.kanade.presentation.extensions.RequestStoragePermission
import eu.kanade.presentation.util.Tab
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.download.anime.animeDownloadTab

View file

@ -121,7 +121,7 @@ class AnimeCoverScreenModel(
@Suppress("BlockingMethodInNonBlockingContext")
context.contentResolver.openInputStream(data)?.use {
try {
anime.editCover(context, it, updateAnime, coverCache)
anime.editCover(Injekt.get(), it, updateAnime, coverCache)
notifyCoverUpdated(context)
} catch (e: Exception) {
notifyFailedCoverUpdate(context, e)

View file

@ -8,7 +8,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.coroutineScope
import eu.kanade.core.prefs.asState
import eu.kanade.core.preference.asState
import eu.kanade.core.util.addOrRemove
import eu.kanade.domain.download.service.DownloadPreferences
import eu.kanade.domain.entries.anime.interactor.UpdateAnime

View file

@ -121,7 +121,7 @@ class MangaCoverScreenModel(
@Suppress("BlockingMethodInNonBlockingContext")
context.contentResolver.openInputStream(data)?.use {
try {
manga.editCover(context, it, updateManga, coverCache)
manga.editCover(Injekt.get(), it, updateManga, coverCache)
notifyCoverUpdated(context)
} catch (e: Exception) {
notifyFailedCoverUpdate(context, e)

View file

@ -8,7 +8,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.coroutineScope
import eu.kanade.core.prefs.asState
import eu.kanade.core.preference.asState
import eu.kanade.core.util.addOrRemove
import eu.kanade.domain.download.service.DownloadPreferences
import eu.kanade.domain.entries.manga.interactor.UpdateManga

View file

@ -14,6 +14,7 @@ import cafe.adriel.voyager.navigator.Navigator
import cafe.adriel.voyager.navigator.tab.LocalTabNavigator
import cafe.adriel.voyager.navigator.tab.TabOptions
import eu.kanade.presentation.components.TabbedScreen
import eu.kanade.presentation.extensions.RequestStoragePermission
import eu.kanade.presentation.util.Tab
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.history.anime.AnimeHistoryScreenModel

View file

@ -7,8 +7,8 @@ import androidx.compose.ui.util.fastAny
import androidx.compose.ui.util.fastMap
import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.coroutineScope
import eu.kanade.core.prefs.PreferenceMutableState
import eu.kanade.core.prefs.asState
import eu.kanade.core.preference.PreferenceMutableState
import eu.kanade.core.preference.asState
import eu.kanade.core.util.fastDistinctBy
import eu.kanade.core.util.fastFilter
import eu.kanade.core.util.fastFilterNot

View file

@ -7,8 +7,8 @@ import androidx.compose.ui.util.fastAny
import androidx.compose.ui.util.fastMap
import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.coroutineScope
import eu.kanade.core.prefs.PreferenceMutableState
import eu.kanade.core.prefs.asState
import eu.kanade.core.preference.PreferenceMutableState
import eu.kanade.core.preference.asState
import eu.kanade.core.util.fastDistinctBy
import eu.kanade.core.util.fastFilter
import eu.kanade.core.util.fastFilterNot

View file

@ -17,7 +17,7 @@ import cafe.adriel.voyager.navigator.Navigator
import cafe.adriel.voyager.navigator.currentOrThrow
import cafe.adriel.voyager.navigator.tab.LocalTabNavigator
import cafe.adriel.voyager.navigator.tab.TabOptions
import eu.kanade.core.prefs.asState
import eu.kanade.core.preference.asState
import eu.kanade.domain.base.BasePreferences
import eu.kanade.domain.library.service.LibraryPreferences
import eu.kanade.presentation.more.MoreScreen

View file

@ -509,7 +509,7 @@ class PlayerViewModel(
viewModelScope.launchNonCancellable {
val result = try {
anime.editCover(context, imageStream)
anime.editCover(Injekt.get(), imageStream)
if (anime.isLocal() || anime.favorite) {
SetAsCoverResult.Success
} else {

View file

@ -774,7 +774,7 @@ class ReaderViewModel(
viewModelScope.launchNonCancellable {
val result = try {
manga.editCover(context, stream())
manga.editCover(Injekt.get(), stream())
if (manga.isLocal() || manga.favorite) {
SetAsCoverResult.Success
} else {

View file

@ -6,7 +6,6 @@ import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.download.manga.MangaDownloadManager
import eu.kanade.tachiyomi.data.download.manga.MangaDownloadProvider
import eu.kanade.tachiyomi.source.MangaSource
import eu.kanade.tachiyomi.source.manga.LocalMangaSource
import eu.kanade.tachiyomi.source.manga.MangaSourceManager
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
@ -14,6 +13,8 @@ import eu.kanade.tachiyomi.ui.reader.setting.ReaderPreferences
import tachiyomi.core.util.lang.withIOContext
import tachiyomi.core.util.system.logcat
import tachiyomi.domain.entries.manga.model.Manga
import tachiyomi.source.local.entries.manga.LocalMangaSource
import tachiyomi.source.local.io.Format
import uy.kohesive.injekt.injectLazy
/**
@ -83,14 +84,14 @@ class ChapterLoader(
source is HttpSource -> HttpPageLoader(chapter, source)
source is LocalMangaSource -> source.getFormat(chapter.chapter).let { format ->
when (format) {
is LocalMangaSource.Format.Directory -> DirectoryPageLoader(format.file)
is LocalMangaSource.Format.Zip -> ZipPageLoader(format.file)
is LocalMangaSource.Format.Rar -> try {
is Format.Directory -> DirectoryPageLoader(format.file)
is Format.Zip -> ZipPageLoader(format.file)
is Format.Rar -> try {
RarPageLoader(format.file)
} catch (e: UnsupportedRarV5Exception) {
error(context.getString(R.string.loader_rar5_error))
}
is LocalMangaSource.Format.Epub -> EpubPageLoader(format.file)
is Format.Epub -> EpubPageLoader(format.file)
}
}
source is MangaSourceManager.StubMangaSource -> throw source.getSourceNotInstalledException()

View file

@ -3,7 +3,7 @@ package eu.kanade.tachiyomi.ui.reader.loader
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
import eu.kanade.tachiyomi.util.system.ImageUtil
import tachiyomi.core.util.system.ImageUtil
import java.io.File
import java.io.FileInputStream

View file

@ -5,7 +5,7 @@ import com.github.junrar.rarfile.FileHeader
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
import eu.kanade.tachiyomi.util.system.ImageUtil
import tachiyomi.core.util.system.ImageUtil
import java.io.File
import java.io.InputStream
import java.io.PipedInputStream

View file

@ -4,7 +4,7 @@ import android.os.Build
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
import eu.kanade.tachiyomi.util.system.ImageUtil
import tachiyomi.core.util.system.ImageUtil
import java.io.File
import java.nio.charset.StandardCharsets
import java.util.zip.ZipFile

View file

@ -13,7 +13,6 @@ import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderPageImageView
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderProgressIndicator
import eu.kanade.tachiyomi.ui.webview.WebViewActivity
import eu.kanade.tachiyomi.util.system.ImageUtil
import eu.kanade.tachiyomi.widget.ViewPagerAdapter
import kotlinx.coroutines.Job
import kotlinx.coroutines.MainScope
@ -23,6 +22,7 @@ import kotlinx.coroutines.supervisorScope
import tachiyomi.core.util.lang.launchIO
import tachiyomi.core.util.lang.withIOContext
import tachiyomi.core.util.lang.withUIContext
import tachiyomi.core.util.system.ImageUtil
import java.io.BufferedInputStream
import java.io.ByteArrayInputStream
import java.io.InputStream

View file

@ -18,7 +18,6 @@ import eu.kanade.tachiyomi.ui.reader.model.StencilPage
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderPageImageView
import eu.kanade.tachiyomi.ui.reader.viewer.ReaderProgressIndicator
import eu.kanade.tachiyomi.ui.webview.WebViewActivity
import eu.kanade.tachiyomi.util.system.ImageUtil
import eu.kanade.tachiyomi.util.system.dpToPx
import kotlinx.coroutines.Job
import kotlinx.coroutines.MainScope
@ -29,6 +28,7 @@ import kotlinx.coroutines.suspendCancellableCoroutine
import tachiyomi.core.util.lang.launchIO
import tachiyomi.core.util.lang.withIOContext
import tachiyomi.core.util.lang.withUIContext
import tachiyomi.core.util.system.ImageUtil
import java.io.BufferedInputStream
import java.io.InputStream

View file

@ -8,7 +8,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.coroutineScope
import eu.kanade.core.prefs.asState
import eu.kanade.core.preference.asState
import eu.kanade.core.util.addOrRemove
import eu.kanade.core.util.insertSeparators
import eu.kanade.domain.download.service.DownloadPreferences

View file

@ -8,7 +8,7 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.coroutineScope
import eu.kanade.core.prefs.asState
import eu.kanade.core.preference.asState
import eu.kanade.core.util.addOrRemove
import eu.kanade.core.util.insertSeparators
import eu.kanade.domain.items.chapter.interactor.SetReadStatus

View file

@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.util
import android.content.Context
import eu.kanade.domain.download.service.DownloadPreferences
import eu.kanade.domain.entries.anime.interactor.UpdateAnime
import eu.kanade.domain.entries.anime.model.hasCustomCover
@ -8,8 +7,8 @@ import eu.kanade.domain.entries.anime.model.isLocal
import eu.kanade.domain.entries.anime.model.toSAnime
import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.data.cache.AnimeCoverCache
import eu.kanade.tachiyomi.source.anime.LocalAnimeSource
import tachiyomi.domain.entries.anime.model.Anime
import tachiyomi.source.local.image.anime.LocalAnimeCoverManager
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.InputStream
@ -77,13 +76,13 @@ fun Anime.shouldDownloadNewEpisodes(dbCategories: List<Long>, preferences: Downl
}
suspend fun Anime.editCover(
context: Context,
coverManager: LocalAnimeCoverManager,
stream: InputStream,
updateAnime: UpdateAnime = Injekt.get(),
coverCache: AnimeCoverCache = Injekt.get(),
) {
if (isLocal()) {
LocalAnimeSource.updateCover(context, toSAnime(), stream)
coverManager.update(toSAnime(), stream)
updateAnime.awaitUpdateCoverLastModified(id)
} else if (favorite) {
coverCache.setCustomCoverToCache(this, stream)

View file

@ -1,15 +1,14 @@
package eu.kanade.tachiyomi.util
import android.content.Context
import eu.kanade.domain.download.service.DownloadPreferences
import eu.kanade.domain.entries.manga.interactor.UpdateManga
import eu.kanade.domain.entries.manga.model.hasCustomCover
import eu.kanade.domain.entries.manga.model.isLocal
import eu.kanade.domain.entries.manga.model.toSManga
import eu.kanade.tachiyomi.data.cache.MangaCoverCache
import eu.kanade.tachiyomi.source.manga.LocalMangaSource
import eu.kanade.tachiyomi.source.model.SManga
import tachiyomi.domain.entries.manga.model.Manga
import tachiyomi.source.local.image.manga.LocalMangaCoverManager
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.InputStream
@ -77,13 +76,13 @@ fun Manga.shouldDownloadNewChapters(dbCategories: List<Long>, preferences: Downl
}
suspend fun Manga.editCover(
context: Context,
coverManager: LocalMangaCoverManager,
stream: InputStream,
updateManga: UpdateManga = Injekt.get(),
coverCache: MangaCoverCache = Injekt.get(),
) {
if (isLocal()) {
LocalMangaSource.updateCover(context, toSManga(), stream)
coverManager.update(toSManga(), stream)
updateManga.awaitUpdateCoverLastModified(id)
} else if (favorite) {
coverCache.setCustomCoverToCache(this, stream)

View file

@ -1,7 +1,7 @@
package eu.kanade.tachiyomi.util.preference
import android.widget.CompoundButton
import eu.kanade.core.prefs.PreferenceMutableState
import eu.kanade.core.preference.PreferenceMutableState
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.onEach

View file

@ -46,7 +46,6 @@ import tachiyomi.core.util.system.logcat
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.File
import kotlin.math.max
import kotlin.math.roundToInt
/**
@ -113,9 +112,6 @@ fun Context.hasPermission(permission: String) = ContextCompat.checkSelfPermissio
}
}
val getDisplayMaxHeightInPx: Int
get() = Resources.getSystem().displayMetrics.let { max(it.heightPixels, it.widthPixels) }
/**
* Converts to px and takes into account LTR/RTL layout.
*/

1
core-metadata/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/build

View file

@ -0,0 +1,21 @@
plugins {
id("com.android.library")
kotlin("android")
kotlin("plugin.serialization")
}
android {
namespace = "tachiyomi.core.metadata"
defaultConfig {
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
consumerProguardFiles("consumer-rules.pro")
}
}
dependencies {
implementation(project(":source-api"))
implementation(kotlinx.bundles.serialization)
}

View file

21
core-metadata/proguard-rules.pro vendored Normal file
View file

@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View file

@ -0,0 +1,2 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest />

View file

@ -1,37 +1,13 @@
package eu.kanade.domain.entries.manga.model
package tachiyomi.core.metadata.comicinfo
import eu.kanade.tachiyomi.source.model.SManga
import kotlinx.serialization.Serializable
import nl.adaptivity.xmlutil.serialization.XmlElement
import nl.adaptivity.xmlutil.serialization.XmlSerialName
import nl.adaptivity.xmlutil.serialization.XmlValue
import tachiyomi.domain.entries.manga.model.Manga
import tachiyomi.domain.items.chapter.model.Chapter
const val COMIC_INFO_FILE = "ComicInfo.xml"
/**
* Creates a ComicInfo instance based on the manga and chapter metadata.
*/
fun getComicInfo(manga: Manga, chapter: Chapter, chapterUrl: String) = ComicInfo(
title = ComicInfo.Title(chapter.name),
series = ComicInfo.Series(manga.title),
web = ComicInfo.Web(chapterUrl),
summary = manga.description?.let { ComicInfo.Summary(it) },
writer = manga.author?.let { ComicInfo.Writer(it) },
penciller = manga.artist?.let { ComicInfo.Penciller(it) },
translator = chapter.scanlator?.let { ComicInfo.Translator(it) },
genre = manga.genre?.let { ComicInfo.Genre(it.joinToString()) },
publishingStatus = ComicInfo.PublishingStatusTachiyomi(
ComicInfoPublishingStatus.toComicInfoValue(manga.status),
),
inker = null,
colorist = null,
letterer = null,
coverArtist = null,
tags = null,
)
fun SManga.copyFromComicInfo(comicInfo: ComicInfo) {
comicInfo.series?.let { title = it.value }
comicInfo.writer?.let { author = it.value }
@ -149,7 +125,7 @@ data class ComicInfo(
data class PublishingStatusTachiyomi(@XmlValue(true) val value: String = "")
}
private enum class ComicInfoPublishingStatus(
enum class ComicInfoPublishingStatus(
val comicInfoValue: String,
val sMangaModelValue: Int,
) {

View file

@ -0,0 +1,13 @@
package tachiyomi.core.metadata.tachiyomi
import kotlinx.serialization.Serializable
@Serializable
class AnimeDetails(
val title: String? = null,
val author: String? = null,
val artist: String? = null,
val description: String? = null,
val genre: List<String>? = null,
val status: Int? = null,
)

View file

@ -0,0 +1,13 @@
package tachiyomi.core.metadata.tachiyomi
import kotlinx.serialization.Serializable
@Serializable
class MangaDetails(
val title: String? = null,
val author: String? = null,
val artist: String? = null,
val description: String? = null,
val genre: List<String>? = null,
val status: Int? = null,
)

View file

@ -27,12 +27,24 @@ dependencies {
api(libs.okhttp.dnsoverhttps)
api(libs.okio)
implementation(libs.image.decoder)
implementation(libs.unifile)
api(kotlinx.coroutines.core)
api(kotlinx.serialization.json)
api(kotlinx.serialization.json.okio)
api(libs.preferencektx)
implementation(libs.jsoup)
// Sort
implementation(libs.natural.comparator)
// JavaScript engine
implementation(libs.bundles.js.engine)
// FFmpeg-kit
implementation(libs.ffmpeg.kit)
}

View file

@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.network
import android.content.Context
import eu.kanade.tachiyomi.network.interceptor.CloudflareInterceptor
import eu.kanade.tachiyomi.network.interceptor.UncaughtExceptionInterceptor
import eu.kanade.tachiyomi.network.interceptor.UserAgentInterceptor
import okhttp3.Cache
import okhttp3.OkHttpClient
@ -33,6 +34,7 @@ class NetworkHelper(
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.callTimeout(2, TimeUnit.MINUTES)
.addInterceptor(UncaughtExceptionInterceptor())
.addInterceptor(userAgentInterceptor)
if (preferences.verboseLogging().get()) {

View file

@ -0,0 +1,24 @@
package eu.kanade.tachiyomi.network.interceptor
import okhttp3.Interceptor
import okhttp3.Response
import java.io.IOException
/**
* Catches any uncaught exceptions from later in the chain and rethrows as a non-fatal
* IOException to avoid catastrophic failure.
*
* This should be the first interceptor in the client.
*
* See https://square.github.io/okhttp/4.x/okhttp/okhttp3/-interceptor/
*/
class UncaughtExceptionInterceptor : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
return try {
chain.proceed(chain.request())
} catch (e: Exception) {
throw IOException(e)
}
}
}

View file

@ -1,15 +1,11 @@
package eu.kanade.tachiyomi.util.storage
import android.Manifest
import android.content.Context
import android.media.MediaScannerConnection
import android.net.Uri
import android.os.Environment
import android.os.StatFs
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.core.content.ContextCompat
import com.google.accompanist.permissions.rememberPermissionState
import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.util.lang.Hash
import java.io.File
@ -117,16 +113,5 @@ object DiskUtil {
}
}
/**
* Launches request for [Manifest.permission.WRITE_EXTERNAL_STORAGE] permission
*/
@Composable
fun RequestStoragePermission() {
val permissionState = rememberPermissionState(permission = Manifest.permission.WRITE_EXTERNAL_STORAGE)
LaunchedEffect(Unit) {
permissionState.launchPermissionRequest()
}
}
const val NOMEDIA_FILE = ".nomedia"
}

View file

@ -1,15 +1,10 @@
package eu.kanade.tachiyomi.util.storage
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import org.jsoup.Jsoup
import org.jsoup.nodes.Document
import java.io.Closeable
import java.io.File
import java.io.InputStream
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.Locale
import java.util.zip.ZipEntry
import java.util.zip.ZipFile
@ -49,58 +44,6 @@ class EpubFile(file: File) : Closeable {
return zip.getEntry(name)
}
/**
* Fills manga metadata using this epub file's metadata.
*/
fun fillMangaMetadata(manga: SManga) {
val ref = getPackageHref()
val doc = getPackageDocument(ref)
val creator = doc.getElementsByTag("dc:creator").first()
val description = doc.getElementsByTag("dc:description").first()
manga.author = creator?.text()
manga.description = description?.text()
}
/**
* Fills chapter metadata using this epub file's metadata.
*/
fun fillChapterMetadata(chapter: SChapter) {
val ref = getPackageHref()
val doc = getPackageDocument(ref)
val title = doc.getElementsByTag("dc:title").first()
val publisher = doc.getElementsByTag("dc:publisher").first()
val creator = doc.getElementsByTag("dc:creator").first()
var date = doc.getElementsByTag("dc:date").first()
if (date == null) {
date = doc.select("meta[property=dcterms:modified]").first()
}
if (title != null) {
chapter.name = title.text()
}
if (publisher != null) {
chapter.scanlator = publisher.text()
} else if (creator != null) {
chapter.scanlator = creator.text()
}
if (date != null) {
val dateFormat = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.getDefault())
try {
val parsedDate = dateFormat.parse(date.text())
if (parsedDate != null) {
chapter.date_upload = parsedDate.time
}
} catch (e: ParseException) {
// Empty
}
}
}
/**
* Returns the path of all the images found in the epub file.
*/
@ -114,7 +57,7 @@ class EpubFile(file: File) : Closeable {
/**
* Returns the path to the package document.
*/
private fun getPackageHref(): String {
fun getPackageHref(): String {
val meta = zip.getEntry(resolveZipPath("META-INF", "container.xml"))
if (meta != null) {
val metaDoc = zip.getInputStream(meta).use { Jsoup.parse(it, null, "") }
@ -129,7 +72,7 @@ class EpubFile(file: File) : Closeable {
/**
* Returns the package document where all the files are listed.
*/
private fun getPackageDocument(ref: String): Document {
fun getPackageDocument(ref: String): Document {
val entry = zip.getEntry(ref)
return zip.getInputStream(entry).use { Jsoup.parse(it, null, "") }
}
@ -137,7 +80,7 @@ class EpubFile(file: File) : Closeable {
/**
* Returns all the pages from the epub.
*/
private fun getPagesFromDocument(document: Document): List<String> {
fun getPagesFromDocument(document: Document): List<String> {
val pages = document.select("manifest > item")
.filter { node -> "application/xhtml+xml" == node.attr("media-type") }
.associateBy { it.attr("id") }

View file

@ -0,0 +1,19 @@
package eu.kanade.tachiyomi.util.storage
import android.content.Context
import android.net.Uri
import android.os.Build
import com.arthenica.ffmpegkit.FFmpegKitConfig
import java.io.File
fun String.toFFmpegString(context: Context): String {
return File(this).getUriCompat(context).toFFmpegString(context)
}
fun Uri.toFFmpegString(context: Context): String {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && this.scheme == "content") {
FFmpegKitConfig.getSafParameter(context, this, "rw")
} else {
this.path!!
}.replace("\"", "\\\"")
}

View file

@ -0,0 +1,24 @@
package eu.kanade.tachiyomi.util.storage
import android.content.Context
import android.net.Uri
import android.os.Build
import androidx.core.content.FileProvider
import androidx.core.net.toUri
import java.io.File
val Context.cacheImageDir: File
get() = File(cacheDir, "shared_image")
/**
* Returns the uri of a file
*
* @param context context of application
*/
fun File.getUriCompat(context: Context): Uri {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
FileProvider.getUriForFile(context, "xyz.jmir.tachiyomi.mi.debug" + ".provider", this)
} else {
this.toUri()
}
}

View file

@ -1,7 +1,8 @@
package eu.kanade.tachiyomi.util.system
package tachiyomi.core.util.system
import android.content.Context
import android.content.res.Configuration
import android.content.res.Resources
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.graphics.BitmapRegionDecoder
@ -22,7 +23,6 @@ import androidx.core.graphics.green
import androidx.core.graphics.red
import com.hippo.unifile.UniFile
import logcat.LogPriority
import tachiyomi.core.util.system.logcat
import tachiyomi.decoder.Format
import tachiyomi.decoder.ImageDecoder
import java.io.BufferedInputStream
@ -31,6 +31,7 @@ import java.io.ByteArrayOutputStream
import java.io.InputStream
import java.net.URLConnection
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.min
object ImageUtil {
@ -587,3 +588,6 @@ object ImageUtil {
"image/jxl" to "jxl",
)
}
val getDisplayMaxHeightInPx: Int
get() = Resources.getSystem().displayMetrics.let { max(it.heightPixels, it.widthPixels) }

View file

@ -1,7 +1,7 @@
package eu.kanade.data.source.anime
package tachiyomi.data.source.anime
import androidx.paging.PagingSource
import androidx.paging.PagingState
import eu.kanade.domain.source.anime.model.AnimeSourcePagingSourceType
import eu.kanade.tachiyomi.animesource.AnimeCatalogueSource
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
import eu.kanade.tachiyomi.animesource.model.AnimesPage
@ -10,6 +10,8 @@ import tachiyomi.core.util.lang.awaitSingle
import tachiyomi.core.util.lang.withIOContext
import tachiyomi.domain.items.episode.model.NoEpisodesException
typealias AnimeSourcePagingSourceType = PagingSource<Long, SAnime>
abstract class AnimeSourcePagingSource(
protected val source: AnimeCatalogueSource,
) : AnimeSourcePagingSourceType() {

View file

@ -1,7 +1,7 @@
package eu.kanade.data.source.manga
package tachiyomi.data.source.manga
import androidx.paging.PagingSource
import androidx.paging.PagingState
import eu.kanade.domain.source.manga.model.SourcePagingSourceType
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
@ -10,6 +10,8 @@ import tachiyomi.core.util.lang.awaitSingle
import tachiyomi.core.util.lang.withIOContext
import tachiyomi.domain.items.chapter.model.NoChaptersException
typealias SourcePagingSourceType = PagingSource<Long, SManga>
abstract class SourcePagingSource(
protected val source: CatalogueSource,
) : SourcePagingSourceType() {

View file

@ -753,7 +753,7 @@
<string name="action_search_hint">بحث…</string>
<string name="skipped_reason_not_always_update">تم التخطي لأن السلسلة لا تتطلب تحديثات</string>
<string name="crash_screen_description">%s واجه خطأ غير متوقع. نقترح عليك أخد لقطة شاشة لهذه الرسالة، وتفريغ سجلات التعطل ، ثم مشاركتها في قناة الدعم الخاصة بنا على Discord.</string>
<string name="crash_screen_title">أخ!</string>
<string name="crash_screen_title">عفوًا!</string>
<string name="crash_screen_restart_application">أعد تشغيل التطبيق</string>
<string name="pref_general_summary">لغة التطبيق، الإشعارات</string>
<string name="pref_appearance_summary">مظهر، التاريخ والوقت</string>
@ -777,4 +777,13 @@
<string name="track_error">%1$s خطأ: %2$s</string>
<string name="information_required_plain">*مطلوب</string>
<string name="action_copy_to_clipboard">نسخ إلى الحافظة</string>
<plurals name="download_amount">
<item quantity="zero">الفصل التالي</item>
<item quantity="one">الفصل التالي</item>
<item quantity="two">%d فصول تالية</item>
<item quantity="few">%d فصول تالية</item>
<item quantity="many">%d فصل تالي</item>
<item quantity="other">%d فصل تالي</item>
</plurals>
<string name="pref_hide_in_library_items">إخفاء الإدخالات الموجودة بالفعل في المكتبة</string>
</resources>

View file

@ -329,7 +329,7 @@
<string name="theme_system">Según ajustes del sistema</string>
<string name="pref_manage_notifications">Gestionar notificaciones</string>
<string name="pref_category_security">Seguridad y privacidad</string>
<string name="lock_with_biometrics">Requiere utilizar desbloqueo por biometría</string>
<string name="lock_with_biometrics">Requiere el uso de desbloqueo por biometría</string>
<string name="lock_when_idle">Bloquear por inactividad</string>
<string name="lock_always">Siempre</string>
<string name="lock_never">Nunca</string>

View file

@ -19,7 +19,7 @@
<string name="pref_remove_after_read">Pagkatapos basahin, kusang burahin</string>
<string name="pref_remove_bookmarked_chapters">Burahin din ang may pananda</string>
<string name="pref_remove_exclude_categories_manga">Kategoryang di-kasama</string>
<string name="pref_auto_update_manga_sync">I-update ang progress pagkabasa</string>
<string name="pref_auto_update_manga_sync">I-update ang progress pagkatapos basahin</string>
<string name="pref_clear_chapter_cache">Linisin ang cache ng kabanata</string>
<string name="pref_auto_clear_chapter_cache">Linisin ang cache ng kabanata pagkasara</string>
<string name="pref_clear_manga_database">Linisin ang database</string>

View file

@ -736,4 +736,10 @@
<string name="hour_short">%dh</string>
<string name="minute_short">%dm</string>
<string name="seconds_short">%ds</string>
<string name="action_copy_to_clipboard">Kopyahin sa clipboard</string>
<string name="pref_hide_in_library_items">Itago ang mga entry na nasa aklatan na</string>
<plurals name="download_amount">
<item quantity="one">Susunod na kabanata</item>
<item quantity="other">Susunod na %d (mga) kabanata</item>
</plurals>
</resources>

View file

@ -2,7 +2,7 @@
<resources>
<string name="name">이름</string>
<string name="manga">만화</string>
<string name="chapters">회차</string>
<string name="chapters">범주</string>
<string name="track">동기화</string>
<string name="label_backup">백업 및 복원</string>
<string name="action_settings">설정</string>
@ -65,7 +65,7 @@
<string name="pref_category_advanced">고급</string>
<string name="pref_category_about">정보</string>
<string name="pref_library_columns">서재 정렬</string>
<string name="label_migration">소스 이동</string>
<string name="label_migration">데이터 이전</string>
<string name="label_extensions">확장기능</string>
<string name="label_extension_info">확장기능 정보</string>
<string name="action_global_search">전체 검색</string>
@ -147,7 +147,7 @@
<string name="pref_enable_acra">오류 보고서 전송</string>
<string name="pref_acra_summary">버그를 수정하는데 도움이 됩니다. 개인 정보는 전송되지 않습니다</string>
<string name="login_title">%1$s 으로 로그인</string>
<string name="username">유저네임</string>
<string name="username">사용자명</string>
<string name="password">비밀번호</string>
<string name="login">로그인</string>
<string name="login_success">로그인 성공</string>
@ -156,7 +156,7 @@
<string name="no_more_results">더이상 결과 없음</string>
<string name="action_global_search_hint">없음 검색…</string>
<string name="latest">최신</string>
<string name="ongoing">연재</string>
<string name="ongoing">연재</string>
<string name="unknown">알 수 없음</string>
<string name="remove_from_library">서재에서 제거</string>
<string name="manga_added_library">서재에 추가되었습니다</string>
@ -214,7 +214,7 @@
<string name="download_notifier_unknown_error">다운로드 중에 예기치 않은 오류가 발생하였습니다</string>
<string name="action_display_download_badge">다운로드한 챕터</string>
<string name="pref_update_only_non_completed">연재가 끝남</string>
<string name="default_category_summary">항상 물어보기</string>
<string name="default_category">기본 범주</string>
<string name="pref_create_backup_summ">현재 서재를 나중에 복구하는 데 사용 가능</string>
<string name="pref_reader_navigation">네비게이션</string>
<string name="pref_page_transitions">페이지 전환 효과 표시</string>
@ -255,7 +255,7 @@
<string name="track_type">종류</string>
<string name="error_category_exists">같은 이름을 가진 카테고리가 이미 존재합니다!</string>
<string name="migration_dialog_what_to_include">포함할 데이터를 선택하세요</string>
<string name="migrate">이동</string>
<string name="migrate">데이터 이전</string>
<string name="copy">복사</string>
<string name="download_queue_error">다운로드 실패. 다운로드 메뉴에서 다시 시도할 수 있습니다</string>
<string name="notification_first_add_to_library">이 행동을 하기 전 서재에 항목을 추가해주세요</string>
@ -300,7 +300,7 @@
<string name="pref_incognito_mode">시크릿 모드</string>
<string name="updated_version">v%1$s 으로 업데이트 됨</string>
<string name="check_for_updates">업데이트 확인</string>
<string name="licenses">오픈 소스 라이</string>
<string name="licenses">오픈 소스 라이</string>
<string name="battery_optimization_setting_activity_not_found">디바이스 설정을 열 수 없습니다</string>
<string name="pref_disable_battery_optimization">배터리 최적화 끄기</string>
<string name="restore_miui_warning">MIUI 최적화가 꺼져 있을 경우 백업/복원 기능이 정상 작동하지 않을 수 있습니다.</string>
@ -471,9 +471,9 @@
<string name="action_global_search_query">\"%1$s\"를 전체 검색합니다</string>
<string name="local_source_help_guide">로컬 저장소 사용법</string>
<string name="no_pinned_sources">핀 설정된 소스가 없습니다</string>
<string name="local_invalid_format">잘못된 회차 포맷</string>
<string name="local_invalid_format">잘못된 챕터형식</string>
<string name="unknown_status">알 수 없는 상태</string>
<string name="on_hiatus">휴재</string>
<string name="on_hiatus">휴재</string>
<string name="manga_info_expand">상세정보 표시</string>
<string name="manga_info_collapse">상세정보 숨김</string>
<string name="clipboard_copy_error">클립보드로 복사에 실패하였습니다</string>

View file

@ -125,7 +125,7 @@
<string name="pref_library_update_restriction">स्वचालित अपडेटहरु उपकरण प्रतिबन्धहरू</string>
<string name="update_weekly">साप्ताहिक</string>
<string name="update_48hour">प्रत्येक २ दिन</string>
<string name="update_24hour">सँधै</string>
<string name="update_24hour">दैनिक</string>
<string name="update_12hour">प्रत्येक १२ घण्टा</string>
<string name="update_6hour">प्रत्येक ६ घण्टा</string>
<string name="update_never">अफ</string>
@ -146,8 +146,8 @@
<string name="secure_screen_summary">एपहरू स्विच गर्दा \"सुरक्षित स्क्रिनले\" एप सामग्रीहरू लुकाउँछ र स्क्रिनसटहरू रोक्छ</string>
<string name="secure_screen">स्क्रिन सुरक्षित गर्नुहोस्</string>
<plurals name="lock_after_mins">
<item quantity="one">%1$s मिनट पछि</item>
<item quantity="other">%1$s मिनट पछि</item>
<item quantity="one">%1$s मिनट पछि</item>
<item quantity="other">%1$s मिनट पछि</item>
</plurals>
<string name="lock_never">कहिल्यै हैन</string>
<string name="lock_always">सधैं</string>
@ -183,13 +183,13 @@
<string name="label_default">पूर्वनिर्धारित</string>
<string name="pref_tablet_ui_mode">ट्याब्लेट UI</string>
<string name="battery_optimization_setting_activity_not_found">यन्त्रको सेटिङहरू खोल्न सकिएन</string>
<string name="pref_incognito_mode">इन्कोग्निटो मोड</string>
<string name="pref_incognito_mode">गुप्त मोड</string>
<string name="label_downloaded_only">डाउनलोड गरिएको मात्र</string>
<string name="pref_acra_summary">कुनै पनि बगहरू ठीक गर्न मद्दत गर्दछ। कुनै संवेदनशील डाटा पठाइने छैन</string>
<string name="pref_enable_acra">क्रेश रिपोर्टहरू पठाउनुहोस्</string>
<string name="updated_version">v%1$s मा अपडेट गरियो</string>
<string name="check_for_updates">अपडेटका लागि चेक गर्नुहोस्</string>
<string name="decode_image_error">चित्र लोड गर्न सकिएन</string>
<string name="decode_image_error">छवि लोड गर्न सकिएन</string>
<string name="download_queue_error">अध्यायहरू डाउनलोड गर्न सकिएन। तपाईं डाउनलोड कतारमा फेरि प्रयास गर्न सक्नुहुन्छ</string>
<string name="ext_uninstall">स्थापना रद्द गर्नुहोस्</string>
<string name="third_to_last">तेस्रो अन्तिम अध्याय</string>
@ -453,8 +453,8 @@
<string name="local_filter_order_by">द्वारा अर्डर गर्नुहोस्</string>
<string name="date">मिति</string>
<string name="login_title">%1$s मा लग इन गर्नुहोस्</string>
<string name="pref_refresh_library_covers">पुस्तकालय का माङ्गा कभरहरू ताजा गर्नुहोस्</string>
<string name="notification_incognito_text">इन्कोग्निटो मोड असक्षम गर्नुहोस्</string>
<string name="pref_refresh_library_covers">पुस्तकालय का कभरहरू ताजा गर्नुहोस्</string>
<string name="notification_incognito_text">गुप्त मोड असक्षम गर्नुहोस्</string>
<string name="unknown_error">अज्ञात एरर</string>
<string name="downloaded_chapters">डाउनलोड गरिएका अध्यायहरू</string>
<string name="chapter_not_found">अध्याय फेला परेन</string>
@ -540,7 +540,7 @@
<string name="confirm_delete_chapters">के तपाईँले चयन गर्नुभएको अध्यायहरू हटाउन चाहनुहुन्छ\?</string>
<string name="chapter_settings">अध्याय सेटिङ</string>
<string name="set_chapter_settings_as_default">पूर्वनिर्धारित रूपमा सेट गर्नुहोस्</string>
<string name="confirm_set_image_as_cover">यो चित्र कभरको रूपमा राख्न चाहनुहुन्छ\?</string>
<string name="confirm_set_image_as_cover">यो छवि कभरको रूपमा राख्न चाहनुहुन्छ\?</string>
<string name="download_all">सबै</string>
<string name="download_unread">नपढिएको</string>
<string name="cover_saved">कभर बचत भयो</string>

View file

@ -313,7 +313,7 @@
<string name="lock_never">Nigdy</string>
<string name="lock_always">Zawsze</string>
<string name="lock_with_biometrics">Wymagaj odblokowania</string>
<string name="pref_category_security">Bezpieczeństwo</string>
<string name="pref_category_security">Bezpieczeństwo i prywatność</string>
<string name="pref_manage_notifications">Zarządzaj powiadomieniami</string>
<string name="theme_system">Systemowy</string>
<string name="theme_dark">Włącz</string>
@ -348,7 +348,7 @@
<string name="restore_in_progress">Kopia zapasowa jest w trakcie przywracania</string>
<string name="backup_in_progress">Kopia zapasowa jest już w trakcie tworzenia</string>
<string name="restore_duration">%02d min, %02d s</string>
<string name="pref_search_pinned_sources_only">Pokazuj tylko przypięte źródła</string>
<string name="pref_search_pinned_sources_only">Wyszukaj tylko w przypiętych źródłach</string>
<string name="pref_webtoon_side_padding">Marginesy boczne</string>
<string name="gray_background">Szary</string>
<string name="pref_true_color_summary">Ogranicza banding, ale może mieć wpływ na wydajność</string>
@ -733,4 +733,12 @@
<string name="pref_worker_info">Informacje o procesach</string>
<string name="track_error">Błąd %1$s: %2$s</string>
<string name="information_required_plain">*wymagane</string>
<string name="pref_hide_in_library_items">Ukryj pozycje znajdujące się już w bibliotece</string>
<string name="action_copy_to_clipboard">Kopiuj do schowka</string>
<plurals name="download_amount">
<item quantity="one">Następny rozdział</item>
<item quantity="few">Następne %d rozdziały</item>
<item quantity="many">Następne %d rozdziałów</item>
<item quantity="other">Następne %d rozdziałów</item>
</plurals>
</resources>

View file

@ -326,7 +326,7 @@
<string name="theme_dark">Ligado</string>
<string name="theme_system">Seguir o do sistema</string>
<string name="pref_manage_notifications">Gerir notificações</string>
<string name="pref_category_security">Segurança</string>
<string name="pref_category_security">Segurança e privacidade.</string>
<string name="lock_with_biometrics">Requerer desbloqueio</string>
<string name="lock_when_idle">Bloquear quando inativo</string>
<string name="lock_always">Sempre</string>
@ -417,7 +417,7 @@
<string name="licenses">Licenças de código aberto</string>
<string name="downloaded_only_summary">Filtra todos os itens nasua biblioteca</string>
<string name="restore_duration">%02d min, %02d seg</string>
<string name="pref_search_pinned_sources_only">Apenas incluir fontes fixadas</string>
<string name="pref_search_pinned_sources_only">Pesquise apenas fontes fixadas na pesquisa global</string>
<plurals name="download_queue_summary">
<item quantity="one">%1$s restante</item>
<item quantity="many">%1$s restantes</item>
@ -793,4 +793,11 @@
<string name="fdroid_warning">Compilações do F-Droid não são suportadas oficialmente.
\nToque para saber mais.</string>
<string name="information_no_entries_found">Nenhum item encontrado nesta categoria</string>
<string name="action_copy_to_clipboard">Copiar para a área de transferência</string>
<string name="pref_hide_in_library_items">Ocultar entradas existentes na biblioteca</string>
<plurals name="download_amount">
<item quantity="one">Próximo capítulo</item>
<item quantity="many">Próximos %d capítulos</item>
<item quantity="other">Próximos %d capítulos</item>
</plurals>
</resources>

View file

@ -782,4 +782,8 @@
<string name="hour_short">%do</string>
<string name="minute_short">%dm</string>
<string name="information_no_manga_category">Kategorija është bosh</string>
<string name="enhanced_services_not_installed">Në dispozicion, por burimi nuk është i instaluar: %s</string>
<string name="pref_skip_dupe_chapters">Kapërceni kapitujt e kopjuar</string>
<string name="pref_hide_in_library_items">Fshih hyrjet tashmë në bibliotekë</string>
<string name="action_copy_to_clipboard">Kopjo në kujtesën e fragmenteve</string>
</resources>

Some files were not shown because too many files have changed in this diff Show more