Compare commits

...

47 Commits

Author SHA1 Message Date
Radon Rosborough
58b25d697f Improve handling of downloads for chapters with same metadata and optionally for OSes that don't support Unicode in filename (#2305)
Co-authored-by: jkim <jhskim@hotmail.com>
Co-authored-by: fatotak <111342761+fatotak@users.noreply.github.com>
Co-authored-by: MajorTanya <39014446+MajorTanya@users.noreply.github.com>
Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
2025-10-08 05:07:09 +06:00
Mend Renovate
1a31c7c7ee Update okhttp monorepo to v5.2.0 (#2564) 2025-10-07 18:51:34 +00:00
Mend Renovate
ad6b651b37 Update softprops/action-gh-release action to v2.4.0 (#2562) 2025-10-08 00:49:25 +06:00
NGB-Was-Taken
96e5131358 Fix disabling incognito mode from notification (#2512)
Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
2025-10-05 16:03:45 +00:00
Mend Renovate
1d5bc8d2c2 Update dependency com.google.firebase:firebase-bom to v34.3.0 (#2508) 2025-10-05 14:47:15 +06:00
Mend Renovate
6cee911239 Update gradle/actions action to v5 (#2554) 2025-10-05 14:47:01 +06:00
Mend Renovate
96347e3f76 Update GitHub Actions (#2552) 2025-10-05 07:58:31 +00:00
Mend Renovate
9a45d248b1 Update dependency org.junit.jupiter:junit-jupiter to v6 (#2553) 2025-10-05 07:57:34 +00:00
Mend Renovate
04168ecec8 Update moko to v0.25.1 (#2550) 2025-10-05 07:41:46 +00:00
Mend Renovate
607f0ea9cd Update dependency io.mockk:mockk to v1.14.6 (#2549) 2025-10-05 07:40:48 +00:00
Secozzi
27a4f6f45c Update markdown to 0.37.0 (#2516) 2025-10-05 13:36:13 +06:00
Mend Renovate
5236d003d2 Update kotlin monorepo to v2.2.20 (#2498) 2025-10-05 13:29:27 +06:00
Mend Renovate
d61a41e819 Update dependency androidx.work:work-runtime to v2.10.5 (#2523) 2025-10-05 13:28:49 +06:00
Mend Renovate
5637860dd2 Update sqlite to v2.6.1 (#2525) 2025-10-05 13:28:37 +06:00
Mend Renovate
d4d18d0898 Update dependency com.diffplug.spotless:spotless-plugin-gradle to v8 (#2526) 2025-10-05 13:28:09 +06:00
Guzmazow
065147472e Improve spoofing of X-Requested-With header to support newer WebView versions (#2491) 2025-09-19 23:35:23 +06:00
Constantin Piber
6f635782c2 Delegate Suwayomi tracker authentication to extension (#2476)
Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
2025-09-18 21:19:09 +00:00
Mend Renovate
86d85f74c0 Update lifecycle.version to v2.9.4 (#2503)
Update dependency androidx.lifecycle:lifecycle-process to v2.9.4
2025-09-18 15:48:14 +06:00
Mend Renovate
29e6a2c4a6 Update sqlite to v2.6.0 (#2504) 2025-09-18 15:47:48 +06:00
Mend Renovate
60c66bbd3a Update dependency androidx.core:core-ktx to v1.17.0 (#2402) 2025-09-17 21:29:03 +06:00
Mend Renovate
060e5b2e2e Update dependency androidx.activity:activity-compose to v1.11.0 (#2499) 2025-09-17 12:21:50 +00:00
AntsyLich
4ac9fcd4d3 Replace compose-stable-marker with compose-runtime-annotation 2025-09-17 18:08:25 +06:00
AntsyLich
d3b7f7e55f Bump compile and target sdk 2025-09-17 18:08:25 +06:00
Mend Renovate
0d926626a1 Update dependency com.google.firebase:firebase-bom to v34.2.0 (#2376) 2025-09-17 17:51:11 +06:00
Mend Renovate
6495a2ea43 Update dependency androidx.benchmark:benchmark-macro-junit4 to v1.4.1 (#2496) 2025-09-17 11:48:09 +00:00
Mend Renovate
94f711ba2a Update dependency androidx.work:work-runtime to v2.10.4 (#2497) 2025-09-17 11:47:49 +00:00
Mend Renovate
9f5c4e03b2 Update dependency androidx.compose:compose-bom to v2025.09.00 (#2401) 2025-09-17 17:39:08 +06:00
Mend Renovate
49562e1915 Update lifecycle.version to v2.9.3 (#2447) 2025-09-17 11:37:56 +00:00
Mend Renovate
57c82b30ba Update dependency org.jsoup:jsoup to v1.21.2 (#2438) 2025-09-17 17:30:26 +06:00
Mend Renovate
e573f72cfd Update dependency io.kotest:kotest-assertions-core to v6.0.3 (#2439) 2025-09-17 17:30:04 +06:00
Mend Renovate
95357a8625 Update GitHub Actions (#2443) 2025-09-17 17:29:14 +06:00
Mend Renovate
bd90307df9 Update dependency com.android.tools.build:gradle to v8.13.0 (#2449) 2025-09-17 17:26:02 +06:00
Secozzi
16b5317b90 Fix migration progress not updating and category flag mischeck (#2484)
- Fixed an issue where migration progress wasn't updated after a manual source search
- Fixed incorrect logic where the category migration flag was ignored due to checking the chapter flag instead
2025-09-17 17:12:21 +06:00
Mend Renovate
83f4b48629 Update plugin firebase-crashlytics to v3.0.6 (#2374) 2025-08-21 07:50:25 +00:00
Mend Renovate
4665dc50f6 Update actions/setup-java action to v5 (#2429) 2025-08-21 13:41:06 +06:00
Mend Renovate
85f5e5019e Update dependency com.github.skydoves:compose-stable-marker to v1.0.7 (#2428) 2025-08-21 13:40:56 +06:00
AntsyLich
4bc3b9f3b6 Bump targetSdk to 35 2025-08-21 12:51:32 +06:00
Mend Renovate
2c0d3678d9 Update actions/dependency-review-action action to v4.7.2 (#2418) 2025-08-21 12:49:14 +06:00
Mend Renovate
feda410152 Update dependency com.android.tools.build:gradle to v8.12.1 (#2417) 2025-08-21 12:48:59 +06:00
Mend Renovate
200c2df5ba Update dependency sh.calvin.reorderable:reorderable to v3 (#2419) 2025-08-21 12:48:43 +06:00
Mend Renovate
be09cddde2 Update dependency io.kotest:kotest-assertions-core to v6 (#2416) 2025-08-21 12:47:04 +06:00
AntsyLich
498317de52 Switch to a fork of QuickJS Java 2025-08-21 12:45:15 +06:00
Mend Renovate
33b876edc6 Update kotlin monorepo to v2.2.10 (#2404) 2025-08-15 08:56:05 +06:00
Mend Renovate
e7251f2034 Update actions/checkout action to v5 (#2395) 2025-08-15 08:55:39 +06:00
Secozzi
3d3c36078a Don't hardcode app name in strings.xml (#2394) 2025-08-11 22:01:07 +06:00
Secozzi
c6a96b3970 Fix height of description not being calculated correctly if images are present (#2382) 2025-08-10 00:21:25 +06:00
AntsyLich
fb3dc1c984 Fix readme CI badge [skip ci] 2025-08-07 20:48:44 +06:00
39 changed files with 329 additions and 159 deletions

View File

@@ -26,20 +26,20 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Dependency Review
if: github.event_name == 'pull_request'
uses: actions/dependency-review-action@da24556b548a50705dd671f47852072ea4c105d9 # v4.7.1
uses: actions/dependency-review-action@56339e523c0409420f6c2c9a2f4292bbb3c07dd3 # v4.8.0
- name: Set up JDK
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
with:
java-version: 17
distribution: temurin
- name: Set up Gradle
uses: gradle/actions/setup-gradle@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2
uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
- name: Check code format
run: ./gradlew spotlessCheck

View File

@@ -29,16 +29,16 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set up JDK
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
with:
java-version: 17
distribution: temurin
- name: Set up Gradle
uses: gradle/actions/setup-gradle@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2
uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
- name: Build
run: ./gradlew assembleRelease -Pinclude-telemetry -Penable-updater
@@ -83,16 +83,16 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Set up JDK
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
with:
java-version: 17
distribution: temurin
- name: Set up Gradle
uses: gradle/actions/setup-gradle@017a9effdb900e5b5b2fddfb590a105619dca3c3 # v4.4.2
uses: gradle/actions/setup-gradle@4d9f0ba0025fe599b4ebab900eb7f3a1d93ef4c2 # v5.0.0
with:
cache-disabled: true
@@ -143,7 +143,7 @@ jobs:
mihon-foss
- name: Create GitHub Release
uses: softprops/action-gh-release@72f2c25fcb47643c292f7107632f7a47c1df5cd8 # v2.3.2
uses: softprops/action-gh-release@aec2ec56f94eb8180ceec724245f64ef008b89f5 # v2.4.0
with:
tag_name: ${{ needs.get_tag.outputs.tag }}
name: Mihon ${{ needs.get_tag.outputs.tag }}

View File

@@ -11,6 +11,21 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co
- `Other` - for technical stuff.
## [Unreleased]
### Added
- Advanced setting to limit download filenames to ASCII characters. This is provided only as a workaround for OSes that do not properly handle standard Unicode filenames. This setting is generally not recommended and should only be used as a last resort ([@raxod502](https://github.com/radian-software)) ([#2305](https://github.com/mihonapp/mihon/pull/2305))
### Changed
- Delegate Suwayomi tracker authentication to extension ([@cpiber](https://github.com/cpiber)) ([#2476](https://github.com/mihonapp/mihon/pull/2476))
### Improved
- Spoofing of `X-Requested-With` header to support newer WebView versions ([@Guzmazow](https://github.com/Guzmazow)) ([#2491](https://github.com/mihonapp/mihon/pull/2491))
- Download support for chapters with the same metadata. Now a hash based on chapter's url is appended to download filename to tell them apart, letting you download both. Existing downloaded chapters will continue to work normally ([@raxod502](https://github.com/radian-software)) ([#2305](https://github.com/mihonapp/mihon/pull/2305))
### Fixed
- Fix height of description not being calculated correctly if images are present ([@Secozzi](https://github.com/Secozzi)) ([#2382](https://github.com/mihonapp/mihon/pull/2382))
- Fix migration progress not updating after manual search ([@Secozzi](https://github.com/Secozzi)) ([#2484](https://github.com/mihonapp/mihon/pull/2484))
- Fix category migration flag being ignored due to incorrect check against chapter flag ([@Secozzi](https://github.com/Secozzi)) ([#2484](https://github.com/mihonapp/mihon/pull/2484))
- Fix disabling incognito mode from notification ([@NGB-Was-Taken](https://github.com/NGB-Was-Taken)) ([#2512](https://github.com/mihonapp/mihon/pull/2512))
## [v0.19.1] - 2025-08-07
### Changed
@@ -19,7 +34,7 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co
### Removed
- Predictive back support ([@AntsyLich](https://github.com/AntsyLich)) ([#2362](https://github.com/mihonapp/mihon/pull/2362))
### Fixes
### Fixed
- Fix scrollbar sometimes not showing during scroll or not reaching the bottom with few items ([@anirudhsnayak](https://github.com/anirudhsnayak)) ([#2304](https://github.com/mihonapp/mihon/pull/2304))
- Fix local source EPUB files not loading ([@AntsyLich](https://github.com/AntsyLich)) ([#2369](https://github.com/mihonapp/mihon/pull/2369))
- Fix title text color in light mode on mass migration list ([@AntsyLich](https://github.com/AntsyLich)) ([#2370](https://github.com/mihonapp/mihon/pull/2370))
@@ -65,7 +80,7 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co
- Make local source default chapter sorting match file explorer behavior ([@AntsyLich](https://github.com/AntsyLich)) ([#2224](https://github.com/mihonapp/mihon/pull/224))
- Include Manga `initialized` status in backup ([@AwkwardPeak7](https://github.com/AwkwardPeak7)) ([#2285](https://github.com/mihonapp/mihon/pull/2285))
### Fixes
### Fixed
- Fix Bangumi search results including novels ([@MajorTanya](https://github.com/MajorTanya)) ([#1885](https://github.com/mihonapp/mihon/pull/1885))
- Fix next chapter button occasionally jumping to the last page of the current chapter ([@perokhe](https://github.com/perokhe)) ([#1920](https://github.com/mihonapp/mihon/pull/1920))
- Fix page number not appearing when opening chapter ([@perokhe](https://github.com/perokhe)) ([#1936](https://github.com/mihonapp/mihon/pull/1936))

View File

@@ -12,7 +12,7 @@ Discover and read manga, webtoons, comics, and more easier than ever on your
[![Discord server](https://img.shields.io/discord/1195734228319617024.svg?label=&labelColor=6A7EC2&color=7389D8&logo=discord&logoColor=FFFFFF)](https://discord.gg/mihon)
[![GitHub downloads](https://img.shields.io/github/downloads/mihonapp/mihon/total?label=downloads&labelColor=27303D&color=0D1117&logo=github&logoColor=FFFFFF&style=flat)](https://mihon.app/download)
[![CI](https://img.shields.io/github/actions/workflow/status/mihonapp/mihon/build_push.yml?labelColor=27303D)](https://github.com/mihonapp/mihon/actions/workflows/build_push.yml)
[![CI](https://img.shields.io/github/actions/workflow/status/mihonapp/mihon/build.yml?labelColor=27303D)](https://github.com/mihonapp/mihon/actions/workflows/build_push.yml)
[![License: Apache-2.0](https://img.shields.io/github/license/mihonapp/mihon?labelColor=27303D&color=0877d2)](/LICENSE)
[![Translation status](https://img.shields.io/weblate/progress/mihon?labelColor=27303D&color=946300)](https://hosted.weblate.org/engage/mihon/)

View File

@@ -114,6 +114,7 @@ class SyncChaptersWithSource(
downloadManager.isChapterDownloaded(
dbChapter.name,
dbChapter.scanlator,
dbChapter.url,
manga.title,
manga.source,
)
@@ -121,12 +122,14 @@ class SyncChaptersWithSource(
if (shouldRenameChapter) {
downloadManager.renameChapter(source, manga, dbChapter, chapter)
}
var toChangeChapter = dbChapter.copy(
name = chapter.name,
chapterNumber = chapter.chapterNumber,
scanlator = chapter.scanlator,
sourceOrder = chapter.sourceOrder,
)
if (chapter.dateUpload != 0L) {
toChangeChapter = toChangeChapter.copy(dateUpload = chapter.dateUpload)
}

View File

@@ -26,6 +26,7 @@ fun List<Chapter>.applyFilters(manga: Manga, downloadManager: DownloadManager):
val downloaded = downloadManager.isChapterDownloaded(
chapter.name,
chapter.scanlator,
chapter.url,
manga.title,
manga.source,
)

View File

@@ -53,6 +53,7 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
@@ -68,6 +69,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.layout.Layout
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.LinkAnnotation
import androidx.compose.ui.text.SpanStyle
@@ -618,6 +620,7 @@ private fun MangaSummary(
targetValue = if (expanded) 1f else 0f,
label = "summary",
)
var infoHeight by remember { mutableIntStateOf(0) }
Layout(
modifier = modifier.clipToBounds(),
contents = listOf(
@@ -630,25 +633,11 @@ private fun MangaSummary(
)
},
{
Column {
MangaNotesSection(
content = notes,
expanded = true,
onEditNotes = onEditNotesClicked,
)
MarkdownRender(
content = description,
modifier = Modifier.secondaryItemAlpha(),
annotator = descriptionAnnotator(
loadImages = loadImages,
linkStyle = getMarkdownLinkStyle().toSpanStyle(),
),
loadImages = loadImages,
)
}
},
{
Column {
Column(
modifier = Modifier.onSizeChanged { size ->
infoHeight = size.height
},
) {
MangaNotesSection(
content = notes,
expanded = expanded,
@@ -685,14 +674,11 @@ private fun MangaSummary(
}
},
),
) { (shrunk, expanded, actual, scrim), constraints ->
) { (shrunk, actual, scrim), constraints ->
val shrunkHeight = shrunk.single()
.measure(constraints)
.height
val expandedHeight = expanded.single()
.measure(constraints)
.height
val heightDelta = expandedHeight - shrunkHeight
val heightDelta = infoHeight - shrunkHeight
val scrimHeight = 24.dp.roundToPx()
val actualPlaceable = actual.single()

View File

@@ -102,13 +102,9 @@ private fun getMarkdownColors(): MarkdownColors {
val codeBackground = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.1f)
return DefaultMarkdownColors(
text = MaterialTheme.colorScheme.onSurface,
codeText = Color.Unspecified,
inlineCodeText = Color.Unspecified,
linkText = Color.Unspecified,
codeBackground = codeBackground,
inlineCodeBackground = codeBackground,
dividerColor = MaterialTheme.colorScheme.outlineVariant,
tableText = Color.Unspecified,
tableBackground = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.05f),
)
}
@@ -139,7 +135,6 @@ private fun getMarkdownTypography(): MarkdownTypography {
ordered = MaterialTheme.typography.bodyMedium,
bullet = MaterialTheme.typography.bodyMedium,
list = MaterialTheme.typography.bodyMedium,
link = link,
textLink = TextLinkStyles(style = link.toSpanStyle()),
table = MaterialTheme.typography.bodyMedium,
)

View File

@@ -323,6 +323,11 @@ object SettingsAdvancedScreen : SearchableSettings {
title = stringResource(MR.strings.pref_update_library_manga_titles),
subtitle = stringResource(MR.strings.pref_update_library_manga_titles_summary),
),
Preference.PreferenceItem.SwitchPreference(
preference = libraryPreferences.disallowNonAsciiFilenames(),
title = stringResource(MR.strings.pref_disallow_non_ascii_filenames),
subtitle = stringResource(MR.strings.pref_disallow_non_ascii_filenames_details),
),
),
)
}

View File

@@ -57,7 +57,7 @@ fun ExtensionRepoCreateDialog(
},
text = {
Column {
Text(text = stringResource(MR.strings.action_add_repo_message))
Text(text = stringResource(MR.strings.action_add_repo_message, stringResource(MR.strings.app_name)))
OutlinedTextField(
modifier = Modifier

View File

@@ -122,7 +122,7 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
val pendingIntent = PendingIntent.getBroadcast(
this@App,
0,
Intent(ACTION_DISABLE_INCOGNITO_MODE),
Intent(ACTION_DISABLE_INCOGNITO_MODE).setPackage(BuildConfig.APPLICATION_ID),
PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE,
)
setContentIntent(pendingIntent)
@@ -220,8 +220,8 @@ class App : Application(), DefaultLifecycleObserver, SingletonImageLoader.Factor
// Override the value passed as X-Requested-With in WebView requests
val stackTrace = Looper.getMainLooper().thread.stackTrace
val isChromiumCall = stackTrace.any { trace ->
trace.className.equals("org.chromium.base.BuildInfo", ignoreCase = true) &&
setOf("getAll", "getPackageName", "<init>").any { trace.methodName.equals(it, ignoreCase = true) }
trace.className.lowercase() in setOf("org.chromium.base.buildinfo", "org.chromium.base.apkinfo") &&
trace.methodName.lowercase() in setOf("getall", "getpackagename", "<init>")
}
if (isChromiumCall) return WebViewUtil.spoofedPackageName(applicationContext)

View File

@@ -128,6 +128,7 @@ class DownloadCache(
*
* @param chapterName the name of the chapter to query.
* @param chapterScanlator scanlator of the chapter to query
* @param chapterUrl the url of the chapter to query
* @param mangaTitle the title of the manga to query.
* @param sourceId the id of the source of the chapter.
* @param skipCache whether to skip the directory cache and check in the filesystem.
@@ -135,13 +136,14 @@ class DownloadCache(
fun isChapterDownloaded(
chapterName: String,
chapterScanlator: String?,
chapterUrl: String,
mangaTitle: String,
sourceId: Long,
skipCache: Boolean,
): Boolean {
if (skipCache) {
val source = sourceManager.getOrStub(sourceId)
return provider.findChapterDir(chapterName, chapterScanlator, mangaTitle, source) != null
return provider.findChapterDir(chapterName, chapterScanlator, chapterUrl, mangaTitle, source) != null
}
renewCache()
@@ -153,6 +155,7 @@ class DownloadCache(
return provider.getValidChapterDirNames(
chapterName,
chapterScanlator,
chapterUrl,
).any { it in mangaDir.chapterDirs }
}
}
@@ -233,7 +236,7 @@ class DownloadCache(
rootDownloadsDirMutex.withLock {
val sourceDir = rootDownloadsDir.sourceDirs[manga.source] ?: return
val mangaDir = sourceDir.mangaDirs[provider.getMangaDirName(manga.title)] ?: return
provider.getValidChapterDirNames(chapter.name, chapter.scanlator).forEach {
provider.getValidChapterDirNames(chapter.name, chapter.scanlator, chapter.url).forEach {
if (it in mangaDir.chapterDirs) {
mangaDir.chapterDirs -= it
}
@@ -254,7 +257,7 @@ class DownloadCache(
val sourceDir = rootDownloadsDir.sourceDirs[manga.source] ?: return
val mangaDir = sourceDir.mangaDirs[provider.getMangaDirName(manga.title)] ?: return
chapters.forEach { chapter ->
provider.getValidChapterDirNames(chapter.name, chapter.scanlator).forEach {
provider.getValidChapterDirNames(chapter.name, chapter.scanlator, chapter.url).forEach {
if (it in mangaDir.chapterDirs) {
mangaDir.chapterDirs -= it
}

View File

@@ -159,7 +159,7 @@ class DownloadManager(
* @return the list of pages from the chapter.
*/
fun buildPageList(source: Source, manga: Manga, chapter: Chapter): List<Page> {
val chapterDir = provider.findChapterDir(chapter.name, chapter.scanlator, manga.title, source)
val chapterDir = provider.findChapterDir(chapter.name, chapter.scanlator, chapter.url, manga.title, source)
val files = chapterDir?.listFiles().orEmpty()
.filter { it.isFile && ImageUtil.isImage(it.name) { it.openInputStream() } }
@@ -185,11 +185,12 @@ class DownloadManager(
fun isChapterDownloaded(
chapterName: String,
chapterScanlator: String?,
chapterUrl: String,
mangaTitle: String,
sourceId: Long,
skipCache: Boolean = false,
): Boolean {
return cache.isChapterDownloaded(chapterName, chapterScanlator, mangaTitle, sourceId, skipCache)
return cache.isChapterDownloaded(chapterName, chapterScanlator, chapterUrl, mangaTitle, sourceId, skipCache)
}
/**
@@ -368,7 +369,7 @@ class DownloadManager(
* @param newChapter the target chapter with the new name.
*/
suspend fun renameChapter(source: Source, manga: Manga, oldChapter: Chapter, newChapter: Chapter) {
val oldNames = provider.getValidChapterDirNames(oldChapter.name, oldChapter.scanlator)
val oldNames = provider.getValidChapterDirNames(oldChapter.name, oldChapter.scanlator, oldChapter.url)
val mangaDir = provider.getMangaDir(manga.title, source).getOrElse { e ->
logcat(LogPriority.ERROR, e) { "Manga download folder doesn't exist. Skipping renaming after source sync" }
return
@@ -379,7 +380,7 @@ class DownloadManager(
.mapNotNull { mangaDir.findFile(it) }
.firstOrNull() ?: return
var newName = provider.getChapterDirName(newChapter.name, newChapter.scanlator)
var newName = provider.getChapterDirName(newChapter.name, newChapter.scanlator, newChapter.url)
if (oldDownload.isFile && oldDownload.extension == "cbz") {
newName += ".cbz"
}

View File

@@ -3,12 +3,14 @@ package eu.kanade.tachiyomi.data.download
import android.content.Context
import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.util.lang.Hash.md5
import eu.kanade.tachiyomi.util.storage.DiskUtil
import logcat.LogPriority
import tachiyomi.core.common.i18n.stringResource
import tachiyomi.core.common.storage.displayablePath
import tachiyomi.core.common.util.system.logcat
import tachiyomi.domain.chapter.model.Chapter
import tachiyomi.domain.library.service.LibraryPreferences
import tachiyomi.domain.manga.model.Manga
import tachiyomi.domain.storage.service.StorageManager
import tachiyomi.i18n.MR
@@ -25,6 +27,7 @@ import java.io.IOException
class DownloadProvider(
private val context: Context,
private val storageManager: StorageManager = Injekt.get(),
private val libraryPreferences: LibraryPreferences = Injekt.get(),
) {
private val downloadsDir: UniFile?
@@ -96,9 +99,15 @@ class DownloadProvider(
* @param mangaTitle the title of the manga to query.
* @param source the source of the chapter.
*/
fun findChapterDir(chapterName: String, chapterScanlator: String?, mangaTitle: String, source: Source): UniFile? {
fun findChapterDir(
chapterName: String,
chapterScanlator: String?,
chapterUrl: String,
mangaTitle: String,
source: Source,
): UniFile? {
val mangaDir = findMangaDir(mangaTitle, source)
return getValidChapterDirNames(chapterName, chapterScanlator).asSequence()
return getValidChapterDirNames(chapterName, chapterScanlator, chapterUrl).asSequence()
.mapNotNull { mangaDir?.findFile(it) }
.firstOrNull()
}
@@ -113,7 +122,7 @@ class DownloadProvider(
fun findChapterDirs(chapters: List<Chapter>, manga: Manga, source: Source): Pair<UniFile?, List<UniFile>> {
val mangaDir = findMangaDir(manga.title, source) ?: return null to emptyList()
return mangaDir to chapters.mapNotNull { chapter ->
getValidChapterDirNames(chapter.name, chapter.scanlator).asSequence()
getValidChapterDirNames(chapter.name, chapter.scanlator, chapter.url).asSequence()
.mapNotNull { mangaDir.findFile(it) }
.firstOrNull()
}
@@ -125,7 +134,10 @@ class DownloadProvider(
* @param source the source to query.
*/
fun getSourceDirName(source: Source): String {
return DiskUtil.buildValidFilename(source.toString())
return DiskUtil.buildValidFilename(
source.toString(),
disallowNonAscii = libraryPreferences.disallowNonAsciiFilenames().get(),
)
}
/**
@@ -134,23 +146,75 @@ class DownloadProvider(
* @param mangaTitle the title of the manga to query.
*/
fun getMangaDirName(mangaTitle: String): String {
return DiskUtil.buildValidFilename(mangaTitle)
return DiskUtil.buildValidFilename(
mangaTitle,
disallowNonAscii = libraryPreferences.disallowNonAsciiFilenames().get(),
)
}
/**
* Returns the chapter directory name for a chapter.
*
* @param chapterName the name of the chapter to query.
* @param chapterScanlator scanlator of the chapter to query
* @param chapterScanlator scanlator of the chapter to query.
* @param chapterUrl url of the chapter to query.
*/
fun getChapterDirName(chapterName: String, chapterScanlator: String?): String {
val newChapterName = sanitizeChapterName(chapterName)
return DiskUtil.buildValidFilename(
fun getChapterDirName(
chapterName: String,
chapterScanlator: String?,
chapterUrl: String,
disallowNonAsciiFilenames: Boolean = libraryPreferences.disallowNonAsciiFilenames().get(),
): String {
var dirName = sanitizeChapterName(chapterName)
if (!chapterScanlator.isNullOrBlank()) {
dirName = chapterScanlator + "_" + dirName
}
// Subtract 7 bytes for hash and underscore, 4 bytes for .cbz
dirName = DiskUtil.buildValidFilename(dirName, DiskUtil.MAX_FILE_NAME_BYTES - 11, disallowNonAsciiFilenames)
dirName += "_" + md5(chapterUrl).take(6)
return dirName
}
/**
* Returns list of names that might have been previously used as
* the directory name for a chapter.
* Add to this list if naming pattern ever changes.
*
* @param chapterName the name of the chapter to query.
* @param chapterScanlator scanlator of the chapter to query.
* @param chapterUrl url of the chapter to query.
*/
private fun getLegacyChapterDirNames(
chapterName: String,
chapterScanlator: String?,
chapterUrl: String,
): List<String> {
val sanitizedChapterName = sanitizeChapterName(chapterName)
val chapterNameV1 = DiskUtil.buildValidFilename(
when {
!chapterScanlator.isNullOrBlank() -> "${chapterScanlator}_$newChapterName"
else -> newChapterName
!chapterScanlator.isNullOrBlank() -> "${chapterScanlator}_$sanitizedChapterName"
else -> sanitizedChapterName
},
)
// Get the filename that would be generated if the user were
// using the other value for the disallow non-ASCII
// filenames setting. This ensures that chapters downloaded
// before the user changed the setting can still be found.
val otherChapterDirName =
getChapterDirName(
chapterName,
chapterScanlator,
chapterUrl,
!libraryPreferences.disallowNonAsciiFilenames().get(),
)
return buildList(2) {
// Chapter name without hash (unable to handle duplicate
// chapter names)
add(chapterNameV1)
add(otherChapterDirName)
}
}
/**
@@ -165,24 +229,30 @@ class DownloadProvider(
}
fun isChapterDirNameChanged(oldChapter: Chapter, newChapter: Chapter): Boolean {
return oldChapter.name != newChapter.name ||
oldChapter.scanlator?.takeIf { it.isNotBlank() } != newChapter.scanlator?.takeIf { it.isNotBlank() }
return getChapterDirName(oldChapter.name, oldChapter.scanlator, oldChapter.url) !=
getChapterDirName(newChapter.name, newChapter.scanlator, newChapter.url)
}
/**
* Returns valid downloaded chapter directory names.
*
* @param chapterName the name of the chapter to query.
* @param chapterScanlator scanlator of the chapter to query
* @param chapter the domain chapter object.
*/
fun getValidChapterDirNames(chapterName: String, chapterScanlator: String?): List<String> {
val chapterDirName = getChapterDirName(chapterName, chapterScanlator)
return buildList(2) {
fun getValidChapterDirNames(chapterName: String, chapterScanlator: String?, chapterUrl: String): List<String> {
val chapterDirName = getChapterDirName(chapterName, chapterScanlator, chapterUrl)
val legacyChapterDirNames = getLegacyChapterDirNames(chapterName, chapterScanlator, chapterUrl)
return buildList {
// Folder of images
add(chapterDirName)
// Archived chapters
add("$chapterDirName.cbz")
// any legacy names
legacyChapterDirNames.forEach {
add(it)
add("$it.cbz")
}
}
}
}

View File

@@ -274,7 +274,7 @@ class Downloader(
val wasEmpty = queueState.value.isEmpty()
val chaptersToQueue = chapters.asSequence()
// Filter out those already downloaded.
.filter { provider.findChapterDir(it.name, it.scanlator, manga.title, source) == null }
.filter { provider.findChapterDir(it.name, it.scanlator, it.url, manga.title, source) == null }
// Add chapters to queue from the start.
.sortedByDescending { it.sourceOrder }
// Filter out those already enqueued.
@@ -299,7 +299,10 @@ class Downloader(
maxDownloadsFromSource > CHAPTERS_PER_SOURCE_QUEUE_WARNING_THRESHOLD
) {
notifier.onWarning(
context.stringResource(MR.strings.download_queue_size_warning),
context.stringResource(
MR.strings.download_queue_size_warning,
context.stringResource(MR.strings.app_name),
),
WARNING_NOTIF_TIMEOUT_MS,
NotificationHandler.openUrl(context, LibraryUpdateNotifier.HELP_WARNING_URL),
)
@@ -333,7 +336,11 @@ class Downloader(
return
}
val chapterDirname = provider.getChapterDirName(download.chapter.name, download.chapter.scanlator)
val chapterDirname = provider.getChapterDirName(
download.chapter.name,
download.chapter.scanlator,
download.chapter.url,
)
val tmpDir = mangaDir.createDirectory(chapterDirname + TMP_DIR_SUFFIX)!!
try {

View File

@@ -1,22 +1,18 @@
package eu.kanade.tachiyomi.data.track.suwayomi
import android.app.Application
import android.content.Context
import android.content.SharedPreferences
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.model.TrackSearch
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.PUT
import eu.kanade.tachiyomi.network.awaitSuccess
import eu.kanade.tachiyomi.network.parseAs
import eu.kanade.tachiyomi.source.online.HttpSource
import kotlinx.serialization.json.Json
import okhttp3.Credentials
import okhttp3.Dns
import okhttp3.FormBody
import okhttp3.Headers
import okhttp3.OkHttpClient
import tachiyomi.core.common.util.lang.withIOContext
import tachiyomi.domain.source.service.SourceManager
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
@@ -25,26 +21,13 @@ import java.security.MessageDigest
class SuwayomiApi(private val trackId: Long) {
private val network: NetworkHelper by injectLazy()
private val json: Json by injectLazy()
private val client: OkHttpClient =
network.client.newBuilder()
.dns(Dns.SYSTEM) // don't use DNS over HTTPS as it breaks IP addressing
.build()
private fun headersBuilder(): Headers.Builder = Headers.Builder().apply {
if (basePassword.isNotEmpty() && baseLogin.isNotEmpty()) {
val credentials = Credentials.basic(baseLogin, basePassword)
add("Authorization", credentials)
}
}
private val headers: Headers by lazy { headersBuilder().build() }
private val baseUrl by lazy { getPrefBaseUrl() }
private val baseLogin by lazy { getPrefBaseLogin() }
private val basePassword by lazy { getPrefBasePassword() }
private val sourceManager: SourceManager by injectLazy()
private val source: HttpSource by lazy { (sourceManager.get(sourceId) as HttpSource) }
private val client: OkHttpClient by lazy { source.client }
private val headers: Headers by lazy { source.headers }
private val baseUrl: String by lazy { source.baseUrl.trimEnd('/') }
suspend fun getTrackSearch(trackUrl: String): TrackSearch = withIOContext {
val url = try {
@@ -105,19 +88,4 @@ class SuwayomiApi(private val trackId: Long) {
val bytes = MessageDigest.getInstance("MD5").digest(key.toByteArray())
(0..7).map { bytes[it].toLong() and 0xff shl 8 * (7 - it) }.reduce(Long::or) and Long.MAX_VALUE
}
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$sourceId", Context.MODE_PRIVATE)
}
private fun getPrefBaseUrl(): String = preferences.getString(ADDRESS_TITLE, ADDRESS_DEFAULT)!!
private fun getPrefBaseLogin(): String = preferences.getString(LOGIN_TITLE, LOGIN_DEFAULT)!!
private fun getPrefBasePassword(): String = preferences.getString(PASSWORD_TITLE, PASSWORD_DEFAULT)!!
}
private const val ADDRESS_TITLE = "Server URL Address"
private const val ADDRESS_DEFAULT = ""
private const val LOGIN_TITLE = "Login (Basic Auth)"
private const val LOGIN_DEFAULT = ""
private const val PASSWORD_TITLE = "Password (Basic Auth)"
private const val PASSWORD_DEFAULT = ""

View File

@@ -484,6 +484,7 @@ class LibraryScreenModel(
downloadManager.isChapterDownloaded(
chapter.name,
chapter.scanlator,
chapter.url,
manga.title,
manga.source,
)

View File

@@ -527,7 +527,13 @@ class MangaScreenModel(
val downloaded = if (isLocal) {
true
} else {
downloadManager.isChapterDownloaded(chapter.name, chapter.scanlator, manga.title, manga.source)
downloadManager.isChapterDownloaded(
chapter.name,
chapter.scanlator,
chapter.url,
manga.title,
manga.source,
)
}
val downloadState = when {
activeDownload != null -> activeDownload.status

View File

@@ -38,7 +38,6 @@ import eu.kanade.tachiyomi.util.chapter.filterDownloaded
import eu.kanade.tachiyomi.util.chapter.removeDuplicates
import eu.kanade.tachiyomi.util.editCover
import eu.kanade.tachiyomi.util.lang.byteSize
import eu.kanade.tachiyomi.util.lang.takeBytes
import eu.kanade.tachiyomi.util.storage.DiskUtil
import eu.kanade.tachiyomi.util.storage.cacheImageDir
import kotlinx.coroutines.CancellationException
@@ -175,6 +174,7 @@ class ReaderViewModel @JvmOverloads constructor(
!downloadManager.isChapterDownloaded(
it.name,
it.scanlator,
it.url,
manga.title,
manga.source,
)
@@ -184,6 +184,7 @@ class ReaderViewModel @JvmOverloads constructor(
downloadManager.isChapterDownloaded(
it.name,
it.scanlator,
it.url,
manga.title,
manga.source,
)
@@ -397,6 +398,7 @@ class ReaderViewModel @JvmOverloads constructor(
val isDownloaded = downloadManager.isChapterDownloaded(
dbChapter.name,
dbChapter.scanlator,
dbChapter.url,
manga.title,
manga.source,
skipCache = true,
@@ -473,6 +475,7 @@ class ReaderViewModel @JvmOverloads constructor(
val isNextChapterDownloaded = downloadManager.isChapterDownloaded(
nextChapter.name,
nextChapter.scanlator,
nextChapter.url,
manga.title,
manga.source,
)
@@ -757,7 +760,8 @@ class ReaderViewModel @JvmOverloads constructor(
val chapter = page.chapter.chapter
val filenameSuffix = " - ${page.number}"
return DiskUtil.buildValidFilename(
"${manga.title} - ${chapter.name}".takeBytes(DiskUtil.MAX_FILE_NAME_BYTES - filenameSuffix.byteSize()),
"${manga.title} - ${chapter.name}",
DiskUtil.MAX_FILE_NAME_BYTES - filenameSuffix.byteSize(),
) + filenameSuffix
}

View File

@@ -80,6 +80,7 @@ class ChapterLoader(
val isDownloaded = downloadManager.isChapterDownloaded(
dbChapter.name,
dbChapter.scanlator,
dbChapter.url,
manga.title,
manga.source,
skipCache = true,

View File

@@ -33,7 +33,13 @@ internal class DownloadPageLoader(
override suspend fun getPages(): List<ReaderPage> {
val dbChapter = chapter.chapter
val chapterPath = downloadProvider.findChapterDir(dbChapter.name, dbChapter.scanlator, manga.title, source)
val chapterPath = downloadProvider.findChapterDir(
dbChapter.name,
dbChapter.scanlator,
dbChapter.url,
manga.title,
source,
)
return if (chapterPath?.isFile == true) {
getPagesFromArchive(chapterPath)
} else {

View File

@@ -37,6 +37,7 @@ class ReaderTransitionView @JvmOverloads constructor(context: Context, attrs: At
downloadManager.isChapterDownloaded(
chapterName = goingToChapter.name,
chapterScanlator = goingToChapter.scanlator,
chapterUrl = goingToChapter.url,
mangaTitle = manga.title,
sourceId = manga.source,
skipCache = true,

View File

@@ -107,6 +107,7 @@ class UpdatesScreenModel(
val downloaded = downloadManager.isChapterDownloaded(
update.chapterName,
update.scanlator,
update.chapterUrl,
update.mangaTitle,
update.sourceId,
)

View File

@@ -15,5 +15,5 @@ fun List<Chapter>.filterDownloaded(manga: Manga): List<Chapter> {
val downloadCache: DownloadCache = Injekt.get()
return filter { downloadCache.isChapterDownloaded(it.name, it.scanlator, manga.title, manga.source, false) }
return filter { downloadCache.isChapterDownloaded(it.name, it.scanlator, it.url, manga.title, manga.source, false) }
}

View File

@@ -89,7 +89,7 @@ class MigrateMangaUseCase(
}
// Update categories
if (MigrationFlag.CHAPTER in flags) {
if (MigrationFlag.CATEGORY in flags) {
val categoryIds = getCategories.await(current.id).map { it.id }
setMangaCategories.await(target.id, categoryIds)
}

View File

@@ -251,6 +251,7 @@ class MigrationListScreenModel(
} catch (_: Exception) {
}
migratingManga.searchResult.value = result.toSuccessSearchResult()
updateMigrationProgress()
}
}

View File

@@ -4,8 +4,8 @@ import org.gradle.api.JavaVersion as GradleJavaVersion
import org.jetbrains.kotlin.gradle.dsl.JvmTarget as KotlinJvmTarget
object AndroidConfig {
const val COMPILE_SDK = 35
const val TARGET_SDK = 34
const val COMPILE_SDK = 36
const val TARGET_SDK = 36
const val MIN_SDK = 26
const val NDK = "27.1.12297006"
const val BUILD_TOOLS = "35.0.1"

View File

@@ -9,6 +9,9 @@ import androidx.core.content.ContextCompat
import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.util.lang.Hash
import java.io.File
import java.nio.ByteBuffer
import java.nio.CharBuffer
import java.nio.charset.CodingErrorAction
object DiskUtil {
@@ -102,26 +105,84 @@ object DiskUtil {
}
/**
* Mutate the given filename to make it valid for a FAT filesystem,
* replacing any invalid characters with "_". This method doesn't allow hidden files (starting
* with a dot), but you can manually add it later.
* Transform a filename fragment to make it safe to use on almost
* all commonly used filesystems. You can pass an entire filename,
* or just part of one, in case you want a specific part of a long
* filename to be truncated, rather than the end of it.
*
* Characters that are potentially unsafe for some filesystems are
* replaced with underscores. This includes the standard ones from
* https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file
* but does allow any other valid Unicode code point.
*
* Excessively long filenames are truncated, by default to 240
* bytes. Note that the truncation is based on bytes rather than
* characters (code points), because this is what is relevant to
* filesystem restrictions in most cases.
*
* Leading periods are stripped, to avoid the creation of hidden
* files by default. If a hidden file is desired, a period can be
* prepended to the return value from this function.
*
* If the optional argument disallowNonAscii is set to true,
* then ANYTHING outside the ASCII range is replaced not with underscores,
* but with its hexadecimal encoding. This is to make it so that distinct
* non-English titles of things remain distinct, since not all
* places where this function is used also take care of
* disambiguation.
*
* We could instead replace only non-ASCII characters known to
* be problematic, but so far nobody with a non-Unicode-compliant
* device has been able to provide either directions to reproduce
* their issue nor any documentation or tests that would allow us
* to determine which characters are problems and which are not.
*/
fun buildValidFilename(origName: String): String {
fun buildValidFilename(
origName: String,
maxBytes: Int = MAX_FILE_NAME_BYTES,
disallowNonAscii: Boolean = false,
): String {
val name = origName.trim('.', ' ')
if (name.isEmpty()) {
return "(invalid)"
}
val sb = StringBuilder(name.length)
name.forEach { c ->
if (isValidFatFilenameChar(c)) {
if (disallowNonAscii && c >= 0x80.toChar()) {
sb.append(
c.toString().toByteArray(Charsets.UTF_8).toHexString(
HexFormat {
upperCase = false
},
),
)
} else if (isValidFatFilenameChar(c)) {
sb.append(c)
} else {
sb.append('_')
}
}
// Even though vfat allows 255 UCS-2 chars, we might eventually write to
// ext4 through a FUSE layer, so use that limit minus 15 reserved characters.
return sb.toString().take(240)
return truncateToLength(sb.toString(), maxBytes)
}
/**
* Truncate a string to a maximum length, while maintaining valid Unicode encoding.
*/
fun truncateToLength(s: String, maxBytes: Int): String {
val charset = Charsets.UTF_8
val decoder = charset.newDecoder()
val sba = s.toByteArray(charset)
if (sba.size <= maxBytes) {
return s
}
// Ensure truncation by having byte buffer = maxBytes
val bb = ByteBuffer.wrap(sba, 0, maxBytes)
val cb = CharBuffer.allocate(maxBytes)
// Ignore an incomplete character
decoder.onMalformedInput(CodingErrorAction.IGNORE)
decoder.decode(bb, cb, true)
decoder.flush(cb)
return String(cb.array(), 0, cb.position())
}
/**
@@ -139,6 +200,8 @@ object DiskUtil {
const val NOMEDIA_FILE = ".nomedia"
// Safe theoretical max filename size is 255 bytes and 1 char = 2-4 bytes (UTF-8)
const val MAX_FILE_NAME_BYTES = 250
// Safe theoretical max filename size is 255 bytes and 1 char = 2-4 bytes (UTF-8).
// To allow for writing to ext4 through a FUSE layer in the future, also subtract 15
// reserved characters.
const val MAX_FILE_NAME_BYTES = 240
}

View File

@@ -52,6 +52,7 @@ class UpdatesRepositoryImpl(
chapterId: Long,
chapterName: String,
scanlator: String?,
chapterUrl: String,
read: Boolean,
bookmark: Boolean,
lastPageRead: Long,
@@ -67,6 +68,7 @@ class UpdatesRepositoryImpl(
chapterId = chapterId,
chapterName = chapterName,
scanlator = scanlator,
chapterUrl = chapterUrl,
read = read,
bookmark = bookmark,
lastPageRead = lastPageRead,

View File

@@ -0,0 +1,24 @@
-- Add chapter urls to updates view
DROP VIEW IF EXISTS updatesView;
CREATE VIEW updatesView AS
SELECT
mangas._id AS mangaId,
mangas.title AS mangaTitle,
chapters._id AS chapterId,
chapters.name AS chapterName,
chapters.scanlator,
chapters.url AS chapterUrl,
chapters.read,
chapters.bookmark,
chapters.last_page_read,
mangas.source,
mangas.favorite,
mangas.thumbnail_url AS thumbnailUrl,
mangas.cover_last_modified AS coverLastModified,
chapters.date_upload AS dateUpload,
chapters.date_fetch AS datefetch
FROM mangas JOIN chapters
ON mangas._id = chapters.manga_id
WHERE favorite = 1
AND date_fetch > date_added
ORDER BY date_fetch DESC;

View File

@@ -5,6 +5,7 @@ SELECT
chapters._id AS chapterId,
chapters.name AS chapterName,
chapters.scanlator,
chapters.url AS chapterUrl,
chapters.read,
chapters.bookmark,
chapters.last_page_read,

View File

@@ -31,7 +31,7 @@ dependencies {
api(libs.sqldelight.android.paging)
compileOnly(libs.compose.stablemarker)
compileOnly(compose.runtime.annotation)
testImplementation(libs.bundles.test)
testImplementation(kotlinx.coroutines.test)

View File

@@ -192,6 +192,8 @@ class LibraryPreferences(
fun updateMangaTitles() = preferenceStore.getBoolean("pref_update_library_manga_titles", false)
fun disallowNonAsciiFilenames() = preferenceStore.getBoolean("disallow_non_ascii_filenames", false)
// endregion
enum class ChapterSwipeAction {

View File

@@ -8,6 +8,7 @@ data class UpdatesWithRelations(
val chapterId: Long,
val chapterName: String,
val scanlator: String?,
val chapterUrl: String,
val read: Boolean,
val bookmark: Boolean,
val lastPageRead: Long,

View File

@@ -1,6 +1,6 @@
[versions]
agp_version = "8.12.0"
lifecycle_version = "2.9.2"
agp_version = "8.13.0"
lifecycle_version = "2.9.4"
paging_version = "3.3.6"
interpolator_version = "1.0.0"
@@ -11,7 +11,7 @@ annotation = "androidx.annotation:annotation:1.9.1"
appcompat = "androidx.appcompat:appcompat:1.7.1"
biometricktx = "androidx.biometric:biometric-ktx:1.2.0-alpha05"
constraintlayout = "androidx.constraintlayout:constraintlayout:2.2.1"
corektx = "androidx.core:core-ktx:1.16.0"
corektx = "androidx.core:core-ktx:1.17.0"
splashscreen = "androidx.core:core-splashscreen:1.0.1"
recyclerview = "androidx.recyclerview:recyclerview:1.4.0"
viewpager = "androidx.viewpager:viewpager:1.1.0"
@@ -21,14 +21,14 @@ lifecycle-common = { module = "androidx.lifecycle:lifecycle-common", version.ref
lifecycle-process = { module = "androidx.lifecycle:lifecycle-process", version.ref = "lifecycle_version" }
lifecycle-runtimektx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycle_version" }
workmanager = "androidx.work:work-runtime:2.10.3"
workmanager = "androidx.work:work-runtime:2.10.5"
paging-runtime = { module = "androidx.paging:paging-runtime", version.ref = "paging_version" }
paging-compose = { module = "androidx.paging:paging-compose", version.ref = "paging_version" }
interpolator = { group = "androidx.interpolator", name = "interpolator", version.ref = "interpolator_version" }
benchmark-macro = "androidx.benchmark:benchmark-macro-junit4:1.4.0"
benchmark-macro = "androidx.benchmark:benchmark-macro-junit4:1.4.1"
test-ext = "androidx.test.ext:junit-ktx:1.3.0"
test-espresso-core = "androidx.test.espresso:espresso-core:3.7.0"
test-uiautomator = "androidx.test.uiautomator:uiautomator:2.3.0"

View File

@@ -1,13 +1,14 @@
[versions]
compose-bom = "2025.07.00"
compose-bom = "2025.09.00"
[libraries]
activity = "androidx.activity:activity-compose:1.10.1"
activity = "androidx.activity:activity-compose:1.11.0"
bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose-bom" }
foundation = { module = "androidx.compose.foundation:foundation" }
animation = { module = "androidx.compose.animation:animation" }
animation-graphics = { module = "androidx.compose.animation:animation-graphics" }
runtime = { module = "androidx.compose.runtime:runtime" }
runtime-annotation = { module = "androidx.compose.runtime:runtime-annotation" }
ui-tooling = { module = "androidx.compose.ui:ui-tooling" }
ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview" }
ui-util = { module = "androidx.compose.ui:ui-util" }

View File

@@ -1,5 +1,5 @@
[versions]
kotlin_version = "2.2.0"
kotlin_version = "2.2.20"
serialization_version = "1.9.0"
xml_serialization_version = "0.91.2"

View File

@@ -1,17 +1,17 @@
[versions]
aboutlib_version = "12.2.4"
leakcanary = "2.14"
moko = "0.25.0"
okhttp_version = "5.1.0"
moko = "0.25.1"
okhttp_version = "5.2.0"
shizuku_version = "13.1.0"
sqldelight = "2.1.0"
sqlite = "2.5.2"
sqlite = "2.6.1"
voyager = "1.1.0-beta03"
spotless = "7.2.1"
spotless = "8.0.0"
ktlint-core = "1.7.1"
firebase-bom = "34.0.0"
markdown = "0.35.0"
junit = "5.13.4"
firebase-bom = "34.3.0"
markdown = "0.37.0"
junit = "6.0.0"
[libraries]
desugar = "com.android.tools:desugar_jdk_libs:2.1.5"
@@ -27,9 +27,9 @@ okio = "com.squareup.okio:okio:3.16.0"
conscrypt-android = "org.conscrypt:conscrypt-android:2.5.3"
quickjs-android = "app.cash.quickjs:quickjs-android:0.9.2"
quickjs-android = { group = "com.github.zhanghai.quickjs-java", name = "quickjs-android", version = "547f5b1597" }
jsoup = "org.jsoup:jsoup:1.21.1"
jsoup = "org.jsoup:jsoup:1.21.2"
disklrucache = "com.jakewharton:disklrucache:2.0.2"
unifile = "com.github.tachiyomiorg:unifile:e0def6b3dc"
@@ -64,8 +64,7 @@ insetter = "dev.chrisbanes.insetter:insetter:0.6.1"
compose-materialmotion = "io.github.fornewid:material-motion-compose-core:2.0.1"
compose-webview = "io.github.kevinnzou:compose-webview:0.33.6"
compose-grid = "io.woong.compose.grid:grid:1.2.2"
compose-stablemarker = "com.github.skydoves:compose-stable-marker:1.0.5"
reorderable = { module = "sh.calvin.reorderable:reorderable", version = "2.5.1" }
reorderable = { module = "sh.calvin.reorderable:reorderable", version = "3.0.0" }
swipe = "me.saket.swipe:swipe:1.3.0"
@@ -92,8 +91,8 @@ sqldelight-dialects-sql = { module = "app.cash.sqldelight:sqlite-3-38-dialect",
junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit" }
junit-platform-launcher = { module = "org.junit.platform:junit-platform-launcher" }
kotest-assertions = "io.kotest:kotest-assertions-core:5.9.1"
mockk = "io.mockk:mockk:1.14.5"
kotest-assertions = "io.kotest:kotest-assertions-core:6.0.3"
mockk = "io.mockk:mockk:1.14.6"
voyager-navigator = { module = "cafe.adriel.voyager:voyager-navigator", version.ref = "voyager" }
voyager-screenmodel = { module = "cafe.adriel.voyager:voyager-screenmodel", version.ref = "voyager" }
@@ -113,7 +112,7 @@ google-services = { id = "com.google.gms.google-services", version = "4.4.3" }
aboutLibraries = { id = "com.mikepenz.aboutlibraries.plugin", version.ref = "aboutlib_version" }
sqldelight = { id = "app.cash.sqldelight", version.ref = "sqldelight" }
moko = { id = "dev.icerock.mobile.multiplatform-resources", version.ref = "moko" }
firebase-crashlytics = { id = "com.google.firebase.crashlytics", version = "3.0.5" }
firebase-crashlytics = { id = "com.google.firebase.crashlytics", version = "3.0.6" }
[bundles]
okhttp = ["okhttp-core", "okhttp-logging", "okhttp-brotli", "okhttp-dnsoverhttps"]

View File

@@ -320,6 +320,8 @@
<string name="pref_mark_duplicate_read_chapter_read_new">After fetching new chapter</string>
<string name="pref_hide_missing_chapter_indicators">Hide missing chapter indicators</string>
<string name="pref_disallow_non_ascii_filenames">Disallow non-ASCII filenames</string>
<string name="pref_disallow_non_ascii_filenames_details">Ensures compatibility with certain storage media that don't support Unicode. When this is enabled, you'll need to manually rename source and manga folders by replacing non-ASCII characters with their lowercase UTF-8 hexadecimal representations. Chapter files don't need to be renamed.</string>
<!-- Extension section -->
<string name="multi_lang">Multi</string>
@@ -364,7 +366,7 @@
<string name="information_empty_repos">You have no repos set.</string>
<string name="action_add_repo">Add repo</string>
<string name="label_add_repo_input">Repo URL</string>
<string name="action_add_repo_message">Add additional repos to Mihon. This should be a URL that ends with \"index.min.json\".</string>
<string name="action_add_repo_message">Add additional repos to %s. This should be a URL that ends with \"index.min.json\".</string>
<string name="error_repo_exists">This repo already exists!</string>
<string name="action_delete_repo">Delete repo</string>
<string name="invalid_repo_name">Invalid repo URL</string>
@@ -904,7 +906,7 @@
<!-- Downloads activity and service -->
<string name="download_queue_error">Couldn\'t download chapters. You can try again in the downloads section</string>
<string name="download_insufficient_space">Couldn\'t download chapters due to low storage space</string>
<string name="download_queue_size_warning">Warning: large bulk downloads may lead to sources becoming slower and/or blocking Mihon. Tap to learn more.</string>
<string name="download_queue_size_warning">Warning: large bulk downloads may lead to sources becoming slower and/or blocking %s. Tap to learn more.</string>
<!-- Library update service notifications -->
<string name="notification_updating_progress">Updating library… (%s)</string>