mirror of
https://github.com/aniyomiorg/aniyomi.git
synced 2024-11-21 12:17:12 +03:00
parent
b8cdb9d55e
commit
325e625aef
92 changed files with 1022 additions and 729 deletions
2
.github/ISSUE_TEMPLATE.md
vendored
2
.github/ISSUE_TEMPLATE.md
vendored
|
@ -5,7 +5,7 @@ I acknowledge that:
|
|||
- I have updated:
|
||||
- To the latest version of the app (stable is v0.12.3.10)
|
||||
- All extensions
|
||||
- I have gone through the FAQ (https://aniyomi.org/help/faq/) and troubleshooting guide (https://aniyomi.org/help/guides/troubleshooting/)
|
||||
- I have gone through the FAQ (https://aniyomi.org/docs/faq/general) and troubleshooting guide (https://aniyomi.org/docs/guides/troubleshooting/)
|
||||
- If this is an issue with an anime extension, that I should be opening an issue in https://github.com/aniyomiorg/aniyomi-extensions
|
||||
- I have searched the existing issues and this is new ticket **NOT** a duplicate or related to another open or closed issue
|
||||
- I will fill out the title and the information in this template
|
||||
|
|
2
.github/ISSUE_TEMPLATE/config.yml
vendored
2
.github/ISSUE_TEMPLATE/config.yml
vendored
|
@ -4,7 +4,7 @@ contact_links:
|
|||
url: https://github.com/aniyomiorg/aniyomi-extensions/issues/new/choose
|
||||
about: Issues and requests for extensions and sources should be opened in the aniyomi-extensions repository instead
|
||||
- name: 📦 Aniyomi extensions
|
||||
url: https://aniyomi.org/extensions
|
||||
url: https://aniyomi.org/extensions/
|
||||
about: Anime extensions and sources
|
||||
- name: 🧑💻 Aniyomi help discord
|
||||
url: https://discord.gg/F32UjdJZrR
|
||||
|
|
2
.github/ISSUE_TEMPLATE/report_issue.yml
vendored
2
.github/ISSUE_TEMPLATE/report_issue.yml
vendored
|
@ -95,7 +95,7 @@ body:
|
|||
required: true
|
||||
- label: If this is an issue with an extension, I should be opening an issue in the [extensions repository](https://github.com/aniyomiorg/aniyomi-extensions/issues/new/choose).
|
||||
required: true
|
||||
- label: I have gone through the [FAQ](https://aniyomi.org/help/faq/) and [troubleshooting guide](https://aniyomi.org/help/guides/troubleshooting/).
|
||||
- label: I have gone through the [FAQ](https://aniyomi.org/docs/faq/general) and [troubleshooting guide](https://aniyomi.org/docs/guides/troubleshooting/).
|
||||
required: true
|
||||
- label: I have updated the app to version **[0.12.3.10](https://github.com/aniyomiorg/aniyomi/releases/latest)**.
|
||||
required: true
|
||||
|
|
11
.github/workflows/build_push.yml
vendored
11
.github/workflows/build_push.yml
vendored
|
@ -120,3 +120,14 @@ jobs:
|
|||
prerelease: false
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
update-website:
|
||||
needs: [ build ]
|
||||
runs-on: ubuntu-latest
|
||||
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'aniyomiorg/aniyomi'
|
||||
steps:
|
||||
- name: Trigger Netlify build hook
|
||||
run: curl -s -X POST -d {} "https://api.netlify.com/build_hooks/${TOKEN}"
|
||||
env:
|
||||
TOKEN: ${{ secrets.NETLIFY_HOOK_RELEASE }}
|
||||
|
||||
|
|
2
.github/workflows/issue_moderator.yml
vendored
2
.github/workflows/issue_moderator.yml
vendored
|
@ -33,7 +33,7 @@ jobs:
|
|||
"regex": ".*(?:fail(?:ed|ure|s)?|can\\s*(?:no|')?t|(?:not|un).*able|(?<!n[o']?t )blocked by|error) (?:to )?(?:get past|by ?pass|penetrate)?.*cloud ?fl?are.*",
|
||||
"ignoreCase": true,
|
||||
"labels": ["Cloudflare protected"],
|
||||
"message": "Refer to the **Solving Cloudflare issues** section at https://aniyomi.org/help/guides/troubleshooting/#solving-cloudflare-issues. If it doesn't work, migrate to other sources or wait until they lower their protection."
|
||||
"message": "Refer to the **Solving Cloudflare issues** section at https://aniyomi.org/docs/guides/troubleshooting/#cloudflare. If it doesn't work, migrate to other sources or wait until they lower their protection."
|
||||
}
|
||||
]
|
||||
auto-close-ignore-label: do-not-autoclose
|
||||
|
|
|
@ -30,7 +30,7 @@ Before you start, please note that the ability to use following technologies is
|
|||
|
||||
# Translations
|
||||
|
||||
Translations are done externally via Weblate. See [our website](https://aniyomi.org/help/contribution/#translation) for more details.
|
||||
Translations are done externally via Weblate. See [our website](https://aniyomi.org/docs/contribute#translation) for more details.
|
||||
|
||||
|
||||
# Forks
|
||||
|
|
|
@ -31,7 +31,7 @@ Please make sure to read the full guidelines. Your issue may be closed without w
|
|||
|
||||
<details><summary>Issues</summary>
|
||||
|
||||
1. **Before reporting a new issue, take a look at the already opened [issues](https://github.com/aniyomiorg/aniyomi/issues).**
|
||||
1. **Before reporting a new issue, take a look at the already opened [issues](https://aniyomi.org/changelogs/).**
|
||||
2. If you are unsure, ask here: [![Discord](https://img.shields.io/discord/841701076242530374?label=discord&labelColor=7289da&color=2c2f33&style=flat)](https://discord.gg/F32UjdJZrR)
|
||||
|
||||
</details>
|
||||
|
|
|
@ -12,7 +12,6 @@ import eu.kanade.tachiyomi.source.model.Page
|
|||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Response
|
||||
import rx.Observable
|
||||
import tachiyomi.core.preference.Preference
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
||||
|
@ -27,15 +26,6 @@ interface DataSaver {
|
|||
}
|
||||
}
|
||||
|
||||
fun HttpSource.fetchImage(page: Page, dataSaver: DataSaver): Observable<Response> {
|
||||
val imageUrl = page.imageUrl ?: return fetchImage(page)
|
||||
page.imageUrl = dataSaver.compress(imageUrl)
|
||||
return fetchImage(page)
|
||||
.doOnNext {
|
||||
page.imageUrl = imageUrl
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun HttpSource.getImage(page: Page, dataSaver: DataSaver): Response {
|
||||
val imageUrl = page.imageUrl ?: return getImage(page)
|
||||
page.imageUrl = dataSaver.compress(imageUrl)
|
||||
|
|
|
@ -77,6 +77,7 @@ import tachiyomi.domain.category.manga.interactor.SetMangaDisplayMode
|
|||
import tachiyomi.domain.category.manga.interactor.SetSortModeForMangaCategory
|
||||
import tachiyomi.domain.category.manga.interactor.UpdateMangaCategory
|
||||
import tachiyomi.domain.category.manga.repository.MangaCategoryRepository
|
||||
import tachiyomi.domain.entries.anime.interactor.AnimeFetchInterval
|
||||
import tachiyomi.domain.entries.anime.interactor.GetAnime
|
||||
import tachiyomi.domain.entries.anime.interactor.GetAnimeFavorites
|
||||
import tachiyomi.domain.entries.anime.interactor.GetAnimeWithEpisodes
|
||||
|
@ -85,17 +86,16 @@ import tachiyomi.domain.entries.anime.interactor.GetLibraryAnime
|
|||
import tachiyomi.domain.entries.anime.interactor.NetworkToLocalAnime
|
||||
import tachiyomi.domain.entries.anime.interactor.ResetAnimeViewerFlags
|
||||
import tachiyomi.domain.entries.anime.interactor.SetAnimeEpisodeFlags
|
||||
import tachiyomi.domain.entries.anime.interactor.SetAnimeFetchInterval
|
||||
import tachiyomi.domain.entries.anime.repository.AnimeRepository
|
||||
import tachiyomi.domain.entries.manga.interactor.GetDuplicateLibraryManga
|
||||
import tachiyomi.domain.entries.manga.interactor.GetLibraryManga
|
||||
import tachiyomi.domain.entries.manga.interactor.GetManga
|
||||
import tachiyomi.domain.entries.manga.interactor.GetMangaFavorites
|
||||
import tachiyomi.domain.entries.manga.interactor.GetMangaWithChapters
|
||||
import tachiyomi.domain.entries.manga.interactor.MangaFetchInterval
|
||||
import tachiyomi.domain.entries.manga.interactor.NetworkToLocalManga
|
||||
import tachiyomi.domain.entries.manga.interactor.ResetMangaViewerFlags
|
||||
import tachiyomi.domain.entries.manga.interactor.SetMangaChapterFlags
|
||||
import tachiyomi.domain.entries.manga.interactor.SetMangaFetchInterval
|
||||
import tachiyomi.domain.entries.manga.repository.MangaRepository
|
||||
import tachiyomi.domain.history.anime.interactor.GetAnimeHistory
|
||||
import tachiyomi.domain.history.anime.interactor.GetNextEpisodes
|
||||
|
@ -188,7 +188,7 @@ class DomainModule : InjektModule {
|
|||
addFactory { GetNextEpisodes(get(), get(), get()) }
|
||||
addFactory { ResetAnimeViewerFlags(get()) }
|
||||
addFactory { SetAnimeEpisodeFlags(get()) }
|
||||
addFactory { SetAnimeFetchInterval(get()) }
|
||||
addFactory { AnimeFetchInterval(get()) }
|
||||
addFactory { SetAnimeDefaultEpisodeFlags(get(), get(), get()) }
|
||||
addFactory { SetAnimeViewerFlags(get()) }
|
||||
addFactory { NetworkToLocalAnime(get()) }
|
||||
|
@ -204,7 +204,7 @@ class DomainModule : InjektModule {
|
|||
addFactory { GetNextChapters(get(), get(), get()) }
|
||||
addFactory { ResetMangaViewerFlags(get()) }
|
||||
addFactory { SetMangaChapterFlags(get()) }
|
||||
addFactory { SetMangaFetchInterval(get()) }
|
||||
addFactory { MangaFetchInterval(get()) }
|
||||
addFactory {
|
||||
SetMangaDefaultChapterFlags(
|
||||
get(),
|
||||
|
|
|
@ -3,7 +3,7 @@ package eu.kanade.domain.entries.anime.interactor
|
|||
import eu.kanade.domain.entries.anime.model.hasCustomCover
|
||||
import eu.kanade.tachiyomi.animesource.model.SAnime
|
||||
import eu.kanade.tachiyomi.data.cache.AnimeCoverCache
|
||||
import tachiyomi.domain.entries.anime.interactor.SetAnimeFetchInterval
|
||||
import tachiyomi.domain.entries.anime.interactor.AnimeFetchInterval
|
||||
import tachiyomi.domain.entries.anime.model.Anime
|
||||
import tachiyomi.domain.entries.anime.model.AnimeUpdate
|
||||
import tachiyomi.domain.entries.anime.repository.AnimeRepository
|
||||
|
@ -15,7 +15,7 @@ import java.util.Date
|
|||
|
||||
class UpdateAnime(
|
||||
private val animeRepository: AnimeRepository,
|
||||
private val setAnimeFetchInterval: SetAnimeFetchInterval,
|
||||
private val animeFetchInterval: AnimeFetchInterval,
|
||||
) {
|
||||
|
||||
suspend fun await(animeUpdate: AnimeUpdate): Boolean {
|
||||
|
@ -79,9 +79,9 @@ class UpdateAnime(
|
|||
suspend fun awaitUpdateFetchInterval(
|
||||
anime: Anime,
|
||||
dateTime: ZonedDateTime = ZonedDateTime.now(),
|
||||
window: Pair<Long, Long> = setAnimeFetchInterval.getWindow(dateTime),
|
||||
window: Pair<Long, Long> = animeFetchInterval.getWindow(dateTime),
|
||||
): Boolean {
|
||||
return setAnimeFetchInterval.toAnimeUpdateOrNull(anime, dateTime, window)
|
||||
return animeFetchInterval.toAnimeUpdateOrNull(anime, dateTime, window)
|
||||
?.let { animeRepository.updateAnime(it) }
|
||||
?: false
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ package eu.kanade.domain.entries.manga.interactor
|
|||
import eu.kanade.domain.entries.manga.model.hasCustomCover
|
||||
import eu.kanade.tachiyomi.data.cache.MangaCoverCache
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import tachiyomi.domain.entries.manga.interactor.SetMangaFetchInterval
|
||||
import tachiyomi.domain.entries.manga.interactor.MangaFetchInterval
|
||||
import tachiyomi.domain.entries.manga.model.Manga
|
||||
import tachiyomi.domain.entries.manga.model.MangaUpdate
|
||||
import tachiyomi.domain.entries.manga.repository.MangaRepository
|
||||
|
@ -15,7 +15,7 @@ import java.util.Date
|
|||
|
||||
class UpdateManga(
|
||||
private val mangaRepository: MangaRepository,
|
||||
private val setMangaFetchInterval: SetMangaFetchInterval,
|
||||
private val mangaFetchInterval: MangaFetchInterval,
|
||||
) {
|
||||
|
||||
suspend fun await(mangaUpdate: MangaUpdate): Boolean {
|
||||
|
@ -79,9 +79,9 @@ class UpdateManga(
|
|||
suspend fun awaitUpdateFetchInterval(
|
||||
manga: Manga,
|
||||
dateTime: ZonedDateTime = ZonedDateTime.now(),
|
||||
window: Pair<Long, Long> = setMangaFetchInterval.getWindow(dateTime),
|
||||
window: Pair<Long, Long> = mangaFetchInterval.getWindow(dateTime),
|
||||
): Boolean {
|
||||
return setMangaFetchInterval.toMangaUpdateOrNull(manga, dateTime, window)
|
||||
return mangaFetchInterval.toMangaUpdateOrNull(manga, dateTime, window)
|
||||
?.let { mangaRepository.updateManga(it) }
|
||||
?: false
|
||||
}
|
||||
|
|
|
@ -28,7 +28,7 @@ class UiPreferences(
|
|||
|
||||
fun themeDarkAmoled() = preferenceStore.getBoolean("pref_theme_dark_amoled_key", false)
|
||||
|
||||
fun relativeTime() = preferenceStore.getInt("relative_time", 7)
|
||||
fun relativeTime() = preferenceStore.getBoolean("relative_time_v2", true)
|
||||
|
||||
fun dateFormat() = preferenceStore.getString("app_date_format", "")
|
||||
|
||||
|
|
|
@ -13,13 +13,18 @@ import java.util.Date
|
|||
fun RelativeDateHeader(
|
||||
modifier: Modifier = Modifier,
|
||||
date: Date,
|
||||
relativeTime: Boolean,
|
||||
dateFormat: DateFormat,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
ListGroupHeader(
|
||||
modifier = modifier,
|
||||
text = remember {
|
||||
date.toRelativeString(context, dateFormat)
|
||||
date.toRelativeString(
|
||||
context,
|
||||
relativeTime,
|
||||
dateFormat,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
@ -159,7 +159,7 @@ fun EntryBottomActionMenu(
|
|||
val previousUnviewed = if (isManga) R.string.action_mark_previous_as_read else R.string.action_mark_previous_as_seen
|
||||
Button(
|
||||
title = stringResource(previousUnviewed),
|
||||
icon = ImageVector.vectorResource(id = R.drawable.ic_done_prev_24dp),
|
||||
icon = ImageVector.vectorResource(R.drawable.ic_done_prev_24dp),
|
||||
toConfirm = confirm[4],
|
||||
onLongClick = { onLongClickItem(4) },
|
||||
onClick = onMarkPreviousAsViewedClicked,
|
||||
|
|
|
@ -98,6 +98,7 @@ fun AnimeScreen(
|
|||
state: AnimeScreenModel.State.Success,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
fetchInterval: Int?,
|
||||
dateRelativeTime: Boolean,
|
||||
dateFormat: DateFormat,
|
||||
isTabletUi: Boolean,
|
||||
episodeSwipeStartAction: LibraryPreferences.EpisodeSwipeAction,
|
||||
|
@ -161,6 +162,7 @@ fun AnimeScreen(
|
|||
AnimeScreenSmallImpl(
|
||||
state = state,
|
||||
snackbarHostState = snackbarHostState,
|
||||
dateRelativeTime = dateRelativeTime,
|
||||
dateFormat = dateFormat,
|
||||
fetchInterval = fetchInterval,
|
||||
episodeSwipeStartAction = episodeSwipeStartAction,
|
||||
|
@ -201,6 +203,7 @@ fun AnimeScreen(
|
|||
AnimeScreenLargeImpl(
|
||||
state = state,
|
||||
snackbarHostState = snackbarHostState,
|
||||
dateRelativeTime = dateRelativeTime,
|
||||
episodeSwipeStartAction = episodeSwipeStartAction,
|
||||
episodeSwipeEndAction = episodeSwipeEndAction,
|
||||
showNextEpisodeAirTime = showNextEpisodeAirTime,
|
||||
|
@ -245,6 +248,7 @@ fun AnimeScreen(
|
|||
private fun AnimeScreenSmallImpl(
|
||||
state: AnimeScreenModel.State.Success,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
dateRelativeTime: Boolean,
|
||||
dateFormat: DateFormat,
|
||||
fetchInterval: Int?,
|
||||
episodeSwipeStartAction: LibraryPreferences.EpisodeSwipeAction,
|
||||
|
@ -316,11 +320,9 @@ private fun AnimeScreenSmallImpl(
|
|||
}
|
||||
val animatedTitleAlpha by animateFloatAsState(
|
||||
if (firstVisibleItemIndex > 0) 1f else 0f,
|
||||
label = "titleAlpha",
|
||||
)
|
||||
val animatedBgAlpha by animateFloatAsState(
|
||||
if (firstVisibleItemIndex > 0 || firstVisibleItemScrollOffset > 0) 1f else 0f,
|
||||
label = "bgAlpha",
|
||||
)
|
||||
EntryToolbar(
|
||||
title = state.anime.title,
|
||||
|
@ -497,6 +499,7 @@ private fun AnimeScreenSmallImpl(
|
|||
sharedEpisodeItems(
|
||||
anime = state.anime,
|
||||
episodes = episodes,
|
||||
dateRelativeTime = dateRelativeTime,
|
||||
dateFormat = dateFormat,
|
||||
episodeSwipeStartAction = episodeSwipeStartAction,
|
||||
episodeSwipeEndAction = episodeSwipeEndAction,
|
||||
|
@ -516,6 +519,7 @@ private fun AnimeScreenSmallImpl(
|
|||
fun AnimeScreenLargeImpl(
|
||||
state: AnimeScreenModel.State.Success,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
dateRelativeTime: Boolean,
|
||||
dateFormat: DateFormat,
|
||||
fetchInterval: Int?,
|
||||
episodeSwipeStartAction: LibraryPreferences.EpisodeSwipeAction,
|
||||
|
@ -762,6 +766,7 @@ fun AnimeScreenLargeImpl(
|
|||
sharedEpisodeItems(
|
||||
anime = state.anime,
|
||||
episodes = episodes,
|
||||
dateRelativeTime = dateRelativeTime,
|
||||
dateFormat = dateFormat,
|
||||
episodeSwipeStartAction = episodeSwipeStartAction,
|
||||
episodeSwipeEndAction = episodeSwipeEndAction,
|
||||
|
@ -832,6 +837,7 @@ private fun SharedAnimeBottomActionMenu(
|
|||
private fun LazyListScope.sharedEpisodeItems(
|
||||
anime: Anime,
|
||||
episodes: List<EpisodeItem>,
|
||||
dateRelativeTime: Boolean,
|
||||
dateFormat: DateFormat,
|
||||
episodeSwipeStartAction: LibraryPreferences.EpisodeSwipeAction,
|
||||
episodeSwipeEndAction: LibraryPreferences.EpisodeSwipeAction,
|
||||
|
@ -860,7 +866,11 @@ private fun LazyListScope.sharedEpisodeItems(
|
|||
date = episodeItem.episode.dateUpload
|
||||
.takeIf { it > 0L }
|
||||
?.let {
|
||||
Date(it).toRelativeString(context, dateFormat)
|
||||
Date(it).toRelativeString(
|
||||
context,
|
||||
dateRelativeTime,
|
||||
dateFormat,
|
||||
)
|
||||
},
|
||||
watchProgress = episodeItem.episode.lastSecondSeen
|
||||
.takeIf { !episodeItem.episode.seen && it > 0L }
|
||||
|
|
|
@ -91,6 +91,7 @@ fun MangaScreen(
|
|||
state: MangaScreenModel.State.Success,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
fetchInterval: Int?,
|
||||
dateRelativeTime: Boolean,
|
||||
dateFormat: DateFormat,
|
||||
isTabletUi: Boolean,
|
||||
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
|
||||
|
@ -151,6 +152,7 @@ fun MangaScreen(
|
|||
MangaScreenSmallImpl(
|
||||
state = state,
|
||||
snackbarHostState = snackbarHostState,
|
||||
dateRelativeTime = dateRelativeTime,
|
||||
dateFormat = dateFormat,
|
||||
fetchInterval = fetchInterval,
|
||||
chapterSwipeStartAction = chapterSwipeStartAction,
|
||||
|
@ -188,6 +190,7 @@ fun MangaScreen(
|
|||
MangaScreenLargeImpl(
|
||||
state = state,
|
||||
snackbarHostState = snackbarHostState,
|
||||
dateRelativeTime = dateRelativeTime,
|
||||
chapterSwipeStartAction = chapterSwipeStartAction,
|
||||
chapterSwipeEndAction = chapterSwipeEndAction,
|
||||
dateFormat = dateFormat,
|
||||
|
@ -228,6 +231,7 @@ fun MangaScreen(
|
|||
private fun MangaScreenSmallImpl(
|
||||
state: MangaScreenModel.State.Success,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
dateRelativeTime: Boolean,
|
||||
dateFormat: DateFormat,
|
||||
fetchInterval: Int?,
|
||||
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
|
||||
|
@ -297,11 +301,9 @@ private fun MangaScreenSmallImpl(
|
|||
}
|
||||
val animatedTitleAlpha by animateFloatAsState(
|
||||
if (firstVisibleItemIndex > 0) 1f else 0f,
|
||||
label = "titleAlpha",
|
||||
)
|
||||
val animatedBgAlpha by animateFloatAsState(
|
||||
if (firstVisibleItemIndex > 0 || firstVisibleItemScrollOffset > 0) 1f else 0f,
|
||||
label = "bgAlpha",
|
||||
)
|
||||
EntryToolbar(
|
||||
title = state.manga.title,
|
||||
|
@ -446,6 +448,7 @@ private fun MangaScreenSmallImpl(
|
|||
sharedChapterItems(
|
||||
manga = state.manga,
|
||||
chapters = chapters,
|
||||
dateRelativeTime = dateRelativeTime,
|
||||
dateFormat = dateFormat,
|
||||
chapterSwipeStartAction = chapterSwipeStartAction,
|
||||
chapterSwipeEndAction = chapterSwipeEndAction,
|
||||
|
@ -464,6 +467,7 @@ private fun MangaScreenSmallImpl(
|
|||
fun MangaScreenLargeImpl(
|
||||
state: MangaScreenModel.State.Success,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
dateRelativeTime: Boolean,
|
||||
dateFormat: DateFormat,
|
||||
fetchInterval: Int?,
|
||||
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
|
||||
|
@ -679,6 +683,7 @@ fun MangaScreenLargeImpl(
|
|||
sharedChapterItems(
|
||||
manga = state.manga,
|
||||
chapters = chapters,
|
||||
dateRelativeTime = dateRelativeTime,
|
||||
dateFormat = dateFormat,
|
||||
chapterSwipeStartAction = chapterSwipeStartAction,
|
||||
chapterSwipeEndAction = chapterSwipeEndAction,
|
||||
|
@ -741,6 +746,7 @@ private fun SharedMangaBottomActionMenu(
|
|||
private fun LazyListScope.sharedChapterItems(
|
||||
manga: Manga,
|
||||
chapters: List<ChapterItem>,
|
||||
dateRelativeTime: Boolean,
|
||||
dateFormat: DateFormat,
|
||||
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
|
||||
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
|
||||
|
@ -769,7 +775,11 @@ private fun LazyListScope.sharedChapterItems(
|
|||
date = chapterItem.chapter.dateUpload
|
||||
.takeIf { it > 0L }
|
||||
?.let {
|
||||
Date(it).toRelativeString(context, dateFormat)
|
||||
Date(it).toRelativeString(
|
||||
context,
|
||||
dateRelativeTime,
|
||||
dateFormat,
|
||||
)
|
||||
},
|
||||
readProgress = chapterItem.chapter.lastPageRead
|
||||
.takeIf { !chapterItem.chapter.read && it > 0L }
|
||||
|
|
|
@ -13,7 +13,6 @@ import tachiyomi.domain.history.anime.model.AnimeHistoryWithRelations
|
|||
import tachiyomi.presentation.core.components.FastScrollLazyColumn
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.text.DateFormat
|
||||
|
||||
@Composable
|
||||
fun AnimeHistoryContent(
|
||||
|
@ -24,7 +23,8 @@ fun AnimeHistoryContent(
|
|||
onClickDelete: (AnimeHistoryWithRelations) -> Unit,
|
||||
preferences: UiPreferences = Injekt.get(),
|
||||
) {
|
||||
val dateFormat: DateFormat = remember { UiPreferences.dateFormat(preferences.dateFormat().get()) }
|
||||
val relativeTime = remember { preferences.relativeTime().get() }
|
||||
val dateFormat = remember { UiPreferences.dateFormat(preferences.dateFormat().get()) }
|
||||
|
||||
FastScrollLazyColumn(
|
||||
contentPadding = contentPadding,
|
||||
|
@ -44,6 +44,7 @@ fun AnimeHistoryContent(
|
|||
RelativeDateHeader(
|
||||
modifier = Modifier.animateItemPlacement(),
|
||||
date = item.date,
|
||||
relativeTime = relativeTime,
|
||||
dateFormat = dateFormat,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -11,7 +11,6 @@ import tachiyomi.domain.history.manga.model.MangaHistoryWithRelations
|
|||
import tachiyomi.presentation.core.components.FastScrollLazyColumn
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.text.DateFormat
|
||||
|
||||
@Composable
|
||||
fun MangaHistoryContent(
|
||||
|
@ -22,7 +21,8 @@ fun MangaHistoryContent(
|
|||
onClickDelete: (MangaHistoryWithRelations) -> Unit,
|
||||
preferences: UiPreferences = Injekt.get(),
|
||||
) {
|
||||
val dateFormat: DateFormat = remember { UiPreferences.dateFormat(preferences.dateFormat().get()) }
|
||||
val relativeTime = remember { preferences.relativeTime().get() }
|
||||
val dateFormat = remember { UiPreferences.dateFormat(preferences.dateFormat().get()) }
|
||||
|
||||
FastScrollLazyColumn(
|
||||
contentPadding = contentPadding,
|
||||
|
@ -42,6 +42,7 @@ fun MangaHistoryContent(
|
|||
RelativeDateHeader(
|
||||
modifier = Modifier.animateItemPlacement(),
|
||||
date = item.date,
|
||||
relativeTime = relativeTime,
|
||||
dateFormat = dateFormat,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -72,7 +72,7 @@ fun MoreScreen(
|
|||
textRes = R.string.fdroid_warning,
|
||||
modifier = Modifier.clickable {
|
||||
uriHandler.openUri(
|
||||
"https://aniyomi.org/help/faq/#how-do-i-migrate-from-the-f-droid-version",
|
||||
"https://aniyomi.org/docs/faq/general#how-do-i-update-from-the-f-droid-builds",
|
||||
)
|
||||
},
|
||||
)
|
||||
|
|
|
@ -129,6 +129,11 @@ object SettingsAppearanceScreen : SearchableSettings {
|
|||
}
|
||||
val now = remember { Date().time }
|
||||
|
||||
val dateFormat by uiPreferences.dateFormat().collectAsState()
|
||||
val formattedNow = remember(dateFormat) {
|
||||
UiPreferences.dateFormat(dateFormat).format(now)
|
||||
}
|
||||
|
||||
LaunchedEffect(currentLanguage) {
|
||||
val locale = if (currentLanguage.isEmpty()) {
|
||||
LocaleListCompat.getEmptyLocaleList()
|
||||
|
@ -199,6 +204,15 @@ object SettingsAppearanceScreen : SearchableSettings {
|
|||
"${it.ifEmpty { stringResource(R.string.label_default) }} ($formattedDate)"
|
||||
},
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
pref = uiPreferences.relativeTime(),
|
||||
title = stringResource(R.string.pref_relative_format),
|
||||
subtitle = stringResource(
|
||||
R.string.pref_relative_format_summary,
|
||||
stringResource(R.string.relative_time_today),
|
||||
formattedNow,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -149,7 +149,7 @@ object AboutScreen : Screen() {
|
|||
title = stringResource(R.string.help_translate),
|
||||
onPreferenceClick = {
|
||||
uriHandler.openUri(
|
||||
"https://aniyomi.org/help/contribution/#translation",
|
||||
"https://aniyomi.org/docs/contribute#translation",
|
||||
)
|
||||
},
|
||||
)
|
||||
|
@ -165,7 +165,7 @@ object AboutScreen : Screen() {
|
|||
item {
|
||||
TextPreferenceWidget(
|
||||
title = stringResource(R.string.privacy_policy),
|
||||
onPreferenceClick = { uriHandler.openUri("https://aniyomi.org/privacy") },
|
||||
onPreferenceClick = { uriHandler.openUri("https://aniyomi.org/privacy/") },
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -40,9 +40,9 @@ class OpenSourceLicensesScreen : Screen() {
|
|||
),
|
||||
onLibraryClick = {
|
||||
val libraryLicenseScreen = OpenSourceLibraryLicenseScreen(
|
||||
name = it.name,
|
||||
website = it.website,
|
||||
license = it.licenses.firstOrNull()?.htmlReadyLicenseContent.orEmpty(),
|
||||
name = it.library.name,
|
||||
website = it.library.website,
|
||||
license = it.library.licenses.firstOrNull()?.htmlReadyLicenseContent.orEmpty(),
|
||||
)
|
||||
navigator.push(libraryLicenseScreen)
|
||||
},
|
||||
|
|
|
@ -1,25 +1,25 @@
|
|||
package eu.kanade.presentation.reader
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.FilterChip
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.foundation.lazy.grid.items
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.kanade.domain.entries.manga.model.orientationType
|
||||
import eu.kanade.presentation.components.AdaptiveSheet
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel
|
||||
import tachiyomi.presentation.core.components.SettingsChipRow
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
import tachiyomi.presentation.core.components.SettingsIconGrid
|
||||
import tachiyomi.presentation.core.components.material.IconToggleButton
|
||||
|
||||
private val orientationTypeOptions = OrientationType.entries.map { it.stringRes to it }
|
||||
|
||||
|
@ -36,22 +36,20 @@ fun OrientationModeSelectDialog(
|
|||
)
|
||||
}
|
||||
|
||||
AdaptiveSheet(
|
||||
onDismissRequest = onDismissRequest,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(vertical = 16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
|
||||
) {
|
||||
SettingsChipRow(R.string.rotation_type) {
|
||||
orientationTypeOptions.map { (stringRes, it) ->
|
||||
FilterChip(
|
||||
selected = it == orientationType,
|
||||
onClick = {
|
||||
screenModel.onChangeOrientation(it)
|
||||
AdaptiveSheet(onDismissRequest = onDismissRequest) {
|
||||
Box(modifier = Modifier.padding(vertical = 16.dp)) {
|
||||
SettingsIconGrid(R.string.rotation_type) {
|
||||
items(orientationTypeOptions) { (stringRes, mode) ->
|
||||
IconToggleButton(
|
||||
checked = mode == orientationType,
|
||||
onCheckedChange = {
|
||||
screenModel.onChangeOrientation(mode)
|
||||
onChange(stringRes)
|
||||
onDismissRequest()
|
||||
},
|
||||
label = { Text(stringResource(stringRes)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
imageVector = ImageVector.vectorResource(mode.iconRes),
|
||||
title = stringResource(stringRes),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,24 +1,25 @@
|
|||
package eu.kanade.presentation.reader
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.FilterChip
|
||||
import androidx.compose.foundation.lazy.grid.items
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import eu.kanade.domain.entries.manga.model.readingModeType
|
||||
import eu.kanade.presentation.components.AdaptiveSheet
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
|
||||
import tachiyomi.presentation.core.components.SettingsChipRow
|
||||
import tachiyomi.presentation.core.components.SettingsIconGrid
|
||||
import tachiyomi.presentation.core.components.material.IconToggleButton
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
|
||||
private val readingModeOptions = ReadingModeType.entries.map { it.stringRes to it }
|
||||
|
@ -36,22 +37,20 @@ fun ReadingModeSelectDialog(
|
|||
)
|
||||
}
|
||||
|
||||
AdaptiveSheet(
|
||||
onDismissRequest = onDismissRequest,
|
||||
) {
|
||||
Row(
|
||||
modifier = Modifier.padding(vertical = 16.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
|
||||
) {
|
||||
SettingsChipRow(R.string.pref_category_reading_mode) {
|
||||
readingModeOptions.map { (stringRes, it) ->
|
||||
FilterChip(
|
||||
selected = it == readingMode,
|
||||
onClick = {
|
||||
screenModel.onChangeReadingMode(it)
|
||||
AdaptiveSheet(onDismissRequest = onDismissRequest) {
|
||||
Box(modifier = Modifier.padding(vertical = MaterialTheme.padding.medium)) {
|
||||
SettingsIconGrid(R.string.pref_category_reading_mode) {
|
||||
items(readingModeOptions) { (stringRes, mode) ->
|
||||
IconToggleButton(
|
||||
checked = mode == readingMode,
|
||||
onCheckedChange = {
|
||||
screenModel.onChangeReadingMode(mode)
|
||||
onChange(stringRes)
|
||||
onDismissRequest()
|
||||
},
|
||||
label = { Text(stringResource(stringRes)) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
imageVector = ImageVector.vectorResource(mode.iconRes),
|
||||
title = stringResource(stringRes),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -229,6 +229,7 @@ fun SearchResultItem(
|
|||
val borderColor = if (selected) MaterialTheme.colorScheme.outline else Color.Transparent
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 12.dp)
|
||||
.clip(shape)
|
||||
.background(MaterialTheme.colorScheme.surface)
|
||||
|
|
|
@ -38,6 +38,7 @@ import kotlin.time.Duration.Companion.seconds
|
|||
fun AnimeUpdateScreen(
|
||||
state: AnimeUpdatesScreenModel.State,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
relativeTime: Boolean,
|
||||
contentPadding: PaddingValues,
|
||||
lastUpdated: Long,
|
||||
onClickCover: (AnimeUpdatesItem) -> Unit,
|
||||
|
@ -100,7 +101,7 @@ fun AnimeUpdateScreen(
|
|||
animeUpdatesLastUpdatedItem(lastUpdated)
|
||||
}
|
||||
animeUpdatesUiItems(
|
||||
uiModels = state.getUiModel(context),
|
||||
uiModels = state.getUiModel(context, relativeTime),
|
||||
selectionMode = state.selectionMode,
|
||||
onUpdateSelected = onUpdateSelected,
|
||||
onClickCover = onClickCover,
|
||||
|
|
|
@ -35,6 +35,7 @@ import kotlin.time.Duration.Companion.seconds
|
|||
fun MangaUpdateScreen(
|
||||
state: MangaUpdatesScreenModel.State,
|
||||
snackbarHostState: SnackbarHostState,
|
||||
relativeTime: Boolean,
|
||||
contentPadding: PaddingValues,
|
||||
lastUpdated: Long,
|
||||
onClickCover: (MangaUpdatesItem) -> Unit,
|
||||
|
@ -97,7 +98,7 @@ fun MangaUpdateScreen(
|
|||
}
|
||||
|
||||
mangaUpdatesUiItems(
|
||||
uiModels = state.getUiModel(context),
|
||||
uiModels = state.getUiModel(context, relativeTime),
|
||||
selectionMode = state.selectionMode,
|
||||
onUpdateSelected = onUpdateSelected,
|
||||
onClickCover = onClickCover,
|
||||
|
|
|
@ -176,7 +176,7 @@ fun WebViewScreenContent(
|
|||
textRes = R.string.information_cloudflare_help,
|
||||
modifier = Modifier.clickable {
|
||||
uriHandler.openUri(
|
||||
"https://aniyomi.org/docs/guides/troubleshooting/#solving-cloudflare-issues",
|
||||
"https://aniyomi.org/docs/guides/troubleshooting/#cloudflare",
|
||||
)
|
||||
},
|
||||
)
|
||||
|
|
|
@ -505,6 +505,12 @@ object Migrations {
|
|||
pref.getAndSet { it - "battery_not_low" }
|
||||
}
|
||||
}
|
||||
if (oldVersion < 106) {
|
||||
val pref = preferenceStore.getInt("relative_time", 7)
|
||||
if (pref.get() == 0) {
|
||||
uiPreferences.relativeTime().set(false)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,9 +30,9 @@ import eu.kanade.tachiyomi.util.system.createFileInCacheDir
|
|||
import kotlinx.coroutines.coroutineScope
|
||||
import kotlinx.coroutines.isActive
|
||||
import tachiyomi.core.util.system.logcat
|
||||
import tachiyomi.domain.entries.anime.interactor.SetAnimeFetchInterval
|
||||
import tachiyomi.domain.entries.anime.interactor.AnimeFetchInterval
|
||||
import tachiyomi.domain.entries.anime.model.Anime
|
||||
import tachiyomi.domain.entries.manga.interactor.SetMangaFetchInterval
|
||||
import tachiyomi.domain.entries.manga.interactor.MangaFetchInterval
|
||||
import tachiyomi.domain.entries.manga.model.Manga
|
||||
import tachiyomi.domain.items.chapter.model.Chapter
|
||||
import tachiyomi.domain.items.chapter.repository.ChapterRepository
|
||||
|
@ -54,15 +54,15 @@ class BackupRestorer(
|
|||
) {
|
||||
private val updateManga: UpdateManga = Injekt.get()
|
||||
private val chapterRepository: ChapterRepository = Injekt.get()
|
||||
private val setMangaFetchInterval: SetMangaFetchInterval = Injekt.get()
|
||||
private val mangaFetchInterval: MangaFetchInterval = Injekt.get()
|
||||
|
||||
private val updateAnime: UpdateAnime = Injekt.get()
|
||||
private val episodeRepository: EpisodeRepository = Injekt.get()
|
||||
private val setAnimeFetchInterval: SetAnimeFetchInterval = Injekt.get()
|
||||
private val animeFetchInterval: AnimeFetchInterval = Injekt.get()
|
||||
|
||||
private var now = ZonedDateTime.now()
|
||||
private var currentMangaFetchWindow = setMangaFetchInterval.getWindow(now)
|
||||
private var currentAnimeFetchWindow = setAnimeFetchInterval.getWindow(now)
|
||||
private var currentMangaFetchWindow = mangaFetchInterval.getWindow(now)
|
||||
private var currentAnimeFetchWindow = animeFetchInterval.getWindow(now)
|
||||
|
||||
private var backupManager = BackupManager(context)
|
||||
|
||||
|
@ -152,8 +152,8 @@ class BackupRestorer(
|
|||
animeSourceMapping = backupAnimeMaps.associate { it.sourceId to it.name }
|
||||
|
||||
now = ZonedDateTime.now()
|
||||
currentMangaFetchWindow = setMangaFetchInterval.getWindow(now)
|
||||
currentAnimeFetchWindow = setAnimeFetchInterval.getWindow(now)
|
||||
currentMangaFetchWindow = mangaFetchInterval.getWindow(now)
|
||||
currentAnimeFetchWindow = animeFetchInterval.getWindow(now)
|
||||
|
||||
return coroutineScope {
|
||||
// Restore individual manga
|
||||
|
|
|
@ -674,7 +674,7 @@ class AnimeDownloader(
|
|||
if (isHls(video) || isMpd(video)) {
|
||||
return ffmpegDownload(video, download, tmpDir, filename)
|
||||
} else {
|
||||
val response = download.source.fetchVideo(video)
|
||||
val response = download.source.getVideo(video)
|
||||
val file = tmpDir.findFile("$filename.tmp") ?: tmpDir.createFile("$filename.tmp")
|
||||
|
||||
// Write to file with pause/resume capability
|
||||
|
|
|
@ -46,7 +46,6 @@ import nl.adaptivity.xmlutil.serialization.XML
|
|||
import okhttp3.Response
|
||||
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
|
||||
|
@ -396,7 +395,7 @@ class MangaDownloader(
|
|||
if (page.imageUrl.isNullOrEmpty()) {
|
||||
page.status = Page.State.LOAD_PAGE
|
||||
try {
|
||||
page.imageUrl = download.source.fetchImageUrl(page).awaitSingle()
|
||||
page.imageUrl = download.source.getImageUrl(page)
|
||||
} catch (e: Throwable) {
|
||||
page.status = Page.State.ERROR
|
||||
}
|
||||
|
|
|
@ -48,9 +48,9 @@ import tachiyomi.core.util.system.logcat
|
|||
import tachiyomi.domain.category.anime.interactor.GetAnimeCategories
|
||||
import tachiyomi.domain.category.model.Category
|
||||
import tachiyomi.domain.download.service.DownloadPreferences
|
||||
import tachiyomi.domain.entries.anime.interactor.AnimeFetchInterval
|
||||
import tachiyomi.domain.entries.anime.interactor.GetAnime
|
||||
import tachiyomi.domain.entries.anime.interactor.GetLibraryAnime
|
||||
import tachiyomi.domain.entries.anime.interactor.SetAnimeFetchInterval
|
||||
import tachiyomi.domain.entries.anime.model.Anime
|
||||
import tachiyomi.domain.entries.anime.model.toAnimeUpdate
|
||||
import tachiyomi.domain.items.episode.model.Episode
|
||||
|
@ -90,7 +90,7 @@ class AnimeLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
|
|||
private val getCategories: GetAnimeCategories = Injekt.get()
|
||||
private val syncEpisodesWithSource: SyncEpisodesWithSource = Injekt.get()
|
||||
private val refreshAnimeTracks: RefreshAnimeTracks = Injekt.get()
|
||||
private val setAnimeFetchInterval: SetAnimeFetchInterval = Injekt.get()
|
||||
private val animeFetchInterval: AnimeFetchInterval = Injekt.get()
|
||||
|
||||
private val notifier = AnimeLibraryUpdateNotifier(context)
|
||||
|
||||
|
@ -216,7 +216,7 @@ class AnimeLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
|
|||
val failedUpdates = CopyOnWriteArrayList<Pair<Anime, String?>>()
|
||||
val hasDownloads = AtomicBoolean(false)
|
||||
val restrictions = libraryPreferences.autoUpdateItemRestrictions().get()
|
||||
val fetchWindow = setAnimeFetchInterval.getWindow(ZonedDateTime.now())
|
||||
val fetchWindow = animeFetchInterval.getWindow(ZonedDateTime.now())
|
||||
|
||||
coroutineScope {
|
||||
animeToUpdate.groupBy { it.anime.source }.values
|
||||
|
@ -537,7 +537,7 @@ class AnimeLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
|
|||
private const val WORK_NAME_AUTO = "AnimeLibraryUpdate-auto"
|
||||
private const val WORK_NAME_MANUAL = "AnimeLibraryUpdate-manual"
|
||||
|
||||
private const val ERROR_LOG_HELP_URL = "https://aniyomi.org/help/guides/troubleshooting"
|
||||
private const val ERROR_LOG_HELP_URL = "https://aniyomi.org/docs/guides/troubleshooting/"
|
||||
|
||||
private const val ANIME_PER_SOURCE_QUEUE_WARNING_THRESHOLD = 60
|
||||
|
||||
|
|
|
@ -377,7 +377,7 @@ class AnimeLibraryUpdateNotifier(private val context: Context) {
|
|||
|
||||
companion object {
|
||||
// TODO: Change when implemented on Aniyomi website
|
||||
const val HELP_WARNING_URL = "https://aniyomi.org/help/faq/#why-does-the-app-warn-about-large-bulk-updates-and-downloads"
|
||||
const val HELP_WARNING_URL = "https://aniyomi.org/docs/faq/library#why-am-i-warned-about-large-bulk-updates-and-downloads"
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -386,4 +386,4 @@ private const val NOTIF_ANIME_TITLE_MAX_LEN = 45
|
|||
private const val NOTIF_ANIME_ICON_SIZE = 192
|
||||
|
||||
// TODO: Change when implemented on Aniyomi website
|
||||
private const val HELP_SKIPPED_ANIME_URL = "https://aniyomi.org/help/faq/#why-does-global-update-skip-some-entries"
|
||||
private const val HELP_SKIPPED_ANIME_URL = "https://aniyomi.org/docs/faq/library#why-is-global-update-skipping-entries"
|
||||
|
|
|
@ -50,7 +50,7 @@ import tachiyomi.domain.category.model.Category
|
|||
import tachiyomi.domain.download.service.DownloadPreferences
|
||||
import tachiyomi.domain.entries.manga.interactor.GetLibraryManga
|
||||
import tachiyomi.domain.entries.manga.interactor.GetManga
|
||||
import tachiyomi.domain.entries.manga.interactor.SetMangaFetchInterval
|
||||
import tachiyomi.domain.entries.manga.interactor.MangaFetchInterval
|
||||
import tachiyomi.domain.entries.manga.model.Manga
|
||||
import tachiyomi.domain.entries.manga.model.toMangaUpdate
|
||||
import tachiyomi.domain.items.chapter.model.Chapter
|
||||
|
@ -90,7 +90,7 @@ class MangaLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
|
|||
private val getCategories: GetMangaCategories = Injekt.get()
|
||||
private val syncChaptersWithSource: SyncChaptersWithSource = Injekt.get()
|
||||
private val refreshMangaTracks: RefreshMangaTracks = Injekt.get()
|
||||
private val setMangaFetchInterval: SetMangaFetchInterval = Injekt.get()
|
||||
private val mangaFetchInterval: MangaFetchInterval = Injekt.get()
|
||||
|
||||
private val notifier = MangaLibraryUpdateNotifier(context)
|
||||
|
||||
|
@ -216,7 +216,7 @@ class MangaLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
|
|||
val failedUpdates = CopyOnWriteArrayList<Pair<Manga, String?>>()
|
||||
val hasDownloads = AtomicBoolean(false)
|
||||
val restrictions = libraryPreferences.autoUpdateItemRestrictions().get()
|
||||
val fetchWindow = setMangaFetchInterval.getWindow(ZonedDateTime.now())
|
||||
val fetchWindow = mangaFetchInterval.getWindow(ZonedDateTime.now())
|
||||
|
||||
coroutineScope {
|
||||
mangaToUpdate.groupBy { it.manga.source }.values
|
||||
|
|
|
@ -380,7 +380,7 @@ class MangaLibraryUpdateNotifier(private val context: Context) {
|
|||
|
||||
companion object {
|
||||
// TODO: Change when implemented on Aniyomi website
|
||||
const val HELP_WARNING_URL = "https://aniyomi.org/help/faq/#why-does-the-app-warn-about-large-bulk-updates-and-downloads"
|
||||
const val HELP_WARNING_URL = "https://aniyomi.org/docs/faq/library#why-am-i-warned-about-large-bulk-updates-and-downloads"
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -389,4 +389,4 @@ private const val NOTIF_MANGA_TITLE_MAX_LEN = 45
|
|||
private const val NOTIF_MANGA_ICON_SIZE = 192
|
||||
|
||||
// TODO: Change when implemented on Aniyomi website
|
||||
private const val HELP_SKIPPED_MANGA_URL = "https://aniyomi.org/help/faq/#why-does-global-update-skip-some-entries"
|
||||
private const val HELP_SKIPPED_MANGA_URL = "https://aniyomi.org/docs/faq/library#why-is-global-update-skipping-entries"
|
||||
|
|
|
@ -32,28 +32,56 @@ class ImageSaver(
|
|||
fun save(image: Image): Uri {
|
||||
val data = image.data
|
||||
|
||||
val type = ImageUtil.findImageType(data) ?: throw Exception("Not an image")
|
||||
val type = ImageUtil.findImageType(data) ?: throw IllegalArgumentException("Not an image")
|
||||
val filename = DiskUtil.buildValidFilename("${image.name}.${type.extension}")
|
||||
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q || image.location !is Location.Pictures) {
|
||||
return save(data(), image.location.directory(context), filename)
|
||||
}
|
||||
|
||||
return saveApi29(image, type, filename, data)
|
||||
}
|
||||
|
||||
private fun save(inputStream: InputStream, directory: File, filename: String): Uri {
|
||||
directory.mkdirs()
|
||||
|
||||
val destFile = File(directory, filename)
|
||||
|
||||
inputStream.use { input ->
|
||||
destFile.outputStream().use { output ->
|
||||
input.copyTo(output)
|
||||
}
|
||||
}
|
||||
|
||||
DiskUtil.scanMedia(context, destFile.toUri())
|
||||
|
||||
return destFile.getUriCompat(context)
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.Q)
|
||||
private fun saveApi29(
|
||||
image: Image,
|
||||
type: ImageUtil.ImageType,
|
||||
filename: String,
|
||||
data: () -> InputStream,
|
||||
): Uri {
|
||||
val pictureDir =
|
||||
MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
|
||||
|
||||
val folderRelativePath = "${Environment.DIRECTORY_PICTURES}/${context.getString(
|
||||
R.string.app_name,
|
||||
)}/"
|
||||
val imageLocation = (image.location as Location.Pictures).relativePath
|
||||
val relativePath = listOf(
|
||||
Environment.DIRECTORY_PICTURES,
|
||||
context.getString(R.string.app_name),
|
||||
imageLocation,
|
||||
).joinToString(File.separator)
|
||||
|
||||
val contentValues = contentValuesOf(
|
||||
MediaStore.Images.Media.RELATIVE_PATH to relativePath,
|
||||
MediaStore.Images.Media.DISPLAY_NAME to image.name,
|
||||
MediaStore.Images.Media.MIME_TYPE to type.mime,
|
||||
MediaStore.Images.Media.RELATIVE_PATH to folderRelativePath + imageLocation,
|
||||
)
|
||||
|
||||
val picture = findUriOrDefault(folderRelativePath, "$imageLocation$filename") {
|
||||
val picture = findUriOrDefault(relativePath, filename) {
|
||||
context.contentResolver.insert(
|
||||
pictureDir,
|
||||
contentValues,
|
||||
|
@ -76,24 +104,8 @@ class ImageSaver(
|
|||
return picture
|
||||
}
|
||||
|
||||
private fun save(inputStream: InputStream, directory: File, filename: String): Uri {
|
||||
directory.mkdirs()
|
||||
|
||||
val destFile = File(directory, filename)
|
||||
|
||||
inputStream.use { input ->
|
||||
destFile.outputStream().use { output ->
|
||||
input.copyTo(output)
|
||||
}
|
||||
}
|
||||
|
||||
DiskUtil.scanMedia(context, destFile.toUri())
|
||||
|
||||
return destFile.getUriCompat(context)
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.Q)
|
||||
private fun findUriOrDefault(relativePath: String, imagePath: String, default: () -> Uri): Uri {
|
||||
private fun findUriOrDefault(path: String, filename: String, default: () -> Uri): Uri {
|
||||
val projection = arrayOf(
|
||||
MediaStore.MediaColumns._ID,
|
||||
MediaStore.MediaColumns.DISPLAY_NAME,
|
||||
|
@ -104,19 +116,19 @@ class ImageSaver(
|
|||
|
||||
val selection = "${MediaStore.MediaColumns.RELATIVE_PATH}=? AND ${MediaStore.MediaColumns.DISPLAY_NAME}=?"
|
||||
|
||||
// Need to make sure it ends with the separator
|
||||
val normalizedPath = "${path.removeSuffix(File.separator)}${File.separator}"
|
||||
|
||||
context.contentResolver.query(
|
||||
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
|
||||
projection,
|
||||
selection,
|
||||
arrayOf(relativePath, imagePath),
|
||||
arrayOf(normalizedPath, filename),
|
||||
null,
|
||||
).use { cursor ->
|
||||
if (cursor != null && cursor.count >= 1) {
|
||||
cursor.moveToFirst().let {
|
||||
val id = cursor.getLong(
|
||||
cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID),
|
||||
)
|
||||
|
||||
if (cursor.moveToFirst()) {
|
||||
val id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID))
|
||||
return ContentUris.withAppendedId(
|
||||
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
|
||||
id,
|
||||
|
@ -124,6 +136,7 @@ class ImageSaver(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
return default()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -161,7 +161,7 @@ internal class AppUpdateNotifier(private val context: Context) {
|
|||
setContentIntent(
|
||||
NotificationHandler.openUrl(
|
||||
context,
|
||||
"https://aniyomi.org/help/faq/#how-do-i-migrate-from-the-f-droid-version",
|
||||
"https://aniyomi.org/docs/faq/general#how-do-i-update-from-the-f-droid-builds",
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
@ -105,7 +105,7 @@ class AnimeExtensionDetailsScreenModel(
|
|||
val extension = state.value.extension ?: return ""
|
||||
|
||||
if (!extension.hasReadme) {
|
||||
return "https://aniyomi.org/help/faq/#extensions"
|
||||
return "https://aniyomi.org/docs/faq/browse/extensions"
|
||||
}
|
||||
|
||||
val pkgName = extension.pkgName.substringAfter("eu.kanade.tachiyomi.animeextension.")
|
||||
|
|
|
@ -31,10 +31,8 @@ import eu.kanade.tachiyomi.util.removeCovers
|
|||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
import kotlinx.coroutines.flow.update
|
||||
|
@ -115,25 +113,20 @@ class BrowseAnimeSourceScreenModel(
|
|||
/**
|
||||
* Flow of Pager flow tied to [State.listing]
|
||||
*/
|
||||
private val hideInLibraryItems = sourcePreferences.hideInAnimeLibraryItems().get()
|
||||
val animePagerFlowFlow = state.map { it.listing }
|
||||
.distinctUntilChanged()
|
||||
.map { listing ->
|
||||
Pager(
|
||||
PagingConfig(pageSize = 25),
|
||||
) {
|
||||
Pager(PagingConfig(pageSize = 25)) {
|
||||
getRemoteAnime.subscribe(sourceId, listing.query ?: "", listing.filters)
|
||||
}.flow.map { pagingData ->
|
||||
pagingData.map {
|
||||
networkToLocalAnime.await(it.toDomainAnime(sourceId))
|
||||
.let { localAnime ->
|
||||
getAnime.subscribe(localAnime.url, localAnime.source)
|
||||
}
|
||||
.let { localAnime -> getAnime.subscribe(localAnime.url, localAnime.source) }
|
||||
.filterNotNull()
|
||||
.filter { localAnime ->
|
||||
!sourcePreferences.hideInAnimeLibraryItems().get() || !localAnime.favorite
|
||||
}
|
||||
.stateIn(ioCoroutineScope)
|
||||
}
|
||||
.filter { !hideInLibraryItems || !it.value.favorite }
|
||||
}
|
||||
.cachedIn(ioCoroutineScope)
|
||||
}
|
||||
|
@ -301,7 +294,6 @@ class BrowseAnimeSourceScreenModel(
|
|||
track.anime_id = anime.id
|
||||
(service as TrackService).animeService.bind(track)
|
||||
insertTrack.await(track.toDomainTrack()!!)
|
||||
|
||||
syncEpisodeProgressWithTrack.await(
|
||||
anime.id,
|
||||
track.toDomainTrack()!!,
|
||||
|
|
|
@ -105,7 +105,7 @@ class MangaExtensionDetailsScreenModel(
|
|||
val extension = state.value.extension ?: return ""
|
||||
|
||||
if (!extension.hasReadme) {
|
||||
return "https://aniyomi.org/help/faq/#extensions"
|
||||
return "https://aniyomi.org/docs/faq/browse/extensions"
|
||||
}
|
||||
|
||||
val pkgName = extension.pkgName.substringAfter("eu.kanade.tachiyomi.extension.")
|
||||
|
|
|
@ -9,6 +9,7 @@ import androidx.compose.ui.unit.dp
|
|||
import androidx.paging.Pager
|
||||
import androidx.paging.PagingConfig
|
||||
import androidx.paging.cachedIn
|
||||
import androidx.paging.filter
|
||||
import androidx.paging.map
|
||||
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||
import cafe.adriel.voyager.core.model.coroutineScope
|
||||
|
@ -30,7 +31,6 @@ import eu.kanade.tachiyomi.util.removeCovers
|
|||
import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.flow.filterNotNull
|
||||
import kotlinx.coroutines.flow.firstOrNull
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
@ -113,25 +113,20 @@ class BrowseMangaSourceScreenModel(
|
|||
/**
|
||||
* Flow of Pager flow tied to [State.listing]
|
||||
*/
|
||||
private val hideInLibraryItems = sourcePreferences.hideInMangaLibraryItems().get()
|
||||
val mangaPagerFlowFlow = state.map { it.listing }
|
||||
.distinctUntilChanged()
|
||||
.map { listing ->
|
||||
Pager(
|
||||
PagingConfig(pageSize = 25),
|
||||
) {
|
||||
Pager(PagingConfig(pageSize = 25)) {
|
||||
getRemoteManga.subscribe(sourceId, listing.query ?: "", listing.filters)
|
||||
}.flow.map { pagingData ->
|
||||
pagingData.map {
|
||||
networkToLocalManga.await(it.toDomainManga(sourceId))
|
||||
.let { localManga ->
|
||||
getManga.subscribe(localManga.url, localManga.source)
|
||||
}
|
||||
.let { localManga -> getManga.subscribe(localManga.url, localManga.source) }
|
||||
.filterNotNull()
|
||||
.filter { localManga ->
|
||||
!sourcePreferences.hideInMangaLibraryItems().get() || !localManga.favorite
|
||||
}
|
||||
.stateIn(ioCoroutineScope)
|
||||
}
|
||||
.filter { !hideInLibraryItems || !it.value.favorite }
|
||||
}
|
||||
.cachedIn(ioCoroutineScope)
|
||||
}
|
||||
|
@ -301,7 +296,6 @@ class BrowseMangaSourceScreenModel(
|
|||
track.manga_id = manga.id
|
||||
(service as TrackService).mangaService.bind(track)
|
||||
insertTrack.await(track.toDomainTrack()!!)
|
||||
|
||||
syncChapterProgressWithTrack.await(
|
||||
manga.id,
|
||||
track.toDomainTrack()!!,
|
||||
|
|
|
@ -19,7 +19,6 @@ import kotlinx.coroutines.flow.update
|
|||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import tachiyomi.core.util.lang.awaitSingle
|
||||
import tachiyomi.domain.entries.manga.interactor.GetManga
|
||||
import tachiyomi.domain.entries.manga.interactor.NetworkToLocalManga
|
||||
import tachiyomi.domain.entries.manga.model.Manga
|
||||
|
@ -138,7 +137,7 @@ abstract class MangaSearchScreenModel(
|
|||
}
|
||||
try {
|
||||
val page = withContext(coroutineDispatcher) {
|
||||
source.fetchSearchManga(1, query, source.getFilterList()).awaitSingle()
|
||||
source.getSearchManga(1, query, source.getFilterList())
|
||||
}
|
||||
|
||||
val titles = page.mangas.map {
|
||||
|
|
|
@ -78,13 +78,17 @@ class AnimeDownloadQueueScreenModel(
|
|||
}
|
||||
reorder(newAnimeDownloads)
|
||||
}
|
||||
R.id.move_to_top_series -> {
|
||||
R.id.move_to_top_series, R.id.move_to_bottom_series -> {
|
||||
val (selectedSeries, otherSeries) = adapter?.currentItems
|
||||
?.filterIsInstance<AnimeDownloadItem>()
|
||||
?.map(AnimeDownloadItem::download)
|
||||
?.partition { item.download.anime.id == it.anime.id }
|
||||
?: Pair(emptyList(), emptyList())
|
||||
reorder(selectedSeries + otherSeries)
|
||||
if (menuItem.itemId == R.id.move_to_top_series) {
|
||||
reorder(selectedSeries + otherSeries)
|
||||
} else {
|
||||
reorder(otherSeries + selectedSeries)
|
||||
}
|
||||
}
|
||||
R.id.cancel_download -> {
|
||||
cancel(listOf(item.download))
|
||||
|
|
|
@ -84,13 +84,17 @@ class MangaDownloadQueueScreenModel(
|
|||
}
|
||||
reorder(newDownloads)
|
||||
}
|
||||
R.id.move_to_top_series -> {
|
||||
R.id.move_to_top_series, R.id.move_to_bottom_series -> {
|
||||
val (selectedSeries, otherSeries) = adapter?.currentItems
|
||||
?.filterIsInstance<MangaDownloadItem>()
|
||||
?.map(MangaDownloadItem::download)
|
||||
?.partition { item.download.manga.id == it.manga.id }
|
||||
?: Pair(emptyList(), emptyList())
|
||||
reorder(selectedSeries + otherSeries)
|
||||
if (menuItem.itemId == R.id.move_to_top_series) {
|
||||
reorder(selectedSeries + otherSeries)
|
||||
} else {
|
||||
reorder(otherSeries + selectedSeries)
|
||||
}
|
||||
}
|
||||
R.id.cancel_download -> {
|
||||
cancel(listOf(item.download))
|
||||
|
|
|
@ -105,6 +105,7 @@ class AnimeScreen(
|
|||
AnimeScreen(
|
||||
state = successState,
|
||||
snackbarHostState = screenModel.snackbarHostState,
|
||||
dateRelativeTime = screenModel.relativeTime,
|
||||
dateFormat = screenModel.dateFormat,
|
||||
fetchInterval = successState.anime.fetchInterval,
|
||||
isTabletUi = isTabletUi(),
|
||||
|
|
|
@ -8,6 +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.preference.asState
|
||||
import eu.kanade.core.util.addOrRemove
|
||||
import eu.kanade.domain.entries.anime.interactor.SetAnimeViewerFlags
|
||||
import eu.kanade.domain.entries.anime.interactor.UpdateAnime
|
||||
|
@ -132,6 +133,7 @@ class AnimeScreenModel(
|
|||
val alwaysUseExternalPlayer = playerPreferences.alwaysUseExternalPlayer().get()
|
||||
val useExternalDownloader = downloadPreferences.useExternalDownloader().get()
|
||||
|
||||
val relativeTime by uiPreferences.relativeTime().asState(coroutineScope)
|
||||
val dateFormat by mutableStateOf(UiPreferences.dateFormat(uiPreferences.dateFormat().get()))
|
||||
|
||||
val isUpdateIntervalEnabled = LibraryPreferences.ENTRY_OUTSIDE_RELEASE_PERIOD in libraryPreferences.autoUpdateItemRestrictions().get()
|
||||
|
|
|
@ -100,6 +100,7 @@ class MangaScreen(
|
|||
MangaScreen(
|
||||
state = successState,
|
||||
snackbarHostState = screenModel.snackbarHostState,
|
||||
dateRelativeTime = screenModel.relativeTime,
|
||||
dateFormat = screenModel.dateFormat,
|
||||
fetchInterval = successState.manga.fetchInterval,
|
||||
isTabletUi = isTabletUi(),
|
||||
|
|
|
@ -127,6 +127,7 @@ class MangaScreenModel(
|
|||
val chapterSwipeStartAction = libraryPreferences.swipeChapterEndAction().get()
|
||||
val chapterSwipeEndAction = libraryPreferences.swipeChapterStartAction().get()
|
||||
|
||||
val relativeTime by uiPreferences.relativeTime().asState(coroutineScope)
|
||||
val dateFormat by mutableStateOf(UiPreferences.dateFormat(uiPreferences.dateFormat().get()))
|
||||
val skipFiltered by readerPreferences.skipFiltered().asState(coroutineScope)
|
||||
|
||||
|
|
|
@ -198,7 +198,7 @@ object AnimeLibraryTab : Tab() {
|
|||
icon = Icons.Outlined.HelpOutline,
|
||||
onClick = {
|
||||
handler.openUri(
|
||||
"https://aniyomi.org/help/guides/getting-started",
|
||||
"https://aniyomi.org/docs/guides/getting-started",
|
||||
)
|
||||
},
|
||||
),
|
||||
|
|
|
@ -193,7 +193,7 @@ object MangaLibraryTab : Tab() {
|
|||
icon = Icons.Outlined.HelpOutline,
|
||||
onClick = {
|
||||
handler.openUri(
|
||||
"https://aniyomi.org/help/guides/getting-started",
|
||||
"https://aniyomi.org/docs/guides/getting-started",
|
||||
)
|
||||
},
|
||||
),
|
||||
|
|
|
@ -340,7 +340,7 @@ class PlayerActivity : BaseActivity() {
|
|||
displayMode = state.anime!!.displayMode,
|
||||
episodeList = viewModel.currentPlaylist,
|
||||
currentEpisodeIndex = viewModel.getCurrentEpisodeIndex(),
|
||||
relativeTime = viewModel.relativeTime,
|
||||
dateRelativeTime = viewModel.relativeTime,
|
||||
dateFormat = viewModel.dateFormat,
|
||||
onBookmarkClicked = viewModel::bookmarkEpisode,
|
||||
onEpisodeClicked = this::changeEpisode,
|
||||
|
|
|
@ -50,7 +50,7 @@ fun EpisodeListDialog(
|
|||
displayMode: Long,
|
||||
currentEpisodeIndex: Int,
|
||||
episodeList: List<Episode>,
|
||||
relativeTime: Int,
|
||||
dateRelativeTime: Boolean,
|
||||
dateFormat: DateFormat,
|
||||
onBookmarkClicked: (Long?, Boolean) -> Unit,
|
||||
onEpisodeClicked: (Long?) -> Unit,
|
||||
|
@ -92,7 +92,7 @@ fun EpisodeListDialog(
|
|||
val date = episode.date_upload
|
||||
.takeIf { it > 0L }
|
||||
?.let {
|
||||
Date(it).toRelativeString(context, dateFormat)
|
||||
Date(it).toRelativeString(context, dateRelativeTime, dateFormat)
|
||||
} ?: ""
|
||||
|
||||
EpisodeListItem(
|
||||
|
|
|
@ -18,7 +18,6 @@ import kotlinx.coroutines.flow.filter
|
|||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.runInterruptible
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import tachiyomi.core.util.lang.awaitSingle
|
||||
import tachiyomi.core.util.lang.launchIO
|
||||
import tachiyomi.core.util.lang.withIOContext
|
||||
import uy.kohesive.injekt.Injekt
|
||||
|
@ -186,7 +185,7 @@ internal class HttpPageLoader(
|
|||
try {
|
||||
if (page.imageUrl.isNullOrEmpty()) {
|
||||
page.status = Page.State.LOAD_PAGE
|
||||
page.imageUrl = source.fetchImageUrl(page).awaitSingle()
|
||||
page.imageUrl = source.getImageUrl(page)
|
||||
}
|
||||
val imageUrl = page.imageUrl!!
|
||||
|
||||
|
|
|
@ -60,6 +60,7 @@ class AnimeUpdatesScreenModel(
|
|||
private val getEpisode: GetEpisode = Injekt.get(),
|
||||
private val libraryPreferences: LibraryPreferences = Injekt.get(),
|
||||
val snackbarHostState: SnackbarHostState = SnackbarHostState(),
|
||||
uiPreferences: UiPreferences = Injekt.get(),
|
||||
downloadPreferences: DownloadPreferences = Injekt.get(),
|
||||
) : StateScreenModel<AnimeUpdatesScreenModel.State>(State()) {
|
||||
|
||||
|
@ -67,6 +68,7 @@ class AnimeUpdatesScreenModel(
|
|||
val events: Flow<Event> = _events.receiveAsFlow()
|
||||
|
||||
val lastUpdated by libraryPreferences.lastUpdatedTimestamp().asState(coroutineScope)
|
||||
val relativeTime by uiPreferences.relativeTime().asState(coroutineScope)
|
||||
|
||||
val useExternalDownloader = downloadPreferences.useExternalDownloader().get()
|
||||
|
||||
|
@ -393,7 +395,7 @@ class AnimeUpdatesScreenModel(
|
|||
val selected = items.filter { it.selected }
|
||||
val selectionMode = selected.isNotEmpty()
|
||||
|
||||
fun getUiModel(context: Context): List<AnimeUpdatesUiModel> {
|
||||
fun getUiModel(context: Context, relativeTime: Boolean): List<AnimeUpdatesUiModel> {
|
||||
val dateFormat by mutableStateOf(
|
||||
UiPreferences.dateFormat(Injekt.get<UiPreferences>().dateFormat().get()),
|
||||
)
|
||||
|
@ -405,7 +407,11 @@ class AnimeUpdatesScreenModel(
|
|||
val afterDate = after?.item?.update?.dateFetch?.toDateKey() ?: Date(0)
|
||||
when {
|
||||
beforeDate.time != afterDate.time && afterDate.time != 0L -> {
|
||||
val text = afterDate.toRelativeString(context, dateFormat)
|
||||
val text = afterDate.toRelativeString(
|
||||
context = context,
|
||||
relative = relativeTime,
|
||||
dateFormat = dateFormat,
|
||||
)
|
||||
AnimeUpdatesUiModel.Header(text)
|
||||
}
|
||||
// Return null to avoid adding a separator between two items.
|
||||
|
|
|
@ -59,6 +59,7 @@ fun Screen.animeUpdatesTab(
|
|||
snackbarHostState = screenModel.snackbarHostState,
|
||||
contentPadding = contentPadding,
|
||||
lastUpdated = screenModel.lastUpdated,
|
||||
relativeTime = screenModel.relativeTime,
|
||||
onClickCover = { item -> navigator.push(AnimeScreen(item.update.animeId)) },
|
||||
onSelectAll = screenModel::toggleAllSelection,
|
||||
onInvertSelection = screenModel::invertSelection,
|
||||
|
|
|
@ -59,12 +59,14 @@ class MangaUpdatesScreenModel(
|
|||
private val getChapter: GetChapter = Injekt.get(),
|
||||
private val libraryPreferences: LibraryPreferences = Injekt.get(),
|
||||
val snackbarHostState: SnackbarHostState = SnackbarHostState(),
|
||||
uiPreferences: UiPreferences = Injekt.get(),
|
||||
) : StateScreenModel<MangaUpdatesScreenModel.State>(State()) {
|
||||
|
||||
private val _events: Channel<Event> = Channel(Int.MAX_VALUE)
|
||||
val events: Flow<Event> = _events.receiveAsFlow()
|
||||
|
||||
val lastUpdated by libraryPreferences.lastUpdatedTimestamp().asState(coroutineScope)
|
||||
val relativeTime by uiPreferences.relativeTime().asState(coroutineScope)
|
||||
|
||||
// First and last selected index in list
|
||||
private val selectedPositions: Array<Int> = arrayOf(-1, -1)
|
||||
|
@ -374,7 +376,7 @@ class MangaUpdatesScreenModel(
|
|||
val selected = items.filter { it.selected }
|
||||
val selectionMode = selected.isNotEmpty()
|
||||
|
||||
fun getUiModel(context: Context): List<MangaUpdatesUiModel> {
|
||||
fun getUiModel(context: Context, relativeTime: Boolean): List<MangaUpdatesUiModel> {
|
||||
val dateFormat by mutableStateOf(
|
||||
UiPreferences.dateFormat(Injekt.get<UiPreferences>().dateFormat().get()),
|
||||
)
|
||||
|
@ -386,7 +388,11 @@ class MangaUpdatesScreenModel(
|
|||
val afterDate = after?.item?.update?.dateFetch?.toDateKey() ?: Date(0)
|
||||
when {
|
||||
beforeDate.time != afterDate.time && afterDate.time != 0L -> {
|
||||
val text = afterDate.toRelativeString(context, dateFormat)
|
||||
val text = afterDate.toRelativeString(
|
||||
context = context,
|
||||
relative = relativeTime,
|
||||
dateFormat = dateFormat,
|
||||
)
|
||||
MangaUpdatesUiModel.Header(text)
|
||||
}
|
||||
// Return null to avoid adding a separator between two items.
|
||||
|
|
|
@ -46,6 +46,7 @@ fun Screen.mangaUpdatesTab(
|
|||
snackbarHostState = screenModel.snackbarHostState,
|
||||
contentPadding = contentPadding,
|
||||
lastUpdated = screenModel.lastUpdated,
|
||||
relativeTime = screenModel.relativeTime,
|
||||
onClickCover = { item -> navigator.push(MangaScreen(item.update.mangaId)) },
|
||||
onSelectAll = screenModel::toggleAllSelection,
|
||||
onInvertSelection = screenModel::invertSelection,
|
||||
|
|
|
@ -4,6 +4,7 @@ import android.content.Context
|
|||
import android.os.Build
|
||||
import eu.kanade.tachiyomi.BuildConfig
|
||||
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
||||
import eu.kanade.tachiyomi.util.system.WebViewUtil
|
||||
import eu.kanade.tachiyomi.util.system.createFileInCacheDir
|
||||
import eu.kanade.tachiyomi.util.system.toShareIntent
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
|
@ -35,6 +36,7 @@ class CrashLogUtil(private val context: Context) {
|
|||
Device name: ${Build.DEVICE}
|
||||
Device model: ${Build.MODEL}
|
||||
Device product name: ${Build.PRODUCT}
|
||||
WebView user agent: ${WebViewUtil.getInferredUserAgent(context)}
|
||||
""".trimIndent()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,7 +8,6 @@ import java.time.LocalDateTime
|
|||
import java.time.ZoneId
|
||||
import java.util.Calendar
|
||||
import java.util.Date
|
||||
import java.util.TimeZone
|
||||
|
||||
fun Date.toDateTimestampString(dateFormatter: DateFormat): String {
|
||||
val date = dateFormatter.format(this)
|
||||
|
@ -46,80 +45,18 @@ fun Long.toDateKey(): Date {
|
|||
return cal.time
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert epoch long to Calendar instance
|
||||
*
|
||||
* @return Calendar instance at supplied epoch time. Null if epoch was 0.
|
||||
*/
|
||||
fun Long.toCalendar(): Calendar? {
|
||||
if (this == 0L) {
|
||||
return null
|
||||
}
|
||||
val cal = Calendar.getInstance()
|
||||
cal.timeInMillis = this
|
||||
return cal
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert local time millisecond value to Calendar instance in UTC
|
||||
*
|
||||
* @return UTC Calendar instance at supplied time. Null if time is 0.
|
||||
*/
|
||||
fun Long.toUtcCalendar(): Calendar? {
|
||||
if (this == 0L) {
|
||||
return null
|
||||
}
|
||||
val rawCalendar = Calendar.getInstance().apply {
|
||||
timeInMillis = this@toUtcCalendar
|
||||
}
|
||||
return Calendar.getInstance(TimeZone.getTimeZone("UTC")).apply {
|
||||
clear()
|
||||
set(
|
||||
rawCalendar.get(Calendar.YEAR),
|
||||
rawCalendar.get(Calendar.MONTH),
|
||||
rawCalendar.get(Calendar.DAY_OF_MONTH),
|
||||
rawCalendar.get(Calendar.HOUR_OF_DAY),
|
||||
rawCalendar.get(Calendar.MINUTE),
|
||||
rawCalendar.get(Calendar.SECOND),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert UTC time millisecond to Calendar instance in local time zone
|
||||
*
|
||||
* @return local Calendar instance at supplied UTC time. Null if time is 0.
|
||||
*/
|
||||
fun Long.toLocalCalendar(): Calendar? {
|
||||
if (this == 0L) {
|
||||
return null
|
||||
}
|
||||
val rawCalendar = Calendar.getInstance(TimeZone.getTimeZone("UTC")).apply {
|
||||
timeInMillis = this@toLocalCalendar
|
||||
}
|
||||
return Calendar.getInstance().apply {
|
||||
clear()
|
||||
set(
|
||||
rawCalendar.get(Calendar.YEAR),
|
||||
rawCalendar.get(Calendar.MONTH),
|
||||
rawCalendar.get(Calendar.DAY_OF_MONTH),
|
||||
rawCalendar.get(Calendar.HOUR_OF_DAY),
|
||||
rawCalendar.get(Calendar.MINUTE),
|
||||
rawCalendar.get(Calendar.SECOND),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private const val MILLISECONDS_IN_DAY = 86_400_000L
|
||||
|
||||
fun Date.toRelativeString(
|
||||
context: Context,
|
||||
relative: Boolean = true,
|
||||
dateFormat: DateFormat = DateFormat.getDateInstance(DateFormat.SHORT),
|
||||
): String {
|
||||
if (!relative) {
|
||||
return dateFormat.format(this)
|
||||
}
|
||||
val now = Date()
|
||||
val difference = now.timeWithOffset.floorNearest(MILLISECONDS_IN_DAY) - this.timeWithOffset.floorNearest(
|
||||
MILLISECONDS_IN_DAY,
|
||||
)
|
||||
val difference = now.timeWithOffset.floorNearest(MILLISECONDS_IN_DAY) - this.timeWithOffset.floorNearest(MILLISECONDS_IN_DAY)
|
||||
val days = difference.floorDiv(MILLISECONDS_IN_DAY).toInt()
|
||||
return when {
|
||||
difference < 0 -> dateFormat.format(this)
|
||||
|
|
|
@ -13,6 +13,10 @@
|
|||
android:id="@+id/move_to_bottom"
|
||||
android:title="@string/action_move_to_bottom" />
|
||||
|
||||
<item
|
||||
android:id="@+id/move_to_bottom_series"
|
||||
android:title="@string/action_move_to_bottom_all_for_series" />
|
||||
|
||||
<item
|
||||
android:id="@+id/cancel_download"
|
||||
android:title="@string/action_cancel" />
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package eu.kanade.tachiyomi.core
|
||||
|
||||
object Constants {
|
||||
const val URL_HELP = "https://aniyomi.org/help/"
|
||||
const val URL_HELP = "https://aniyomi.org/docs/guides/troubleshooting/"
|
||||
|
||||
const val MANGA_EXTRA = "manga"
|
||||
|
||||
|
|
|
@ -58,6 +58,15 @@ fun Call.asObservable(): Observable<Response> {
|
|||
}
|
||||
}
|
||||
|
||||
fun Call.asObservableSuccess(): Observable<Response> {
|
||||
return asObservable().doOnNext { response ->
|
||||
if (!response.isSuccessful) {
|
||||
response.close()
|
||||
throw HttpException(response.code)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Based on https://github.com/gildor/kotlin-coroutines-okhttp
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private suspend fun Call.await(callStack: Array<StackTraceElement>): Response {
|
||||
|
@ -95,6 +104,9 @@ suspend fun Call.await(): Response {
|
|||
return await(callStack)
|
||||
}
|
||||
|
||||
/**
|
||||
* @since extensions-lib 1.5
|
||||
*/
|
||||
suspend fun Call.awaitSuccess(): Response {
|
||||
val callStack = Exception().stackTrace.run { copyOfRange(1, size) }
|
||||
val response = await(callStack)
|
||||
|
@ -105,15 +117,6 @@ suspend fun Call.awaitSuccess(): Response {
|
|||
return response
|
||||
}
|
||||
|
||||
fun Call.asObservableSuccess(): Observable<Response> {
|
||||
return asObservable().doOnNext { response ->
|
||||
if (!response.isSuccessful) {
|
||||
response.close()
|
||||
throw HttpException(response.code)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun OkHttpClient.newCachelessCallWithProgress(request: Request, listener: ProgressListener): Call {
|
||||
val progressClient = newBuilder()
|
||||
.cache(null)
|
||||
|
|
|
@ -14,7 +14,24 @@ import kotlin.coroutines.resume
|
|||
object WebViewUtil {
|
||||
const val SPOOF_PACKAGE_NAME = "org.chromium.chrome"
|
||||
|
||||
const val MINIMUM_WEBVIEW_VERSION = 111
|
||||
const val MINIMUM_WEBVIEW_VERSION = 114
|
||||
|
||||
/**
|
||||
* Uses the WebView's user agent string to create something similar to what Chrome on Android
|
||||
* would return.
|
||||
*
|
||||
* Example of WebView user agent string:
|
||||
* Mozilla/5.0 (Linux; Android 13; Pixel 7 Build/TQ3A.230901.001; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/116.0.0.0 Mobile Safari/537.36
|
||||
*
|
||||
* Example of Chrome on Android:
|
||||
* Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Mobile Safari/537.3
|
||||
*/
|
||||
fun getInferredUserAgent(context: Context): String {
|
||||
return WebView(context)
|
||||
.getDefaultUserAgentString()
|
||||
.replace("; Android .*?\\)".toRegex(), "; Android 10; K)")
|
||||
.replace("Version/.* Chrome/".toRegex(), "Chrome/")
|
||||
}
|
||||
|
||||
fun supportsWebView(context: Context): Boolean {
|
||||
try {
|
||||
|
|
|
@ -16,18 +16,21 @@ class AnimeSourceSearchPagingSource(
|
|||
val filters: AnimeFilterList,
|
||||
) : AnimeSourcePagingSource(source) {
|
||||
override suspend fun requestNextPage(currentPage: Int): AnimesPage {
|
||||
// Replace with getSearchAnime
|
||||
return source.fetchSearchAnime(currentPage, query, filters).awaitSingle()
|
||||
}
|
||||
}
|
||||
|
||||
class AnimeSourcePopularPagingSource(source: AnimeCatalogueSource) : AnimeSourcePagingSource(source) {
|
||||
override suspend fun requestNextPage(currentPage: Int): AnimesPage {
|
||||
// Replace with getPopularAnime
|
||||
return source.fetchPopularAnime(currentPage).awaitSingle()
|
||||
}
|
||||
}
|
||||
|
||||
class AnimeSourceLatestPagingSource(source: AnimeCatalogueSource) : AnimeSourcePagingSource(source) {
|
||||
override suspend fun requestNextPage(currentPage: Int): AnimesPage {
|
||||
// Replace with getLatestUpdates
|
||||
return source.fetchLatestUpdates(currentPage).awaitSingle()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,7 +5,6 @@ import eu.kanade.tachiyomi.source.CatalogueSource
|
|||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import tachiyomi.core.util.lang.awaitSingle
|
||||
import tachiyomi.core.util.lang.withIOContext
|
||||
import tachiyomi.domain.items.chapter.model.NoChaptersException
|
||||
import tachiyomi.domain.source.manga.repository.SourcePagingSourceType
|
||||
|
@ -14,19 +13,19 @@ class SourceSearchPagingSource(source: CatalogueSource, val query: String, val f
|
|||
source,
|
||||
) {
|
||||
override suspend fun requestNextPage(currentPage: Int): MangasPage {
|
||||
return source.fetchSearchManga(currentPage, query, filters).awaitSingle()
|
||||
return source.getSearchManga(currentPage, query, filters)
|
||||
}
|
||||
}
|
||||
|
||||
class SourcePopularPagingSource(source: CatalogueSource) : SourcePagingSource(source) {
|
||||
override suspend fun requestNextPage(currentPage: Int): MangasPage {
|
||||
return source.fetchPopularManga(currentPage).awaitSingle()
|
||||
return source.getPopularManga(currentPage)
|
||||
}
|
||||
}
|
||||
|
||||
class SourceLatestPagingSource(source: CatalogueSource) : SourcePagingSource(source) {
|
||||
override suspend fun requestNextPage(currentPage: Int): MangasPage {
|
||||
return source.fetchLatestUpdates(currentPage).awaitSingle()
|
||||
return source.getLatestUpdates(currentPage)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -4,16 +4,13 @@ import tachiyomi.domain.entries.anime.model.Anime
|
|||
import tachiyomi.domain.entries.anime.model.AnimeUpdate
|
||||
import tachiyomi.domain.items.episode.interactor.GetEpisodeByAnimeId
|
||||
import tachiyomi.domain.items.episode.model.Episode
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.time.Instant
|
||||
import java.time.ZoneId
|
||||
import java.time.ZonedDateTime
|
||||
import java.time.temporal.ChronoUnit
|
||||
import kotlin.math.absoluteValue
|
||||
|
||||
const val MAX_FETCH_INTERVAL = 28
|
||||
private const val FETCH_INTERVAL_GRACE_PERIOD = 1
|
||||
|
||||
class SetAnimeFetchInterval(
|
||||
class AnimeFetchInterval(
|
||||
private val getEpisodeByAnimeId: GetEpisodeByAnimeId,
|
||||
) {
|
||||
|
||||
|
@ -30,7 +27,7 @@ class SetAnimeFetchInterval(
|
|||
val episodes = getEpisodeByAnimeId.await(anime.id)
|
||||
val interval = anime.fetchInterval.takeIf { it < 0 } ?: calculateInterval(
|
||||
episodes,
|
||||
dateTime,
|
||||
dateTime.zone,
|
||||
)
|
||||
val nextUpdate = calculateNextUpdate(anime, interval, dateTime, currentWindow)
|
||||
|
||||
|
@ -43,33 +40,34 @@ class SetAnimeFetchInterval(
|
|||
|
||||
fun getWindow(dateTime: ZonedDateTime): Pair<Long, Long> {
|
||||
val today = dateTime.toLocalDate().atStartOfDay(dateTime.zone)
|
||||
val lowerBound = today.minusDays(FETCH_INTERVAL_GRACE_PERIOD.toLong())
|
||||
val upperBound = today.plusDays(FETCH_INTERVAL_GRACE_PERIOD.toLong())
|
||||
val lowerBound = today.minusDays(GRACE_PERIOD)
|
||||
val upperBound = today.plusDays(GRACE_PERIOD)
|
||||
return Pair(lowerBound.toEpochSecond() * 1000, upperBound.toEpochSecond() * 1000 - 1)
|
||||
}
|
||||
|
||||
internal fun calculateInterval(episodes: List<Episode>, zonedDateTime: ZonedDateTime): Int {
|
||||
val sortedEpisodes = episodes
|
||||
.sortedWith(
|
||||
compareByDescending<Episode> { it.dateUpload }.thenByDescending { it.dateFetch },
|
||||
)
|
||||
.take(50)
|
||||
|
||||
val uploadDates = sortedEpisodes
|
||||
internal fun calculateInterval(episodes: List<Episode>, zone: ZoneId): Int {
|
||||
val uploadDates = episodes.asSequence()
|
||||
.filter { it.dateUpload > 0L }
|
||||
.sortedByDescending { it.dateUpload }
|
||||
.map {
|
||||
ZonedDateTime.ofInstant(Instant.ofEpochMilli(it.dateUpload), zonedDateTime.zone)
|
||||
ZonedDateTime.ofInstant(Instant.ofEpochMilli(it.dateUpload), zone)
|
||||
.toLocalDate()
|
||||
.atStartOfDay()
|
||||
}
|
||||
.distinct()
|
||||
val fetchDates = sortedEpisodes
|
||||
.take(10)
|
||||
.toList()
|
||||
|
||||
val fetchDates = episodes.asSequence()
|
||||
.sortedByDescending { it.dateFetch }
|
||||
.map {
|
||||
ZonedDateTime.ofInstant(Instant.ofEpochMilli(it.dateFetch), zonedDateTime.zone)
|
||||
ZonedDateTime.ofInstant(Instant.ofEpochMilli(it.dateFetch), zone)
|
||||
.toLocalDate()
|
||||
.atStartOfDay()
|
||||
}
|
||||
.distinct()
|
||||
.take(10)
|
||||
.toList()
|
||||
|
||||
val interval = when {
|
||||
// Enough upload date from source
|
||||
|
@ -88,7 +86,7 @@ class SetAnimeFetchInterval(
|
|||
else -> 7
|
||||
}
|
||||
|
||||
return interval.coerceIn(1, MAX_FETCH_INTERVAL)
|
||||
return interval.coerceIn(1, MAX_INTERVAL)
|
||||
}
|
||||
|
||||
private fun calculateNextUpdate(
|
||||
|
@ -119,7 +117,7 @@ class SetAnimeFetchInterval(
|
|||
}
|
||||
|
||||
private fun doubleInterval(delta: Int, timeSinceLatest: Int, doubleWhenOver: Int): Int {
|
||||
if (delta >= MAX_FETCH_INTERVAL) return MAX_FETCH_INTERVAL
|
||||
if (delta >= MAX_INTERVAL) return MAX_INTERVAL
|
||||
|
||||
// double delta again if missed more than 9 check in new delta
|
||||
val cycle = timeSinceLatest.floorDiv(delta) + 1
|
||||
|
@ -129,4 +127,10 @@ class SetAnimeFetchInterval(
|
|||
delta
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val MAX_INTERVAL = 28
|
||||
|
||||
private const val GRACE_PERIOD = 1L
|
||||
}
|
||||
}
|
|
@ -4,16 +4,13 @@ import tachiyomi.domain.entries.manga.model.Manga
|
|||
import tachiyomi.domain.entries.manga.model.MangaUpdate
|
||||
import tachiyomi.domain.items.chapter.interactor.GetChapterByMangaId
|
||||
import tachiyomi.domain.items.chapter.model.Chapter
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.time.Instant
|
||||
import java.time.ZoneId
|
||||
import java.time.ZonedDateTime
|
||||
import java.time.temporal.ChronoUnit
|
||||
import kotlin.math.absoluteValue
|
||||
|
||||
const val MAX_FETCH_INTERVAL = 28
|
||||
private const val FETCH_INTERVAL_GRACE_PERIOD = 1
|
||||
|
||||
class SetMangaFetchInterval(
|
||||
class MangaFetchInterval(
|
||||
private val getChapterByMangaId: GetChapterByMangaId,
|
||||
) {
|
||||
|
||||
|
@ -30,7 +27,7 @@ class SetMangaFetchInterval(
|
|||
val chapters = getChapterByMangaId.await(manga.id)
|
||||
val interval = manga.fetchInterval.takeIf { it < 0 } ?: calculateInterval(
|
||||
chapters,
|
||||
dateTime,
|
||||
dateTime.zone,
|
||||
)
|
||||
val nextUpdate = calculateNextUpdate(manga, interval, dateTime, currentWindow)
|
||||
|
||||
|
@ -43,33 +40,34 @@ class SetMangaFetchInterval(
|
|||
|
||||
fun getWindow(dateTime: ZonedDateTime): Pair<Long, Long> {
|
||||
val today = dateTime.toLocalDate().atStartOfDay(dateTime.zone)
|
||||
val lowerBound = today.minusDays(FETCH_INTERVAL_GRACE_PERIOD.toLong())
|
||||
val upperBound = today.plusDays(FETCH_INTERVAL_GRACE_PERIOD.toLong())
|
||||
val lowerBound = today.minusDays(GRACE_PERIOD)
|
||||
val upperBound = today.plusDays(GRACE_PERIOD)
|
||||
return Pair(lowerBound.toEpochSecond() * 1000, upperBound.toEpochSecond() * 1000 - 1)
|
||||
}
|
||||
|
||||
internal fun calculateInterval(chapters: List<Chapter>, zonedDateTime: ZonedDateTime): Int {
|
||||
val sortedChapters = chapters
|
||||
.sortedWith(
|
||||
compareByDescending<Chapter> { it.dateUpload }.thenByDescending { it.dateFetch },
|
||||
)
|
||||
.take(50)
|
||||
|
||||
val uploadDates = sortedChapters
|
||||
internal fun calculateInterval(chapters: List<Chapter>, zone: ZoneId): Int {
|
||||
val uploadDates = chapters.asSequence()
|
||||
.filter { it.dateUpload > 0L }
|
||||
.sortedByDescending { it.dateUpload }
|
||||
.map {
|
||||
ZonedDateTime.ofInstant(Instant.ofEpochMilli(it.dateUpload), zonedDateTime.zone)
|
||||
ZonedDateTime.ofInstant(Instant.ofEpochMilli(it.dateUpload), zone)
|
||||
.toLocalDate()
|
||||
.atStartOfDay()
|
||||
}
|
||||
.distinct()
|
||||
val fetchDates = sortedChapters
|
||||
.take(10)
|
||||
.toList()
|
||||
|
||||
val fetchDates = chapters.asSequence()
|
||||
.sortedByDescending { it.dateFetch }
|
||||
.map {
|
||||
ZonedDateTime.ofInstant(Instant.ofEpochMilli(it.dateFetch), zonedDateTime.zone)
|
||||
ZonedDateTime.ofInstant(Instant.ofEpochMilli(it.dateFetch), zone)
|
||||
.toLocalDate()
|
||||
.atStartOfDay()
|
||||
}
|
||||
.distinct()
|
||||
.take(10)
|
||||
.toList()
|
||||
|
||||
val interval = when {
|
||||
// Enough upload date from source
|
||||
|
@ -88,7 +86,7 @@ class SetMangaFetchInterval(
|
|||
else -> 7
|
||||
}
|
||||
|
||||
return interval.coerceIn(1, MAX_FETCH_INTERVAL)
|
||||
return interval.coerceIn(1, MAX_INTERVAL)
|
||||
}
|
||||
|
||||
private fun calculateNextUpdate(
|
||||
|
@ -119,7 +117,7 @@ class SetMangaFetchInterval(
|
|||
}
|
||||
|
||||
private fun doubleInterval(delta: Int, timeSinceLatest: Int, doubleWhenOver: Int): Int {
|
||||
if (delta >= MAX_FETCH_INTERVAL) return MAX_FETCH_INTERVAL
|
||||
if (delta >= MAX_INTERVAL) return MAX_INTERVAL
|
||||
|
||||
// double delta again if missed more than 9 check in new delta
|
||||
val cycle = timeSinceLatest.floorDiv(delta) + 1
|
||||
|
@ -129,4 +127,10 @@ class SetMangaFetchInterval(
|
|||
delta
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val MAX_INTERVAL = 28
|
||||
|
||||
private const val GRACE_PERIOD = 1L
|
||||
}
|
||||
}
|
|
@ -14,20 +14,16 @@ class StubAnimeSource(
|
|||
|
||||
private val isInvalid: Boolean = name.isBlank() || lang.isBlank()
|
||||
|
||||
override suspend fun getAnimeDetails(anime: SAnime): SAnime {
|
||||
override suspend fun getAnimeDetails(anime: SAnime): SAnime =
|
||||
throw AnimeSourceNotInstalledException()
|
||||
}
|
||||
|
||||
override suspend fun getEpisodeList(anime: SAnime): List<SEpisode> {
|
||||
override suspend fun getEpisodeList(anime: SAnime): List<SEpisode> =
|
||||
throw AnimeSourceNotInstalledException()
|
||||
}
|
||||
|
||||
override suspend fun getVideoList(episode: SEpisode): List<Video> {
|
||||
override suspend fun getVideoList(episode: SEpisode): List<Video> =
|
||||
throw AnimeSourceNotInstalledException()
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return if (isInvalid.not()) "$name (${lang.uppercase()})" else id.toString()
|
||||
}
|
||||
override fun toString(): String =
|
||||
if (isInvalid.not()) "$name (${lang.uppercase()})" else id.toString()
|
||||
}
|
||||
class AnimeSourceNotInstalledException : Exception()
|
||||
|
|
|
@ -4,7 +4,6 @@ import eu.kanade.tachiyomi.source.MangaSource
|
|||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import eu.kanade.tachiyomi.source.model.SChapter
|
||||
import eu.kanade.tachiyomi.source.model.SManga
|
||||
import rx.Observable
|
||||
|
||||
@Suppress("OverridingDeprecatedMember")
|
||||
class StubMangaSource(
|
||||
|
@ -15,36 +14,17 @@ class StubMangaSource(
|
|||
|
||||
private val isInvalid: Boolean = name.isBlank() || lang.isBlank()
|
||||
|
||||
override suspend fun getMangaDetails(manga: SManga): SManga {
|
||||
override suspend fun getMangaDetails(manga: SManga): SManga =
|
||||
throw SourceNotInstalledException()
|
||||
}
|
||||
|
||||
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getMangaDetails"))
|
||||
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
||||
return Observable.error(SourceNotInstalledException())
|
||||
}
|
||||
|
||||
override suspend fun getChapterList(manga: SManga): List<SChapter> {
|
||||
override suspend fun getChapterList(manga: SManga): List<SChapter> =
|
||||
throw SourceNotInstalledException()
|
||||
}
|
||||
|
||||
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getChapterList"))
|
||||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
||||
return Observable.error(SourceNotInstalledException())
|
||||
}
|
||||
|
||||
override suspend fun getPageList(chapter: SChapter): List<Page> {
|
||||
override suspend fun getPageList(chapter: SChapter): List<Page> =
|
||||
throw SourceNotInstalledException()
|
||||
}
|
||||
|
||||
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getPageList"))
|
||||
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
|
||||
return Observable.error(SourceNotInstalledException())
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return if (isInvalid.not()) "$name (${lang.uppercase()})" else id.toString()
|
||||
}
|
||||
override fun toString(): String =
|
||||
if (isInvalid.not()) "$name (${lang.uppercase()})" else id.toString()
|
||||
}
|
||||
|
||||
class SourceNotInstalledException : Exception()
|
||||
|
|
|
@ -0,0 +1,127 @@
|
|||
package tachiyomi.domain.entries.anime.interactor
|
||||
|
||||
import io.kotest.matchers.shouldBe
|
||||
import io.mockk.mockk
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.parallel.Execution
|
||||
import org.junit.jupiter.api.parallel.ExecutionMode
|
||||
import tachiyomi.domain.items.episode.model.Episode
|
||||
import java.time.ZoneOffset
|
||||
import java.time.ZonedDateTime
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.days
|
||||
import kotlin.time.Duration.Companion.hours
|
||||
import kotlin.time.DurationUnit
|
||||
import kotlin.time.toDuration
|
||||
import kotlin.time.toJavaDuration
|
||||
|
||||
@Execution(ExecutionMode.CONCURRENT)
|
||||
class AnimeFetchIntervalTest {
|
||||
|
||||
private val testTime = ZonedDateTime.parse("2020-01-01T00:00:00Z")
|
||||
private val testZoneId = ZoneOffset.UTC
|
||||
private var episode = Episode.create().copy(
|
||||
dateFetch = testTime.toEpochSecond() * 1000,
|
||||
dateUpload = testTime.toEpochSecond() * 1000,
|
||||
)
|
||||
|
||||
private val fetchInterval = AnimeFetchInterval(mockk())
|
||||
|
||||
@Test
|
||||
fun `returns default interval of 7 days when not enough distinct days`() {
|
||||
val episodesWithUploadDate = (1..50).map {
|
||||
chapterWithTime(episode, 1.days)
|
||||
}
|
||||
fetchInterval.calculateInterval(episodesWithUploadDate, testZoneId) shouldBe 7
|
||||
|
||||
val episodesWithoutUploadDate = episodesWithUploadDate.map {
|
||||
it.copy(dateUpload = 0L)
|
||||
}
|
||||
fetchInterval.calculateInterval(episodesWithoutUploadDate, testZoneId) shouldBe 7
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns interval based on more recent episodes`() {
|
||||
val oldEpisodes = (1..5).map {
|
||||
chapterWithTime(episode, (it * 7).days) // Would have interval of 7 days
|
||||
}
|
||||
val newEpisodes = (1..10).map {
|
||||
chapterWithTime(episode, oldEpisodes.lastUploadDate() + it.days)
|
||||
}
|
||||
|
||||
val episodes = oldEpisodes + newEpisodes
|
||||
|
||||
fetchInterval.calculateInterval(episodes, testZoneId) shouldBe 1
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns interval of 7 days when multiple episodes in 1 day`() {
|
||||
val episodes = (1..10).map {
|
||||
chapterWithTime(episode, 10.hours)
|
||||
}
|
||||
fetchInterval.calculateInterval(episodes, testZoneId) shouldBe 7
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns interval of 7 days when multiple episodes in 2 days`() {
|
||||
val episodes = (1..2).map {
|
||||
chapterWithTime(episode, 1.days)
|
||||
} + (1..5).map {
|
||||
chapterWithTime(episode, 2.days)
|
||||
}
|
||||
fetchInterval.calculateInterval(episodes, testZoneId) shouldBe 7
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns interval of 1 day when episodes are released every 1 day`() {
|
||||
val episodes = (1..20).map {
|
||||
chapterWithTime(episode, it.days)
|
||||
}
|
||||
fetchInterval.calculateInterval(episodes, testZoneId) shouldBe 1
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns interval of 1 day when delta is less than 1 day`() {
|
||||
val episodes = (1..20).map {
|
||||
chapterWithTime(episode, (15 * it).hours)
|
||||
}
|
||||
fetchInterval.calculateInterval(episodes, testZoneId) shouldBe 1
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns interval of 2 days when episodes are released every 2 days`() {
|
||||
val episodes = (1..20).map {
|
||||
chapterWithTime(episode, (2 * it).days)
|
||||
}
|
||||
fetchInterval.calculateInterval(episodes, testZoneId) shouldBe 2
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns interval with floored value when interval is decimal`() {
|
||||
val episodesWithUploadDate = (1..5).map {
|
||||
chapterWithTime(episode, (25 * it).hours)
|
||||
}
|
||||
fetchInterval.calculateInterval(episodesWithUploadDate, testZoneId) shouldBe 1
|
||||
|
||||
val episodesWithoutUploadDate = episodesWithUploadDate.map {
|
||||
it.copy(dateUpload = 0L)
|
||||
}
|
||||
fetchInterval.calculateInterval(episodesWithoutUploadDate, testZoneId) shouldBe 1
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns interval of 1 day when episodes are released just below every 2 days`() {
|
||||
val episodes = (1..20).map {
|
||||
chapterWithTime(episode, (43 * it).hours)
|
||||
}
|
||||
fetchInterval.calculateInterval(episodes, testZoneId) shouldBe 1
|
||||
}
|
||||
|
||||
private fun chapterWithTime(episode: Episode, duration: Duration): Episode {
|
||||
val newTime = testTime.plus(duration.toJavaDuration()).toEpochSecond() * 1000
|
||||
return episode.copy(dateFetch = newTime, dateUpload = newTime)
|
||||
}
|
||||
|
||||
private fun List<Episode>.lastUploadDate() =
|
||||
last().dateUpload.toDuration(DurationUnit.MILLISECONDS)
|
||||
}
|
|
@ -1,104 +0,0 @@
|
|||
package tachiyomi.domain.entries.anime.interactor
|
||||
|
||||
import io.kotest.matchers.shouldBe
|
||||
import io.mockk.mockk
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.parallel.Execution
|
||||
import org.junit.jupiter.api.parallel.ExecutionMode
|
||||
import tachiyomi.domain.items.episode.model.Episode
|
||||
import java.time.ZonedDateTime
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.hours
|
||||
import kotlin.time.toJavaDuration
|
||||
|
||||
@Execution(ExecutionMode.CONCURRENT)
|
||||
class SetAnimeFetchIntervalTest {
|
||||
|
||||
private val testTime = ZonedDateTime.parse("2020-01-01T00:00:00Z")
|
||||
private var episode = Episode.create().copy(
|
||||
dateFetch = testTime.toEpochSecond() * 1000,
|
||||
dateUpload = testTime.toEpochSecond() * 1000,
|
||||
)
|
||||
|
||||
private val setAnimeFetchInterval = SetAnimeFetchInterval(mockk())
|
||||
|
||||
@Test
|
||||
fun `calculateInterval returns default of 7 days when less than 3 distinct days`() {
|
||||
val episodes = (1..2).map {
|
||||
episodeWithTime(episode, 10.hours)
|
||||
}
|
||||
setAnimeFetchInterval.calculateInterval(episodes, testTime) shouldBe 7
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `calculateInterval returns 7 when 5 episodes in 1 day`() {
|
||||
val episodes = (1..5).map {
|
||||
episodeWithTime(episode, 10.hours)
|
||||
}
|
||||
setAnimeFetchInterval.calculateInterval(episodes, testTime) shouldBe 7
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `calculateInterval returns 7 when 7 episodes in 48 hours, 2 day`() {
|
||||
val episodes = (1..2).map {
|
||||
episodeWithTime(episode, 24.hours)
|
||||
} + (1..5).map {
|
||||
episodeWithTime(episode, 48.hours)
|
||||
}
|
||||
setAnimeFetchInterval.calculateInterval(episodes, testTime) shouldBe 7
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `calculateInterval returns default of 1 day when interval less than 1`() {
|
||||
val episodes = (1..5).map {
|
||||
episodeWithTime(episode, (15 * it).hours)
|
||||
}
|
||||
setAnimeFetchInterval.calculateInterval(episodes, testTime) shouldBe 1
|
||||
}
|
||||
|
||||
// Normal interval calculation
|
||||
@Test
|
||||
fun `calculateInterval returns 1 when 5 episodes in 120 hours, 5 days`() {
|
||||
val episodes = (1..5).map {
|
||||
episodeWithTime(episode, (24 * it).hours)
|
||||
}
|
||||
setAnimeFetchInterval.calculateInterval(episodes, testTime) shouldBe 1
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `calculateInterval returns 2 when 5 episodes in 240 hours, 10 days`() {
|
||||
val episodes = (1..5).map {
|
||||
episodeWithTime(episode, (48 * it).hours)
|
||||
}
|
||||
setAnimeFetchInterval.calculateInterval(episodes, testTime) shouldBe 2
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `calculateInterval returns floored value when interval is decimal`() {
|
||||
val episodes = (1..5).map {
|
||||
episodeWithTime(episode, (25 * it).hours)
|
||||
}
|
||||
setAnimeFetchInterval.calculateInterval(episodes, testTime) shouldBe 1
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `calculateInterval returns 1 when 5 episodes in 215 hours, 5 days`() {
|
||||
val episodes = (1..5).map {
|
||||
episodeWithTime(episode, (43 * it).hours)
|
||||
}
|
||||
setAnimeFetchInterval.calculateInterval(episodes, testTime) shouldBe 1
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `calculateInterval returns interval based on fetch time if upload time not available`() {
|
||||
val episodes = (1..5).map {
|
||||
episodeWithTime(episode, (25 * it).hours).copy(dateUpload = 0L)
|
||||
}
|
||||
setAnimeFetchInterval.calculateInterval(episodes, testTime) shouldBe 1
|
||||
}
|
||||
|
||||
private fun episodeWithTime(episode: Episode, duration: Duration): Episode {
|
||||
val newTime = testTime.plus(duration.toJavaDuration()).toEpochSecond() * 1000
|
||||
return episode.copy(dateFetch = newTime, dateUpload = newTime)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,127 @@
|
|||
package tachiyomi.domain.entries.manga.interactor
|
||||
|
||||
import io.kotest.matchers.shouldBe
|
||||
import io.mockk.mockk
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.parallel.Execution
|
||||
import org.junit.jupiter.api.parallel.ExecutionMode
|
||||
import tachiyomi.domain.items.chapter.model.Chapter
|
||||
import java.time.ZoneOffset
|
||||
import java.time.ZonedDateTime
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.days
|
||||
import kotlin.time.Duration.Companion.hours
|
||||
import kotlin.time.DurationUnit
|
||||
import kotlin.time.toDuration
|
||||
import kotlin.time.toJavaDuration
|
||||
|
||||
@Execution(ExecutionMode.CONCURRENT)
|
||||
class MangaFetchIntervalTest {
|
||||
|
||||
private val testTime = ZonedDateTime.parse("2020-01-01T00:00:00Z")
|
||||
private val testZoneId = ZoneOffset.UTC
|
||||
private var chapter = Chapter.create().copy(
|
||||
dateFetch = testTime.toEpochSecond() * 1000,
|
||||
dateUpload = testTime.toEpochSecond() * 1000,
|
||||
)
|
||||
|
||||
private val fetchInterval = MangaFetchInterval(mockk())
|
||||
|
||||
@Test
|
||||
fun `returns default interval of 7 days when not enough distinct days`() {
|
||||
val chaptersWithUploadDate = (1..50).map {
|
||||
chapterWithTime(chapter, 1.days)
|
||||
}
|
||||
fetchInterval.calculateInterval(chaptersWithUploadDate, testZoneId) shouldBe 7
|
||||
|
||||
val chaptersWithoutUploadDate = chaptersWithUploadDate.map {
|
||||
it.copy(dateUpload = 0L)
|
||||
}
|
||||
fetchInterval.calculateInterval(chaptersWithoutUploadDate, testZoneId) shouldBe 7
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns interval based on more recent chapters`() {
|
||||
val oldChapters = (1..5).map {
|
||||
chapterWithTime(chapter, (it * 7).days) // Would have interval of 7 days
|
||||
}
|
||||
val newChapters = (1..10).map {
|
||||
chapterWithTime(chapter, oldChapters.lastUploadDate() + it.days)
|
||||
}
|
||||
|
||||
val chapters = oldChapters + newChapters
|
||||
|
||||
fetchInterval.calculateInterval(chapters, testZoneId) shouldBe 1
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns interval of 7 days when multiple chapters in 1 day`() {
|
||||
val chapters = (1..10).map {
|
||||
chapterWithTime(chapter, 10.hours)
|
||||
}
|
||||
fetchInterval.calculateInterval(chapters, testZoneId) shouldBe 7
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns interval of 7 days when multiple chapters in 2 days`() {
|
||||
val chapters = (1..2).map {
|
||||
chapterWithTime(chapter, 1.days)
|
||||
} + (1..5).map {
|
||||
chapterWithTime(chapter, 2.days)
|
||||
}
|
||||
fetchInterval.calculateInterval(chapters, testZoneId) shouldBe 7
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns interval of 1 day when chapters are released every 1 day`() {
|
||||
val chapters = (1..20).map {
|
||||
chapterWithTime(chapter, it.days)
|
||||
}
|
||||
fetchInterval.calculateInterval(chapters, testZoneId) shouldBe 1
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns interval of 1 day when delta is less than 1 day`() {
|
||||
val chapters = (1..20).map {
|
||||
chapterWithTime(chapter, (15 * it).hours)
|
||||
}
|
||||
fetchInterval.calculateInterval(chapters, testZoneId) shouldBe 1
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns interval of 2 days when chapters are released every 2 days`() {
|
||||
val chapters = (1..20).map {
|
||||
chapterWithTime(chapter, (2 * it).days)
|
||||
}
|
||||
fetchInterval.calculateInterval(chapters, testZoneId) shouldBe 2
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns interval with floored value when interval is decimal`() {
|
||||
val chaptersWithUploadDate = (1..5).map {
|
||||
chapterWithTime(chapter, (25 * it).hours)
|
||||
}
|
||||
fetchInterval.calculateInterval(chaptersWithUploadDate, testZoneId) shouldBe 1
|
||||
|
||||
val chaptersWithoutUploadDate = chaptersWithUploadDate.map {
|
||||
it.copy(dateUpload = 0L)
|
||||
}
|
||||
fetchInterval.calculateInterval(chaptersWithoutUploadDate, testZoneId) shouldBe 1
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `returns interval of 1 day when chapters are released just below every 2 days`() {
|
||||
val chapters = (1..20).map {
|
||||
chapterWithTime(chapter, (43 * it).hours)
|
||||
}
|
||||
fetchInterval.calculateInterval(chapters, testZoneId) shouldBe 1
|
||||
}
|
||||
|
||||
private fun chapterWithTime(chapter: Chapter, duration: Duration): Chapter {
|
||||
val newTime = testTime.plus(duration.toJavaDuration()).toEpochSecond() * 1000
|
||||
return chapter.copy(dateFetch = newTime, dateUpload = newTime)
|
||||
}
|
||||
|
||||
private fun List<Chapter>.lastUploadDate() =
|
||||
last().dateUpload.toDuration(DurationUnit.MILLISECONDS)
|
||||
}
|
|
@ -1,104 +0,0 @@
|
|||
package tachiyomi.domain.entries.manga.interactor
|
||||
|
||||
import io.kotest.matchers.shouldBe
|
||||
import io.mockk.mockk
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.parallel.Execution
|
||||
import org.junit.jupiter.api.parallel.ExecutionMode
|
||||
import tachiyomi.domain.items.chapter.model.Chapter
|
||||
import java.time.ZonedDateTime
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.hours
|
||||
import kotlin.time.toJavaDuration
|
||||
|
||||
@Execution(ExecutionMode.CONCURRENT)
|
||||
class SetMangaFetchIntervalTest {
|
||||
|
||||
private val testTime = ZonedDateTime.parse("2020-01-01T00:00:00Z")
|
||||
private var chapter = Chapter.create().copy(
|
||||
dateFetch = testTime.toEpochSecond() * 1000,
|
||||
dateUpload = testTime.toEpochSecond() * 1000,
|
||||
)
|
||||
|
||||
private val setMangaFetchInterval = SetMangaFetchInterval(mockk())
|
||||
|
||||
@Test
|
||||
fun `calculateInterval returns default of 7 days when less than 3 distinct days`() {
|
||||
val chapters = (1..2).map {
|
||||
chapterWithTime(chapter, 10.hours)
|
||||
}
|
||||
setMangaFetchInterval.calculateInterval(chapters, testTime) shouldBe 7
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `calculateInterval returns 7 when 5 chapters in 1 day`() {
|
||||
val chapters = (1..5).map {
|
||||
chapterWithTime(chapter, 10.hours)
|
||||
}
|
||||
setMangaFetchInterval.calculateInterval(chapters, testTime) shouldBe 7
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `calculateInterval returns 7 when 7 chapters in 48 hours, 2 day`() {
|
||||
val chapters = (1..2).map {
|
||||
chapterWithTime(chapter, 24.hours)
|
||||
} + (1..5).map {
|
||||
chapterWithTime(chapter, 48.hours)
|
||||
}
|
||||
setMangaFetchInterval.calculateInterval(chapters, testTime) shouldBe 7
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `calculateInterval returns default of 1 day when interval less than 1`() {
|
||||
val chapters = (1..5).map {
|
||||
chapterWithTime(chapter, (15 * it).hours)
|
||||
}
|
||||
setMangaFetchInterval.calculateInterval(chapters, testTime) shouldBe 1
|
||||
}
|
||||
|
||||
// Normal interval calculation
|
||||
@Test
|
||||
fun `calculateInterval returns 1 when 5 chapters in 120 hours, 5 days`() {
|
||||
val chapters = (1..5).map {
|
||||
chapterWithTime(chapter, (24 * it).hours)
|
||||
}
|
||||
setMangaFetchInterval.calculateInterval(chapters, testTime) shouldBe 1
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `calculateInterval returns 2 when 5 chapters in 240 hours, 10 days`() {
|
||||
val chapters = (1..5).map {
|
||||
chapterWithTime(chapter, (48 * it).hours)
|
||||
}
|
||||
setMangaFetchInterval.calculateInterval(chapters, testTime) shouldBe 2
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `calculateInterval returns floored value when interval is decimal`() {
|
||||
val chapters = (1..5).map {
|
||||
chapterWithTime(chapter, (25 * it).hours)
|
||||
}
|
||||
setMangaFetchInterval.calculateInterval(chapters, testTime) shouldBe 1
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `calculateInterval returns 1 when 5 chapters in 215 hours, 5 days`() {
|
||||
val chapters = (1..5).map {
|
||||
chapterWithTime(chapter, (43 * it).hours)
|
||||
}
|
||||
setMangaFetchInterval.calculateInterval(chapters, testTime) shouldBe 1
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `calculateInterval returns interval based on fetch time if upload time not available`() {
|
||||
val chapters = (1..5).map {
|
||||
chapterWithTime(chapter, (25 * it).hours).copy(dateUpload = 0L)
|
||||
}
|
||||
setMangaFetchInterval.calculateInterval(chapters, testTime) shouldBe 1
|
||||
}
|
||||
|
||||
private fun chapterWithTime(chapter: Chapter, duration: Duration): Chapter {
|
||||
val newTime = testTime.plus(duration.toJavaDuration()).toEpochSecond() * 1000
|
||||
return chapter.copy(dateFetch = newTime, dateUpload = newTime)
|
||||
}
|
||||
}
|
|
@ -28,7 +28,7 @@ guava = "com.google.guava:guava:32.1.2-android"
|
|||
paging-runtime = { module = "androidx.paging:paging-runtime", version.ref = "paging_version" }
|
||||
paging-compose = { module = "androidx.paging:paging-compose", version.ref = "paging_version" }
|
||||
|
||||
benchmark-macro = "androidx.benchmark:benchmark-macro-junit4:1.2.0-beta05"
|
||||
benchmark-macro = "androidx.benchmark:benchmark-macro-junit4:1.2.0-rc01"
|
||||
test-ext = "androidx.test.ext:junit-ktx:1.2.0-alpha01"
|
||||
test-espresso-core = "androidx.test.espresso:espresso-core:3.6.0-alpha01"
|
||||
test-uiautomator = "androidx.test.uiautomator:uiautomator:2.3.0-alpha04"
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
[versions]
|
||||
compiler = "1.5.2"
|
||||
compiler = "1.5.3"
|
||||
compose-bom = "2023.09.00-alpha02"
|
||||
accompanist = "0.33.1-alpha"
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
[versions]
|
||||
kotlin_version = "1.9.0"
|
||||
kotlin_version = "1.9.10"
|
||||
serialization_version = "1.6.0"
|
||||
xml_serialization_version = "0.86.1"
|
||||
xml_serialization_version = "0.86.2"
|
||||
|
||||
[libraries]
|
||||
reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin_version" }
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
[versions]
|
||||
aboutlib_version = "10.8.3"
|
||||
aboutlib_version = "10.9.0"
|
||||
okhttp_version = "5.0.0-alpha.11"
|
||||
shizuku_version = "12.2.0"
|
||||
sqlite = "2.3.1"
|
||||
|
@ -56,7 +56,7 @@ flexible-adapter-core = "com.github.arkon.FlexibleAdapter:flexible-adapter:c8013
|
|||
photoview = "com.github.chrisbanes:PhotoView:2.3.0"
|
||||
directionalviewpager = "com.github.tachiyomiorg:DirectionalViewPager:1.0.0"
|
||||
insetter = "dev.chrisbanes.insetter:insetter:0.6.1"
|
||||
compose-materialmotion = "io.github.fornewid:material-motion-compose-core:1.0.6"
|
||||
compose-materialmotion = "io.github.fornewid:material-motion-compose-core:1.1.0"
|
||||
compose-simpleicons = "br.com.devsrsouza.compose.icons.android:simple-icons:1.0.0"
|
||||
|
||||
swipe = "me.saket.swipe:swipe:1.2.0"
|
||||
|
@ -83,13 +83,13 @@ sqldelight-gradle = { module = "app.cash.sqldelight:gradle-plugin", version.ref
|
|||
|
||||
junit = "org.junit.jupiter:junit-jupiter:5.10.0"
|
||||
kotest-assertions = "io.kotest:kotest-assertions-core:5.7.2"
|
||||
mockk = "io.mockk:mockk:1.13.7"
|
||||
mockk = "io.mockk:mockk:1.13.8"
|
||||
|
||||
voyager-navigator = { module = "cafe.adriel.voyager:voyager-navigator", version.ref = "voyager" }
|
||||
voyager-tab-navigator = { module = "cafe.adriel.voyager:voyager-tab-navigator", version.ref = "voyager" }
|
||||
voyager-transitions = { module = "cafe.adriel.voyager:voyager-transitions", version.ref = "voyager" }
|
||||
|
||||
ktlint = "org.jlleitschuh.gradle:ktlint-gradle:11.5.1"
|
||||
ktlint = "org.jlleitschuh.gradle:ktlint-gradle:11.6.0"
|
||||
|
||||
aniyomi-mpv = "com.github.aniyomiorg:aniyomi-mpv-lib:1.10.n"
|
||||
ffmpeg-kit = "com.github.jmir1:ffmpeg-kit:1.10"
|
||||
|
|
|
@ -137,6 +137,7 @@
|
|||
<string name="action_move_to_top">Move to top</string>
|
||||
<string name="action_move_to_top_all_for_series">Move series to top</string>
|
||||
<string name="action_move_to_bottom">Move to bottom</string>
|
||||
<string name="action_move_to_bottom_all_for_series">Move series to bottom</string>
|
||||
<string name="action_install">Install</string>
|
||||
<string name="action_share">Share</string>
|
||||
<string name="action_save">Save</string>
|
||||
|
@ -205,6 +206,9 @@
|
|||
<string name="theme_matrix">Matrix</string>
|
||||
<string name="theme_tidalwave">Tidal Wave</string>
|
||||
<string name="pref_dark_theme_pure_black">Pure black dark mode</string>
|
||||
<string name="pref_relative_format">Relative timestamps</string>
|
||||
<!-- "Today" instead of "2023-12-31" -->
|
||||
<string name="pref_relative_format_summary">\"%1$s\" instead of \"%2$s\"</string>
|
||||
<string name="pref_date_format">Date format</string>
|
||||
|
||||
<string name="pref_manage_notifications">Manage notifications</string>
|
||||
|
@ -401,8 +405,8 @@
|
|||
<string name="double_tap_anim_speed_0">No animation</string>
|
||||
<string name="double_tap_anim_speed_normal">Normal</string>
|
||||
<string name="double_tap_anim_speed_fast">Fast</string>
|
||||
<string name="pref_rotation_type">Default rotation type</string>
|
||||
<string name="rotation_type">Rotation type</string>
|
||||
<string name="pref_rotation_type">Default rotation</string>
|
||||
<string name="rotation_type">Rotation</string>
|
||||
<string name="rotation_free">Free</string>
|
||||
<string name="rotation_portrait">Portrait</string>
|
||||
<string name="rotation_reverse_portrait">Reverse portrait</string>
|
||||
|
@ -451,13 +455,13 @@
|
|||
|
||||
<!-- Tracking section -->
|
||||
<string name="tracking_guide">Tracking guide</string>
|
||||
<string name="services">Services</string>
|
||||
<string name="tracking_info">One-way sync to update the chapter progress in tracking services. Set up tracking for individual entries from their tracking button.</string>
|
||||
<string name="enhanced_services">Enhanced services</string>
|
||||
<string name="services">Trackers</string>
|
||||
<string name="tracking_info">One-way sync to update the chapter progress in external tracker services. Set up tracking for individual entries from their tracking button.</string>
|
||||
<string name="enhanced_services">Enhanced trackers</string>
|
||||
<string name="enhanced_services_not_installed">Available but source not installed: %s</string>
|
||||
<string name="enhanced_tracking_info">Services that provide enhanced features for specific sources. Entries are automatically tracked when added to your library.</string>
|
||||
<string name="enhanced_tracking_info">Provides enhanced features for specific sources. Entries are automatically tracked when added to your library.</string>
|
||||
<string name="action_track">Track</string>
|
||||
<string name="track_activity_name">Tracking login</string>
|
||||
<string name="track_activity_name">Tracker login</string>
|
||||
|
||||
<!-- Browse section -->
|
||||
<string name="pref_hide_in_library_items">Hide entries already in library</string>
|
||||
|
|
|
@ -15,6 +15,9 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
|||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.widthIn
|
||||
import androidx.compose.foundation.lazy.grid.GridCells
|
||||
import androidx.compose.foundation.lazy.grid.LazyGridScope
|
||||
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.ContentAlpha
|
||||
import androidx.compose.material.icons.Icons
|
||||
|
@ -54,6 +57,7 @@ import kotlinx.coroutines.delay
|
|||
import tachiyomi.core.preference.Preference
|
||||
import tachiyomi.core.preference.TriState
|
||||
import tachiyomi.core.preference.toggle
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
import tachiyomi.presentation.core.theme.header
|
||||
import tachiyomi.presentation.core.util.collectAsState
|
||||
|
||||
|
@ -297,7 +301,7 @@ fun TriStateItem(
|
|||
vertical = SettingsItemsPaddings.Vertical,
|
||||
),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(24.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.large),
|
||||
) {
|
||||
val stateAlpha = if (enabled && onClick != null) 1f else ContentAlpha.disabled
|
||||
|
||||
|
@ -504,7 +508,25 @@ fun SettingsChipRow(
|
|||
end = SettingsItemsPaddings.Horizontal,
|
||||
bottom = SettingsItemsPaddings.Vertical,
|
||||
),
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SettingsIconGrid(@StringRes labelRes: Int, content: LazyGridScope.() -> Unit) {
|
||||
Column {
|
||||
HeadingItem(labelRes)
|
||||
LazyVerticalGrid(
|
||||
columns = GridCells.Adaptive(128.dp),
|
||||
modifier = Modifier.padding(
|
||||
start = SettingsItemsPaddings.Horizontal,
|
||||
end = SettingsItemsPaddings.Horizontal,
|
||||
bottom = SettingsItemsPaddings.Vertical,
|
||||
),
|
||||
verticalArrangement = Arrangement.spacedBy(MaterialTheme.padding.tiny),
|
||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
|
||||
content = content,
|
||||
)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
package tachiyomi.presentation.core.components.material
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.FilledIconToggleButton
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
fun IconToggleButton(
|
||||
checked: Boolean,
|
||||
onCheckedChange: (Boolean) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
imageVector: ImageVector,
|
||||
title: String,
|
||||
) {
|
||||
FilledIconToggleButton(
|
||||
checked = checked,
|
||||
onCheckedChange = onCheckedChange,
|
||||
modifier = modifier
|
||||
.height(48.dp),
|
||||
) {
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(MaterialTheme.padding.small),
|
||||
) {
|
||||
Icon(
|
||||
imageVector = imageVector,
|
||||
contentDescription = null,
|
||||
)
|
||||
|
||||
Text(
|
||||
text = title,
|
||||
maxLines = 2,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.animesource
|
|||
import eu.kanade.tachiyomi.animesource.model.AnimeFilterList
|
||||
import eu.kanade.tachiyomi.animesource.model.AnimesPage
|
||||
import rx.Observable
|
||||
import tachiyomi.core.util.lang.awaitSingle
|
||||
|
||||
interface AnimeCatalogueSource : AnimeSource {
|
||||
|
||||
|
@ -17,30 +18,63 @@ interface AnimeCatalogueSource : AnimeSource {
|
|||
val supportsLatest: Boolean
|
||||
|
||||
/**
|
||||
* Returns an observable containing a page with a list of anime.
|
||||
* Get a page with a list of anime.
|
||||
*
|
||||
* @since extensions-lib 1.5
|
||||
* @param page the page number to retrieve.
|
||||
*/
|
||||
fun fetchPopularAnime(page: Int): Observable<AnimesPage>
|
||||
@Suppress("DEPRECATION")
|
||||
suspend fun getPopularAnime(page: Int): AnimesPage {
|
||||
return fetchPopularAnime(page).awaitSingle()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an observable containing a page with a list of anime.
|
||||
* Get a page with a list of anime.
|
||||
*
|
||||
* @since extensions-lib 1.5
|
||||
* @param page the page number to retrieve.
|
||||
* @param query the search query.
|
||||
* @param filters the list of filters to apply.
|
||||
*/
|
||||
fun fetchSearchAnime(page: Int, query: String, filters: AnimeFilterList): Observable<AnimesPage>
|
||||
@Suppress("DEPRECATION")
|
||||
suspend fun getSearchAnime(page: Int, query: String, filters: AnimeFilterList): AnimesPage {
|
||||
return fetchSearchAnime(page, query, filters).awaitSingle()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an observable containing a page with a list of latest anime updates.
|
||||
* Get a page with a list of latest anime updates.
|
||||
*
|
||||
* @since extensions-lib 1.5
|
||||
* @param page the page number to retrieve.
|
||||
*/
|
||||
fun fetchLatestUpdates(page: Int): Observable<AnimesPage>
|
||||
@Suppress("DEPRECATION")
|
||||
suspend fun getLatestUpdates(page: Int): AnimesPage {
|
||||
return fetchLatestUpdates(page).awaitSingle()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of filters for the source.
|
||||
*/
|
||||
fun getFilterList(): AnimeFilterList
|
||||
|
||||
// Should be replaced as soon as Anime Extension reach 1.5
|
||||
@Deprecated(
|
||||
"Use the non-RxJava API instead",
|
||||
ReplaceWith("getPopularAnime"),
|
||||
)
|
||||
fun fetchPopularAnime(page: Int): Observable<AnimesPage>
|
||||
|
||||
// Should be replaced as soon as Anime Extension reach 1.5
|
||||
@Deprecated(
|
||||
"Use the non-RxJava API instead",
|
||||
ReplaceWith("getSearchAnime"),
|
||||
)
|
||||
fun fetchSearchAnime(page: Int, query: String, filters: AnimeFilterList): Observable<AnimesPage>
|
||||
|
||||
// Should be replaced as soon as Anime Extension reach 1.5
|
||||
@Deprecated(
|
||||
"Use the non-RxJava API instead",
|
||||
ReplaceWith("getLatestUpdates"),
|
||||
)
|
||||
fun fetchLatestUpdates(page: Int): Observable<AnimesPage>
|
||||
}
|
||||
|
|
|
@ -27,7 +27,7 @@ interface AnimeSource {
|
|||
/**
|
||||
* Get the updated details for a anime.
|
||||
*
|
||||
* @since extensions-lib 1.4
|
||||
* @since extensions-lib 1.5
|
||||
* @param anime the anime to update.
|
||||
* @return the updated anime.
|
||||
*/
|
||||
|
@ -39,7 +39,7 @@ interface AnimeSource {
|
|||
/**
|
||||
* Get all the available episodes for a anime.
|
||||
*
|
||||
* @since extensions-lib 1.4
|
||||
* @since extensions-lib 1.5
|
||||
* @param anime the anime to update.
|
||||
* @return the episodes for the anime.
|
||||
*/
|
||||
|
@ -52,7 +52,7 @@ interface AnimeSource {
|
|||
* Get the list of videos a episode has. Pages should be returned
|
||||
* in the expected order; the index is ignored.
|
||||
*
|
||||
* @since extensions-lib 1.4
|
||||
* @since extensions-lib 1.5
|
||||
* @param episode the episode.
|
||||
* @return the videos for the episode.
|
||||
*/
|
||||
|
@ -61,41 +61,24 @@ interface AnimeSource {
|
|||
return fetchVideoList(episode).awaitSingle()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an observable with the updated details for a anime.
|
||||
*
|
||||
* @param anime the anime to update.
|
||||
*/
|
||||
@Deprecated(
|
||||
"Use the non-RxJava API instead",
|
||||
ReplaceWith("getAnimeDetails"),
|
||||
)
|
||||
fun fetchAnimeDetails(anime: SAnime): Observable<SAnime> = throw IllegalStateException(
|
||||
"Not used",
|
||||
)
|
||||
fun fetchAnimeDetails(anime: SAnime): Observable<SAnime> =
|
||||
throw IllegalStateException("Not used")
|
||||
|
||||
/**
|
||||
* Returns an observable with all the available episodes for a anime.
|
||||
*
|
||||
* @param anime the anime to update.
|
||||
*/
|
||||
@Deprecated(
|
||||
"Use the non-RxJava API instead",
|
||||
ReplaceWith("getEpisodeList"),
|
||||
)
|
||||
fun fetchEpisodeList(anime: SAnime): Observable<List<SEpisode>> = throw IllegalStateException(
|
||||
"Not used",
|
||||
)
|
||||
fun fetchEpisodeList(anime: SAnime): Observable<List<SEpisode>> =
|
||||
throw IllegalStateException("Not used")
|
||||
|
||||
/**
|
||||
* Returns an observable with the list of videos a episode has. Videos should be returned
|
||||
* in the expected order; the index is ignored.
|
||||
*
|
||||
* @param episode the episode.
|
||||
*/
|
||||
@Deprecated(
|
||||
"Use the non-RxJava API instead",
|
||||
ReplaceWith("getVideoList"),
|
||||
)
|
||||
fun fetchVideoList(episode: SEpisode): Observable<List<Video>> = Observable.empty()
|
||||
fun fetchVideoList(episode: SEpisode): Observable<List<Video>> =
|
||||
throw IllegalStateException("Not used")
|
||||
}
|
||||
|
|
|
@ -1,8 +1,21 @@
|
|||
package eu.kanade.tachiyomi.animesource
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import eu.kanade.tachiyomi.PreferenceScreen
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
interface ConfigurableAnimeSource : AnimeSource {
|
||||
|
||||
/**
|
||||
* Gets instance of [SharedPreferences] scoped to the specific source.
|
||||
*
|
||||
* @since extensions-lib 1.5
|
||||
*/
|
||||
fun getPreferences(): SharedPreferences =
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", Context.MODE_PRIVATE)
|
||||
|
||||
fun setupPreferenceScreen(screen: PreferenceScreen)
|
||||
}
|
||||
|
|
|
@ -7,30 +7,25 @@ import eu.kanade.tachiyomi.animesource.model.SAnime
|
|||
import eu.kanade.tachiyomi.animesource.model.SEpisode
|
||||
import eu.kanade.tachiyomi.animesource.model.Video
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.HttpException
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||
import eu.kanade.tachiyomi.network.awaitSuccess
|
||||
import eu.kanade.tachiyomi.network.newCachelessCallWithProgress
|
||||
import okhttp3.Call
|
||||
import okhttp3.Callback
|
||||
import okhttp3.Headers
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import rx.Observable
|
||||
import tachiyomi.core.util.lang.awaitSingle
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.IOException
|
||||
import java.net.URI
|
||||
import java.net.URISyntaxException
|
||||
import java.security.MessageDigest
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
/**
|
||||
* A simple implementation for sources from a website.
|
||||
*/
|
||||
@Suppress("unused")
|
||||
abstract class AnimeHttpSource : AnimeCatalogueSource {
|
||||
|
||||
/**
|
||||
|
@ -88,6 +83,7 @@ abstract class AnimeHttpSource : AnimeCatalogueSource {
|
|||
* @param versionId [Int] the version ID of the source
|
||||
* @return a unique ID for the source
|
||||
*/
|
||||
@Suppress("MemberVisibilityCanBePrivate")
|
||||
protected fun generateId(name: String, lang: String, versionId: Int): Long {
|
||||
val key = "${name.lowercase()}/$lang/$versionId"
|
||||
val bytes = MessageDigest.getInstance("MD5").digest(key.toByteArray())
|
||||
|
@ -201,11 +197,18 @@ abstract class AnimeHttpSource : AnimeCatalogueSource {
|
|||
protected abstract fun latestUpdatesParse(response: Response): AnimesPage
|
||||
|
||||
/**
|
||||
* Returns an observable with the updated details for a nanime. Normally it's not needed to
|
||||
* override this method.
|
||||
* Get the updated details for a anime.
|
||||
* Normally it's not needed to override this method.
|
||||
*
|
||||
* @param anime the anime to be updated.
|
||||
* @return the updated anime.
|
||||
*/
|
||||
@Suppress("DEPRECATION")
|
||||
override suspend fun getAnimeDetails(anime: SAnime): SAnime {
|
||||
return fetchAnimeDetails(anime).awaitSingle()
|
||||
}
|
||||
|
||||
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getAnimeDetails"))
|
||||
override fun fetchAnimeDetails(anime: SAnime): Observable<SAnime> {
|
||||
return client.newCall(animeDetailsRequest(anime))
|
||||
.asObservableSuccess()
|
||||
|
@ -232,11 +235,23 @@ abstract class AnimeHttpSource : AnimeCatalogueSource {
|
|||
protected abstract fun animeDetailsParse(response: Response): SAnime
|
||||
|
||||
/**
|
||||
* Returns an observable with the updated episode list for an anime. Normally it's not needed to
|
||||
* override this method. If an anime is licensed an empty episode list observable is returned
|
||||
* Get all the available episodes for an anime.
|
||||
* Normally it's not needed to override this method.
|
||||
*
|
||||
* @param anime the anime to look for episodes.
|
||||
* @param anime the anime to update.
|
||||
* @return the chapters for the manga.
|
||||
* @throws LicensedEntryItemsException if a anime is licensed and therefore no episodes are available.
|
||||
*/
|
||||
/*@Suppress("DEPRECATION")
|
||||
override suspend fun getEpisodeList(anime: SAnime): List<SEpisode> {
|
||||
if (anime.status == SAnime.LICENSED) {
|
||||
throw LicensedEntryItemsException()
|
||||
}
|
||||
|
||||
return fetchEpisodeList(anime).awaitSingle()
|
||||
}*/
|
||||
|
||||
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getEpisodeList"))
|
||||
override fun fetchEpisodeList(anime: SAnime): Observable<List<SEpisode>> {
|
||||
return if (anime.status != SAnime.LICENSED) {
|
||||
client.newCall(episodeListRequest(anime))
|
||||
|
@ -267,10 +282,18 @@ abstract class AnimeHttpSource : AnimeCatalogueSource {
|
|||
protected abstract fun episodeListParse(response: Response): List<SEpisode>
|
||||
|
||||
/**
|
||||
* Returns an observable with the page list for a chapter.
|
||||
* Get the list of videos a episode has. Videos should be returned
|
||||
* in the expected order; the index is ignored.
|
||||
*
|
||||
* @param episode the episode whose video list has to be fetched.
|
||||
* @param episode the episode.
|
||||
* @return the videos for the episode.
|
||||
*/
|
||||
/*@Suppress("DEPRECATION")
|
||||
override suspend fun getVideoList(episode: SEpisode): List<Video> {
|
||||
return fetchVideoList(episode).awaitSingle()
|
||||
}*/
|
||||
|
||||
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getVideoList"))
|
||||
override fun fetchVideoList(episode: SEpisode): Observable<List<Video>> {
|
||||
return client.newCall(videoListRequest(episode))
|
||||
.asObservableSuccess()
|
||||
|
@ -307,8 +330,15 @@ abstract class AnimeHttpSource : AnimeCatalogueSource {
|
|||
* Returns an observable with the page containing the source url of the image. If there's any
|
||||
* error, it will return null instead of throwing an exception.
|
||||
*
|
||||
* @since extensions-lib 1.5
|
||||
* @param video the video whose source image has to be fetched.
|
||||
*/
|
||||
/*@Suppress("DEPRECATION")
|
||||
open suspend fun getVideoUrl(video: Video): String {
|
||||
return fetchVideoUrl(video).awaitSingle()
|
||||
}*/
|
||||
|
||||
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getVideoUrl"))
|
||||
open fun fetchVideoUrl(video: Video): Observable<String> {
|
||||
return client.newCall(videoUrlRequest(video))
|
||||
.asObservableSuccess()
|
||||
|
@ -333,36 +363,15 @@ abstract class AnimeHttpSource : AnimeCatalogueSource {
|
|||
protected abstract fun videoUrlParse(response: Response): String
|
||||
|
||||
/**
|
||||
* Returns an observable with the response of the source image.
|
||||
* Returns the response of the source video.
|
||||
* Typically does not need to be overridden.
|
||||
*
|
||||
* @param video the page whose source image has to be downloaded.
|
||||
* @since extensions-lib 1.5
|
||||
* @param video the video whose source video has to be downloaded.
|
||||
*/
|
||||
suspend fun fetchVideo(video: Video): Response {
|
||||
val animeDownloadClient = client.newBuilder()
|
||||
.callTimeout(30, TimeUnit.MINUTES)
|
||||
.build()
|
||||
|
||||
return suspendCoroutine { continuation ->
|
||||
animeDownloadClient.newCachelessCallWithProgress(
|
||||
videoRequest(video, video.totalBytesDownloaded),
|
||||
video,
|
||||
)
|
||||
.enqueue(
|
||||
object : Callback {
|
||||
override fun onFailure(call: Call, e: IOException) {
|
||||
continuation.resumeWithException(e)
|
||||
}
|
||||
|
||||
override fun onResponse(call: Call, response: Response) {
|
||||
if (response.isSuccessful) {
|
||||
continuation.resume(response)
|
||||
} else {
|
||||
continuation.resumeWithException(HttpException(response.code))
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
open suspend fun getVideo(video: Video): Response {
|
||||
return client.newCachelessCallWithProgress(videoRequest(video), video)
|
||||
.awaitSuccess()
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -440,7 +449,7 @@ abstract class AnimeHttpSource : AnimeCatalogueSource {
|
|||
* @param episode the episode
|
||||
* @return url of the episode
|
||||
*/
|
||||
open fun getChapterUrl(episode: SEpisode): String {
|
||||
open fun getEpisodeUrl(episode: SEpisode): String {
|
||||
return episode.url.toString()
|
||||
}
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.source
|
|||
import eu.kanade.tachiyomi.source.model.FilterList
|
||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||
import rx.Observable
|
||||
import tachiyomi.core.util.lang.awaitSingle
|
||||
|
||||
interface CatalogueSource : MangaSource {
|
||||
|
||||
|
@ -17,30 +18,63 @@ interface CatalogueSource : MangaSource {
|
|||
val supportsLatest: Boolean
|
||||
|
||||
/**
|
||||
* Returns an observable containing a page with a list of manga.
|
||||
* Get a page with a list of manga.
|
||||
*
|
||||
* @since extensions-lib 1.5
|
||||
* @param page the page number to retrieve.
|
||||
*/
|
||||
fun fetchPopularManga(page: Int): Observable<MangasPage>
|
||||
@Suppress("DEPRECATION")
|
||||
suspend fun getPopularManga(page: Int): MangasPage {
|
||||
return fetchPopularManga(page).awaitSingle()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an observable containing a page with a list of manga.
|
||||
* Get a page with a list of manga.
|
||||
*
|
||||
* @since extensions-lib 1.5
|
||||
* @param page the page number to retrieve.
|
||||
* @param query the search query.
|
||||
* @param filters the list of filters to apply.
|
||||
*/
|
||||
fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage>
|
||||
@Suppress("DEPRECATION")
|
||||
suspend fun getSearchManga(page: Int, query: String, filters: FilterList): MangasPage {
|
||||
return fetchSearchManga(page, query, filters).awaitSingle()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an observable containing a page with a list of latest manga updates.
|
||||
* Get a page with a list of latest manga updates.
|
||||
*
|
||||
* @since extensions-lib 1.5
|
||||
* @param page the page number to retrieve.
|
||||
*/
|
||||
fun fetchLatestUpdates(page: Int): Observable<MangasPage>
|
||||
@Suppress("DEPRECATION")
|
||||
suspend fun getLatestUpdates(page: Int): MangasPage {
|
||||
return fetchLatestUpdates(page).awaitSingle()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the list of filters for the source.
|
||||
*/
|
||||
fun getFilterList(): FilterList
|
||||
|
||||
@Deprecated(
|
||||
"Use the non-RxJava API instead",
|
||||
ReplaceWith("getPopularManga"),
|
||||
)
|
||||
fun fetchPopularManga(page: Int): Observable<MangasPage> =
|
||||
throw IllegalStateException("Not used")
|
||||
|
||||
@Deprecated(
|
||||
"Use the non-RxJava API instead",
|
||||
ReplaceWith("getSearchManga"),
|
||||
)
|
||||
fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> =
|
||||
throw IllegalStateException("Not used")
|
||||
|
||||
@Deprecated(
|
||||
"Use the non-RxJava API instead",
|
||||
ReplaceWith("getLatestUpdates"),
|
||||
)
|
||||
fun fetchLatestUpdates(page: Int): Observable<MangasPage> =
|
||||
throw IllegalStateException("Not used")
|
||||
}
|
||||
|
|
|
@ -1,8 +1,21 @@
|
|||
package eu.kanade.tachiyomi.source
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import eu.kanade.tachiyomi.PreferenceScreen
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
interface ConfigurableSource : MangaSource {
|
||||
|
||||
/**
|
||||
* Gets instance of [SharedPreferences] scoped to the specific source.
|
||||
*
|
||||
* @since extensions-lib 1.5
|
||||
*/
|
||||
fun getPreferences(): SharedPreferences =
|
||||
Injekt.get<Application>().getSharedPreferences("source_$id", Context.MODE_PRIVATE)
|
||||
|
||||
fun setupPreferenceScreen(screen: PreferenceScreen)
|
||||
}
|
||||
|
|
|
@ -27,7 +27,7 @@ interface MangaSource {
|
|||
/**
|
||||
* Get the updated details for a manga.
|
||||
*
|
||||
* @since extensions-lib 1.4
|
||||
* @since extensions-lib 1.5
|
||||
* @param manga the manga to update.
|
||||
* @return the updated manga.
|
||||
*/
|
||||
|
@ -39,7 +39,7 @@ interface MangaSource {
|
|||
/**
|
||||
* Get all the available chapters for a manga.
|
||||
*
|
||||
* @since extensions-lib 1.4
|
||||
* @since extensions-lib 1.5
|
||||
* @param manga the manga to update.
|
||||
* @return the chapters for the manga.
|
||||
*/
|
||||
|
@ -52,7 +52,7 @@ interface MangaSource {
|
|||
* Get the list of pages a chapter has. Pages should be returned
|
||||
* in the expected order; the index is ignored.
|
||||
*
|
||||
* @since extensions-lib 1.4
|
||||
* @since extensions-lib 1.5
|
||||
* @param chapter the chapter.
|
||||
* @return the pages for the chapter.
|
||||
*/
|
||||
|
@ -61,41 +61,24 @@ interface MangaSource {
|
|||
return fetchPageList(chapter).awaitSingle()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an observable with the updated details for a manga.
|
||||
*
|
||||
* @param manga the manga to update.
|
||||
*/
|
||||
@Deprecated(
|
||||
"Use the non-RxJava API instead",
|
||||
ReplaceWith("getMangaDetails"),
|
||||
)
|
||||
fun fetchMangaDetails(manga: SManga): Observable<SManga> = throw IllegalStateException(
|
||||
"Not used",
|
||||
)
|
||||
fun fetchMangaDetails(manga: SManga): Observable<SManga> =
|
||||
throw IllegalStateException("Not used")
|
||||
|
||||
/**
|
||||
* Returns an observable with all the available chapters for a manga.
|
||||
*
|
||||
* @param manga the manga to update.
|
||||
*/
|
||||
@Deprecated(
|
||||
"Use the non-RxJava API instead",
|
||||
ReplaceWith("getChapterList"),
|
||||
)
|
||||
fun fetchChapterList(manga: SManga): Observable<List<SChapter>> = throw IllegalStateException(
|
||||
"Not used",
|
||||
)
|
||||
fun fetchChapterList(manga: SManga): Observable<List<SChapter>> =
|
||||
throw IllegalStateException("Not used")
|
||||
|
||||
/**
|
||||
* Returns an observable with the list of pages a chapter has. Pages should be returned
|
||||
* in the expected order; the index is ignored.
|
||||
*
|
||||
* @param chapter the chapter.
|
||||
*/
|
||||
@Deprecated(
|
||||
"Use the non-RxJava API instead",
|
||||
ReplaceWith("getPageList"),
|
||||
)
|
||||
fun fetchPageList(chapter: SChapter): Observable<List<Page>> = Observable.empty()
|
||||
fun fetchPageList(chapter: SChapter): Observable<List<Page>> =
|
||||
throw IllegalStateException("Not used")
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ import okhttp3.OkHttpClient
|
|||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import rx.Observable
|
||||
import tachiyomi.core.util.lang.awaitSingle
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.net.URI
|
||||
import java.net.URISyntaxException
|
||||
|
@ -24,6 +25,7 @@ import java.security.MessageDigest
|
|||
/**
|
||||
* A simple implementation for sources from a website.
|
||||
*/
|
||||
@Suppress("unused")
|
||||
abstract class HttpSource : CatalogueSource {
|
||||
|
||||
/**
|
||||
|
@ -81,6 +83,7 @@ abstract class HttpSource : CatalogueSource {
|
|||
* @param versionId [Int] the version ID of the source
|
||||
* @return a unique ID for the source
|
||||
*/
|
||||
@Suppress("MemberVisibilityCanBePrivate")
|
||||
protected fun generateId(name: String, lang: String, versionId: Int): Long {
|
||||
val key = "${name.lowercase()}/$lang/$versionId"
|
||||
val bytes = MessageDigest.getInstance("MD5").digest(key.toByteArray())
|
||||
|
@ -194,11 +197,18 @@ abstract class HttpSource : CatalogueSource {
|
|||
protected abstract fun latestUpdatesParse(response: Response): MangasPage
|
||||
|
||||
/**
|
||||
* Returns an observable with the updated details for a manga. Normally it's not needed to
|
||||
* override this method.
|
||||
* Get the updated details for a manga.
|
||||
* Normally it's not needed to override this method.
|
||||
*
|
||||
* @param manga the manga to be updated.
|
||||
* @return the updated manga.
|
||||
*/
|
||||
@Suppress("DEPRECATION")
|
||||
override suspend fun getMangaDetails(manga: SManga): SManga {
|
||||
return fetchMangaDetails(manga).awaitSingle()
|
||||
}
|
||||
|
||||
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getMangaDetails"))
|
||||
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
||||
return client.newCall(mangaDetailsRequest(manga))
|
||||
.asObservableSuccess()
|
||||
|
@ -225,11 +235,23 @@ abstract class HttpSource : CatalogueSource {
|
|||
protected abstract fun mangaDetailsParse(response: Response): SManga
|
||||
|
||||
/**
|
||||
* Returns an observable with the updated chapter list for a manga. Normally it's not needed to
|
||||
* override this method. If a manga is licensed an empty chapter list observable is returned
|
||||
* Get all the available chapters for a manga.
|
||||
* Normally it's not needed to override this method.
|
||||
*
|
||||
* @param manga the manga to look for chapters.
|
||||
* @param manga the manga to update.
|
||||
* @return the chapters for the manga.
|
||||
* @throws LicensedEntryItemsException if a manga is licensed and therefore no chapters are available.
|
||||
*/
|
||||
@Suppress("DEPRECATION")
|
||||
override suspend fun getChapterList(manga: SManga): List<SChapter> {
|
||||
if (manga.status == SManga.LICENSED) {
|
||||
throw LicensedEntryItemsException()
|
||||
}
|
||||
|
||||
return fetchChapterList(manga).awaitSingle()
|
||||
}
|
||||
|
||||
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getChapterList"))
|
||||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
||||
return if (manga.status != SManga.LICENSED) {
|
||||
client.newCall(chapterListRequest(manga))
|
||||
|
@ -260,10 +282,18 @@ abstract class HttpSource : CatalogueSource {
|
|||
protected abstract fun chapterListParse(response: Response): List<SChapter>
|
||||
|
||||
/**
|
||||
* Returns an observable with the page list for a chapter.
|
||||
* Get the list of pages a chapter has. Pages should be returned
|
||||
* in the expected order; the index is ignored.
|
||||
*
|
||||
* @param chapter the chapter whose page list has to be fetched.
|
||||
* @param chapter the chapter.
|
||||
* @return the pages for the chapter.
|
||||
*/
|
||||
@Suppress("DEPRECATION")
|
||||
override suspend fun getPageList(chapter: SChapter): List<Page> {
|
||||
return fetchPageList(chapter).awaitSingle()
|
||||
}
|
||||
|
||||
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getPageList"))
|
||||
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
|
||||
return client.newCall(pageListRequest(chapter))
|
||||
.asObservableSuccess()
|
||||
|
@ -293,8 +323,15 @@ abstract class HttpSource : CatalogueSource {
|
|||
* Returns an observable with the page containing the source url of the image. If there's any
|
||||
* error, it will return null instead of throwing an exception.
|
||||
*
|
||||
* @since extensions-lib 1.5
|
||||
* @param page the page whose source image has to be fetched.
|
||||
*/
|
||||
@Suppress("DEPRECATION")
|
||||
open suspend fun getImageUrl(page: Page): String {
|
||||
return fetchImageUrl(page).awaitSingle()
|
||||
}
|
||||
|
||||
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getImageUrl"))
|
||||
open fun fetchImageUrl(page: Page): Observable<String> {
|
||||
return client.newCall(imageUrlRequest(page))
|
||||
.asObservableSuccess()
|
||||
|
@ -318,24 +355,14 @@ abstract class HttpSource : CatalogueSource {
|
|||
*/
|
||||
protected abstract fun imageUrlParse(response: Response): String
|
||||
|
||||
/**
|
||||
* Returns an observable with the response of the source image.
|
||||
*
|
||||
* @param page the page whose source image has to be downloaded.
|
||||
*/
|
||||
fun fetchImage(page: Page): Observable<Response> {
|
||||
// images will be cached or saved manually, so don't take up network cache
|
||||
return client.newCachelessCallWithProgress(imageRequest(page), page)
|
||||
.asObservableSuccess()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the response of the source image.
|
||||
* Typically does not need to be overridden.
|
||||
*
|
||||
* @since extensions-lib 1.5
|
||||
* @param page the page whose source image has to be downloaded.
|
||||
*/
|
||||
open suspend fun getImage(page: Page): Response {
|
||||
// images will be cached or saved manually, so don't take up network cache
|
||||
return client.newCachelessCallWithProgress(imageRequest(page), page)
|
||||
.awaitSuccess()
|
||||
}
|
||||
|
|
|
@ -1,25 +0,0 @@
|
|||
package eu.kanade.tachiyomi.source.online
|
||||
|
||||
import eu.kanade.tachiyomi.source.model.Page
|
||||
import rx.Observable
|
||||
|
||||
fun HttpSource.fetchAllImageUrlsFromPageList(pages: List<Page>): Observable<Page> {
|
||||
return Observable.from(pages)
|
||||
.filter { !it.imageUrl.isNullOrEmpty() }
|
||||
.mergeWith(fetchRemainingImageUrlsFromPageList(pages))
|
||||
}
|
||||
|
||||
private fun HttpSource.fetchRemainingImageUrlsFromPageList(pages: List<Page>): Observable<Page> {
|
||||
return Observable.from(pages)
|
||||
.filter { it.imageUrl.isNullOrEmpty() }
|
||||
.concatMap { getImageUrl(it) }
|
||||
}
|
||||
|
||||
private fun HttpSource.getImageUrl(page: Page): Observable<Page> {
|
||||
page.status = Page.State.LOAD_PAGE
|
||||
return fetchImageUrl(page)
|
||||
.doOnError { page.status = Page.State.ERROR }
|
||||
.onErrorReturn { null }
|
||||
.doOnNext { page.imageUrl = it }
|
||||
.map { page }
|
||||
}
|
|
@ -56,11 +56,11 @@ actual class LocalAnimeSource(
|
|||
override val supportsLatest = true
|
||||
|
||||
// Browse related
|
||||
override fun fetchPopularAnime(page: Int) = fetchSearchAnime(page, "", POPULAR_FILTERS)
|
||||
override suspend fun getPopularAnime(page: Int) = getSearchAnime(page, "", POPULAR_FILTERS)
|
||||
|
||||
override fun fetchLatestUpdates(page: Int) = fetchSearchAnime(page, "", LATEST_FILTERS)
|
||||
override suspend fun getLatestUpdates(page: Int) = getSearchAnime(page, "", LATEST_FILTERS)
|
||||
|
||||
override fun fetchSearchAnime(page: Int, query: String, filters: AnimeFilterList): Observable<AnimesPage> {
|
||||
override suspend fun getSearchAnime(page: Int, query: String, filters: AnimeFilterList): AnimesPage {
|
||||
val baseDirsFiles = fileSystem.getFilesInBaseDirectories()
|
||||
val lastModifiedLimit by lazy { if (filters === LATEST_FILTERS) System.currentTimeMillis() - LATEST_THRESHOLD else 0L }
|
||||
|
||||
|
@ -132,6 +132,85 @@ actual class LocalAnimeSource(
|
|||
}
|
||||
}
|
||||
|
||||
return AnimesPage(animes.toList(), false)
|
||||
}
|
||||
|
||||
// Old fetch functions
|
||||
|
||||
// TODO: Should be replaced when Anime Extensions get to 1.15
|
||||
|
||||
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getPopularAnime"))
|
||||
override fun fetchPopularAnime(page: Int) = fetchSearchAnime(page, "", POPULAR_FILTERS)
|
||||
|
||||
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getLatestUpdates"))
|
||||
override fun fetchLatestUpdates(page: Int) = fetchSearchAnime(page, "", LATEST_FILTERS)
|
||||
|
||||
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getSearchAnime"))
|
||||
override fun fetchSearchAnime(page: Int, query: String, filters: AnimeFilterList): Observable<AnimesPage> {
|
||||
val baseDirsFiles = fileSystem.getFilesInBaseDirectories()
|
||||
val lastModifiedLimit by lazy { if (filters === LATEST_FILTERS) System.currentTimeMillis() - LATEST_THRESHOLD else 0L }
|
||||
var animeDirs = baseDirsFiles
|
||||
// Filter out files that are hidden and is not a folder
|
||||
.filter { it.isDirectory && !it.name.startsWith('.') }
|
||||
.distinctBy { it.name }
|
||||
.filter { // Filter by query or last modified
|
||||
if (lastModifiedLimit == 0L) {
|
||||
it.name.contains(query, ignoreCase = true)
|
||||
} else {
|
||||
it.lastModified() >= lastModifiedLimit
|
||||
}
|
||||
}
|
||||
filters.forEach { filter ->
|
||||
when (filter) {
|
||||
is AnimeOrderBy.Popular -> {
|
||||
animeDirs = if (filter.state!!.ascending) {
|
||||
animeDirs.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name })
|
||||
} else {
|
||||
animeDirs.sortedWith(
|
||||
compareByDescending(String.CASE_INSENSITIVE_ORDER) { it.name },
|
||||
)
|
||||
}
|
||||
}
|
||||
is AnimeOrderBy.Latest -> {
|
||||
animeDirs = if (filter.state!!.ascending) {
|
||||
animeDirs.sortedBy(File::lastModified)
|
||||
} else {
|
||||
animeDirs.sortedByDescending(File::lastModified)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
/* Do nothing */
|
||||
}
|
||||
}
|
||||
}
|
||||
// Transform animeDirs to list of SAnime
|
||||
val animes = animeDirs.map { animeDir ->
|
||||
SAnime.create().apply {
|
||||
title = animeDir.name
|
||||
url = animeDir.name
|
||||
// Try to find the cover
|
||||
coverManager.find(animeDir.name)
|
||||
?.takeIf(File::exists)
|
||||
?.let { thumbnail_url = it.absolutePath }
|
||||
}
|
||||
}
|
||||
// Fetch episodes of all the anime
|
||||
animes.forEach { anime ->
|
||||
runBlocking {
|
||||
val episodes = getEpisodeList(anime)
|
||||
if (episodes.isNotEmpty()) {
|
||||
val episode = episodes.last()
|
||||
// Copy the cover from the first episode found if not available
|
||||
if (anime.thumbnail_url == null) {
|
||||
try {
|
||||
updateCoverFromVideo(episode, anime)
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.ERROR) { "Couldn't extract thumbnail from video." }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return Observable.just(AnimesPage(animes.toList(), false))
|
||||
}
|
||||
|
||||
|
|
|
@ -16,7 +16,6 @@ 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.metadata.comicinfo.COMIC_INFO_FILE
|
||||
import tachiyomi.core.metadata.comicinfo.ComicInfo
|
||||
import tachiyomi.core.metadata.comicinfo.copyFromComicInfo
|
||||
|
@ -70,11 +69,11 @@ actual class LocalMangaSource(
|
|||
override val supportsLatest: Boolean = true
|
||||
|
||||
// Browse related
|
||||
override fun fetchPopularManga(page: Int) = fetchSearchManga(page, "", POPULAR_FILTERS)
|
||||
override suspend fun getPopularManga(page: Int) = getSearchManga(page, "", POPULAR_FILTERS)
|
||||
|
||||
override fun fetchLatestUpdates(page: Int) = fetchSearchManga(page, "", LATEST_FILTERS)
|
||||
override suspend fun getLatestUpdates(page: Int) = getSearchManga(page, "", LATEST_FILTERS)
|
||||
|
||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||
override suspend fun getSearchManga(page: Int, query: String, filters: FilterList): MangasPage {
|
||||
val baseDirsFiles = fileSystem.getFilesInBaseDirectories()
|
||||
val lastModifiedLimit by lazy { if (filters === LATEST_FILTERS) System.currentTimeMillis() - LATEST_THRESHOLD else 0L }
|
||||
|
||||
|
@ -150,7 +149,7 @@ actual class LocalMangaSource(
|
|||
}
|
||||
}
|
||||
|
||||
return Observable.just(MangasPage(mangas.toList(), false))
|
||||
return MangasPage(mangas.toList(), false)
|
||||
}
|
||||
|
||||
// Manga details related
|
||||
|
|
Loading…
Reference in a new issue