Compare commits
No commits in common. "main" and "v1.13.6-forgejo.40" have entirely different histories.
main
...
v1.13.6-fo
6 changed files with 63 additions and 49 deletions
|
|
@ -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).
|
||||||
|
|
|
||||||
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue