Compare commits

..

No commits in common. "master" and "1.11.2" have entirely different histories.

581 changed files with 22791 additions and 52757 deletions

View file

@ -7,5 +7,5 @@ contact_links:
url: https://komga.org/docs/faq
about: Guides, troubleshooting, and answers to common questions
- name: ⚠️ Mihon extension
url: https://github.com/keiyoushi/extensions-source
url: https://github.com/keiyoushi/extensions
about: Issues and requests about the Mihon extension should be opened in the keiyoushi/extensions repository instead

View file

@ -44,9 +44,7 @@ body:
attributes:
label: Logs
description: |
:warning: **Do not share logs with Kobo Sync information publicly !**
If applicable, add an excerpt of the log file (max 20 lines) _AND_ attach the complete log file or a link to a gist/pastebin containing the log file ([where to find the logs](https://komga.org/docs/faq#where-can-i-find-the-log-files)).
If applicable, add an excerpt of the log file (max 20 lines) _AND_ attach the complete log file or a link to a gist/pastebin containing the log file ([where to find the logs](https://komga.org/faq/#where-can-i-find-the-log-files)).
placeholder: |
You can paste the logs in pure text or upload it as an attachment.

View file

@ -1,13 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="298" height="64" fill="none" viewBox="0 0 298 64">
<defs>
<linearGradient id="a" x1=".850001" x2="62.62" y1="62.72" y2="1.81" gradientUnits="userSpaceOnUse">
<stop stop-color="#FF9419"/>
<stop offset=".43" stop-color="#FF021D"/>
<stop offset=".99" stop-color="#E600FF"/>
</linearGradient>
</defs>
<path fill="#000" d="M86.4844 40.5858c0 .8464-.1792 1.5933-.5377 2.2505-.3585.6573-.8564 1.1651-1.5137 1.5236-.6572.3585-1.3941.5378-2.2406.5378H78v6.1044h5.0787c1.912 0 3.6248-.4282 5.1484-1.2846 1.5236-.8564 2.7186-2.0415 3.585-3.5452.8663-1.5037 1.3045-3.1966 1.3045-5.0886V21.0178h-6.6322v19.568Zm17.8556-1.8224h13.891v-5.6065H104.34v-6.3633h15.355v-5.7758H97.8766v29.9743h22.2464v-5.7757H104.34v-6.453Zm17.865-11.8005h8.882v24.0193h6.633V26.9629h8.842v-5.9451h-24.367v5.9551l.01-.01Zm47.022 9.0022c-.517-.2788-1.085-.4879-1.673-.6472.449-.1295.877-.2888 1.275-.488 1.096-.5676 1.962-1.3643 2.579-2.39.618-1.0257.936-2.2007.936-3.5351 0-1.5237-.418-2.8879-1.244-4.0929-.827-1.195-1.992-2.131-3.486-2.8082-1.494-.6672-3.206-1.0058-5.118-1.0058h-13.315v29.9743h13.574c2.011 0 3.804-.3485 5.387-1.0556 1.573-.707 2.798-1.6829 3.675-2.9476.866-1.2547 1.304-2.6887 1.304-4.302 0-1.4837-.338-2.8082-1.026-3.9833-.687-1.175-1.633-2.0812-2.858-2.7285l-.01.0099Zm-13.603-9.9184h5.886c.816 0 1.533.1494 2.161.4382.627.2888 1.115.707 1.464 1.2547.348.5378.527 1.1751.527 1.9021 0 .7269-.179 1.414-.527 1.9817-.349.5676-.837.9958-1.464 1.3045-.628.3087-1.345.4581-2.161.4581h-5.886v-7.3492.0099Zm10.138 18.134c-.378.5676-.916 1.0058-1.603 1.3145-.697.3087-1.484.4581-2.39.4581h-6.145v-7.6878h6.145c.886 0 1.673.1693 2.37.4979.687.3286 1.235.7867 1.613 1.3842.378.5975.578 1.2747.578 2.0414 0 .7668-.19 1.4241-.568 1.9917Zm29.596-5.3077c1.663-.7967 2.947-1.922 3.864-3.3659.916-1.444 1.374-3.117 1.374-5.0289 0-1.912-.448-3.5253-1.344-4.9592-.897-1.434-2.171-2.5394-3.814-3.3261-1.644-.7867-3.546-1.1751-5.717-1.1751h-13.124v29.9743h6.642V40.0779h4.322l6.084 10.9142h7.578l-6.851-11.7208c.339-.1195.677-.249.996-.3983h-.01Zm-2.151-6.1244c-.369.6274-.896 1.1154-1.583 1.444-.688.3386-1.494.5079-2.42.5079h-5.975v-8.2953h5.975c.926 0 1.732.1693 2.42.4979.687.3287 1.214.8166 1.583 1.434.368.6174.558 1.3544.558 2.1908 0 .8365-.19 1.5734-.558 2.2008v.0199Zm20.594-11.7308-10.706 29.9743h6.742l2.121-6.6122h11.114l2.27 6.6122h6.612L220.99 21.0178h-7.189Zm-.339 18.3431 3.445-10.5756.409-1.922.408 1.922 3.685 10.5756h-7.947Zm20.693 11.6312h6.851V21.0178h-6.851v29.9743Zm31.02-9.6993-12.896-20.275h-6.463v29.9743h6.055V30.7172l12.826 20.2749h6.533V21.0178h-6.055v20.275Zm31.528-3.3559c-.647-1.2448-1.564-2.2904-2.729-3.1369-1.165-.8464-2.509-1.4041-4.023-1.6929l-5.098-1.0456c-.797-.1892-1.434-.5178-1.902-.9958-.469-.478-.708-1.0755-.708-1.7825 0-.6473.17-1.205.518-1.683.339-.478.827-.8464 1.444-1.1153.618-.2689 1.335-.3983 2.151-.3983.817 0 1.554.1394 2.181.4182.627.2788 1.115.6672 1.464 1.1751s.528 1.0755.528 1.7228h6.642c-.04-1.7427-.528-3.2863-1.444-4.6207-.916-1.3443-2.201-2.3899-3.834-3.1468-1.633-.7568-3.505-1.1352-5.597-1.1352-2.091 0-3.943.3884-5.566 1.1751-1.623.7867-2.898 1.8721-3.804 3.2663-.906 1.3941-1.364 2.9775-1.364 4.76 0 1.444.288 2.7485.876 3.9036.587 1.1652 1.414 2.1311 2.479 2.8979 1.076.7668 2.311 1.3045 3.725 1.6033l5.397 1.1153c.886.2091 1.584.5975 2.101 1.1551.518.5577.767 1.2448.767 2.0813 0 .6672-.189 1.2747-.567 1.8025-.379.5277-.907.936-1.584 1.2248-.677.2888-1.474.4282-2.39.4282-.916 0-1.782-.1593-2.529-.478-.747-.3186-1.325-.7767-1.733-1.3742-.418-.5875-.617-1.2747-.617-2.0414h-6.642c.029 1.8721.527 3.5152 1.513 4.9492.976 1.424 2.32 2.5394 4.033 3.336 1.713.7967 3.675 1.195 5.886 1.195 2.21 0 4.202-.4083 5.915-1.2249 1.723-.8165 3.057-1.9418 4.023-3.3758.966-1.434 1.444-3.0572 1.444-4.8696 0-1.4838-.329-2.848-.976-4.1028l.02.01Z"/>
<path fill="url(#a)" d="M20.34 3.66 3.66 20.34C1.32 22.68 0 25.86 0 29.18V59c0 2.76 2.24 5 5 5h29.82c3.32 0 6.49-1.32 8.84-3.66l16.68-16.68c2.34-2.34 3.66-5.52 3.66-8.84V5c0-2.76-2.24-5-5-5H29.18c-3.32 0-6.49 1.32-8.84 3.66Z"/>
<path fill="#000" d="M48 16H8v40h40V16Z"/>
<path fill="#fff" d="M30 47H13v4h17v-4Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View file

@ -1,35 +0,0 @@
name: Update Browserslist database
on:
workflow_dispatch:
schedule:
- cron: '0 2 1 * *'
permissions:
contents: write
pull-requests: write
jobs:
update-browserslist-database:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Configure git
run: |
# Setup for commiting using built-in token. See https://github.com/actions/checkout#push-a-commit-using-the-built-in-token
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
- name: Update Browserslist database and create PR if applies
uses: c2corg/browserslist-update-action@v2
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
branch: browserslist-update
base_branch: master
directory: ./komga-webui
commit_message: 'build(webui): update Browserslist db'
title: 'Browserslist database update'
body: Auto-generated by [browserslist-update-action](https://github.com/c2corg/browserslist-update-action/)
labels: 'github_actions'

View file

@ -1,18 +0,0 @@
name: Dispatch events
on:
push:
branches: [ master ]
paths:
- '**/openapi.json'
jobs:
dispatch:
runs-on: ubuntu-latest
steps:
- name: Repository Dispatch
uses: peter-evans/repository-dispatch@v4
with:
token: ${{ secrets.REPO_ACCESS_TOKEN }}
repository: gotson/komga-website
event-type: openapi

View file

@ -15,7 +15,7 @@ jobs:
steps:
- uses: actions/checkout@master
- name: DockerHub Description
uses: peter-evans/dockerhub-description@v5.0.0
uses: peter-evans/dockerhub-description@v4.0.0
env:
DOCKERHUB_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKERHUB_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}

View file

@ -0,0 +1,14 @@
name: "Validate Gradle Wrapper"
on:
pull_request:
push:
branches-ignore:
- 'dependabot/**'
jobs:
validation:
name: "Validation"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: gradle/actions/wrapper-validation@v3

View file

@ -12,7 +12,7 @@ jobs:
lock:
runs-on: ubuntu-latest
steps:
- uses: dessant/lock-threads@v6
- uses: dessant/lock-threads@v5
with:
github-token: ${{ github.token }}
issue-inactive-days: '30'

View file

@ -43,24 +43,24 @@ jobs:
version_next: ${{ steps.versions.outputs.version_next }}
should_release: ${{ steps.versions.outputs.should_release }}
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Homebrew
id: set-up-homebrew
uses: Homebrew/actions/setup-homebrew@main
uses: Homebrew/actions/setup-homebrew@master
- name: Install svu
run: brew install --cask caarlos0/tap/svu
run: brew install caarlos0/tap/svu
- name: Compute next version for release
run: |
echo "VERSION_NEXT=`svu ${{ inputs.bump }}`" | tee -a $GITHUB_ENV
echo "VERSION_NEXT_SUFFIX=`svu ${{ inputs.bump }}`" | tee -a $GITHUB_ENV
echo "VERSION_NEXT=`svu --pattern="[0-9]*" --strip-prefix ${{ inputs.bump }}`" | tee -a $GITHUB_ENV
echo "VERSION_NEXT_SUFFIX=`svu --pattern="[0-9]*" --strip-prefix ${{ inputs.bump }}`" | tee -a $GITHUB_ENV
- name: Set Versions
id: versions
run: |
echo "version_current=`svu current`" >> $GITHUB_OUTPUT
echo "version_current=`svu --pattern="[0-9]*" --strip-prefix current`" >> $GITHUB_OUTPUT
echo "version_next=${{ env.VERSION_NEXT_SUFFIX }}" >> $GITHUB_OUTPUT
[[ `svu current` != ${{ env.VERSION_NEXT }} ]] && echo "should_release=true" >> $GITHUB_OUTPUT || echo
[[ `svu --pattern="[0-9]*" --strip-prefix current` != ${{ env.VERSION_NEXT }} ]] && echo "should_release=true" >> $GITHUB_OUTPUT || echo
release:
name: Release
@ -72,7 +72,7 @@ jobs:
sudo rm -rf /usr/share/dotnet
sudo rm -rf "$AGENT_TOOLSDIRECTORY"
- uses: actions/checkout@v6
- uses: actions/checkout@v4
with:
fetch-depth: 0
@ -84,40 +84,44 @@ jobs:
if: needs.version.outputs.should_release #only redo if the version changed
run: sed -i -e "s/version=.*/version=${{ needs.version.outputs.version_next }}/" gradle.properties
- uses: actions/setup-node@v6
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'npm'
cache-dependency-path: komga-webui/package-lock.json
- name: Setup Java 21
uses: actions/setup-java@v5
- uses: actions/setup-java@v4
with:
java-version: 21
java-package: 'jdk'
distribution: 'temurin'
- uses: actions/setup-java@v4
with:
java-version: 17
java-package: 'jdk'
distribution: 'temurin'
- name: Set up QEMU
uses: docker/setup-qemu-action@v4
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v4
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to Docker Hub
uses: docker/login-action@v4
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v6
- uses: gradle/actions/setup-gradle@v3
- name: Build
run: ./gradlew :komga:prepareThymeLeaf :komga:bootJar :komga-tray:jar
@ -143,7 +147,7 @@ jobs:
JRELEASER_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: JReleaser Changelog output
if: always() && needs.version.outputs.should_release
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@v4
with:
name: jreleaser-changelog
path: |
@ -151,7 +155,7 @@ jobs:
build/jreleaser/output.properties
- name: Release commit and push
uses: EndBug/add-and-commit@v10
uses: EndBug/add-and-commit@v9
if: needs.version.outputs.should_release #only redo if the version changed
with:
message: 'chore(release): ${{ needs.version.outputs.version_next }} [skip ci]'
@ -167,7 +171,7 @@ jobs:
echo $APPLE_PRIVATE_KEY | base64 --decode > ./secret/apple_private_key.p8
- name: Conveyor make copied-site
uses: hydraulic-software/conveyor/actions/build@v22.0
uses: hydraulic-software/conveyor/actions/build@v14.3
if: inputs.conveyor-copied-site
with:
command: --cache-limit=2.0 -f conveyor.ci.conf make copied-site -o ./output/site
@ -182,7 +186,7 @@ jobs:
AWS_SECRET_ACCESS_KEY: ${{ secrets.B2_SECRET_ACCESS_KEY }}
- name: Upload Conveyor log
if: always() && inputs.conveyor-copied-site
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@v4
with:
name: conveyor-make-copied-site
path: ~/.cache/hydraulic/conveyor/logs/log.latest.txt
@ -194,13 +198,27 @@ jobs:
JRELEASER_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: JReleaser Release output
if: always() && inputs.github_release
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@v4
with:
name: jreleaser-release
path: |
build/jreleaser/trace.log
build/jreleaser/output.properties
- name: JReleaser Announce
if: inputs.github_release
run: ./gradlew jreleaserAnnounce
env:
JRELEASER_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: JReleaser Announce output
if: always() && inputs.github_release
uses: actions/upload-artifact@v4
with:
name: jreleaser-announce
path: |
build/jreleaser/trace.log
build/jreleaser/output.properties
# Sometimes the workflow will fail because it's out of disk space
- name: Cleanup Conveyor output
run: rm -fr ./output
@ -212,7 +230,7 @@ jobs:
JRELEASER_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: JReleaser Publish output
if: always() && inputs.docker_release
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@v4
with:
name: jreleaser-publish
path: |
@ -220,7 +238,7 @@ jobs:
build/jreleaser/output.properties
- name: Conveyor - publish to Microsoft Store
uses: hydraulic-software/conveyor/actions/build@v22.0
uses: hydraulic-software/conveyor/actions/build@v14.3
if: inputs.msstore_release
with:
command: --cache-limit=2.0 -f conveyor.msstore.ci.conf make ms-store-release -o ./output/msstore
@ -236,18 +254,7 @@ jobs:
AWS_SECRET_ACCESS_KEY: ${{ secrets.B2_SECRET_ACCESS_KEY }}
- name: Upload Conveyor log
if: always() && inputs.msstore_release
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@v4
with:
name: conveyor-ms-store-release
path: ~/.cache/hydraulic/conveyor/logs/log.latest.txt
dispatch:
needs: release
runs-on: ubuntu-latest
steps:
- name: Repository Dispatch
uses: peter-evans/repository-dispatch@v4
with:
token: ${{ secrets.REPO_ACCESS_TOKEN }}
repository: gotson/komga-website
event-type: komga-release

View file

@ -12,51 +12,43 @@ on:
jobs:
test:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ ubuntu-latest, macos-latest, windows-latest ]
fail-fast: false
name: Test server - ${{ matrix.os }}
runs-on: ubuntu-latest
name: Test server
steps:
- uses: actions/checkout@v6
- name: Setup Java 21
uses: actions/setup-java@v5
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
java-version: 21
java-package: 'jdk'
distribution: 'temurin'
- uses: actions/setup-java@v4
with:
java-version: 17
java-package: 'jdk'
distribution: 'temurin'
- name: Setup Gradle
uses: gradle/actions/setup-gradle@v6
- uses: gradle/actions/setup-gradle@v3
- name: Build
run: ./gradlew build :komga-tray:jar
- name: Upload Unit Test Results
if: always()
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@v4
with:
name: test-results-${{ matrix.os }}
name: test-results
path: komga/build/test-results/
- name: Upload Unit Test Reports
if: always()
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@v4
with:
name: test-reports-${{ matrix.os }}
name: test-reports
path: komga/build/reports/tests/
- name: Publish Test Report
uses: mikepenz/action-junit-report@v6
if: always()
with:
report_paths: '**/build/test-results/test/TEST-*.xml'
check_name: 'JUnit Test Report: ${{ matrix.os }}'
- name: Conveyor - compute JDK module list
if: github.event_name == 'push' && github.repository_owner == 'gotson' && contains(matrix.os, 'ubuntu')
uses: hydraulic-software/conveyor/actions/build@v22.0
if: github.event_name == 'push'
uses: hydraulic-software/conveyor/actions/build@v14.3
with:
command: -f conveyor.detect.conf -Kapp.machines=mac.aarch64 make processed-jars
signing_key: ${{ secrets.CONVEYOR_SIGNING_KEY }}
@ -64,12 +56,12 @@ jobs:
- name: Compare JDK required modules
id: conveyor_compare
if: github.event_name == 'push' && github.repository_owner == 'gotson' && contains(matrix.os, 'ubuntu')
if: github.event_name == 'push'
run: diff --unified ./komga-tray/conveyor/required-jdk-modules.txt ./output/required-jdk-modules.txt
- name: Upload JDK required modules
if: steps.conveyor_compare.outcome == 'failure'
uses: actions/upload-artifact@v7
uses: actions/upload-artifact@v4
with:
name: conveyor-required-jdk-modules
path: ./output/required-jdk-modules.txt
@ -78,8 +70,8 @@ jobs:
runs-on: ubuntu-latest
name: Test webui builds
steps:
- uses: actions/checkout@v6
- uses: actions/setup-node@v6
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
cache: 'npm'

3
.gitignore vendored
View file

@ -2,9 +2,6 @@
.gradle
!gradle/wrapper/gradle-wrapper.jar
### Kotlin
.kotlin
### NodeJS
node_modules

View file

@ -1,3 +0,0 @@
tag:
prefix: ''
pattern: '[0-9]*'

File diff suppressed because it is too large Load diff

View file

@ -1,4 +1,4 @@
1. **Before reporting a new issue, take a look at the [FAQ](https://komga.org/docs/faq/), the [changelog](https://github.com/gotson/komga/blob/master/CHANGELOG.md) and the already opened [issues](https://github.com/gotson/komga/issues).**
1. **Before reporting a new issue, take a look at the [FAQ](https://komga.org/faq/), the [changelog](https://github.com/gotson/komga/blob/master/CHANGELOG.md) and the already opened [issues](https://github.com/gotson/komga/issues).**
1. If you are unsure, ask here: [![Discord](https://img.shields.io/discord/678794935368941569?label=Discord)](https://discord.gg/TdRpkDu)
1. **DO NOT** reply on existing issues to say _"+1"_ or _"I am interested in this"_.
1. **DO** show your enthusiasm for an existing issue by adding a :+1: reaction on the first message in the discussion.

View file

@ -6,7 +6,7 @@ Thanks a lot for contributing to Komga!
You will need:
- Java JDK version 21+
- Java JDK version 17 & 21
- Nodejs version 18+ (check the `.nvmrc` file)
## Setting up the project

View file

@ -37,9 +37,3 @@
| ERR_1031 | ComicRack CBL Book is missing series or number |
| ERR_1032 | EPUB file has wrong media type |
| ERR_1033 | Some entries are missing |
| ERR_1034 | An API key with that comment already exists |
| ERR_1035 | Error while getting EPUB TOC |
| ERR_1036 | Error while getting EPUB Landmarks |
| ERR_1037 | Error while getting EPUB page list |
| ERR_1038 | Error while getting EPUB divina pages |
| ERR_1039 | Error while getting EPUB positions |

View file

@ -22,9 +22,6 @@ Komga is a media server for your comics, mangas, BDs, magazines and eBooks.
- Webreader with multiple reading modes
- Manage multiple users, with per-library access control, age restrictions, and labels restrictions
- Offers a REST API, many community tools and scripts can interact with Komga
- OPDS v1 and v2 support
- Kobo Sync with your Kobo eReader
- KOReader Sync
- Download book files, whole series, or read lists
- Duplicate files detection
- Duplicate pages detection and removal
@ -47,15 +44,9 @@ Check the [development guidelines](./DEVELOPING.md).
[![Translation status](https://hosted.weblate.org/widgets/komga/-/webui/horizontal-auto.svg)](https://hosted.weblate.org/engage/komga/)
## Powered by
## Sponsors
[![Jetbrains_logo](./.github/readme-images/jetbrains.svg)](https://www.jetbrains.com/?from=Komga)
Thanks to [JetBrains](https://www.jetbrains.com/?from=Komga) for providing the development environment that helps us develop Komga.
[![Chromatic logo](https://user-images.githubusercontent.com/321738/84662277-e3db4f80-af1b-11ea-88f5-91d67a5e59f6.png)](https://www.chromatic.com)
Thanks to [Chromatic](https://www.chromatic.com/) for providing the visual testing platform that helps us review UI changes and catch visual regressions.
[![Jetbrains_logo](./.github/readme-images/sponsors-jetbrains.png)](https://www.jetbrains.com/?from=Komga)
## Credits

View file

@ -7,14 +7,14 @@ import kotlin.io.path.exists
plugins {
run {
val kotlinVersion = "2.2.0"
val kotlinVersion = "1.9.21"
kotlin("jvm") version kotlinVersion
kotlin("plugin.spring") version kotlinVersion
kotlin("kapt") version kotlinVersion
}
id("org.jlleitschuh.gradle.ktlint") version "13.0.0"
id("com.github.ben-manes.versions") version "0.52.0"
id("org.jreleaser") version "1.19.0"
id("org.jlleitschuh.gradle.ktlint") version "12.1.0"
id("com.github.ben-manes.versions") version "0.50.0"
id("org.jreleaser") version "1.10.0"
}
fun isNonStable(version: String): Boolean {
@ -44,16 +44,12 @@ allprojects {
}
configure<org.jlleitschuh.gradle.ktlint.KtlintExtension> {
version = "1.7.1"
filter {
exclude("**/generated-src/**")
exclude("**/generated/**")
}
version = "1.1.1"
}
}
tasks.wrapper {
gradleVersion = "8.14.3"
gradleVersion = "8.5"
distributionType = Wrapper.DistributionType.ALL
}
@ -164,7 +160,7 @@ jreleaser {
packagers {
docker {
active = Active.RELEASE
continueOnError = false
continueOnError = true
templateDirectory = rootDir.resolve("komga/docker")
repository.active = Active.NEVER
buildArgs = listOf("--cache-from", "gotson/komga:latest")

View file

@ -1,5 +1,4 @@
include "#!./gradlew -q :komga-tray:printConveyorConfig"
include required("/stdlib/jdk/23/eclipse.conf")
app {
display-name = Komga
@ -10,14 +9,9 @@ app {
license = MIT
icons = "res/komga_text_as_path.svg"
machines = [
windows.amd64,
mac
]
jvm {
// for NightMonkeys & NightCompress
options += "--enable-native-access=ALL-UNNAMED"
// for NightMonkeys
options += "--enable-preview"
mac.options += "-Dspring.profiles.include=mac"
@ -51,16 +45,12 @@ app {
exe-installer-basename = "KomgaInstaller"
manifests.msix.background-color = transparent
inputs += ./komga-tray/lib/windows/x64/
amd64.inputs += "https://github.com/pgaskin/kepubify/releases/latest/download/kepubify-windows-64bit.exe" -> kepubify.exe
aarch64.inputs += "https://github.com/pgaskin/kepubify/releases/latest/download/kepubify-windows-arm64.exe" -> kepubify.exe
}
mac {
info-plist.LSMinimumSystemVersion = 13
info-plist.LSMinimumSystemVersion = 12
aarch64.inputs += ./komga-tray/lib/mac/aarch64/
aarch64.inputs += "https://github.com/pgaskin/kepubify/releases/latest/download/kepubify-darwin-arm64" -> kepubify
amd64.inputs += ./komga-tray/lib/mac/x64/
amd64.inputs += "https://github.com/pgaskin/kepubify/releases/latest/download/kepubify-darwin-64bit" -> kepubify
}
site {
@ -68,4 +58,4 @@ app {
}
}
conveyor.compatibility-level = 18
conveyor.compatibility-level = 13

View file

@ -1,2 +1,2 @@
version=1.24.4
version=1.11.2
org.gradle.jvmargs=-Xmx2G

View file

@ -1,10 +0,0 @@
[versions]
sqliteJdbc = "3.50.2.0"
nightmonkeys = "1.0.0"
twelvemonkeys = "3.12.0"
springboot = "3.5.14"
lucene = "9.9.1" # v10 requires JDK 21
jooq = "3.19.32" # should be aligned with the version provided by Spring Boot
[plugins]
gradleGitProperties = {id = "com.gorylenko.gradle-git-properties", version = "2.5.7"}

Binary file not shown.

View file

@ -1,6 +1,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-all.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME

12
gradlew vendored
View file

@ -15,8 +15,6 @@
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
@ -57,7 +55,7 @@
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
@ -86,7 +84,7 @@ done
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
@ -114,7 +112,7 @@ case "$( uname )" in #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH="\\\"\\\""
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
@ -205,7 +203,7 @@ fi
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
@ -213,7 +211,7 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.

26
gradlew.bat vendored
View file

@ -13,8 +13,6 @@
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@ -45,11 +43,11 @@ set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
@ -59,22 +57,22 @@ set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell

View file

@ -1,28 +1,20 @@
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
run {
kotlin("jvm")
kotlin("plugin.spring")
}
alias(libs.plugins.gradleGitProperties)
id("org.jetbrains.compose") version "1.8.2"
id("org.jetbrains.kotlin.plugin.compose") version "2.2.0"
id("dev.hydraulic.conveyor") version "1.12"
id("com.gorylenko.gradle-git-properties") version "2.4.1"
id("org.jetbrains.compose") version "1.5.11"
id("dev.hydraulic.conveyor") version "1.8"
application
}
group = "org.gotson"
repositories {
mavenCentral()
google()
}
kotlin {
compilerOptions {
jvmTarget = JvmTarget.JVM_17
}
jvmToolchain(21)
}
tasks {
@ -30,13 +22,17 @@ tasks {
sourceCompatibility = "17"
targetCompatibility = "17"
}
withType<KotlinCompile> {
kotlinOptions {
jvmTarget = "17"
}
}
}
dependencies {
implementation(project(":komga"))
implementation(compose.desktop.currentOs)
implementation(compose.components.resources)
linuxAmd64(compose.desktop.linux_x64)
macAmd64(compose.desktop.macos_x64)

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -16,6 +16,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.loadSvgPainter
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpSize
@ -26,7 +27,6 @@ import androidx.compose.ui.window.WindowPosition
import androidx.compose.ui.window.WindowState
import androidx.compose.ui.window.application
import org.gotson.komga.RB
import org.jetbrains.compose.resources.decodeToSvgPainter
import org.springframework.core.io.ClassPathResource
@Preview
@ -50,7 +50,7 @@ fun showErrorDialog(
Dp.Unspecified,
),
),
icon = ClassPathResource("icons/komga-color.svg").inputStream.readAllBytes().decodeToSvgPainter(LocalDensity.current),
icon = loadSvgPainter(ClassPathResource("icons/komga-color.svg").inputStream, LocalDensity.current),
) {
Column(
modifier = Modifier.padding(16.dp),
@ -60,7 +60,7 @@ fun showErrorDialog(
modifier = Modifier.padding(bottom = 16.dp),
) {
Image(
painter = ClassPathResource("icons/komga-color.svg").inputStream.readAllBytes().decodeToSvgPainter(LocalDensity.current),
painter = loadSvgPainter(ClassPathResource("icons/komga-color.svg").inputStream, LocalDensity.current),
contentDescription = "Komga logo",
modifier =
Modifier

View file

@ -1,13 +1,13 @@
package org.gotson.komga.application.gui
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.loadSvgPainter
import androidx.compose.ui.window.Tray
import androidx.compose.ui.window.application
import org.gotson.komga.RB
import org.gotson.komga.infrastructure.web.WebServerEffectiveSettings
import org.gotson.komga.openExplorer
import org.gotson.komga.openUrl
import org.jetbrains.compose.resources.decodeToSvgPainter
import org.springframework.beans.factory.annotation.Value
import org.springframework.boot.ApplicationArguments
import org.springframework.boot.ApplicationRunner
@ -21,8 +21,8 @@ import java.io.File
@Profile("!test")
@Component
class TrayIconRunner(
@param:Value("#{komgaProperties.configDir}") komgaConfigDir: String,
@param:Value($$"${logging.file.name}") logFileName: String,
@Value("#{komgaProperties.configDir}") komgaConfigDir: String,
@Value("\${logging.file.name}") logFileName: String,
serverSettings: WebServerEffectiveSettings,
env: Environment,
) : ApplicationRunner {
@ -39,7 +39,7 @@ class TrayIconRunner(
private fun runTray() {
application {
Tray(
icon = ClassPathResource("icons/$iconFileName").inputStream.readAllBytes().decodeToSvgPainter(LocalDensity.current),
icon = loadSvgPainter(ClassPathResource("icons/$iconFileName").inputStream, LocalDensity.current),
menu = {
Item(RB.getString("menu.open_komga"), onClick = { openUrl(komgaUrl) })
Item(RB.getString("menu.show_log"), onClick = { openExplorer(logFile) })

View file

@ -3,10 +3,3 @@ logging:
name: ${user.home}/Library/Logs/Komga/komga.log
komga:
config-dir: ${user.home}/Library/Application Support/Komga
kobo.kepubify-path: kepubify
spring:
config:
import:
- "optional:file:${komga.config-dir}/application.yml"
- "optional:file:${komga.config-dir}/application.yaml"
- "optional:file:${komga.config-dir}/application.properties"

View file

@ -1,9 +1,2 @@
komga:
config-dir: ${LOCALAPPDATA}/Komga
kobo.kepubify-path: kepubify.exe
spring:
config:
import:
- "optional:file:${komga.config-dir}/application.yml"
- "optional:file:${komga.config-dir}/application.yaml"
- "optional:file:${komga.config-dir}/application.properties"

View file

@ -1,9 +0,0 @@
dialog_error.close=إغلاق
dialog_error.copy_clipboard=النسخ إلى الحافظة
dialog_error.title=Komga لم يقدر على البدء
error_message.port_in_use=الباب {} على قيد الاستعمال.\nربما Komga جارٍ بالفعل.\nتحقق أيقونة علبة النظام أو القائمة لأيقونة Komga.
error_message.unexpected=حصل خطأ غير متوقع.
menu.open_komga=فتح Komga
menu.quit=إغلاق Komga
menu.show_conf_dir=فتح موقع الإعدادات
menu.show_log=إظهار ملف السجل

View file

@ -1,9 +0,0 @@
dialog_error.close=বন্ধ কৰক
dialog_error.copy_clipboard=ক্লিপব’ৰ্ডলৈ কপি কৰক
dialog_error.title=কমগা আৰম্ভ কৰাত বিফল
error_message.port_in_use={} প’ৰ্ট ইতিমধ্যে ব্যৱহৃত।\nকমগা সম্ভৱতঃ ইতিমধ্যে চলি আছে।\nকমগা আইকনৰ বাবে ট্ৰে আইকন বা মেনু বাৰ পৰীক্ষা কৰক।
error_message.unexpected=এক অপ্ৰত্যাশিত ত্ৰুটি ঘটিল।
menu.open_komga=কমগা খোলক
menu.quit=কমগা বন্ধ কৰক
menu.show_conf_dir=বিন্যাস ডাইৰেক্টৰি খোলক
menu.show_log=লগ ফাইল দেখুৱাওক

View file

@ -1,9 +0,0 @@
dialog_error.close=Затвори
dialog_error.copy_clipboard=Копирай в клипборда
dialog_error.title=Komga не успя да стартира
error_message.port_in_use=Порт {} вече се използва.\nKomga сигурно вече работи.\nПровери tray иконите или menu лентата за иконата на Komga.
error_message.unexpected=Възникна неочаквана грешка.
menu.open_komga=Отвори Komga
menu.quit=Изключи Komga
menu.show_conf_dir=Отвори конфигурационната директория
menu.show_log=Отвори файла с логовете

View file

@ -1,9 +1,9 @@
dialog_error.close=Zavřít
dialog_error.copy_clipboard=Zkopírovat do schránky
dialog_error.title=Aplikace Komga se nepodařila spustit
error_message.port_in_use=Port {} je aktuálně používán.\nAplikace Komga je pravděpodobně již spuštěna.\nZkontrolujte, zda ikona Komga není již v systémové liště nebo mezi schovanými ikonami.
error_message.unexpected=Nastala neočekávaná chyba.
menu.open_komga=Otevřít aplikaci Komga
menu.quit=Ukončit aplikaci Komga
dialog_error.title=Komga se nepodařila spustit
error_message.port_in_use=Port {} je aktuálně používán.\nKomga je pravděpodobně již zapnutá.\nZkontrolujte, zda ikona Komga není již v systémové liště nebo mezi schovanými ikony.
error_message.unexpected=Nastal neočekávaný error.
menu.open_komga=Otevřít Komga
menu.quit=Zavřít Komga
menu.show_conf_dir=Otevřít konfigurační složku
menu.show_log=Zobrazit logy

View file

@ -1,9 +0,0 @@
dialog_error.close=Luk
dialog_error.copy_clipboard=Kopier til udklipsholder
dialog_error.title=Komga kunne ikke starte
error_message.port_in_use=Port {} er allerede i brug.\nKomga kører sandsynligvis allerede.\nTjek systembakken eller menulinjen for Komga-ikonet.
error_message.unexpected=Der opstod en uventet fejl.
menu.open_komga=Åbn Komga
menu.quit=Afslut Komga
menu.show_conf_dir=Åbn konfigurationsmappen
menu.show_log=Vis logfil

View file

@ -1,9 +0,0 @@
dialog_error.close=Pechar
dialog_error.copy_clipboard=Copiar ao portapapeis
dialog_error.title=Komga fallou ao iniciar
error_message.port_in_use=O porto {} está en uso.\nÉ probable que Komga xa esté a executarse.\nBusca a icona de Komga na bandexa de sistema ou a barra de menú.
error_message.unexpected=Aconteceu un erro imprevisto.
menu.open_komga=Abrir Komga
menu.quit=Saír de Komga
menu.show_conf_dir=Abrir o cartafol de configuración
menu.show_log=Amosar o ficheiro de rexistro

View file

@ -1,9 +0,0 @@
dialog_error.close=Zatvori
dialog_error.copy_clipboard=Kopiraj u međuspremnik
dialog_error.title=Komga se nije mogao pokrenuti
error_message.port_in_use=Priključak {} se već koristi.\nKomga vjerojatno već radi.\nPotraži Komga ikonu u traci ikona ili traci izbornika.
error_message.unexpected=Dogodila se neočekivana greška.
menu.open_komga=Otvori Komga
menu.quit=Zatvori Komga
menu.show_conf_dir=Otvori mapu konfiguracije
menu.show_log=Prikaži log-datoteku

View file

@ -1,9 +0,0 @@
dialog_error.close=Bezárás
dialog_error.copy_clipboard=Másolás a vágólapra
dialog_error.title=A Komga elindulása meghiúsult
error_message.port_in_use=A Port {} már használatban van.\nValószínűleg már fut a Komga.\nKeresd a Komga ikont a tálcán, vagy a menüsávon.
error_message.unexpected=Váratlan hiba történt.
menu.open_komga=Komga megnyitása
menu.quit=Komga bezárása
menu.show_conf_dir=Konfigurációs könyvtár megnyitása
menu.show_log=Eseménynapló fájl megjelenítése

View file

@ -1,9 +0,0 @@
dialog_error.close=Tutup
dialog_error.copy_clipboard=Salin ke papan klip
dialog_error.title=Komga gagal dimuat
error_message.port_in_use=Port {} telah digunakan.\nKomga telah berjalan.\nCari ikon Komga di baki ikon atau menu bar.
error_message.unexpected=Terjadi galat tak terduga.
menu.open_komga=Buka Komga
menu.quit=Tutup komga
menu.show_conf_dir=Buka direktori pengaturan
menu.show_log=Buka berkas catatan

View file

@ -1,9 +0,0 @@
dialog_error.close=Zamknij
dialog_error.copy_clipboard=Skopiuj do schowka
dialog_error.title=Nie udało się uruchomić Komgi
error_message.port_in_use=Port {} jest już używany.\nNajprawdopodobniej Komga jest już uruchomiona.\nSprawdź czy w zasobniku jest ikona Komgi.
error_message.unexpected=Napotkano nieoczekiwany błąd.
menu.open_komga=Otwórz Komgę
menu.quit=Wyjdź z Komgi
menu.show_conf_dir=Otwórz katalog z konfiguracją
menu.show_log=Pokaż dziennik

View file

@ -1,9 +0,0 @@
dialog_error.close=Fechar
dialog_error.copy_clipboard=Copiar para área de transferência
dialog_error.title=Falha no arranque do Konga
error_message.port_in_use=A porta {} já está em utilização.\nO Komga provavelmente já está em execução.\nVerifique o ícone do Komga na barra de tarefas/barra de menus.
error_message.unexpected=Ocorreu um erro inesperado.
menu.open_komga=Abrir Komga
menu.quit=Fechar Komga
menu.show_conf_dir=Abrir pasta de configuração
menu.show_log=Mostrar ficheiro de historial

View file

@ -1,9 +0,0 @@
dialog_error.close=Fechar
dialog_error.copy_clipboard=Copiar para área de transferência
dialog_error.title=Komga falhou ao inicializar
error_message.port_in_use=A porta {} já está em uso.\nProvavelmente o Komga já está rodando.\nVerifique o ícone na barra de tarefas ou a barra de menu.
error_message.unexpected=Ocorreu um erro inesperado.
menu.open_komga=Abrir Komga
menu.quit=Sair do Komga
menu.show_conf_dir=Abrir configuração de diretório
menu.show_log=Mostrar arquivo de logs

View file

@ -1,7 +1,7 @@
dialog_error.close=Закрыть
dialog_error.copy_clipboard=Скопировать в буфер обмена
dialog_error.title=Komga не удалось запустить
error_message.port_in_use=Порт {} уже используется.\nВероятно, Komga уже запущен.\nПроверьте область уведомлений или панель меню на наличие иконки Komga.
dialog_error.title=Komga не смог запуститься
error_message.port_in_use=Порт {] уже используется.\nKomga возможно уже работает.\nПроверьте панель задач или меню на наличие иконки Komga.
error_message.unexpected=Произошла непредвиденная ошибка.
menu.open_komga=Открыть Komga
menu.quit=Закрыть Komga

View file

@ -1,9 +0,0 @@
dialog_error.close=Zavrieť
dialog_error.copy_clipboard=Skopírovať do schránky
dialog_error.title=Aplikáciu Komga sa nepodarilo spustiť
error_message.port_in_use=Port {} sa aktuálne používa.\nAplikácia Komga je pravdepodobne už spustená.\nSkontrolujte, či ikona Komga už nie je v systémovej lište alebo medzi schovanými ikonami.
error_message.unexpected=Nastala neočakávaná chyba.
menu.open_komga=Otvoriť aplikáciu Komga
menu.quit=Zavrieť aplikáciu Komga
menu.show_conf_dir=Otvoriť konfiguračný priečinok
menu.show_log=Zobraziť logy

View file

@ -1,9 +0,0 @@
dialog_error.close=மூடு
dialog_error.copy_clipboard=இடைநிலைப்பலகைக்கு நகலெடுக்கவும்
dialog_error.title=கோம்கா தொடங்கத் தவறிவிட்டார்
error_message.port_in_use=துறைமுகம் {} ஏற்கனவே பயன்பாட்டில் உள்ளது.\n கோம்கா ஏற்கனவே இயங்குகிறது.\n கோம்கா ஐகானுக்கான தட்டு படவுரு அல்லது பட்டியல் பட்டியை சரிபார்க்கவும்.
error_message.unexpected=எதிர்பாராத பிழை ஏற்பட்டது.
menu.open_komga=திறந்த கொம்கா
menu.quit=கொங்காவை விட்டு வெளியேறவும்
menu.show_conf_dir=திறந்த உள்ளமைவு அடைவு
menu.show_log=பதிவு கோப்பைக் காட்டு

View file

@ -1,9 +0,0 @@
dialog_error.close=ปิด
dialog_error.copy_clipboard=คัดลอกไปยังคลิปบอร์ด
dialog_error.title=Komga ไม่สามารถเริ่มต้นได้
error_message.port_in_use=พอร์ต {} ถูกใช้งานอยู่แล้ว\nKomga อาจกำลังทำงานอยู่แล้ว\nตรวจสอบ ไอคอน Komga บนถาดไอคอนหรือแถบเมนู
error_message.unexpected=เกิดข้อผิดพลาดที่ไม่คาดคิด
menu.open_komga=เปิด Komga
menu.quit=ออก Komga
menu.show_conf_dir=เปิดไดเรกทอรีการกำหนดค่า
menu.show_log=แสดงไฟล์บันทึก

View file

@ -1,9 +0,0 @@
dialog_error.close=Закрити
dialog_error.copy_clipboard=Копіювати в буфер обміну
dialog_error.title=Помилка при запуску Komga
error_message.port_in_use=Порт {} вже використовується.\nKomga напевно вже запущена.\nПеревірте панель запущених програм на наявність іконки Komga.
error_message.unexpected=Сталася невідома помилка.
menu.open_komga=Відкрити Komga
menu.quit=Закрити Komga
menu.show_conf_dir=Відкрити теку з налаштуваннями
menu.show_log=Відкрити log журнал

File diff suppressed because it is too large Load diff

View file

@ -10,17 +10,16 @@
},
"dependencies": {
"@d-i-t-a/reader": "github:gotson/R2D2BC#fork",
"@w0s/isbn-verify": "^3.1.2",
"axios": "^1.15.0",
"@saekitominaga/isbn-verify": "^2.0.1",
"axios": "^1.6.0",
"chart.js": "^2.9.4",
"core-js": "^3.8.3",
"date-fns": "^2.30.0",
"filesize": "^10.0.12",
"js-file-downloader": "^1.1.25",
"language-tags": "^1.0.9",
"lodash": "^4.18.1",
"marked": "^15.0.4",
"qs": "^6.14.2",
"lodash": "^4.17.21",
"qs": "^6.11.2",
"screenfull": "^5.2.0",
"vue": "^2.6.14",
"vue-chartkick": "^0.6.1",

View file

@ -1,182 +0,0 @@
<template>
<div style="position: relative">
<div v-if="apiKeys">
<div v-if="apiKeys.length > 0">
<v-list elevation="3"
three-line
>
<div v-for="(apiKey, index) in apiKeys" :key="apiKey.id">
<v-list-item>
<v-list-item-content>
<v-list-item-title>{{ apiKey.comment }}</v-list-item-title>
<v-list-item-subtitle>
{{
$t('account_settings.api_key.created_date', {
date:
new Intl.DateTimeFormat($i18n.locale, {
dateStyle: 'medium',
timeStyle: 'short'
}).format(apiKey.createdDate)
})
}}
</v-list-item-subtitle>
<v-list-item-subtitle v-if="apiKeyLastActivity[apiKey.id] !== undefined">
{{
$t('settings_user.latest_activity', {
date:
new Intl.DateTimeFormat($i18n.locale, {
dateStyle: 'medium',
timeStyle: 'short'
}).format(apiKeyLastActivity[apiKey.id])
})
}}
</v-list-item-subtitle>
<v-list-item-subtitle v-else>{{ $t('settings_user.no_recent_activity') }}</v-list-item-subtitle>
</v-list-item-content>
<v-list-item-action>
<v-tooltip bottom>
<template v-slot:activator="{ on }">
<v-btn icon @click="promptSyncPointDelete(apiKey)" v-on="on">
<v-icon>mdi-book-refresh</v-icon>
</v-btn>
</template>
<span>{{ $t('account_settings.api_key.force_kobo_sync') }}</span>
</v-tooltip>
</v-list-item-action>
<v-list-item-action>
<v-btn icon @click="promptDeleteApiKey(apiKey)">
<v-icon>mdi-delete</v-icon>
</v-btn>
</v-list-item-action>
</v-list-item>
<v-divider v-if="index !== apiKeys.length-1"/>
</div>
</v-list>
<v-btn fab absolute bottom color="primary"
:right="!$vuetify.rtl"
:left="$vuetify.rtl"
class="mx-6"
small
@click="generateApiKey"
>
<v-icon>mdi-plus</v-icon>
</v-btn>
</div>
<div v-else>
<v-container fluid class="pa-0">
<v-row>
<v-col>{{ $t('account_settings.api_key.no_keys') }}</v-col>
</v-row>
<v-row>
<v-col>
<v-btn color="primary" @click="generateApiKey">{{
$t('account_settings.api_key.generate_api_key')
}}
</v-btn>
</v-col>
</v-row>
</v-container>
</div>
</div>
<confirmation-dialog
v-model="modalDeleteSyncPoints"
:title="$t('dialog.force_kobo_sync.dialog_title')"
:body-html="$t('dialog.force_kobo_sync.warning_html')"
:button-confirm="$t('common.i_understand')"
button-confirm-color="warning"
@confirm="deleteSyncPoint"
/>
<confirmation-dialog
v-model="modalDeleteApiKey"
:title="$t('dialog.delete_apikey.dialog_title')"
:body-html="$t('dialog.delete_apikey.warning_html')"
:confirm-text=" $t('dialog.delete_apikey.confirm_delete', {name: apiKeyToDelete.comment})"
:button-confirm="$t('dialog.delete_apikey.button_confirm')"
button-confirm-color="error"
@confirm="deleteApiKey"
/>
<api-key-add-dialog
v-model="modalGenerateApiKey"
@generate="loadApiKeys"
/>
</div>
</template>
<script lang="ts">
import Vue from 'vue'
import {ApiKeyDto} from '@/types/komga-users'
import {ERROR} from '@/types/events'
import ConfirmationDialog from '@/components/dialogs/ConfirmationDialog.vue'
import ApiKeyAddDialog from '@/components/dialogs/ApiKeyAddDialog.vue'
export default Vue.extend({
name: 'ApiKeyTable',
components: {ApiKeyAddDialog, ConfirmationDialog},
data: () => {
return {
apiKeys: undefined as ApiKeyDto[],
apiKeyToDelete: {} as ApiKeyDto,
apiKeySyncPointsToDelete: {} as ApiKeyDto,
modalDeleteApiKey: false,
modalDeleteSyncPoints: false,
modalGenerateApiKey: false,
apiKeyLastActivity: {} as any,
}
},
mounted() {
this.loadApiKeys()
},
methods: {
async loadApiKeys() {
try {
this.apiKeys = await this.$komgaUsers.getApiKeys()
this.apiKeys.forEach((a: ApiKeyDto) => {
this.$komgaUsers.getLatestAuthenticationActivityForUser(a.userId, a.id)
.then(value => this.$set(this.apiKeyLastActivity, `${a.id}`, value.dateTime))
.catch(e => {
})
})
} catch (e) {
this.$eventHub.$emit(ERROR, {message: e.message} as ErrorEvent)
}
},
promptDeleteApiKey(apiKey: ApiKeyDto) {
this.apiKeyToDelete = apiKey
this.modalDeleteApiKey = true
},
promptSyncPointDelete(apiKey: ApiKeyDto) {
this.apiKeySyncPointsToDelete = apiKey
this.modalDeleteSyncPoints = true
},
async deleteSyncPoint() {
try {
await this.$komgaSyncPoints.deleteMySyncPointsByApiKey(this.apiKeySyncPointsToDelete.id)
} catch (e) {
this.$eventHub.$emit(ERROR, {message: e.message} as ErrorEvent)
}
},
async deleteApiKey() {
try {
await this.$komgaUsers.deleteApiKey(this.apiKeyToDelete.id)
await this.loadApiKeys()
} catch (e) {
this.$eventHub.$emit(ERROR, {message: e.message} as ErrorEvent)
}
},
generateApiKey() {
this.modalGenerateApiKey = true
},
},
})
</script>

View file

@ -33,7 +33,7 @@
<script lang="ts">
import Vue from 'vue'
import {ERROR} from '@/types/events'
import {AuthenticationActivityDto} from '@/types/komga-users'
import { AuthenticationActivityDto } from '@/types/komga-users'
export default Vue.extend({
name: 'AuthenticationActivityTable',
@ -68,7 +68,6 @@ export default Vue.extend({
{text: this.$t('authentication_activity.user_agent').toString(), value: 'userAgent'},
{text: this.$t('authentication_activity.success').toString(), value: 'success'},
{text: this.$t('authentication_activity.source').toString(), value: 'source'},
{text: this.$t('authentication_activity.api_key').toString(), value: 'apiKeyComment'},
{text: this.$t('authentication_activity.error').toString(), value: 'error'},
{text: this.$t('authentication_activity.datetime').toString(), value: 'dateTime', groupable: false},
)

View file

@ -10,7 +10,6 @@
@scroll-changed="(percent) => scrollChanged(collectionsLoaders[index], percent)"
>
<template v-slot:prepend>
<slot name="prepend" v-bind:collection="c"/>
<router-link class="text-overline"
:to="{name: 'browse-collection', params: {collectionId: c.id}}"
>{{ $t('collections_expansion_panel.manage_collection') }}

View file

@ -31,19 +31,17 @@
<template v-else>
<div style="height: 2em" class="missing"></div>
</template>
<series-picker-dialog v-model="modalSeriesPicker" @update:series="pickedSeries" :include-oneshots="true"/>
<series-picker-dialog v-model="modalSeriesPicker" @update:series="pickedSeries" :include-oneshots="false"/>
</td>
<!-- Book number chooser -->
<td>
<v-text-field v-if="!selectedSeries?.oneshot"
v-model.number="bookNumber"
<v-text-field v-model.number="bookNumber"
type="number"
step="0.1"
dense
:disabled="!selectedSeries"
/>
<span v-else>{{ $t('common.oneshot') }}</span>
</td>
<!-- Book details -->
@ -131,8 +129,7 @@ import TransientBookViewerDialog from '@/components/dialogs/TransientBookViewerD
import {bookPageUrl, transientBookPageUrl} from '@/functions/urls'
import {convertErrorCodes} from '@/functions/error-codes'
import FileNameChooserDialog from '@/components/dialogs/FileNameChooserDialog.vue'
import {SeriesSelected} from '@/types/series-slim'
import {BookSearch, SearchConditionSeriesId, SearchOperatorIs} from '@/types/komga-search'
import {ReadListRequestBookMatchSeriesDto} from '@/types/komga-readlists'
export default Vue.extend({
name: 'FileImportRow',
@ -170,7 +167,6 @@ export default Vue.extend({
if (val) this.selectedSeries = {
seriesId: val.id,
title: val.metadata.title,
oneshot: val.oneshot,
}
},
immediate: true,
@ -193,7 +189,7 @@ export default Vue.extend({
convertErrorCodes,
innerSelect: false,
bookAnalyzed: undefined as unknown as TransientBookDto,
selectedSeries: undefined as SeriesSelected | undefined,
selectedSeries: undefined as ReadListRequestBookMatchSeriesDto | undefined,
seriesBooks: [] as BookDto[],
bookToUpgrade: undefined as BookDto | undefined,
bookToUpgradePages: [] as PageDto[],
@ -257,18 +253,12 @@ export default Vue.extend({
this.selectedSeries = {
seriesId: seriesDto.id,
title: seriesDto.metadata.title,
oneshot: seriesDto.oneshot,
}
}
},
async getSeriesBooks(series: SeriesSelected) {
async getSeriesBooks(series: ReadListRequestBookMatchSeriesDto) {
if (series) {
this.seriesBooks = (await this.$komgaBooks.getBooksList({
condition: new SearchConditionSeriesId(new SearchOperatorIs(series.seriesId)),
} as BookSearch, {unpaged: true, sort: 'metadata.numberSort'})).content
if (series.oneshot) {
this.bookNumber = this.seriesBooks[0].metadata.numberSort
}
this.seriesBooks = (await this.$komgaSeries.getBooks(series.seriesId, {unpaged: true})).content
this.checkForUpgrade(this.bookNumber)
}
},
@ -281,7 +271,6 @@ export default Vue.extend({
this.selectedSeries = {
seriesId: series.id,
title: series.metadata.title,
oneshot: series.oneshot,
}
},
},

View file

@ -5,14 +5,14 @@
>
<v-subheader v-if="f.name">{{ f.name }}</v-subheader>
<v-list-item v-for="v in f.values"
:key="JSON.stringify(v.value)"
:key="v.value"
@click.stop="click(key, v.value, v.nValue)"
>
<v-list-item-icon>
<v-icon v-if="key in filtersActive && includes(filtersActive[key], v.nValue)" color="secondary">
<v-icon v-if="key in filtersActive && filtersActive[key].includes(v.nValue)" color="secondary">
mdi-minus-box
</v-icon>
<v-icon v-else-if="key in filtersActive && includes(filtersActive[key], v.value)" color="secondary">
<v-icon v-else-if="key in filtersActive && filtersActive[key].includes(v.value)" color="secondary">
mdi-checkbox-marked
</v-icon>
<v-icon v-else>
@ -27,7 +27,6 @@
<script lang="ts">
import Vue from 'vue'
import {FiltersActive, FiltersOptions} from '@/types/filter'
export default Vue.extend({
name: 'FilterList',
@ -42,24 +41,16 @@ export default Vue.extend({
},
},
methods: {
includes(array: any[], value: any): boolean {
return this.$_.isObject(value) ? this.$_.some(array, value) : this.$_.includes(array, value)
},
click(key: string, value: any, nValue?: any) {
click(key: string, value: string, nValue?: string) {
let r = this.$_.cloneDeep(this.filtersActive)
if (!(key in r)) r[key] = []
const pull = this.$_.isObject(value) ? this.$_.remove : this.$_.pull
const includes = this.$_.isObject(value) ? this.$_.some : this.$_.includes
if (nValue && includes(r[key], nValue))
pull(r[key], nValue)
else if (includes(r[key], value)) {
pull(r[key], value)
if (nValue && r[key].includes(nValue))
this.$_.pull(r[key], (nValue))
else if (r[key].includes(value)) {
this.$_.pull(r[key], (value))
if (nValue)
r[key].push(nValue)
} else
r[key].push(value)
} else r[key].push(value)
this.$emit('update:filtersActive', r)
},

View file

@ -5,18 +5,11 @@
:key="key"
:disabled="(f.values && f.values.length === 0) && !f.search"
>
<v-expansion-panel-header class="text-uppercase ps-1">
<v-expansion-panel-header class="text-uppercase">
<v-icon
color="secondary"
style="max-width: 24px"
class="mx-0"
@click.stop="clickFilterMode(key, false)"
>{{ groupAllOfActive(key) ? 'mdi-filter-multiple' : '' }}
</v-icon>
<v-icon
color="secondary"
style="max-width: 24px"
class="me-2"
class="mx-2"
@click.stop="clear(key)"
>{{ groupActive(key) ? 'mdi-checkbox-marked' : '' }}
</v-icon>
@ -33,35 +26,11 @@
</template>
</search-box-base>
<div style="position: absolute; right: 0; z-index: 1">
<v-btn-toggle v-if="f.anyAllSelector || groupAllOfActive(key)" mandatory class="semi-transparent"
:value="filtersActiveMode[key]?.allOf">
<v-tooltip bottom>
<template v-slot:activator="{ on }">
<v-btn small icon :value="false" v-on="on" @click.stop="clickFilterMode(key, false)">
<v-icon small>mdi-filter-outline</v-icon>
</v-btn>
</template>
<span>{{ $t('common.any_of') }}</span>
</v-tooltip>
<v-tooltip bottom>
<template v-slot:activator="{ on }">
<v-btn small icon :value="true" v-on="on" @click.stop="clickFilterMode(key, true)">
<v-icon small>mdi-filter-multiple-outline</v-icon>
</v-btn>
</template>
<span>{{ $t('common.all_of') }}</span>
</v-tooltip>
</v-btn-toggle>
</div>
<v-list
v-if="f.search || f.values"
v-if="f.search"
dense
>
<!-- Dynamic content from search -->
<v-list-item v-for="(v, i) in searchFiltersActive(key)"
<v-list-item v-for="(v, i) in filtersActive[key]"
:key="i"
@click.stop="click(key, v)"
>
@ -70,17 +39,18 @@
</v-list-item-icon>
<v-list-item-title>{{ v }}</v-list-item-title>
</v-list-item>
</v-list>
<!-- Static content from filters options -->
<v-list
v-if="f.values"
dense
>
<v-list-item v-for="v in f.values"
:key="JSON.stringify(v.value)"
@click.stop="click(key, v.value, v.nValue)"
:key="v.value"
@click.stop="click(key, v.value)"
>
<v-list-item-icon>
<v-icon v-if="key in filtersActive && includes(filtersActive[key], v.nValue)" color="secondary">
mdi-minus-box
</v-icon>
<v-icon v-else-if="key in filtersActive && includes(filtersActive[key], v.value)" color="secondary">
<v-icon v-if="key in filtersActive && filtersActive[key].includes(v.value)" color="secondary">
mdi-checkbox-marked
</v-icon>
<v-icon v-else>
@ -98,7 +68,6 @@
<script lang="ts">
import Vue, {PropType} from 'vue'
import SearchBoxBase from '@/components/SearchBoxBase.vue'
import {FiltersActive, FiltersActiveMode, FiltersOptions} from '@/types/filter'
export default Vue.extend({
name: 'FilterPanels',
@ -112,25 +81,11 @@ export default Vue.extend({
type: Object as PropType<FiltersActive>,
required: true,
},
filtersActiveMode: {
type: Object as PropType<FiltersActiveMode>,
required: false,
},
},
methods: {
// filtersActive, filtered to not show options that are in filtersOptions
searchFiltersActive(key: string): FiltersActive[] {
if (!(key in this.filtersActive)) return []
const listedOptions = this.filtersOptions[key]?.values?.flatMap(x => [x.value, x.nValue]).map(x => JSON.stringify(x))
return this.filtersActive[key].filter((x: string) => !this.$_.includes(listedOptions, JSON.stringify(x)))
},
includes(array: any[], value: any): boolean {
return this.$_.isObject(value) ? this.$_.some(array, value) : this.$_.includes(array, value)
},
clear(key: string) {
let r = this.$_.cloneDeep(this.filtersActive)
r[key] = []
if (!this.filtersOptions[key].anyAllSelector) this.clickFilterMode(key, false)
this.$emit('update:filtersActive', r)
},
@ -138,39 +93,14 @@ export default Vue.extend({
if (!(key in this.filtersActive)) return false
return this.filtersActive[key].length > 0
},
groupAllOfActive(key: string): boolean {
if (!this.filtersActiveMode || !(key in this.filtersActiveMode)) return false
return this.filtersActiveMode[key].allOf
},
click(key: string, value: any, nValue?: any) {
click(key: string, value: string) {
let r = this.$_.cloneDeep(this.filtersActive)
if (!(key in r)) r[key] = []
const pull = this.$_.isObject(value) ? this.$_.remove : this.$_.pull
const includes = this.$_.isObject(value) ? this.$_.some : this.$_.includes
if (nValue && includes(r[key], nValue))
pull(r[key], nValue)
else if (includes(r[key], value)) {
pull(r[key], value)
if (nValue) {
r[key].push(nValue)
this.clickFilterMode(key, true)
}
} else
r[key].push(value)
if (!this.filtersOptions[key].anyAllSelector && r[key].length == 0) this.clickFilterMode(key, false)
if (r[key].includes(value)) this.$_.pull(r[key], (value))
else r[key].push(value)
this.$emit('update:filtersActive', r)
},
clickFilterMode(key: string, value: boolean) {
if (!this.filtersActiveMode) return
let r = this.$_.cloneDeep(this.filtersActiveMode)
r[key] = {allOf: value}
this.$emit('update:filtersActiveMode', r)
},
},
})
</script>
@ -179,12 +109,4 @@ export default Vue.extend({
.no-padding .v-expansion-panel-content__wrap {
padding: 0;
}
.semi-transparent {
opacity: 0.5;
}
.semi-transparent:hover {
opacity: 1;
}
</style>

View file

@ -8,13 +8,9 @@
:class="flexClass"
handle=".handle"
v-bind="dragOptions"
:forceFallback="true"
:scroll-sensitivity="200"
@start="transitions = false"
@end="transitions = true"
>
<transition-group type="transition"
:name="transitions ? 'flip-list' : null"
:name="!draggable ? 'flip-list' : null"
:class="flexClass"
>
<v-item
@ -37,24 +33,9 @@
:preselect="shouldPreselect"
:onEdit="(draggable || deletable) ? undefined : editFunction"
:onSelected="(draggable || deletable) ? undefined : selectable ? (item, event) => handleSelectClick(toggle, item, event): undefined"
:action-menu="(draggable || deletable) ? false : actionMenu"
:disable-fab="draggable || deletable"
:action-menu="actionMenu"
></item-card>
<v-slide-y-reverse-transition>
<v-text-field v-if="draggable"
v-model="localItemsIndex[JSON.stringify(item)]"
type="number"
min="0"
:max="localItems.length - 1"
solo
style="position: absolute; top: 0; left: 0;"
ref=""
@blur="updateIndex(item)"
@keydown.enter="updateIndex(item)"
/>
</v-slide-y-reverse-transition>
<v-slide-y-reverse-transition>
<v-icon v-if="draggable"
class="handle"
@ -148,12 +129,10 @@ export default Vue.extend({
data: () => {
return {
selectedItems: [] as any[],
localItems: [] as any[],
localItemsIndex: {} as Record<string, any>,
localItems: [],
lastClickedNoShift: undefined as any,
lastClickedShift: undefined as any,
width: 150,
transitions: true,
}
},
watch: {
@ -172,10 +151,6 @@ export default Vue.extend({
items: {
handler() {
this.localItems = this.items as []
this.localItemsIndex = {}
for (const [i, value] of this.localItems.entries()) {
this.$set(this.localItemsIndex, JSON.stringify(value), i)
}
},
immediate: true,
},
@ -249,12 +224,6 @@ export default Vue.extend({
const index = this.localItems.findIndex((e: any) => e.id === item.id)
this.localItems.splice(index, 1)
},
updateIndex(item: any) {
const oldIndex = this.localItems.indexOf(item)
const newIndex = Math.min(Math.max(this.localItemsIndex[JSON.stringify(item)], 0), this.localItems.length - 1)
if (oldIndex != newIndex)
this.localItems.splice(oldIndex, 1, this.localItems.splice(newIndex, 1, this.localItems[oldIndex])[0])
},
},
})
</script>

View file

@ -12,9 +12,7 @@
:src="thumbnailUrl"
:lazy-src="thumbnailError ? coverBase64 : undefined"
aspect-ratio="0.7071"
:contain="!isStretch"
:position="isStretch ? 'top' : undefined"
:class="shouldBlurPoster ? 'blur' : undefined"
contain
@error="thumbnailError = true"
@load="thumbnailError = false"
>
@ -50,7 +48,7 @@
<!-- FAB reading (center) -->
<v-btn
v-if="showFab"
v-if="bookReady && !selected && !preselect && canReadPages"
fab
x-large
color="accent"
@ -177,7 +175,6 @@ import {
import {coverBase64} from '@/types/image'
import {ReadListDto} from '@/types/komga-readlists'
import OneShotActionsMenu from '@/components/menus/OneshotActionsMenu.vue'
import {CLIENT_SETTING} from '@/types/komga-clientsettings'
export default Vue.extend({
name: 'ItemCard',
@ -233,11 +230,6 @@ export default Vue.extend({
type: Boolean,
default: true,
},
// force disable fab
disableFab: {
type: Boolean,
default: false,
},
},
data: () => {
return {
@ -275,20 +267,11 @@ export default Vue.extend({
this.$eventHub.$off(THUMBNAILCOLLECTION_DELETED, this.thumbnailCollectionChanged)
},
computed: {
isStretch(): boolean {
return this.$store.getters.getClientSettings[CLIENT_SETTING.WEBUI_POSTER_STRETCH]?.value === 'true'
},
isBlurUnread(): boolean {
return this.$store.getters.getClientSettings[CLIENT_SETTING.WEBUI_POSTER_BLUR_UNREAD]?.value === 'true'
},
shouldBlurPoster(): boolean | undefined {
return (this.isUnread || this.allUnread) && this.isBlurUnread
},
canReadPages(): boolean {
return this.$store.getters.mePageStreaming && this.computedItem.type() === ItemTypes.BOOK
},
overlay(): boolean {
return this.onEdit !== undefined || this.onSelected !== undefined || this.showFab || this.actionMenu
return this.onEdit !== undefined || this.onSelected !== undefined || this.bookReady || this.canReadPages || this.actionMenu
},
computedItem(): Item<BookDto | SeriesDto | CollectionDto | ReadListDto> {
let item = this.item
@ -324,10 +307,6 @@ export default Vue.extend({
if (this.computedItem.type() === ItemTypes.SERIES) return (this.item as SeriesDto).booksUnreadCount + (this.item as SeriesDto).booksInProgressCount
return undefined
},
allUnread(): boolean | undefined {
if (this.computedItem.type() === ItemTypes.SERIES) return (this.item as SeriesDto).booksCount == (this.item as SeriesDto).booksUnreadCount
return undefined
},
readProgressPercentage(): number {
if (this.computedItem.type() === ItemTypes.BOOK) return getReadProgressPercentage(this.item as BookDto)
return 0
@ -338,9 +317,6 @@ export default Vue.extend({
}
return false
},
showFab(): boolean {
return !this.disableFab && this.bookReady && !this.selected && !this.preselect && this.canReadPages
},
to(): RawLocation {
return this.computedItem.to()
},
@ -396,10 +372,6 @@ export default Vue.extend({
</script>
<style>
.blur > .v-image__image {
filter: blur(5px);
}
.no-link {
cursor: default;
}

View file

@ -1,7 +1,7 @@
<template>
<div>
<v-bottom-navigation
v-if="bottomNavigation"
v-if="show && bottomNavigation"
grow color="primary"
:app="$vuetify.breakpoint.smAndUp"
:fixed="bottomNavigation"
@ -9,25 +9,20 @@
<v-btn v-if="showRecommended"
:to="{name: 'recommended-libraries', params: {libraryId: libraryId}}"
>
<span class="caption">{{ $t('library_navigation.recommended') }}</span>
<span>{{ $t('library_navigation.recommended') }}</span>
<v-icon>mdi-star</v-icon>
</v-btn>
<v-btn :to="{name: 'browse-libraries', params: {libraryId: libraryId}}">
<span class="caption">{{ $t('library_navigation.browse_series') }}</span>
<span>{{ $t('library_navigation.browse') }}</span>
<v-icon>mdi-bookshelf</v-icon>
</v-btn>
<v-btn :to="{name: 'browse-books', params: {libraryId: libraryId}}">
<span class="caption">{{ $t('library_navigation.browse_books') }}</span>
<v-icon>mdi-book-multiple</v-icon>
</v-btn>
<v-btn
v-if="collectionsCount > 0"
:to="{name: 'browse-collections', params: {libraryId: libraryId}}"
>
<span class="caption">{{ $t('library_navigation.collections') }}</span>
<span>{{ $t('library_navigation.collections') }}</span>
<v-icon>mdi-layers-triple</v-icon>
</v-btn>
@ -35,14 +30,14 @@
v-if="readListsCount > 0"
:to="{name: 'browse-readlists', params: {libraryId: libraryId}}"
>
<span class="caption">{{ $t('library_navigation.readlists') }}</span>
<v-icon>mdi-bookmark-multiple</v-icon>
<span>{{ $t('library_navigation.readlists') }}</span>
<v-icon>mdi-book-multiple</v-icon>
</v-btn>
</v-bottom-navigation>
<template
v-if="!bottomNavigation"
v-if="show && !bottomNavigation"
>
<v-btn v-if="showRecommended"
:to="{name: 'recommended-libraries', params: {libraryId: libraryId}}"
@ -56,14 +51,7 @@
text
class="mx-1"
>
{{ $t('library_navigation.browse_series') }}
</v-btn>
<v-btn :to="{name: 'browse-books', params: {libraryId: libraryId}}"
text
class="mx-1"
>
{{ $t('library_navigation.browse_books') }}
{{ $t('library_navigation.browse') }}
</v-btn>
<v-btn
@ -92,7 +80,6 @@
import Vue from 'vue'
import {COLLECTION_ADDED, COLLECTION_DELETED, READLIST_ADDED, READLIST_DELETED} from '@/types/events'
import {LIBRARIES_ALL} from '@/types/library'
import {LibraryDto} from '@/types/komga-libraries'
export default Vue.extend({
name: 'LibraryNavigation',
@ -120,14 +107,6 @@ export default Vue.extend({
},
immediate: true,
},
'$store.getters.getLibrariesPinned': {
handler(val) {
if (this.libraryId === LIBRARIES_ALL) {
this.loadCollectionCounts(this.libraryId)
this.loadReadListCounts(this.libraryId)
}
},
},
},
created() {
this.$eventHub.$on(COLLECTION_ADDED, this.collectionAdded)
@ -145,6 +124,9 @@ export default Vue.extend({
showRecommended(): boolean {
return this.libraryId !== LIBRARIES_ALL
},
show(): boolean {
return this.collectionsCount > 0 || this.readListsCount > 0 || this.showRecommended
},
},
methods: {
readListAdded() {
@ -160,12 +142,12 @@ export default Vue.extend({
if(this.collectionsCount === 1) this.loadCollectionCounts(this.libraryId)
},
async loadCollectionCounts(libraryId: string) {
const lib = libraryId !== LIBRARIES_ALL ? [libraryId] : this.$store.getters.getLibrariesPinned.map((it: LibraryDto) => it.id)
const lib = libraryId !== LIBRARIES_ALL ? [libraryId] : undefined
this.$komgaCollections.getCollections(lib, {size: 0})
.then(v => this.collectionsCount = v.totalElements)
},
async loadReadListCounts(libraryId: string) {
const lib = libraryId !== LIBRARIES_ALL ? [libraryId] : this.$store.getters.getLibrariesPinned.map((it: LibraryDto) => it.id)
const lib = libraryId !== LIBRARIES_ALL ? [libraryId] : undefined
await this.$komgaReadLists.getReadLists(lib, {size: 0})
.then(v => this.readListsCount = v.totalElements)
},

View file

@ -5,7 +5,7 @@
<v-icon>mdi-view-grid-plus</v-icon>
</v-btn>
</template>
<v-list :dark="dark">
<v-list>
<v-list-item-group v-model="selection">
<v-list-item v-for="(item, index) in items"
@ -38,10 +38,6 @@ export default Vue.extend({
type: Number,
required: true,
},
dark: {
type: Boolean,
default: false,
},
},
watch: {
value: {

View file

@ -15,8 +15,7 @@
year: 'numeric',
timeZone: 'UTC'
}).format(new Date(series.releaseDate))
}}
</div>
}}</div>
</template>
<template v-else>
<div style="height: 2em" class="missing"></div>
@ -72,7 +71,6 @@ import {
ReadListRequestBookMatchSeriesDto,
} from '@/types/komga-readlists'
import BookPickerDialog from '@/components/dialogs/BookPickerDialog.vue'
import {BookSearch, SearchConditionSeriesId, SearchOperatorIs} from '@/types/komga-search'
export default Vue.extend({
name: 'ReadListMatchRow',
@ -97,10 +95,6 @@ export default Vue.extend({
type: Boolean,
default: false,
},
error: {
type: String,
default: '',
},
},
watch: {
series: {
@ -122,13 +116,16 @@ export default Vue.extend({
existingFileNames(): string[] {
return this.seriesBooks.map(x => x.name)
},
error(): string {
if (!this.series) return this.$t('book_import.row.error_choose_series').toString()
if (!this.book) return this.$t('readlist_import.row.error_choose_book').toString()
return ''
},
},
methods: {
openBookPicker() {
if (!this.seriesBooksCached) {
this.$komgaBooks.getBooksList({
condition: new SearchConditionSeriesId(new SearchOperatorIs(this.series?.seriesId)),
} as BookSearch, {unpaged: true, sort: 'metadata.numberSort'})
this.$komgaSeries.getBooks(this.series?.seriesId, {unpaged: true})
.then(r => {
this.seriesBooks = r.content
this.seriesBooksCached = true

View file

@ -10,7 +10,6 @@
@scroll-changed="(percent) => scrollChanged(readListsLoaders[index], percent)"
>
<template v-slot:prepend>
<slot name="prepend" v-bind:readlist="r"/>
<router-link class="text-overline"
:to="{name: 'browse-readlist', params: {readListId: r.id}}"
>{{ $t('readlists_expansion_panel.manage_readlist') }}

View file

@ -1,5 +1,5 @@
<template>
<vue-read-more-smooth no-shadow :lines="4" :open.sync="open">
<vue-read-more-smooth no-shadow :lines="4">
<div style="white-space: pre-wrap" class="body-2">
<slot/>
</div>
@ -19,25 +19,8 @@ import Vue from 'vue'
export default Vue.extend({
name: 'ReadMore',
components: {VueReadMoreSmooth},
data: () => {
return {
open: false,
}
},
watch: {
value(val) {
this.open = val
},
open(val) {
this.$emit('input', val)
},
},
components: { VueReadMoreSmooth },
props: {
value: {
type: Boolean,
default: false,
},
i18nMore: {
type: String,
default: 'read_more.more',

View file

@ -1,126 +0,0 @@
<template>
<div>
<v-list>
<v-list-item class="contrast-1">
<v-list-item-content>
<v-list-item-title class="text-uppercase">{{ $t('common.reorder') }}</v-list-item-title>
</v-list-item-content>
<v-list-item-action class="ma-0">
<v-btn icon @click.stop.capture.prevent="dismiss">
<v-icon>mdi-close</v-icon>
</v-btn>
</v-list-item-action>
</v-list-item>
<v-list-item class="text--disabled">
<v-list-item-icon>
<v-icon>mdi-home</v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title>{{ $t('navigation.home') }}</v-list-item-title>
</v-list-item-content>
</v-list-item>
<draggable
v-model="localItems"
v-bind="dragOptions"
handle=".handle"
>
<v-list-item v-for="l in localItems" :key="l.id">
<v-list-item-icon>
<v-icon class="handle">mdi-drag-horizontal-variant</v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title class="handle">{{ l.name }}</v-list-item-title>
</v-list-item-content>
<v-list-item-icon>
<v-btn icon v-if="!l.unpinned" @click.stop.capture.prevent="unpin(l.id)" x-small>
<v-icon>mdi-pin</v-icon>
</v-btn>
<v-btn icon v-if="l.unpinned" @click.stop.capture.prevent="pin(l.id)" x-small>
<v-icon>mdi-pin-off</v-icon>
</v-btn>
</v-list-item-icon>
</v-list-item>
</draggable>
</v-list>
</div>
</template>
<script lang="ts">
import Vue from 'vue'
import draggable from 'vuedraggable'
import {LibraryDto} from '@/types/komga-libraries'
import {ClientSettingLibraryUpdate} from '@/types/komga-clientsettings'
export default Vue.extend({
name: 'ReorderLibraries',
components: {draggable},
data: () => {
return {
localItems: [] as LibraryDto[],
unwatch: false,
}
},
computed: {
dragOptions(): any {
return {
animation: 200,
ghostClass: 'ghost',
}
},
},
mounted() {
this.localItems = this.$store.getters.getLibraries
},
watch: {
localItems: {
handler(val: LibraryDto[]) {
const newSettings = val.map((it, index) => ({
libraryId: it.id,
patch: {
order: index,
},
} as ClientSettingLibraryUpdate))
this.$store.dispatch('updateLibrariesSettings', newSettings)
},
immediate: true,
},
},
methods: {
dismiss() {
this.$emit('dismiss')
},
unpin(libraryId: string) {
this.$store.dispatch('updateLibrarySetting', {
libraryId: libraryId,
patch: {
unpinned: true,
},
} as ClientSettingLibraryUpdate)
this.localItems.find(it => it.id == libraryId).unpinned = true
},
pin(libraryId: string) {
this.$store.dispatch('updateLibrarySetting', {
libraryId: libraryId,
patch: {
unpinned: false,
},
} as ClientSettingLibraryUpdate)
this.localItems.find(it => it.id == libraryId).unpinned = false
},
},
})
</script>
<style scoped>
.handle {
cursor: grab !important;
}
.ghost {
opacity: 0.5;
background: #c8ebfb;
}
</style>

View file

@ -123,12 +123,6 @@ import {SeriesDto} from '@/types/komga-series'
import {getReadProgress} from '@/functions/book-progress'
import {ReadStatus} from '@/types/enum-books'
import {ReadListDto} from '@/types/komga-readlists'
import {
BookSearch,
SearchConditionOneShot,
SearchOperatorIsFalse,
SeriesSearch,
} from '@/types/komga-search'
export default Vue.extend({
name: 'SearchBox',
@ -208,13 +202,8 @@ export default Vue.extend({
searchItems: debounce(async function (this: any, query: string) {
if (query) {
this.loading = true
this.series = (await this.$komgaSeries.getSeriesList({
fullTextSearch: query,
condition: new SearchConditionOneShot(new SearchOperatorIsFalse()),
} as SeriesSearch, {size: this.pageSize})).content
this.books = (await this.$komgaBooks.getBooksList({
fullTextSearch: query,
} as BookSearch, {size: this.pageSize})).content
this.series = (await this.$komgaSeries.getSeries(undefined, {size: this.pageSize}, query, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, false)).content
this.books = (await this.$komgaBooks.getBooks(undefined, {size: this.pageSize}, query)).content
this.collections = (await this.$komgaCollections.getCollections(undefined, {size: this.pageSize}, query)).content
this.readLists = (await this.$komgaReadLists.getReadLists(undefined, {size: this.pageSize}, query)).content
this.showResults = true

View file

@ -161,7 +161,7 @@ export default Vue.extend({
watch: {
users(val) {
val.forEach((u: UserDto) => {
this.$komgaUsers.getLatestAuthenticationActivityForUser(u.id)
this.$komgaUsers.getLatestAuthenticationActivityForUser(u)
.then(value => this.$set(this.usersLastActivity, `${u.id}`, value.dateTime))
.catch(e => {
})

View file

@ -52,7 +52,7 @@
</v-tooltip>
</v-btn>
<v-btn icon @click="addToReadList" v-if="isAdmin && (kind === 'books' || kind === 'series')">
<v-btn icon @click="addToReadList" v-if="isAdmin && (kind === 'books' || (kind === 'series' && oneshots))">
<v-tooltip bottom>
<template v-slot:activator="{ on }">
<v-icon v-on="on">mdi-book-plus-multiple</v-icon>

View file

@ -1,172 +0,0 @@
<template>
<v-dialog v-model="modal"
max-width="600"
>
<v-card>
<v-card-title>{{ $t('dialog.add_api_key.dialog_title') }}</v-card-title>
<v-btn icon absolute top right @click="dialogClose">
<v-icon>mdi-close</v-icon>
</v-btn>
<v-card-text>
<v-container fluid>
<v-row>
<v-col>{{ $t('dialog.add_api_key.context') }}</v-col>
</v-row>
<v-row v-if="!apiKey">
<v-col>
<v-text-field v-model.trim="form.comment"
autofocus
:label="$t('dialog.add_api_key.field_comment')"
:hint="$t('dialog.add_api_key.field_comment_hint')"
:error-messages="getErrors('comment')"
@blur="$v.form.comment.$touch()"
/>
</v-col>
</v-row>
<v-row v-if="apiKey">
<v-col>
<v-alert type="info" class="body-2">{{ $t('dialog.add_api_key.info_copy') }}</v-alert>
</v-col>
</v-row>
<v-row v-if="apiKey">
<v-col>
<v-icon color="success">mdi-check</v-icon>
{{ apiKey.key }}
<v-tooltip top v-model="copied" v-if="isClipboardApiAvailable">
<template v-slot:activator="on">
<v-btn v-on="on"
icon
x-small
class="align-content-end"
@click="copyApiKeyToClipboard"
>
<v-icon v-if="copied" color="success">mdi-check</v-icon>
<v-icon v-else>mdi-content-copy</v-icon>
</v-btn>
</template>
<span>{{ $t('common.copied') }}</span>
</v-tooltip>
</v-col>
</v-row>
</v-container>
</v-card-text>
<v-card-actions>
<v-spacer/>
<v-btn text @click="dialogClose">{{ $t('common.close') }}</v-btn>
<v-btn color="primary" @click="generateApiKey" :disabled="!!apiKey">{{ $t('dialog.add_api_key.button_confirm') }}</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script lang="ts">
import {UserRoles} from '@/types/enum-users'
import Vue from 'vue'
import {required} from 'vuelidate/lib/validators'
import {ERROR} from '@/types/events'
import {ApiKeyDto, ApiKeyRequestDto} from '@/types/komga-users'
function validComment(value: string) {
return !this.alreadyUsedComment.includes(value)
}
export default Vue.extend({
name: 'ApiKeyAddDialog',
data: function () {
return {
UserRoles,
modal: false,
apiKey: undefined as ApiKeyDto,
copied: false,
alreadyUsedComment: [] as string[],
form: {
comment: '',
},
}
},
props: {
value: Boolean,
},
watch: {
value(val) {
this.modal = val
if (val) {
this.clear()
}
},
modal(val) {
!val && this.dialogClose()
},
},
validations: {
form: {
comment: {required, validComment},
},
},
computed: {
isClipboardApiAvailable(): boolean {
return !!navigator.clipboard
},
},
methods: {
clear() {
this.apiKey = undefined
this.alreadyUsedComment = []
this.form.comment = ''
this.$v.$reset()
},
dialogClose() {
this.$emit('input', false)
},
getErrors(fieldName: string): string[] {
const errors = [] as string[]
const field = this.$v.form!![fieldName] as any
if (field && field.$invalid && field.$dirty) {
if (!field.validComment) errors.push(this.$t('error_codes.ERR_1034').toString())
if (!field.required) errors.push(this.$t('common.required').toString())
}
return errors
},
validateInput(): ApiKeyRequestDto {
this.$v.$touch()
if (!this.$v.$invalid) {
return {
comment: this.form.comment,
}
}
return undefined
},
async generateApiKey() {
const apiKeyRequest = this.validateInput()
if (apiKeyRequest) {
try {
this.apiKey = await this.$komgaUsers.createApiKey(apiKeyRequest)
this.$emit('generate')
} catch (e) {
if (e.message.includes('ERR_1034'))
this.alreadyUsedComment.push(this.form.comment)
else
this.$eventHub.$emit(ERROR, {message: e.message} as ErrorEvent)
}
}
},
copyApiKeyToClipboard() {
navigator.clipboard.writeText(this.apiKey.key)
this.copied = true
setTimeout(() => this.copied = false, 3000)
},
},
})
</script>
<style scoped>
</style>

View file

@ -122,7 +122,7 @@
<!-- Sort Number -->
<v-col cols="2">
<v-text-field v-model.number="form[book.id].numberSort"
<v-text-field v-model="form[book.id].numberSort"
type="number"
step="0.1"
dense
@ -200,7 +200,7 @@
<script lang="ts">
import Vue from 'vue'
import {BookDto} from '@/types/komga-books'
import IsbnVerify from '@w0s/isbn-verify'
import IsbnVerify from '@saekitominaga/isbn-verify'
import {isMatch} from 'date-fns'
import {ERROR} from '@/types/events'
@ -283,7 +283,7 @@ export default Vue.extend({
return value || value === 0 ? true : this.$t('common.required').toString()
},
validateReleaseDate(date: string): string | boolean {
return date && (!isMatch(date, 'yyyy-MM-dd') || date.length !== 10) ? this.$t('dialog.edit_books.field_release_date_error').toString() : true
return date && !isMatch(date, 'yyyy-MM-dd') ? this.$t('dialog.edit_books.field_release_date_error').toString() : true
},
bookDisplayName(book: BookDto): string {
const parts = book.url.split('/')

View file

@ -100,6 +100,7 @@
import {UserRoles} from '@/types/enum-users'
import Vue from 'vue'
import {ERROR, ErrorEvent} from '@/types/events'
import {LibraryDto} from '@/types/komga-libraries'
import ThumbnailCard from '@/components/ThumbnailCard.vue'
import DropZone from '@/components/DropZone.vue'
@ -151,6 +152,9 @@ export default Vue.extend({
},
},
computed: {
libraries(): LibraryDto[] {
return this.$store.state.komgaLibraries.libraries
},
getErrorsName(): string {
if (this.form.name === '') return this.$t('common.required').toString()
if (this.form.name?.toLowerCase() !== this.collection.name?.toLowerCase() && this.collections.some(e => e.name.toLowerCase() === this.form.name.toLowerCase())) {

View file

@ -430,15 +430,14 @@ import {authorRoles} from '@/types/author-roles'
import Vue from 'vue'
import {helpers, requiredIf} from 'vuelidate/lib/validators'
import {BookDto, BookThumbnailDto} from '@/types/komga-books'
import IsbnVerify from '@w0s/isbn-verify'
import IsbnVerify from '@saekitominaga/isbn-verify'
import {isMatch} from 'date-fns'
import {debounce} from 'lodash'
import {ERROR, ErrorEvent} from '@/types/events'
import DropZone from '@/components/DropZone.vue'
import ThumbnailCard from '@/components/ThumbnailCard.vue'
import {NameValue} from '@/types/filter'
const validDate = (value: string) => !helpers.req(value) || isMatch(value, 'yyyy-MM-dd') && value.length == 10
const validDate = (value: string) => !helpers.req(value) || isMatch(value, 'yyyy-MM-dd')
const validIsbn = (value: string) => !helpers.req(value) || new IsbnVerify(value).isIsbn13({check_digit: true})
export default Vue.extend({

View file

@ -570,7 +570,7 @@ import DropZone from '@/components/DropZone.vue'
import ThumbnailCard from '@/components/ThumbnailCard.vue'
import {BookDto, BookThumbnailDto} from '@/types/komga-books'
import {isMatch} from 'date-fns'
import IsbnVerify from '@w0s/isbn-verify'
import IsbnVerify from '@saekitominaga/isbn-verify'
import {debounce} from 'lodash'
import {authorRoles} from '@/types/author-roles'
import {groupAuthorsByRole} from '@/functions/authors'

View file

@ -1,136 +0,0 @@
<template>
<v-dialog v-model="modal"
max-width="450"
:fullscreen="$vuetify.breakpoint.xsOnly"
>
<v-card>
<v-card-title>{{ $t('dialog.edit_recommended.dialog_title') }}</v-card-title>
<v-btn icon absolute top right @click="dialogClose">
<v-icon>mdi-close</v-icon>
</v-btn>
<v-card-text :class="$vuetify.breakpoint.xsOnly ? 'px-0' : undefined">
<v-list>
<draggable
v-model="localItems"
v-bind="dragOptions"
handle=".handle"
>
<v-list-item v-for="(l, index) in localItems" :key="index">
<v-list-item-icon>
<v-icon class="handle">mdi-drag-horizontal-variant</v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title class="handle">{{ $t(`dashboard.${l.section.toLowerCase()}`) }}</v-list-item-title>
</v-list-item-content>
<v-list-item-action>
<v-switch v-model="enabled[l.section]"/>
</v-list-item-action>
</v-list-item>
</draggable>
</v-list>
</v-card-text>
<v-card-actions>
<v-spacer/>
<v-btn v-if="$vuetify.breakpoint.smAndUp" text @click="dialogClose">{{ $t('common.cancel') }}</v-btn>
<v-btn color="error" @click="resetToDefault">{{
$t('dialog.edit_recommended.button_reset')
}}
</v-btn>
<v-btn color="primary" @click="saveChanges">{{
$t('dialog.edit_recommended.button_confirm')
}}
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script lang="ts">
import {UserRoles} from '@/types/enum-users'
import draggable from 'vuedraggable'
import Vue, {PropType} from 'vue'
import {
ClientSettingsRecommendedView,
ClientSettingsRecommendedViewSection,
RECOMMENDED_DEFAULT,
} from '@/types/komga-clientsettings'
export default Vue.extend({
name: 'EditRecommendedDialog',
components: {draggable},
data: function () {
return {
UserRoles,
modal: false,
localItems: [] as ClientSettingsRecommendedViewSection[],
enabled: {} as Record<string, boolean>,
}
},
props: {
value: Boolean,
viewConfig: {
type: Object as PropType<ClientSettingsRecommendedView>,
required: true,
},
},
watch: {
value(val) {
this.modal = val
if (val) {
this.reset(this.viewConfig)
}
},
modal(val) {
!val && this.dialogClose()
},
},
computed: {
dragOptions(): any {
return {
animation: 200,
ghostClass: 'ghost',
}
},
},
methods: {
reset(viewConfig: ClientSettingsRecommendedView) {
this.localItems = viewConfig?.sections || []
this.enabled = []
this.localItems.forEach(it => this.enabled[it.section] = true)
RECOMMENDED_DEFAULT.sections
.filter(it => !viewConfig?.sections.some(s => s.section === it.section))
.forEach(it => this.localItems.push(it))
},
dialogClose() {
this.$emit('input', false)
},
resetToDefault() {
this.$emit('reset-defaults')
this.dialogClose()
},
saveChanges() {
const sections = this.localItems.filter(it => this.enabled[it.section])
const updated = {
sections: sections,
} as ClientSettingsRecommendedView
this.$emit('update:viewConfig', updated)
this.dialogClose()
},
},
})
</script>
<style scoped>
.handle {
cursor: grab !important;
}
.ghost {
opacity: 0.5;
background: #c8ebfb;
}
</style>

View file

@ -17,7 +17,7 @@
<template v-if="directoryListing.hasOwnProperty('parent')">
<v-list-item
@click.prevent="selectParent(directoryListing.parent)"
@click.prevent="select(directoryListing.parent)"
>
<v-list-item-icon>
<v-icon>mdi-arrow-left</v-icon>
@ -32,7 +32,7 @@
<div v-for="(d, index) in directoryListing.directories" :key="index">
<v-list-item
@click.prevent="select(d)"
@click.prevent="select(d.path)"
>
<v-list-item-icon>
<v-icon>{{ d.type === 'directory' ? 'mdi-folder' : 'mdi-file' }}</v-icon>
@ -47,24 +47,6 @@
<v-divider v-if="index !== directoryListing.directories.length-1"/>
</div>
<div v-for="(d, index) in directoryListing.files" :key="index">
<v-list-item
@click.prevent="select(d)"
>
<v-list-item-icon>
<v-icon>{{ d.type === 'directory' ? 'mdi-folder' : 'mdi-file' }}</v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title>
{{ d.name }}
</v-list-item-title>
</v-list-item-content>
</v-list-item>
<v-divider v-if="index !== directoryListing.files.length-1"/>
</div>
</v-list>
</v-card-text>
@ -109,10 +91,6 @@ export default Vue.extend({
type: String,
required: false,
},
showFiles: {
type: Boolean,
default: false,
},
dialogTitle: {
type: String,
default: function (): string {
@ -141,23 +119,18 @@ export default Vue.extend({
dialogConfirm() {
this.$emit('input', false)
this.$emit('update:path', this.selectedPath)
this.$emit('confirm')
},
async getDirs(path?: string) {
try {
this.directoryListing = await this.$komgaFileSystem.getDirectoryListing(path, this.showFiles)
this.directoryListing = await this.$komgaFileSystem.getDirectoryListing(path)
} catch (e) {
this.$eventHub.$emit(ERROR, {message: e.message} as ErrorEvent)
}
},
selectParent(path: string) {
select(path: string) {
this.selectedPath = path
this.getDirs(path)
},
select(path: PathDto) {
this.selectedPath = path.path
if(path.type == 'directory') this.getDirs(path.path)
},
},
})
</script>

View file

@ -108,9 +108,6 @@ export default Vue.extend({
},
immediate: true,
},
modal(val) {
!val && this.dialogClose()
},
},
methods: {
clear() {

View file

@ -215,22 +215,6 @@
</template>
</v-checkbox>
<v-checkbox
v-model="form.hashKoreader"
:label="$t('dialog.edit_library.field_analysis_hash_koreader')"
hide-details
class="mx-4"
>
<template v-slot:append>
<v-tooltip bottom>
<template v-slot:activator="{ on }">
<v-icon v-on="on" color="warning">mdi-alert-circle-outline</v-icon>
</template>
{{ $t('dialog.edit_library.tooltip_use_resources') }}
</v-tooltip>
</template>
</v-checkbox>
<v-checkbox
v-model="form.analyzeDimensions"
:label="$t('dialog.edit_library.field_analysis_analyze_dimensions')"
@ -481,7 +465,6 @@ export default Vue.extend({
seriesCover: SeriesCoverDto.FIRST as SeriesCoverDto,
hashFiles: true,
hashPages: false,
hashKoreader: false,
analyzeDimensions: true,
oneshotsDirectory: '',
},
@ -641,7 +624,6 @@ export default Vue.extend({
this.form.seriesCover = library ? library.seriesCover : SeriesCoverDto.FIRST
this.form.hashFiles = library ? library.hashFiles : true
this.form.hashPages = library ? library.hashPages : false
this.form.hashKoreader = library ? library.hashKoreader : false
this.form.analyzeDimensions = library ? library.analyzeDimensions : true
this.form.oneshotsDirectory = library ? library.oneshotsDirectory : ''
this.$v.$reset()
@ -676,7 +658,6 @@ export default Vue.extend({
seriesCover: this.form.seriesCover,
hashFiles: this.form.hashFiles,
hashPages: this.form.hashPages,
hashKoreader: this.form.hashKoreader,
analyzeDimensions: this.form.analyzeDimensions,
oneshotsDirectory: this.form.oneshotsDirectory,
}

View file

@ -113,6 +113,7 @@
import {UserRoles} from '@/types/enum-users'
import Vue from 'vue'
import {ERROR, ErrorEvent} from '@/types/events'
import {LibraryDto} from '@/types/komga-libraries'
import DropZone from '@/components/DropZone.vue'
import ThumbnailCard from '@/components/ThumbnailCard.vue'
import {ReadListDto, ReadListThumbnailDto, ReadListUpdateDto} from '@/types/komga-readlists'
@ -166,6 +167,9 @@ export default Vue.extend({
},
},
computed: {
libraries(): LibraryDto[] {
return this.$store.state.komgaLibraries.libraries
},
getErrorsName(): string {
if (this.form.name === '') return this.$t('common.required').toString()
if (this.form.name?.toLowerCase() !== this.readList.name?.toLowerCase() && this.readLists.some(e => e.name.toLowerCase() === this.form.name.toLowerCase())) {

View file

@ -82,7 +82,6 @@ import Vue, {PropType} from 'vue'
import {SeriesDto} from '@/types/komga-series'
import {debounce} from 'lodash'
import {seriesThumbnailUrl} from '@/functions/urls'
import {SearchConditionOneShot, SearchOperatorIsFalse, SeriesSearch} from '@/types/komga-search'
export default Vue.extend({
name: 'SeriesPickerDialog',
@ -124,10 +123,7 @@ export default Vue.extend({
searchItems: debounce(async function (this: any, query: string) {
if (query) {
this.showResults = false
this.results = (await this.$komgaSeries.getSeriesList({
fullTextSearch: query,
condition: this.includeOneshots ? undefined : new SearchConditionOneShot(new SearchOperatorIsFalse()),
} as SeriesSearch, {unpaged: true})).content
this.results = (await this.$komgaSeries.getSeries(undefined, {unpaged: true}, query, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, undefined, this.includeOneshots ? undefined : false)).content
this.showResults = true
} else {
this.clear()

View file

@ -9,12 +9,6 @@
:total-visible="perPage"
:length="Math.ceil(thumbnails.length/perPage)"
></v-pagination>
<page-size-select
v-model="perPage"
dark
:items="[10, 20, 50, 100]"
/>
</v-card-title>
<v-card-text>
<v-container fluid>
@ -35,7 +29,7 @@
@click="input = false; goTo(((page - 1 ) * perPage + i + 1))"
style="cursor: pointer"
/>
<div class="white--text text-center font-weight-bold">{{ (page - 1) * perPage + i + 1 }}</div>
<div class="white--text text-center font-weight-bold">{{ (page - 1 ) * perPage + i + 1 }}</div>
</div>
</v-row>
@ -48,11 +42,9 @@
<script lang="ts">
import Vue from 'vue'
import {bookPageThumbnailUrl} from '@/functions/urls'
import PageSizeSelect from '@/components/PageSizeSelect.vue'
export default Vue.extend({
name: 'ThumbnailExplorerDialog',
components: {PageSizeSelect},
props: {
pagesCount: {
type: Number,
@ -68,45 +60,39 @@ export default Vue.extend({
return {
input: '',
page: 1,
perPage: 10,
perPage: 8,
}
},
watch: {
value(val) {
value (val) {
this.input = val
},
input(val) {
input (val) {
!val && this.$emit('input', false)
},
perPage(val) {
this.$store.commit('setThumbnailsPageSize', val)
},
},
mounted() {
this.perPage = this.$store.state.persistedState.thumbnailsPageSize || this.perPage
},
computed: {
thumbnails(): string[] {
thumbnails (): string[] {
let thumbnails = []
for (let p = 1; p <= this.pagesCount; p++) {
thumbnails.push(this.getThumbnailUrl(p))
}
return thumbnails
},
visibleThumbnails(): String[] {
visibleThumbnails (): String[] {
let a: number = (this.page - 1) * this.perPage
let b: number = this.page * this.perPage
return this.thumbnails.slice(a, b)
},
},
methods: {
updateInput() {
updateInput () {
this.$emit('input', this.input)
},
goTo(page: number) {
goTo (page: number) {
this.$emit('go', page)
},
getThumbnailUrl(page: number): string {
getThumbnailUrl (page: number): string {
return bookPageThumbnailUrl(this.bookId, page)
},
},

View file

@ -39,7 +39,7 @@
<tr>
<td class="font-weight-medium">{{ $t('dialog.transient_book_details.label_format') }}</td>
<td :class="rightBook ? 'diff' : ''">{{ getBookFormatFromMediaType(leftBook.mediaType).type }}</td>
<td v-if="rightBook">{{ getBookFormatFromMedia(rightBook.media).type }}</td>
<td v-if="rightBook">{{ getBookFormatFromMediaType(rightBook.media.mediaType).type }}</td>
</tr>
<tr>
@ -69,7 +69,7 @@
import Vue, {PropType} from 'vue'
import {TransientBookDto} from '@/types/komga-transientbooks'
import {BookDto, PageDto} from '@/types/komga-books'
import {getBookFormatFromMedia, getBookFormatFromMediaType} from '@/functions/book-format'
import {getBookFormatFromMediaType} from '@/functions/book-format'
import PagesTable from '@/components/PagesTable.vue'
export default Vue.extend({
@ -78,8 +78,7 @@ export default Vue.extend({
data: () => {
return {
modal: false,
getBookFormatFromMediaType: getBookFormatFromMediaType,
getBookFormatFromMedia: getBookFormatFromMedia,
getBookFormatFromMediaType,
}
},
props: {

View file

@ -50,12 +50,24 @@
<v-row>
<v-col>
<span>{{ $t('common.roles') }}</span>
<v-checkbox v-for="role in userRoles" :key="role.value"
v-model="form.roles"
:label="role.text"
:value="role.value"
hide-details
<span>{{ $t('dialog.add_user.label_roles') }}</span>
<v-checkbox
v-model="form.roles"
:label="$t('dialog.add_user.field_role_administrator')"
:value="UserRoles.ADMIN"
hide-details
/>
<v-checkbox
v-model="form.roles"
:label="$t('dialog.add_user.field_role_page_streaming')"
:value="UserRoles.PAGE_STREAMING"
hide-details
/>
<v-checkbox
v-model="form.roles"
:label="$t('dialog.add_user.field_role_file_download')"
:value="UserRoles.FILE_DOWNLOAD"
hide-details
/>
</v-col>
</v-row>
@ -83,6 +95,7 @@ export default Vue.extend({
name: 'UserAddDialog',
data: function () {
return {
UserRoles,
modalAddUser: true,
showPassword: false,
dialogTitle: this.$i18n.t('dialog.add_user.dialog_title').toString(),
@ -105,14 +118,6 @@ export default Vue.extend({
password: {required},
},
},
computed: {
userRoles(): any[] {
return Object.keys(UserRoles).map(x => ({
text: this.$t(`user_roles.${x}`),
value: x,
}))
},
},
methods: {
getErrors(fieldName: string): string[] {
const errors = [] as string[]

View file

@ -15,11 +15,23 @@
<v-row>
<v-col>
<v-checkbox v-for="role in userRoles" :key="role.value"
v-model="roles"
:label="role.text"
:value="role.value"
hide-details
<v-checkbox
v-model="roles"
:label="$t('dialog.add_user.field_role_administrator')"
:value="UserRoles.ADMIN"
hide-details
/>
<v-checkbox
v-model="roles"
:label="$t('dialog.add_user.field_role_page_streaming')"
:value="UserRoles.PAGE_STREAMING"
hide-details
/>
<v-checkbox
v-model="roles"
:label="$t('dialog.add_user.field_role_file_download')"
:value="UserRoles.FILE_DOWNLOAD"
hide-details
/>
</v-col>
</v-row>
@ -43,12 +55,14 @@
import {UserRoles} from '@/types/enum-users'
import Vue from 'vue'
import {ERROR} from '@/types/events'
import {LibraryDto} from '@/types/komga-libraries'
import {UserDto, UserUpdateDto} from '@/types/komga-users'
export default Vue.extend({
name: 'UserEditDialog',
data: () => {
return {
UserRoles,
modal: false,
roles: [] as string[],
}
@ -72,11 +86,8 @@ export default Vue.extend({
},
},
computed: {
userRoles(): any[] {
return Object.keys(UserRoles).map(x => ({
text: this.$t(`user_roles.${x}`),
value: x,
}))
libraries(): LibraryDto[] {
return this.$store.state.komgaLibraries.libraries
},
},
methods: {

View file

@ -1,70 +0,0 @@
<template>
<div>
<v-menu offset-y>
<template v-slot:activator="{ on }">
<v-btn icon v-on="on" @click.prevent="">
<v-icon>mdi-dots-vertical</v-icon>
</v-btn>
</template>
<v-list dense>
<v-list-item @click="reorder">
<v-list-item-title>{{ $t('common.reorder') }}</v-list-item-title>
</v-list-item>
<v-list-item @click="scan(false)" v-if="isAdmin">
<v-list-item-title>{{ $t('server.server_management.button_scan_libraries') }}</v-list-item-title>
</v-list-item>
<v-list-item @click="scan(true)" class="list-warning" v-if="isAdmin">
<v-list-item-title>{{ $t('server.server_management.button_scan_libraries_deep') }}</v-list-item-title>
</v-list-item>
<v-list-item @click="confirmEmptyTrash = true" v-if="isAdmin">
<v-list-item-title>{{ $t('server.server_management.button_empty_trash') }}</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
<confirmation-dialog
v-model="confirmEmptyTrash"
:title="$t('dialog.empty_trash.title')"
:body="$t('dialog.empty_trash.body')"
:button-confirm="$t('dialog.empty_trash.button_confirm')"
@confirm="emptyTrash"
/>
</div>
</template>
<script lang="ts">
import Vue from 'vue'
import ConfirmationDialog from '@/components/dialogs/ConfirmationDialog.vue'
export default Vue.extend({
name: 'LibrariesActionsMenu',
components: {ConfirmationDialog},
data: () => {
return {
confirmEmptyTrash: false,
}
},
computed: {
isAdmin(): boolean {
return this.$store.getters.meAdmin
},
},
methods: {
reorder() {
this.$emit('reorder')
},
scan(scanDeep: boolean) {
this.$store.state.komgaLibraries.libraries.forEach(library => {
this.$komgaLibraries.scanLibrary(library, scanDeep)
})
},
emptyTrash() {
this.$store.state.komgaLibraries.libraries.forEach(library => {
this.$komgaLibraries.emptyTrash(library)
})
},
},
})
</script>
<style scoped>
@import "../../styles/list-warning.css";
</style>

View file

@ -1,34 +1,32 @@
<template>
<div>
<v-menu offset-y>
<v-menu offset-y v-if="isAdmin">
<template v-slot:activator="{ on }">
<v-btn icon v-on="on" @click.prevent="">
<v-icon>mdi-dots-vertical</v-icon>
</v-btn>
</template>
<v-list dense>
<v-list-item @click="scan(false)" v-if="isAdmin">
<v-list-item @click="scan(false)">
<v-list-item-title>{{ $t('menu.scan_library_files') }}</v-list-item-title>
</v-list-item>
<v-list-item @click="scan(true)" class="list-warning" v-if="isAdmin">
<v-list-item @click="scan(true)" class="list-warning">
<v-list-item-title>{{ $t('menu.scan_library_files_deep') }}</v-list-item-title>
</v-list-item>
<v-list-item @click="confirmAnalyzeModal = true" v-if="isAdmin">
<v-list-item @click="confirmAnalyzeModal = true">
<v-list-item-title>{{ $t('menu.analyze') }}</v-list-item-title>
</v-list-item>
<v-list-item @click="confirmRefreshMetadataModal = true" v-if="isAdmin">
<v-list-item @click="confirmRefreshMetadataModal = true">
<v-list-item-title>{{ $t('menu.refresh_metadata') }}</v-list-item-title>
</v-list-item>
<v-list-item @click="confirmEmptyTrash = true" v-if="isAdmin">
<v-list-item @click="confirmEmptyTrash = true">
<v-list-item-title>{{ $t('menu.empty_trash') }}</v-list-item-title>
</v-list-item>
<v-list-item @click="edit" v-if="isAdmin">
<v-list-item @click="edit">
<v-list-item-title>{{ $t('menu.edit') }}</v-list-item-title>
</v-list-item>
<v-list-item @click="promptDeleteLibrary"
class="list-danger"
v-if="isAdmin"
>
class="list-danger">
<v-list-item-title>{{ $t('menu.delete') }}</v-list-item-title>
</v-list-item>
</v-list>

View file

@ -38,7 +38,6 @@ import {ReadStatus} from '@/types/enum-books'
import Vue from 'vue'
import {BookDto} from '@/types/komga-books'
import {SeriesDto} from '@/types/komga-series'
import {BookSearch, SearchConditionSeriesId, SearchOperatorIs} from '@/types/komga-search'
export default Vue.extend({
name: 'OneShotActionsMenu',
@ -94,9 +93,7 @@ export default Vue.extend({
this.$store.dispatch('dialogAddSeriesToCollection', [this.seriesId])
},
async addToReadList() {
if (!this.book && !this.localBookId) this.localBookId = (await this.$komgaBooks.getBooksList({
condition: new SearchConditionSeriesId(new SearchOperatorIs(this.seriesId)),
} as BookSearch)).content[0].id
if (!this.book && !this.localBookId) this.localBookId = (await this.$komgaSeries.getBooks(this.seriesId)).content[0].id
this.$store.dispatch('dialogAddBooksToReadList', [this.book?.id || this.localBookId])
},
async markRead() {

View file

@ -16,9 +16,6 @@
<v-list-item @click="addToCollection" v-if="isAdmin">
<v-list-item-title>{{ $t('menu.add_to_collection') }}</v-list-item-title>
</v-list-item>
<v-list-item @click="addToReadList" v-if="isAdmin">
<v-list-item-title>{{ $t('menu.add_to_readlist') }}</v-list-item-title>
</v-list-item>
<v-list-item @click="markRead" v-if="!isRead">
<v-list-item-title>{{ $t('menu.mark_read') }}</v-list-item-title>
</v-list-item>
@ -35,7 +32,6 @@
<script lang="ts">
import Vue from 'vue'
import {SeriesDto} from '@/types/komga-series'
import {BookSearch, SearchConditionSeriesId, SearchOperatorIs} from '@/types/komga-search'
export default Vue.extend({
name: 'SeriesActionsMenu',
@ -80,12 +76,6 @@ export default Vue.extend({
addToCollection() {
this.$store.dispatch('dialogAddSeriesToCollection', [this.series.id])
},
async addToReadList() {
const books = await this.$komgaBooks.getBooksList({
condition: new SearchConditionSeriesId(new SearchOperatorIs(this.series.id)),
} as BookSearch, {unpaged: true, sort: ['metadata.numberSort']})
this.$store.dispatch('dialogAddBooksToReadList', books.content.map(b => b.id))
},
async markRead() {
await this.$komgaSeries.markAsRead(this.series.id)
},

Some files were not shown because too many files have changed in this diff Show more