Compare commits
12 Commits
51a67328eb
...
ba96a8a2ee
| Author | SHA1 | Date | |
|---|---|---|---|
| ba96a8a2ee | |||
|
|
230cf3764a | ||
|
|
600d8ca4ff | ||
|
|
13657ba2dc | ||
|
|
6780871c0f | ||
|
|
05bf6b5904 | ||
|
|
dd0f7dd467 | ||
|
|
0168daab08 | ||
|
|
db005805d5 | ||
|
|
f207b2a3c2 | ||
|
|
a867c9caf5 | ||
|
|
f04e4c92e7 |
28
.github/workflows/create_release.yml
vendored
28
.github/workflows/create_release.yml
vendored
@@ -25,8 +25,8 @@ jobs:
|
||||
id: pr_info
|
||||
run: |
|
||||
# Fetch and checkout master to analyze history
|
||||
git fetch origin master:master
|
||||
git checkout master
|
||||
git fetch origin master
|
||||
git checkout -B master origin/master
|
||||
|
||||
# Initialize variables
|
||||
FOUND_PR=false
|
||||
@@ -36,9 +36,11 @@ jobs:
|
||||
|
||||
for COMMIT in $MERGE_COMMITS; do
|
||||
# Extract PR number
|
||||
PR_NUMBER=$(git show --format=%B -s $COMMIT | grep -oP 'Merge pull request #\K\d+')
|
||||
PR_NUMBER=$(git show --format=%B -s $COMMIT | grep -oP 'Merge pull request #\K\d+' || true)
|
||||
|
||||
# Fallback: Try to match squash merge pattern "Title (#123)"
|
||||
if [ -z "$PR_NUMBER" ]; then
|
||||
PR_NUMBER=$(git show --format=%B -s $COMMIT | grep -oP '\(#\K\d+(?=\))')
|
||||
PR_NUMBER=$(git show --format=%B -s $COMMIT | grep -oP '\(#\K\d+(?=\))' || true)
|
||||
fi
|
||||
|
||||
if [ -n "$PR_NUMBER" ]; then
|
||||
@@ -153,6 +155,12 @@ jobs:
|
||||
echo "EOF"
|
||||
} >> $GITHUB_OUTPUT
|
||||
|
||||
# Switch back to 'repo' branch to upload the APK
|
||||
- name: Final Switch to repo branch
|
||||
if: steps.check_release.outputs.exists == 'false'
|
||||
run: |
|
||||
git checkout repo
|
||||
|
||||
- name: Create Release
|
||||
if: steps.check_release.outputs.exists == 'false'
|
||||
uses: softprops/action-gh-release@v2
|
||||
@@ -163,15 +171,15 @@ jobs:
|
||||
body: |
|
||||
## 🚀 Release ${{ steps.apk_info.outputs.version }}
|
||||
|
||||
### 📝 Pull Request
|
||||
${{ steps.pr_info.outputs.pr_number && format('- Pull Request #%s by @%s', steps.pr_info.outputs.pr_number, steps.pr_info.outputs.pr_author) || 'Manual release' }}
|
||||
${{ steps.pr_info.outputs.pr_title && format('- Title: %s', steps.pr_info.outputs.pr_title) || '' }}
|
||||
${{ steps.pr_info.outputs.is_first_contributor == 'true' && format('### 🎉 Welcome @%s on their first contribution!', steps.pr_info.outputs.pr_author) || '' }}
|
||||
${{ steps.pr_info.outputs.pr_body }}
|
||||
|
||||
${{ steps.pr_info.outputs.pr_body && format('### 📋 Description:\n%s', steps.pr_info.outputs.pr_body) || '' }}
|
||||
---
|
||||
|
||||
<details>
|
||||
<summary>📦 View Full Commit Log</summary>
|
||||
|
||||
### 📦 Changes since last release:
|
||||
${{ steps.changelog.outputs.changelog }}
|
||||
</details>
|
||||
|
||||
---
|
||||
🤖 *This release was automatically created*
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -11,3 +11,7 @@ apk/
|
||||
gen
|
||||
generated-src/
|
||||
.kotlin
|
||||
signingkey.jks
|
||||
output.json
|
||||
tmp/
|
||||
Inspector.jar
|
||||
|
||||
@@ -25,3 +25,5 @@ android.useAndroidX=true
|
||||
android.enableBuildConfigAsBytecode=true
|
||||
android.defaults.buildfeatures.resvalues=false
|
||||
android.defaults.buildfeatures.shaders=false
|
||||
|
||||
android.overridePathCheck=true
|
||||
|
||||
@@ -2,7 +2,7 @@ ext {
|
||||
kmkVersionCode = 1
|
||||
extName = 'Kavita'
|
||||
extClass = '.KavitaFactory'
|
||||
extVersionCode = 21
|
||||
extVersionCode = 22
|
||||
isNsfw = false
|
||||
}
|
||||
|
||||
|
||||
@@ -125,10 +125,10 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
|
||||
override val name = "${KavitaInt.KAVITA_NAME} (${preferences.getString(KavitaConstants.customSourceNamePref, suffix)})"
|
||||
override val lang = "all"
|
||||
override val supportsLatest = true
|
||||
private val apiUrl: String by lazy { getPrefApiUrl() }
|
||||
private val apiKey: String by lazy { getPrefApiKey() }
|
||||
override val baseUrl by lazy { getPrefBaseUrl() }
|
||||
private val address by lazy { getPrefAddress() } // Address for the Kavita OPDS url. Should be http(s)://host:(port)/api/opds/api-key
|
||||
private val apiUrl: String get() = getPrefApiUrl()
|
||||
private val apiKey: String get() = getPrefApiKey()
|
||||
override val baseUrl: String get() = getPrefBaseUrl()
|
||||
private val address: String get() = getPrefAddress() // Address for the Kavita OPDS url. Should be http(s)://host:(port)/api/opds/api-key
|
||||
private var jwtToken = "" // * JWT Token for authentication with the server. Stored in memory.
|
||||
private val LOG_TAG = """Kavita_${"[$suffix]_" + preferences.getString(KavitaConstants.customSourceNamePref, "[$suffix]")!!.replace(' ', '_')}"""
|
||||
private var isLogged = false // Used to know if login was correct and not send login requests anymore
|
||||
@@ -265,14 +265,17 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
|
||||
* Custom implementation for fetch popular, latest and search
|
||||
* Handles and logs errors to provide a more detailed exception to the users.
|
||||
*/
|
||||
private suspend fun fetch(request: Request): MangasPage = withContext(Dispatchers.IO) {
|
||||
private suspend fun fetch(
|
||||
request: Request,
|
||||
parser: (Response) -> MangasPage = ::popularMangaParse,
|
||||
): MangasPage = withContext(Dispatchers.IO) {
|
||||
try {
|
||||
val response = client.newCall(request).execute()
|
||||
if (!response.isSuccessful) {
|
||||
val code = response.code
|
||||
throw IOException("Http Error: $code\n ${intl["http_errors_$code"]}\n${intl["check_version"]}")
|
||||
}
|
||||
popularMangaParse(response)
|
||||
parser(response)
|
||||
} catch (e: Exception) {
|
||||
Log.e(LOG_TAG, "Error fetching manga", e)
|
||||
throw e
|
||||
@@ -288,7 +291,7 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
|
||||
popularMangaParse(response)
|
||||
} catch (e: Exception) {
|
||||
Log.e(LOG_TAG, "Error fetching popular manga", e)
|
||||
MangasPage(emptyList(), false)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
|
||||
@@ -303,7 +306,7 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
|
||||
readingListParse(response)
|
||||
}
|
||||
else -> { // Regular searches (including Want to Read)
|
||||
fetch(searchMangaRequest(page, query, filters))
|
||||
fetch(searchMangaRequest(page, query, filters), ::searchMangaParse)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1693,7 +1696,7 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
|
||||
scanlatorFormat = preferences.scanlatorFormat,
|
||||
volumePageCount = volume.pages,
|
||||
)
|
||||
sChapter.url = "/Chapter/${chapter.id}"
|
||||
sChapter.url = chapterUrlWithPageCount(chapter, volume.pages)
|
||||
|
||||
// For singleFileVolume, ensure the scanlator field reflects webtoon terminology
|
||||
sChapter.scanlator = if (isWebtoon) "Season" else "Volume"
|
||||
@@ -1712,7 +1715,7 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
|
||||
scanlatorFormat = preferences.scanlatorFormat,
|
||||
)
|
||||
|
||||
sChapter.url = "/Chapter/${chapter.id}"
|
||||
sChapter.url = chapterUrlWithPageCount(chapter)
|
||||
|
||||
allChapters.add(sChapter)
|
||||
}
|
||||
@@ -1757,6 +1760,48 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
|
||||
return GET("$apiUrl/$chapterId", headersBuilder().build())
|
||||
}
|
||||
|
||||
private fun chapterUrlWithPageCount(chapter: ChapterDto, pageCount: Int = chapter.pages): String {
|
||||
val params = mutableListOf<String>()
|
||||
if (pageCount > 0) {
|
||||
params += "pages=$pageCount"
|
||||
}
|
||||
params += "extractPdf=${shouldExtractPdf(chapter)}"
|
||||
if (chapter.fileCount > 1) {
|
||||
params += "split=${chapter.fileCount}"
|
||||
}
|
||||
return buildString {
|
||||
append("/Chapter/${chapter.id}")
|
||||
if (params.isNotEmpty()) {
|
||||
append("?")
|
||||
append(params.joinToString("&"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun pageCountFromChapterUrl(chapter: SChapter): Int? =
|
||||
chapter.url.substringAfter("pages=", "")
|
||||
.substringBefore("&")
|
||||
.toIntOrNull()
|
||||
?.takeIf { it > 0 }
|
||||
|
||||
private fun shouldExtractPdf(chapter: ChapterDto): Boolean =
|
||||
chapter.files.orEmpty().any { file ->
|
||||
file.format == MangaFormat.Pdf.format || file.extension.equals("pdf", ignoreCase = true)
|
||||
}
|
||||
|
||||
private fun extractPdfFromChapterUrl(chapter: SChapter): Boolean =
|
||||
chapter.url.substringAfter("extractPdf=", "")
|
||||
.substringBefore("&")
|
||||
.equals("true", ignoreCase = true)
|
||||
|
||||
private fun readerImageUrl(chapterId: Any, page: Int, extractPdf: Boolean): String =
|
||||
"$apiUrl/Reader/image?chapterId=$chapterId&page=$page&extractPdf=$extractPdf&apiKey=$apiKey"
|
||||
|
||||
private fun readerPage(index: Int, chapterId: Any, page: Int = index, extractPdf: Boolean = false): Page {
|
||||
val imageUrl = readerImageUrl(chapterId, page, extractPdf)
|
||||
return Page(index = index, url = imageUrl, imageUrl = imageUrl)
|
||||
}
|
||||
|
||||
override suspend fun getPageList(chapter: SChapter): List<Page> = withContext(Dispatchers.IO) {
|
||||
// Check if this is a reading list item (has readingListId in URL)
|
||||
if (chapter.url.contains("readingListId=")) {
|
||||
@@ -1804,12 +1849,8 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
|
||||
}
|
||||
|
||||
// Generate pages with consistent URL format
|
||||
return@withContext (0 until chapterDetails.pages).map { i ->
|
||||
Page(
|
||||
index = i,
|
||||
imageUrl = "$apiUrl/Reader/image?chapterId=${chapterDetails.id}&page=$i&extractPdf=true&apiKey=$apiKey",
|
||||
)
|
||||
}
|
||||
val extractPdf = shouldExtractPdf(chapterDetails)
|
||||
return@withContext (0 until chapterDetails.pages).map { i -> readerPage(i, chapterDetails.id, extractPdf = extractPdf) }
|
||||
} catch (e: Exception) {
|
||||
Log.e(LOG_TAG, "Error processing reading list item", e)
|
||||
throw e
|
||||
@@ -1835,31 +1876,22 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
|
||||
// Get all pages in this volume
|
||||
val volumeRequest = GET("$apiUrl/Volume/$volumeId", headersBuilder().build())
|
||||
val volume = client.newCall(volumeRequest).execute().parseAs<VolumeDto>()
|
||||
val matchingChapter = volume.chapters.firstOrNull() // or match a chapterId if available
|
||||
?: throw IOException(intl["error_no_chapters_found"])
|
||||
|
||||
val initialPages = (0 until matchingChapter.pages).map { i ->
|
||||
Page(
|
||||
index = i,
|
||||
imageUrl = "$apiUrl/Reader/image?chapterId=${matchingChapter.id}&page=$i&extractPdf=true&apiKey=$apiKey",
|
||||
)
|
||||
if (volume.chapters.isEmpty()) {
|
||||
throw IOException(intl["error_no_chapters_found"])
|
||||
}
|
||||
|
||||
val remainingPages = volume.chapters
|
||||
val volumePages = volume.chapters
|
||||
.sortedBy { it.number.toFloatOrNull() ?: 0f }
|
||||
.runningFold(initialPages.size) { acc, chapter -> acc + chapter.pages }
|
||||
.drop(1)
|
||||
.runningFold(0) { acc, chapter -> acc + chapter.pages }
|
||||
.zip(volume.chapters.sortedBy { it.number.toFloatOrNull() ?: 0f })
|
||||
.flatMap { (offset, chapter) ->
|
||||
.flatMap { (startIndex, chapter) ->
|
||||
val extractPdf = shouldExtractPdf(chapter)
|
||||
(0 until chapter.pages).map { i ->
|
||||
Page(
|
||||
index = offset - chapter.pages + i,
|
||||
imageUrl = "$apiUrl/Reader/image?chapterId=${chapter.id}&page=$i&extractPdf=true&apiKey=$apiKey",
|
||||
)
|
||||
readerPage(startIndex + i, chapter.id, i, extractPdf)
|
||||
}
|
||||
}
|
||||
|
||||
return@withContext (initialPages + remainingPages).toList()
|
||||
return@withContext volumePages
|
||||
} else {
|
||||
// Original chapter handling
|
||||
val chapterId = when {
|
||||
@@ -1868,6 +1900,11 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
|
||||
else -> throw IOException("Invalid chapter URL format")
|
||||
}
|
||||
|
||||
val extractPdf = extractPdfFromChapterUrl(chapter)
|
||||
pageCountFromChapterUrl(chapter)?.let { pageCount ->
|
||||
return@withContext (0 until pageCount).map { i -> readerPage(i, chapterId, extractPdf = extractPdf) }
|
||||
}
|
||||
|
||||
val chapterRequest = GET("$apiUrl/Chapter?chapterId=$chapterId", headersBuilder().build())
|
||||
val response = client.newCall(chapterRequest).execute()
|
||||
|
||||
@@ -1886,23 +1923,19 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
|
||||
throw IOException(errorMessage)
|
||||
}
|
||||
|
||||
return@withContext (0 until chapterDetails.pages).map { i ->
|
||||
Page(
|
||||
index = i,
|
||||
imageUrl = "$apiUrl/Reader/image?chapterId=$chapterId&page=$i&extractPdf=true&apiKey=$apiKey",
|
||||
)
|
||||
}
|
||||
val fetchedExtractPdf = shouldExtractPdf(chapterDetails)
|
||||
return@withContext (0 until chapterDetails.pages).map { i -> readerPage(i, chapterId, extractPdf = fetchedExtractPdf) }
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// Fallback to using the scanlator field if we can't get chapter details
|
||||
Log.e(LOG_TAG, "Error fetching chapter details, using fallback", e)
|
||||
val fallbackPageCount = chapter.scanlator?.replace(" pages", "")?.toIntOrNull() ?: 1
|
||||
return@withContext (0 until fallbackPageCount).map { i ->
|
||||
Page(
|
||||
index = i,
|
||||
imageUrl = "$apiUrl/Reader/image?chapterId=${chapter.url.substringBefore("_")}&page=$i&extractPdf=true&apiKey=$apiKey",
|
||||
)
|
||||
}
|
||||
val fallbackChapterId = chapter.url
|
||||
.substringAfter("/Chapter/", chapter.url)
|
||||
.substringAfter("chapter_", chapter.url)
|
||||
.substringBefore("_")
|
||||
.substringBefore("?")
|
||||
return@withContext (0 until fallbackPageCount).map { i -> readerPage(i, fallbackChapterId, extractPdf = extractPdfFromChapterUrl(chapter)) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1930,8 +1963,7 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
|
||||
MangasPage(mangaList, false)
|
||||
} catch (e: Exception) {
|
||||
Log.e(LOG_TAG, "Error parsing search results", e)
|
||||
// Return empty result instead of throwing exception
|
||||
MangasPage(emptyList(), false)
|
||||
throw IOException(intl["check_version"], e)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2233,7 +2265,7 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
|
||||
return EditTextPreference(context).apply {
|
||||
key = preKey
|
||||
this.title = title
|
||||
val input = preferences.getString(title, null)
|
||||
val input = preferences.getString(preKey, null)
|
||||
this.summary = if (input == null || input.isEmpty()) summary else input
|
||||
this.setDefaultValue(default)
|
||||
dialogTitle = title
|
||||
@@ -2249,7 +2281,7 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
|
||||
val opdsUrlInPref = opdsUrlInPreferences(newValue.toString()) // We don't allow hot have multiple sources with same ip or domain
|
||||
if (opdsUrlInPref.isNotEmpty()) {
|
||||
// TODO("Add option to allow multiple sources with same url at the cost of tracking")
|
||||
preferences.edit().putString(title, "").apply()
|
||||
preferences.edit().putString(preKey, "").apply()
|
||||
|
||||
Toast.makeText(
|
||||
context,
|
||||
@@ -2259,7 +2291,9 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
|
||||
throw OpdsurlExistsInPref(intl["pref_opds_duplicated_source_url"] + opdsUrlInPref)
|
||||
}
|
||||
|
||||
val res = preferences.edit().putString(title, newValue as String).commit()
|
||||
val res = preferences.edit().putString(preKey, newValue as String).commit()
|
||||
jwtToken = ""
|
||||
isLogged = false
|
||||
Toast.makeText(
|
||||
context,
|
||||
intl["restartapp_settings"],
|
||||
@@ -2537,14 +2571,15 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
|
||||
|
||||
private fun doLogin() {
|
||||
Log.d(LOG_TAG, "Attempting login with address: ${address.takeIf { it.isNotEmpty() } ?: "EMPTY"}")
|
||||
if (address.isEmpty()) {
|
||||
val hasStoredLogin = apiUrl.isNotBlank() && getPrefKey().isNotBlank()
|
||||
if (address.isEmpty() && !hasStoredLogin) {
|
||||
Log.e(LOG_TAG, "OPDS URL is empty or null")
|
||||
throw IOException(intl["pref_opds_must_setup_address"])
|
||||
}
|
||||
if (address.split("/opds/").size != 2) {
|
||||
if (address.isNotEmpty() && address.split("/opds/").size != 2) {
|
||||
throw IOException(intl["pref_opds_badformed_url"])
|
||||
}
|
||||
if (jwtToken.isEmpty()) setupLogin()
|
||||
if (jwtToken.isEmpty() && address.isNotEmpty()) setupLogin()
|
||||
Log.v(LOG_TAG, "[Login] Starting login")
|
||||
val request = POST(
|
||||
"$apiUrl/Plugin/authenticate?apiKey=${getPrefKey()}&pluginName=Tachiyomi-Kavita",
|
||||
@@ -2587,10 +2622,8 @@ class Kavita(private val suffix: String = "") : ConfigurableSource, UnmeteredSou
|
||||
try {
|
||||
Log.d(LOG_TAG, "Starting Kavita extension initialization")
|
||||
|
||||
// First attempt login with timeout
|
||||
val loginJob = async { doLogin() }
|
||||
try {
|
||||
loginJob.await()
|
||||
doLogin()
|
||||
} catch (e: Exception) {
|
||||
Log.e(LOG_TAG, "Login failed during initialization", e)
|
||||
initializationError = e
|
||||
|
||||
@@ -106,16 +106,7 @@ class KavitaHelper {
|
||||
ChapterType.Regular -> {
|
||||
val chapterNum = formatChapterNumber(chapter)
|
||||
val volNum = formatVolumeNumber(volume)
|
||||
val cleanedTitle = cleanChapterTitle(
|
||||
titleName,
|
||||
ChapterTitleContext(
|
||||
mangaTitle = mangaTitle,
|
||||
chapterNumber = chapterNum,
|
||||
volumeNumber = volNum,
|
||||
volumeName = volume.name.ifBlank { title },
|
||||
isWebtoon = isWebtoon,
|
||||
),
|
||||
)
|
||||
val cleanedTitle = kavitaDisplayTitle(type, chapter, volume, libraryType, isWebtoon)
|
||||
val finalCleanTitle = cleanedTitle.ifBlank {
|
||||
defaultCleanTitle(type, chapterNum, volNum, isWebtoon)
|
||||
}
|
||||
@@ -160,7 +151,8 @@ class KavitaHelper {
|
||||
pages = volume.pages,
|
||||
fileSize = volume.chapters.flatMap { it.files ?: emptyList() }.sumOf { it.bytes }.toDouble(),
|
||||
volumeNumber = volumeNumber,
|
||||
cleanTitle = when {
|
||||
cleanTitle = kavitaDisplayTitle(type, chapter, volume, libraryType, isWebtoon).ifBlank {
|
||||
when {
|
||||
volume.name.isNotBlank() && !volume.name.matches(Regex("^\\d+$")) -> volume.name
|
||||
volume.name.isNotBlank() && volume.name.matches(Regex("^\\d+$")) -> {
|
||||
// Handle case where volume name is only a number (e.g., "2")
|
||||
@@ -175,6 +167,7 @@ class KavitaHelper {
|
||||
else -> {
|
||||
if (isWebtoon) "Season $volumeNumber" else "Volume $volumeNumber"
|
||||
}
|
||||
}
|
||||
},
|
||||
seriesName = seriesName,
|
||||
libraryName = seriesMap.getSeries(volume.seriesId)?.libraryName ?: "",
|
||||
@@ -202,7 +195,8 @@ class KavitaHelper {
|
||||
pages = chapter.pages,
|
||||
fileSize = chapter.files?.sumOf { it.bytes }?.toDouble() ?: 0.0,
|
||||
volumeNumber = formatVolumeNumber(volume),
|
||||
cleanTitle = when {
|
||||
cleanTitle = kavitaDisplayTitle(type, chapter, volume, libraryType, isWebtoon).ifBlank {
|
||||
when {
|
||||
titleName.isNotBlank() && !titleName.matches(Regex("^\\d+$")) -> titleName
|
||||
titleName.isNotBlank() && titleName.matches(Regex("^\\d+$")) -> {
|
||||
// Handle case where titleName is only a number (e.g., "2")
|
||||
@@ -222,6 +216,7 @@ class KavitaHelper {
|
||||
"Special $num"
|
||||
}
|
||||
else -> "Special"
|
||||
}
|
||||
},
|
||||
seriesName = seriesName,
|
||||
libraryName = seriesMap.getSeries(volume.seriesId)?.libraryName ?: "",
|
||||
@@ -237,15 +232,7 @@ class KavitaHelper {
|
||||
|
||||
ChapterType.Chapter -> {
|
||||
val chapterNum = formatChapterNumber(chapter)
|
||||
val cleanedTitle = cleanChapterTitle(
|
||||
titleName,
|
||||
ChapterTitleContext(
|
||||
mangaTitle = mangaTitle,
|
||||
chapterNumber = chapterNum,
|
||||
volumeName = volume.name,
|
||||
isWebtoon = isWebtoon,
|
||||
),
|
||||
)
|
||||
val cleanedTitle = kavitaDisplayTitle(type, chapter, volume, libraryType, isWebtoon)
|
||||
val finalCleanTitle = cleanedTitle.ifBlank {
|
||||
defaultCleanTitle(type, chapterNum, formatVolumeNumber(volume), isWebtoon)
|
||||
}
|
||||
@@ -280,7 +267,8 @@ class KavitaHelper {
|
||||
pages = chapter.pages,
|
||||
fileSize = chapter.files?.sumOf { it.bytes }?.toDouble() ?: 0.0,
|
||||
volumeNumber = formatVolumeNumber(volume),
|
||||
cleanTitle = when {
|
||||
cleanTitle = kavitaDisplayTitle(type, chapter, volume, libraryType, isWebtoon).ifBlank {
|
||||
when {
|
||||
titleName.isNotBlank() && !titleName.matches(Regex("^\\d+$")) && titleName.any { it.isLetter() } -> titleName
|
||||
titleName.isNotBlank() && titleName.matches(Regex("^\\d+$")) -> {
|
||||
// Handle case where titleName is only a number (e.g., "2")
|
||||
@@ -290,6 +278,7 @@ class KavitaHelper {
|
||||
else -> "Issue #$issueNum"
|
||||
}.ifBlank {
|
||||
defaultCleanTitle(type, issueNum, formatVolumeNumber(volume), isWebtoon)
|
||||
}
|
||||
},
|
||||
seriesName = seriesName,
|
||||
libraryName = seriesMap.getSeries(volume.seriesId)?.libraryName ?: "",
|
||||
@@ -470,6 +459,47 @@ class KavitaHelper {
|
||||
}
|
||||
}
|
||||
|
||||
internal fun kavitaDisplayTitle(
|
||||
type: ChapterType,
|
||||
chapter: ChapterDto,
|
||||
volume: VolumeDto,
|
||||
libraryType: LibraryTypeEnum? = null,
|
||||
isWebtoon: Boolean = false,
|
||||
): String {
|
||||
val titleName = chapter.titleName?.trim().orEmpty()
|
||||
val title = chapter.title.trim()
|
||||
val range = chapter.range.trim()
|
||||
val volumeName = volume.name.trim()
|
||||
val usableVolumeName = volumeName.takeUnless { it.isBlank() || it == KavitaConstants.UNNUMBERED_VOLUME_STR }
|
||||
|
||||
fun fallbackChapterLabel(): String {
|
||||
val number = range.takeUnless { it.isBlank() || it == KavitaConstants.UNNUMBERED_VOLUME_STR }
|
||||
?: formatChapterNumber(chapter)
|
||||
return when (libraryType) {
|
||||
LibraryTypeEnum.Comic, LibraryTypeEnum.ComicVine -> "Issue #$number"
|
||||
LibraryTypeEnum.Book, LibraryTypeEnum.LightNovel -> "Book $number"
|
||||
else -> if (isWebtoon) "Episode $number" else "Chapter $number"
|
||||
}
|
||||
}
|
||||
|
||||
fun joinTitle(left: String?, right: String?): String =
|
||||
listOfNotNull(
|
||||
left?.takeIf { it.isNotBlank() },
|
||||
right?.takeIf { it.isNotBlank() && it != left },
|
||||
).joinToString(" - ")
|
||||
|
||||
return when (type) {
|
||||
ChapterType.SingleFileVolume -> joinTitle(usableVolumeName, titleName)
|
||||
.ifBlank { titleName }
|
||||
.ifBlank { usableVolumeName.orEmpty() }
|
||||
.ifBlank { title }
|
||||
ChapterType.Special -> title.ifBlank { titleName }.ifBlank { range }
|
||||
ChapterType.Regular -> joinTitle(usableVolumeName, titleName.ifBlank { fallbackChapterLabel() })
|
||||
ChapterType.Chapter -> titleName.ifBlank { fallbackChapterLabel() }
|
||||
ChapterType.Issue -> titleName.ifBlank { fallbackChapterLabel() }
|
||||
}
|
||||
}
|
||||
|
||||
internal fun cleanChapterTitle(
|
||||
originalTitle: String,
|
||||
context: ChapterTitleContext? = null,
|
||||
|
||||
Reference in New Issue
Block a user