Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
22f26ca
fix(downloads): stop the queue dropping chapters and deleting other d…
PoonHarpoon Oct 31, 2026
96aacf3
fix(backup): restore the selected sources instead of adding to them
PoonHarpoon Oct 31, 2026
4835d2a
fix(settings): restore float preferences from backups
PoonHarpoon Nov 1, 2026
9a295db
fix(local): scan storage outside the index rebuild transaction
PoonHarpoon Nov 1, 2026
acb4e67
fix(suggestions): keep the most relevant suggestions, not the least
PoonHarpoon Nov 1, 2026
21f5cc9
fix(details): hide NSFW related manga when NSFW content is disabled
PoonHarpoon Nov 1, 2026
f2454dd
feat(suggestions): rank by tag rarity and fall back to tags for relat…
PoonHarpoon Oct 31, 2026
bcb45d0
fix(webtoon): fix broken page sizing, page joins and pull-to-switch-c…
PoonHarpoon Oct 31, 2026
f337f6b
fix(tracker): correct new-chapter counting and stop resetting tracks
PoonHarpoon Oct 31, 2026
1c70098
feat(feed): swipe to remove a feed item, with undo
PoonHarpoon Oct 31, 2026
c958745
feat(tracker): auto-download new chapters of recently read manga
PoonHarpoon Oct 31, 2026
5b1ed3c
perf(tracker): per-source limits, stop at blocked sources, save fixes
PoonHarpoon Oct 31, 2026
9e641d0
feat(tracker): re-check captcha-blocked manga after a solve
PoonHarpoon Oct 31, 2026
1677c4f
fix(cloudflare): keep WebView interception alive and remember the cle…
PoonHarpoon Oct 31, 2026
62bc9fe
feat(cloudflare): solve captchas automatically on explicit actions (e…
PoonHarpoon Oct 31, 2026
6376811
fix(history): drop the leading "-" from reading progress badges
PoonHarpoon Nov 1, 2026
7141068
feat(shortcuts): hide NSFW manga from launcher shortcuts
PoonHarpoon Oct 31, 2026
5ddd84f
feat(library): pin favourites, quick filters and search within Histor…
PoonHarpoon Oct 31, 2026
ab1f195
feat(details): offer to search alternatives when a manga is unavailable
PoonHarpoon Oct 31, 2026
782a154
feat(explore): source presets
PoonHarpoon Oct 31, 2026
61c799e
feat(alternatives): replace source, search options and alternative co…
PoonHarpoon Oct 31, 2026
5be5b2a
feat(image): add JPEG XL support
PoonHarpoon Oct 31, 2026
ad20298
fix(history): keep the reading position across chapter id changes and…
PoonHarpoon Oct 31, 2026
736fd59
feat(reader): double-page rework
PoonHarpoon Oct 31, 2026
60c2ac6
feat(reader): E-Ink flash on page change
PoonHarpoon Oct 31, 2026
929b18e
Don't resume a manual new-chapters check hours later
PoonHarpoon Nov 4, 2026
827b216
Close the captcha screen without waiting for the solve to be recorded
PoonHarpoon Nov 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,7 @@ dependencies {
implementation libs.coil.gif
implementation libs.coil.svg
implementation libs.avif.decoder
implementation libs.jxl.coder
implementation libs.ssiv
implementation libs.disk.lru.cache
implementation libs.markwon
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,19 @@ class ProgressUpdateRaceTest {
assertEquals(result, saved.percent)
}

@Test
fun recalculationKeepsChaptersCountForSmartResume() = runBlocking {
val fixture = fixture()
val dao = database.getHistoryDao()
// read while the source had 29 chapters; chapter 30 has been released since
dao.update(dao.find(fixture.manga.id)!!.copy(chaptersCount = 29))
val job = async(Dispatchers.IO) { recalculate(fixture.manga) }
withTimeout(30_000) { fixture.started.await() }
fixture.release()
withTimeout(30_000) { job.await() }
assertEquals(29, dao.find(fixture.manga.id)!!.chaptersCount)
}

@Test
fun historyRemovedDuringRecalculationStaysRemoved() = runBlocking {
val fixture = fixture()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,29 @@ class AppBackupAgentTest {
assertTrue(SampleData.tag in allTags)
}

@Test
fun restoreReplacesEnabledSources() = runTest {
val dao = database.getSourcesDao()
dao.setEnabled("MANGADEX", true)
dao.setEnabled("MANGAPARK", true)

val agent = AppBackupAgent()
val backup = agent.createBackupFile(
context = InstrumentationRegistry.getInstrumentation().targetContext,
repository = backupRepository,
)

// as after a fresh install, where other sources are enabled by default
dao.setEnabled("MANGADEX", false)
dao.setEnabled("NHENTAI", true)

backup.inputStream().use {
agent.restoreBackupFile(it.fd, backup.length(), backupRepository)
}

assertEquals(setOf("MANGADEX", "MANGAPARK"), dao.findAllEnabledNames().toSet())
}

@Test
fun restoreOldBackup() {
val agent = AppBackupAgent()
Expand Down
17 changes: 17 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,13 @@
android:autoRemoveFromRecents="true"
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
android:windowSoftInputMode="adjustResize" />
<activity
android:name="io.github.landwarderer.futon.browser.cloudflare.CloudFlareHiddenActivity"
android:autoRemoveFromRecents="true"
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
android:excludeFromRecents="true"
android:theme="@style/Theme.Futon.CloudFlareHidden"
android:windowSoftInputMode="adjustNothing" />
<activity
android:name="io.github.landwarderer.futon.settings.sources.auth.SourceAuthActivity"
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
Expand Down Expand Up @@ -278,6 +285,9 @@
<activity
android:name="io.github.landwarderer.futon.settings.override.OverrideConfigActivity"
android:label="@string/edit" />
<activity
android:name="io.github.landwarderer.futon.alternatives.ui.covers.AlternativeCoversActivity"
android:label="@string/alternative_covers" />
<activity
android:name="io.github.landwarderer.futon.sync.ui.SyncAuthActivity"
android:exported="true"
Expand Down Expand Up @@ -342,6 +352,13 @@
<activity
android:name="io.github.landwarderer.futon.alternatives.ui.AlternativesActivity"
android:label="@string/alternatives" />
<activity
android:name="io.github.landwarderer.futon.alternatives.ui.SourceReplacementActivity"
android:label="@string/replace_source" />
<activity android:name="io.github.landwarderer.futon.explore.ui.preset.SourcePresetEditActivity" />
<activity
android:name="io.github.landwarderer.futon.explore.ui.preset.SourcePresetListActivity"
android:label="@string/source_presets" />
<activity
android:name="io.github.landwarderer.futon.tracker.ui.debug.TrackerDebugActivity"
android:label="@string/tracker_debug_info" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package io.github.landwarderer.futon.alternatives.domain

import org.koitharu.kotatsu.parsers.model.Manga

data class AlternativesSearchOptions(
val query: String,
val sourceScope: AlternativeSourceScope = AlternativeSourceScope.Enabled,
val sameLanguageOnly: Boolean = false,
val sameContentTypeOnly: Boolean = false,
val hideNoChapters: Boolean = false,
val sortOrder: AlternativeSortOrder = AlternativeSortOrder.BEST_MATCH,
) {

fun isDefaultFor(title: String, defaultScope: AlternativeSourceScope): Boolean =
this == AlternativesSearchOptions(query = title, sourceScope = defaultScope)
}

/** Which sources an alternatives search or a source replacement runs across. */
sealed interface AlternativeSourceScope {

data object Enabled : AlternativeSourceScope

/** The sources the user has not enabled - where an alternative is most often hiding. */
data object Disabled : AlternativeSourceScope

/** Any saved preset, not only the active one. */
data class Preset(val id: Long) : AlternativeSourceScope
}

enum class AlternativeSortOrder {
BEST_MATCH,
MOST_CHAPTERS,
CLOSEST_CHAPTER_COUNT,
SOURCE_PRIORITY,
}

sealed interface AlternativeSearchEvent {

data class Result(val manga: Manga) : AlternativeSearchEvent

data class Progress(
val completedSources: Int,
val totalSources: Int,
) : AlternativeSearchEvent
}

data class AlternativeSearchStatus(
val completedSources: Int = 0,
val totalSources: Int = 0,
val resultsCount: Int = 0,
val isRunning: Boolean = false,
)

data class AlternativeSourceScopeOptions(
val defaultScope: AlternativeSourceScope,
/** Presets that have at least one candidate source, in the user's preset order. */
val presets: List<PresetChoice> = emptyList(),
/** False when every candidate is already enabled, or every source is enabled. */
val hasDisabledSources: Boolean = false,
) {

data class PresetChoice(val id: Long, val title: String)

fun isAvailable(scope: AlternativeSourceScope): Boolean = when (scope) {
AlternativeSourceScope.Enabled -> true
AlternativeSourceScope.Disabled -> hasDisabledSources
is AlternativeSourceScope.Preset -> presets.any { it.id == scope.id }
}

fun presetTitle(id: Long): String? = presets.firstOrNull { it.id == id }?.title
}
Loading
Loading