mirror of
https://github.com/aniyomiorg/aniyomi.git
synced 2024-11-25 06:11:04 +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:
|
- I have updated:
|
||||||
- To the latest version of the app (stable is v0.12.3.10)
|
- To the latest version of the app (stable is v0.12.3.10)
|
||||||
- All extensions
|
- 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
|
- 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 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
|
- 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
|
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
|
about: Issues and requests for extensions and sources should be opened in the aniyomi-extensions repository instead
|
||||||
- name: 📦 Aniyomi extensions
|
- name: 📦 Aniyomi extensions
|
||||||
url: https://aniyomi.org/extensions
|
url: https://aniyomi.org/extensions/
|
||||||
about: Anime extensions and sources
|
about: Anime extensions and sources
|
||||||
- name: 🧑💻 Aniyomi help discord
|
- name: 🧑💻 Aniyomi help discord
|
||||||
url: https://discord.gg/F32UjdJZrR
|
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
|
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).
|
- 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
|
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
|
required: true
|
||||||
- label: I have updated the app to version **[0.12.3.10](https://github.com/aniyomiorg/aniyomi/releases/latest)**.
|
- label: I have updated the app to version **[0.12.3.10](https://github.com/aniyomiorg/aniyomi/releases/latest)**.
|
||||||
required: true
|
required: true
|
||||||
|
|
11
.github/workflows/build_push.yml
vendored
11
.github/workflows/build_push.yml
vendored
|
@ -120,3 +120,14 @@ jobs:
|
||||||
prerelease: false
|
prerelease: false
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
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.*",
|
"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,
|
"ignoreCase": true,
|
||||||
"labels": ["Cloudflare protected"],
|
"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
|
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
|
||||||
|
|
||||||
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
|
# Forks
|
||||||
|
|
|
@ -31,7 +31,7 @@ Please make sure to read the full guidelines. Your issue may be closed without w
|
||||||
|
|
||||||
<details><summary>Issues</summary>
|
<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)
|
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>
|
</details>
|
||||||
|
|
|
@ -12,7 +12,6 @@ import eu.kanade.tachiyomi.source.model.Page
|
||||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import rx.Observable
|
|
||||||
import tachiyomi.core.preference.Preference
|
import tachiyomi.core.preference.Preference
|
||||||
import uy.kohesive.injekt.injectLazy
|
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 {
|
suspend fun HttpSource.getImage(page: Page, dataSaver: DataSaver): Response {
|
||||||
val imageUrl = page.imageUrl ?: return getImage(page)
|
val imageUrl = page.imageUrl ?: return getImage(page)
|
||||||
page.imageUrl = dataSaver.compress(imageUrl)
|
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.SetSortModeForMangaCategory
|
||||||
import tachiyomi.domain.category.manga.interactor.UpdateMangaCategory
|
import tachiyomi.domain.category.manga.interactor.UpdateMangaCategory
|
||||||
import tachiyomi.domain.category.manga.repository.MangaCategoryRepository
|
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.GetAnime
|
||||||
import tachiyomi.domain.entries.anime.interactor.GetAnimeFavorites
|
import tachiyomi.domain.entries.anime.interactor.GetAnimeFavorites
|
||||||
import tachiyomi.domain.entries.anime.interactor.GetAnimeWithEpisodes
|
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.NetworkToLocalAnime
|
||||||
import tachiyomi.domain.entries.anime.interactor.ResetAnimeViewerFlags
|
import tachiyomi.domain.entries.anime.interactor.ResetAnimeViewerFlags
|
||||||
import tachiyomi.domain.entries.anime.interactor.SetAnimeEpisodeFlags
|
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.anime.repository.AnimeRepository
|
||||||
import tachiyomi.domain.entries.manga.interactor.GetDuplicateLibraryManga
|
import tachiyomi.domain.entries.manga.interactor.GetDuplicateLibraryManga
|
||||||
import tachiyomi.domain.entries.manga.interactor.GetLibraryManga
|
import tachiyomi.domain.entries.manga.interactor.GetLibraryManga
|
||||||
import tachiyomi.domain.entries.manga.interactor.GetManga
|
import tachiyomi.domain.entries.manga.interactor.GetManga
|
||||||
import tachiyomi.domain.entries.manga.interactor.GetMangaFavorites
|
import tachiyomi.domain.entries.manga.interactor.GetMangaFavorites
|
||||||
import tachiyomi.domain.entries.manga.interactor.GetMangaWithChapters
|
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.NetworkToLocalManga
|
||||||
import tachiyomi.domain.entries.manga.interactor.ResetMangaViewerFlags
|
import tachiyomi.domain.entries.manga.interactor.ResetMangaViewerFlags
|
||||||
import tachiyomi.domain.entries.manga.interactor.SetMangaChapterFlags
|
import tachiyomi.domain.entries.manga.interactor.SetMangaChapterFlags
|
||||||
import tachiyomi.domain.entries.manga.interactor.SetMangaFetchInterval
|
|
||||||
import tachiyomi.domain.entries.manga.repository.MangaRepository
|
import tachiyomi.domain.entries.manga.repository.MangaRepository
|
||||||
import tachiyomi.domain.history.anime.interactor.GetAnimeHistory
|
import tachiyomi.domain.history.anime.interactor.GetAnimeHistory
|
||||||
import tachiyomi.domain.history.anime.interactor.GetNextEpisodes
|
import tachiyomi.domain.history.anime.interactor.GetNextEpisodes
|
||||||
|
@ -188,7 +188,7 @@ class DomainModule : InjektModule {
|
||||||
addFactory { GetNextEpisodes(get(), get(), get()) }
|
addFactory { GetNextEpisodes(get(), get(), get()) }
|
||||||
addFactory { ResetAnimeViewerFlags(get()) }
|
addFactory { ResetAnimeViewerFlags(get()) }
|
||||||
addFactory { SetAnimeEpisodeFlags(get()) }
|
addFactory { SetAnimeEpisodeFlags(get()) }
|
||||||
addFactory { SetAnimeFetchInterval(get()) }
|
addFactory { AnimeFetchInterval(get()) }
|
||||||
addFactory { SetAnimeDefaultEpisodeFlags(get(), get(), get()) }
|
addFactory { SetAnimeDefaultEpisodeFlags(get(), get(), get()) }
|
||||||
addFactory { SetAnimeViewerFlags(get()) }
|
addFactory { SetAnimeViewerFlags(get()) }
|
||||||
addFactory { NetworkToLocalAnime(get()) }
|
addFactory { NetworkToLocalAnime(get()) }
|
||||||
|
@ -204,7 +204,7 @@ class DomainModule : InjektModule {
|
||||||
addFactory { GetNextChapters(get(), get(), get()) }
|
addFactory { GetNextChapters(get(), get(), get()) }
|
||||||
addFactory { ResetMangaViewerFlags(get()) }
|
addFactory { ResetMangaViewerFlags(get()) }
|
||||||
addFactory { SetMangaChapterFlags(get()) }
|
addFactory { SetMangaChapterFlags(get()) }
|
||||||
addFactory { SetMangaFetchInterval(get()) }
|
addFactory { MangaFetchInterval(get()) }
|
||||||
addFactory {
|
addFactory {
|
||||||
SetMangaDefaultChapterFlags(
|
SetMangaDefaultChapterFlags(
|
||||||
get(),
|
get(),
|
||||||
|
|
|
@ -3,7 +3,7 @@ package eu.kanade.domain.entries.anime.interactor
|
||||||
import eu.kanade.domain.entries.anime.model.hasCustomCover
|
import eu.kanade.domain.entries.anime.model.hasCustomCover
|
||||||
import eu.kanade.tachiyomi.animesource.model.SAnime
|
import eu.kanade.tachiyomi.animesource.model.SAnime
|
||||||
import eu.kanade.tachiyomi.data.cache.AnimeCoverCache
|
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.Anime
|
||||||
import tachiyomi.domain.entries.anime.model.AnimeUpdate
|
import tachiyomi.domain.entries.anime.model.AnimeUpdate
|
||||||
import tachiyomi.domain.entries.anime.repository.AnimeRepository
|
import tachiyomi.domain.entries.anime.repository.AnimeRepository
|
||||||
|
@ -15,7 +15,7 @@ import java.util.Date
|
||||||
|
|
||||||
class UpdateAnime(
|
class UpdateAnime(
|
||||||
private val animeRepository: AnimeRepository,
|
private val animeRepository: AnimeRepository,
|
||||||
private val setAnimeFetchInterval: SetAnimeFetchInterval,
|
private val animeFetchInterval: AnimeFetchInterval,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
suspend fun await(animeUpdate: AnimeUpdate): Boolean {
|
suspend fun await(animeUpdate: AnimeUpdate): Boolean {
|
||||||
|
@ -79,9 +79,9 @@ class UpdateAnime(
|
||||||
suspend fun awaitUpdateFetchInterval(
|
suspend fun awaitUpdateFetchInterval(
|
||||||
anime: Anime,
|
anime: Anime,
|
||||||
dateTime: ZonedDateTime = ZonedDateTime.now(),
|
dateTime: ZonedDateTime = ZonedDateTime.now(),
|
||||||
window: Pair<Long, Long> = setAnimeFetchInterval.getWindow(dateTime),
|
window: Pair<Long, Long> = animeFetchInterval.getWindow(dateTime),
|
||||||
): Boolean {
|
): Boolean {
|
||||||
return setAnimeFetchInterval.toAnimeUpdateOrNull(anime, dateTime, window)
|
return animeFetchInterval.toAnimeUpdateOrNull(anime, dateTime, window)
|
||||||
?.let { animeRepository.updateAnime(it) }
|
?.let { animeRepository.updateAnime(it) }
|
||||||
?: false
|
?: false
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,7 @@ package eu.kanade.domain.entries.manga.interactor
|
||||||
import eu.kanade.domain.entries.manga.model.hasCustomCover
|
import eu.kanade.domain.entries.manga.model.hasCustomCover
|
||||||
import eu.kanade.tachiyomi.data.cache.MangaCoverCache
|
import eu.kanade.tachiyomi.data.cache.MangaCoverCache
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
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.Manga
|
||||||
import tachiyomi.domain.entries.manga.model.MangaUpdate
|
import tachiyomi.domain.entries.manga.model.MangaUpdate
|
||||||
import tachiyomi.domain.entries.manga.repository.MangaRepository
|
import tachiyomi.domain.entries.manga.repository.MangaRepository
|
||||||
|
@ -15,7 +15,7 @@ import java.util.Date
|
||||||
|
|
||||||
class UpdateManga(
|
class UpdateManga(
|
||||||
private val mangaRepository: MangaRepository,
|
private val mangaRepository: MangaRepository,
|
||||||
private val setMangaFetchInterval: SetMangaFetchInterval,
|
private val mangaFetchInterval: MangaFetchInterval,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
suspend fun await(mangaUpdate: MangaUpdate): Boolean {
|
suspend fun await(mangaUpdate: MangaUpdate): Boolean {
|
||||||
|
@ -79,9 +79,9 @@ class UpdateManga(
|
||||||
suspend fun awaitUpdateFetchInterval(
|
suspend fun awaitUpdateFetchInterval(
|
||||||
manga: Manga,
|
manga: Manga,
|
||||||
dateTime: ZonedDateTime = ZonedDateTime.now(),
|
dateTime: ZonedDateTime = ZonedDateTime.now(),
|
||||||
window: Pair<Long, Long> = setMangaFetchInterval.getWindow(dateTime),
|
window: Pair<Long, Long> = mangaFetchInterval.getWindow(dateTime),
|
||||||
): Boolean {
|
): Boolean {
|
||||||
return setMangaFetchInterval.toMangaUpdateOrNull(manga, dateTime, window)
|
return mangaFetchInterval.toMangaUpdateOrNull(manga, dateTime, window)
|
||||||
?.let { mangaRepository.updateManga(it) }
|
?.let { mangaRepository.updateManga(it) }
|
||||||
?: false
|
?: false
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,7 +28,7 @@ class UiPreferences(
|
||||||
|
|
||||||
fun themeDarkAmoled() = preferenceStore.getBoolean("pref_theme_dark_amoled_key", false)
|
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", "")
|
fun dateFormat() = preferenceStore.getString("app_date_format", "")
|
||||||
|
|
||||||
|
|
|
@ -13,13 +13,18 @@ import java.util.Date
|
||||||
fun RelativeDateHeader(
|
fun RelativeDateHeader(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
date: Date,
|
date: Date,
|
||||||
|
relativeTime: Boolean,
|
||||||
dateFormat: DateFormat,
|
dateFormat: DateFormat,
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
ListGroupHeader(
|
ListGroupHeader(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
text = remember {
|
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
|
val previousUnviewed = if (isManga) R.string.action_mark_previous_as_read else R.string.action_mark_previous_as_seen
|
||||||
Button(
|
Button(
|
||||||
title = stringResource(previousUnviewed),
|
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],
|
toConfirm = confirm[4],
|
||||||
onLongClick = { onLongClickItem(4) },
|
onLongClick = { onLongClickItem(4) },
|
||||||
onClick = onMarkPreviousAsViewedClicked,
|
onClick = onMarkPreviousAsViewedClicked,
|
||||||
|
|
|
@ -98,6 +98,7 @@ fun AnimeScreen(
|
||||||
state: AnimeScreenModel.State.Success,
|
state: AnimeScreenModel.State.Success,
|
||||||
snackbarHostState: SnackbarHostState,
|
snackbarHostState: SnackbarHostState,
|
||||||
fetchInterval: Int?,
|
fetchInterval: Int?,
|
||||||
|
dateRelativeTime: Boolean,
|
||||||
dateFormat: DateFormat,
|
dateFormat: DateFormat,
|
||||||
isTabletUi: Boolean,
|
isTabletUi: Boolean,
|
||||||
episodeSwipeStartAction: LibraryPreferences.EpisodeSwipeAction,
|
episodeSwipeStartAction: LibraryPreferences.EpisodeSwipeAction,
|
||||||
|
@ -161,6 +162,7 @@ fun AnimeScreen(
|
||||||
AnimeScreenSmallImpl(
|
AnimeScreenSmallImpl(
|
||||||
state = state,
|
state = state,
|
||||||
snackbarHostState = snackbarHostState,
|
snackbarHostState = snackbarHostState,
|
||||||
|
dateRelativeTime = dateRelativeTime,
|
||||||
dateFormat = dateFormat,
|
dateFormat = dateFormat,
|
||||||
fetchInterval = fetchInterval,
|
fetchInterval = fetchInterval,
|
||||||
episodeSwipeStartAction = episodeSwipeStartAction,
|
episodeSwipeStartAction = episodeSwipeStartAction,
|
||||||
|
@ -201,6 +203,7 @@ fun AnimeScreen(
|
||||||
AnimeScreenLargeImpl(
|
AnimeScreenLargeImpl(
|
||||||
state = state,
|
state = state,
|
||||||
snackbarHostState = snackbarHostState,
|
snackbarHostState = snackbarHostState,
|
||||||
|
dateRelativeTime = dateRelativeTime,
|
||||||
episodeSwipeStartAction = episodeSwipeStartAction,
|
episodeSwipeStartAction = episodeSwipeStartAction,
|
||||||
episodeSwipeEndAction = episodeSwipeEndAction,
|
episodeSwipeEndAction = episodeSwipeEndAction,
|
||||||
showNextEpisodeAirTime = showNextEpisodeAirTime,
|
showNextEpisodeAirTime = showNextEpisodeAirTime,
|
||||||
|
@ -245,6 +248,7 @@ fun AnimeScreen(
|
||||||
private fun AnimeScreenSmallImpl(
|
private fun AnimeScreenSmallImpl(
|
||||||
state: AnimeScreenModel.State.Success,
|
state: AnimeScreenModel.State.Success,
|
||||||
snackbarHostState: SnackbarHostState,
|
snackbarHostState: SnackbarHostState,
|
||||||
|
dateRelativeTime: Boolean,
|
||||||
dateFormat: DateFormat,
|
dateFormat: DateFormat,
|
||||||
fetchInterval: Int?,
|
fetchInterval: Int?,
|
||||||
episodeSwipeStartAction: LibraryPreferences.EpisodeSwipeAction,
|
episodeSwipeStartAction: LibraryPreferences.EpisodeSwipeAction,
|
||||||
|
@ -316,11 +320,9 @@ private fun AnimeScreenSmallImpl(
|
||||||
}
|
}
|
||||||
val animatedTitleAlpha by animateFloatAsState(
|
val animatedTitleAlpha by animateFloatAsState(
|
||||||
if (firstVisibleItemIndex > 0) 1f else 0f,
|
if (firstVisibleItemIndex > 0) 1f else 0f,
|
||||||
label = "titleAlpha",
|
|
||||||
)
|
)
|
||||||
val animatedBgAlpha by animateFloatAsState(
|
val animatedBgAlpha by animateFloatAsState(
|
||||||
if (firstVisibleItemIndex > 0 || firstVisibleItemScrollOffset > 0) 1f else 0f,
|
if (firstVisibleItemIndex > 0 || firstVisibleItemScrollOffset > 0) 1f else 0f,
|
||||||
label = "bgAlpha",
|
|
||||||
)
|
)
|
||||||
EntryToolbar(
|
EntryToolbar(
|
||||||
title = state.anime.title,
|
title = state.anime.title,
|
||||||
|
@ -497,6 +499,7 @@ private fun AnimeScreenSmallImpl(
|
||||||
sharedEpisodeItems(
|
sharedEpisodeItems(
|
||||||
anime = state.anime,
|
anime = state.anime,
|
||||||
episodes = episodes,
|
episodes = episodes,
|
||||||
|
dateRelativeTime = dateRelativeTime,
|
||||||
dateFormat = dateFormat,
|
dateFormat = dateFormat,
|
||||||
episodeSwipeStartAction = episodeSwipeStartAction,
|
episodeSwipeStartAction = episodeSwipeStartAction,
|
||||||
episodeSwipeEndAction = episodeSwipeEndAction,
|
episodeSwipeEndAction = episodeSwipeEndAction,
|
||||||
|
@ -516,6 +519,7 @@ private fun AnimeScreenSmallImpl(
|
||||||
fun AnimeScreenLargeImpl(
|
fun AnimeScreenLargeImpl(
|
||||||
state: AnimeScreenModel.State.Success,
|
state: AnimeScreenModel.State.Success,
|
||||||
snackbarHostState: SnackbarHostState,
|
snackbarHostState: SnackbarHostState,
|
||||||
|
dateRelativeTime: Boolean,
|
||||||
dateFormat: DateFormat,
|
dateFormat: DateFormat,
|
||||||
fetchInterval: Int?,
|
fetchInterval: Int?,
|
||||||
episodeSwipeStartAction: LibraryPreferences.EpisodeSwipeAction,
|
episodeSwipeStartAction: LibraryPreferences.EpisodeSwipeAction,
|
||||||
|
@ -762,6 +766,7 @@ fun AnimeScreenLargeImpl(
|
||||||
sharedEpisodeItems(
|
sharedEpisodeItems(
|
||||||
anime = state.anime,
|
anime = state.anime,
|
||||||
episodes = episodes,
|
episodes = episodes,
|
||||||
|
dateRelativeTime = dateRelativeTime,
|
||||||
dateFormat = dateFormat,
|
dateFormat = dateFormat,
|
||||||
episodeSwipeStartAction = episodeSwipeStartAction,
|
episodeSwipeStartAction = episodeSwipeStartAction,
|
||||||
episodeSwipeEndAction = episodeSwipeEndAction,
|
episodeSwipeEndAction = episodeSwipeEndAction,
|
||||||
|
@ -832,6 +837,7 @@ private fun SharedAnimeBottomActionMenu(
|
||||||
private fun LazyListScope.sharedEpisodeItems(
|
private fun LazyListScope.sharedEpisodeItems(
|
||||||
anime: Anime,
|
anime: Anime,
|
||||||
episodes: List<EpisodeItem>,
|
episodes: List<EpisodeItem>,
|
||||||
|
dateRelativeTime: Boolean,
|
||||||
dateFormat: DateFormat,
|
dateFormat: DateFormat,
|
||||||
episodeSwipeStartAction: LibraryPreferences.EpisodeSwipeAction,
|
episodeSwipeStartAction: LibraryPreferences.EpisodeSwipeAction,
|
||||||
episodeSwipeEndAction: LibraryPreferences.EpisodeSwipeAction,
|
episodeSwipeEndAction: LibraryPreferences.EpisodeSwipeAction,
|
||||||
|
@ -860,7 +866,11 @@ private fun LazyListScope.sharedEpisodeItems(
|
||||||
date = episodeItem.episode.dateUpload
|
date = episodeItem.episode.dateUpload
|
||||||
.takeIf { it > 0L }
|
.takeIf { it > 0L }
|
||||||
?.let {
|
?.let {
|
||||||
Date(it).toRelativeString(context, dateFormat)
|
Date(it).toRelativeString(
|
||||||
|
context,
|
||||||
|
dateRelativeTime,
|
||||||
|
dateFormat,
|
||||||
|
)
|
||||||
},
|
},
|
||||||
watchProgress = episodeItem.episode.lastSecondSeen
|
watchProgress = episodeItem.episode.lastSecondSeen
|
||||||
.takeIf { !episodeItem.episode.seen && it > 0L }
|
.takeIf { !episodeItem.episode.seen && it > 0L }
|
||||||
|
|
|
@ -91,6 +91,7 @@ fun MangaScreen(
|
||||||
state: MangaScreenModel.State.Success,
|
state: MangaScreenModel.State.Success,
|
||||||
snackbarHostState: SnackbarHostState,
|
snackbarHostState: SnackbarHostState,
|
||||||
fetchInterval: Int?,
|
fetchInterval: Int?,
|
||||||
|
dateRelativeTime: Boolean,
|
||||||
dateFormat: DateFormat,
|
dateFormat: DateFormat,
|
||||||
isTabletUi: Boolean,
|
isTabletUi: Boolean,
|
||||||
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
|
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
|
||||||
|
@ -151,6 +152,7 @@ fun MangaScreen(
|
||||||
MangaScreenSmallImpl(
|
MangaScreenSmallImpl(
|
||||||
state = state,
|
state = state,
|
||||||
snackbarHostState = snackbarHostState,
|
snackbarHostState = snackbarHostState,
|
||||||
|
dateRelativeTime = dateRelativeTime,
|
||||||
dateFormat = dateFormat,
|
dateFormat = dateFormat,
|
||||||
fetchInterval = fetchInterval,
|
fetchInterval = fetchInterval,
|
||||||
chapterSwipeStartAction = chapterSwipeStartAction,
|
chapterSwipeStartAction = chapterSwipeStartAction,
|
||||||
|
@ -188,6 +190,7 @@ fun MangaScreen(
|
||||||
MangaScreenLargeImpl(
|
MangaScreenLargeImpl(
|
||||||
state = state,
|
state = state,
|
||||||
snackbarHostState = snackbarHostState,
|
snackbarHostState = snackbarHostState,
|
||||||
|
dateRelativeTime = dateRelativeTime,
|
||||||
chapterSwipeStartAction = chapterSwipeStartAction,
|
chapterSwipeStartAction = chapterSwipeStartAction,
|
||||||
chapterSwipeEndAction = chapterSwipeEndAction,
|
chapterSwipeEndAction = chapterSwipeEndAction,
|
||||||
dateFormat = dateFormat,
|
dateFormat = dateFormat,
|
||||||
|
@ -228,6 +231,7 @@ fun MangaScreen(
|
||||||
private fun MangaScreenSmallImpl(
|
private fun MangaScreenSmallImpl(
|
||||||
state: MangaScreenModel.State.Success,
|
state: MangaScreenModel.State.Success,
|
||||||
snackbarHostState: SnackbarHostState,
|
snackbarHostState: SnackbarHostState,
|
||||||
|
dateRelativeTime: Boolean,
|
||||||
dateFormat: DateFormat,
|
dateFormat: DateFormat,
|
||||||
fetchInterval: Int?,
|
fetchInterval: Int?,
|
||||||
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
|
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
|
||||||
|
@ -297,11 +301,9 @@ private fun MangaScreenSmallImpl(
|
||||||
}
|
}
|
||||||
val animatedTitleAlpha by animateFloatAsState(
|
val animatedTitleAlpha by animateFloatAsState(
|
||||||
if (firstVisibleItemIndex > 0) 1f else 0f,
|
if (firstVisibleItemIndex > 0) 1f else 0f,
|
||||||
label = "titleAlpha",
|
|
||||||
)
|
)
|
||||||
val animatedBgAlpha by animateFloatAsState(
|
val animatedBgAlpha by animateFloatAsState(
|
||||||
if (firstVisibleItemIndex > 0 || firstVisibleItemScrollOffset > 0) 1f else 0f,
|
if (firstVisibleItemIndex > 0 || firstVisibleItemScrollOffset > 0) 1f else 0f,
|
||||||
label = "bgAlpha",
|
|
||||||
)
|
)
|
||||||
EntryToolbar(
|
EntryToolbar(
|
||||||
title = state.manga.title,
|
title = state.manga.title,
|
||||||
|
@ -446,6 +448,7 @@ private fun MangaScreenSmallImpl(
|
||||||
sharedChapterItems(
|
sharedChapterItems(
|
||||||
manga = state.manga,
|
manga = state.manga,
|
||||||
chapters = chapters,
|
chapters = chapters,
|
||||||
|
dateRelativeTime = dateRelativeTime,
|
||||||
dateFormat = dateFormat,
|
dateFormat = dateFormat,
|
||||||
chapterSwipeStartAction = chapterSwipeStartAction,
|
chapterSwipeStartAction = chapterSwipeStartAction,
|
||||||
chapterSwipeEndAction = chapterSwipeEndAction,
|
chapterSwipeEndAction = chapterSwipeEndAction,
|
||||||
|
@ -464,6 +467,7 @@ private fun MangaScreenSmallImpl(
|
||||||
fun MangaScreenLargeImpl(
|
fun MangaScreenLargeImpl(
|
||||||
state: MangaScreenModel.State.Success,
|
state: MangaScreenModel.State.Success,
|
||||||
snackbarHostState: SnackbarHostState,
|
snackbarHostState: SnackbarHostState,
|
||||||
|
dateRelativeTime: Boolean,
|
||||||
dateFormat: DateFormat,
|
dateFormat: DateFormat,
|
||||||
fetchInterval: Int?,
|
fetchInterval: Int?,
|
||||||
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
|
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
|
||||||
|
@ -679,6 +683,7 @@ fun MangaScreenLargeImpl(
|
||||||
sharedChapterItems(
|
sharedChapterItems(
|
||||||
manga = state.manga,
|
manga = state.manga,
|
||||||
chapters = chapters,
|
chapters = chapters,
|
||||||
|
dateRelativeTime = dateRelativeTime,
|
||||||
dateFormat = dateFormat,
|
dateFormat = dateFormat,
|
||||||
chapterSwipeStartAction = chapterSwipeStartAction,
|
chapterSwipeStartAction = chapterSwipeStartAction,
|
||||||
chapterSwipeEndAction = chapterSwipeEndAction,
|
chapterSwipeEndAction = chapterSwipeEndAction,
|
||||||
|
@ -741,6 +746,7 @@ private fun SharedMangaBottomActionMenu(
|
||||||
private fun LazyListScope.sharedChapterItems(
|
private fun LazyListScope.sharedChapterItems(
|
||||||
manga: Manga,
|
manga: Manga,
|
||||||
chapters: List<ChapterItem>,
|
chapters: List<ChapterItem>,
|
||||||
|
dateRelativeTime: Boolean,
|
||||||
dateFormat: DateFormat,
|
dateFormat: DateFormat,
|
||||||
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
|
chapterSwipeStartAction: LibraryPreferences.ChapterSwipeAction,
|
||||||
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
|
chapterSwipeEndAction: LibraryPreferences.ChapterSwipeAction,
|
||||||
|
@ -769,7 +775,11 @@ private fun LazyListScope.sharedChapterItems(
|
||||||
date = chapterItem.chapter.dateUpload
|
date = chapterItem.chapter.dateUpload
|
||||||
.takeIf { it > 0L }
|
.takeIf { it > 0L }
|
||||||
?.let {
|
?.let {
|
||||||
Date(it).toRelativeString(context, dateFormat)
|
Date(it).toRelativeString(
|
||||||
|
context,
|
||||||
|
dateRelativeTime,
|
||||||
|
dateFormat,
|
||||||
|
)
|
||||||
},
|
},
|
||||||
readProgress = chapterItem.chapter.lastPageRead
|
readProgress = chapterItem.chapter.lastPageRead
|
||||||
.takeIf { !chapterItem.chapter.read && it > 0L }
|
.takeIf { !chapterItem.chapter.read && it > 0L }
|
||||||
|
|
|
@ -13,7 +13,6 @@ import tachiyomi.domain.history.anime.model.AnimeHistoryWithRelations
|
||||||
import tachiyomi.presentation.core.components.FastScrollLazyColumn
|
import tachiyomi.presentation.core.components.FastScrollLazyColumn
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.text.DateFormat
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AnimeHistoryContent(
|
fun AnimeHistoryContent(
|
||||||
|
@ -24,7 +23,8 @@ fun AnimeHistoryContent(
|
||||||
onClickDelete: (AnimeHistoryWithRelations) -> Unit,
|
onClickDelete: (AnimeHistoryWithRelations) -> Unit,
|
||||||
preferences: UiPreferences = Injekt.get(),
|
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(
|
FastScrollLazyColumn(
|
||||||
contentPadding = contentPadding,
|
contentPadding = contentPadding,
|
||||||
|
@ -44,6 +44,7 @@ fun AnimeHistoryContent(
|
||||||
RelativeDateHeader(
|
RelativeDateHeader(
|
||||||
modifier = Modifier.animateItemPlacement(),
|
modifier = Modifier.animateItemPlacement(),
|
||||||
date = item.date,
|
date = item.date,
|
||||||
|
relativeTime = relativeTime,
|
||||||
dateFormat = dateFormat,
|
dateFormat = dateFormat,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,6 @@ import tachiyomi.domain.history.manga.model.MangaHistoryWithRelations
|
||||||
import tachiyomi.presentation.core.components.FastScrollLazyColumn
|
import tachiyomi.presentation.core.components.FastScrollLazyColumn
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.text.DateFormat
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MangaHistoryContent(
|
fun MangaHistoryContent(
|
||||||
|
@ -22,7 +21,8 @@ fun MangaHistoryContent(
|
||||||
onClickDelete: (MangaHistoryWithRelations) -> Unit,
|
onClickDelete: (MangaHistoryWithRelations) -> Unit,
|
||||||
preferences: UiPreferences = Injekt.get(),
|
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(
|
FastScrollLazyColumn(
|
||||||
contentPadding = contentPadding,
|
contentPadding = contentPadding,
|
||||||
|
@ -42,6 +42,7 @@ fun MangaHistoryContent(
|
||||||
RelativeDateHeader(
|
RelativeDateHeader(
|
||||||
modifier = Modifier.animateItemPlacement(),
|
modifier = Modifier.animateItemPlacement(),
|
||||||
date = item.date,
|
date = item.date,
|
||||||
|
relativeTime = relativeTime,
|
||||||
dateFormat = dateFormat,
|
dateFormat = dateFormat,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -72,7 +72,7 @@ fun MoreScreen(
|
||||||
textRes = R.string.fdroid_warning,
|
textRes = R.string.fdroid_warning,
|
||||||
modifier = Modifier.clickable {
|
modifier = Modifier.clickable {
|
||||||
uriHandler.openUri(
|
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 now = remember { Date().time }
|
||||||
|
|
||||||
|
val dateFormat by uiPreferences.dateFormat().collectAsState()
|
||||||
|
val formattedNow = remember(dateFormat) {
|
||||||
|
UiPreferences.dateFormat(dateFormat).format(now)
|
||||||
|
}
|
||||||
|
|
||||||
LaunchedEffect(currentLanguage) {
|
LaunchedEffect(currentLanguage) {
|
||||||
val locale = if (currentLanguage.isEmpty()) {
|
val locale = if (currentLanguage.isEmpty()) {
|
||||||
LocaleListCompat.getEmptyLocaleList()
|
LocaleListCompat.getEmptyLocaleList()
|
||||||
|
@ -199,6 +204,15 @@ object SettingsAppearanceScreen : SearchableSettings {
|
||||||
"${it.ifEmpty { stringResource(R.string.label_default) }} ($formattedDate)"
|
"${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),
|
title = stringResource(R.string.help_translate),
|
||||||
onPreferenceClick = {
|
onPreferenceClick = {
|
||||||
uriHandler.openUri(
|
uriHandler.openUri(
|
||||||
"https://aniyomi.org/help/contribution/#translation",
|
"https://aniyomi.org/docs/contribute#translation",
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
@ -165,7 +165,7 @@ object AboutScreen : Screen() {
|
||||||
item {
|
item {
|
||||||
TextPreferenceWidget(
|
TextPreferenceWidget(
|
||||||
title = stringResource(R.string.privacy_policy),
|
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 = {
|
onLibraryClick = {
|
||||||
val libraryLicenseScreen = OpenSourceLibraryLicenseScreen(
|
val libraryLicenseScreen = OpenSourceLibraryLicenseScreen(
|
||||||
name = it.name,
|
name = it.library.name,
|
||||||
website = it.website,
|
website = it.library.website,
|
||||||
license = it.licenses.firstOrNull()?.htmlReadyLicenseContent.orEmpty(),
|
license = it.library.licenses.firstOrNull()?.htmlReadyLicenseContent.orEmpty(),
|
||||||
)
|
)
|
||||||
navigator.push(libraryLicenseScreen)
|
navigator.push(libraryLicenseScreen)
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,25 +1,25 @@
|
||||||
package eu.kanade.presentation.reader
|
package eu.kanade.presentation.reader
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
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.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.res.vectorResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import eu.kanade.domain.entries.manga.model.orientationType
|
import eu.kanade.domain.entries.manga.model.orientationType
|
||||||
import eu.kanade.presentation.components.AdaptiveSheet
|
import eu.kanade.presentation.components.AdaptiveSheet
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
|
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel
|
import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel
|
||||||
import tachiyomi.presentation.core.components.SettingsChipRow
|
import tachiyomi.presentation.core.components.SettingsIconGrid
|
||||||
import tachiyomi.presentation.core.components.material.padding
|
import tachiyomi.presentation.core.components.material.IconToggleButton
|
||||||
|
|
||||||
private val orientationTypeOptions = OrientationType.entries.map { it.stringRes to it }
|
private val orientationTypeOptions = OrientationType.entries.map { it.stringRes to it }
|
||||||
|
|
||||||
|
@ -36,22 +36,20 @@ fun OrientationModeSelectDialog(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
AdaptiveSheet(
|
AdaptiveSheet(onDismissRequest = onDismissRequest) {
|
||||||
onDismissRequest = onDismissRequest,
|
Box(modifier = Modifier.padding(vertical = 16.dp)) {
|
||||||
) {
|
SettingsIconGrid(R.string.rotation_type) {
|
||||||
Row(
|
items(orientationTypeOptions) { (stringRes, mode) ->
|
||||||
modifier = Modifier.padding(vertical = 16.dp),
|
IconToggleButton(
|
||||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
|
checked = mode == orientationType,
|
||||||
) {
|
onCheckedChange = {
|
||||||
SettingsChipRow(R.string.rotation_type) {
|
screenModel.onChangeOrientation(mode)
|
||||||
orientationTypeOptions.map { (stringRes, it) ->
|
|
||||||
FilterChip(
|
|
||||||
selected = it == orientationType,
|
|
||||||
onClick = {
|
|
||||||
screenModel.onChangeOrientation(it)
|
|
||||||
onChange(stringRes)
|
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
|
package eu.kanade.presentation.reader
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
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.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.compose.ui.res.stringResource
|
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.domain.entries.manga.model.readingModeType
|
||||||
import eu.kanade.presentation.components.AdaptiveSheet
|
import eu.kanade.presentation.components.AdaptiveSheet
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel
|
import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsScreenModel
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
|
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
|
import tachiyomi.presentation.core.components.material.padding
|
||||||
|
|
||||||
private val readingModeOptions = ReadingModeType.entries.map { it.stringRes to it }
|
private val readingModeOptions = ReadingModeType.entries.map { it.stringRes to it }
|
||||||
|
@ -36,22 +37,20 @@ fun ReadingModeSelectDialog(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
AdaptiveSheet(
|
AdaptiveSheet(onDismissRequest = onDismissRequest) {
|
||||||
onDismissRequest = onDismissRequest,
|
Box(modifier = Modifier.padding(vertical = MaterialTheme.padding.medium)) {
|
||||||
) {
|
SettingsIconGrid(R.string.pref_category_reading_mode) {
|
||||||
Row(
|
items(readingModeOptions) { (stringRes, mode) ->
|
||||||
modifier = Modifier.padding(vertical = 16.dp),
|
IconToggleButton(
|
||||||
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
|
checked = mode == readingMode,
|
||||||
) {
|
onCheckedChange = {
|
||||||
SettingsChipRow(R.string.pref_category_reading_mode) {
|
screenModel.onChangeReadingMode(mode)
|
||||||
readingModeOptions.map { (stringRes, it) ->
|
|
||||||
FilterChip(
|
|
||||||
selected = it == readingMode,
|
|
||||||
onClick = {
|
|
||||||
screenModel.onChangeReadingMode(it)
|
|
||||||
onChange(stringRes)
|
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
|
val borderColor = if (selected) MaterialTheme.colorScheme.outline else Color.Transparent
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
.padding(horizontal = 12.dp)
|
.padding(horizontal = 12.dp)
|
||||||
.clip(shape)
|
.clip(shape)
|
||||||
.background(MaterialTheme.colorScheme.surface)
|
.background(MaterialTheme.colorScheme.surface)
|
||||||
|
|
|
@ -38,6 +38,7 @@ import kotlin.time.Duration.Companion.seconds
|
||||||
fun AnimeUpdateScreen(
|
fun AnimeUpdateScreen(
|
||||||
state: AnimeUpdatesScreenModel.State,
|
state: AnimeUpdatesScreenModel.State,
|
||||||
snackbarHostState: SnackbarHostState,
|
snackbarHostState: SnackbarHostState,
|
||||||
|
relativeTime: Boolean,
|
||||||
contentPadding: PaddingValues,
|
contentPadding: PaddingValues,
|
||||||
lastUpdated: Long,
|
lastUpdated: Long,
|
||||||
onClickCover: (AnimeUpdatesItem) -> Unit,
|
onClickCover: (AnimeUpdatesItem) -> Unit,
|
||||||
|
@ -100,7 +101,7 @@ fun AnimeUpdateScreen(
|
||||||
animeUpdatesLastUpdatedItem(lastUpdated)
|
animeUpdatesLastUpdatedItem(lastUpdated)
|
||||||
}
|
}
|
||||||
animeUpdatesUiItems(
|
animeUpdatesUiItems(
|
||||||
uiModels = state.getUiModel(context),
|
uiModels = state.getUiModel(context, relativeTime),
|
||||||
selectionMode = state.selectionMode,
|
selectionMode = state.selectionMode,
|
||||||
onUpdateSelected = onUpdateSelected,
|
onUpdateSelected = onUpdateSelected,
|
||||||
onClickCover = onClickCover,
|
onClickCover = onClickCover,
|
||||||
|
|
|
@ -35,6 +35,7 @@ import kotlin.time.Duration.Companion.seconds
|
||||||
fun MangaUpdateScreen(
|
fun MangaUpdateScreen(
|
||||||
state: MangaUpdatesScreenModel.State,
|
state: MangaUpdatesScreenModel.State,
|
||||||
snackbarHostState: SnackbarHostState,
|
snackbarHostState: SnackbarHostState,
|
||||||
|
relativeTime: Boolean,
|
||||||
contentPadding: PaddingValues,
|
contentPadding: PaddingValues,
|
||||||
lastUpdated: Long,
|
lastUpdated: Long,
|
||||||
onClickCover: (MangaUpdatesItem) -> Unit,
|
onClickCover: (MangaUpdatesItem) -> Unit,
|
||||||
|
@ -97,7 +98,7 @@ fun MangaUpdateScreen(
|
||||||
}
|
}
|
||||||
|
|
||||||
mangaUpdatesUiItems(
|
mangaUpdatesUiItems(
|
||||||
uiModels = state.getUiModel(context),
|
uiModels = state.getUiModel(context, relativeTime),
|
||||||
selectionMode = state.selectionMode,
|
selectionMode = state.selectionMode,
|
||||||
onUpdateSelected = onUpdateSelected,
|
onUpdateSelected = onUpdateSelected,
|
||||||
onClickCover = onClickCover,
|
onClickCover = onClickCover,
|
||||||
|
|
|
@ -176,7 +176,7 @@ fun WebViewScreenContent(
|
||||||
textRes = R.string.information_cloudflare_help,
|
textRes = R.string.information_cloudflare_help,
|
||||||
modifier = Modifier.clickable {
|
modifier = Modifier.clickable {
|
||||||
uriHandler.openUri(
|
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" }
|
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
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,9 +30,9 @@ import eu.kanade.tachiyomi.util.system.createFileInCacheDir
|
||||||
import kotlinx.coroutines.coroutineScope
|
import kotlinx.coroutines.coroutineScope
|
||||||
import kotlinx.coroutines.isActive
|
import kotlinx.coroutines.isActive
|
||||||
import tachiyomi.core.util.system.logcat
|
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.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.entries.manga.model.Manga
|
||||||
import tachiyomi.domain.items.chapter.model.Chapter
|
import tachiyomi.domain.items.chapter.model.Chapter
|
||||||
import tachiyomi.domain.items.chapter.repository.ChapterRepository
|
import tachiyomi.domain.items.chapter.repository.ChapterRepository
|
||||||
|
@ -54,15 +54,15 @@ class BackupRestorer(
|
||||||
) {
|
) {
|
||||||
private val updateManga: UpdateManga = Injekt.get()
|
private val updateManga: UpdateManga = Injekt.get()
|
||||||
private val chapterRepository: ChapterRepository = 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 updateAnime: UpdateAnime = Injekt.get()
|
||||||
private val episodeRepository: EpisodeRepository = 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 now = ZonedDateTime.now()
|
||||||
private var currentMangaFetchWindow = setMangaFetchInterval.getWindow(now)
|
private var currentMangaFetchWindow = mangaFetchInterval.getWindow(now)
|
||||||
private var currentAnimeFetchWindow = setAnimeFetchInterval.getWindow(now)
|
private var currentAnimeFetchWindow = animeFetchInterval.getWindow(now)
|
||||||
|
|
||||||
private var backupManager = BackupManager(context)
|
private var backupManager = BackupManager(context)
|
||||||
|
|
||||||
|
@ -152,8 +152,8 @@ class BackupRestorer(
|
||||||
animeSourceMapping = backupAnimeMaps.associate { it.sourceId to it.name }
|
animeSourceMapping = backupAnimeMaps.associate { it.sourceId to it.name }
|
||||||
|
|
||||||
now = ZonedDateTime.now()
|
now = ZonedDateTime.now()
|
||||||
currentMangaFetchWindow = setMangaFetchInterval.getWindow(now)
|
currentMangaFetchWindow = mangaFetchInterval.getWindow(now)
|
||||||
currentAnimeFetchWindow = setAnimeFetchInterval.getWindow(now)
|
currentAnimeFetchWindow = animeFetchInterval.getWindow(now)
|
||||||
|
|
||||||
return coroutineScope {
|
return coroutineScope {
|
||||||
// Restore individual manga
|
// Restore individual manga
|
||||||
|
|
|
@ -674,7 +674,7 @@ class AnimeDownloader(
|
||||||
if (isHls(video) || isMpd(video)) {
|
if (isHls(video) || isMpd(video)) {
|
||||||
return ffmpegDownload(video, download, tmpDir, filename)
|
return ffmpegDownload(video, download, tmpDir, filename)
|
||||||
} else {
|
} else {
|
||||||
val response = download.source.fetchVideo(video)
|
val response = download.source.getVideo(video)
|
||||||
val file = tmpDir.findFile("$filename.tmp") ?: tmpDir.createFile("$filename.tmp")
|
val file = tmpDir.findFile("$filename.tmp") ?: tmpDir.createFile("$filename.tmp")
|
||||||
|
|
||||||
// Write to file with pause/resume capability
|
// Write to file with pause/resume capability
|
||||||
|
|
|
@ -46,7 +46,6 @@ import nl.adaptivity.xmlutil.serialization.XML
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import tachiyomi.core.metadata.comicinfo.COMIC_INFO_FILE
|
import tachiyomi.core.metadata.comicinfo.COMIC_INFO_FILE
|
||||||
import tachiyomi.core.metadata.comicinfo.ComicInfo
|
import tachiyomi.core.metadata.comicinfo.ComicInfo
|
||||||
import tachiyomi.core.util.lang.awaitSingle
|
|
||||||
import tachiyomi.core.util.lang.launchIO
|
import tachiyomi.core.util.lang.launchIO
|
||||||
import tachiyomi.core.util.lang.launchNow
|
import tachiyomi.core.util.lang.launchNow
|
||||||
import tachiyomi.core.util.lang.withIOContext
|
import tachiyomi.core.util.lang.withIOContext
|
||||||
|
@ -396,7 +395,7 @@ class MangaDownloader(
|
||||||
if (page.imageUrl.isNullOrEmpty()) {
|
if (page.imageUrl.isNullOrEmpty()) {
|
||||||
page.status = Page.State.LOAD_PAGE
|
page.status = Page.State.LOAD_PAGE
|
||||||
try {
|
try {
|
||||||
page.imageUrl = download.source.fetchImageUrl(page).awaitSingle()
|
page.imageUrl = download.source.getImageUrl(page)
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
page.status = Page.State.ERROR
|
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.anime.interactor.GetAnimeCategories
|
||||||
import tachiyomi.domain.category.model.Category
|
import tachiyomi.domain.category.model.Category
|
||||||
import tachiyomi.domain.download.service.DownloadPreferences
|
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.GetAnime
|
||||||
import tachiyomi.domain.entries.anime.interactor.GetLibraryAnime
|
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.Anime
|
||||||
import tachiyomi.domain.entries.anime.model.toAnimeUpdate
|
import tachiyomi.domain.entries.anime.model.toAnimeUpdate
|
||||||
import tachiyomi.domain.items.episode.model.Episode
|
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 getCategories: GetAnimeCategories = Injekt.get()
|
||||||
private val syncEpisodesWithSource: SyncEpisodesWithSource = Injekt.get()
|
private val syncEpisodesWithSource: SyncEpisodesWithSource = Injekt.get()
|
||||||
private val refreshAnimeTracks: RefreshAnimeTracks = 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)
|
private val notifier = AnimeLibraryUpdateNotifier(context)
|
||||||
|
|
||||||
|
@ -216,7 +216,7 @@ class AnimeLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
|
||||||
val failedUpdates = CopyOnWriteArrayList<Pair<Anime, String?>>()
|
val failedUpdates = CopyOnWriteArrayList<Pair<Anime, String?>>()
|
||||||
val hasDownloads = AtomicBoolean(false)
|
val hasDownloads = AtomicBoolean(false)
|
||||||
val restrictions = libraryPreferences.autoUpdateItemRestrictions().get()
|
val restrictions = libraryPreferences.autoUpdateItemRestrictions().get()
|
||||||
val fetchWindow = setAnimeFetchInterval.getWindow(ZonedDateTime.now())
|
val fetchWindow = animeFetchInterval.getWindow(ZonedDateTime.now())
|
||||||
|
|
||||||
coroutineScope {
|
coroutineScope {
|
||||||
animeToUpdate.groupBy { it.anime.source }.values
|
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_AUTO = "AnimeLibraryUpdate-auto"
|
||||||
private const val WORK_NAME_MANUAL = "AnimeLibraryUpdate-manual"
|
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
|
private const val ANIME_PER_SOURCE_QUEUE_WARNING_THRESHOLD = 60
|
||||||
|
|
||||||
|
|
|
@ -377,7 +377,7 @@ class AnimeLibraryUpdateNotifier(private val context: Context) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
// TODO: Change when implemented on Aniyomi website
|
// 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
|
private const val NOTIF_ANIME_ICON_SIZE = 192
|
||||||
|
|
||||||
// TODO: Change when implemented on Aniyomi website
|
// 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.download.service.DownloadPreferences
|
||||||
import tachiyomi.domain.entries.manga.interactor.GetLibraryManga
|
import tachiyomi.domain.entries.manga.interactor.GetLibraryManga
|
||||||
import tachiyomi.domain.entries.manga.interactor.GetManga
|
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.Manga
|
||||||
import tachiyomi.domain.entries.manga.model.toMangaUpdate
|
import tachiyomi.domain.entries.manga.model.toMangaUpdate
|
||||||
import tachiyomi.domain.items.chapter.model.Chapter
|
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 getCategories: GetMangaCategories = Injekt.get()
|
||||||
private val syncChaptersWithSource: SyncChaptersWithSource = Injekt.get()
|
private val syncChaptersWithSource: SyncChaptersWithSource = Injekt.get()
|
||||||
private val refreshMangaTracks: RefreshMangaTracks = 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)
|
private val notifier = MangaLibraryUpdateNotifier(context)
|
||||||
|
|
||||||
|
@ -216,7 +216,7 @@ class MangaLibraryUpdateJob(private val context: Context, workerParams: WorkerPa
|
||||||
val failedUpdates = CopyOnWriteArrayList<Pair<Manga, String?>>()
|
val failedUpdates = CopyOnWriteArrayList<Pair<Manga, String?>>()
|
||||||
val hasDownloads = AtomicBoolean(false)
|
val hasDownloads = AtomicBoolean(false)
|
||||||
val restrictions = libraryPreferences.autoUpdateItemRestrictions().get()
|
val restrictions = libraryPreferences.autoUpdateItemRestrictions().get()
|
||||||
val fetchWindow = setMangaFetchInterval.getWindow(ZonedDateTime.now())
|
val fetchWindow = mangaFetchInterval.getWindow(ZonedDateTime.now())
|
||||||
|
|
||||||
coroutineScope {
|
coroutineScope {
|
||||||
mangaToUpdate.groupBy { it.manga.source }.values
|
mangaToUpdate.groupBy { it.manga.source }.values
|
||||||
|
|
|
@ -380,7 +380,7 @@ class MangaLibraryUpdateNotifier(private val context: Context) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
// TODO: Change when implemented on Aniyomi website
|
// 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
|
private const val NOTIF_MANGA_ICON_SIZE = 192
|
||||||
|
|
||||||
// TODO: Change when implemented on Aniyomi website
|
// 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 {
|
fun save(image: Image): Uri {
|
||||||
val data = image.data
|
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}")
|
val filename = DiskUtil.buildValidFilename("${image.name}.${type.extension}")
|
||||||
|
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q || image.location !is Location.Pictures) {
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q || image.location !is Location.Pictures) {
|
||||||
return save(data(), image.location.directory(context), filename)
|
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 =
|
val pictureDir =
|
||||||
MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
|
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 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(
|
val contentValues = contentValuesOf(
|
||||||
|
MediaStore.Images.Media.RELATIVE_PATH to relativePath,
|
||||||
MediaStore.Images.Media.DISPLAY_NAME to image.name,
|
MediaStore.Images.Media.DISPLAY_NAME to image.name,
|
||||||
MediaStore.Images.Media.MIME_TYPE to type.mime,
|
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(
|
context.contentResolver.insert(
|
||||||
pictureDir,
|
pictureDir,
|
||||||
contentValues,
|
contentValues,
|
||||||
|
@ -76,24 +104,8 @@ class ImageSaver(
|
||||||
return picture
|
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)
|
@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(
|
val projection = arrayOf(
|
||||||
MediaStore.MediaColumns._ID,
|
MediaStore.MediaColumns._ID,
|
||||||
MediaStore.MediaColumns.DISPLAY_NAME,
|
MediaStore.MediaColumns.DISPLAY_NAME,
|
||||||
|
@ -104,19 +116,19 @@ class ImageSaver(
|
||||||
|
|
||||||
val selection = "${MediaStore.MediaColumns.RELATIVE_PATH}=? AND ${MediaStore.MediaColumns.DISPLAY_NAME}=?"
|
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(
|
context.contentResolver.query(
|
||||||
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
|
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
|
||||||
projection,
|
projection,
|
||||||
selection,
|
selection,
|
||||||
arrayOf(relativePath, imagePath),
|
arrayOf(normalizedPath, filename),
|
||||||
null,
|
null,
|
||||||
).use { cursor ->
|
).use { cursor ->
|
||||||
if (cursor != null && cursor.count >= 1) {
|
if (cursor != null && cursor.count >= 1) {
|
||||||
cursor.moveToFirst().let {
|
if (cursor.moveToFirst()) {
|
||||||
val id = cursor.getLong(
|
val id = cursor.getLong(cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID))
|
||||||
cursor.getColumnIndexOrThrow(MediaStore.MediaColumns._ID),
|
|
||||||
)
|
|
||||||
|
|
||||||
return ContentUris.withAppendedId(
|
return ContentUris.withAppendedId(
|
||||||
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
|
MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
|
||||||
id,
|
id,
|
||||||
|
@ -124,6 +136,7 @@ class ImageSaver(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return default()
|
return default()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -161,7 +161,7 @@ internal class AppUpdateNotifier(private val context: Context) {
|
||||||
setContentIntent(
|
setContentIntent(
|
||||||
NotificationHandler.openUrl(
|
NotificationHandler.openUrl(
|
||||||
context,
|
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 ""
|
val extension = state.value.extension ?: return ""
|
||||||
|
|
||||||
if (!extension.hasReadme) {
|
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.")
|
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.SharingStarted
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.emptyFlow
|
import kotlinx.coroutines.flow.emptyFlow
|
||||||
import kotlinx.coroutines.flow.filter
|
|
||||||
import kotlinx.coroutines.flow.filterNotNull
|
import kotlinx.coroutines.flow.filterNotNull
|
||||||
import kotlinx.coroutines.flow.firstOrNull
|
import kotlinx.coroutines.flow.firstOrNull
|
||||||
import kotlinx.coroutines.flow.flow
|
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
|
@ -115,25 +113,20 @@ class BrowseAnimeSourceScreenModel(
|
||||||
/**
|
/**
|
||||||
* Flow of Pager flow tied to [State.listing]
|
* Flow of Pager flow tied to [State.listing]
|
||||||
*/
|
*/
|
||||||
|
private val hideInLibraryItems = sourcePreferences.hideInAnimeLibraryItems().get()
|
||||||
val animePagerFlowFlow = state.map { it.listing }
|
val animePagerFlowFlow = state.map { it.listing }
|
||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
.map { listing ->
|
.map { listing ->
|
||||||
Pager(
|
Pager(PagingConfig(pageSize = 25)) {
|
||||||
PagingConfig(pageSize = 25),
|
|
||||||
) {
|
|
||||||
getRemoteAnime.subscribe(sourceId, listing.query ?: "", listing.filters)
|
getRemoteAnime.subscribe(sourceId, listing.query ?: "", listing.filters)
|
||||||
}.flow.map { pagingData ->
|
}.flow.map { pagingData ->
|
||||||
pagingData.map {
|
pagingData.map {
|
||||||
networkToLocalAnime.await(it.toDomainAnime(sourceId))
|
networkToLocalAnime.await(it.toDomainAnime(sourceId))
|
||||||
.let { localAnime ->
|
.let { localAnime -> getAnime.subscribe(localAnime.url, localAnime.source) }
|
||||||
getAnime.subscribe(localAnime.url, localAnime.source)
|
|
||||||
}
|
|
||||||
.filterNotNull()
|
.filterNotNull()
|
||||||
.filter { localAnime ->
|
|
||||||
!sourcePreferences.hideInAnimeLibraryItems().get() || !localAnime.favorite
|
|
||||||
}
|
|
||||||
.stateIn(ioCoroutineScope)
|
.stateIn(ioCoroutineScope)
|
||||||
}
|
}
|
||||||
|
.filter { !hideInLibraryItems || !it.value.favorite }
|
||||||
}
|
}
|
||||||
.cachedIn(ioCoroutineScope)
|
.cachedIn(ioCoroutineScope)
|
||||||
}
|
}
|
||||||
|
@ -301,7 +294,6 @@ class BrowseAnimeSourceScreenModel(
|
||||||
track.anime_id = anime.id
|
track.anime_id = anime.id
|
||||||
(service as TrackService).animeService.bind(track)
|
(service as TrackService).animeService.bind(track)
|
||||||
insertTrack.await(track.toDomainTrack()!!)
|
insertTrack.await(track.toDomainTrack()!!)
|
||||||
|
|
||||||
syncEpisodeProgressWithTrack.await(
|
syncEpisodeProgressWithTrack.await(
|
||||||
anime.id,
|
anime.id,
|
||||||
track.toDomainTrack()!!,
|
track.toDomainTrack()!!,
|
||||||
|
|
|
@ -105,7 +105,7 @@ class MangaExtensionDetailsScreenModel(
|
||||||
val extension = state.value.extension ?: return ""
|
val extension = state.value.extension ?: return ""
|
||||||
|
|
||||||
if (!extension.hasReadme) {
|
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.")
|
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.Pager
|
||||||
import androidx.paging.PagingConfig
|
import androidx.paging.PagingConfig
|
||||||
import androidx.paging.cachedIn
|
import androidx.paging.cachedIn
|
||||||
|
import androidx.paging.filter
|
||||||
import androidx.paging.map
|
import androidx.paging.map
|
||||||
import cafe.adriel.voyager.core.model.StateScreenModel
|
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||||
import cafe.adriel.voyager.core.model.coroutineScope
|
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.SharingStarted
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
import kotlinx.coroutines.flow.emptyFlow
|
import kotlinx.coroutines.flow.emptyFlow
|
||||||
import kotlinx.coroutines.flow.filter
|
|
||||||
import kotlinx.coroutines.flow.filterNotNull
|
import kotlinx.coroutines.flow.filterNotNull
|
||||||
import kotlinx.coroutines.flow.firstOrNull
|
import kotlinx.coroutines.flow.firstOrNull
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
|
@ -113,25 +113,20 @@ class BrowseMangaSourceScreenModel(
|
||||||
/**
|
/**
|
||||||
* Flow of Pager flow tied to [State.listing]
|
* Flow of Pager flow tied to [State.listing]
|
||||||
*/
|
*/
|
||||||
|
private val hideInLibraryItems = sourcePreferences.hideInMangaLibraryItems().get()
|
||||||
val mangaPagerFlowFlow = state.map { it.listing }
|
val mangaPagerFlowFlow = state.map { it.listing }
|
||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
.map { listing ->
|
.map { listing ->
|
||||||
Pager(
|
Pager(PagingConfig(pageSize = 25)) {
|
||||||
PagingConfig(pageSize = 25),
|
|
||||||
) {
|
|
||||||
getRemoteManga.subscribe(sourceId, listing.query ?: "", listing.filters)
|
getRemoteManga.subscribe(sourceId, listing.query ?: "", listing.filters)
|
||||||
}.flow.map { pagingData ->
|
}.flow.map { pagingData ->
|
||||||
pagingData.map {
|
pagingData.map {
|
||||||
networkToLocalManga.await(it.toDomainManga(sourceId))
|
networkToLocalManga.await(it.toDomainManga(sourceId))
|
||||||
.let { localManga ->
|
.let { localManga -> getManga.subscribe(localManga.url, localManga.source) }
|
||||||
getManga.subscribe(localManga.url, localManga.source)
|
|
||||||
}
|
|
||||||
.filterNotNull()
|
.filterNotNull()
|
||||||
.filter { localManga ->
|
|
||||||
!sourcePreferences.hideInMangaLibraryItems().get() || !localManga.favorite
|
|
||||||
}
|
|
||||||
.stateIn(ioCoroutineScope)
|
.stateIn(ioCoroutineScope)
|
||||||
}
|
}
|
||||||
|
.filter { !hideInLibraryItems || !it.value.favorite }
|
||||||
}
|
}
|
||||||
.cachedIn(ioCoroutineScope)
|
.cachedIn(ioCoroutineScope)
|
||||||
}
|
}
|
||||||
|
@ -301,7 +296,6 @@ class BrowseMangaSourceScreenModel(
|
||||||
track.manga_id = manga.id
|
track.manga_id = manga.id
|
||||||
(service as TrackService).mangaService.bind(track)
|
(service as TrackService).mangaService.bind(track)
|
||||||
insertTrack.await(track.toDomainTrack()!!)
|
insertTrack.await(track.toDomainTrack()!!)
|
||||||
|
|
||||||
syncChapterProgressWithTrack.await(
|
syncChapterProgressWithTrack.await(
|
||||||
manga.id,
|
manga.id,
|
||||||
track.toDomainTrack()!!,
|
track.toDomainTrack()!!,
|
||||||
|
|
|
@ -19,7 +19,6 @@ import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.isActive
|
import kotlinx.coroutines.isActive
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
import tachiyomi.core.util.lang.awaitSingle
|
|
||||||
import tachiyomi.domain.entries.manga.interactor.GetManga
|
import tachiyomi.domain.entries.manga.interactor.GetManga
|
||||||
import tachiyomi.domain.entries.manga.interactor.NetworkToLocalManga
|
import tachiyomi.domain.entries.manga.interactor.NetworkToLocalManga
|
||||||
import tachiyomi.domain.entries.manga.model.Manga
|
import tachiyomi.domain.entries.manga.model.Manga
|
||||||
|
@ -138,7 +137,7 @@ abstract class MangaSearchScreenModel(
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
val page = withContext(coroutineDispatcher) {
|
val page = withContext(coroutineDispatcher) {
|
||||||
source.fetchSearchManga(1, query, source.getFilterList()).awaitSingle()
|
source.getSearchManga(1, query, source.getFilterList())
|
||||||
}
|
}
|
||||||
|
|
||||||
val titles = page.mangas.map {
|
val titles = page.mangas.map {
|
||||||
|
|
|
@ -78,13 +78,17 @@ class AnimeDownloadQueueScreenModel(
|
||||||
}
|
}
|
||||||
reorder(newAnimeDownloads)
|
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
|
val (selectedSeries, otherSeries) = adapter?.currentItems
|
||||||
?.filterIsInstance<AnimeDownloadItem>()
|
?.filterIsInstance<AnimeDownloadItem>()
|
||||||
?.map(AnimeDownloadItem::download)
|
?.map(AnimeDownloadItem::download)
|
||||||
?.partition { item.download.anime.id == it.anime.id }
|
?.partition { item.download.anime.id == it.anime.id }
|
||||||
?: Pair(emptyList(), emptyList())
|
?: Pair(emptyList(), emptyList())
|
||||||
|
if (menuItem.itemId == R.id.move_to_top_series) {
|
||||||
reorder(selectedSeries + otherSeries)
|
reorder(selectedSeries + otherSeries)
|
||||||
|
} else {
|
||||||
|
reorder(otherSeries + selectedSeries)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
R.id.cancel_download -> {
|
R.id.cancel_download -> {
|
||||||
cancel(listOf(item.download))
|
cancel(listOf(item.download))
|
||||||
|
|
|
@ -84,13 +84,17 @@ class MangaDownloadQueueScreenModel(
|
||||||
}
|
}
|
||||||
reorder(newDownloads)
|
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
|
val (selectedSeries, otherSeries) = adapter?.currentItems
|
||||||
?.filterIsInstance<MangaDownloadItem>()
|
?.filterIsInstance<MangaDownloadItem>()
|
||||||
?.map(MangaDownloadItem::download)
|
?.map(MangaDownloadItem::download)
|
||||||
?.partition { item.download.manga.id == it.manga.id }
|
?.partition { item.download.manga.id == it.manga.id }
|
||||||
?: Pair(emptyList(), emptyList())
|
?: Pair(emptyList(), emptyList())
|
||||||
|
if (menuItem.itemId == R.id.move_to_top_series) {
|
||||||
reorder(selectedSeries + otherSeries)
|
reorder(selectedSeries + otherSeries)
|
||||||
|
} else {
|
||||||
|
reorder(otherSeries + selectedSeries)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
R.id.cancel_download -> {
|
R.id.cancel_download -> {
|
||||||
cancel(listOf(item.download))
|
cancel(listOf(item.download))
|
||||||
|
|
|
@ -105,6 +105,7 @@ class AnimeScreen(
|
||||||
AnimeScreen(
|
AnimeScreen(
|
||||||
state = successState,
|
state = successState,
|
||||||
snackbarHostState = screenModel.snackbarHostState,
|
snackbarHostState = screenModel.snackbarHostState,
|
||||||
|
dateRelativeTime = screenModel.relativeTime,
|
||||||
dateFormat = screenModel.dateFormat,
|
dateFormat = screenModel.dateFormat,
|
||||||
fetchInterval = successState.anime.fetchInterval,
|
fetchInterval = successState.anime.fetchInterval,
|
||||||
isTabletUi = isTabletUi(),
|
isTabletUi = isTabletUi(),
|
||||||
|
|
|
@ -8,6 +8,7 @@ import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import cafe.adriel.voyager.core.model.StateScreenModel
|
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||||
import cafe.adriel.voyager.core.model.coroutineScope
|
import cafe.adriel.voyager.core.model.coroutineScope
|
||||||
|
import eu.kanade.core.preference.asState
|
||||||
import eu.kanade.core.util.addOrRemove
|
import eu.kanade.core.util.addOrRemove
|
||||||
import eu.kanade.domain.entries.anime.interactor.SetAnimeViewerFlags
|
import eu.kanade.domain.entries.anime.interactor.SetAnimeViewerFlags
|
||||||
import eu.kanade.domain.entries.anime.interactor.UpdateAnime
|
import eu.kanade.domain.entries.anime.interactor.UpdateAnime
|
||||||
|
@ -132,6 +133,7 @@ class AnimeScreenModel(
|
||||||
val alwaysUseExternalPlayer = playerPreferences.alwaysUseExternalPlayer().get()
|
val alwaysUseExternalPlayer = playerPreferences.alwaysUseExternalPlayer().get()
|
||||||
val useExternalDownloader = downloadPreferences.useExternalDownloader().get()
|
val useExternalDownloader = downloadPreferences.useExternalDownloader().get()
|
||||||
|
|
||||||
|
val relativeTime by uiPreferences.relativeTime().asState(coroutineScope)
|
||||||
val dateFormat by mutableStateOf(UiPreferences.dateFormat(uiPreferences.dateFormat().get()))
|
val dateFormat by mutableStateOf(UiPreferences.dateFormat(uiPreferences.dateFormat().get()))
|
||||||
|
|
||||||
val isUpdateIntervalEnabled = LibraryPreferences.ENTRY_OUTSIDE_RELEASE_PERIOD in libraryPreferences.autoUpdateItemRestrictions().get()
|
val isUpdateIntervalEnabled = LibraryPreferences.ENTRY_OUTSIDE_RELEASE_PERIOD in libraryPreferences.autoUpdateItemRestrictions().get()
|
||||||
|
|
|
@ -100,6 +100,7 @@ class MangaScreen(
|
||||||
MangaScreen(
|
MangaScreen(
|
||||||
state = successState,
|
state = successState,
|
||||||
snackbarHostState = screenModel.snackbarHostState,
|
snackbarHostState = screenModel.snackbarHostState,
|
||||||
|
dateRelativeTime = screenModel.relativeTime,
|
||||||
dateFormat = screenModel.dateFormat,
|
dateFormat = screenModel.dateFormat,
|
||||||
fetchInterval = successState.manga.fetchInterval,
|
fetchInterval = successState.manga.fetchInterval,
|
||||||
isTabletUi = isTabletUi(),
|
isTabletUi = isTabletUi(),
|
||||||
|
|
|
@ -127,6 +127,7 @@ class MangaScreenModel(
|
||||||
val chapterSwipeStartAction = libraryPreferences.swipeChapterEndAction().get()
|
val chapterSwipeStartAction = libraryPreferences.swipeChapterEndAction().get()
|
||||||
val chapterSwipeEndAction = libraryPreferences.swipeChapterStartAction().get()
|
val chapterSwipeEndAction = libraryPreferences.swipeChapterStartAction().get()
|
||||||
|
|
||||||
|
val relativeTime by uiPreferences.relativeTime().asState(coroutineScope)
|
||||||
val dateFormat by mutableStateOf(UiPreferences.dateFormat(uiPreferences.dateFormat().get()))
|
val dateFormat by mutableStateOf(UiPreferences.dateFormat(uiPreferences.dateFormat().get()))
|
||||||
val skipFiltered by readerPreferences.skipFiltered().asState(coroutineScope)
|
val skipFiltered by readerPreferences.skipFiltered().asState(coroutineScope)
|
||||||
|
|
||||||
|
|
|
@ -198,7 +198,7 @@ object AnimeLibraryTab : Tab() {
|
||||||
icon = Icons.Outlined.HelpOutline,
|
icon = Icons.Outlined.HelpOutline,
|
||||||
onClick = {
|
onClick = {
|
||||||
handler.openUri(
|
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,
|
icon = Icons.Outlined.HelpOutline,
|
||||||
onClick = {
|
onClick = {
|
||||||
handler.openUri(
|
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,
|
displayMode = state.anime!!.displayMode,
|
||||||
episodeList = viewModel.currentPlaylist,
|
episodeList = viewModel.currentPlaylist,
|
||||||
currentEpisodeIndex = viewModel.getCurrentEpisodeIndex(),
|
currentEpisodeIndex = viewModel.getCurrentEpisodeIndex(),
|
||||||
relativeTime = viewModel.relativeTime,
|
dateRelativeTime = viewModel.relativeTime,
|
||||||
dateFormat = viewModel.dateFormat,
|
dateFormat = viewModel.dateFormat,
|
||||||
onBookmarkClicked = viewModel::bookmarkEpisode,
|
onBookmarkClicked = viewModel::bookmarkEpisode,
|
||||||
onEpisodeClicked = this::changeEpisode,
|
onEpisodeClicked = this::changeEpisode,
|
||||||
|
|
|
@ -50,7 +50,7 @@ fun EpisodeListDialog(
|
||||||
displayMode: Long,
|
displayMode: Long,
|
||||||
currentEpisodeIndex: Int,
|
currentEpisodeIndex: Int,
|
||||||
episodeList: List<Episode>,
|
episodeList: List<Episode>,
|
||||||
relativeTime: Int,
|
dateRelativeTime: Boolean,
|
||||||
dateFormat: DateFormat,
|
dateFormat: DateFormat,
|
||||||
onBookmarkClicked: (Long?, Boolean) -> Unit,
|
onBookmarkClicked: (Long?, Boolean) -> Unit,
|
||||||
onEpisodeClicked: (Long?) -> Unit,
|
onEpisodeClicked: (Long?) -> Unit,
|
||||||
|
@ -92,7 +92,7 @@ fun EpisodeListDialog(
|
||||||
val date = episode.date_upload
|
val date = episode.date_upload
|
||||||
.takeIf { it > 0L }
|
.takeIf { it > 0L }
|
||||||
?.let {
|
?.let {
|
||||||
Date(it).toRelativeString(context, dateFormat)
|
Date(it).toRelativeString(context, dateRelativeTime, dateFormat)
|
||||||
} ?: ""
|
} ?: ""
|
||||||
|
|
||||||
EpisodeListItem(
|
EpisodeListItem(
|
||||||
|
|
|
@ -18,7 +18,6 @@ import kotlinx.coroutines.flow.filter
|
||||||
import kotlinx.coroutines.flow.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
import kotlinx.coroutines.runInterruptible
|
import kotlinx.coroutines.runInterruptible
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
import tachiyomi.core.util.lang.awaitSingle
|
|
||||||
import tachiyomi.core.util.lang.launchIO
|
import tachiyomi.core.util.lang.launchIO
|
||||||
import tachiyomi.core.util.lang.withIOContext
|
import tachiyomi.core.util.lang.withIOContext
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
|
@ -186,7 +185,7 @@ internal class HttpPageLoader(
|
||||||
try {
|
try {
|
||||||
if (page.imageUrl.isNullOrEmpty()) {
|
if (page.imageUrl.isNullOrEmpty()) {
|
||||||
page.status = Page.State.LOAD_PAGE
|
page.status = Page.State.LOAD_PAGE
|
||||||
page.imageUrl = source.fetchImageUrl(page).awaitSingle()
|
page.imageUrl = source.getImageUrl(page)
|
||||||
}
|
}
|
||||||
val imageUrl = page.imageUrl!!
|
val imageUrl = page.imageUrl!!
|
||||||
|
|
||||||
|
|
|
@ -60,6 +60,7 @@ class AnimeUpdatesScreenModel(
|
||||||
private val getEpisode: GetEpisode = Injekt.get(),
|
private val getEpisode: GetEpisode = Injekt.get(),
|
||||||
private val libraryPreferences: LibraryPreferences = Injekt.get(),
|
private val libraryPreferences: LibraryPreferences = Injekt.get(),
|
||||||
val snackbarHostState: SnackbarHostState = SnackbarHostState(),
|
val snackbarHostState: SnackbarHostState = SnackbarHostState(),
|
||||||
|
uiPreferences: UiPreferences = Injekt.get(),
|
||||||
downloadPreferences: DownloadPreferences = Injekt.get(),
|
downloadPreferences: DownloadPreferences = Injekt.get(),
|
||||||
) : StateScreenModel<AnimeUpdatesScreenModel.State>(State()) {
|
) : StateScreenModel<AnimeUpdatesScreenModel.State>(State()) {
|
||||||
|
|
||||||
|
@ -67,6 +68,7 @@ class AnimeUpdatesScreenModel(
|
||||||
val events: Flow<Event> = _events.receiveAsFlow()
|
val events: Flow<Event> = _events.receiveAsFlow()
|
||||||
|
|
||||||
val lastUpdated by libraryPreferences.lastUpdatedTimestamp().asState(coroutineScope)
|
val lastUpdated by libraryPreferences.lastUpdatedTimestamp().asState(coroutineScope)
|
||||||
|
val relativeTime by uiPreferences.relativeTime().asState(coroutineScope)
|
||||||
|
|
||||||
val useExternalDownloader = downloadPreferences.useExternalDownloader().get()
|
val useExternalDownloader = downloadPreferences.useExternalDownloader().get()
|
||||||
|
|
||||||
|
@ -393,7 +395,7 @@ class AnimeUpdatesScreenModel(
|
||||||
val selected = items.filter { it.selected }
|
val selected = items.filter { it.selected }
|
||||||
val selectionMode = selected.isNotEmpty()
|
val selectionMode = selected.isNotEmpty()
|
||||||
|
|
||||||
fun getUiModel(context: Context): List<AnimeUpdatesUiModel> {
|
fun getUiModel(context: Context, relativeTime: Boolean): List<AnimeUpdatesUiModel> {
|
||||||
val dateFormat by mutableStateOf(
|
val dateFormat by mutableStateOf(
|
||||||
UiPreferences.dateFormat(Injekt.get<UiPreferences>().dateFormat().get()),
|
UiPreferences.dateFormat(Injekt.get<UiPreferences>().dateFormat().get()),
|
||||||
)
|
)
|
||||||
|
@ -405,7 +407,11 @@ class AnimeUpdatesScreenModel(
|
||||||
val afterDate = after?.item?.update?.dateFetch?.toDateKey() ?: Date(0)
|
val afterDate = after?.item?.update?.dateFetch?.toDateKey() ?: Date(0)
|
||||||
when {
|
when {
|
||||||
beforeDate.time != afterDate.time && afterDate.time != 0L -> {
|
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)
|
AnimeUpdatesUiModel.Header(text)
|
||||||
}
|
}
|
||||||
// Return null to avoid adding a separator between two items.
|
// Return null to avoid adding a separator between two items.
|
||||||
|
|
|
@ -59,6 +59,7 @@ fun Screen.animeUpdatesTab(
|
||||||
snackbarHostState = screenModel.snackbarHostState,
|
snackbarHostState = screenModel.snackbarHostState,
|
||||||
contentPadding = contentPadding,
|
contentPadding = contentPadding,
|
||||||
lastUpdated = screenModel.lastUpdated,
|
lastUpdated = screenModel.lastUpdated,
|
||||||
|
relativeTime = screenModel.relativeTime,
|
||||||
onClickCover = { item -> navigator.push(AnimeScreen(item.update.animeId)) },
|
onClickCover = { item -> navigator.push(AnimeScreen(item.update.animeId)) },
|
||||||
onSelectAll = screenModel::toggleAllSelection,
|
onSelectAll = screenModel::toggleAllSelection,
|
||||||
onInvertSelection = screenModel::invertSelection,
|
onInvertSelection = screenModel::invertSelection,
|
||||||
|
|
|
@ -59,12 +59,14 @@ class MangaUpdatesScreenModel(
|
||||||
private val getChapter: GetChapter = Injekt.get(),
|
private val getChapter: GetChapter = Injekt.get(),
|
||||||
private val libraryPreferences: LibraryPreferences = Injekt.get(),
|
private val libraryPreferences: LibraryPreferences = Injekt.get(),
|
||||||
val snackbarHostState: SnackbarHostState = SnackbarHostState(),
|
val snackbarHostState: SnackbarHostState = SnackbarHostState(),
|
||||||
|
uiPreferences: UiPreferences = Injekt.get(),
|
||||||
) : StateScreenModel<MangaUpdatesScreenModel.State>(State()) {
|
) : StateScreenModel<MangaUpdatesScreenModel.State>(State()) {
|
||||||
|
|
||||||
private val _events: Channel<Event> = Channel(Int.MAX_VALUE)
|
private val _events: Channel<Event> = Channel(Int.MAX_VALUE)
|
||||||
val events: Flow<Event> = _events.receiveAsFlow()
|
val events: Flow<Event> = _events.receiveAsFlow()
|
||||||
|
|
||||||
val lastUpdated by libraryPreferences.lastUpdatedTimestamp().asState(coroutineScope)
|
val lastUpdated by libraryPreferences.lastUpdatedTimestamp().asState(coroutineScope)
|
||||||
|
val relativeTime by uiPreferences.relativeTime().asState(coroutineScope)
|
||||||
|
|
||||||
// First and last selected index in list
|
// First and last selected index in list
|
||||||
private val selectedPositions: Array<Int> = arrayOf(-1, -1)
|
private val selectedPositions: Array<Int> = arrayOf(-1, -1)
|
||||||
|
@ -374,7 +376,7 @@ class MangaUpdatesScreenModel(
|
||||||
val selected = items.filter { it.selected }
|
val selected = items.filter { it.selected }
|
||||||
val selectionMode = selected.isNotEmpty()
|
val selectionMode = selected.isNotEmpty()
|
||||||
|
|
||||||
fun getUiModel(context: Context): List<MangaUpdatesUiModel> {
|
fun getUiModel(context: Context, relativeTime: Boolean): List<MangaUpdatesUiModel> {
|
||||||
val dateFormat by mutableStateOf(
|
val dateFormat by mutableStateOf(
|
||||||
UiPreferences.dateFormat(Injekt.get<UiPreferences>().dateFormat().get()),
|
UiPreferences.dateFormat(Injekt.get<UiPreferences>().dateFormat().get()),
|
||||||
)
|
)
|
||||||
|
@ -386,7 +388,11 @@ class MangaUpdatesScreenModel(
|
||||||
val afterDate = after?.item?.update?.dateFetch?.toDateKey() ?: Date(0)
|
val afterDate = after?.item?.update?.dateFetch?.toDateKey() ?: Date(0)
|
||||||
when {
|
when {
|
||||||
beforeDate.time != afterDate.time && afterDate.time != 0L -> {
|
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)
|
MangaUpdatesUiModel.Header(text)
|
||||||
}
|
}
|
||||||
// Return null to avoid adding a separator between two items.
|
// Return null to avoid adding a separator between two items.
|
||||||
|
|
|
@ -46,6 +46,7 @@ fun Screen.mangaUpdatesTab(
|
||||||
snackbarHostState = screenModel.snackbarHostState,
|
snackbarHostState = screenModel.snackbarHostState,
|
||||||
contentPadding = contentPadding,
|
contentPadding = contentPadding,
|
||||||
lastUpdated = screenModel.lastUpdated,
|
lastUpdated = screenModel.lastUpdated,
|
||||||
|
relativeTime = screenModel.relativeTime,
|
||||||
onClickCover = { item -> navigator.push(MangaScreen(item.update.mangaId)) },
|
onClickCover = { item -> navigator.push(MangaScreen(item.update.mangaId)) },
|
||||||
onSelectAll = screenModel::toggleAllSelection,
|
onSelectAll = screenModel::toggleAllSelection,
|
||||||
onInvertSelection = screenModel::invertSelection,
|
onInvertSelection = screenModel::invertSelection,
|
||||||
|
|
|
@ -4,6 +4,7 @@ import android.content.Context
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import eu.kanade.tachiyomi.BuildConfig
|
import eu.kanade.tachiyomi.BuildConfig
|
||||||
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
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.createFileInCacheDir
|
||||||
import eu.kanade.tachiyomi.util.system.toShareIntent
|
import eu.kanade.tachiyomi.util.system.toShareIntent
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
|
@ -35,6 +36,7 @@ class CrashLogUtil(private val context: Context) {
|
||||||
Device name: ${Build.DEVICE}
|
Device name: ${Build.DEVICE}
|
||||||
Device model: ${Build.MODEL}
|
Device model: ${Build.MODEL}
|
||||||
Device product name: ${Build.PRODUCT}
|
Device product name: ${Build.PRODUCT}
|
||||||
|
WebView user agent: ${WebViewUtil.getInferredUserAgent(context)}
|
||||||
""".trimIndent()
|
""".trimIndent()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,7 +8,6 @@ import java.time.LocalDateTime
|
||||||
import java.time.ZoneId
|
import java.time.ZoneId
|
||||||
import java.util.Calendar
|
import java.util.Calendar
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
import java.util.TimeZone
|
|
||||||
|
|
||||||
fun Date.toDateTimestampString(dateFormatter: DateFormat): String {
|
fun Date.toDateTimestampString(dateFormatter: DateFormat): String {
|
||||||
val date = dateFormatter.format(this)
|
val date = dateFormatter.format(this)
|
||||||
|
@ -46,80 +45,18 @@ fun Long.toDateKey(): Date {
|
||||||
return cal.time
|
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
|
private const val MILLISECONDS_IN_DAY = 86_400_000L
|
||||||
|
|
||||||
fun Date.toRelativeString(
|
fun Date.toRelativeString(
|
||||||
context: Context,
|
context: Context,
|
||||||
|
relative: Boolean = true,
|
||||||
dateFormat: DateFormat = DateFormat.getDateInstance(DateFormat.SHORT),
|
dateFormat: DateFormat = DateFormat.getDateInstance(DateFormat.SHORT),
|
||||||
): String {
|
): String {
|
||||||
|
if (!relative) {
|
||||||
|
return dateFormat.format(this)
|
||||||
|
}
|
||||||
val now = Date()
|
val now = Date()
|
||||||
val difference = now.timeWithOffset.floorNearest(MILLISECONDS_IN_DAY) - this.timeWithOffset.floorNearest(
|
val difference = now.timeWithOffset.floorNearest(MILLISECONDS_IN_DAY) - this.timeWithOffset.floorNearest(MILLISECONDS_IN_DAY)
|
||||||
MILLISECONDS_IN_DAY,
|
|
||||||
)
|
|
||||||
val days = difference.floorDiv(MILLISECONDS_IN_DAY).toInt()
|
val days = difference.floorDiv(MILLISECONDS_IN_DAY).toInt()
|
||||||
return when {
|
return when {
|
||||||
difference < 0 -> dateFormat.format(this)
|
difference < 0 -> dateFormat.format(this)
|
||||||
|
|
|
@ -13,6 +13,10 @@
|
||||||
android:id="@+id/move_to_bottom"
|
android:id="@+id/move_to_bottom"
|
||||||
android:title="@string/action_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
|
<item
|
||||||
android:id="@+id/cancel_download"
|
android:id="@+id/cancel_download"
|
||||||
android:title="@string/action_cancel" />
|
android:title="@string/action_cancel" />
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
package eu.kanade.tachiyomi.core
|
package eu.kanade.tachiyomi.core
|
||||||
|
|
||||||
object Constants {
|
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"
|
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
|
// Based on https://github.com/gildor/kotlin-coroutines-okhttp
|
||||||
@OptIn(ExperimentalCoroutinesApi::class)
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
private suspend fun Call.await(callStack: Array<StackTraceElement>): Response {
|
private suspend fun Call.await(callStack: Array<StackTraceElement>): Response {
|
||||||
|
@ -95,6 +104,9 @@ suspend fun Call.await(): Response {
|
||||||
return await(callStack)
|
return await(callStack)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @since extensions-lib 1.5
|
||||||
|
*/
|
||||||
suspend fun Call.awaitSuccess(): Response {
|
suspend fun Call.awaitSuccess(): Response {
|
||||||
val callStack = Exception().stackTrace.run { copyOfRange(1, size) }
|
val callStack = Exception().stackTrace.run { copyOfRange(1, size) }
|
||||||
val response = await(callStack)
|
val response = await(callStack)
|
||||||
|
@ -105,15 +117,6 @@ suspend fun Call.awaitSuccess(): Response {
|
||||||
return 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 {
|
fun OkHttpClient.newCachelessCallWithProgress(request: Request, listener: ProgressListener): Call {
|
||||||
val progressClient = newBuilder()
|
val progressClient = newBuilder()
|
||||||
.cache(null)
|
.cache(null)
|
||||||
|
|
|
@ -14,7 +14,24 @@ import kotlin.coroutines.resume
|
||||||
object WebViewUtil {
|
object WebViewUtil {
|
||||||
const val SPOOF_PACKAGE_NAME = "org.chromium.chrome"
|
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 {
|
fun supportsWebView(context: Context): Boolean {
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -16,18 +16,21 @@ class AnimeSourceSearchPagingSource(
|
||||||
val filters: AnimeFilterList,
|
val filters: AnimeFilterList,
|
||||||
) : AnimeSourcePagingSource(source) {
|
) : AnimeSourcePagingSource(source) {
|
||||||
override suspend fun requestNextPage(currentPage: Int): AnimesPage {
|
override suspend fun requestNextPage(currentPage: Int): AnimesPage {
|
||||||
|
// Replace with getSearchAnime
|
||||||
return source.fetchSearchAnime(currentPage, query, filters).awaitSingle()
|
return source.fetchSearchAnime(currentPage, query, filters).awaitSingle()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class AnimeSourcePopularPagingSource(source: AnimeCatalogueSource) : AnimeSourcePagingSource(source) {
|
class AnimeSourcePopularPagingSource(source: AnimeCatalogueSource) : AnimeSourcePagingSource(source) {
|
||||||
override suspend fun requestNextPage(currentPage: Int): AnimesPage {
|
override suspend fun requestNextPage(currentPage: Int): AnimesPage {
|
||||||
|
// Replace with getPopularAnime
|
||||||
return source.fetchPopularAnime(currentPage).awaitSingle()
|
return source.fetchPopularAnime(currentPage).awaitSingle()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class AnimeSourceLatestPagingSource(source: AnimeCatalogueSource) : AnimeSourcePagingSource(source) {
|
class AnimeSourceLatestPagingSource(source: AnimeCatalogueSource) : AnimeSourcePagingSource(source) {
|
||||||
override suspend fun requestNextPage(currentPage: Int): AnimesPage {
|
override suspend fun requestNextPage(currentPage: Int): AnimesPage {
|
||||||
|
// Replace with getLatestUpdates
|
||||||
return source.fetchLatestUpdates(currentPage).awaitSingle()
|
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.FilterList
|
||||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import tachiyomi.core.util.lang.awaitSingle
|
|
||||||
import tachiyomi.core.util.lang.withIOContext
|
import tachiyomi.core.util.lang.withIOContext
|
||||||
import tachiyomi.domain.items.chapter.model.NoChaptersException
|
import tachiyomi.domain.items.chapter.model.NoChaptersException
|
||||||
import tachiyomi.domain.source.manga.repository.SourcePagingSourceType
|
import tachiyomi.domain.source.manga.repository.SourcePagingSourceType
|
||||||
|
@ -14,19 +13,19 @@ class SourceSearchPagingSource(source: CatalogueSource, val query: String, val f
|
||||||
source,
|
source,
|
||||||
) {
|
) {
|
||||||
override suspend fun requestNextPage(currentPage: Int): MangasPage {
|
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) {
|
class SourcePopularPagingSource(source: CatalogueSource) : SourcePagingSource(source) {
|
||||||
override suspend fun requestNextPage(currentPage: Int): MangasPage {
|
override suspend fun requestNextPage(currentPage: Int): MangasPage {
|
||||||
return source.fetchPopularManga(currentPage).awaitSingle()
|
return source.getPopularManga(currentPage)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class SourceLatestPagingSource(source: CatalogueSource) : SourcePagingSource(source) {
|
class SourceLatestPagingSource(source: CatalogueSource) : SourcePagingSource(source) {
|
||||||
override suspend fun requestNextPage(currentPage: Int): MangasPage {
|
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.entries.anime.model.AnimeUpdate
|
||||||
import tachiyomi.domain.items.episode.interactor.GetEpisodeByAnimeId
|
import tachiyomi.domain.items.episode.interactor.GetEpisodeByAnimeId
|
||||||
import tachiyomi.domain.items.episode.model.Episode
|
import tachiyomi.domain.items.episode.model.Episode
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
|
import java.time.ZoneId
|
||||||
import java.time.ZonedDateTime
|
import java.time.ZonedDateTime
|
||||||
import java.time.temporal.ChronoUnit
|
import java.time.temporal.ChronoUnit
|
||||||
import kotlin.math.absoluteValue
|
import kotlin.math.absoluteValue
|
||||||
|
|
||||||
const val MAX_FETCH_INTERVAL = 28
|
class AnimeFetchInterval(
|
||||||
private const val FETCH_INTERVAL_GRACE_PERIOD = 1
|
|
||||||
|
|
||||||
class SetAnimeFetchInterval(
|
|
||||||
private val getEpisodeByAnimeId: GetEpisodeByAnimeId,
|
private val getEpisodeByAnimeId: GetEpisodeByAnimeId,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
@ -30,7 +27,7 @@ class SetAnimeFetchInterval(
|
||||||
val episodes = getEpisodeByAnimeId.await(anime.id)
|
val episodes = getEpisodeByAnimeId.await(anime.id)
|
||||||
val interval = anime.fetchInterval.takeIf { it < 0 } ?: calculateInterval(
|
val interval = anime.fetchInterval.takeIf { it < 0 } ?: calculateInterval(
|
||||||
episodes,
|
episodes,
|
||||||
dateTime,
|
dateTime.zone,
|
||||||
)
|
)
|
||||||
val nextUpdate = calculateNextUpdate(anime, interval, dateTime, currentWindow)
|
val nextUpdate = calculateNextUpdate(anime, interval, dateTime, currentWindow)
|
||||||
|
|
||||||
|
@ -43,33 +40,34 @@ class SetAnimeFetchInterval(
|
||||||
|
|
||||||
fun getWindow(dateTime: ZonedDateTime): Pair<Long, Long> {
|
fun getWindow(dateTime: ZonedDateTime): Pair<Long, Long> {
|
||||||
val today = dateTime.toLocalDate().atStartOfDay(dateTime.zone)
|
val today = dateTime.toLocalDate().atStartOfDay(dateTime.zone)
|
||||||
val lowerBound = today.minusDays(FETCH_INTERVAL_GRACE_PERIOD.toLong())
|
val lowerBound = today.minusDays(GRACE_PERIOD)
|
||||||
val upperBound = today.plusDays(FETCH_INTERVAL_GRACE_PERIOD.toLong())
|
val upperBound = today.plusDays(GRACE_PERIOD)
|
||||||
return Pair(lowerBound.toEpochSecond() * 1000, upperBound.toEpochSecond() * 1000 - 1)
|
return Pair(lowerBound.toEpochSecond() * 1000, upperBound.toEpochSecond() * 1000 - 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun calculateInterval(episodes: List<Episode>, zonedDateTime: ZonedDateTime): Int {
|
internal fun calculateInterval(episodes: List<Episode>, zone: ZoneId): Int {
|
||||||
val sortedEpisodes = episodes
|
val uploadDates = episodes.asSequence()
|
||||||
.sortedWith(
|
|
||||||
compareByDescending<Episode> { it.dateUpload }.thenByDescending { it.dateFetch },
|
|
||||||
)
|
|
||||||
.take(50)
|
|
||||||
|
|
||||||
val uploadDates = sortedEpisodes
|
|
||||||
.filter { it.dateUpload > 0L }
|
.filter { it.dateUpload > 0L }
|
||||||
|
.sortedByDescending { it.dateUpload }
|
||||||
.map {
|
.map {
|
||||||
ZonedDateTime.ofInstant(Instant.ofEpochMilli(it.dateUpload), zonedDateTime.zone)
|
ZonedDateTime.ofInstant(Instant.ofEpochMilli(it.dateUpload), zone)
|
||||||
.toLocalDate()
|
.toLocalDate()
|
||||||
.atStartOfDay()
|
.atStartOfDay()
|
||||||
}
|
}
|
||||||
.distinct()
|
.distinct()
|
||||||
val fetchDates = sortedEpisodes
|
.take(10)
|
||||||
|
.toList()
|
||||||
|
|
||||||
|
val fetchDates = episodes.asSequence()
|
||||||
|
.sortedByDescending { it.dateFetch }
|
||||||
.map {
|
.map {
|
||||||
ZonedDateTime.ofInstant(Instant.ofEpochMilli(it.dateFetch), zonedDateTime.zone)
|
ZonedDateTime.ofInstant(Instant.ofEpochMilli(it.dateFetch), zone)
|
||||||
.toLocalDate()
|
.toLocalDate()
|
||||||
.atStartOfDay()
|
.atStartOfDay()
|
||||||
}
|
}
|
||||||
.distinct()
|
.distinct()
|
||||||
|
.take(10)
|
||||||
|
.toList()
|
||||||
|
|
||||||
val interval = when {
|
val interval = when {
|
||||||
// Enough upload date from source
|
// Enough upload date from source
|
||||||
|
@ -88,7 +86,7 @@ class SetAnimeFetchInterval(
|
||||||
else -> 7
|
else -> 7
|
||||||
}
|
}
|
||||||
|
|
||||||
return interval.coerceIn(1, MAX_FETCH_INTERVAL)
|
return interval.coerceIn(1, MAX_INTERVAL)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun calculateNextUpdate(
|
private fun calculateNextUpdate(
|
||||||
|
@ -119,7 +117,7 @@ class SetAnimeFetchInterval(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun doubleInterval(delta: Int, timeSinceLatest: Int, doubleWhenOver: Int): Int {
|
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
|
// double delta again if missed more than 9 check in new delta
|
||||||
val cycle = timeSinceLatest.floorDiv(delta) + 1
|
val cycle = timeSinceLatest.floorDiv(delta) + 1
|
||||||
|
@ -129,4 +127,10 @@ class SetAnimeFetchInterval(
|
||||||
delta
|
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.entries.manga.model.MangaUpdate
|
||||||
import tachiyomi.domain.items.chapter.interactor.GetChapterByMangaId
|
import tachiyomi.domain.items.chapter.interactor.GetChapterByMangaId
|
||||||
import tachiyomi.domain.items.chapter.model.Chapter
|
import tachiyomi.domain.items.chapter.model.Chapter
|
||||||
import uy.kohesive.injekt.api.get
|
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
|
import java.time.ZoneId
|
||||||
import java.time.ZonedDateTime
|
import java.time.ZonedDateTime
|
||||||
import java.time.temporal.ChronoUnit
|
import java.time.temporal.ChronoUnit
|
||||||
import kotlin.math.absoluteValue
|
import kotlin.math.absoluteValue
|
||||||
|
|
||||||
const val MAX_FETCH_INTERVAL = 28
|
class MangaFetchInterval(
|
||||||
private const val FETCH_INTERVAL_GRACE_PERIOD = 1
|
|
||||||
|
|
||||||
class SetMangaFetchInterval(
|
|
||||||
private val getChapterByMangaId: GetChapterByMangaId,
|
private val getChapterByMangaId: GetChapterByMangaId,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
@ -30,7 +27,7 @@ class SetMangaFetchInterval(
|
||||||
val chapters = getChapterByMangaId.await(manga.id)
|
val chapters = getChapterByMangaId.await(manga.id)
|
||||||
val interval = manga.fetchInterval.takeIf { it < 0 } ?: calculateInterval(
|
val interval = manga.fetchInterval.takeIf { it < 0 } ?: calculateInterval(
|
||||||
chapters,
|
chapters,
|
||||||
dateTime,
|
dateTime.zone,
|
||||||
)
|
)
|
||||||
val nextUpdate = calculateNextUpdate(manga, interval, dateTime, currentWindow)
|
val nextUpdate = calculateNextUpdate(manga, interval, dateTime, currentWindow)
|
||||||
|
|
||||||
|
@ -43,33 +40,34 @@ class SetMangaFetchInterval(
|
||||||
|
|
||||||
fun getWindow(dateTime: ZonedDateTime): Pair<Long, Long> {
|
fun getWindow(dateTime: ZonedDateTime): Pair<Long, Long> {
|
||||||
val today = dateTime.toLocalDate().atStartOfDay(dateTime.zone)
|
val today = dateTime.toLocalDate().atStartOfDay(dateTime.zone)
|
||||||
val lowerBound = today.minusDays(FETCH_INTERVAL_GRACE_PERIOD.toLong())
|
val lowerBound = today.minusDays(GRACE_PERIOD)
|
||||||
val upperBound = today.plusDays(FETCH_INTERVAL_GRACE_PERIOD.toLong())
|
val upperBound = today.plusDays(GRACE_PERIOD)
|
||||||
return Pair(lowerBound.toEpochSecond() * 1000, upperBound.toEpochSecond() * 1000 - 1)
|
return Pair(lowerBound.toEpochSecond() * 1000, upperBound.toEpochSecond() * 1000 - 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun calculateInterval(chapters: List<Chapter>, zonedDateTime: ZonedDateTime): Int {
|
internal fun calculateInterval(chapters: List<Chapter>, zone: ZoneId): Int {
|
||||||
val sortedChapters = chapters
|
val uploadDates = chapters.asSequence()
|
||||||
.sortedWith(
|
|
||||||
compareByDescending<Chapter> { it.dateUpload }.thenByDescending { it.dateFetch },
|
|
||||||
)
|
|
||||||
.take(50)
|
|
||||||
|
|
||||||
val uploadDates = sortedChapters
|
|
||||||
.filter { it.dateUpload > 0L }
|
.filter { it.dateUpload > 0L }
|
||||||
|
.sortedByDescending { it.dateUpload }
|
||||||
.map {
|
.map {
|
||||||
ZonedDateTime.ofInstant(Instant.ofEpochMilli(it.dateUpload), zonedDateTime.zone)
|
ZonedDateTime.ofInstant(Instant.ofEpochMilli(it.dateUpload), zone)
|
||||||
.toLocalDate()
|
.toLocalDate()
|
||||||
.atStartOfDay()
|
.atStartOfDay()
|
||||||
}
|
}
|
||||||
.distinct()
|
.distinct()
|
||||||
val fetchDates = sortedChapters
|
.take(10)
|
||||||
|
.toList()
|
||||||
|
|
||||||
|
val fetchDates = chapters.asSequence()
|
||||||
|
.sortedByDescending { it.dateFetch }
|
||||||
.map {
|
.map {
|
||||||
ZonedDateTime.ofInstant(Instant.ofEpochMilli(it.dateFetch), zonedDateTime.zone)
|
ZonedDateTime.ofInstant(Instant.ofEpochMilli(it.dateFetch), zone)
|
||||||
.toLocalDate()
|
.toLocalDate()
|
||||||
.atStartOfDay()
|
.atStartOfDay()
|
||||||
}
|
}
|
||||||
.distinct()
|
.distinct()
|
||||||
|
.take(10)
|
||||||
|
.toList()
|
||||||
|
|
||||||
val interval = when {
|
val interval = when {
|
||||||
// Enough upload date from source
|
// Enough upload date from source
|
||||||
|
@ -88,7 +86,7 @@ class SetMangaFetchInterval(
|
||||||
else -> 7
|
else -> 7
|
||||||
}
|
}
|
||||||
|
|
||||||
return interval.coerceIn(1, MAX_FETCH_INTERVAL)
|
return interval.coerceIn(1, MAX_INTERVAL)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun calculateNextUpdate(
|
private fun calculateNextUpdate(
|
||||||
|
@ -119,7 +117,7 @@ class SetMangaFetchInterval(
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun doubleInterval(delta: Int, timeSinceLatest: Int, doubleWhenOver: Int): Int {
|
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
|
// double delta again if missed more than 9 check in new delta
|
||||||
val cycle = timeSinceLatest.floorDiv(delta) + 1
|
val cycle = timeSinceLatest.floorDiv(delta) + 1
|
||||||
|
@ -129,4 +127,10 @@ class SetMangaFetchInterval(
|
||||||
delta
|
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()
|
private val isInvalid: Boolean = name.isBlank() || lang.isBlank()
|
||||||
|
|
||||||
override suspend fun getAnimeDetails(anime: SAnime): SAnime {
|
override suspend fun getAnimeDetails(anime: SAnime): SAnime =
|
||||||
throw AnimeSourceNotInstalledException()
|
throw AnimeSourceNotInstalledException()
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getEpisodeList(anime: SAnime): List<SEpisode> {
|
override suspend fun getEpisodeList(anime: SAnime): List<SEpisode> =
|
||||||
throw AnimeSourceNotInstalledException()
|
throw AnimeSourceNotInstalledException()
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getVideoList(episode: SEpisode): List<Video> {
|
override suspend fun getVideoList(episode: SEpisode): List<Video> =
|
||||||
throw AnimeSourceNotInstalledException()
|
throw AnimeSourceNotInstalledException()
|
||||||
}
|
|
||||||
|
|
||||||
override fun toString(): String {
|
override fun toString(): String =
|
||||||
return if (isInvalid.not()) "$name (${lang.uppercase()})" else id.toString()
|
if (isInvalid.not()) "$name (${lang.uppercase()})" else id.toString()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
class AnimeSourceNotInstalledException : Exception()
|
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.Page
|
||||||
import eu.kanade.tachiyomi.source.model.SChapter
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
import eu.kanade.tachiyomi.source.model.SManga
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
import rx.Observable
|
|
||||||
|
|
||||||
@Suppress("OverridingDeprecatedMember")
|
@Suppress("OverridingDeprecatedMember")
|
||||||
class StubMangaSource(
|
class StubMangaSource(
|
||||||
|
@ -15,36 +14,17 @@ class StubMangaSource(
|
||||||
|
|
||||||
private val isInvalid: Boolean = name.isBlank() || lang.isBlank()
|
private val isInvalid: Boolean = name.isBlank() || lang.isBlank()
|
||||||
|
|
||||||
override suspend fun getMangaDetails(manga: SManga): SManga {
|
override suspend fun getMangaDetails(manga: SManga): SManga =
|
||||||
throw SourceNotInstalledException()
|
throw SourceNotInstalledException()
|
||||||
}
|
|
||||||
|
|
||||||
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getMangaDetails"))
|
override suspend fun getChapterList(manga: SManga): List<SChapter> =
|
||||||
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
|
||||||
return Observable.error(SourceNotInstalledException())
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getChapterList(manga: SManga): List<SChapter> {
|
|
||||||
throw SourceNotInstalledException()
|
throw SourceNotInstalledException()
|
||||||
}
|
|
||||||
|
|
||||||
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getChapterList"))
|
override suspend fun getPageList(chapter: SChapter): List<Page> =
|
||||||
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
|
||||||
return Observable.error(SourceNotInstalledException())
|
|
||||||
}
|
|
||||||
|
|
||||||
override suspend fun getPageList(chapter: SChapter): List<Page> {
|
|
||||||
throw SourceNotInstalledException()
|
throw SourceNotInstalledException()
|
||||||
}
|
|
||||||
|
|
||||||
@Deprecated("Use the non-RxJava API instead", replaceWith = ReplaceWith("getPageList"))
|
override fun toString(): String =
|
||||||
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
|
if (isInvalid.not()) "$name (${lang.uppercase()})" else id.toString()
|
||||||
return Observable.error(SourceNotInstalledException())
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun toString(): String {
|
|
||||||
return if (isInvalid.not()) "$name (${lang.uppercase()})" else id.toString()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class SourceNotInstalledException : Exception()
|
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-runtime = { module = "androidx.paging:paging-runtime", version.ref = "paging_version" }
|
||||||
paging-compose = { module = "androidx.paging:paging-compose", 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-ext = "androidx.test.ext:junit-ktx:1.2.0-alpha01"
|
||||||
test-espresso-core = "androidx.test.espresso:espresso-core:3.6.0-alpha01"
|
test-espresso-core = "androidx.test.espresso:espresso-core:3.6.0-alpha01"
|
||||||
test-uiautomator = "androidx.test.uiautomator:uiautomator:2.3.0-alpha04"
|
test-uiautomator = "androidx.test.uiautomator:uiautomator:2.3.0-alpha04"
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
[versions]
|
[versions]
|
||||||
compiler = "1.5.2"
|
compiler = "1.5.3"
|
||||||
compose-bom = "2023.09.00-alpha02"
|
compose-bom = "2023.09.00-alpha02"
|
||||||
accompanist = "0.33.1-alpha"
|
accompanist = "0.33.1-alpha"
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
[versions]
|
[versions]
|
||||||
kotlin_version = "1.9.0"
|
kotlin_version = "1.9.10"
|
||||||
serialization_version = "1.6.0"
|
serialization_version = "1.6.0"
|
||||||
xml_serialization_version = "0.86.1"
|
xml_serialization_version = "0.86.2"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin_version" }
|
reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin_version" }
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
[versions]
|
[versions]
|
||||||
aboutlib_version = "10.8.3"
|
aboutlib_version = "10.9.0"
|
||||||
okhttp_version = "5.0.0-alpha.11"
|
okhttp_version = "5.0.0-alpha.11"
|
||||||
shizuku_version = "12.2.0"
|
shizuku_version = "12.2.0"
|
||||||
sqlite = "2.3.1"
|
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"
|
photoview = "com.github.chrisbanes:PhotoView:2.3.0"
|
||||||
directionalviewpager = "com.github.tachiyomiorg:DirectionalViewPager:1.0.0"
|
directionalviewpager = "com.github.tachiyomiorg:DirectionalViewPager:1.0.0"
|
||||||
insetter = "dev.chrisbanes.insetter:insetter:0.6.1"
|
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"
|
compose-simpleicons = "br.com.devsrsouza.compose.icons.android:simple-icons:1.0.0"
|
||||||
|
|
||||||
swipe = "me.saket.swipe:swipe:1.2.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"
|
junit = "org.junit.jupiter:junit-jupiter:5.10.0"
|
||||||
kotest-assertions = "io.kotest:kotest-assertions-core:5.7.2"
|
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-navigator = { module = "cafe.adriel.voyager:voyager-navigator", version.ref = "voyager" }
|
||||||
voyager-tab-navigator = { module = "cafe.adriel.voyager:voyager-tab-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" }
|
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"
|
aniyomi-mpv = "com.github.aniyomiorg:aniyomi-mpv-lib:1.10.n"
|
||||||
ffmpeg-kit = "com.github.jmir1:ffmpeg-kit:1.10"
|
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">Move to top</string>
|
||||||
<string name="action_move_to_top_all_for_series">Move series 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">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_install">Install</string>
|
||||||
<string name="action_share">Share</string>
|
<string name="action_share">Share</string>
|
||||||
<string name="action_save">Save</string>
|
<string name="action_save">Save</string>
|
||||||
|
@ -205,6 +206,9 @@
|
||||||
<string name="theme_matrix">Matrix</string>
|
<string name="theme_matrix">Matrix</string>
|
||||||
<string name="theme_tidalwave">Tidal Wave</string>
|
<string name="theme_tidalwave">Tidal Wave</string>
|
||||||
<string name="pref_dark_theme_pure_black">Pure black dark mode</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_date_format">Date format</string>
|
||||||
|
|
||||||
<string name="pref_manage_notifications">Manage notifications</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_0">No animation</string>
|
||||||
<string name="double_tap_anim_speed_normal">Normal</string>
|
<string name="double_tap_anim_speed_normal">Normal</string>
|
||||||
<string name="double_tap_anim_speed_fast">Fast</string>
|
<string name="double_tap_anim_speed_fast">Fast</string>
|
||||||
<string name="pref_rotation_type">Default rotation type</string>
|
<string name="pref_rotation_type">Default rotation</string>
|
||||||
<string name="rotation_type">Rotation type</string>
|
<string name="rotation_type">Rotation</string>
|
||||||
<string name="rotation_free">Free</string>
|
<string name="rotation_free">Free</string>
|
||||||
<string name="rotation_portrait">Portrait</string>
|
<string name="rotation_portrait">Portrait</string>
|
||||||
<string name="rotation_reverse_portrait">Reverse portrait</string>
|
<string name="rotation_reverse_portrait">Reverse portrait</string>
|
||||||
|
@ -451,13 +455,13 @@
|
||||||
|
|
||||||
<!-- Tracking section -->
|
<!-- Tracking section -->
|
||||||
<string name="tracking_guide">Tracking guide</string>
|
<string name="tracking_guide">Tracking guide</string>
|
||||||
<string name="services">Services</string>
|
<string name="services">Trackers</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="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 services</string>
|
<string name="enhanced_services">Enhanced trackers</string>
|
||||||
<string name="enhanced_services_not_installed">Available but source not installed: %s</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="action_track">Track</string>
|
||||||
<string name="track_activity_name">Tracking login</string>
|
<string name="track_activity_name">Tracker login</string>
|
||||||
|
|
||||||
<!-- Browse section -->
|
<!-- Browse section -->
|
||||||
<string name="pref_hide_in_library_items">Hide entries already in library</string>
|
<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.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.layout.widthIn
|
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.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.material.ContentAlpha
|
import androidx.compose.material.ContentAlpha
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
|
@ -54,6 +57,7 @@ import kotlinx.coroutines.delay
|
||||||
import tachiyomi.core.preference.Preference
|
import tachiyomi.core.preference.Preference
|
||||||
import tachiyomi.core.preference.TriState
|
import tachiyomi.core.preference.TriState
|
||||||
import tachiyomi.core.preference.toggle
|
import tachiyomi.core.preference.toggle
|
||||||
|
import tachiyomi.presentation.core.components.material.padding
|
||||||
import tachiyomi.presentation.core.theme.header
|
import tachiyomi.presentation.core.theme.header
|
||||||
import tachiyomi.presentation.core.util.collectAsState
|
import tachiyomi.presentation.core.util.collectAsState
|
||||||
|
|
||||||
|
@ -297,7 +301,7 @@ fun TriStateItem(
|
||||||
vertical = SettingsItemsPaddings.Vertical,
|
vertical = SettingsItemsPaddings.Vertical,
|
||||||
),
|
),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
horizontalArrangement = Arrangement.spacedBy(24.dp),
|
horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.large),
|
||||||
) {
|
) {
|
||||||
val stateAlpha = if (enabled && onClick != null) 1f else ContentAlpha.disabled
|
val stateAlpha = if (enabled && onClick != null) 1f else ContentAlpha.disabled
|
||||||
|
|
||||||
|
@ -504,7 +508,25 @@ fun SettingsChipRow(
|
||||||
end = SettingsItemsPaddings.Horizontal,
|
end = SettingsItemsPaddings.Horizontal,
|
||||||
bottom = SettingsItemsPaddings.Vertical,
|
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,
|
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.AnimeFilterList
|
||||||
import eu.kanade.tachiyomi.animesource.model.AnimesPage
|
import eu.kanade.tachiyomi.animesource.model.AnimesPage
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
|
import tachiyomi.core.util.lang.awaitSingle
|
||||||
|
|
||||||
interface AnimeCatalogueSource : AnimeSource {
|
interface AnimeCatalogueSource : AnimeSource {
|
||||||
|
|
||||||
|
@ -17,30 +18,63 @@ interface AnimeCatalogueSource : AnimeSource {
|
||||||
val supportsLatest: Boolean
|
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.
|
* @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 page the page number to retrieve.
|
||||||
* @param query the search query.
|
* @param query the search query.
|
||||||
* @param filters the list of filters to apply.
|
* @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.
|
* @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.
|
* Returns the list of filters for the source.
|
||||||
*/
|
*/
|
||||||
fun getFilterList(): AnimeFilterList
|
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.
|
* Get the updated details for a anime.
|
||||||
*
|
*
|
||||||
* @since extensions-lib 1.4
|
* @since extensions-lib 1.5
|
||||||
* @param anime the anime to update.
|
* @param anime the anime to update.
|
||||||
* @return the updated anime.
|
* @return the updated anime.
|
||||||
*/
|
*/
|
||||||
|
@ -39,7 +39,7 @@ interface AnimeSource {
|
||||||
/**
|
/**
|
||||||
* Get all the available episodes for a anime.
|
* Get all the available episodes for a anime.
|
||||||
*
|
*
|
||||||
* @since extensions-lib 1.4
|
* @since extensions-lib 1.5
|
||||||
* @param anime the anime to update.
|
* @param anime the anime to update.
|
||||||
* @return the episodes for the anime.
|
* @return the episodes for the anime.
|
||||||
*/
|
*/
|
||||||
|
@ -52,7 +52,7 @@ interface AnimeSource {
|
||||||
* Get the list of videos a episode has. Pages should be returned
|
* Get the list of videos a episode has. Pages should be returned
|
||||||
* in the expected order; the index is ignored.
|
* in the expected order; the index is ignored.
|
||||||
*
|
*
|
||||||
* @since extensions-lib 1.4
|
* @since extensions-lib 1.5
|
||||||
* @param episode the episode.
|
* @param episode the episode.
|
||||||
* @return the videos for the episode.
|
* @return the videos for the episode.
|
||||||
*/
|
*/
|
||||||
|
@ -61,41 +61,24 @@ interface AnimeSource {
|
||||||
return fetchVideoList(episode).awaitSingle()
|
return fetchVideoList(episode).awaitSingle()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an observable with the updated details for a anime.
|
|
||||||
*
|
|
||||||
* @param anime the anime to update.
|
|
||||||
*/
|
|
||||||
@Deprecated(
|
@Deprecated(
|
||||||
"Use the non-RxJava API instead",
|
"Use the non-RxJava API instead",
|
||||||
ReplaceWith("getAnimeDetails"),
|
ReplaceWith("getAnimeDetails"),
|
||||||
)
|
)
|
||||||
fun fetchAnimeDetails(anime: SAnime): Observable<SAnime> = throw IllegalStateException(
|
fun fetchAnimeDetails(anime: SAnime): Observable<SAnime> =
|
||||||
"Not used",
|
throw IllegalStateException("Not used")
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an observable with all the available episodes for a anime.
|
|
||||||
*
|
|
||||||
* @param anime the anime to update.
|
|
||||||
*/
|
|
||||||
@Deprecated(
|
@Deprecated(
|
||||||
"Use the non-RxJava API instead",
|
"Use the non-RxJava API instead",
|
||||||
ReplaceWith("getEpisodeList"),
|
ReplaceWith("getEpisodeList"),
|
||||||
)
|
)
|
||||||
fun fetchEpisodeList(anime: SAnime): Observable<List<SEpisode>> = throw IllegalStateException(
|
fun fetchEpisodeList(anime: SAnime): Observable<List<SEpisode>> =
|
||||||
"Not used",
|
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(
|
@Deprecated(
|
||||||
"Use the non-RxJava API instead",
|
"Use the non-RxJava API instead",
|
||||||
ReplaceWith("getVideoList"),
|
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
|
package eu.kanade.tachiyomi.animesource
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.SharedPreferences
|
||||||
import eu.kanade.tachiyomi.PreferenceScreen
|
import eu.kanade.tachiyomi.PreferenceScreen
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
interface ConfigurableAnimeSource : AnimeSource {
|
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)
|
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.SEpisode
|
||||||
import eu.kanade.tachiyomi.animesource.model.Video
|
import eu.kanade.tachiyomi.animesource.model.Video
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.network.HttpException
|
|
||||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||||
import eu.kanade.tachiyomi.network.asObservableSuccess
|
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||||
|
import eu.kanade.tachiyomi.network.awaitSuccess
|
||||||
import eu.kanade.tachiyomi.network.newCachelessCallWithProgress
|
import eu.kanade.tachiyomi.network.newCachelessCallWithProgress
|
||||||
import okhttp3.Call
|
|
||||||
import okhttp3.Callback
|
|
||||||
import okhttp3.Headers
|
import okhttp3.Headers
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.Request
|
import okhttp3.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
|
import tachiyomi.core.util.lang.awaitSingle
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.io.IOException
|
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
import java.net.URISyntaxException
|
import java.net.URISyntaxException
|
||||||
import java.security.MessageDigest
|
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.
|
* A simple implementation for sources from a website.
|
||||||
*/
|
*/
|
||||||
|
@Suppress("unused")
|
||||||
abstract class AnimeHttpSource : AnimeCatalogueSource {
|
abstract class AnimeHttpSource : AnimeCatalogueSource {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -88,6 +83,7 @@ abstract class AnimeHttpSource : AnimeCatalogueSource {
|
||||||
* @param versionId [Int] the version ID of the source
|
* @param versionId [Int] the version ID of the source
|
||||||
* @return a unique ID for the source
|
* @return a unique ID for the source
|
||||||
*/
|
*/
|
||||||
|
@Suppress("MemberVisibilityCanBePrivate")
|
||||||
protected fun generateId(name: String, lang: String, versionId: Int): Long {
|
protected fun generateId(name: String, lang: String, versionId: Int): Long {
|
||||||
val key = "${name.lowercase()}/$lang/$versionId"
|
val key = "${name.lowercase()}/$lang/$versionId"
|
||||||
val bytes = MessageDigest.getInstance("MD5").digest(key.toByteArray())
|
val bytes = MessageDigest.getInstance("MD5").digest(key.toByteArray())
|
||||||
|
@ -201,11 +197,18 @@ abstract class AnimeHttpSource : AnimeCatalogueSource {
|
||||||
protected abstract fun latestUpdatesParse(response: Response): AnimesPage
|
protected abstract fun latestUpdatesParse(response: Response): AnimesPage
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an observable with the updated details for a nanime. Normally it's not needed to
|
* Get the updated details for a anime.
|
||||||
* override this method.
|
* Normally it's not needed to override this method.
|
||||||
*
|
*
|
||||||
* @param anime the anime to be updated.
|
* @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> {
|
override fun fetchAnimeDetails(anime: SAnime): Observable<SAnime> {
|
||||||
return client.newCall(animeDetailsRequest(anime))
|
return client.newCall(animeDetailsRequest(anime))
|
||||||
.asObservableSuccess()
|
.asObservableSuccess()
|
||||||
|
@ -232,11 +235,23 @@ abstract class AnimeHttpSource : AnimeCatalogueSource {
|
||||||
protected abstract fun animeDetailsParse(response: Response): SAnime
|
protected abstract fun animeDetailsParse(response: Response): SAnime
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an observable with the updated episode list for an anime. Normally it's not needed to
|
* Get all the available episodes for an anime.
|
||||||
* override this method. If an anime is licensed an empty episode list observable is returned
|
* 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>> {
|
override fun fetchEpisodeList(anime: SAnime): Observable<List<SEpisode>> {
|
||||||
return if (anime.status != SAnime.LICENSED) {
|
return if (anime.status != SAnime.LICENSED) {
|
||||||
client.newCall(episodeListRequest(anime))
|
client.newCall(episodeListRequest(anime))
|
||||||
|
@ -267,10 +282,18 @@ abstract class AnimeHttpSource : AnimeCatalogueSource {
|
||||||
protected abstract fun episodeListParse(response: Response): List<SEpisode>
|
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>> {
|
override fun fetchVideoList(episode: SEpisode): Observable<List<Video>> {
|
||||||
return client.newCall(videoListRequest(episode))
|
return client.newCall(videoListRequest(episode))
|
||||||
.asObservableSuccess()
|
.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
|
* 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.
|
* 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.
|
* @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> {
|
open fun fetchVideoUrl(video: Video): Observable<String> {
|
||||||
return client.newCall(videoUrlRequest(video))
|
return client.newCall(videoUrlRequest(video))
|
||||||
.asObservableSuccess()
|
.asObservableSuccess()
|
||||||
|
@ -333,36 +363,15 @@ abstract class AnimeHttpSource : AnimeCatalogueSource {
|
||||||
protected abstract fun videoUrlParse(response: Response): String
|
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 {
|
open suspend fun getVideo(video: Video): Response {
|
||||||
val animeDownloadClient = client.newBuilder()
|
return client.newCachelessCallWithProgress(videoRequest(video), video)
|
||||||
.callTimeout(30, TimeUnit.MINUTES)
|
.awaitSuccess()
|
||||||
.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))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -440,7 +449,7 @@ abstract class AnimeHttpSource : AnimeCatalogueSource {
|
||||||
* @param episode the episode
|
* @param episode the episode
|
||||||
* @return url of the episode
|
* @return url of the episode
|
||||||
*/
|
*/
|
||||||
open fun getChapterUrl(episode: SEpisode): String {
|
open fun getEpisodeUrl(episode: SEpisode): String {
|
||||||
return episode.url.toString()
|
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.FilterList
|
||||||
import eu.kanade.tachiyomi.source.model.MangasPage
|
import eu.kanade.tachiyomi.source.model.MangasPage
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
|
import tachiyomi.core.util.lang.awaitSingle
|
||||||
|
|
||||||
interface CatalogueSource : MangaSource {
|
interface CatalogueSource : MangaSource {
|
||||||
|
|
||||||
|
@ -17,30 +18,63 @@ interface CatalogueSource : MangaSource {
|
||||||
val supportsLatest: Boolean
|
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.
|
* @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 page the page number to retrieve.
|
||||||
* @param query the search query.
|
* @param query the search query.
|
||||||
* @param filters the list of filters to apply.
|
* @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.
|
* @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.
|
* Returns the list of filters for the source.
|
||||||
*/
|
*/
|
||||||
fun getFilterList(): FilterList
|
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
|
package eu.kanade.tachiyomi.source
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.SharedPreferences
|
||||||
import eu.kanade.tachiyomi.PreferenceScreen
|
import eu.kanade.tachiyomi.PreferenceScreen
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
interface ConfigurableSource : MangaSource {
|
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)
|
fun setupPreferenceScreen(screen: PreferenceScreen)
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,7 +27,7 @@ interface MangaSource {
|
||||||
/**
|
/**
|
||||||
* Get the updated details for a manga.
|
* Get the updated details for a manga.
|
||||||
*
|
*
|
||||||
* @since extensions-lib 1.4
|
* @since extensions-lib 1.5
|
||||||
* @param manga the manga to update.
|
* @param manga the manga to update.
|
||||||
* @return the updated manga.
|
* @return the updated manga.
|
||||||
*/
|
*/
|
||||||
|
@ -39,7 +39,7 @@ interface MangaSource {
|
||||||
/**
|
/**
|
||||||
* Get all the available chapters for a manga.
|
* Get all the available chapters for a manga.
|
||||||
*
|
*
|
||||||
* @since extensions-lib 1.4
|
* @since extensions-lib 1.5
|
||||||
* @param manga the manga to update.
|
* @param manga the manga to update.
|
||||||
* @return the chapters for the manga.
|
* @return the chapters for the manga.
|
||||||
*/
|
*/
|
||||||
|
@ -52,7 +52,7 @@ interface MangaSource {
|
||||||
* Get the list of pages a chapter has. Pages should be returned
|
* Get the list of pages a chapter has. Pages should be returned
|
||||||
* in the expected order; the index is ignored.
|
* in the expected order; the index is ignored.
|
||||||
*
|
*
|
||||||
* @since extensions-lib 1.4
|
* @since extensions-lib 1.5
|
||||||
* @param chapter the chapter.
|
* @param chapter the chapter.
|
||||||
* @return the pages for the chapter.
|
* @return the pages for the chapter.
|
||||||
*/
|
*/
|
||||||
|
@ -61,41 +61,24 @@ interface MangaSource {
|
||||||
return fetchPageList(chapter).awaitSingle()
|
return fetchPageList(chapter).awaitSingle()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an observable with the updated details for a manga.
|
|
||||||
*
|
|
||||||
* @param manga the manga to update.
|
|
||||||
*/
|
|
||||||
@Deprecated(
|
@Deprecated(
|
||||||
"Use the non-RxJava API instead",
|
"Use the non-RxJava API instead",
|
||||||
ReplaceWith("getMangaDetails"),
|
ReplaceWith("getMangaDetails"),
|
||||||
)
|
)
|
||||||
fun fetchMangaDetails(manga: SManga): Observable<SManga> = throw IllegalStateException(
|
fun fetchMangaDetails(manga: SManga): Observable<SManga> =
|
||||||
"Not used",
|
throw IllegalStateException("Not used")
|
||||||
)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an observable with all the available chapters for a manga.
|
|
||||||
*
|
|
||||||
* @param manga the manga to update.
|
|
||||||
*/
|
|
||||||
@Deprecated(
|
@Deprecated(
|
||||||
"Use the non-RxJava API instead",
|
"Use the non-RxJava API instead",
|
||||||
ReplaceWith("getChapterList"),
|
ReplaceWith("getChapterList"),
|
||||||
)
|
)
|
||||||
fun fetchChapterList(manga: SManga): Observable<List<SChapter>> = throw IllegalStateException(
|
fun fetchChapterList(manga: SManga): Observable<List<SChapter>> =
|
||||||
"Not used",
|
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(
|
@Deprecated(
|
||||||
"Use the non-RxJava API instead",
|
"Use the non-RxJava API instead",
|
||||||
ReplaceWith("getPageList"),
|
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.Request
|
||||||
import okhttp3.Response
|
import okhttp3.Response
|
||||||
import rx.Observable
|
import rx.Observable
|
||||||
|
import tachiyomi.core.util.lang.awaitSingle
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
import java.net.URISyntaxException
|
import java.net.URISyntaxException
|
||||||
|
@ -24,6 +25,7 @@ import java.security.MessageDigest
|
||||||
/**
|
/**
|
||||||
* A simple implementation for sources from a website.
|
* A simple implementation for sources from a website.
|
||||||
*/
|
*/
|
||||||
|
@Suppress("unused")
|
||||||
abstract class HttpSource : CatalogueSource {
|
abstract class HttpSource : CatalogueSource {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -81,6 +83,7 @@ abstract class HttpSource : CatalogueSource {
|
||||||
* @param versionId [Int] the version ID of the source
|
* @param versionId [Int] the version ID of the source
|
||||||
* @return a unique ID for the source
|
* @return a unique ID for the source
|
||||||
*/
|
*/
|
||||||
|
@Suppress("MemberVisibilityCanBePrivate")
|
||||||
protected fun generateId(name: String, lang: String, versionId: Int): Long {
|
protected fun generateId(name: String, lang: String, versionId: Int): Long {
|
||||||
val key = "${name.lowercase()}/$lang/$versionId"
|
val key = "${name.lowercase()}/$lang/$versionId"
|
||||||
val bytes = MessageDigest.getInstance("MD5").digest(key.toByteArray())
|
val bytes = MessageDigest.getInstance("MD5").digest(key.toByteArray())
|
||||||
|
@ -194,11 +197,18 @@ abstract class HttpSource : CatalogueSource {
|
||||||
protected abstract fun latestUpdatesParse(response: Response): MangasPage
|
protected abstract fun latestUpdatesParse(response: Response): MangasPage
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an observable with the updated details for a manga. Normally it's not needed to
|
* Get the updated details for a manga.
|
||||||
* override this method.
|
* Normally it's not needed to override this method.
|
||||||
*
|
*
|
||||||
* @param manga the manga to be updated.
|
* @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> {
|
override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
|
||||||
return client.newCall(mangaDetailsRequest(manga))
|
return client.newCall(mangaDetailsRequest(manga))
|
||||||
.asObservableSuccess()
|
.asObservableSuccess()
|
||||||
|
@ -225,11 +235,23 @@ abstract class HttpSource : CatalogueSource {
|
||||||
protected abstract fun mangaDetailsParse(response: Response): SManga
|
protected abstract fun mangaDetailsParse(response: Response): SManga
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an observable with the updated chapter list for a manga. Normally it's not needed to
|
* Get all the available chapters for a manga.
|
||||||
* override this method. If a manga is licensed an empty chapter list observable is returned
|
* 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>> {
|
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
|
||||||
return if (manga.status != SManga.LICENSED) {
|
return if (manga.status != SManga.LICENSED) {
|
||||||
client.newCall(chapterListRequest(manga))
|
client.newCall(chapterListRequest(manga))
|
||||||
|
@ -260,10 +282,18 @@ abstract class HttpSource : CatalogueSource {
|
||||||
protected abstract fun chapterListParse(response: Response): List<SChapter>
|
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>> {
|
override fun fetchPageList(chapter: SChapter): Observable<List<Page>> {
|
||||||
return client.newCall(pageListRequest(chapter))
|
return client.newCall(pageListRequest(chapter))
|
||||||
.asObservableSuccess()
|
.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
|
* 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.
|
* 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.
|
* @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> {
|
open fun fetchImageUrl(page: Page): Observable<String> {
|
||||||
return client.newCall(imageUrlRequest(page))
|
return client.newCall(imageUrlRequest(page))
|
||||||
.asObservableSuccess()
|
.asObservableSuccess()
|
||||||
|
@ -318,24 +355,14 @@ abstract class HttpSource : CatalogueSource {
|
||||||
*/
|
*/
|
||||||
protected abstract fun imageUrlParse(response: Response): String
|
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.
|
* 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.
|
* @param page the page whose source image has to be downloaded.
|
||||||
*/
|
*/
|
||||||
open suspend fun getImage(page: Page): Response {
|
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)
|
return client.newCachelessCallWithProgress(imageRequest(page), page)
|
||||||
.awaitSuccess()
|
.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
|
override val supportsLatest = true
|
||||||
|
|
||||||
// Browse related
|
// 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 baseDirsFiles = fileSystem.getFilesInBaseDirectories()
|
||||||
val lastModifiedLimit by lazy { if (filters === LATEST_FILTERS) System.currentTimeMillis() - LATEST_THRESHOLD else 0L }
|
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))
|
return Observable.just(AnimesPage(animes.toList(), false))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -16,7 +16,6 @@ import kotlinx.serialization.json.decodeFromStream
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
import nl.adaptivity.xmlutil.AndroidXmlReader
|
import nl.adaptivity.xmlutil.AndroidXmlReader
|
||||||
import nl.adaptivity.xmlutil.serialization.XML
|
import nl.adaptivity.xmlutil.serialization.XML
|
||||||
import rx.Observable
|
|
||||||
import tachiyomi.core.metadata.comicinfo.COMIC_INFO_FILE
|
import tachiyomi.core.metadata.comicinfo.COMIC_INFO_FILE
|
||||||
import tachiyomi.core.metadata.comicinfo.ComicInfo
|
import tachiyomi.core.metadata.comicinfo.ComicInfo
|
||||||
import tachiyomi.core.metadata.comicinfo.copyFromComicInfo
|
import tachiyomi.core.metadata.comicinfo.copyFromComicInfo
|
||||||
|
@ -70,11 +69,11 @@ actual class LocalMangaSource(
|
||||||
override val supportsLatest: Boolean = true
|
override val supportsLatest: Boolean = true
|
||||||
|
|
||||||
// Browse related
|
// 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 baseDirsFiles = fileSystem.getFilesInBaseDirectories()
|
||||||
val lastModifiedLimit by lazy { if (filters === LATEST_FILTERS) System.currentTimeMillis() - LATEST_THRESHOLD else 0L }
|
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
|
// Manga details related
|
||||||
|
|
Loading…
Reference in a new issue