Compare commits
No commits in common. "main" and "v0.0.0-forgejo-docker-20260427171135-jsonfix" have entirely different histories.
main
...
v0.0.0-for
290 changed files with 3440 additions and 7914 deletions
|
|
@ -5,12 +5,6 @@ on:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- v*
|
- 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:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
|
@ -21,25 +15,18 @@ concurrency:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
release:
|
||||||
runs-on: desktop-release
|
runs-on: forgejo-release
|
||||||
steps:
|
steps:
|
||||||
- name: Clone repository
|
- name: Clone repository
|
||||||
shell: bash
|
shell: bash
|
||||||
env:
|
env:
|
||||||
FORGEJO_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
FORGEJO_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
DISPATCH_TAG: ${{ github.event.inputs.tag }}
|
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
rm -rf .git
|
rm -rf .git
|
||||||
git init .
|
git init .
|
||||||
origin_url="${GITHUB_SERVER_URL#https://}"
|
git remote add origin "http://copilot:${FORGEJO_TOKEN}@127.0.0.1:3030/${GITHUB_REPOSITORY}.git"
|
||||||
origin_url="${origin_url#http://}"
|
git fetch --tags --prune --depth=1 origin "${GITHUB_SHA}"
|
||||||
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
|
git checkout --force FETCH_HEAD
|
||||||
|
|
||||||
- name: Set up JDK
|
- name: Set up JDK
|
||||||
|
|
@ -76,20 +63,21 @@ jobs:
|
||||||
fi
|
fi
|
||||||
java -version
|
java -version
|
||||||
fi
|
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
|
|
||||||
|
|
||||||
- name: Configure persistent Gradle cache
|
- name: Write google-services.json
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
gradle_home="${KOMIKKU_GRADLE_CACHE:-${HOME}/.cache/komikku-ci-gradle}"
|
printf '%s' "${{ secrets.GOOGLE_SERVICES_JSON }}" > app/google-services.json
|
||||||
mkdir -p "${gradle_home}"
|
python3 -m json.tool app/google-services.json > /dev/null
|
||||||
echo "GRADLE_USER_HOME=${gradle_home}" >> "${GITHUB_ENV}"
|
|
||||||
echo "Gradle dependencies/cache: ${gradle_home}"
|
- name: Write client_secrets.json
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
mkdir -p app/src/main/assets
|
||||||
|
printf '%s' "${{ secrets.GOOGLE_CLIENT_SECRETS_JSON }}" > app/src/main/assets/client_secrets.json
|
||||||
|
python3 -m json.tool app/src/main/assets/client_secrets.json > /dev/null
|
||||||
|
|
||||||
- name: Prepare release metadata
|
- name: Prepare release metadata
|
||||||
id: prepare
|
id: prepare
|
||||||
|
|
@ -97,233 +85,46 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
if [ "${GITHUB_EVENT_NAME}" = "workflow_dispatch" ]; then
|
version_tag="${GITHUB_REF_NAME:-${GITHUB_REF#refs/tags/}}"
|
||||||
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//' )"
|
|
||||||
previous_tag="$(git tag --list 'v*' --sort=-version:refname | grep -Fxv "$version_tag" | head -n 1 || true)"
|
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_TAG=$version_tag" >> "$GITHUB_OUTPUT"
|
||||||
echo "VERSION_DISPLAY=$version_display" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "PREV_TAG_NAME=$previous_tag" >> "$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
|
if [ -n "$previous_tag" ]; then
|
||||||
log_range="${previous_tag}..HEAD"
|
commit_logs="$(git log --pretty=format:'- %s (@%an)' "${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}"
|
|
||||||
else
|
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
|
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_LOGS<<EOF'
|
||||||
echo "COMMIT_LOG_FILE=${commit_log_file}" >> "${GITHUB_ENV}"
|
printf '%s\n' "$commit_logs"
|
||||||
|
echo 'EOF'
|
||||||
|
} >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: Set up Gradle
|
- name: Set up Gradle
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
sdk_candidates=(
|
if [ -d /opt/android-sdk-linux ]; then
|
||||||
"${ANDROID_HOME:-}"
|
export ANDROID_HOME=/opt/android-sdk-linux
|
||||||
"${ANDROID_SDK_ROOT:-}"
|
export ANDROID_SDK_ROOT=/opt/android-sdk-linux
|
||||||
"/opt/android-sdk-linux"
|
echo "ANDROID_HOME=$ANDROID_HOME" >> "$GITHUB_ENV"
|
||||||
"/home/yuna/Android/Sdk"
|
echo "ANDROID_SDK_ROOT=$ANDROID_SDK_ROOT" >> "$GITHUB_ENV"
|
||||||
"/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
|
|
||||||
|
|
||||||
- name: Build app
|
|
||||||
run: |
|
|
||||||
if [ -n "${ANDROID_HOME:-}" ]; then
|
|
||||||
printf 'sdk.dir=%s\n' "$ANDROID_HOME" > local.properties
|
printf 'sdk.dir=%s\n' "$ANDROID_HOME" > local.properties
|
||||||
fi
|
fi
|
||||||
./gradlew --no-daemon --max-workers=1 \
|
chmod +x ./gradlew
|
||||||
"-Dorg.gradle.jvmargs=-Xmx4096m -XX:MaxMetaspaceSize=1024m -XX:ReservedCodeCacheSize=256m -Dfile.encoding=UTF-8" \
|
./gradlew --no-daemon --version
|
||||||
-Dorg.gradle.parallel=false \
|
|
||||||
-Dorg.gradle.configureondemand=false \
|
- name: Check code format
|
||||||
-Dorg.gradle.vfs.watch=false \
|
run: ./gradlew --no-daemon --max-workers=2 spotlessCheck
|
||||||
-Dkotlin.daemon.enabled=false \
|
|
||||||
"-Dkotlin.daemon.jvmargs=-Xmx2048m,-XX:MaxMetaspaceSize=512m,-XX:ReservedCodeCacheSize=256m" \
|
- name: Build app
|
||||||
-Dkotlin.compiler.execution.strategy=in-process \
|
run: ./gradlew --no-daemon --max-workers=2 assembleRelease -Pinclude-telemetry -Penable-updater
|
||||||
-Pkotlin.incremental=true \
|
|
||||||
--stacktrace \
|
- name: Run unit tests
|
||||||
assembleRelease -Penable-updater -Pdisable-code-shrink
|
run: ./gradlew --no-daemon --max-workers=2 testReleaseUnitTest
|
||||||
|
|
||||||
- name: Rename release artifacts
|
- name: Rename release artifacts
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
@ -338,80 +139,7 @@ jobs:
|
||||||
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-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-x86_64-release-unsigned.apk "Komikku-x86_64-${version_tag}-unsigned.apk"
|
||||||
|
|
||||||
if [ -d app/build/outputs/mapping/release ]; then
|
zip -qr "Komikku-mapping-${version_tag}.zip" app/build/outputs/mapping/release
|
||||||
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 or update release
|
- name: Create or update release
|
||||||
id: release
|
id: release
|
||||||
|
|
@ -419,330 +147,142 @@ jobs:
|
||||||
env:
|
env:
|
||||||
FORGEJO_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
FORGEJO_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
VERSION_TAG: ${{ steps.prepare.outputs.VERSION_TAG }}
|
VERSION_TAG: ${{ steps.prepare.outputs.VERSION_TAG }}
|
||||||
VERSION_DISPLAY: ${{ steps.prepare.outputs.VERSION_DISPLAY }}
|
|
||||||
PREV_TAG_NAME: ${{ steps.prepare.outputs.PREV_TAG_NAME }}
|
PREV_TAG_NAME: ${{ steps.prepare.outputs.PREV_TAG_NAME }}
|
||||||
|
COMMIT_LOGS: ${{ steps.prepare.outputs.COMMIT_LOGS }}
|
||||||
REPO_URL: ${{ github.server_url }}/${{ github.repository }}
|
REPO_URL: ${{ github.server_url }}/${{ github.repository }}
|
||||||
REPOSITORY_NAME: ${{ github.repository }}
|
REPOSITORY_NAME: ${{ github.repository }}
|
||||||
FORGEJO_CURL_INSECURE: "true"
|
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
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"
|
api_base="${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/releases"
|
||||||
payload_file="$(mktemp)"
|
payload_file="$(mktemp)"
|
||||||
response_file="$(mktemp)"
|
response_file="$(mktemp)"
|
||||||
|
|
||||||
commit_logs=""
|
python3 - <<'PY' > "$payload_file"
|
||||||
if [ -n "${COMMIT_LOG_FILE:-}" ] && [ -f "${COMMIT_LOG_FILE}" ]; then
|
import json
|
||||||
commit_logs="$(cat "${COMMIT_LOG_FILE}")"
|
import os
|
||||||
fi
|
import textwrap
|
||||||
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
|
|
||||||
|
|
||||||
full_changelog=""
|
version_tag = os.environ["VERSION_TAG"]
|
||||||
if [ -n "${PREV_TAG_NAME:-}" ]; then
|
previous_tag = os.environ.get("PREV_TAG_NAME", "").strip()
|
||||||
full_changelog="**Full Changelog**: [${REPOSITORY_NAME}@${PREV_TAG_NAME}...${VERSION_TAG}](${REPO_URL}/compare/${PREV_TAG_NAME}...${VERSION_TAG})"
|
commit_logs = os.environ.get("COMMIT_LOGS", "").strip() or "- Initial release"
|
||||||
fi
|
repo_url = os.environ["REPO_URL"]
|
||||||
|
repository_name = os.environ["REPOSITORY_NAME"]
|
||||||
|
|
||||||
release_body_file="$(mktemp)"
|
if previous_tag:
|
||||||
{
|
full_changelog = (
|
||||||
printf '%s\n' \
|
f"**Full Changelog**: "
|
||||||
"#### What's Changed" \
|
f"[{repository_name}@{previous_tag}...{version_tag}]"
|
||||||
"" \
|
f"({repo_url}/compare/{previous_tag}...{version_tag})"
|
||||||
"${commit_logs}" \
|
)
|
||||||
""
|
else:
|
||||||
if [ -n "${full_changelog}" ]; then
|
full_changelog = ""
|
||||||
printf '%s\n' "${full_changelog}" ""
|
|
||||||
fi
|
|
||||||
printf '%s\n' \
|
|
||||||
"<!-->" \
|
|
||||||
"> [!TIP]" \
|
|
||||||
">" \
|
|
||||||
"> ### If you are unsure which version to download then go with \`Komikku-${VERSION_TAG}.apk\`"
|
|
||||||
} > "${release_body_file}"
|
|
||||||
|
|
||||||
# Build JSON — body from file only (jq --arg body "$huge" hits ARG_MAX).
|
body = textwrap.dedent(
|
||||||
"${JQ_BIN}" -n \
|
f"""\
|
||||||
--arg tag "${VERSION_TAG}" \
|
#### What's Changed
|
||||||
--arg title "Komikku ${VERSION_DISPLAY}" \
|
##### New
|
||||||
--rawfile body "${release_body_file}" \
|
|
||||||
'{tag_name: $tag, name: $title, body: $body, draft: false, prerelease: false}' > "${payload_file}"
|
|
||||||
|
|
||||||
tag_path_enc="$("${JQ_BIN}" -nr --arg t "${VERSION_TAG}" '$t|@uri')"
|
##### Improve
|
||||||
tag_get_url="${api_base}/tags/${tag_path_enc}"
|
|
||||||
echo "Forgejo release API: GET ${tag_get_url}"
|
|
||||||
|
|
||||||
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}" \
|
-H "Authorization: token ${FORGEJO_TOKEN}" \
|
||||||
"${tag_get_url}")"
|
"${api_base}/tags/${VERSION_TAG}")"
|
||||||
|
|
||||||
echo "release-by-tag GET HTTP status: ${status}"
|
if [ "$status" = "200" ]; then
|
||||||
if [ "${status}" != "200" ] && [ "${status}" != "404" ]; then
|
release_id="$(python3 -c 'import json, sys; print(json.load(sys.stdin)["id"])' < "$response_file")"
|
||||||
echo "Unexpected GET response body:"
|
curl -fsS \
|
||||||
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}' \
|
|
||||||
-X PATCH \
|
-X PATCH \
|
||||||
-H "Authorization: token ${FORGEJO_TOKEN}" \
|
-H "Authorization: token ${FORGEJO_TOKEN}" \
|
||||||
-H 'Content-Type: application/json' \
|
-H 'Content-Type: application/json' \
|
||||||
--data @"$payload_file" \
|
--data @"$payload_file" \
|
||||||
"${api_base}/${release_id}")"
|
"${api_base}/${release_id}" \
|
||||||
echo "PATCH HTTP status: ${patch_status}"
|
> "$response_file"
|
||||||
if [ "${patch_status}" != "200" ]; then
|
elif [ "$status" = "404" ]; then
|
||||||
echo "PATCH response body:"
|
curl -fsS \
|
||||||
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}' \
|
|
||||||
-X POST \
|
-X POST \
|
||||||
-H "Authorization: token ${FORGEJO_TOKEN}" \
|
-H "Authorization: token ${FORGEJO_TOKEN}" \
|
||||||
-H 'Content-Type: application/json' \
|
-H 'Content-Type: application/json' \
|
||||||
--data @"$payload_file" \
|
--data @"$payload_file" \
|
||||||
"${api_base}")"
|
"${api_base}" \
|
||||||
echo "POST HTTP status: ${post_status}"
|
> "$response_file"
|
||||||
if [ "${post_status}" != "201" ] && [ "${post_status}" != "200" ]; then
|
release_id="$(python3 -c 'import json, sys; print(json.load(sys.stdin)["id"])' < "$response_file")"
|
||||||
echo "POST response body:"
|
else
|
||||||
cat "$response_file"
|
cat "$response_file"
|
||||||
exit 1
|
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
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "RELEASE_ID=${release_id}" >> "$GITHUB_OUTPUT"
|
echo "RELEASE_ID=$release_id" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: Upload release assets
|
- name: Upload release assets
|
||||||
shell: bash
|
shell: bash
|
||||||
env:
|
env:
|
||||||
FORGEJO_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
FORGEJO_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
RELEASE_ID: ${{ steps.release.outputs.RELEASE_ID }}
|
RELEASE_ID: ${{ steps.release.outputs.RELEASE_ID }}
|
||||||
FORGEJO_CURL_INSECURE: "true"
|
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
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}"
|
release_api="${GITHUB_API_URL}/repos/${GITHUB_REPOSITORY}/releases/${RELEASE_ID}"
|
||||||
assets_file="$(mktemp)"
|
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}" \
|
-H "Authorization: token ${FORGEJO_TOKEN}" \
|
||||||
"${release_api}/assets")"
|
"${release_api}/assets" \
|
||||||
if [ "${assets_status}" != "200" ]; then
|
> "$assets_file"
|
||||||
echo "List assets failed HTTP ${assets_status}:"
|
|
||||||
cat "$assets_file"
|
|
||||||
rm -f "$assets_file" "$err_body"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
for file in Komikku-*.apk Komikku-mapping-*.zip; do
|
for file in Komikku-*.apk Komikku-mapping-*.zip; do
|
||||||
[ -e "$file" ] || continue
|
[ -e "$file" ] || continue
|
||||||
|
|
||||||
if [ "${file#*-unsigned.apk}" != "${file}" ]; then
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
|
|
||||||
asset_name="$(basename "$file")"
|
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
|
if [ -n "$asset_id" ]; then
|
||||||
del_status="$(curl "${curl_tls_args[@]}" -sS -o "$err_body" -w '%{http_code}' \
|
curl -fsS \
|
||||||
-X DELETE \
|
-X DELETE \
|
||||||
-H "Authorization: token ${FORGEJO_TOKEN}" \
|
-H "Authorization: token ${FORGEJO_TOKEN}" \
|
||||||
"${release_api}/assets/${asset_id}")"
|
"${release_api}/assets/${asset_id}" \
|
||||||
if [ "${del_status}" != "204" ] && [ "${del_status}" != "200" ] && [ "${del_status}" != "404" ]; then
|
> /dev/null
|
||||||
echo "DELETE old asset ${asset_name} failed HTTP ${del_status}:"
|
|
||||||
cat "$err_body"
|
|
||||||
rm -f "$assets_file" "$err_body"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
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 \
|
-X POST \
|
||||||
-H "Authorization: token ${FORGEJO_TOKEN}" \
|
-H "Authorization: token ${FORGEJO_TOKEN}" \
|
||||||
-H 'Content-Type: application/octet-stream' \
|
-H 'Content-Type: application/octet-stream' \
|
||||||
--data-binary @"$file" \
|
--data-binary @"$file" \
|
||||||
"${release_api}/assets?name=${encoded_name}")"
|
"${release_api}/assets?name=${encoded_name}" \
|
||||||
if [ "${up_status}" != "201" ] && [ "${up_status}" != "200" ]; then
|
> /dev/null
|
||||||
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})"
|
|
||||||
done
|
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
|
echo "VERSION_TAG=$version_tag" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Clone Repository (${{ steps.get_tag.outputs.VERSION_TAG }})
|
- name: Clone Repository (${{ steps.get_tag.outputs.VERSION_TAG }})
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@v4 # Forgejo-compatible (node20)
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
|
|
@ -57,18 +57,18 @@ jobs:
|
||||||
needs: prepare-build
|
needs: prepare-build
|
||||||
steps:
|
steps:
|
||||||
- name: Clone Repository (${{ needs.prepare-build.outputs.TAG_NAME }})
|
- name: Clone Repository (${{ needs.prepare-build.outputs.TAG_NAME }})
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@v4 # Forgejo-compatible (node20)
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Set up JDK
|
- name: Set up JDK
|
||||||
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
|
uses: actions/setup-java@v4 # Forgejo-compatible (node20)
|
||||||
with:
|
with:
|
||||||
java-version-file: .github/.java-version
|
java-version: 17
|
||||||
distribution: temurin
|
distribution: temurin
|
||||||
|
|
||||||
- name: Set up gradle
|
- 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
|
- name: Check code format
|
||||||
run: ./gradlew spotlessCheck
|
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
|
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
|
- name: Upload artifacts
|
||||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
uses: actions/upload-artifact@v4 # Forgejo-compatible (node20)
|
||||||
with:
|
with:
|
||||||
path: "**/*.apk"
|
path: "**/*.apk"
|
||||||
retention-days: 30
|
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:
|
steps:
|
||||||
- name: Clone repo
|
- name: Clone repo
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@v4 # Forgejo-compatible (node20)
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
|
|
@ -53,7 +53,7 @@ jobs:
|
||||||
uses: tj-actions/branch-names@5250492686b253f06fa55861556d1027b067aeb5 # v9.0.2
|
uses: tj-actions/branch-names@5250492686b253f06fa55861556d1027b067aeb5 # v9.0.2
|
||||||
|
|
||||||
- name: Invoke workflow in preview repo
|
- 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:
|
with:
|
||||||
workflow: build_app.yml
|
workflow: build_app.yml
|
||||||
repo: komikku-app/komikku-preview
|
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
|
echo "VERSION_TAG=$version_tag" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Clone Repository (${{ steps.get_tag.outputs.VERSION_TAG }})
|
- name: Clone Repository (${{ steps.get_tag.outputs.VERSION_TAG }})
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@v4 # Forgejo-compatible (node20)
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
|
|
@ -119,14 +119,14 @@ jobs:
|
||||||
if: github.ref == 'refs/heads/master'
|
if: github.ref == 'refs/heads/master'
|
||||||
steps:
|
steps:
|
||||||
- name: Clone Repository (${{ needs.prepare-build.outputs.VERSION_TAG }} - ${{ needs.prepare-build.outputs.TAG_NAME }})
|
- 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:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Set up JDK
|
- name: Set up JDK
|
||||||
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
|
uses: actions/setup-java@v4 # Forgejo-compatible (node20)
|
||||||
with:
|
with:
|
||||||
java-version-file: .github/.java-version
|
java-version: 17
|
||||||
distribution: temurin
|
distribution: temurin
|
||||||
|
|
||||||
- name: Write google-services.json
|
- name: Write google-services.json
|
||||||
|
|
@ -144,7 +144,7 @@ jobs:
|
||||||
write-mode: overwrite
|
write-mode: overwrite
|
||||||
|
|
||||||
- name: Set up gradle
|
- 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
|
- name: Check code format
|
||||||
run: ./gradlew spotlessCheck
|
run: ./gradlew spotlessCheck
|
||||||
|
|
@ -156,7 +156,7 @@ jobs:
|
||||||
run: ./gradlew testReleaseUnitTest
|
run: ./gradlew testReleaseUnitTest
|
||||||
|
|
||||||
- name: Upload artifacts
|
- name: Upload artifacts
|
||||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
uses: actions/upload-artifact@v4 # Forgejo-compatible (node20)
|
||||||
with:
|
with:
|
||||||
path: "**/*.apk"
|
path: "**/*.apk"
|
||||||
retention-days: 1
|
retention-days: 1
|
||||||
|
|
@ -168,7 +168,7 @@ jobs:
|
||||||
- build-app
|
- build-app
|
||||||
steps:
|
steps:
|
||||||
- name: Download artifacts
|
- name: Download artifacts
|
||||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
uses: actions/download-artifact@v4 # Forgejo-compatible (node20)
|
||||||
with:
|
with:
|
||||||
merge-multiple: true
|
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
|
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
|
- name: Create release
|
||||||
uses: softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b # v3.0.1
|
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
|
||||||
with:
|
with:
|
||||||
tag_name: ${{ needs.prepare-build.outputs.TAG_NAME }}
|
tag_name: ${{ needs.prepare-build.outputs.TAG_NAME }}
|
||||||
name: Komikku Preview ${{ needs.prepare-build.outputs.TAG_NAME }} (${{ needs.prepare-build.outputs.VERSION_TAG }})
|
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:
|
steps:
|
||||||
- name: Clone repo
|
- name: Clone repo
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@v4 # Forgejo-compatible (node20)
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Dependency Review
|
- 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
|
- 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
|
- name: Set up JDK
|
||||||
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
|
uses: actions/setup-java@v4 # Forgejo-compatible (node20)
|
||||||
with:
|
with:
|
||||||
java-version-file: .github/.java-version
|
java-version: 17
|
||||||
distribution: temurin
|
distribution: temurin
|
||||||
|
|
||||||
- name: Write google-services.json
|
- name: Write google-services.json
|
||||||
|
|
@ -63,7 +63,7 @@ jobs:
|
||||||
write-mode: overwrite
|
write-mode: overwrite
|
||||||
|
|
||||||
- name: Set up gradle
|
- 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
|
- name: Check code format
|
||||||
run: ./gradlew spotlessCheck
|
run: ./gradlew spotlessCheck
|
||||||
|
|
@ -82,7 +82,7 @@ jobs:
|
||||||
|
|
||||||
- name: Upload test report
|
- name: Upload test report
|
||||||
if: steps.unit_tests.outcome == 'failure'
|
if: steps.unit_tests.outcome == 'failure'
|
||||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
uses: actions/upload-artifact@v4 # Forgejo-compatible (node20)
|
||||||
with:
|
with:
|
||||||
name: test-report-${{ github.sha }}
|
name: test-report-${{ github.sha }}
|
||||||
path: app/build/reports/tests/testReleaseUnitTest
|
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
|
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
|
- name: Upload APK
|
||||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
uses: actions/upload-artifact@v4 # Forgejo-compatible (node20)
|
||||||
with:
|
with:
|
||||||
name: Komikku-${{ steps.current_commit.outputs.VERSION_TAG }}-r${{ steps.current_commit.outputs.COMMIT_COUNT }}.apk
|
name: Komikku-${{ steps.current_commit.outputs.VERSION_TAG }}-r${{ steps.current_commit.outputs.COMMIT_COUNT }}.apk
|
||||||
path: ./*.apk
|
path: ./*.apk
|
||||||
|
|
||||||
- name: Upload mapping
|
- name: Upload mapping
|
||||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
uses: actions/upload-artifact@v4 # Forgejo-compatible (node20)
|
||||||
with:
|
with:
|
||||||
name: mapping-${{ github.sha }}
|
name: mapping-${{ github.sha }}
|
||||||
path: app/build/outputs/mapping/preview
|
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:
|
steps:
|
||||||
- name: Clone repo
|
- name: Clone repo
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@v4 # Forgejo-compatible (node20)
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Set up JDK
|
- name: Set up JDK
|
||||||
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
|
uses: actions/setup-java@v4 # Forgejo-compatible (node20)
|
||||||
with:
|
with:
|
||||||
java-version-file: .github/.java-version
|
java-version: 17
|
||||||
distribution: temurin
|
distribution: temurin
|
||||||
|
|
||||||
- name: Write google-services.json
|
- name: Write google-services.json
|
||||||
|
|
@ -52,7 +52,7 @@ jobs:
|
||||||
write-mode: overwrite
|
write-mode: overwrite
|
||||||
|
|
||||||
- name: Set up gradle
|
- 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
|
- name: Check code format
|
||||||
run: ./gradlew spotlessCheck
|
run: ./gradlew spotlessCheck
|
||||||
|
|
@ -66,7 +66,7 @@ jobs:
|
||||||
|
|
||||||
- name: Upload test report
|
- name: Upload test report
|
||||||
if: steps.unit_tests.outcome == 'failure'
|
if: steps.unit_tests.outcome == 'failure'
|
||||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
uses: actions/upload-artifact@v4 # Forgejo-compatible (node20)
|
||||||
with:
|
with:
|
||||||
name: test-report-${{ github.sha }}
|
name: test-report-${{ github.sha }}
|
||||||
path: app/build/reports/tests/testReleaseUnitTest
|
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
|
mv app/build/outputs/apk/preview/app-universal-preview-signed.apk Komikku-$version_tag-r$commit_count.apk
|
||||||
|
|
||||||
- name: Upload APK
|
- name: Upload APK
|
||||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
uses: actions/upload-artifact@v4 # Forgejo-compatible (node20)
|
||||||
with:
|
with:
|
||||||
name: Komikku-${{ steps.current_commit.outputs.VERSION_TAG }}-r${{ steps.current_commit.outputs.COMMIT_COUNT }}.apk
|
name: Komikku-${{ steps.current_commit.outputs.VERSION_TAG }}-r${{ steps.current_commit.outputs.COMMIT_COUNT }}.apk
|
||||||
path: ./*.apk
|
path: ./*.apk
|
||||||
|
|
||||||
- name: Upload mapping
|
- name: Upload mapping
|
||||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
uses: actions/upload-artifact@v4 # Forgejo-compatible (node20)
|
||||||
with:
|
with:
|
||||||
name: mapping-${{ github.sha }}
|
name: mapping-${{ github.sha }}
|
||||||
path: app/build/outputs/mapping/preview
|
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
|
echo "VERSION_TAG=$version_tag" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Clone Repository (${{ steps.get_tag.outputs.VERSION_TAG }})
|
- name: Clone Repository (${{ steps.get_tag.outputs.VERSION_TAG }})
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@v4 # Forgejo-compatible (node20)
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
|
|
@ -87,14 +87,14 @@ jobs:
|
||||||
if: startsWith(github.ref, 'refs/tags/v')
|
if: startsWith(github.ref, 'refs/tags/v')
|
||||||
steps:
|
steps:
|
||||||
- name: Clone Repository (${{ needs.prepare-build.outputs.VERSION_TAG }})
|
- name: Clone Repository (${{ needs.prepare-build.outputs.VERSION_TAG }})
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
uses: actions/checkout@v4 # Forgejo-compatible (node20)
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
- name: Set up JDK
|
- name: Set up JDK
|
||||||
uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520 # v5.4.0
|
uses: actions/setup-java@v4 # Forgejo-compatible (node20)
|
||||||
with:
|
with:
|
||||||
java-version-file: .github/.java-version
|
java-version: 17
|
||||||
distribution: temurin
|
distribution: temurin
|
||||||
|
|
||||||
- name: Write google-services.json
|
- name: Write google-services.json
|
||||||
|
|
@ -112,7 +112,7 @@ jobs:
|
||||||
write-mode: overwrite
|
write-mode: overwrite
|
||||||
|
|
||||||
- name: Set up gradle
|
- 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
|
- name: Check code format
|
||||||
run: ./gradlew spotlessCheck
|
run: ./gradlew spotlessCheck
|
||||||
|
|
@ -124,13 +124,13 @@ jobs:
|
||||||
run: ./gradlew testReleaseUnitTest
|
run: ./gradlew testReleaseUnitTest
|
||||||
|
|
||||||
- name: Upload artifacts
|
- name: Upload artifacts
|
||||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
uses: actions/upload-artifact@v4 # Forgejo-compatible (node20)
|
||||||
with:
|
with:
|
||||||
path: "**/*.apk"
|
path: "**/*.apk"
|
||||||
retention-days: 1
|
retention-days: 1
|
||||||
|
|
||||||
- name: Upload mapping
|
- name: Upload mapping
|
||||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
uses: actions/upload-artifact@v4 # Forgejo-compatible (node20)
|
||||||
with:
|
with:
|
||||||
name: mapping-${{ github.sha }}
|
name: mapping-${{ github.sha }}
|
||||||
path: app/build/outputs/mapping/release
|
path: app/build/outputs/mapping/release
|
||||||
|
|
@ -142,7 +142,7 @@ jobs:
|
||||||
- build-app
|
- build-app
|
||||||
steps:
|
steps:
|
||||||
- name: Download artifacts
|
- name: Download artifacts
|
||||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
uses: actions/download-artifact@v4 # Forgejo-compatible (node20)
|
||||||
with:
|
with:
|
||||||
merge-multiple: true
|
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
|
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
|
- name: Create release
|
||||||
uses: softprops/action-gh-release@718ea10b132b3b2eba29c1007bb80653f286566b # v3.0.1
|
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
|
||||||
with:
|
with:
|
||||||
tag_name: ${{ needs.prepare-build.outputs.VERSION_TAG }}
|
tag_name: ${{ needs.prepare-build.outputs.VERSION_TAG }}
|
||||||
name: Komikku ${{ 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'
|
if: github.repository == 'komikku-app/komikku'
|
||||||
runs-on: 'ubuntu-24.04'
|
runs-on: 'ubuntu-24.04'
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
- uses: actions/checkout@v4 # Forgejo-compatible (node20)
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
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
|
- uses: pixta-dev/repository-mirroring-action@674e65a7d483ca28dafaacba0d07351bdcc8bd75 # v1.1.1
|
||||||
with:
|
with:
|
||||||
target_repo_url: "git@codeberg.org:cuong-tran/komikku.git"
|
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
|
if: github.event.pull_request.merged == true && github.repository == github.event.pull_request.head.repo.full_name
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
|
uses: actions/checkout@v4 # Forgejo-compatible (node20)
|
||||||
|
|
||||||
- name: Delete branch
|
- name: Delete branch
|
||||||
run: |
|
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:
|
steps:
|
||||||
- name: Check PR and Add Label
|
- name: Check PR and Add Label
|
||||||
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
|
uses: actions/github-script@v7 # Forgejo-compatible (node20)
|
||||||
with:
|
with:
|
||||||
script: |
|
script: |
|
||||||
const prAuthor = context.payload.pull_request.user.login;
|
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:
|
steps:
|
||||||
- name: Update website on release
|
- 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:
|
with:
|
||||||
workflow: Deploy
|
workflow: Deploy
|
||||||
repo: komikku-app/komikku-app.github.io
|
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]
|
## [Unreleased]
|
||||||
### Added
|
### 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))
|
- 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))
|
- 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 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:` 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))
|
- 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))
|
- 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))
|
- 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))
|
- 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
|
### Changed
|
||||||
- Update tracker icons ([@AntsyLich](https://github.com/AntsyLich)) ([#2773](https://github.com/mihonapp/mihon/pull/2773))
|
- 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 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
|
### 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))
|
- 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 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 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 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
|
### 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))
|
- 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.*
|
*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")
|
[](https://github.com/sponsors/cuong-tran "Sponsor me on GitHub")
|
||||||
|
|
||||||
<div align="left">
|
<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.
|
- Long-click to add/remove single entry to/from library, everywhere.
|
||||||
- Docking Read/Resume button to left/right.
|
- Docking Read/Resume button to left/right.
|
||||||
- In-app progress banner shows Library syncing / Backup restoring / Library updating progress.
|
- 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.
|
- Auto-install app update.
|
||||||
- Configurable interval to refresh entries from downloaded storage.
|
- Configurable interval to refresh entries from downloaded storage.
|
||||||
- Forked from SY so everything from SY.
|
- 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:
|
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)
|
* 8Muses (EroMuse)
|
||||||
|
* HBrowse
|
||||||
* Mangadex
|
* Mangadex
|
||||||
* NHentai
|
* NHentai
|
||||||
* Puruin
|
* 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 {
|
defaultConfig {
|
||||||
applicationId = "app.komikku"
|
applicationId = "app.komikku"
|
||||||
|
|
||||||
versionCode = 79
|
versionCode = 78
|
||||||
versionName = "1.13.6"
|
versionName = "1.13.6"
|
||||||
|
|
||||||
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
|
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.GSSManager
|
||||||
-dontwarn org.ietf.jgss.GSSName
|
-dontwarn org.ietf.jgss.GSSName
|
||||||
-dontwarn org.ietf.jgss.Oid
|
-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" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
<!-- Legacy deeplink to add extension store -->
|
<!-- Deep link to add repos -->
|
||||||
<intent-filter android:label="@string/action_addExtensionStore">
|
<intent-filter android:label="@string/action_add_repo">
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
|
@ -84,17 +84,6 @@
|
||||||
<data android:host="add-repo" />
|
<data android:host="add-repo" />
|
||||||
</intent-filter>
|
</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 -->
|
<!-- Open backup files -->
|
||||||
<intent-filter android:label="@string/pref_restore_backup">
|
<intent-filter android:label="@string/pref_restore_backup">
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
@ -252,7 +241,7 @@
|
||||||
<service
|
<service
|
||||||
android:name=".extension.util.ExtensionInstallService"
|
android:name=".extension.util.ExtensionInstallService"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:foregroundServiceType="dataSync" />
|
android:foregroundServiceType="shortService" />
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
|
android:name="androidx.appcompat.app.AppLocalesMetadataHolderService"
|
||||||
|
|
@ -344,6 +333,22 @@
|
||||||
|
|
||||||
<data android:pathPattern="/g/..*" />
|
<data android:pathPattern="/g/..*" />
|
||||||
</intent-filter>
|
</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 -->
|
<!-- Pururin -->
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
|
@ -358,6 +363,21 @@
|
||||||
|
|
||||||
<data android:pathPattern="/gallery/..*" />
|
<data android:pathPattern="/gallery/..*" />
|
||||||
</intent-filter>
|
</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 -->
|
<!-- Mangadex -->
|
||||||
<intent-filter android:autoVerify="true">
|
<intent-filter android:autoVerify="true">
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<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
|
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/all/NHentai;
|
||||||
Leu/kanade/tachiyomi/source/online/english/EightMuses;
|
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/Pururin;
|
||||||
|
Leu/kanade/tachiyomi/source/online/english/Tsumino;
|
||||||
Leu/kanade/tachiyomi/ui/base/activity/BaseActivity;
|
Leu/kanade/tachiyomi/ui/base/activity/BaseActivity;
|
||||||
HSPLeu/kanade/tachiyomi/ui/base/activity/BaseActivity;-><init>()V
|
HSPLeu/kanade/tachiyomi/ui/base/activity/BaseActivity;-><init>()V
|
||||||
PLeu/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.RefreshTracks
|
||||||
import eu.kanade.domain.track.interactor.SyncChapterProgressWithTrack
|
import eu.kanade.domain.track.interactor.SyncChapterProgressWithTrack
|
||||||
import eu.kanade.domain.track.interactor.TrackChapter
|
import eu.kanade.domain.track.interactor.TrackChapter
|
||||||
import mihon.data.extension.repository.ExtensionStoreRepositoryImpl
|
import mihon.data.repository.ExtensionRepoRepositoryImpl
|
||||||
import mihon.data.extension.service.ExtensionStoreService
|
|
||||||
import mihon.domain.chapter.interactor.FilterChaptersForDownload
|
import mihon.domain.chapter.interactor.FilterChaptersForDownload
|
||||||
import mihon.domain.chapter.interactor.GetChaptersForLibraryUpdateDownload
|
import mihon.domain.extensionrepo.interactor.CreateExtensionRepo
|
||||||
import mihon.domain.extension.interactor.AddExtensionStore
|
import mihon.domain.extensionrepo.interactor.DeleteExtensionRepo
|
||||||
import mihon.domain.extension.interactor.GetExtensionStoreCountAsFlow
|
import mihon.domain.extensionrepo.interactor.GetExtensionRepo
|
||||||
import mihon.domain.extension.interactor.GetExtensionStores
|
import mihon.domain.extensionrepo.interactor.GetExtensionRepoCount
|
||||||
import mihon.domain.extension.interactor.RemoveExtensionStore
|
import mihon.domain.extensionrepo.interactor.ReplaceExtensionRepo
|
||||||
import mihon.domain.extension.interactor.UpdateExtensionStores
|
import mihon.domain.extensionrepo.interactor.UpdateExtensionRepo
|
||||||
import mihon.domain.extension.repository.ExtensionStoreRepository
|
import mihon.domain.extensionrepo.repository.ExtensionRepoRepository
|
||||||
|
import mihon.domain.extensionrepo.service.ExtensionRepoService
|
||||||
import mihon.domain.migration.usecases.MigrateMangaUseCase
|
import mihon.domain.migration.usecases.MigrateMangaUseCase
|
||||||
import mihon.domain.upcoming.interactor.GetUpcomingManga
|
import mihon.domain.upcoming.interactor.GetUpcomingManga
|
||||||
import tachiyomi.data.category.CategoryRepositoryImpl
|
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.SetSortModeForCategory
|
||||||
import tachiyomi.domain.category.interactor.UpdateCategory
|
import tachiyomi.domain.category.interactor.UpdateCategory
|
||||||
import tachiyomi.domain.category.repository.CategoryRepository
|
import tachiyomi.domain.category.repository.CategoryRepository
|
||||||
import tachiyomi.domain.chapter.interactor.GetBookmarkedChaptersByMangaId
|
|
||||||
import tachiyomi.domain.chapter.interactor.GetChapter
|
import tachiyomi.domain.chapter.interactor.GetChapter
|
||||||
import tachiyomi.domain.chapter.interactor.GetChapterByUrlAndMangaId
|
import tachiyomi.domain.chapter.interactor.GetChapterByUrlAndMangaId
|
||||||
import tachiyomi.domain.chapter.interactor.GetChaptersByMangaId
|
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.ShouldUpdateDbChapter
|
||||||
import tachiyomi.domain.chapter.interactor.UpdateChapter
|
import tachiyomi.domain.chapter.interactor.UpdateChapter
|
||||||
import tachiyomi.domain.chapter.repository.ChapterRepository
|
import tachiyomi.domain.chapter.repository.ChapterRepository
|
||||||
import tachiyomi.domain.history.interactor.GetCrossMangaPageRateHistorySamples
|
|
||||||
import tachiyomi.domain.history.interactor.GetHistory
|
import tachiyomi.domain.history.interactor.GetHistory
|
||||||
import tachiyomi.domain.history.interactor.GetHistoryRevision
|
|
||||||
import tachiyomi.domain.history.interactor.GetNextChapters
|
import tachiyomi.domain.history.interactor.GetNextChapters
|
||||||
import tachiyomi.domain.history.interactor.GetReadDurationEntriesByMangaIds
|
|
||||||
import tachiyomi.domain.history.interactor.GetTotalReadDuration
|
import tachiyomi.domain.history.interactor.GetTotalReadDuration
|
||||||
import tachiyomi.domain.history.interactor.RemoveHistory
|
import tachiyomi.domain.history.interactor.RemoveHistory
|
||||||
import tachiyomi.domain.history.interactor.UpsertHistory
|
import tachiyomi.domain.history.interactor.UpsertHistory
|
||||||
|
|
@ -165,7 +161,6 @@ class DomainModule : InjektModule {
|
||||||
addSingletonFactory<ChapterRepository> { ChapterRepositoryImpl(get()) }
|
addSingletonFactory<ChapterRepository> { ChapterRepositoryImpl(get()) }
|
||||||
addFactory { GetChapter(get()) }
|
addFactory { GetChapter(get()) }
|
||||||
addFactory { GetChaptersByMangaId(get()) }
|
addFactory { GetChaptersByMangaId(get()) }
|
||||||
addFactory { GetBookmarkedChaptersByMangaId(get(), get(), get()) }
|
|
||||||
addFactory { GetChapterByUrlAndMangaId(get()) }
|
addFactory { GetChapterByUrlAndMangaId(get()) }
|
||||||
addFactory { UpdateChapter(get()) }
|
addFactory { UpdateChapter(get()) }
|
||||||
addFactory { SetReadStatus(get(), get(), get(), get(), 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 { SyncChaptersWithSource(get(), get(), get(), get(), get(), get(), get(), get(), get()) }
|
||||||
addFactory { GetAvailableScanlators(get()) }
|
addFactory { GetAvailableScanlators(get()) }
|
||||||
addFactory { FilterChaptersForDownload(get(), get(), get(), get()) }
|
addFactory { FilterChaptersForDownload(get(), get(), get(), get()) }
|
||||||
addFactory { GetChaptersForLibraryUpdateDownload(get(), get()) }
|
|
||||||
|
|
||||||
addSingletonFactory<HistoryRepository> { HistoryRepositoryImpl(get()) }
|
addSingletonFactory<HistoryRepository> { HistoryRepositoryImpl(get()) }
|
||||||
addFactory { GetHistory(get()) }
|
addFactory { GetHistory(get()) }
|
||||||
addFactory { GetHistoryRevision(get()) }
|
|
||||||
addFactory { UpsertHistory(get()) }
|
addFactory { UpsertHistory(get()) }
|
||||||
addFactory { RemoveHistory(get()) }
|
addFactory { RemoveHistory(get()) }
|
||||||
addFactory { GetTotalReadDuration(get()) }
|
addFactory { GetTotalReadDuration(get()) }
|
||||||
addFactory { GetReadDurationEntriesByMangaIds(get()) }
|
|
||||||
addFactory { GetCrossMangaPageRateHistorySamples(get(), get()) }
|
|
||||||
|
|
||||||
addFactory { DeleteDownload(get(), get()) }
|
addFactory { DeleteDownload(get(), get()) }
|
||||||
|
|
||||||
|
|
@ -206,14 +197,14 @@ class DomainModule : InjektModule {
|
||||||
addFactory { ToggleSourcePin(get()) }
|
addFactory { ToggleSourcePin(get()) }
|
||||||
addFactory { TrustExtension(get(), get()) }
|
addFactory { TrustExtension(get(), get()) }
|
||||||
|
|
||||||
addSingletonFactory { ExtensionStoreService(get(), get(), get()) }
|
addSingletonFactory<ExtensionRepoRepository> { ExtensionRepoRepositoryImpl(get()) }
|
||||||
addSingletonFactory<ExtensionStoreRepository> { ExtensionStoreRepositoryImpl(get(), get()) }
|
addFactory { ExtensionRepoService(get(), get()) }
|
||||||
addFactory { AddExtensionStore(get()) }
|
addFactory { GetExtensionRepo(get()) }
|
||||||
addFactory { GetExtensionStoreCountAsFlow(get()) }
|
addFactory { GetExtensionRepoCount(get()) }
|
||||||
addFactory { GetExtensionStores(get()) }
|
addFactory { CreateExtensionRepo(get(), get()) }
|
||||||
addFactory { RemoveExtensionStore(get()) }
|
addFactory { DeleteExtensionRepo(get()) }
|
||||||
addFactory { UpdateExtensionStores(get()) }
|
addFactory { ReplaceExtensionRepo(get()) }
|
||||||
|
addFactory { UpdateExtensionRepo(get(), get()) }
|
||||||
addFactory { ToggleIncognito(get()) }
|
addFactory { ToggleIncognito(get()) }
|
||||||
addFactory { GetIncognitoState(get(), get(), get()) }
|
addFactory { GetIncognitoState(get(), get(), get()) }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -200,7 +200,7 @@ class SyncChaptersWithSource(
|
||||||
bookmark = chapter.chapterNumber in deletedBookmarkedChapterNumbers,
|
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 {
|
deletedChapterNumberDateFetchMap[chapter.chapterNumber]?.let {
|
||||||
chapter = chapter.copy(dateFetch = it)
|
chapter = chapter.copy(dateFetch = it)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,11 @@ class GetExtensionLanguages(
|
||||||
) { enabledLanguage, availableExtensions ->
|
) { enabledLanguage, availableExtensions ->
|
||||||
availableExtensions
|
availableExtensions
|
||||||
.flatMap { ext ->
|
.flatMap { ext ->
|
||||||
ext.sources.map { it.lang }
|
if (ext.sources.isEmpty()) {
|
||||||
|
listOf(ext.lang)
|
||||||
|
} else {
|
||||||
|
ext.sources.map { it.lang }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.distinct()
|
.distinct()
|
||||||
.sortedWith(
|
.sortedWith(
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,9 @@ class GetExtensionsByType(
|
||||||
(showNsfwSources || !extension.isNsfw)
|
(showNsfwSources || !extension.isNsfw)
|
||||||
}
|
}
|
||||||
.flatMap { ext ->
|
.flatMap { ext ->
|
||||||
|
if (ext.sources.isEmpty()) {
|
||||||
|
return@flatMap if (ext.lang in enabledLanguages) listOf(ext) else emptyList()
|
||||||
|
}
|
||||||
ext.sources.filter { it.lang in enabledLanguages }
|
ext.sources.filter { it.lang in enabledLanguages }
|
||||||
.map {
|
.map {
|
||||||
ext.copy(
|
ext.copy(
|
||||||
|
|
|
||||||
|
|
@ -4,21 +4,21 @@ import android.content.pm.PackageInfo
|
||||||
import androidx.core.content.pm.PackageInfoCompat
|
import androidx.core.content.pm.PackageInfoCompat
|
||||||
import eu.kanade.domain.source.service.SourcePreferences
|
import eu.kanade.domain.source.service.SourcePreferences
|
||||||
import eu.kanade.tachiyomi.util.system.isDebugBuildType
|
import eu.kanade.tachiyomi.util.system.isDebugBuildType
|
||||||
import mihon.domain.extension.model.KOMIKKU_SIGNATURE
|
import mihon.domain.extensionrepo.interactor.CreateExtensionRepo
|
||||||
import mihon.domain.extension.repository.ExtensionStoreRepository
|
import mihon.domain.extensionrepo.repository.ExtensionRepoRepository
|
||||||
import tachiyomi.core.common.preference.getAndSet
|
import tachiyomi.core.common.preference.getAndSet
|
||||||
|
|
||||||
class TrustExtension(
|
class TrustExtension(
|
||||||
private val repository: ExtensionStoreRepository,
|
private val extensionRepoRepository: ExtensionRepoRepository,
|
||||||
private val preferences: SourcePreferences,
|
private val preferences: SourcePreferences,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
suspend fun isTrusted(pkgInfo: PackageInfo, fingerprints: List<String>): Boolean {
|
suspend fun isTrusted(pkgInfo: PackageInfo, fingerprints: List<String>): Boolean {
|
||||||
// KMK -->
|
// KMK -->
|
||||||
if (isDebugBuildType) return true
|
if (isDebugBuildType) return true
|
||||||
if (fingerprints.contains(KOMIKKU_SIGNATURE)) return true
|
if (fingerprints.contains(CreateExtensionRepo.KOMIKKU_SIGNATURE)) return true
|
||||||
// KMK <--
|
// 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()}"
|
val key = "${pkgInfo.packageName}:${PackageInfoCompat.getLongVersionCode(pkgInfo)}:${fingerprints.last()}"
|
||||||
return trustedFingerprints.any { fingerprints.contains(it) } || key in preferences.trustedExtensions().get()
|
return trustedFingerprints.any { fingerprints.contains(it) } || key in preferences.trustedExtensions().get()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,8 @@ import java.util.UUID
|
||||||
class SyncPreferences(
|
class SyncPreferences(
|
||||||
private val preferenceStore: PreferenceStore,
|
private val preferenceStore: PreferenceStore,
|
||||||
) {
|
) {
|
||||||
fun clientHost() = preferenceStore.getString("connection_sync_client_host", "https://sync.tachiyomi.org")
|
fun clientHost() = preferenceStore.getString("sync_client_host", "https://sync.tachiyomi.org")
|
||||||
fun clientAPIKey() = preferenceStore.getString("connection_sync_client_api_key", "")
|
fun clientAPIKey() = preferenceStore.getString("sync_client_api_key", "")
|
||||||
fun lastSyncTimestamp() = preferenceStore.getLong(Preference.appStateKey("last_sync_timestamp"), 0L)
|
fun lastSyncTimestamp() = preferenceStore.getLong(Preference.appStateKey("last_sync_timestamp"), 0L)
|
||||||
|
|
||||||
fun lastSyncEtag() = preferenceStore.getString("sync_etag", "")
|
fun lastSyncEtag() = preferenceStore.getString("sync_etag", "")
|
||||||
|
|
@ -20,19 +20,19 @@ class SyncPreferences(
|
||||||
fun syncService() = preferenceStore.getInt("sync_service", 0)
|
fun syncService() = preferenceStore.getInt("sync_service", 0)
|
||||||
|
|
||||||
// KMK -->
|
// KMK -->
|
||||||
fun webDavUrl() = preferenceStore.getString("connection_webdav_url", "")
|
fun webDavUrl() = preferenceStore.getString("webdav_url", "")
|
||||||
fun webDavUsername() = preferenceStore.getString("connection_webdav_username", "")
|
fun webDavUsername() = preferenceStore.getString("webdav_username", "")
|
||||||
fun webDavPassword() = preferenceStore.getString("connection_webdav_password", "")
|
fun webDavPassword() = preferenceStore.getString("webdav_password", "")
|
||||||
fun webDavFolder() = preferenceStore.getString("connection_webdav_folder", "komikku")
|
fun webDavFolder() = preferenceStore.getString("webdav_folder", "komikku")
|
||||||
// KMK <--
|
// KMK <--
|
||||||
|
|
||||||
fun googleDriveAccessToken() = preferenceStore.getString(
|
fun googleDriveAccessToken() = preferenceStore.getString(
|
||||||
Preference.appStateKey("connection_google_drive_access_token"),
|
Preference.appStateKey("google_drive_access_token"),
|
||||||
"",
|
"",
|
||||||
)
|
)
|
||||||
|
|
||||||
fun googleDriveRefreshToken() = preferenceStore.getString(
|
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 {
|
fun getSyncSettings(): SyncSettings {
|
||||||
return SyncSettings(
|
return SyncSettings(
|
||||||
libraryEntries = preferenceStore.getBoolean("sync_library_entries", true).get(),
|
libraryEntries = preferenceStore.getBoolean("library_entries", true).get(),
|
||||||
categories = preferenceStore.getBoolean("sync_categories", true).get(),
|
categories = preferenceStore.getBoolean("categories", true).get(),
|
||||||
chapters = preferenceStore.getBoolean("sync_chapters", true).get(),
|
chapters = preferenceStore.getBoolean("chapters", true).get(),
|
||||||
tracking = preferenceStore.getBoolean("sync_tracking", true).get(),
|
tracking = preferenceStore.getBoolean("tracking", true).get(),
|
||||||
history = preferenceStore.getBoolean("sync_history", true).get(),
|
history = preferenceStore.getBoolean("history", true).get(),
|
||||||
appSettings = preferenceStore.getBoolean("sync_appSettings", true).get(),
|
appSettings = preferenceStore.getBoolean("appSettings", true).get(),
|
||||||
extensionStores = preferenceStore.getBoolean("sync_extensionStores", true).get(),
|
extensionRepoSettings = preferenceStore.getBoolean("extensionRepoSettings", true).get(),
|
||||||
sourceSettings = preferenceStore.getBoolean("sync_sourceSettings", true).get(),
|
sourceSettings = preferenceStore.getBoolean("sourceSettings", true).get(),
|
||||||
privateSettings = preferenceStore.getBoolean("sync_privateSettings", true).get(),
|
privateSettings = preferenceStore.getBoolean("privateSettings", true).get(),
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
customInfo = preferenceStore.getBoolean("sync_customInfo", true).get(),
|
customInfo = preferenceStore.getBoolean("customInfo", true).get(),
|
||||||
readEntries = preferenceStore.getBoolean("sync_readEntries", true).get(),
|
readEntries = preferenceStore.getBoolean("readEntries", true).get(),
|
||||||
savedSearchesFeeds = preferenceStore.getBoolean("sync_savedSearchesFeeds", true).get(),
|
savedSearchesFeeds = preferenceStore.getBoolean("savedSearchesFeeds", true).get(),
|
||||||
// SY <--
|
// SY <--
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun setSyncSettings(syncSettings: SyncSettings) {
|
fun setSyncSettings(syncSettings: SyncSettings) {
|
||||||
preferenceStore.getBoolean("sync_library_entries", true).set(syncSettings.libraryEntries)
|
preferenceStore.getBoolean("library_entries", true).set(syncSettings.libraryEntries)
|
||||||
preferenceStore.getBoolean("sync_categories", true).set(syncSettings.categories)
|
preferenceStore.getBoolean("categories", true).set(syncSettings.categories)
|
||||||
preferenceStore.getBoolean("sync_chapters", true).set(syncSettings.chapters)
|
preferenceStore.getBoolean("chapters", true).set(syncSettings.chapters)
|
||||||
preferenceStore.getBoolean("sync_tracking", true).set(syncSettings.tracking)
|
preferenceStore.getBoolean("tracking", true).set(syncSettings.tracking)
|
||||||
preferenceStore.getBoolean("sync_history", true).set(syncSettings.history)
|
preferenceStore.getBoolean("history", true).set(syncSettings.history)
|
||||||
preferenceStore.getBoolean("sync_appSettings", true).set(syncSettings.appSettings)
|
preferenceStore.getBoolean("appSettings", true).set(syncSettings.appSettings)
|
||||||
preferenceStore.getBoolean("sync_extensionStores", true).set(syncSettings.extensionStores)
|
preferenceStore.getBoolean("extensionRepoSettings", true).set(syncSettings.extensionRepoSettings)
|
||||||
preferenceStore.getBoolean("sync_sourceSettings", true).set(syncSettings.sourceSettings)
|
preferenceStore.getBoolean("sourceSettings", true).set(syncSettings.sourceSettings)
|
||||||
preferenceStore.getBoolean("sync_privateSettings", true).set(syncSettings.privateSettings)
|
preferenceStore.getBoolean("privateSettings", true).set(syncSettings.privateSettings)
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
preferenceStore.getBoolean("sync_customInfo", true).set(syncSettings.customInfo)
|
preferenceStore.getBoolean("customInfo", true).set(syncSettings.customInfo)
|
||||||
preferenceStore.getBoolean("sync_readEntries", true).set(syncSettings.readEntries)
|
preferenceStore.getBoolean("readEntries", true).set(syncSettings.readEntries)
|
||||||
preferenceStore.getBoolean("sync_savedSearchesFeeds", true).set(syncSettings.savedSearchesFeeds)
|
preferenceStore.getBoolean("savedSearchesFeeds", true).set(syncSettings.savedSearchesFeeds)
|
||||||
// SY <--
|
// SY <--
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ data class SyncSettings(
|
||||||
val tracking: Boolean = true,
|
val tracking: Boolean = true,
|
||||||
val history: Boolean = true,
|
val history: Boolean = true,
|
||||||
val appSettings: Boolean = true,
|
val appSettings: Boolean = true,
|
||||||
val extensionStores: Boolean = true,
|
val extensionRepoSettings: Boolean = true,
|
||||||
val sourceSettings: Boolean = true,
|
val sourceSettings: Boolean = true,
|
||||||
val privateSettings: Boolean = false,
|
val privateSettings: Boolean = false,
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -86,12 +86,12 @@ fun ExtensionDetailsScreen(
|
||||||
val uriHandler = LocalUriHandler.current
|
val uriHandler = LocalUriHandler.current
|
||||||
val url = remember(state.extension) {
|
val url = remember(state.extension) {
|
||||||
val regex = """https://raw.githubusercontent.com/(.+?)/(.+?)/.+""".toRegex()
|
val regex = """https://raw.githubusercontent.com/(.+?)/(.+?)/.+""".toRegex()
|
||||||
regex.find(state.extension?.store?.indexUrl.orEmpty())
|
regex.find(state.extension?.repoUrl.orEmpty())
|
||||||
?.let {
|
?.let {
|
||||||
val (user, repo) = it.destructured
|
val (user, repo) = it.destructured
|
||||||
"https://github.com/$user/$repo"
|
"https://github.com/$user/$repo"
|
||||||
}
|
}
|
||||||
?: state.extension?.store?.indexUrl
|
?: state.extension?.repoUrl
|
||||||
}
|
}
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
|
|
@ -270,17 +270,14 @@ private fun DetailsHeader(
|
||||||
|
|
||||||
if (extension is Extension.Installed) {
|
if (extension is Extension.Installed) {
|
||||||
append("\n\n")
|
append("\n\n")
|
||||||
appendLine(
|
append(
|
||||||
"""
|
"""
|
||||||
Update available: ${extension.hasUpdate}
|
Update available: ${extension.hasUpdate}
|
||||||
Orphaned: ${extension.isObsolete}
|
Obsolete: ${extension.isObsolete}
|
||||||
Shared: ${extension.isShared}
|
Shared: ${extension.isShared}
|
||||||
|
Repository: ${extension.repoUrl}
|
||||||
""".trimIndent(),
|
""".trimIndent(),
|
||||||
)
|
)
|
||||||
val store = extension.store
|
|
||||||
if (store != null) {
|
|
||||||
append("Repository: ${store.indexUrl}")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
context.copyToClipboard("Extension Debug information", extDebugInfo)
|
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.browse.components.ExtensionIcon
|
||||||
import eu.kanade.presentation.components.WarningBanner
|
import eu.kanade.presentation.components.WarningBanner
|
||||||
import eu.kanade.presentation.manga.components.DotSeparatorNoSpaceText
|
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.animateItemFastScroll
|
||||||
import eu.kanade.presentation.util.rememberRequestPackageInstallsPermissionState
|
import eu.kanade.presentation.util.rememberRequestPackageInstallsPermissionState
|
||||||
import eu.kanade.tachiyomi.extension.model.Extension
|
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.LocaleHelper
|
||||||
import eu.kanade.tachiyomi.util.system.launchRequestPackageInstallsPermission
|
import eu.kanade.tachiyomi.util.system.launchRequestPackageInstallsPermission
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
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.MR
|
||||||
import tachiyomi.i18n.kmk.KMR
|
import tachiyomi.i18n.kmk.KMR
|
||||||
import tachiyomi.i18n.sy.SYMR
|
import tachiyomi.i18n.sy.SYMR
|
||||||
|
|
@ -112,9 +110,9 @@ fun ExtensionScreen(
|
||||||
modifier = Modifier.padding(contentPadding),
|
modifier = Modifier.padding(contentPadding),
|
||||||
actions = persistentListOf(
|
actions = persistentListOf(
|
||||||
EmptyScreenAction(
|
EmptyScreenAction(
|
||||||
stringRes = MR.strings.extensionStores,
|
stringRes = MR.strings.label_extension_repos,
|
||||||
icon = Icons.Outlined.Settings,
|
icon = Icons.Outlined.Settings,
|
||||||
onClick = { navigator.push(ExtensionStoresScreen()) },
|
onClick = { navigator.push(ExtensionReposScreen()) },
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
@ -197,9 +195,9 @@ private fun ExtensionContent(
|
||||||
// KMK -->
|
// KMK -->
|
||||||
KMR.strings.extensions_page_more -> {
|
KMR.strings.extensions_page_more -> {
|
||||||
{
|
{
|
||||||
Button(onClick = { navigator?.push(ExtensionStoresScreen()) }) {
|
Button(onClick = { navigator?.push(ExtensionReposScreen()) }) {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(MR.strings.action_addExtensionStore),
|
text = stringResource(MR.strings.action_add_repo),
|
||||||
style = LocalTextStyle.current.copy(
|
style = LocalTextStyle.current.copy(
|
||||||
color = MaterialTheme.colorScheme.onPrimary,
|
color = MaterialTheme.colorScheme.onPrimary,
|
||||||
),
|
),
|
||||||
|
|
@ -403,7 +401,7 @@ private fun ExtensionItemContent(
|
||||||
}
|
}
|
||||||
|
|
||||||
// KMK -->
|
// KMK -->
|
||||||
Text(text = extension.storeName?.let { "@$it" } ?: "(?)")
|
Text(text = extension.repoName?.let { "@$it" } ?: "(?)")
|
||||||
// KMK <--
|
// KMK <--
|
||||||
|
|
||||||
val warning = when {
|
val warning = when {
|
||||||
|
|
@ -606,11 +604,11 @@ private fun ExtensionItemContentPreview() {
|
||||||
libVersion = 1.0,
|
libVersion = 1.0,
|
||||||
isNsfw = true,
|
isNsfw = true,
|
||||||
signatureHash = "900000",
|
signatureHash = "900000",
|
||||||
storeName = "Repository",
|
repoName = "Repository",
|
||||||
sources = emptyList(),
|
sources = emptyList(),
|
||||||
apkUrl = "Test",
|
apkName = "Test",
|
||||||
iconUrl = "",
|
iconUrl = "",
|
||||||
store = ExtensionStore("https://repo", "Komikku", "", KOMIKKU_SIGNATURE, ExtensionStore.Contact("", ""), false),
|
repoUrl = "",
|
||||||
)
|
)
|
||||||
val extInstalled = Extension.Installed(
|
val extInstalled = Extension.Installed(
|
||||||
name = "Tachiyomi",
|
name = "Tachiyomi",
|
||||||
|
|
@ -621,9 +619,9 @@ private fun ExtensionItemContentPreview() {
|
||||||
libVersion = 1.0,
|
libVersion = 1.0,
|
||||||
isNsfw = true,
|
isNsfw = true,
|
||||||
signatureHash = "900000",
|
signatureHash = "900000",
|
||||||
storeName = "Repository",
|
repoName = "Repository",
|
||||||
sources = emptyList(),
|
sources = emptyList(),
|
||||||
store = ExtensionStore("https://repo", "Komikku", "", KOMIKKU_SIGNATURE, ExtensionStore.Contact("", ""), false),
|
repoUrl = "",
|
||||||
pkgFactory = null,
|
pkgFactory = null,
|
||||||
icon = null,
|
icon = null,
|
||||||
hasUpdate = false,
|
hasUpdate = false,
|
||||||
|
|
@ -640,12 +638,12 @@ private fun ExtensionItemContentPreview() {
|
||||||
libVersion = 1.0,
|
libVersion = 1.0,
|
||||||
isNsfw = true,
|
isNsfw = true,
|
||||||
signatureHash = "900000",
|
signatureHash = "900000",
|
||||||
storeName = "Repository",
|
repoName = "Repository",
|
||||||
)
|
)
|
||||||
Column {
|
Column {
|
||||||
ExtensionItemContent(
|
ExtensionItemContent(
|
||||||
extension = extAvail.copy(
|
extension = extAvail.copy(
|
||||||
storeName = "Repository extensions minion multiple languages various sources",
|
repoName = "Repository extensions minion multiple languages various sources",
|
||||||
),
|
),
|
||||||
installStep = InstallStep.Idle,
|
installStep = InstallStep.Idle,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -336,46 +336,47 @@ fun SourceFeedToolbar(
|
||||||
// KMK -->
|
// KMK -->
|
||||||
actions = {
|
actions = {
|
||||||
AppBarActions(
|
AppBarActions(
|
||||||
actions = persistentListOf<AppBar.AppBarAction>().builder().apply {
|
actions = persistentListOf<AppBar.AppBarAction>().builder()
|
||||||
add(bulkSelectionButton(isRunning, toggleSelectionMode))
|
.apply {
|
||||||
|
add(bulkSelectionButton(isRunning, toggleSelectionMode))
|
||||||
|
|
||||||
onWebViewClick?.let { func ->
|
onWebViewClick?.let {
|
||||||
add(
|
add(
|
||||||
AppBar.Action(
|
AppBar.Action(
|
||||||
title = stringResource(MR.strings.action_web_view),
|
title = stringResource(MR.strings.action_web_view),
|
||||||
onClick = { func() },
|
onClick = { onWebViewClick() },
|
||||||
icon = Icons.Outlined.Public,
|
icon = Icons.Outlined.Public,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// KMK -->
|
// KMK -->
|
||||||
add(
|
|
||||||
AppBar.OverflowAction(
|
|
||||||
title = stringResource(MR.strings.pref_incognito_mode),
|
|
||||||
onClick = onToggleIncognito,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
// KMK <--
|
|
||||||
|
|
||||||
onSortFeedClick?.let { func ->
|
|
||||||
add(
|
add(
|
||||||
AppBar.OverflowAction(
|
AppBar.OverflowAction(
|
||||||
title = stringResource(KMR.strings.action_sort_feed),
|
title = stringResource(MR.strings.pref_incognito_mode),
|
||||||
onClick = { func() },
|
onClick = onToggleIncognito,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
// KMK <--
|
||||||
|
|
||||||
onSourceSettingClick?.let { func ->
|
onSortFeedClick?.let {
|
||||||
add(
|
add(
|
||||||
AppBar.OverflowAction(
|
AppBar.OverflowAction(
|
||||||
title = stringResource(MR.strings.label_settings),
|
title = stringResource(KMR.strings.action_sort_feed),
|
||||||
onClick = { func() },
|
onClick = { onSortFeedClick() },
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
onSourceSettingClick?.let {
|
||||||
|
add(
|
||||||
|
AppBar.OverflowAction(
|
||||||
|
title = stringResource(MR.strings.label_settings),
|
||||||
|
onClick = { onSourceSettingClick() },
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
.build(),
|
.build(),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -209,7 +209,7 @@ fun BrowseSourceEHentaiListItem(
|
||||||
MangaCoverHide.Book(
|
MangaCoverHide.Book(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxHeight(),
|
.fillMaxHeight(),
|
||||||
bgColor = bgColor ?: MaterialTheme.colorScheme.surface.takeIf { isSelected },
|
bgColor = bgColor ?: (MaterialTheme.colorScheme.surface.takeIf { isSelected }),
|
||||||
tint = onBgColor,
|
tint = onBgColor,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -219,7 +219,7 @@ fun BrowseSourceEHentaiListItem(
|
||||||
.fillMaxHeight(),
|
.fillMaxHeight(),
|
||||||
// KMK -->
|
// KMK -->
|
||||||
alpha = if (isSelected) GRID_SELECTED_COVER_ALPHA else coverAlpha,
|
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,
|
tint = onBgColor,
|
||||||
// KMK <--
|
// KMK <--
|
||||||
data = coverData,
|
data = coverData,
|
||||||
|
|
|
||||||
|
|
@ -39,26 +39,27 @@ fun BrowseSourceSimpleToolbar(
|
||||||
var selectingDisplayMode by remember { mutableStateOf(false) }
|
var selectingDisplayMode by remember { mutableStateOf(false) }
|
||||||
// KMK -->
|
// KMK -->
|
||||||
AppBarActions(
|
AppBarActions(
|
||||||
actions = persistentListOf<AppBar.AppBarAction>().builder().apply {
|
actions = persistentListOf<AppBar.AppBarAction>().builder()
|
||||||
displayMode?.let { mode ->
|
.apply {
|
||||||
add(
|
displayMode?.let {
|
||||||
AppBar.Action(
|
add(
|
||||||
title = stringResource(MR.strings.action_display_mode),
|
AppBar.Action(
|
||||||
icon = if (mode == LibraryDisplayMode.List) {
|
title = stringResource(MR.strings.action_display_mode),
|
||||||
Icons.AutoMirrored.Filled.ViewList
|
icon = if (displayMode == LibraryDisplayMode.List) {
|
||||||
} else {
|
Icons.AutoMirrored.Filled.ViewList
|
||||||
Icons.Filled.ViewModule
|
} else {
|
||||||
},
|
Icons.Filled.ViewModule
|
||||||
onClick = { selectingDisplayMode = true },
|
},
|
||||||
),
|
onClick = { selectingDisplayMode = true },
|
||||||
)
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
toggleSelectionMode?.let {
|
||||||
|
add(
|
||||||
|
bulkSelectionButton(isRunning, toggleSelectionMode),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
toggleSelectionMode?.let { mode ->
|
|
||||||
add(
|
|
||||||
bulkSelectionButton(isRunning, mode),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.build(),
|
.build(),
|
||||||
)
|
)
|
||||||
// KMK <--
|
// KMK <--
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,7 @@ fun Screen.BulkFavoriteDialogs(
|
||||||
AddDuplicateMangaDialog(
|
AddDuplicateMangaDialog(
|
||||||
dialog = dialog,
|
dialog = dialog,
|
||||||
navigator = navigator,
|
navigator = navigator,
|
||||||
|
state = bulkFavoriteState,
|
||||||
onDismiss = bulkFavoriteScreenModel::dismissDialog,
|
onDismiss = bulkFavoriteScreenModel::dismissDialog,
|
||||||
stopRunning = bulkFavoriteScreenModel::stopRunning,
|
stopRunning = bulkFavoriteScreenModel::stopRunning,
|
||||||
toggleSelectionMode = bulkFavoriteScreenModel::toggleSelectionMode,
|
toggleSelectionMode = bulkFavoriteScreenModel::toggleSelectionMode,
|
||||||
|
|
@ -133,9 +134,10 @@ private fun Screen.ShowMigrateDialog(
|
||||||
private fun AddDuplicateMangaDialog(
|
private fun AddDuplicateMangaDialog(
|
||||||
dialog: Dialog.AddDuplicateManga,
|
dialog: Dialog.AddDuplicateManga,
|
||||||
navigator: Navigator?,
|
navigator: Navigator?,
|
||||||
|
state: BulkFavoriteScreenModel.State,
|
||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
stopRunning: () -> Unit,
|
stopRunning: () -> Unit,
|
||||||
toggleSelectionMode: (Boolean) -> Unit,
|
toggleSelectionMode: () -> Unit,
|
||||||
addFavorite: (Manga) -> Unit,
|
addFavorite: (Manga) -> Unit,
|
||||||
showMigrateDialog: (manga: Manga, duplicate: Manga) -> Unit,
|
showMigrateDialog: (manga: Manga, duplicate: Manga) -> Unit,
|
||||||
) {
|
) {
|
||||||
|
|
@ -145,7 +147,9 @@ private fun AddDuplicateMangaDialog(
|
||||||
duplicates = dialog.duplicates,
|
duplicates = dialog.duplicates,
|
||||||
onDismissRequest = onDismiss,
|
onDismissRequest = onDismiss,
|
||||||
onConfirm = {
|
onConfirm = {
|
||||||
toggleSelectionMode(false)
|
if (state.selectionMode) {
|
||||||
|
toggleSelectionMode()
|
||||||
|
}
|
||||||
addFavorite(dialog.manga)
|
addFavorite(dialog.manga)
|
||||||
},
|
},
|
||||||
onOpenManga = { navigator?.push(MangaScreen(it.id)) },
|
onOpenManga = { navigator?.push(MangaScreen(it.id)) },
|
||||||
|
|
|
||||||
|
|
@ -30,51 +30,53 @@ fun BulkSelectionToolbar(
|
||||||
titleContent = { Text(text = "$selectedCount") },
|
titleContent = { Text(text = "$selectedCount") },
|
||||||
actions = {
|
actions = {
|
||||||
AppBarActions(
|
AppBarActions(
|
||||||
actions = persistentListOf<AppBar.AppBarAction>().builder().apply {
|
actions = persistentListOf<AppBar.AppBarAction>()
|
||||||
if (onSelectAll != null) {
|
.builder()
|
||||||
add(
|
.apply {
|
||||||
AppBar.Action(
|
if (onSelectAll != null) {
|
||||||
title = stringResource(MR.strings.action_select_all),
|
add(
|
||||||
icon = Icons.Filled.SelectAll,
|
AppBar.Action(
|
||||||
onClick = onSelectAll,
|
title = stringResource(MR.strings.action_select_all),
|
||||||
),
|
icon = Icons.Filled.SelectAll,
|
||||||
)
|
onClick = onSelectAll,
|
||||||
}
|
),
|
||||||
if (onReverseSelection != null) {
|
)
|
||||||
add(
|
}
|
||||||
AppBar.Action(
|
if (onReverseSelection != null) {
|
||||||
title = stringResource(MR.strings.action_select_inverse),
|
add(
|
||||||
icon = Icons.Outlined.FlipToBack,
|
AppBar.Action(
|
||||||
onClick = onReverseSelection,
|
title = stringResource(MR.strings.action_select_inverse),
|
||||||
),
|
icon = Icons.Outlined.FlipToBack,
|
||||||
)
|
onClick = onReverseSelection,
|
||||||
}
|
),
|
||||||
if (isRunning) {
|
)
|
||||||
add(
|
}
|
||||||
AppBar.ActionCompose(
|
if (isRunning) {
|
||||||
title = stringResource(KMR.strings.action_bulk_select),
|
add(
|
||||||
) {
|
AppBar.ActionCompose(
|
||||||
CircularProgressIndicator(
|
title = stringResource(KMR.strings.action_bulk_select),
|
||||||
modifier = Modifier
|
) {
|
||||||
.size(24.dp),
|
CircularProgressIndicator(
|
||||||
strokeWidth = 2.dp,
|
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()
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
),
|
)
|
||||||
)
|
} else {
|
||||||
}
|
add(
|
||||||
}.build(),
|
AppBar.Action(
|
||||||
|
title = stringResource(MR.strings.add_to_library),
|
||||||
|
icon = Icons.Filled.Favorite,
|
||||||
|
onClick = {
|
||||||
|
if (selectedCount > 0) {
|
||||||
|
onChangeCategoryClick()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}.build(),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
isActionMode = true,
|
isActionMode = true,
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,6 @@ private fun DownloadDropdownMenuItems(
|
||||||
DownloadAction.NEXT_10_CHAPTERS to pluralStringResource(MR.plurals.download_amount, 10, 10),
|
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.NEXT_25_CHAPTERS to pluralStringResource(MR.plurals.download_amount, 25, 25),
|
||||||
DownloadAction.UNREAD_CHAPTERS to stringResource(MR.strings.download_unread),
|
DownloadAction.UNREAD_CHAPTERS to stringResource(MR.strings.download_unread),
|
||||||
DownloadAction.BOOKMARKED_CHAPTERS to stringResource(MR.strings.download_bookmarked),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
options.forEach { (downloadAction, string) ->
|
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_chapter_fetch_date to LibrarySort.Type.ChapterFetchDate,
|
||||||
MR.strings.action_sort_date_added to LibrarySort.Type.DateAdded,
|
MR.strings.action_sort_date_added to LibrarySort.Type.DateAdded,
|
||||||
trackerMeanPair,
|
trackerMeanPair,
|
||||||
SYMR.strings.action_sort_estimated_time_left to LibrarySort.Type.EstimatedTimeLeft,
|
|
||||||
// SY -->
|
// SY -->
|
||||||
tagSortPair,
|
tagSortPair,
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
@ -366,10 +365,6 @@ private fun ColumnScope.DisplayPage(
|
||||||
label = stringResource(MR.strings.action_display_unread_badge),
|
label = stringResource(MR.strings.action_display_unread_badge),
|
||||||
pref = screenModel.libraryPreferences.unreadBadge(),
|
pref = screenModel.libraryPreferences.unreadBadge(),
|
||||||
)
|
)
|
||||||
CheckboxItem(
|
|
||||||
label = stringResource(SYMR.strings.action_display_estimated_time_left_badge),
|
|
||||||
pref = screenModel.libraryPreferences.estimatedTimeLeftBadge(),
|
|
||||||
)
|
|
||||||
CheckboxItem(
|
CheckboxItem(
|
||||||
label = stringResource(MR.strings.action_display_local_badge),
|
label = stringResource(MR.strings.action_display_local_badge),
|
||||||
pref = screenModel.libraryPreferences.localBadge(),
|
pref = screenModel.libraryPreferences.localBadge(),
|
||||||
|
|
|
||||||
|
|
@ -105,7 +105,7 @@ fun MangaCompactGridItem(
|
||||||
MangaCoverHide.Book(
|
MangaCoverHide.Book(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth(),
|
.fillMaxWidth(),
|
||||||
bgColor = bgColor ?: MaterialTheme.colorScheme.surface.takeIf { isSelected },
|
bgColor = bgColor ?: (MaterialTheme.colorScheme.surface.takeIf { isSelected }),
|
||||||
tint = onBgColor,
|
tint = onBgColor,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -119,7 +119,7 @@ fun MangaCompactGridItem(
|
||||||
data = coverData,
|
data = coverData,
|
||||||
// KMK -->
|
// KMK -->
|
||||||
alpha = if (isSelected) GRID_SELECTED_COVER_ALPHA else coverAlpha,
|
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,
|
tint = onBgColor,
|
||||||
// KMK <--
|
// KMK <--
|
||||||
)
|
)
|
||||||
|
|
@ -241,7 +241,7 @@ fun MangaComfortableGridItem(
|
||||||
MangaCoverHide.Book(
|
MangaCoverHide.Book(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth(),
|
.fillMaxWidth(),
|
||||||
bgColor = bgColor ?: MaterialTheme.colorScheme.surface.takeIf { isSelected },
|
bgColor = bgColor ?: (MaterialTheme.colorScheme.surface.takeIf { isSelected }),
|
||||||
tint = onBgColor,
|
tint = onBgColor,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -255,7 +255,7 @@ fun MangaComfortableGridItem(
|
||||||
data = coverData,
|
data = coverData,
|
||||||
// KMK -->
|
// KMK -->
|
||||||
alpha = if (isSelected) GRID_SELECTED_COVER_ALPHA else coverAlpha,
|
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,
|
tint = onBgColor,
|
||||||
onCoverLoaded = { _, result ->
|
onCoverLoaded = { _, result ->
|
||||||
val image = result.result.image
|
val image = result.result.image
|
||||||
|
|
@ -274,7 +274,7 @@ fun MangaComfortableGridItem(
|
||||||
data = coverData,
|
data = coverData,
|
||||||
// KMK -->
|
// KMK -->
|
||||||
alpha = if (isSelected) GRID_SELECTED_COVER_ALPHA else coverAlpha,
|
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,
|
tint = onBgColor,
|
||||||
onCoverLoaded = { _, result ->
|
onCoverLoaded = { _, result ->
|
||||||
val image = result.result.image
|
val image = result.result.image
|
||||||
|
|
@ -461,7 +461,7 @@ fun MangaListItem(
|
||||||
MangaCoverHide.Square(
|
MangaCoverHide.Square(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxHeight(),
|
.fillMaxHeight(),
|
||||||
bgColor = bgColor ?: MaterialTheme.colorScheme.surface.takeIf { isSelected },
|
bgColor = bgColor ?: (MaterialTheme.colorScheme.surface.takeIf { isSelected }),
|
||||||
tint = onBgColor,
|
tint = onBgColor,
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -475,7 +475,7 @@ fun MangaListItem(
|
||||||
data = coverData,
|
data = coverData,
|
||||||
// KMK -->
|
// KMK -->
|
||||||
alpha = coverAlpha,
|
alpha = coverAlpha,
|
||||||
bgColor = bgColor ?: MaterialTheme.colorScheme.surface.takeIf { isSelected },
|
bgColor = bgColor ?: (MaterialTheme.colorScheme.surface.takeIf { isSelected }),
|
||||||
tint = onBgColor,
|
tint = onBgColor,
|
||||||
size = MangaCover.Size.Big,
|
size = MangaCover.Size.Big,
|
||||||
// KMK <--
|
// KMK <--
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ import androidx.compose.material.icons.outlined.Folder
|
||||||
import androidx.compose.material.icons.outlined.LocalLibrary
|
import androidx.compose.material.icons.outlined.LocalLibrary
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.scale
|
import androidx.compose.ui.draw.scale
|
||||||
import androidx.compose.ui.graphics.Color
|
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.domain.source.model.icon
|
||||||
import eu.kanade.presentation.theme.TachiyomiPreviewTheme
|
import eu.kanade.presentation.theme.TachiyomiPreviewTheme
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.presentation.util.toDurationString
|
|
||||||
import tachiyomi.domain.source.model.Source
|
import tachiyomi.domain.source.model.Source
|
||||||
import tachiyomi.presentation.core.components.Badge
|
import tachiyomi.presentation.core.components.Badge
|
||||||
import tachiyomi.presentation.core.components.BadgeGroup
|
import tachiyomi.presentation.core.components.BadgeGroup
|
||||||
import tachiyomi.source.local.isLocal
|
import tachiyomi.source.local.isLocal
|
||||||
import kotlin.time.DurationUnit
|
|
||||||
import kotlin.time.toDuration
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
internal fun DownloadsBadge(count: Long) {
|
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
|
@Composable
|
||||||
internal fun LanguageBadge(
|
internal fun LanguageBadge(
|
||||||
isLocal: Boolean,
|
isLocal: Boolean,
|
||||||
|
|
@ -141,7 +121,6 @@ private fun BadgePreview() {
|
||||||
BadgeGroup {
|
BadgeGroup {
|
||||||
DownloadsBadge(count = 10)
|
DownloadsBadge(count = 10)
|
||||||
UnreadBadge(count = 10)
|
UnreadBadge(count = 10)
|
||||||
EstimatedTimeLeftBadge(etaMillis = 4_500_000)
|
|
||||||
LanguageBadge(isLocal = true, sourceLanguage = "en")
|
LanguageBadge(isLocal = true, sourceLanguage = "en")
|
||||||
LanguageBadge(isLocal = false, sourceLanguage = "en", useLangIcon = false)
|
LanguageBadge(isLocal = false, sourceLanguage = "en", useLangIcon = false)
|
||||||
LanguageBadge(isLocal = false, sourceLanguage = "vi")
|
LanguageBadge(isLocal = false, sourceLanguage = "vi")
|
||||||
|
|
|
||||||
|
|
@ -49,7 +49,6 @@ internal fun LibraryComfortableGrid(
|
||||||
coverBadgeStart = {
|
coverBadgeStart = {
|
||||||
DownloadsBadge(count = libraryItem.downloadCount)
|
DownloadsBadge(count = libraryItem.downloadCount)
|
||||||
UnreadBadge(count = libraryItem.unreadCount)
|
UnreadBadge(count = libraryItem.unreadCount)
|
||||||
EstimatedTimeLeftBadge(etaMillis = libraryItem.estimatedTimeLeftMillis)
|
|
||||||
},
|
},
|
||||||
coverBadgeEnd = {
|
coverBadgeEnd = {
|
||||||
LanguageBadge(
|
LanguageBadge(
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,6 @@ internal fun LibraryCompactGrid(
|
||||||
coverBadgeStart = {
|
coverBadgeStart = {
|
||||||
DownloadsBadge(count = libraryItem.downloadCount)
|
DownloadsBadge(count = libraryItem.downloadCount)
|
||||||
UnreadBadge(count = libraryItem.unreadCount)
|
UnreadBadge(count = libraryItem.unreadCount)
|
||||||
EstimatedTimeLeftBadge(etaMillis = libraryItem.estimatedTimeLeftMillis)
|
|
||||||
},
|
},
|
||||||
coverBadgeEnd = {
|
coverBadgeEnd = {
|
||||||
LanguageBadge(
|
LanguageBadge(
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,6 @@ internal fun LibraryList(
|
||||||
badge = {
|
badge = {
|
||||||
DownloadsBadge(count = libraryItem.downloadCount)
|
DownloadsBadge(count = libraryItem.downloadCount)
|
||||||
UnreadBadge(count = libraryItem.unreadCount)
|
UnreadBadge(count = libraryItem.unreadCount)
|
||||||
EstimatedTimeLeftBadge(etaMillis = libraryItem.estimatedTimeLeftMillis)
|
|
||||||
LanguageBadge(
|
LanguageBadge(
|
||||||
isLocal = libraryItem.isLocal,
|
isLocal = libraryItem.isLocal,
|
||||||
sourceLanguage = libraryItem.sourceLanguage,
|
sourceLanguage = libraryItem.sourceLanguage,
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,7 @@ import androidx.compose.material3.Surface
|
||||||
import androidx.compose.material3.TopAppBarScrollBehavior
|
import androidx.compose.material3.TopAppBarScrollBehavior
|
||||||
import androidx.compose.material3.surfaceColorAtElevation
|
import androidx.compose.material3.surfaceColorAtElevation
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.derivedStateOf
|
import androidx.compose.runtime.derivedStateOf
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateListOf
|
import androidx.compose.runtime.mutableStateListOf
|
||||||
|
|
@ -38,7 +39,6 @@ import androidx.compose.ui.platform.LocalHapticFeedback
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import eu.kanade.presentation.components.AppBar
|
import eu.kanade.presentation.components.AppBar
|
||||||
import eu.kanade.presentation.components.AppBarActions
|
import eu.kanade.presentation.components.AppBarActions
|
||||||
import eu.kanade.presentation.libraryUpdateError.components.LibraryUpdateErrorUiModel
|
|
||||||
import eu.kanade.presentation.libraryUpdateError.components.libraryUpdateErrorUiItems
|
import eu.kanade.presentation.libraryUpdateError.components.libraryUpdateErrorUiItems
|
||||||
import eu.kanade.presentation.manga.components.Button
|
import eu.kanade.presentation.manga.components.Button
|
||||||
import eu.kanade.tachiyomi.ui.libraryUpdateError.LibraryUpdateErrorItem
|
import eu.kanade.tachiyomi.ui.libraryUpdateError.LibraryUpdateErrorItem
|
||||||
|
|
@ -67,7 +67,7 @@ fun LibraryUpdateErrorScreen(
|
||||||
onInvertSelection: () -> Unit,
|
onInvertSelection: () -> Unit,
|
||||||
onErrorsDelete: () -> Unit,
|
onErrorsDelete: () -> Unit,
|
||||||
onErrorDelete: (Long) -> Unit,
|
onErrorDelete: (Long) -> Unit,
|
||||||
onErrorSelected: (LibraryUpdateErrorItem, Boolean, Boolean) -> Unit,
|
onErrorSelected: (LibraryUpdateErrorItem, Boolean, Boolean, Boolean) -> Unit,
|
||||||
navigateUp: () -> Unit,
|
navigateUp: () -> Unit,
|
||||||
) {
|
) {
|
||||||
BackHandler(enabled = state.selectionMode, onBack = { onSelectAll(false) })
|
BackHandler(enabled = state.selectionMode, onBack = { onSelectAll(false) })
|
||||||
|
|
@ -75,33 +75,31 @@ fun LibraryUpdateErrorScreen(
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
val listState = rememberLazyListState()
|
val listState = rememberLazyListState()
|
||||||
|
|
||||||
val uiModels = remember(state) { state.getUiModel() }
|
val headerIndexes = remember { mutableStateOf<List<Int>>(emptyList()) }
|
||||||
val headerIndexes = remember(state) {
|
LaunchedEffect(state) {
|
||||||
uiModels.withIndex()
|
headerIndexes.value = state.getHeaderIndexes()
|
||||||
.filter { it.value is LibraryUpdateErrorUiModel.Header }
|
|
||||||
.map { it.index }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val enableScrollToTop by remember(listState) {
|
val enableScrollToTop by remember {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
listState.firstVisibleItemIndex > 0
|
listState.firstVisibleItemIndex > 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val enableScrollToBottom by remember(listState) {
|
val enableScrollToBottom by remember {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
listState.canScrollForward
|
listState.canScrollForward
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val enableScrollToPrevious by remember(headerIndexes, listState) {
|
val enableScrollToPrevious by remember {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
headerIndexes.any { it < listState.firstVisibleItemIndex }
|
headerIndexes.value.any { it < listState.firstVisibleItemIndex }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val enableScrollToNext by remember(headerIndexes, listState) {
|
val enableScrollToNext by remember {
|
||||||
derivedStateOf {
|
derivedStateOf {
|
||||||
headerIndexes.any { it > listState.firstVisibleItemIndex }
|
headerIndexes.value.any { it > listState.firstVisibleItemIndex }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -143,7 +141,7 @@ fun LibraryUpdateErrorScreen(
|
||||||
scrollToPrevious = {
|
scrollToPrevious = {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
listState.scrollToItem(
|
listState.scrollToItem(
|
||||||
headerIndexes
|
state.getHeaderIndexes()
|
||||||
.filter { it < listState.firstVisibleItemIndex }
|
.filter { it < listState.firstVisibleItemIndex }
|
||||||
.maxOrNull() ?: 0,
|
.maxOrNull() ?: 0,
|
||||||
)
|
)
|
||||||
|
|
@ -152,7 +150,7 @@ fun LibraryUpdateErrorScreen(
|
||||||
scrollToNext = {
|
scrollToNext = {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
listState.scrollToItem(
|
listState.scrollToItem(
|
||||||
headerIndexes
|
state.getHeaderIndexes()
|
||||||
.filter { it > listState.firstVisibleItemIndex }
|
.filter { it > listState.firstVisibleItemIndex }
|
||||||
.minOrNull() ?: 0,
|
.minOrNull() ?: 0,
|
||||||
)
|
)
|
||||||
|
|
@ -175,7 +173,7 @@ fun LibraryUpdateErrorScreen(
|
||||||
state = listState,
|
state = listState,
|
||||||
) {
|
) {
|
||||||
libraryUpdateErrorUiItems(
|
libraryUpdateErrorUiItems(
|
||||||
uiModels = uiModels,
|
uiModels = state.getUiModel(),
|
||||||
selectionMode = state.selectionMode,
|
selectionMode = state.selectionMode,
|
||||||
onErrorSelected = onErrorSelected,
|
onErrorSelected = onErrorSelected,
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ import tachiyomi.presentation.core.util.selectedBackground
|
||||||
internal fun LazyListScope.libraryUpdateErrorUiItems(
|
internal fun LazyListScope.libraryUpdateErrorUiItems(
|
||||||
uiModels: List<LibraryUpdateErrorUiModel>,
|
uiModels: List<LibraryUpdateErrorUiModel>,
|
||||||
selectionMode: Boolean,
|
selectionMode: Boolean,
|
||||||
onErrorSelected: (LibraryUpdateErrorItem, Boolean, Boolean) -> Unit,
|
onErrorSelected: (LibraryUpdateErrorItem, Boolean, Boolean, Boolean) -> Unit,
|
||||||
onClick: (LibraryUpdateErrorItem) -> Unit,
|
onClick: (LibraryUpdateErrorItem) -> Unit,
|
||||||
onClickCover: (LibraryUpdateErrorItem) -> Unit,
|
onClickCover: (LibraryUpdateErrorItem) -> Unit,
|
||||||
onDelete: (Long) -> Unit,
|
onDelete: (Long) -> Unit,
|
||||||
|
|
@ -81,6 +81,7 @@ internal fun LazyListScope.libraryUpdateErrorUiItems(
|
||||||
selectionMode -> onErrorSelected(
|
selectionMode -> onErrorSelected(
|
||||||
libraryUpdateErrorItem,
|
libraryUpdateErrorItem,
|
||||||
!libraryUpdateErrorItem.selected,
|
!libraryUpdateErrorItem.selected,
|
||||||
|
true,
|
||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -92,6 +93,7 @@ internal fun LazyListScope.libraryUpdateErrorUiItems(
|
||||||
libraryUpdateErrorItem,
|
libraryUpdateErrorItem,
|
||||||
!libraryUpdateErrorItem.selected,
|
!libraryUpdateErrorItem.selected,
|
||||||
true,
|
true,
|
||||||
|
true,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
onClickCover = { onClickCover(libraryUpdateErrorItem) }.takeIf { !selectionMode },
|
onClickCover = { onClickCover(libraryUpdateErrorItem) }.takeIf { !selectionMode },
|
||||||
|
|
@ -142,8 +144,8 @@ private fun LibraryUpdateErrorUiItem(
|
||||||
.combinedClickable(
|
.combinedClickable(
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
onLongClick = {
|
onLongClick = {
|
||||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
|
||||||
onLongClick()
|
onLongClick()
|
||||||
|
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.padding(horizontal = MaterialTheme.padding.medium),
|
.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.MangaDex
|
||||||
import eu.kanade.tachiyomi.source.online.all.NHentai
|
import eu.kanade.tachiyomi.source.online.all.NHentai
|
||||||
import eu.kanade.tachiyomi.source.online.english.EightMuses
|
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.Pururin
|
||||||
|
import eu.kanade.tachiyomi.source.online.english.Tsumino
|
||||||
import eu.kanade.tachiyomi.ui.manga.ChapterList
|
import eu.kanade.tachiyomi.ui.manga.ChapterList
|
||||||
import eu.kanade.tachiyomi.ui.manga.MangaScreenModel
|
import eu.kanade.tachiyomi.ui.manga.MangaScreenModel
|
||||||
import eu.kanade.tachiyomi.ui.manga.MergedMangaData
|
import eu.kanade.tachiyomi.ui.manga.MergedMangaData
|
||||||
|
|
@ -106,10 +108,12 @@ import exh.source.getMainSource
|
||||||
import exh.source.isEhBasedManga
|
import exh.source.isEhBasedManga
|
||||||
import exh.ui.metadata.adapters.EHentaiDescription
|
import exh.ui.metadata.adapters.EHentaiDescription
|
||||||
import exh.ui.metadata.adapters.EightMusesDescription
|
import exh.ui.metadata.adapters.EightMusesDescription
|
||||||
|
import exh.ui.metadata.adapters.HBrowseDescription
|
||||||
import exh.ui.metadata.adapters.LanraragiDescription
|
import exh.ui.metadata.adapters.LanraragiDescription
|
||||||
import exh.ui.metadata.adapters.MangaDexDescription
|
import exh.ui.metadata.adapters.MangaDexDescription
|
||||||
import exh.ui.metadata.adapters.NHentaiDescription
|
import exh.ui.metadata.adapters.NHentaiDescription
|
||||||
import exh.ui.metadata.adapters.PururinDescription
|
import exh.ui.metadata.adapters.PururinDescription
|
||||||
|
import exh.ui.metadata.adapters.TsuminoDescription
|
||||||
import tachiyomi.domain.chapter.model.Chapter
|
import tachiyomi.domain.chapter.model.Chapter
|
||||||
import tachiyomi.domain.chapter.service.missingChaptersCount
|
import tachiyomi.domain.chapter.service.missingChaptersCount
|
||||||
import tachiyomi.domain.library.service.LibraryPreferences
|
import tachiyomi.domain.library.service.LibraryPreferences
|
||||||
|
|
@ -190,7 +194,7 @@ fun MangaScreen(
|
||||||
onChapterSwipe: (ChapterList.Item, LibraryPreferences.ChapterSwipeAction) -> Unit,
|
onChapterSwipe: (ChapterList.Item, LibraryPreferences.ChapterSwipeAction) -> Unit,
|
||||||
|
|
||||||
// Chapter selection
|
// Chapter selection
|
||||||
onChapterSelected: (ChapterList.Item, Boolean, Boolean) -> Unit,
|
onChapterSelected: (ChapterList.Item, Boolean, Boolean, Boolean) -> Unit,
|
||||||
onAllChapterSelected: (Boolean) -> Unit,
|
onAllChapterSelected: (Boolean) -> Unit,
|
||||||
onInvertSelection: () -> Unit,
|
onInvertSelection: () -> Unit,
|
||||||
|
|
||||||
|
|
@ -400,7 +404,7 @@ private fun MangaScreenSmallImpl(
|
||||||
onChapterSwipe: (ChapterList.Item, LibraryPreferences.ChapterSwipeAction) -> Unit,
|
onChapterSwipe: (ChapterList.Item, LibraryPreferences.ChapterSwipeAction) -> Unit,
|
||||||
|
|
||||||
// Chapter selection
|
// Chapter selection
|
||||||
onChapterSelected: (ChapterList.Item, Boolean, Boolean) -> Unit,
|
onChapterSelected: (ChapterList.Item, Boolean, Boolean, Boolean) -> Unit,
|
||||||
onAllChapterSelected: (Boolean) -> Unit,
|
onAllChapterSelected: (Boolean) -> Unit,
|
||||||
onInvertSelection: () -> Unit,
|
onInvertSelection: () -> Unit,
|
||||||
|
|
||||||
|
|
@ -864,7 +868,7 @@ private fun MangaScreenLargeImpl(
|
||||||
onChapterSwipe: (ChapterList.Item, LibraryPreferences.ChapterSwipeAction) -> Unit,
|
onChapterSwipe: (ChapterList.Item, LibraryPreferences.ChapterSwipeAction) -> Unit,
|
||||||
|
|
||||||
// Chapter selection
|
// Chapter selection
|
||||||
onChapterSelected: (ChapterList.Item, Boolean, Boolean) -> Unit,
|
onChapterSelected: (ChapterList.Item, Boolean, Boolean, Boolean) -> Unit,
|
||||||
onAllChapterSelected: (Boolean) -> Unit,
|
onAllChapterSelected: (Boolean) -> Unit,
|
||||||
onInvertSelection: () -> Unit,
|
onInvertSelection: () -> Unit,
|
||||||
|
|
||||||
|
|
@ -1303,7 +1307,7 @@ private fun LazyListScope.sharedChapterItems(
|
||||||
// SY <--
|
// SY <--
|
||||||
onChapterClicked: (Chapter) -> Unit,
|
onChapterClicked: (Chapter) -> Unit,
|
||||||
onDownloadChapter: ((List<ChapterList.Item>, ChapterDownloadAction) -> 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,
|
onChapterSwipe: (ChapterList.Item, LibraryPreferences.ChapterSwipeAction) -> Unit,
|
||||||
) {
|
) {
|
||||||
items(
|
items(
|
||||||
|
|
@ -1372,14 +1376,14 @@ private fun LazyListScope.sharedChapterItems(
|
||||||
chapterSwipeStartAction = chapterSwipeStartAction,
|
chapterSwipeStartAction = chapterSwipeStartAction,
|
||||||
chapterSwipeEndAction = chapterSwipeEndAction,
|
chapterSwipeEndAction = chapterSwipeEndAction,
|
||||||
onLongClick = {
|
onLongClick = {
|
||||||
|
onChapterSelected(item, !item.selected, true, true)
|
||||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||||
onChapterSelected(item, !item.selected, true)
|
|
||||||
},
|
},
|
||||||
onClick = {
|
onClick = {
|
||||||
onChapterItemClick(
|
onChapterItemClick(
|
||||||
chapterItem = item,
|
chapterItem = item,
|
||||||
isAnyChapterSelected = isAnyChapterSelected,
|
isAnyChapterSelected = isAnyChapterSelected,
|
||||||
onToggleSelection = { onChapterSelected(item, !item.selected, false) },
|
onToggleSelection = { onChapterSelected(item, !item.selected, true, false) },
|
||||||
onChapterClicked = onChapterClicked,
|
onChapterClicked = onChapterClicked,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
@ -1434,9 +1438,15 @@ fun metadataDescription(source: Source): MetadataDescriptionComposable? {
|
||||||
is EightMuses -> { state, openMetadataViewer, _ ->
|
is EightMuses -> { state, openMetadataViewer, _ ->
|
||||||
EightMusesDescription(state, openMetadataViewer)
|
EightMusesDescription(state, openMetadataViewer)
|
||||||
}
|
}
|
||||||
|
is HBrowse -> { state, openMetadataViewer, _ ->
|
||||||
|
HBrowseDescription(state, openMetadataViewer)
|
||||||
|
}
|
||||||
is Pururin -> { state, openMetadataViewer, _ ->
|
is Pururin -> { state, openMetadataViewer, _ ->
|
||||||
PururinDescription(state, openMetadataViewer)
|
PururinDescription(state, openMetadataViewer)
|
||||||
}
|
}
|
||||||
|
is Tsumino -> { state, openMetadataViewer, _ ->
|
||||||
|
TsuminoDescription(state, openMetadataViewer)
|
||||||
|
}
|
||||||
is Lanraragi -> { state, openMetadataViewer, _ ->
|
is Lanraragi -> { state, openMetadataViewer, _ ->
|
||||||
LanraragiDescription(state, openMetadataViewer)
|
LanraragiDescription(state, openMetadataViewer)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,6 @@ enum class DownloadAction {
|
||||||
NEXT_10_CHAPTERS,
|
NEXT_10_CHAPTERS,
|
||||||
NEXT_25_CHAPTERS,
|
NEXT_25_CHAPTERS,
|
||||||
UNREAD_CHAPTERS,
|
UNREAD_CHAPTERS,
|
||||||
BOOKMARKED_CHAPTERS,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
enum class EditCoverAction {
|
enum class EditCoverAction {
|
||||||
|
|
|
||||||
|
|
@ -43,8 +43,8 @@ fun BaseMangaListItem(
|
||||||
onClick = onClickItem,
|
onClick = onClickItem,
|
||||||
// KMK -->
|
// KMK -->
|
||||||
onLongClick = {
|
onLongClick = {
|
||||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
|
||||||
onLongClick()
|
onLongClick()
|
||||||
|
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||||
},
|
},
|
||||||
// KMK <--
|
// KMK <--
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -738,7 +738,7 @@ object SettingsAdvancedScreen : SearchableSettings {
|
||||||
subtitle = stringResource(SYMR.strings.data_saver_image_quality_summary),
|
subtitle = stringResource(SYMR.strings.data_saver_image_quality_summary),
|
||||||
enabled = dataSaver != DataSaver.NONE,
|
enabled = dataSaver != DataSaver.NONE,
|
||||||
),
|
),
|
||||||
run {
|
kotlin.run {
|
||||||
val dataSaverImageFormatJpeg by sourcePreferences.dataSaverImageFormatJpeg()
|
val dataSaverImageFormatJpeg by sourcePreferences.dataSaverImageFormatJpeg()
|
||||||
.collectAsState()
|
.collectAsState()
|
||||||
Preference.PreferenceItem.SwitchPreference(
|
Preference.PreferenceItem.SwitchPreference(
|
||||||
|
|
@ -808,7 +808,7 @@ object SettingsAdvancedScreen : SearchableSettings {
|
||||||
title = stringResource(SYMR.strings.log_level),
|
title = stringResource(SYMR.strings.log_level),
|
||||||
subtitle = stringResource(SYMR.strings.log_level_summary),
|
subtitle = stringResource(SYMR.strings.log_level_summary),
|
||||||
),
|
),
|
||||||
run {
|
kotlin.run {
|
||||||
var enableEncryptDatabase by rememberSaveable { mutableStateOf(false) }
|
var enableEncryptDatabase by rememberSaveable { mutableStateOf(false) }
|
||||||
|
|
||||||
if (enableEncryptDatabase) {
|
if (enableEncryptDatabase) {
|
||||||
|
|
|
||||||
|
|
@ -14,11 +14,11 @@ import eu.kanade.core.preference.asState
|
||||||
import eu.kanade.domain.source.service.SourcePreferences
|
import eu.kanade.domain.source.service.SourcePreferences
|
||||||
import eu.kanade.domain.ui.UiPreferences
|
import eu.kanade.domain.ui.UiPreferences
|
||||||
import eu.kanade.presentation.more.settings.Preference
|
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.ui.category.sources.SourceCategoryScreen
|
||||||
import eu.kanade.tachiyomi.util.system.AuthenticatorUtil.authenticate
|
import eu.kanade.tachiyomi.util.system.AuthenticatorUtil.authenticate
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
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.core.common.i18n.stringResource
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.i18n.kmk.KMR
|
import tachiyomi.i18n.kmk.KMR
|
||||||
|
|
@ -43,9 +43,9 @@ object SettingsBrowseScreen : SearchableSettings {
|
||||||
val navigator = LocalNavigator.currentOrThrow
|
val navigator = LocalNavigator.currentOrThrow
|
||||||
|
|
||||||
val sourcePreferences = remember { Injekt.get<SourcePreferences>() }
|
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 -->
|
// SY -->
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
|
|
@ -85,7 +85,7 @@ object SettingsBrowseScreen : SearchableSettings {
|
||||||
enabled = sourcePreferences.relatedMangas().get(),
|
enabled = sourcePreferences.relatedMangas().get(),
|
||||||
),
|
),
|
||||||
// KMK <--
|
// KMK <--
|
||||||
run {
|
kotlin.run {
|
||||||
val count by sourcePreferences.sourcesTabCategories().collectAsState()
|
val count by sourcePreferences.sourcesTabCategories().collectAsState()
|
||||||
Preference.PreferenceItem.TextPreference(
|
Preference.PreferenceItem.TextPreference(
|
||||||
title = stringResource(MR.strings.action_edit_categories),
|
title = stringResource(MR.strings.action_edit_categories),
|
||||||
|
|
@ -142,10 +142,10 @@ object SettingsBrowseScreen : SearchableSettings {
|
||||||
title = stringResource(MR.strings.pref_hide_in_library_items),
|
title = stringResource(MR.strings.pref_hide_in_library_items),
|
||||||
),
|
),
|
||||||
Preference.PreferenceItem.TextPreference(
|
Preference.PreferenceItem.TextPreference(
|
||||||
title = stringResource(MR.strings.extensionStores),
|
title = stringResource(MR.strings.label_extension_repos),
|
||||||
subtitle = pluralStringResource(MR.plurals.num_repos, reposCount.toInt(), reposCount),
|
subtitle = pluralStringResource(MR.plurals.num_repos, reposCount, reposCount),
|
||||||
onClick = {
|
onClick = {
|
||||||
navigator.push(ExtensionStoresScreen())
|
navigator.push(ExtensionReposScreen())
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -48,7 +48,6 @@ import com.journeyapps.barcodescanner.ScanContract
|
||||||
import com.journeyapps.barcodescanner.ScanOptions
|
import com.journeyapps.barcodescanner.ScanOptions
|
||||||
import eu.kanade.domain.sync.SyncPreferences
|
import eu.kanade.domain.sync.SyncPreferences
|
||||||
import eu.kanade.presentation.more.settings.Preference
|
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.CreateBackupScreen
|
||||||
import eu.kanade.presentation.more.settings.screen.data.RestoreBackupScreen
|
import eu.kanade.presentation.more.settings.screen.data.RestoreBackupScreen
|
||||||
import eu.kanade.presentation.more.settings.screen.data.StorageInfo
|
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.SYNCYOMI.value to stringResource(SYMR.strings.syncyomi),
|
||||||
SyncManager.SyncService.GOOGLE_DRIVE.value to stringResource(SYMR.strings.google_drive),
|
SyncManager.SyncService.GOOGLE_DRIVE.value to stringResource(SYMR.strings.google_drive),
|
||||||
// KMK -->
|
// KMK -->
|
||||||
SyncManager.SyncService.WEB_DAV.value to stringResource(KMR.strings.web_dav),
|
SyncManager.SyncService.WebDAV.value to stringResource(KMR.strings.web_dav),
|
||||||
// KMK <--
|
// KMK <--
|
||||||
),
|
),
|
||||||
title = stringResource(SYMR.strings.pref_sync_service),
|
title = stringResource(SYMR.strings.pref_sync_service),
|
||||||
|
|
@ -607,7 +606,7 @@ object SettingsDataScreen : SearchableSettings {
|
||||||
SyncManager.SyncService.SYNCYOMI -> getSelfHostPreferences(syncPreferences)
|
SyncManager.SyncService.SYNCYOMI -> getSelfHostPreferences(syncPreferences)
|
||||||
SyncManager.SyncService.GOOGLE_DRIVE -> getGoogleDrivePreferences()
|
SyncManager.SyncService.GOOGLE_DRIVE -> getGoogleDrivePreferences()
|
||||||
// KMK -->
|
// KMK -->
|
||||||
SyncManager.SyncService.WEB_DAV -> getWebDavPreferences(syncPreferences)
|
SyncManager.SyncService.WebDAV -> getWebDavPreferences(syncPreferences)
|
||||||
// KMK <--
|
// KMK <--
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -760,9 +759,7 @@ object SettingsDataScreen : SearchableSettings {
|
||||||
title = stringResource(SYMR.strings.pref_sync_api_key),
|
title = stringResource(SYMR.strings.pref_sync_api_key),
|
||||||
subtitle = stringResource(SYMR.strings.pref_sync_api_key_summ),
|
subtitle = stringResource(SYMR.strings.pref_sync_api_key_summ),
|
||||||
onConfirm = {
|
onConfirm = {
|
||||||
scope.launch {
|
syncPreferences.clientAPIKey().set(it)
|
||||||
syncPreferences.clientAPIKey().set(it)
|
|
||||||
}
|
|
||||||
true
|
true
|
||||||
},
|
},
|
||||||
icon = null,
|
icon = null,
|
||||||
|
|
@ -811,28 +808,17 @@ object SettingsDataScreen : SearchableSettings {
|
||||||
true
|
true
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
run {
|
Preference.PreferenceItem.EditTextPreference(
|
||||||
var dialogOpen by remember { mutableStateOf(false) }
|
preference = syncPreferences.webDavPassword(),
|
||||||
if (dialogOpen) {
|
title = stringResource(KMR.strings.pref_webdav_password),
|
||||||
PasswordDialog(
|
subtitle = stringResource(KMR.strings.pref_webdav_password_summ),
|
||||||
onDismissRequest = { dialogOpen = false },
|
onValueChanged = { newValue ->
|
||||||
onReturnPassword = { password ->
|
scope.launch {
|
||||||
dialogOpen = false
|
syncPreferences.webDavPassword().set(newValue)
|
||||||
scope.launch {
|
}
|
||||||
syncPreferences.webDavPassword().set(password.replace("\n", ""))
|
true
|
||||||
}
|
},
|
||||||
},
|
),
|
||||||
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.PreferenceItem.EditTextPreference(
|
||||||
preference = syncPreferences.webDavFolder(),
|
preference = syncPreferences.webDavFolder(),
|
||||||
title = stringResource(KMR.strings.pref_webdav_folder),
|
title = stringResource(KMR.strings.pref_webdav_folder),
|
||||||
|
|
@ -846,7 +832,7 @@ object SettingsDataScreen : SearchableSettings {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// KMK <--
|
// MK <--
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun getSyncNowPref(): Preference.PreferenceGroup {
|
private fun getSyncNowPref(): Preference.PreferenceGroup {
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,6 @@ import kotlinx.collections.immutable.toImmutableMap
|
||||||
import tachiyomi.domain.category.interactor.GetCategories
|
import tachiyomi.domain.category.interactor.GetCategories
|
||||||
import tachiyomi.domain.category.model.Category
|
import tachiyomi.domain.category.model.Category
|
||||||
import tachiyomi.domain.download.service.DownloadPreferences
|
import tachiyomi.domain.download.service.DownloadPreferences
|
||||||
import tachiyomi.domain.download.service.LibraryUpdateDownloadMode
|
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.i18n.kmk.KMR
|
import tachiyomi.i18n.kmk.KMR
|
||||||
import tachiyomi.presentation.core.i18n.pluralStringResource
|
import tachiyomi.presentation.core.i18n.pluralStringResource
|
||||||
|
|
@ -78,7 +77,6 @@ object SettingsDownloadScreen : SearchableSettings {
|
||||||
allCategories = allCategories,
|
allCategories = allCategories,
|
||||||
),
|
),
|
||||||
getDownloadAheadGroup(downloadPreferences = downloadPreferences),
|
getDownloadAheadGroup(downloadPreferences = downloadPreferences),
|
||||||
getLibraryUpdateDownloadGroup(downloadPreferences = downloadPreferences),
|
|
||||||
// KMK -->
|
// KMK -->
|
||||||
getDownloadCacheRenewInterval(downloadPreferences = downloadPreferences),
|
getDownloadCacheRenewInterval(downloadPreferences = downloadPreferences),
|
||||||
// KMK <--
|
// 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 -->
|
// KMK -->
|
||||||
@Composable
|
@Composable
|
||||||
private fun getDownloadCacheRenewInterval(
|
private fun getDownloadCacheRenewInterval(
|
||||||
|
|
|
||||||
|
|
@ -293,14 +293,9 @@ object SettingsLibraryScreen : SearchableSettings {
|
||||||
title = stringResource(KMR.strings.pref_show_empty_categories_search),
|
title = stringResource(KMR.strings.pref_show_empty_categories_search),
|
||||||
),
|
),
|
||||||
Preference.PreferenceItem.SwitchPreference(
|
Preference.PreferenceItem.SwitchPreference(
|
||||||
preference = libraryPreferences.fetchMetadataOnAdd(),
|
preference = libraryPreferences.syncOnAdd(),
|
||||||
title = stringResource(KMR.strings.pref_fetch_manga_metadata_on_add),
|
title = stringResource(KMR.strings.pref_sync_manga_on_add),
|
||||||
subtitle = stringResource(KMR.strings.pref_fetch_manga_metadata_on_add_description),
|
subtitle = stringResource(KMR.strings.pref_sync_manga_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),
|
|
||||||
),
|
),
|
||||||
// KMK <--
|
// KMK <--
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,6 @@ import eu.kanade.tachiyomi.util.system.hasDisplayCutout
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
import kotlinx.collections.immutable.persistentMapOf
|
import kotlinx.collections.immutable.persistentMapOf
|
||||||
import kotlinx.collections.immutable.toImmutableMap
|
import kotlinx.collections.immutable.toImmutableMap
|
||||||
import tachiyomi.domain.reading.service.ReadingEtaPreferences
|
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.i18n.kmk.KMR
|
import tachiyomi.i18n.kmk.KMR
|
||||||
import tachiyomi.i18n.sy.SYMR
|
import tachiyomi.i18n.sy.SYMR
|
||||||
|
|
@ -102,7 +101,6 @@ object SettingsReaderScreen : SearchableSettings {
|
||||||
),
|
),
|
||||||
SY <-- */
|
SY <-- */
|
||||||
getDisplayGroup(readerPreferences = readerPref),
|
getDisplayGroup(readerPreferences = readerPref),
|
||||||
getReadingEtaTuningGroup(),
|
|
||||||
getEInkGroup(readerPreferences = readerPref),
|
getEInkGroup(readerPreferences = readerPref),
|
||||||
getReadingGroup(readerPreferences = readerPref),
|
getReadingGroup(readerPreferences = readerPref),
|
||||||
getPagedGroup(readerPreferences = readerPref),
|
getPagedGroup(readerPreferences = readerPref),
|
||||||
|
|
@ -123,7 +121,6 @@ object SettingsReaderScreen : SearchableSettings {
|
||||||
private fun getDisplayGroup(readerPreferences: ReaderPreferences): Preference.PreferenceGroup {
|
private fun getDisplayGroup(readerPreferences: ReaderPreferences): Preference.PreferenceGroup {
|
||||||
val fullscreenPref = readerPreferences.fullscreen()
|
val fullscreenPref = readerPreferences.fullscreen()
|
||||||
val fullscreen by fullscreenPref.collectAsState()
|
val fullscreen by fullscreenPref.collectAsState()
|
||||||
val showSeriesEta by readerPreferences.showSeriesEta().collectAsState()
|
|
||||||
return Preference.PreferenceGroup(
|
return Preference.PreferenceGroup(
|
||||||
title = stringResource(MR.strings.pref_category_display),
|
title = stringResource(MR.strings.pref_category_display),
|
||||||
preferenceItems = persistentListOf(
|
preferenceItems = persistentListOf(
|
||||||
|
|
@ -161,70 +158,6 @@ object SettingsReaderScreen : SearchableSettings {
|
||||||
preference = readerPreferences.showPageNumber(),
|
preference = readerPreferences.showPageNumber(),
|
||||||
title = stringResource(MR.strings.pref_show_page_number),
|
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,
|
enabled = passwordProtectDownloads,
|
||||||
|
|
||||||
),
|
),
|
||||||
run {
|
kotlin.run {
|
||||||
var dialogOpen by remember { mutableStateOf(false) }
|
var dialogOpen by remember { mutableStateOf(false) }
|
||||||
if (dialogOpen) {
|
if (dialogOpen) {
|
||||||
PasswordDialog(
|
PasswordDialog(
|
||||||
|
|
@ -183,7 +183,7 @@ object SettingsSecurityScreen : SearchableSettings {
|
||||||
securityPreferences.cbzPassword().set("")
|
securityPreferences.cbzPassword().set("")
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
run {
|
kotlin.run {
|
||||||
val navigator = LocalNavigator.currentOrThrow
|
val navigator = LocalNavigator.currentOrThrow
|
||||||
val count by securityPreferences.authenticatorTimeRanges().collectAsState()
|
val count by securityPreferences.authenticatorTimeRanges().collectAsState()
|
||||||
Preference.PreferenceItem.TextPreference(
|
Preference.PreferenceItem.TextPreference(
|
||||||
|
|
@ -199,7 +199,7 @@ object SettingsSecurityScreen : SearchableSettings {
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
run {
|
kotlin.run {
|
||||||
val selection by securityPreferences.authenticatorDays().collectAsState()
|
val selection by securityPreferences.authenticatorDays().collectAsState()
|
||||||
var dialogOpen by remember { mutableStateOf(false) }
|
var dialogOpen by remember { mutableStateOf(false) }
|
||||||
if (dialogOpen) {
|
if (dialogOpen) {
|
||||||
|
|
@ -332,16 +332,13 @@ object SettingsSecurityScreen : SearchableSettings {
|
||||||
fun PasswordDialog(
|
fun PasswordDialog(
|
||||||
onDismissRequest: () -> Unit,
|
onDismissRequest: () -> Unit,
|
||||||
onReturnPassword: (String) -> Unit,
|
onReturnPassword: (String) -> Unit,
|
||||||
// KMK -->
|
|
||||||
title: StringResource = SYMR.strings.cbz_archive_password,
|
|
||||||
// KMK <--
|
|
||||||
) {
|
) {
|
||||||
var password by rememberSaveable { mutableStateOf("") }
|
var password by rememberSaveable { mutableStateOf("") }
|
||||||
var passwordVisibility by remember { mutableStateOf(false) }
|
var passwordVisibility by remember { mutableStateOf(false) }
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = onDismissRequest,
|
onDismissRequest = onDismissRequest,
|
||||||
|
|
||||||
title = { Text(text = stringResource(title)) },
|
title = { Text(text = stringResource(SYMR.strings.cbz_archive_password)) },
|
||||||
text = {
|
text = {
|
||||||
TextField(
|
TextField(
|
||||||
value = password,
|
value = password,
|
||||||
|
|
|
||||||
|
|
@ -8,18 +8,20 @@ import androidx.compose.ui.platform.LocalContext
|
||||||
import cafe.adriel.voyager.core.model.rememberScreenModel
|
import cafe.adriel.voyager.core.model.rememberScreenModel
|
||||||
import cafe.adriel.voyager.navigator.LocalNavigator
|
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
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.ExtensionRepoConfirmDialog
|
||||||
import eu.kanade.presentation.more.settings.screen.browse.components.ExtensionStoreCreateDialog
|
import eu.kanade.presentation.more.settings.screen.browse.components.ExtensionRepoConflictDialog
|
||||||
import eu.kanade.presentation.more.settings.screen.browse.components.ExtensionStoreDeleteDialog
|
import eu.kanade.presentation.more.settings.screen.browse.components.ExtensionRepoCreateDialog
|
||||||
import eu.kanade.presentation.more.settings.screen.browse.components.ExtensionStoresScreen
|
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.presentation.util.Screen
|
||||||
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
|
||||||
import eu.kanade.tachiyomi.util.system.openInBrowser
|
import eu.kanade.tachiyomi.util.system.openInBrowser
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
|
import kotlinx.collections.immutable.toImmutableSet
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import tachiyomi.i18n.kmk.KMR
|
import tachiyomi.i18n.kmk.KMR
|
||||||
import tachiyomi.presentation.core.screens.LoadingScreen
|
import tachiyomi.presentation.core.screens.LoadingScreen
|
||||||
|
|
||||||
class ExtensionStoresScreen(
|
class ExtensionReposScreen(
|
||||||
private val url: String? = null,
|
private val url: String? = null,
|
||||||
) : Screen() {
|
) : Screen() {
|
||||||
|
|
||||||
|
|
@ -28,72 +30,80 @@ class ExtensionStoresScreen(
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val navigator = LocalNavigator.currentOrThrow
|
val navigator = LocalNavigator.currentOrThrow
|
||||||
|
|
||||||
val screenModel = rememberScreenModel { ExtensionStoresScreenModel() }
|
val screenModel = rememberScreenModel { ExtensionReposScreenModel() }
|
||||||
val state by screenModel.state.collectAsState()
|
val state by screenModel.state.collectAsState()
|
||||||
|
|
||||||
LaunchedEffect(url) {
|
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()
|
LoadingScreen()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val successState = state as ExtensionStoreScreenState.Success
|
val successState = state as RepoScreenState.Success
|
||||||
|
|
||||||
ExtensionStoresScreen(
|
ExtensionReposScreen(
|
||||||
state = successState,
|
state = successState,
|
||||||
onClickCreate = { screenModel.showDialog(ExtensionStoreDialog.Create()) },
|
onClickCreate = { screenModel.showDialog(RepoDialog.Create) },
|
||||||
onCopy = { context.copyToClipboard(it.indexUrl, it.indexUrl) },
|
onOpenWebsite = { context.openInBrowser(it.website) },
|
||||||
onOpenWebsite = { it.contact.website.let(context::openInBrowser) },
|
onClickDelete = { screenModel.showDialog(RepoDialog.Delete(it)) },
|
||||||
onOpenDiscord = { it.contact.discord?.let(context::openInBrowser) },
|
|
||||||
// KMK -->
|
// KMK -->
|
||||||
onClickEnable = {
|
onClickEnable = {
|
||||||
screenModel.enableStore(it.indexUrl)
|
screenModel.enableRepo(it)
|
||||||
screenModel.refreshExtensionList()
|
screenModel.refreshExtensionList()
|
||||||
context.toast(KMR.strings.extensions_page_need_refresh)
|
context.toast(KMR.strings.extensions_page_need_refresh)
|
||||||
},
|
},
|
||||||
onClickDisable = {
|
onClickDisable = {
|
||||||
screenModel.disableStore(it.indexUrl)
|
screenModel.disableRepo(it)
|
||||||
screenModel.refreshExtensionList()
|
screenModel.refreshExtensionList()
|
||||||
context.toast(KMR.strings.extensions_page_need_refresh)
|
context.toast(KMR.strings.extensions_page_need_refresh)
|
||||||
},
|
},
|
||||||
// KMK <--
|
// KMK <--
|
||||||
onClickDelete = { screenModel.showDialog(ExtensionStoreDialog.Delete(it)) },
|
|
||||||
onClickRefresh = { screenModel.refreshRepos() },
|
onClickRefresh = { screenModel.refreshRepos() },
|
||||||
navigateUp = navigator::pop,
|
navigateUp = navigator::pop,
|
||||||
)
|
)
|
||||||
|
|
||||||
when (val dialog = successState.dialog) {
|
when (val dialog = successState.dialog) {
|
||||||
null -> {}
|
null -> {}
|
||||||
is ExtensionStoreDialog.Create -> {
|
is RepoDialog.Create -> {
|
||||||
ExtensionStoreCreateDialog(
|
ExtensionRepoCreateDialog(
|
||||||
onDismissRequest = screenModel::dismissDialog,
|
onDismissRequest = screenModel::dismissDialog,
|
||||||
onCreate = { screenModel.createRepo(it) },
|
onCreate = { screenModel.createRepo(it) },
|
||||||
storeIndexUrls = successState.stores.map { it.indexUrl }.toSet(),
|
repoUrls = successState.repos.map { it.baseUrl }.toImmutableSet(),
|
||||||
processing = dialog.processing,
|
|
||||||
errorMessage = dialog.errorMessage,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is ExtensionStoreDialog.Delete -> {
|
is RepoDialog.Delete -> {
|
||||||
ExtensionStoreDeleteDialog(
|
ExtensionRepoDeleteDialog(
|
||||||
onDismissRequest = screenModel::dismissDialog,
|
onDismissRequest = screenModel::dismissDialog,
|
||||||
onDelete = { screenModel.deleteRepo(dialog.store.indexUrl) },
|
onDelete = { screenModel.deleteRepo(dialog.repo) },
|
||||||
storeName = dialog.store.name,
|
repo = dialog.repo,
|
||||||
storeIndexUrl = dialog.store.indexUrl,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is ExtensionStoreDialog.Confirm -> {
|
is RepoDialog.Conflict -> {
|
||||||
ExtensionStoreConfirmDialog(
|
ExtensionRepoConflictDialog(
|
||||||
|
onDismissRequest = screenModel::dismissDialog,
|
||||||
|
onMigrate = { screenModel.replaceRepo(dialog.newRepo) },
|
||||||
|
oldRepo = dialog.oldRepo,
|
||||||
|
newRepo = dialog.newRepo,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is RepoDialog.Confirm -> {
|
||||||
|
ExtensionRepoConfirmDialog(
|
||||||
onDismissRequest = screenModel::dismissDialog,
|
onDismissRequest = screenModel::dismissDialog,
|
||||||
onCreate = { screenModel.createRepo(dialog.url) },
|
onCreate = { screenModel.createRepo(dialog.url) },
|
||||||
storeIndexUrl = dialog.url,
|
repo = dialog.url,
|
||||||
storeAlreadyExists = dialog.alreadyExists,
|
|
||||||
processing = dialog.processing,
|
|
||||||
errorMessage = dialog.errorMessage,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.LazyListState
|
import androidx.compose.foundation.lazy.LazyListState
|
||||||
import androidx.compose.material.icons.Icons
|
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.ContentCopy
|
||||||
import androidx.compose.material.icons.outlined.Delete
|
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.Visibility
|
||||||
import androidx.compose.material.icons.outlined.VisibilityOff
|
import androidx.compose.material.icons.outlined.VisibilityOff
|
||||||
import androidx.compose.material3.ElevatedCard
|
import androidx.compose.material3.ElevatedCard
|
||||||
|
|
@ -26,33 +26,33 @@ import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
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.text.style.TextDecoration
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import eu.kanade.tachiyomi.util.system.copyToClipboard
|
||||||
import mihon.domain.extension.model.ExtensionStore
|
import kotlinx.collections.immutable.ImmutableSet
|
||||||
import mihon.domain.extension.model.KOMIKKU_SIGNATURE
|
import kotlinx.collections.immutable.persistentSetOf
|
||||||
import mihon.domain.extension.model.REPO_SIGNATURE
|
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.i18n.MR
|
||||||
import tachiyomi.presentation.core.components.material.padding
|
import tachiyomi.presentation.core.components.material.padding
|
||||||
import tachiyomi.presentation.core.i18n.stringResource
|
import tachiyomi.presentation.core.i18n.stringResource
|
||||||
import tachiyomi.presentation.core.icons.CustomIcons
|
|
||||||
import tachiyomi.presentation.core.icons.Discord
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ExtensionStoresContent(
|
fun ExtensionReposContent(
|
||||||
repos: List<ExtensionStore>,
|
repos: ImmutableSet<ExtensionRepo>,
|
||||||
lazyListState: LazyListState,
|
lazyListState: LazyListState,
|
||||||
paddingValues: PaddingValues,
|
paddingValues: PaddingValues,
|
||||||
onCopy: (ExtensionStore) -> Unit,
|
onOpenWebsite: (ExtensionRepo) -> Unit,
|
||||||
onOpenWebsite: (ExtensionStore) -> Unit,
|
onClickDelete: (String) -> Unit,
|
||||||
onOpenDiscord: (ExtensionStore) -> Unit,
|
|
||||||
onClickDelete: (ExtensionStore) -> Unit,
|
|
||||||
// KMK -->
|
// KMK -->
|
||||||
onClickEnable: (ExtensionStore) -> Unit,
|
onClickEnable: (String) -> Unit,
|
||||||
onClickDisable: (ExtensionStore) -> Unit,
|
onClickDisable: (String) -> Unit,
|
||||||
disabledRepos: Set<String>,
|
disabledRepos: Set<String>,
|
||||||
// KMK <--
|
// KMK <--
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
|
|
@ -65,17 +65,15 @@ fun ExtensionStoresContent(
|
||||||
) {
|
) {
|
||||||
repos.forEach {
|
repos.forEach {
|
||||||
item {
|
item {
|
||||||
ExtensionStoresListItem(
|
ExtensionRepoListItem(
|
||||||
modifier = Modifier.animateItem(),
|
modifier = Modifier.animateItem(),
|
||||||
store = it,
|
repo = it,
|
||||||
onOpenWebsite = { onOpenWebsite(it) },
|
onOpenWebsite = { onOpenWebsite(it) },
|
||||||
onOpenDiscord = { onOpenDiscord(it) },
|
onDelete = { onClickDelete(it.baseUrl) },
|
||||||
onCopy = { onCopy(it) },
|
|
||||||
onDelete = { onClickDelete(it) },
|
|
||||||
// KMK -->
|
// KMK -->
|
||||||
onEnable = { onClickEnable(it) },
|
onEnable = { onClickEnable(it.baseUrl) },
|
||||||
onDisable = { onClickDisable(it) },
|
onDisable = { onClickDisable(it.baseUrl) },
|
||||||
isDisabled = it.indexUrl in disabledRepos,
|
isDisabled = it.baseUrl in disabledRepos,
|
||||||
// KMK <--
|
// KMK <--
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -84,11 +82,9 @@ fun ExtensionStoresContent(
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun ExtensionStoresListItem(
|
private fun ExtensionRepoListItem(
|
||||||
store: ExtensionStore,
|
repo: ExtensionRepo,
|
||||||
onOpenWebsite: () -> Unit,
|
onOpenWebsite: () -> Unit,
|
||||||
onOpenDiscord: () -> Unit,
|
|
||||||
onCopy: () -> Unit,
|
|
||||||
onDelete: () -> Unit,
|
onDelete: () -> Unit,
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
// KMK -->
|
// KMK -->
|
||||||
|
|
@ -97,6 +93,8 @@ private fun ExtensionStoresListItem(
|
||||||
onDisable: () -> Unit,
|
onDisable: () -> Unit,
|
||||||
// KMK <--
|
// KMK <--
|
||||||
) {
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
ElevatedCard(
|
ElevatedCard(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
) {
|
) {
|
||||||
|
|
@ -105,9 +103,9 @@ private fun ExtensionStoresListItem(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.padding(start = MaterialTheme.padding.medium),
|
.padding(start = MaterialTheme.padding.medium),
|
||||||
) {
|
) {
|
||||||
val resId = repoResId(store.signingKey)
|
val resId = repoResId(repo.signingKeyFingerprint)
|
||||||
Image(
|
Image(
|
||||||
painter = painterResource(id = resId),
|
bitmap = ImageBitmap.imageResource(id = resId),
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
alpha = if (isDisabled) 0.4f else 1f,
|
alpha = if (isDisabled) 0.4f else 1f,
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|
@ -128,7 +126,7 @@ private fun ExtensionStoresListItem(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
) {
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = store.name,
|
text = repo.name,
|
||||||
// KMK: modifier = Modifier.padding(start = MaterialTheme.padding.medium),
|
// KMK: modifier = Modifier.padding(start = MaterialTheme.padding.medium),
|
||||||
style = MaterialTheme.typography.titleMedium,
|
style = MaterialTheme.typography.titleMedium,
|
||||||
// KMK -->
|
// KMK -->
|
||||||
|
|
@ -144,21 +142,17 @@ private fun ExtensionStoresListItem(
|
||||||
) {
|
) {
|
||||||
IconButton(onClick = onOpenWebsite) {
|
IconButton(onClick = onOpenWebsite) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Outlined.Public,
|
imageVector = Icons.AutoMirrored.Outlined.OpenInNew,
|
||||||
contentDescription = stringResource(MR.strings.action_open_in_browser),
|
contentDescription = stringResource(MR.strings.action_open_in_browser),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (store.contact.discord != null) {
|
IconButton(
|
||||||
IconButton(onClick = onOpenDiscord) {
|
onClick = {
|
||||||
Icon(
|
val url = "${repo.baseUrl}/index.min.json"
|
||||||
imageVector = CustomIcons.Discord,
|
context.copyToClipboard(url, url)
|
||||||
contentDescription = null,
|
},
|
||||||
)
|
) {
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
IconButton(onClick = onCopy) {
|
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Outlined.ContentCopy,
|
imageVector = Icons.Outlined.ContentCopy,
|
||||||
contentDescription = stringResource(MR.strings.action_copy_to_clipboard),
|
contentDescription = stringResource(MR.strings.action_copy_to_clipboard),
|
||||||
|
|
@ -196,18 +190,16 @@ fun repoResId(signKey: String) = when (signKey) {
|
||||||
@Preview
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
fun ExtensionReposContentPreview() {
|
fun ExtensionReposContentPreview() {
|
||||||
val repos = persistentListOf(
|
val repos = persistentSetOf(
|
||||||
ExtensionStore("https://repo", "Komikku", "", KOMIKKU_SIGNATURE, ExtensionStore.Contact("", ""), false),
|
ExtensionRepo("https://repo", "Komikku", "", "", KOMIKKU_SIGNATURE),
|
||||||
ExtensionStore("https://repo", "Repo", "", REPO_SIGNATURE, ExtensionStore.Contact("", ""), false),
|
ExtensionRepo("https://repo", "Repo", "", "", REPO_SIGNATURE),
|
||||||
ExtensionStore("https://repo", "Other", "", "key2", ExtensionStore.Contact("", ""), true),
|
ExtensionRepo("https://repo", "Other", "", "", "key2"),
|
||||||
)
|
)
|
||||||
ExtensionStoresContent(
|
ExtensionReposContent(
|
||||||
repos = repos,
|
repos = repos,
|
||||||
lazyListState = LazyListState(),
|
lazyListState = LazyListState(),
|
||||||
paddingValues = PaddingValues(),
|
paddingValues = PaddingValues(),
|
||||||
onCopy = {},
|
|
||||||
onOpenWebsite = {},
|
onOpenWebsite = {},
|
||||||
onOpenDiscord = {},
|
|
||||||
onClickDelete = {},
|
onClickDelete = {},
|
||||||
onClickEnable = {},
|
onClickEnable = {},
|
||||||
onClickDisable = {},
|
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
|
package eu.kanade.presentation.more.settings.screen.browse.components
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
|
@ -19,13 +21,13 @@ import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.tooling.preview.Preview
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
import eu.kanade.presentation.category.components.CategoryFloatingActionButton
|
import eu.kanade.presentation.category.components.CategoryFloatingActionButton
|
||||||
import eu.kanade.presentation.components.AppBar
|
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 eu.kanade.tachiyomi.util.system.openInBrowser
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentSetOf
|
||||||
import mihon.domain.extension.model.ExtensionStore
|
import mihon.domain.extensionrepo.interactor.CreateExtensionRepo.Companion.KOMIKKU_SIGNATURE
|
||||||
import mihon.domain.extension.model.KOMIKKU_SIGNATURE
|
import mihon.domain.extensionrepo.interactor.CreateExtensionRepo.Companion.REPO_HELP
|
||||||
import mihon.domain.extension.model.REPO_HELP
|
import mihon.domain.extensionrepo.interactor.CreateExtensionRepo.Companion.REPO_SIGNATURE
|
||||||
import mihon.domain.extension.model.REPO_SIGNATURE
|
import mihon.domain.extensionrepo.model.ExtensionRepo
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import tachiyomi.presentation.core.components.material.Scaffold
|
import tachiyomi.presentation.core.components.material.Scaffold
|
||||||
import tachiyomi.presentation.core.components.material.padding
|
import tachiyomi.presentation.core.components.material.padding
|
||||||
|
|
@ -35,16 +37,14 @@ import tachiyomi.presentation.core.screens.EmptyScreen
|
||||||
import tachiyomi.presentation.core.util.plus
|
import tachiyomi.presentation.core.util.plus
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ExtensionStoresScreen(
|
fun ExtensionReposScreen(
|
||||||
state: ExtensionStoreScreenState.Success,
|
state: RepoScreenState.Success,
|
||||||
onClickCreate: () -> Unit,
|
onClickCreate: () -> Unit,
|
||||||
onCopy: (ExtensionStore) -> Unit,
|
onOpenWebsite: (ExtensionRepo) -> Unit,
|
||||||
onOpenWebsite: (ExtensionStore) -> Unit,
|
onClickDelete: (String) -> Unit,
|
||||||
onOpenDiscord: (ExtensionStore) -> Unit,
|
|
||||||
onClickDelete: (ExtensionStore) -> Unit,
|
|
||||||
// KMK -->
|
// KMK -->
|
||||||
onClickEnable: (ExtensionStore) -> Unit,
|
onClickEnable: (String) -> Unit,
|
||||||
onClickDisable: (ExtensionStore) -> Unit,
|
onClickDisable: (String) -> Unit,
|
||||||
// KMK <--
|
// KMK <--
|
||||||
onClickRefresh: () -> Unit,
|
onClickRefresh: () -> Unit,
|
||||||
navigateUp: () -> Unit,
|
navigateUp: () -> Unit,
|
||||||
|
|
@ -54,7 +54,7 @@ fun ExtensionStoresScreen(
|
||||||
topBar = { scrollBehavior ->
|
topBar = { scrollBehavior ->
|
||||||
AppBar(
|
AppBar(
|
||||||
navigateUp = navigateUp,
|
navigateUp = navigateUp,
|
||||||
title = stringResource(MR.strings.extensionStores),
|
title = stringResource(MR.strings.label_extension_repos),
|
||||||
scrollBehavior = scrollBehavior,
|
scrollBehavior = scrollBehavior,
|
||||||
actions = {
|
actions = {
|
||||||
IconButton(onClick = onClickRefresh) {
|
IconButton(onClick = onClickRefresh) {
|
||||||
|
|
@ -76,7 +76,7 @@ fun ExtensionStoresScreen(
|
||||||
if (state.isEmpty) {
|
if (state.isEmpty) {
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
EmptyScreen(
|
EmptyScreen(
|
||||||
MR.strings.extensionStoresScreen_emptyLabel,
|
MR.strings.information_empty_repos,
|
||||||
modifier = Modifier.padding(paddingValues),
|
modifier = Modifier.padding(paddingValues),
|
||||||
// KMK -->
|
// KMK -->
|
||||||
help = {
|
help = {
|
||||||
|
|
@ -94,14 +94,12 @@ fun ExtensionStoresScreen(
|
||||||
return@Scaffold
|
return@Scaffold
|
||||||
}
|
}
|
||||||
|
|
||||||
ExtensionStoresContent(
|
ExtensionReposContent(
|
||||||
repos = state.stores,
|
repos = state.repos,
|
||||||
lazyListState = lazyListState,
|
lazyListState = lazyListState,
|
||||||
paddingValues = paddingValues + topSmallPaddingValues +
|
paddingValues = paddingValues + topSmallPaddingValues +
|
||||||
PaddingValues(horizontal = MaterialTheme.padding.medium),
|
PaddingValues(horizontal = MaterialTheme.padding.medium),
|
||||||
onCopy = onCopy,
|
|
||||||
onOpenWebsite = onOpenWebsite,
|
onOpenWebsite = onOpenWebsite,
|
||||||
onOpenDiscord = onOpenDiscord,
|
|
||||||
onClickDelete = onClickDelete,
|
onClickDelete = onClickDelete,
|
||||||
// KMK -->
|
// KMK -->
|
||||||
onClickEnable = onClickEnable,
|
onClickEnable = onClickEnable,
|
||||||
|
|
@ -115,21 +113,19 @@ fun ExtensionStoresScreen(
|
||||||
// KMK -->
|
// KMK -->
|
||||||
@Preview
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
private fun ExtensionStoresScreenPreview() {
|
private fun ExtensionReposScreenPreview() {
|
||||||
val state = ExtensionStoreScreenState.Success(
|
val state = RepoScreenState.Success(
|
||||||
stores = persistentListOf(
|
repos = persistentSetOf(
|
||||||
ExtensionStore("https://repo", "Komikku", "", KOMIKKU_SIGNATURE, ExtensionStore.Contact("", ""), false),
|
ExtensionRepo("https://repo", "Komikku", "", "", KOMIKKU_SIGNATURE),
|
||||||
ExtensionStore("https://repo", "Repo", "", REPO_SIGNATURE, ExtensionStore.Contact("", ""), false),
|
ExtensionRepo("https://repo", "Repo", "", "", REPO_SIGNATURE),
|
||||||
ExtensionStore("https://repo", "Other", "", "key2", ExtensionStore.Contact("", ""), true),
|
ExtensionRepo("https://repo", "Other", "", "", "key2"),
|
||||||
),
|
),
|
||||||
disabledRepos = setOf("https://repo"),
|
disabledRepos = setOf("https://repo"),
|
||||||
)
|
)
|
||||||
ExtensionStoresScreen(
|
ExtensionReposScreen(
|
||||||
state = state,
|
state = state,
|
||||||
onClickCreate = { },
|
onClickCreate = { },
|
||||||
onCopy = { },
|
|
||||||
onOpenWebsite = { },
|
onOpenWebsite = { },
|
||||||
onOpenDiscord = { },
|
|
||||||
onClickDelete = { },
|
onClickDelete = { },
|
||||||
onClickEnable = { },
|
onClickEnable = { },
|
||||||
onClickDisable = { },
|
onClickDisable = { },
|
||||||
|
|
@ -140,14 +136,12 @@ private fun ExtensionStoresScreenPreview() {
|
||||||
|
|
||||||
@Preview
|
@Preview
|
||||||
@Composable
|
@Composable
|
||||||
private fun ExtensionStoresScreenEmptyPreview() {
|
private fun ExtensionReposScreenEmptyPreview() {
|
||||||
val state = ExtensionStoreScreenState.Success(stores = persistentListOf())
|
val state = RepoScreenState.Success(repos = persistentSetOf())
|
||||||
ExtensionStoresScreen(
|
ExtensionReposScreen(
|
||||||
state = state,
|
state = state,
|
||||||
onClickCreate = { },
|
onClickCreate = { },
|
||||||
onCopy = { },
|
|
||||||
onOpenWebsite = { },
|
onOpenWebsite = { },
|
||||||
onOpenDiscord = { },
|
|
||||||
onClickDelete = { },
|
onClickDelete = { },
|
||||||
onClickEnable = { },
|
onClickEnable = { },
|
||||||
onClickDisable = { },
|
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,
|
tracking = syncSettings.tracking,
|
||||||
history = syncSettings.history,
|
history = syncSettings.history,
|
||||||
appSettings = syncSettings.appSettings,
|
appSettings = syncSettings.appSettings,
|
||||||
extensionStores = syncSettings.extensionStores,
|
extensionRepoSettings = syncSettings.extensionRepoSettings,
|
||||||
sourceSettings = syncSettings.sourceSettings,
|
sourceSettings = syncSettings.sourceSettings,
|
||||||
privateSettings = syncSettings.privateSettings,
|
privateSettings = syncSettings.privateSettings,
|
||||||
|
|
||||||
|
|
@ -134,7 +134,7 @@ private class SyncSettingsSelectorModel(
|
||||||
tracking = backupOptions.tracking,
|
tracking = backupOptions.tracking,
|
||||||
history = backupOptions.history,
|
history = backupOptions.history,
|
||||||
appSettings = backupOptions.appSettings,
|
appSettings = backupOptions.appSettings,
|
||||||
extensionStores = backupOptions.extensionStores,
|
extensionRepoSettings = backupOptions.extensionRepoSettings,
|
||||||
sourceSettings = backupOptions.sourceSettings,
|
sourceSettings = backupOptions.sourceSettings,
|
||||||
privateSettings = backupOptions.privateSettings,
|
privateSettings = backupOptions.privateSettings,
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,6 @@ import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
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.padding
|
||||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.material3.surfaceColorAtElevation
|
import androidx.compose.material3.surfaceColorAtElevation
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.unit.IntOffset
|
import androidx.compose.ui.unit.IntOffset
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import eu.kanade.presentation.reader.components.ChapterNavigator
|
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.ReaderOrientation
|
||||||
import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode
|
import eu.kanade.tachiyomi.ui.reader.setting.ReadingMode
|
||||||
import eu.kanade.tachiyomi.ui.reader.viewer.Viewer
|
import eu.kanade.tachiyomi.ui.reader.viewer.Viewer
|
||||||
import eu.kanade.tachiyomi.ui.reader.viewer.pager.R2LPagerViewer
|
import eu.kanade.tachiyomi.ui.reader.viewer.pager.R2LPagerViewer
|
||||||
import kotlinx.collections.immutable.ImmutableSet
|
import kotlinx.collections.immutable.ImmutableSet
|
||||||
import tachiyomi.i18n.sy.SYMR
|
|
||||||
import tachiyomi.presentation.core.components.material.padding
|
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 readerBarsSlideAnimationSpec = tween<IntOffset>(200)
|
||||||
private val readerBarsFadeAnimationSpec = tween<Float>(150)
|
private val readerBarsFadeAnimationSpec = tween<Float>(150)
|
||||||
|
|
@ -100,9 +91,6 @@ fun ReaderAppBars(
|
||||||
onClickBoostPageHelp: () -> Unit,
|
onClickBoostPageHelp: () -> Unit,
|
||||||
navBarType: NavBarType,
|
navBarType: NavBarType,
|
||||||
currentPageText: String,
|
currentPageText: String,
|
||||||
seriesReadingProgress: ReaderViewModel.SeriesReadingProgress?,
|
|
||||||
showSeriesProgress: Boolean,
|
|
||||||
showSeriesEta: Boolean,
|
|
||||||
enabledButtons: ImmutableSet<String>,
|
enabledButtons: ImmutableSet<String>,
|
||||||
currentReadingMode: ReadingMode,
|
currentReadingMode: ReadingMode,
|
||||||
dualPageSplitEnabled: Boolean,
|
dualPageSplitEnabled: Boolean,
|
||||||
|
|
@ -262,11 +250,6 @@ fun ReaderAppBars(
|
||||||
// SY <--
|
// SY <--
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
ReaderSeriesProgressInfo(
|
|
||||||
progress = seriesReadingProgress,
|
|
||||||
showSeriesProgress = showSeriesProgress,
|
|
||||||
showSeriesEta = showSeriesEta,
|
|
||||||
)
|
|
||||||
ReaderBottomBar(
|
ReaderBottomBar(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.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(),
|
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 -->
|
// SY -->
|
||||||
val forceHorizontalSeekbar by screenModel.preferences.forceHorizontalSeekbar().collectAsState()
|
val forceHorizontalSeekbar by screenModel.preferences.forceHorizontalSeekbar().collectAsState()
|
||||||
CheckboxItem(
|
CheckboxItem(
|
||||||
|
|
|
||||||
|
|
@ -112,9 +112,6 @@ fun UpdateScreen(
|
||||||
else -> {
|
else -> {
|
||||||
val scope = rememberCoroutineScope()
|
val scope = rememberCoroutineScope()
|
||||||
var isRefreshing by remember { mutableStateOf(false) }
|
var isRefreshing by remember { mutableStateOf(false) }
|
||||||
// KMK -->
|
|
||||||
val uiModels = remember(state.items) { state.getUiModel() }
|
|
||||||
// KMK <--
|
|
||||||
|
|
||||||
PullRefresh(
|
PullRefresh(
|
||||||
refreshing = isRefreshing,
|
refreshing = isRefreshing,
|
||||||
|
|
@ -137,8 +134,8 @@ fun UpdateScreen(
|
||||||
updatesLastUpdatedItem(lastUpdated)
|
updatesLastUpdatedItem(lastUpdated)
|
||||||
|
|
||||||
updatesUiItems(
|
updatesUiItems(
|
||||||
|
uiModels = state.getUiModel(),
|
||||||
// KMK -->
|
// KMK -->
|
||||||
uiModels = uiModels,
|
|
||||||
expandedState = state.expandedState,
|
expandedState = state.expandedState,
|
||||||
collapseToggle = collapseToggle,
|
collapseToggle = collapseToggle,
|
||||||
usePanoramaCover = usePanoramaCover,
|
usePanoramaCover = usePanoramaCover,
|
||||||
|
|
|
||||||
|
|
@ -171,6 +171,7 @@ internal fun LazyListScope.updatesUiItems(
|
||||||
// KMK -->
|
// KMK -->
|
||||||
UpdateSelectionOptions(
|
UpdateSelectionOptions(
|
||||||
selected = !updatesItem.selected,
|
selected = !updatesItem.selected,
|
||||||
|
userSelected = true,
|
||||||
fromLongPress = true,
|
fromLongPress = true,
|
||||||
isGroup = isLeader && item.isExpandable,
|
isGroup = isLeader && item.isExpandable,
|
||||||
isExpanded = isExpanded,
|
isExpanded = isExpanded,
|
||||||
|
|
@ -185,6 +186,7 @@ internal fun LazyListScope.updatesUiItems(
|
||||||
// KMK -->
|
// KMK -->
|
||||||
UpdateSelectionOptions(
|
UpdateSelectionOptions(
|
||||||
selected = !updatesItem.selected,
|
selected = !updatesItem.selected,
|
||||||
|
userSelected = true,
|
||||||
fromLongPress = false,
|
fromLongPress = false,
|
||||||
isGroup = isLeader && item.isExpandable,
|
isGroup = isLeader && item.isExpandable,
|
||||||
isExpanded = isExpanded,
|
isExpanded = isExpanded,
|
||||||
|
|
@ -286,8 +288,8 @@ private fun UpdatesUiItem(
|
||||||
.combinedClickable(
|
.combinedClickable(
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
onLongClick = {
|
onLongClick = {
|
||||||
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
|
||||||
onLongClick()
|
onLongClick()
|
||||||
|
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.padding(top = if (isLeader) MaterialTheme.padding.small else 0.dp)
|
.padding(top = if (isLeader) MaterialTheme.padding.small else 0.dp)
|
||||||
|
|
@ -309,7 +311,7 @@ private fun UpdatesUiItem(
|
||||||
MangaCoverHide.Book(
|
MangaCoverHide.Book(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.width(UpdateItemWidth),
|
.width(UpdateItemWidth),
|
||||||
bgColor = bgColor ?: MaterialTheme.colorScheme.surface.takeIf { selected },
|
bgColor = bgColor ?: (MaterialTheme.colorScheme.surface.takeIf { selected }),
|
||||||
tint = onBgColor,
|
tint = onBgColor,
|
||||||
size = MangaCover.Size.Medium,
|
size = MangaCover.Size.Medium,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -80,7 +80,7 @@ fun EhLoginWebViewScreen(
|
||||||
)
|
)
|
||||||
is LoadingState.Loading -> {
|
is LoadingState.Loading -> {
|
||||||
val animatedProgress by animateFloatAsState(
|
val animatedProgress by animateFloatAsState(
|
||||||
loadingState.progress,
|
(loadingState as? LoadingState.Loading)?.progress ?: 1f,
|
||||||
animationSpec = ProgressIndicatorDefaults.ProgressAnimationSpec,
|
animationSpec = ProgressIndicatorDefaults.ProgressAnimationSpec,
|
||||||
label = "webview_loading",
|
label = "webview_loading",
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import com.hippo.unifile.UniFile
|
||||||
import eu.kanade.tachiyomi.BuildConfig
|
import eu.kanade.tachiyomi.BuildConfig
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupFileValidator
|
import eu.kanade.tachiyomi.data.backup.BackupFileValidator
|
||||||
import eu.kanade.tachiyomi.data.backup.create.creators.CategoriesBackupCreator
|
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.FeedBackupCreator
|
||||||
import eu.kanade.tachiyomi.data.backup.create.creators.MangaBackupCreator
|
import eu.kanade.tachiyomi.data.backup.create.creators.MangaBackupCreator
|
||||||
import eu.kanade.tachiyomi.data.backup.create.creators.PreferenceBackupCreator
|
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.create.creators.SourcesBackupCreator
|
||||||
import eu.kanade.tachiyomi.data.backup.models.Backup
|
import eu.kanade.tachiyomi.data.backup.models.Backup
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupCategory
|
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.BackupFeed
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupManga
|
import eu.kanade.tachiyomi.data.backup.models.BackupManga
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupPreference
|
import eu.kanade.tachiyomi.data.backup.models.BackupPreference
|
||||||
|
|
@ -54,7 +54,7 @@ class BackupCreator(
|
||||||
private val categoriesBackupCreator: CategoriesBackupCreator = CategoriesBackupCreator(),
|
private val categoriesBackupCreator: CategoriesBackupCreator = CategoriesBackupCreator(),
|
||||||
private val mangaBackupCreator: MangaBackupCreator = MangaBackupCreator(),
|
private val mangaBackupCreator: MangaBackupCreator = MangaBackupCreator(),
|
||||||
private val preferenceBackupCreator: PreferenceBackupCreator = PreferenceBackupCreator(),
|
private val preferenceBackupCreator: PreferenceBackupCreator = PreferenceBackupCreator(),
|
||||||
private val extensionStoresBackupCreator: ExtensionStoresBackupCreator = ExtensionStoresBackupCreator(),
|
private val extensionRepoBackupCreator: ExtensionRepoBackupCreator = ExtensionRepoBackupCreator(),
|
||||||
private val sourcesBackupCreator: SourcesBackupCreator = SourcesBackupCreator(),
|
private val sourcesBackupCreator: SourcesBackupCreator = SourcesBackupCreator(),
|
||||||
// KMK -->
|
// KMK -->
|
||||||
private val feedBackupCreator: FeedBackupCreator = FeedBackupCreator(),
|
private val feedBackupCreator: FeedBackupCreator = FeedBackupCreator(),
|
||||||
|
|
@ -101,7 +101,7 @@ class BackupCreator(
|
||||||
backupCategories = backupCategories(options),
|
backupCategories = backupCategories(options),
|
||||||
backupSources = backupSources(backupManga),
|
backupSources = backupSources(backupManga),
|
||||||
backupPreferences = backupAppPreferences(options),
|
backupPreferences = backupAppPreferences(options),
|
||||||
backupExtensionStores = backupExtensionStores(options),
|
backupExtensionRepo = backupExtensionRepos(options),
|
||||||
backupSourcePreferences = backupSourcePreferences(options),
|
backupSourcePreferences = backupSourcePreferences(options),
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
|
|
@ -159,16 +159,16 @@ class BackupCreator(
|
||||||
return sourcesBackupCreator(mangas)
|
return sourcesBackupCreator(mangas)
|
||||||
}
|
}
|
||||||
|
|
||||||
internal fun backupAppPreferences(options: BackupOptions): List<BackupPreference> {
|
/* KMK --> */ suspend /* KMK <-- */ fun backupAppPreferences(options: BackupOptions): List<BackupPreference> {
|
||||||
if (!options.appSettings) return emptyList()
|
if (!options.appSettings) return emptyList()
|
||||||
|
|
||||||
return preferenceBackupCreator.createApp(includePrivatePreferences = options.privateSettings)
|
return preferenceBackupCreator.createApp(includePrivatePreferences = options.privateSettings)
|
||||||
}
|
}
|
||||||
|
|
||||||
internal suspend fun backupExtensionStores(options: BackupOptions): List<BackupExtensionStore> {
|
suspend fun backupExtensionRepos(options: BackupOptions): List<BackupExtensionRepos> {
|
||||||
if (!options.extensionStores) return emptyList()
|
if (!options.extensionRepoSettings) return emptyList()
|
||||||
|
|
||||||
return extensionStoresBackupCreator()
|
return extensionRepoBackupCreator()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun backupSourcePreferences(options: BackupOptions): List<BackupSourcePreferences> {
|
fun backupSourcePreferences(options: BackupOptions): List<BackupSourcePreferences> {
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ data class BackupOptions(
|
||||||
val history: Boolean = true,
|
val history: Boolean = true,
|
||||||
val readEntries: Boolean = true,
|
val readEntries: Boolean = true,
|
||||||
val appSettings: Boolean = true,
|
val appSettings: Boolean = true,
|
||||||
val extensionStores: Boolean = true,
|
val extensionRepoSettings: Boolean = true,
|
||||||
val sourceSettings: Boolean = true,
|
val sourceSettings: Boolean = true,
|
||||||
val privateSettings: Boolean = false,
|
val privateSettings: Boolean = false,
|
||||||
// SY -->
|
// SY -->
|
||||||
|
|
@ -31,7 +31,7 @@ data class BackupOptions(
|
||||||
history,
|
history,
|
||||||
readEntries,
|
readEntries,
|
||||||
appSettings,
|
appSettings,
|
||||||
extensionStores,
|
extensionRepoSettings,
|
||||||
sourceSettings,
|
sourceSettings,
|
||||||
privateSettings,
|
privateSettings,
|
||||||
// SY -->
|
// SY -->
|
||||||
|
|
@ -41,7 +41,7 @@ data class BackupOptions(
|
||||||
)
|
)
|
||||||
|
|
||||||
fun canCreate() =
|
fun canCreate() =
|
||||||
libraryEntries || categories || appSettings || extensionStores || sourceSettings || savedSearchesFeeds
|
libraryEntries || categories || appSettings || extensionRepoSettings || sourceSettings || savedSearchesFeeds
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val libraryOptions = persistentListOf(
|
val libraryOptions = persistentListOf(
|
||||||
|
|
@ -103,9 +103,9 @@ data class BackupOptions(
|
||||||
setter = { options, enabled -> options.copy(appSettings = enabled) },
|
setter = { options, enabled -> options.copy(appSettings = enabled) },
|
||||||
),
|
),
|
||||||
Entry(
|
Entry(
|
||||||
label = MR.strings.extensionStores,
|
label = MR.strings.extensionRepo_settings,
|
||||||
getter = BackupOptions::extensionStores,
|
getter = BackupOptions::extensionRepoSettings,
|
||||||
setter = { options, enabled -> options.copy(extensionStores = enabled) },
|
setter = { options, enabled -> options.copy(extensionRepoSettings = enabled) },
|
||||||
),
|
),
|
||||||
Entry(
|
Entry(
|
||||||
label = MR.strings.source_settings,
|
label = MR.strings.source_settings,
|
||||||
|
|
@ -128,7 +128,7 @@ data class BackupOptions(
|
||||||
history = array[4],
|
history = array[4],
|
||||||
readEntries = array[5],
|
readEntries = array[5],
|
||||||
appSettings = array[6],
|
appSettings = array[6],
|
||||||
extensionStores = array[7],
|
extensionRepoSettings = array[7],
|
||||||
sourceSettings = array[8],
|
sourceSettings = array[8],
|
||||||
privateSettings = array[9],
|
privateSettings = array[9],
|
||||||
// SY -->
|
// 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()) {
|
if (historyByMangaId.isNotEmpty()) {
|
||||||
val history = historyByMangaId.map { history ->
|
val history = historyByMangaId.map { history ->
|
||||||
val chapter = handler.awaitOne { chaptersQueries.getChapterById(history.chapterId) }
|
val chapter = handler.awaitOne { chaptersQueries.getChapterById(history.chapterId) }
|
||||||
BackupHistory(
|
BackupHistory(chapter.url, history.readAt?.time ?: 0L, history.readDuration)
|
||||||
url = chapter.url,
|
|
||||||
lastRead = history.readAt?.time ?: 0L,
|
|
||||||
readDuration = history.readDuration,
|
|
||||||
readPages = history.readPages,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
if (history.isNotEmpty()) {
|
if (history.isNotEmpty()) {
|
||||||
mangaObject.history = history
|
mangaObject.history = history
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ data class Backup(
|
||||||
@ProtoNumber(101) var backupSources: List<BackupSource> = emptyList(),
|
@ProtoNumber(101) var backupSources: List<BackupSource> = emptyList(),
|
||||||
@ProtoNumber(104) var backupPreferences: List<BackupPreference> = emptyList(),
|
@ProtoNumber(104) var backupPreferences: List<BackupPreference> = emptyList(),
|
||||||
@ProtoNumber(105) var backupSourcePreferences: List<BackupSourcePreferences> = 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
|
// SY specific values
|
||||||
@ProtoNumber(600) var backupSavedSearches: List<BackupSavedSearch> = emptyList(),
|
@ProtoNumber(600) var backupSavedSearches: List<BackupSavedSearch> = emptyList(),
|
||||||
// KMK -->
|
// 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(1) var url: String,
|
||||||
@ProtoNumber(2) var lastRead: Long,
|
@ProtoNumber(2) var lastRead: Long,
|
||||||
@ProtoNumber(3) var readDuration: Long = 0,
|
@ProtoNumber(3) var readDuration: Long = 0,
|
||||||
@ProtoNumber(4) var readPages: Long = 0,
|
|
||||||
) {
|
) {
|
||||||
fun getHistoryImpl(): History {
|
fun getHistoryImpl(): History {
|
||||||
return History.create().copy(
|
return History.create().copy(
|
||||||
readAt = Date(lastRead),
|
readAt = Date(lastRead),
|
||||||
readDuration = readDuration,
|
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.BackupDecoder
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupNotifier
|
import eu.kanade.tachiyomi.data.backup.BackupNotifier
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupCategory
|
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.BackupFeed
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupManga
|
import eu.kanade.tachiyomi.data.backup.models.BackupManga
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupPreference
|
import eu.kanade.tachiyomi.data.backup.models.BackupPreference
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupSavedSearch
|
import eu.kanade.tachiyomi.data.backup.models.BackupSavedSearch
|
||||||
import eu.kanade.tachiyomi.data.backup.models.BackupSourcePreferences
|
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.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.FeedRestorer
|
||||||
import eu.kanade.tachiyomi.data.backup.restore.restorers.MangaRestorer
|
import eu.kanade.tachiyomi.data.backup.restore.restorers.MangaRestorer
|
||||||
import eu.kanade.tachiyomi.data.backup.restore.restorers.PreferenceRestorer
|
import eu.kanade.tachiyomi.data.backup.restore.restorers.PreferenceRestorer
|
||||||
|
|
@ -38,7 +38,7 @@ class BackupRestorer(
|
||||||
|
|
||||||
private val categoriesRestorer: CategoriesRestorer = CategoriesRestorer(),
|
private val categoriesRestorer: CategoriesRestorer = CategoriesRestorer(),
|
||||||
private val preferenceRestorer: PreferenceRestorer = PreferenceRestorer(context),
|
private val preferenceRestorer: PreferenceRestorer = PreferenceRestorer(context),
|
||||||
private val extensionStoreRestorer: ExtensionStoreRestorer = ExtensionStoreRestorer(),
|
private val extensionRepoRestorer: ExtensionRepoRestorer = ExtensionRepoRestorer(),
|
||||||
private val mangaRestorer: MangaRestorer = MangaRestorer(isSync),
|
private val mangaRestorer: MangaRestorer = MangaRestorer(isSync),
|
||||||
// SY -->
|
// SY -->
|
||||||
private val savedSearchRestorer: SavedSearchRestorer = SavedSearchRestorer(),
|
private val savedSearchRestorer: SavedSearchRestorer = SavedSearchRestorer(),
|
||||||
|
|
@ -96,8 +96,8 @@ class BackupRestorer(
|
||||||
if (options.appSettings) {
|
if (options.appSettings) {
|
||||||
restoreAmount += 1
|
restoreAmount += 1
|
||||||
}
|
}
|
||||||
if (options.extensionStores) {
|
if (options.extensionRepoSettings) {
|
||||||
restoreAmount += backup.backupExtensionStores.size
|
restoreAmount += backup.backupExtensionRepo.size
|
||||||
}
|
}
|
||||||
if (options.sourceSettings) {
|
if (options.sourceSettings) {
|
||||||
restoreAmount += 1
|
restoreAmount += 1
|
||||||
|
|
@ -126,8 +126,8 @@ class BackupRestorer(
|
||||||
if (options.libraryEntries) {
|
if (options.libraryEntries) {
|
||||||
restoreManga(backup.backupManga, if (options.categories) backup.backupCategories else emptyList())
|
restoreManga(backup.backupManga, if (options.categories) backup.backupCategories else emptyList())
|
||||||
}
|
}
|
||||||
if (options.extensionStores) {
|
if (options.extensionRepoSettings) {
|
||||||
restoreExtensionStores(backup.backupExtensionStores)
|
restoreExtensionRepos(backup.backupExtensionRepo)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: optionally trigger online library + tracker update
|
// TODO: optionally trigger online library + tracker update
|
||||||
|
|
@ -248,25 +248,23 @@ class BackupRestorer(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun CoroutineScope.restoreExtensionStores(
|
private fun CoroutineScope.restoreExtensionRepos(
|
||||||
backupExtensionStores: List<BackupExtensionStore>,
|
backupExtensionRepo: List<BackupExtensionRepos>,
|
||||||
) = launch {
|
) = launch {
|
||||||
backupExtensionStores
|
backupExtensionRepo
|
||||||
.forEach {
|
.forEach {
|
||||||
ensureActive()
|
ensureActive()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
extensionStoreRestorer(it)
|
extensionRepoRestorer(it)
|
||||||
} catch (e: Exception) {
|
} 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
|
restoreProgress += 1
|
||||||
// KMK -->
|
|
||||||
with(notifier) {
|
with(notifier) {
|
||||||
// KMK <--
|
|
||||||
showRestoreProgress(
|
showRestoreProgress(
|
||||||
context.stringResource(MR.strings.extensionStores),
|
context.stringResource(MR.strings.extensionRepo_settings),
|
||||||
restoreProgress,
|
restoreProgress,
|
||||||
restoreAmount,
|
restoreAmount,
|
||||||
isSync,
|
isSync,
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,7 @@ data class RestoreOptions(
|
||||||
val libraryEntries: Boolean = true,
|
val libraryEntries: Boolean = true,
|
||||||
val categories: Boolean = true,
|
val categories: Boolean = true,
|
||||||
val appSettings: Boolean = true,
|
val appSettings: Boolean = true,
|
||||||
val extensionStores: Boolean = true,
|
val extensionRepoSettings: Boolean = true,
|
||||||
val sourceSettings: Boolean = true,
|
val sourceSettings: Boolean = true,
|
||||||
// SY -->
|
// SY -->
|
||||||
val savedSearchesFeeds: Boolean = true,
|
val savedSearchesFeeds: Boolean = true,
|
||||||
|
|
@ -20,7 +20,7 @@ data class RestoreOptions(
|
||||||
libraryEntries,
|
libraryEntries,
|
||||||
categories,
|
categories,
|
||||||
appSettings,
|
appSettings,
|
||||||
extensionStores,
|
extensionRepoSettings,
|
||||||
sourceSettings,
|
sourceSettings,
|
||||||
// SY -->
|
// SY -->
|
||||||
savedSearchesFeeds,
|
savedSearchesFeeds,
|
||||||
|
|
@ -31,7 +31,7 @@ data class RestoreOptions(
|
||||||
libraryEntries ||
|
libraryEntries ||
|
||||||
categories ||
|
categories ||
|
||||||
appSettings ||
|
appSettings ||
|
||||||
extensionStores ||
|
extensionRepoSettings ||
|
||||||
sourceSettings /* SY --> */ ||
|
sourceSettings /* SY --> */ ||
|
||||||
savedSearchesFeeds /* SY <-- */
|
savedSearchesFeeds /* SY <-- */
|
||||||
|
|
||||||
|
|
@ -53,9 +53,9 @@ data class RestoreOptions(
|
||||||
setter = { options, enabled -> options.copy(appSettings = enabled) },
|
setter = { options, enabled -> options.copy(appSettings = enabled) },
|
||||||
),
|
),
|
||||||
Entry(
|
Entry(
|
||||||
label = MR.strings.extensionStores,
|
label = MR.strings.extensionRepo_settings,
|
||||||
getter = RestoreOptions::extensionStores,
|
getter = RestoreOptions::extensionRepoSettings,
|
||||||
setter = { options, enabled -> options.copy(extensionStores = enabled) },
|
setter = { options, enabled -> options.copy(extensionRepoSettings = enabled) },
|
||||||
),
|
),
|
||||||
Entry(
|
Entry(
|
||||||
label = MR.strings.source_settings,
|
label = MR.strings.source_settings,
|
||||||
|
|
@ -77,7 +77,7 @@ data class RestoreOptions(
|
||||||
libraryEntries = array[0],
|
libraryEntries = array[0],
|
||||||
categories = array[1],
|
categories = array[1],
|
||||||
appSettings = array[2],
|
appSettings = array[2],
|
||||||
extensionStores = array[3],
|
extensionRepoSettings = array[3],
|
||||||
sourceSettings = array[4],
|
sourceSettings = array[4],
|
||||||
// SY -->
|
// SY -->
|
||||||
savedSearchesFeeds = array[5],
|
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 }
|
.takeIf { it > 0L }
|
||||||
?.let { Date(it) },
|
?.let { Date(it) },
|
||||||
readDuration = max(item.readDuration, dbHistory.time_read) - dbHistory.time_read,
|
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.chapterId,
|
||||||
it.readAt,
|
it.readAt,
|
||||||
it.readDuration,
|
it.readDuration,
|
||||||
it.readPages,
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,6 @@ import kotlinx.coroutines.sync.Semaphore
|
||||||
import kotlinx.coroutines.sync.withPermit
|
import kotlinx.coroutines.sync.withPermit
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
import mihon.domain.chapter.interactor.FilterChaptersForDownload
|
import mihon.domain.chapter.interactor.FilterChaptersForDownload
|
||||||
import mihon.domain.chapter.interactor.GetChaptersForLibraryUpdateDownload
|
|
||||||
import tachiyomi.core.common.i18n.stringResource
|
import tachiyomi.core.common.i18n.stringResource
|
||||||
import tachiyomi.core.common.preference.getAndSet
|
import tachiyomi.core.common.preference.getAndSet
|
||||||
import tachiyomi.core.common.util.lang.withIOContext
|
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 syncChaptersWithSource: SyncChaptersWithSource = Injekt.get()
|
||||||
private val fetchInterval: FetchInterval = Injekt.get()
|
private val fetchInterval: FetchInterval = Injekt.get()
|
||||||
private val filterChaptersForDownload: FilterChaptersForDownload = Injekt.get()
|
private val filterChaptersForDownload: FilterChaptersForDownload = Injekt.get()
|
||||||
private val getChaptersForLibraryUpdateDownload: GetChaptersForLibraryUpdateDownload = Injekt.get()
|
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
private val getFavorites: GetFavorites = Injekt.get()
|
private val getFavorites: GetFavorites = Injekt.get()
|
||||||
|
|
@ -467,7 +465,6 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
||||||
// Convert to the manga that contains new chapters
|
// Convert to the manga that contains new chapters
|
||||||
newUpdates.add(manga to newChapters.toTypedArray())
|
newUpdates.add(manga to newChapters.toTypedArray())
|
||||||
}
|
}
|
||||||
|
|
||||||
clearErrorFromDB(mangaId = manga.id)
|
clearErrorFromDB(mangaId = manga.id)
|
||||||
} catch (e: Throwable) {
|
} catch (e: Throwable) {
|
||||||
val errorMessage = when (e) {
|
val errorMessage = when (e) {
|
||||||
|
|
@ -484,17 +481,6 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
||||||
writeErrorToDB(manga to errorMessage)
|
writeErrorToDB(manga to errorMessage)
|
||||||
failedUpdates.add(manga to errorMessage)
|
failedUpdates.add(manga to errorMessage)
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
|
||||||
val libraryUpdateChapters = getChaptersForLibraryUpdateDownload.await(manga)
|
|
||||||
if (libraryUpdateChapters.isNotEmpty()) {
|
|
||||||
downloadChapters(manga, libraryUpdateChapters)
|
|
||||||
hasDownloads.store(true)
|
|
||||||
}
|
|
||||||
} catch (e: Throwable) {
|
|
||||||
if (e is CancellationException) throw e
|
|
||||||
xLogE("Failed to queue library update downloads for ${manga.title}", e)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -507,10 +493,9 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
||||||
|
|
||||||
if (newUpdates.isNotEmpty()) {
|
if (newUpdates.isNotEmpty()) {
|
||||||
notifier.showUpdateNotifications(newUpdates)
|
notifier.showUpdateNotifications(newUpdates)
|
||||||
}
|
if (hasDownloads.load()) {
|
||||||
|
downloadManager.startDownloads()
|
||||||
if (hasDownloads.load()) {
|
}
|
||||||
downloadManager.startDownloads()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (failedUpdates.isNotEmpty()) {
|
if (failedUpdates.isNotEmpty()) {
|
||||||
|
|
@ -763,6 +748,10 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
||||||
private const val KEY_MANGA_IDS = "manga_ids"
|
private const val KEY_MANGA_IDS = "manga_ids"
|
||||||
// KMK <--
|
// KMK <--
|
||||||
|
|
||||||
|
fun cancelAllWorks(context: Context) {
|
||||||
|
context.workManager.cancelAllWorkByTag(TAG)
|
||||||
|
}
|
||||||
|
|
||||||
fun setupTask(
|
fun setupTask(
|
||||||
context: Context,
|
context: Context,
|
||||||
prefInterval: Int? = null,
|
prefInterval: Int? = null,
|
||||||
|
|
@ -776,19 +765,16 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
|
||||||
} else {
|
} else {
|
||||||
NetworkType.CONNECTED
|
NetworkType.CONNECTED
|
||||||
}
|
}
|
||||||
val networkRequest = NetworkRequest.Builder().apply {
|
val networkRequestBuilder = NetworkRequest.Builder()
|
||||||
removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
|
if (DEVICE_ONLY_ON_WIFI in restrictions) {
|
||||||
if (DEVICE_ONLY_ON_WIFI in restrictions) {
|
networkRequestBuilder.addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
|
||||||
addTransportType(NetworkCapabilities.TRANSPORT_WIFI)
|
}
|
||||||
}
|
if (DEVICE_NETWORK_NOT_METERED in restrictions) {
|
||||||
if (DEVICE_NETWORK_NOT_METERED in restrictions) {
|
networkRequestBuilder.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
|
||||||
addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_METERED)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.build()
|
|
||||||
val constraints = Constraints.Builder()
|
val constraints = Constraints.Builder()
|
||||||
// 'networkRequest' only applies to Android 9+, otherwise 'networkType' is used
|
// 'networkRequest' only applies to Android 9+, otherwise 'networkType' is used
|
||||||
.setRequiredNetworkRequest(networkRequest, networkType)
|
.setRequiredNetworkRequest(networkRequestBuilder.build(), networkType)
|
||||||
.setRequiresCharging(DEVICE_CHARGING in restrictions)
|
.setRequiresCharging(DEVICE_CHARGING in restrictions)
|
||||||
.setRequiresBatteryNotLow(true)
|
.setRequiresBatteryNotLow(true)
|
||||||
.build()
|
.build()
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,7 @@ class SyncManager(
|
||||||
SYNCYOMI(1),
|
SYNCYOMI(1),
|
||||||
GOOGLE_DRIVE(2),
|
GOOGLE_DRIVE(2),
|
||||||
// KMK -->
|
// KMK -->
|
||||||
WEB_DAV(3),
|
WebDAV(3),
|
||||||
// KMK <--
|
// KMK <--
|
||||||
;
|
;
|
||||||
|
|
||||||
|
|
@ -88,7 +88,7 @@ class SyncManager(
|
||||||
chapters = syncOptions.chapters,
|
chapters = syncOptions.chapters,
|
||||||
tracking = syncOptions.tracking,
|
tracking = syncOptions.tracking,
|
||||||
history = syncOptions.history,
|
history = syncOptions.history,
|
||||||
extensionStores = syncOptions.extensionStores,
|
extensionRepoSettings = syncOptions.extensionRepoSettings,
|
||||||
appSettings = syncOptions.appSettings,
|
appSettings = syncOptions.appSettings,
|
||||||
sourceSettings = syncOptions.sourceSettings,
|
sourceSettings = syncOptions.sourceSettings,
|
||||||
privateSettings = syncOptions.privateSettings,
|
privateSettings = syncOptions.privateSettings,
|
||||||
|
|
@ -107,8 +107,8 @@ class SyncManager(
|
||||||
backupCategories = backupCreator.backupCategories(backupOptions),
|
backupCategories = backupCreator.backupCategories(backupOptions),
|
||||||
backupSources = backupCreator.backupSources(backupManga),
|
backupSources = backupCreator.backupSources(backupManga),
|
||||||
backupPreferences = backupCreator.backupAppPreferences(backupOptions),
|
backupPreferences = backupCreator.backupAppPreferences(backupOptions),
|
||||||
|
backupExtensionRepo = backupCreator.backupExtensionRepos(backupOptions),
|
||||||
backupSourcePreferences = backupCreator.backupSourcePreferences(backupOptions),
|
backupSourcePreferences = backupCreator.backupSourcePreferences(backupOptions),
|
||||||
backupExtensionStores = backupCreator.backupExtensionStores(backupOptions),
|
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
backupSavedSearches = backupCreator.backupSavedSearches(backupOptions),
|
backupSavedSearches = backupCreator.backupSavedSearches(backupOptions),
|
||||||
|
|
@ -142,7 +142,7 @@ class SyncManager(
|
||||||
}
|
}
|
||||||
|
|
||||||
// KMK -->
|
// KMK -->
|
||||||
SyncService.WEB_DAV -> {
|
SyncService.WebDAV -> {
|
||||||
WebDavSyncService(context, json, syncPreferences, notifier)
|
WebDavSyncService(context, json, syncPreferences, notifier)
|
||||||
}
|
}
|
||||||
// KMK <--
|
// KMK <--
|
||||||
|
|
@ -192,7 +192,7 @@ class SyncManager(
|
||||||
backupSources = remoteBackup.backupSources,
|
backupSources = remoteBackup.backupSources,
|
||||||
backupPreferences = remoteBackup.backupPreferences,
|
backupPreferences = remoteBackup.backupPreferences,
|
||||||
backupSourcePreferences = remoteBackup.backupSourcePreferences,
|
backupSourcePreferences = remoteBackup.backupSourcePreferences,
|
||||||
backupExtensionStores = remoteBackup.backupExtensionStores,
|
backupExtensionRepo = remoteBackup.backupExtensionRepo,
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
backupSavedSearches = remoteBackup.backupSavedSearches,
|
backupSavedSearches = remoteBackup.backupSavedSearches,
|
||||||
|
|
@ -222,7 +222,7 @@ class SyncManager(
|
||||||
appSettings = true,
|
appSettings = true,
|
||||||
sourceSettings = true,
|
sourceSettings = true,
|
||||||
libraryEntries = 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.backup.models.Backup
|
||||||
import eu.kanade.tachiyomi.data.sync.SyncNotifier
|
import eu.kanade.tachiyomi.data.sync.SyncNotifier
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.network.POST
|
|
||||||
import eu.kanade.tachiyomi.network.PUT
|
import eu.kanade.tachiyomi.network.PUT
|
||||||
import eu.kanade.tachiyomi.network.await
|
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.SerializationException
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import kotlinx.serialization.protobuf.ProtoBuf
|
import kotlinx.serialization.protobuf.ProtoBuf
|
||||||
|
|
@ -27,6 +21,7 @@ import tachiyomi.core.common.i18n.stringResource
|
||||||
import tachiyomi.i18n.MR
|
import tachiyomi.i18n.MR
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
class SyncYomiSyncService(
|
class SyncYomiSyncService(
|
||||||
context: Context,
|
context: Context,
|
||||||
|
|
@ -37,34 +32,9 @@ class SyncYomiSyncService(
|
||||||
private val protoBuf: ProtoBuf = Injekt.get(),
|
private val protoBuf: ProtoBuf = Injekt.get(),
|
||||||
) : SyncService(context, json, syncPreferences) {
|
) : SyncService(context, json, syncPreferences) {
|
||||||
|
|
||||||
// KMK -->
|
|
||||||
companion object {
|
|
||||||
private val client by lazy { OkHttpClient() }
|
|
||||||
}
|
|
||||||
// KMK <--
|
|
||||||
|
|
||||||
private class SyncYomiException(message: String?) : Exception(message)
|
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? {
|
override suspend fun doSync(syncData: SyncData): Backup? {
|
||||||
reportSyncEvent(SyncEventStatus.SYNC_STARTED)
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
val (remoteData, etag) = pullSyncData()
|
val (remoteData, etag) = pullSyncData()
|
||||||
|
|
||||||
|
|
@ -82,26 +52,11 @@ class SyncYomiSyncService(
|
||||||
syncData
|
syncData
|
||||||
}
|
}
|
||||||
|
|
||||||
val success = pushSyncData(finalSyncData, etag)
|
pushSyncData(finalSyncData, etag)
|
||||||
|
|
||||||
if (success) {
|
|
||||||
reportSyncEvent(SyncEventStatus.SYNC_SUCCESS)
|
|
||||||
} else {
|
|
||||||
reportSyncEvent(SyncEventStatus.SYNC_FAILED, "Failed to push sync data")
|
|
||||||
// KMK -->
|
|
||||||
return null
|
|
||||||
// KMK <--
|
|
||||||
}
|
|
||||||
|
|
||||||
return finalSyncData.backup
|
return finalSyncData.backup
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
if (e is CancellationException) {
|
|
||||||
reportSyncEvent(SyncEventStatus.SYNC_CANCELLED, e.message)
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
logcat(LogPriority.ERROR) { "Error syncing: ${e.message}" }
|
logcat(LogPriority.ERROR) { "Error syncing: ${e.message}" }
|
||||||
notifier.showSyncError(e.message)
|
notifier.showSyncError(e.message)
|
||||||
reportSyncEvent(SyncEventStatus.SYNC_ERROR, e.message)
|
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -123,6 +78,7 @@ class SyncYomiSyncService(
|
||||||
headers = headers,
|
headers = headers,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val client = OkHttpClient()
|
||||||
val response = client.newCall(downloadRequest).await()
|
val response = client.newCall(downloadRequest).await()
|
||||||
|
|
||||||
if (response.code == HttpStatus.SC_NOT_MODIFIED) {
|
if (response.code == HttpStatus.SC_NOT_MODIFIED) {
|
||||||
|
|
@ -167,12 +123,13 @@ class SyncYomiSyncService(
|
||||||
/**
|
/**
|
||||||
* Return true if update success
|
* Return true if update success
|
||||||
*/
|
*/
|
||||||
private suspend fun pushSyncData(syncData: SyncData, eTag: String): Boolean {
|
private suspend fun pushSyncData(syncData: SyncData, eTag: String) {
|
||||||
val backup = syncData.backup ?: return true
|
val backup = syncData.backup ?: return
|
||||||
|
|
||||||
val host = syncPreferences.clientHost().get()
|
val host = syncPreferences.clientHost().get()
|
||||||
val apiKey = syncPreferences.clientAPIKey().get()
|
val apiKey = syncPreferences.clientAPIKey().get()
|
||||||
val uploadUrl = "$host/api/sync/content"
|
val uploadUrl = "$host/api/sync/content"
|
||||||
|
val timeout = 30L
|
||||||
|
|
||||||
val headersBuilder = Headers.Builder().add("X-API-Token", apiKey)
|
val headersBuilder = Headers.Builder().add("X-API-Token", apiKey)
|
||||||
if (eTag.isNotEmpty()) {
|
if (eTag.isNotEmpty()) {
|
||||||
|
|
@ -180,6 +137,13 @@ class SyncYomiSyncService(
|
||||||
}
|
}
|
||||||
val headers = headersBuilder.build()
|
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)
|
val byteArray = protoBuf.encodeToByteArray(Backup.serializer(), backup)
|
||||||
if (byteArray.isEmpty()) {
|
if (byteArray.isEmpty()) {
|
||||||
throw IllegalStateException(context.stringResource(MR.strings.empty_backup_error))
|
throw IllegalStateException(context.stringResource(MR.strings.empty_backup_error))
|
||||||
|
|
@ -192,58 +156,20 @@ class SyncYomiSyncService(
|
||||||
body = body,
|
body = body,
|
||||||
)
|
)
|
||||||
|
|
||||||
client.newCall(uploadRequest).await()
|
val response = 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun reportSyncEvent(event: SyncEventStatus, message: String? = null) {
|
if (response.isSuccessful) {
|
||||||
withContext(NonCancellable) {
|
val newETag = response.headers["ETag"]
|
||||||
try {
|
.takeIf { it?.isNotEmpty() == true } ?: throw SyncYomiException("Missing ETag")
|
||||||
val host = syncPreferences.clientHost().get()
|
syncPreferences.lastSyncEtag().set(newETag)
|
||||||
val apiKey = syncPreferences.clientAPIKey().get()
|
logcat(LogPriority.DEBUG) { "SyncYomi sync completed" }
|
||||||
val url = "$host/api/sync/event"
|
} else if (response.code == HttpStatus.SC_PRECONDITION_FAILED) {
|
||||||
|
// other clients updated remote data, will try next time
|
||||||
val headersBuilder = Headers.Builder().add("X-API-Token", apiKey)
|
logcat(LogPriority.DEBUG) { "SyncYomi sync failed with 412" }
|
||||||
val headers = headersBuilder.build()
|
} else {
|
||||||
|
val responseBody = response.body.string()
|
||||||
val bodyObj = SyncEvent(
|
notifier.showSyncError("Failed to upload sync data: $responseBody")
|
||||||
event = event,
|
logcat(LogPriority.ERROR) { "SyncError: $responseBody" }
|
||||||
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}" }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -40,7 +40,7 @@ class WebDavSyncService(
|
||||||
private val url: String = syncPreferences.webDavUrl().get().trim()
|
private val url: String = syncPreferences.webDavUrl().get().trim()
|
||||||
private val folder: String = syncPreferences.webDavFolder().get().trim('/')
|
private val folder: String = syncPreferences.webDavFolder().get().trim('/')
|
||||||
private val username: String = syncPreferences.webDavUsername().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 val credentials: String = Credentials.basic(username, password)
|
||||||
|
|
||||||
private fun buildWebDavFileUrl(fileName: String = "backup.proto"): String {
|
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.database.models.Track
|
||||||
import eu.kanade.tachiyomi.data.track.anilist.dto.ALAddMangaResult
|
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.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.ALIdSearchResult
|
||||||
import eu.kanade.tachiyomi.data.track.anilist.dto.ALMangaMetadata
|
import eu.kanade.tachiyomi.data.track.anilist.dto.ALMangaMetadata
|
||||||
import eu.kanade.tachiyomi.data.track.anilist.dto.ALOAuth
|
import eu.kanade.tachiyomi.data.track.anilist.dto.ALOAuth
|
||||||
|
|
@ -27,7 +26,6 @@ import kotlinx.serialization.json.put
|
||||||
import kotlinx.serialization.json.putJsonObject
|
import kotlinx.serialization.json.putJsonObject
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
import okhttp3.Response
|
|
||||||
import tachiyomi.core.common.util.lang.withIOContext
|
import tachiyomi.core.common.util.lang.withIOContext
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
import java.time.Instant
|
import java.time.Instant
|
||||||
|
|
@ -45,25 +43,6 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||||
.rateLimit(permits = 85, period = 1.minutes)
|
.rateLimit(permits = 85, period = 1.minutes)
|
||||||
.build()
|
.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 {
|
suspend fun addLibManga(track: Track): Track {
|
||||||
return withIOContext {
|
return withIOContext {
|
||||||
val query = $$"""
|
val query = $$"""
|
||||||
|
|
@ -92,9 +71,6 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.awaitSuccess()
|
.awaitSuccess()
|
||||||
// KMK -->
|
|
||||||
.also { it.parseALError() }
|
|
||||||
// KMK <--
|
|
||||||
.parseAs<ALAddMangaResult>()
|
.parseAs<ALAddMangaResult>()
|
||||||
.let {
|
.let {
|
||||||
track.library_id = it.data.entry.id
|
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)))
|
authClient.newCall(POST(API_URL, body = payload.toString().toRequestBody(jsonMime)))
|
||||||
.awaitSuccess()
|
.awaitSuccess()
|
||||||
// KMK -->
|
|
||||||
.use { it.parseALError() }
|
|
||||||
// KMK <--
|
|
||||||
track
|
track
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -161,9 +134,6 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||||
}
|
}
|
||||||
authClient.newCall(POST(API_URL, body = payload.toString().toRequestBody(jsonMime)))
|
authClient.newCall(POST(API_URL, body = payload.toString().toRequestBody(jsonMime)))
|
||||||
.awaitSuccess()
|
.awaitSuccess()
|
||||||
// KMK -->
|
|
||||||
.use { it.parseALError() }
|
|
||||||
// KMK <--
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -222,9 +192,6 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.awaitSuccess()
|
.awaitSuccess()
|
||||||
// KMK -->
|
|
||||||
.also { it.parseALError() }
|
|
||||||
// KMK <--
|
|
||||||
.parseAs<ALSearchResult>()
|
.parseAs<ALSearchResult>()
|
||||||
.data.page.media
|
.data.page.media
|
||||||
.map { it.toALManga().toTrack() }
|
.map { it.toALManga().toTrack() }
|
||||||
|
|
@ -304,9 +271,6 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.awaitSuccess()
|
.awaitSuccess()
|
||||||
// KMK -->
|
|
||||||
.also { it.parseALError() }
|
|
||||||
// KMK <--
|
|
||||||
.parseAs<ALUserListMangaQueryResult>()
|
.parseAs<ALUserListMangaQueryResult>()
|
||||||
.data.page.mediaList
|
.data.page.mediaList
|
||||||
.map { it.toALUserManga() }
|
.map { it.toALUserManga() }
|
||||||
|
|
@ -348,9 +312,6 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.awaitSuccess()
|
.awaitSuccess()
|
||||||
// KMK -->
|
|
||||||
.also { it.parseALError() }
|
|
||||||
// KMK <--
|
|
||||||
.parseAs<ALCurrentUserResult>()
|
.parseAs<ALCurrentUserResult>()
|
||||||
.let {
|
.let {
|
||||||
val viewer = it.data.viewer
|
val viewer = it.data.viewer
|
||||||
|
|
@ -362,9 +323,9 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||||
|
|
||||||
suspend fun getMangaMetadata(track: DomainTrack): TrackMangaMetadata {
|
suspend fun getMangaMetadata(track: DomainTrack): TrackMangaMetadata {
|
||||||
return withIOContext {
|
return withIOContext {
|
||||||
val query = $$"""
|
val query = """
|
||||||
|query ($mangaId: Int!) {
|
|query (${'$'}mangaId: Int!) {
|
||||||
|Media (id: $mangaId) {
|
|Media (id: ${'$'}mangaId) {
|
||||||
|id
|
|id
|
||||||
|title {
|
|title {
|
||||||
|userPreferred
|
|userPreferred
|
||||||
|
|
@ -404,12 +365,9 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.awaitSuccess()
|
.awaitSuccess()
|
||||||
// KMK -->
|
|
||||||
.also { it.parseALError() }
|
|
||||||
// KMK <--
|
|
||||||
.parseAs<ALMangaMetadata>()
|
.parseAs<ALMangaMetadata>()
|
||||||
.let { metadata ->
|
.let {
|
||||||
val media = metadata.data.media
|
val media = it.data.media
|
||||||
TrackMangaMetadata(
|
TrackMangaMetadata(
|
||||||
remoteId = media.id,
|
remoteId = media.id,
|
||||||
title = media.title.userPreferred,
|
title = media.title.userPreferred,
|
||||||
|
|
@ -434,9 +392,9 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||||
// SY -->
|
// SY -->
|
||||||
suspend fun searchById(id: String): TrackSearch {
|
suspend fun searchById(id: String): TrackSearch {
|
||||||
return withIOContext {
|
return withIOContext {
|
||||||
val query = $$"""
|
val query = """
|
||||||
|query ($mangaId: Int!) {
|
|query (${'$'}mangaId: Int!) {
|
||||||
|Media (id: $mangaId) {
|
|Media (id: ${'$'}mangaId) {
|
||||||
|id
|
|id
|
||||||
|title {
|
|title {
|
||||||
|userPreferred
|
|userPreferred
|
||||||
|
|
@ -472,9 +430,6 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.awaitSuccess()
|
.awaitSuccess()
|
||||||
// KMK -->
|
|
||||||
.also { it.parseALError() }
|
|
||||||
// KMK <--
|
|
||||||
.parseAs<ALIdSearchResult>()
|
.parseAs<ALIdSearchResult>()
|
||||||
.data.media
|
.data.media
|
||||||
.toALManga()
|
.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.data.track.myanimelist.dto.MALUser
|
||||||
import eu.kanade.tachiyomi.network.DELETE
|
import eu.kanade.tachiyomi.network.DELETE
|
||||||
import eu.kanade.tachiyomi.network.GET
|
import eu.kanade.tachiyomi.network.GET
|
||||||
import eu.kanade.tachiyomi.network.HttpException
|
|
||||||
import eu.kanade.tachiyomi.network.POST
|
import eu.kanade.tachiyomi.network.POST
|
||||||
import eu.kanade.tachiyomi.network.await
|
|
||||||
import eu.kanade.tachiyomi.network.awaitSuccess
|
import eu.kanade.tachiyomi.network.awaitSuccess
|
||||||
import eu.kanade.tachiyomi.network.parseAs
|
import eu.kanade.tachiyomi.network.parseAs
|
||||||
import eu.kanade.tachiyomi.util.PkceUtil
|
import eu.kanade.tachiyomi.util.PkceUtil
|
||||||
|
|
@ -126,23 +124,8 @@ class MyAnimeListApi(
|
||||||
.put(formBodyBuilder.build())
|
.put(formBodyBuilder.build())
|
||||||
.build()
|
.build()
|
||||||
with(json) {
|
with(json) {
|
||||||
val response = authClient
|
authClient.newCall(request)
|
||||||
.newCall(request)
|
.awaitSuccess()
|
||||||
.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
|
|
||||||
.parseAs<MALListItemStatus>()
|
.parseAs<MALListItemStatus>()
|
||||||
.let { parseMangaItem(it, track) }
|
.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 MALTokenRefreshFailed : IOException("MAL: Failed to refresh account token")
|
||||||
class MALTokenExpired : IOException("MAL: Login has expired")
|
class MALTokenExpired : IOException("MAL: Login has expired")
|
||||||
|
|
|
||||||
|
|
@ -93,17 +93,13 @@ class AppUpdateChecker(
|
||||||
// KMK <--
|
// 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() }
|
val GITHUB_REPO: String by lazy { getGithubRepo() }
|
||||||
|
|
||||||
fun getGithubRepo(peekIntoPreview: Boolean = false): String =
|
fun getGithubRepo(peekIntoPreview: Boolean = false): String =
|
||||||
if (isPreviewBuildType || peekIntoPreview) {
|
if (isPreviewBuildType || peekIntoPreview) {
|
||||||
FORK_PREVIEW_RELEASE_REPO
|
"komikku-app/komikku-preview"
|
||||||
} else {
|
} else {
|
||||||
FORK_RELEASE_REPO
|
"komikku-app/komikku"
|
||||||
}
|
}
|
||||||
|
|
||||||
val RELEASE_TAG: String by lazy { getReleaseTag() }
|
val RELEASE_TAG: String by lazy { getReleaseTag() }
|
||||||
|
|
@ -115,4 +111,4 @@ fun getReleaseTag(peekIntoPreview: Boolean = false): String =
|
||||||
"v${BuildConfig.VERSION_NAME}"
|
"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 {
|
Json {
|
||||||
ignoreUnknownKeys = true
|
ignoreUnknownKeys = true
|
||||||
explicitNulls = false
|
explicitNulls = false
|
||||||
// Gitea/Forgejo often uses JSON null for empty release body or assets array.
|
|
||||||
coerceInputValues = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
addSingletonFactory {
|
addSingletonFactory {
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,6 @@ import tachiyomi.domain.backup.service.BackupPreferences
|
||||||
import tachiyomi.domain.download.service.DownloadPreferences
|
import tachiyomi.domain.download.service.DownloadPreferences
|
||||||
import tachiyomi.domain.history.service.HistoryPreferences
|
import tachiyomi.domain.history.service.HistoryPreferences
|
||||||
import tachiyomi.domain.library.service.LibraryPreferences
|
import tachiyomi.domain.library.service.LibraryPreferences
|
||||||
import tachiyomi.domain.reading.service.ReadingEtaPreferences
|
|
||||||
import tachiyomi.domain.storage.service.StoragePreferences
|
import tachiyomi.domain.storage.service.StoragePreferences
|
||||||
import tachiyomi.domain.updates.service.UpdatesPreferences
|
import tachiyomi.domain.updates.service.UpdatesPreferences
|
||||||
import uy.kohesive.injekt.api.InjektModule
|
import uy.kohesive.injekt.api.InjektModule
|
||||||
|
|
@ -62,9 +61,6 @@ class PreferenceModule(val app: Application) : InjektModule {
|
||||||
addSingletonFactory {
|
addSingletonFactory {
|
||||||
ReaderPreferences(get())
|
ReaderPreferences(get())
|
||||||
}
|
}
|
||||||
addSingletonFactory {
|
|
||||||
ReadingEtaPreferences(get())
|
|
||||||
}
|
|
||||||
addSingletonFactory {
|
addSingletonFactory {
|
||||||
TrackPreferences(get())
|
TrackPreferences(get())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -200,7 +200,7 @@ class ExtensionManager(
|
||||||
availableExtensionMapFlow.value = extensions.associateBy {
|
availableExtensionMapFlow.value = extensions.associateBy {
|
||||||
it.pkgName +
|
it.pkgName +
|
||||||
// KMK -->
|
// KMK -->
|
||||||
"_${it.signatureHash}"
|
":${it.signatureHash}"
|
||||||
// KMK <--
|
// KMK <--
|
||||||
}
|
}
|
||||||
updatedInstalledExtensionsStatuses(extensions)
|
updatedInstalledExtensionsStatuses(extensions)
|
||||||
|
|
@ -273,14 +273,14 @@ class ExtensionManager(
|
||||||
// Ext found: Update installed extensions with new information from repo
|
// Ext found: Update installed extensions with new information from repo
|
||||||
// Also clear isObsolete and set new repo Name if needed
|
// Also clear isObsolete and set new repo Name if needed
|
||||||
val hasUpdate = extension.updateExists(availableExt)
|
val hasUpdate = extension.updateExists(availableExt)
|
||||||
// KMK -->
|
|
||||||
installedExtensionsMap[pkgName] = extension.copy(
|
installedExtensionsMap[pkgName] = extension.copy(
|
||||||
hasUpdate = hasUpdate,
|
hasUpdate = hasUpdate,
|
||||||
store = availableExt.store,
|
repoUrl = availableExt.repoUrl,
|
||||||
|
// KMK -->
|
||||||
isObsolete = false,
|
isObsolete = false,
|
||||||
storeName = extension.storeName ?: availableExt.storeName,
|
repoName = extension.repoName ?: availableExt.repoName,
|
||||||
|
// KMK <--
|
||||||
)
|
)
|
||||||
// KMK <--
|
|
||||||
changed = true
|
changed = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -298,7 +298,7 @@ class ExtensionManager(
|
||||||
* @param extension The extension to be installed.
|
* @param extension The extension to be installed.
|
||||||
*/
|
*/
|
||||||
fun installExtension(extension: Extension.Available): Flow<InstallStep> {
|
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[
|
val availableExt = availableExtensionMapFlow.value[
|
||||||
extension.pkgName +
|
extension.pkgName +
|
||||||
// KMK -->
|
// KMK -->
|
||||||
"_${extension.signatureHash}",
|
":${extension.signatureHash}",
|
||||||
// KMK <--
|
// KMK <--
|
||||||
] ?: return emptyFlow()
|
] ?: return emptyFlow()
|
||||||
return installExtension(availableExt)
|
return installExtension(availableExt)
|
||||||
|
|
@ -322,7 +322,7 @@ class ExtensionManager(
|
||||||
installer.cancelInstall(
|
installer.cancelInstall(
|
||||||
extension.pkgName +
|
extension.pkgName +
|
||||||
// KMK -->
|
// KMK -->
|
||||||
"_${extension.signatureHash}",
|
":${extension.signatureHash}",
|
||||||
// KMK <--
|
// KMK <--
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,13 +6,24 @@ import eu.kanade.tachiyomi.extension.ExtensionManager
|
||||||
import eu.kanade.tachiyomi.extension.model.Extension
|
import eu.kanade.tachiyomi.extension.model.Extension
|
||||||
import eu.kanade.tachiyomi.extension.model.LoadResult
|
import eu.kanade.tachiyomi.extension.model.LoadResult
|
||||||
import eu.kanade.tachiyomi.extension.util.ExtensionLoader
|
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.BlacklistedSources
|
||||||
import exh.source.ExhPreferences
|
import exh.source.ExhPreferences
|
||||||
import mihon.domain.extension.interactor.UpdateExtensionStores
|
import kotlinx.coroutines.async
|
||||||
import mihon.domain.extension.repository.ExtensionStoreRepository
|
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.Preference
|
||||||
import tachiyomi.core.common.preference.PreferenceStore
|
import tachiyomi.core.common.preference.PreferenceStore
|
||||||
import tachiyomi.core.common.util.lang.withIOContext
|
import tachiyomi.core.common.util.lang.withIOContext
|
||||||
|
import tachiyomi.core.common.util.system.logcat
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import uy.kohesive.injekt.injectLazy
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
@ -21,16 +32,19 @@ import kotlin.time.Duration.Companion.days
|
||||||
|
|
||||||
internal class ExtensionApi {
|
internal class ExtensionApi {
|
||||||
|
|
||||||
private val repository: ExtensionStoreRepository by injectLazy()
|
private val networkService: NetworkHelper by injectLazy()
|
||||||
|
|
||||||
private val preferenceStore: PreferenceStore 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()
|
private val extensionManager: ExtensionManager by injectLazy()
|
||||||
|
|
||||||
// SY -->
|
// SY -->
|
||||||
private val sourcePreferences: SourcePreferences by injectLazy()
|
private val sourcePreferences: SourcePreferences by injectLazy()
|
||||||
|
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
||||||
|
private val json: Json by injectLazy()
|
||||||
|
|
||||||
private val lastExtCheck: Preference<Long> by lazy {
|
private val lastExtCheck: Preference<Long> by lazy {
|
||||||
preferenceStore.getLong(Preference.appStateKey("last_ext_check"), 0)
|
preferenceStore.getLong(Preference.appStateKey("last_ext_check"), 0)
|
||||||
}
|
}
|
||||||
|
|
@ -40,11 +54,37 @@ internal class ExtensionApi {
|
||||||
val disabledRepos = sourcePreferences.disabledRepos().get()
|
val disabledRepos = sourcePreferences.disabledRepos().get()
|
||||||
// KMK <--
|
// KMK <--
|
||||||
return withIOContext {
|
return withIOContext {
|
||||||
repository.fetchExtensions(
|
getExtensionRepo.getAll()
|
||||||
// KMK -->
|
// KMK -->
|
||||||
disabledRepos,
|
.filterNot { it.baseUrl in disabledRepos }
|
||||||
// KMK <--
|
// 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
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
updateExtensionStores()
|
// Update extension repo details
|
||||||
|
updateExtensionRepo.awaitAll()
|
||||||
|
|
||||||
val extensions = if (fromAvailableExtensionList) {
|
val extensions = if (fromAvailableExtensionList) {
|
||||||
extensionManager.availableExtensionsFlow.value
|
extensionManager.availableExtensionsFlow.value
|
||||||
|
|
@ -97,6 +138,47 @@ internal class ExtensionApi {
|
||||||
return extensionsWithUpdate
|
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 -->
|
// SY -->
|
||||||
private fun Extension.isBlacklisted(
|
private fun Extension.isBlacklisted(
|
||||||
blacklistEnabled: Boolean = sourcePreferences.enableSourceBlacklist().get(),
|
blacklistEnabled: Boolean = sourcePreferences.enableSourceBlacklist().get(),
|
||||||
|
|
@ -112,3 +194,32 @@ internal class ExtensionApi {
|
||||||
}
|
}
|
||||||
// SY <--
|
// 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 val extensionManager: ExtensionManager by injectLazy()
|
||||||
|
|
||||||
private var waitingInstall = AtomicReference<Entry?>(null)
|
private var waitingInstall = AtomicReference<Entry?>(null)
|
||||||
// KMK -->
|
private val queue = Collections.synchronizedList(mutableListOf<Entry>())
|
||||||
private val queue = Collections.synchronizedSet(mutableSetOf<Entry>())
|
|
||||||
// KMK <--
|
|
||||||
|
|
||||||
private val cancelReceiver = object : BroadcastReceiver() {
|
private val cancelReceiver = object : BroadcastReceiver() {
|
||||||
override fun onReceive(context: Context, intent: Intent) {
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
|
@ -106,9 +104,7 @@ abstract class Installer(private val service: Service) {
|
||||||
}
|
}
|
||||||
val nextEntry = queue.first()
|
val nextEntry = queue.first()
|
||||||
if (waitingInstall.compareAndSet(null, nextEntry)) {
|
if (waitingInstall.compareAndSet(null, nextEntry)) {
|
||||||
// KMK -->
|
queue.removeAt(0)
|
||||||
queue.remove(nextEntry)
|
|
||||||
// KMK <--
|
|
||||||
processEntry(nextEntry)
|
processEntry(nextEntry)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -133,11 +129,7 @@ abstract class Installer(private val service: Service) {
|
||||||
*/
|
*/
|
||||||
private fun cancelQueue(downloadId: Long) {
|
private fun cancelQueue(downloadId: Long) {
|
||||||
val waitingInstall = this.waitingInstall.load()
|
val waitingInstall = this.waitingInstall.load()
|
||||||
// KMK -->
|
val toCancel = queue.find { it.downloadId == downloadId } ?: waitingInstall ?: return
|
||||||
val toCancel = synchronized(queue) { queue.find { it.downloadId == downloadId } }
|
|
||||||
?: waitingInstall?.takeIf { it.downloadId == downloadId }
|
|
||||||
// KMK <--
|
|
||||||
?: return
|
|
||||||
if (cancelEntry(toCancel)) {
|
if (cancelEntry(toCancel)) {
|
||||||
queue.remove(toCancel)
|
queue.remove(toCancel)
|
||||||
if (waitingInstall == toCancel) {
|
if (waitingInstall == toCancel) {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
package eu.kanade.tachiyomi.extension.installer
|
package eu.kanade.tachiyomi.extension.installer
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.app.PendingIntent
|
import android.app.PendingIntent
|
||||||
import android.app.Service
|
import android.app.Service
|
||||||
import android.content.BroadcastReceiver
|
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.lang.use
|
||||||
import eu.kanade.tachiyomi.util.system.getParcelableExtraCompat
|
import eu.kanade.tachiyomi.util.system.getParcelableExtraCompat
|
||||||
import eu.kanade.tachiyomi.util.system.getUriSize
|
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) {
|
class PackageInstallerInstaller(private val service: Service) : Installer(service) {
|
||||||
|
|
||||||
|
|
@ -41,7 +41,7 @@ class PackageInstallerInstaller(private val service: Service) : Installer(servic
|
||||||
.sanitizeByFiltering(this)
|
.sanitizeByFiltering(this)
|
||||||
}
|
}
|
||||||
if (userAction == null) {
|
if (userAction == null) {
|
||||||
xLogE("Fatal error for $intent")
|
logcat(LogPriority.ERROR) { "Fatal error for $intent" }
|
||||||
continueQueue(InstallStep.Error)
|
continueQueue(InstallStep.Error)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -82,7 +82,6 @@ class PackageInstallerInstaller(private val service: Service) : Installer(servic
|
||||||
inputStream.copyTo(outputStream)
|
inputStream.copyTo(outputStream)
|
||||||
session.fsync(outputStream)
|
session.fsync(outputStream)
|
||||||
}
|
}
|
||||||
service.contentResolver.delete(entry.uri, null, null)
|
|
||||||
|
|
||||||
val intentSender = PendingIntent.getBroadcast(
|
val intentSender = PendingIntent.getBroadcast(
|
||||||
service,
|
service,
|
||||||
|
|
@ -90,11 +89,10 @@ class PackageInstallerInstaller(private val service: Service) : Installer(servic
|
||||||
Intent(INSTALL_ACTION).setPackage(service.packageName),
|
Intent(INSTALL_ACTION).setPackage(service.packageName),
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_MUTABLE else 0,
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) PendingIntent.FLAG_MUTABLE else 0,
|
||||||
).intentSender
|
).intentSender
|
||||||
@SuppressLint("RequestInstallPackagesPolicy")
|
|
||||||
session.commit(intentSender)
|
session.commit(intentSender)
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} 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) ->
|
activeSession?.let { (_, sessionId) ->
|
||||||
packageInstaller.abandonSession(sessionId)
|
packageInstaller.abandonSession(sessionId)
|
||||||
}
|
}
|
||||||
|
|
@ -105,13 +103,8 @@ class PackageInstallerInstaller(private val service: Service) : Installer(servic
|
||||||
override fun cancelEntry(entry: Entry): Boolean {
|
override fun cancelEntry(entry: Entry): Boolean {
|
||||||
activeSession?.let { (activeEntry, sessionId) ->
|
activeSession?.let { (activeEntry, sessionId) ->
|
||||||
if (activeEntry == entry) {
|
if (activeEntry == entry) {
|
||||||
return try {
|
packageInstaller.abandonSession(sessionId)
|
||||||
packageInstaller.abandonSession(sessionId)
|
return false
|
||||||
false
|
|
||||||
} catch (_: SecurityException) {
|
|
||||||
// Highly likely the session has succeeded
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
|
|
|
||||||
|
|
@ -111,17 +111,20 @@ class ShizukuInstaller(private val service: Service) : Installer(service) {
|
||||||
|
|
||||||
override fun processEntry(entry: Entry) {
|
override fun processEntry(entry: Entry) {
|
||||||
super.processEntry(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 {
|
try {
|
||||||
service.contentResolver.openAssetFileDescriptor(entry.uri, "r")?.use {
|
service.contentResolver.openAssetFileDescriptor(entry.uri, "r")?.use {
|
||||||
shellInterface?.install(it)
|
installer.install(it)
|
||||||
// KMK -->
|
|
||||||
?: throw Exception("Shell interface is not available")
|
|
||||||
// KMK <--
|
|
||||||
}
|
}
|
||||||
// KMK -->
|
|
||||||
?: throw Exception("Failed to open asset file descriptor")
|
|
||||||
// KMK <--
|
// KMK <--
|
||||||
service.contentResolver.delete(entry.uri, null, null)
|
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
logcat(LogPriority.ERROR, e) { "Failed to install extension ${entry.downloadId} ${entry.uri}" }
|
logcat(LogPriority.ERROR, e) { "Failed to install extension ${entry.downloadId} ${entry.uri}" }
|
||||||
continueQueue(InstallStep.Error)
|
continueQueue(InstallStep.Error)
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ package eu.kanade.tachiyomi.extension.model
|
||||||
|
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
import eu.kanade.tachiyomi.source.Source
|
import eu.kanade.tachiyomi.source.Source
|
||||||
import mihon.domain.extension.model.ExtensionStore
|
|
||||||
import tachiyomi.domain.source.model.StubSource
|
import tachiyomi.domain.source.model.StubSource
|
||||||
|
|
||||||
sealed class Extension {
|
sealed class Extension {
|
||||||
|
|
@ -17,7 +16,7 @@ sealed class Extension {
|
||||||
|
|
||||||
// KMK -->
|
// KMK -->
|
||||||
abstract val signatureHash: String
|
abstract val signatureHash: String
|
||||||
abstract val storeName: String?
|
abstract val repoName: String?
|
||||||
// KMK <--
|
// KMK <--
|
||||||
|
|
||||||
data class Installed(
|
data class Installed(
|
||||||
|
|
@ -30,8 +29,8 @@ sealed class Extension {
|
||||||
override val isNsfw: Boolean,
|
override val isNsfw: Boolean,
|
||||||
// KMK -->
|
// KMK -->
|
||||||
override val signatureHash: String,
|
override val signatureHash: String,
|
||||||
/** Guessing store name from built-in signatures preset */
|
/** Guessing repo name from built-in signatures preset */
|
||||||
override val storeName: String? = null,
|
override val repoName: String? = null,
|
||||||
// KMK <--
|
// KMK <--
|
||||||
val pkgFactory: String?,
|
val pkgFactory: String?,
|
||||||
val sources: List<Source>,
|
val sources: List<Source>,
|
||||||
|
|
@ -39,7 +38,7 @@ sealed class Extension {
|
||||||
val hasUpdate: Boolean = false,
|
val hasUpdate: Boolean = false,
|
||||||
val isObsolete: Boolean = false,
|
val isObsolete: Boolean = false,
|
||||||
val isShared: Boolean,
|
val isShared: Boolean,
|
||||||
val store: ExtensionStore? = null,
|
val repoUrl: String? = null,
|
||||||
// SY -->
|
// SY -->
|
||||||
val isRedundant: Boolean = false,
|
val isRedundant: Boolean = false,
|
||||||
// SY <--
|
// SY <--
|
||||||
|
|
@ -55,12 +54,12 @@ sealed class Extension {
|
||||||
override val isNsfw: Boolean,
|
override val isNsfw: Boolean,
|
||||||
// KMK -->
|
// KMK -->
|
||||||
override val signatureHash: String,
|
override val signatureHash: String,
|
||||||
override val storeName: String,
|
override val repoName: String,
|
||||||
// KMK <--
|
// KMK <--
|
||||||
val sources: List<Source>,
|
val sources: List<Source>,
|
||||||
val apkUrl: String,
|
val apkName: String,
|
||||||
val iconUrl: String,
|
val iconUrl: String,
|
||||||
val store: ExtensionStore,
|
val repoUrl: String,
|
||||||
) : Extension() {
|
) : Extension() {
|
||||||
|
|
||||||
data class Source(
|
data class Source(
|
||||||
|
|
@ -88,7 +87,7 @@ sealed class Extension {
|
||||||
/* KMK --> */
|
/* KMK --> */
|
||||||
override /* KMK <-- */ val signatureHash: String,
|
override /* KMK <-- */ val signatureHash: String,
|
||||||
// KMK -->
|
// KMK -->
|
||||||
override val storeName: String? = null,
|
override val repoName: String? = null,
|
||||||
// KMK <--
|
// KMK <--
|
||||||
override val lang: String? = null,
|
override val lang: String? = null,
|
||||||
override val isNsfw: Boolean = false,
|
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) {
|
private fun checkInstallationResult(resultCode: Int) {
|
||||||
val downloadId = intent.extras!!.getLong(ExtensionInstaller.EXTRA_DOWNLOAD_ID)
|
val downloadId = intent.extras!!.getLong(ExtensionInstaller.EXTRA_DOWNLOAD_ID)
|
||||||
val extensionManager = Injekt.get<ExtensionManager>()
|
val extensionManager = Injekt.get<ExtensionManager>()
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,8 @@ package eu.kanade.tachiyomi.extension.util
|
||||||
import android.app.Service
|
import android.app.Service
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.ServiceInfo
|
|
||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import eu.kanade.domain.base.BasePreferences
|
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.extension.util.ExtensionInstaller.Companion.EXTRA_DOWNLOAD_ID
|
||||||
import eu.kanade.tachiyomi.util.system.getSerializableExtraCompat
|
import eu.kanade.tachiyomi.util.system.getSerializableExtraCompat
|
||||||
import eu.kanade.tachiyomi.util.system.notificationBuilder
|
import eu.kanade.tachiyomi.util.system.notificationBuilder
|
||||||
import exh.log.xLogE
|
import logcat.LogPriority
|
||||||
import tachiyomi.core.common.i18n.stringResource
|
import tachiyomi.core.common.i18n.stringResource
|
||||||
|
import tachiyomi.core.common.util.system.logcat
|
||||||
import tachiyomi.i18n.MR
|
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() {
|
class ExtensionInstallService : Service() {
|
||||||
|
|
||||||
private var installer: Installer? = null
|
private var installer: Installer? = null
|
||||||
|
|
@ -42,17 +36,7 @@ class ExtensionInstallService : Service() {
|
||||||
setContentTitle(stringResource(MR.strings.ext_install_service_notif))
|
setContentTitle(stringResource(MR.strings.ext_install_service_notif))
|
||||||
setProgress(100, 100, true)
|
setProgress(100, 100, true)
|
||||||
}.build()
|
}.build()
|
||||||
// KMK -->
|
startForeground(Notifications.ID_EXTENSION_INSTALLER, notification)
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
|
|
@ -69,7 +53,7 @@ class ExtensionInstallService : Service() {
|
||||||
BasePreferences.ExtensionInstaller.PACKAGEINSTALLER -> PackageInstallerInstaller(this)
|
BasePreferences.ExtensionInstaller.PACKAGEINSTALLER -> PackageInstallerInstaller(this)
|
||||||
BasePreferences.ExtensionInstaller.SHIZUKU -> ShizukuInstaller(this)
|
BasePreferences.ExtensionInstaller.SHIZUKU -> ShizukuInstaller(this)
|
||||||
else -> {
|
else -> {
|
||||||
xLogE("Not implemented for installer $installerUsed")
|
logcat(LogPriority.ERROR) { "Not implemented for installer $installerUsed" }
|
||||||
stopSelf()
|
stopSelf()
|
||||||
return START_NOT_STICKY
|
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