Compare commits

..

No commits in common. "main" and "v1.13.6-forgejo.40" have entirely different histories.

6 changed files with 63 additions and 49 deletions

View file

@ -413,6 +413,33 @@ jobs:
sha256sum "${signed_apk}" sha256sum "${signed_apk}"
done done
- name: Create Obtainium-friendly aliases
shell: bash
run: |
set -euo pipefail
alias_asset() {
src="$1"
dst="$2"
if [ -e "$src" ]; then
cp -f "$src" "$dst"
fi
}
# Only publish signed APK aliases.
alias_asset "$(ls -1t Komikku-arm64-v8a-*.apk 2>/dev/null | head -n1)" "Komikku-arm64-v8a.apk"
for candidate in Komikku-*.apk; do
case "$candidate" in
*arm64-v8a*|*armeabi-v7a*|*x86_64*|*x86*|*-unsigned.apk)
;;
*)
alias_asset "$candidate" "Komikku-universal.apk"
break
;;
esac
done
- name: Create or update release - name: Create or update release
id: release id: release
shell: bash shell: bash
@ -534,7 +561,7 @@ jobs:
"<!-->" \ "<!-->" \
"> [!TIP]" \ "> [!TIP]" \
">" \ ">" \
"> ### If you are unsure which version to download then go with \`Komikku-${VERSION_TAG}.apk\`" "> ### If you are unsure which version to download then go with \`Komikku-universal.apk\`"
} > "${release_body_file}" } > "${release_body_file}"
# Build JSON — body from file only (jq --arg body "$huge" hits ARG_MAX). # Build JSON — body from file only (jq --arg body "$huge" hits ARG_MAX).

View file

@ -468,6 +468,11 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
newUpdates.add(manga to newChapters.toTypedArray()) newUpdates.add(manga to newChapters.toTypedArray())
} }
val libraryUpdateChapters = getChaptersForLibraryUpdateDownload.await(manga)
if (libraryUpdateChapters.isNotEmpty()) {
downloadChapters(manga, libraryUpdateChapters)
hasDownloads.store(true)
}
clearErrorFromDB(mangaId = manga.id) clearErrorFromDB(mangaId = manga.id)
} catch (e: Throwable) { } catch (e: Throwable) {
val errorMessage = when (e) { val errorMessage = when (e) {
@ -484,17 +489,6 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
writeErrorToDB(manga to errorMessage) writeErrorToDB(manga to errorMessage)
failedUpdates.add(manga to errorMessage) failedUpdates.add(manga to errorMessage)
} }
try {
val libraryUpdateChapters = getChaptersForLibraryUpdateDownload.await(manga)
if (libraryUpdateChapters.isNotEmpty()) {
downloadChapters(manga, libraryUpdateChapters)
hasDownloads.store(true)
}
} catch (e: Throwable) {
if (e is CancellationException) throw e
xLogE("Failed to queue library update downloads for ${manga.title}", e)
}
} }
} }
} }

View file

@ -98,10 +98,10 @@ import tachiyomi.domain.chapter.interactor.GetBookmarkedChaptersByMangaId
import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
import tachiyomi.domain.chapter.interactor.GetMergedChaptersByMangaId import tachiyomi.domain.chapter.interactor.GetMergedChaptersByMangaId
import tachiyomi.domain.chapter.model.Chapter import tachiyomi.domain.chapter.model.Chapter
import tachiyomi.domain.history.interactor.GetNextChapters
import tachiyomi.domain.chapter.service.getChapterSort import tachiyomi.domain.chapter.service.getChapterSort
import tachiyomi.domain.history.interactor.GetCrossMangaPageRateHistorySamples import tachiyomi.domain.history.interactor.GetCrossMangaPageRateHistorySamples
import tachiyomi.domain.history.interactor.GetHistoryRevision import tachiyomi.domain.history.interactor.GetHistoryRevision
import tachiyomi.domain.history.interactor.GetNextChapters
import tachiyomi.domain.history.interactor.GetReadDurationEntriesByMangaIds import tachiyomi.domain.history.interactor.GetReadDurationEntriesByMangaIds
import tachiyomi.domain.history.interactor.GetTotalReadDuration import tachiyomi.domain.history.interactor.GetTotalReadDuration
import tachiyomi.domain.library.model.LibraryDisplayMode import tachiyomi.domain.library.model.LibraryDisplayMode
@ -110,6 +110,9 @@ import tachiyomi.domain.library.model.LibraryManga
import tachiyomi.domain.library.model.LibrarySort import tachiyomi.domain.library.model.LibrarySort
import tachiyomi.domain.library.model.sort import tachiyomi.domain.library.model.sort
import tachiyomi.domain.library.service.LibraryPreferences import tachiyomi.domain.library.service.LibraryPreferences
import tachiyomi.domain.reading.ReadingEtaLimits
import tachiyomi.domain.reading.SeriesReadingEtaCalculator
import tachiyomi.domain.reading.service.ReadingEtaPreferences
import tachiyomi.domain.manga.interactor.GetIdsOfFavoriteMangaWithMetadata import tachiyomi.domain.manga.interactor.GetIdsOfFavoriteMangaWithMetadata
import tachiyomi.domain.manga.interactor.GetLibraryManga import tachiyomi.domain.manga.interactor.GetLibraryManga
import tachiyomi.domain.manga.interactor.GetMergedMangaById import tachiyomi.domain.manga.interactor.GetMergedMangaById
@ -120,9 +123,6 @@ import tachiyomi.domain.manga.model.CustomMangaInfo
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.manga.model.MangaUpdate import tachiyomi.domain.manga.model.MangaUpdate
import tachiyomi.domain.manga.model.applyFilter import tachiyomi.domain.manga.model.applyFilter
import tachiyomi.domain.reading.ReadingEtaLimits
import tachiyomi.domain.reading.SeriesReadingEtaCalculator
import tachiyomi.domain.reading.service.ReadingEtaPreferences
import tachiyomi.domain.source.model.StubSource import tachiyomi.domain.source.model.StubSource
import tachiyomi.domain.source.service.SourceManager import tachiyomi.domain.source.service.SourceManager
import tachiyomi.domain.track.interactor.GetTracks import tachiyomi.domain.track.interactor.GetTracks
@ -1028,7 +1028,7 @@ class LibraryScreenModel(
if (manga.source == MERGED_SOURCE_ID) { if (manga.source == MERGED_SOURCE_ID) {
val mergedMangas = getMergedMangaById.await(manga.id) val mergedMangas = getMergedMangaById.await(manga.id)
.associateBy { it.id } .associateBy { it.id }
getNextChapters.awaitFromLastRead(manga.id) getNextChapters.await(manga.id)
.let { if (amount != null) it.take(amount) else it } .let { if (amount != null) it.take(amount) else it }
.groupBy { it.mangaId } .groupBy { it.mangaId }
.forEach ab@{ (mangaId, chapters) -> .forEach ab@{ (mangaId, chapters) ->
@ -1051,7 +1051,7 @@ class LibraryScreenModel(
} }
// SY <-- // SY <--
val chapters = getNextChapters.awaitFromLastRead(manga.id) val chapters = getNextChapters.await(manga.id)
.fastFilterNot { chapter -> .fastFilterNot { chapter ->
downloadManager.getQueuedDownloadOrNull(chapter.id) != null || downloadManager.getQueuedDownloadOrNull(chapter.id) != null ||
downloadManager.isChapterDownloaded( downloadManager.isChapterDownloaded(

View file

@ -8,6 +8,8 @@ Forgejo Actions workflow: [`.forgejo/workflows/build_release.yml`](../.forgejo/w
- Builds release APKs (`assembleRelease -Penable-updater -Pdisable-code-shrink`, no telemetry) - Builds release APKs (`assembleRelease -Penable-updater -Pdisable-code-shrink`, no telemetry)
- Signs APKs with your release keystore - Signs APKs with your release keystore
- Creates or updates a Forgejo release and uploads APK assets - Creates or updates a Forgejo release and uploads APK assets
- Publishes stable Obtainium-friendly asset names alongside versioned files
The workflow uses shell + Forgejo API calls only (no GitHub-only release actions), so it runs on self-hosted Forgejo runners. The workflow uses shell + Forgejo API calls only (no GitHub-only release actions), so it runs on self-hosted Forgejo runners.
## Cut a release ## Cut a release
@ -88,7 +90,7 @@ export KOMIKKU_GRADLE_CACHE="$HOME/.cache/komikku-ci-gradle"
## Release assets ## Release assets
One APK per architecture per tag: Versioned (one set per tag):
- `Komikku-v1.13.6.apk` — universal - `Komikku-v1.13.6.apk` — universal
- `Komikku-arm64-v8a-v1.13.6.apk` - `Komikku-arm64-v8a-v1.13.6.apk`
@ -97,6 +99,11 @@ One APK per architecture per tag:
- `Komikku-x86_64-v1.13.6.apk` - `Komikku-x86_64-v1.13.6.apk`
- `Komikku-mapping-v1.13.6.zip` — ProGuard mapping (when present) - `Komikku-mapping-v1.13.6.zip` — ProGuard mapping (when present)
Stable aliases (same filename every release; best for Obtainium):
- `Komikku-universal.apk`
- `Komikku-arm64-v8a.apk`
## Obtainium ## Obtainium
Obtainium can install directly from Forgejo release assets. Obtainium can install directly from Forgejo release assets.
@ -105,14 +112,14 @@ Obtainium can install directly from Forgejo release assets.
2. **Add app****Override source**: **Forgejo** (required for self-hosted instances). 2. **Add app****Override source**: **Forgejo** (required for self-hosted instances).
3. Repository URL, e.g. `https://forgejo.example/youruser/komikku` (no `/releases` suffix). 3. Repository URL, e.g. `https://forgejo.example/youruser/komikku` (no `/releases` suffix).
4. Enable **Allow insecure HTTP** or disable SSL verification if Forgejo uses a self-signed certificate. 4. Enable **Allow insecure HTTP** or disable SSL verification if Forgejo uses a self-signed certificate.
5. **APK filter** (regex): `Komikku-arm64-v8a-v.*\.apk` (or `Komikku-v.*\.apk` for universal). 5. **APK filter** (regex): `Komikku-arm64-v8a\.apk` (or `Komikku-universal\.apk`).
6. **Trim version string (regex):** `^v?([0-9]+\.[0-9]+\.[0-9]+)` — needed for fork tags like `v1.13.6-forgejo.39`. 6. **Trim version string (regex):** `^v?([0-9]+\.[0-9]+\.[0-9]+)` — needed for fork tags like `v1.13.6-forgejo.39`.
7. Wait until the Forgejo release workflow has finished before adding or updating the app. 7. Wait until the Forgejo release workflow has finished before adding or updating the app.
Direct download URL pattern (for reference): Direct download URL pattern (for reference):
```text ```text
https://forgejo.example/<owner>/<repo>/releases/download/<tag>/Komikku-arm64-v8a-v1.13.6.apk https://forgejo.example/<owner>/<repo>/releases/download/<tag>/Komikku-arm64-v8a.apk
``` ```
## Notes ## Notes

View file

@ -17,7 +17,7 @@ class GetChaptersForLibraryUpdateDownload(
val mode = downloadPreferences.libraryUpdateDownloadMode().get() val mode = downloadPreferences.libraryUpdateDownloadMode().get()
if (mode == LibraryUpdateDownloadMode.DISABLED) return emptyList() if (mode == LibraryUpdateDownloadMode.DISABLED) return emptyList()
val chapters = getNextChapters.awaitFromLastRead(manga.id) val chapters = getNextChapters.await(manga.id)
if (chapters.isEmpty()) return emptyList() if (chapters.isEmpty()) return emptyList()
return when (mode) { return when (mode) {

View file

@ -8,6 +8,7 @@ import tachiyomi.domain.chapter.model.Chapter
import tachiyomi.domain.chapter.service.getChapterSort import tachiyomi.domain.chapter.service.getChapterSort
import tachiyomi.domain.history.repository.HistoryRepository import tachiyomi.domain.history.repository.HistoryRepository
import tachiyomi.domain.manga.interactor.GetManga import tachiyomi.domain.manga.interactor.GetManga
import kotlin.math.max
class GetNextChapters( class GetNextChapters(
private val getChaptersByMangaId: GetChaptersByMangaId, private val getChaptersByMangaId: GetChaptersByMangaId,
@ -64,37 +65,22 @@ class GetNextChapters(
fromChapterId: Long, fromChapterId: Long,
onlyUnread: Boolean = true, onlyUnread: Boolean = true,
): List<Chapter> { ): List<Chapter> {
val allChapters = await(mangaId, onlyUnread = false) val chapters = await(mangaId, onlyUnread)
val currChapterIndex = allChapters.indexOfFirst { it.id == fromChapterId } val currChapterIndex = chapters.indexOfFirst { it.id == fromChapterId }
val nextChapters = chapters.subList(max(0, currChapterIndex), chapters.size)
if (currChapterIndex == -1) { if (onlyUnread) {
return if (onlyUnread) await(mangaId, onlyUnread = true) else allChapters return nextChapters
} }
val fromChapter = allChapters[currChapterIndex] // The "next chapter" is either:
val startIndex = when { // - The current chapter if it isn't completely read
fromChapter.read -> currChapterIndex + 1 // - The chapters after the current chapter if the current one is completely read
else -> currChapterIndex val fromChapter = chapters.getOrNull(currChapterIndex)
}.coerceAtMost(allChapters.size) return if (fromChapter != null && !fromChapter.read) {
nextChapters
val fromPosition = allChapters.subList(startIndex, allChapters.size)
return if (onlyUnread) {
fromPosition.filterNot { it.read }
} else { } else {
fromPosition nextChapters.drop(1)
}
}
suspend fun awaitFromLastRead(mangaId: Long, onlyUnread: Boolean = true): List<Chapter> {
val lastReadChapterId = historyRepository.getHistoryByMangaId(mangaId)
.maxByOrNull { it.readAt?.time ?: 0L }
?.chapterId
return if (lastReadChapterId != null) {
await(mangaId, lastReadChapterId, onlyUnread)
} else {
await(mangaId, onlyUnread)
} }
} }
} }