mirror of
https://github.com/mihonapp/mihon.git
synced 2025-10-09 12:59:34 +02:00
Compare commits
47 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
58b25d697f | ||
|
1a31c7c7ee | ||
|
ad6b651b37 | ||
|
96e5131358 | ||
|
1d5bc8d2c2 | ||
|
6cee911239 | ||
|
96347e3f76 | ||
|
9a45d248b1 | ||
|
04168ecec8 | ||
|
607f0ea9cd | ||
|
27a4f6f45c | ||
|
5236d003d2 | ||
|
d61a41e819 | ||
|
5637860dd2 | ||
|
d4d18d0898 | ||
|
065147472e | ||
|
6f635782c2 | ||
|
86d85f74c0 | ||
|
29e6a2c4a6 | ||
|
60c66bbd3a | ||
|
060e5b2e2e | ||
|
4ac9fcd4d3 | ||
|
d3b7f7e55f | ||
|
0d926626a1 | ||
|
6495a2ea43 | ||
|
94f711ba2a | ||
|
9f5c4e03b2 | ||
|
49562e1915 | ||
|
57c82b30ba | ||
|
e573f72cfd | ||
|
95357a8625 | ||
|
bd90307df9 | ||
|
16b5317b90 | ||
|
83f4b48629 | ||
|
4665dc50f6 | ||
|
85f5e5019e | ||
|
4bc3b9f3b6 | ||
|
2c0d3678d9 | ||
|
feda410152 | ||
|
200c2df5ba | ||
|
be09cddde2 | ||
|
498317de52 | ||
|
33b876edc6 | ||
|
e7251f2034 | ||
|
3d3c36078a | ||
|
c6a96b3970 | ||
|
fb3dc1c984 |
8
.github/workflows/build.yml
vendored
8
.github/workflows/build.yml
vendored
@@ -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
|
||||
|
14
.github/workflows/release.yml
vendored
14
.github/workflows/release.yml
vendored
@@ -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 }}
|
||||
|
19
CHANGELOG.md
19
CHANGELOG.md
@@ -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))
|
||||
|
@@ -12,7 +12,7 @@ Discover and read manga, webtoons, comics, and more – easier than ever on your
|
||||
[](https://discord.gg/mihon)
|
||||
[](https://mihon.app/download)
|
||||
|
||||
[](https://github.com/mihonapp/mihon/actions/workflows/build_push.yml)
|
||||
[](https://github.com/mihonapp/mihon/actions/workflows/build_push.yml)
|
||||
[](/LICENSE)
|
||||
[](https://hosted.weblate.org/engage/mihon/)
|
||||
|
||||
|
@@ -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)
|
||||
}
|
||||
|
@@ -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,
|
||||
)
|
||||
|
@@ -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()
|
||||
|
@@ -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,
|
||||
)
|
||||
|
@@ -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),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -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)
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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"
|
||||
}
|
||||
|
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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 {
|
||||
|
@@ -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 = ""
|
||||
|
@@ -484,6 +484,7 @@ class LibraryScreenModel(
|
||||
downloadManager.isChapterDownloaded(
|
||||
chapter.name,
|
||||
chapter.scanlator,
|
||||
chapter.url,
|
||||
manga.title,
|
||||
manga.source,
|
||||
)
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
}
|
||||
|
||||
|
@@ -80,6 +80,7 @@ class ChapterLoader(
|
||||
val isDownloaded = downloadManager.isChapterDownloaded(
|
||||
dbChapter.name,
|
||||
dbChapter.scanlator,
|
||||
dbChapter.url,
|
||||
manga.title,
|
||||
manga.source,
|
||||
skipCache = true,
|
||||
|
@@ -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 {
|
||||
|
@@ -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,
|
||||
|
@@ -107,6 +107,7 @@ class UpdatesScreenModel(
|
||||
val downloaded = downloadManager.isChapterDownloaded(
|
||||
update.chapterName,
|
||||
update.scanlator,
|
||||
update.chapterUrl,
|
||||
update.mangaTitle,
|
||||
update.sourceId,
|
||||
)
|
||||
|
@@ -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) }
|
||||
}
|
||||
|
@@ -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)
|
||||
}
|
||||
|
@@ -251,6 +251,7 @@ class MigrationListScreenModel(
|
||||
} catch (_: Exception) {
|
||||
}
|
||||
migratingManga.searchResult.value = result.toSuccessSearchResult()
|
||||
updateMigrationProgress()
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -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"
|
||||
|
@@ -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
|
||||
}
|
||||
|
@@ -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,
|
||||
|
24
data/src/main/sqldelight/tachiyomi/migrations/7.sqm
Normal file
24
data/src/main/sqldelight/tachiyomi/migrations/7.sqm
Normal 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;
|
@@ -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,
|
||||
@@ -31,4 +32,4 @@ SELECT *
|
||||
FROM updatesView
|
||||
WHERE read = :read
|
||||
AND dateUpload > :after
|
||||
LIMIT :limit;
|
||||
LIMIT :limit;
|
||||
|
@@ -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)
|
||||
|
@@ -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 {
|
||||
|
@@ -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,
|
||||
|
@@ -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"
|
||||
|
@@ -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" }
|
||||
|
@@ -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"
|
||||
|
||||
|
@@ -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"]
|
||||
|
@@ -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>
|
||||
|
Reference in New Issue
Block a user