Compare commits

..

8 Commits

Author SHA1 Message Date
Koitharu
8d7f44d2da Fix DateTimeAgo formatting
(cherry picked from commit 5f0514638a)
2025-02-24 18:09:03 +02:00
Koitharu
930d4dfd83 Fix checking for removed manga updates (close #1064)
(cherry picked from commit 1b8d35d424)
2025-02-24 18:07:49 +02:00
Koitharu
290cb652ee Update parsers 2025-02-24 18:05:51 +02:00
Koitharu
8a26587250 Database migration for downgrade from 24 to 23 (#1269, #1270) 2025-02-09 10:15:24 +02:00
Koitharu
bb68869fe1 Fix crashes 2025-02-09 10:11:24 +02:00
Koitharu
e60ca7115a Update parsers 2025-02-09 10:01:28 +02:00
Koitharu
e03a200c32 Fix build 2025-01-25 12:51:13 +02:00
Koitharu
8713faa487 Update parsers 2025-01-25 12:14:01 +02:00
13 changed files with 64 additions and 199 deletions

View File

@@ -1,114 +0,0 @@
name: Build automatic release
on:
schedule:
- cron: '0 0 * * *'
workflow_dispatch:
jobs:
check:
runs-on: ubuntu-24.04
outputs:
should_build: ${{ steps.check-updates.outputs.has_changes }}
steps:
- name: Check for updates 🌏
id: check-updates
run: |
last_run=$(curl -s "https://api.github.com/repos/${{ github.repository }}/releases/latest" | jq -r '.created_at')
kotatsu_updated=$(curl -s "https://api.github.com/repos/KotatsuApp/Kotatsu/commits?since=$last_run" | jq '. | length')
parsers_updated=$(curl -s "https://api.github.com/repos/KotatsuApp/kotatsu-parsers/commits?since=$last_run" | jq '. | length')
if [ "$kotatsu_updated" -gt "0" ] || [ "$parsers_updated" -gt "0" ]; then
echo "has_changes=true" >> $GITHUB_OUTPUT
else
echo "has_changes=false" >> $GITHUB_OUTPUT
fi
build:
needs: check
if: needs.check.outputs.should_build == 'true'
runs-on: ubuntu-24.04
outputs:
new_tag: ${{ steps.tagger.outputs.new_tag }}
steps:
- uses: actions/checkout@v3
with:
ref: autobuild
- uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
cache: 'gradle'
- name: Setup Android SDK 💻
uses: android-actions/setup-android@v3
- name: Grant permissions 💻
run: chmod a+x gradlew
- name: Generate build number 📆
id: tagger
run: |
echo "new_tag=$(./gradlew -q versionInfo -DbuildNumberIncrement=true)" >> $GITHUB_OUTPUT
echo "formatted_date=$(date +'%Y/%m/%d')" >> $GITHUB_OUTPUT
- name: Decode Keystore
id: decode_keystore
uses: timheuer/base64-to-file@v1
with:
fileName: 'keystore/kotatsu.jks'
encodedString: ${{ secrets.ANDROID_SIGNING_KEY }}
- name: Building new APK 💻
run: >-
./gradlew assembleRelease
-DparsersVersionOverride=$(curl -s https://api.github.com/repos/kotatsuapp/kotatsu-parsers/commits/master -H "Accept: application/vnd.github.sha" | cut -c -10)
- name: Prepare to Upload 🌏
run: |
mv ${{steps.sign_app.outputs.signedFile}} app/build/outputs/apk/release/release.apk
echo "SIGNED_APK=app/build/outputs/apk/release/release.apk" >> $GITHUB_ENV
- name: Get latest changes 📑
id: changelog
run: |
CHANGELOG=$(cat CHANGELOG.txt)
echo "content<<EOF" >> $GITHUB_OUTPUT
echo "$CHANGELOG" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
- name: Create new GH Release + Uploading 🌏
uses: softprops/action-gh-release@v2
with:
tag_name: v${{ steps.tagger.outputs.new_tag }}
name: "Build ${{ steps.tagger.outputs.new_tag }}"
body: |
Automated build generated on ${{ steps.tagger.outputs.formatted_date }}
${{ steps.changelog.outputs.content }}
files: ${{ env.SIGNED_APK }}
prerelease: false
update:
needs: build
if: needs.check.outputs.should_build == 'true'
runs-on: ubuntu-24.04
permissions:
contents: write
steps:
- uses: actions/checkout@v3
with:
ref: autobuild
fetch-depth: 0
- name: Commit 🌏
run: |
git config --local user.email "autorelease@users.noreply.github.com"
git config --local user.name "autorelease"
if [[ -n $(git status -s) ]]; then
git add README.md
git commit -m "Automatic release v${{ needs.build.outputs.new_tag }}"
git push origin autobuild
else
echo "No changes to push!"
fi

View File

@@ -9,24 +9,17 @@ plugins {
id 'dagger.hilt.android.plugin'
}
def Properties versionProps = getVersionProps()
android {
compileSdk = 35
buildToolsVersion = '35.0.0'
namespace = 'org.koitharu.kotatsu'
defaultConfig {
def code = versionProps['code'].toInteger()
def base = versionProps['base'].trim()
def build = versionProps['build'].toInteger()
def variant = versionProps['variant'].trim()
applicationId 'org.koitharu.kotatsu'
minSdk = 21
targetSdk = 35
versionCode = code * 1000 + build
versionName = base + (build == 0 ? '' : '.' + build) + (variant == '' ? '' : '-') + variant
versionCode = 703
versionName = '7.7.11'
generatedDensities = []
testInstrumentationRunner 'org.koitharu.kotatsu.HiltTestRunner'
ksp {
@@ -37,22 +30,6 @@ android {
generateLocaleConfig true
}
}
signingConfigs {
release {
def tmpFilePath = System.getProperty("user.home") + "/work/_temp/keystore/"
def allFilesFromDir = new File(tmpFilePath).listFiles()
if (allFilesFromDir != null) {
def keystoreFile = allFilesFromDir.first()
keystoreFile.renameTo("keystore/kotatsu.jks")
}
storeFile = file("keystore/kotatsu.jks")
storePassword System.getenv("SIGNING_STORE_PASSWORD")
keyAlias System.getenv("SIGNING_KEY_ALIAS")
keyPassword System.getenv("SIGNING_KEY_PASSWORD")
}
}
buildTypes {
debug {
applicationIdSuffix = '.debug'
@@ -61,7 +38,6 @@ android {
minifyEnabled true
shrinkResources true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.release
}
nightly {
initWith release
@@ -218,22 +194,3 @@ dependencies {
androidTestImplementation libs.hilt.android.testing
kaptAndroidTest libs.hilt.android.compiler
}
tasks.register('versionInfo') {
def base = versionProps['base'].trim()
def build = versionProps['build'].toInteger()
def variant = versionProps['variant'].trim()
println base + (build == 0 ? '' : '.' + build) + (variant == '' ? '' : '-') + variant
}
def getVersionProps() {
def versionPropsFile = file('version.properties')
def Properties versionProps = new Properties()
versionProps.load(new FileInputStream(versionPropsFile))
if (System.getProperty('buildNumberIncrement') == 'true') {
def code = versionProps['build'].toInteger() + 1
versionProps['build'] = code.toString()
versionProps.store(versionPropsFile.newWriter(), null)
}
return versionProps
}

View File

@@ -10,6 +10,7 @@ import org.koitharu.kotatsu.core.ui.list.fastscroll.FastScroller
import org.koitharu.kotatsu.list.ui.adapter.ListHeaderClickListener
import org.koitharu.kotatsu.list.ui.adapter.ListItemType
import org.koitharu.kotatsu.list.ui.adapter.emptyStateListAD
import org.koitharu.kotatsu.list.ui.adapter.errorStateListAD
import org.koitharu.kotatsu.list.ui.adapter.listHeaderAD
import org.koitharu.kotatsu.list.ui.adapter.loadingFooterAD
import org.koitharu.kotatsu.list.ui.adapter.loadingStateAD
@@ -25,6 +26,7 @@ class BookmarksAdapter(
init {
addDelegate(ListItemType.PAGE_THUMB, bookmarkLargeAD(coil, lifecycleOwner, clickListener))
addDelegate(ListItemType.HEADER, listHeaderAD(headerClickListener))
addDelegate(ListItemType.STATE_ERROR, errorStateListAD(null))
addDelegate(ListItemType.FOOTER_LOADING, loadingFooterAD())
addDelegate(ListItemType.STATE_LOADING, loadingStateAD())
addDelegate(ListItemType.STATE_EMPTY, emptyStateListAD(coil, lifecycleOwner, null))

View File

@@ -36,6 +36,7 @@ import org.koitharu.kotatsu.core.db.migrations.Migration1To2
import org.koitharu.kotatsu.core.db.migrations.Migration20To21
import org.koitharu.kotatsu.core.db.migrations.Migration21To22
import org.koitharu.kotatsu.core.db.migrations.Migration22To23
import org.koitharu.kotatsu.core.db.migrations.Migration24To23
import org.koitharu.kotatsu.core.db.migrations.Migration2To3
import org.koitharu.kotatsu.core.db.migrations.Migration3To4
import org.koitharu.kotatsu.core.db.migrations.Migration4To5
@@ -128,6 +129,7 @@ fun getDatabaseMigrations(context: Context): Array<Migration> = arrayOf(
Migration20To21(),
Migration21To22(),
Migration22To23(),
Migration24To23(),
)
fun MangaDatabase(context: Context): MangaDatabase = Room

View File

@@ -0,0 +1,11 @@
package org.koitharu.kotatsu.core.db.migrations
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
class Migration24To23 : Migration(24, 23) {
override fun migrate(db: SupportSQLiteDatabase) {
db.execSQL("DROP TABLE IF EXISTS `chapters`")
}
}

View File

@@ -1,17 +1,18 @@
package org.koitharu.kotatsu.core.ui.model
import android.content.res.Resources
import android.content.Context
import android.text.format.DateUtils
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.util.ext.toMillis
import java.time.LocalDate
import java.time.format.DateTimeFormatter
sealed class DateTimeAgo {
abstract fun format(resources: Resources): String
abstract fun format(context: Context): String
object JustNow : DateTimeAgo() {
override fun format(resources: Resources): String {
return resources.getString(R.string.just_now)
override fun format(context: Context): String {
return context.getString(R.string.just_now)
}
override fun toString() = "just_now"
@@ -20,24 +21,32 @@ sealed class DateTimeAgo {
}
data class MinutesAgo(val minutes: Int) : DateTimeAgo() {
override fun format(resources: Resources): String {
return resources.getQuantityString(R.plurals.minutes_ago, minutes, minutes)
override fun format(context: Context): String {
return context.resources.getQuantityString(
R.plurals.minutes_ago,
minutes,
minutes,
)
}
override fun toString() = "minutes_ago_$minutes"
}
data class HoursAgo(val hours: Int) : DateTimeAgo() {
override fun format(resources: Resources): String {
return resources.getQuantityString(R.plurals.hours_ago, hours, hours)
override fun format(context: Context): String {
return context.resources.getQuantityString(
R.plurals.hours_ago,
hours,
hours,
)
}
override fun toString() = "hours_ago_$hours"
}
object Today : DateTimeAgo() {
override fun format(resources: Resources): String {
return resources.getString(R.string.today)
override fun format(context: Context): String {
return context.getString(R.string.today)
}
override fun toString() = "today"
@@ -46,8 +55,8 @@ sealed class DateTimeAgo {
}
object Yesterday : DateTimeAgo() {
override fun format(resources: Resources): String {
return resources.getString(R.string.yesterday)
override fun format(context: Context): String {
return context.getString(R.string.yesterday)
}
override fun toString() = "yesterday"
@@ -56,44 +65,46 @@ sealed class DateTimeAgo {
}
data class DaysAgo(val days: Int) : DateTimeAgo() {
override fun format(resources: Resources): String {
return resources.getQuantityString(R.plurals.days_ago, days, days)
override fun format(context: Context): String {
return context.resources.getQuantityString(R.plurals.days_ago, days, days)
}
override fun toString() = "days_ago_$days"
}
data class MonthsAgo(val months: Int) : DateTimeAgo() {
override fun format(resources: Resources): String {
override fun format(context: Context): String {
return if (months == 0) {
resources.getString(R.string.this_month)
context.getString(R.string.this_month)
} else {
resources.getQuantityString(R.plurals.months_ago, months, months)
context.resources.getQuantityString(
R.plurals.months_ago,
months,
months,
)
}
}
}
data class Absolute(private val date: LocalDate) : DateTimeAgo() {
override fun format(resources: Resources): String {
override fun format(context: Context): String {
return if (date == EPOCH_DATE) {
resources.getString(R.string.unknown)
context.getString(R.string.unknown)
} else {
date.format(formatter)
DateUtils.formatDateTime(context, date.toMillis(), DateUtils.FORMAT_SHOW_DATE)
}
}
override fun toString() = "abs_${date.toEpochDay()}"
companion object {
// TODO: Use Java 9's LocalDate.EPOCH.
private val EPOCH_DATE = LocalDate.of(1970, 1, 1)
private val formatter = DateTimeFormatter.ofPattern("d MMMM")
private companion object {
val EPOCH_DATE: LocalDate = LocalDate.of(1970, 1, 1)
}
}
object LongAgo : DateTimeAgo() {
override fun format(resources: Resources): String {
return resources.getString(R.string.long_ago)
override fun format(context: Context): String {
return context.getString(R.string.long_ago)
}
override fun toString() = "long_ago"

View File

@@ -35,6 +35,7 @@ fun calculateTimeAgo(instant: Instant, showMonths: Boolean = false): DateTimeAgo
}
}
@Suppress("KotlinConstantConditions")
fun Long.toInstantOrNull() = if (this == 0L) null else Instant.ofEpochMilli(this)
fun Resources.formatDurationShort(millis: Long): String? {
@@ -50,3 +51,5 @@ fun Resources.formatDurationShort(millis: Long): String? {
else -> getString(R.string.seconds_short, seconds)
}
}
fun LocalDate.toMillis(): Long = atStartOfDay(ZoneId.systemDefault()).toInstant().toEpochMilli()

View File

@@ -36,7 +36,7 @@ data class ListHeader private constructor(
fun getText(context: Context): CharSequence? = when (textRaw) {
is CharSequence -> textRaw
is Int -> if (textRaw != 0) context.getString(textRaw) else null
is DateTimeAgo -> textRaw.format(context.resources)
is DateTimeAgo -> textRaw.format(context)
else -> null
}

View File

@@ -62,7 +62,7 @@ class LocalMangaParser(private val uri: Uri) {
mangaInfo.copy(
source = LocalMangaSource,
url = rootFile.toUri().toString(),
coverUrl = coverEntry?.let { uri.child(it, resolve = true).toString() }.orEmpty(),
coverUrl = coverEntry?.let { uri.child(it, resolve = true).toString() },
largeCoverUrl = null,
chapters = if (withDetails) {
mangaInfo.chapters?.mapNotNull { c ->
@@ -92,9 +92,7 @@ class LocalMangaParser(private val uri: Uri) {
url = rootFile.toUri().toString(),
publicUrl = rootFile.toUri().toString(),
source = LocalMangaSource,
coverUrl = coverEntry?.let {
uri.child(it, resolve = true).toString()
}.orEmpty(),
coverUrl = coverEntry?.let { uri.child(it, resolve = true).toString() },
chapters = if (withDetails) {
val chapters = fileSystem.listRecursively(rootPath)
.mapNotNullTo(HashSet()) { path ->

View File

@@ -37,7 +37,7 @@ class MangaStatsSheet : BaseAdaptiveSheet<SheetStatsMangaBinding>(), View.OnClic
binding.chartView.barColor = KotatsuColors.ofManga(binding.root.context, viewModel.manga)
viewModel.stats.observe(viewLifecycleOwner, ::onStatsChanged)
viewModel.startDate.observe(viewLifecycleOwner) {
binding.textViewStart.textAndVisible = it?.format(resources)
binding.textViewStart.textAndVisible = it?.format(binding.root.context)
}
viewModel.totalPagesRead.observe(viewLifecycleOwner) {
binding.textViewPages.text = getString(R.string.pages_read_s, it.format())

View File

@@ -67,7 +67,7 @@ abstract class TracksDao : MangaQueryBuilder.ConditionCallback {
@Query("DELETE FROM tracks WHERE manga_id = :mangaId")
abstract suspend fun delete(mangaId: Long)
@Query("DELETE FROM tracks WHERE manga_id NOT IN (SELECT manga_id FROM history UNION SELECT manga_id FROM favourites WHERE category_id IN (SELECT category_id FROM favourite_categories WHERE track = 1))")
@Query("DELETE FROM tracks WHERE manga_id NOT IN (SELECT manga_id FROM history WHERE history.deleted_at = 0 UNION SELECT manga_id FROM favourites WHERE favourites.deleted_at = 0 AND category_id IN (SELECT category_id FROM favourite_categories WHERE favourite_categories.deleted_at = 0 AND track = 1))")
abstract suspend fun gc()
@Upsert

View File

@@ -1,5 +0,0 @@
#Wed Jan 22 19:14:17 EET 2025
code=1
build=8
variant=
base=7.7

View File

@@ -31,7 +31,7 @@ material = "1.12.0"
moshi = "1.15.2"
okhttp = "4.12.0"
okio = "3.9.1"
parsers = "51ed1b2db8"
parsers = "1.6"
preference = "1.2.1"
recyclerview = "1.3.2"
room = "2.6.1"