From 34ac39e7e5e4d8846a55b396ac02d0758f1c0561 Mon Sep 17 00:00:00 2001 From: arkon Date: Sat, 13 Aug 2022 10:13:37 -0400 Subject: [PATCH 01/26] Update AGP/Gradle --- .github/runner-files/ci-gradle.properties | 5 - .github/workflows/build_pull_request.yml | 5 - .github/workflows/build_push.yml | 5 - gradle.properties | 2 +- gradle/androidx.versions.toml | 2 +- gradle/wrapper/gradle-wrapper.jar | Bin 59203 -> 60756 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 275 +++++++++++++--------- gradlew.bat | 14 +- 9 files changed, 176 insertions(+), 134 deletions(-) delete mode 100644 .github/runner-files/ci-gradle.properties diff --git a/.github/runner-files/ci-gradle.properties b/.github/runner-files/ci-gradle.properties deleted file mode 100644 index 3b340e9573..0000000000 --- a/.github/runner-files/ci-gradle.properties +++ /dev/null @@ -1,5 +0,0 @@ -org.gradle.daemon=false -org.gradle.jvmargs=-Xmx5120m -org.gradle.workers.max=2 - -kotlin.incremental=false \ No newline at end of file diff --git a/.github/workflows/build_pull_request.yml b/.github/workflows/build_pull_request.yml index 50c6fc2cfe..be8d0d34a0 100644 --- a/.github/workflows/build_pull_request.yml +++ b/.github/workflows/build_pull_request.yml @@ -29,11 +29,6 @@ jobs: java-version: 11 distribution: adopt - - name: Copy CI gradle.properties - run: | - mkdir -p ~/.gradle - cp .github/runner-files/ci-gradle.properties ~/.gradle/gradle.properties - - name: Build app uses: gradle/gradle-command-action@v2 with: diff --git a/.github/workflows/build_push.yml b/.github/workflows/build_push.yml index ee2efbd963..843673ba06 100644 --- a/.github/workflows/build_push.yml +++ b/.github/workflows/build_push.yml @@ -30,11 +30,6 @@ jobs: java-version: 11 distribution: adopt - - name: Copy CI gradle.properties - run: | - mkdir -p ~/.gradle - cp .github/runner-files/ci-gradle.properties ~/.gradle/gradle.properties - - name: Build app uses: gradle/gradle-command-action@v2 with: diff --git a/gradle.properties b/gradle.properties index ff77efb1c0..dda7175753 100644 --- a/gradle.properties +++ b/gradle.properties @@ -11,7 +11,7 @@ # The setting is particularly useful for tweaking memory settings. # Default value: -Xmx10248m -XX:MaxPermSize=256m # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 -org.gradle.jvmargs=-Xmx4096m +org.gradle.jvmargs=-Xmx5120m # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. More details, visit diff --git a/gradle/androidx.versions.toml b/gradle/androidx.versions.toml index d5ab4b6c42..867b9d2b43 100644 --- a/gradle/androidx.versions.toml +++ b/gradle/androidx.versions.toml @@ -1,5 +1,5 @@ [versions] -agp_version = "7.1.3" +agp_version = "7.2.2" lifecycle_version = "2.5.0" [libraries] diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index e708b1c023ec8b20f512888fe07c5bd3ff77bb8f..249e5832f090a2944b7473328c07c9755baa3196 100644 GIT binary patch delta 21931 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 delta 20228 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 \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` +APP_BASE_NAME=${0##*/} # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" +MAX_FD=maximum warn () { echo "$*" -} +} >&2 die () { echo echo "$*" echo exit 1 -} +} >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar @@ -87,9 +121,9 @@ CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACMD=$JAVA_HOME/jre/sh/java else - JAVACMD="$JAVA_HOME/bin/java" + JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME @@ -98,7 +132,7 @@ Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else - JAVACMD="java" + JAVACMD=java which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the @@ -106,80 +140,101 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi -fi - -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi - -# For Cygwin or MSYS, switch paths to Windows format before running java -if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi - # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" - fi - i=`expr $i + 1` - done - case $i in - 0) set -- ;; - 1) set -- "$args0" ;; - 2) set -- "$args0" "$args1" ;; - 3) set -- "$args0" "$args1" "$args2" ;; - 4) set -- "$args0" "$args1" "$args2" "$args3" ;; - 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" esac fi -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=`save "$@"` +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index 107acd32c4..f127cfd49d 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -14,7 +14,7 @@ @rem limitations under the License. @rem -@if "%DEBUG%" == "" @echo off +@if "%DEBUG%"=="" @echo off @rem ########################################################################## @rem @rem Gradle startup script for Windows @@ -25,7 +25,7 @@ if "%OS%"=="Windows_NT" setlocal set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. +if "%DIRNAME%"=="" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @@ -40,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto execute +if %ERRORLEVEL% equ 0 goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. @@ -75,13 +75,15 @@ set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar :end @rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd +if %ERRORLEVEL% equ 0 goto mainEnd :fail rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% :mainEnd if "%OS%"=="Windows_NT" endlocal From 4a71022a609711378df48b62725ee863353193c8 Mon Sep 17 00:00:00 2001 From: arkon Date: Sat, 13 Aug 2022 12:37:02 -0400 Subject: [PATCH 02/26] Update chapter recognition and related tests Includes 3e07100dc2725cb2d42050571232dd5d485b4de5 Co-authored-by: Saud-97 --- .github/workflows/build_pull_request.yml | 10 +- .github/workflows/build_push.yml | 14 +- .github/workflows/cancel_pull_request.yml | 16 - app/build.gradle.kts | 12 +- .../util/chapter/ChapterRecognition.kt | 4 +- .../CustomRobolectricGradleTestRunner.kt | 12 - .../test/java/eu/kanade/tachiyomi/TestApp.kt | 8 - .../tachiyomi/data/backup/BackupTest.kt | 377 ------------- .../tachiyomi/data/database/CategoryTest.kt | 109 ---- .../data/database/ChapterRecognitionTest.kt | 497 ------------------ .../data/library/LibraryUpdateServiceTest.kt | 136 ----- .../util/chapter/ChapterRecognitionTest.kt | 275 ++++++++++ gradle/libs.versions.toml | 9 +- 13 files changed, 299 insertions(+), 1180 deletions(-) delete mode 100644 .github/workflows/cancel_pull_request.yml delete mode 100644 app/src/test/java/eu/kanade/tachiyomi/CustomRobolectricGradleTestRunner.kt delete mode 100644 app/src/test/java/eu/kanade/tachiyomi/TestApp.kt delete mode 100644 app/src/test/java/eu/kanade/tachiyomi/data/backup/BackupTest.kt delete mode 100644 app/src/test/java/eu/kanade/tachiyomi/data/database/CategoryTest.kt delete mode 100644 app/src/test/java/eu/kanade/tachiyomi/data/database/ChapterRecognitionTest.kt delete mode 100644 app/src/test/java/eu/kanade/tachiyomi/data/library/LibraryUpdateServiceTest.kt create mode 100644 app/src/test/java/eu/kanade/tachiyomi/util/chapter/ChapterRecognitionTest.kt diff --git a/.github/workflows/build_pull_request.yml b/.github/workflows/build_pull_request.yml index be8d0d34a0..ebda14e825 100644 --- a/.github/workflows/build_pull_request.yml +++ b/.github/workflows/build_pull_request.yml @@ -5,6 +5,10 @@ on: - '**.md' - 'app/src/main/res/**/strings.xml' +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number }} + cancel-in-progress: true + permissions: contents: read @@ -21,7 +25,7 @@ jobs: uses: gradle/wrapper-validation-action@v1 - name: Dependency Review - uses: actions/dependency-review-action@v1 + uses: actions/dependency-review-action@v2 - name: Set up JDK 11 uses: actions/setup-java@v3 @@ -29,7 +33,7 @@ jobs: java-version: 11 distribution: adopt - - name: Build app + - name: Build app and run unit tests uses: gradle/gradle-command-action@v2 with: - arguments: assembleStandardRelease + arguments: assembleStandardRelease testStandardReleaseUnitTest \ No newline at end of file diff --git a/.github/workflows/build_push.yml b/.github/workflows/build_push.yml index 843673ba06..192116fc87 100644 --- a/.github/workflows/build_push.yml +++ b/.github/workflows/build_push.yml @@ -6,18 +6,16 @@ on: tags: - v* +concurrency: + group: ${{ github.workflow }} + cancel-in-progress: true + jobs: build: name: Build app runs-on: ubuntu-latest steps: - - name: Cancel previous runs - uses: styfle/cancel-workflow-action@0.9.1 - with: - access_token: ${{ github.token }} - all_but_latest: true - - name: Clone repo uses: actions/checkout@v3 @@ -30,10 +28,10 @@ jobs: java-version: 11 distribution: adopt - - name: Build app + - name: Build app and run unit tests uses: gradle/gradle-command-action@v2 with: - arguments: assembleStandardRelease + arguments: assembleStandardRelease testStandardReleaseUnitTest # Sign APK and create release for tags diff --git a/.github/workflows/cancel_pull_request.yml b/.github/workflows/cancel_pull_request.yml deleted file mode 100644 index 82d572614c..0000000000 --- a/.github/workflows/cancel_pull_request.yml +++ /dev/null @@ -1,16 +0,0 @@ -name: Cancel old pull request workflows - -on: - workflow_run: - workflows: ["PR build check"] - types: - - requested - -jobs: - cancel: - runs-on: ubuntu-latest - steps: - - uses: styfle/cancel-workflow-action@0.9.1 - with: - all_but_latest: true - workflow_id: ${{ github.event.workflow.id }} diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 809186baa4..4ab6582389 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,3 +1,4 @@ +import org.gradle.api.tasks.testing.logging.TestLogEvent import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { @@ -242,16 +243,19 @@ dependencies { // Tests testImplementation(libs.junit) - testImplementation(libs.assertj.core) - testImplementation(libs.mockito.core) - - testImplementation(libs.bundles.robolectric) // For detecting memory leaks; see https://square.github.io/leakcanary/ // debugImplementation(libs.leakcanary.android) } tasks { + withType { + useJUnitPlatform() + testLogging { + events(TestLogEvent.PASSED, TestLogEvent.SKIPPED, TestLogEvent.FAILED) + } + } + // See https://kotlinlang.org/docs/reference/experimental.html#experimental-status-of-experimental-api(-markers) withType { kotlinOptions.freeCompilerArgs += listOf( diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterRecognition.kt b/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterRecognition.kt index dd44f42be8..f9dfb076e7 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterRecognition.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterRecognition.kt @@ -46,8 +46,8 @@ object ChapterRecognition { // Get chapter title with lower case var name = chapter.name.lowercase() - // Remove comma's from chapter. - name = name.replace(',', '.') + // Remove comma's or hyphens. + name = name.replace(',', '.').replace('-', '.') // Remove unwanted white spaces. unwantedWhiteSpace.findAll(name).let { diff --git a/app/src/test/java/eu/kanade/tachiyomi/CustomRobolectricGradleTestRunner.kt b/app/src/test/java/eu/kanade/tachiyomi/CustomRobolectricGradleTestRunner.kt deleted file mode 100644 index 3b334be41b..0000000000 --- a/app/src/test/java/eu/kanade/tachiyomi/CustomRobolectricGradleTestRunner.kt +++ /dev/null @@ -1,12 +0,0 @@ -package eu.kanade.tachiyomi - -import org.robolectric.RobolectricTestRunner -import org.robolectric.annotation.Config -import org.robolectric.manifest.AndroidManifest - -class CustomRobolectricGradleTestRunner(klass: Class<*>) : RobolectricTestRunner(klass) { - - override fun getAppManifest(config: Config): AndroidManifest { - return super.getAppManifest(config).apply { packageName = "eu.kanade.tachiyomi" } - } -} diff --git a/app/src/test/java/eu/kanade/tachiyomi/TestApp.kt b/app/src/test/java/eu/kanade/tachiyomi/TestApp.kt deleted file mode 100644 index 35a74ceb22..0000000000 --- a/app/src/test/java/eu/kanade/tachiyomi/TestApp.kt +++ /dev/null @@ -1,8 +0,0 @@ -package eu.kanade.tachiyomi - -open class TestApp : App() { - - override fun setupAcra() { - // Do nothing - } -} diff --git a/app/src/test/java/eu/kanade/tachiyomi/data/backup/BackupTest.kt b/app/src/test/java/eu/kanade/tachiyomi/data/backup/BackupTest.kt deleted file mode 100644 index 75fe1320c9..0000000000 --- a/app/src/test/java/eu/kanade/tachiyomi/data/backup/BackupTest.kt +++ /dev/null @@ -1,377 +0,0 @@ -package eu.kanade.tachiyomi.data.backup - -import android.app.Application -import android.content.Context -import android.os.Build -import eu.kanade.tachiyomi.BuildConfig -import eu.kanade.tachiyomi.CustomRobolectricGradleTestRunner -import eu.kanade.tachiyomi.data.backup.legacy.LegacyBackupManager -import eu.kanade.tachiyomi.data.backup.legacy.models.Backup -import eu.kanade.tachiyomi.data.backup.legacy.models.DHistory -import eu.kanade.tachiyomi.data.database.DatabaseHelper -import eu.kanade.tachiyomi.data.database.models.Category -import eu.kanade.tachiyomi.data.database.models.Chapter -import eu.kanade.tachiyomi.data.database.models.ChapterImpl -import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.data.database.models.MangaImpl -import eu.kanade.tachiyomi.data.database.models.Track -import eu.kanade.tachiyomi.data.database.models.TrackImpl -import eu.kanade.tachiyomi.source.SourceManager -import eu.kanade.tachiyomi.source.online.HttpSource -import eu.kanade.tachiyomi.ui.reader.setting.OrientationType -import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType -import kotlinx.coroutines.runBlocking -import kotlinx.serialization.decodeFromString -import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.buildJsonObject -import org.assertj.core.api.Assertions.assertThat -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.Mockito.RETURNS_DEEP_STUBS -import org.mockito.Mockito.anyLong -import org.mockito.Mockito.mock -import org.mockito.Mockito.`when` -import org.robolectric.RuntimeEnvironment -import org.robolectric.annotation.Config -import rx.Observable -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.InjektModule -import uy.kohesive.injekt.api.InjektRegistrar -import uy.kohesive.injekt.api.addSingleton - -/** - * Test class for the [LegacyBackupManager]. - * Note that this does not include the backup create/restore services. - */ -@Config(constants = BuildConfig::class, sdk = [Build.VERSION_CODES.M]) -@RunWith(CustomRobolectricGradleTestRunner::class) -class BackupTest { - // Create root object - var root = Backup() - - // Create information object - var information = buildJsonObject {} - - lateinit var app: Application - lateinit var context: Context - lateinit var source: HttpSource - - lateinit var legacyBackupManager: LegacyBackupManager - - lateinit var db: DatabaseHelper - - @Before - fun setup() { - app = RuntimeEnvironment.application - context = app.applicationContext - legacyBackupManager = LegacyBackupManager(context, 2) - db = legacyBackupManager.databaseHelper - - // Mock the source manager - val module = object : InjektModule { - override fun InjektRegistrar.registerInjectables() { - addSingleton(mock(SourceManager::class.java, RETURNS_DEEP_STUBS)) - } - } - Injekt.importModule(module) - - source = mock(HttpSource::class.java) - `when`(legacyBackupManager.sourceManager.get(anyLong())).thenReturn(source) - } - - /** - * Test that checks if no crashes when no categories in library. - */ - @Test - fun testRestoreEmptyCategory() { - // Restore Json - legacyBackupManager.restoreCategories(root.categories ?: emptyList()) - - // Check if empty - val dbCats = db.getCategories().executeAsBlocking() - assertThat(dbCats).isEmpty() - } - - /** - * Test to check if single category gets restored - */ - @Test - fun testRestoreSingleCategory() { - // Create category and add to json - val category = addSingleCategory("category") - - // Restore Json - legacyBackupManager.restoreCategories(root.categories ?: emptyList()) - - // Check if successful - val dbCats = legacyBackupManager.databaseHelper.getCategories().executeAsBlocking() - assertThat(dbCats).hasSize(1) - assertThat(dbCats[0].name).isEqualTo(category.name) - } - - /** - * Test to check if multiple categories get restored. - */ - @Test - fun testRestoreMultipleCategories() { - // Create category and add to json - val category = addSingleCategory("category") - val category2 = addSingleCategory("category2") - val category3 = addSingleCategory("category3") - val category4 = addSingleCategory("category4") - val category5 = addSingleCategory("category5") - - // Insert category to test if no duplicates on restore. - db.insertCategory(category).executeAsBlocking() - - // Restore Json - legacyBackupManager.restoreCategories(root.categories ?: emptyList()) - - // Check if successful - val dbCats = legacyBackupManager.databaseHelper.getCategories().executeAsBlocking() - assertThat(dbCats).hasSize(5) - assertThat(dbCats[0].name).isEqualTo(category.name) - assertThat(dbCats[1].name).isEqualTo(category2.name) - assertThat(dbCats[2].name).isEqualTo(category3.name) - assertThat(dbCats[3].name).isEqualTo(category4.name) - assertThat(dbCats[4].name).isEqualTo(category5.name) - } - - /** - * Test if restore of manga is successful - */ - @Test - fun testRestoreManga() { - // Add manga to database - val manga = getSingleManga("One Piece") - manga.readingModeType = ReadingModeType.VERTICAL.flagValue - manga.orientationType = OrientationType.PORTRAIT.flagValue - manga.id = db.insertManga(manga).executeAsBlocking().insertedId() - - var favoriteManga = legacyBackupManager.databaseHelper.getFavoriteMangas().executeAsBlocking() - assertThat(favoriteManga).hasSize(1) - assertThat(favoriteManga[0].readingModeType).isEqualTo(ReadingModeType.VERTICAL.flagValue) - assertThat(favoriteManga[0].orientationType).isEqualTo(OrientationType.PORTRAIT.flagValue) - - // Change manga in database to default values - val dbManga = getSingleManga("One Piece") - dbManga.id = manga.id - db.insertManga(dbManga).executeAsBlocking() - - favoriteManga = legacyBackupManager.databaseHelper.getFavoriteMangas().executeAsBlocking() - assertThat(favoriteManga).hasSize(1) - assertThat(favoriteManga[0].readingModeType).isEqualTo(ReadingModeType.DEFAULT.flagValue) - assertThat(favoriteManga[0].orientationType).isEqualTo(OrientationType.DEFAULT.flagValue) - - // Restore local manga - legacyBackupManager.restoreMangaNoFetch(manga, dbManga) - - // Test if restore successful - favoriteManga = legacyBackupManager.databaseHelper.getFavoriteMangas().executeAsBlocking() - assertThat(favoriteManga).hasSize(1) - assertThat(favoriteManga[0].readingModeType).isEqualTo(ReadingModeType.VERTICAL.flagValue) - assertThat(favoriteManga[0].orientationType).isEqualTo(OrientationType.PORTRAIT.flagValue) - - // Clear database to test manga fetch - clearDatabase() - - // Test if successful - favoriteManga = legacyBackupManager.databaseHelper.getFavoriteMangas().executeAsBlocking() - assertThat(favoriteManga).hasSize(0) - - // Restore Json - // Create JSON from manga to test parser - val json = legacyBackupManager.parser.encodeToString(manga) - // Restore JSON from manga to test parser - val jsonManga = legacyBackupManager.parser.decodeFromString(json) - - // Restore manga with fetch observable - val networkManga = getSingleManga("One Piece") - networkManga.description = "This is a description" - `when`(source.fetchMangaDetails(jsonManga)).thenReturn(Observable.just(networkManga)) - - runBlocking { - legacyBackupManager.fetchManga(source, jsonManga) - - // Check if restore successful - val dbCats = legacyBackupManager.databaseHelper.getFavoriteMangas().executeAsBlocking() - assertThat(dbCats).hasSize(1) - assertThat(dbCats[0].readingModeType).isEqualTo(ReadingModeType.VERTICAL.flagValue) - assertThat(dbCats[0].orientationType).isEqualTo(OrientationType.PORTRAIT.flagValue) - assertThat(dbCats[0].description).isEqualTo("This is a description") - } - } - - /** - * Test if chapter restore is successful - */ - @Test - fun testRestoreChapters() { - // Insert manga - val manga = getSingleManga("One Piece") - manga.id = legacyBackupManager.databaseHelper.insertManga(manga).executeAsBlocking().insertedId() - - // Create restore list - val chapters = mutableListOf() - for (i in 1..8) { - val chapter = getSingleChapter("Chapter $i") - chapter.read = true - chapters.add(chapter) - } - - // Check parser - val chaptersJson = legacyBackupManager.parser.encodeToString(chapters) - val restoredChapters = legacyBackupManager.parser.decodeFromString>(chaptersJson) - - // Fetch chapters from upstream - // Create list - val chaptersRemote = mutableListOf() - (1..10).mapTo(chaptersRemote) { getSingleChapter("Chapter $it") } - `when`(source.fetchChapterList(manga)).thenReturn(Observable.just(chaptersRemote)) - - runBlocking { - legacyBackupManager.restoreChapters(source, manga, restoredChapters) - - val dbCats = legacyBackupManager.databaseHelper.getChapters(manga).executeAsBlocking() - assertThat(dbCats).hasSize(10) - assertThat(dbCats[0].read).isEqualTo(true) - } - } - - /** - * Test to check if history restore works - */ - @Test - fun restoreHistoryForManga() { - val manga = getSingleManga("One Piece") - manga.id = legacyBackupManager.databaseHelper.insertManga(manga).executeAsBlocking().insertedId() - - // Create chapter - val chapter = getSingleChapter("Chapter 1") - chapter.manga_id = manga.id - chapter.read = true - chapter.id = legacyBackupManager.databaseHelper.insertChapter(chapter).executeAsBlocking().insertedId() - - val historyJson = getSingleHistory(chapter) - - val historyList = mutableListOf() - historyList.add(historyJson) - - // Check parser - val historyListJson = legacyBackupManager.parser.encodeToString(historyList) - val history = legacyBackupManager.parser.decodeFromString>(historyListJson) - - // Restore categories - legacyBackupManager.restoreHistoryForManga(history) - - val historyDB = legacyBackupManager.databaseHelper.getHistoryByMangaId(manga.id!!).executeAsBlocking() - assertThat(historyDB).hasSize(1) - assertThat(historyDB[0].last_read).isEqualTo(1000) - } - - /** - * Test to check if tracking restore works - */ - @Test - fun restoreTrackForManga() { - // Create mangas - val manga = getSingleManga("One Piece") - val manga2 = getSingleManga("Bleach") - manga.id = legacyBackupManager.databaseHelper.insertManga(manga).executeAsBlocking().insertedId() - manga2.id = legacyBackupManager.databaseHelper.insertManga(manga2).executeAsBlocking().insertedId() - - // Create track and add it to database - // This tests duplicate errors. - val track = getSingleTrack(manga) - track.last_chapter_read = 5F - legacyBackupManager.databaseHelper.insertTrack(track).executeAsBlocking() - var trackDB = legacyBackupManager.databaseHelper.getTracks(manga).executeAsBlocking() - assertThat(trackDB).hasSize(1) - assertThat(trackDB[0].last_chapter_read).isEqualTo(5) - track.last_chapter_read = 7F - - // Create track for different manga to test track not in database - val track2 = getSingleTrack(manga2) - track2.last_chapter_read = 10F - - // Check parser and restore already in database - var trackList = listOf(track) - // Check parser - var trackListJson = legacyBackupManager.parser.encodeToString(trackList) - var trackListRestore = legacyBackupManager.parser.decodeFromString>(trackListJson) - legacyBackupManager.restoreTrackForManga(manga, trackListRestore) - - // Assert if restore works. - trackDB = legacyBackupManager.databaseHelper.getTracks(manga).executeAsBlocking() - assertThat(trackDB).hasSize(1) - assertThat(trackDB[0].last_chapter_read).isEqualTo(7) - - // Check parser and restore already in database with lower chapter_read - track.last_chapter_read = 5F - trackList = listOf(track) - legacyBackupManager.restoreTrackForManga(manga, trackList) - - // Assert if restore works. - trackDB = legacyBackupManager.databaseHelper.getTracks(manga).executeAsBlocking() - assertThat(trackDB).hasSize(1) - assertThat(trackDB[0].last_chapter_read).isEqualTo(7) - - // Check parser and restore, track not in database - trackList = listOf(track2) - - // Check parser - trackListJson = legacyBackupManager.parser.encodeToString(trackList) - trackListRestore = legacyBackupManager.parser.decodeFromString>(trackListJson) - legacyBackupManager.restoreTrackForManga(manga2, trackListRestore) - - // Assert if restore works. - trackDB = legacyBackupManager.databaseHelper.getTracks(manga2).executeAsBlocking() - assertThat(trackDB).hasSize(1) - assertThat(trackDB[0].last_chapter_read).isEqualTo(10) - } - - private fun clearJson() { - root = Backup() - information = buildJsonObject {} - } - - private fun addSingleCategory(name: String): Category { - val category = Category.create(name) - root.categories = listOf(category) - return category - } - - private fun clearDatabase() { - db.deleteMangas().executeAsBlocking() - db.deleteHistory().executeAsBlocking() - } - - private fun getSingleHistory(chapter: Chapter): DHistory { - return DHistory(chapter.url, 1000) - } - - private fun getSingleTrack(manga: Manga): TrackImpl { - val track = TrackImpl() - track.title = manga.title - track.manga_id = manga.id!! - track.sync_id = 1 - return track - } - - private fun getSingleManga(title: String): MangaImpl { - val manga = MangaImpl() - manga.source = 1 - manga.title = title - manga.url = "/manga/$title" - manga.favorite = true - return manga - } - - private fun getSingleChapter(name: String): ChapterImpl { - val chapter = ChapterImpl() - chapter.name = name - chapter.url = "/read-online/$name-page-1.html" - return chapter - } -} diff --git a/app/src/test/java/eu/kanade/tachiyomi/data/database/CategoryTest.kt b/app/src/test/java/eu/kanade/tachiyomi/data/database/CategoryTest.kt deleted file mode 100644 index c24baa1c63..0000000000 --- a/app/src/test/java/eu/kanade/tachiyomi/data/database/CategoryTest.kt +++ /dev/null @@ -1,109 +0,0 @@ -package eu.kanade.tachiyomi.data.database - -import android.os.Build -import eu.kanade.tachiyomi.BuildConfig -import eu.kanade.tachiyomi.CustomRobolectricGradleTestRunner -import eu.kanade.tachiyomi.data.database.models.CategoryImpl -import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.data.database.models.MangaCategory -import org.assertj.core.api.Assertions.assertThat -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RuntimeEnvironment -import org.robolectric.annotation.Config - -@Config(constants = BuildConfig::class, sdk = [Build.VERSION_CODES.M]) -@RunWith(CustomRobolectricGradleTestRunner::class) -class CategoryTest { - - lateinit var db: DatabaseHelper - - @Before - fun setup() { - val app = RuntimeEnvironment.application - db = DatabaseHelper(app) - - // Create 5 manga - createManga("a") - createManga("b") - createManga("c") - createManga("d") - createManga("e") - } - - @Test - fun testHasCategories() { - // Create 2 categories - createCategory("Reading") - createCategory("Hold") - - val categories = db.getCategories().executeAsBlocking() - assertThat(categories).hasSize(2) - } - - @Test - fun testHasLibraryMangas() { - val mangas = db.getLibraryMangas().executeAsBlocking() - assertThat(mangas).hasSize(5) - } - - @Test - fun testHasCorrectFavorites() { - val m = Manga.create(0) - m.title = "title" - m.author = "" - m.artist = "" - m.thumbnail_url = "" - m.genre = "a list of genres" - m.description = "long description" - m.url = "url to manga" - m.favorite = false - db.insertManga(m).executeAsBlocking() - val mangas = db.getLibraryMangas().executeAsBlocking() - assertThat(mangas).hasSize(5) - } - - @Test - fun testMangaInCategory() { - // Create 2 categories - createCategory("Reading") - createCategory("Hold") - - // It should not have 0 as id - val c = db.getCategories().executeAsBlocking()[0] - assertThat(c.id).isNotZero - - // Add a manga to a category - val m = db.getLibraryMangas().executeAsBlocking()[0] - val mc = MangaCategory.create(m, c) - db.insertMangaCategory(mc).executeAsBlocking() - - // Get mangas from library and assert manga category is the same - val mangas = db.getLibraryMangas().executeAsBlocking() - for (manga in mangas) { - if (manga.id == m.id) { - assertThat(manga.category).isEqualTo(c.id) - } - } - } - - private fun createManga(title: String) { - val m = Manga.create(0) - m.title = title - m.author = "" - m.artist = "" - m.thumbnail_url = "" - m.genre = "a list of genres" - m.description = "long description" - m.url = "url to manga" - m.favorite = true - db.insertManga(m).executeAsBlocking() - } - - private fun createCategory(name: String) { - val c = CategoryImpl() - c.name = name - db.insertCategory(c).executeAsBlocking() - } -} diff --git a/app/src/test/java/eu/kanade/tachiyomi/data/database/ChapterRecognitionTest.kt b/app/src/test/java/eu/kanade/tachiyomi/data/database/ChapterRecognitionTest.kt deleted file mode 100644 index 33a0f2d176..0000000000 --- a/app/src/test/java/eu/kanade/tachiyomi/data/database/ChapterRecognitionTest.kt +++ /dev/null @@ -1,497 +0,0 @@ -package eu.kanade.tachiyomi.data.database - -import eu.kanade.tachiyomi.data.database.models.Chapter -import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.util.chapter.ChapterRecognition -import org.assertj.core.api.Assertions.assertThat -import org.junit.Before -import org.junit.Test - -class ChapterRecognitionTest { - /** - * The manga containing manga title - */ - lateinit var manga: Manga - - /** - * The chapter containing chapter name - */ - lateinit var chapter: Chapter - - /** - * Set chapter title - * @param name name of chapter - * @return chapter object - */ - private fun createChapter(name: String): Chapter { - chapter = Chapter.create() - chapter.name = name - return chapter - } - - /** - * Set manga title - * @param title title of manga - * @return manga object - */ - private fun createManga(title: String): Manga { - manga.title = title - return manga - } - - /** - * Called before test - */ - @Before - fun setup() { - manga = Manga.create(0).apply { title = "random" } - chapter = Chapter.create() - } - - /** - * Ch.xx base case - */ - @Test - fun ChCaseBase() { - createManga("Mokushiroku Alice") - - createChapter("Mokushiroku Alice Vol.1 Ch.4: Misrepresentation") - ChapterRecognition.parseChapterNumber(chapter, manga) - assertThat(chapter.chapter_number).isEqualTo(4f) - } - - /** - * Ch. xx base case but space after period - */ - @Test - fun ChCaseBase2() { - createManga("Mokushiroku Alice") - - createChapter("Mokushiroku Alice Vol. 1 Ch. 4: Misrepresentation") - ChapterRecognition.parseChapterNumber(chapter, manga) - assertThat(chapter.chapter_number).isEqualTo(4f) - } - - /** - * Ch.xx.x base case - */ - @Test - fun ChCaseDecimal() { - createManga("Mokushiroku Alice") - - createChapter("Mokushiroku Alice Vol.1 Ch.4.1: Misrepresentation") - ChapterRecognition.parseChapterNumber(chapter, manga) - assertThat(chapter.chapter_number).isEqualTo(4.1f) - - createChapter("Mokushiroku Alice Vol.1 Ch.4.4: Misrepresentation") - ChapterRecognition.parseChapterNumber(chapter, manga) - assertThat(chapter.chapter_number).isEqualTo(4.4f) - } - - /** - * Ch.xx.a base case - */ - @Test - fun ChCaseAlpha() { - createManga("Mokushiroku Alice") - - createChapter("Mokushiroku Alice Vol.1 Ch.4.a: Misrepresentation") - ChapterRecognition.parseChapterNumber(chapter, manga) - assertThat(chapter.chapter_number).isEqualTo(4.1f) - - createChapter("Mokushiroku Alice Vol.1 Ch.4.b: Misrepresentation") - ChapterRecognition.parseChapterNumber(chapter, manga) - assertThat(chapter.chapter_number).isEqualTo(4.2f) - - createChapter("Mokushiroku Alice Vol.1 Ch.4.extra: Misrepresentation") - ChapterRecognition.parseChapterNumber(chapter, manga) - assertThat(chapter.chapter_number).isEqualTo(4.99f) - } - - /** - * Name containing one number base case - */ - @Test - fun OneNumberCaseBase() { - createManga("Bleach") - - createChapter("Bleach 567 Down With Snowwhite") - ChapterRecognition.parseChapterNumber(chapter, manga) - assertThat(chapter.chapter_number).isEqualTo(567f) - } - - /** - * Name containing one number and decimal case - */ - @Test - fun OneNumberCaseDecimal() { - createManga("Bleach") - - createChapter("Bleach 567.1 Down With Snowwhite") - ChapterRecognition.parseChapterNumber(chapter, manga) - assertThat(chapter.chapter_number).isEqualTo(567.1f) - - createChapter("Bleach 567.4 Down With Snowwhite") - ChapterRecognition.parseChapterNumber(chapter, manga) - assertThat(chapter.chapter_number).isEqualTo(567.4f) - } - - /** - * Name containing one number and alpha case - */ - @Test - fun OneNumberCaseAlpha() { - createManga("Bleach") - - createChapter("Bleach 567.a Down With Snowwhite") - ChapterRecognition.parseChapterNumber(chapter, manga) - assertThat(chapter.chapter_number).isEqualTo(567.1f) - - createChapter("Bleach 567.b Down With Snowwhite") - ChapterRecognition.parseChapterNumber(chapter, manga) - assertThat(chapter.chapter_number).isEqualTo(567.2f) - - createChapter("Bleach 567.extra Down With Snowwhite") - ChapterRecognition.parseChapterNumber(chapter, manga) - assertThat(chapter.chapter_number).isEqualTo(567.99f) - } - - /** - * Chapter containing manga title and number base case - */ - @Test - fun MangaTitleCaseBase() { - createManga("Solanin") - - createChapter("Solanin 028 Vol. 2") - ChapterRecognition.parseChapterNumber(chapter, manga) - assertThat(chapter.chapter_number).isEqualTo(28f) - } - - /** - * Chapter containing manga title and number decimal case - */ - @Test - fun MangaTitleCaseDecimal() { - createManga("Solanin") - - createChapter("Solanin 028.1 Vol. 2") - ChapterRecognition.parseChapterNumber(chapter, manga) - assertThat(chapter.chapter_number).isEqualTo(28.1f) - - createChapter("Solanin 028.4 Vol. 2") - ChapterRecognition.parseChapterNumber(chapter, manga) - assertThat(chapter.chapter_number).isEqualTo(28.4f) - } - - /** - * Chapter containing manga title and number alpha case - */ - @Test - fun MangaTitleCaseAlpha() { - createManga("Solanin") - - createChapter("Solanin 028.a Vol. 2") - ChapterRecognition.parseChapterNumber(chapter, manga) - assertThat(chapter.chapter_number).isEqualTo(28.1f) - - createChapter("Solanin 028.b Vol. 2") - ChapterRecognition.parseChapterNumber(chapter, manga) - assertThat(chapter.chapter_number).isEqualTo(28.2f) - - createChapter("Solanin 028.extra Vol. 2") - ChapterRecognition.parseChapterNumber(chapter, manga) - assertThat(chapter.chapter_number).isEqualTo(28.99f) - } - - /** - * Extreme base case - */ - @Test - fun ExtremeCaseBase() { - createManga("Onepunch-Man") - - createChapter("Onepunch-Man Punch Ver002 028") - ChapterRecognition.parseChapterNumber(chapter, manga) - assertThat(chapter.chapter_number).isEqualTo(28f) - } - - /** - * Extreme base case decimal - */ - @Test - fun ExtremeCaseDecimal() { - createManga("Onepunch-Man") - - createChapter("Onepunch-Man Punch Ver002 028.1") - ChapterRecognition.parseChapterNumber(chapter, manga) - assertThat(chapter.chapter_number).isEqualTo(28.1f) - - createChapter("Onepunch-Man Punch Ver002 028.4") - ChapterRecognition.parseChapterNumber(chapter, manga) - assertThat(chapter.chapter_number).isEqualTo(28.4f) - } - - /** - * Extreme base case alpha - */ - @Test - fun ExtremeCaseAlpha() { - createManga("Onepunch-Man") - - createChapter("Onepunch-Man Punch Ver002 028.a") - ChapterRecognition.parseChapterNumber(chapter, manga) - assertThat(chapter.chapter_number).isEqualTo(28.1f) - - createChapter("Onepunch-Man Punch Ver002 028.b") - ChapterRecognition.parseChapterNumber(chapter, manga) - assertThat(chapter.chapter_number).isEqualTo(28.2f) - - createChapter("Onepunch-Man Punch Ver002 028.extra") - ChapterRecognition.parseChapterNumber(chapter, manga) - assertThat(chapter.chapter_number).isEqualTo(28.99f) - } - - /** - * Chapter containing .v2 - */ - @Test - fun dotV2Case() { - createChapter("Vol.1 Ch.5v.2: Alones") - ChapterRecognition.parseChapterNumber(chapter, manga) - assertThat(chapter.chapter_number).isEqualTo(5f) - } - - /** - * Check for case with number in manga title - */ - @Test - fun numberInMangaTitleCase() { - createManga("Ayame 14") - createChapter("Ayame 14 1 - The summer of 14") - ChapterRecognition.parseChapterNumber(chapter, manga) - assertThat(chapter.chapter_number).isEqualTo(1f) - } - - /** - * Case with space between ch. x - */ - @Test - fun spaceAfterChapterCase() { - createManga("Mokushiroku Alice") - createChapter("Mokushiroku Alice Vol.1 Ch. 4: Misrepresentation") - ChapterRecognition.parseChapterNumber(chapter, manga) - assertThat(chapter.chapter_number).isEqualTo(4f) - } - - /** - * Chapter containing mar(ch) - */ - @Test - fun marchInChapterCase() { - createManga("Ayame 14") - createChapter("Vol.1 Ch.1: March 25 (First Day Cohabiting)") - ChapterRecognition.parseChapterNumber(chapter, manga) - assertThat(chapter.chapter_number).isEqualTo(1f) - } - - /** - * Chapter containing range - */ - @Test - fun rangeInChapterCase() { - createChapter("Ch.191-200 Read Online") - ChapterRecognition.parseChapterNumber(chapter, manga) - assertThat(chapter.chapter_number).isEqualTo(191f) - } - - /** - * Chapter containing multiple zeros - */ - @Test - fun multipleZerosCase() { - createChapter("Vol.001 Ch.003: Kaguya Doesn't Know Much") - ChapterRecognition.parseChapterNumber(chapter, manga) - assertThat(chapter.chapter_number).isEqualTo(3f) - } - - /** - * Chapter with version before number - */ - @Test - fun chapterBeforeNumberCase() { - createManga("Onepunch-Man") - createChapter("Onepunch-Man Punch Ver002 086 : Creeping Darkness [3]") - ChapterRecognition.parseChapterNumber(chapter, manga) - assertThat(chapter.chapter_number).isEqualTo(86f) - } - - /** - * Case with version attached to chapter number - */ - @Test - fun vAttachedToChapterCase() { - createManga("Ansatsu Kyoushitsu") - createChapter("Ansatsu Kyoushitsu 011v002: Assembly Time") - ChapterRecognition.parseChapterNumber(chapter, manga) - assertThat(chapter.chapter_number).isEqualTo(11f) - } - - /** - * Case where the chapter title contains the chapter - * But wait it's not actual the chapter number. - */ - @Test - fun NumberAfterMangaTitleWithChapterInChapterTitleCase() { - createChapter("Tokyo ESP 027: Part 002: Chapter 001") - createManga("Tokyo ESP") - ChapterRecognition.parseChapterNumber(chapter, manga) - assertThat(chapter.chapter_number).isEqualTo(027f) - } - - /** - * unParsable chapter - */ - @Test - fun unParsableCase() { - createChapter("Foo") - ChapterRecognition.parseChapterNumber(chapter, manga) - assertThat(chapter.chapter_number).isEqualTo(-1f) - } - - /** - * chapter with time in title - */ - @Test - fun timeChapterCase() { - createChapter("Fairy Tail 404: 00:00") - ChapterRecognition.parseChapterNumber(chapter, manga) - assertThat(chapter.chapter_number).isEqualTo(404f) - } - - /** - * chapter with alpha without dot - */ - @Test - fun alphaWithoutDotCase() { - createChapter("Asu No Yoichi 19a") - ChapterRecognition.parseChapterNumber(chapter, manga) - assertThat(chapter.chapter_number).isEqualTo(19.1f) - } - - /** - * Chapter title containing extra and vol - */ - @Test - fun chapterContainingExtraCase() { - createManga("Fairy Tail") - - createChapter("Fairy Tail 404.extravol002") - ChapterRecognition.parseChapterNumber(chapter, manga) - assertThat(chapter.chapter_number).isEqualTo(404.99f) - - createChapter("Fairy Tail 404 extravol002") - ChapterRecognition.parseChapterNumber(chapter, manga) - assertThat(chapter.chapter_number).isEqualTo(404.99f) - - createChapter("Fairy Tail 404.evol002") - ChapterRecognition.parseChapterNumber(chapter, manga) - assertThat(chapter.chapter_number).isEqualTo(404.5f) - } - - /** - * Chapter title containing omake (japanese extra) and vol - */ - @Test - fun chapterContainingOmakeCase() { - createManga("Fairy Tail") - - createChapter("Fairy Tail 404.omakevol002") - ChapterRecognition.parseChapterNumber(chapter, manga) - assertThat(chapter.chapter_number).isEqualTo(404.98f) - - createChapter("Fairy Tail 404 omakevol002") - ChapterRecognition.parseChapterNumber(chapter, manga) - assertThat(chapter.chapter_number).isEqualTo(404.98f) - - createChapter("Fairy Tail 404.ovol002") - ChapterRecognition.parseChapterNumber(chapter, manga) - assertThat(chapter.chapter_number).isEqualTo(404.15f) - } - - /** - * Chapter title containing special and vol - */ - @Test - fun chapterContainingSpecialCase() { - createManga("Fairy Tail") - - createChapter("Fairy Tail 404.specialvol002") - ChapterRecognition.parseChapterNumber(chapter, manga) - assertThat(chapter.chapter_number).isEqualTo(404.97f) - - createChapter("Fairy Tail 404 specialvol002") - ChapterRecognition.parseChapterNumber(chapter, manga) - assertThat(chapter.chapter_number).isEqualTo(404.97f) - - createChapter("Fairy Tail 404.svol002") - ChapterRecognition.parseChapterNumber(chapter, manga) - assertThat(chapter.chapter_number).isEqualTo(404.19f) - } - - /** - * Chapter title containing comma's - */ - @Test - fun chapterContainingCommasCase() { - createManga("One Piece") - - createChapter("One Piece 300,a") - ChapterRecognition.parseChapterNumber(chapter, manga) - assertThat(chapter.chapter_number).isEqualTo(300.1f) - - createChapter("One Piece Ch,123,extra") - ChapterRecognition.parseChapterNumber(chapter, manga) - assertThat(chapter.chapter_number).isEqualTo(123.99f) - - createChapter("One Piece the sunny, goes swimming 024,005") - ChapterRecognition.parseChapterNumber(chapter, manga) - assertThat(chapter.chapter_number).isEqualTo(24.005f) - } - - /** - * Test for chapters containing season - */ - @Test - fun chapterContainingSeasonCase() { - createManga("D.I.C.E") - - createChapter("D.I.C.E[Season 001] Ep. 007") - ChapterRecognition.parseChapterNumber(chapter, manga) - assertThat(chapter.chapter_number).isEqualTo(7f) - } - - /** - * Test for chapters in format sx - chapter xx - */ - @Test - fun chapterContainingSeasonCase2() { - createManga("The Gamer") - - createChapter("S3 - Chapter 20") - ChapterRecognition.parseChapterNumber(chapter, manga) - assertThat(chapter.chapter_number).isEqualTo(20f) - } - - /** - * Test for chapters ending with s - */ - @Test - fun chaptersEndingWithS() { - createManga("One Outs") - - createChapter("One Outs 001") - ChapterRecognition.parseChapterNumber(chapter, manga) - assertThat(chapter.chapter_number).isEqualTo(1f) - } -} diff --git a/app/src/test/java/eu/kanade/tachiyomi/data/library/LibraryUpdateServiceTest.kt b/app/src/test/java/eu/kanade/tachiyomi/data/library/LibraryUpdateServiceTest.kt deleted file mode 100644 index bd96644c41..0000000000 --- a/app/src/test/java/eu/kanade/tachiyomi/data/library/LibraryUpdateServiceTest.kt +++ /dev/null @@ -1,136 +0,0 @@ -package eu.kanade.tachiyomi.data.library - -import android.app.Application -import android.content.Context -import android.content.Intent -import android.os.Build -import eu.kanade.tachiyomi.BuildConfig -import eu.kanade.tachiyomi.CustomRobolectricGradleTestRunner -import eu.kanade.tachiyomi.data.database.models.Chapter -import eu.kanade.tachiyomi.data.database.models.LibraryManga -import eu.kanade.tachiyomi.source.SourceManager -import eu.kanade.tachiyomi.source.online.HttpSource -import kotlinx.coroutines.runBlocking -import org.assertj.core.api.Assertions.assertThat -import org.junit.Before -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.Matchers.anyLong -import org.mockito.Mockito.RETURNS_DEEP_STUBS -import org.mockito.Mockito.mock -import org.mockito.Mockito.`when` -import org.robolectric.Robolectric -import org.robolectric.RuntimeEnvironment -import org.robolectric.annotation.Config -import rx.Observable -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.InjektModule -import uy.kohesive.injekt.api.InjektRegistrar -import uy.kohesive.injekt.api.addSingleton - -@Config(constants = BuildConfig::class, sdk = [Build.VERSION_CODES.M]) -@RunWith(CustomRobolectricGradleTestRunner::class) -class LibraryUpdateServiceTest { - - lateinit var app: Application - lateinit var context: Context - lateinit var service: LibraryUpdateService - lateinit var source: HttpSource - - @Before - fun setup() { - app = RuntimeEnvironment.application - context = app.applicationContext - - // Mock the source manager - val module = object : InjektModule { - override fun InjektRegistrar.registerInjectables() { - addSingleton(mock(SourceManager::class.java, RETURNS_DEEP_STUBS)) - } - } - Injekt.importModule(module) - - service = Robolectric.setupService(LibraryUpdateService::class.java) - source = mock(HttpSource::class.java) - `when`(service.sourceManager.get(anyLong())).thenReturn(source) - } - - @Test - fun testLifecycle() { - // Smoke test - Robolectric.buildService(LibraryUpdateService::class.java) - .attach() - .create() - .startCommand(0, 0) - .destroy() - .get() - } - - @Test - fun testUpdateManga() { - val manga = createManga("/manga1")[0] - manga.id = 1L - service.db.insertManga(manga).executeAsBlocking() - - val sourceChapters = createChapters("/chapter1", "/chapter2") - - `when`(source.fetchChapterList(manga)).thenReturn(Observable.just(sourceChapters)) - - runBlocking { - service.updateManga(manga) - - assertThat(service.db.getChapters(manga).executeAsBlocking()).hasSize(2) - } - } - - @Test - fun testContinuesUpdatingWhenAMangaFails() { - var favManga = createManga("/manga1", "/manga2", "/manga3") - service.db.insertMangas(favManga).executeAsBlocking() - favManga = service.db.getLibraryMangas().executeAsBlocking() - - val chapters = createChapters("/chapter1", "/chapter2") - val chapters3 = createChapters("/achapter1", "/achapter2") - - // One of the updates will fail - `when`(source.fetchChapterList(favManga[0])).thenReturn(Observable.just(chapters)) - `when`(source.fetchChapterList(favManga[1])).thenReturn(Observable.error(Exception())) - `when`(source.fetchChapterList(favManga[2])).thenReturn(Observable.just(chapters3)) - - val intent = Intent() - val categoryId = intent.getIntExtra(LibraryUpdateService.KEY_CATEGORY, -1) - val target = LibraryUpdateService.Target.CHAPTERS - runBlocking { - service.addMangaToQueue(categoryId, target) - service.updateChapterList() - - // There are 3 network attempts and 2 insertions (1 request failed) - assertThat(service.db.getChapters(favManga[0]).executeAsBlocking()).hasSize(2) - assertThat(service.db.getChapters(favManga[1]).executeAsBlocking()).hasSize(0) - assertThat(service.db.getChapters(favManga[2]).executeAsBlocking()).hasSize(2) - } - } - - private fun createChapters(vararg urls: String): List { - val list = mutableListOf() - for (url in urls) { - val c = Chapter.create() - c.url = url - c.name = url.substring(1) - list.add(c) - } - return list - } - - private fun createManga(vararg urls: String): List { - val list = mutableListOf() - for (url in urls) { - val m = LibraryManga() - m.url = url - m.title = url.substring(1) - m.favorite = true - list.add(m) - } - return list - } -} diff --git a/app/src/test/java/eu/kanade/tachiyomi/util/chapter/ChapterRecognitionTest.kt b/app/src/test/java/eu/kanade/tachiyomi/util/chapter/ChapterRecognitionTest.kt new file mode 100644 index 0000000000..1887ff4da7 --- /dev/null +++ b/app/src/test/java/eu/kanade/tachiyomi/util/chapter/ChapterRecognitionTest.kt @@ -0,0 +1,275 @@ +package eu.kanade.tachiyomi.util.chapter + +import eu.kanade.tachiyomi.data.database.models.Chapter +import eu.kanade.tachiyomi.data.database.models.Manga +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.parallel.Execution +import org.junit.jupiter.api.parallel.ExecutionMode + +@Execution(ExecutionMode.CONCURRENT) +class ChapterRecognitionTest { + + @Test + fun `Basic Ch prefix`() { + val mangaTitle = "Mokushiroku Alice" + + assertChapter(mangaTitle, "Mokushiroku Alice Vol.1 Ch.4: Misrepresentation", 4f) + } + + @Test + fun `Basic Ch prefix with space after period`() { + val mangaTitle = "Mokushiroku Alice" + + assertChapter(mangaTitle, "Mokushiroku Alice Vol. 1 Ch. 4: Misrepresentation", 4f) + } + + @Test + fun `Basic Ch prefix with decimal`() { + val mangaTitle = "Mokushiroku Alice" + + assertChapter(mangaTitle, "Mokushiroku Alice Vol.1 Ch.4.1: Misrepresentation", 4.1f) + assertChapter(mangaTitle, "Mokushiroku Alice Vol.1 Ch.4.4: Misrepresentation", 4.4f) + } + + @Test + fun `Basic Ch prefix with alpha postfix`() { + val mangaTitle = "Mokushiroku Alice" + + assertChapter(mangaTitle, "Mokushiroku Alice Vol.1 Ch.4.a: Misrepresentation", 4.1f) + assertChapter(mangaTitle, "Mokushiroku Alice Vol.1 Ch.4.b: Misrepresentation", 4.2f) + assertChapter(mangaTitle, "Mokushiroku Alice Vol.1 Ch.4.extra: Misrepresentation", 4.99f) + } + + @Test + fun `Name containing one number`() { + val mangaTitle = "Bleach" + + assertChapter(mangaTitle, "Bleach 567 Down With Snowwhite", 567f) + } + + @Test + fun `Name containing one number and decimal`() { + val mangaTitle = "Bleach" + + assertChapter(mangaTitle, "Bleach 567.1 Down With Snowwhite", 567.1f) + assertChapter(mangaTitle, "Bleach 567.4 Down With Snowwhite", 567.4f) + } + + @Test + fun `Name containing one number and alpha`() { + val mangaTitle = "Bleach" + + assertChapter(mangaTitle, "Bleach 567.a Down With Snowwhite", 567.1f) + assertChapter(mangaTitle, "Bleach 567.b Down With Snowwhite", 567.2f) + assertChapter(mangaTitle, "Bleach 567.extra Down With Snowwhite", 567.99f) + } + + @Test + fun `Chapter containing manga title and number`() { + val mangaTitle = "Solanin" + + assertChapter(mangaTitle, "Solanin 028 Vol. 2", 28f) + } + + @Test + fun `Chapter containing manga title and number decimal`() { + val mangaTitle = "Solanin" + + assertChapter(mangaTitle, "Solanin 028.1 Vol. 2", 28.1f) + assertChapter(mangaTitle, "Solanin 028.4 Vol. 2", 28.4f) + } + + @Test + fun `Chapter containing manga title and number alpha`() { + val mangaTitle = "Solanin" + + assertChapter(mangaTitle, "Solanin 028.a Vol. 2", 28.1f) + assertChapter(mangaTitle, "Solanin 028.b Vol. 2", 28.2f) + assertChapter(mangaTitle, "Solanin 028.extra Vol. 2", 28.99f) + } + + @Test + fun `Extreme case`() { + val mangaTitle = "Onepunch-Man" + + assertChapter(mangaTitle, "Onepunch-Man Punch Ver002 028", 28f) + } + + @Test + fun `Extreme case with decimal`() { + val mangaTitle = "Onepunch-Man" + + assertChapter(mangaTitle, "Onepunch-Man Punch Ver002 028.1", 28.1f) + assertChapter(mangaTitle, "Onepunch-Man Punch Ver002 028.4", 28.4f) + } + + @Test + fun `Extreme case with alpha`() { + val mangaTitle = "Onepunch-Man" + + assertChapter(mangaTitle, "Onepunch-Man Punch Ver002 028.a", 28.1f) + assertChapter(mangaTitle, "Onepunch-Man Punch Ver002 028.b", 28.2f) + assertChapter(mangaTitle, "Onepunch-Man Punch Ver002 028.extra", 28.99f) + } + + @Test + fun `Chapter containing dot v2`() { + val mangaTitle = "random" + + assertChapter(mangaTitle, "Vol.1 Ch.5v.2: Alones", 5f) + } + + @Test + fun `Number in manga title`() { + val mangaTitle = "Ayame 14" + + assertChapter(mangaTitle, "Ayame 14 1 - The summer of 14", 1f) + } + + @Test + fun `Space between ch x`() { + val mangaTitle = "Mokushiroku Alice" + + assertChapter(mangaTitle, "Mokushiroku Alice Vol.1 Ch. 4: Misrepresentation", 4f) + } + + @Test + fun `Chapter title with ch substring`() { + val mangaTitle = "Ayame 14" + + assertChapter(mangaTitle, "Vol.1 Ch.1: March 25 (First Day Cohabiting)", 1f) + } + + @Test + fun `Chapter containing multiple zeros`() { + val mangaTitle = "random" + + assertChapter(mangaTitle, "Vol.001 Ch.003: Kaguya Doesn't Know Much", 3f) + } + + @Test + fun `Chapter with version before number`() { + val mangaTitle = "Onepunch-Man" + + assertChapter(mangaTitle, "Onepunch-Man Punch Ver002 086 : Creeping Darkness [3]", 86f) + } + + @Test + fun `Version attached to chapter number`() { + val mangaTitle = "Ansatsu Kyoushitsu" + + assertChapter(mangaTitle, "Ansatsu Kyoushitsu 011v002: Assembly Time", 11f) + } + + /** + * Case where the chapter title contains the chapter + * But wait it's not actual the chapter number. + */ + @Test + fun `Number after manga title with chapter in chapter title case`() { + val mangaTitle = "Tokyo ESP" + + assertChapter(mangaTitle, "Tokyo ESP 027: Part 002: Chapter 001", 027f) + } + + @Test + fun `Unparseable chapter`() { + val mangaTitle = "random" + + assertChapter(mangaTitle, "Foo", -1f) + } + + @Test + fun `Chapter with time in title`() { + val mangaTitle = "random" + + assertChapter(mangaTitle, "Fairy Tail 404: 00:00", 404f) + } + + @Test + fun `Chapter with alpha without dot`() { + val mangaTitle = "random" + + assertChapter(mangaTitle, "Asu No Yoichi 19a", 19.1f) + } + + @Test + fun `Chapter title containing extra and vol`() { + val mangaTitle = "Fairy Tail" + + assertChapter(mangaTitle, "Fairy Tail 404.extravol002", 404.99f) + assertChapter(mangaTitle, "Fairy Tail 404 extravol002", 404.99f) + assertChapter(mangaTitle, "Fairy Tail 404.evol002", 404.5f) + } + + @Test + fun `Chapter title containing omake (japanese extra) and vol`() { + val mangaTitle = "Fairy Tail" + + assertChapter(mangaTitle, "Fairy Tail 404.omakevol002", 404.98f) + assertChapter(mangaTitle, "Fairy Tail 404 omakevol002", 404.98f) + assertChapter(mangaTitle, "Fairy Tail 404.ovol002", 404.15f) + } + + @Test + fun `Chapter title containing special and vol`() { + val mangaTitle = "Fairy Tail" + + assertChapter(mangaTitle, "Fairy Tail 404.specialvol002", 404.97f) + assertChapter(mangaTitle, "Fairy Tail 404 specialvol002", 404.97f) + assertChapter(mangaTitle, "Fairy Tail 404.svol002", 404.19f) + } + + @Test + fun `Chapter title containing commas`() { + val mangaTitle = "One Piece" + + assertChapter(mangaTitle, "One Piece 300,a", 300.1f) + assertChapter(mangaTitle, "One Piece Ch,123,extra", 123.99f) + assertChapter(mangaTitle, "One Piece the sunny, goes swimming 024,005", 24.005f) + } + + @Test + fun `Chapter title containing hyphens`() { + val mangaTitle = "Solo Leveling" + + assertChapter(mangaTitle, "ch 122-a", 122.1f) + assertChapter(mangaTitle, "Solo Leveling Ch.123-extra", 123.99f) + assertChapter(mangaTitle, "Solo Leveling, 024-005", 24.005f) + assertChapter(mangaTitle, "Ch.191-200 Read Online", 191.200f) + } + + @Test + fun `Chapters containing season`() { + assertChapter("D.I.C.E", "D.I.C.E[Season 001] Ep. 007", 7f) + } + + @Test + fun `Chapters in format sx - chapter xx`() { + assertChapter("The Gamer", "S3 - Chapter 20", 20f) + } + + @Test + fun `Chapters ending with s`() { + assertChapter("One Outs", "One Outs 001", 1f) + } + + private fun assertChapter(mangaTitle: String, name: String, expected: Float) { + val chapter = createChapter(name) + ChapterRecognition.parseChapterNumber(chapter, createManga(mangaTitle)) + assertEquals(expected, chapter.chapter_number) + } + + private fun createManga(title: String): Manga { + val manga = Manga.create(0) + manga.title = title + return manga + } + + private fun createChapter(name: String): Chapter { + val chapter = Chapter.create() + chapter.name = name + return chapter + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ac3459b93f..417351534a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -6,7 +6,6 @@ coil_version = "2.0.0-rc03" conductor_version = "3.1.5" flowbinding_version = "1.2.0" shizuku_version = "12.1.0" -robolectric_version = "3.1.4" [libraries] android-shortcut-gradle = "com.github.zellius:android-shortcut-gradle-plugin:0.1.2" @@ -87,12 +86,7 @@ aboutlibraries-gradle = { module = "com.mikepenz.aboutlibraries.plugin:aboutlibr shizuku-api = { module = "dev.rikka.shizuku:api", version.ref = "shizuku_version" } shizuku-provider = { module = "dev.rikka.shizuku:provider", version.ref = "shizuku_version" } -junit = "junit:junit:4.13.2" -assertj-core = "org.assertj:assertj-core:3.16.1" -mockito-core = "org.mockito:mockito-core:1.10.19" - -robolectric-core = { module = "org.robolectric:robolectric", version.ref = "robolectric_version" } -robolectric-playservices = { module = "org.robolectric:shadows-play-services", version.ref = "robolectric_version" } +junit = "org.junit.jupiter:junit-jupiter:5.9.0" leakcanary-android = "com.squareup.leakcanary:leakcanary-android:2.7" @@ -106,7 +100,6 @@ coil = ["coil-core","coil-gif",] flowbinding = ["flowbinding-android","flowbinding-appcompat","flowbinding-recyclerview","flowbinding-swiperefreshlayout","flowbinding-viewpager"] conductor = ["conductor-core","conductor-viewpager","conductor-support-preference"] shizuku = ["shizuku-api","shizuku-provider"] -robolectric = ["robolectric-core","robolectric-playservices"] [plugins] kotlinter = { id = "org.jmailen.kotlinter", version = "3.10.0"} From be33a57d43ac7803cbec168c69108c05653fc520 Mon Sep 17 00:00:00 2001 From: arkon Date: Sat, 13 Aug 2022 12:37:13 -0400 Subject: [PATCH 03/26] Update .editorconfig --- .editorconfig | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.editorconfig b/.editorconfig index b0aa6f2c24..bbef1d7522 100644 --- a/.editorconfig +++ b/.editorconfig @@ -2,4 +2,6 @@ indent_size=4 insert_final_newline=true ij_kotlin_allow_trailing_comma=true -ij_kotlin_allow_trailing_comma_on_call_site=true \ No newline at end of file +ij_kotlin_allow_trailing_comma_on_call_site=true +ij_kotlin_name_count_to_use_star_import = 2147483647 +ij_kotlin_name_count_to_use_star_import_for_members = 2147483647 \ No newline at end of file From 3966a917ee76f007e581cf5c94a48d29090ef2c6 Mon Sep 17 00:00:00 2001 From: arkon Date: Sat, 13 Aug 2022 12:46:00 -0400 Subject: [PATCH 04/26] Bump dependencies + compile SDK to 33 + linting --- app/build.gradle.kts | 20 +++++----- app/src/main/AndroidManifest.xml | 3 +- .../data/updater/AppUpdateChecker.kt | 1 + .../util/ExtensionInstallReceiver.kt | 5 ++- .../controller/SearchableNucleusController.kt | 21 +++++----- .../source/browse/BrowseSourceController.kt | 1 + .../ui/download/DownloadHeaderHolder.kt | 3 +- .../tachiyomi/ui/library/LibraryController.kt | 4 +- .../ui/library/LibrarySettingsSheet.kt | 2 + .../kanade/tachiyomi/ui/main/MainActivity.kt | 2 +- .../ui/manga/chapter/ChaptersSettingsSheet.kt | 1 + .../tachiyomi/ui/reader/ReaderActivity.kt | 17 ++++---- .../ui/reader/model/ReaderChapter.kt | 6 +-- .../ui/reader/setting/ReaderSettingsSheet.kt | 35 ++++++++-------- .../ui/reader/viewer/ReaderPageImageView.kt | 3 +- .../ui/reader/viewer/webtoon/WebtoonFrame.kt | 8 ++-- .../ui/setting/SettingsMainController.kt | 4 +- .../search/SettingsSearchController.kt | 4 +- .../tachiyomi/ui/webview/WebViewActivity.kt | 4 +- .../widget/TachiyomiBottomNavigationView.kt | 13 +++--- .../widget/preference/ThemesPreference.kt | 13 +++--- .../widget/sheet/BottomSheetViewPager.kt | 11 ++--- buildSrc/src/main/kotlin/AndroidConfig.kt | 2 +- gradle/androidx.versions.toml | 6 +-- gradle/kotlinx.versions.toml | 13 +++--- gradle/libs.versions.toml | 40 ++++++++++--------- 26 files changed, 128 insertions(+), 114 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 4ab6582389..83121b5e9b 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -18,6 +18,7 @@ shortcutHelper.setFilePath("./shortcuts.xml") val SUPPORTED_ABIS = setOf("armeabi-v7a", "arm64-v8a", "x86") android { + namespace = "eu.kanade.tachiyomi" compileSdk = AndroidConfig.compileSdk ndkVersion = AndroidConfig.ndk @@ -246,6 +247,7 @@ dependencies { // For detecting memory leaks; see https://square.github.io/leakcanary/ // debugImplementation(libs.leakcanary.android) + implementation(libs.leakcanary.plumber) } tasks { @@ -259,19 +261,19 @@ tasks { // See https://kotlinlang.org/docs/reference/experimental.html#experimental-status-of-experimental-api(-markers) withType { kotlinOptions.freeCompilerArgs += listOf( - "-Xopt-in=kotlin.Experimental", - "-Xopt-in=kotlin.RequiresOptIn", - "-Xopt-in=kotlin.ExperimentalStdlibApi", - "-Xopt-in=kotlinx.coroutines.FlowPreview", - "-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi", - "-Xopt-in=kotlinx.coroutines.InternalCoroutinesApi", - "-Xopt-in=kotlinx.serialization.ExperimentalSerializationApi", - "-Xopt-in=coil.annotation.ExperimentalCoilApi", + "-opt-in=kotlin.Experimental", + "-opt-in=kotlin.RequiresOptIn", + "-opt-in=kotlin.ExperimentalStdlibApi", + "-opt-in=kotlinx.coroutines.FlowPreview", + "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi", + "-opt-in=kotlinx.coroutines.InternalCoroutinesApi", + "-opt-in=kotlinx.serialization.ExperimentalSerializationApi", + "-opt-in=coil.annotation.ExperimentalCoilApi", ) } // Duplicating Hebrew string assets due to some locale code issues on different devices - val copyHebrewStrings = task("copyHebrewStrings", type = Copy::class) { + val copyHebrewStrings by registering(Copy::class) { from("./src/main/res/values-he") into("./src/main/res/values-iw") include("**/*") diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 8688d7f3d0..3fc615c534 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,6 +1,5 @@ - + diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/updater/AppUpdateChecker.kt b/app/src/main/java/eu/kanade/tachiyomi/data/updater/AppUpdateChecker.kt index 60460dc982..d6a15452e9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/updater/AppUpdateChecker.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/updater/AppUpdateChecker.kt @@ -47,6 +47,7 @@ class AppUpdateChecker { when (result) { is AppUpdateResult.NewUpdate -> AppUpdateNotifier(context).promptUpdate(result.release) is AppUpdateResult.NewUpdateFdroidInstallation -> AppUpdateNotifier(context).promptFdroidUpdate() + else -> {} } result diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstallReceiver.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstallReceiver.kt index cdef6e4368..943e828454 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstallReceiver.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstallReceiver.kt @@ -52,6 +52,7 @@ internal class ExtensionInstallReceiver(private val listener: Listener) : when (val result = getExtensionFromIntent(context, intent)) { is LoadResult.Success -> listener.onExtensionInstalled(result.extension) is LoadResult.Untrusted -> listener.onExtensionUntrusted(result.extension) + else -> {} } } } @@ -60,8 +61,8 @@ internal class ExtensionInstallReceiver(private val listener: Listener) : when (val result = getExtensionFromIntent(context, intent)) { is LoadResult.Success -> listener.onExtensionUpdated(result.extension) // Not needed as a package can't be upgraded if the signature is different - is LoadResult.Untrusted -> { - } + is LoadResult.Untrusted -> {} + else -> {} } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/SearchableNucleusController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/SearchableNucleusController.kt index 15f677bbc0..fa0321304b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/SearchableNucleusController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/SearchableNucleusController.kt @@ -59,16 +59,17 @@ abstract class SearchableNucleusController filter.state = 1 is Filter.CheckBox -> filter.state = true + else -> {} } filterList = presenter.sourceFilters break@filter diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadHeaderHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadHeaderHolder.kt index 59d604974b..3362c99ed3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadHeaderHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/download/DownloadHeaderHolder.kt @@ -28,8 +28,7 @@ class DownloadHeaderHolder(view: View, adapter: FlexibleAdapter<*>) : Expandable override fun onItemReleased(position: Int) { super.onItemReleased(position) binding.container.isDragged = false - mAdapter as DownloadAdapter mAdapter.expandAll() - mAdapter.downloadItemListener.onItemReleased(position) + (mAdapter as DownloadAdapter).downloadItemListener.onItemReleased(position) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt index aa8b9efa18..9bbaa47fde 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt @@ -388,7 +388,7 @@ class LibraryController( override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { createOptionsMenu(menu, inflater, R.menu.library, R.id.action_search) // Mutate the filter icon because it needs to be tinted and the resource is shared. - menu.findItem(R.id.action_filter).icon.mutate() + menu.findItem(R.id.action_filter).icon?.mutate() } fun search(query: String) { @@ -414,7 +414,7 @@ class LibraryController( // Tint icon if there's a filter active if (settingsSheet.filters.hasActiveFilters()) { val filterColor = activity!!.getResourceColor(R.attr.colorFilterActive) - filterItem.icon.setTint(filterColor) + filterItem.icon?.setTint(filterColor) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySettingsSheet.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySettingsSheet.kt index 2991a9729d..dcd0c5cd68 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySettingsSheet.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySettingsSheet.kt @@ -394,6 +394,7 @@ class LibrarySettingsSheet( unreadBadge -> preferences.unreadBadge().set((item.checked)) localBadge -> preferences.localBadge().set((item.checked)) languageBadge -> preferences.languageBadge().set((item.checked)) + else -> {} } adapter.notifyItemChanged(item) } @@ -418,6 +419,7 @@ class LibrarySettingsSheet( when (item) { showTabs -> preferences.categoryTabs().set(item.checked) showNumberOfItems -> preferences.categoryNumberOfItems().set(item.checked) + else -> {} } adapter.notifyItemChanged(item) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt index 04dc530363..e31a8afe8d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/main/MainActivity.kt @@ -466,7 +466,7 @@ class MainActivity : BaseActivity() { // Binding sometimes isn't actually instantiated yet somehow nav?.setOnItemSelectedListener(null) - binding?.toolbar.setNavigationOnClickListener(null) + binding?.toolbar?.setNavigationOnClickListener(null) } override fun onBackPressed() { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersSettingsSheet.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersSettingsSheet.kt index b63d0db3cb..9c04ba10d1 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersSettingsSheet.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersSettingsSheet.kt @@ -113,6 +113,7 @@ class ChaptersSettingsSheet( downloaded -> presenter.setDownloadedFilter(newState) unread -> presenter.setUnreadFilter(newState) bookmarked -> presenter.setBookmarkedFilter(newState) + else -> {} } initModels() diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt index 5b8a07af39..110e02aa17 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt @@ -360,15 +360,16 @@ class ReaderActivity : BaseRxActivity() { } // Init listeners on bottom menu - binding.pageSlider.addOnSliderTouchListener(object : Slider.OnSliderTouchListener { - override fun onStartTrackingTouch(slider: Slider) { - isScrollingThroughPages = true - } + binding.pageSlider.addOnSliderTouchListener( + object : Slider.OnSliderTouchListener { + override fun onStartTrackingTouch(slider: Slider) { + isScrollingThroughPages = true + } - override fun onStopTrackingTouch(slider: Slider) { - isScrollingThroughPages = false - } - }, + override fun onStopTrackingTouch(slider: Slider) { + isScrollingThroughPages = false + } + }, ) binding.pageSlider.addOnChangeListener { slider, value, fromUser -> if (viewer != null && fromUser) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/model/ReaderChapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/model/ReaderChapter.kt index b475c6a4fc..a332a96ecb 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/model/ReaderChapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/model/ReaderChapter.kt @@ -10,9 +10,9 @@ data class ReaderChapter(val chapter: Chapter) { var state: State = State.Wait set(value) { - field = value - stateRelay.call(value) - } + field = value + stateRelay.call(value) + } private val stateRelay by lazy { BehaviorRelay.create(state) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderSettingsSheet.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderSettingsSheet.kt index 4e22f84180..14386d3c55 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderSettingsSheet.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/setting/ReaderSettingsSheet.kt @@ -34,27 +34,28 @@ class ReaderSettingsSheet( behavior.halfExpandedRatio = 0.25f val filterTabIndex = getTabViews().indexOf(colorFilterSettings) - binding.tabs.addOnTabSelectedListener(object : SimpleTabSelectedListener() { - override fun onTabSelected(tab: TabLayout.Tab?) { - val isFilterTab = tab?.position == filterTabIndex + binding.tabs.addOnTabSelectedListener( + object : SimpleTabSelectedListener() { + override fun onTabSelected(tab: TabLayout.Tab?) { + val isFilterTab = tab?.position == filterTabIndex - // Remove dimmed backdrop so color filter changes can be previewed - backgroundDimAnimator.run { - if (isFilterTab) { - if (animatedFraction < 1f) { - start() + // Remove dimmed backdrop so color filter changes can be previewed + backgroundDimAnimator.run { + if (isFilterTab) { + if (animatedFraction < 1f) { + start() + } + } else if (animatedFraction > 0f) { + reverse() } - } else if (animatedFraction > 0f) { - reverse() + } + + // Hide toolbars + if (activity.menuVisible != !isFilterTab) { + activity.setMenuVisibility(!isFilterTab) } } - - // Hide toolbars - if (activity.menuVisible != !isFilterTab) { - activity.setMenuVisibility(!isFilterTab) - } - } - }, + }, ) if (showColorFilterSettings) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderPageImageView.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderPageImageView.kt index be427bab36..c6f114088c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderPageImageView.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderPageImageView.kt @@ -249,6 +249,7 @@ open class ReaderPageImageView @JvmOverloads constructor( ZoomStartPosition.LEFT -> setScaleAndCenter(scale, PointF(0F, 0F)) ZoomStartPosition.RIGHT -> setScaleAndCenter(scale, PointF(sWidth.toFloat(), 0F)) ZoomStartPosition.CENTER -> setScaleAndCenter(scale, center.also { it?.y = 0F }) + else -> {} } } @@ -310,7 +311,7 @@ open class ReaderPageImageView @JvmOverloads constructor( return true } - override fun onSingleTapConfirmed(e: MotionEvent?): Boolean { + override fun onSingleTapConfirmed(e: MotionEvent): Boolean { this@ReaderPageImageView.onViewClicked() return super.onSingleTapConfirmed(e) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonFrame.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonFrame.kt index db711efbbe..323cf0f63f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonFrame.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonFrame.kt @@ -44,7 +44,7 @@ class WebtoonFrame(context: Context) : FrameLayout(context) { * Scale listener used to delegate events to the recycler view. */ inner class ScaleListener : ScaleGestureDetector.SimpleOnScaleGestureListener() { - override fun onScaleBegin(detector: ScaleGestureDetector?): Boolean { + override fun onScaleBegin(detector: ScaleGestureDetector): Boolean { recycler?.onScaleBegin() return true } @@ -63,13 +63,13 @@ class WebtoonFrame(context: Context) : FrameLayout(context) { * Fling listener used to delegate events to the recycler view. */ inner class FlingListener : GestureDetector.SimpleOnGestureListener() { - override fun onDown(e: MotionEvent?): Boolean { + override fun onDown(e: MotionEvent): Boolean { return true } override fun onFling( - e1: MotionEvent?, - e2: MotionEvent?, + e1: MotionEvent, + e2: MotionEvent, velocityX: Float, velocityY: Float, ): Boolean { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsMainController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsMainController.kt index 3c1de2a572..69bbc0f81d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsMainController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsMainController.kt @@ -102,13 +102,13 @@ class SettingsMainController : SettingsController() { searchItem.setOnActionExpandListener( object : MenuItem.OnActionExpandListener { - override fun onMenuItemActionExpand(item: MenuItem?): Boolean { + override fun onMenuItemActionExpand(item: MenuItem): Boolean { preferences.lastSearchQuerySearchSettings().set("") // reset saved search query router.pushController(SettingsSearchController().withFadeTransaction()) return true } - override fun onMenuItemActionCollapse(item: MenuItem?): Boolean { + override fun onMenuItemActionCollapse(item: MenuItem): Boolean { return true } }, diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/search/SettingsSearchController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/search/SettingsSearchController.kt index 462042abc3..7d1049032a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/search/SettingsSearchController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/search/SettingsSearchController.kt @@ -74,11 +74,11 @@ class SettingsSearchController : searchItem.setOnActionExpandListener( object : MenuItem.OnActionExpandListener { - override fun onMenuItemActionExpand(item: MenuItem?): Boolean { + override fun onMenuItemActionExpand(item: MenuItem): Boolean { return true } - override fun onMenuItemActionCollapse(item: MenuItem?): Boolean { + override fun onMenuItemActionCollapse(item: MenuItem): Boolean { router.popCurrentController() return false } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/webview/WebViewActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/webview/WebViewActivity.kt index 794df90970..0ba84840ae 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/webview/WebViewActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/webview/WebViewActivity.kt @@ -166,12 +166,12 @@ class WebViewActivity : BaseActivity() { menu.findItem(R.id.action_web_back).apply { isEnabled = binding.webview.canGoBack() - icon.setTint(if (binding.webview.canGoBack()) iconTintColor else translucentIconTintColor) + icon?.setTint(if (binding.webview.canGoBack()) iconTintColor else translucentIconTintColor) } menu.findItem(R.id.action_web_forward).apply { isEnabled = binding.webview.canGoForward() - icon.setTint(if (binding.webview.canGoForward()) iconTintColor else translucentIconTintColor) + icon?.setTint(if (binding.webview.canGoForward()) iconTintColor else translucentIconTintColor) } return super.onPrepareOptionsMenu(menu) diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/TachiyomiBottomNavigationView.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/TachiyomiBottomNavigationView.kt index 169dad27fd..190fd72e4b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/TachiyomiBottomNavigationView.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/TachiyomiBottomNavigationView.kt @@ -115,12 +115,13 @@ class TachiyomiBottomNavigationView @JvmOverloads constructor( .setInterpolator(interpolator) .setDuration(duration) .applySystemAnimatorScale(context) - .setListener(object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator?) { - currentAnimator = null - postInvalidate() - } - }, + .setListener( + object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + currentAnimator = null + postInvalidate() + } + }, ) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/preference/ThemesPreference.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/preference/ThemesPreference.kt index e8769990ad..576859fe6b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/preference/ThemesPreference.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/preference/ThemesPreference.kt @@ -37,12 +37,13 @@ class ThemesPreference @JvmOverloads constructor(context: Context, attrs: Attrib recycler?.adapter = adapter // Retain scroll position on activity recreate after changing theme - recycler?.addOnScrollListener(object : RecyclerView.OnScrollListener() { - override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { - super.onScrolled(recyclerView, dx, dy) - lastScrollPosition = recyclerView.computeHorizontalScrollOffset() - } - }, + recycler?.addOnScrollListener( + object : RecyclerView.OnScrollListener() { + override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { + super.onScrolled(recyclerView, dx, dy) + lastScrollPosition = recyclerView.computeHorizontalScrollOffset() + } + }, ) lastScrollPosition?.let { scrollToOffset(it) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/sheet/BottomSheetViewPager.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/sheet/BottomSheetViewPager.kt index f72eb31126..91b7107576 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/sheet/BottomSheetViewPager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/sheet/BottomSheetViewPager.kt @@ -45,11 +45,12 @@ class BottomSheetViewPager @JvmOverloads constructor( } init { - addOnPageChangeListener(object : SimpleOnPageChangeListener() { - override fun onPageSelected(position: Int) { - requestLayout() - } - }, + addOnPageChangeListener( + object : SimpleOnPageChangeListener() { + override fun onPageSelected(position: Int) { + requestLayout() + } + }, ) } } diff --git a/buildSrc/src/main/kotlin/AndroidConfig.kt b/buildSrc/src/main/kotlin/AndroidConfig.kt index ccf121dc28..f0cf6fc23b 100644 --- a/buildSrc/src/main/kotlin/AndroidConfig.kt +++ b/buildSrc/src/main/kotlin/AndroidConfig.kt @@ -1,5 +1,5 @@ object AndroidConfig { - const val compileSdk = 32 + const val compileSdk = 33 const val minSdk = 23 const val targetSdk = 29 const val ndk = "22.1.7171670" diff --git a/gradle/androidx.versions.toml b/gradle/androidx.versions.toml index 867b9d2b43..f9a7c210e8 100644 --- a/gradle/androidx.versions.toml +++ b/gradle/androidx.versions.toml @@ -1,6 +1,6 @@ [versions] agp_version = "7.2.2" -lifecycle_version = "2.5.0" +lifecycle_version = "2.5.1" [libraries] annotation = "androidx.annotation:annotation:1.4.0" @@ -10,7 +10,7 @@ constraintlayout = "androidx.constraintlayout:constraintlayout:2.1.4" coordinatorlayout = "androidx.coordinatorlayout:coordinatorlayout:1.2.0" corektx = "androidx.core:core-ktx:1.8.0" splashscreen = "androidx.core:core-splashscreen:1.0.0-alpha02" -recyclerview = "androidx.recyclerview:recyclerview:1.3.0-beta01" +recyclerview = "androidx.recyclerview:recyclerview:1.3.0-beta02" swiperefreshlayout = "androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01" viewpager = "androidx.viewpager:viewpager:1.1.0-alpha01" @@ -27,4 +27,4 @@ workmanager = ["work-runtime", "guava"] [plugins] application = { id = "com.android.application", version.ref = "agp_version" } -library = { id = "com.android.library", version.ref = "agp_version" } \ No newline at end of file +library = { id = "com.android.library", version.ref = "agp_version" } diff --git a/gradle/kotlinx.versions.toml b/gradle/kotlinx.versions.toml index 268e66b5f4..8d3cce7c16 100644 --- a/gradle/kotlinx.versions.toml +++ b/gradle/kotlinx.versions.toml @@ -1,7 +1,7 @@ [versions] -kotlin_version = "1.6.20" -coroutines_version = "1.6.1" -serialization_version = "1.3.2" +kotlin_version = "1.7.10" +coroutines_version = "1.6.4" +serialization_version = "1.3.3" [libraries] reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin_version" } @@ -12,12 +12,11 @@ coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-androi serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization_version" } serialization-protobuf = { module = "org.jetbrains.kotlinx:kotlinx-serialization-protobuf", version.ref = "serialization_version" } -serialization-gradle = { module = "org.jetbrains.kotlin:kotlin-serialization", version.ref = "kotlin_version"} +serialization-gradle = { module = "org.jetbrains.kotlin:kotlin-serialization", version.ref = "kotlin_version" } [bundles] coroutines = ["coroutines-core", "coroutines-android"] -serialization = ["serialization-json","serialization-protobuf"] +serialization = ["serialization-json", "serialization-protobuf"] [plugins] - -android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin_version"} \ No newline at end of file +android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin_version" } \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 417351534a..0e1b8a7a25 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,14 +2,15 @@ aboutlib_version = "8.9.4" okhttp_version = "4.10.0" nucleus_version = "3.0.0" -coil_version = "2.0.0-rc03" -conductor_version = "3.1.5" +coil_version = "2.1.0" +conductor_version = "3.1.7" flowbinding_version = "1.2.0" shizuku_version = "12.1.0" +leakcanary = "2.9.1" [libraries] android-shortcut-gradle = "com.github.zellius:android-shortcut-gradle-plugin:0.1.2" -google-services-gradle = "com.google.gms:google-services:4.3.10" +google-services-gradle = "com.google.gms:google-services:4.3.13" tachiyomi-api = "org.tachiyomi:source-api:1.1" @@ -33,13 +34,13 @@ jsoup = "org.jsoup:jsoup:1.14.3" disklrucache = "com.jakewharton:disklrucache:2.0.2" unifile = "com.github.tachiyomiorg:unifile:17bec43" -junrar = "com.github.junrar:junrar:7.5.2" +junrar = "com.github.junrar:junrar:7.5.3" -sqlitektx = "androidx.sqlite:sqlite-ktx:2.3.0-alpha02" +sqlitektx = "androidx.sqlite:sqlite-ktx:2.3.0-alpha03" sqlite-android = "com.github.requery:sqlite-android:3.36.0" preferencektx = "androidx.preference:preference-ktx:1.2.0" -flowpreferences = "com.fredporciuncula:flow-preferences:1.7.0" +flowpreferences = "com.fredporciuncula:flow-preferences:1.8.0" nucleus-core = { module = "info.android15.nucleus:nucleus", version.ref = "nucleus_version" } nucleus-supportv7 = { module = "info.android15.nucleus:nucleus-support-v7", version.ref = "nucleus_version" } @@ -77,8 +78,8 @@ flowbinding-viewpager = { module = "io.github.reactivecircus.flowbinding:flowbin logcat = "com.squareup.logcat:logcat:0.1" -acra-http = "ch.acra:acra-http:5.9.5" -firebase-analytics = "com.google.firebase:firebase-analytics-ktx:21.0.0" +acra-http = "ch.acra:acra-http:5.9.6" +firebase-analytics = "com.google.firebase:firebase-analytics-ktx:21.1.0" aboutlibraries-core = { module = "com.mikepenz:aboutlibraries-core", version.ref = "aboutlib_version" } aboutlibraries-gradle = { module = "com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin", version.ref = "aboutlib_version" } @@ -86,21 +87,22 @@ aboutlibraries-gradle = { module = "com.mikepenz.aboutlibraries.plugin:aboutlibr shizuku-api = { module = "dev.rikka.shizuku:api", version.ref = "shizuku_version" } shizuku-provider = { module = "dev.rikka.shizuku:provider", version.ref = "shizuku_version" } +leakcanary-android = { module = "com.squareup.leakcanary:leakcanary-android", version.ref = "leakcanary" } +leakcanary-plumber = { module = "com.squareup.leakcanary:plumber-android", version.ref = "leakcanary" } + junit = "org.junit.jupiter:junit-jupiter:5.9.0" -leakcanary-android = "com.squareup.leakcanary:leakcanary-android:2.7" - [bundles] -reactivex = ["rxandroid","rxjava","rxrelay"] -okhttp = ["okhttp-core","okhttp-logging","okhttp-dnsoverhttps"] +reactivex = ["rxandroid", "rxjava", "rxrelay"] +okhttp = ["okhttp-core", "okhttp-logging", "okhttp-dnsoverhttps"] js-engine = ["quickjs-android", "duktape-android"] sqlite = ["sqlitektx", "sqlite-android"] -nucleus = ["nucleus-core","nucleus-supportv7"] -coil = ["coil-core","coil-gif",] -flowbinding = ["flowbinding-android","flowbinding-appcompat","flowbinding-recyclerview","flowbinding-swiperefreshlayout","flowbinding-viewpager"] -conductor = ["conductor-core","conductor-viewpager","conductor-support-preference"] -shizuku = ["shizuku-api","shizuku-provider"] +nucleus = ["nucleus-core", "nucleus-supportv7"] +coil = ["coil-core", "coil-gif"] +flowbinding = ["flowbinding-android", "flowbinding-appcompat", "flowbinding-recyclerview", "flowbinding-swiperefreshlayout", "flowbinding-viewpager"] +conductor = ["conductor-core", "conductor-viewpager", "conductor-support-preference"] +shizuku = ["shizuku-api", "shizuku-provider"] [plugins] -kotlinter = { id = "org.jmailen.kotlinter", version = "3.10.0"} -versionsx = { id = "com.github.ben-manes.versions", version = "0.42.0"} \ No newline at end of file +kotlinter = { id = "org.jmailen.kotlinter", version = "3.11.1" } +versionsx = { id = "com.github.ben-manes.versions", version = "0.42.0" } \ No newline at end of file From e0d23cd688070ab729da1aea2bb9d29dcad331c8 Mon Sep 17 00:00:00 2001 From: arkon Date: Sat, 9 Jul 2022 00:00:18 -0400 Subject: [PATCH 05/26] Use Material3 switches in XML layouts (cherry picked from commit da7a64b40dda3368565b329e519da3283c797131) --- app/src/main/res/layout/pref_settings.xml | 2 +- .../layout/pref_widget_switch_material.xml | 2 +- .../layout/reader_color_filter_settings.xml | 24 +++++------ .../res/layout/reader_general_settings.xml | 42 +++++++++---------- .../main/res/layout/reader_pager_settings.xml | 30 ++++++------- .../res/layout/reader_webtoon_settings.xml | 18 ++++---- app/src/main/res/values/themes.xml | 2 + gradle/libs.versions.toml | 2 +- 8 files changed, 62 insertions(+), 60 deletions(-) diff --git a/app/src/main/res/layout/pref_settings.xml b/app/src/main/res/layout/pref_settings.xml index a4b6f6ebac..413139331a 100644 --- a/app/src/main/res/layout/pref_settings.xml +++ b/app/src/main/res/layout/pref_settings.xml @@ -17,7 +17,7 @@ app:tint="?attr/colorOnBackground" /> - diff --git a/app/src/main/res/layout/pref_widget_switch_material.xml b/app/src/main/res/layout/pref_widget_switch_material.xml index 65bcaf6dc1..53dea7ec00 100644 --- a/app/src/main/res/layout/pref_widget_switch_material.xml +++ b/app/src/main/res/layout/pref_widget_switch_material.xml @@ -1,5 +1,5 @@ - - @@ -61,12 +61,12 @@ - - - diff --git a/app/src/main/res/layout/reader_general_settings.xml b/app/src/main/res/layout/reader_general_settings.xml index 00e248ec1b..1d9fe94a32 100644 --- a/app/src/main/res/layout/reader_general_settings.xml +++ b/app/src/main/res/layout/reader_general_settings.xml @@ -17,68 +17,68 @@ android:entries="@array/reader_themes" app:title="@string/pref_reader_theme" /> - - - - - - - diff --git a/app/src/main/res/layout/reader_pager_settings.xml b/app/src/main/res/layout/reader_pager_settings.xml index b792de12d8..086c2d1a55 100644 --- a/app/src/main/res/layout/reader_pager_settings.xml +++ b/app/src/main/res/layout/reader_pager_settings.xml @@ -37,12 +37,12 @@ android:entries="@array/image_scale_type" app:title="@string/pref_image_scale_type" /> - @@ -53,39 +53,39 @@ android:entries="@array/zoom_start" app:title="@string/pref_zoom_start" /> - - - - - - - @style/Widget.Tachiyomi.BottomNavigationView @style/Widget.Tachiyomi.NavigationRailView @style/Widget.Tachiyomi.Switch + @style/Widget.Material3.CompoundButton.MaterialSwitch + @style/Widget.Tachiyomi.Switch @style/Widget.Tachiyomi.Slider @style/Widget.Material3.CardView.Elevated diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 0e1b8a7a25..baa29f9158 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -57,7 +57,7 @@ natural-comparator = "com.github.gpanther:java-nat-sort:natural-comparator-1.1" markwon = "io.noties.markwon:core:4.6.2" -material = "com.google.android.material:material:1.7.0-alpha01" +material = "com.google.android.material:material:1.7.0-alpha02" androidprocessbutton = "com.github.dmytrodanylyk.android-process-button:library:1.0.4" flexible-adapter-core = "com.github.arkon.FlexibleAdapter:flexible-adapter:c8013533" flexible-adapter-ui = "com.github.arkon.FlexibleAdapter:flexible-adapter-ui:c8013533" From ac4f98e152c66c388dd5f098725f4196df3a7339 Mon Sep 17 00:00:00 2001 From: arkon Date: Sat, 13 Aug 2022 13:08:16 -0400 Subject: [PATCH 06/26] Configure SQLite - Turn on `foreign_keys` to cascade on delete properly - Turn on `journal_mode` and set `synchronous` to NORMAL which may help performance for larger libraries Based on d977b89af1f2a8850437ebd978535fc3fbfd257e Co-authored-by: ghostbear --- .../eu/kanade/tachiyomi/data/database/DbOpenCallback.kt | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/DbOpenCallback.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/DbOpenCallback.kt index 31702be039..22b992fec4 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/DbOpenCallback.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/DbOpenCallback.kt @@ -98,5 +98,14 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) { override fun onConfigure(db: SupportSQLiteDatabase) { db.setForeignKeyConstraintsEnabled(true) + setPragma(db, "foreign_keys = ON") + setPragma(db, "journal_mode = WAL") + setPragma(db, "synchronous = NORMAL") + } + + private fun setPragma(db: SupportSQLiteDatabase, pragma: String) { + val cursor = db.query("PRAGMA $pragma") + cursor.moveToFirst() + cursor.close() } } From c7e44aa22f61e70afb278e00b3e751bf42ec9847 Mon Sep 17 00:00:00 2001 From: arkon Date: Sat, 9 Jul 2022 17:51:58 -0400 Subject: [PATCH 07/26] Replace deprecated ACTION_MEDIA_SCANNER_SCAN_FILE intent (cherry picked from commit 0b4f3f553263281333c6475cdd1a9aea414ce877) --- .../data/notification/NotificationReceiver.kt | 3 ++- .../eu/kanade/tachiyomi/data/saver/ImageSaver.kt | 3 ++- .../eu/kanade/tachiyomi/util/storage/DiskUtil.kt | 15 ++------------- 3 files changed, 6 insertions(+), 15 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt b/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt index 26fc7dea77..2b1d0d0b0d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt @@ -7,6 +7,7 @@ import android.content.Intent import android.net.Uri import android.os.Build import androidx.core.content.ContextCompat +import androidx.core.net.toUri import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.backup.BackupRestoreService import eu.kanade.tachiyomi.data.database.DatabaseHelper @@ -193,7 +194,7 @@ class NotificationReceiver : BroadcastReceiver() { val file = File(path) file.delete() - DiskUtil.scanMedia(context, file) + DiskUtil.scanMedia(context, file.toUri()) } /** diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/saver/ImageSaver.kt b/app/src/main/java/eu/kanade/tachiyomi/data/saver/ImageSaver.kt index 673b936549..63604f1c0a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/saver/ImageSaver.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/saver/ImageSaver.kt @@ -8,6 +8,7 @@ import android.net.Uri import android.os.Build import android.os.Environment import android.provider.MediaStore +import androidx.core.net.toUri import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.util.storage.DiskUtil import eu.kanade.tachiyomi.util.storage.cacheImageDir @@ -82,7 +83,7 @@ class ImageSaver( } } - DiskUtil.scanMedia(context, destFile) + DiskUtil.scanMedia(context, destFile.toUri()) return destFile.getUriCompat(context) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/storage/DiskUtil.kt b/app/src/main/java/eu/kanade/tachiyomi/util/storage/DiskUtil.kt index 26fa1d93a7..bcbbf25209 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/storage/DiskUtil.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/storage/DiskUtil.kt @@ -1,12 +1,11 @@ package eu.kanade.tachiyomi.util.storage import android.content.Context -import android.content.Intent +import android.media.MediaScannerConnection import android.net.Uri import android.os.Environment import android.os.StatFs import androidx.core.content.ContextCompat -import androidx.core.net.toUri import com.hippo.unifile.UniFile import eu.kanade.tachiyomi.util.lang.Hash import java.io.File @@ -74,21 +73,11 @@ object DiskUtil { } } - /** - * Scans the given file so that it can be shown in gallery apps, for example. - */ - fun scanMedia(context: Context, file: File) { - scanMedia(context, file.toUri()) - } - /** * Scans the given file so that it can be shown in gallery apps, for example. */ fun scanMedia(context: Context, uri: Uri) { - val action = Intent.ACTION_MEDIA_SCANNER_SCAN_FILE - val mediaScanIntent = Intent(action) - mediaScanIntent.data = uri - context.sendBroadcast(mediaScanIntent) + MediaScannerConnection.scanFile(context, arrayOf(uri.path), null, null) } /** From 7c7bd72c8e1170966b396384d190437da12c3e32 Mon Sep 17 00:00:00 2001 From: arkon Date: Thu, 14 Jul 2022 23:01:19 -0400 Subject: [PATCH 08/26] Make default user agent string configurable (cherry picked from commit 4ee1d72b6f8278d84da6f75d218a51261d175e18) --- .../data/preference/PreferenceKeys.kt | 2 ++ .../data/preference/PreferencesHelper.kt | 2 ++ .../kanade/tachiyomi/network/NetworkHelper.kt | 4 ++++ .../interceptor/CloudflareInterceptor.kt | 3 +-- .../interceptor/UserAgentInterceptor.kt | 8 +++++-- .../tachiyomi/source/online/HttpSource.kt | 7 ++---- .../ui/setting/SettingsAdvancedController.kt | 23 +++++++++++++++++++ app/src/main/res/values/strings.xml | 2 ++ 8 files changed, 42 insertions(+), 9 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt index 026f40722f..83bdac3717 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt @@ -63,6 +63,8 @@ object PreferenceKeys { const val dohProvider = "doh_provider" + const val defaultUserAgent = "default_user_agent" + const val defaultChapterFilterByRead = "default_chapter_filter_by_read" const val defaultChapterFilterByDownloaded = "default_chapter_filter_by_downloaded" diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt index 9fbe9fe03d..92de28c6e6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt @@ -297,6 +297,8 @@ class PreferencesHelper(val context: Context) { fun dohProvider() = prefs.getInt(Keys.dohProvider, -1) + fun defaultUserAgent() = flowPrefs.getString(Keys.defaultUserAgent, "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.5005.124 Safari/537.36 Edg/102.0.1245.44") + fun lastSearchQuerySearchSettings() = flowPrefs.getString("last_search_query", "") fun filterChapterByRead() = prefs.getInt(Keys.defaultChapterFilterByRead, Manga.SHOW_ALL) diff --git a/app/src/main/java/eu/kanade/tachiyomi/network/NetworkHelper.kt b/app/src/main/java/eu/kanade/tachiyomi/network/NetworkHelper.kt index ea3106cb86..4eaa5e839f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/network/NetworkHelper.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/network/NetworkHelper.kt @@ -59,4 +59,8 @@ class NetworkHelper(context: Context) { .addInterceptor(CloudflareInterceptor(context)) .build() } + + val defaultUserAgent by lazy { + preferences.defaultUserAgent().get() + } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/network/interceptor/CloudflareInterceptor.kt b/app/src/main/java/eu/kanade/tachiyomi/network/interceptor/CloudflareInterceptor.kt index b697a9f167..2e43e01b71 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/network/interceptor/CloudflareInterceptor.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/network/interceptor/CloudflareInterceptor.kt @@ -9,7 +9,6 @@ import android.widget.Toast import androidx.core.content.ContextCompat import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.network.NetworkHelper -import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.util.lang.launchUI import eu.kanade.tachiyomi.util.system.DeviceUtil import eu.kanade.tachiyomi.util.system.WebViewClientCompat @@ -109,7 +108,7 @@ class CloudflareInterceptor(private val context: Context) : Interceptor { // Avoid sending empty User-Agent, Chromium WebView will reset to default if empty webview.settings.userAgentString = request.header("User-Agent") - ?: HttpSource.DEFAULT_USER_AGENT + ?: networkHelper.defaultUserAgent webview.webViewClient = object : WebViewClientCompat() { override fun onPageFinished(view: WebView, url: String) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/network/interceptor/UserAgentInterceptor.kt b/app/src/main/java/eu/kanade/tachiyomi/network/interceptor/UserAgentInterceptor.kt index 5a3789eeca..e5d1c26566 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/network/interceptor/UserAgentInterceptor.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/network/interceptor/UserAgentInterceptor.kt @@ -1,10 +1,14 @@ package eu.kanade.tachiyomi.network.interceptor -import eu.kanade.tachiyomi.source.online.HttpSource +import eu.kanade.tachiyomi.network.NetworkHelper import okhttp3.Interceptor import okhttp3.Response +import uy.kohesive.injekt.injectLazy class UserAgentInterceptor : Interceptor { + + private val networkHelper: NetworkHelper by injectLazy() + override fun intercept(chain: Interceptor.Chain): Response { val originalRequest = chain.request() @@ -12,7 +16,7 @@ class UserAgentInterceptor : Interceptor { val newRequest = originalRequest .newBuilder() .removeHeader("User-Agent") - .addHeader("User-Agent", HttpSource.DEFAULT_USER_AGENT) + .addHeader("User-Agent", networkHelper.defaultUserAgent) .build() chain.proceed(newRequest) } else { diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/online/HttpSource.kt b/app/src/main/java/eu/kanade/tachiyomi/source/online/HttpSource.kt index 0a390de7cc..1868581ea2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/online/HttpSource.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/online/HttpSource.kt @@ -15,6 +15,7 @@ import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.Response import rx.Observable +import uy.kohesive.injekt.api.get import uy.kohesive.injekt.injectLazy import java.net.URI import java.net.URISyntaxException @@ -67,7 +68,7 @@ abstract class HttpSource : CatalogueSource { * Headers builder for requests. Implementations can override this method for custom headers. */ protected open fun headersBuilder() = Headers.Builder().apply { - add("User-Agent", DEFAULT_USER_AGENT) + add("User-Agent", network.defaultUserAgent) } /** @@ -369,8 +370,4 @@ abstract class HttpSource : CatalogueSource { * Returns the list of filters for the source. */ override fun getFilterList() = FilterList() - - companion object { - const val DEFAULT_USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.5005.124 Safari/537.36 Edg/102.0.1245.44" - } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAdvancedController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAdvancedController.kt index 666058b926..f93fd8b93e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAdvancedController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAdvancedController.kt @@ -32,6 +32,7 @@ import eu.kanade.tachiyomi.util.lang.launchIO import eu.kanade.tachiyomi.util.lang.withUIContext import eu.kanade.tachiyomi.util.preference.bindTo import eu.kanade.tachiyomi.util.preference.defaultValue +import eu.kanade.tachiyomi.util.preference.editTextPreference import eu.kanade.tachiyomi.util.preference.entriesRes import eu.kanade.tachiyomi.util.preference.intListPreference import eu.kanade.tachiyomi.util.preference.listPreference @@ -210,6 +211,28 @@ class SettingsAdvancedController : SettingsController() { true } } + editTextPreference { + key = Keys.defaultUserAgent + titleRes = R.string.pref_user_agent_string + text = preferences.defaultUserAgent().get() + summary = network.defaultUserAgent + + onChange { + activity?.toast(R.string.requires_app_restart) + true + } + } + if (preferences.defaultUserAgent().isSet()) { + preference { + key = "pref_reset_user_agent" + titleRes = R.string.pref_reset_user_agent_string + + onClick { + preferences.defaultUserAgent().delete() + activity?.toast(R.string.requires_app_restart) + } + } + } } preferenceCategory { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2632544b80..7bdbc96b3d 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -466,6 +466,8 @@ Network Clear cookies DNS over HTTPS (DoH) + Default user agent string + Reset default user agent string Requires app restart to take effect Cookies cleared Data From dd676b6d14c1bf4ac3d49f5be29b98c4c790457d Mon Sep 17 00:00:00 2001 From: f1998f1998 <71004883+f1998f1998@users.noreply.github.com> Date: Mon, 18 Jul 2022 23:22:09 +0600 Subject: [PATCH 09/26] fix concurrent download (#7552) * Fix concurrent download * lower Concurrency * artist Update app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt Co-authored-by: Vetle Ledaal Co-authored-by: Vetle Ledaal (cherry picked from commit b635f02d93502f1021f0fe87533dfd96d052ed2f) --- .../main/java/eu/kanade/tachiyomi/data/download/Downloader.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt index 485c0122cf..1653fe0ef4 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt @@ -341,8 +341,8 @@ class Downloader( // Get all the URLs to the source images, fetch pages if necessary .flatMap { download.source.fetchAllImageUrlsFromPageList(it) } // Start downloading images, consider we can have downloaded images already - // Concurrently do 5 pages at a time - .flatMap({ page -> getOrDownloadImage(page, download, tmpDir) }, 5) + // Concurrently do 2 pages at a time + .flatMap({ page -> getOrDownloadImage(page, download, tmpDir).subscribeOn(Schedulers.io()) }, 2) .onBackpressureLatest() // Do when page is downloaded. .doOnNext { notifier.onProgressChange(download) } From e296d56e098482ecac082d950cf9c62c8a6ebf98 Mon Sep 17 00:00:00 2001 From: stevenyomi <95685115+stevenyomi@users.noreply.github.com> Date: Wed, 20 Jul 2022 21:10:41 +0800 Subject: [PATCH 10/26] Fix image MIME issues that cause download errors (#7562) * Downloader: ignore non-image MIME to prevent .bin extensions * ProgressResponseBody: allow null content type Co-authored-by: anenasa <84259093+anenasa@users.noreply.github.com> Co-authored-by: anenasa <84259093+anenasa@users.noreply.github.com> (cherry picked from commit 3547d0142f96c44da7fe1ee5bd6424fea679efa6) --- .../main/java/eu/kanade/tachiyomi/data/download/Downloader.kt | 2 +- .../java/eu/kanade/tachiyomi/network/ProgressResponseBody.kt | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt index 1653fe0ef4..3221168e76 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt @@ -465,7 +465,7 @@ class Downloader( */ private fun getImageExtension(response: Response, file: UniFile): String { // Read content type if available. - val mime = response.body?.contentType()?.let { ct -> "${ct.type}/${ct.subtype}" } + val mime = response.body?.contentType()?.run { if (type == "image") "image/$subtype" else null } // Else guess from the uri. ?: context.contentResolver.getType(file.uri) // Else read magic numbers. diff --git a/app/src/main/java/eu/kanade/tachiyomi/network/ProgressResponseBody.kt b/app/src/main/java/eu/kanade/tachiyomi/network/ProgressResponseBody.kt index ff56520b55..72248f17b7 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/network/ProgressResponseBody.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/network/ProgressResponseBody.kt @@ -15,8 +15,8 @@ class ProgressResponseBody(private val responseBody: ResponseBody, private val p source(responseBody.source()).buffer() } - override fun contentType(): MediaType { - return responseBody.contentType()!! + override fun contentType(): MediaType? { + return responseBody.contentType() } override fun contentLength(): Long { From 85f2996ae9bbbfac18fb6a836d9334574812f8c9 Mon Sep 17 00:00:00 2001 From: AntsyLich <59261191+AntsyLich@users.noreply.github.com> Date: Fri, 22 Jul 2022 08:23:59 +0600 Subject: [PATCH 11/26] Fix logic of app unlock (#7569) (cherry picked from commit 8ea05e852efd621ee987c7e45d6db64a083eeffd) --- app/src/main/java/eu/kanade/tachiyomi/App.kt | 2 ++ .../eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt | 2 +- .../kanade/tachiyomi/ui/base/delegate/SecureActivityDelegate.kt | 2 +- .../main/java/eu/kanade/tachiyomi/ui/security/UnlockActivity.kt | 2 -- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/App.kt b/app/src/main/java/eu/kanade/tachiyomi/App.kt index 32a290be7e..c14a687a52 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/App.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/App.kt @@ -52,6 +52,7 @@ import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import uy.kohesive.injekt.injectLazy import java.security.Security +import java.util.Date open class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory { @@ -148,6 +149,7 @@ open class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory { } override fun onStop(owner: LifecycleOwner) { + preferences.lastAppClosed().set(Date().time) if (!AuthenticatorUtil.isAuthenticating && preferences.lockAppAfter().get() >= 0) { SecureActivityDelegate.locked = true } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt index 92de28c6e6..96b0989b27 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt @@ -56,7 +56,7 @@ class PreferencesHelper(val context: Context) { fun lockAppAfter() = flowPrefs.getInt("lock_app_after", 0) - fun lastAppUnlock() = flowPrefs.getLong("last_app_unlock", 0) + fun lastAppClosed() = flowPrefs.getLong("last_app_closed", 0) fun secureScreen() = flowPrefs.getEnum("secure_screen_v2", Values.SecureScreenMode.INCOGNITO) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/delegate/SecureActivityDelegate.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/delegate/SecureActivityDelegate.kt index 2c8be25042..1518913445 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/base/delegate/SecureActivityDelegate.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/base/delegate/SecureActivityDelegate.kt @@ -68,6 +68,6 @@ class SecureActivityDelegateImpl : SecureActivityDelegate, DefaultLifecycleObser private fun isAppLocked(): Boolean { if (!SecureActivityDelegate.locked) return false return preferences.lockAppAfter().get() <= 0 || - Date().time >= preferences.lastAppUnlock().get() + 60 * 1000 * preferences.lockAppAfter().get() + Date().time >= preferences.lastAppClosed().get() + 60 * 1000 * preferences.lockAppAfter().get() } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/security/UnlockActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/security/UnlockActivity.kt index a066bf64dd..e33760af1c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/security/UnlockActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/security/UnlockActivity.kt @@ -10,7 +10,6 @@ import eu.kanade.tachiyomi.util.system.AuthenticatorUtil import eu.kanade.tachiyomi.util.system.AuthenticatorUtil.startAuthentication import eu.kanade.tachiyomi.util.system.logcat import logcat.LogPriority -import java.util.Date /** * Blank activity with a BiometricPrompt. @@ -39,7 +38,6 @@ class UnlockActivity : BaseActivity() { ) { super.onAuthenticationSucceeded(activity, result) SecureActivityDelegate.locked = false - preferences.lastAppUnlock().set(Date().time) finish() } }, From 87ec71142bd31fd25df4123de2c3bf85080eeea3 Mon Sep 17 00:00:00 2001 From: nzoba <55888232+nzoba@users.noreply.github.com> Date: Sat, 23 Jul 2022 00:55:31 +0200 Subject: [PATCH 12/26] Add downloaded icon in TransitionView when chapter is downloaded (#7575) * Add downloaded icon in TransitionView * Change icon (cherry picked from commit e8b7743826e9bf9aa0d15020b81ca0569cbe999d) --- .../ui/reader/viewer/ReaderTransitionView.kt | 71 +++++++++++++++---- .../viewer/pager/PagerTransitionHolder.kt | 2 +- .../ui/reader/viewer/pager/PagerViewer.kt | 4 ++ .../viewer/webtoon/WebtoonTransitionHolder.kt | 2 +- .../ui/reader/viewer/webtoon/WebtoonViewer.kt | 4 ++ .../main/res/drawable/ic_offline_pin_24dp.xml | 9 +++ 6 files changed, 75 insertions(+), 17 deletions(-) create mode 100644 app/src/main/res/drawable/ic_offline_pin_24dp.xml diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderTransitionView.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderTransitionView.kt index 38786142a2..77ed8348e1 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderTransitionView.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/ReaderTransitionView.kt @@ -1,15 +1,24 @@ package eu.kanade.tachiyomi.ui.reader.viewer import android.content.Context +import android.text.SpannableStringBuilder +import android.text.style.ImageSpan import android.util.AttributeSet import android.view.LayoutInflater import android.widget.LinearLayout +import androidx.core.content.ContextCompat import androidx.core.text.bold import androidx.core.text.buildSpannedString +import androidx.core.text.inSpans import androidx.core.view.isVisible import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.databinding.ReaderTransitionViewBinding +import eu.kanade.tachiyomi.ui.reader.loader.DownloadPageLoader import eu.kanade.tachiyomi.ui.reader.model.ChapterTransition +import eu.kanade.tachiyomi.util.system.dpToPx +import kotlin.math.roundToInt class ReaderTransitionView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : LinearLayout(context, attrs) { @@ -21,10 +30,11 @@ class ReaderTransitionView @JvmOverloads constructor(context: Context, attrs: At layoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT) } - fun bind(transition: ChapterTransition) { + fun bind(transition: ChapterTransition, downloadManager: DownloadManager, manga: Manga?) { + manga ?: return when (transition) { - is ChapterTransition.Prev -> bindPrevChapterTransition(transition) - is ChapterTransition.Next -> bindNextChapterTransition(transition) + is ChapterTransition.Prev -> bindPrevChapterTransition(transition, downloadManager, manga) + is ChapterTransition.Next -> bindNextChapterTransition(transition, downloadManager, manga) } missingChapterWarning(transition) } @@ -32,20 +42,30 @@ class ReaderTransitionView @JvmOverloads constructor(context: Context, attrs: At /** * Binds a previous chapter transition on this view and subscribes to the page load status. */ - private fun bindPrevChapterTransition(transition: ChapterTransition) { - val prevChapter = transition.to + private fun bindPrevChapterTransition( + transition: ChapterTransition, + downloadManager: DownloadManager, + manga: Manga, + ) { + val prevChapter = transition.to?.chapter - val hasPrevChapter = prevChapter != null - binding.lowerText.isVisible = hasPrevChapter - if (hasPrevChapter) { + binding.lowerText.isVisible = prevChapter != null + if (prevChapter != null) { binding.upperText.textAlignment = TEXT_ALIGNMENT_TEXT_START + val isPrevDownloaded = downloadManager.isChapterDownloaded( + prevChapter, + manga, + ) + val isCurrentDownloaded = transition.from.pageLoader is DownloadPageLoader binding.upperText.text = buildSpannedString { bold { append(context.getString(R.string.transition_previous)) } - append("\n${prevChapter!!.chapter.name}") + append("\n${prevChapter.name}") + if (isPrevDownloaded) addDLImageSpan() } binding.lowerText.text = buildSpannedString { bold { append(context.getString(R.string.transition_current)) } append("\n${transition.from.chapter.name}") + if (isCurrentDownloaded) addDLImageSpan() } } else { binding.upperText.textAlignment = TEXT_ALIGNMENT_CENTER @@ -56,20 +76,30 @@ class ReaderTransitionView @JvmOverloads constructor(context: Context, attrs: At /** * Binds a next chapter transition on this view and subscribes to the load status. */ - private fun bindNextChapterTransition(transition: ChapterTransition) { - val nextChapter = transition.to + private fun bindNextChapterTransition( + transition: ChapterTransition, + downloadManager: DownloadManager, + manga: Manga, + ) { + val nextChapter = transition.to?.chapter - val hasNextChapter = nextChapter != null - binding.lowerText.isVisible = hasNextChapter - if (hasNextChapter) { + binding.lowerText.isVisible = nextChapter != null + if (nextChapter != null) { binding.upperText.textAlignment = TEXT_ALIGNMENT_TEXT_START + val isCurrentDownloaded = transition.from.pageLoader is DownloadPageLoader + val isNextDownloaded = downloadManager.isChapterDownloaded( + nextChapter, + manga, + ) binding.upperText.text = buildSpannedString { bold { append(context.getString(R.string.transition_finished)) } append("\n${transition.from.chapter.name}") + if (isCurrentDownloaded) addDLImageSpan() } binding.lowerText.text = buildSpannedString { bold { append(context.getString(R.string.transition_next)) } - append("\n${nextChapter!!.chapter.name}") + append("\n${nextChapter.name}") + if (isNextDownloaded) addDLImageSpan() } } else { binding.upperText.textAlignment = TEXT_ALIGNMENT_CENTER @@ -77,6 +107,17 @@ class ReaderTransitionView @JvmOverloads constructor(context: Context, attrs: At } } + private fun SpannableStringBuilder.addDLImageSpan() { + val icon = ContextCompat.getDrawable(context, R.drawable.ic_offline_pin_24dp)?.mutate() + ?.apply { + val size = binding.lowerText.textSize + 4.dpToPx + setTint(binding.lowerText.currentTextColor) + setBounds(0, 0, size.roundToInt(), size.roundToInt()) + } ?: return + append(" ") + inSpans(ImageSpan(icon)) { append("image") } + } + private fun missingChapterWarning(transition: ChapterTransition) { if (transition.to == null) { binding.warning.isVisible = false diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerTransitionHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerTransitionHolder.kt index 9f712c2da1..4445923d81 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerTransitionHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerTransitionHolder.kt @@ -61,7 +61,7 @@ class PagerTransitionHolder( addView(transitionView) addView(pagesContainer) - transitionView.bind(transition) + transitionView.bind(transition, viewer.downloadManager, viewer.activity.presenter.manga) transition.to?.let { observeStatus(it) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerViewer.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerViewer.kt index 364740fad4..856789fd09 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerViewer.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerViewer.kt @@ -11,6 +11,7 @@ import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.viewpager.widget.ViewPager import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.ui.reader.ReaderActivity import eu.kanade.tachiyomi.ui.reader.model.ChapterTransition import eu.kanade.tachiyomi.ui.reader.model.InsertPage @@ -21,6 +22,7 @@ import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation.NavigationRegion import eu.kanade.tachiyomi.util.system.logcat import kotlinx.coroutines.MainScope import kotlinx.coroutines.cancel +import uy.kohesive.injekt.injectLazy import kotlin.math.min /** @@ -29,6 +31,8 @@ import kotlin.math.min @Suppress("LeakingThis") abstract class PagerViewer(val activity: ReaderActivity) : BaseViewer { + val downloadManager: DownloadManager by injectLazy() + private val scope = MainScope() /** diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonTransitionHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonTransitionHolder.kt index c905f1275a..67e9b5b1d7 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonTransitionHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonTransitionHolder.kt @@ -63,7 +63,7 @@ class WebtoonTransitionHolder( * Binds the given [transition] with this view holder, subscribing to its state. */ fun bind(transition: ChapterTransition) { - transitionView.bind(transition) + transitionView.bind(transition, viewer.downloadManager, viewer.activity.presenter.manga) transition.to?.let { observeStatus(it, transition) } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonViewer.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonViewer.kt index 9ec03ea6dd..0f9f7cb0d2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonViewer.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonViewer.kt @@ -11,6 +11,7 @@ import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.WebtoonLayoutManager +import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.ui.reader.ReaderActivity import eu.kanade.tachiyomi.ui.reader.model.ChapterTransition @@ -24,6 +25,7 @@ import kotlinx.coroutines.cancel import rx.subscriptions.CompositeSubscription import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get +import uy.kohesive.injekt.injectLazy import kotlin.math.max import kotlin.math.min @@ -32,6 +34,8 @@ import kotlin.math.min */ class WebtoonViewer(val activity: ReaderActivity, val isContinuous: Boolean = true) : BaseViewer { + val downloadManager: DownloadManager by injectLazy() + private val scope = MainScope() /** diff --git a/app/src/main/res/drawable/ic_offline_pin_24dp.xml b/app/src/main/res/drawable/ic_offline_pin_24dp.xml new file mode 100644 index 0000000000..9a9cc213f6 --- /dev/null +++ b/app/src/main/res/drawable/ic_offline_pin_24dp.xml @@ -0,0 +1,9 @@ + + + From 5194bdb2294df101b94c37142c10623a7cdd81bc Mon Sep 17 00:00:00 2001 From: arkon Date: Sat, 23 Jul 2022 11:13:52 -0400 Subject: [PATCH 13/26] Show better error when trying to open RARv5 file (cherry picked from commit a84305438853cafa9aff194b89fa221603f2f743) --- .../eu/kanade/tachiyomi/ui/reader/loader/ChapterLoader.kt | 7 ++++++- app/src/main/res/values/strings.xml | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ChapterLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ChapterLoader.kt index d214f889ad..929df8e643 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ChapterLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ChapterLoader.kt @@ -1,6 +1,7 @@ package eu.kanade.tachiyomi.ui.reader.loader import android.content.Context +import com.github.junrar.exception.UnsupportedRarV5Exception import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.download.DownloadManager @@ -83,7 +84,11 @@ class ChapterLoader( when (format) { is LocalSource.Format.Directory -> DirectoryPageLoader(format.file) is LocalSource.Format.Zip -> ZipPageLoader(format.file) - is LocalSource.Format.Rar -> RarPageLoader(format.file) + is LocalSource.Format.Rar -> try { + RarPageLoader(format.file) + } catch (e: UnsupportedRarV5Exception) { + error(context.getString(R.string.loader_rar5_error)) + } is LocalSource.Format.Epub -> EpubPageLoader(format.file) } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 7bdbc96b3d..a85736a223 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -701,6 +701,7 @@ Failed to load pages: %1$s No pages found Source not found + RARv5 format is not supported Skipping %d chapter, either the source is missing it or it has been filtered out Skipping %d chapters, either the source is missing them or they have been filtered out From d4adb664cc17c3da61480d74d22fc806509a7eca Mon Sep 17 00:00:00 2001 From: arkon Date: Sat, 23 Jul 2022 11:14:34 -0400 Subject: [PATCH 14/26] Avoid catastrophic failure when cover can't be created in local source (fixes #7577) (cherry picked from commit d6977e5676377f6090c0e0b4eb15fd043fa01e11) --- .../eu/kanade/tachiyomi/source/LocalSource.kt | 74 ++++++++++--------- 1 file changed, 38 insertions(+), 36 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/LocalSource.kt b/app/src/main/java/eu/kanade/tachiyomi/source/LocalSource.kt index a03e8d6fa3..e8709e4686 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/LocalSource.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/LocalSource.kt @@ -4,7 +4,6 @@ import android.content.Context import com.github.junrar.Archive import com.hippo.unifile.UniFile import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.source.model.Filter import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.MangasPage @@ -19,6 +18,7 @@ import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder import eu.kanade.tachiyomi.util.storage.DiskUtil import eu.kanade.tachiyomi.util.storage.EpubFile import eu.kanade.tachiyomi.util.system.ImageUtil +import eu.kanade.tachiyomi.util.system.logcat import kotlinx.coroutines.runBlocking import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonObject @@ -27,11 +27,10 @@ import kotlinx.serialization.json.decodeFromStream import kotlinx.serialization.json.intOrNull import kotlinx.serialization.json.jsonArray import kotlinx.serialization.json.jsonPrimitive +import logcat.LogPriority import rx.Observable import tachiyomi.source.model.ChapterInfo import tachiyomi.source.model.MangaInfo -import uy.kohesive.injekt.Injekt -import uy.kohesive.injekt.api.get import uy.kohesive.injekt.injectLazy import java.io.File import java.io.FileInputStream @@ -41,7 +40,6 @@ import java.util.zip.ZipFile class LocalSource( private val context: Context, - private val coverCache: CoverCache = Injekt.get(), ) : CatalogueSource, UnmeteredSource { private val json: Json by injectLazy() @@ -254,41 +252,46 @@ class LocalSource( } private fun updateCover(chapter: SChapter, manga: SManga): File? { - return when (val format = getFormat(chapter)) { - is Format.Directory -> { - val entry = format.file.listFiles() - ?.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) } - ?.find { !it.isDirectory && ImageUtil.isImage(it.name) { FileInputStream(it) } } + return try { + when (val format = getFormat(chapter)) { + is Format.Directory -> { + val entry = format.file.listFiles() + ?.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) } + ?.find { !it.isDirectory && ImageUtil.isImage(it.name) { FileInputStream(it) } } - entry?.let { updateCover(context, manga, it.inputStream()) } - } - is Format.Zip -> { - ZipFile(format.file).use { zip -> - val entry = zip.entries().toList() - .sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) } - .find { !it.isDirectory && ImageUtil.isImage(it.name) { zip.getInputStream(it) } } + entry?.let { updateCover(context, manga, it.inputStream()) } + } + is Format.Zip -> { + ZipFile(format.file).use { zip -> + val entry = zip.entries().toList() + .sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) } + .find { !it.isDirectory && ImageUtil.isImage(it.name) { zip.getInputStream(it) } } - entry?.let { updateCover(context, manga, zip.getInputStream(it)) } - } - } - is Format.Rar -> { - Archive(format.file).use { archive -> - val entry = archive.fileHeaders - .sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) } - .find { !it.isDirectory && ImageUtil.isImage(it.fileName) { archive.getInputStream(it) } } - - entry?.let { updateCover(context, manga, archive.getInputStream(it)) } - } - } - is Format.Epub -> { - EpubFile(format.file).use { epub -> - val entry = epub.getImagesFromPages() - .firstOrNull() - ?.let { epub.getEntry(it) } - - entry?.let { updateCover(context, manga, epub.getInputStream(it)) } + entry?.let { updateCover(context, manga, zip.getInputStream(it)) } + } + } + is Format.Rar -> { + Archive(format.file).use { archive -> + val entry = archive.fileHeaders + .sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) } + .find { !it.isDirectory && ImageUtil.isImage(it.fileName) { archive.getInputStream(it) } } + + entry?.let { updateCover(context, manga, archive.getInputStream(it)) } + } + } + is Format.Epub -> { + EpubFile(format.file).use { epub -> + val entry = epub.getImagesFromPages() + .firstOrNull() + ?.let { epub.getEntry(it) } + + entry?.let { updateCover(context, manga, epub.getInputStream(it)) } + } } } + } catch (e: Throwable) { + logcat(LogPriority.ERROR, e) { "Error updating cover for ${manga.title}" } + null } .also { coverCache.clearMemoryCache() } } @@ -366,7 +369,6 @@ class LocalSource( } } - // Create a .nomedia file DiskUtil.createNoMediaFile(UniFile.fromFile(mangaDir), context) manga.thumbnail_url = coverFile.absolutePath From 650c2dc6e787313e56d34fdd96bb0bbced821e8c Mon Sep 17 00:00:00 2001 From: MatchaSoba <76941874+MatchaSoba@users.noreply.github.com> Date: Sat, 30 Jul 2022 23:53:25 +0800 Subject: [PATCH 15/26] Fix logic for searchWithGenre (#7559) (cherry picked from commit b563e85c3b744595272718f7e82e3272e2a7c57b) --- .../source/browse/BrowseSourceController.kt | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceController.kt index 313ccf2736..ab20ee493d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceController.kt @@ -343,11 +343,11 @@ open class BrowseSourceController(bundle: Bundle) : * @param genreName the name of the genre */ fun searchWithGenre(genreName: String) { - presenter.sourceFilters = presenter.source.getFilterList() + val defaultFilters = presenter.source.getFilterList() - var filterList: FilterList? = null + var genreExists = false - filter@ for (sourceFilter in presenter.sourceFilters) { + filter@ for (sourceFilter in defaultFilters) { if (sourceFilter is Filter.Group<*>) { for (filter in sourceFilter.state) { if (filter is Filter<*> && filter.name.equals(genreName, true)) { @@ -356,7 +356,7 @@ open class BrowseSourceController(bundle: Bundle) : is Filter.CheckBox -> filter.state = true else -> {} } - filterList = presenter.sourceFilters + genreExists = true break@filter } } @@ -366,19 +366,20 @@ open class BrowseSourceController(bundle: Bundle) : if (index != -1) { sourceFilter.state = index - filterList = presenter.sourceFilters + genreExists = true break } } } - if (filterList != null) { + if (genreExists) { + presenter.sourceFilters = defaultFilters filterSheet?.setFilters(presenter.filterItems) showProgressBar() adapter?.clear() - presenter.restartPager("", filterList) + presenter.restartPager("", defaultFilters) } else { searchWithQuery(genreName) } From b3a11eca0f998096814e3d9ce24051bc561af4f3 Mon Sep 17 00:00:00 2001 From: Andreas Date: Sun, 31 Jul 2022 17:17:43 +0200 Subject: [PATCH 16/26] Remove deprecated LibrarySort (#7659) * Remove deprecated LibrarySort * Apply suggestions from code review (cherry picked from commit 58acf0a8aa3aa37b5d9c63d99987a05ee2f8790f) --- .../java/eu/kanade/tachiyomi/Migrations.kt | 23 ++++++++----------- .../tachiyomi/ui/library/LibrarySort.kt | 17 -------------- 2 files changed, 10 insertions(+), 30 deletions(-) delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySort.kt diff --git a/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt b/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt index 21470392cf..1eab977406 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/Migrations.kt @@ -13,7 +13,6 @@ import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.data.updater.AppUpdateJob import eu.kanade.tachiyomi.extension.ExtensionUpdateJob import eu.kanade.tachiyomi.network.PREF_DOH_CLOUDFLARE -import eu.kanade.tachiyomi.ui.library.LibrarySort import eu.kanade.tachiyomi.ui.library.setting.SortDirectionSetting import eu.kanade.tachiyomi.ui.library.setting.SortModeSetting import eu.kanade.tachiyomi.ui.reader.setting.OrientationType @@ -104,10 +103,9 @@ object Migrations { // Reset sorting preference if using removed sort by source val oldSortingMode = prefs.getInt(PreferenceKeys.librarySortingMode, 0) - @Suppress("DEPRECATION") - if (oldSortingMode == LibrarySort.SOURCE) { + if (oldSortingMode == 5 /* SOURCE */) { prefs.edit { - putInt(PreferenceKeys.librarySortingMode, LibrarySort.ALPHA) + putInt(PreferenceKeys.librarySortingMode, 0 /* ALPHABETICAL */) } } } @@ -200,16 +198,15 @@ object Migrations { val oldSortingMode = prefs.getInt(PreferenceKeys.librarySortingMode, 0) val oldSortingDirection = prefs.getBoolean(PreferenceKeys.librarySortingDirection, true) - @Suppress("DEPRECATION") val newSortingMode = when (oldSortingMode) { - LibrarySort.ALPHA -> SortModeSetting.ALPHABETICAL - LibrarySort.LAST_READ -> SortModeSetting.LAST_READ - LibrarySort.LAST_CHECKED -> SortModeSetting.LAST_CHECKED - LibrarySort.UNREAD -> SortModeSetting.UNREAD - LibrarySort.TOTAL -> SortModeSetting.TOTAL_CHAPTERS - LibrarySort.LATEST_CHAPTER -> SortModeSetting.LATEST_CHAPTER - LibrarySort.CHAPTER_FETCH_DATE -> SortModeSetting.DATE_FETCHED - LibrarySort.DATE_ADDED -> SortModeSetting.DATE_ADDED + 0 -> SortModeSetting.ALPHABETICAL + 1 -> SortModeSetting.LAST_READ + 2 -> SortModeSetting.LAST_CHECKED + 3 -> SortModeSetting.UNREAD + 4 -> SortModeSetting.TOTAL_CHAPTERS + 6 -> SortModeSetting.LATEST_CHAPTER + 8 -> SortModeSetting.DATE_FETCHED + 7 -> SortModeSetting.DATE_ADDED else -> SortModeSetting.ALPHABETICAL } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySort.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySort.kt deleted file mode 100644 index 1190d61c1d..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySort.kt +++ /dev/null @@ -1,17 +0,0 @@ -package eu.kanade.tachiyomi.ui.library - -@Deprecated("Deprecated in favor for SortModeSetting") -object LibrarySort { - - const val ALPHA = 0 - const val LAST_READ = 1 - const val LAST_CHECKED = 2 - const val UNREAD = 3 - const val TOTAL = 4 - const val LATEST_CHAPTER = 6 - const val CHAPTER_FETCH_DATE = 8 - const val DATE_ADDED = 7 - - @Deprecated("Removed in favor of searching by source") - const val SOURCE = 5 -} From 09a3509d795101f65c13d5936b00df8e15c43315 Mon Sep 17 00:00:00 2001 From: stevenyomi <95685115+stevenyomi@users.noreply.github.com> Date: Sun, 31 Jul 2022 23:18:12 +0800 Subject: [PATCH 17/26] Filter out empty genres before saving manga to database (#7655) (cherry picked from commit 4efb736e56dd1e9f6438502dac915467f5b64f03) --- .../java/eu/kanade/tachiyomi/data/database/models/Manga.kt | 5 ----- .../main/java/eu/kanade/tachiyomi/source/model/SManga.kt | 7 ++++++- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Manga.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Manga.kt index 35efbe1ffe..de086bb68d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Manga.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/models/Manga.kt @@ -32,11 +32,6 @@ interface Manga : SManga { return chapter_flags and CHAPTER_SORT_MASK == CHAPTER_SORT_DESC } - fun getGenres(): List? { - if (genre.isNullOrBlank()) return null - return genre?.split(", ")?.map { it.trim() }?.filterNot { it.isBlank() }?.distinct() - } - private fun setChapterFlags(flag: Int, mask: Int) { chapter_flags = chapter_flags and mask.inv() or (flag and mask) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/model/SManga.kt b/app/src/main/java/eu/kanade/tachiyomi/source/model/SManga.kt index 489938aba2..a3fad6d8a1 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/model/SManga.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/model/SManga.kt @@ -23,6 +23,11 @@ interface SManga : Serializable { var initialized: Boolean + fun getGenres(): List? { + if (genre.isNullOrBlank()) return null + return genre?.split(", ")?.map { it.trim() }?.filterNot { it.isBlank() }?.distinct() + } + fun copyFrom(other: SManga) { if (other.author != null) { author = other.author @@ -73,7 +78,7 @@ fun SManga.toMangaInfo(): MangaInfo { artist = this.artist ?: "", author = this.author ?: "", description = this.description ?: "", - genres = this.genre?.split(", ") ?: emptyList(), + genres = this.getGenres() ?: emptyList(), status = this.status, cover = this.thumbnail_url ?: "", ) From 03e4eb1061dbdd325bd8995cdc4aaa936b91cb2b Mon Sep 17 00:00:00 2001 From: Alessandro Jean <14254807+alessandrojean@users.noreply.github.com> Date: Thu, 4 Aug 2022 23:17:43 -0300 Subject: [PATCH 18/26] Add missing `Authorization` header on MAL refresh token request (#7686) * Add missing Authorization header on MAL refresh token request. * Make sure to also close the response when it have failed. (cherry picked from commit 531546790853dd9adb91777de8d9560a610c4838) --- .../data/track/myanimelist/MyAnimeListApi.kt | 15 ++++++++++++--- .../myanimelist/MyAnimeListInterceptor.kt | 19 ++++++++++++++++--- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListApi.kt index 5eef11d576..13d13acbb0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListApi.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListApi.kt @@ -22,6 +22,7 @@ import kotlinx.serialization.json.jsonArray import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonPrimitive import okhttp3.FormBody +import okhttp3.Headers import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.RequestBody @@ -256,13 +257,21 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI .appendPath("my_list_status") .build() - fun refreshTokenRequest(refreshToken: String): Request { + fun refreshTokenRequest(oauth: OAuth): Request { val formBody: RequestBody = FormBody.Builder() .add("client_id", clientId) - .add("refresh_token", refreshToken) + .add("refresh_token", oauth.refresh_token) .add("grant_type", "refresh_token") .build() - return POST("$baseOAuthUrl/token", body = formBody) + + // Add the Authorization header manually as this particular + // request is called by the interceptor itself so it doesn't reach + // the part where the token is added automatically. + val headers = Headers.Builder() + .add("Authorization", "Bearer ${oauth.access_token}") + .build() + + return POST("$baseOAuthUrl/token", body = formBody, headers = headers) } private fun getPkceChallengeCode(): String { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListInterceptor.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListInterceptor.kt index 571b9a59f3..22da4c1212 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListInterceptor.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListInterceptor.kt @@ -1,9 +1,11 @@ package eu.kanade.tachiyomi.data.track.myanimelist +import eu.kanade.tachiyomi.network.parseAs import kotlinx.serialization.decodeFromString import kotlinx.serialization.json.Json import okhttp3.Interceptor import okhttp3.Response +import okhttp3.internal.closeQuietly import uy.kohesive.injekt.injectLazy import java.io.IOException @@ -24,11 +26,22 @@ class MyAnimeListInterceptor(private val myanimelist: MyAnimeList, private var t } // Refresh access token if expired if (oauth != null && oauth!!.isExpired()) { - chain.proceed(MyAnimeListApi.refreshTokenRequest(oauth!!.refresh_token)).use { - if (it.isSuccessful) { - setAuth(json.decodeFromString(it.body!!.string())) + val newOauth = runCatching { + val oauthResponse = chain.proceed(MyAnimeListApi.refreshTokenRequest(oauth!!)) + + if (oauthResponse.isSuccessful) { + oauthResponse.parseAs() + } else { + oauthResponse.closeQuietly() + null } } + + if (newOauth.getOrNull() == null) { + throw IOException("Failed to refresh the access token") + } + + setAuth(newOauth.getOrNull()) } if (oauth == null) { throw IOException("No authentication token") From e58945a20934a99dfc5e570314ff66269e091960 Mon Sep 17 00:00:00 2001 From: Andreas Date: Wed, 10 Aug 2022 21:53:47 +0200 Subject: [PATCH 19/26] Log extension loading errors directly (#7716) (cherry picked from commit 7892cc1519ef0ecf0dc0b519a0df8806eba05e99) --- .../tachiyomi/extension/model/LoadResult.kt | 4 +--- .../util/ExtensionInstallReceiver.kt | 7 ++++++- .../extension/util/ExtensionLoader.kt | 21 ++++++++++++------- 3 files changed, 20 insertions(+), 12 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/model/LoadResult.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/model/LoadResult.kt index 0cf470fe85..f1982b2f26 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/model/LoadResult.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/model/LoadResult.kt @@ -4,7 +4,5 @@ sealed class LoadResult { class Success(val extension: Extension.Installed) : LoadResult() class Untrusted(val extension: Extension.Untrusted) : LoadResult() - class Error(val message: String? = null) : LoadResult() { - constructor(exception: Throwable) : this(exception.message) - } + object Error : LoadResult() } diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstallReceiver.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstallReceiver.kt index 943e828454..08bea8ff97 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstallReceiver.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionInstallReceiver.kt @@ -7,10 +7,12 @@ import android.content.IntentFilter import eu.kanade.tachiyomi.extension.model.Extension import eu.kanade.tachiyomi.extension.model.LoadResult import eu.kanade.tachiyomi.util.lang.launchNow +import eu.kanade.tachiyomi.util.system.logcat import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.async +import logcat.LogPriority /** * Broadcast receiver that listens for the system's packages installed, updated or removed, and only @@ -94,7 +96,10 @@ internal class ExtensionInstallReceiver(private val listener: Listener) : */ private suspend fun getExtensionFromIntent(context: Context, intent: Intent?): LoadResult { val pkgName = getPackageNameFromIntent(intent) - ?: return LoadResult.Error("Package name not found") + if (pkgName == null) { + logcat(LogPriority.WARN) { "Package name not found" } + return LoadResult.Error + } return GlobalScope.async(Dispatchers.Default, CoroutineStart.DEFAULT) { ExtensionLoader.loadExtensionFromPkgName(context, pkgName) }.await() } diff --git a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionLoader.kt b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionLoader.kt index 240cb1f657..6d5f763bcf 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionLoader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/extension/util/ExtensionLoader.kt @@ -80,10 +80,12 @@ internal object ExtensionLoader { context.packageManager.getPackageInfo(pkgName, PACKAGE_FLAGS) } catch (error: PackageManager.NameNotFoundException) { // Unlikely, but the package may have been uninstalled at this point - return LoadResult.Error(error) + logcat(LogPriority.ERROR, error) + return LoadResult.Error } if (!isPackageAnExtension(pkgInfo)) { - return LoadResult.Error("Tried to load a package that wasn't a extension") + logcat(LogPriority.WARN) { "Tried to load a package that wasn't a extension ($pkgName)" } + return LoadResult.Error } return loadExtension(context, pkgName, pkgInfo) } @@ -102,7 +104,8 @@ internal object ExtensionLoader { pkgManager.getApplicationInfo(pkgName, PackageManager.GET_META_DATA) } catch (error: PackageManager.NameNotFoundException) { // Unlikely, but the package may have been uninstalled at this point - return LoadResult.Error(error) + logcat(LogPriority.ERROR, error) + return LoadResult.Error } val extName = pkgManager.getApplicationLabel(appInfo).toString().substringAfter("Tachiyomi: ") @@ -112,7 +115,7 @@ internal object ExtensionLoader { if (versionName.isNullOrEmpty()) { val exception = Exception("Missing versionName for extension $extName") logcat(LogPriority.WARN, exception) - return LoadResult.Error(exception) + return LoadResult.Error } // Validate lib version @@ -123,13 +126,14 @@ internal object ExtensionLoader { "$LIB_VERSION_MIN to $LIB_VERSION_MAX are allowed", ) logcat(LogPriority.WARN, exception) - return LoadResult.Error(exception) + return LoadResult.Error } val signatureHash = getSignatureHash(pkgInfo) if (signatureHash == null) { - return LoadResult.Error("Package $pkgName isn't signed") + logcat(LogPriority.WARN) { "Package $pkgName isn't signed" } + return LoadResult.Error } else if (signatureHash !in trustedSignatures) { val extension = Extension.Untrusted(extName, pkgName, versionName, versionCode, signatureHash) logcat(LogPriority.WARN) { "Extension $pkgName isn't trusted" } @@ -138,7 +142,8 @@ internal object ExtensionLoader { val isNsfw = appInfo.metaData.getInt(METADATA_NSFW) == 1 if (!loadNsfwSource && isNsfw) { - return LoadResult.Error("NSFW extension $pkgName not allowed") + logcat(LogPriority.WARN) { "NSFW extension $pkgName not allowed" } + return LoadResult.Error } val hasReadme = appInfo.metaData.getInt(METADATA_HAS_README, 0) == 1 @@ -165,7 +170,7 @@ internal object ExtensionLoader { } } catch (e: Throwable) { logcat(LogPriority.ERROR, e) { "Extension load error: $extName ($it)" } - return LoadResult.Error(e) + return LoadResult.Error } } From 6db2becd3023046558c1100a6658a8a76eac1148 Mon Sep 17 00:00:00 2001 From: arkon Date: Sat, 13 Aug 2022 14:56:08 -0400 Subject: [PATCH 20/26] Add auto split tall images setting Also includes some fixes for bad merges in earlier commits Co-authored-by: Saud-97 Co-authored-by: AntsyLich --- .../tachiyomi/data/download/Downloader.kt | 46 +++-- .../data/preference/PreferencesHelper.kt | 2 + .../myanimelist/MyAnimeListInterceptor.kt | 1 - .../eu/kanade/tachiyomi/source/LocalSource.kt | 4 + .../ui/reader/viewer/pager/PagerPageHolder.kt | 5 +- .../viewer/webtoon/WebtoonPageHolder.kt | 5 +- .../ui/setting/SettingsDownloadController.kt | 7 + .../util/system/ContextExtensions.kt | 12 +- .../kanade/tachiyomi/util/system/ImageUtil.kt | 178 +++++++++++++++--- app/src/main/res/values/strings.xml | 5 + 10 files changed, 217 insertions(+), 48 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt b/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt index 3221168e76..6ae972c82b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt @@ -273,7 +273,7 @@ class Downloader( // Start downloader if needed if (autoStart && wasEmpty) { - val queuedDownloads = queue.filter { it.source !is UnmeteredSource }.count() + val queuedDownloads = queue.count { it.source !is UnmeteredSource } val maxDownloadsFromSource = queue .groupBy { it.source } .filterKeys { it !is UnmeteredSource } @@ -352,6 +352,7 @@ class Downloader( .doOnNext { ensureSuccessfulDownload(download, mangaDir, tmpDir, chapterDirname) } // If the page list threw, it will resume here .onErrorReturn { error -> + logcat(LogPriority.ERROR, error) download.status = Download.State.ERROR notifier.onError(error.message, download.chapter.name, download.manga.title) download @@ -379,7 +380,7 @@ class Downloader( tmpFile?.delete() // Try to find the image file. - val imageFile = tmpDir.listFiles()!!.find { it.name!!.startsWith("$filename.") } + val imageFile = tmpDir.listFiles()!!.find { it.name!!.startsWith("$filename.") || it.name!!.contains("${filename}__001") } // If the image is already downloaded, do nothing. Otherwise download from network val pageObservable = when { @@ -389,8 +390,12 @@ class Downloader( } return pageObservable - // When the image is ready, set image path, progress (just in case) and status + // When the page is ready, set page path, progress (just in case) and status .doOnNext { file -> + val success = splitTallImageIfNeeded(page, tmpDir) + if (success.not()) { + notifier.onError(context.getString(R.string.download_notifier_split_failed), download.chapter.name, download.manga.title) + } page.uri = file.uri page.progress = 100 download.downloadedImages++ @@ -401,6 +406,7 @@ class Downloader( .onErrorReturn { page.progress = 0 page.status = Page.ERROR + notifier.onError(it.message, download.chapter.name, download.manga.title) page } } @@ -474,6 +480,26 @@ class Downloader( return ImageUtil.getExtensionFromMimeType(mime) } + private fun splitTallImageIfNeeded(page: Page, tmpDir: UniFile): Boolean { + if (!preferences.splitTallImages().get()) return true + + val filename = String.format("%03d", page.number) + val imageFile = tmpDir.listFiles()?.find { it.name!!.startsWith(filename) } + ?: throw Error(context.getString(R.string.download_notifier_split_page_not_found, page.number)) + val imageFilePath = imageFile.filePath + ?: throw Error(context.getString(R.string.download_notifier_split_page_path_not_found, page.number)) + + // check if the original page was previously splitted before then skip. + if (imageFile.name!!.contains("__")) return true + + return try { + ImageUtil.splitTallImage(imageFile, imageFilePath) + } catch (e: Exception) { + logcat(LogPriority.ERROR, e) + false + } + } + /** * Checks if the download was successful. * @@ -489,16 +515,10 @@ class Downloader( dirname: String, ) { // Ensure that the chapter folder has all the images. - val downloadedImages = tmpDir.listFiles().orEmpty().filterNot { it.name!!.endsWith(".tmp") } + val downloadedImages = tmpDir.listFiles().orEmpty().filterNot { it.name!!.endsWith(".tmp") || (it.name!!.contains("__") && !it.name!!.contains("__001.jpg")) } download.status = if (downloadedImages.size == download.pages!!.size) { - Download.State.DOWNLOADED - } else { - Download.State.ERROR - } - - // Only rename the directory if it's downloaded. - if (download.status == Download.State.DOWNLOADED) { + // Only rename the directory if it's downloaded. if (preferences.saveChaptersAsCBZ().get()) { archiveChapter(mangaDir, dirname, tmpDir) } else { @@ -507,6 +527,10 @@ class Downloader( cache.addChapter(dirname, mangaDir, download.manga) DiskUtil.createNoMediaFile(tmpDir, context) + + Download.State.DOWNLOADED + } else { + Download.State.ERROR } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt index 96b0989b27..45c568b204 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt @@ -206,6 +206,8 @@ class PreferencesHelper(val context: Context) { fun saveChaptersAsCBZ() = flowPrefs.getBoolean("save_chapter_as_cbz", true) + fun splitTallImages() = flowPrefs.getBoolean("split_tall_images", false) + fun folderPerManga() = prefs.getBoolean(Keys.folderPerManga, false) fun numberOfBackups() = flowPrefs.getInt("backup_slots", 2) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListInterceptor.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListInterceptor.kt index 22da4c1212..cf26d57e8e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListInterceptor.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/myanimelist/MyAnimeListInterceptor.kt @@ -1,7 +1,6 @@ package eu.kanade.tachiyomi.data.track.myanimelist import eu.kanade.tachiyomi.network.parseAs -import kotlinx.serialization.decodeFromString import kotlinx.serialization.json.Json import okhttp3.Interceptor import okhttp3.Response diff --git a/app/src/main/java/eu/kanade/tachiyomi/source/LocalSource.kt b/app/src/main/java/eu/kanade/tachiyomi/source/LocalSource.kt index e8709e4686..65cd080241 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/source/LocalSource.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/source/LocalSource.kt @@ -4,6 +4,7 @@ import android.content.Context import com.github.junrar.Archive import com.hippo.unifile.UniFile import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.source.model.Filter import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.MangasPage @@ -31,6 +32,8 @@ import logcat.LogPriority import rx.Observable import tachiyomi.source.model.ChapterInfo import tachiyomi.source.model.MangaInfo +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get import uy.kohesive.injekt.injectLazy import java.io.File import java.io.FileInputStream @@ -40,6 +43,7 @@ import java.util.zip.ZipFile class LocalSource( private val context: Context, + private val coverCache: CoverCache = Injekt.get(), ) : CatalogueSource, UnmeteredSource { private val json: Json by injectLazy() diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt index 7cfea4cc92..694c47da6b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt @@ -19,6 +19,7 @@ import rx.Observable import rx.Subscription import rx.android.schedulers.AndroidSchedulers import rx.schedulers.Schedulers +import java.io.BufferedInputStream import java.io.ByteArrayInputStream import java.io.InputStream import java.util.concurrent.TimeUnit @@ -238,7 +239,7 @@ class PagerPageHolder( .subscribe({}, {}) } - private fun process(page: ReaderPage, imageStream: InputStream): InputStream { + private fun process(page: ReaderPage, imageStream: BufferedInputStream): InputStream { if (!viewer.config.dualPageSplit) { return imageStream } @@ -247,7 +248,7 @@ class PagerPageHolder( return splitInHalf(imageStream) } - val isDoublePage = ImageUtil.isDoublePage(imageStream) + val isDoublePage = ImageUtil.isWideImage(imageStream) if (!isDoublePage) { return imageStream } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonPageHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonPageHolder.kt index c8b23b673c..3463eafd62 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonPageHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/webtoon/WebtoonPageHolder.kt @@ -23,6 +23,7 @@ import rx.Observable import rx.Subscription import rx.android.schedulers.AndroidSchedulers import rx.schedulers.Schedulers +import java.io.BufferedInputStream import java.io.InputStream import java.util.concurrent.TimeUnit @@ -272,12 +273,12 @@ class WebtoonPageHolder( addSubscription(readImageHeaderSubscription) } - private fun process(imageStream: InputStream): InputStream { + private fun process(imageStream: BufferedInputStream): InputStream { if (!viewer.config.dualPageSplit) { return imageStream } - val isDoublePage = ImageUtil.isDoublePage(imageStream) + val isDoublePage = ImageUtil.isWideImage(imageStream) if (!isDoublePage) { return imageStream } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsDownloadController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsDownloadController.kt index 74fce340b6..eaf43573e6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsDownloadController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsDownloadController.kt @@ -24,6 +24,7 @@ import eu.kanade.tachiyomi.util.preference.multiSelectListPreference import eu.kanade.tachiyomi.util.preference.onClick import eu.kanade.tachiyomi.util.preference.preference import eu.kanade.tachiyomi.util.preference.preferenceCategory +import eu.kanade.tachiyomi.util.preference.summaryRes import eu.kanade.tachiyomi.util.preference.switchPreference import eu.kanade.tachiyomi.util.preference.titleRes import eu.kanade.tachiyomi.util.system.toast @@ -72,6 +73,12 @@ class SettingsDownloadController : SettingsController() { bindTo(preferences.saveChaptersAsCBZ()) titleRes = R.string.save_chapter_as_cbz } + switchPreference { + bindTo(preferences.splitTallImages()) + titleRes = R.string.split_tall_images + summaryRes = R.string.split_tall_images_summary + } + preferenceCategory { titleRes = R.string.pref_category_delete_chapters diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt index d9a76d31e4..b041210037 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/system/ContextExtensions.kt @@ -47,6 +47,7 @@ import logcat.LogPriority import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get import java.io.File +import kotlin.math.max import kotlin.math.roundToInt private const val TABLET_UI_MIN_SCREEN_WIDTH_DP = 720 @@ -166,6 +167,9 @@ fun Context.hasPermission(permission: String) = ContextCompat.checkSelfPermissio } } +val getDisplayMaxHeightInPx: Int + get() = Resources.getSystem().displayMetrics.let { max(it.heightPixels, it.widthPixels) } + /** * Converts to dp. */ @@ -258,7 +262,7 @@ fun Context.openInBrowser(uri: Uri, forceDefaultBrowser: Boolean = false) { } fun Context.defaultBrowserPackageName(): String? { - val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse("http://")) + val browserIntent = Intent(Intent.ACTION_VIEW, "http://".toUri()) return packageManager.resolveActivity(browserIntent, PackageManager.MATCH_DEFAULT_ONLY) ?.activityInfo?.packageName ?.takeUnless { it in DeviceUtil.invalidDefaultBrowsers } @@ -315,8 +319,8 @@ fun Context.isNightMode(): Boolean { * https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:appcompat/appcompat/src/main/java/androidx/appcompat/app/AppCompatDelegateImpl.java;l=348;drc=e28752c96fc3fb4d3354781469a1af3dbded4898 */ fun Context.createReaderThemeContext(): Context { - val prefs = Injekt.get() - val isDarkBackground = when (prefs.readerTheme().get()) { + val preferences = Injekt.get() + val isDarkBackground = when (preferences.readerTheme().get()) { 1, 2 -> true // Black, Gray 3 -> applicationContext.isNightMode() // Automatic bg uses activity background by default else -> false // White @@ -329,7 +333,7 @@ fun Context.createReaderThemeContext(): Context { val wrappedContext = ContextThemeWrapper(this, R.style.Theme_Tachiyomi) wrappedContext.applyOverrideConfiguration(overrideConf) - ThemingDelegate.getThemeResIds(prefs.appTheme().get(), prefs.themeDarkAmoled().get()) + ThemingDelegate.getThemeResIds(preferences.appTheme().get(), preferences.themeDarkAmoled().get()) .forEach { wrappedContext.theme.applyStyle(it, true) } return wrappedContext } diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/system/ImageUtil.kt b/app/src/main/java/eu/kanade/tachiyomi/util/system/ImageUtil.kt index f50c9151d0..2d9bec7183 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/system/ImageUtil.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/system/ImageUtil.kt @@ -4,6 +4,7 @@ import android.content.Context import android.content.res.Configuration import android.graphics.Bitmap import android.graphics.BitmapFactory +import android.graphics.BitmapRegionDecoder import android.graphics.Color import android.graphics.Rect import android.graphics.drawable.ColorDrawable @@ -11,19 +12,27 @@ import android.graphics.drawable.Drawable import android.graphics.drawable.GradientDrawable import android.os.Build import android.webkit.MimeTypeMap +import androidx.annotation.ColorInt import androidx.core.graphics.alpha import androidx.core.graphics.applyCanvas import androidx.core.graphics.blue import androidx.core.graphics.createBitmap +import androidx.core.graphics.get import androidx.core.graphics.green import androidx.core.graphics.red +import com.hippo.unifile.UniFile +import logcat.LogPriority import tachiyomi.decoder.Format import tachiyomi.decoder.ImageDecoder +import java.io.BufferedInputStream import java.io.ByteArrayInputStream import java.io.ByteArrayOutputStream +import java.io.File +import java.io.FileOutputStream import java.io.InputStream import java.net.URLConnection import kotlin.math.abs +import kotlin.math.min object ImageUtil { @@ -73,8 +82,7 @@ object ImageUtil { Format.Webp -> type.isAnimated && Build.VERSION.SDK_INT >= Build.VERSION_CODES.P else -> false } - } catch (e: Exception) { - } + } catch (e: Exception) { /* Do Nothing */ } return false } @@ -106,19 +114,12 @@ object ImageUtil { } /** - * Check whether the image is a double-page spread + * Check whether the image is wide (which we consider a double-page spread). + * * @return true if the width is greater than the height */ - fun isDoublePage(imageStream: InputStream): Boolean { - imageStream.mark(imageStream.available() + 1) - - val imageBytes = imageStream.readBytes() - - val options = BitmapFactory.Options().apply { inJustDecodeBounds = true } - BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size, options) - - imageStream.reset() - + fun isWideImage(imageStream: BufferedInputStream): Boolean { + val options = extractImageOptions(imageStream) return options.outWidth > options.outHeight } @@ -185,6 +186,111 @@ object ImageUtil { RIGHT, LEFT } + /** + * Check whether the image is considered a tall image. + * + * @return true if the height:width ratio is greater than 3. + */ + private fun isTallImage(imageStream: InputStream): Boolean { + val options = extractImageOptions(imageStream, resetAfterExtraction = false) + return (options.outHeight / options.outWidth) > 3 + } + + /** + * Splits tall images to improve performance of reader + */ + fun splitTallImage(imageFile: UniFile, imageFilePath: String): Boolean { + if (isAnimatedAndSupported(imageFile.openInputStream()) || !isTallImage(imageFile.openInputStream())) { + return true + } + + val options = extractImageOptions(imageFile.openInputStream(), resetAfterExtraction = false).apply { inJustDecodeBounds = false } + // Values are stored as they get modified during split loop + val imageHeight = options.outHeight + val imageWidth = options.outWidth + + val splitHeight = (getDisplayMaxHeightInPx * 1.5).toInt() + // -1 so it doesn't try to split when imageHeight = getDisplayHeightInPx + val partCount = (imageHeight - 1) / splitHeight + 1 + + val optimalSplitHeight = imageHeight / partCount + + val splitDataList = (0 until partCount).fold(mutableListOf()) { list, index -> + list.apply { + // Only continue if the list is empty or there is image remaining + if (isEmpty() || imageHeight > last().bottomOffset) { + val topOffset = index * optimalSplitHeight + var outputImageHeight = min(optimalSplitHeight, imageHeight - topOffset) + + val remainingHeight = imageHeight - (topOffset + outputImageHeight) + // If remaining height is smaller or equal to 1/3th of + // optimal split height then include it in current page + if (remainingHeight <= (optimalSplitHeight / 3)) { + outputImageHeight += remainingHeight + } + add(SplitData(index, topOffset, outputImageHeight)) + } + } + } + + val bitmapRegionDecoder = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + BitmapRegionDecoder.newInstance(imageFile.openInputStream()) + } else { + @Suppress("DEPRECATION") + BitmapRegionDecoder.newInstance(imageFile.openInputStream(), false) + } + + if (bitmapRegionDecoder == null) { + logcat { "Failed to create new instance of BitmapRegionDecoder" } + return false + } + + logcat { + "Splitting image with height of $imageHeight into $partCount part " + + "with estimated ${optimalSplitHeight}px height per split" + } + + return try { + splitDataList.forEach { splitData -> + val splitPath = splitImagePath(imageFilePath, splitData.index) + + val region = Rect(0, splitData.topOffset, imageWidth, splitData.bottomOffset) + + FileOutputStream(splitPath).use { outputStream -> + val splitBitmap = bitmapRegionDecoder.decodeRegion(region, options) + splitBitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream) + splitBitmap.recycle() + } + logcat { + "Success: Split #${splitData.index + 1} with topOffset=${splitData.topOffset} " + + "height=${splitData.outputImageHeight} bottomOffset=${splitData.bottomOffset}" + } + } + imageFile.delete() + true + } catch (e: Exception) { + // Image splits were not successfully saved so delete them and keep the original image + splitDataList + .map { splitImagePath(imageFilePath, it.index) } + .forEach { File(it).delete() } + logcat(LogPriority.ERROR, e) + false + } finally { + bitmapRegionDecoder.recycle() + } + } + + private fun splitImagePath(imageFilePath: String, index: Int) = + imageFilePath.substringBeforeLast(".") + "__${"%03d".format(index + 1)}.jpg" + + data class SplitData( + val index: Int, + val topOffset: Int, + val outputImageHeight: Int, + ) { + val bottomOffset = topOffset + outputImageHeight + } + /** * Algorithm for determining what background to accompany a comic/manga page */ @@ -209,14 +315,14 @@ object ImageUtil { val leftOffsetX = left - offsetX val rightOffsetX = right + offsetX - val topLeftPixel = image.getPixel(left, top) - val topRightPixel = image.getPixel(right, top) - val midLeftPixel = image.getPixel(left, midY) - val midRightPixel = image.getPixel(right, midY) - val topCenterPixel = image.getPixel(midX, top) - val botLeftPixel = image.getPixel(left, bot) - val bottomCenterPixel = image.getPixel(midX, bot) - val botRightPixel = image.getPixel(right, bot) + val topLeftPixel = image[left, top] + val topRightPixel = image[right, top] + val midLeftPixel = image[left, midY] + val midRightPixel = image[right, midY] + val topCenterPixel = image[midX, top] + val botLeftPixel = image[left, bot] + val bottomCenterPixel = image[midX, bot] + val botRightPixel = image[right, bot] val topLeftIsDark = topLeftPixel.isDark() val topRightIsDark = topRightPixel.isDark() @@ -269,8 +375,8 @@ object ImageUtil { var whiteStreak = false val notOffset = x == left || x == right inner@ for ((index, y) in (0 until image.height step image.height / 25).withIndex()) { - val pixel = image.getPixel(x, y) - val pixelOff = image.getPixel(x + (if (x < image.width / 2) -offsetX else offsetX), y) + val pixel = image[x, y] + val pixelOff = image[x + (if (x < image.width / 2) -offsetX else offsetX), y] if (pixel.isWhite()) { whitePixelsStreak++ whitePixels++ @@ -361,8 +467,8 @@ object ImageUtil { val topCornersIsDark = topLeftIsDark && topRightIsDark val botCornersIsDark = botLeftIsDark && botRightIsDark - val topOffsetCornersIsDark = image.getPixel(leftOffsetX, top).isDark() && image.getPixel(rightOffsetX, top).isDark() - val botOffsetCornersIsDark = image.getPixel(leftOffsetX, bot).isDark() && image.getPixel(rightOffsetX, bot).isDark() + val topOffsetCornersIsDark = image[leftOffsetX, top].isDark() && image[rightOffsetX, top].isDark() + val botOffsetCornersIsDark = image[leftOffsetX, bot].isDark() && image[rightOffsetX, bot].isDark() val gradient = when { darkBG && botCornersIsWhite -> { @@ -391,15 +497,31 @@ object ImageUtil { ) } - private fun Int.isDark(): Boolean = + private fun @receiver:ColorInt Int.isDark(): Boolean = red < 40 && blue < 40 && green < 40 && alpha > 200 - private fun Int.isCloseTo(other: Int): Boolean = + private fun @receiver:ColorInt Int.isCloseTo(other: Int): Boolean = abs(red - other.red) < 30 && abs(green - other.green) < 30 && abs(blue - other.blue) < 30 - private fun Int.isWhite(): Boolean = + private fun @receiver:ColorInt Int.isWhite(): Boolean = red + blue + green > 740 + /** + * Used to check an image's dimensions without loading it in the memory. + */ + private fun extractImageOptions( + imageStream: InputStream, + resetAfterExtraction: Boolean = true, + ): BitmapFactory.Options { + imageStream.mark(imageStream.available() + 1) + + val imageBytes = imageStream.readBytes() + val options = BitmapFactory.Options().apply { inJustDecodeBounds = true } + BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size, options) + if (resetAfterExtraction) imageStream.reset() + return options + } + // Android doesn't include some mappings private val SUPPLEMENTARY_MIMETYPE_MAPPING = mapOf( // https://issuetracker.google.com/issues/182703810 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index a85736a223..37dede11e4 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -410,6 +410,8 @@ Download new chapters Manga in excluded categories will not be downloaded even if they are also in included categories. Save as CBZ archive + Auto split tall images + Improves reader performance by splitting tall downloaded images. Tracking guide @@ -809,6 +811,9 @@ No network connection available Download paused Download completed + Page %d not found while splitting + Couldn\'t find file path of page %d + Couldn\'t split downloaded image Common From f00e03e5ea3ac261d4a19fe52a878fe0ed824835 Mon Sep 17 00:00:00 2001 From: Saud-97 <39028181+Saud-97@users.noreply.github.com> Date: Sat, 4 Jun 2022 19:52:35 +0300 Subject: [PATCH 21/26] New: Migrating titles maintains custom covers (#7196) * New: Migrating titles maintains custom covers #7189 * Added Custom Covers to MigrationFlags.kt, strings.xml * Reworded covers --> cover * Updated logic to show/hide Migration flags titles depending on manga. (cherry picked from commit 5ea03fad8793a810514b41fe8308f89b25368c4d) --- .../ui/browse/migration/MigrationFlags.kt | 41 ++++++++++++++++--- .../migration/search/SearchController.kt | 2 +- .../migration/search/SearchPresenter.kt | 14 ++++++- app/src/main/res/values/strings.xml | 1 + 4 files changed, 50 insertions(+), 8 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/MigrationFlags.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/MigrationFlags.kt index 826eee35ea..a6307bcf2d 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/MigrationFlags.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/MigrationFlags.kt @@ -1,20 +1,29 @@ package eu.kanade.tachiyomi.ui.browse.migration import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.cache.CoverCache +import eu.kanade.tachiyomi.data.database.DatabaseHelper +import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.util.hasCustomCover +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get +import uy.kohesive.injekt.injectLazy object MigrationFlags { - private const val CHAPTERS = 0b001 - private const val CATEGORIES = 0b010 - private const val TRACK = 0b100 + private const val CHAPTERS = 0b0001 + private const val CATEGORIES = 0b0010 + private const val TRACK = 0b0100 + private const val CUSTOM_COVER = 0b1000 private const val CHAPTERS2 = 0x1 private const val CATEGORIES2 = 0x2 private const val TRACK2 = 0x4 - val titles get() = arrayOf(R.string.chapters, R.string.categories, R.string.track) + private val coverCache: CoverCache by injectLazy() + private val db: DatabaseHelper = Injekt.get() - val flags get() = arrayOf(CHAPTERS, CATEGORIES, TRACK) + val flags get() = arrayOf(CHAPTERS, CATEGORIES, TRACK, CUSTOM_COVER) fun hasChapters(value: Int): Boolean { return value and CHAPTERS != 0 @@ -28,11 +37,31 @@ object MigrationFlags { return value and TRACK != 0 } + fun hasCustomCover(value: Int): Boolean { + return value and CUSTOM_COVER != 0 + } + fun getEnabledFlagsPositions(value: Int): List { return flags.mapIndexedNotNull { index, flag -> if (value and flag != 0) index else null } } fun getFlagsFromPositions(positions: Array): Int { - return positions.fold(0, { accumulated, position -> accumulated or (1 shl position) }) + return positions.fold(0) { accumulated, position -> accumulated or (1 shl position) } } + + fun titles(manga: Manga?): Array { + val titles = arrayOf(R.string.chapters, R.string.categories).toMutableList() + if (manga != null) { + db.inTransaction { + if (db.getTracks(manga).executeAsBlocking().isNotEmpty()) { + titles.add(R.string.track) + } + + if (manga.hasCustomCover(coverCache)) { + titles.add(R.string.custom_cover) + } + } + } + return titles.toTypedArray() + } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SearchController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SearchController.kt index 1e529cb79e..bc59850d2a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SearchController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SearchController.kt @@ -95,7 +95,7 @@ class SearchController( override fun onCreateDialog(savedViewState: Bundle?): Dialog { val prefValue = preferences.migrateFlags().get() val enabledFlagsPositions = MigrationFlags.getEnabledFlagsPositions(prefValue) - val items = MigrationFlags.titles + val items = MigrationFlags.titles(manga) .map { resources?.getString(it) } .toTypedArray() val selected = items diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SearchPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SearchPresenter.kt index 5c671b575a..6f17a92e7a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SearchPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SearchPresenter.kt @@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.ui.browse.migration.search import android.os.Bundle import com.jakewharton.rxrelay.BehaviorRelay +import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.MangaCategory import eu.kanade.tachiyomi.data.database.models.toMangaInfo @@ -17,12 +18,14 @@ import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchCardItem import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchItem import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchPresenter import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource +import eu.kanade.tachiyomi.util.hasCustomCover import eu.kanade.tachiyomi.util.lang.launchIO import eu.kanade.tachiyomi.util.lang.launchUI import eu.kanade.tachiyomi.util.lang.withUIContext import eu.kanade.tachiyomi.util.system.toast import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get +import uy.kohesive.injekt.injectLazy import java.util.Date class SearchPresenter( @@ -31,7 +34,7 @@ class SearchPresenter( ) : GlobalSearchPresenter(initialQuery) { private val replacingMangaRelay = BehaviorRelay.create>() - + private val coverCache: CoverCache by injectLazy() private val enhancedServices by lazy { Injekt.get().services.filterIsInstance() } override fun onCreate(savedState: Bundle?) { @@ -103,6 +106,10 @@ class SearchPresenter( MigrationFlags.hasTracks( flags, ) + val migrateCustomCover = + MigrationFlags.hasCustomCover( + flags, + ) db.inTransaction { // Update chapters read @@ -174,6 +181,11 @@ class SearchPresenter( manga.date_added = Date().time } + // Update custom cover + if (migrateCustomCover) { + coverCache.setCustomCoverToCache(manga, coverCache.getCustomCoverFile(prevManga).inputStream()) + } + // SearchPresenter#networkToLocalManga may have updated the manga title, // so ensure db gets updated title too db.insertManga(manga).executeAsBlocking() diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 37dede11e4..33b2c72d97 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -621,6 +621,7 @@ Custom All Unread + Custom cover Cover Cover saved Error saving cover From b63578974054c76b62e543bf9f7c7b08b6be5211 Mon Sep 17 00:00:00 2001 From: arkon Date: Fri, 10 Jun 2022 09:49:50 -0400 Subject: [PATCH 22/26] Actually compare chapter numbers as numbers when sorting (fixes #7247) (cherry picked from commit da8669c826e6575a76751842bda3da59dc2f07c7) --- .../ui/browse/migration/MigrationFlags.kt | 32 +++++++++---------- .../tachiyomi/util/chapter/ChapterSorter.kt | 7 ++-- 2 files changed, 19 insertions(+), 20 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/MigrationFlags.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/MigrationFlags.kt index a6307bcf2d..649c48d211 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/MigrationFlags.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/MigrationFlags.kt @@ -20,8 +20,8 @@ object MigrationFlags { private const val CATEGORIES2 = 0x2 private const val TRACK2 = 0x4 - private val coverCache: CoverCache by injectLazy() - private val db: DatabaseHelper = Injekt.get() + private val coverCache: CoverCache by injectLazy() + private val db: DatabaseHelper = Injekt.get() val flags get() = arrayOf(CHAPTERS, CATEGORIES, TRACK, CUSTOM_COVER) @@ -49,19 +49,19 @@ object MigrationFlags { return positions.fold(0) { accumulated, position -> accumulated or (1 shl position) } } - fun titles(manga: Manga?): Array { - val titles = arrayOf(R.string.chapters, R.string.categories).toMutableList() - if (manga != null) { - db.inTransaction { - if (db.getTracks(manga).executeAsBlocking().isNotEmpty()) { - titles.add(R.string.track) - } + fun titles(manga: Manga?): Array { + val titles = arrayOf(R.string.chapters, R.string.categories).toMutableList() + if (manga != null) { + db.inTransaction { + if (db.getTracks(manga).executeAsBlocking().isNotEmpty()) { + titles.add(R.string.track) + } - if (manga.hasCustomCover(coverCache)) { - titles.add(R.string.custom_cover) - } - } - } - return titles.toTypedArray() - } + if (manga.hasCustomCover(coverCache)) { + titles.add(R.string.custom_cover) + } + } + } + return titles.toTypedArray() + } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterSorter.kt b/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterSorter.kt index 764f00491c..a069a2c7c4 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterSorter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterSorter.kt @@ -2,7 +2,6 @@ package eu.kanade.tachiyomi.util.chapter import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder fun getChapterSort(manga: Manga, sortDescending: Boolean = manga.sortDescending()): (Chapter, Chapter) -> Int { return when (manga.sorting) { @@ -11,13 +10,13 @@ fun getChapterSort(manga: Manga, sortDescending: Boolean = manga.sortDescending( false -> { c1, c2 -> c2.source_order.compareTo(c1.source_order) } } Manga.CHAPTER_SORTING_NUMBER -> when (sortDescending) { - true -> { c1, c2 -> c2.chapter_number.toString().compareToCaseInsensitiveNaturalOrder(c1.chapter_number.toString()) } - false -> { c1, c2 -> c1.chapter_number.toString().compareToCaseInsensitiveNaturalOrder(c2.chapter_number.toString()) } + true -> { c1, c2 -> c2.chapter_number.compareTo(c1.chapter_number) } + false -> { c1, c2 -> c1.chapter_number.compareTo(c2.chapter_number) } } Manga.CHAPTER_SORTING_UPLOAD_DATE -> when (sortDescending) { true -> { c1, c2 -> c2.date_upload.compareTo(c1.date_upload) } false -> { c1, c2 -> c1.date_upload.compareTo(c2.date_upload) } } - else -> throw NotImplementedError("Unimplemented sorting method") + else -> throw NotImplementedError("Invalid chapter sorting method: ${manga.sorting}") } } From f461c71625ff05360487f99aeeda208bd2b76c1f Mon Sep 17 00:00:00 2001 From: nicki <72807749+curche@users.noreply.github.com> Date: Sun, 12 Jun 2022 19:54:39 +0530 Subject: [PATCH 23/26] Fix Links to Changelog/Readme/Commits for `multisrc` (#7252) * Fix Links to Changelog/Readme/Commits for `multisrc` working basic fix. Needs to be refactored into `createUrl()` * Refactor back into `createUrl` hopefully the logic is understandable there's three cases: - when multisrc, if `path` isn't mentioned, then we're trying to open commmit history - when multisrc, if `path` is mentioned, then its either a changelog or a readme to a multisrc extension, the files are stored in the `overrides` subfolder - when not multisrc, we're looking at a single source where the links are constructed in the same way regardless of it being changelog/readme/commit history (cherry picked from commit e7695aef78c92c10e5bae953b24a19e67ac156af) --- .../extension/details/ExtensionDetailsController.kt | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsController.kt index 7d76967e1a..c80f1a9d42 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsController.kt @@ -247,9 +247,13 @@ class ExtensionDetailsController(bundle: Bundle? = null) : } private fun createUrl(url: String, pkgName: String, pkgFactory: String?, path: String = ""): String { - return when { - !pkgFactory.isNullOrEmpty() -> "$url/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/$pkgFactory$path" - else -> "$url/src/${pkgName.replace(".", "/")}$path" + return if (!pkgFactory.isNullOrEmpty()) { + when (path.isEmpty()) { + true -> "$url/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/$pkgFactory" + else -> "$url/multisrc/overrides/$pkgFactory/" + (pkgName.split(".").lastOrNull() ?: "") + path + } + } else { + url + "/src/" + pkgName.replace(".", "/") + path } } From 431c04e54f4acd1c660e83c27886c38955b67871 Mon Sep 17 00:00:00 2001 From: CVIUS <84634607+CVIUS@users.noreply.github.com> Date: Thu, 12 May 2022 20:58:37 +0800 Subject: [PATCH 24/26] Detect identical mangas when long pressing to add to library (#7095) * Detect identical mangas when long pressing to add to library * Use extracted duplicate manga dialog to avoid duplication * Partially revert previous commit * Review changes * Review changes part 2 (cherry picked from commit f1afeac0bcd3904c323e24d67dd945c85c666f92) --- .../details/ExtensionDetailsController.kt | 2 +- .../source/browse/BrowseSourceController.kt | 74 +++++++++++-------- .../source/browse/BrowseSourcePresenter.kt | 4 + .../ui/manga/AddDuplicateMangaDialog.kt | 48 ++++++++++++ .../tachiyomi/ui/manga/MangaController.kt | 15 +--- 5 files changed, 98 insertions(+), 45 deletions(-) create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/manga/AddDuplicateMangaDialog.kt diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsController.kt index c80f1a9d42..d0634cda65 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/extension/details/ExtensionDetailsController.kt @@ -253,7 +253,7 @@ class ExtensionDetailsController(bundle: Bundle? = null) : else -> "$url/multisrc/overrides/$pkgFactory/" + (pkgName.split(".").lastOrNull() ?: "") + path } } else { - url + "/src/" + pkgName.replace(".", "/") + path + url + "/src/" + pkgName.replace(".", "/") + path } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceController.kt index ab20ee493d..cdf418c906 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceController.kt @@ -37,6 +37,7 @@ import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController import eu.kanade.tachiyomi.ui.library.ChangeMangaCategoriesDialog import eu.kanade.tachiyomi.ui.library.setting.DisplayModeSetting import eu.kanade.tachiyomi.ui.main.MainActivity +import eu.kanade.tachiyomi.ui.manga.AddDuplicateMangaDialog import eu.kanade.tachiyomi.ui.manga.MangaController import eu.kanade.tachiyomi.ui.more.MoreController import eu.kanade.tachiyomi.ui.webview.WebViewActivity @@ -588,6 +589,7 @@ open class BrowseSourceController(bundle: Bundle) : override fun onItemLongClick(position: Int) { val activity = activity ?: return val manga = (adapter?.getItem(position) as? SourceItem?)?.manga ?: return + val duplicateManga = presenter.getDuplicateLibraryManga(manga) if (manga.favorite) { MaterialAlertDialogBuilder(activity) @@ -603,43 +605,53 @@ open class BrowseSourceController(bundle: Bundle) : } .show() } else { - val categories = presenter.getCategories() - val defaultCategoryId = preferences.defaultCategory() - val defaultCategory = categories.find { it.id == defaultCategoryId } + if (duplicateManga != null) { + AddDuplicateMangaDialog(this, duplicateManga) { addToLibrary(manga, position) } + .showDialog(router) + } else { + addToLibrary(manga, position) + } + } + } - when { - // Default category set - defaultCategory != null -> { - presenter.moveMangaToCategory(manga, defaultCategory) + private fun addToLibrary(newManga: Manga, position: Int) { + val activity = activity ?: return + val categories = presenter.getCategories() + val defaultCategoryId = preferences.defaultCategory() + val defaultCategory = categories.find { it.id == defaultCategoryId } - presenter.changeMangaFavorite(manga) - adapter?.notifyItemChanged(position) - activity.toast(activity.getString(R.string.manga_added_library)) - } + when { + // Default category set + defaultCategory != null -> { + presenter.moveMangaToCategory(newManga, defaultCategory) - // Automatic 'Default' or no categories - defaultCategoryId == 0 || categories.isEmpty() -> { - presenter.moveMangaToCategory(manga, null) + presenter.changeMangaFavorite(newManga) + adapter?.notifyItemChanged(position) + activity.toast(activity.getString(R.string.manga_added_library)) + } - presenter.changeMangaFavorite(manga) - adapter?.notifyItemChanged(position) - activity.toast(activity.getString(R.string.manga_added_library)) - } + // Automatic 'Default' or no categories + defaultCategoryId == 0 || categories.isEmpty() -> { + presenter.moveMangaToCategory(newManga, null) - // Choose a category - else -> { - val ids = presenter.getMangaCategoryIds(manga) - val preselected = categories.map { - if (it.id in ids) { - QuadStateTextView.State.CHECKED.ordinal - } else { - QuadStateTextView.State.UNCHECKED.ordinal - } - }.toTypedArray() + presenter.changeMangaFavorite(newManga) + adapter?.notifyItemChanged(position) + activity.toast(activity.getString(R.string.manga_added_library)) + } - ChangeMangaCategoriesDialog(this, listOf(manga), categories, preselected) - .showDialog(router) - } + // Choose a category + else -> { + val ids = presenter.getMangaCategoryIds(newManga) + val preselected = categories.map { + if (it.id in ids) { + QuadStateTextView.State.CHECKED.ordinal + } else { + QuadStateTextView.State.UNCHECKED.ordinal + } + }.toTypedArray() + + ChangeMangaCategoriesDialog(this, listOf(newManga), categories, preselected) + .showDialog(router) } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourcePresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourcePresenter.kt index 1bae71a314..afcb0c60ef 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourcePresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourcePresenter.kt @@ -351,6 +351,10 @@ open class BrowseSourcePresenter( return db.getCategories().executeAsBlocking() } + fun getDuplicateLibraryManga(manga: Manga): Manga? { + return db.getDuplicateLibraryManga(manga).executeAsBlocking() + } + /** * Gets the category id's the manga is in, if the manga is not in a category, returns the default id. * diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/AddDuplicateMangaDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/AddDuplicateMangaDialog.kt new file mode 100644 index 0000000000..8cac9c030f --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/AddDuplicateMangaDialog.kt @@ -0,0 +1,48 @@ +package eu.kanade.tachiyomi.ui.manga + +import android.app.Dialog +import android.os.Bundle +import com.bluelinelabs.conductor.Controller +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.source.SourceManager +import eu.kanade.tachiyomi.ui.base.controller.DialogController +import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction +import uy.kohesive.injekt.injectLazy + +class AddDuplicateMangaDialog(bundle: Bundle? = null) : DialogController(bundle) { + + private val sourceManager: SourceManager by injectLazy() + + private lateinit var libraryManga: Manga + private lateinit var onAddToLibrary: () -> Unit + + constructor( + target: Controller, + libraryManga: Manga, + onAddToLibrary: () -> Unit, + ) : this() { + targetController = target + + this.libraryManga = libraryManga + this.onAddToLibrary = onAddToLibrary + } + + override fun onCreateDialog(savedViewState: Bundle?): Dialog { + val source = sourceManager.getOrStub(libraryManga.source) + + return MaterialAlertDialogBuilder(activity!!) + .setMessage(activity?.getString(R.string.confirm_manga_add_duplicate, source.name)) + .setPositiveButton(activity?.getString(R.string.action_add)) { _, _ -> + onAddToLibrary() + } + .setNegativeButton(android.R.string.cancel, null) + .setNeutralButton(activity?.getString(R.string.action_show_manga)) { _, _ -> + dismissDialog() + router.pushController(MangaController(libraryManga.id!!).withFadeTransaction()) + } + .setCancelable(true) + .create() + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt index c381f1194b..85fdccdcc9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt @@ -29,7 +29,6 @@ import coil.request.ImageRequest import com.bluelinelabs.conductor.Controller import com.bluelinelabs.conductor.ControllerChangeHandler import com.bluelinelabs.conductor.ControllerChangeType -import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton import com.google.android.material.snackbar.Snackbar import dev.chrisbanes.insetter.applyInsetter @@ -542,18 +541,8 @@ class MangaController : private fun showAddDuplicateDialog(newManga: Manga, libraryManga: Manga) { activity?.let { - val source = sourceManager.getOrStub(libraryManga.source) - MaterialAlertDialogBuilder(it).apply { - setMessage(activity?.getString(R.string.confirm_manga_add_duplicate, source.name)) - setPositiveButton(activity?.getString(R.string.action_add)) { _, _ -> - addToLibrary(newManga) - } - setNegativeButton(activity?.getString(R.string.action_cancel)) { _, _ -> } - setNeutralButton(activity?.getString(R.string.action_show_manga)) { _, _ -> - router.pushController(MangaController(libraryManga).withFadeTransaction()) - } - setCancelable(true) - }.create().show() + AddDuplicateMangaDialog(this, libraryManga) { addToLibrary(newManga) } + .showDialog(router) } } From a89651810d08820d1d809ccea3659d4a364cb5f4 Mon Sep 17 00:00:00 2001 From: arkon Date: Sat, 13 Aug 2022 15:15:14 -0400 Subject: [PATCH 25/26] Don't allow swiping away app update install notification Based on 85ef40d0ffa3c3759d88f800d471fd24db8879de --- .../java/eu/kanade/tachiyomi/data/updater/AppUpdateNotifier.kt | 1 + app/src/main/res/values/strings.xml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/updater/AppUpdateNotifier.kt b/app/src/main/java/eu/kanade/tachiyomi/data/updater/AppUpdateNotifier.kt index 242835f380..d0979e8251 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/updater/AppUpdateNotifier.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/updater/AppUpdateNotifier.kt @@ -116,6 +116,7 @@ internal class AppUpdateNotifier(private val context: Context) { setOnlyAlertOnce(false) setProgress(0, 0, false) setContentIntent(installIntent) + setOngoing(true) clearActions() addAction( diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 33b2c72d97..cf9840cee8 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -775,7 +775,7 @@ Downloading… - Download complete + Tap to install Download error New version available! A new version is available from the official releases. Tap to learn how to migrate from unofficial F-Droid releases. From 8811d951d0aa9dd4e3f899743a44a79c83575ddd Mon Sep 17 00:00:00 2001 From: arkon Date: Sun, 14 Aug 2022 10:32:04 -0400 Subject: [PATCH 26/26] Release v0.13.6 --- .github/ISSUE_TEMPLATE.md | 2 +- .github/ISSUE_TEMPLATE/report_issue.yml | 4 ++-- .github/ISSUE_TEMPLATE/request_feature.yml | 2 +- app/build.gradle.kts | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index af7a654073..55808bb685 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -3,7 +3,7 @@ I acknowledge that: - I have updated: - - To the latest version of the app (stable is v0.13.5) + - To the latest version of the app (stable is v0.13.6) - All extensions - I have tried the troubleshooting guide: https://tachiyomi.org/help/guides/troubleshooting-problems/ - If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions diff --git a/.github/ISSUE_TEMPLATE/report_issue.yml b/.github/ISSUE_TEMPLATE/report_issue.yml index 2505eb52b9..de2f7f0540 100644 --- a/.github/ISSUE_TEMPLATE/report_issue.yml +++ b/.github/ISSUE_TEMPLATE/report_issue.yml @@ -53,7 +53,7 @@ body: label: Tachiyomi version description: You can find your Tachiyomi version in **More → About**. placeholder: | - Example: "0.13.5" + Example: "0.13.6" validations: required: true @@ -98,7 +98,7 @@ body: required: true - label: I have tried the [troubleshooting guide](https://tachiyomi.org/help/guides/troubleshooting/). required: true - - label: I have updated the app to version **[0.13.5](https://github.com/tachiyomiorg/tachiyomi/releases/latest)**. + - label: I have updated the app to version **[0.13.6](https://github.com/tachiyomiorg/tachiyomi/releases/latest)**. required: true - label: I have updated all installed extensions. required: true diff --git a/.github/ISSUE_TEMPLATE/request_feature.yml b/.github/ISSUE_TEMPLATE/request_feature.yml index f464e08b6e..ff898c3837 100644 --- a/.github/ISSUE_TEMPLATE/request_feature.yml +++ b/.github/ISSUE_TEMPLATE/request_feature.yml @@ -33,7 +33,7 @@ body: required: true - label: If this is an issue with an extension, I should be opening an issue in the [extensions repository](https://github.com/tachiyomiorg/tachiyomi-extensions/issues/new/choose). required: true - - label: I have updated the app to version **[0.13.5](https://github.com/tachiyomiorg/tachiyomi/releases/latest)**. + - label: I have updated the app to version **[0.13.6](https://github.com/tachiyomiorg/tachiyomi/releases/latest)**. required: true - label: I will fill out all of the requested information in this form. required: true diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 83121b5e9b..5c3ff6ceee 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -26,8 +26,8 @@ android { applicationId = "eu.kanade.tachiyomi" minSdk = AndroidConfig.minSdk targetSdk = AndroidConfig.targetSdk - versionCode = 81 - versionName = "0.13.5" + versionCode = 82 + versionName = "0.13.6" buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"") buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")