Compare commits
No commits in common. "main" and "v1.13.5-runner-test-20260426171738" have entirely different histories.
main
...
v1.13.5-ru
289 changed files with 3440 additions and 7966 deletions
|
|
@ -1,16 +1,9 @@
|
|||
name: Forgejo Release Builder
|
||||
# NOTE: keep Forgejo action refs node20-compatible for runner v3.5.1.
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- v*
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
tag:
|
||||
description: 'Release tag to build and publish (e.g. v1.13.6). Must already exist on the remote.'
|
||||
required: true
|
||||
type: string
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
|
@ -21,75 +14,32 @@ concurrency:
|
|||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: desktop-release
|
||||
runs-on: forgejo-release
|
||||
steps:
|
||||
- name: Clone repository
|
||||
shell: bash
|
||||
env:
|
||||
FORGEJO_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
DISPATCH_TAG: ${{ github.event.inputs.tag }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
rm -rf .git
|
||||
git init .
|
||||
origin_url="${GITHUB_SERVER_URL#https://}"
|
||||
origin_url="${origin_url#http://}"
|
||||
git remote add origin "https://copilot:${FORGEJO_TOKEN}@${origin_url}/${GITHUB_REPOSITORY}.git"
|
||||
if [ "${GITHUB_EVENT_NAME}" = "workflow_dispatch" ]; then
|
||||
git -c http.sslVerify=false fetch --tags --prune origin "refs/tags/${DISPATCH_TAG}"
|
||||
else
|
||||
git -c http.sslVerify=false fetch --tags --prune origin "${GITHUB_SHA}"
|
||||
fi
|
||||
git checkout --force FETCH_HEAD
|
||||
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up JDK
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
pkg_install() {
|
||||
if [ "$(id -u)" -eq 0 ]; then
|
||||
"$@"
|
||||
elif command -v sudo >/dev/null 2>&1; then
|
||||
sudo "$@"
|
||||
else
|
||||
echo "Need root or sudo to install packages."
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
if command -v java >/dev/null 2>&1; then
|
||||
java -version
|
||||
else
|
||||
echo "PATH=$PATH"
|
||||
echo "pkg managers: dnf=$(command -v dnf || true) apt=$(command -v apt-get || true) apk=$(command -v apk || true) pacman=$(command -v pacman || true)"
|
||||
if [ -x /usr/bin/dnf ] || command -v dnf >/dev/null 2>&1; then
|
||||
pkg_install dnf install -y java-17-openjdk-devel
|
||||
elif [ -x /usr/bin/apt-get ] || command -v apt-get >/dev/null 2>&1; then
|
||||
pkg_install apt-get update
|
||||
pkg_install apt-get install -y openjdk-17-jdk
|
||||
elif [ -x /sbin/apk ] || [ -x /usr/sbin/apk ] || command -v apk >/dev/null 2>&1; then
|
||||
pkg_install apk add --no-cache openjdk17
|
||||
elif [ -x /usr/bin/pacman ] || command -v pacman >/dev/null 2>&1; then
|
||||
pkg_install pacman -Sy --noconfirm jdk17-openjdk
|
||||
else
|
||||
echo "No supported package manager found for JDK installation."
|
||||
exit 1
|
||||
fi
|
||||
java -version
|
||||
fi
|
||||
if command -v javac >/dev/null 2>&1; then
|
||||
javac -version
|
||||
java_home="$(dirname "$(dirname "$(readlink -f "$(command -v javac)")")")"
|
||||
echo "JAVA_HOME=$java_home" >> "$GITHUB_ENV"
|
||||
fi
|
||||
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0
|
||||
with:
|
||||
java-version: 17
|
||||
distribution: temurin
|
||||
|
||||
- name: Configure persistent Gradle cache
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
gradle_home="${KOMIKKU_GRADLE_CACHE:-${HOME}/.cache/komikku-ci-gradle}"
|
||||
mkdir -p "${gradle_home}"
|
||||
echo "GRADLE_USER_HOME=${gradle_home}" >> "${GITHUB_ENV}"
|
||||
echo "Gradle dependencies/cache: ${gradle_home}"
|
||||
- name: Write google-services.json
|
||||
uses: DamianReeves/write-file-action@6929a9a6d1807689191dcc8bbe62b54d70a32b42 # v1.3
|
||||
with:
|
||||
path: app/google-services.json
|
||||
contents: ${{ secrets.GOOGLE_SERVICES_JSON }}
|
||||
write-mode: overwrite
|
||||
|
||||
- name: Write client_secrets.json
|
||||
uses: DamianReeves/write-file-action@6929a9a6d1807689191dcc8bbe62b54d70a32b42 # v1.3
|
||||
with:
|
||||
path: app/src/main/assets/client_secrets.json
|
||||
contents: ${{ secrets.GOOGLE_CLIENT_SECRETS_JSON }}
|
||||
write-mode: overwrite
|
||||
|
||||
- name: Prepare release metadata
|
||||
id: prepare
|
||||
|
|
@ -97,233 +47,46 @@ jobs:
|
|||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
if [ "${GITHUB_EVENT_NAME}" = "workflow_dispatch" ]; then
|
||||
version_tag="${{ github.event.inputs.tag }}"
|
||||
else
|
||||
version_tag="${GITHUB_REF_NAME:-${GITHUB_REF#refs/tags/}}"
|
||||
fi
|
||||
version_display="$(printf '%s' "$version_tag" | sed -E 's/^v//' )"
|
||||
version_tag="${GITHUB_REF_NAME:-${GITHUB_REF#refs/tags/}}"
|
||||
previous_tag="$(git tag --list 'v*' --sort=-version:refname | grep -Fxv "$version_tag" | head -n 1 || true)"
|
||||
upstream_repo="komikku-app/komikku"
|
||||
upstream_ref=""
|
||||
upstream_base=""
|
||||
for ref in master main; do
|
||||
candidate="$(git ls-remote "https://github.com/${upstream_repo}.git" "refs/heads/${ref}" | awk '{print $1}' || true)"
|
||||
if [ -n "$candidate" ]; then
|
||||
upstream_ref="$ref"
|
||||
upstream_base="$candidate"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
echo "VERSION_TAG=$version_tag" >> "$GITHUB_OUTPUT"
|
||||
echo "VERSION_DISPLAY=$version_display" >> "$GITHUB_OUTPUT"
|
||||
echo "PREV_TAG_NAME=$previous_tag" >> "$GITHUB_OUTPUT"
|
||||
echo "UPSTREAM_REPO=$upstream_repo" >> "$GITHUB_OUTPUT"
|
||||
echo "UPSTREAM_REF=$upstream_ref" >> "$GITHUB_OUTPUT"
|
||||
echo "UPSTREAM_BASE=$upstream_base" >> "$GITHUB_OUTPUT"
|
||||
|
||||
if [ -n "$upstream_base" ]; then
|
||||
git fetch --no-tags --depth=1 "https://github.com/${upstream_repo}.git" "$upstream_base" || true
|
||||
fi
|
||||
|
||||
# Release notes: only commits since the *previous release tag* (short, correct scope).
|
||||
# Do not use upstream_base..HEAD here — that lists the whole fork delta and is huge.
|
||||
changelog_commit_limit=50
|
||||
log_range=""
|
||||
if [ -n "$previous_tag" ]; then
|
||||
log_range="${previous_tag}..HEAD"
|
||||
fi
|
||||
|
||||
commit_log_file="$(mktemp /tmp/komikku-commit-logs.XXXXXX)"
|
||||
declare -a new_lines fix_lines improve_lines other_lines
|
||||
|
||||
bucket_for_subject() {
|
||||
local s="$1"
|
||||
local b=other
|
||||
shopt -s nocasematch
|
||||
local r_feat r_feature r_add r_fix r_perf r_refactor r_chore r_docs r_style r_test r_ci r_build r_improve
|
||||
r_feat='^feat\([^)]*\):'
|
||||
r_feature='^feature\([^)]*\):'
|
||||
r_add='^add\([^)]*\):'
|
||||
r_fix='^fix\([^)]*\):'
|
||||
r_perf='^perf\([^)]*\):'
|
||||
r_refactor='^refactor\([^)]*\):'
|
||||
r_chore='^chore\([^)]*\):'
|
||||
r_docs='^docs\([^)]*\):'
|
||||
r_style='^style\([^)]*\):'
|
||||
r_test='^test\([^)]*\):'
|
||||
r_ci='^ci\([^)]*\):'
|
||||
r_build='^build\([^)]*\):'
|
||||
r_improve='^improve\([^)]*\):'
|
||||
if [[ "$s" =~ ^feat: ]] || [[ "$s" =~ $r_feat ]]; then b=new
|
||||
elif [[ "$s" =~ ^feature: ]] || [[ "$s" =~ $r_feature ]]; then b=new
|
||||
elif [[ "$s" =~ ^add: ]] || [[ "$s" =~ $r_add ]]; then b=new
|
||||
elif [[ "$s" =~ ^fix: ]] || [[ "$s" =~ $r_fix ]]; then b=fix
|
||||
elif [[ "$s" =~ ^perf: ]] || [[ "$s" =~ $r_perf ]]; then b=improve
|
||||
elif [[ "$s" =~ ^refactor: ]] || [[ "$s" =~ $r_refactor ]]; then b=improve
|
||||
elif [[ "$s" =~ ^chore: ]] || [[ "$s" =~ $r_chore ]]; then b=improve
|
||||
elif [[ "$s" =~ ^docs: ]] || [[ "$s" =~ $r_docs ]]; then b=improve
|
||||
elif [[ "$s" =~ ^style: ]] || [[ "$s" =~ $r_style ]]; then b=improve
|
||||
elif [[ "$s" =~ ^test: ]] || [[ "$s" =~ $r_test ]]; then b=improve
|
||||
elif [[ "$s" =~ ^ci: ]] || [[ "$s" =~ $r_ci ]]; then b=improve
|
||||
elif [[ "$s" =~ ^build: ]] || [[ "$s" =~ $r_build ]]; then b=improve
|
||||
elif [[ "$s" =~ ^improve: ]] || [[ "$s" =~ $r_improve ]]; then b=improve
|
||||
fi
|
||||
shopt -u nocasematch
|
||||
printf '%s' "$b"
|
||||
}
|
||||
|
||||
clear_buckets() {
|
||||
new_lines=() fix_lines=() improve_lines=() other_lines=()
|
||||
}
|
||||
|
||||
fill_buckets_from_git() {
|
||||
local rev_range="$1"
|
||||
shift
|
||||
while IFS= read -r line || [ -n "${line:-}" ]; do
|
||||
[ -z "${line:-}" ] && continue
|
||||
subject="${line%%$'\x1f'*}"
|
||||
author="${line#*$'\x1f'}"
|
||||
entry="- ${subject} (@${author})"
|
||||
case "$(bucket_for_subject "$subject")" in
|
||||
new) new_lines+=("$entry") ;;
|
||||
fix) fix_lines+=("$entry") ;;
|
||||
improve) improve_lines+=("$entry") ;;
|
||||
*) other_lines+=("$entry") ;;
|
||||
esac
|
||||
done < <(git log "$@" --pretty=tformat:'%s%x1f%an' "$rev_range")
|
||||
}
|
||||
|
||||
emit_buckets_from_arrays() {
|
||||
local any=0
|
||||
if [ ${#new_lines[@]} -gt 0 ]; then
|
||||
printf '%s\n' "##### New" ""
|
||||
printf '%s\n' "${new_lines[@]}"
|
||||
printf '\n'
|
||||
any=1
|
||||
fi
|
||||
if [ ${#fix_lines[@]} -gt 0 ]; then
|
||||
printf '%s\n' "##### Fix" ""
|
||||
printf '%s\n' "${fix_lines[@]}"
|
||||
printf '\n'
|
||||
any=1
|
||||
fi
|
||||
if [ ${#improve_lines[@]} -gt 0 ]; then
|
||||
printf '%s\n' "##### Improve" ""
|
||||
printf '%s\n' "${improve_lines[@]}"
|
||||
printf '\n'
|
||||
any=1
|
||||
fi
|
||||
if [ ${#other_lines[@]} -gt 0 ]; then
|
||||
printf '%s\n' "##### Other" ""
|
||||
printf '%s\n' "${other_lines[@]}"
|
||||
printf '\n'
|
||||
any=1
|
||||
fi
|
||||
[ "$any" -eq 1 ]
|
||||
}
|
||||
|
||||
if [ -n "$log_range" ]; then
|
||||
total_in_range="$(git rev-list --count "${log_range}" 2>/dev/null || printf '0')"
|
||||
clear_buckets
|
||||
fill_buckets_from_git "$log_range" "-n${changelog_commit_limit}"
|
||||
{
|
||||
if emit_buckets_from_arrays; then
|
||||
if [ "${total_in_range}" -gt "${changelog_commit_limit}" ] 2>/dev/null; then
|
||||
printf '%s\n' "" "_Showing the ${changelog_commit_limit} most recent commits since ${previous_tag}._" \
|
||||
"_Older commits in this release: use **Full Changelog** below._" ""
|
||||
fi
|
||||
else
|
||||
printf '%s\n' "##### Other" "" "- No commit messages in this tag range." ""
|
||||
fi
|
||||
} > "${commit_log_file}"
|
||||
commit_logs="$(git log --pretty=format:'- %s (@%an)' "${previous_tag}..HEAD")"
|
||||
else
|
||||
printf '%s\n' "##### Other" "" "- First release tag, or no prior \`v*\` tag found — see repository history." "" > "${commit_log_file}"
|
||||
commit_logs='- Initial release'
|
||||
fi
|
||||
|
||||
# Do not pass multiline commit text via step outputs / env (breaks some Forgejo/act_runner
|
||||
# runners before the shell starts — empty logs). Only pass a temp file path.
|
||||
echo "COMMIT_LOG_FILE=${commit_log_file}" >> "${GITHUB_ENV}"
|
||||
{
|
||||
echo 'COMMIT_LOGS<<EOF'
|
||||
printf '%s\n' "$commit_logs"
|
||||
echo 'EOF'
|
||||
} >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Set up Gradle
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
sdk_candidates=(
|
||||
"${ANDROID_HOME:-}"
|
||||
"${ANDROID_SDK_ROOT:-}"
|
||||
"/opt/android-sdk-linux"
|
||||
"/home/yuna/Android/Sdk"
|
||||
"/home/runner/Android/Sdk"
|
||||
"/root/Android/Sdk"
|
||||
)
|
||||
sdk_dir=""
|
||||
for candidate in "${sdk_candidates[@]}"; do
|
||||
if [ -n "$candidate" ] && [ -d "$candidate/platforms" ] && [ -d "$candidate/build-tools" ]; then
|
||||
sdk_dir="$candidate"
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [ -z "$sdk_dir" ]; then
|
||||
echo "Android SDK not found. Checked:"
|
||||
for candidate in "${sdk_candidates[@]}"; do
|
||||
[ -n "$candidate" ] && echo " - $candidate"
|
||||
done
|
||||
exit 1
|
||||
fi
|
||||
export ANDROID_HOME="$sdk_dir"
|
||||
export ANDROID_SDK_ROOT="$sdk_dir"
|
||||
echo "ANDROID_HOME=$ANDROID_HOME" >> "$GITHUB_ENV"
|
||||
echo "ANDROID_SDK_ROOT=$ANDROID_SDK_ROOT" >> "$GITHUB_ENV"
|
||||
sdkmanager_bin="$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager"
|
||||
if [ -x "$sdkmanager_bin" ]; then
|
||||
if [ ! -x "$ANDROID_HOME/build-tools/35.0.0/aidl" ]; then
|
||||
echo "Installing Android SDK components via sdkmanager..."
|
||||
set +o pipefail
|
||||
yes | "$sdkmanager_bin" --licenses >/dev/null || true
|
||||
yes | "$sdkmanager_bin" "platform-tools" "build-tools;35.0.0"
|
||||
set -o pipefail
|
||||
else
|
||||
echo "Android build-tools 35.0.0 present; skipping sdkmanager (saves time)."
|
||||
fi
|
||||
else
|
||||
echo "sdkmanager not found at $sdkmanager_bin"
|
||||
fi
|
||||
if [ ! -x "$ANDROID_HOME/build-tools/35.0.0/aidl" ]; then
|
||||
echo "Expected executable missing: $ANDROID_HOME/build-tools/35.0.0/aidl"
|
||||
ls -la "$ANDROID_HOME/build-tools" || true
|
||||
exit 1
|
||||
fi
|
||||
printf 'sdk.dir=%s\n' "$ANDROID_HOME" > local.properties
|
||||
chmod +x ./gradlew
|
||||
./gradlew --no-daemon --max-workers=1 \
|
||||
"-Dorg.gradle.jvmargs=-Xmx4096m -XX:MaxMetaspaceSize=1024m -XX:ReservedCodeCacheSize=256m -Dfile.encoding=UTF-8" \
|
||||
-Dorg.gradle.parallel=false \
|
||||
-Dorg.gradle.configureondemand=false \
|
||||
-Dorg.gradle.vfs.watch=false \
|
||||
-Dkotlin.daemon.enabled=false \
|
||||
"-Dkotlin.daemon.jvmargs=-Xmx2048m,-XX:MaxMetaspaceSize=512m,-XX:ReservedCodeCacheSize=256m" \
|
||||
-Dkotlin.compiler.execution.strategy=in-process \
|
||||
-Pkotlin.incremental=true \
|
||||
--version
|
||||
uses: gradle/actions/setup-gradle@f29f5a9d7b09a7c6b29859002d29d24e1674c884 # v5
|
||||
|
||||
- name: Check code format
|
||||
run: ./gradlew spotlessCheck
|
||||
|
||||
- name: Build app
|
||||
run: |
|
||||
if [ -n "${ANDROID_HOME:-}" ]; then
|
||||
printf 'sdk.dir=%s\n' "$ANDROID_HOME" > local.properties
|
||||
fi
|
||||
./gradlew --no-daemon --max-workers=1 \
|
||||
"-Dorg.gradle.jvmargs=-Xmx4096m -XX:MaxMetaspaceSize=1024m -XX:ReservedCodeCacheSize=256m -Dfile.encoding=UTF-8" \
|
||||
-Dorg.gradle.parallel=false \
|
||||
-Dorg.gradle.configureondemand=false \
|
||||
-Dorg.gradle.vfs.watch=false \
|
||||
-Dkotlin.daemon.enabled=false \
|
||||
"-Dkotlin.daemon.jvmargs=-Xmx2048m,-XX:MaxMetaspaceSize=512m,-XX:ReservedCodeCacheSize=256m" \
|
||||
-Dkotlin.compiler.execution.strategy=in-process \
|
||||
-Pkotlin.incremental=true \
|
||||
--stacktrace \
|
||||
assembleRelease -Penable-updater -Pdisable-code-shrink
|
||||
run: ./gradlew assembleRelease -Pinclude-telemetry -Penable-updater
|
||||
|
||||
- name: Run unit tests
|
||||
run: ./gradlew testReleaseUnitTest
|
||||
|
||||
- name: Sign APK
|
||||
uses: r0adkll/sign-android-release@f30bdd30588842ac76044ecdbd4b6d0e3e813478
|
||||
with:
|
||||
releaseDirectory: app/build/outputs/apk/release
|
||||
signingKeyBase64: ${{ secrets.SIGNING_KEY }}
|
||||
alias: ${{ secrets.ALIAS }}
|
||||
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
|
||||
keyPassword: ${{ secrets.KEY_PASSWORD }}
|
||||
env:
|
||||
BUILD_TOOLS_VERSION: '35.0.1'
|
||||
|
||||
- name: Rename release artifacts
|
||||
shell: bash
|
||||
|
|
@ -332,113 +95,13 @@ jobs:
|
|||
|
||||
version_tag="${{ steps.prepare.outputs.VERSION_TAG }}"
|
||||
|
||||
mv app/build/outputs/apk/release/app-universal-release-unsigned.apk "Komikku-${version_tag}-unsigned.apk"
|
||||
mv app/build/outputs/apk/release/app-arm64-v8a-release-unsigned.apk "Komikku-arm64-v8a-${version_tag}-unsigned.apk"
|
||||
mv app/build/outputs/apk/release/app-armeabi-v7a-release-unsigned.apk "Komikku-armeabi-v7a-${version_tag}-unsigned.apk"
|
||||
mv app/build/outputs/apk/release/app-x86-release-unsigned.apk "Komikku-x86-${version_tag}-unsigned.apk"
|
||||
mv app/build/outputs/apk/release/app-x86_64-release-unsigned.apk "Komikku-x86_64-${version_tag}-unsigned.apk"
|
||||
mv app/build/outputs/apk/release/app-universal-release-unsigned-signed.apk "Komikku-${version_tag}.apk"
|
||||
mv app/build/outputs/apk/release/app-arm64-v8a-release-unsigned-signed.apk "Komikku-arm64-v8a-${version_tag}.apk"
|
||||
mv app/build/outputs/apk/release/app-armeabi-v7a-release-unsigned-signed.apk "Komikku-armeabi-v7a-${version_tag}.apk"
|
||||
mv app/build/outputs/apk/release/app-x86-release-unsigned-signed.apk "Komikku-x86-${version_tag}.apk"
|
||||
mv app/build/outputs/apk/release/app-x86_64-release-unsigned-signed.apk "Komikku-x86_64-${version_tag}.apk"
|
||||
|
||||
if [ -d app/build/outputs/mapping/release ]; then
|
||||
zip -qr "Komikku-mapping-${version_tag}.zip" app/build/outputs/mapping/release
|
||||
fi
|
||||
|
||||
- name: Sign APKs
|
||||
shell: bash
|
||||
env:
|
||||
SIGNING_KEYSTORE_B64: ${{ secrets.SIGNING_KEYSTORE_B64 }}
|
||||
SIGNING_KEYSTORE_PASSWORD: ${{ secrets.SIGNING_KEYSTORE_PASSWORD }}
|
||||
SIGNING_KEY_ALIAS: ${{ secrets.SIGNING_KEY_ALIAS }}
|
||||
SIGNING_KEY_PASSWORD: ${{ secrets.SIGNING_KEY_PASSWORD }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
if [ -z "${SIGNING_KEYSTORE_B64}" ] || [ -z "${SIGNING_KEYSTORE_PASSWORD}" ] || [ -z "${SIGNING_KEY_ALIAS}" ] || [ -z "${SIGNING_KEY_PASSWORD}" ]; then
|
||||
echo "Signing secrets are required for installable release APKs."
|
||||
echo "Set SIGNING_KEYSTORE_B64, SIGNING_KEYSTORE_PASSWORD, SIGNING_KEY_ALIAS, SIGNING_KEY_PASSWORD."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
keystore_file="$(mktemp)"
|
||||
printf '%s' "${SIGNING_KEYSTORE_B64}" | base64 -d > "${keystore_file}"
|
||||
|
||||
apksigner_bin=""
|
||||
if command -v apksigner >/dev/null 2>&1; then
|
||||
apksigner_bin="$(command -v apksigner)"
|
||||
elif [ -n "${ANDROID_HOME:-}" ] && [ -d "${ANDROID_HOME}/build-tools" ]; then
|
||||
apksigner_bin="$(ls -1 "${ANDROID_HOME}"/build-tools/*/apksigner 2>/dev/null | sort -V | tail -n 1 || true)"
|
||||
fi
|
||||
|
||||
if [ -z "${apksigner_bin}" ]; then
|
||||
echo "apksigner not found in PATH or ANDROID_HOME/build-tools."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for unsigned_apk in Komikku-*-unsigned.apk; do
|
||||
[ -e "${unsigned_apk}" ] || continue
|
||||
signed_apk="${unsigned_apk/-unsigned.apk/.apk}"
|
||||
"${apksigner_bin}" sign \
|
||||
--ks "${keystore_file}" \
|
||||
--ks-key-alias "${SIGNING_KEY_ALIAS}" \
|
||||
--ks-pass "pass:${SIGNING_KEYSTORE_PASSWORD}" \
|
||||
--key-pass "pass:${SIGNING_KEY_PASSWORD}" \
|
||||
--out "${signed_apk}" \
|
||||
"${unsigned_apk}"
|
||||
"${apksigner_bin}" verify "${signed_apk}"
|
||||
done
|
||||
|
||||
# Catch corrupt / non-APK artifacts before upload (e.g. truncated build output). Same
|
||||
# manifest parse path the installer uses for "Paket konnte nicht geparst werden".
|
||||
aapt_bin=""
|
||||
if [ -n "${ANDROID_HOME:-}" ] && [ -d "${ANDROID_HOME}/build-tools" ]; then
|
||||
aapt_bin="$(ls -1 "${ANDROID_HOME}"/build-tools/*/aapt 2>/dev/null | sort -V | tail -n 1 || true)"
|
||||
fi
|
||||
if [ -z "${aapt_bin}" ] || [ ! -x "${aapt_bin}" ]; then
|
||||
echo "aapt not found under ANDROID_HOME; cannot verify APK manifests."
|
||||
exit 1
|
||||
fi
|
||||
for signed_apk in Komikku-*.apk; do
|
||||
case "${signed_apk}" in *-unsigned.apk) continue ;; esac
|
||||
echo "Verifying installable APK: ${signed_apk}"
|
||||
if ! unzip -tqq "${signed_apk}" >/dev/null 2>&1; then
|
||||
echo "ERROR: ${signed_apk} is not a valid zip (truncated or corrupt build output?)."
|
||||
exit 1
|
||||
fi
|
||||
first_line="$("${aapt_bin}" dump badging "${signed_apk}" 2>/dev/null | head -n 1 || true)"
|
||||
if ! printf '%s\n' "${first_line}" | grep -q '^package:'; then
|
||||
echo "ERROR: ${signed_apk} failed manifest parse (first line: ${first_line:-<empty>})."
|
||||
echo "First 64 bytes (expect PK zip header 504b0304):"
|
||||
head -c 64 "${signed_apk}" | hexdump -C || true
|
||||
exit 1
|
||||
fi
|
||||
sha256sum "${signed_apk}"
|
||||
done
|
||||
|
||||
- name: Create Obtainium-friendly aliases
|
||||
shell: bash
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
alias_asset() {
|
||||
src="$1"
|
||||
dst="$2"
|
||||
if [ -e "$src" ]; then
|
||||
cp -f "$src" "$dst"
|
||||
fi
|
||||
}
|
||||
|
||||
# Only publish signed APK aliases.
|
||||
alias_asset "$(ls -1t Komikku-arm64-v8a-*.apk 2>/dev/null | head -n1)" "Komikku-arm64-v8a.apk"
|
||||
|
||||
for candidate in Komikku-*.apk; do
|
||||
case "$candidate" in
|
||||
*arm64-v8a*|*armeabi-v7a*|*x86_64*|*x86*|*-unsigned.apk)
|
||||
;;
|
||||
*)
|
||||
alias_asset "$candidate" "Komikku-universal.apk"
|
||||
break
|
||||
;;
|
||||
esac
|
||||
done
|
||||
zip -qr "Komikku-mapping-${version_tag}.zip" app/build/outputs/mapping/release
|
||||
|
||||
- name: Create or update release
|
||||
id: release
|
||||
|
|
@ -446,330 +109,142 @@ jobs:
|
|||
env:
|
||||
FORGEJO_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
VERSION_TAG: ${{ steps.prepare.outputs.VERSION_TAG }}
|
||||
VERSION_DISPLAY: ${{ steps.prepare.outputs.VERSION_DISPLAY }}
|
||||
PREV_TAG_NAME: ${{ steps.prepare.outputs.PREV_TAG_NAME }}
|
||||
COMMIT_LOGS: ${{ steps.prepare.outputs.COMMIT_LOGS }}
|
||||
REPO_URL: ${{ github.server_url }}/${{ github.repository }}
|
||||
REPOSITORY_NAME: ${{ github.repository }}
|
||||
FORGEJO_CURL_INSECURE: "true"
|
||||
run: |
|
||||
set -euo pipefail
|
||||
echo "Create or update release: shell running; COMMIT_LOG_FILE=${COMMIT_LOG_FILE:-<unset>}"
|
||||
|
||||
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PATH:-}"
|
||||
|
||||
discover_jq() {
|
||||
if command -v jq >/dev/null 2>&1; then
|
||||
JQ_BIN="$(command -v jq)"
|
||||
export JQ_BIN
|
||||
return 0
|
||||
fi
|
||||
for cand in /usr/bin/jq /usr/local/bin/jq /bin/jq; do
|
||||
if [ -x "$cand" ]; then
|
||||
JQ_BIN="$cand"
|
||||
export JQ_BIN
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
ensure_jq() {
|
||||
if discover_jq; then
|
||||
return 0
|
||||
fi
|
||||
run_as_root() {
|
||||
if [ "$(id -u)" -eq 0 ]; then
|
||||
"$@"
|
||||
elif command -v sudo >/dev/null 2>&1; then
|
||||
sudo "$@"
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
if command -v dnf >/dev/null 2>&1; then
|
||||
run_as_root dnf install -y jq
|
||||
elif command -v apt-get >/dev/null 2>&1; then
|
||||
run_as_root apt-get update
|
||||
run_as_root apt-get install -y jq
|
||||
elif command -v apk >/dev/null 2>&1; then
|
||||
run_as_root apk add --no-cache jq
|
||||
elif command -v pacman >/dev/null 2>&1; then
|
||||
run_as_root pacman -Sy --noconfirm jq
|
||||
fi
|
||||
discover_jq
|
||||
}
|
||||
ensure_curl() {
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
run_as_root() {
|
||||
if [ "$(id -u)" -eq 0 ]; then
|
||||
"$@"
|
||||
elif command -v sudo >/dev/null 2>&1; then
|
||||
sudo "$@"
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
if command -v dnf >/dev/null 2>&1; then
|
||||
run_as_root dnf install -y curl
|
||||
elif command -v apt-get >/dev/null 2>&1; then
|
||||
run_as_root apt-get update
|
||||
run_as_root apt-get install -y curl
|
||||
elif command -v apk >/dev/null 2>&1; then
|
||||
run_as_root apk add --no-cache curl
|
||||
elif command -v pacman >/dev/null 2>&1; then
|
||||
run_as_root pacman -Sy --noconfirm curl
|
||||
fi
|
||||
command -v curl >/dev/null 2>&1
|
||||
}
|
||||
ensure_jq || { echo "jq not found; install jq on the runner (dnf install -y jq)."; exit 1; }
|
||||
ensure_curl || { echo "curl not found; install curl on the runner (dnf install -y curl)."; exit 1; }
|
||||
curl_tls_args=()
|
||||
if [ "${FORGEJO_CURL_INSECURE:-false}" = "true" ]; then
|
||||
curl_tls_args+=(--insecure)
|
||||
fi
|
||||
|
||||
api_base="${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/releases"
|
||||
payload_file="$(mktemp)"
|
||||
response_file="$(mktemp)"
|
||||
|
||||
commit_logs=""
|
||||
if [ -n "${COMMIT_LOG_FILE:-}" ] && [ -f "${COMMIT_LOG_FILE}" ]; then
|
||||
commit_logs="$(cat "${COMMIT_LOG_FILE}")"
|
||||
fi
|
||||
commit_logs="$(printf '%s' "$commit_logs" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')"
|
||||
if [ -z "$commit_logs" ]; then
|
||||
commit_logs=$'##### Other\n\n- Initial release\n'
|
||||
fi
|
||||
python3 - <<'PY' > "$payload_file"
|
||||
import json
|
||||
import os
|
||||
import textwrap
|
||||
|
||||
full_changelog=""
|
||||
if [ -n "${PREV_TAG_NAME:-}" ]; then
|
||||
full_changelog="**Full Changelog**: [${REPOSITORY_NAME}@${PREV_TAG_NAME}...${VERSION_TAG}](${REPO_URL}/compare/${PREV_TAG_NAME}...${VERSION_TAG})"
|
||||
fi
|
||||
version_tag = os.environ["VERSION_TAG"]
|
||||
previous_tag = os.environ.get("PREV_TAG_NAME", "").strip()
|
||||
commit_logs = os.environ.get("COMMIT_LOGS", "").strip() or "- Initial release"
|
||||
repo_url = os.environ["REPO_URL"]
|
||||
repository_name = os.environ["REPOSITORY_NAME"]
|
||||
|
||||
release_body_file="$(mktemp)"
|
||||
{
|
||||
printf '%s\n' \
|
||||
"#### What's Changed" \
|
||||
"" \
|
||||
"${commit_logs}" \
|
||||
""
|
||||
if [ -n "${full_changelog}" ]; then
|
||||
printf '%s\n' "${full_changelog}" ""
|
||||
fi
|
||||
printf '%s\n' \
|
||||
"<!-->" \
|
||||
"> [!TIP]" \
|
||||
">" \
|
||||
"> ### If you are unsure which version to download then go with \`Komikku-universal.apk\`"
|
||||
} > "${release_body_file}"
|
||||
if previous_tag:
|
||||
full_changelog = (
|
||||
f"**Full Changelog**: "
|
||||
f"[{repository_name}@{previous_tag}...{version_tag}]"
|
||||
f"({repo_url}/compare/{previous_tag}...{version_tag})"
|
||||
)
|
||||
else:
|
||||
full_changelog = ""
|
||||
|
||||
# Build JSON — body from file only (jq --arg body "$huge" hits ARG_MAX).
|
||||
"${JQ_BIN}" -n \
|
||||
--arg tag "${VERSION_TAG}" \
|
||||
--arg title "Komikku ${VERSION_DISPLAY}" \
|
||||
--rawfile body "${release_body_file}" \
|
||||
'{tag_name: $tag, name: $title, body: $body, draft: false, prerelease: false}' > "${payload_file}"
|
||||
body = textwrap.dedent(
|
||||
f"""\
|
||||
#### What's Changed
|
||||
##### New
|
||||
|
||||
tag_path_enc="$("${JQ_BIN}" -nr --arg t "${VERSION_TAG}" '$t|@uri')"
|
||||
tag_get_url="${api_base}/tags/${tag_path_enc}"
|
||||
echo "Forgejo release API: GET ${tag_get_url}"
|
||||
##### Improve
|
||||
|
||||
status="$(curl "${curl_tls_args[@]}" -sS -o "$response_file" -w '%{http_code}' \
|
||||
##### Fix
|
||||
|
||||
{commit_logs}
|
||||
|
||||
##### Based on
|
||||
|
||||
{full_changelog}
|
||||
|
||||
<!-->
|
||||
> [!TIP]
|
||||
>
|
||||
> ### If you are unsure which version to download then go with `Komikku-{version_tag}.apk`
|
||||
""",
|
||||
)
|
||||
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"tag_name": version_tag,
|
||||
"name": f"Komikku {version_tag}",
|
||||
"body": body,
|
||||
"draft": True,
|
||||
"prerelease": False,
|
||||
},
|
||||
),
|
||||
)
|
||||
PY
|
||||
|
||||
status="$(curl -sS -o "$response_file" -w '%{http_code}' \
|
||||
-H "Authorization: token ${FORGEJO_TOKEN}" \
|
||||
"${tag_get_url}")"
|
||||
"${api_base}/tags/${VERSION_TAG}")"
|
||||
|
||||
echo "release-by-tag GET HTTP status: ${status}"
|
||||
if [ "${status}" != "200" ] && [ "${status}" != "404" ]; then
|
||||
echo "Unexpected GET response body:"
|
||||
cat "$response_file"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
release_id=""
|
||||
if [ "${status}" = "200" ]; then
|
||||
release_id="$("${JQ_BIN}" -r '.id // empty' < "$response_file")"
|
||||
if [ -z "${release_id}" ] || [ "${release_id}" = "null" ]; then
|
||||
echo "GET returned 200 but missing release id; body:"
|
||||
cat "$response_file"
|
||||
exit 1
|
||||
fi
|
||||
echo "PATCH existing release id=${release_id}"
|
||||
patch_status="$(curl "${curl_tls_args[@]}" -sS -o "$response_file" -w '%{http_code}' \
|
||||
if [ "$status" = "200" ]; then
|
||||
release_id="$(python3 -c 'import json, sys; print(json.load(sys.stdin)["id"])' < "$response_file")"
|
||||
curl -fsS \
|
||||
-X PATCH \
|
||||
-H "Authorization: token ${FORGEJO_TOKEN}" \
|
||||
-H 'Content-Type: application/json' \
|
||||
--data @"$payload_file" \
|
||||
"${api_base}/${release_id}")"
|
||||
echo "PATCH HTTP status: ${patch_status}"
|
||||
if [ "${patch_status}" != "200" ]; then
|
||||
echo "PATCH response body:"
|
||||
cat "$response_file"
|
||||
exit 1
|
||||
fi
|
||||
elif [ "${status}" = "404" ]; then
|
||||
echo "POST new release for tag ${VERSION_TAG}"
|
||||
post_status="$(curl "${curl_tls_args[@]}" -sS -o "$response_file" -w '%{http_code}' \
|
||||
"${api_base}/${release_id}" \
|
||||
> "$response_file"
|
||||
elif [ "$status" = "404" ]; then
|
||||
curl -fsS \
|
||||
-X POST \
|
||||
-H "Authorization: token ${FORGEJO_TOKEN}" \
|
||||
-H 'Content-Type: application/json' \
|
||||
--data @"$payload_file" \
|
||||
"${api_base}")"
|
||||
echo "POST HTTP status: ${post_status}"
|
||||
if [ "${post_status}" != "201" ] && [ "${post_status}" != "200" ]; then
|
||||
echo "POST response body:"
|
||||
cat "$response_file"
|
||||
exit 1
|
||||
fi
|
||||
release_id="$("${JQ_BIN}" -r '.id // empty' < "$response_file")"
|
||||
if [ -z "${release_id}" ] || [ "${release_id}" = "null" ]; then
|
||||
echo "POST succeeded but missing release id; body:"
|
||||
cat "$response_file"
|
||||
exit 1
|
||||
fi
|
||||
"${api_base}" \
|
||||
> "$response_file"
|
||||
release_id="$(python3 -c 'import json, sys; print(json.load(sys.stdin)["id"])' < "$response_file")"
|
||||
else
|
||||
cat "$response_file"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "RELEASE_ID=${release_id}" >> "$GITHUB_OUTPUT"
|
||||
echo "RELEASE_ID=$release_id" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Upload release assets
|
||||
shell: bash
|
||||
env:
|
||||
FORGEJO_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
RELEASE_ID: ${{ steps.release.outputs.RELEASE_ID }}
|
||||
FORGEJO_CURL_INSECURE: "true"
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:${PATH:-}"
|
||||
|
||||
discover_jq() {
|
||||
if command -v jq >/dev/null 2>&1; then
|
||||
JQ_BIN="$(command -v jq)"
|
||||
export JQ_BIN
|
||||
return 0
|
||||
fi
|
||||
for cand in /usr/bin/jq /usr/local/bin/jq /bin/jq; do
|
||||
if [ -x "$cand" ]; then
|
||||
JQ_BIN="$cand"
|
||||
export JQ_BIN
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
ensure_jq() {
|
||||
if discover_jq; then
|
||||
return 0
|
||||
fi
|
||||
run_as_root() {
|
||||
if [ "$(id -u)" -eq 0 ]; then
|
||||
"$@"
|
||||
elif command -v sudo >/dev/null 2>&1; then
|
||||
sudo "$@"
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
if command -v dnf >/dev/null 2>&1; then
|
||||
run_as_root dnf install -y jq
|
||||
elif command -v apt-get >/dev/null 2>&1; then
|
||||
run_as_root apt-get update
|
||||
run_as_root apt-get install -y jq
|
||||
elif command -v apk >/dev/null 2>&1; then
|
||||
run_as_root apk add --no-cache jq
|
||||
elif command -v pacman >/dev/null 2>&1; then
|
||||
run_as_root pacman -Sy --noconfirm jq
|
||||
fi
|
||||
discover_jq
|
||||
}
|
||||
ensure_curl() {
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
run_as_root() {
|
||||
if [ "$(id -u)" -eq 0 ]; then
|
||||
"$@"
|
||||
elif command -v sudo >/dev/null 2>&1; then
|
||||
sudo "$@"
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
if command -v dnf >/dev/null 2>&1; then
|
||||
run_as_root dnf install -y curl
|
||||
elif command -v apt-get >/dev/null 2>&1; then
|
||||
run_as_root apt-get update
|
||||
run_as_root apt-get install -y curl
|
||||
elif command -v apk >/dev/null 2>&1; then
|
||||
run_as_root apk add --no-cache curl
|
||||
elif command -v pacman >/dev/null 2>&1; then
|
||||
run_as_root pacman -Sy --noconfirm curl
|
||||
fi
|
||||
command -v curl >/dev/null 2>&1
|
||||
}
|
||||
ensure_jq || { echo "jq not found; install jq on the runner (dnf install -y jq)."; exit 1; }
|
||||
ensure_curl || { echo "curl not found; install curl on the runner (dnf install -y curl)."; exit 1; }
|
||||
curl_tls_args=()
|
||||
if [ "${FORGEJO_CURL_INSECURE:-false}" = "true" ]; then
|
||||
curl_tls_args+=(--insecure)
|
||||
fi
|
||||
|
||||
if [ -z "${RELEASE_ID:-}" ] || [ "${RELEASE_ID}" = "null" ]; then
|
||||
echo "RELEASE_ID missing or null — create/update release step likely failed."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
release_api="${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/releases/${RELEASE_ID}"
|
||||
assets_file="$(mktemp)"
|
||||
err_body="$(mktemp)"
|
||||
|
||||
assets_status="$(curl "${curl_tls_args[@]}" -sS -o "$assets_file" -w '%{http_code}' \
|
||||
curl -fsS \
|
||||
-H "Authorization: token ${FORGEJO_TOKEN}" \
|
||||
"${release_api}/assets")"
|
||||
if [ "${assets_status}" != "200" ]; then
|
||||
echo "List assets failed HTTP ${assets_status}:"
|
||||
cat "$assets_file"
|
||||
rm -f "$assets_file" "$err_body"
|
||||
exit 1
|
||||
fi
|
||||
"${release_api}/assets" \
|
||||
> "$assets_file"
|
||||
|
||||
for file in Komikku-*.apk Komikku-mapping-*.zip; do
|
||||
[ -e "$file" ] || continue
|
||||
|
||||
if [ "${file#*-unsigned.apk}" != "${file}" ]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
asset_name="$(basename "$file")"
|
||||
asset_id="$("${JQ_BIN}" -r --arg n "${asset_name}" '(.[] | select(.name == $n) | .id) // empty' "$assets_file")"
|
||||
asset_id="$(
|
||||
python3 -c 'import json, sys; assets = json.load(open(sys.argv[1], encoding="utf-8")); name = sys.argv[2]; print(next((str(asset["id"]) for asset in assets if asset.get("name") == name), ""))' \
|
||||
"$assets_file" \
|
||||
"$asset_name"
|
||||
)"
|
||||
|
||||
if [ -n "$asset_id" ] && [ "$asset_id" != "null" ]; then
|
||||
del_status="$(curl "${curl_tls_args[@]}" -sS -o "$err_body" -w '%{http_code}' \
|
||||
if [ -n "$asset_id" ]; then
|
||||
curl -fsS \
|
||||
-X DELETE \
|
||||
-H "Authorization: token ${FORGEJO_TOKEN}" \
|
||||
"${release_api}/assets/${asset_id}")"
|
||||
if [ "${del_status}" != "204" ] && [ "${del_status}" != "200" ] && [ "${del_status}" != "404" ]; then
|
||||
echo "DELETE old asset ${asset_name} failed HTTP ${del_status}:"
|
||||
cat "$err_body"
|
||||
rm -f "$assets_file" "$err_body"
|
||||
exit 1
|
||||
fi
|
||||
"${release_api}/assets/${asset_id}" \
|
||||
> /dev/null
|
||||
fi
|
||||
|
||||
encoded_name="$("${JQ_BIN}" -nr --arg s "${asset_name}" '$s | @uri')"
|
||||
encoded_name="$(python3 -c 'import sys, urllib.parse; print(urllib.parse.quote(sys.argv[1]))' "$asset_name")"
|
||||
|
||||
up_status="$(curl "${curl_tls_args[@]}" -sS -o "$err_body" -w '%{http_code}' \
|
||||
curl -fsS \
|
||||
-X POST \
|
||||
-H "Authorization: token ${FORGEJO_TOKEN}" \
|
||||
-H 'Content-Type: application/octet-stream' \
|
||||
--data-binary @"$file" \
|
||||
"${release_api}/assets?name=${encoded_name}")"
|
||||
if [ "${up_status}" != "201" ] && [ "${up_status}" != "200" ]; then
|
||||
echo "Upload ${asset_name} failed HTTP ${up_status}:"
|
||||
cat "$err_body"
|
||||
rm -f "$assets_file" "$err_body"
|
||||
exit 1
|
||||
fi
|
||||
echo "Uploaded ${asset_name} (HTTP ${up_status})"
|
||||
"${release_api}/assets?name=${encoded_name}" \
|
||||
> /dev/null
|
||||
done
|
||||
rm -f "$assets_file" "$err_body"
|
||||
|
|
|
|||
|
|
@ -1,100 +0,0 @@
|
|||
name: Upstream sync status
|
||||
# Lightweight job: fetch upstream from GitHub, write status files, optional issue.
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: '0 6 * * 1'
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
issues: write
|
||||
|
||||
concurrency:
|
||||
group: upstream-status
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout and check upstream
|
||||
shell: bash
|
||||
env:
|
||||
FORGEJO_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
BEHIND_WARN: '10'
|
||||
run: |
|
||||
set -euo pipefail
|
||||
rm -rf .git
|
||||
git init .
|
||||
origin_url="${GITHUB_SERVER_URL#https://}"
|
||||
origin_url="${origin_url#http://}"
|
||||
git remote add origin "https://x-access-token:${FORGEJO_TOKEN}@${origin_url}/${GITHUB_REPOSITORY}.git"
|
||||
git -c http.sslVerify=false fetch --prune origin "${GITHUB_REF_NAME}"
|
||||
git checkout --force FETCH_HEAD
|
||||
git remote add upstream https://github.com/komikku-app/komikku.git
|
||||
chmod +x scripts/check-upstream.sh
|
||||
./scripts/check-upstream.sh --write
|
||||
behind="$(python3 -c 'import json; print(json.load(open("docs/upstream-status.json"))["behind"])')"
|
||||
ahead="$(python3 -c 'import json; print(json.load(open("docs/upstream-status.json"))["ahead"])')"
|
||||
status="$(python3 -c 'import json; print(json.load(open("docs/upstream-status.json"))["status"])')"
|
||||
echo "BEHIND=$behind" >> "$GITHUB_ENV"
|
||||
echo "AHEAD=$ahead" >> "$GITHUB_ENV"
|
||||
echo "STATUS=$status" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Commit status files
|
||||
shell: bash
|
||||
env:
|
||||
FORGEJO_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git config user.name "forgejo-actions[bot]"
|
||||
git config user.email "forgejo-actions[bot]@users.noreply.local"
|
||||
git add UPSTREAM_STATUS.md docs/upstream-status.json
|
||||
if git diff --staged --quiet; then
|
||||
echo "No upstream status changes."
|
||||
exit 0
|
||||
fi
|
||||
git commit -m "chore(ci): update upstream sync status"
|
||||
origin_url="${GITHUB_SERVER_URL#https://}"
|
||||
origin_url="${origin_url#http://}"
|
||||
git push "https://x-access-token:${FORGEJO_TOKEN}@${origin_url}/${GITHUB_REPOSITORY}.git" "HEAD:${GITHUB_REF_NAME}"
|
||||
|
||||
- name: Open issue when far behind
|
||||
if: env.STATUS == 'behind'
|
||||
shell: bash
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
title="Upstream sync: ${BEHIND} commits behind komikku-app/komikku"
|
||||
body=$(cat <<EOF
|
||||
The weekly upstream check found this fork is **${BEHIND} commits behind** upstream and **${AHEAD} commits ahead** with fork-specific changes.
|
||||
|
||||
See [UPSTREAM_STATUS.md](${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/src/branch/${GITHUB_REF_NAME}/UPSTREAM_STATUS.md) and [docs/upstream-sync.md](${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/src/branch/${GITHUB_REF_NAME}/docs/upstream-sync.md).
|
||||
|
||||
To merge upstream locally:
|
||||
|
||||
\`\`\`sh
|
||||
git fetch upstream
|
||||
git merge upstream/master
|
||||
\`\`\`
|
||||
EOF
|
||||
)
|
||||
api="${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/issues?state=open&labels=upstream-sync"
|
||||
existing="$(curl -fsSL -H "Authorization: token ${GH_TOKEN}" "$api" | python3 -c 'import json,sys; issues=json.load(sys.stdin); print(next((str(i["number"]) for i in issues if i.get("title")==sys.argv[1]), ""))' "$title")"
|
||||
if [[ -n "$existing" ]]; then
|
||||
curl -fsSL -X PATCH \
|
||||
-H "Authorization: token ${GH_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$(python3 -c 'import json,sys; print(json.dumps({"body": sys.argv[1]}))' "$body")" \
|
||||
"${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/issues/${existing}"
|
||||
echo "Updated issue #${existing}"
|
||||
exit 0
|
||||
fi
|
||||
curl -fsSL -X POST \
|
||||
-H "Authorization: token ${GH_TOKEN}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$(python3 -c 'import json,sys; print(json.dumps({"title": sys.argv[1], "body": sys.argv[2], "labels": ["upstream-sync"]}))' "$title" "$body")" \
|
||||
"${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/issues"
|
||||
echo "Created upstream sync issue"
|
||||
1
.github/.java-version
vendored
1
.github/.java-version
vendored
|
|
@ -1 +0,0 @@
|
|||
21
|
||||
12
.github/workflows/build_benchmark.yml
vendored
Normal file → Executable file
12
.github/workflows/build_benchmark.yml
vendored
Normal file → Executable file
|
|
@ -29,7 +29,7 @@ jobs:
|
|||
echo "VERSION_TAG=$version_tag" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Clone Repository (${{ steps.get_tag.outputs.VERSION_TAG }})
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@v4 # Forgejo-compatible (node20)
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
|
|
@ -57,18 +57,18 @@ jobs:
|
|||
needs: prepare-build
|
||||
steps:
|
||||
- name: Clone Repository (${{ needs.prepare-build.outputs.TAG_NAME }})
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@v4 # Forgejo-compatible (node20)
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
|
||||
uses: actions/setup-java@v4 # Forgejo-compatible (node20)
|
||||
with:
|
||||
java-version-file: .github/.java-version
|
||||
java-version: 17
|
||||
distribution: temurin
|
||||
|
||||
- name: Set up gradle
|
||||
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6
|
||||
uses: gradle/actions/setup-gradle@v4.4.2 # Forgejo-compatible (node20)
|
||||
|
||||
- name: Check code format
|
||||
run: ./gradlew spotlessCheck
|
||||
|
|
@ -101,7 +101,7 @@ jobs:
|
|||
mv app/build/outputs/apk/${{ needs.prepare-build.outputs.ARTIFACTS_PREFIX }}-x86_64-benchmark-signed.apk Komikku-x86_64-${{ needs.prepare-build.outputs.TAG_NAME }}.apk
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: actions/upload-artifact@v4 # Forgejo-compatible (node20)
|
||||
with:
|
||||
path: "**/*.apk"
|
||||
retention-days: 30
|
||||
|
|
|
|||
4
.github/workflows/build_dispatch_preview.yml
vendored
Normal file → Executable file
4
.github/workflows/build_dispatch_preview.yml
vendored
Normal file → Executable file
|
|
@ -30,7 +30,7 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Clone repo
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@v4 # Forgejo-compatible (node20)
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
|
|
@ -53,7 +53,7 @@ jobs:
|
|||
uses: tj-actions/branch-names@5250492686b253f06fa55861556d1027b067aeb5 # v9.0.2
|
||||
|
||||
- name: Invoke workflow in preview repo
|
||||
uses: benc-uk/workflow-dispatch@31e2b3319479a63f0ab15bf800eff9e913504e26 # v1.3.2
|
||||
uses: benc-uk/workflow-dispatch@v1.2.2 # Forgejo-compatible (node16)
|
||||
with:
|
||||
workflow: build_app.yml
|
||||
repo: komikku-app/komikku-preview
|
||||
|
|
|
|||
16
.github/workflows/build_preview.yml
vendored
Normal file → Executable file
16
.github/workflows/build_preview.yml
vendored
Normal file → Executable file
|
|
@ -43,7 +43,7 @@ jobs:
|
|||
echo "VERSION_TAG=$version_tag" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Clone Repository (${{ steps.get_tag.outputs.VERSION_TAG }})
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@v4 # Forgejo-compatible (node20)
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
|
|
@ -119,14 +119,14 @@ jobs:
|
|||
if: github.ref == 'refs/heads/master'
|
||||
steps:
|
||||
- name: Clone Repository (${{ needs.prepare-build.outputs.VERSION_TAG }} - ${{ needs.prepare-build.outputs.TAG_NAME }})
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@v4 # Forgejo-compatible (node20)
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
|
||||
uses: actions/setup-java@v4 # Forgejo-compatible (node20)
|
||||
with:
|
||||
java-version-file: .github/.java-version
|
||||
java-version: 17
|
||||
distribution: temurin
|
||||
|
||||
- name: Write google-services.json
|
||||
|
|
@ -144,7 +144,7 @@ jobs:
|
|||
write-mode: overwrite
|
||||
|
||||
- name: Set up gradle
|
||||
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6
|
||||
uses: gradle/actions/setup-gradle@v4.4.2 # Forgejo-compatible (node20)
|
||||
|
||||
- name: Check code format
|
||||
run: ./gradlew spotlessCheck
|
||||
|
|
@ -156,7 +156,7 @@ jobs:
|
|||
run: ./gradlew testReleaseUnitTest
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: actions/upload-artifact@v4 # Forgejo-compatible (node20)
|
||||
with:
|
||||
path: "**/*.apk"
|
||||
retention-days: 1
|
||||
|
|
@ -168,7 +168,7 @@ jobs:
|
|||
- build-app
|
||||
steps:
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
uses: actions/download-artifact@v4 # Forgejo-compatible (node20)
|
||||
with:
|
||||
merge-multiple: true
|
||||
|
||||
|
|
@ -195,7 +195,7 @@ jobs:
|
|||
mv app/build/outputs/apk/${{ needs.prepare-build.outputs.ARTIFACTS_PREFIX }}-x86_64-preview-signed.apk Komikku-x86_64-${{ needs.prepare-build.outputs.TAG_NAME }}.apk
|
||||
|
||||
- name: Create release
|
||||
uses: softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b # v3.0.1
|
||||
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
|
||||
with:
|
||||
tag_name: ${{ needs.prepare-build.outputs.TAG_NAME }}
|
||||
name: Komikku Preview ${{ needs.prepare-build.outputs.TAG_NAME }} (${{ needs.prepare-build.outputs.VERSION_TAG }})
|
||||
|
|
|
|||
18
.github/workflows/build_pull_request.yml
vendored
Normal file → Executable file
18
.github/workflows/build_pull_request.yml
vendored
Normal file → Executable file
|
|
@ -30,20 +30,20 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Clone repo
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@v4 # Forgejo-compatible (node20)
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Dependency Review
|
||||
uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0
|
||||
uses: actions/dependency-review-action@05fe4576374b728f0c523d6a13d64c25081e0803 # v4.8.3
|
||||
|
||||
- name: Validate Gradle Wrapper
|
||||
uses: gradle/actions/wrapper-validation@3f131e8634966bd73d06cc69884922b02e6faf92 # v6.2.0
|
||||
uses: gradle/actions/wrapper-validation@v4.4.2 # Forgejo-compatible (node20)
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
|
||||
uses: actions/setup-java@v4 # Forgejo-compatible (node20)
|
||||
with:
|
||||
java-version-file: .github/.java-version
|
||||
java-version: 17
|
||||
distribution: temurin
|
||||
|
||||
- name: Write google-services.json
|
||||
|
|
@ -63,7 +63,7 @@ jobs:
|
|||
write-mode: overwrite
|
||||
|
||||
- name: Set up gradle
|
||||
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6
|
||||
uses: gradle/actions/setup-gradle@v4.4.2 # Forgejo-compatible (node20)
|
||||
|
||||
- name: Check code format
|
||||
run: ./gradlew spotlessCheck
|
||||
|
|
@ -82,7 +82,7 @@ jobs:
|
|||
|
||||
- name: Upload test report
|
||||
if: steps.unit_tests.outcome == 'failure'
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: actions/upload-artifact@v4 # Forgejo-compatible (node20)
|
||||
with:
|
||||
name: test-report-${{ github.sha }}
|
||||
path: app/build/reports/tests/testReleaseUnitTest
|
||||
|
|
@ -119,13 +119,13 @@ jobs:
|
|||
mv app/build/outputs/apk/preview/app-universal-preview${{ steps.signed_filename.outputs.SIGNED_TRAIL }}.apk Komikku-$version_tag-r$commit_count.apk
|
||||
|
||||
- name: Upload APK
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: actions/upload-artifact@v4 # Forgejo-compatible (node20)
|
||||
with:
|
||||
name: Komikku-${{ steps.current_commit.outputs.VERSION_TAG }}-r${{ steps.current_commit.outputs.COMMIT_COUNT }}.apk
|
||||
path: ./*.apk
|
||||
|
||||
- name: Upload mapping
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: actions/upload-artifact@v4 # Forgejo-compatible (node20)
|
||||
with:
|
||||
name: mapping-${{ github.sha }}
|
||||
path: app/build/outputs/mapping/preview
|
||||
|
|
|
|||
14
.github/workflows/build_push.yml
vendored
Normal file → Executable file
14
.github/workflows/build_push.yml
vendored
Normal file → Executable file
|
|
@ -27,14 +27,14 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Clone repo
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@v4 # Forgejo-compatible (node20)
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
|
||||
uses: actions/setup-java@v4 # Forgejo-compatible (node20)
|
||||
with:
|
||||
java-version-file: .github/.java-version
|
||||
java-version: 17
|
||||
distribution: temurin
|
||||
|
||||
- name: Write google-services.json
|
||||
|
|
@ -52,7 +52,7 @@ jobs:
|
|||
write-mode: overwrite
|
||||
|
||||
- name: Set up gradle
|
||||
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6
|
||||
uses: gradle/actions/setup-gradle@v4.4.2 # Forgejo-compatible (node20)
|
||||
|
||||
- name: Check code format
|
||||
run: ./gradlew spotlessCheck
|
||||
|
|
@ -66,7 +66,7 @@ jobs:
|
|||
|
||||
- name: Upload test report
|
||||
if: steps.unit_tests.outcome == 'failure'
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: actions/upload-artifact@v4 # Forgejo-compatible (node20)
|
||||
with:
|
||||
name: test-report-${{ github.sha }}
|
||||
path: app/build/reports/tests/testReleaseUnitTest
|
||||
|
|
@ -95,13 +95,13 @@ jobs:
|
|||
mv app/build/outputs/apk/preview/app-universal-preview-signed.apk Komikku-$version_tag-r$commit_count.apk
|
||||
|
||||
- name: Upload APK
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: actions/upload-artifact@v4 # Forgejo-compatible (node20)
|
||||
with:
|
||||
name: Komikku-${{ steps.current_commit.outputs.VERSION_TAG }}-r${{ steps.current_commit.outputs.COMMIT_COUNT }}.apk
|
||||
path: ./*.apk
|
||||
|
||||
- name: Upload mapping
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: actions/upload-artifact@v4 # Forgejo-compatible (node20)
|
||||
with:
|
||||
name: mapping-${{ github.sha }}
|
||||
path: app/build/outputs/mapping/preview
|
||||
|
|
|
|||
18
.github/workflows/build_release.yml
vendored
Normal file → Executable file
18
.github/workflows/build_release.yml
vendored
Normal file → Executable file
|
|
@ -31,7 +31,7 @@ jobs:
|
|||
echo "VERSION_TAG=$version_tag" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Clone Repository (${{ steps.get_tag.outputs.VERSION_TAG }})
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@v4 # Forgejo-compatible (node20)
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
|
|
@ -87,14 +87,14 @@ jobs:
|
|||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
steps:
|
||||
- name: Clone Repository (${{ needs.prepare-build.outputs.VERSION_TAG }})
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
uses: actions/checkout@v4 # Forgejo-compatible (node20)
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
|
||||
uses: actions/setup-java@v4 # Forgejo-compatible (node20)
|
||||
with:
|
||||
java-version-file: .github/.java-version
|
||||
java-version: 17
|
||||
distribution: temurin
|
||||
|
||||
- name: Write google-services.json
|
||||
|
|
@ -112,7 +112,7 @@ jobs:
|
|||
write-mode: overwrite
|
||||
|
||||
- name: Set up gradle
|
||||
uses: gradle/actions/setup-gradle@3f131e8634966bd73d06cc69884922b02e6faf92 # v6
|
||||
uses: gradle/actions/setup-gradle@v4.4.2 # Forgejo-compatible (node20)
|
||||
|
||||
- name: Check code format
|
||||
run: ./gradlew spotlessCheck
|
||||
|
|
@ -124,13 +124,13 @@ jobs:
|
|||
run: ./gradlew testReleaseUnitTest
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: actions/upload-artifact@v4 # Forgejo-compatible (node20)
|
||||
with:
|
||||
path: "**/*.apk"
|
||||
retention-days: 1
|
||||
|
||||
- name: Upload mapping
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
uses: actions/upload-artifact@v4 # Forgejo-compatible (node20)
|
||||
with:
|
||||
name: mapping-${{ github.sha }}
|
||||
path: app/build/outputs/mapping/release
|
||||
|
|
@ -142,7 +142,7 @@ jobs:
|
|||
- build-app
|
||||
steps:
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
uses: actions/download-artifact@v4 # Forgejo-compatible (node20)
|
||||
with:
|
||||
merge-multiple: true
|
||||
|
||||
|
|
@ -169,7 +169,7 @@ jobs:
|
|||
mv app/build/outputs/apk/release/app-x86_64-release-unsigned-signed.apk Komikku-x86_64-${{ needs.prepare-build.outputs.VERSION_TAG }}.apk
|
||||
|
||||
- name: Create release
|
||||
uses: softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b # v3.0.1
|
||||
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
|
||||
with:
|
||||
tag_name: ${{ needs.prepare-build.outputs.VERSION_TAG }}
|
||||
name: Komikku ${{ needs.prepare-build.outputs.VERSION_TAG }}
|
||||
|
|
|
|||
7
.github/workflows/codeberg_mirror.yml
vendored
Normal file → Executable file
7
.github/workflows/codeberg_mirror.yml
vendored
Normal file → Executable file
|
|
@ -13,14 +13,9 @@ jobs:
|
|||
if: github.repository == 'komikku-app/komikku'
|
||||
runs-on: 'ubuntu-24.04'
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
- uses: actions/checkout@v4 # Forgejo-compatible (node20)
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: Setup SSH key
|
||||
run: |
|
||||
mkdir -p ~/.ssh
|
||||
echo "${{ secrets.CODEBERG_SSH }}" >> ~/.ssh/id_rsa
|
||||
chmod 600 ~/.ssh/id_rsa
|
||||
- uses: pixta-dev/repository-mirroring-action@674e65a7d483ca28dafaacba0d07351bdcc8bd75 # v1.1.1
|
||||
with:
|
||||
target_repo_url: "git@codeberg.org:cuong-tran/komikku.git"
|
||||
|
|
|
|||
2
.github/workflows/delete_merged_branch.yml
vendored
Normal file → Executable file
2
.github/workflows/delete_merged_branch.yml
vendored
Normal file → Executable file
|
|
@ -13,7 +13,7 @@ jobs:
|
|||
if: github.event.pull_request.merged == true && github.repository == github.event.pull_request.head.repo.full_name
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
||||
uses: actions/checkout@v4 # Forgejo-compatible (node20)
|
||||
|
||||
- name: Delete branch
|
||||
run: |
|
||||
|
|
|
|||
2
.github/workflows/pr_label.yml
vendored
Normal file → Executable file
2
.github/workflows/pr_label.yml
vendored
Normal file → Executable file
|
|
@ -15,7 +15,7 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Check PR and Add Label
|
||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
|
||||
uses: actions/github-script@v7 # Forgejo-compatible (node20)
|
||||
with:
|
||||
script: |
|
||||
const prAuthor = context.payload.pull_request.user.login;
|
||||
|
|
|
|||
2
.github/workflows/update_website.yml
vendored
Normal file → Executable file
2
.github/workflows/update_website.yml
vendored
Normal file → Executable file
|
|
@ -13,7 +13,7 @@ jobs:
|
|||
|
||||
steps:
|
||||
- name: Update website on release
|
||||
uses: benc-uk/workflow-dispatch@31e2b3319479a63f0ab15bf800eff9e913504e26 # v1.3.2
|
||||
uses: benc-uk/workflow-dispatch@v1.2.2 # Forgejo-compatible (node16)
|
||||
with:
|
||||
workflow: Deploy
|
||||
repo: komikku-app/komikku-app.github.io
|
||||
|
|
|
|||
218
AGENTS.md
218
AGENTS.md
|
|
@ -1,218 +0,0 @@
|
|||
# Komikku – AI Agent Guide
|
||||
|
||||
Komikku is an Android manga reader (min SDK 26, target SDK 36, JVM 17 / Kotlin) forked from **Mihon** + **TachiyomiSY**. Stack: Jetpack Compose + Material3, Voyager navigation, SQLDelight, Injekt DI. `applicationId`: `app.komikku`.
|
||||
|
||||
---
|
||||
|
||||
## Mandatory rules for AI agents
|
||||
|
||||
**Read this section before every change.** These rules override shortcuts (e.g. copying nearby `MR` imports or only running `compileDebugKotlin`).
|
||||
|
||||
### Git
|
||||
|
||||
| Rule | Required behavior |
|
||||
|------|-------------------|
|
||||
| Branch | Create a **feature branch** for the task (`git checkout -b <type>/<short-description>`). |
|
||||
| Commit | **OK** on a feature branch when work is ready. **Never** commit directly to `master` / `main` unless the user explicitly asks. |
|
||||
| Push | **OK** to push the **current feature branch** when work is ready. **Never** push to `master` / `main` unless the user explicitly asks. |
|
||||
|
||||
Before `git push`, confirm the current branch is not `master` or `main` (`git branch --show-current`).
|
||||
|
||||
### Internationalization (strings)
|
||||
|
||||
| String kind | Module | Resource class | Base folder only |
|
||||
|-------------|--------|----------------|------------------|
|
||||
| Komikku-only (new features, KMK UI, library-update errors, WebDAV, Discord, etc.) | `i18n-kmk/` | **`KMR`** | `i18n-kmk/src/commonMain/moko-resources/base/` |
|
||||
| Shared Mihon / upstream behavior | `i18n/` | **`MR`** | `i18n/src/commonMain/moko-resources/base/` |
|
||||
| TachiyomiSY-only | `i18n-sy/` | **`SYMR`** | `i18n-sy/src/commonMain/moko-resources/base/` |
|
||||
|
||||
**Hard rules:**
|
||||
|
||||
- **Never** add Komikku-specific strings to `i18n/` or `i18n-sy/`.
|
||||
- **Never** edit non-`base` locale `strings.xml` or `plurals.xml` files in `i18n-kmk/`, `i18n/`, or `i18n-sy/` (Weblate owns translations).
|
||||
- Import: `import tachiyomi.i18n.kmk.KMR` for Komikku strings.
|
||||
- If a change is inside `// KMK -->` … `// KMK <--` or adds Komikku-only behavior, default to **`KMR` + `i18n-kmk`**.
|
||||
|
||||
**Self-check before finishing:** `git diff` must not add new `<string name="…">` or `<plurals name="…">` entries under non-`base` locales in `i18n-kmk/src/`, `i18n/src/`, or `i18n-sy/src/`.
|
||||
|
||||
### Formatting & build verification
|
||||
|
||||
**“Build passes” is not enough.** After Kotlin/XML edits, run **in this order** before marking work complete:
|
||||
|
||||
```bash
|
||||
./gradlew spotlessApply # fix formatting
|
||||
./gradlew spotlessCheck # must pass (same as CI)
|
||||
./gradlew assembleDebug # or :app:compileDebugKotlin for a faster compile-only check
|
||||
```
|
||||
|
||||
- **Do not** skip `spotlessCheck` when verifying changes.
|
||||
- If `spotlessCheck` fails, run `spotlessApply` and re-run `spotlessCheck`.
|
||||
- On Cloud VM, export `ANDROID_HOME` and `JAVA_HOME` first (see [Cursor Cloud](#cursor-cloud-specific-instructions)).
|
||||
|
||||
---
|
||||
|
||||
## Module layout
|
||||
|
||||
| Module | Purpose |
|
||||
|--------|---------|
|
||||
| `app/` | UI (`eu.kanade.*`, `exh/`, `mihon/`), DI, workers, build variants |
|
||||
| `domain/` | Use cases in `…/interactor/` (e.g. `GetManga`), models, repo interfaces |
|
||||
| `data/` | SQLDelight DB, `*RepositoryImpl` (`tachiyomi.data.*`) |
|
||||
| `core:common/` | Network (OkHttp), security, storage, shared utils |
|
||||
| `core:archive/` | CBZ/archive reading with optional encryption |
|
||||
| `core-metadata/` | Comic-info metadata parsing |
|
||||
| `source-api/` / `source-local/` | Extension `Source` API + local source |
|
||||
| `presentation-core/` | Shared Compose components |
|
||||
| `presentation-widget/` | Home-screen Glance widget |
|
||||
| `i18n/` | Mihon strings → `MR` (moko-resources) |
|
||||
| `i18n-kmk/` | Komikku strings → `KMR` |
|
||||
| `i18n-sy/` | TachiyomiSY strings → `SYMR` |
|
||||
| `flagkit/` | Country-flag drawables |
|
||||
| `telemetry/` | Firebase/Crashlytics (noop unless `-Pinclude-telemetry`) |
|
||||
| `macrobenchmark/` | Macrobenchmark tests |
|
||||
|
||||
Dependency flow: `app` → `domain` → `source-api`; `data` implements `domain` repos.
|
||||
|
||||
Version catalogs: `gradle/libs.versions.toml`, `kotlinx.versions.toml`, `androidx.versions.toml`, `compose.versions.toml`, `sy.versions.toml`.
|
||||
|
||||
---
|
||||
|
||||
## Architecture
|
||||
|
||||
**DI** – `uy.kohesive.injekt` (not Hilt). Register in `AppModule.kt`, `DomainModule.kt`, `KMKDomainModule.kt`, `SYDomainModule.kt` via `addSingleton` / `addSingletonFactory`. Resolve with `Injekt.get<T>()` or `injectLazy<T>()`.
|
||||
|
||||
**UI & navigation** – [Voyager](https://voyager.adriel.cafe/): `Screen` in `eu.kanade.tachiyomi.ui.*`, composables in `eu.kanade.presentation.*`. Base type: `eu.kanade.presentation.util.Screen`. State via `rememberScreenModel { … }`; most models extend `StateScreenModel<State>` or bases like `SearchScreenModel`; some use plain `ScreenModel`. Prefer `screenModelScope` and `ioCoroutineScope`; use `launchIO` / `withIOContext` from `tachiyomi.core.common.util.lang`. `rememberCoroutineScope()` is fine in Compose; long-lived services may use their own `CoroutineScope`.
|
||||
|
||||
**Activities (not Voyager)** – `MainActivity` (shell), `ReaderActivity` + `ReaderViewModel`, `WebViewActivity`, `UnlockActivity`, OAuth login activities, `DeepLinkActivity`. Reader: `ReaderActivity.newIntent(context, mangaId, chapterId)`. Web: both `WebViewScreen` (Voyager) and `WebViewActivity.newIntent(...)`.
|
||||
|
||||
Example: `DeepLinkScreen` + `DeepLinkScreenModel` in `app/src/main/java/eu/kanade/tachiyomi/ui/deeplink/`.
|
||||
|
||||
**Domain / data** – One class per operation under `domain/…/interactor/` (verb names, not `*Interactor` suffix). Also `app/src/main/java/eu/kanade/domain/…/interactor/` for app-specific cases. Wire repos in `eu.kanade.domain.DomainModule.kt` (+ `KMKDomainModule`, `SYDomainModule`).
|
||||
|
||||
**Database** – SQLDelight in `data/src/main/sqldelight/tachiyomi/` (`.sq` queries, `migrations/*.sqm`). After schema changes add a new `.sqm` and often `// KMK` blocks in `.sq` / mappers. Regenerate: `./gradlew :data:generateSqlDelightInterface` (or any compile that touches `:data`).
|
||||
|
||||
**App preference migrations** – `app/src/main/java/mihon/core/migration/migrations/` (`mihon.core.migration.Migration`).
|
||||
|
||||
**Images** – Coil 3 (`coil3.*`, `context.imageLoader`). No Glide/Picasso.
|
||||
|
||||
---
|
||||
|
||||
## Komikku-specific work
|
||||
|
||||
- **Strings:** see [Mandatory rules – Internationalization](#mandatory-rules-for-ai-agents). Summary: Komikku → **`KMR`** / `i18n-kmk/…/base/` only.
|
||||
- Do not edit locale `strings.xml` in `i18n/` or `i18n-sy/` except when syncing upstream; translations via [Weblate](https://hosted.weblate.org/engage/komikku-app/).
|
||||
- Komikku code/DI: search `// KMK` (e.g. `KMKDomainModule`, `HideCategory`, library-update errors).
|
||||
- Prefs: `eu.kanade.domain.*.service.*Preferences` (e.g. `SourcePreferences.relatedMangas()`).
|
||||
|
||||
**Examples (Komikku → `i18n-kmk`, not `i18n`):** library update error UI, sync-before-update messages, WebDAV/Discord settings, updater notifications, `mihon/feature/*` Komikku screens.
|
||||
|
||||
---
|
||||
|
||||
## Extensions & sources
|
||||
|
||||
- Catalog sources: installable APK extensions (not in this repo).
|
||||
- In-repo: delegated sources and metadata in `exh/` (E-Hentai, NHentai, MangaDex, `exh/recs/`).
|
||||
- `source-api`: `eu.kanade.tachiyomi.source.*` — avoid breaking extension ABI.
|
||||
|
||||
---
|
||||
|
||||
## Build & CI
|
||||
|
||||
Build types: `debug` (`.dev`), `release`, `releaseTest` (`.rt`), `foss` (`.foss`), `preview` (`.beta`, CI default), `benchmark`.
|
||||
|
||||
Gradle `-P` flags (`buildSrc/.../BuildConfig.kt`):
|
||||
|
||||
| Flag | Effect |
|
||||
|------|--------|
|
||||
| `include-telemetry` | Firebase Analytics + Crashlytics |
|
||||
| `enable-updater` | In-app update checker |
|
||||
| `disable-code-shrink` | Skip R8 minification |
|
||||
| `include-dependency-info` | Dependency metadata in APK |
|
||||
|
||||
```bash
|
||||
./gradlew spotlessApply # format (run before spotlessCheck)
|
||||
./gradlew spotlessCheck # REQUIRED before considering work done (CI gate)
|
||||
./gradlew assemblePreview # main CI/dev APK
|
||||
./gradlew assemblePreview -Pinclude-telemetry -Penable-updater # full upstream CI build
|
||||
./gradlew testReleaseUnitTest # CI unit tests (or ./gradlew test for all modules)
|
||||
./gradlew installDebug # device install
|
||||
./gradlew :data:generateSqlDelightInterface # after .sq / .sqm changes
|
||||
```
|
||||
|
||||
**Agent verification checklist (minimum):** `spotlessApply` → `spotlessCheck` → `assembleDebug` (or `compileDebugKotlin` only if the user asked for a quick compile check—but still run Spotless).
|
||||
|
||||
JDK **17**.
|
||||
|
||||
---
|
||||
|
||||
## Fork-origin markers
|
||||
|
||||
Preserve inline blocks when editing:
|
||||
|
||||
```kotlin
|
||||
// KMK --> … // KMK <-- Komikku
|
||||
// SY --> … // SY <-- TachiyomiSY
|
||||
// EXH --> … // EXH <-- E-Hentai / exh (existing); prefer KMK for new Komikku-only code
|
||||
```
|
||||
|
||||
Package roots: `eu.kanade.tachiyomi.*` (legacy UI), `tachiyomi.*` (domain/data), `mihon.*` (Mihon upstream), `exh.*` (enhanced sources).
|
||||
|
||||
---
|
||||
|
||||
## Tests
|
||||
|
||||
- Unit tests: `domain/src/test/`; app: `app/src/test/.../MigratorTest.kt`. No broad UI test suite.
|
||||
|
||||
---
|
||||
|
||||
## Conventions
|
||||
|
||||
- **Logging** – Prefer `xLogE()` / `xLog()` helpers from `exh.log` for Komikku code, Mihon uses `logcat { }` from `tachiyomi.core.common.util.system`. Avoid raw `android.util.Log`.
|
||||
- **Formatting** – Spotless + ktlint (`buildSrc/.../mihon.code.lint.gradle.kts`). Agents **must** run `spotlessApply` and `spotlessCheck` (see [Mandatory rules](#mandatory-rules-for-ai-agents)).
|
||||
- **Fork edits** – New Komikku features inside `// KMK` islands; keep `// SY` / `// EXH` blocks intact when merging upstream.
|
||||
|
||||
---
|
||||
|
||||
## Key files
|
||||
|
||||
- `App.kt` – Injekt bootstrap, logging setup
|
||||
- `MainActivity.kt` – Voyager host
|
||||
- `app/src/main/java/eu/kanade/tachiyomi/di/AppModule.kt` – core DI
|
||||
- `app/src/main/java/eu/kanade/domain/DomainModule.kt` – domain interactors
|
||||
- `buildSrc/.../BuildConfig.kt`, `AndroidConfig.kt` – flags, SDK versions
|
||||
- `app/build.gradle.kts`, `settings.gradle.kts`
|
||||
|
||||
---
|
||||
|
||||
## Cursor Cloud specific instructions
|
||||
|
||||
### Environment
|
||||
|
||||
The VM update script installs the Android SDK (platform 36, build-tools 35.0.1, platform-tools, cmdline-tools) into `/opt/android-sdk` and writes `local.properties` with `sdk.dir`. JDK 21 is pre-installed and works fine for compiling to JVM target 17. `ANDROID_HOME`, `JAVA_HOME`, and `PATH` are set in `~/.bashrc`.
|
||||
|
||||
### Running key commands
|
||||
|
||||
All Gradle commands require the environment variables set above. Export them before invoking `./gradlew` if running in a fresh shell:
|
||||
|
||||
```bash
|
||||
export ANDROID_HOME=/opt/android-sdk
|
||||
export JAVA_HOME=/usr/lib/jvm/java-21-openjdk-amd64
|
||||
```
|
||||
|
||||
| Task | Command |
|
||||
|------|---------|
|
||||
| **Required format fix** | `./gradlew spotlessApply` (run first after code edits) |
|
||||
| **Required format gate** | `./gradlew spotlessCheck` (must pass before task is done) |
|
||||
| Debug APK build | `./gradlew assembleDebug` |
|
||||
| Preview APK build (CI) | `./gradlew assemblePreview` |
|
||||
| Unit tests (CI) | `./gradlew testReleaseUnitTest` |
|
||||
| All module tests | `./gradlew test` |
|
||||
| SQLDelight codegen | `./gradlew :data:generateSqlDelightInterface` |
|
||||
|
||||
### Gotchas
|
||||
|
||||
- First Gradle build downloads ~1 GB of dependencies; subsequent builds use the Gradle cache and are much faster.
|
||||
- `local.properties` is `.gitignore`d — it must be recreated if missing (the update script handles this).
|
||||
- No Android emulator or device is available on the Cloud VM, so `installDebug` will fail. Build verification is done via `assembleDebug`.
|
||||
- `google-services.json` and `client_secrets.json` are not present (CI secrets); builds without `-Pinclude-telemetry` succeed without them.
|
||||
- Gradle daemon may use significant memory (`-Xmx4g` in `gradle.properties`). If OOM occurs, kill and restart the daemon with `./gradlew --stop`.
|
||||
55
CHANGELOG.md
55
CHANGELOG.md
|
|
@ -12,54 +12,9 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co
|
|||
|
||||
## [Unreleased]
|
||||
### Added
|
||||
- Add support for `tachiyomix` extension index format ([@AntsyLich](https://github.com/AntsyLich)) ([#3349](https://github.com/mihonapp/mihon/pull/3349))
|
||||
|
||||
### Changed
|
||||
- Change all reference of extension repo to extension store ([@AntsyLich](https://github.com/AntsyLich)) ([#3349](https://github.com/mihonapp/mihon/pull/3349))
|
||||
- Change the term "Obsolete" to "Orphaned" for extensions ([@AntsyLich](https://github.com/AntsyLich)) ([#3383](https://github.com/mihonapp/mihon/pull/3383))
|
||||
|
||||
### Fixed
|
||||
- Add missing `outlineVariant` color to Nord theme ([@CompileConnected](https://github.com/CompileConnected)) ([#3184](https://github.com/mihonapp/mihon/pull/3184))
|
||||
|
||||
## [v0.19.9] - 2026-04-11
|
||||
### Fixed
|
||||
- Regression with installing/updating extension ([@AntsyLich](https://github.com/AntsyLich))
|
||||
|
||||
## [v0.19.8] - 2026-04-11
|
||||
### Added
|
||||
- Add dedicated "Support Us" screen ([@AntsyLich](https://github.com/AntsyLich)) ([#3200](https://github.com/mihonapp/mihon/pull/3200))
|
||||
- Add a one time popup asking for donation from long time users ([@AntsyLich](https://github.com/AntsyLich)) ([#3203](https://github.com/mihonapp/mihon/pull/3203))
|
||||
|
||||
### Improved
|
||||
- Show informative error when trying to add unapproved titles to list on MAL ([@MajorTanya](https://github.com/MajorTanya)) ([#3155](https://github.com/mihonapp/mihon/pull/3155))
|
||||
|
||||
## [v0.19.7] - 2026-03-23
|
||||
Same as v0.19.6
|
||||
|
||||
## [v0.19.6] - 2026-03-23
|
||||
### Fixed
|
||||
- Fix app crashing when trying to add extension repo with existing signature ([@AntsyLich](https://github.com/AntsyLich)) ([`cb0e329`](https://github.com/mihonapp/mihon/commit/cb0e329))
|
||||
- Potentially fix 'database is locked' crash ([@AntsyLich](https://github.com/AntsyLich)) ([`f8e82b9`](https://github.com/mihonapp/mihon/commit/f8e82b9))
|
||||
- Fix occasional crash when mass installing/uninstalling extension using `PackageManager` ([@AntsyLich](https://github.com/AntsyLich)) ([`42daa3f`](https://github.com/mihonapp/mihon/commit/42daa3f))
|
||||
- Fix app crash on startup on some Android TV ([@AntsyLich](https://github.com/AntsyLich)) ([`b40f1d3`](https://github.com/mihonapp/mihon/commit/b40f1d3))
|
||||
|
||||
## [v0.19.5] - 2026-03-20
|
||||
### Changed
|
||||
- Retry in reader now redownloads image ([@AntsyLich](https://github.com/AntsyLich)) ([#3089](https://github.com/mihonapp/mihon/pull/3089))
|
||||
|
||||
### Fixed
|
||||
- Fix performance regression introduced in v0.19.4 ([@AntsyLich](https://github.com/AntsyLich)) ([#3082](https://github.com/mihonapp/mihon/pull/3082))
|
||||
- Fix duplicate key crash in duplicate detection ([@leodyversemilla07](https://github.com/leodyversemilla07)) ([#3040](https://github.com/mihonapp/mihon/pull/3040))
|
||||
- Fix MangaUpdates HTTP 4XX errors ([@leodyversemilla07](https://github.com/leodyversemilla07)) ([#3021](https://github.com/mihonapp/mihon/pull/3021))
|
||||
- Fix WebView JavaScript dialogs popup after screen is closed ([@leodyversemilla07](https://github.com/leodyversemilla07)) ([#3041](https://github.com/mihonapp/mihon/pull/3041))
|
||||
- Fix extension actions disappearing after installing and uninstalling in same session ([@leodyversemilla07](https://github.com/leodyversemilla07)) ([#3049](https://github.com/mihonapp/mihon/pull/3049))
|
||||
|
||||
## [v0.19.4] - 2026-02-25
|
||||
### Added
|
||||
- Automatically remove downloads on Suwayomi after reading, configurable via extension settings ([@cpiber](https://github.com/cpiber)) ([#2673](https://github.com/mihonapp/mihon/pull/2673))
|
||||
- Display author & artist name in MAL search results ([@MajorTanya](https://github.com/MajorTanya)) ([#2833](https://github.com/mihonapp/mihon/pull/2833))
|
||||
- Add filter options to Updates tab ([@MajorTanya](https://github.com/MajorTanya)) ([#2851](https://github.com/mihonapp/mihon/pull/2851))
|
||||
- Add bookmarked chapters to chapter download options ([@NarwhalHorns](https://github.com/NarwhalHorns)) ([#2891](https://github.com/mihonapp/mihon/pull/2891))
|
||||
- Add `src:` prefix to search the library by source ID ([@MajorTanya](https://github.com/MajorTanya)) ([#2927](https://github.com/mihonapp/mihon/pull/2927))
|
||||
- Add `src:local` as a way to search for Local Source entries ([@MajorTanya](https://github.com/MajorTanya)) ([#2928](https://github.com/mihonapp/mihon/pull/2928))
|
||||
|
||||
|
|
@ -67,16 +22,10 @@ Same as v0.19.6
|
|||
- Minimize memory usage by reducing in-memory cover cache size ([@Lolle2000la](https://github.com/Lolle2000la)) ([#2266](https://github.com/mihonapp/mihon/pull/2266))
|
||||
- Optimize MAL search queries ([@MajorTanya](https://github.com/MajorTanya)) ([#2832](https://github.com/mihonapp/mihon/pull/2832))
|
||||
- Reword download reindexing message to avoid confusion ([@MajorTanya](https://github.com/MajorTanya)) ([#2874](https://github.com/mihonapp/mihon/pull/2874))
|
||||
- Rework internals for better performance ([@Lolle2000la](https://github.com/Lolle2000la)) ([#2955](https://github.com/mihonapp/mihon/pull/2955))
|
||||
- Optimize tracked library filter ([@NarwhalHorns](https://github.com/NarwhalHorns)) ([#2977](https://github.com/mihonapp/mihon/pull/2977))
|
||||
- Utilize tracker for library duplicate detection ([@NarwhalHorns](https://github.com/NarwhalHorns)) ([#2977](https://github.com/mihonapp/mihon/pull/2978))
|
||||
|
||||
### Changed
|
||||
- Update tracker icons ([@AntsyLich](https://github.com/AntsyLich)) ([#2773](https://github.com/mihonapp/mihon/pull/2773))
|
||||
- Add a small increment to chapter number before comparison to fix progress sync issues for Suwayomi ([@cpiber](https://github.com/cpiber)) ([#2657](https://github.com/mihonapp/mihon/pull/2675))
|
||||
- Add all pages of adjacent chapters in the UI instead of only the first or last three ([@AntsyLich](https://github.com/AntsyLich)) ([#2995](https://github.com/mihonapp/mihon/pull/2995))
|
||||
- Going back now first clears search query on browse extension tab ([@cuong-tran](https://github.com/cuong-tran)) ([#2906](https://github.com/mihonapp/mihon/pull/2906))
|
||||
- Automatic library updates now run even when connected to a VPN ([@AntsyLich](https://github.com/AntsyLich)) ([#2773](https://github.com/mihonapp/mihon/pull/2773))
|
||||
|
||||
### Fixed
|
||||
- Fix reader tap zones triggering after scrolling is stopped by tapping ([@NGB-Was-Taken](https://github.com/NGB-Was-Taken)) ([#2680](https://github.com/mihonapp/mihon/pull/2680))
|
||||
|
|
@ -85,10 +34,6 @@ Same as v0.19.6
|
|||
- Fix reader not saving read duration when changing chapter ([@AntsyLich](https://github.com/AntsyLich), [@KotlinHero](https://github.com/KotlinHero)) ([#2784](https://github.com/mihonapp/mihon/pull/2784))
|
||||
- Fix pre-1970 upload date display in chapter list ([@MajorTanya](https://github.com/MajorTanya)) ([#2779](https://github.com/mihonapp/mihon/pull/2779))
|
||||
- Fix crash when trying to install/update extensions while shizuku is not running ([@NGB-Was-Taken](https://github.com/NGB-Was-Taken)) ([#2837](https://github.com/mihonapp/mihon/pull/2837))
|
||||
- Fix Add Repo input not taking up the full dialog width ([@cuong-tran](https://github.com/cuong-tran)) ([#2816](https://github.com/mihonapp/mihon/pull/2816))
|
||||
- Fix migration's selected sources order not preserved ([@AntsyLich](https://github.com/AntsyLich)) ([#2773](https://github.com/mihonapp/mihon/pull/2993))
|
||||
- Fix migration dialog not showing for consecutive prompts from the same screen ([@AntsyLich](https://github.com/AntsyLich)) ([#2773](https://github.com/mihonapp/mihon/pull/2994))
|
||||
- Fix extension install/update stuck at pending ([@AntsyLich](https://github.com/AntsyLich)) ([#3000](https://github.com/mihonapp/mihon/pull/3000))
|
||||
|
||||
### Other
|
||||
- Enable logcat logging on stable and debug builds without enabling verbose logging ([@NGB-Was-Taken](https://github.com/NGB-Was-Taken)) ([#2836](https://github.com/mihonapp/mihon/pull/2836))
|
||||
|
|
|
|||
|
|
@ -24,10 +24,6 @@
|
|||
|
||||
*Requires Android 8.0 or higher.*
|
||||
|
||||
Self-hosted Forgejo releases (including [Obtainium](https://github.com/ImranR98/Obtainium) install) are documented in [docs/forgejo-release-runner.md](docs/forgejo-release-runner.md).
|
||||
|
||||
**Fork upstream status:** see [UPSTREAM_STATUS.md](UPSTREAM_STATUS.md) (updated weekly; manual check: `./scripts/check-upstream.sh`). Merge guide: [docs/upstream-sync.md](docs/upstream-sync.md).
|
||||
|
||||
[](https://github.com/sponsors/cuong-tran "Sponsor me on GitHub")
|
||||
|
||||
<div align="left">
|
||||
|
|
@ -63,7 +59,7 @@ A free and open source manga reader which is based off TachiyomiSY & Mihon/Tachi
|
|||
- Long-click to add/remove single entry to/from library, everywhere.
|
||||
- Docking Read/Resume button to left/right.
|
||||
- In-app progress banner shows Library syncing / Backup restoring / Library updating progress.
|
||||
- `Series ETA` / reading time estimate in the reader, with optional persistence across sessions; library time-left badges use the same tuning. See [docs/reading-eta.md](docs/reading-eta.md).
|
||||
- `Series ETA` / reading time estimate in the reader, with optional persistence across sessions.
|
||||
- Auto-install app update.
|
||||
- Configurable interval to refresh entries from downloaded storage.
|
||||
- Forked from SY so everything from SY.
|
||||
|
|
@ -126,6 +122,7 @@ Custom sources:
|
|||
|
||||
Additional features for some extensions, features include custom description, opening in app, batch add to library, and a bunch of other things based on the source:
|
||||
* 8Muses (EroMuse)
|
||||
* HBrowse
|
||||
* Mangadex
|
||||
* NHentai
|
||||
* Puruin
|
||||
|
|
|
|||
|
|
@ -1,48 +0,0 @@
|
|||
# Upstream sync status
|
||||
|
||||
> Auto-generated by [`scripts/check-upstream.sh`](scripts/check-upstream.sh). Last check: **2026-07-10 07:56 UTC**.
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| Status | 🔴 **Behind upstream** |
|
||||
| Behind upstream | **73** commits on [`upstream/master`](https://github.com/komikku-app/komikku.git) |
|
||||
| Ahead of upstream | **66** fork-only commits on `main` |
|
||||
| Merge base | `bdf81596e9` |
|
||||
| Fork HEAD | `b299a9ce81` |
|
||||
| Upstream HEAD | `87a173929b` |
|
||||
|
||||
## Action recommended
|
||||
|
||||
This fork is **73 commits behind** [komikku-app/komikku](https://github.com/komikku-app/komikku.git). Consider merging upstream soon.
|
||||
|
||||
See [docs/upstream-sync.md](docs/upstream-sync.md) for the manual sync procedure.
|
||||
|
||||
## Recent upstream commits not in fork
|
||||
|
||||
```
|
||||
87a173929b Adapt network implementation for TachiyomiX 1.6 (mihonapp/mihon#3248)
|
||||
4ae732a17b fix(deps): Matching coroutines version
|
||||
eb84ab8439 Ensure old extension store is removed if index url changes (mihonapp/mihon#3408)
|
||||
5e809fefd4 Add auto migration support from legacy extension store index (mihonapp/mihon#3398)
|
||||
1763d947df chore(sync): migrate sync preference keys (#1747)
|
||||
ce6a0ea7fa fix(extension-store): Migrate to extension store during restore/sync legacy backup (#1746)
|
||||
da57b3db07 Change the term "Obsolete" to "Orphaned" for extensions (mihonapp/mihon#3383)
|
||||
da7ee11bb3 Change extension repo to extension store and add support for newer extension index format (#1744)
|
||||
21c1b85796 Add informative error for unapproved MAL titles (mihonapp/mihon#3155)
|
||||
0f556ea9b5 Address bundleOf deprecation (mihonapp/mihon#3073)
|
||||
```
|
||||
|
||||
## Recent fork-only commits
|
||||
|
||||
```
|
||||
b299a9ce81 docs(reading-eta): document unified limits, long chapters, and settings
|
||||
f178a79195 feat(reading-eta): unify library and reader limits; make caps tunable
|
||||
e0f4262407 ci(forgejo): verify signed APKs with unzip + aapt before upload
|
||||
6316b85a09 fix(release): tolerate Forgejo null body/assets in release JSON
|
||||
f91af6fc72 ci(forgejo): shorten release notes to since-last-tag + cap
|
||||
c15d280cb4 feat(eta): cross-manga page-rate priors for reading time left
|
||||
07019175d1 fix(eta): tame reading-time estimates (idle + cumulative history)
|
||||
9ce3f0ba29 ci(forgejo): group release notes by prefix and fork vs merges
|
||||
7fab10c8ef CI: build release JSON with jq --rawfile (avoid ARG_MAX on large bodies
|
||||
e89f342881 CI: fix release step env — multiline COMMIT_LOGS broke act_runner
|
||||
```
|
||||
|
|
@ -28,7 +28,7 @@ android {
|
|||
defaultConfig {
|
||||
applicationId = "app.komikku"
|
||||
|
||||
versionCode = 79
|
||||
versionCode = 78
|
||||
versionName = "1.13.6"
|
||||
|
||||
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
||||
|
|
|
|||
2
app/proguard-rules.pro
vendored
2
app/proguard-rules.pro
vendored
|
|
@ -307,5 +307,3 @@
|
|||
-dontwarn org.ietf.jgss.GSSManager
|
||||
-dontwarn org.ietf.jgss.GSSName
|
||||
-dontwarn org.ietf.jgss.Oid
|
||||
-dontwarn com.google.re2j.Matcher
|
||||
-dontwarn com.google.re2j.Pattern
|
||||
|
|
|
|||
|
|
@ -73,8 +73,8 @@
|
|||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- Legacy deeplink to add extension store -->
|
||||
<intent-filter android:label="@string/action_addExtensionStore">
|
||||
<!-- Deep link to add repos -->
|
||||
<intent-filter android:label="@string/action_add_repo">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
|
@ -84,17 +84,6 @@
|
|||
<data android:host="add-repo" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- Deeplink to add extension store -->
|
||||
<intent-filter android:label="@string/action_addExtensionStore">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="mihon" />
|
||||
<data android:host="extension-store" />
|
||||
</intent-filter>
|
||||
|
||||
<!-- Open backup files -->
|
||||
<intent-filter android:label="@string/pref_restore_backup">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
|
@ -252,7 +241,7 @@
|
|||
<service
|
||||
android:name=".extension.util.ExtensionInstallService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="dataSync" />
|
||||
android:foregroundServiceType="shortService" />
|
||||
|
||||
<service
|
||||
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
|
||||
|
|
@ -344,6 +333,22 @@
|
|||
|
||||
<data android:pathPattern="/g/..*" />
|
||||
</intent-filter>
|
||||
<!-- Tsumino -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="https" />
|
||||
<data android:scheme="http" />
|
||||
|
||||
<data android:host="tsumino.com" />
|
||||
<data android:host="www.tsumino.com" />
|
||||
|
||||
<data android:pathPattern="/Read/View/..*" />
|
||||
<data android:pathPattern="/Book/Info/..*" />
|
||||
</intent-filter>
|
||||
<!-- Pururin -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
|
@ -358,6 +363,21 @@
|
|||
|
||||
<data android:pathPattern="/gallery/..*" />
|
||||
</intent-filter>
|
||||
<!-- HBrowse -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
|
||||
<data android:scheme="https" />
|
||||
<data android:scheme="http" />
|
||||
|
||||
<data android:host="hbrowse.com" />
|
||||
<data android:host="www.hbrowse.com" />
|
||||
|
||||
<!--<data android:pathPattern="/gallery/..*" />-->
|
||||
</intent-filter>
|
||||
<!-- Mangadex -->
|
||||
<intent-filter android:autoVerify="true">
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
|
|
|||
|
|
@ -16701,7 +16701,9 @@ HSPLeu/kanade/tachiyomi/source/online/all/MergedSource$special$$inlined$injectLa
|
|||
PLeu/kanade/tachiyomi/source/online/all/MergedSource$special$$inlined$injectLazy$9;-><init>()V
|
||||
Leu/kanade/tachiyomi/source/online/all/NHentai;
|
||||
Leu/kanade/tachiyomi/source/online/english/EightMuses;
|
||||
Leu/kanade/tachiyomi/source/online/english/HBrowse;
|
||||
Leu/kanade/tachiyomi/source/online/english/Pururin;
|
||||
Leu/kanade/tachiyomi/source/online/english/Tsumino;
|
||||
Leu/kanade/tachiyomi/ui/base/activity/BaseActivity;
|
||||
HSPLeu/kanade/tachiyomi/ui/base/activity/BaseActivity;-><init>()V
|
||||
PLeu/kanade/tachiyomi/ui/base/activity/BaseActivity;-><init>()V
|
||||
|
|
|
|||
|
|
@ -25,16 +25,16 @@ import eu.kanade.domain.track.interactor.AddTracks
|
|||
import eu.kanade.domain.track.interactor.RefreshTracks
|
||||
import eu.kanade.domain.track.interactor.SyncChapterProgressWithTrack
|
||||
import eu.kanade.domain.track.interactor.TrackChapter
|
||||
import mihon.data.extension.repository.ExtensionStoreRepositoryImpl
|
||||
import mihon.data.extension.service.ExtensionStoreService
|
||||
import mihon.data.repository.ExtensionRepoRepositoryImpl
|
||||
import mihon.domain.chapter.interactor.FilterChaptersForDownload
|
||||
import mihon.domain.chapter.interactor.GetChaptersForLibraryUpdateDownload
|
||||
import mihon.domain.extension.interactor.AddExtensionStore
|
||||
import mihon.domain.extension.interactor.GetExtensionStoreCountAsFlow
|
||||
import mihon.domain.extension.interactor.GetExtensionStores
|
||||
import mihon.domain.extension.interactor.RemoveExtensionStore
|
||||
import mihon.domain.extension.interactor.UpdateExtensionStores
|
||||
import mihon.domain.extension.repository.ExtensionStoreRepository
|
||||
import mihon.domain.extensionrepo.interactor.CreateExtensionRepo
|
||||
import mihon.domain.extensionrepo.interactor.DeleteExtensionRepo
|
||||
import mihon.domain.extensionrepo.interactor.GetExtensionRepo
|
||||
import mihon.domain.extensionrepo.interactor.GetExtensionRepoCount
|
||||
import mihon.domain.extensionrepo.interactor.ReplaceExtensionRepo
|
||||
import mihon.domain.extensionrepo.interactor.UpdateExtensionRepo
|
||||
import mihon.domain.extensionrepo.repository.ExtensionRepoRepository
|
||||
import mihon.domain.extensionrepo.service.ExtensionRepoService
|
||||
import mihon.domain.migration.usecases.MigrateMangaUseCase
|
||||
import mihon.domain.upcoming.interactor.GetUpcomingManga
|
||||
import tachiyomi.data.category.CategoryRepositoryImpl
|
||||
|
|
@ -58,7 +58,6 @@ import tachiyomi.domain.category.interactor.SetMangaCategories
|
|||
import tachiyomi.domain.category.interactor.SetSortModeForCategory
|
||||
import tachiyomi.domain.category.interactor.UpdateCategory
|
||||
import tachiyomi.domain.category.repository.CategoryRepository
|
||||
import tachiyomi.domain.chapter.interactor.GetBookmarkedChaptersByMangaId
|
||||
import tachiyomi.domain.chapter.interactor.GetChapter
|
||||
import tachiyomi.domain.chapter.interactor.GetChapterByUrlAndMangaId
|
||||
import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
|
||||
|
|
@ -66,11 +65,8 @@ import tachiyomi.domain.chapter.interactor.SetMangaDefaultChapterFlags
|
|||
import tachiyomi.domain.chapter.interactor.ShouldUpdateDbChapter
|
||||
import tachiyomi.domain.chapter.interactor.UpdateChapter
|
||||
import tachiyomi.domain.chapter.repository.ChapterRepository
|
||||
import tachiyomi.domain.history.interactor.GetCrossMangaPageRateHistorySamples
|
||||
import tachiyomi.domain.history.interactor.GetHistory
|
||||
import tachiyomi.domain.history.interactor.GetHistoryRevision
|
||||
import tachiyomi.domain.history.interactor.GetNextChapters
|
||||
import tachiyomi.domain.history.interactor.GetReadDurationEntriesByMangaIds
|
||||
import tachiyomi.domain.history.interactor.GetTotalReadDuration
|
||||
import tachiyomi.domain.history.interactor.RemoveHistory
|
||||
import tachiyomi.domain.history.interactor.UpsertHistory
|
||||
|
|
@ -165,7 +161,6 @@ class DomainModule : InjektModule {
|
|||
addSingletonFactory<ChapterRepository> { ChapterRepositoryImpl(get()) }
|
||||
addFactory { GetChapter(get()) }
|
||||
addFactory { GetChaptersByMangaId(get()) }
|
||||
addFactory { GetBookmarkedChaptersByMangaId(get(), get(), get()) }
|
||||
addFactory { GetChapterByUrlAndMangaId(get()) }
|
||||
addFactory { UpdateChapter(get()) }
|
||||
addFactory { SetReadStatus(get(), get(), get(), get(), get()) }
|
||||
|
|
@ -173,16 +168,12 @@ class DomainModule : InjektModule {
|
|||
addFactory { SyncChaptersWithSource(get(), get(), get(), get(), get(), get(), get(), get(), get()) }
|
||||
addFactory { GetAvailableScanlators(get()) }
|
||||
addFactory { FilterChaptersForDownload(get(), get(), get(), get()) }
|
||||
addFactory { GetChaptersForLibraryUpdateDownload(get(), get()) }
|
||||
|
||||
addSingletonFactory<HistoryRepository> { HistoryRepositoryImpl(get()) }
|
||||
addFactory { GetHistory(get()) }
|
||||
addFactory { GetHistoryRevision(get()) }
|
||||
addFactory { UpsertHistory(get()) }
|
||||
addFactory { RemoveHistory(get()) }
|
||||
addFactory { GetTotalReadDuration(get()) }
|
||||
addFactory { GetReadDurationEntriesByMangaIds(get()) }
|
||||
addFactory { GetCrossMangaPageRateHistorySamples(get(), get()) }
|
||||
|
||||
addFactory { DeleteDownload(get(), get()) }
|
||||
|
||||
|
|
@ -206,14 +197,14 @@ class DomainModule : InjektModule {
|
|||
addFactory { ToggleSourcePin(get()) }
|
||||
addFactory { TrustExtension(get(), get()) }
|
||||
|
||||
addSingletonFactory { ExtensionStoreService(get(), get(), get()) }
|
||||
addSingletonFactory<ExtensionStoreRepository> { ExtensionStoreRepositoryImpl(get(), get()) }
|
||||
addFactory { AddExtensionStore(get()) }
|
||||
addFactory { GetExtensionStoreCountAsFlow(get()) }
|
||||
addFactory { GetExtensionStores(get()) }
|
||||
addFactory { RemoveExtensionStore(get()) }
|
||||
addFactory { UpdateExtensionStores(get()) }
|
||||
|
||||
addSingletonFactory<ExtensionRepoRepository> { ExtensionRepoRepositoryImpl(get()) }
|
||||
addFactory { ExtensionRepoService(get(), get()) }
|
||||
addFactory { GetExtensionRepo(get()) }
|
||||
addFactory { GetExtensionRepoCount(get()) }
|
||||
addFactory { CreateExtensionRepo(get(), get()) }
|
||||
addFactory { DeleteExtensionRepo(get()) }
|
||||
addFactory { ReplaceExtensionRepo(get()) }
|
||||
addFactory { UpdateExtensionRepo(get(), get()) }
|
||||
addFactory { ToggleIncognito(get()) }
|
||||
addFactory { GetIncognitoState(get(), get(), get()) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -200,7 +200,7 @@ class SyncChaptersWithSource(
|
|||
bookmark = chapter.chapterNumber in deletedBookmarkedChapterNumbers,
|
||||
)
|
||||
|
||||
// Try to use the fetch date of the original entry to not pollute 'Updates' tab
|
||||
// Try to to use the fetch date of the original entry to not pollute 'Updates' tab
|
||||
deletedChapterNumberDateFetchMap[chapter.chapterNumber]?.let {
|
||||
chapter = chapter.copy(dateFetch = it)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,7 +28,11 @@ class GetExtensionLanguages(
|
|||
) { enabledLanguage, availableExtensions ->
|
||||
availableExtensions
|
||||
.flatMap { ext ->
|
||||
ext.sources.map { it.lang }
|
||||
if (ext.sources.isEmpty()) {
|
||||
listOf(ext.lang)
|
||||
} else {
|
||||
ext.sources.map { it.lang }
|
||||
}
|
||||
}
|
||||
.distinct()
|
||||
.sortedWith(
|
||||
|
|
|
|||
|
|
@ -50,6 +50,9 @@ class GetExtensionsByType(
|
|||
(showNsfwSources || !extension.isNsfw)
|
||||
}
|
||||
.flatMap { ext ->
|
||||
if (ext.sources.isEmpty()) {
|
||||
return@flatMap if (ext.lang in enabledLanguages) listOf(ext) else emptyList()
|
||||
}
|
||||
ext.sources.filter { it.lang in enabledLanguages }
|
||||
.map {
|
||||
ext.copy(
|
||||
|
|
|
|||
|
|
@ -4,21 +4,21 @@ import android.content.pm.PackageInfo
|
|||
import androidx.core.content.pm.PackageInfoCompat
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import eu.kanade.tachiyomi.util.system.isDebugBuildType
|
||||
import mihon.domain.extension.model.KOMIKKU_SIGNATURE
|
||||
import mihon.domain.extension.repository.ExtensionStoreRepository
|
||||
import mihon.domain.extensionrepo.interactor.CreateExtensionRepo
|
||||
import mihon.domain.extensionrepo.repository.ExtensionRepoRepository
|
||||
import tachiyomi.core.common.preference.getAndSet
|
||||
|
||||
class TrustExtension(
|
||||
private val repository: ExtensionStoreRepository,
|
||||
private val extensionRepoRepository: ExtensionRepoRepository,
|
||||
private val preferences: SourcePreferences,
|
||||
) {
|
||||
|
||||
suspend fun isTrusted(pkgInfo: PackageInfo, fingerprints: List<String>): Boolean {
|
||||
// KMK -->
|
||||
if (isDebugBuildType) return true
|
||||
if (fingerprints.contains(KOMIKKU_SIGNATURE)) return true
|
||||
if (fingerprints.contains(CreateExtensionRepo.KOMIKKU_SIGNATURE)) return true
|
||||
// KMK <--
|
||||
val trustedFingerprints = repository.getAll().map { it.signingKey }.toHashSet()
|
||||
val trustedFingerprints = extensionRepoRepository.getAll().map { it.signingKeyFingerprint }.toHashSet()
|
||||
val key = "${pkgInfo.packageName}:${PackageInfoCompat.getLongVersionCode(pkgInfo)}:${fingerprints.last()}"
|
||||
return trustedFingerprints.any { fingerprints.contains(it) } || key in preferences.trustedExtensions().get()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,8 +10,8 @@ import java.util.UUID
|
|||
class SyncPreferences(
|
||||
private val preferenceStore: PreferenceStore,
|
||||
) {
|
||||
fun clientHost() = preferenceStore.getString("connection_sync_client_host", "https://sync.tachiyomi.org")
|
||||
fun clientAPIKey() = preferenceStore.getString("connection_sync_client_api_key", "")
|
||||
fun clientHost() = preferenceStore.getString("sync_client_host", "https://sync.tachiyomi.org")
|
||||
fun clientAPIKey() = preferenceStore.getString("sync_client_api_key", "")
|
||||
fun lastSyncTimestamp() = preferenceStore.getLong(Preference.appStateKey("last_sync_timestamp"), 0L)
|
||||
|
||||
fun lastSyncEtag() = preferenceStore.getString("sync_etag", "")
|
||||
|
|
@ -20,19 +20,19 @@ class SyncPreferences(
|
|||
fun syncService() = preferenceStore.getInt("sync_service", 0)
|
||||
|
||||
// KMK -->
|
||||
fun webDavUrl() = preferenceStore.getString("connection_webdav_url", "")
|
||||
fun webDavUsername() = preferenceStore.getString("connection_webdav_username", "")
|
||||
fun webDavPassword() = preferenceStore.getString("connection_webdav_password", "")
|
||||
fun webDavFolder() = preferenceStore.getString("connection_webdav_folder", "komikku")
|
||||
fun webDavUrl() = preferenceStore.getString("webdav_url", "")
|
||||
fun webDavUsername() = preferenceStore.getString("webdav_username", "")
|
||||
fun webDavPassword() = preferenceStore.getString("webdav_password", "")
|
||||
fun webDavFolder() = preferenceStore.getString("webdav_folder", "komikku")
|
||||
// KMK <--
|
||||
|
||||
fun googleDriveAccessToken() = preferenceStore.getString(
|
||||
Preference.appStateKey("connection_google_drive_access_token"),
|
||||
Preference.appStateKey("google_drive_access_token"),
|
||||
"",
|
||||
)
|
||||
|
||||
fun googleDriveRefreshToken() = preferenceStore.getString(
|
||||
Preference.appStateKey("connection_google_drive_refresh_token"),
|
||||
Preference.appStateKey("google_drive_refresh_token"),
|
||||
"",
|
||||
)
|
||||
|
||||
|
|
@ -55,39 +55,39 @@ class SyncPreferences(
|
|||
|
||||
fun getSyncSettings(): SyncSettings {
|
||||
return SyncSettings(
|
||||
libraryEntries = preferenceStore.getBoolean("sync_library_entries", true).get(),
|
||||
categories = preferenceStore.getBoolean("sync_categories", true).get(),
|
||||
chapters = preferenceStore.getBoolean("sync_chapters", true).get(),
|
||||
tracking = preferenceStore.getBoolean("sync_tracking", true).get(),
|
||||
history = preferenceStore.getBoolean("sync_history", true).get(),
|
||||
appSettings = preferenceStore.getBoolean("sync_appSettings", true).get(),
|
||||
extensionStores = preferenceStore.getBoolean("sync_extensionStores", true).get(),
|
||||
sourceSettings = preferenceStore.getBoolean("sync_sourceSettings", true).get(),
|
||||
privateSettings = preferenceStore.getBoolean("sync_privateSettings", true).get(),
|
||||
libraryEntries = preferenceStore.getBoolean("library_entries", true).get(),
|
||||
categories = preferenceStore.getBoolean("categories", true).get(),
|
||||
chapters = preferenceStore.getBoolean("chapters", true).get(),
|
||||
tracking = preferenceStore.getBoolean("tracking", true).get(),
|
||||
history = preferenceStore.getBoolean("history", true).get(),
|
||||
appSettings = preferenceStore.getBoolean("appSettings", true).get(),
|
||||
extensionRepoSettings = preferenceStore.getBoolean("extensionRepoSettings", true).get(),
|
||||
sourceSettings = preferenceStore.getBoolean("sourceSettings", true).get(),
|
||||
privateSettings = preferenceStore.getBoolean("privateSettings", true).get(),
|
||||
|
||||
// SY -->
|
||||
customInfo = preferenceStore.getBoolean("sync_customInfo", true).get(),
|
||||
readEntries = preferenceStore.getBoolean("sync_readEntries", true).get(),
|
||||
savedSearchesFeeds = preferenceStore.getBoolean("sync_savedSearchesFeeds", true).get(),
|
||||
customInfo = preferenceStore.getBoolean("customInfo", true).get(),
|
||||
readEntries = preferenceStore.getBoolean("readEntries", true).get(),
|
||||
savedSearchesFeeds = preferenceStore.getBoolean("savedSearchesFeeds", true).get(),
|
||||
// SY <--
|
||||
)
|
||||
}
|
||||
|
||||
fun setSyncSettings(syncSettings: SyncSettings) {
|
||||
preferenceStore.getBoolean("sync_library_entries", true).set(syncSettings.libraryEntries)
|
||||
preferenceStore.getBoolean("sync_categories", true).set(syncSettings.categories)
|
||||
preferenceStore.getBoolean("sync_chapters", true).set(syncSettings.chapters)
|
||||
preferenceStore.getBoolean("sync_tracking", true).set(syncSettings.tracking)
|
||||
preferenceStore.getBoolean("sync_history", true).set(syncSettings.history)
|
||||
preferenceStore.getBoolean("sync_appSettings", true).set(syncSettings.appSettings)
|
||||
preferenceStore.getBoolean("sync_extensionStores", true).set(syncSettings.extensionStores)
|
||||
preferenceStore.getBoolean("sync_sourceSettings", true).set(syncSettings.sourceSettings)
|
||||
preferenceStore.getBoolean("sync_privateSettings", true).set(syncSettings.privateSettings)
|
||||
preferenceStore.getBoolean("library_entries", true).set(syncSettings.libraryEntries)
|
||||
preferenceStore.getBoolean("categories", true).set(syncSettings.categories)
|
||||
preferenceStore.getBoolean("chapters", true).set(syncSettings.chapters)
|
||||
preferenceStore.getBoolean("tracking", true).set(syncSettings.tracking)
|
||||
preferenceStore.getBoolean("history", true).set(syncSettings.history)
|
||||
preferenceStore.getBoolean("appSettings", true).set(syncSettings.appSettings)
|
||||
preferenceStore.getBoolean("extensionRepoSettings", true).set(syncSettings.extensionRepoSettings)
|
||||
preferenceStore.getBoolean("sourceSettings", true).set(syncSettings.sourceSettings)
|
||||
preferenceStore.getBoolean("privateSettings", true).set(syncSettings.privateSettings)
|
||||
|
||||
// SY -->
|
||||
preferenceStore.getBoolean("sync_customInfo", true).set(syncSettings.customInfo)
|
||||
preferenceStore.getBoolean("sync_readEntries", true).set(syncSettings.readEntries)
|
||||
preferenceStore.getBoolean("sync_savedSearchesFeeds", true).set(syncSettings.savedSearchesFeeds)
|
||||
preferenceStore.getBoolean("customInfo", true).set(syncSettings.customInfo)
|
||||
preferenceStore.getBoolean("readEntries", true).set(syncSettings.readEntries)
|
||||
preferenceStore.getBoolean("savedSearchesFeeds", true).set(syncSettings.savedSearchesFeeds)
|
||||
// SY <--
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ data class SyncSettings(
|
|||
val tracking: Boolean = true,
|
||||
val history: Boolean = true,
|
||||
val appSettings: Boolean = true,
|
||||
val extensionStores: Boolean = true,
|
||||
val extensionRepoSettings: Boolean = true,
|
||||
val sourceSettings: Boolean = true,
|
||||
val privateSettings: Boolean = false,
|
||||
|
||||
|
|
|
|||
|
|
@ -86,12 +86,12 @@ fun ExtensionDetailsScreen(
|
|||
val uriHandler = LocalUriHandler.current
|
||||
val url = remember(state.extension) {
|
||||
val regex = """https://raw.githubusercontent.com/(.+?)/(.+?)/.+""".toRegex()
|
||||
regex.find(state.extension?.store?.indexUrl.orEmpty())
|
||||
regex.find(state.extension?.repoUrl.orEmpty())
|
||||
?.let {
|
||||
val (user, repo) = it.destructured
|
||||
"https://github.com/$user/$repo"
|
||||
}
|
||||
?: state.extension?.store?.indexUrl
|
||||
?: state.extension?.repoUrl
|
||||
}
|
||||
|
||||
Scaffold(
|
||||
|
|
@ -270,17 +270,14 @@ private fun DetailsHeader(
|
|||
|
||||
if (extension is Extension.Installed) {
|
||||
append("\n\n")
|
||||
appendLine(
|
||||
append(
|
||||
"""
|
||||
Update available: ${extension.hasUpdate}
|
||||
Orphaned: ${extension.isObsolete}
|
||||
Obsolete: ${extension.isObsolete}
|
||||
Shared: ${extension.isShared}
|
||||
Repository: ${extension.repoUrl}
|
||||
""".trimIndent(),
|
||||
)
|
||||
val store = extension.store
|
||||
if (store != null) {
|
||||
append("Repository: ${store.indexUrl}")
|
||||
}
|
||||
}
|
||||
}
|
||||
context.copyToClipboard("Extension Debug information", extDebugInfo)
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ import eu.kanade.presentation.browse.components.BaseBrowseItem
|
|||
import eu.kanade.presentation.browse.components.ExtensionIcon
|
||||
import eu.kanade.presentation.components.WarningBanner
|
||||
import eu.kanade.presentation.manga.components.DotSeparatorNoSpaceText
|
||||
import eu.kanade.presentation.more.settings.screen.browse.ExtensionStoresScreen
|
||||
import eu.kanade.presentation.more.settings.screen.browse.ExtensionReposScreen
|
||||
import eu.kanade.presentation.util.animateItemFastScroll
|
||||
import eu.kanade.presentation.util.rememberRequestPackageInstallsPermissionState
|
||||
import eu.kanade.tachiyomi.extension.model.Extension
|
||||
|
|
@ -58,8 +58,6 @@ import eu.kanade.tachiyomi.ui.browse.extension.ExtensionsScreenModel
|
|||
import eu.kanade.tachiyomi.util.system.LocaleHelper
|
||||
import eu.kanade.tachiyomi.util.system.launchRequestPackageInstallsPermission
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import mihon.domain.extension.model.ExtensionStore
|
||||
import mihon.domain.extension.model.KOMIKKU_SIGNATURE
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.i18n.kmk.KMR
|
||||
import tachiyomi.i18n.sy.SYMR
|
||||
|
|
@ -112,9 +110,9 @@ fun ExtensionScreen(
|
|||
modifier = Modifier.padding(contentPadding),
|
||||
actions = persistentListOf(
|
||||
EmptyScreenAction(
|
||||
stringRes = MR.strings.extensionStores,
|
||||
stringRes = MR.strings.label_extension_repos,
|
||||
icon = Icons.Outlined.Settings,
|
||||
onClick = { navigator.push(ExtensionStoresScreen()) },
|
||||
onClick = { navigator.push(ExtensionReposScreen()) },
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
@ -197,9 +195,9 @@ private fun ExtensionContent(
|
|||
// KMK -->
|
||||
KMR.strings.extensions_page_more -> {
|
||||
{
|
||||
Button(onClick = { navigator?.push(ExtensionStoresScreen()) }) {
|
||||
Button(onClick = { navigator?.push(ExtensionReposScreen()) }) {
|
||||
Text(
|
||||
text = stringResource(MR.strings.action_addExtensionStore),
|
||||
text = stringResource(MR.strings.action_add_repo),
|
||||
style = LocalTextStyle.current.copy(
|
||||
color = MaterialTheme.colorScheme.onPrimary,
|
||||
),
|
||||
|
|
@ -403,7 +401,7 @@ private fun ExtensionItemContent(
|
|||
}
|
||||
|
||||
// KMK -->
|
||||
Text(text = extension.storeName?.let { "@$it" } ?: "(?)")
|
||||
Text(text = extension.repoName?.let { "@$it" } ?: "(?)")
|
||||
// KMK <--
|
||||
|
||||
val warning = when {
|
||||
|
|
@ -606,11 +604,11 @@ private fun ExtensionItemContentPreview() {
|
|||
libVersion = 1.0,
|
||||
isNsfw = true,
|
||||
signatureHash = "900000",
|
||||
storeName = "Repository",
|
||||
repoName = "Repository",
|
||||
sources = emptyList(),
|
||||
apkUrl = "Test",
|
||||
apkName = "Test",
|
||||
iconUrl = "",
|
||||
store = ExtensionStore("https://repo", "Komikku", "", KOMIKKU_SIGNATURE, ExtensionStore.Contact("", ""), false),
|
||||
repoUrl = "",
|
||||
)
|
||||
val extInstalled = Extension.Installed(
|
||||
name = "Tachiyomi",
|
||||
|
|
@ -621,9 +619,9 @@ private fun ExtensionItemContentPreview() {
|
|||
libVersion = 1.0,
|
||||
isNsfw = true,
|
||||
signatureHash = "900000",
|
||||
storeName = "Repository",
|
||||
repoName = "Repository",
|
||||
sources = emptyList(),
|
||||
store = ExtensionStore("https://repo", "Komikku", "", KOMIKKU_SIGNATURE, ExtensionStore.Contact("", ""), false),
|
||||
repoUrl = "",
|
||||
pkgFactory = null,
|
||||
icon = null,
|
||||
hasUpdate = false,
|
||||
|
|
@ -640,12 +638,12 @@ private fun ExtensionItemContentPreview() {
|
|||
libVersion = 1.0,
|
||||
isNsfw = true,
|
||||
signatureHash = "900000",
|
||||
storeName = "Repository",
|
||||
repoName = "Repository",
|
||||
)
|
||||
Column {
|
||||
ExtensionItemContent(
|
||||
extension = extAvail.copy(
|
||||
storeName = "Repository extensions minion multiple languages various sources",
|
||||
repoName = "Repository extensions minion multiple languages various sources",
|
||||
),
|
||||
installStep = InstallStep.Idle,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -336,46 +336,47 @@ fun SourceFeedToolbar(
|
|||
// KMK -->
|
||||
actions = {
|
||||
AppBarActions(
|
||||
actions = persistentListOf<AppBar.AppBarAction>().builder().apply {
|
||||
add(bulkSelectionButton(isRunning, toggleSelectionMode))
|
||||
actions = persistentListOf<AppBar.AppBarAction>().builder()
|
||||
.apply {
|
||||
add(bulkSelectionButton(isRunning, toggleSelectionMode))
|
||||
|
||||
onWebViewClick?.let { func ->
|
||||
add(
|
||||
AppBar.Action(
|
||||
title = stringResource(MR.strings.action_web_view),
|
||||
onClick = { func() },
|
||||
icon = Icons.Outlined.Public,
|
||||
),
|
||||
)
|
||||
}
|
||||
onWebViewClick?.let {
|
||||
add(
|
||||
AppBar.Action(
|
||||
title = stringResource(MR.strings.action_web_view),
|
||||
onClick = { onWebViewClick() },
|
||||
icon = Icons.Outlined.Public,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// KMK -->
|
||||
add(
|
||||
AppBar.OverflowAction(
|
||||
title = stringResource(MR.strings.pref_incognito_mode),
|
||||
onClick = onToggleIncognito,
|
||||
),
|
||||
)
|
||||
// KMK <--
|
||||
|
||||
onSortFeedClick?.let { func ->
|
||||
// KMK -->
|
||||
add(
|
||||
AppBar.OverflowAction(
|
||||
title = stringResource(KMR.strings.action_sort_feed),
|
||||
onClick = { func() },
|
||||
title = stringResource(MR.strings.pref_incognito_mode),
|
||||
onClick = onToggleIncognito,
|
||||
),
|
||||
)
|
||||
}
|
||||
// KMK <--
|
||||
|
||||
onSourceSettingClick?.let { func ->
|
||||
add(
|
||||
AppBar.OverflowAction(
|
||||
title = stringResource(MR.strings.label_settings),
|
||||
onClick = { func() },
|
||||
),
|
||||
)
|
||||
onSortFeedClick?.let {
|
||||
add(
|
||||
AppBar.OverflowAction(
|
||||
title = stringResource(KMR.strings.action_sort_feed),
|
||||
onClick = { onSortFeedClick() },
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
onSourceSettingClick?.let {
|
||||
add(
|
||||
AppBar.OverflowAction(
|
||||
title = stringResource(MR.strings.label_settings),
|
||||
onClick = { onSourceSettingClick() },
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
.build(),
|
||||
)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -209,7 +209,7 @@ fun BrowseSourceEHentaiListItem(
|
|||
MangaCoverHide.Book(
|
||||
modifier = Modifier
|
||||
.fillMaxHeight(),
|
||||
bgColor = bgColor ?: MaterialTheme.colorScheme.surface.takeIf { isSelected },
|
||||
bgColor = bgColor ?: (MaterialTheme.colorScheme.surface.takeIf { isSelected }),
|
||||
tint = onBgColor,
|
||||
)
|
||||
} else {
|
||||
|
|
@ -219,7 +219,7 @@ fun BrowseSourceEHentaiListItem(
|
|||
.fillMaxHeight(),
|
||||
// KMK -->
|
||||
alpha = if (isSelected) GRID_SELECTED_COVER_ALPHA else coverAlpha,
|
||||
bgColor = bgColor ?: MaterialTheme.colorScheme.surface.takeIf { isSelected },
|
||||
bgColor = bgColor ?: (MaterialTheme.colorScheme.surface.takeIf { isSelected }),
|
||||
tint = onBgColor,
|
||||
// KMK <--
|
||||
data = coverData,
|
||||
|
|
|
|||
|
|
@ -39,26 +39,27 @@ fun BrowseSourceSimpleToolbar(
|
|||
var selectingDisplayMode by remember { mutableStateOf(false) }
|
||||
// KMK -->
|
||||
AppBarActions(
|
||||
actions = persistentListOf<AppBar.AppBarAction>().builder().apply {
|
||||
displayMode?.let { mode ->
|
||||
add(
|
||||
AppBar.Action(
|
||||
title = stringResource(MR.strings.action_display_mode),
|
||||
icon = if (mode == LibraryDisplayMode.List) {
|
||||
Icons.AutoMirrored.Filled.ViewList
|
||||
} else {
|
||||
Icons.Filled.ViewModule
|
||||
},
|
||||
onClick = { selectingDisplayMode = true },
|
||||
),
|
||||
)
|
||||
actions = persistentListOf<AppBar.AppBarAction>().builder()
|
||||
.apply {
|
||||
displayMode?.let {
|
||||
add(
|
||||
AppBar.Action(
|
||||
title = stringResource(MR.strings.action_display_mode),
|
||||
icon = if (displayMode == LibraryDisplayMode.List) {
|
||||
Icons.AutoMirrored.Filled.ViewList
|
||||
} else {
|
||||
Icons.Filled.ViewModule
|
||||
},
|
||||
onClick = { selectingDisplayMode = true },
|
||||
),
|
||||
)
|
||||
}
|
||||
toggleSelectionMode?.let {
|
||||
add(
|
||||
bulkSelectionButton(isRunning, toggleSelectionMode),
|
||||
)
|
||||
}
|
||||
}
|
||||
toggleSelectionMode?.let { mode ->
|
||||
add(
|
||||
bulkSelectionButton(isRunning, mode),
|
||||
)
|
||||
}
|
||||
}
|
||||
.build(),
|
||||
)
|
||||
// KMK <--
|
||||
|
|
|
|||
|
|
@ -67,6 +67,7 @@ fun Screen.BulkFavoriteDialogs(
|
|||
AddDuplicateMangaDialog(
|
||||
dialog = dialog,
|
||||
navigator = navigator,
|
||||
state = bulkFavoriteState,
|
||||
onDismiss = bulkFavoriteScreenModel::dismissDialog,
|
||||
stopRunning = bulkFavoriteScreenModel::stopRunning,
|
||||
toggleSelectionMode = bulkFavoriteScreenModel::toggleSelectionMode,
|
||||
|
|
@ -133,9 +134,10 @@ private fun Screen.ShowMigrateDialog(
|
|||
private fun AddDuplicateMangaDialog(
|
||||
dialog: Dialog.AddDuplicateManga,
|
||||
navigator: Navigator?,
|
||||
state: BulkFavoriteScreenModel.State,
|
||||
onDismiss: () -> Unit,
|
||||
stopRunning: () -> Unit,
|
||||
toggleSelectionMode: (Boolean) -> Unit,
|
||||
toggleSelectionMode: () -> Unit,
|
||||
addFavorite: (Manga) -> Unit,
|
||||
showMigrateDialog: (manga: Manga, duplicate: Manga) -> Unit,
|
||||
) {
|
||||
|
|
@ -145,7 +147,9 @@ private fun AddDuplicateMangaDialog(
|
|||
duplicates = dialog.duplicates,
|
||||
onDismissRequest = onDismiss,
|
||||
onConfirm = {
|
||||
toggleSelectionMode(false)
|
||||
if (state.selectionMode) {
|
||||
toggleSelectionMode()
|
||||
}
|
||||
addFavorite(dialog.manga)
|
||||
},
|
||||
onOpenManga = { navigator?.push(MangaScreen(it.id)) },
|
||||
|
|
|
|||
|
|
@ -30,51 +30,53 @@ fun BulkSelectionToolbar(
|
|||
titleContent = { Text(text = "$selectedCount") },
|
||||
actions = {
|
||||
AppBarActions(
|
||||
actions = persistentListOf<AppBar.AppBarAction>().builder().apply {
|
||||
if (onSelectAll != null) {
|
||||
add(
|
||||
AppBar.Action(
|
||||
title = stringResource(MR.strings.action_select_all),
|
||||
icon = Icons.Filled.SelectAll,
|
||||
onClick = onSelectAll,
|
||||
),
|
||||
)
|
||||
}
|
||||
if (onReverseSelection != null) {
|
||||
add(
|
||||
AppBar.Action(
|
||||
title = stringResource(MR.strings.action_select_inverse),
|
||||
icon = Icons.Outlined.FlipToBack,
|
||||
onClick = onReverseSelection,
|
||||
),
|
||||
)
|
||||
}
|
||||
if (isRunning) {
|
||||
add(
|
||||
AppBar.ActionCompose(
|
||||
title = stringResource(KMR.strings.action_bulk_select),
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier
|
||||
.size(24.dp),
|
||||
strokeWidth = 2.dp,
|
||||
)
|
||||
},
|
||||
)
|
||||
} else {
|
||||
add(
|
||||
AppBar.Action(
|
||||
title = stringResource(MR.strings.add_to_library),
|
||||
icon = Icons.Filled.Favorite,
|
||||
onClick = {
|
||||
if (selectedCount > 0) {
|
||||
onChangeCategoryClick()
|
||||
}
|
||||
actions = persistentListOf<AppBar.AppBarAction>()
|
||||
.builder()
|
||||
.apply {
|
||||
if (onSelectAll != null) {
|
||||
add(
|
||||
AppBar.Action(
|
||||
title = stringResource(MR.strings.action_select_all),
|
||||
icon = Icons.Filled.SelectAll,
|
||||
onClick = onSelectAll,
|
||||
),
|
||||
)
|
||||
}
|
||||
if (onReverseSelection != null) {
|
||||
add(
|
||||
AppBar.Action(
|
||||
title = stringResource(MR.strings.action_select_inverse),
|
||||
icon = Icons.Outlined.FlipToBack,
|
||||
onClick = onReverseSelection,
|
||||
),
|
||||
)
|
||||
}
|
||||
if (isRunning) {
|
||||
add(
|
||||
AppBar.ActionCompose(
|
||||
title = stringResource(KMR.strings.action_bulk_select),
|
||||
) {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier
|
||||
.size(24.dp),
|
||||
strokeWidth = 2.dp,
|
||||
)
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
}.build(),
|
||||
)
|
||||
} else {
|
||||
add(
|
||||
AppBar.Action(
|
||||
title = stringResource(MR.strings.add_to_library),
|
||||
icon = Icons.Filled.Favorite,
|
||||
onClick = {
|
||||
if (selectedCount > 0) {
|
||||
onChangeCategoryClick()
|
||||
}
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
}.build(),
|
||||
)
|
||||
},
|
||||
isActionMode = true,
|
||||
|
|
|
|||
|
|
@ -44,7 +44,6 @@ private fun DownloadDropdownMenuItems(
|
|||
DownloadAction.NEXT_10_CHAPTERS to pluralStringResource(MR.plurals.download_amount, 10, 10),
|
||||
DownloadAction.NEXT_25_CHAPTERS to pluralStringResource(MR.plurals.download_amount, 25, 25),
|
||||
DownloadAction.UNREAD_CHAPTERS to stringResource(MR.strings.download_unread),
|
||||
DownloadAction.BOOKMARKED_CHAPTERS to stringResource(MR.strings.download_bookmarked),
|
||||
)
|
||||
|
||||
options.forEach { (downloadAction, string) ->
|
||||
|
|
|
|||
|
|
@ -263,7 +263,6 @@ private fun ColumnScope.SortPage(
|
|||
MR.strings.action_sort_chapter_fetch_date to LibrarySort.Type.ChapterFetchDate,
|
||||
MR.strings.action_sort_date_added to LibrarySort.Type.DateAdded,
|
||||
trackerMeanPair,
|
||||
SYMR.strings.action_sort_estimated_time_left to LibrarySort.Type.EstimatedTimeLeft,
|
||||
// SY -->
|
||||
tagSortPair,
|
||||
// SY <--
|
||||
|
|
@ -366,10 +365,6 @@ private fun ColumnScope.DisplayPage(
|
|||
label = stringResource(MR.strings.action_display_unread_badge),
|
||||
pref = screenModel.libraryPreferences.unreadBadge(),
|
||||
)
|
||||
CheckboxItem(
|
||||
label = stringResource(SYMR.strings.action_display_estimated_time_left_badge),
|
||||
pref = screenModel.libraryPreferences.estimatedTimeLeftBadge(),
|
||||
)
|
||||
CheckboxItem(
|
||||
label = stringResource(MR.strings.action_display_local_badge),
|
||||
pref = screenModel.libraryPreferences.localBadge(),
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@ fun MangaCompactGridItem(
|
|||
MangaCoverHide.Book(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
bgColor = bgColor ?: MaterialTheme.colorScheme.surface.takeIf { isSelected },
|
||||
bgColor = bgColor ?: (MaterialTheme.colorScheme.surface.takeIf { isSelected }),
|
||||
tint = onBgColor,
|
||||
)
|
||||
} else {
|
||||
|
|
@ -119,7 +119,7 @@ fun MangaCompactGridItem(
|
|||
data = coverData,
|
||||
// KMK -->
|
||||
alpha = if (isSelected) GRID_SELECTED_COVER_ALPHA else coverAlpha,
|
||||
bgColor = bgColor ?: MaterialTheme.colorScheme.surface.takeIf { isSelected },
|
||||
bgColor = bgColor ?: (MaterialTheme.colorScheme.surface.takeIf { isSelected }),
|
||||
tint = onBgColor,
|
||||
// KMK <--
|
||||
)
|
||||
|
|
@ -241,7 +241,7 @@ fun MangaComfortableGridItem(
|
|||
MangaCoverHide.Book(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
bgColor = bgColor ?: MaterialTheme.colorScheme.surface.takeIf { isSelected },
|
||||
bgColor = bgColor ?: (MaterialTheme.colorScheme.surface.takeIf { isSelected }),
|
||||
tint = onBgColor,
|
||||
)
|
||||
} else {
|
||||
|
|
@ -255,7 +255,7 @@ fun MangaComfortableGridItem(
|
|||
data = coverData,
|
||||
// KMK -->
|
||||
alpha = if (isSelected) GRID_SELECTED_COVER_ALPHA else coverAlpha,
|
||||
bgColor = bgColor ?: MaterialTheme.colorScheme.surface.takeIf { isSelected },
|
||||
bgColor = bgColor ?: (MaterialTheme.colorScheme.surface.takeIf { isSelected }),
|
||||
tint = onBgColor,
|
||||
onCoverLoaded = { _, result ->
|
||||
val image = result.result.image
|
||||
|
|
@ -274,7 +274,7 @@ fun MangaComfortableGridItem(
|
|||
data = coverData,
|
||||
// KMK -->
|
||||
alpha = if (isSelected) GRID_SELECTED_COVER_ALPHA else coverAlpha,
|
||||
bgColor = bgColor ?: MaterialTheme.colorScheme.surface.takeIf { isSelected },
|
||||
bgColor = bgColor ?: (MaterialTheme.colorScheme.surface.takeIf { isSelected }),
|
||||
tint = onBgColor,
|
||||
onCoverLoaded = { _, result ->
|
||||
val image = result.result.image
|
||||
|
|
@ -461,7 +461,7 @@ fun MangaListItem(
|
|||
MangaCoverHide.Square(
|
||||
modifier = Modifier
|
||||
.fillMaxHeight(),
|
||||
bgColor = bgColor ?: MaterialTheme.colorScheme.surface.takeIf { isSelected },
|
||||
bgColor = bgColor ?: (MaterialTheme.colorScheme.surface.takeIf { isSelected }),
|
||||
tint = onBgColor,
|
||||
)
|
||||
} else {
|
||||
|
|
@ -475,7 +475,7 @@ fun MangaListItem(
|
|||
data = coverData,
|
||||
// KMK -->
|
||||
alpha = coverAlpha,
|
||||
bgColor = bgColor ?: MaterialTheme.colorScheme.surface.takeIf { isSelected },
|
||||
bgColor = bgColor ?: (MaterialTheme.colorScheme.surface.takeIf { isSelected }),
|
||||
tint = onBgColor,
|
||||
size = MangaCover.Size.Big,
|
||||
// KMK <--
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import androidx.compose.material.icons.outlined.Folder
|
|||
import androidx.compose.material.icons.outlined.LocalLibrary
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.scale
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
|
@ -19,13 +18,10 @@ import eu.kanade.domain.extension.interactor.GetExtensionLanguages.Companion.get
|
|||
import eu.kanade.domain.source.model.icon
|
||||
import eu.kanade.presentation.theme.TachiyomiPreviewTheme
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.presentation.util.toDurationString
|
||||
import tachiyomi.domain.source.model.Source
|
||||
import tachiyomi.presentation.core.components.Badge
|
||||
import tachiyomi.presentation.core.components.BadgeGroup
|
||||
import tachiyomi.source.local.isLocal
|
||||
import kotlin.time.DurationUnit
|
||||
import kotlin.time.toDuration
|
||||
|
||||
@Composable
|
||||
internal fun DownloadsBadge(count: Long) {
|
||||
|
|
@ -45,22 +41,6 @@ internal fun UnreadBadge(count: Long) {
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun EstimatedTimeLeftBadge(etaMillis: Long?) {
|
||||
val normalizedMillis = etaMillis?.takeIf { it > 0L } ?: return
|
||||
val context = LocalContext.current
|
||||
val text = normalizedMillis
|
||||
.toDuration(DurationUnit.MILLISECONDS)
|
||||
.toDurationString(context, fallback = "")
|
||||
.ifBlank { return }
|
||||
|
||||
Badge(
|
||||
text = text,
|
||||
color = MaterialTheme.colorScheme.secondary,
|
||||
textColor = MaterialTheme.colorScheme.onSecondary,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
internal fun LanguageBadge(
|
||||
isLocal: Boolean,
|
||||
|
|
@ -141,7 +121,6 @@ private fun BadgePreview() {
|
|||
BadgeGroup {
|
||||
DownloadsBadge(count = 10)
|
||||
UnreadBadge(count = 10)
|
||||
EstimatedTimeLeftBadge(etaMillis = 4_500_000)
|
||||
LanguageBadge(isLocal = true, sourceLanguage = "en")
|
||||
LanguageBadge(isLocal = false, sourceLanguage = "en", useLangIcon = false)
|
||||
LanguageBadge(isLocal = false, sourceLanguage = "vi")
|
||||
|
|
|
|||
|
|
@ -49,7 +49,6 @@ internal fun LibraryComfortableGrid(
|
|||
coverBadgeStart = {
|
||||
DownloadsBadge(count = libraryItem.downloadCount)
|
||||
UnreadBadge(count = libraryItem.unreadCount)
|
||||
EstimatedTimeLeftBadge(etaMillis = libraryItem.estimatedTimeLeftMillis)
|
||||
},
|
||||
coverBadgeEnd = {
|
||||
LanguageBadge(
|
||||
|
|
|
|||
|
|
@ -47,7 +47,6 @@ internal fun LibraryCompactGrid(
|
|||
coverBadgeStart = {
|
||||
DownloadsBadge(count = libraryItem.downloadCount)
|
||||
UnreadBadge(count = libraryItem.unreadCount)
|
||||
EstimatedTimeLeftBadge(etaMillis = libraryItem.estimatedTimeLeftMillis)
|
||||
},
|
||||
coverBadgeEnd = {
|
||||
LanguageBadge(
|
||||
|
|
|
|||
|
|
@ -56,7 +56,6 @@ internal fun LibraryList(
|
|||
badge = {
|
||||
DownloadsBadge(count = libraryItem.downloadCount)
|
||||
UnreadBadge(count = libraryItem.unreadCount)
|
||||
EstimatedTimeLeftBadge(etaMillis = libraryItem.estimatedTimeLeftMillis)
|
||||
LanguageBadge(
|
||||
isLocal = libraryItem.isLocal,
|
||||
sourceLanguage = libraryItem.sourceLanguage,
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import androidx.compose.material3.Surface
|
|||
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||
import androidx.compose.material3.surfaceColorAtElevation
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
|
|
@ -38,7 +39,6 @@ import androidx.compose.ui.platform.LocalHapticFeedback
|
|||
import androidx.compose.ui.unit.dp
|
||||
import eu.kanade.presentation.components.AppBar
|
||||
import eu.kanade.presentation.components.AppBarActions
|
||||
import eu.kanade.presentation.libraryUpdateError.components.LibraryUpdateErrorUiModel
|
||||
import eu.kanade.presentation.libraryUpdateError.components.libraryUpdateErrorUiItems
|
||||
import eu.kanade.presentation.manga.components.Button
|
||||
import eu.kanade.tachiyomi.ui.libraryUpdateError.LibraryUpdateErrorItem
|
||||
|
|
@ -67,7 +67,7 @@ fun LibraryUpdateErrorScreen(
|
|||
onInvertSelection: () -> Unit,
|
||||
onErrorsDelete: () -> Unit,
|
||||
onErrorDelete: (Long) -> Unit,
|
||||
onErrorSelected: (LibraryUpdateErrorItem, Boolean, Boolean) -> Unit,
|
||||
onErrorSelected: (LibraryUpdateErrorItem, Boolean, Boolean, Boolean) -> Unit,
|
||||
navigateUp: () -> Unit,
|
||||
) {
|
||||
BackHandler(enabled = state.selectionMode, onBack = { onSelectAll(false) })
|
||||
|
|
@ -75,33 +75,31 @@ fun LibraryUpdateErrorScreen(
|
|||
val scope = rememberCoroutineScope()
|
||||
val listState = rememberLazyListState()
|
||||
|
||||
val uiModels = remember(state) { state.getUiModel() }
|
||||
val headerIndexes = remember(state) {
|
||||
uiModels.withIndex()
|
||||
.filter { it.value is LibraryUpdateErrorUiModel.Header }
|
||||
.map { it.index }
|
||||
val headerIndexes = remember { mutableStateOf<List<Int>>(emptyList()) }
|
||||
LaunchedEffect(state) {
|
||||
headerIndexes.value = state.getHeaderIndexes()
|
||||
}
|
||||
|
||||
val enableScrollToTop by remember(listState) {
|
||||
val enableScrollToTop by remember {
|
||||
derivedStateOf {
|
||||
listState.firstVisibleItemIndex > 0
|
||||
}
|
||||
}
|
||||
|
||||
val enableScrollToBottom by remember(listState) {
|
||||
val enableScrollToBottom by remember {
|
||||
derivedStateOf {
|
||||
listState.canScrollForward
|
||||
}
|
||||
}
|
||||
|
||||
val enableScrollToPrevious by remember(headerIndexes, listState) {
|
||||
val enableScrollToPrevious by remember {
|
||||
derivedStateOf {
|
||||
headerIndexes.any { it < listState.firstVisibleItemIndex }
|
||||
headerIndexes.value.any { it < listState.firstVisibleItemIndex }
|
||||
}
|
||||
}
|
||||
val enableScrollToNext by remember(headerIndexes, listState) {
|
||||
val enableScrollToNext by remember {
|
||||
derivedStateOf {
|
||||
headerIndexes.any { it > listState.firstVisibleItemIndex }
|
||||
headerIndexes.value.any { it > listState.firstVisibleItemIndex }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -143,7 +141,7 @@ fun LibraryUpdateErrorScreen(
|
|||
scrollToPrevious = {
|
||||
scope.launch {
|
||||
listState.scrollToItem(
|
||||
headerIndexes
|
||||
state.getHeaderIndexes()
|
||||
.filter { it < listState.firstVisibleItemIndex }
|
||||
.maxOrNull() ?: 0,
|
||||
)
|
||||
|
|
@ -152,7 +150,7 @@ fun LibraryUpdateErrorScreen(
|
|||
scrollToNext = {
|
||||
scope.launch {
|
||||
listState.scrollToItem(
|
||||
headerIndexes
|
||||
state.getHeaderIndexes()
|
||||
.filter { it > listState.firstVisibleItemIndex }
|
||||
.minOrNull() ?: 0,
|
||||
)
|
||||
|
|
@ -175,7 +173,7 @@ fun LibraryUpdateErrorScreen(
|
|||
state = listState,
|
||||
) {
|
||||
libraryUpdateErrorUiItems(
|
||||
uiModels = uiModels,
|
||||
uiModels = state.getUiModel(),
|
||||
selectionMode = state.selectionMode,
|
||||
onErrorSelected = onErrorSelected,
|
||||
onClick = onClick,
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ import tachiyomi.presentation.core.util.selectedBackground
|
|||
internal fun LazyListScope.libraryUpdateErrorUiItems(
|
||||
uiModels: List<LibraryUpdateErrorUiModel>,
|
||||
selectionMode: Boolean,
|
||||
onErrorSelected: (LibraryUpdateErrorItem, Boolean, Boolean) -> Unit,
|
||||
onErrorSelected: (LibraryUpdateErrorItem, Boolean, Boolean, Boolean) -> Unit,
|
||||
onClick: (LibraryUpdateErrorItem) -> Unit,
|
||||
onClickCover: (LibraryUpdateErrorItem) -> Unit,
|
||||
onDelete: (Long) -> Unit,
|
||||
|
|
@ -81,6 +81,7 @@ internal fun LazyListScope.libraryUpdateErrorUiItems(
|
|||
selectionMode -> onErrorSelected(
|
||||
libraryUpdateErrorItem,
|
||||
!libraryUpdateErrorItem.selected,
|
||||
true,
|
||||
false,
|
||||
)
|
||||
|
||||
|
|
@ -92,6 +93,7 @@ internal fun LazyListScope.libraryUpdateErrorUiItems(
|
|||
libraryUpdateErrorItem,
|
||||
!libraryUpdateErrorItem.selected,
|
||||
true,
|
||||
true,
|
||||
)
|
||||
},
|
||||
onClickCover = { onClickCover(libraryUpdateErrorItem) }.takeIf { !selectionMode },
|
||||
|
|
@ -142,8 +144,8 @@ private fun LibraryUpdateErrorUiItem(
|
|||
.combinedClickable(
|
||||
onClick = onClick,
|
||||
onLongClick = {
|
||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
onLongClick()
|
||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
},
|
||||
)
|
||||
.padding(horizontal = MaterialTheme.padding.medium),
|
||||
|
|
|
|||
|
|
@ -94,7 +94,9 @@ import eu.kanade.tachiyomi.source.online.all.Lanraragi
|
|||
import eu.kanade.tachiyomi.source.online.all.MangaDex
|
||||
import eu.kanade.tachiyomi.source.online.all.NHentai
|
||||
import eu.kanade.tachiyomi.source.online.english.EightMuses
|
||||
import eu.kanade.tachiyomi.source.online.english.HBrowse
|
||||
import eu.kanade.tachiyomi.source.online.english.Pururin
|
||||
import eu.kanade.tachiyomi.source.online.english.Tsumino
|
||||
import eu.kanade.tachiyomi.ui.manga.ChapterList
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaScreenModel
|
||||
import eu.kanade.tachiyomi.ui.manga.MergedMangaData
|
||||
|
|
@ -106,10 +108,12 @@ import exh.source.getMainSource
|
|||
import exh.source.isEhBasedManga
|
||||
import exh.ui.metadata.adapters.EHentaiDescription
|
||||
import exh.ui.metadata.adapters.EightMusesDescription
|
||||
import exh.ui.metadata.adapters.HBrowseDescription
|
||||
import exh.ui.metadata.adapters.LanraragiDescription
|
||||
import exh.ui.metadata.adapters.MangaDexDescription
|
||||
import exh.ui.metadata.adapters.NHentaiDescription
|
||||
import exh.ui.metadata.adapters.PururinDescription
|
||||
import exh.ui.metadata.adapters.TsuminoDescription
|
||||
import tachiyomi.domain.chapter.model.Chapter
|
||||
import tachiyomi.domain.chapter.service.missingChaptersCount
|
||||
import tachiyomi.domain.library.service.LibraryPreferences
|
||||
|
|
@ -190,7 +194,7 @@ fun MangaScreen(
|
|||
onChapterSwipe: (ChapterList.Item, LibraryPreferences.ChapterSwipeAction) -> Unit,
|
||||
|
||||
// Chapter selection
|
||||
onChapterSelected: (ChapterList.Item, Boolean, Boolean) -> Unit,
|
||||
onChapterSelected: (ChapterList.Item, Boolean, Boolean, Boolean) -> Unit,
|
||||
onAllChapterSelected: (Boolean) -> Unit,
|
||||
onInvertSelection: () -> Unit,
|
||||
|
||||
|
|
@ -400,7 +404,7 @@ private fun MangaScreenSmallImpl(
|
|||
onChapterSwipe: (ChapterList.Item, LibraryPreferences.ChapterSwipeAction) -> Unit,
|
||||
|
||||
// Chapter selection
|
||||
onChapterSelected: (ChapterList.Item, Boolean, Boolean) -> Unit,
|
||||
onChapterSelected: (ChapterList.Item, Boolean, Boolean, Boolean) -> Unit,
|
||||
onAllChapterSelected: (Boolean) -> Unit,
|
||||
onInvertSelection: () -> Unit,
|
||||
|
||||
|
|
@ -864,7 +868,7 @@ private fun MangaScreenLargeImpl(
|
|||
onChapterSwipe: (ChapterList.Item, LibraryPreferences.ChapterSwipeAction) -> Unit,
|
||||
|
||||
// Chapter selection
|
||||
onChapterSelected: (ChapterList.Item, Boolean, Boolean) -> Unit,
|
||||
onChapterSelected: (ChapterList.Item, Boolean, Boolean, Boolean) -> Unit,
|
||||
onAllChapterSelected: (Boolean) -> Unit,
|
||||
onInvertSelection: () -> Unit,
|
||||
|
||||
|
|
@ -1303,7 +1307,7 @@ private fun LazyListScope.sharedChapterItems(
|
|||
// SY <--
|
||||
onChapterClicked: (Chapter) -> Unit,
|
||||
onDownloadChapter: ((List<ChapterList.Item>, ChapterDownloadAction) -> Unit)?,
|
||||
onChapterSelected: (ChapterList.Item, Boolean, Boolean) -> Unit,
|
||||
onChapterSelected: (ChapterList.Item, Boolean, Boolean, Boolean) -> Unit,
|
||||
onChapterSwipe: (ChapterList.Item, LibraryPreferences.ChapterSwipeAction) -> Unit,
|
||||
) {
|
||||
items(
|
||||
|
|
@ -1372,14 +1376,14 @@ private fun LazyListScope.sharedChapterItems(
|
|||
chapterSwipeStartAction = chapterSwipeStartAction,
|
||||
chapterSwipeEndAction = chapterSwipeEndAction,
|
||||
onLongClick = {
|
||||
onChapterSelected(item, !item.selected, true, true)
|
||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
onChapterSelected(item, !item.selected, true)
|
||||
},
|
||||
onClick = {
|
||||
onChapterItemClick(
|
||||
chapterItem = item,
|
||||
isAnyChapterSelected = isAnyChapterSelected,
|
||||
onToggleSelection = { onChapterSelected(item, !item.selected, false) },
|
||||
onToggleSelection = { onChapterSelected(item, !item.selected, true, false) },
|
||||
onChapterClicked = onChapterClicked,
|
||||
)
|
||||
},
|
||||
|
|
@ -1434,9 +1438,15 @@ fun metadataDescription(source: Source): MetadataDescriptionComposable? {
|
|||
is EightMuses -> { state, openMetadataViewer, _ ->
|
||||
EightMusesDescription(state, openMetadataViewer)
|
||||
}
|
||||
is HBrowse -> { state, openMetadataViewer, _ ->
|
||||
HBrowseDescription(state, openMetadataViewer)
|
||||
}
|
||||
is Pururin -> { state, openMetadataViewer, _ ->
|
||||
PururinDescription(state, openMetadataViewer)
|
||||
}
|
||||
is Tsumino -> { state, openMetadataViewer, _ ->
|
||||
TsuminoDescription(state, openMetadataViewer)
|
||||
}
|
||||
is Lanraragi -> { state, openMetadataViewer, _ ->
|
||||
LanraragiDescription(state, openMetadataViewer)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ enum class DownloadAction {
|
|||
NEXT_10_CHAPTERS,
|
||||
NEXT_25_CHAPTERS,
|
||||
UNREAD_CHAPTERS,
|
||||
BOOKMARKED_CHAPTERS,
|
||||
}
|
||||
|
||||
enum class EditCoverAction {
|
||||
|
|
|
|||
|
|
@ -43,8 +43,8 @@ fun BaseMangaListItem(
|
|||
onClick = onClickItem,
|
||||
// KMK -->
|
||||
onLongClick = {
|
||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
onLongClick()
|
||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
},
|
||||
// KMK <--
|
||||
)
|
||||
|
|
|
|||
|
|
@ -738,7 +738,7 @@ object SettingsAdvancedScreen : SearchableSettings {
|
|||
subtitle = stringResource(SYMR.strings.data_saver_image_quality_summary),
|
||||
enabled = dataSaver != DataSaver.NONE,
|
||||
),
|
||||
run {
|
||||
kotlin.run {
|
||||
val dataSaverImageFormatJpeg by sourcePreferences.dataSaverImageFormatJpeg()
|
||||
.collectAsState()
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
|
|
@ -808,7 +808,7 @@ object SettingsAdvancedScreen : SearchableSettings {
|
|||
title = stringResource(SYMR.strings.log_level),
|
||||
subtitle = stringResource(SYMR.strings.log_level_summary),
|
||||
),
|
||||
run {
|
||||
kotlin.run {
|
||||
var enableEncryptDatabase by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
if (enableEncryptDatabase) {
|
||||
|
|
|
|||
|
|
@ -14,11 +14,11 @@ import eu.kanade.core.preference.asState
|
|||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import eu.kanade.domain.ui.UiPreferences
|
||||
import eu.kanade.presentation.more.settings.Preference
|
||||
import eu.kanade.presentation.more.settings.screen.browse.ExtensionStoresScreen
|
||||
import eu.kanade.presentation.more.settings.screen.browse.ExtensionReposScreen
|
||||
import eu.kanade.tachiyomi.ui.category.sources.SourceCategoryScreen
|
||||
import eu.kanade.tachiyomi.util.system.AuthenticatorUtil.authenticate
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import mihon.domain.extension.interactor.GetExtensionStoreCountAsFlow
|
||||
import mihon.domain.extensionrepo.interactor.GetExtensionRepoCount
|
||||
import tachiyomi.core.common.i18n.stringResource
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.i18n.kmk.KMR
|
||||
|
|
@ -43,9 +43,9 @@ object SettingsBrowseScreen : SearchableSettings {
|
|||
val navigator = LocalNavigator.currentOrThrow
|
||||
|
||||
val sourcePreferences = remember { Injekt.get<SourcePreferences>() }
|
||||
val getExtensionStoreCountAsFlow = remember { Injekt.get<GetExtensionStoreCountAsFlow>() }
|
||||
val getExtensionRepoCount = remember { Injekt.get<GetExtensionRepoCount>() }
|
||||
|
||||
val reposCount by getExtensionStoreCountAsFlow().collectAsState(0)
|
||||
val reposCount by getExtensionRepoCount.subscribe().collectAsState(0)
|
||||
|
||||
// SY -->
|
||||
val scope = rememberCoroutineScope()
|
||||
|
|
@ -85,7 +85,7 @@ object SettingsBrowseScreen : SearchableSettings {
|
|||
enabled = sourcePreferences.relatedMangas().get(),
|
||||
),
|
||||
// KMK <--
|
||||
run {
|
||||
kotlin.run {
|
||||
val count by sourcePreferences.sourcesTabCategories().collectAsState()
|
||||
Preference.PreferenceItem.TextPreference(
|
||||
title = stringResource(MR.strings.action_edit_categories),
|
||||
|
|
@ -142,10 +142,10 @@ object SettingsBrowseScreen : SearchableSettings {
|
|||
title = stringResource(MR.strings.pref_hide_in_library_items),
|
||||
),
|
||||
Preference.PreferenceItem.TextPreference(
|
||||
title = stringResource(MR.strings.extensionStores),
|
||||
subtitle = pluralStringResource(MR.plurals.num_repos, reposCount.toInt(), reposCount),
|
||||
title = stringResource(MR.strings.label_extension_repos),
|
||||
subtitle = pluralStringResource(MR.plurals.num_repos, reposCount, reposCount),
|
||||
onClick = {
|
||||
navigator.push(ExtensionStoresScreen())
|
||||
navigator.push(ExtensionReposScreen())
|
||||
},
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -48,7 +48,6 @@ import com.journeyapps.barcodescanner.ScanContract
|
|||
import com.journeyapps.barcodescanner.ScanOptions
|
||||
import eu.kanade.domain.sync.SyncPreferences
|
||||
import eu.kanade.presentation.more.settings.Preference
|
||||
import eu.kanade.presentation.more.settings.screen.SettingsSecurityScreen.PasswordDialog
|
||||
import eu.kanade.presentation.more.settings.screen.data.CreateBackupScreen
|
||||
import eu.kanade.presentation.more.settings.screen.data.RestoreBackupScreen
|
||||
import eu.kanade.presentation.more.settings.screen.data.StorageInfo
|
||||
|
|
@ -563,7 +562,7 @@ object SettingsDataScreen : SearchableSettings {
|
|||
SyncManager.SyncService.SYNCYOMI.value to stringResource(SYMR.strings.syncyomi),
|
||||
SyncManager.SyncService.GOOGLE_DRIVE.value to stringResource(SYMR.strings.google_drive),
|
||||
// KMK -->
|
||||
SyncManager.SyncService.WEB_DAV.value to stringResource(KMR.strings.web_dav),
|
||||
SyncManager.SyncService.WebDAV.value to stringResource(KMR.strings.web_dav),
|
||||
// KMK <--
|
||||
),
|
||||
title = stringResource(SYMR.strings.pref_sync_service),
|
||||
|
|
@ -607,7 +606,7 @@ object SettingsDataScreen : SearchableSettings {
|
|||
SyncManager.SyncService.SYNCYOMI -> getSelfHostPreferences(syncPreferences)
|
||||
SyncManager.SyncService.GOOGLE_DRIVE -> getGoogleDrivePreferences()
|
||||
// KMK -->
|
||||
SyncManager.SyncService.WEB_DAV -> getWebDavPreferences(syncPreferences)
|
||||
SyncManager.SyncService.WebDAV -> getWebDavPreferences(syncPreferences)
|
||||
// KMK <--
|
||||
}
|
||||
|
||||
|
|
@ -760,9 +759,7 @@ object SettingsDataScreen : SearchableSettings {
|
|||
title = stringResource(SYMR.strings.pref_sync_api_key),
|
||||
subtitle = stringResource(SYMR.strings.pref_sync_api_key_summ),
|
||||
onConfirm = {
|
||||
scope.launch {
|
||||
syncPreferences.clientAPIKey().set(it)
|
||||
}
|
||||
syncPreferences.clientAPIKey().set(it)
|
||||
true
|
||||
},
|
||||
icon = null,
|
||||
|
|
@ -811,28 +808,17 @@ object SettingsDataScreen : SearchableSettings {
|
|||
true
|
||||
},
|
||||
),
|
||||
run {
|
||||
var dialogOpen by remember { mutableStateOf(false) }
|
||||
if (dialogOpen) {
|
||||
PasswordDialog(
|
||||
onDismissRequest = { dialogOpen = false },
|
||||
onReturnPassword = { password ->
|
||||
dialogOpen = false
|
||||
scope.launch {
|
||||
syncPreferences.webDavPassword().set(password.replace("\n", ""))
|
||||
}
|
||||
},
|
||||
title = KMR.strings.pref_webdav_password,
|
||||
)
|
||||
}
|
||||
Preference.PreferenceItem.TextPreference(
|
||||
title = stringResource(KMR.strings.pref_webdav_password),
|
||||
subtitle = stringResource(KMR.strings.pref_webdav_password_summ),
|
||||
onClick = {
|
||||
dialogOpen = true
|
||||
},
|
||||
)
|
||||
},
|
||||
Preference.PreferenceItem.EditTextPreference(
|
||||
preference = syncPreferences.webDavPassword(),
|
||||
title = stringResource(KMR.strings.pref_webdav_password),
|
||||
subtitle = stringResource(KMR.strings.pref_webdav_password_summ),
|
||||
onValueChanged = { newValue ->
|
||||
scope.launch {
|
||||
syncPreferences.webDavPassword().set(newValue)
|
||||
}
|
||||
true
|
||||
},
|
||||
),
|
||||
Preference.PreferenceItem.EditTextPreference(
|
||||
preference = syncPreferences.webDavFolder(),
|
||||
title = stringResource(KMR.strings.pref_webdav_folder),
|
||||
|
|
@ -846,7 +832,7 @@ object SettingsDataScreen : SearchableSettings {
|
|||
),
|
||||
)
|
||||
}
|
||||
// KMK <--
|
||||
// MK <--
|
||||
|
||||
@Composable
|
||||
private fun getSyncNowPref(): Preference.PreferenceGroup {
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@ import kotlinx.collections.immutable.toImmutableMap
|
|||
import tachiyomi.domain.category.interactor.GetCategories
|
||||
import tachiyomi.domain.category.model.Category
|
||||
import tachiyomi.domain.download.service.DownloadPreferences
|
||||
import tachiyomi.domain.download.service.LibraryUpdateDownloadMode
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.i18n.kmk.KMR
|
||||
import tachiyomi.presentation.core.i18n.pluralStringResource
|
||||
|
|
@ -78,7 +77,6 @@ object SettingsDownloadScreen : SearchableSettings {
|
|||
allCategories = allCategories,
|
||||
),
|
||||
getDownloadAheadGroup(downloadPreferences = downloadPreferences),
|
||||
getLibraryUpdateDownloadGroup(downloadPreferences = downloadPreferences),
|
||||
// KMK -->
|
||||
getDownloadCacheRenewInterval(downloadPreferences = downloadPreferences),
|
||||
// KMK <--
|
||||
|
|
@ -218,39 +216,6 @@ object SettingsDownloadScreen : SearchableSettings {
|
|||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getLibraryUpdateDownloadGroup(
|
||||
downloadPreferences: DownloadPreferences,
|
||||
): Preference.PreferenceGroup {
|
||||
val modePref = downloadPreferences.libraryUpdateDownloadMode()
|
||||
val amountPref = downloadPreferences.libraryUpdateDownloadAmount()
|
||||
val mode by modePref.collectAsState()
|
||||
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(KMR.strings.library_update_download),
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
preference = modePref,
|
||||
entries = persistentMapOf(
|
||||
LibraryUpdateDownloadMode.DISABLED to stringResource(MR.strings.disabled),
|
||||
LibraryUpdateDownloadMode.NEXT_X to stringResource(KMR.strings.library_update_download_mode_next_x),
|
||||
LibraryUpdateDownloadMode.ALL to stringResource(KMR.strings.library_update_download_mode_all),
|
||||
),
|
||||
title = stringResource(KMR.strings.library_update_download_mode),
|
||||
),
|
||||
Preference.PreferenceItem.ListPreference(
|
||||
preference = amountPref,
|
||||
entries = listOf(2, 3, 5, 10)
|
||||
.associateWith { pluralStringResource(MR.plurals.next_unread_chapters, count = it, it) }
|
||||
.toImmutableMap(),
|
||||
title = stringResource(KMR.strings.library_update_download_amount),
|
||||
enabled = mode == LibraryUpdateDownloadMode.NEXT_X,
|
||||
),
|
||||
Preference.PreferenceItem.InfoPreference(stringResource(KMR.strings.library_update_download_info)),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
// KMK -->
|
||||
@Composable
|
||||
private fun getDownloadCacheRenewInterval(
|
||||
|
|
|
|||
|
|
@ -293,14 +293,9 @@ object SettingsLibraryScreen : SearchableSettings {
|
|||
title = stringResource(KMR.strings.pref_show_empty_categories_search),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
preference = libraryPreferences.fetchMetadataOnAdd(),
|
||||
title = stringResource(KMR.strings.pref_fetch_manga_metadata_on_add),
|
||||
subtitle = stringResource(KMR.strings.pref_fetch_manga_metadata_on_add_description),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
preference = libraryPreferences.fetchChaptersOnAdd(),
|
||||
title = stringResource(KMR.strings.pref_fetch_manga_chapters_on_add),
|
||||
subtitle = stringResource(KMR.strings.pref_fetch_manga_chapters_on_add_description),
|
||||
preference = libraryPreferences.syncOnAdd(),
|
||||
title = stringResource(KMR.strings.pref_sync_manga_on_add),
|
||||
subtitle = stringResource(KMR.strings.pref_sync_manga_on_add_description),
|
||||
),
|
||||
// KMK <--
|
||||
),
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@ import eu.kanade.tachiyomi.util.system.hasDisplayCutout
|
|||
import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.persistentMapOf
|
||||
import kotlinx.collections.immutable.toImmutableMap
|
||||
import tachiyomi.domain.reading.service.ReadingEtaPreferences
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.i18n.kmk.KMR
|
||||
import tachiyomi.i18n.sy.SYMR
|
||||
|
|
@ -102,7 +101,6 @@ object SettingsReaderScreen : SearchableSettings {
|
|||
),
|
||||
SY <-- */
|
||||
getDisplayGroup(readerPreferences = readerPref),
|
||||
getReadingEtaTuningGroup(),
|
||||
getEInkGroup(readerPreferences = readerPref),
|
||||
getReadingGroup(readerPreferences = readerPref),
|
||||
getPagedGroup(readerPreferences = readerPref),
|
||||
|
|
@ -123,7 +121,6 @@ object SettingsReaderScreen : SearchableSettings {
|
|||
private fun getDisplayGroup(readerPreferences: ReaderPreferences): Preference.PreferenceGroup {
|
||||
val fullscreenPref = readerPreferences.fullscreen()
|
||||
val fullscreen by fullscreenPref.collectAsState()
|
||||
val showSeriesEta by readerPreferences.showSeriesEta().collectAsState()
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(MR.strings.pref_category_display),
|
||||
preferenceItems = persistentListOf(
|
||||
|
|
@ -161,70 +158,6 @@ object SettingsReaderScreen : SearchableSettings {
|
|||
preference = readerPreferences.showPageNumber(),
|
||||
title = stringResource(MR.strings.pref_show_page_number),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
preference = readerPreferences.showSeriesProgress(),
|
||||
title = stringResource(SYMR.strings.pref_show_series_progress),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
preference = readerPreferences.showSeriesEta(),
|
||||
title = stringResource(SYMR.strings.pref_show_series_eta),
|
||||
),
|
||||
Preference.PreferenceItem.SwitchPreference(
|
||||
preference = readerPreferences.persistentSeriesEta(),
|
||||
title = stringResource(SYMR.strings.pref_persist_series_eta),
|
||||
subtitle = stringResource(SYMR.strings.pref_persist_series_eta_summary),
|
||||
enabled = showSeriesEta,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun getReadingEtaTuningGroup(): Preference.PreferenceGroup {
|
||||
val readingEtaPrefs = remember { Injekt.get<ReadingEtaPreferences>() }
|
||||
val etaMaxStatsPref = readingEtaPrefs.maxStatsDurationMinutes()
|
||||
val etaMaxTrustedPref = readingEtaPrefs.maxTrustedSessionMinutes()
|
||||
val etaSamplePref = readingEtaPrefs.sampleChapterLimit()
|
||||
val etaMaxPagesPref = readingEtaPrefs.maxPagesReadForPageRate()
|
||||
val etaMaxStats by etaMaxStatsPref.collectAsState()
|
||||
val etaMaxTrusted by etaMaxTrustedPref.collectAsState()
|
||||
val etaSample by etaSamplePref.collectAsState()
|
||||
val etaMaxPages by etaMaxPagesPref.collectAsState()
|
||||
return Preference.PreferenceGroup(
|
||||
title = stringResource(KMR.strings.pref_reading_eta_group_title),
|
||||
preferenceItems = persistentListOf(
|
||||
Preference.PreferenceItem.SliderPreference(
|
||||
value = etaMaxStats,
|
||||
valueRange = 5..180,
|
||||
title = stringResource(KMR.strings.pref_reading_eta_max_stats_min),
|
||||
subtitle = stringResource(KMR.strings.pref_reading_eta_max_stats_min_summary),
|
||||
valueString = stringResource(KMR.strings.pref_reading_eta_max_stats_min_value, etaMaxStats),
|
||||
onValueChanged = { etaMaxStatsPref.set(it) },
|
||||
),
|
||||
Preference.PreferenceItem.SliderPreference(
|
||||
value = etaMaxTrusted,
|
||||
valueRange = 60..1440,
|
||||
title = stringResource(KMR.strings.pref_reading_eta_max_trusted_min),
|
||||
subtitle = stringResource(KMR.strings.pref_reading_eta_max_trusted_min_summary),
|
||||
valueString = stringResource(KMR.strings.pref_reading_eta_max_trusted_min_value, etaMaxTrusted),
|
||||
onValueChanged = { etaMaxTrustedPref.set(it) },
|
||||
),
|
||||
Preference.PreferenceItem.SliderPreference(
|
||||
value = etaSample,
|
||||
valueRange = 5..100,
|
||||
title = stringResource(KMR.strings.pref_reading_eta_sample_chapters),
|
||||
subtitle = stringResource(KMR.strings.pref_reading_eta_sample_chapters_summary),
|
||||
valueString = stringResource(KMR.strings.pref_reading_eta_sample_chapters_value, etaSample),
|
||||
onValueChanged = { etaSamplePref.set(it) },
|
||||
),
|
||||
Preference.PreferenceItem.SliderPreference(
|
||||
value = (etaMaxPages / 100).coerceIn(10, 500),
|
||||
valueRange = 10..500,
|
||||
title = stringResource(KMR.strings.pref_reading_eta_max_pages_sample),
|
||||
subtitle = stringResource(KMR.strings.pref_reading_eta_max_pages_sample_summary),
|
||||
valueString = stringResource(KMR.strings.pref_reading_eta_max_pages_sample_value, etaMaxPages),
|
||||
onValueChanged = { etaMaxPagesPref.set(it * 100) },
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -155,7 +155,7 @@ object SettingsSecurityScreen : SearchableSettings {
|
|||
enabled = passwordProtectDownloads,
|
||||
|
||||
),
|
||||
run {
|
||||
kotlin.run {
|
||||
var dialogOpen by remember { mutableStateOf(false) }
|
||||
if (dialogOpen) {
|
||||
PasswordDialog(
|
||||
|
|
@ -183,7 +183,7 @@ object SettingsSecurityScreen : SearchableSettings {
|
|||
securityPreferences.cbzPassword().set("")
|
||||
},
|
||||
),
|
||||
run {
|
||||
kotlin.run {
|
||||
val navigator = LocalNavigator.currentOrThrow
|
||||
val count by securityPreferences.authenticatorTimeRanges().collectAsState()
|
||||
Preference.PreferenceItem.TextPreference(
|
||||
|
|
@ -199,7 +199,7 @@ object SettingsSecurityScreen : SearchableSettings {
|
|||
},
|
||||
)
|
||||
},
|
||||
run {
|
||||
kotlin.run {
|
||||
val selection by securityPreferences.authenticatorDays().collectAsState()
|
||||
var dialogOpen by remember { mutableStateOf(false) }
|
||||
if (dialogOpen) {
|
||||
|
|
@ -332,16 +332,13 @@ object SettingsSecurityScreen : SearchableSettings {
|
|||
fun PasswordDialog(
|
||||
onDismissRequest: () -> Unit,
|
||||
onReturnPassword: (String) -> Unit,
|
||||
// KMK -->
|
||||
title: StringResource = SYMR.strings.cbz_archive_password,
|
||||
// KMK <--
|
||||
) {
|
||||
var password by rememberSaveable { mutableStateOf("") }
|
||||
var passwordVisibility by remember { mutableStateOf(false) }
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
|
||||
title = { Text(text = stringResource(title)) },
|
||||
title = { Text(text = stringResource(SYMR.strings.cbz_archive_password)) },
|
||||
text = {
|
||||
TextField(
|
||||
value = password,
|
||||
|
|
|
|||
|
|
@ -8,18 +8,20 @@ import androidx.compose.ui.platform.LocalContext
|
|||
import cafe.adriel.voyager.core.model.rememberScreenModel
|
||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||
import eu.kanade.presentation.more.settings.screen.browse.components.ExtensionStoreConfirmDialog
|
||||
import eu.kanade.presentation.more.settings.screen.browse.components.ExtensionStoreCreateDialog
|
||||
import eu.kanade.presentation.more.settings.screen.browse.components.ExtensionStoreDeleteDialog
|
||||
import eu.kanade.presentation.more.settings.screen.browse.components.ExtensionStoresScreen
|
||||
import eu.kanade.presentation.more.settings.screen.browse.components.ExtensionRepoConfirmDialog
|
||||
import eu.kanade.presentation.more.settings.screen.browse.components.ExtensionRepoConflictDialog
|
||||
import eu.kanade.presentation.more.settings.screen.browse.components.ExtensionRepoCreateDialog
|
||||
import eu.kanade.presentation.more.settings.screen.browse.components.ExtensionRepoDeleteDialog
|
||||
import eu.kanade.presentation.more.settings.screen.browse.components.ExtensionReposScreen
|
||||
import eu.kanade.presentation.util.Screen
|
||||
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
||||
import eu.kanade.tachiyomi.util.system.openInBrowser
|
||||
import eu.kanade.tachiyomi.util.system.toast
|
||||
import kotlinx.collections.immutable.toImmutableSet
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import tachiyomi.i18n.kmk.KMR
|
||||
import tachiyomi.presentation.core.screens.LoadingScreen
|
||||
|
||||
class ExtensionStoresScreen(
|
||||
class ExtensionReposScreen(
|
||||
private val url: String? = null,
|
||||
) : Screen() {
|
||||
|
||||
|
|
@ -28,72 +30,80 @@ class ExtensionStoresScreen(
|
|||
val context = LocalContext.current
|
||||
val navigator = LocalNavigator.currentOrThrow
|
||||
|
||||
val screenModel = rememberScreenModel { ExtensionStoresScreenModel() }
|
||||
val screenModel = rememberScreenModel { ExtensionReposScreenModel() }
|
||||
val state by screenModel.state.collectAsState()
|
||||
|
||||
LaunchedEffect(url) {
|
||||
url?.let { screenModel.addFromDeeplink(url) }
|
||||
url?.let { screenModel.showDialog(RepoDialog.Confirm(it)) }
|
||||
}
|
||||
|
||||
if (state is ExtensionStoreScreenState.Loading) {
|
||||
if (state is RepoScreenState.Loading) {
|
||||
LoadingScreen()
|
||||
return
|
||||
}
|
||||
|
||||
val successState = state as ExtensionStoreScreenState.Success
|
||||
val successState = state as RepoScreenState.Success
|
||||
|
||||
ExtensionStoresScreen(
|
||||
ExtensionReposScreen(
|
||||
state = successState,
|
||||
onClickCreate = { screenModel.showDialog(ExtensionStoreDialog.Create()) },
|
||||
onCopy = { context.copyToClipboard(it.indexUrl, it.indexUrl) },
|
||||
onOpenWebsite = { it.contact.website.let(context::openInBrowser) },
|
||||
onOpenDiscord = { it.contact.discord?.let(context::openInBrowser) },
|
||||
onClickCreate = { screenModel.showDialog(RepoDialog.Create) },
|
||||
onOpenWebsite = { context.openInBrowser(it.website) },
|
||||
onClickDelete = { screenModel.showDialog(RepoDialog.Delete(it)) },
|
||||
// KMK -->
|
||||
onClickEnable = {
|
||||
screenModel.enableStore(it.indexUrl)
|
||||
screenModel.enableRepo(it)
|
||||
screenModel.refreshExtensionList()
|
||||
context.toast(KMR.strings.extensions_page_need_refresh)
|
||||
},
|
||||
onClickDisable = {
|
||||
screenModel.disableStore(it.indexUrl)
|
||||
screenModel.disableRepo(it)
|
||||
screenModel.refreshExtensionList()
|
||||
context.toast(KMR.strings.extensions_page_need_refresh)
|
||||
},
|
||||
// KMK <--
|
||||
onClickDelete = { screenModel.showDialog(ExtensionStoreDialog.Delete(it)) },
|
||||
onClickRefresh = { screenModel.refreshRepos() },
|
||||
navigateUp = navigator::pop,
|
||||
)
|
||||
|
||||
when (val dialog = successState.dialog) {
|
||||
null -> {}
|
||||
is ExtensionStoreDialog.Create -> {
|
||||
ExtensionStoreCreateDialog(
|
||||
is RepoDialog.Create -> {
|
||||
ExtensionRepoCreateDialog(
|
||||
onDismissRequest = screenModel::dismissDialog,
|
||||
onCreate = { screenModel.createRepo(it) },
|
||||
storeIndexUrls = successState.stores.map { it.indexUrl }.toSet(),
|
||||
processing = dialog.processing,
|
||||
errorMessage = dialog.errorMessage,
|
||||
repoUrls = successState.repos.map { it.baseUrl }.toImmutableSet(),
|
||||
)
|
||||
}
|
||||
is ExtensionStoreDialog.Delete -> {
|
||||
ExtensionStoreDeleteDialog(
|
||||
is RepoDialog.Delete -> {
|
||||
ExtensionRepoDeleteDialog(
|
||||
onDismissRequest = screenModel::dismissDialog,
|
||||
onDelete = { screenModel.deleteRepo(dialog.store.indexUrl) },
|
||||
storeName = dialog.store.name,
|
||||
storeIndexUrl = dialog.store.indexUrl,
|
||||
onDelete = { screenModel.deleteRepo(dialog.repo) },
|
||||
repo = dialog.repo,
|
||||
)
|
||||
}
|
||||
is ExtensionStoreDialog.Confirm -> {
|
||||
ExtensionStoreConfirmDialog(
|
||||
is RepoDialog.Conflict -> {
|
||||
ExtensionRepoConflictDialog(
|
||||
onDismissRequest = screenModel::dismissDialog,
|
||||
onMigrate = { screenModel.replaceRepo(dialog.newRepo) },
|
||||
oldRepo = dialog.oldRepo,
|
||||
newRepo = dialog.newRepo,
|
||||
)
|
||||
}
|
||||
is RepoDialog.Confirm -> {
|
||||
ExtensionRepoConfirmDialog(
|
||||
onDismissRequest = screenModel::dismissDialog,
|
||||
onCreate = { screenModel.createRepo(dialog.url) },
|
||||
storeIndexUrl = dialog.url,
|
||||
storeAlreadyExists = dialog.alreadyExists,
|
||||
processing = dialog.processing,
|
||||
errorMessage = dialog.errorMessage,
|
||||
repo = dialog.url,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
screenModel.events.collectLatest { event ->
|
||||
if (event is RepoEvent.LocalizedMessage) {
|
||||
context.toast(event.stringRes)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,205 @@
|
|||
package eu.kanade.presentation.more.settings.screen.browse
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||
import cafe.adriel.voyager.core.model.screenModelScope
|
||||
import dev.icerock.moko.resources.StringResource
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import eu.kanade.tachiyomi.extension.ExtensionManager
|
||||
import kotlinx.collections.immutable.ImmutableSet
|
||||
import kotlinx.collections.immutable.toImmutableSet
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import mihon.domain.extensionrepo.interactor.CreateExtensionRepo
|
||||
import mihon.domain.extensionrepo.interactor.DeleteExtensionRepo
|
||||
import mihon.domain.extensionrepo.interactor.GetExtensionRepo
|
||||
import mihon.domain.extensionrepo.interactor.ReplaceExtensionRepo
|
||||
import mihon.domain.extensionrepo.interactor.UpdateExtensionRepo
|
||||
import mihon.domain.extensionrepo.model.ExtensionRepo
|
||||
import tachiyomi.core.common.util.lang.launchIO
|
||||
import tachiyomi.i18n.MR
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class ExtensionReposScreenModel(
|
||||
private val getExtensionRepo: GetExtensionRepo = Injekt.get(),
|
||||
private val createExtensionRepo: CreateExtensionRepo = Injekt.get(),
|
||||
private val deleteExtensionRepo: DeleteExtensionRepo = Injekt.get(),
|
||||
private val replaceExtensionRepo: ReplaceExtensionRepo = Injekt.get(),
|
||||
private val updateExtensionRepo: UpdateExtensionRepo = Injekt.get(),
|
||||
private val extensionManager: ExtensionManager = Injekt.get(),
|
||||
// KMK -->
|
||||
private val sourcePreferences: SourcePreferences = Injekt.get(),
|
||||
// KMK <--
|
||||
) : StateScreenModel<RepoScreenState>(RepoScreenState.Loading) {
|
||||
|
||||
private val _events: Channel<RepoEvent> = Channel(Int.MAX_VALUE)
|
||||
val events = _events.receiveAsFlow()
|
||||
|
||||
init {
|
||||
screenModelScope.launchIO {
|
||||
getExtensionRepo.subscribeAll()
|
||||
.collectLatest { repos ->
|
||||
mutableState.update {
|
||||
RepoScreenState.Success(
|
||||
repos = repos.toImmutableSet(),
|
||||
// KMK -->
|
||||
disabledRepos = sourcePreferences.disabledRepos().get(),
|
||||
// KMK <--
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// KMK -->
|
||||
sourcePreferences.disabledRepos().changes()
|
||||
.onEach { disabledRepos ->
|
||||
mutableState.update {
|
||||
when (it) {
|
||||
is RepoScreenState.Success -> it.copy(disabledRepos = disabledRepos)
|
||||
else -> it
|
||||
}
|
||||
}
|
||||
}
|
||||
.launchIn(screenModelScope)
|
||||
// KMK <--
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and adds a new repo to the database.
|
||||
*
|
||||
* @param baseUrl The baseUrl of the repo to create.
|
||||
*/
|
||||
fun createRepo(baseUrl: String) {
|
||||
screenModelScope.launchIO {
|
||||
when (val result = createExtensionRepo.await(baseUrl)) {
|
||||
CreateExtensionRepo.Result.Success -> extensionManager.findAvailableExtensions()
|
||||
CreateExtensionRepo.Result.InvalidUrl -> _events.send(RepoEvent.InvalidUrl)
|
||||
CreateExtensionRepo.Result.RepoAlreadyExists -> _events.send(RepoEvent.RepoAlreadyExists)
|
||||
is CreateExtensionRepo.Result.DuplicateFingerprint -> {
|
||||
showDialog(RepoDialog.Conflict(result.oldRepo, result.newRepo))
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Inserts a repo to the database, replace a matching repo with the same signing key fingerprint if found.
|
||||
*
|
||||
* @param newRepo The repo to insert
|
||||
*/
|
||||
fun replaceRepo(newRepo: ExtensionRepo) {
|
||||
screenModelScope.launchIO {
|
||||
replaceExtensionRepo.await(newRepo)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes information for each repository.
|
||||
*/
|
||||
fun refreshRepos() {
|
||||
val status = state.value
|
||||
|
||||
if (status is RepoScreenState.Success) {
|
||||
screenModelScope.launchIO {
|
||||
updateExtensionRepo.awaitAll()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the given repo from the database
|
||||
*/
|
||||
fun deleteRepo(baseUrl: String) {
|
||||
// KMK -->
|
||||
// Remove repo from disabled list
|
||||
enableRepo(baseUrl)
|
||||
// KMK <--
|
||||
screenModelScope.launchIO {
|
||||
deleteExtensionRepo.await(baseUrl)
|
||||
extensionManager.findAvailableExtensions()
|
||||
}
|
||||
}
|
||||
|
||||
// KMK -->
|
||||
fun enableRepo(baseUrl: String) {
|
||||
val disabledRepos = sourcePreferences.disabledRepos().get()
|
||||
if (baseUrl in disabledRepos) {
|
||||
sourcePreferences.disabledRepos().set(
|
||||
disabledRepos.filterNot { it == baseUrl }.toSet(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun disableRepo(baseUrl: String) {
|
||||
val disabledRepos = sourcePreferences.disabledRepos().get()
|
||||
if (baseUrl !in disabledRepos) {
|
||||
sourcePreferences.disabledRepos().set(
|
||||
disabledRepos + baseUrl,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun refreshExtensionList() {
|
||||
screenModelScope.launchIO {
|
||||
extensionManager.findAvailableExtensions()
|
||||
}
|
||||
}
|
||||
// KMK <--
|
||||
|
||||
fun showDialog(dialog: RepoDialog) {
|
||||
mutableState.update {
|
||||
when (it) {
|
||||
RepoScreenState.Loading -> it
|
||||
is RepoScreenState.Success -> it.copy(dialog = dialog)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun dismissDialog() {
|
||||
mutableState.update {
|
||||
when (it) {
|
||||
RepoScreenState.Loading -> it
|
||||
is RepoScreenState.Success -> it.copy(dialog = null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed class RepoEvent {
|
||||
sealed class LocalizedMessage(val stringRes: StringResource) : RepoEvent()
|
||||
data object InvalidUrl : LocalizedMessage(MR.strings.invalid_repo_name)
|
||||
data object RepoAlreadyExists : LocalizedMessage(MR.strings.error_repo_exists)
|
||||
}
|
||||
|
||||
sealed class RepoDialog {
|
||||
data object Create : RepoDialog()
|
||||
data class Delete(val repo: String) : RepoDialog()
|
||||
data class Conflict(val oldRepo: ExtensionRepo, val newRepo: ExtensionRepo) : RepoDialog()
|
||||
data class Confirm(val url: String) : RepoDialog()
|
||||
}
|
||||
|
||||
sealed class RepoScreenState {
|
||||
|
||||
@Immutable
|
||||
data object Loading : RepoScreenState()
|
||||
|
||||
@Immutable
|
||||
data class Success(
|
||||
val repos: ImmutableSet<ExtensionRepo>,
|
||||
val oldRepos: ImmutableSet<String>? = null,
|
||||
val dialog: RepoDialog? = null,
|
||||
// KMK -->
|
||||
val disabledRepos: Set<String> = emptySet(),
|
||||
// KMK <--
|
||||
) : RepoScreenState() {
|
||||
|
||||
val isEmpty: Boolean
|
||||
get() = repos.isEmpty()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,223 +0,0 @@
|
|||
package eu.kanade.presentation.more.settings.screen.browse
|
||||
|
||||
import androidx.compose.runtime.Immutable
|
||||
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||
import cafe.adriel.voyager.core.model.screenModelScope
|
||||
import eu.kanade.domain.source.service.SourcePreferences
|
||||
import eu.kanade.tachiyomi.extension.ExtensionManager
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import kotlinx.coroutines.flow.update
|
||||
import mihon.domain.extension.interactor.AddExtensionStore
|
||||
import mihon.domain.extension.interactor.GetExtensionStores
|
||||
import mihon.domain.extension.interactor.RemoveExtensionStore
|
||||
import mihon.domain.extension.interactor.UpdateExtensionStores
|
||||
import mihon.domain.extension.model.ExtensionStore
|
||||
import tachiyomi.core.common.util.lang.launchIO
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class ExtensionStoresScreenModel(
|
||||
private val getExtensionStores: GetExtensionStores = Injekt.get(),
|
||||
private val addExtensionStore: AddExtensionStore = Injekt.get(),
|
||||
private val removeExtensionStore: RemoveExtensionStore = Injekt.get(),
|
||||
private val updateExtensionStores: UpdateExtensionStores = Injekt.get(),
|
||||
private val extensionManager: ExtensionManager = Injekt.get(),
|
||||
// KMK -->
|
||||
private val sourcePreferences: SourcePreferences = Injekt.get(),
|
||||
// KMK <--
|
||||
) : StateScreenModel<ExtensionStoreScreenState>(ExtensionStoreScreenState.Loading) {
|
||||
|
||||
private inline fun updateSuccessState(
|
||||
func: (ExtensionStoreScreenState.Success) -> ExtensionStoreScreenState.Success,
|
||||
) {
|
||||
mutableState.update {
|
||||
when (it) {
|
||||
ExtensionStoreScreenState.Loading -> it
|
||||
is ExtensionStoreScreenState.Success -> func(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
screenModelScope.launchIO {
|
||||
getExtensionStores.subscribe()
|
||||
.collectLatest { stores ->
|
||||
mutableState.update {
|
||||
when (it) {
|
||||
ExtensionStoreScreenState.Loading -> ExtensionStoreScreenState.Success(
|
||||
stores = stores,
|
||||
// KMK -->
|
||||
disabledRepos = sourcePreferences.disabledRepos().get(),
|
||||
// KMK <--
|
||||
)
|
||||
is ExtensionStoreScreenState.Success -> it.copy(stores = stores)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// KMK -->
|
||||
sourcePreferences.disabledRepos().changes()
|
||||
.onEach { disabledRepos ->
|
||||
mutableState.update {
|
||||
when (it) {
|
||||
is ExtensionStoreScreenState.Success -> it.copy(disabledRepos = disabledRepos)
|
||||
else -> it
|
||||
}
|
||||
}
|
||||
}
|
||||
.launchIn(screenModelScope)
|
||||
// KMK <--
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates and adds a new repo to the database.
|
||||
*
|
||||
* @param indexUrl The baseUrl of the repo to create.
|
||||
*/
|
||||
fun createRepo(indexUrl: String) {
|
||||
// KMK -->
|
||||
screenModelScope.launchIO {
|
||||
// KMK <--
|
||||
updateSuccessState {
|
||||
it.copy(
|
||||
dialog = when (it.dialog) {
|
||||
is ExtensionStoreDialog.Create -> it.dialog.copy(processing = true)
|
||||
is ExtensionStoreDialog.Confirm -> it.dialog.copy(processing = true)
|
||||
else -> it.dialog
|
||||
},
|
||||
)
|
||||
}
|
||||
addExtensionStore(indexUrl)
|
||||
.onSuccess {
|
||||
extensionManager.findAvailableExtensions()
|
||||
dismissDialog()
|
||||
}
|
||||
.onFailure { throwable ->
|
||||
updateSuccessState {
|
||||
it.copy(
|
||||
dialog = when (it.dialog) {
|
||||
is ExtensionStoreDialog.Create -> it.dialog.copy(
|
||||
processing = false,
|
||||
errorMessage = throwable.message ?: "unknown error",
|
||||
)
|
||||
is ExtensionStoreDialog.Confirm -> it.dialog.copy(
|
||||
processing = false,
|
||||
errorMessage = throwable.message ?: "unknown error",
|
||||
)
|
||||
else -> it.dialog
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes information for each repository.
|
||||
*/
|
||||
fun refreshRepos() {
|
||||
val status = state.value
|
||||
|
||||
if (status is ExtensionStoreScreenState.Success) {
|
||||
screenModelScope.launchIO {
|
||||
updateExtensionStores()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the given repo from the database
|
||||
*/
|
||||
fun deleteRepo(indexUrl: String) {
|
||||
// KMK -->
|
||||
// Remove repo from disabled list
|
||||
enableStore(indexUrl)
|
||||
// KMK <--
|
||||
screenModelScope.launchIO {
|
||||
removeExtensionStore(indexUrl)
|
||||
extensionManager.findAvailableExtensions()
|
||||
}
|
||||
}
|
||||
|
||||
// KMK -->
|
||||
fun enableStore(indexUrl: String) {
|
||||
val disabledRepos = sourcePreferences.disabledRepos().get()
|
||||
if (indexUrl in disabledRepos) {
|
||||
sourcePreferences.disabledRepos().set(
|
||||
disabledRepos.filterNot { it == indexUrl }.toSet(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun disableStore(indexUrl: String) {
|
||||
val disabledRepos = sourcePreferences.disabledRepos().get()
|
||||
if (indexUrl !in disabledRepos) {
|
||||
sourcePreferences.disabledRepos().set(
|
||||
disabledRepos + indexUrl,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun refreshExtensionList() {
|
||||
screenModelScope.launchIO {
|
||||
extensionManager.findAvailableExtensions()
|
||||
}
|
||||
}
|
||||
// KMK <--
|
||||
|
||||
fun addFromDeeplink(storeIndexUrl: String) {
|
||||
updateSuccessState { state ->
|
||||
state.copy(
|
||||
dialog = ExtensionStoreDialog.Confirm(
|
||||
url = storeIndexUrl,
|
||||
alreadyExists = state.stores.any { it.indexUrl == storeIndexUrl },
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun showDialog(dialog: ExtensionStoreDialog) {
|
||||
updateSuccessState { state ->
|
||||
state.copy(dialog = dialog)
|
||||
}
|
||||
}
|
||||
|
||||
fun dismissDialog() {
|
||||
updateSuccessState {
|
||||
it.copy(dialog = null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sealed class ExtensionStoreDialog {
|
||||
data class Create(val processing: Boolean = false, val errorMessage: String? = null) : ExtensionStoreDialog()
|
||||
data class Delete(val store: ExtensionStore) : ExtensionStoreDialog()
|
||||
data class Confirm(
|
||||
val url: String,
|
||||
val alreadyExists: Boolean = false,
|
||||
val processing: Boolean = false,
|
||||
val errorMessage: String? = null,
|
||||
) : ExtensionStoreDialog()
|
||||
}
|
||||
|
||||
sealed class ExtensionStoreScreenState {
|
||||
|
||||
@Immutable
|
||||
data object Loading : ExtensionStoreScreenState()
|
||||
|
||||
@Immutable
|
||||
data class Success(
|
||||
val stores: List<ExtensionStore>,
|
||||
val dialog: ExtensionStoreDialog? = null,
|
||||
// KMK -->
|
||||
val disabledRepos: Set<String> = emptySet(),
|
||||
// KMK <--
|
||||
) : ExtensionStoreScreenState() {
|
||||
|
||||
val isEmpty: Boolean
|
||||
get() = stores.isEmpty()
|
||||
}
|
||||
}
|
||||
|
|
@ -11,9 +11,9 @@ import androidx.compose.foundation.layout.size
|
|||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.automirrored.outlined.OpenInNew
|
||||
import androidx.compose.material.icons.outlined.ContentCopy
|
||||
import androidx.compose.material.icons.outlined.Delete
|
||||
import androidx.compose.material.icons.outlined.Public
|
||||
import androidx.compose.material.icons.outlined.Visibility
|
||||
import androidx.compose.material.icons.outlined.VisibilityOff
|
||||
import androidx.compose.material3.ElevatedCard
|
||||
|
|
@ -26,33 +26,33 @@ import androidx.compose.runtime.Composable
|
|||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.graphics.ImageBitmap
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.imageResource
|
||||
import androidx.compose.ui.text.style.TextDecoration
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.kanade.tachiyomi.R
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import mihon.domain.extension.model.ExtensionStore
|
||||
import mihon.domain.extension.model.KOMIKKU_SIGNATURE
|
||||
import mihon.domain.extension.model.REPO_SIGNATURE
|
||||
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
||||
import kotlinx.collections.immutable.ImmutableSet
|
||||
import kotlinx.collections.immutable.persistentSetOf
|
||||
import mihon.domain.extensionrepo.interactor.CreateExtensionRepo.Companion.KOMIKKU_SIGNATURE
|
||||
import mihon.domain.extensionrepo.interactor.CreateExtensionRepo.Companion.REPO_SIGNATURE
|
||||
import mihon.domain.extensionrepo.model.ExtensionRepo
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import tachiyomi.presentation.core.icons.CustomIcons
|
||||
import tachiyomi.presentation.core.icons.Discord
|
||||
|
||||
@Composable
|
||||
fun ExtensionStoresContent(
|
||||
repos: List<ExtensionStore>,
|
||||
fun ExtensionReposContent(
|
||||
repos: ImmutableSet<ExtensionRepo>,
|
||||
lazyListState: LazyListState,
|
||||
paddingValues: PaddingValues,
|
||||
onCopy: (ExtensionStore) -> Unit,
|
||||
onOpenWebsite: (ExtensionStore) -> Unit,
|
||||
onOpenDiscord: (ExtensionStore) -> Unit,
|
||||
onClickDelete: (ExtensionStore) -> Unit,
|
||||
onOpenWebsite: (ExtensionRepo) -> Unit,
|
||||
onClickDelete: (String) -> Unit,
|
||||
// KMK -->
|
||||
onClickEnable: (ExtensionStore) -> Unit,
|
||||
onClickDisable: (ExtensionStore) -> Unit,
|
||||
onClickEnable: (String) -> Unit,
|
||||
onClickDisable: (String) -> Unit,
|
||||
disabledRepos: Set<String>,
|
||||
// KMK <--
|
||||
modifier: Modifier = Modifier,
|
||||
|
|
@ -65,17 +65,15 @@ fun ExtensionStoresContent(
|
|||
) {
|
||||
repos.forEach {
|
||||
item {
|
||||
ExtensionStoresListItem(
|
||||
ExtensionRepoListItem(
|
||||
modifier = Modifier.animateItem(),
|
||||
store = it,
|
||||
repo = it,
|
||||
onOpenWebsite = { onOpenWebsite(it) },
|
||||
onOpenDiscord = { onOpenDiscord(it) },
|
||||
onCopy = { onCopy(it) },
|
||||
onDelete = { onClickDelete(it) },
|
||||
onDelete = { onClickDelete(it.baseUrl) },
|
||||
// KMK -->
|
||||
onEnable = { onClickEnable(it) },
|
||||
onDisable = { onClickDisable(it) },
|
||||
isDisabled = it.indexUrl in disabledRepos,
|
||||
onEnable = { onClickEnable(it.baseUrl) },
|
||||
onDisable = { onClickDisable(it.baseUrl) },
|
||||
isDisabled = it.baseUrl in disabledRepos,
|
||||
// KMK <--
|
||||
)
|
||||
}
|
||||
|
|
@ -84,11 +82,9 @@ fun ExtensionStoresContent(
|
|||
}
|
||||
|
||||
@Composable
|
||||
private fun ExtensionStoresListItem(
|
||||
store: ExtensionStore,
|
||||
private fun ExtensionRepoListItem(
|
||||
repo: ExtensionRepo,
|
||||
onOpenWebsite: () -> Unit,
|
||||
onOpenDiscord: () -> Unit,
|
||||
onCopy: () -> Unit,
|
||||
onDelete: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
// KMK -->
|
||||
|
|
@ -97,6 +93,8 @@ private fun ExtensionStoresListItem(
|
|||
onDisable: () -> Unit,
|
||||
// KMK <--
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
ElevatedCard(
|
||||
modifier = modifier,
|
||||
) {
|
||||
|
|
@ -105,9 +103,9 @@ private fun ExtensionStoresListItem(
|
|||
modifier = Modifier
|
||||
.padding(start = MaterialTheme.padding.medium),
|
||||
) {
|
||||
val resId = repoResId(store.signingKey)
|
||||
val resId = repoResId(repo.signingKeyFingerprint)
|
||||
Image(
|
||||
painter = painterResource(id = resId),
|
||||
bitmap = ImageBitmap.imageResource(id = resId),
|
||||
contentDescription = null,
|
||||
alpha = if (isDisabled) 0.4f else 1f,
|
||||
modifier = Modifier
|
||||
|
|
@ -128,7 +126,7 @@ private fun ExtensionStoresListItem(
|
|||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Text(
|
||||
text = store.name,
|
||||
text = repo.name,
|
||||
// KMK: modifier = Modifier.padding(start = MaterialTheme.padding.medium),
|
||||
style = MaterialTheme.typography.titleMedium,
|
||||
// KMK -->
|
||||
|
|
@ -144,21 +142,17 @@ private fun ExtensionStoresListItem(
|
|||
) {
|
||||
IconButton(onClick = onOpenWebsite) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.Public,
|
||||
imageVector = Icons.AutoMirrored.Outlined.OpenInNew,
|
||||
contentDescription = stringResource(MR.strings.action_open_in_browser),
|
||||
)
|
||||
}
|
||||
|
||||
if (store.contact.discord != null) {
|
||||
IconButton(onClick = onOpenDiscord) {
|
||||
Icon(
|
||||
imageVector = CustomIcons.Discord,
|
||||
contentDescription = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
IconButton(onClick = onCopy) {
|
||||
IconButton(
|
||||
onClick = {
|
||||
val url = "${repo.baseUrl}/index.min.json"
|
||||
context.copyToClipboard(url, url)
|
||||
},
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Outlined.ContentCopy,
|
||||
contentDescription = stringResource(MR.strings.action_copy_to_clipboard),
|
||||
|
|
@ -196,18 +190,16 @@ fun repoResId(signKey: String) = when (signKey) {
|
|||
@Preview
|
||||
@Composable
|
||||
fun ExtensionReposContentPreview() {
|
||||
val repos = persistentListOf(
|
||||
ExtensionStore("https://repo", "Komikku", "", KOMIKKU_SIGNATURE, ExtensionStore.Contact("", ""), false),
|
||||
ExtensionStore("https://repo", "Repo", "", REPO_SIGNATURE, ExtensionStore.Contact("", ""), false),
|
||||
ExtensionStore("https://repo", "Other", "", "key2", ExtensionStore.Contact("", ""), true),
|
||||
val repos = persistentSetOf(
|
||||
ExtensionRepo("https://repo", "Komikku", "", "", KOMIKKU_SIGNATURE),
|
||||
ExtensionRepo("https://repo", "Repo", "", "", REPO_SIGNATURE),
|
||||
ExtensionRepo("https://repo", "Other", "", "", "key2"),
|
||||
)
|
||||
ExtensionStoresContent(
|
||||
ExtensionReposContent(
|
||||
repos = repos,
|
||||
lazyListState = LazyListState(),
|
||||
paddingValues = PaddingValues(),
|
||||
onCopy = {},
|
||||
onOpenWebsite = {},
|
||||
onOpenDiscord = {},
|
||||
onClickDelete = {},
|
||||
onClickEnable = {},
|
||||
onClickDisable = {},
|
||||
|
|
@ -0,0 +1,188 @@
|
|||
package eu.kanade.presentation.more.settings.screen.browse.components
|
||||
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import kotlinx.collections.immutable.ImmutableSet
|
||||
import kotlinx.coroutines.delay
|
||||
import mihon.domain.extensionrepo.model.ExtensionRepo
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@Composable
|
||||
fun ExtensionRepoCreateDialog(
|
||||
onDismissRequest: () -> Unit,
|
||||
onCreate: (String) -> Unit,
|
||||
repoUrls: ImmutableSet<String>,
|
||||
) {
|
||||
var name by remember { mutableStateOf("") }
|
||||
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
val nameAlreadyExists = remember(name) { repoUrls.contains(name) }
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
enabled = name.isNotEmpty() && !nameAlreadyExists,
|
||||
onClick = {
|
||||
onCreate(name)
|
||||
onDismissRequest()
|
||||
},
|
||||
) {
|
||||
Text(text = stringResource(MR.strings.action_add))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismissRequest) {
|
||||
Text(text = stringResource(MR.strings.action_cancel))
|
||||
}
|
||||
},
|
||||
title = {
|
||||
Text(text = stringResource(MR.strings.action_add_repo))
|
||||
},
|
||||
text = {
|
||||
Column {
|
||||
Text(text = stringResource(MR.strings.action_add_repo_message, stringResource(MR.strings.app_name)))
|
||||
|
||||
OutlinedTextField(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.focusRequester(focusRequester),
|
||||
value = name,
|
||||
onValueChange = { name = it },
|
||||
label = {
|
||||
Text(text = stringResource(MR.strings.label_add_repo_input))
|
||||
},
|
||||
supportingText = {
|
||||
val msgRes = if (name.isNotEmpty() && nameAlreadyExists) {
|
||||
MR.strings.error_repo_exists
|
||||
} else {
|
||||
MR.strings.information_required_plain
|
||||
}
|
||||
Text(text = stringResource(msgRes))
|
||||
},
|
||||
isError = name.isNotEmpty() && nameAlreadyExists,
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Uri),
|
||||
singleLine = true,
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
LaunchedEffect(focusRequester) {
|
||||
// TODO: https://issuetracker.google.com/issues/204502668
|
||||
delay(0.1.seconds)
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ExtensionRepoDeleteDialog(
|
||||
onDismissRequest: () -> Unit,
|
||||
onDelete: () -> Unit,
|
||||
repo: String,
|
||||
) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
onDelete()
|
||||
onDismissRequest()
|
||||
}) {
|
||||
Text(text = stringResource(MR.strings.action_ok))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismissRequest) {
|
||||
Text(text = stringResource(MR.strings.action_cancel))
|
||||
}
|
||||
},
|
||||
title = {
|
||||
Text(text = stringResource(MR.strings.action_delete_repo))
|
||||
},
|
||||
text = {
|
||||
Text(text = stringResource(MR.strings.delete_repo_confirmation, repo))
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ExtensionRepoConflictDialog(
|
||||
oldRepo: ExtensionRepo,
|
||||
newRepo: ExtensionRepo,
|
||||
onDismissRequest: () -> Unit,
|
||||
onMigrate: () -> Unit,
|
||||
) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
onMigrate()
|
||||
onDismissRequest()
|
||||
},
|
||||
) {
|
||||
Text(text = stringResource(MR.strings.action_replace_repo))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismissRequest) {
|
||||
Text(text = stringResource(MR.strings.action_cancel))
|
||||
}
|
||||
},
|
||||
title = {
|
||||
Text(text = stringResource(MR.strings.action_replace_repo_title))
|
||||
},
|
||||
text = {
|
||||
Text(text = stringResource(MR.strings.action_replace_repo_message, newRepo.name, oldRepo.name))
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ExtensionRepoConfirmDialog(
|
||||
onDismissRequest: () -> Unit,
|
||||
onCreate: () -> Unit,
|
||||
repo: String,
|
||||
) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
title = {
|
||||
Text(text = stringResource(MR.strings.action_add_repo))
|
||||
},
|
||||
text = {
|
||||
Text(text = stringResource(MR.strings.add_repo_confirmation, repo))
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
onCreate()
|
||||
onDismissRequest()
|
||||
},
|
||||
) {
|
||||
Text(text = stringResource(MR.strings.action_add))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismissRequest) {
|
||||
Text(text = stringResource(MR.strings.action_cancel))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
@file:JvmName("ExtensionReposScreenKt")
|
||||
|
||||
package eu.kanade.presentation.more.settings.screen.browse.components
|
||||
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
|
|
@ -19,13 +21,13 @@ import androidx.compose.ui.platform.LocalContext
|
|||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import eu.kanade.presentation.category.components.CategoryFloatingActionButton
|
||||
import eu.kanade.presentation.components.AppBar
|
||||
import eu.kanade.presentation.more.settings.screen.browse.ExtensionStoreScreenState
|
||||
import eu.kanade.presentation.more.settings.screen.browse.RepoScreenState
|
||||
import eu.kanade.tachiyomi.util.system.openInBrowser
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
import mihon.domain.extension.model.ExtensionStore
|
||||
import mihon.domain.extension.model.KOMIKKU_SIGNATURE
|
||||
import mihon.domain.extension.model.REPO_HELP
|
||||
import mihon.domain.extension.model.REPO_SIGNATURE
|
||||
import kotlinx.collections.immutable.persistentSetOf
|
||||
import mihon.domain.extensionrepo.interactor.CreateExtensionRepo.Companion.KOMIKKU_SIGNATURE
|
||||
import mihon.domain.extensionrepo.interactor.CreateExtensionRepo.Companion.REPO_HELP
|
||||
import mihon.domain.extensionrepo.interactor.CreateExtensionRepo.Companion.REPO_SIGNATURE
|
||||
import mihon.domain.extensionrepo.model.ExtensionRepo
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.components.material.Scaffold
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
|
|
@ -35,16 +37,14 @@ import tachiyomi.presentation.core.screens.EmptyScreen
|
|||
import tachiyomi.presentation.core.util.plus
|
||||
|
||||
@Composable
|
||||
fun ExtensionStoresScreen(
|
||||
state: ExtensionStoreScreenState.Success,
|
||||
fun ExtensionReposScreen(
|
||||
state: RepoScreenState.Success,
|
||||
onClickCreate: () -> Unit,
|
||||
onCopy: (ExtensionStore) -> Unit,
|
||||
onOpenWebsite: (ExtensionStore) -> Unit,
|
||||
onOpenDiscord: (ExtensionStore) -> Unit,
|
||||
onClickDelete: (ExtensionStore) -> Unit,
|
||||
onOpenWebsite: (ExtensionRepo) -> Unit,
|
||||
onClickDelete: (String) -> Unit,
|
||||
// KMK -->
|
||||
onClickEnable: (ExtensionStore) -> Unit,
|
||||
onClickDisable: (ExtensionStore) -> Unit,
|
||||
onClickEnable: (String) -> Unit,
|
||||
onClickDisable: (String) -> Unit,
|
||||
// KMK <--
|
||||
onClickRefresh: () -> Unit,
|
||||
navigateUp: () -> Unit,
|
||||
|
|
@ -54,7 +54,7 @@ fun ExtensionStoresScreen(
|
|||
topBar = { scrollBehavior ->
|
||||
AppBar(
|
||||
navigateUp = navigateUp,
|
||||
title = stringResource(MR.strings.extensionStores),
|
||||
title = stringResource(MR.strings.label_extension_repos),
|
||||
scrollBehavior = scrollBehavior,
|
||||
actions = {
|
||||
IconButton(onClick = onClickRefresh) {
|
||||
|
|
@ -76,7 +76,7 @@ fun ExtensionStoresScreen(
|
|||
if (state.isEmpty) {
|
||||
val context = LocalContext.current
|
||||
EmptyScreen(
|
||||
MR.strings.extensionStoresScreen_emptyLabel,
|
||||
MR.strings.information_empty_repos,
|
||||
modifier = Modifier.padding(paddingValues),
|
||||
// KMK -->
|
||||
help = {
|
||||
|
|
@ -94,14 +94,12 @@ fun ExtensionStoresScreen(
|
|||
return@Scaffold
|
||||
}
|
||||
|
||||
ExtensionStoresContent(
|
||||
repos = state.stores,
|
||||
ExtensionReposContent(
|
||||
repos = state.repos,
|
||||
lazyListState = lazyListState,
|
||||
paddingValues = paddingValues + topSmallPaddingValues +
|
||||
PaddingValues(horizontal = MaterialTheme.padding.medium),
|
||||
onCopy = onCopy,
|
||||
onOpenWebsite = onOpenWebsite,
|
||||
onOpenDiscord = onOpenDiscord,
|
||||
onClickDelete = onClickDelete,
|
||||
// KMK -->
|
||||
onClickEnable = onClickEnable,
|
||||
|
|
@ -115,21 +113,19 @@ fun ExtensionStoresScreen(
|
|||
// KMK -->
|
||||
@Preview
|
||||
@Composable
|
||||
private fun ExtensionStoresScreenPreview() {
|
||||
val state = ExtensionStoreScreenState.Success(
|
||||
stores = persistentListOf(
|
||||
ExtensionStore("https://repo", "Komikku", "", KOMIKKU_SIGNATURE, ExtensionStore.Contact("", ""), false),
|
||||
ExtensionStore("https://repo", "Repo", "", REPO_SIGNATURE, ExtensionStore.Contact("", ""), false),
|
||||
ExtensionStore("https://repo", "Other", "", "key2", ExtensionStore.Contact("", ""), true),
|
||||
private fun ExtensionReposScreenPreview() {
|
||||
val state = RepoScreenState.Success(
|
||||
repos = persistentSetOf(
|
||||
ExtensionRepo("https://repo", "Komikku", "", "", KOMIKKU_SIGNATURE),
|
||||
ExtensionRepo("https://repo", "Repo", "", "", REPO_SIGNATURE),
|
||||
ExtensionRepo("https://repo", "Other", "", "", "key2"),
|
||||
),
|
||||
disabledRepos = setOf("https://repo"),
|
||||
)
|
||||
ExtensionStoresScreen(
|
||||
ExtensionReposScreen(
|
||||
state = state,
|
||||
onClickCreate = { },
|
||||
onCopy = { },
|
||||
onOpenWebsite = { },
|
||||
onOpenDiscord = { },
|
||||
onClickDelete = { },
|
||||
onClickEnable = { },
|
||||
onClickDisable = { },
|
||||
|
|
@ -140,14 +136,12 @@ private fun ExtensionStoresScreenPreview() {
|
|||
|
||||
@Preview
|
||||
@Composable
|
||||
private fun ExtensionStoresScreenEmptyPreview() {
|
||||
val state = ExtensionStoreScreenState.Success(stores = persistentListOf())
|
||||
ExtensionStoresScreen(
|
||||
private fun ExtensionReposScreenEmptyPreview() {
|
||||
val state = RepoScreenState.Success(repos = persistentSetOf())
|
||||
ExtensionReposScreen(
|
||||
state = state,
|
||||
onClickCreate = { },
|
||||
onCopy = { },
|
||||
onOpenWebsite = { },
|
||||
onOpenDiscord = { },
|
||||
onClickDelete = { },
|
||||
onClickEnable = { },
|
||||
onClickDisable = { },
|
||||
|
|
@ -1,192 +0,0 @@
|
|||
package eu.kanade.presentation.more.settings.screen.browse.components
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.foundation.text.input.TextFieldLineLimits
|
||||
import androidx.compose.foundation.text.input.rememberTextFieldState
|
||||
import androidx.compose.material3.AlertDialog
|
||||
import androidx.compose.material3.OutlinedTextField
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.derivedStateOf
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.unit.dp
|
||||
import kotlinx.coroutines.delay
|
||||
import tachiyomi.i18n.MR
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
@Composable
|
||||
fun ExtensionStoreCreateDialog(
|
||||
onDismissRequest: () -> Unit,
|
||||
onCreate: (String) -> Unit,
|
||||
storeIndexUrls: Set<String>,
|
||||
processing: Boolean,
|
||||
errorMessage: String?,
|
||||
) {
|
||||
val state = rememberTextFieldState()
|
||||
val storeAlreadyExists by remember(storeIndexUrls) {
|
||||
derivedStateOf {
|
||||
val indexUrl = state.text.toString()
|
||||
storeIndexUrls.contains(indexUrl)
|
||||
}
|
||||
}
|
||||
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
title = {
|
||||
Text(text = stringResource(MR.strings.extensionStoresScreen_addStore_title))
|
||||
},
|
||||
text = {
|
||||
OutlinedTextField(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.focusRequester(focusRequester),
|
||||
state = state,
|
||||
label = {
|
||||
Text(text = stringResource(MR.strings.extensionStoresScreen_addStoreInput_inputLabel))
|
||||
},
|
||||
supportingText = {
|
||||
val msgRes = if (storeAlreadyExists) {
|
||||
MR.strings.extensionStoresScreen_addStore_alreadyExists
|
||||
} else {
|
||||
MR.strings.information_required_plain
|
||||
}
|
||||
Text(text = errorMessage ?: stringResource(msgRes))
|
||||
},
|
||||
isError = errorMessage != null || storeAlreadyExists,
|
||||
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Uri),
|
||||
lineLimits = TextFieldLineLimits.SingleLine,
|
||||
)
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = { onCreate(state.text.toString()) },
|
||||
enabled = !processing && state.text.isNotEmpty() && !storeAlreadyExists,
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(
|
||||
resource = if (processing) {
|
||||
MR.strings.extensionStoresScreen_addStore_processing
|
||||
} else {
|
||||
MR.strings.action_add
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismissRequest) {
|
||||
Text(text = stringResource(MR.strings.action_cancel))
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
LaunchedEffect(focusRequester) {
|
||||
// TODO: https://issuetracker.google.com/issues/204502668
|
||||
delay(0.1.seconds)
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ExtensionStoreDeleteDialog(
|
||||
onDismissRequest: () -> Unit,
|
||||
onDelete: () -> Unit,
|
||||
storeName: String,
|
||||
storeIndexUrl: String,
|
||||
) {
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
title = {
|
||||
Text(text = stringResource(MR.strings.extensionStoresScreen_deleteStore_title))
|
||||
},
|
||||
text = {
|
||||
Text(text = stringResource(MR.strings.extensionStoresScreen_deleteStore_body, storeName, storeIndexUrl))
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = {
|
||||
onDelete()
|
||||
onDismissRequest()
|
||||
}) {
|
||||
Text(text = stringResource(MR.strings.action_ok))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismissRequest) {
|
||||
Text(text = stringResource(MR.strings.action_cancel))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ExtensionStoreConfirmDialog(
|
||||
onDismissRequest: () -> Unit,
|
||||
onCreate: () -> Unit,
|
||||
storeIndexUrl: String,
|
||||
storeAlreadyExists: Boolean,
|
||||
processing: Boolean,
|
||||
errorMessage: String?,
|
||||
) {
|
||||
val state = rememberTextFieldState(initialText = storeIndexUrl)
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismissRequest,
|
||||
title = {
|
||||
Text(text = stringResource(MR.strings.extensionStoresScreen_addStore_title))
|
||||
},
|
||||
text = {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(4.dp)) {
|
||||
Text(text = stringResource(MR.strings.extensionStoresScreen_addStoreDeeplink_bodyText))
|
||||
OutlinedTextField(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
state = state,
|
||||
readOnly = true,
|
||||
supportingText = when {
|
||||
storeAlreadyExists -> {
|
||||
{
|
||||
Text(text = stringResource(MR.strings.extensionStoresScreen_addStore_alreadyExists))
|
||||
}
|
||||
}
|
||||
errorMessage != null -> {
|
||||
{
|
||||
Text(text = errorMessage)
|
||||
}
|
||||
}
|
||||
else -> null
|
||||
},
|
||||
isError = errorMessage != null || storeAlreadyExists,
|
||||
)
|
||||
}
|
||||
},
|
||||
confirmButton = {
|
||||
TextButton(onClick = onCreate, enabled = !storeAlreadyExists && !processing) {
|
||||
Text(
|
||||
text = stringResource(
|
||||
resource = if (processing) {
|
||||
MR.strings.extensionStoresScreen_addStore_processing
|
||||
} else {
|
||||
MR.strings.action_add
|
||||
},
|
||||
),
|
||||
)
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
TextButton(onClick = onDismissRequest) {
|
||||
Text(text = stringResource(MR.strings.action_cancel))
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -114,7 +114,7 @@ private class SyncSettingsSelectorModel(
|
|||
tracking = syncSettings.tracking,
|
||||
history = syncSettings.history,
|
||||
appSettings = syncSettings.appSettings,
|
||||
extensionStores = syncSettings.extensionStores,
|
||||
extensionRepoSettings = syncSettings.extensionRepoSettings,
|
||||
sourceSettings = syncSettings.sourceSettings,
|
||||
privateSettings = syncSettings.privateSettings,
|
||||
|
||||
|
|
@ -134,7 +134,7 @@ private class SyncSettingsSelectorModel(
|
|||
tracking = backupOptions.tracking,
|
||||
history = backupOptions.history,
|
||||
appSettings = backupOptions.appSettings,
|
||||
extensionStores = backupOptions.extensionStores,
|
||||
extensionRepoSettings = backupOptions.extensionRepoSettings,
|
||||
sourceSettings = backupOptions.sourceSettings,
|
||||
privateSettings = backupOptions.privateSettings,
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@ import androidx.compose.foundation.clickable
|
|||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
|
|
@ -22,27 +21,19 @@ import androidx.compose.foundation.layout.navigationBars
|
|||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.surfaceColorAtElevation
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import eu.kanade.presentation.reader.components.ChapterNavigator
|
||||
import eu.kanade.presentation.util.toDurationString
|
||||
import eu.kanade.tachiyomi.ui.reader.ReaderViewModel
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.ReaderOrientation
|
||||
import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.Viewer
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.pager.R2LPagerViewer
|
||||
import kotlinx.collections.immutable.ImmutableSet
|
||||
import tachiyomi.i18n.sy.SYMR
|
||||
import tachiyomi.presentation.core.components.material.padding
|
||||
import tachiyomi.presentation.core.i18n.stringResource
|
||||
import kotlin.time.DurationUnit
|
||||
import kotlin.time.toDuration
|
||||
|
||||
private val readerBarsSlideAnimationSpec = tween<IntOffset>(200)
|
||||
private val readerBarsFadeAnimationSpec = tween<Float>(150)
|
||||
|
|
@ -100,9 +91,6 @@ fun ReaderAppBars(
|
|||
onClickBoostPageHelp: () -> Unit,
|
||||
navBarType: NavBarType,
|
||||
currentPageText: String,
|
||||
seriesReadingProgress: ReaderViewModel.SeriesReadingProgress?,
|
||||
showSeriesProgress: Boolean,
|
||||
showSeriesEta: Boolean,
|
||||
enabledButtons: ImmutableSet<String>,
|
||||
currentReadingMode: ReadingMode,
|
||||
dualPageSplitEnabled: Boolean,
|
||||
|
|
@ -262,11 +250,6 @@ fun ReaderAppBars(
|
|||
// SY <--
|
||||
)
|
||||
}
|
||||
ReaderSeriesProgressInfo(
|
||||
progress = seriesReadingProgress,
|
||||
showSeriesProgress = showSeriesProgress,
|
||||
showSeriesEta = showSeriesEta,
|
||||
)
|
||||
ReaderBottomBar(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
|
|
@ -297,61 +280,3 @@ fun ReaderAppBars(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ReaderSeriesProgressInfo(
|
||||
progress: ReaderViewModel.SeriesReadingProgress?,
|
||||
showSeriesProgress: Boolean,
|
||||
showSeriesEta: Boolean,
|
||||
) {
|
||||
if (progress == null || (!showSeriesProgress && !showSeriesEta)) return
|
||||
|
||||
val context = LocalContext.current
|
||||
val infoParts = buildList {
|
||||
if (showSeriesProgress) {
|
||||
add(
|
||||
stringResource(
|
||||
SYMR.strings.reader_series_progress_value,
|
||||
progress.currentChapter,
|
||||
progress.totalChapters,
|
||||
),
|
||||
)
|
||||
}
|
||||
if (showSeriesEta) {
|
||||
val etaText = progress.etaMillis
|
||||
?.toDuration(DurationUnit.MILLISECONDS)
|
||||
?.toDurationString(context, fallback = "")
|
||||
?.takeIf { it.isNotBlank() }
|
||||
add(
|
||||
etaText
|
||||
?.let { stringResource(SYMR.strings.reader_series_eta_value, it) }
|
||||
?: stringResource(SYMR.strings.reader_series_eta_unknown),
|
||||
)
|
||||
}
|
||||
}
|
||||
if (infoParts.isEmpty()) return
|
||||
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = MaterialTheme.padding.medium),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
) {
|
||||
Text(
|
||||
text = infoParts.joinToString(separator = " • "),
|
||||
modifier = Modifier
|
||||
.background(
|
||||
color = MaterialTheme.colorScheme
|
||||
.surfaceColorAtElevation(6.dp)
|
||||
.copy(alpha = 0.92f),
|
||||
shape = MaterialTheme.shapes.small,
|
||||
)
|
||||
.padding(
|
||||
horizontal = MaterialTheme.padding.small,
|
||||
vertical = MaterialTheme.padding.extraSmall,
|
||||
),
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -61,24 +61,6 @@ internal fun GeneralPage(screenModel: ReaderSettingsScreenModel) {
|
|||
pref = screenModel.preferences.showPageNumber(),
|
||||
)
|
||||
|
||||
CheckboxItem(
|
||||
label = stringResource(SYMR.strings.pref_show_series_progress),
|
||||
pref = screenModel.preferences.showSeriesProgress(),
|
||||
)
|
||||
|
||||
val showSeriesEta by screenModel.preferences.showSeriesEta().collectAsState()
|
||||
CheckboxItem(
|
||||
label = stringResource(SYMR.strings.pref_show_series_eta),
|
||||
pref = screenModel.preferences.showSeriesEta(),
|
||||
)
|
||||
|
||||
if (showSeriesEta) {
|
||||
CheckboxItem(
|
||||
label = stringResource(SYMR.strings.pref_persist_series_eta),
|
||||
pref = screenModel.preferences.persistentSeriesEta(),
|
||||
)
|
||||
}
|
||||
|
||||
// SY -->
|
||||
val forceHorizontalSeekbar by screenModel.preferences.forceHorizontalSeekbar().collectAsState()
|
||||
CheckboxItem(
|
||||
|
|
|
|||
|
|
@ -112,9 +112,6 @@ fun UpdateScreen(
|
|||
else -> {
|
||||
val scope = rememberCoroutineScope()
|
||||
var isRefreshing by remember { mutableStateOf(false) }
|
||||
// KMK -->
|
||||
val uiModels = remember(state.items) { state.getUiModel() }
|
||||
// KMK <--
|
||||
|
||||
PullRefresh(
|
||||
refreshing = isRefreshing,
|
||||
|
|
@ -137,8 +134,8 @@ fun UpdateScreen(
|
|||
updatesLastUpdatedItem(lastUpdated)
|
||||
|
||||
updatesUiItems(
|
||||
uiModels = state.getUiModel(),
|
||||
// KMK -->
|
||||
uiModels = uiModels,
|
||||
expandedState = state.expandedState,
|
||||
collapseToggle = collapseToggle,
|
||||
usePanoramaCover = usePanoramaCover,
|
||||
|
|
|
|||
|
|
@ -171,6 +171,7 @@ internal fun LazyListScope.updatesUiItems(
|
|||
// KMK -->
|
||||
UpdateSelectionOptions(
|
||||
selected = !updatesItem.selected,
|
||||
userSelected = true,
|
||||
fromLongPress = true,
|
||||
isGroup = isLeader && item.isExpandable,
|
||||
isExpanded = isExpanded,
|
||||
|
|
@ -185,6 +186,7 @@ internal fun LazyListScope.updatesUiItems(
|
|||
// KMK -->
|
||||
UpdateSelectionOptions(
|
||||
selected = !updatesItem.selected,
|
||||
userSelected = true,
|
||||
fromLongPress = false,
|
||||
isGroup = isLeader && item.isExpandable,
|
||||
isExpanded = isExpanded,
|
||||
|
|
@ -286,8 +288,8 @@ private fun UpdatesUiItem(
|
|||
.combinedClickable(
|
||||
onClick = onClick,
|
||||
onLongClick = {
|
||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
onLongClick()
|
||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||
},
|
||||
)
|
||||
.padding(top = if (isLeader) MaterialTheme.padding.small else 0.dp)
|
||||
|
|
@ -309,7 +311,7 @@ private fun UpdatesUiItem(
|
|||
MangaCoverHide.Book(
|
||||
modifier = Modifier
|
||||
.width(UpdateItemWidth),
|
||||
bgColor = bgColor ?: MaterialTheme.colorScheme.surface.takeIf { selected },
|
||||
bgColor = bgColor ?: (MaterialTheme.colorScheme.surface.takeIf { selected }),
|
||||
tint = onBgColor,
|
||||
size = MangaCover.Size.Medium,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ fun EhLoginWebViewScreen(
|
|||
)
|
||||
is LoadingState.Loading -> {
|
||||
val animatedProgress by animateFloatAsState(
|
||||
loadingState.progress,
|
||||
(loadingState as? LoadingState.Loading)?.progress ?: 1f,
|
||||
animationSpec = ProgressIndicatorDefaults.ProgressAnimationSpec,
|
||||
label = "webview_loading",
|
||||
)
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import com.hippo.unifile.UniFile
|
|||
import eu.kanade.tachiyomi.BuildConfig
|
||||
import eu.kanade.tachiyomi.data.backup.BackupFileValidator
|
||||
import eu.kanade.tachiyomi.data.backup.create.creators.CategoriesBackupCreator
|
||||
import eu.kanade.tachiyomi.data.backup.create.creators.ExtensionStoresBackupCreator
|
||||
import eu.kanade.tachiyomi.data.backup.create.creators.ExtensionRepoBackupCreator
|
||||
import eu.kanade.tachiyomi.data.backup.create.creators.FeedBackupCreator
|
||||
import eu.kanade.tachiyomi.data.backup.create.creators.MangaBackupCreator
|
||||
import eu.kanade.tachiyomi.data.backup.create.creators.PreferenceBackupCreator
|
||||
|
|
@ -14,7 +14,7 @@ import eu.kanade.tachiyomi.data.backup.create.creators.SavedSearchBackupCreator
|
|||
import eu.kanade.tachiyomi.data.backup.create.creators.SourcesBackupCreator
|
||||
import eu.kanade.tachiyomi.data.backup.models.Backup
|
||||
import eu.kanade.tachiyomi.data.backup.models.BackupCategory
|
||||
import eu.kanade.tachiyomi.data.backup.models.BackupExtensionStore
|
||||
import eu.kanade.tachiyomi.data.backup.models.BackupExtensionRepos
|
||||
import eu.kanade.tachiyomi.data.backup.models.BackupFeed
|
||||
import eu.kanade.tachiyomi.data.backup.models.BackupManga
|
||||
import eu.kanade.tachiyomi.data.backup.models.BackupPreference
|
||||
|
|
@ -54,7 +54,7 @@ class BackupCreator(
|
|||
private val categoriesBackupCreator: CategoriesBackupCreator = CategoriesBackupCreator(),
|
||||
private val mangaBackupCreator: MangaBackupCreator = MangaBackupCreator(),
|
||||
private val preferenceBackupCreator: PreferenceBackupCreator = PreferenceBackupCreator(),
|
||||
private val extensionStoresBackupCreator: ExtensionStoresBackupCreator = ExtensionStoresBackupCreator(),
|
||||
private val extensionRepoBackupCreator: ExtensionRepoBackupCreator = ExtensionRepoBackupCreator(),
|
||||
private val sourcesBackupCreator: SourcesBackupCreator = SourcesBackupCreator(),
|
||||
// KMK -->
|
||||
private val feedBackupCreator: FeedBackupCreator = FeedBackupCreator(),
|
||||
|
|
@ -101,7 +101,7 @@ class BackupCreator(
|
|||
backupCategories = backupCategories(options),
|
||||
backupSources = backupSources(backupManga),
|
||||
backupPreferences = backupAppPreferences(options),
|
||||
backupExtensionStores = backupExtensionStores(options),
|
||||
backupExtensionRepo = backupExtensionRepos(options),
|
||||
backupSourcePreferences = backupSourcePreferences(options),
|
||||
|
||||
// SY -->
|
||||
|
|
@ -159,16 +159,16 @@ class BackupCreator(
|
|||
return sourcesBackupCreator(mangas)
|
||||
}
|
||||
|
||||
internal fun backupAppPreferences(options: BackupOptions): List<BackupPreference> {
|
||||
/* KMK --> */ suspend /* KMK <-- */ fun backupAppPreferences(options: BackupOptions): List<BackupPreference> {
|
||||
if (!options.appSettings) return emptyList()
|
||||
|
||||
return preferenceBackupCreator.createApp(includePrivatePreferences = options.privateSettings)
|
||||
}
|
||||
|
||||
internal suspend fun backupExtensionStores(options: BackupOptions): List<BackupExtensionStore> {
|
||||
if (!options.extensionStores) return emptyList()
|
||||
suspend fun backupExtensionRepos(options: BackupOptions): List<BackupExtensionRepos> {
|
||||
if (!options.extensionRepoSettings) return emptyList()
|
||||
|
||||
return extensionStoresBackupCreator()
|
||||
return extensionRepoBackupCreator()
|
||||
}
|
||||
|
||||
fun backupSourcePreferences(options: BackupOptions): List<BackupSourcePreferences> {
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ data class BackupOptions(
|
|||
val history: Boolean = true,
|
||||
val readEntries: Boolean = true,
|
||||
val appSettings: Boolean = true,
|
||||
val extensionStores: Boolean = true,
|
||||
val extensionRepoSettings: Boolean = true,
|
||||
val sourceSettings: Boolean = true,
|
||||
val privateSettings: Boolean = false,
|
||||
// SY -->
|
||||
|
|
@ -31,7 +31,7 @@ data class BackupOptions(
|
|||
history,
|
||||
readEntries,
|
||||
appSettings,
|
||||
extensionStores,
|
||||
extensionRepoSettings,
|
||||
sourceSettings,
|
||||
privateSettings,
|
||||
// SY -->
|
||||
|
|
@ -41,7 +41,7 @@ data class BackupOptions(
|
|||
)
|
||||
|
||||
fun canCreate() =
|
||||
libraryEntries || categories || appSettings || extensionStores || sourceSettings || savedSearchesFeeds
|
||||
libraryEntries || categories || appSettings || extensionRepoSettings || sourceSettings || savedSearchesFeeds
|
||||
|
||||
companion object {
|
||||
val libraryOptions = persistentListOf(
|
||||
|
|
@ -103,9 +103,9 @@ data class BackupOptions(
|
|||
setter = { options, enabled -> options.copy(appSettings = enabled) },
|
||||
),
|
||||
Entry(
|
||||
label = MR.strings.extensionStores,
|
||||
getter = BackupOptions::extensionStores,
|
||||
setter = { options, enabled -> options.copy(extensionStores = enabled) },
|
||||
label = MR.strings.extensionRepo_settings,
|
||||
getter = BackupOptions::extensionRepoSettings,
|
||||
setter = { options, enabled -> options.copy(extensionRepoSettings = enabled) },
|
||||
),
|
||||
Entry(
|
||||
label = MR.strings.source_settings,
|
||||
|
|
@ -128,7 +128,7 @@ data class BackupOptions(
|
|||
history = array[4],
|
||||
readEntries = array[5],
|
||||
appSettings = array[6],
|
||||
extensionStores = array[7],
|
||||
extensionRepoSettings = array[7],
|
||||
sourceSettings = array[8],
|
||||
privateSettings = array[9],
|
||||
// SY -->
|
||||
|
|
|
|||
|
|
@ -0,0 +1,17 @@
|
|||
package eu.kanade.tachiyomi.data.backup.create.creators
|
||||
|
||||
import eu.kanade.tachiyomi.data.backup.models.BackupExtensionRepos
|
||||
import eu.kanade.tachiyomi.data.backup.models.backupExtensionReposMapper
|
||||
import mihon.domain.extensionrepo.interactor.GetExtensionRepo
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class ExtensionRepoBackupCreator(
|
||||
private val getExtensionRepos: GetExtensionRepo = Injekt.get(),
|
||||
) {
|
||||
|
||||
suspend operator fun invoke(): List<BackupExtensionRepos> {
|
||||
return getExtensionRepos.getAll()
|
||||
.map(backupExtensionReposMapper)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
package eu.kanade.tachiyomi.data.backup.create.creators
|
||||
|
||||
import eu.kanade.tachiyomi.data.backup.models.BackupExtensionStore
|
||||
import eu.kanade.tachiyomi.data.backup.models.backupExtensionStoreMapper
|
||||
import mihon.domain.extension.interactor.GetExtensionStores
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class ExtensionStoresBackupCreator(
|
||||
private val getExtensionStores: GetExtensionStores = Injekt.get(),
|
||||
) {
|
||||
|
||||
suspend operator fun invoke(): List<BackupExtensionStore> {
|
||||
return getExtensionStores.get()
|
||||
.map(backupExtensionStoreMapper)
|
||||
}
|
||||
}
|
||||
|
|
@ -108,12 +108,7 @@ class MangaBackupCreator(
|
|||
if (historyByMangaId.isNotEmpty()) {
|
||||
val history = historyByMangaId.map { history ->
|
||||
val chapter = handler.awaitOne { chaptersQueries.getChapterById(history.chapterId) }
|
||||
BackupHistory(
|
||||
url = chapter.url,
|
||||
lastRead = history.readAt?.time ?: 0L,
|
||||
readDuration = history.readDuration,
|
||||
readPages = history.readPages,
|
||||
)
|
||||
BackupHistory(chapter.url, history.readAt?.time ?: 0L, history.readDuration)
|
||||
}
|
||||
if (history.isNotEmpty()) {
|
||||
mangaObject.history = history
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@ data class Backup(
|
|||
@ProtoNumber(101) var backupSources: List<BackupSource> = emptyList(),
|
||||
@ProtoNumber(104) var backupPreferences: List<BackupPreference> = emptyList(),
|
||||
@ProtoNumber(105) var backupSourcePreferences: List<BackupSourcePreferences> = emptyList(),
|
||||
@ProtoNumber(106) var backupExtensionStores: List<BackupExtensionStore> = emptyList(),
|
||||
@ProtoNumber(106) var backupExtensionRepo: List<BackupExtensionRepos> = emptyList(),
|
||||
// SY specific values
|
||||
@ProtoNumber(600) var backupSavedSearches: List<BackupSavedSearch> = emptyList(),
|
||||
// KMK -->
|
||||
|
|
|
|||
|
|
@ -0,0 +1,24 @@
|
|||
package eu.kanade.tachiyomi.data.backup.models
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.protobuf.ProtoNumber
|
||||
import mihon.domain.extensionrepo.model.ExtensionRepo
|
||||
|
||||
@Serializable
|
||||
class BackupExtensionRepos(
|
||||
@ProtoNumber(1) var baseUrl: String,
|
||||
@ProtoNumber(2) var name: String,
|
||||
@ProtoNumber(3) var shortName: String?,
|
||||
@ProtoNumber(4) var website: String,
|
||||
@ProtoNumber(5) var signingKeyFingerprint: String,
|
||||
)
|
||||
|
||||
val backupExtensionReposMapper = { repo: ExtensionRepo ->
|
||||
BackupExtensionRepos(
|
||||
baseUrl = repo.baseUrl,
|
||||
name = repo.name,
|
||||
shortName = repo.shortName,
|
||||
website = repo.website,
|
||||
signingKeyFingerprint = repo.signingKeyFingerprint,
|
||||
)
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
package eu.kanade.tachiyomi.data.backup.models
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.protobuf.ProtoNumber
|
||||
import mihon.domain.extension.model.ExtensionStore
|
||||
|
||||
@Serializable
|
||||
class BackupExtensionStore(
|
||||
@ProtoNumber(1) var indexUrl: String,
|
||||
@ProtoNumber(2) var name: String,
|
||||
@ProtoNumber(3) var badgeLabel: String?,
|
||||
@ProtoNumber(5) var signingKey: String,
|
||||
@ProtoNumber(4) var contactWebsite: String,
|
||||
@ProtoNumber(6) var contactDiscord: String?,
|
||||
@ProtoNumber(7) var isLegacy: Boolean?,
|
||||
)
|
||||
|
||||
val backupExtensionStoreMapper = { store: ExtensionStore ->
|
||||
BackupExtensionStore(
|
||||
indexUrl = store.indexUrl,
|
||||
name = store.name,
|
||||
badgeLabel = store.badgeLabel,
|
||||
signingKey = store.signingKey,
|
||||
contactWebsite = store.contact.website,
|
||||
contactDiscord = store.contact.discord,
|
||||
isLegacy = store.isLegacy,
|
||||
)
|
||||
}
|
||||
|
|
@ -10,13 +10,11 @@ data class BackupHistory(
|
|||
@ProtoNumber(1) var url: String,
|
||||
@ProtoNumber(2) var lastRead: Long,
|
||||
@ProtoNumber(3) var readDuration: Long = 0,
|
||||
@ProtoNumber(4) var readPages: Long = 0,
|
||||
) {
|
||||
fun getHistoryImpl(): History {
|
||||
return History.create().copy(
|
||||
readAt = Date(lastRead),
|
||||
readDuration = readDuration,
|
||||
readPages = readPages,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,14 +5,14 @@ import android.net.Uri
|
|||
import eu.kanade.tachiyomi.data.backup.BackupDecoder
|
||||
import eu.kanade.tachiyomi.data.backup.BackupNotifier
|
||||
import eu.kanade.tachiyomi.data.backup.models.BackupCategory
|
||||
import eu.kanade.tachiyomi.data.backup.models.BackupExtensionStore
|
||||
import eu.kanade.tachiyomi.data.backup.models.BackupExtensionRepos
|
||||
import eu.kanade.tachiyomi.data.backup.models.BackupFeed
|
||||
import eu.kanade.tachiyomi.data.backup.models.BackupManga
|
||||
import eu.kanade.tachiyomi.data.backup.models.BackupPreference
|
||||
import eu.kanade.tachiyomi.data.backup.models.BackupSavedSearch
|
||||
import eu.kanade.tachiyomi.data.backup.models.BackupSourcePreferences
|
||||
import eu.kanade.tachiyomi.data.backup.restore.restorers.CategoriesRestorer
|
||||
import eu.kanade.tachiyomi.data.backup.restore.restorers.ExtensionStoreRestorer
|
||||
import eu.kanade.tachiyomi.data.backup.restore.restorers.ExtensionRepoRestorer
|
||||
import eu.kanade.tachiyomi.data.backup.restore.restorers.FeedRestorer
|
||||
import eu.kanade.tachiyomi.data.backup.restore.restorers.MangaRestorer
|
||||
import eu.kanade.tachiyomi.data.backup.restore.restorers.PreferenceRestorer
|
||||
|
|
@ -38,7 +38,7 @@ class BackupRestorer(
|
|||
|
||||
private val categoriesRestorer: CategoriesRestorer = CategoriesRestorer(),
|
||||
private val preferenceRestorer: PreferenceRestorer = PreferenceRestorer(context),
|
||||
private val extensionStoreRestorer: ExtensionStoreRestorer = ExtensionStoreRestorer(),
|
||||
private val extensionRepoRestorer: ExtensionRepoRestorer = ExtensionRepoRestorer(),
|
||||
private val mangaRestorer: MangaRestorer = MangaRestorer(isSync),
|
||||
// SY -->
|
||||
private val savedSearchRestorer: SavedSearchRestorer = SavedSearchRestorer(),
|
||||
|
|
@ -96,8 +96,8 @@ class BackupRestorer(
|
|||
if (options.appSettings) {
|
||||
restoreAmount += 1
|
||||
}
|
||||
if (options.extensionStores) {
|
||||
restoreAmount += backup.backupExtensionStores.size
|
||||
if (options.extensionRepoSettings) {
|
||||
restoreAmount += backup.backupExtensionRepo.size
|
||||
}
|
||||
if (options.sourceSettings) {
|
||||
restoreAmount += 1
|
||||
|
|
@ -126,8 +126,8 @@ class BackupRestorer(
|
|||
if (options.libraryEntries) {
|
||||
restoreManga(backup.backupManga, if (options.categories) backup.backupCategories else emptyList())
|
||||
}
|
||||
if (options.extensionStores) {
|
||||
restoreExtensionStores(backup.backupExtensionStores)
|
||||
if (options.extensionRepoSettings) {
|
||||
restoreExtensionRepos(backup.backupExtensionRepo)
|
||||
}
|
||||
|
||||
// TODO: optionally trigger online library + tracker update
|
||||
|
|
@ -248,25 +248,23 @@ class BackupRestorer(
|
|||
}
|
||||
}
|
||||
|
||||
private fun CoroutineScope.restoreExtensionStores(
|
||||
backupExtensionStores: List<BackupExtensionStore>,
|
||||
private fun CoroutineScope.restoreExtensionRepos(
|
||||
backupExtensionRepo: List<BackupExtensionRepos>,
|
||||
) = launch {
|
||||
backupExtensionStores
|
||||
backupExtensionRepo
|
||||
.forEach {
|
||||
ensureActive()
|
||||
|
||||
try {
|
||||
extensionStoreRestorer(it)
|
||||
extensionRepoRestorer(it)
|
||||
} catch (e: Exception) {
|
||||
errors.add(Date() to "Error adding extension store: ${it.name} : ${e.message}")
|
||||
errors.add(Date() to "Error Adding Repo: ${it.name} : ${e.message}")
|
||||
}
|
||||
|
||||
restoreProgress += 1
|
||||
// KMK -->
|
||||
with(notifier) {
|
||||
// KMK <--
|
||||
showRestoreProgress(
|
||||
context.stringResource(MR.strings.extensionStores),
|
||||
context.stringResource(MR.strings.extensionRepo_settings),
|
||||
restoreProgress,
|
||||
restoreAmount,
|
||||
isSync,
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ data class RestoreOptions(
|
|||
val libraryEntries: Boolean = true,
|
||||
val categories: Boolean = true,
|
||||
val appSettings: Boolean = true,
|
||||
val extensionStores: Boolean = true,
|
||||
val extensionRepoSettings: Boolean = true,
|
||||
val sourceSettings: Boolean = true,
|
||||
// SY -->
|
||||
val savedSearchesFeeds: Boolean = true,
|
||||
|
|
@ -20,7 +20,7 @@ data class RestoreOptions(
|
|||
libraryEntries,
|
||||
categories,
|
||||
appSettings,
|
||||
extensionStores,
|
||||
extensionRepoSettings,
|
||||
sourceSettings,
|
||||
// SY -->
|
||||
savedSearchesFeeds,
|
||||
|
|
@ -31,7 +31,7 @@ data class RestoreOptions(
|
|||
libraryEntries ||
|
||||
categories ||
|
||||
appSettings ||
|
||||
extensionStores ||
|
||||
extensionRepoSettings ||
|
||||
sourceSettings /* SY --> */ ||
|
||||
savedSearchesFeeds /* SY <-- */
|
||||
|
||||
|
|
@ -53,9 +53,9 @@ data class RestoreOptions(
|
|||
setter = { options, enabled -> options.copy(appSettings = enabled) },
|
||||
),
|
||||
Entry(
|
||||
label = MR.strings.extensionStores,
|
||||
getter = RestoreOptions::extensionStores,
|
||||
setter = { options, enabled -> options.copy(extensionStores = enabled) },
|
||||
label = MR.strings.extensionRepo_settings,
|
||||
getter = RestoreOptions::extensionRepoSettings,
|
||||
setter = { options, enabled -> options.copy(extensionRepoSettings = enabled) },
|
||||
),
|
||||
Entry(
|
||||
label = MR.strings.source_settings,
|
||||
|
|
@ -77,7 +77,7 @@ data class RestoreOptions(
|
|||
libraryEntries = array[0],
|
||||
categories = array[1],
|
||||
appSettings = array[2],
|
||||
extensionStores = array[3],
|
||||
extensionRepoSettings = array[3],
|
||||
sourceSettings = array[4],
|
||||
// SY -->
|
||||
savedSearchesFeeds = array[5],
|
||||
|
|
|
|||
|
|
@ -0,0 +1,40 @@
|
|||
package eu.kanade.tachiyomi.data.backup.restore.restorers
|
||||
|
||||
import eu.kanade.tachiyomi.data.backup.models.BackupExtensionRepos
|
||||
import mihon.domain.extensionrepo.interactor.GetExtensionRepo
|
||||
import tachiyomi.data.DatabaseHandler
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class ExtensionRepoRestorer(
|
||||
private val handler: DatabaseHandler = Injekt.get(),
|
||||
private val getExtensionRepos: GetExtensionRepo = Injekt.get(),
|
||||
) {
|
||||
|
||||
suspend operator fun invoke(
|
||||
backupRepo: BackupExtensionRepos,
|
||||
) {
|
||||
val dbRepos = getExtensionRepos.getAll()
|
||||
val existingReposBySHA = dbRepos.associateBy { it.signingKeyFingerprint }
|
||||
val existingReposByUrl = dbRepos.associateBy { it.baseUrl }
|
||||
|
||||
val urlExists = existingReposByUrl[backupRepo.baseUrl]
|
||||
val shaExists = existingReposBySHA[backupRepo.signingKeyFingerprint]
|
||||
|
||||
if (urlExists != null && urlExists.signingKeyFingerprint != backupRepo.signingKeyFingerprint) {
|
||||
error("Already Exists with different signing key fingerprint")
|
||||
} else if (shaExists != null) {
|
||||
error("${shaExists.name} has the same signing key fingerprint")
|
||||
} else {
|
||||
handler.await {
|
||||
extension_reposQueries.insert(
|
||||
backupRepo.baseUrl,
|
||||
backupRepo.name,
|
||||
backupRepo.shortName,
|
||||
backupRepo.website,
|
||||
backupRepo.signingKeyFingerprint,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
package eu.kanade.tachiyomi.data.backup.restore.restorers
|
||||
|
||||
import eu.kanade.tachiyomi.data.backup.models.BackupExtensionStore
|
||||
import tachiyomi.data.Database
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class ExtensionStoreRestorer(
|
||||
private val database: Database = Injekt.get(),
|
||||
) {
|
||||
|
||||
suspend operator fun invoke(
|
||||
backupStore: BackupExtensionStore,
|
||||
) {
|
||||
// KMK -->
|
||||
val indexUrl = if (backupStore.isLegacy == null) {
|
||||
backupStore.indexUrl.removeSuffix("/index.min.json").removeSuffix("/index.json") + "/repo.json"
|
||||
} else {
|
||||
backupStore.indexUrl
|
||||
}
|
||||
// KMK <--
|
||||
database.extension_storeQueries.upsert(
|
||||
indexUrl = indexUrl,
|
||||
name = backupStore.name,
|
||||
badgeLabel = backupStore.badgeLabel ?: backupStore.name,
|
||||
signingKey = backupStore.signingKey,
|
||||
contactWebsite = backupStore.contactWebsite,
|
||||
contactDiscord = backupStore.contactDiscord,
|
||||
isLegacy = backupStore.isLegacy ?: true,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -427,7 +427,6 @@ class MangaRestorer(
|
|||
.takeIf { it > 0L }
|
||||
?.let { Date(it) },
|
||||
readDuration = max(item.readDuration, dbHistory.time_read) - dbHistory.time_read,
|
||||
readPages = max(item.readPages, dbHistory.pages_read) - dbHistory.pages_read,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -438,7 +437,6 @@ class MangaRestorer(
|
|||
it.chapterId,
|
||||
it.readAt,
|
||||
it.readDuration,
|
||||
it.readPages,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,7 +58,6 @@ import kotlinx.coroutines.sync.Semaphore
|
|||
import kotlinx.coroutines.sync.withPermit
|
||||
import logcat.LogPriority
|
||||
import mihon.domain.chapter.interactor.FilterChaptersForDownload
|
||||
import mihon.domain.chapter.interactor.GetChaptersForLibraryUpdateDownload
|
||||
import tachiyomi.core.common.i18n.stringResource
|
||||
import tachiyomi.core.common.preference.getAndSet
|
||||
import tachiyomi.core.common.util.lang.withIOContext
|
||||
|
|
@ -120,7 +119,6 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
|||
private val syncChaptersWithSource: SyncChaptersWithSource = Injekt.get()
|
||||
private val fetchInterval: FetchInterval = Injekt.get()
|
||||
private val filterChaptersForDownload: FilterChaptersForDownload = Injekt.get()
|
||||
private val getChaptersForLibraryUpdateDownload: GetChaptersForLibraryUpdateDownload = Injekt.get()
|
||||
|
||||
// SY -->
|
||||
private val getFavorites: GetFavorites = Injekt.get()
|
||||
|
|
@ -467,12 +465,6 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
|||
// Convert to the manga that contains new chapters
|
||||
newUpdates.add(manga to newChapters.toTypedArray())
|
||||
}
|
||||
|
||||
val libraryUpdateChapters = getChaptersForLibraryUpdateDownload.await(manga)
|
||||
if (libraryUpdateChapters.isNotEmpty()) {
|
||||
downloadChapters(manga, libraryUpdateChapters)
|
||||
hasDownloads.store(true)
|
||||
}
|
||||
clearErrorFromDB(mangaId = manga.id)
|
||||
} catch (e: Throwable) {
|
||||
val errorMessage = when (e) {
|
||||
|
|
@ -501,10 +493,9 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
|||
|
||||
if (newUpdates.isNotEmpty()) {
|
||||
notifier.showUpdateNotifications(newUpdates)
|
||||
}
|
||||
|
||||
if (hasDownloads.load()) {
|
||||
downloadManager.startDownloads()
|
||||
if (hasDownloads.load()) {
|
||||
downloadManager.startDownloads()
|
||||
}
|
||||
}
|
||||
|
||||
if (failedUpdates.isNotEmpty()) {
|
||||
|
|
@ -757,6 +748,10 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
|||
private const val KEY_MANGA_IDS = "manga_ids"
|
||||
// KMK <--
|
||||
|
||||
fun cancelAllWorks(context: Context) {
|
||||
context.workManager.cancelAllWorkByTag(TAG)
|
||||
}
|
||||
|
||||
fun setupTask(
|
||||
context: Context,
|
||||
prefInterval: Int? = null,
|
||||
|
|
@ -770,19 +765,16 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
|||
} else {
|
||||
NetworkType.CONNECTED
|
||||
}
|
||||
val networkRequest = NetworkRequest.Builder().apply {
|
||||
removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
|
||||
if (DEVICE_ONLY_ON_WIFI in restrictions) {
|
||||
addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
|
||||
}
|
||||
if (DEVICE_NETWORK_NOT_METERED in restrictions) {
|
||||
addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
|
||||
}
|
||||
val networkRequestBuilder = NetworkRequest.Builder()
|
||||
if (DEVICE_ONLY_ON_WIFI in restrictions) {
|
||||
networkRequestBuilder.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
|
||||
}
|
||||
if (DEVICE_NETWORK_NOT_METERED in restrictions) {
|
||||
networkRequestBuilder.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
|
||||
}
|
||||
.build()
|
||||
val constraints = Constraints.Builder()
|
||||
// 'networkRequest' only applies to Android 9+, otherwise 'networkType' is used
|
||||
.setRequiredNetworkRequest(networkRequest, networkType)
|
||||
.setRequiredNetworkRequest(networkRequestBuilder.build(), networkType)
|
||||
.setRequiresCharging(DEVICE_CHARGING in restrictions)
|
||||
.setRequiresBatteryNotLow(true)
|
||||
.build()
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ class SyncManager(
|
|||
SYNCYOMI(1),
|
||||
GOOGLE_DRIVE(2),
|
||||
// KMK -->
|
||||
WEB_DAV(3),
|
||||
WebDAV(3),
|
||||
// KMK <--
|
||||
;
|
||||
|
||||
|
|
@ -88,7 +88,7 @@ class SyncManager(
|
|||
chapters = syncOptions.chapters,
|
||||
tracking = syncOptions.tracking,
|
||||
history = syncOptions.history,
|
||||
extensionStores = syncOptions.extensionStores,
|
||||
extensionRepoSettings = syncOptions.extensionRepoSettings,
|
||||
appSettings = syncOptions.appSettings,
|
||||
sourceSettings = syncOptions.sourceSettings,
|
||||
privateSettings = syncOptions.privateSettings,
|
||||
|
|
@ -107,8 +107,8 @@ class SyncManager(
|
|||
backupCategories = backupCreator.backupCategories(backupOptions),
|
||||
backupSources = backupCreator.backupSources(backupManga),
|
||||
backupPreferences = backupCreator.backupAppPreferences(backupOptions),
|
||||
backupExtensionRepo = backupCreator.backupExtensionRepos(backupOptions),
|
||||
backupSourcePreferences = backupCreator.backupSourcePreferences(backupOptions),
|
||||
backupExtensionStores = backupCreator.backupExtensionStores(backupOptions),
|
||||
|
||||
// SY -->
|
||||
backupSavedSearches = backupCreator.backupSavedSearches(backupOptions),
|
||||
|
|
@ -142,7 +142,7 @@ class SyncManager(
|
|||
}
|
||||
|
||||
// KMK -->
|
||||
SyncService.WEB_DAV -> {
|
||||
SyncService.WebDAV -> {
|
||||
WebDavSyncService(context, json, syncPreferences, notifier)
|
||||
}
|
||||
// KMK <--
|
||||
|
|
@ -192,7 +192,7 @@ class SyncManager(
|
|||
backupSources = remoteBackup.backupSources,
|
||||
backupPreferences = remoteBackup.backupPreferences,
|
||||
backupSourcePreferences = remoteBackup.backupSourcePreferences,
|
||||
backupExtensionStores = remoteBackup.backupExtensionStores,
|
||||
backupExtensionRepo = remoteBackup.backupExtensionRepo,
|
||||
|
||||
// SY -->
|
||||
backupSavedSearches = remoteBackup.backupSavedSearches,
|
||||
|
|
@ -222,7 +222,7 @@ class SyncManager(
|
|||
appSettings = true,
|
||||
sourceSettings = true,
|
||||
libraryEntries = true,
|
||||
extensionStores = true,
|
||||
extensionRepoSettings = true,
|
||||
),
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -5,14 +5,8 @@ import eu.kanade.domain.sync.SyncPreferences
|
|||
import eu.kanade.tachiyomi.data.backup.models.Backup
|
||||
import eu.kanade.tachiyomi.data.sync.SyncNotifier
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.network.PUT
|
||||
import eu.kanade.tachiyomi.network.await
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.coroutines.NonCancellable
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.SerializationException
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.protobuf.ProtoBuf
|
||||
|
|
@ -27,6 +21,7 @@ import tachiyomi.core.common.i18n.stringResource
|
|||
import tachiyomi.i18n.MR
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class SyncYomiSyncService(
|
||||
context: Context,
|
||||
|
|
@ -37,34 +32,9 @@ class SyncYomiSyncService(
|
|||
private val protoBuf: ProtoBuf = Injekt.get(),
|
||||
) : SyncService(context, json, syncPreferences) {
|
||||
|
||||
// KMK -->
|
||||
companion object {
|
||||
private val client by lazy { OkHttpClient() }
|
||||
}
|
||||
// KMK <--
|
||||
|
||||
private class SyncYomiException(message: String?) : Exception(message)
|
||||
|
||||
@Serializable
|
||||
private data class SyncEvent(
|
||||
val event: SyncEventStatus,
|
||||
@SerialName("device_name")
|
||||
val deviceName: String? = null,
|
||||
val message: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
private enum class SyncEventStatus {
|
||||
SYNC_STARTED,
|
||||
SYNC_SUCCESS,
|
||||
SYNC_FAILED,
|
||||
SYNC_ERROR,
|
||||
SYNC_CANCELLED,
|
||||
}
|
||||
|
||||
override suspend fun doSync(syncData: SyncData): Backup? {
|
||||
reportSyncEvent(SyncEventStatus.SYNC_STARTED)
|
||||
|
||||
try {
|
||||
val (remoteData, etag) = pullSyncData()
|
||||
|
||||
|
|
@ -82,26 +52,11 @@ class SyncYomiSyncService(
|
|||
syncData
|
||||
}
|
||||
|
||||
val success = pushSyncData(finalSyncData, etag)
|
||||
|
||||
if (success) {
|
||||
reportSyncEvent(SyncEventStatus.SYNC_SUCCESS)
|
||||
} else {
|
||||
reportSyncEvent(SyncEventStatus.SYNC_FAILED, "Failed to push sync data")
|
||||
// KMK -->
|
||||
return null
|
||||
// KMK <--
|
||||
}
|
||||
|
||||
pushSyncData(finalSyncData, etag)
|
||||
return finalSyncData.backup
|
||||
} catch (e: Exception) {
|
||||
if (e is CancellationException) {
|
||||
reportSyncEvent(SyncEventStatus.SYNC_CANCELLED, e.message)
|
||||
throw e
|
||||
}
|
||||
logcat(LogPriority.ERROR) { "Error syncing: ${e.message}" }
|
||||
notifier.showSyncError(e.message)
|
||||
reportSyncEvent(SyncEventStatus.SYNC_ERROR, e.message)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
|
@ -123,6 +78,7 @@ class SyncYomiSyncService(
|
|||
headers = headers,
|
||||
)
|
||||
|
||||
val client = OkHttpClient()
|
||||
val response = client.newCall(downloadRequest).await()
|
||||
|
||||
if (response.code == HttpStatus.SC_NOT_MODIFIED) {
|
||||
|
|
@ -167,12 +123,13 @@ class SyncYomiSyncService(
|
|||
/**
|
||||
* Return true if update success
|
||||
*/
|
||||
private suspend fun pushSyncData(syncData: SyncData, eTag: String): Boolean {
|
||||
val backup = syncData.backup ?: return true
|
||||
private suspend fun pushSyncData(syncData: SyncData, eTag: String) {
|
||||
val backup = syncData.backup ?: return
|
||||
|
||||
val host = syncPreferences.clientHost().get()
|
||||
val apiKey = syncPreferences.clientAPIKey().get()
|
||||
val uploadUrl = "$host/api/sync/content"
|
||||
val timeout = 30L
|
||||
|
||||
val headersBuilder = Headers.Builder().add("X-API-Token", apiKey)
|
||||
if (eTag.isNotEmpty()) {
|
||||
|
|
@ -180,6 +137,13 @@ class SyncYomiSyncService(
|
|||
}
|
||||
val headers = headersBuilder.build()
|
||||
|
||||
// Set timeout to 30 seconds
|
||||
val client = OkHttpClient.Builder()
|
||||
.connectTimeout(timeout, TimeUnit.SECONDS)
|
||||
.readTimeout(timeout, TimeUnit.SECONDS)
|
||||
.writeTimeout(timeout, TimeUnit.SECONDS)
|
||||
.build()
|
||||
|
||||
val byteArray = protoBuf.encodeToByteArray(Backup.serializer(), backup)
|
||||
if (byteArray.isEmpty()) {
|
||||
throw IllegalStateException(context.stringResource(MR.strings.empty_backup_error))
|
||||
|
|
@ -192,58 +156,20 @@ class SyncYomiSyncService(
|
|||
body = body,
|
||||
)
|
||||
|
||||
client.newCall(uploadRequest).await()
|
||||
// KMK -->
|
||||
.use { response ->
|
||||
// KMK <--
|
||||
if (response.isSuccessful) {
|
||||
val newETag = response.headers["ETag"]
|
||||
.takeIf { it?.isNotEmpty() == true } ?: throw SyncYomiException("Missing ETag")
|
||||
syncPreferences.lastSyncEtag().set(newETag)
|
||||
logcat(LogPriority.DEBUG) { "SyncYomi sync completed" }
|
||||
return true
|
||||
} else if (response.code == HttpStatus.SC_PRECONDITION_FAILED) {
|
||||
// other clients updated remote data, will try next time
|
||||
logcat(LogPriority.DEBUG) { "SyncYomi sync failed with 412" }
|
||||
return false
|
||||
} else {
|
||||
val responseBody = response.body.string()
|
||||
notifier.showSyncError("Failed to upload sync data: $responseBody")
|
||||
logcat(LogPriority.ERROR) { "SyncError: $responseBody" }
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
val response = client.newCall(uploadRequest).await()
|
||||
|
||||
private suspend fun reportSyncEvent(event: SyncEventStatus, message: String? = null) {
|
||||
withContext(NonCancellable) {
|
||||
try {
|
||||
val host = syncPreferences.clientHost().get()
|
||||
val apiKey = syncPreferences.clientAPIKey().get()
|
||||
val url = "$host/api/sync/event"
|
||||
|
||||
val headersBuilder = Headers.Builder().add("X-API-Token", apiKey)
|
||||
val headers = headersBuilder.build()
|
||||
|
||||
val bodyObj = SyncEvent(
|
||||
event = event,
|
||||
deviceName = android.os.Build.MODEL,
|
||||
message = message,
|
||||
)
|
||||
|
||||
val jsonBody = json.encodeToString(SyncEvent.serializer(), bodyObj)
|
||||
val requestBody = jsonBody.toRequestBody("application/json; charset=utf-8".toMediaType())
|
||||
|
||||
val request = POST(
|
||||
url = url,
|
||||
headers = headers,
|
||||
body = requestBody,
|
||||
)
|
||||
|
||||
client.newCall(request).await().close()
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.ERROR) { "Failed to report sync event: ${e.message}" }
|
||||
}
|
||||
if (response.isSuccessful) {
|
||||
val newETag = response.headers["ETag"]
|
||||
.takeIf { it?.isNotEmpty() == true } ?: throw SyncYomiException("Missing ETag")
|
||||
syncPreferences.lastSyncEtag().set(newETag)
|
||||
logcat(LogPriority.DEBUG) { "SyncYomi sync completed" }
|
||||
} else if (response.code == HttpStatus.SC_PRECONDITION_FAILED) {
|
||||
// other clients updated remote data, will try next time
|
||||
logcat(LogPriority.DEBUG) { "SyncYomi sync failed with 412" }
|
||||
} else {
|
||||
val responseBody = response.body.string()
|
||||
notifier.showSyncError("Failed to upload sync data: $responseBody")
|
||||
logcat(LogPriority.ERROR) { "SyncError: $responseBody" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ class WebDavSyncService(
|
|||
private val url: String = syncPreferences.webDavUrl().get().trim()
|
||||
private val folder: String = syncPreferences.webDavFolder().get().trim('/')
|
||||
private val username: String = syncPreferences.webDavUsername().get().trim()
|
||||
private val password: String = syncPreferences.webDavPassword().get()
|
||||
private val password: String = syncPreferences.webDavPassword().get().trim()
|
||||
private val credentials: String = Credentials.basic(username, password)
|
||||
|
||||
private fun buildWebDavFileUrl(fileName: String = "backup.proto"): String {
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import androidx.core.net.toUri
|
|||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.track.anilist.dto.ALAddMangaResult
|
||||
import eu.kanade.tachiyomi.data.track.anilist.dto.ALCurrentUserResult
|
||||
import eu.kanade.tachiyomi.data.track.anilist.dto.ALError
|
||||
import eu.kanade.tachiyomi.data.track.anilist.dto.ALIdSearchResult
|
||||
import eu.kanade.tachiyomi.data.track.anilist.dto.ALMangaMetadata
|
||||
import eu.kanade.tachiyomi.data.track.anilist.dto.ALOAuth
|
||||
|
|
@ -27,7 +26,6 @@ import kotlinx.serialization.json.put
|
|||
import kotlinx.serialization.json.putJsonObject
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import okhttp3.Response
|
||||
import tachiyomi.core.common.util.lang.withIOContext
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.time.Instant
|
||||
|
|
@ -45,25 +43,6 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
|||
.rateLimit(permits = 85, period = 1.minutes)
|
||||
.build()
|
||||
|
||||
// KMK -->
|
||||
private fun Response.parseALError() {
|
||||
val bodyString = peekBody(1024 * 1024).string()
|
||||
val errorObj = try {
|
||||
json.decodeFromString<ALError>(bodyString)
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
|
||||
errorObj?.errors?.firstOrNull()?.let {
|
||||
val msg = it.message
|
||||
if (msg.contains("Invalid token") || it.status == 401) {
|
||||
throw Exception("AniList token expired, please login again")
|
||||
}
|
||||
throw Exception(msg)
|
||||
}
|
||||
}
|
||||
// KMK <--
|
||||
|
||||
suspend fun addLibManga(track: Track): Track {
|
||||
return withIOContext {
|
||||
val query = $$"""
|
||||
|
|
@ -92,9 +71,6 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
|||
),
|
||||
)
|
||||
.awaitSuccess()
|
||||
// KMK -->
|
||||
.also { it.parseALError() }
|
||||
// KMK <--
|
||||
.parseAs<ALAddMangaResult>()
|
||||
.let {
|
||||
track.library_id = it.data.entry.id
|
||||
|
|
@ -136,9 +112,6 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
|||
}
|
||||
authClient.newCall(POST(API_URL, body = payload.toString().toRequestBody(jsonMime)))
|
||||
.awaitSuccess()
|
||||
// KMK -->
|
||||
.use { it.parseALError() }
|
||||
// KMK <--
|
||||
track
|
||||
}
|
||||
}
|
||||
|
|
@ -161,9 +134,6 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
|||
}
|
||||
authClient.newCall(POST(API_URL, body = payload.toString().toRequestBody(jsonMime)))
|
||||
.awaitSuccess()
|
||||
// KMK -->
|
||||
.use { it.parseALError() }
|
||||
// KMK <--
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -222,9 +192,6 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
|||
),
|
||||
)
|
||||
.awaitSuccess()
|
||||
// KMK -->
|
||||
.also { it.parseALError() }
|
||||
// KMK <--
|
||||
.parseAs<ALSearchResult>()
|
||||
.data.page.media
|
||||
.map { it.toALManga().toTrack() }
|
||||
|
|
@ -304,9 +271,6 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
|||
),
|
||||
)
|
||||
.awaitSuccess()
|
||||
// KMK -->
|
||||
.also { it.parseALError() }
|
||||
// KMK <--
|
||||
.parseAs<ALUserListMangaQueryResult>()
|
||||
.data.page.mediaList
|
||||
.map { it.toALUserManga() }
|
||||
|
|
@ -348,9 +312,6 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
|||
),
|
||||
)
|
||||
.awaitSuccess()
|
||||
// KMK -->
|
||||
.also { it.parseALError() }
|
||||
// KMK <--
|
||||
.parseAs<ALCurrentUserResult>()
|
||||
.let {
|
||||
val viewer = it.data.viewer
|
||||
|
|
@ -362,9 +323,9 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
|||
|
||||
suspend fun getMangaMetadata(track: DomainTrack): TrackMangaMetadata {
|
||||
return withIOContext {
|
||||
val query = $$"""
|
||||
|query ($mangaId: Int!) {
|
||||
|Media (id: $mangaId) {
|
||||
val query = """
|
||||
|query (${'$'}mangaId: Int!) {
|
||||
|Media (id: ${'$'}mangaId) {
|
||||
|id
|
||||
|title {
|
||||
|userPreferred
|
||||
|
|
@ -404,12 +365,9 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
|||
),
|
||||
)
|
||||
.awaitSuccess()
|
||||
// KMK -->
|
||||
.also { it.parseALError() }
|
||||
// KMK <--
|
||||
.parseAs<ALMangaMetadata>()
|
||||
.let { metadata ->
|
||||
val media = metadata.data.media
|
||||
.let {
|
||||
val media = it.data.media
|
||||
TrackMangaMetadata(
|
||||
remoteId = media.id,
|
||||
title = media.title.userPreferred,
|
||||
|
|
@ -434,9 +392,9 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
|||
// SY -->
|
||||
suspend fun searchById(id: String): TrackSearch {
|
||||
return withIOContext {
|
||||
val query = $$"""
|
||||
|query ($mangaId: Int!) {
|
||||
|Media (id: $mangaId) {
|
||||
val query = """
|
||||
|query (${'$'}mangaId: Int!) {
|
||||
|Media (id: ${'$'}mangaId) {
|
||||
|id
|
||||
|title {
|
||||
|userPreferred
|
||||
|
|
@ -472,9 +430,6 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
|||
),
|
||||
)
|
||||
.awaitSuccess()
|
||||
// KMK -->
|
||||
.also { it.parseALError() }
|
||||
// KMK <--
|
||||
.parseAs<ALIdSearchResult>()
|
||||
.data.media
|
||||
.toALManga()
|
||||
|
|
|
|||
|
|
@ -1,14 +0,0 @@
|
|||
package eu.kanade.tachiyomi.data.track.anilist.dto
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class ALError(
|
||||
val errors: List<ALErrorItem>? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ALErrorItem(
|
||||
val message: String,
|
||||
val status: Int? = null,
|
||||
)
|
||||
|
|
@ -14,9 +14,7 @@ import eu.kanade.tachiyomi.data.track.myanimelist.dto.MALSearchResult
|
|||
import eu.kanade.tachiyomi.data.track.myanimelist.dto.MALUser
|
||||
import eu.kanade.tachiyomi.network.DELETE
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.HttpException
|
||||
import eu.kanade.tachiyomi.network.POST
|
||||
import eu.kanade.tachiyomi.network.await
|
||||
import eu.kanade.tachiyomi.network.awaitSuccess
|
||||
import eu.kanade.tachiyomi.network.parseAs
|
||||
import eu.kanade.tachiyomi.util.PkceUtil
|
||||
|
|
@ -126,23 +124,8 @@ class MyAnimeListApi(
|
|||
.put(formBodyBuilder.build())
|
||||
.build()
|
||||
with(json) {
|
||||
val response = authClient
|
||||
.newCall(request)
|
||||
.await()
|
||||
|
||||
if (!response.isSuccessful) {
|
||||
if (response.body.string().contains("invalid_content")) {
|
||||
// MAL returns unapproved titles in search but does not allow adding them to the list
|
||||
// returns 400 with this body: {"message":"Invalid content","error":"invalid_content"}
|
||||
// These unapproved titles cannot be filtered out in search and are also returned by the
|
||||
// endpoint we use for id prefix search
|
||||
throw MALTitleNotApproved()
|
||||
} else {
|
||||
throw HttpException(response.code)
|
||||
}
|
||||
}
|
||||
|
||||
response
|
||||
authClient.newCall(request)
|
||||
.awaitSuccess()
|
||||
.parseAs<MALListItemStatus>()
|
||||
.let { parseMangaItem(it, track) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -80,6 +80,5 @@ class MyAnimeListInterceptor(private val myanimelist: MyAnimeList) : Interceptor
|
|||
}
|
||||
}
|
||||
|
||||
class MALTitleNotApproved : IOException("MAL: This title can't be added because it is waiting for approval.")
|
||||
class MALTokenRefreshFailed : IOException("MAL: Failed to refresh account token")
|
||||
class MALTokenExpired : IOException("MAL: Login has expired")
|
||||
|
|
|
|||
|
|
@ -93,17 +93,13 @@ class AppUpdateChecker(
|
|||
// KMK <--
|
||||
}
|
||||
|
||||
private const val RELEASES_WEB_BASE_URL = "https://192.168.1.155:3030"
|
||||
private const val FORK_RELEASE_REPO = "copilot/komikku"
|
||||
private const val FORK_PREVIEW_RELEASE_REPO = "copilot/komikku-preview"
|
||||
|
||||
val GITHUB_REPO: String by lazy { getGithubRepo() }
|
||||
|
||||
fun getGithubRepo(peekIntoPreview: Boolean = false): String =
|
||||
if (isPreviewBuildType || peekIntoPreview) {
|
||||
FORK_PREVIEW_RELEASE_REPO
|
||||
"komikku-app/komikku-preview"
|
||||
} else {
|
||||
FORK_RELEASE_REPO
|
||||
"komikku-app/komikku"
|
||||
}
|
||||
|
||||
val RELEASE_TAG: String by lazy { getReleaseTag() }
|
||||
|
|
@ -115,4 +111,4 @@ fun getReleaseTag(peekIntoPreview: Boolean = false): String =
|
|||
"v${BuildConfig.VERSION_NAME}"
|
||||
}
|
||||
|
||||
val RELEASE_URL = "$RELEASES_WEB_BASE_URL/$GITHUB_REPO/releases/tag/$RELEASE_TAG"
|
||||
val RELEASE_URL = "https://github.com/$GITHUB_REPO/releases/tag/$RELEASE_TAG"
|
||||
|
|
|
|||
|
|
@ -128,8 +128,6 @@ class AppModule(val app: Application) : InjektModule {
|
|||
Json {
|
||||
ignoreUnknownKeys = true
|
||||
explicitNulls = false
|
||||
// Gitea/Forgejo often uses JSON null for empty release body or assets array.
|
||||
coerceInputValues = true
|
||||
}
|
||||
}
|
||||
addSingletonFactory {
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ import tachiyomi.domain.backup.service.BackupPreferences
|
|||
import tachiyomi.domain.download.service.DownloadPreferences
|
||||
import tachiyomi.domain.history.service.HistoryPreferences
|
||||
import tachiyomi.domain.library.service.LibraryPreferences
|
||||
import tachiyomi.domain.reading.service.ReadingEtaPreferences
|
||||
import tachiyomi.domain.storage.service.StoragePreferences
|
||||
import tachiyomi.domain.updates.service.UpdatesPreferences
|
||||
import uy.kohesive.injekt.api.InjektModule
|
||||
|
|
@ -62,9 +61,6 @@ class PreferenceModule(val app: Application) : InjektModule {
|
|||
addSingletonFactory {
|
||||
ReaderPreferences(get())
|
||||
}
|
||||
addSingletonFactory {
|
||||
ReadingEtaPreferences(get())
|
||||
}
|
||||
addSingletonFactory {
|
||||
TrackPreferences(get())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -200,7 +200,7 @@ class ExtensionManager(
|
|||
availableExtensionMapFlow.value = extensions.associateBy {
|
||||
it.pkgName +
|
||||
// KMK -->
|
||||
"_${it.signatureHash}"
|
||||
":${it.signatureHash}"
|
||||
// KMK <--
|
||||
}
|
||||
updatedInstalledExtensionsStatuses(extensions)
|
||||
|
|
@ -273,14 +273,14 @@ class ExtensionManager(
|
|||
// Ext found: Update installed extensions with new information from repo
|
||||
// Also clear isObsolete and set new repo Name if needed
|
||||
val hasUpdate = extension.updateExists(availableExt)
|
||||
// KMK -->
|
||||
installedExtensionsMap[pkgName] = extension.copy(
|
||||
hasUpdate = hasUpdate,
|
||||
store = availableExt.store,
|
||||
repoUrl = availableExt.repoUrl,
|
||||
// KMK -->
|
||||
isObsolete = false,
|
||||
storeName = extension.storeName ?: availableExt.storeName,
|
||||
repoName = extension.repoName ?: availableExt.repoName,
|
||||
// KMK <--
|
||||
)
|
||||
// KMK <--
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
|
|
@ -298,7 +298,7 @@ class ExtensionManager(
|
|||
* @param extension The extension to be installed.
|
||||
*/
|
||||
fun installExtension(extension: Extension.Available): Flow<InstallStep> {
|
||||
return installer.downloadAndInstall(extension.apkUrl, extension)
|
||||
return installer.downloadAndInstall(api.getApkUrl(extension), extension)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -312,7 +312,7 @@ class ExtensionManager(
|
|||
val availableExt = availableExtensionMapFlow.value[
|
||||
extension.pkgName +
|
||||
// KMK -->
|
||||
"_${extension.signatureHash}",
|
||||
":${extension.signatureHash}",
|
||||
// KMK <--
|
||||
] ?: return emptyFlow()
|
||||
return installExtension(availableExt)
|
||||
|
|
@ -322,7 +322,7 @@ class ExtensionManager(
|
|||
installer.cancelInstall(
|
||||
extension.pkgName +
|
||||
// KMK -->
|
||||
"_${extension.signatureHash}",
|
||||
":${extension.signatureHash}",
|
||||
// KMK <--
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,13 +6,24 @@ import eu.kanade.tachiyomi.extension.ExtensionManager
|
|||
import eu.kanade.tachiyomi.extension.model.Extension
|
||||
import eu.kanade.tachiyomi.extension.model.LoadResult
|
||||
import eu.kanade.tachiyomi.extension.util.ExtensionLoader
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
import eu.kanade.tachiyomi.network.NetworkHelper
|
||||
import eu.kanade.tachiyomi.network.awaitSuccess
|
||||
import eu.kanade.tachiyomi.network.parseAs
|
||||
import exh.source.BlacklistedSources
|
||||
import exh.source.ExhPreferences
|
||||
import mihon.domain.extension.interactor.UpdateExtensionStores
|
||||
import mihon.domain.extension.repository.ExtensionStoreRepository
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.awaitAll
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
import logcat.LogPriority
|
||||
import mihon.domain.extensionrepo.interactor.GetExtensionRepo
|
||||
import mihon.domain.extensionrepo.interactor.UpdateExtensionRepo
|
||||
import mihon.domain.extensionrepo.model.ExtensionRepo
|
||||
import tachiyomi.core.common.preference.Preference
|
||||
import tachiyomi.core.common.preference.PreferenceStore
|
||||
import tachiyomi.core.common.util.lang.withIOContext
|
||||
import tachiyomi.core.common.util.system.logcat
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
|
|
@ -21,16 +32,19 @@ import kotlin.time.Duration.Companion.days
|
|||
|
||||
internal class ExtensionApi {
|
||||
|
||||
private val repository: ExtensionStoreRepository by injectLazy()
|
||||
|
||||
private val networkService: NetworkHelper by injectLazy()
|
||||
private val preferenceStore: PreferenceStore by injectLazy()
|
||||
private val updateExtensionStores: UpdateExtensionStores by injectLazy()
|
||||
private val getExtensionRepo: GetExtensionRepo by injectLazy()
|
||||
private val updateExtensionRepo: UpdateExtensionRepo by injectLazy()
|
||||
private val extensionManager: ExtensionManager by injectLazy()
|
||||
|
||||
// SY -->
|
||||
private val sourcePreferences: SourcePreferences by injectLazy()
|
||||
|
||||
// SY <--
|
||||
|
||||
private val json: Json by injectLazy()
|
||||
|
||||
private val lastExtCheck: Preference<Long> by lazy {
|
||||
preferenceStore.getLong(Preference.appStateKey("last_ext_check"), 0)
|
||||
}
|
||||
|
|
@ -40,11 +54,37 @@ internal class ExtensionApi {
|
|||
val disabledRepos = sourcePreferences.disabledRepos().get()
|
||||
// KMK <--
|
||||
return withIOContext {
|
||||
repository.fetchExtensions(
|
||||
getExtensionRepo.getAll()
|
||||
// KMK -->
|
||||
disabledRepos,
|
||||
.filterNot { it.baseUrl in disabledRepos }
|
||||
// KMK <--
|
||||
)
|
||||
.map { async { getExtensions(it) } }
|
||||
.awaitAll()
|
||||
.flatten()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getExtensions(extRepo: ExtensionRepo): List<Extension.Available> {
|
||||
val repoBaseUrl = extRepo.baseUrl
|
||||
return try {
|
||||
val response = networkService.client
|
||||
.newCall(GET("$repoBaseUrl/index.min.json"))
|
||||
.awaitSuccess()
|
||||
|
||||
with(json) {
|
||||
response
|
||||
.parseAs<List<ExtensionJsonObject>>()
|
||||
.toExtensions(
|
||||
repoBaseUrl,
|
||||
// KMK -->
|
||||
signature = extRepo.signingKeyFingerprint,
|
||||
repoName = extRepo.shortName ?: extRepo.name,
|
||||
// KMK <--
|
||||
)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
logcat(LogPriority.ERROR, e) { "Failed to get extensions from $repoBaseUrl" }
|
||||
emptyList()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -59,7 +99,8 @@ internal class ExtensionApi {
|
|||
return null
|
||||
}
|
||||
|
||||
updateExtensionStores()
|
||||
// Update extension repo details
|
||||
updateExtensionRepo.awaitAll()
|
||||
|
||||
val extensions = if (fromAvailableExtensionList) {
|
||||
extensionManager.availableExtensionsFlow.value
|
||||
|
|
@ -97,6 +138,47 @@ internal class ExtensionApi {
|
|||
return extensionsWithUpdate
|
||||
}
|
||||
|
||||
private fun List<ExtensionJsonObject>.toExtensions(
|
||||
repoUrl: String,
|
||||
// KMK -->
|
||||
signature: String,
|
||||
repoName: String,
|
||||
// KMK <--
|
||||
): List<Extension.Available> {
|
||||
return this
|
||||
.filter {
|
||||
val libVersion = it.extractLibVersion()
|
||||
libVersion >= ExtensionLoader.LIB_VERSION_MIN && libVersion <= ExtensionLoader.LIB_VERSION_MAX
|
||||
}
|
||||
.map {
|
||||
Extension.Available(
|
||||
name = it.name.substringAfter("Tachiyomi: "),
|
||||
pkgName = it.pkg,
|
||||
versionName = it.version,
|
||||
versionCode = it.code,
|
||||
libVersion = it.extractLibVersion(),
|
||||
lang = it.lang,
|
||||
isNsfw = it.nsfw == 1,
|
||||
sources = it.sources?.map(extensionSourceMapper).orEmpty(),
|
||||
apkName = it.apk,
|
||||
iconUrl = "$repoUrl/icon/${it.pkg}.png",
|
||||
repoUrl = repoUrl,
|
||||
// KMK -->
|
||||
signatureHash = signature,
|
||||
repoName = repoName,
|
||||
// KMK <--
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun getApkUrl(extension: Extension.Available): String {
|
||||
return "${extension.repoUrl}/apk/${extension.apkName}"
|
||||
}
|
||||
|
||||
private fun ExtensionJsonObject.extractLibVersion(): Double {
|
||||
return version.substringBeforeLast('.').toDouble()
|
||||
}
|
||||
|
||||
// SY -->
|
||||
private fun Extension.isBlacklisted(
|
||||
blacklistEnabled: Boolean = sourcePreferences.enableSourceBlacklist().get(),
|
||||
|
|
@ -112,3 +194,32 @@ internal class ExtensionApi {
|
|||
}
|
||||
// SY <--
|
||||
}
|
||||
|
||||
@Serializable
|
||||
private data class ExtensionJsonObject(
|
||||
val name: String,
|
||||
val pkg: String,
|
||||
val apk: String,
|
||||
val lang: String,
|
||||
val code: Long,
|
||||
val version: String,
|
||||
val nsfw: Int,
|
||||
val sources: List<ExtensionSourceJsonObject>?,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
private data class ExtensionSourceJsonObject(
|
||||
val id: Long,
|
||||
val lang: String,
|
||||
val name: String,
|
||||
val baseUrl: String,
|
||||
)
|
||||
|
||||
private val extensionSourceMapper: (ExtensionSourceJsonObject) -> Extension.Available.Source = {
|
||||
Extension.Available.Source(
|
||||
id = it.id,
|
||||
lang = it.lang,
|
||||
name = it.name,
|
||||
baseUrl = it.baseUrl,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,9 +24,7 @@ abstract class Installer(private val service: Service) {
|
|||
private val extensionManager: ExtensionManager by injectLazy()
|
||||
|
||||
private var waitingInstall = AtomicReference<Entry?>(null)
|
||||
// KMK -->
|
||||
private val queue = Collections.synchronizedSet(mutableSetOf<Entry>())
|
||||
// KMK <--
|
||||
private val queue = Collections.synchronizedList(mutableListOf<Entry>())
|
||||
|
||||
private val cancelReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
|
|
@ -106,9 +104,7 @@ abstract class Installer(private val service: Service) {
|
|||
}
|
||||
val nextEntry = queue.first()
|
||||
if (waitingInstall.compareAndSet(null, nextEntry)) {
|
||||
// KMK -->
|
||||
queue.remove(nextEntry)
|
||||
// KMK <--
|
||||
queue.removeAt(0)
|
||||
processEntry(nextEntry)
|
||||
}
|
||||
}
|
||||
|
|
@ -133,11 +129,7 @@ abstract class Installer(private val service: Service) {
|
|||
*/
|
||||
private fun cancelQueue(downloadId: Long) {
|
||||
val waitingInstall = this.waitingInstall.load()
|
||||
// KMK -->
|
||||
val toCancel = synchronized(queue) { queue.find { it.downloadId == downloadId } }
|
||||
?: waitingInstall?.takeIf { it.downloadId == downloadId }
|
||||
// KMK <--
|
||||
?: return
|
||||
val toCancel = queue.find { it.downloadId == downloadId } ?: waitingInstall ?: return
|
||||
if (cancelEntry(toCancel)) {
|
||||
queue.remove(toCancel)
|
||||
if (waitingInstall == toCancel) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
package eu.kanade.tachiyomi.extension.installer
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.PendingIntent
|
||||
import android.app.Service
|
||||
import android.content.BroadcastReceiver
|
||||
|
|
@ -15,7 +14,8 @@ import eu.kanade.tachiyomi.extension.model.InstallStep
|
|||
import eu.kanade.tachiyomi.util.lang.use
|
||||
import eu.kanade.tachiyomi.util.system.getParcelableExtraCompat
|
||||
import eu.kanade.tachiyomi.util.system.getUriSize
|
||||
import exh.log.xLogE
|
||||
import logcat.LogPriority
|
||||
import tachiyomi.core.common.util.system.logcat
|
||||
|
||||
class PackageInstallerInstaller(private val service: Service) : Installer(service) {
|
||||
|
||||
|
|
@ -41,7 +41,7 @@ class PackageInstallerInstaller(private val service: Service) : Installer(servic
|
|||
.sanitizeByFiltering(this)
|
||||
}
|
||||
if (userAction == null) {
|
||||
xLogE("Fatal error for $intent")
|
||||
logcat(LogPriority.ERROR) { "Fatal error for $intent" }
|
||||
continueQueue(InstallStep.Error)
|
||||
return
|
||||
}
|
||||
|
|
@ -82,7 +82,6 @@ class PackageInstallerInstaller(private val service: Service) : Installer(servic
|
|||
inputStream.copyTo(outputStream)
|
||||
session.fsync(outputStream)
|
||||
}
|
||||
service.contentResolver.delete(entry.uri, null, null)
|
||||
|
||||
val intentSender = PendingIntent.getBroadcast(
|
||||
service,
|
||||
|
|
@ -90,11 +89,10 @@ class PackageInstallerInstaller(private val service: Service) : Installer(servic
|
|||
Intent(INSTALL_ACTION).setPackage(service.packageName),
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_MUTABLE else 0,
|
||||
).intentSender
|
||||
@SuppressLint("RequestInstallPackagesPolicy")
|
||||
session.commit(intentSender)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
xLogE("Failed to install extension ${entry.downloadId} ${entry.uri}", e)
|
||||
logcat(LogPriority.ERROR, e) { "Failed to install extension ${entry.downloadId} ${entry.uri}" }
|
||||
activeSession?.let { (_, sessionId) ->
|
||||
packageInstaller.abandonSession(sessionId)
|
||||
}
|
||||
|
|
@ -105,13 +103,8 @@ class PackageInstallerInstaller(private val service: Service) : Installer(servic
|
|||
override fun cancelEntry(entry: Entry): Boolean {
|
||||
activeSession?.let { (activeEntry, sessionId) ->
|
||||
if (activeEntry == entry) {
|
||||
return try {
|
||||
packageInstaller.abandonSession(sessionId)
|
||||
false
|
||||
} catch (_: SecurityException) {
|
||||
// Highly likely the session has succeeded
|
||||
true
|
||||
}
|
||||
packageInstaller.abandonSession(sessionId)
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
|
|
|
|||
|
|
@ -111,17 +111,20 @@ class ShizukuInstaller(private val service: Service) : Installer(service) {
|
|||
|
||||
override fun processEntry(entry: Entry) {
|
||||
super.processEntry(entry)
|
||||
|
||||
// KMK -->
|
||||
val installer = shellInterface
|
||||
if (installer == null) {
|
||||
logcat(LogPriority.ERROR) { "Shizuku shell interface not available for ${entry.downloadId}" }
|
||||
continueQueue(InstallStep.Error)
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
service.contentResolver.openAssetFileDescriptor(entry.uri, "r")?.use {
|
||||
shellInterface?.install(it)
|
||||
// KMK -->
|
||||
?: throw Exception("Shell interface is not available")
|
||||
// KMK <--
|
||||
installer.install(it)
|
||||
}
|
||||
// KMK -->
|
||||
?: throw Exception("Failed to open asset file descriptor")
|
||||
// KMK <--
|
||||
service.contentResolver.delete(entry.uri, null, null)
|
||||
} catch (e: Exception) {
|
||||
logcat(LogPriority.ERROR, e) { "Failed to install extension ${entry.downloadId} ${entry.uri}" }
|
||||
continueQueue(InstallStep.Error)
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ package eu.kanade.tachiyomi.extension.model
|
|||
|
||||
import android.graphics.drawable.Drawable
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import mihon.domain.extension.model.ExtensionStore
|
||||
import tachiyomi.domain.source.model.StubSource
|
||||
|
||||
sealed class Extension {
|
||||
|
|
@ -17,7 +16,7 @@ sealed class Extension {
|
|||
|
||||
// KMK -->
|
||||
abstract val signatureHash: String
|
||||
abstract val storeName: String?
|
||||
abstract val repoName: String?
|
||||
// KMK <--
|
||||
|
||||
data class Installed(
|
||||
|
|
@ -30,8 +29,8 @@ sealed class Extension {
|
|||
override val isNsfw: Boolean,
|
||||
// KMK -->
|
||||
override val signatureHash: String,
|
||||
/** Guessing store name from built-in signatures preset */
|
||||
override val storeName: String? = null,
|
||||
/** Guessing repo name from built-in signatures preset */
|
||||
override val repoName: String? = null,
|
||||
// KMK <--
|
||||
val pkgFactory: String?,
|
||||
val sources: List<Source>,
|
||||
|
|
@ -39,7 +38,7 @@ sealed class Extension {
|
|||
val hasUpdate: Boolean = false,
|
||||
val isObsolete: Boolean = false,
|
||||
val isShared: Boolean,
|
||||
val store: ExtensionStore? = null,
|
||||
val repoUrl: String? = null,
|
||||
// SY -->
|
||||
val isRedundant: Boolean = false,
|
||||
// SY <--
|
||||
|
|
@ -55,12 +54,12 @@ sealed class Extension {
|
|||
override val isNsfw: Boolean,
|
||||
// KMK -->
|
||||
override val signatureHash: String,
|
||||
override val storeName: String,
|
||||
override val repoName: String,
|
||||
// KMK <--
|
||||
val sources: List<Source>,
|
||||
val apkUrl: String,
|
||||
val apkName: String,
|
||||
val iconUrl: String,
|
||||
val store: ExtensionStore,
|
||||
val repoUrl: String,
|
||||
) : Extension() {
|
||||
|
||||
data class Source(
|
||||
|
|
@ -88,7 +87,7 @@ sealed class Extension {
|
|||
/* KMK --> */
|
||||
override /* KMK <-- */ val signatureHash: String,
|
||||
// KMK -->
|
||||
override val storeName: String? = null,
|
||||
override val repoName: String? = null,
|
||||
// KMK <--
|
||||
override val lang: String? = null,
|
||||
override val isNsfw: Boolean = false,
|
||||
|
|
@ -64,11 +64,6 @@ class ExtensionInstallActivity : Activity() {
|
|||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
intent.data?.let { contentResolver.delete(it, null, null) }
|
||||
}
|
||||
|
||||
private fun checkInstallationResult(resultCode: Int) {
|
||||
val downloadId = intent.extras!!.getLong(ExtensionInstaller.EXTRA_DOWNLOAD_ID)
|
||||
val extensionManager = Injekt.get<ExtensionManager>()
|
||||
|
|
|
|||
|
|
@ -3,10 +3,8 @@ package eu.kanade.tachiyomi.extension.util
|
|||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.ServiceInfo
|
||||
import android.graphics.BitmapFactory
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import androidx.core.content.ContextCompat
|
||||
import eu.kanade.domain.base.BasePreferences
|
||||
|
|
@ -18,15 +16,11 @@ import eu.kanade.tachiyomi.extension.installer.ShizukuInstaller
|
|||
import eu.kanade.tachiyomi.extension.util.ExtensionInstaller.Companion.EXTRA_DOWNLOAD_ID
|
||||
import eu.kanade.tachiyomi.util.system.getSerializableExtraCompat
|
||||
import eu.kanade.tachiyomi.util.system.notificationBuilder
|
||||
import exh.log.xLogE
|
||||
import logcat.LogPriority
|
||||
import tachiyomi.core.common.i18n.stringResource
|
||||
import tachiyomi.core.common.util.system.logcat
|
||||
import tachiyomi.i18n.MR
|
||||
|
||||
/**
|
||||
* Foreground service that stages and installs extension APKs via [PackageInstallerInstaller] or
|
||||
* [ShizukuInstaller]. Uses [ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC] because installs can
|
||||
* wait on multiple system confirmation dialogs; `shortService` times out after ~3 minutes.
|
||||
*/
|
||||
class ExtensionInstallService : Service() {
|
||||
|
||||
private var installer: Installer? = null
|
||||
|
|
@ -42,17 +36,7 @@ class ExtensionInstallService : Service() {
|
|||
setContentTitle(stringResource(MR.strings.ext_install_service_notif))
|
||||
setProgress(100, 100, true)
|
||||
}.build()
|
||||
// KMK -->
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
startForeground(
|
||||
Notifications.ID_EXTENSION_INSTALLER,
|
||||
notification,
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC,
|
||||
)
|
||||
} else {
|
||||
// KMK <--
|
||||
startForeground(Notifications.ID_EXTENSION_INSTALLER, notification)
|
||||
}
|
||||
startForeground(Notifications.ID_EXTENSION_INSTALLER, notification)
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
|
|
@ -69,7 +53,7 @@ class ExtensionInstallService : Service() {
|
|||
BasePreferences.ExtensionInstaller.PACKAGEINSTALLER -> PackageInstallerInstaller(this)
|
||||
BasePreferences.ExtensionInstaller.SHIZUKU -> ShizukuInstaller(this)
|
||||
else -> {
|
||||
xLogE("Not implemented for installer $installerUsed")
|
||||
logcat(LogPriority.ERROR) { "Not implemented for installer $installerUsed" }
|
||||
stopSelf()
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue