mirror of
https://github.com/gotson/komga.git
synced 2026-01-26 18:16:03 +01:00
Merge branch 'master' into recommended-library
# Conflicts: # komga-webui/src/services/komga-books.service.ts
This commit is contained in:
commit
c99b6cdf3e
101 changed files with 6350 additions and 750 deletions
18
.github/stale.yml
vendored
18
.github/stale.yml
vendored
|
|
@ -1,18 +0,0 @@
|
|||
# Number of days of inactivity before an issue becomes stale
|
||||
daysUntilStale: 60
|
||||
# Number of days of inactivity before a stale issue is closed
|
||||
daysUntilClose: 7
|
||||
# Issues with these labels will never be considered stale
|
||||
exemptLabels:
|
||||
- pinned
|
||||
- security
|
||||
- tech debt
|
||||
# Label to use when marking an issue as stale
|
||||
staleLabel: stale
|
||||
# Comment to post when marking an issue as stale. Set to `false` to disable
|
||||
markComment: >
|
||||
This issue has been automatically marked as stale because it has not had
|
||||
recent activity. It will be closed if no further activity occurs. Thank you
|
||||
for your contributions.
|
||||
# Comment to post when closing a stale issue. Set to `false` to disable
|
||||
closeComment: false
|
||||
3
.github/workflows/ci.yml
vendored
3
.github/workflows/ci.yml
vendored
|
|
@ -67,6 +67,9 @@ jobs:
|
|||
restore-keys: ${{ runner.os }}-gradle
|
||||
- name: npmBuild
|
||||
run: ./gradlew npmBuild
|
||||
- name: npmTest
|
||||
run: npm run test:unit
|
||||
working-directory: komga-webui
|
||||
|
||||
release:
|
||||
name: Semantic Release
|
||||
|
|
|
|||
23
.github/workflows/stale.yml
vendored
Normal file
23
.github/workflows/stale.yml
vendored
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
name: 'Close stale issues and PRs'
|
||||
on:
|
||||
schedule:
|
||||
- cron: '30 1 * * *'
|
||||
|
||||
jobs:
|
||||
stale:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/stale@v3
|
||||
with:
|
||||
stale-issue-message: >
|
||||
This issue has been automatically marked as stale because it has not had
|
||||
recent activity. It will be closed if no further activity occurs. Thank you
|
||||
for your contributions.
|
||||
stale-pr-message: >
|
||||
This pull request has been automatically marked as stale because it has not had
|
||||
recent activity. It will be closed if no further activity occurs. Thank you
|
||||
for your contributions.
|
||||
days-before-stale: 90
|
||||
days-before-close: 14
|
||||
exempt-issue-labels: 'pinned,security,tech debt'
|
||||
exempt-all-assignees: true
|
||||
|
|
@ -2,4 +2,4 @@
|
|||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
npm run --prefix komga-webui lint
|
||||
./gradlew ktlintFormat
|
||||
./gradlew ktlintCheck
|
||||
|
|
|
|||
|
|
@ -2,31 +2,15 @@
|
|||
<code_scheme name="Project" version="173">
|
||||
<JetCodeStyleSettings>
|
||||
<option name="PACKAGES_TO_USE_STAR_IMPORTS">
|
||||
<value>
|
||||
<package name="kotlinx.android.synthetic" alias="false" withSubpackages="true" />
|
||||
</value>
|
||||
<value />
|
||||
</option>
|
||||
<option name="NAME_COUNT_TO_USE_STAR_IMPORT" value="2147483647" />
|
||||
<option name="NAME_COUNT_TO_USE_STAR_IMPORT_FOR_MEMBERS" value="2147483647" />
|
||||
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||
</JetCodeStyleSettings>
|
||||
<codeStyleSettings language="XML">
|
||||
<indentOptions>
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="4" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
<codeStyleSettings language="kotlin">
|
||||
<option name="CODE_STYLE_DEFAULTS" value="KOTLIN_OFFICIAL" />
|
||||
<option name="LINE_COMMENT_AT_FIRST_COLUMN" value="false" />
|
||||
<option name="LINE_COMMENT_ADD_SPACE" value="true" />
|
||||
<option name="KEEP_BLANK_LINES_IN_DECLARATIONS" value="1" />
|
||||
<option name="KEEP_BLANK_LINES_IN_CODE" value="1" />
|
||||
<option name="KEEP_BLANK_LINES_BEFORE_RBRACE" value="0" />
|
||||
<option name="ALIGN_MULTILINE_PARAMETERS" value="false" />
|
||||
<indentOptions>
|
||||
<option name="INDENT_SIZE" value="2" />
|
||||
<option name="CONTINUATION_INDENT_SIZE" value="4" />
|
||||
</indentOptions>
|
||||
</codeStyleSettings>
|
||||
</code_scheme>
|
||||
</component>
|
||||
82
CHANGELOG.md
82
CHANGELOG.md
|
|
@ -1,3 +1,85 @@
|
|||
## [0.88.1](https://github.com/gotson/komga/compare/v0.88.0...v0.88.1) (2021-04-26)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* replace java webp library ([f658f9a](https://github.com/gotson/komga/commit/f658f9abe04896cbce6590bae0783836f4e59f74))
|
||||
|
||||
# [0.88.0](https://github.com/gotson/komga/compare/v0.87.5...v0.88.0) (2021-04-26)
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **webui:** ability to read incognito ([dc0cc13](https://github.com/gotson/komga/commit/dc0cc1380793bf870e11444debf1872a6cefc95c))
|
||||
|
||||
## [0.87.5](https://github.com/gotson/komga/compare/v0.87.4...v0.87.5) (2021-04-26)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **webui:** double pages could show duplicate pages ([76ba55a](https://github.com/gotson/komga/commit/76ba55a1242cfc90bceaddef8520719ac1165854))
|
||||
* **webui:** mark read progress instantly ([97cc3e0](https://github.com/gotson/komga/commit/97cc3e043d15255bc53641bdf0b5cd53cb5c14c4)), closes [#475](https://github.com/gotson/komga/issues/475)
|
||||
* **webui:** scroll continuous reader to top on book change ([d27828d](https://github.com/gotson/komga/commit/d27828de13fdbe348055fe4c23d129a29ad974e8)), closes [#475](https://github.com/gotson/komga/issues/475)
|
||||
|
||||
## [0.87.4](https://github.com/gotson/komga/compare/v0.87.3...v0.87.4) (2021-04-21)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **api:** filter referential data by access rights ([09fa5e9](https://github.com/gotson/komga/commit/09fa5e95a5a59f2136a7992468ca77a257a267a5)), closes [#492](https://github.com/gotson/komga/issues/492)
|
||||
|
||||
## [0.87.3](https://github.com/gotson/komga/compare/v0.87.2...v0.87.3) (2021-04-21)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* error importing reading list with non-numerical number ([3ef0240](https://github.com/gotson/komga/commit/3ef02409bac07ee64c0136e34aac6afcf7c1aba4)), closes [#499](https://github.com/gotson/komga/issues/499)
|
||||
* **webui:** stored filters are not validated ([fbb4171](https://github.com/gotson/komga/commit/fbb4171d9bd3727d3826e650fe97822ec146bd4d)), closes [#504](https://github.com/gotson/komga/issues/504)
|
||||
|
||||
## [0.87.2](https://github.com/gotson/komga/compare/v0.87.1...v0.87.2) (2021-04-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* translated using Weblate (Esperanto) ([19ff72f](https://github.com/gotson/komga/commit/19ff72f9e34fce599b3f9691dcaa78b8a21909ef))
|
||||
* **importer:** keep metadata when upgrading book ([a3b8866](https://github.com/gotson/komga/commit/a3b88667d0364edf68f5a906053af0977c176135))
|
||||
* prevent transient scanning of directories that are part of existing libraries ([8a92b84](https://github.com/gotson/komga/commit/8a92b84fd06c7b6b8ae3ab6975f7802708598de3))
|
||||
* **importer:** prevent import of files that are in an existing library ([b0170c7](https://github.com/gotson/komga/commit/b0170c7c8840089811108e275e76cf4bb0350d8b))
|
||||
|
||||
## [0.87.1](https://github.com/gotson/komga/compare/v0.87.0...v0.87.1) (2021-04-20)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **importer:** hardlink fails inside docker ([24cf181](https://github.com/gotson/komga/commit/24cf1819b02dd2625d7a6327a17ef2081cf52e64))
|
||||
|
||||
# [0.87.0](https://github.com/gotson/komga/compare/v0.86.0...v0.87.0) (2021-04-19)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* translated using Weblate (Esperanto) ([#506](https://github.com/gotson/komga/issues/506)) ([248f474](https://github.com/gotson/komga/commit/248f47458cb82d777e0f5823ddbe044ca76092c7))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* **api:** import books ([d41dcef](https://github.com/gotson/komga/commit/d41dcefd3efd4f9844d5b3b1d336a246c320a1ec))
|
||||
* **api:** support for transient books ([02b0893](https://github.com/gotson/komga/commit/02b08932babd27b5b309b3038279885ac65d0821))
|
||||
* **webui:** import books ([13b304d](https://github.com/gotson/komga/commit/13b304dd147f3102345c2edb85d41f87ccae1871))
|
||||
* added translation using Weblate (Esperanto, Polish) ([f3cc6f6](https://github.com/gotson/komga/commit/f3cc6f6e916862741cd7ff3aafa98a4c587653c6))
|
||||
|
||||
# [0.86.0](https://github.com/gotson/komga/compare/v0.85.1...v0.86.0) (2021-04-07)
|
||||
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **webui:** series year incorrectly formatted ([d166207](https://github.com/gotson/komga/commit/d16620791243201f2e2eb0910201f73e2c2975f7))
|
||||
|
||||
|
||||
### Features
|
||||
|
||||
* added translation using Weblate (Finnish) ([81142ab](https://github.com/gotson/komga/commit/81142ab570ea9ce1cfd964e7c3205d0c1a9ead7a))
|
||||
|
||||
## [0.85.1](https://github.com/gotson/komga/compare/v0.85.0...v0.85.1) (2021-03-31)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
|
||||
# Error codes
|
||||
|
||||
Code | Description
|
||||
|
|
@ -18,3 +19,5 @@ ERR_1012 | No match for series
|
|||
ERR_1013 | No unique match for book number within series
|
||||
ERR_1014 | No match for book number within series
|
||||
ERR_1015 | Error while deserializing ComicRack ReadingList
|
||||
ERR_1016 | Directory not accessible or not a directory
|
||||
ERR_1017 | Cannot scan folder that is part of an existing library
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
import com.github.benmanes.gradle.versions.updates.DependencyUpdatesTask
|
||||
|
||||
plugins {
|
||||
run {
|
||||
val kotlinVersion = "1.4.31"
|
||||
|
|
@ -9,12 +11,29 @@ plugins {
|
|||
id("com.github.ben-manes.versions") version "0.38.0"
|
||||
}
|
||||
|
||||
fun isNonStable(version: String): Boolean {
|
||||
val stableKeyword = listOf("RELEASE", "FINAL", "GA").any { version.toUpperCase().contains(it) }
|
||||
val unstableKeyword = listOf("ALPHA", "RC").any { version.toUpperCase().contains(it) }
|
||||
val regex = "^[0-9,.v-]+(-r)?$".toRegex()
|
||||
val isStable = stableKeyword || regex.matches(version)
|
||||
return unstableKeyword || !isStable
|
||||
}
|
||||
|
||||
allprojects {
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
apply(plugin = "org.jlleitschuh.gradle.ktlint")
|
||||
apply(plugin = "com.github.ben-manes.versions")
|
||||
|
||||
tasks.named<DependencyUpdatesTask>("dependencyUpdates").configure {
|
||||
// disallow release candidates as upgradable versions from stable versions
|
||||
rejectVersionIf {
|
||||
isNonStable(candidate.version) && !isNonStable(currentVersion)
|
||||
}
|
||||
gradleReleaseChannel = "current"
|
||||
checkConstraints = true
|
||||
}
|
||||
}
|
||||
|
||||
tasks.wrapper {
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
version=0.85.1
|
||||
version=0.88.1
|
||||
|
|
|
|||
262
komga-webui/src/components/FileImportRow.vue
Normal file
262
komga-webui/src/components/FileImportRow.vue
Normal file
|
|
@ -0,0 +1,262 @@
|
|||
<template>
|
||||
<tr v-if="book">
|
||||
<td>
|
||||
<slot></slot>
|
||||
</td>
|
||||
|
||||
<td>{{ book.name }}</td>
|
||||
|
||||
<!-- Status icon -->
|
||||
<td>
|
||||
<template v-if="bookAnalyzed">
|
||||
<v-tooltip bottom :disabled="!status.message">
|
||||
<template v-slot:activator="{ on }">
|
||||
<v-icon :color="status.color" v-on="on">{{ status.icon }}</v-icon>
|
||||
</template>
|
||||
{{ convertErrorCodes(status.message) }}
|
||||
</v-tooltip>
|
||||
</template>
|
||||
<v-progress-circular
|
||||
v-else
|
||||
indeterminate
|
||||
color="primary"
|
||||
:size="20"
|
||||
:width="2"
|
||||
/>
|
||||
</td>
|
||||
|
||||
<!-- Series picker -->
|
||||
<td @click="modalSeriesPicker = true" style="cursor: pointer">
|
||||
<template v-if="selectedSeries">{{ selectedSeries.metadata.title }}</template>
|
||||
<template v-else>
|
||||
<div style="height: 2em" class="missing"></div>
|
||||
</template>
|
||||
<series-picker-dialog v-model="modalSeriesPicker" :series.sync="selectedSeries"></series-picker-dialog>
|
||||
</td>
|
||||
|
||||
<!-- Book number chooser -->
|
||||
<td>
|
||||
<v-text-field v-model.number="bookNumber"
|
||||
type="number"
|
||||
step="0.1"
|
||||
dense
|
||||
:disabled="!selectedSeries"
|
||||
/>
|
||||
</td>
|
||||
|
||||
<!-- Book details -->
|
||||
<td class="px-1">
|
||||
<v-btn icon elevation="1" :disabled="!bookAnalyzed" @click="modalBookDetails = true">
|
||||
<v-icon v-if="bookToUpgrade">mdi-file-compare</v-icon>
|
||||
<v-icon v-else>mdi-book-information-variant</v-icon>
|
||||
</v-btn>
|
||||
<transient-book-details-dialog
|
||||
v-model="modalBookDetails"
|
||||
:left-book="bookAnalyzed"
|
||||
:right-book="bookToUpgrade"
|
||||
:right-pages="bookToUpgradePages"
|
||||
/>
|
||||
</td>
|
||||
|
||||
<!-- Book viewer -->
|
||||
<td class="px-1">
|
||||
<v-btn icon
|
||||
elevation="1"
|
||||
@click="modalViewer = true"
|
||||
:disabled="!bookAnalyzed || bookAnalyzed.status !== MediaStatus.READY"
|
||||
>
|
||||
<v-icon v-if="bookToUpgrade">mdi-compare</v-icon>
|
||||
<v-icon v-else>mdi-image</v-icon>
|
||||
</v-btn>
|
||||
<transient-book-viewer-dialog
|
||||
v-model="modalViewer"
|
||||
:left-pages="leftPagesWithUrl"
|
||||
:right-pages="rightPagesWithUrl"
|
||||
/>
|
||||
</td>
|
||||
|
||||
<!-- Destination name chooser -->
|
||||
<td class="px-1">
|
||||
<v-btn icon
|
||||
elevation="1"
|
||||
@click="modalNameChooser = true"
|
||||
:disabled="!bookAnalyzed || bookAnalyzed.status !== MediaStatus.READY"
|
||||
>
|
||||
<v-icon>mdi-format-color-text</v-icon>
|
||||
</v-btn>
|
||||
<file-name-chooser-dialog
|
||||
v-model="modalNameChooser"
|
||||
:books="seriesBooks"
|
||||
:name.sync="destinationName"
|
||||
:existing="book.name"
|
||||
/>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
{{ destinationName }}
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<template v-if="error">
|
||||
<v-tooltip bottom>
|
||||
<template v-slot:activator="{ on }">
|
||||
<v-icon color="error" v-on="on">mdi-alert-circle</v-icon>
|
||||
</template>
|
||||
{{ error }}
|
||||
</v-tooltip>
|
||||
</template>
|
||||
<template v-if="!error && bookToUpgrade">
|
||||
<v-tooltip bottom>
|
||||
<template v-slot:activator="{ on }">
|
||||
<v-icon color="warning" v-on="on">mdi-comment-alert</v-icon>
|
||||
</template>
|
||||
{{ $t('book_import.row.warning_upgrade') }}
|
||||
</v-tooltip>
|
||||
</template>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue, {PropType} from 'vue'
|
||||
import {MediaStatus} from "@/types/enum-books";
|
||||
import {SeriesDto} from "@/types/komga-series";
|
||||
import {TransientBookDto} from "@/types/komga-transientbooks";
|
||||
import {BookDto, BookImportDto, PageDto, PageDtoWithUrl} from "@/types/komga-books";
|
||||
import SeriesPickerDialog from "@/components/dialogs/SeriesPickerDialog.vue";
|
||||
import TransientBookDetailsDialog from "@/components/dialogs/TransientBookDetailsDialog.vue";
|
||||
import TransientBookViewerDialog from "@/components/dialogs/TransientBookViewerDialog.vue";
|
||||
import {bookPageUrl, transientBookPageUrl} from "@/functions/urls";
|
||||
import {convertErrorCodes} from "@/functions/error-codes";
|
||||
import FileNameChooserDialog from "@/components/dialogs/FileNameChooserDialog.vue";
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'FileImportRow',
|
||||
components: {SeriesPickerDialog, TransientBookDetailsDialog, TransientBookViewerDialog, FileNameChooserDialog},
|
||||
props: {
|
||||
book: {
|
||||
type: Object as PropType<TransientBookDto>,
|
||||
required: true,
|
||||
},
|
||||
series: {
|
||||
type: Object as PropType<SeriesDto>,
|
||||
required: false,
|
||||
},
|
||||
payload: {
|
||||
type: Object,
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
book: {
|
||||
handler(val) {
|
||||
this.analyze(val)
|
||||
this.destinationName = val.name
|
||||
},
|
||||
immediate: true,
|
||||
},
|
||||
selectedSeries: {
|
||||
handler(val) {
|
||||
this.getSeriesBooks(val)
|
||||
},
|
||||
immediate: true,
|
||||
},
|
||||
series: {
|
||||
handler(val) {
|
||||
if (val) this.selectedSeries = this.$_.cloneDeep(val)
|
||||
},
|
||||
immediate: true,
|
||||
},
|
||||
bookNumber: {
|
||||
handler(val) {
|
||||
this.checkForUpgrade(val)
|
||||
},
|
||||
immediate: true,
|
||||
},
|
||||
importPayload: {
|
||||
handler(val) {
|
||||
this.$emit("update:payload", val)
|
||||
},
|
||||
immediate: true,
|
||||
},
|
||||
},
|
||||
data: () => ({
|
||||
MediaStatus,
|
||||
convertErrorCodes,
|
||||
innerSelect: false,
|
||||
bookAnalyzed: undefined as unknown as TransientBookDto,
|
||||
selectedSeries: undefined as SeriesDto | undefined,
|
||||
seriesBooks: [] as BookDto[],
|
||||
bookToUpgrade: undefined as BookDto | undefined,
|
||||
bookToUpgradePages: [] as PageDto[],
|
||||
modalSeriesPicker: false,
|
||||
modalBookDetails: false,
|
||||
modalViewer: false,
|
||||
modalNameChooser: false,
|
||||
bookNumber: undefined as number | undefined,
|
||||
destinationName: '',
|
||||
}),
|
||||
computed: {
|
||||
leftPagesWithUrl(): PageDtoWithUrl[] {
|
||||
return this.bookAnalyzed ? this.bookAnalyzed.pages.map(p => ({
|
||||
...p,
|
||||
url: transientBookPageUrl(this.bookAnalyzed.id, p.number),
|
||||
})) : []
|
||||
},
|
||||
rightPagesWithUrl(): PageDtoWithUrl[] {
|
||||
return this.bookToUpgrade ? this.bookToUpgradePages.map(p => ({
|
||||
...p,
|
||||
url: bookPageUrl(this.bookToUpgrade!!.id, p.number),
|
||||
})) : []
|
||||
},
|
||||
status(): object {
|
||||
if (!this.bookAnalyzed) return {}
|
||||
switch (this.bookAnalyzed.status) {
|
||||
case MediaStatus.READY:
|
||||
return {icon: 'mdi-check-circle', color: 'success', message: ''}
|
||||
default:
|
||||
return {icon: 'mdi-alert-circle', color: 'error', message: this.bookAnalyzed.comment}
|
||||
}
|
||||
},
|
||||
existingFileNames(): string[] {
|
||||
return this.seriesBooks.map(x => x.name)
|
||||
},
|
||||
error(): string {
|
||||
if (!this.bookAnalyzed) return this.$t('book_import.row.error_analyze_first').toString()
|
||||
if (this.bookAnalyzed.status != MediaStatus.READY) return this.$t('book_import.row.error_only_import_no_errors').toString()
|
||||
if (!this.selectedSeries) return this.$t('book_import.row.error_choose_series').toString()
|
||||
return ''
|
||||
},
|
||||
importPayload(): BookImportDto | undefined {
|
||||
if (this.error || !this.selectedSeries) return undefined
|
||||
return {
|
||||
seriesId: this.selectedSeries?.id,
|
||||
sourceFile: this.book.url,
|
||||
upgradeBookId: this.bookToUpgrade?.id,
|
||||
destinationName: this.destinationName,
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async analyze(book: TransientBookDto) {
|
||||
this.bookAnalyzed = await this.$komgaTransientBooks.analyze(book.id)
|
||||
},
|
||||
async getSeriesBooks(series: SeriesDto) {
|
||||
if (series) {
|
||||
this.seriesBooks = (await this.$komgaSeries.getBooks(series.id, {unpaged: true})).content
|
||||
this.checkForUpgrade(this.bookNumber)
|
||||
}
|
||||
},
|
||||
async checkForUpgrade(number: number | undefined) {
|
||||
this.bookToUpgrade = this.seriesBooks.find(b => b.metadata.numberSort === number)
|
||||
if (this.bookToUpgrade) this.bookToUpgradePages = await this.$komgaBooks.getBookPages(this.bookToUpgrade.id)
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.missing {
|
||||
border: 2px dashed red;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -7,14 +7,12 @@
|
|||
<v-btn icon
|
||||
:disabled="!canScrollBackward"
|
||||
@click="doScroll('backward')">
|
||||
<v-icon v-if="$vuetify.rtl">mdi-chevron-right</v-icon>
|
||||
<v-icon v-else>mdi-chevron-left</v-icon>
|
||||
<rtl-icon icon="mdi-chevron-left" rtl="mdi-chevron-right"/>
|
||||
</v-btn>
|
||||
<v-btn icon
|
||||
:disabled="!canScrollForward"
|
||||
@click="doScroll('forward')">
|
||||
<v-icon v-if="$vuetify.rtl">mdi-chevron-left</v-icon>
|
||||
<v-icon v-else>mdi-chevron-right</v-icon>
|
||||
<rtl-icon icon="mdi-chevron-right" rtl="mdi-chevron-left"/>
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
|
|
@ -31,9 +29,11 @@
|
|||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue'
|
||||
import RtlIcon from "@/components/RtlIcon.vue";
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'HorizontalScroller',
|
||||
components: {RtlIcon},
|
||||
data: function () {
|
||||
const uniqueId = this.$_.uniqueId()
|
||||
return {
|
||||
|
|
@ -65,7 +65,7 @@ export default Vue.extend({
|
|||
let increment = (this.container.clientWidth - this.adjustment)
|
||||
let scrollLeft = Math.round(this.container.scrollLeft)
|
||||
let target
|
||||
if (this.$vuetify.rtl){
|
||||
if (this.$vuetify.rtl) {
|
||||
if (direction === 'backward')
|
||||
target = scrollLeft + increment
|
||||
else
|
||||
|
|
|
|||
69
komga-webui/src/components/PagesTable.vue
Normal file
69
komga-webui/src/components/PagesTable.vue
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
<template>
|
||||
<v-simple-table v-if="leftPages.length > 0">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ $t('dialog.transient_book_details.pages_table.index') }}</th>
|
||||
<th>{{ $t('dialog.transient_book_details.pages_table.filename') }}</th>
|
||||
<th>{{ $t('dialog.transient_book_details.pages_table.media_type') }}</th>
|
||||
<th>{{ $t('dialog.transient_book_details.pages_table.width') }}</th>
|
||||
<th>{{ $t('dialog.transient_book_details.pages_table.height') }}</th>
|
||||
<template v-if="rightPages.length > 0">
|
||||
<th>{{ $t('dialog.transient_book_details.pages_table.filename') }}</th>
|
||||
<th>{{ $t('dialog.transient_book_details.pages_table.media_type') }}</th>
|
||||
<th>{{ $t('dialog.transient_book_details.pages_table.width') }}</th>
|
||||
<th>{{ $t('dialog.transient_book_details.pages_table.height') }}</th>
|
||||
</template>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr
|
||||
v-for="(n, i) in numberRows"
|
||||
:key="i"
|
||||
>
|
||||
<td>{{ n }}</td>
|
||||
<td>{{ $_.get(leftPages[n-1], 'fileName', '') }}</td>
|
||||
<td>{{ $_.get(leftPages[n-1], 'mediaType', '') }}</td>
|
||||
<td>{{ $_.get(leftPages[n-1], 'width', '') }}</td>
|
||||
<td>{{ $_.get(leftPages[n-1], 'height', '') }}</td>
|
||||
<template v-if="rightPages.length > 0">
|
||||
<td>{{ $_.get(rightPages[n-1], 'fileName', '') }}</td>
|
||||
<td>{{ $_.get(rightPages[n-1], 'mediaType', '') }}</td>
|
||||
<td>{{ $_.get(rightPages[n-1], 'width', '') }}</td>
|
||||
<td>{{ $_.get(rightPages[n-1], 'height', '') }}</td>
|
||||
</template>
|
||||
</tr>
|
||||
</tbody>
|
||||
</v-simple-table>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue, {PropType} from 'vue'
|
||||
import {PageDto} from "@/types/komga-books";
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'PagesTable',
|
||||
props: {
|
||||
leftPages: {
|
||||
type: Array as PropType<PageDto[]>,
|
||||
default: [],
|
||||
},
|
||||
rightPages: {
|
||||
type: Array as PropType<PageDto[]>,
|
||||
default: [],
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
numberRows(): number {
|
||||
if(this.leftPages) {
|
||||
if(this.rightPages) return Math.max(this.leftPages.length, this.rightPages.length)
|
||||
return this.leftPages.length
|
||||
}
|
||||
return 0
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
26
komga-webui/src/components/RtlIcon.vue
Normal file
26
komga-webui/src/components/RtlIcon.vue
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
<template>
|
||||
<v-icon v-if="$vuetify.rtl">{{ rtl }}</v-icon>
|
||||
<v-icon v-else>{{ icon }}</v-icon>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue'
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'RtlIcon',
|
||||
props: {
|
||||
icon: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
rtl: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
134
komga-webui/src/components/dialogs/FileNameChooserDialog.vue
Normal file
134
komga-webui/src/components/dialogs/FileNameChooserDialog.vue
Normal file
|
|
@ -0,0 +1,134 @@
|
|||
<template>
|
||||
<div>
|
||||
<v-dialog v-model="modal"
|
||||
max-width="600"
|
||||
scrollable
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title>{{ $t('dialog.filename_chooser.title') }}</v-card-title>
|
||||
<v-btn icon absolute top right @click="dialogClose">
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<v-divider/>
|
||||
|
||||
<v-card-text style="height: 50%">
|
||||
<v-container fluid>
|
||||
|
||||
<v-row>
|
||||
<v-col>
|
||||
<div class="text-subtitle-1">{{ $t('dialog.filename_chooser.label_source_filename') }}</div>
|
||||
<div style="cursor: pointer" @click="change(existing)">{{ existing }}</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row align="center">
|
||||
<v-col>
|
||||
<v-text-field
|
||||
v-model="nameInternal"
|
||||
autofocus
|
||||
:label="$t('dialog.filename_chooser.field_destination_filename')"
|
||||
@keydown.enter="choose"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col cols="auto">
|
||||
<v-btn @click="choose"
|
||||
:disabled="!nameInternal"
|
||||
>{{ $t('dialog.filename_chooser.button_choose') }}</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-divider/>
|
||||
|
||||
<v-simple-table
|
||||
v-if="books.length > 0"
|
||||
fixed-header
|
||||
:height="$vuetify.breakpoint.height / 2"
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ $t('dialog.filename_chooser.table.order') }}</th>
|
||||
<th>{{ $t('dialog.filename_chooser.table.existing_file') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="(b, index) in books"
|
||||
:key="index"
|
||||
>
|
||||
<td>{{ b.number }}</td>
|
||||
<td style="cursor: pointer" @click="change(b.name)">{{ b.name }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</v-simple-table>
|
||||
|
||||
</v-container>
|
||||
</v-card-text>
|
||||
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue'
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'FileNameChooserDialog',
|
||||
data: () => {
|
||||
return {
|
||||
modal: false,
|
||||
nameInternal: '',
|
||||
}
|
||||
},
|
||||
props: {
|
||||
value: Boolean,
|
||||
name: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
existing: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
books: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
value(val) {
|
||||
this.modal = val
|
||||
if (val) {
|
||||
this.clear()
|
||||
}
|
||||
},
|
||||
existing: {
|
||||
handler(val) {
|
||||
this.nameInternal = val
|
||||
},
|
||||
immediate: true,
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
clear() {
|
||||
this.nameInternal = this.name || this.existing || ''
|
||||
},
|
||||
change(val: string) {
|
||||
this.nameInternal = val
|
||||
},
|
||||
choose(){
|
||||
if(this.nameInternal) {
|
||||
this.$emit('update:name', this.nameInternal)
|
||||
this.dialogClose()
|
||||
}
|
||||
},
|
||||
dialogClose() {
|
||||
this.$emit('input', false)
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
127
komga-webui/src/components/dialogs/SeriesPickerDialog.vue
Normal file
127
komga-webui/src/components/dialogs/SeriesPickerDialog.vue
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
<template>
|
||||
<div>
|
||||
<v-dialog v-model="modal"
|
||||
max-width="450"
|
||||
scrollable
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title>{{ $t('dialog.series_picker.title') }}</v-card-title>
|
||||
<v-btn icon absolute top right @click="dialogClose">
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<v-divider/>
|
||||
|
||||
<v-card-text style="height: 50%">
|
||||
<v-container fluid>
|
||||
|
||||
<v-row align="center">
|
||||
<v-col>
|
||||
<v-text-field
|
||||
v-model="search"
|
||||
autofocus
|
||||
:label="$t('dialog.series_picker.label_search_series')"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-divider/>
|
||||
|
||||
<v-row v-if="results">
|
||||
<v-col>
|
||||
<v-list elevation="5" v-if="results.length > 0">
|
||||
<div v-for="(s, index) in results"
|
||||
:key="index"
|
||||
>
|
||||
<v-list-item @click="select(s)">
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>{{ s.metadata.title }}</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
<v-divider v-if="index !== results.length-1"/>
|
||||
</div>
|
||||
</v-list>
|
||||
|
||||
<v-alert
|
||||
v-if="results.length === 0 && showResults"
|
||||
type="info"
|
||||
text
|
||||
>No Series found
|
||||
</v-alert>
|
||||
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
</v-container>
|
||||
</v-card-text>
|
||||
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue, {PropType} from 'vue'
|
||||
import {SeriesDto} from "@/types/komga-series"
|
||||
import {debounce} from 'lodash'
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'SeriesPickerDialog',
|
||||
data: () => {
|
||||
return {
|
||||
modal: false,
|
||||
results: [] as SeriesDto[],
|
||||
search: '',
|
||||
showResults: false,
|
||||
}
|
||||
},
|
||||
props: {
|
||||
value: Boolean,
|
||||
series: {
|
||||
type: Object as PropType<SeriesDto>,
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
value(val) {
|
||||
this.modal = val
|
||||
if (val) {
|
||||
this.clear()
|
||||
}
|
||||
},
|
||||
search(val) {
|
||||
this.searchItems(val)
|
||||
},
|
||||
modal(val) {
|
||||
!val && this.dialogClose()
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
searchItems: debounce(async function (this: any, query: string) {
|
||||
if (query) {
|
||||
this.showResults = false
|
||||
this.results = (await this.$komgaSeries.getSeries(undefined, {unpaged: true}, query)).content
|
||||
this.showResults = true
|
||||
} else {
|
||||
this.clear()
|
||||
}
|
||||
}, 500),
|
||||
clear() {
|
||||
this.search = ''
|
||||
this.showResults = false
|
||||
this.results = []
|
||||
},
|
||||
select(s: SeriesDto) {
|
||||
this.$emit("update:series", s)
|
||||
this.dialogClose()
|
||||
},
|
||||
dialogClose() {
|
||||
this.$emit('input', false)
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
|
|
@ -0,0 +1,117 @@
|
|||
<template>
|
||||
<div v-if="leftBook">
|
||||
<v-dialog v-model="modal"
|
||||
scrollable
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title v-if="!rightBook">{{ $t('dialog.transient_book_details.title') }}</v-card-title>
|
||||
<v-card-title v-else>{{ $t('dialog.transient_book_details.title_comparison') }}</v-card-title>
|
||||
<v-btn icon absolute top right @click="dialogClose">
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<v-divider/>
|
||||
|
||||
<v-card-text style="height: 50%">
|
||||
<v-container fluid>
|
||||
|
||||
<v-simple-table class="body-2">
|
||||
<thead v-if="rightBook">
|
||||
<tr>
|
||||
<th></th>
|
||||
<th>{{ $t('dialog.transient_book_details.label_candidate') }}</th>
|
||||
<th>{{ $t('dialog.transient_book_details.label_existing') }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="font-weight-medium">{{ $t('dialog.transient_book_details.label_name') }}</td>
|
||||
<td>{{ leftBook.name }}</td>
|
||||
<td v-if="rightBook">{{ rightBook.metadata.title }}</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td class="font-weight-medium">{{ $t('dialog.transient_book_details.label_size') }}</td>
|
||||
<td>{{ leftBook.size }}</td>
|
||||
<td v-if="rightBook">{{ rightBook.size }}</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td class="font-weight-medium">{{ $t('dialog.transient_book_details.label_format') }}</td>
|
||||
<td>{{ getBookFormatFromMediaType(leftBook.mediaType).type }}</td>
|
||||
<td v-if="rightBook">{{ getBookFormatFromMediaType(rightBook.media.mediaType).type }}</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td class="font-weight-medium">{{ $t('dialog.transient_book_details.label_pages') }}</td>
|
||||
<td>{{ leftBook.pages.length }}</td>
|
||||
<td v-if="rightBook">{{ rightBook.media.pagesCount }}</td>
|
||||
</tr>
|
||||
|
||||
<tr>
|
||||
<td :colspan="rightBook ? 3 : 2" class="pa-0">
|
||||
<pages-table :left-pages="leftBook.pages" :right-pages="rightPages"></pages-table>
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
</tbody>
|
||||
</v-simple-table>
|
||||
|
||||
</v-container>
|
||||
</v-card-text>
|
||||
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue, {PropType} from 'vue'
|
||||
import {TransientBookDto} from "@/types/komga-transientbooks";
|
||||
import {BookDto, PageDto} from "@/types/komga-books";
|
||||
import {getBookFormatFromMediaType} from "@/functions/book-format";
|
||||
import PagesTable from "@/components/PagesTable.vue";
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'TransientBookDetailsDialog',
|
||||
components: {PagesTable},
|
||||
data: () => {
|
||||
return {
|
||||
modal: false,
|
||||
getBookFormatFromMediaType,
|
||||
}
|
||||
},
|
||||
props: {
|
||||
value: Boolean,
|
||||
leftBook: {
|
||||
type: Object as PropType<TransientBookDto>,
|
||||
required: false,
|
||||
},
|
||||
rightBook: {
|
||||
type: Object as PropType<BookDto>,
|
||||
required: false,
|
||||
},
|
||||
rightPages: {
|
||||
type: Array as PropType<PageDto[]>,
|
||||
required: false,
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
async value(val) {
|
||||
this.modal = val
|
||||
},
|
||||
modal(val) {
|
||||
!val && this.dialogClose()
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
dialogClose() {
|
||||
this.$emit('input', false)
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
228
komga-webui/src/components/dialogs/TransientBookViewerDialog.vue
Normal file
228
komga-webui/src/components/dialogs/TransientBookViewerDialog.vue
Normal file
|
|
@ -0,0 +1,228 @@
|
|||
<template>
|
||||
<div>
|
||||
<v-dialog v-model="modal"
|
||||
scrollable
|
||||
fullscreen
|
||||
>
|
||||
<v-card>
|
||||
<v-card-title v-if="single">{{ $t('dialog.transient_book_viewer.title') }}</v-card-title>
|
||||
<v-card-title v-else>{{ $t('dialog.transient_book_viewer.title_comparison') }}</v-card-title>
|
||||
<v-btn icon absolute top right @click="dialogClose">
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<v-divider/>
|
||||
|
||||
<v-card-text style="height: 50%">
|
||||
<v-container fluid class="pa-0">
|
||||
|
||||
<v-row justify="space-around" v-if="!single">
|
||||
<v-col cols="auto" class="pa-1">{{ $t('dialog.transient_book_viewer.label_candidate') }}</v-col>
|
||||
<v-col cols="auto" class="pa-1">{{ $t('dialog.transient_book_viewer.label_existing') }}</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row justify="space-around">
|
||||
<v-col cols="auto" class="pa-1">
|
||||
<v-btn icon :disabled="!canPrevLeft" @click="firstPageLeft">
|
||||
<rtl-icon icon="mdi-chevron-double-left" rtl="mdi-chevron-double-right"/>
|
||||
</v-btn>
|
||||
<v-btn icon :disabled="!canPrevLeft" @click="previousPageLeft">
|
||||
<rtl-icon icon="mdi-chevron-left" rtl="mdi-chevron-right"/>
|
||||
</v-btn>
|
||||
{{ $t('dialog.transient_book_viewer.page_of_pages', {page: leftPageNumber, pages: leftPages.length}) }}
|
||||
<v-btn icon :disabled="!canNextLeft" @click="nextPageLeft">
|
||||
<rtl-icon icon="mdi-chevron-right" rtl="mdi-chevron-left"/>
|
||||
</v-btn>
|
||||
<v-btn icon :disabled="!canNextLeft" @click="lastPageLeft">
|
||||
<rtl-icon icon="mdi-chevron-double-right" rtl="mdi-chevron-double-left"/>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
|
||||
|
||||
<v-col cols="auto" class="pa-1" v-if="!single">
|
||||
<v-btn icon :disabled="!canPrevAny" @click="firstPageBoth">
|
||||
<rtl-icon icon="mdi-chevron-double-left" rtl="mdi-chevron-double-right"/>
|
||||
</v-btn>
|
||||
<v-btn icon :disabled="!canPrevAny" @click="previousPageBoth">
|
||||
<rtl-icon icon="mdi-chevron-left" rtl="mdi-chevron-right"/>
|
||||
</v-btn>
|
||||
<v-btn icon :disabled="!canNextAny" @click="nextPageBoth">
|
||||
<rtl-icon icon="mdi-chevron-right" rtl="mdi-chevron-left"/>
|
||||
</v-btn>
|
||||
<v-btn icon :disabled="!canNextAny" @click="lastPageBoth">
|
||||
<rtl-icon icon="mdi-chevron-double-right" rtl="mdi-chevron-double-left"/>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="auto" class="pa-1" v-if="!single">
|
||||
<v-btn icon :disabled="!canPrevRight" @click="firstPageRight">
|
||||
<rtl-icon icon="mdi-chevron-double-left" rtl="mdi-chevron-double-right"/>
|
||||
</v-btn>
|
||||
<v-btn icon :disabled="!canPrevRight" @click="previousPageRight">
|
||||
<rtl-icon icon="mdi-chevron-left" rtl="mdi-chevron-right"/>
|
||||
</v-btn>
|
||||
{{ $t('dialog.transient_book_viewer.page_of_pages', {page: rightPageNumber, pages: rightPages.length}) }}
|
||||
<v-btn icon :disabled="!canNextRight" @click="nextPageRight">
|
||||
<rtl-icon icon="mdi-chevron-right" rtl="mdi-chevron-left"/>
|
||||
</v-btn>
|
||||
<v-btn icon :disabled="!canNextRight" @click="lastPageRight">
|
||||
<rtl-icon icon="mdi-chevron-double-right" rtl="mdi-chevron-double-left"/>
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row justify="center">
|
||||
<v-col cols="6" class="pa-0">
|
||||
<v-img
|
||||
:src="$_.get(pageLeft, 'url', '')"
|
||||
contain
|
||||
:max-height="$vuetify.breakpoint.height * .75"
|
||||
>
|
||||
</v-img>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="6" class="pa-0" v-if="!single">
|
||||
<v-img
|
||||
:src="$_.get(pageRight, 'url', '')"
|
||||
:max-height="$vuetify.breakpoint.height * .75"
|
||||
contain
|
||||
>
|
||||
</v-img>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<v-row justify="space-around">
|
||||
<v-col cols="auto" class="pa-1">
|
||||
w: {{ $_.get(pageLeft, 'width', '') }} h: {{ $_.get(pageLeft, 'height', '') }}
|
||||
{{ $_.get(pageLeft, 'mediaType', '') }}
|
||||
</v-col>
|
||||
<v-col cols="auto" class="pa-1" v-if="!single">
|
||||
w: {{ $_.get(pageRight, 'width', '') }} h: {{ $_.get(pageRight, 'height', '') }}
|
||||
{{ $_.get(pageRight, 'mediaType', '') }}
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
</v-container>
|
||||
</v-card-text>
|
||||
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue, {PropType} from 'vue'
|
||||
import {PageDtoWithUrl} from "@/types/komga-books"
|
||||
import RtlIcon from "@/components/RtlIcon.vue";
|
||||
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'TransientBookViewerDialog',
|
||||
components: {RtlIcon},
|
||||
data: () => {
|
||||
return {
|
||||
modal: false,
|
||||
leftPageNumber: 1,
|
||||
rightPageNumber: 1,
|
||||
}
|
||||
},
|
||||
props: {
|
||||
value: Boolean,
|
||||
leftPages: {
|
||||
type: Array as PropType<PageDtoWithUrl[]>,
|
||||
default: [],
|
||||
},
|
||||
rightPages: {
|
||||
type: Array as PropType<PageDtoWithUrl[]>,
|
||||
default: [],
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
async value(val) {
|
||||
this.modal = val
|
||||
if (val) this.firstPageBoth()
|
||||
},
|
||||
modal(val) {
|
||||
!val && this.dialogClose()
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
pageLeft(): PageDtoWithUrl {
|
||||
return this.leftPages[this.leftPageNumber - 1]
|
||||
},
|
||||
pageRight(): PageDtoWithUrl {
|
||||
return this.rightPages[this.rightPageNumber - 1]
|
||||
},
|
||||
canPrevLeft(): boolean {
|
||||
return this.leftPageNumber > 1
|
||||
},
|
||||
canPrevRight(): boolean {
|
||||
return this.rightPageNumber > 1
|
||||
},
|
||||
canPrevAny(): boolean {
|
||||
return this.canPrevLeft || this.canPrevRight
|
||||
},
|
||||
canNextLeft(): boolean {
|
||||
return this.leftPageNumber < this.leftPages.length
|
||||
},
|
||||
canNextRight(): boolean {
|
||||
return this.rightPageNumber < this.rightPages.length
|
||||
},
|
||||
canNextAny(): boolean {
|
||||
return this.canNextLeft || this.canNextRight
|
||||
},
|
||||
single(): boolean {
|
||||
return this.rightPages.length === 0
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
firstPageLeft() {
|
||||
this.leftPageNumber = 1
|
||||
},
|
||||
firstPageRight() {
|
||||
this.rightPageNumber = 1
|
||||
},
|
||||
firstPageBoth() {
|
||||
this.firstPageLeft()
|
||||
this.firstPageRight()
|
||||
},
|
||||
previousPageLeft() {
|
||||
if (this.canPrevLeft) this.leftPageNumber--
|
||||
},
|
||||
previousPageRight() {
|
||||
if (this.canPrevRight) this.rightPageNumber--
|
||||
},
|
||||
nextPageLeft() {
|
||||
if (this.canNextLeft) this.leftPageNumber++
|
||||
},
|
||||
nextPageRight() {
|
||||
if (this.canNextRight) this.rightPageNumber++
|
||||
},
|
||||
previousPageBoth() {
|
||||
this.previousPageLeft()
|
||||
this.previousPageRight()
|
||||
},
|
||||
nextPageBoth() {
|
||||
this.nextPageLeft()
|
||||
this.nextPageRight()
|
||||
},
|
||||
lastPageLeft() {
|
||||
this.leftPageNumber = this.leftPages.length
|
||||
},
|
||||
lastPageRight() {
|
||||
this.rightPageNumber = this.rightPages.length
|
||||
},
|
||||
lastPageBoth() {
|
||||
this.lastPageLeft()
|
||||
this.lastPageRight()
|
||||
},
|
||||
dialogClose() {
|
||||
this.$emit('input', false)
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
|
|
@ -37,8 +37,8 @@
|
|||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue'
|
||||
import { ContinuousScaleType } from '@/types/enum-reader'
|
||||
import { PageDtoWithUrl } from '@/types/komga-books'
|
||||
import {ContinuousScaleType} from '@/types/enum-reader'
|
||||
import {PageDtoWithUrl} from '@/types/komga-books'
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'ContinuousReader',
|
||||
|
|
@ -74,8 +74,9 @@ export default Vue.extend({
|
|||
},
|
||||
watch: {
|
||||
pages: {
|
||||
handler (val) {
|
||||
handler(val) {
|
||||
this.seen = new Array(val.length).fill(false)
|
||||
if (this.page === 1) window.scrollTo(0, 0)
|
||||
},
|
||||
immediate: true,
|
||||
},
|
||||
|
|
@ -90,7 +91,7 @@ export default Vue.extend({
|
|||
immediate: false,
|
||||
},
|
||||
},
|
||||
mounted () {
|
||||
mounted() {
|
||||
if (this.page != this.currentPage) {
|
||||
this.$vuetify.goTo(`#page${this.page}`, {
|
||||
duration: 0,
|
||||
|
|
@ -98,26 +99,26 @@ export default Vue.extend({
|
|||
}
|
||||
},
|
||||
computed: {
|
||||
canPrev (): boolean {
|
||||
canPrev(): boolean {
|
||||
return this.offsetTop > 0
|
||||
},
|
||||
canNext (): boolean {
|
||||
canNext(): boolean {
|
||||
return this.offsetTop + this.$vuetify.breakpoint.height < this.totalHeight
|
||||
},
|
||||
goToOptions (): object | undefined {
|
||||
goToOptions(): object | undefined {
|
||||
if (this.animations) return undefined
|
||||
return { duration: 0 }
|
||||
return {duration: 0}
|
||||
},
|
||||
totalSidePadding (): number {
|
||||
totalSidePadding(): number {
|
||||
return this.sidePadding * 2
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onScroll (e: any) {
|
||||
onScroll(e: any) {
|
||||
this.offsetTop = e.target.scrollingElement.scrollTop
|
||||
this.totalHeight = e.target.scrollingElement.scrollHeight
|
||||
},
|
||||
onIntersect (entries: any) {
|
||||
onIntersect(entries: any) {
|
||||
if (entries[0].isIntersecting) {
|
||||
const page = parseInt(entries[0].target.id.replace('page', ''))
|
||||
this.seen.splice(page - 1, 1, true)
|
||||
|
|
@ -125,10 +126,10 @@ export default Vue.extend({
|
|||
this.$emit('update:page', page)
|
||||
}
|
||||
},
|
||||
shouldLoad (page: number): boolean {
|
||||
shouldLoad(page: number): boolean {
|
||||
return page == 0 || this.seen[page] || Math.abs((this.currentPage - 1) - page) <= 2
|
||||
},
|
||||
calcHeight (page: PageDtoWithUrl): number | undefined {
|
||||
calcHeight(page: PageDtoWithUrl): number | undefined {
|
||||
switch (this.scale) {
|
||||
case ContinuousScaleType.WIDTH:
|
||||
if (page.height && page.width)
|
||||
|
|
@ -140,7 +141,7 @@ export default Vue.extend({
|
|||
return undefined
|
||||
}
|
||||
},
|
||||
calcWidth (page: PageDtoWithUrl): number | undefined {
|
||||
calcWidth(page: PageDtoWithUrl): number | undefined {
|
||||
switch (this.scale) {
|
||||
case ContinuousScaleType.WIDTH:
|
||||
return this.$vuetify.breakpoint.width - (this.$vuetify.breakpoint.width * this.totalSidePadding) / 100
|
||||
|
|
@ -150,10 +151,10 @@ export default Vue.extend({
|
|||
return undefined
|
||||
}
|
||||
},
|
||||
centerClick () {
|
||||
centerClick() {
|
||||
this.$emit('menu')
|
||||
},
|
||||
prev () {
|
||||
prev() {
|
||||
if (this.canPrev) {
|
||||
const step = this.$vuetify.breakpoint.height * 0.95
|
||||
this.$vuetify.goTo(this.offsetTop - step, this.goToOptions)
|
||||
|
|
@ -161,7 +162,7 @@ export default Vue.extend({
|
|||
this.$emit('jump-previous')
|
||||
}
|
||||
},
|
||||
next () {
|
||||
next() {
|
||||
if (this.canNext) {
|
||||
const step = this.$vuetify.breakpoint.height * 0.95
|
||||
this.$vuetify.goTo(this.offsetTop + step, this.goToOptions)
|
||||
|
|
|
|||
|
|
@ -75,12 +75,12 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { isPageLandscape } from '@/functions/page'
|
||||
import Vue from 'vue'
|
||||
import { ReadingDirection } from '@/types/enum-books'
|
||||
import { PagedReaderLayout, ScaleType } from '@/types/enum-reader'
|
||||
import { shortcutsLTR, shortcutsRTL, shortcutsVertical } from '@/functions/shortcuts/paged-reader'
|
||||
import { PageDtoWithUrl } from '@/types/komga-books'
|
||||
import {ReadingDirection} from '@/types/enum-books'
|
||||
import {PagedReaderLayout, ScaleType} from '@/types/enum-reader'
|
||||
import {shortcutsLTR, shortcutsRTL, shortcutsVertical} from '@/functions/shortcuts/paged-reader'
|
||||
import {PageDtoWithUrl} from '@/types/komga-books'
|
||||
import {buildSpreads} from "@/functions/book-spreads";
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'PagedReader',
|
||||
|
|
@ -122,8 +122,8 @@ export default Vue.extend({
|
|||
},
|
||||
watch: {
|
||||
pages: {
|
||||
handler () {
|
||||
this.spreads = this.buildSpreads()
|
||||
handler (val) {
|
||||
this.spreads = buildSpreads(val, this.pageLayout)
|
||||
},
|
||||
immediate: true,
|
||||
},
|
||||
|
|
@ -134,9 +134,9 @@ export default Vue.extend({
|
|||
this.carouselPage = this.toSpreadIndex(val)
|
||||
},
|
||||
pageLayout: {
|
||||
handler () {
|
||||
handler (val) {
|
||||
const current = this.page
|
||||
this.spreads = this.buildSpreads()
|
||||
this.spreads = buildSpreads(this.pages, val)
|
||||
this.carouselPage = this.toSpreadIndex(current)
|
||||
},
|
||||
immediate: true,
|
||||
|
|
@ -196,43 +196,6 @@ export default Vue.extend({
|
|||
keyPressed (e: KeyboardEvent) {
|
||||
this.shortcuts[e.key]?.execute(this)
|
||||
},
|
||||
buildSpreads (): PageDtoWithUrl[][] {
|
||||
if (this.pages.length === 0) return []
|
||||
if (this.isDoublePages) {
|
||||
const spreads = []
|
||||
let pages: PageDtoWithUrl[]
|
||||
if (this.pageLayout === PagedReaderLayout.DOUBLE_PAGES) {
|
||||
spreads.push([this.pages[0]])
|
||||
pages = this.$_.drop(this.$_.dropRight(this.pages))
|
||||
} else {
|
||||
pages = this.$_.cloneDeep(this.pages)
|
||||
}
|
||||
while (pages.length > 0) {
|
||||
const p = pages.shift() as PageDtoWithUrl
|
||||
if (isPageLandscape(p)) {
|
||||
spreads.push([p])
|
||||
} else {
|
||||
if (pages.length > 0) {
|
||||
const p2 = pages.shift() as PageDtoWithUrl
|
||||
if (isPageLandscape(p2)) {
|
||||
spreads.push([p])
|
||||
spreads.push([p2])
|
||||
} else {
|
||||
spreads.push([p, p2])
|
||||
}
|
||||
} else {
|
||||
spreads.push([p])
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this.pageLayout === PagedReaderLayout.DOUBLE_PAGES) {
|
||||
spreads.push([this.pages[this.pages.length - 1]])
|
||||
}
|
||||
return spreads
|
||||
} else {
|
||||
return this.pages.map(p => [p])
|
||||
}
|
||||
},
|
||||
imgClass (spread: PageDtoWithUrl[]): string {
|
||||
const double = spread.length > 1
|
||||
switch (this.scale) {
|
||||
|
|
|
|||
43
komga-webui/src/functions/book-spreads.ts
Normal file
43
komga-webui/src/functions/book-spreads.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import {PageDtoWithUrl} from "@/types/komga-books"
|
||||
import {PagedReaderLayout} from "@/types/enum-reader"
|
||||
import {isPageLandscape} from "@/functions/page"
|
||||
import {cloneDeep, drop, dropRight} from 'lodash'
|
||||
|
||||
|
||||
export function buildSpreads(pages: PageDtoWithUrl[], pageLayout: PagedReaderLayout): PageDtoWithUrl[][] {
|
||||
if (pages.length === 0) return []
|
||||
if (pageLayout !== PagedReaderLayout.SINGLE_PAGE) {
|
||||
const spreads = []
|
||||
let pagesClone: PageDtoWithUrl[]
|
||||
let lastPage = undefined
|
||||
if (pageLayout === PagedReaderLayout.DOUBLE_PAGES) {
|
||||
spreads.push([pages[0]])
|
||||
pagesClone = drop(pages)
|
||||
if (pagesClone.length > 0) lastPage = dropRight(pagesClone)
|
||||
} else {
|
||||
pagesClone = cloneDeep(pages)
|
||||
}
|
||||
while (pagesClone.length > 0) {
|
||||
const p = pagesClone.shift() as PageDtoWithUrl
|
||||
if (isPageLandscape(p)) {
|
||||
spreads.push([p])
|
||||
} else {
|
||||
if (pagesClone.length > 0) {
|
||||
const p2 = pagesClone.shift() as PageDtoWithUrl
|
||||
if (isPageLandscape(p2)) {
|
||||
spreads.push([p])
|
||||
spreads.push([p2])
|
||||
} else {
|
||||
spreads.push([p, p2])
|
||||
}
|
||||
} else {
|
||||
spreads.push([p])
|
||||
}
|
||||
}
|
||||
}
|
||||
if (lastPage) spreads.push(lastPage)
|
||||
return spreads
|
||||
} else {
|
||||
return pages.map(p => [p])
|
||||
}
|
||||
}
|
||||
|
|
@ -11,6 +11,10 @@ export function parseQuerySort (querySort: any, sortOptions: SortOption[]): Sort
|
|||
return customSort
|
||||
}
|
||||
|
||||
export function parseQueryFilter (queryStatus: any, enumeration: string[]): string[] {
|
||||
return queryStatus ? queryStatus.toString().split(',').filter((x: string) => enumeration.includes(x)) : []
|
||||
export function parseQueryParamAndFilter (queryParam: any, enumeration: string[]): string[] {
|
||||
return queryParam ? queryParam.toString().split(',').filter((x: string) => enumeration.includes(x)) : []
|
||||
}
|
||||
|
||||
export function parseQueryParam (queryParam: any): string[] {
|
||||
return queryParam ? queryParam.toString().split(',') : []
|
||||
}
|
||||
|
|
|
|||
|
|
@ -47,3 +47,7 @@ export function collectionThumbnailUrl (collectionId: string): string {
|
|||
export function readListThumbnailUrl (readListId: string): string {
|
||||
return `${urls.originNoSlash}/api/v1/readlists/${readListId}/thumbnail`
|
||||
}
|
||||
|
||||
export function transientBookPageUrl (transientBookId: string, page: number): string {
|
||||
return `${urls.originNoSlash}/api/v1/transient-books/${transientBookId}/pages/${page}`
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,36 +19,42 @@
|
|||
},
|
||||
"author_roles": {
|
||||
"colorist": "Kolorist",
|
||||
"cover": "Titelbild (Cover)",
|
||||
"cover": "Titelbild",
|
||||
"editor": "Editoren",
|
||||
"inker": "Tuscher",
|
||||
"letterer": "Setzer",
|
||||
"penciller": "Zeichner",
|
||||
"writer": "Schriftsteller"
|
||||
},
|
||||
"book_card": {
|
||||
"error": "Fehler",
|
||||
"unknown": "Noch nicht analisiert",
|
||||
"unsupported": "Nicht unterstützt"
|
||||
},
|
||||
"bookreader": {
|
||||
"beginning_of_book": "Sie sind am Anfang des Buches.",
|
||||
"changing_reading_direction": "Ändere Leserichtung zu",
|
||||
"cycling_page_layout": "Ändere Seitendarstellung",
|
||||
"cycling_scale": "Ändere die Skalierung",
|
||||
"changing_reading_direction": "Leserichtung ändern zu",
|
||||
"cycling_page_layout": "Seitendarstellung ändern",
|
||||
"cycling_scale": "Skalierung ändern",
|
||||
"cycling_side_padding": "Seitenrand ändern",
|
||||
"download_current_page": "Aktuelle Seite herunterladen",
|
||||
"end_of_book": "Sie haben das Ende des Buches erreicht.",
|
||||
"from_series_metadata": "Metadaten von Serie",
|
||||
"move_next": "Klicken oder drücken Sie \"Weiter\" um zum nächsten Buch zu gelangen.",
|
||||
"move_next_exit": "Klicken oder drücken Sie \"Weiter\" erneut um den Lesemodus zu beenden.",
|
||||
"move_previous": "Klicken oder drücken Sie erneut auf \"Vorherige Seite\", um zum letzten Buch zu wechseln.",
|
||||
"move_next": "Klicken oder drücken Sie „Weiter“, um zum nächsten Buch zu gelangen.",
|
||||
"move_next_exit": "Klicken oder drücken Sie „Weiter“ erneut, um den Lesemodus zu beenden.",
|
||||
"move_previous": "Klicken oder drücken Sie erneut auf „Vorherige Seite“, um zum letzten Buch zu wechseln.",
|
||||
"paged_reader_layout": {
|
||||
"double": "Doppelseite",
|
||||
"double_no_cover": "Doppelseitig ohne Titelbild",
|
||||
"single": "Einseitig"
|
||||
"single": "Einzelne Seite"
|
||||
},
|
||||
"reader_settings": "Reader Einstellungen",
|
||||
"reader_settings": "Lesemoduseinstellungen",
|
||||
"scale_type": {
|
||||
"continuous_original": "Original",
|
||||
"continuous_width": "An Breite ausrichten",
|
||||
"height": "An Höhe ausrichten",
|
||||
"original": "Original",
|
||||
"screen": "Vollbild",
|
||||
"screen": "Bildschirm",
|
||||
"width": "An Breite ausrichten"
|
||||
},
|
||||
"settings": {
|
||||
|
|
@ -59,29 +65,29 @@
|
|||
"white": "Weiß"
|
||||
},
|
||||
"display": "Anzeige",
|
||||
"general": "Allgemeines",
|
||||
"general": "Allgemein",
|
||||
"gestures": "Gesten",
|
||||
"page_layout": "Seiteneinstellungen",
|
||||
"paged": "Einstellungen zur Seitenausrichtung",
|
||||
"reading_mode": "Lesemodus",
|
||||
"scale_type": "Maßstab",
|
||||
"scale_type": "Skalierung",
|
||||
"side_padding": "Seitenränder",
|
||||
"side_padding_none": "Keine",
|
||||
"webtoon": "Webtoon Lese-Einstellungen"
|
||||
},
|
||||
"shortcuts": {
|
||||
"close": "Verlasse den Reader",
|
||||
"close": "Schließen",
|
||||
"cycle_page_layout": "Wechsle Seitendarstellung",
|
||||
"cycle_scale": "Ändern der Skalierung",
|
||||
"cycle_side_padding": "Seitenrand ändern",
|
||||
"first_page": "Erste Seite",
|
||||
"last_page": "Letzte Seite",
|
||||
"left_to_right": "von Links nach Rechts",
|
||||
"menus": "Menü",
|
||||
"left_to_right": "Von links nach rechts",
|
||||
"menus": "Menüs",
|
||||
"next_page": "Nächste Seite",
|
||||
"previous_page": "Vorherige Seite",
|
||||
"reader_navigation": "Reader Steuerung",
|
||||
"right_to_left": "von Rechts nach Links",
|
||||
"right_to_left": "Von rechts nach links",
|
||||
"settings": "Einstellungen",
|
||||
"show_hide_help": "Zeige/Verstecke Hilfe",
|
||||
"show_hide_settings": "Zeige/Verstecke Einstellungsmenü",
|
||||
|
|
@ -94,15 +100,15 @@
|
|||
"browse_book": {
|
||||
"comment": "Kommentar",
|
||||
"download_file": "Datei herunterladen",
|
||||
"file": "Datei",
|
||||
"format": "Format",
|
||||
"file": "DATEI",
|
||||
"format": "FORMAT",
|
||||
"isbn": "ISBN",
|
||||
"navigation_within_readlist": "Wechseln zur Leseliste: {name}",
|
||||
"read_book": "Lese Buch",
|
||||
"size": "Dateigröße"
|
||||
"size": "GRÖẞE"
|
||||
},
|
||||
"browse_collection": {
|
||||
"edit_collection": "Ändere Sammlung",
|
||||
"edit_collection": "Sammlung bearbeiten",
|
||||
"edit_elements": "Ändern",
|
||||
"manual_ordering": "Manuelle Sortierung"
|
||||
},
|
||||
|
|
@ -125,28 +131,28 @@
|
|||
"books_n": "Kein Buch | 1 Buch | {count} Bücher",
|
||||
"cancel": "Abbrechen",
|
||||
"close": "Schließen",
|
||||
"collections": "Sammlung",
|
||||
"create": "Erstelle",
|
||||
"collections": "Sammlungen",
|
||||
"create": "Erstellen",
|
||||
"delete": "Löschen",
|
||||
"download": "Download",
|
||||
"download": "Herunterladen",
|
||||
"email": "E-Mail",
|
||||
"filter_no_matches": "Die Suchkriterien haben kein Ergebnis geliefert",
|
||||
"genre": "Genre/Gattung",
|
||||
"genre": "Genre",
|
||||
"go_to_library": "Gehe zu Bibliothek",
|
||||
"locale_name": "Deutsch",
|
||||
"locale_rtl": "false",
|
||||
"n_selected": "{count} markiert",
|
||||
"nothing_to_show": "Keine Treffer gefunden die angezeigt werden könnten",
|
||||
"nothing_to_show": "Nichts anzuzeigen",
|
||||
"pages": "Seiten",
|
||||
"pages_n": "Keine Seite | 1 Seite | {count} Seiten",
|
||||
"password": "Passwort",
|
||||
"publisher": "Verlag",
|
||||
"read": "Lese",
|
||||
"read": "Lesen",
|
||||
"readlists": "Leseliste",
|
||||
"required": "Benötigt",
|
||||
"required": "Notwendig",
|
||||
"roles": "Rollen",
|
||||
"series": "Series/Reihe",
|
||||
"tags": "Tags/Stichworte",
|
||||
"series": "Serie",
|
||||
"tags": "Stichwörter",
|
||||
"use_filter_panel_to_change_filter": "Mit dem Suchfeld können die Suchkriterien eingestellt werden",
|
||||
"year": "Jahr"
|
||||
},
|
||||
|
|
@ -157,17 +163,29 @@
|
|||
"recently_added_series": "Kürzlich hinzugefügte Serien",
|
||||
"recently_updated_series": "Kürzlich aktualisierte Serien"
|
||||
},
|
||||
"data_import": {
|
||||
"book_number": "Buch Nummer: {name}",
|
||||
"book_series": "Serie: {name}",
|
||||
"button_import": "Importieren",
|
||||
"comicrack_preambule_html": "Es ist möglich ComicRack Leselisten im Format <code>.cbl</code> zu übernehmen.<br>Komga versucht die übernommenen Serien und Buchnummern mit den Serien und Büchern in der Bibliothek abzugleichen.",
|
||||
"field_files_label": "ComicRack Leseliste (.cbl)",
|
||||
"import_read_lists": "Übernehme Leseliste",
|
||||
"imported_as": "Übernehme als {name}",
|
||||
"results_preambule": "Das Ergebnis der Übernahme wird unten angezeigt. Es ist möglich für jedes Buch, das nicht gefunden wurde, eine Einzelauswahl durchzuführen.",
|
||||
"size_limit": "Die Größe darf {size} MB nicht überschreiten",
|
||||
"tab_title": "Datenübernahme"
|
||||
},
|
||||
"dialog": {
|
||||
"add_to_collection": {
|
||||
"button_create": "Erstelle",
|
||||
"button_create": "Erstellen",
|
||||
"card_collection_subtitle": "Keine Serie | 1 Serie | {count} Serien",
|
||||
"dialog_title": "Füge zu Sammlung hinzu",
|
||||
"dialog_title": "Zu Sammlung hinzufügen",
|
||||
"field_search_create": "Suche oder erzeuge eine Sammlung",
|
||||
"field_search_create_error": "Eine Sammlung mit diesem Namen ist bereits vorhanden",
|
||||
"label_no_matching_collection": "Keine Sammlung gefunden"
|
||||
},
|
||||
"add_to_readlist": {
|
||||
"button_create": "Erzeuge",
|
||||
"button_create": "Erstellen",
|
||||
"card_readlist_subtitle": "Kein Buch | 1 Buch| {count} Bücher",
|
||||
"dialog_title": "Zur Leseliste hinzufügen",
|
||||
"field_search_create": "Durchsuche oder Erzeuge eine Leseliste",
|
||||
|
|
@ -175,49 +193,49 @@
|
|||
"label_no_matching_readlist": "Keine passende Leseliste"
|
||||
},
|
||||
"add_user": {
|
||||
"button_cancel": "Abbruch",
|
||||
"button_cancel": "Abbrechen",
|
||||
"button_confirm": "Hinzufügen",
|
||||
"dialog_title": "Anwender hinzufügen",
|
||||
"dialog_title": "Benutzer hinzufügen",
|
||||
"field_email": "E-Mail",
|
||||
"field_email_error": "Die E-Mail Adresse muss eine gültige Adresse sein",
|
||||
"field_email_error": "Die E-Mail-Adresse muss eine gültige Adresse sein",
|
||||
"field_password": "Passwort",
|
||||
"field_role_administrator": "Administrator",
|
||||
"field_role_file_download": "Datei herunterladen",
|
||||
"field_role_administrator": "Systemverwalter",
|
||||
"field_role_file_download": "Datei-Herunterladen",
|
||||
"field_role_page_streaming": "Seitenaufbau",
|
||||
"label_roles": "Rollen"
|
||||
},
|
||||
"delete_collection": {
|
||||
"button_cancel": "Abbruch",
|
||||
"button_confirm": "Lösche",
|
||||
"confirm_delete": "Ja, lösche die Sammlung \"{name}\"",
|
||||
"dialog_title": "Lösche Sammmlung",
|
||||
"button_cancel": "Abbrechen",
|
||||
"button_confirm": "Löschen",
|
||||
"confirm_delete": "Ja, die Sammlung „{name}“ löschen",
|
||||
"dialog_title": "Sammlung löschen",
|
||||
"warning_html": "Die Sammlung <b>{name}</b> wird vom Server entfernt. Die physikalischen Dateien bleiben unverändert. Dies kann <b>NICHT</b> rückgängig gemacht werden. Fortfahren?"
|
||||
},
|
||||
"delete_library": {
|
||||
"button_cancel": "Abbruch",
|
||||
"button_confirm": "Lösche",
|
||||
"confirm_delete": "Ja, lösche die Bibliothek \"{name}\"",
|
||||
"title": "Lösche Bibliothek",
|
||||
"button_cancel": "Abbrechen",
|
||||
"button_confirm": "Löschen",
|
||||
"confirm_delete": "Ja, die Bibliothek „{name}“ löschen",
|
||||
"title": "Bibliothek löschen",
|
||||
"warning_html": "Die Bibliothek <b>{name}</b> wird vom Server gelöscht. Die physikalischen Dateien bleiben unverändert. Dies kann <b>NICHT</b> rückgängig gemacht werden. Fortfahren?"
|
||||
},
|
||||
"delete_readlist": {
|
||||
"button_cancel": "Abbruch",
|
||||
"button_cancel": "Abbrechen",
|
||||
"button_confirm": "Löschen",
|
||||
"confirm_delete": "Ja, lösche die Leseliste \"{name}\"",
|
||||
"dialog_title": "Lösche Leseliste",
|
||||
"confirm_delete": "Ja, die Leseliste „{name}“ löschen",
|
||||
"dialog_title": "Leseliste löschen",
|
||||
"warning_html": "Die Leseliste <b>{name}</b> wird vom Server entfernt. Die physikalischen Dateien bleiben unverändert. Dies kann <b>NICHT</b> rückgängig gemacht werden. Fortfahren?"
|
||||
},
|
||||
"delete_user": {
|
||||
"button_cancel": "Abbruch",
|
||||
"button_confirm": "Lösche",
|
||||
"confirm_delete": "Ja, lösche den Anwender \"{name}\"",
|
||||
"dialog_title": "Anwender löschen",
|
||||
"warning_html": "Der Anwender <b>{name}</b> wird gelöscht. Dies kann <b>NICHT</b> rückgängig gemacht werden. Fortfahren?"
|
||||
"button_cancel": "Abbrechen",
|
||||
"button_confirm": "Löschen",
|
||||
"confirm_delete": "Ja, den Benutzer „{name}“ löschen",
|
||||
"dialog_title": "Benutzer löschen",
|
||||
"warning_html": "Der Benutzer <b>{name}</b> wird gelöscht. Dies kann <b>NICHT</b> rückgängig gemacht werden. Fortfahren?"
|
||||
},
|
||||
"edit_books": {
|
||||
"authors_notice_multiple_edit": "Sie verändern mehrere Bücher. Dies wird die existierenden Einträge der Autoren jedes einzelnen Buches durch den neuen Eintrag ersetzen.",
|
||||
"button_cancel": "Abbruch",
|
||||
"button_confirm": "Speichere die Änderungen",
|
||||
"button_cancel": "Abbrechen",
|
||||
"button_confirm": "Änderungen speichern",
|
||||
"dialog_title_multiple": "Bearbeite {count} Bücher|Bearbeite {count} Bücher",
|
||||
"dialog_title_single": "Bearbeite {book}",
|
||||
"field_isbn": "ISBN",
|
||||
|
|
@ -225,43 +243,43 @@
|
|||
"field_number": "Nummer",
|
||||
"field_number_sort": "Reihenfolgennummer",
|
||||
"field_number_sort_hint": "Sie können Dezimalzahlen verwenden",
|
||||
"field_release_date": "Erscheinungsdatum",
|
||||
"field_release_date_error": "Das Datum muss im Format YYYY-MM-DD sein",
|
||||
"field_release_date": "Veröffentlichungsdatum",
|
||||
"field_release_date_error": "Das Datum muss im Format JJJJ-MM-TT sein",
|
||||
"field_summary": "Zusammenfassung",
|
||||
"field_tags": "Stichworte (Tags)",
|
||||
"field_tags": "Stichwörter",
|
||||
"field_title": "Titel",
|
||||
"tab_authors": "Authoren",
|
||||
"tab_authors": "Autoren",
|
||||
"tab_general": "Allgemein",
|
||||
"tab_tags": "Stichworte (Tags)",
|
||||
"tab_tags": "Stichwörter",
|
||||
"tags_notice_multiple_edit": "Sie verändern die Stichwörter von mehreren Büchern. Dies wird die existierenden Einträge der Stichwörter jedes einzelnen Buches durch den neuen Eintrag ersetzen."
|
||||
},
|
||||
"edit_collection": {
|
||||
"button_cancel": "Abbruch",
|
||||
"button_confirm": "Speichere die Änderungen",
|
||||
"dialog_title": "Bearbeite Sammlung",
|
||||
"button_cancel": "Abrechen",
|
||||
"button_confirm": "Änderungen speichern",
|
||||
"dialog_title": "Sammlung bearbeiten",
|
||||
"field_manual_ordering": "Manuelle Sortierung",
|
||||
"label_ordering": "Sammlungen werden standardmäßig nach Namen sortiert. Sie können die manuelle Sortierung aktivieren und eine eigene Sortierfolge festlegen."
|
||||
},
|
||||
"edit_library": {
|
||||
"button_browse": "Stöbern",
|
||||
"button_cancel": "Abbruch",
|
||||
"button_browse": "Durchsuchen",
|
||||
"button_cancel": "Abbrechen",
|
||||
"button_confirm_add": "Hinzufügen",
|
||||
"button_confirm_edit": "Bearbeite",
|
||||
"dialog_title_add": "Füge Bibliothek hinzu",
|
||||
"dialot_title_edit": "Bearbeite Bibliothek",
|
||||
"button_confirm_edit": "Bearbeiten",
|
||||
"dialog_title_add": "Bibliothek hinzufügen",
|
||||
"dialot_title_edit": "Bibliothek bearbeiten",
|
||||
"field_import_barcode_isbn": "ISBN Strichcode",
|
||||
"field_import_comicinfo_book": "Buch Metadaten",
|
||||
"field_import_comicinfo_collections": "Sammlung",
|
||||
"field_import_comicinfo_collections": "Sammlungen",
|
||||
"field_import_comicinfo_readlists": "Leseliste",
|
||||
"field_import_comicinfo_series": "Serien Metadaten",
|
||||
"field_import_epub_book": "Buch Metadaten",
|
||||
"field_import_epub_series": "Serien Metadaten",
|
||||
"field_import_local_artwork": "Lokale Artwork",
|
||||
"field_name": "Name",
|
||||
"field_root_folder": "Hauptverzeichnis",
|
||||
"field_root_folder": "Stammordner",
|
||||
"field_scanner_deep_scan": "Tiefensuche",
|
||||
"field_scanner_force_directory_modified_time": "Erzwinge das Durchsuchen des Verzeichnis auf Basis des Zeitstempels",
|
||||
"file_browser_dialog_button_confirm": "Akzeptiert",
|
||||
"file_browser_dialog_button_confirm": "Auswählen",
|
||||
"file_browser_dialog_title": "Bibliothekshauptverzeichnis",
|
||||
"label_import_barcode_isbn": "Importieren Sie die ISBN aus einem Barcode",
|
||||
"label_import_comicinfo": "Importiere aus einer vorhandenen ComicInfo.xml Datei die Metadaten für CBR/CBZ",
|
||||
|
|
@ -272,63 +290,63 @@
|
|||
"tab_options": "Optionen"
|
||||
},
|
||||
"edit_readlist": {
|
||||
"button_cancel": "Abbruch",
|
||||
"button_cancel": "Abbrechen",
|
||||
"button_confirm": "Änderungen speichern",
|
||||
"dialog_title": "Bearbeite die Leseliste",
|
||||
"field_name": "Name der Leseliste"
|
||||
"field_name": "Name"
|
||||
},
|
||||
"edit_series": {
|
||||
"button_cancel": "Abbruch",
|
||||
"button_cancel": "Abbrechen",
|
||||
"button_confirm": "Speichere die Änderungen",
|
||||
"dialog_title_multiple": "Bearbeite {count} Serien | Bearbeite {count} Serien",
|
||||
"dialog_title_single": "Bearbeite {series}",
|
||||
"field_age_rating": "Altersfreigabe",
|
||||
"field_age_rating_error": "Altersfreigabe muß größer 0 sein",
|
||||
"field_genres": "Genre/Gattung",
|
||||
"field_genres": "Genre",
|
||||
"field_language": "Sprache",
|
||||
"field_publisher": "Verlag",
|
||||
"field_reading_direction": "Leserichtung",
|
||||
"field_sort_title": "Nach Titel sortiert",
|
||||
"field_status": "Status",
|
||||
"field_summary": "Zusammenfassung",
|
||||
"field_tags": "Stichworte (Tags)",
|
||||
"field_tags": "Stichwörter",
|
||||
"field_title": "Titel",
|
||||
"mixed": "Vermischtes",
|
||||
"tab_general": "Allgemein",
|
||||
"tab_tags": "Stichworte (Tags)",
|
||||
"tab_tags": "Stichwörter",
|
||||
"tags_notice_multiple_edit": "Sie verändern mehrere Stichwörter. Dies wird die existierenden Einträge der Stichwörter aller Serien durch den neuen Eintrag ersetzen."
|
||||
},
|
||||
"edit_user": {
|
||||
"button_cancel": "Abbruch",
|
||||
"button_cancel": "Abbrechen",
|
||||
"button_confirm": "Änderungen speichern",
|
||||
"dialog_title": "Bearbeite Anwender",
|
||||
"dialog_title": "Benutzerdaten bearbeiten",
|
||||
"label_roles_for": "Rollen für {name}"
|
||||
},
|
||||
"edit_user_shared_libraries": {
|
||||
"button_cancel": "Abbruch",
|
||||
"button_cancel": "Abbrechen",
|
||||
"button_confirm": "Änderungen speichern",
|
||||
"dialog_title": "Bearbeite gemeinsame Bibliotheken",
|
||||
"field_all_libraries": "Alle Bibliotheken",
|
||||
"label_shared_with": "Teile mit {name}"
|
||||
},
|
||||
"file_browser": {
|
||||
"button_cancel": "Abbruch",
|
||||
"button_confirm_default": "Akzeptiert",
|
||||
"dialog_title_default": "Datei Browser",
|
||||
"parent_directory": "Übergeordnetes Verzeichnis"
|
||||
"button_cancel": "Abbrechen",
|
||||
"button_confirm_default": "Auswählen",
|
||||
"dialog_title_default": "Dateibrowser",
|
||||
"parent_directory": "Übergeordnet"
|
||||
},
|
||||
"password_change": {
|
||||
"button_cancel": "Abbruch",
|
||||
"button_confirm": "Ändere das Passwort",
|
||||
"dialog_title": "Passwortänderung",
|
||||
"button_cancel": "Abbrechen",
|
||||
"button_confirm": "Passwort ändern",
|
||||
"dialog_title": "Passwort ändern",
|
||||
"field_new_password": "Neues Passwort",
|
||||
"field_new_password_error": "Passworterneuerung ist notwendig.",
|
||||
"field_repeat_password": "Geben Sie das Passwort erneut ein",
|
||||
"field_repeat_password": "Neues Passwort wiederholen",
|
||||
"field_repeat_password_error": "Das Passwort muss übereinstimmen."
|
||||
},
|
||||
"server_stop": {
|
||||
"button_cancel": "Abbruch",
|
||||
"button_confirm": "Stop",
|
||||
"button_cancel": "Abbrechen",
|
||||
"button_confirm": "Herunterfahren",
|
||||
"confirmation_message": "Sind Sie sicher das Sie Komga beenden wollen?",
|
||||
"dialog_title": "Server herunterfahren"
|
||||
},
|
||||
|
|
@ -341,13 +359,13 @@
|
|||
"media_status": {
|
||||
"ERROR": "Fehler",
|
||||
"OUTDATED": "Veraltet",
|
||||
"READY": "Bereit",
|
||||
"READY": "Fertig",
|
||||
"UNKNOWN": "Unbekannt",
|
||||
"UNSUPPORTED": "Nicht unterstützt"
|
||||
},
|
||||
"reading_direction": {
|
||||
"LEFT_TO_RIGHT": "von Links nach Rechts",
|
||||
"RIGHT_TO_LEFT": "von Rechts nach Links",
|
||||
"LEFT_TO_RIGHT": "Von links nach rechts",
|
||||
"RIGHT_TO_LEFT": "Von rechts nach links",
|
||||
"VERTICAL": "Vertikal",
|
||||
"WEBTOON": "Webtoon"
|
||||
},
|
||||
|
|
@ -367,57 +385,64 @@
|
|||
"ERR_1005": "Unbekannter Fehler beim Analysieren des Buchs",
|
||||
"ERR_1006": "Das Buch enthält keine Seiten",
|
||||
"ERR_1007": "Einige Einträge konnten nicht ausgewertet werden",
|
||||
"ERR_1008": "Unbekannter Fehler beim Abrufen der Bucheinträge"
|
||||
"ERR_1008": "Unbekannter Fehler beim Abrufen der Bucheinträge",
|
||||
"ERR_1009": "Eine Leseliste mit diesem Namen existiert bereits",
|
||||
"ERR_1010": "Für dies Bücher in der Leseliste gab es keine Übereinstimmung in der Bibliothek",
|
||||
"ERR_1011": "Kein eindeutige Übereinstimmung für diese Serie",
|
||||
"ERR_1012": "Keine Übereinstimmung für die Serie",
|
||||
"ERR_1013": "Kein eindeutige Übereinstimmung für die Bücher der Serie",
|
||||
"ERR_1014": "Keine Übereinstimmung für die Bücher für die Serie",
|
||||
"ERR_1015": "Fehler bei der Abarbeitung der ComicRack Leseliste"
|
||||
},
|
||||
"filter": {
|
||||
"age_rating": "Altersfreigabe",
|
||||
"age_rating_none": "Bei Serien ohne Altersbeschränkungen soll dieser Wert im Filter angezeigt werden",
|
||||
"genre": "Genre/Gattung",
|
||||
"age_rating_none": "Keine",
|
||||
"genre": "Genre",
|
||||
"language": "Sprache",
|
||||
"library": "Bibliothek",
|
||||
"publisher": "Verlag",
|
||||
"release_date": "Erscheinungsdatum",
|
||||
"release_date": "Veröffentlichungsdatum",
|
||||
"status": "Status",
|
||||
"tag": "Stichwort (Tag)",
|
||||
"tag": "Srichwort",
|
||||
"unread": "Ungelesen"
|
||||
},
|
||||
"filter_drawer": {
|
||||
"filter": "Auswahlkriterium (Filter)",
|
||||
"filter": "Filter",
|
||||
"sort": "Sortierung"
|
||||
},
|
||||
"home": {
|
||||
"theme": "Vorlage",
|
||||
"theme": "Design",
|
||||
"translation": "Übersetzung"
|
||||
},
|
||||
"library_navigation": {
|
||||
"browse": "Stöbern",
|
||||
"collections": "Sammlung",
|
||||
"browse": "Durchsuchen",
|
||||
"collections": "Sammlungen",
|
||||
"readlists": "Leseliste"
|
||||
},
|
||||
"login": {
|
||||
"create_user_account": "Erstelle Anwenderkonto",
|
||||
"login": "Login",
|
||||
"unclaimed_html": "Dieser Komga Server ist noch nicht bereit. Sie müssen zuerst einen Anwenderkonto anlegen um sich anmelden zu können.<br><br>Hinterlegen Sie eine <strong>E-Mail Adresse</strong> und vergeben Sie ein <strong>Passwort</strong>. Klicken Sie dann auf <strong>Erstelle Anwenderkonto</strong>."
|
||||
"create_user_account": "Benutzerkonto erstellen",
|
||||
"login": "Anmelden",
|
||||
"unclaimed_html": "Dieser Komga-Server ist noch nicht bereit. Sie müssen zuerst einen Benutzerkonto anlegen, um sich anmelden zu können.<br><br>Hinterlegen Sie eine <strong>E-Mail-Adresse</strong> und vergeben Sie ein <strong>Passwort</strong>. Klicken Sie dann auf <strong>Benutzerkonto erstellen</strong>."
|
||||
},
|
||||
"media_analysis": {
|
||||
"comment": "Kommentar",
|
||||
"media_analysis": "Medien Analyse",
|
||||
"media_type": "Medienart",
|
||||
"media_type": "Medientyp",
|
||||
"name": "Name",
|
||||
"size": "Größe",
|
||||
"status": "Status",
|
||||
"url": "URL"
|
||||
},
|
||||
"menu": {
|
||||
"add_to_collection": "Füge zu Sammlung hinzu",
|
||||
"add_to_collection": "Zu Sammlung hinzufügen",
|
||||
"add_to_readlist": "Füge zur Leseliste hinzu",
|
||||
"analyze": "Analysiere",
|
||||
"delete": "Lösche",
|
||||
"analyze": "Analysieren",
|
||||
"delete": "Löschen",
|
||||
"download_series": "Lade die gesamte Serie herunter",
|
||||
"edit": "Ändere",
|
||||
"edit_metadata": "Ändere Metadaten",
|
||||
"mark_read": "Markiere als gelesen",
|
||||
"mark_unread": "Markiere als ungelesen",
|
||||
"edit": "Bearbeiten",
|
||||
"edit_metadata": "Metadaten bearbeiten",
|
||||
"mark_read": "Als gelesen markieren",
|
||||
"mark_unread": "Als ungelesen markieren",
|
||||
"refresh_metadata": "Erneuere Metadaten",
|
||||
"scan_library_files": "Überprüfe (Scan) Bibliotheksdateien"
|
||||
},
|
||||
|
|
@ -427,13 +452,13 @@
|
|||
"logout": "Abmelden"
|
||||
},
|
||||
"page_not_found": {
|
||||
"go_back_to_home_page": "Zurück zur Übersicht",
|
||||
"go_back_to_home_page": "Zurück zur Startseite",
|
||||
"page_does_not_exist": "Die ausgewählte Seite existiert nicht.",
|
||||
"page_not_found": "Seite nicht gefunden"
|
||||
},
|
||||
"read_more": {
|
||||
"less": "Zeige weniger an",
|
||||
"more": "Zeige den ganzen Text an"
|
||||
"more": "Weiterlesen"
|
||||
},
|
||||
"readlists_expansion_panel": {
|
||||
"manage_readlist": "Leselistenverwaltung",
|
||||
|
|
@ -443,10 +468,10 @@
|
|||
"no_results": "Die Suche lieferte keine Treffer",
|
||||
"search": "Suche",
|
||||
"search_for_something_else": "Versuche ein anderes Suchkriterium",
|
||||
"search_results_for": "Suchergegnis für \"{name}\""
|
||||
"search_results_for": "Suchergegnis für „{name}“"
|
||||
},
|
||||
"searchbox": {
|
||||
"no_results": "Kein Ergebnis",
|
||||
"no_results": "Keine Ergebnisse",
|
||||
"search_all": "Durchsuche alles nach …"
|
||||
},
|
||||
"server": {
|
||||
|
|
@ -461,20 +486,20 @@
|
|||
},
|
||||
"settings_user": {
|
||||
"edit_shared_libraries": "Bearbeite gemeinsame Bibliotheken",
|
||||
"edit_user": "Bearbeite den Anwender",
|
||||
"role_administrator": "Administrator",
|
||||
"role_user": "Anwender"
|
||||
"edit_user": "Benutzerdaten bearbeiten",
|
||||
"role_administrator": "Systemverwalter",
|
||||
"role_user": "Benutzer"
|
||||
},
|
||||
"sort": {
|
||||
"books_count": "Anzahl der Bücher",
|
||||
"date_added": "Nach Hinzufügedatum sortiert",
|
||||
"date_updated": "Nach letzter Aktuallisierung sortiert",
|
||||
"date_added": "Hinzufügedatum",
|
||||
"date_updated": "Aktualisierungsdatum",
|
||||
"file_name": "Dateiname",
|
||||
"file_size": "Nach Dateigröße sortiert",
|
||||
"folder_name": "Name des Ordners",
|
||||
"file_size": "Dateigröße",
|
||||
"folder_name": "Ordnername",
|
||||
"name": "Nach Name sortiert",
|
||||
"number": "Nach Nummer sortiert",
|
||||
"release_date": "Nach Erscheinungsdatum sortiert"
|
||||
"number": "Nummer",
|
||||
"release_date": "Veröffentlichungsdatum"
|
||||
},
|
||||
"theme": {
|
||||
"dark": "Dunkel",
|
||||
|
|
@ -482,16 +507,16 @@
|
|||
"system": "System"
|
||||
},
|
||||
"user_roles": {
|
||||
"ADMIN": "Administrator",
|
||||
"FILE_DOWNLOAD": "Datei herunterladen",
|
||||
"ADMIN": "Systemverwalter",
|
||||
"FILE_DOWNLOAD": "Datei-Herunterladen",
|
||||
"PAGE_STREAMING": "Online lesen",
|
||||
"USER": "Anwender"
|
||||
"USER": "Benutzer"
|
||||
},
|
||||
"users": {
|
||||
"users": "Anwender"
|
||||
"users": "Benutzer"
|
||||
},
|
||||
"welcome": {
|
||||
"add_library": "Füge Bibliothek hinzu",
|
||||
"add_library": "Bibliothek hinzufügen",
|
||||
"no_libraries_yet": "Es wurden bisher noch keine Bibliothken hinzugefügt!",
|
||||
"welcome_message": "Willkommen zu Komga"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -95,7 +95,8 @@
|
|||
"show_hide_toolbars": "Show/hide toolbars",
|
||||
"vertical": "Vertical",
|
||||
"webtoon": "Webtoon"
|
||||
}
|
||||
},
|
||||
"tooltip_incognito": "Read progress will not be saved"
|
||||
},
|
||||
"browse_book": {
|
||||
"comment": "COMMENT",
|
||||
|
|
@ -353,6 +354,44 @@
|
|||
"shortcut_help": {
|
||||
"label_description": "Description",
|
||||
"label_key": "Key"
|
||||
},
|
||||
"series_picker": {
|
||||
"title": "Select Series",
|
||||
"label_search_series": "Search Series"
|
||||
},
|
||||
"filename_chooser": {
|
||||
"title": "Destination File Name",
|
||||
"label_source_filename": "Source File Name",
|
||||
"field_destination_filename": "Destination file name",
|
||||
"button_choose": "Choose",
|
||||
"table": {
|
||||
"order": "Order",
|
||||
"existing_file": "Existing File"
|
||||
}
|
||||
},
|
||||
"transient_book_details": {
|
||||
"title": "Book Details",
|
||||
"title_comparison": "Book Comparison",
|
||||
"label_candidate": "Candidate",
|
||||
"label_existing": "Existing",
|
||||
"label_name": "Name",
|
||||
"label_size": "Size",
|
||||
"label_format": "Format",
|
||||
"label_pages": "Pages",
|
||||
"pages_table": {
|
||||
"index": "Index",
|
||||
"filename": "File name",
|
||||
"media_type": "Media type",
|
||||
"width": "Width",
|
||||
"height": "Height"
|
||||
}
|
||||
},
|
||||
"transient_book_viewer": {
|
||||
"title": "Inspect Book",
|
||||
"title_comparison": "Book Comparison",
|
||||
"label_candidate": "Candidate",
|
||||
"label_existing": "Existing",
|
||||
"page_of_pages": "{page} / {pages}"
|
||||
}
|
||||
},
|
||||
"enums": {
|
||||
|
|
@ -374,6 +413,10 @@
|
|||
"ENDED": "Ended",
|
||||
"HIATUS": "Hiatus",
|
||||
"ONGOING": "Ongoing"
|
||||
},
|
||||
"copy_mode": {
|
||||
"HARDLINK": "Hardlink/Copy Files",
|
||||
"MOVE": "Move Files"
|
||||
}
|
||||
},
|
||||
"error_codes": {
|
||||
|
|
@ -392,7 +435,9 @@
|
|||
"ERR_1012": "No match for series",
|
||||
"ERR_1013": "No unique match for book number within series",
|
||||
"ERR_1014": "No match for book number within series",
|
||||
"ERR_1015": "Error while deserializing ComicRack ReadingList"
|
||||
"ERR_1015": "Error while deserializing ComicRack ReadingList",
|
||||
"ERR_1016": "Directory not accessible or not a directory",
|
||||
"ERR_1017": "Cannot scan folder that is part of an existing library"
|
||||
},
|
||||
"filter": {
|
||||
"age_rating": "age rating",
|
||||
|
|
@ -520,5 +565,28 @@
|
|||
"add_library": "Add library",
|
||||
"no_libraries_yet": "No libraries have been added yet!",
|
||||
"welcome_message": "Welcome to Komga"
|
||||
},
|
||||
"book_import": {
|
||||
"title": "Import",
|
||||
"field_import_path": "Import from folder",
|
||||
"button_browse": "Browse",
|
||||
"button_scan": "Scan",
|
||||
"table": {
|
||||
"file_name": "File name",
|
||||
"series": "Series",
|
||||
"number": "Number",
|
||||
"destination_name": "Destination name"
|
||||
},
|
||||
"button_select_series": "Select Series",
|
||||
"button_import": "Import",
|
||||
"row": {
|
||||
"warning_upgrade": "Existing book will be upgraded",
|
||||
"error_analyze_first": "Book needs to be analyzed first",
|
||||
"error_only_import_no_errors": "Can only import books without errors",
|
||||
"error_choose_series": "Choose a series"
|
||||
},
|
||||
"warning_early_feature": "Book Import is still an early feature, and must be used with caution. Make sure your source\n and destination files\n are backed up before using the Import feature.",
|
||||
"info_part1": "This screen lets you import files that are outside your existing libraries. You can only import files into\n existing Series, in which case Komga will move or copy the files into the directory of the chosen Series.",
|
||||
"info_part2": "If you choose a number for a book, and a book already exists with that number, then\n you will be able to compare the 2 books. If you decide to import the book, Komga will upgrade the existing book\n with the new one, effectively replacing the old file with the new."
|
||||
}
|
||||
}
|
||||
|
|
|
|||
585
komga-webui/src/locales/eo.json
Normal file
585
komga-webui/src/locales/eo.json
Normal file
|
|
@ -0,0 +1,585 @@
|
|||
{
|
||||
"$vuetify": {
|
||||
"dataFooter": {
|
||||
"pageText": "{0}-{1} el {2}"
|
||||
},
|
||||
"dataTable": {
|
||||
"itemsPerPageText": "Rikordoj en paĝo:",
|
||||
"sortBy": "Ordigi laŭ"
|
||||
},
|
||||
"fileInput": {
|
||||
"counter": "{0} dosieroj",
|
||||
"counterSize": "{0} dosieroj ({1} entute)"
|
||||
},
|
||||
"noDataText": "Neniuj datenoj disponeblas"
|
||||
},
|
||||
"account_settings": {
|
||||
"account_settings": "Agordoj pri Konto",
|
||||
"change_password": "ŝanĝi pasvorton"
|
||||
},
|
||||
"author_roles": {
|
||||
"colorist": "kolorigistoj",
|
||||
"cover": "kovrilartistoj",
|
||||
"editor": "redaktoroj",
|
||||
"inker": "inkistoj",
|
||||
"letterer": "skribistoj",
|
||||
"penciller": "desegnistoj",
|
||||
"writer": "verkistoj"
|
||||
},
|
||||
"book_card": {
|
||||
"error": "Eraro",
|
||||
"unknown": "Analizota",
|
||||
"unsupported": "Ne subtenata"
|
||||
},
|
||||
"book_import": {
|
||||
"button_browse": "Foliumi",
|
||||
"button_import": "Enporti",
|
||||
"button_scan": "Skani",
|
||||
"button_select_series": "Elekti Serion",
|
||||
"field_import_path": "Enporti el dosierujo",
|
||||
"row": {
|
||||
"error_analyze_first": "Libro devas esti analizita antaŭe",
|
||||
"error_choose_series": "Elekti serion",
|
||||
"error_only_import_no_errors": "Nur libroj sen eraro estas enporteblaj",
|
||||
"warning_upgrade": "Ekzistanta libro estas ĝisdatigota"
|
||||
},
|
||||
"table": {
|
||||
"destination_name": "Nomo de celo",
|
||||
"file_name": "Nomo de dosiero",
|
||||
"number": "Numero",
|
||||
"series": "Serio"
|
||||
},
|
||||
"title": "Enporti"
|
||||
},
|
||||
"bookreader": {
|
||||
"beginning_of_book": "Vi estas ĉe la komenco de la libro.",
|
||||
"changing_reading_direction": "Ŝanĝante Direkton de Legado al",
|
||||
"cycling_page_layout": "Cikle Ŝanĝante Aranĝon de Paĝoj",
|
||||
"cycling_scale": "Cikle ŝanĝante zomnivelojn",
|
||||
"cycling_side_padding": "Cikle ŝanĝante flankajn marĝenojn",
|
||||
"download_current_page": "Elŝuti aktualan paĝon",
|
||||
"end_of_book": "Vi atingis la finon de la libro.",
|
||||
"from_series_metadata": "el metadatenoj de la serio",
|
||||
"move_next": "Alklaku aŭ premu \"Sekva\" denove por iri al la sekva libro.",
|
||||
"move_next_exit": "Alklaku aŭ premu \"Sekva\" denove por eliri el la legilo.",
|
||||
"move_previous": "Alklaku aŭ premu \"Antaŭa\" denove por iri al la antaŭa libro.",
|
||||
"paged_reader_layout": {
|
||||
"double": "Duope",
|
||||
"double_no_cover": "Duope (sen kovrilo)",
|
||||
"single": "Unuope"
|
||||
},
|
||||
"reader_settings": "Agordoj pri Legilo",
|
||||
"scale_type": {
|
||||
"continuous_original": "Originalo",
|
||||
"continuous_width": "Adapti al larĝo",
|
||||
"height": "Adapti al alto",
|
||||
"original": "Originalo",
|
||||
"screen": "Ekrano",
|
||||
"width": "Adapti al larĝo"
|
||||
},
|
||||
"settings": {
|
||||
"animate_page_transitions": "Montri turnadon de paĝoj",
|
||||
"background_color": "Fona koloro",
|
||||
"background_colors": {
|
||||
"black": "Nigro",
|
||||
"white": "Blanko"
|
||||
},
|
||||
"display": "Montrado",
|
||||
"general": "Ĝenerala",
|
||||
"gestures": "Gestoj",
|
||||
"page_layout": "Aranĝo de paĝoj",
|
||||
"paged": "Opcioj pri Paĝolegilo",
|
||||
"reading_mode": "Reĝimo de legado",
|
||||
"scale_type": "Zomnivelo",
|
||||
"side_padding": "Flanka marĝeno",
|
||||
"side_padding_none": "Neniom",
|
||||
"webtoon": "Opcioj pri Legilo de Retkomiksoj"
|
||||
},
|
||||
"shortcuts": {
|
||||
"close": "Fermi",
|
||||
"cycle_page_layout": "Cikle ŝanĝi aranĝon de paĝoj",
|
||||
"cycle_scale": "Cikle ŝanĝi zomnivelojn",
|
||||
"cycle_side_padding": "Cikle ŝanĝi flankajn marĝenojn",
|
||||
"first_page": "Unua paĝo",
|
||||
"last_page": "Lasta paĝo",
|
||||
"left_to_right": "Demaldekstre Dekstren",
|
||||
"menus": "Menuoj",
|
||||
"next_page": "Sekva paĝo",
|
||||
"previous_page": "Antaŭa paĝo",
|
||||
"reader_navigation": "Navigado de Legilo",
|
||||
"right_to_left": "Dedekstre Maldekstren",
|
||||
"settings": "Agordoj",
|
||||
"show_hide_help": "Montri/kaŝi helpon",
|
||||
"show_hide_settings": "Montri/kaŝi menuon pri agordoj",
|
||||
"show_hide_thumbnails": "Montri/kaŝi miniaturojn",
|
||||
"show_hide_toolbars": "Montri/kaŝi ilobretojn",
|
||||
"vertical": "Vertikala",
|
||||
"webtoon": "Retkomikso"
|
||||
}
|
||||
},
|
||||
"browse_book": {
|
||||
"comment": "KOMENTO",
|
||||
"download_file": "Elŝuti dosieron",
|
||||
"file": "DOSIERO",
|
||||
"format": "FORMO",
|
||||
"isbn": "ISBN",
|
||||
"navigation_within_readlist": "Navigado en la legolisto {name}",
|
||||
"read_book": "Legi libron",
|
||||
"size": "GRANDO"
|
||||
},
|
||||
"browse_collection": {
|
||||
"edit_collection": "Redakti kolekton",
|
||||
"edit_elements": "Redakti elementojn",
|
||||
"manual_ordering": "mana ordigo"
|
||||
},
|
||||
"browse_readlist": {
|
||||
"edit_elements": "Redakti elementojn",
|
||||
"edit_readlist": "Redakti legoliston"
|
||||
},
|
||||
"browse_series": {
|
||||
"earliest_year_from_release_dates": "Jen la plej frua el la datoj de eldono de ĉiuj libroj en la serio",
|
||||
"series_no_summary": "Resumo mankas al ĉi tiu serio; do ni elektis unu por vi!",
|
||||
"summary_from_book": "Resumo el libro {number}:"
|
||||
},
|
||||
"collections_expansion_panel": {
|
||||
"manage_collection": "Mastrumi kolekton",
|
||||
"title": "Kolekto {name}"
|
||||
},
|
||||
"common": {
|
||||
"all_libraries": "Ĉiuj Bibliotekoj",
|
||||
"books": "Libroj",
|
||||
"books_n": "Neniuj libroj | 1 libro | {count} libroj",
|
||||
"cancel": "Nuligi",
|
||||
"close": "Fermi",
|
||||
"collections": "Kolektoj",
|
||||
"create": "Krei",
|
||||
"delete": "Forigi",
|
||||
"download": "Elŝuti",
|
||||
"email": "Retpoŝta adreso",
|
||||
"filter_no_matches": "La aktiva filtrilo ne lasas ion ajn",
|
||||
"genre": "Ĝenro",
|
||||
"go_to_library": "Iri al biblioteko",
|
||||
"locale_name": "Esperanto",
|
||||
"locale_rtl": "false",
|
||||
"n_selected": "{count} elektiĝis",
|
||||
"nothing_to_show": "Nenio por montri",
|
||||
"pages": "paĝoj",
|
||||
"pages_n": "Neniuj paĝoj | 1 paĝo | {count} paĝoj",
|
||||
"password": "Pasvorto",
|
||||
"publisher": "Eldonejo",
|
||||
"read": "Legi",
|
||||
"readlists": "Legolistoj",
|
||||
"required": "Deviga",
|
||||
"roles": "Roloj",
|
||||
"series": "Serio",
|
||||
"tags": "Etikedoj",
|
||||
"use_filter_panel_to_change_filter": "Uzu la filtrilan panelon por ŝanĝi la aktivan filtrilon",
|
||||
"year": "jaro"
|
||||
},
|
||||
"dashboard": {
|
||||
"keep_reading": "Legi Daŭre",
|
||||
"on_deck": "Unua Nelegita Libro",
|
||||
"recently_added_books": "Ĵus Aldonitaj Libroj",
|
||||
"recently_added_series": "Ĵus Aldonitaj Serioj",
|
||||
"recently_updated_series": "Ĵus Ĝisdatigitaj Libroj"
|
||||
},
|
||||
"data_import": {
|
||||
"book_number": "Numero de libro: {name}",
|
||||
"book_series": "Serio: {name}",
|
||||
"button_import": "Enporti",
|
||||
"comicrack_preambule_html": "Vi povas enporti ekzistantajn legolistojn ComicRack de la dosiersufikso <code>.cbl</code>.<br>Komga provos identigi la provizitajn seriojn kaj libronumerojn kun la serioj kaj libroj en viaj bibliotekoj.",
|
||||
"field_files_label": "Legolistoj ComicRack (.cbl)",
|
||||
"import_read_lists": "Enporti Legolistojn",
|
||||
"imported_as": "Enportita kiel {name}",
|
||||
"results_preambule": "Jen la rezulto de la enporto. Vi ankaŭ povas kontroli la neidentigitajn librojn por ĉiu provizita dosiero.",
|
||||
"size_limit": "La grando devas esti malpli ol {size} megabajtoj",
|
||||
"tab_title": "Enportado de Datenoj"
|
||||
},
|
||||
"dialog": {
|
||||
"add_to_collection": {
|
||||
"button_create": "Krei",
|
||||
"card_collection_subtitle": "Neniuj serioj | 1 serio | {count} serioj",
|
||||
"dialog_title": "Aldoni en kolekton",
|
||||
"field_search_create": "Serĉi aŭ krei kolekton",
|
||||
"field_search_create_error": "Jam ekzistas kolekto de tiu nomo",
|
||||
"label_no_matching_collection": "Neniu kongrua kolekto"
|
||||
},
|
||||
"add_to_readlist": {
|
||||
"button_create": "Krei",
|
||||
"card_readlist_subtitle": "Neniuj libroj | 1 libro | {count} libroj",
|
||||
"dialog_title": "Aldoni en legoliston",
|
||||
"field_search_create": "Serĉi aŭ krei legoliston",
|
||||
"field_search_create_error": "Jam ekzistas legolisto de tiu nomo",
|
||||
"label_no_matching_readlist": "Neniu kongrua legolisto"
|
||||
},
|
||||
"add_user": {
|
||||
"button_cancel": "Nuligi",
|
||||
"button_confirm": "Aldoni",
|
||||
"dialog_title": "Aldoni Uzanton",
|
||||
"field_email": "Retpoŝta adreso",
|
||||
"field_email_error": "Devas esti valida retpoŝta adreso",
|
||||
"field_password": "Pasvorto",
|
||||
"field_role_administrator": "Administranto",
|
||||
"field_role_file_download": "Elŝutanto de Dosieroj",
|
||||
"field_role_page_streaming": "Popaĝa Leganto",
|
||||
"label_roles": "Roloj"
|
||||
},
|
||||
"delete_collection": {
|
||||
"button_cancel": "Nuligi",
|
||||
"button_confirm": "Forigi",
|
||||
"confirm_delete": "Jes, forigu la kolekton \"{name}\"",
|
||||
"dialog_title": "Forigi Kolekton",
|
||||
"warning_html": "La kolekto <b>{name}</b> estas forigota de ĉi tiu servilo. Viaj dosieroj ne ŝanĝiĝos. Tio <b>ne</b> esteas malfarebla. Ĉu daŭrigi?"
|
||||
},
|
||||
"delete_library": {
|
||||
"button_cancel": "Nuligi",
|
||||
"button_confirm": "Forigi",
|
||||
"confirm_delete": "Jes, forigu la bibliotekon \"{name}\"",
|
||||
"title": "Forigi Bibliotekon",
|
||||
"warning_html": "La biblioteko <b>{name}</b> estas forigota de ĉi tiu servilo. Viaj dosieroj ne ŝanĝiĝos. Tio <b>ne</b> esteas malfarebla. Ĉu daŭrigi?"
|
||||
},
|
||||
"delete_readlist": {
|
||||
"button_cancel": "Nuligi",
|
||||
"button_confirm": "Forigi",
|
||||
"confirm_delete": "Jes, forigu la legoliston \"{name}\"",
|
||||
"dialog_title": "Forigi Legoliston",
|
||||
"warning_html": "La legolisto <b>{name}</b> estas forigota de ĉi tiu servilo. Viaj dosieroj ne ŝanĝiĝos. Tio <b>ne</b> esteas malfarebla. Ĉu daŭrigi?"
|
||||
},
|
||||
"delete_user": {
|
||||
"button_cancel": "Nuligi",
|
||||
"button_confirm": "Forigi",
|
||||
"confirm_delete": "Jes, forigu la uzanton \"{name}\"",
|
||||
"dialog_title": "Forigi Uzanton",
|
||||
"warning_html": "La uzanto <b>{name}</b> estas forigota de ĉi tiu servilo. Tio <b>ne</b> esteas malfarebla. Ĉu daŭrigi?"
|
||||
},
|
||||
"edit_books": {
|
||||
"authors_notice_multiple_edit": "Vi redaktas aŭtorojn de pluraj libroj. Tio ŝanĝos la aŭtorojn de ĉiu el tiuj libroj.",
|
||||
"button_cancel": "Nuligi",
|
||||
"button_confirm": "Konservi ŝanĝojn",
|
||||
"dialog_title_multiple": "Redakti {count} libron | Redakti {count} librojn",
|
||||
"dialog_title_single": "Redakti libron {book}",
|
||||
"field_isbn": "ISBN",
|
||||
"field_isbn_error": "Devas esti valida ISBN-13",
|
||||
"field_number": "Numero",
|
||||
"field_number_sort": "Ordiga Numero",
|
||||
"field_number_sort_hint": "Vi povas uzi decimalojn",
|
||||
"field_release_date": "Dato de Eldono",
|
||||
"field_release_date_error": "Devas esti valida dato de la aranĝo JJJJ-MM-TT",
|
||||
"field_summary": "Resumo",
|
||||
"field_tags": "Etikedoj",
|
||||
"field_title": "Titolo",
|
||||
"tab_authors": "Aŭtoroj",
|
||||
"tab_general": "Ĝenerala",
|
||||
"tab_tags": "Etikedoj",
|
||||
"tags_notice_multiple_edit": "Vi redaktas etikedojn de pluraj libroj. Tio ŝanĝos la etikedojn de ĉiu el tiuj libroj."
|
||||
},
|
||||
"edit_collection": {
|
||||
"button_cancel": "Nuligi",
|
||||
"button_confirm": "Konservi ŝanĝojn",
|
||||
"dialog_title": "Redakti kolekton",
|
||||
"field_manual_ordering": "Mana ordigo",
|
||||
"label_ordering": "Implicite, serioj en kolekto estas ordigitaj laŭ nomo. Vi povas ŝalti manan ordigon por difini vian propran ordon."
|
||||
},
|
||||
"edit_library": {
|
||||
"button_browse": "Foliumi",
|
||||
"button_cancel": "Nuligi",
|
||||
"button_confirm_add": "Aldoni",
|
||||
"button_confirm_edit": "Redakti",
|
||||
"dialog_title_add": "Aldoni Bibliotekon",
|
||||
"dialot_title_edit": "Redakti Bibliotekon",
|
||||
"field_import_barcode_isbn": "Strikodo de ISBN",
|
||||
"field_import_comicinfo_book": "Metadatenoj pri libro",
|
||||
"field_import_comicinfo_collections": "Kolektoj",
|
||||
"field_import_comicinfo_readlists": "Legolistoj",
|
||||
"field_import_comicinfo_series": "Metadatenoj pri serio",
|
||||
"field_import_epub_book": "Metadatenoj pri libro",
|
||||
"field_import_epub_series": "Metadatenoj pri serio",
|
||||
"field_import_local_artwork": "Lokaj bildoj",
|
||||
"field_name": "Nomo",
|
||||
"field_root_folder": "Radika dosierujo",
|
||||
"field_scanner_deep_scan": "Profunde skani",
|
||||
"field_scanner_force_directory_modified_time": "Devigi modifotempon de dosierujo",
|
||||
"file_browser_dialog_button_confirm": "Elekti",
|
||||
"file_browser_dialog_title": "La radika dosierujo de biblioteko",
|
||||
"label_import_barcode_isbn": "Enporti ISBN el strikodo",
|
||||
"label_import_comicinfo": "Enporti metadatenojn el CBR/CBZ-dosiero enhavanta dosieron ComicsInfo.xml",
|
||||
"label_import_epub": "Enporti metadatenojn el EPUB-dosieroj",
|
||||
"label_import_local": "Enporti lokajn bildojn",
|
||||
"label_scanner": "Skanilo",
|
||||
"tab_general": "Ĝenerala",
|
||||
"tab_options": "Opcioj"
|
||||
},
|
||||
"edit_readlist": {
|
||||
"button_cancel": "Nuligi",
|
||||
"button_confirm": "Konservi ŝanĝojn",
|
||||
"dialog_title": "Redakti legoliston",
|
||||
"field_name": "Nomo"
|
||||
},
|
||||
"edit_series": {
|
||||
"button_cancel": "Nuligi",
|
||||
"button_confirm": "Konservi ŝanĝojn",
|
||||
"dialog_title_multiple": "Redakti {count} serion | Redakti {count} seriojn",
|
||||
"dialog_title_single": "Redakti serion {series}",
|
||||
"field_age_rating": "Aĝolimo",
|
||||
"field_age_rating_error": "Aĝolimo devas esti ne negativa",
|
||||
"field_genres": "Ĝenroj",
|
||||
"field_language": "Lingvo",
|
||||
"field_publisher": "Eldonejo",
|
||||
"field_reading_direction": "Direkto de Legado",
|
||||
"field_sort_title": "Titolo por Ordigo",
|
||||
"field_status": "Stato",
|
||||
"field_summary": "Resumo",
|
||||
"field_tags": "Etikedoj",
|
||||
"field_title": "Titolo",
|
||||
"mixed": "MIKSITA",
|
||||
"tab_general": "Ĝenerala",
|
||||
"tab_tags": "Etikedoj",
|
||||
"tags_notice_multiple_edit": "Vi redaktas etikedojn de pluraj serioj. Tio ŝanĝos la aŭtorojn de ĉiu el tiuj serioj."
|
||||
},
|
||||
"edit_user": {
|
||||
"button_cancel": "Nuligi",
|
||||
"button_confirm": "Konservi ŝanĝojn",
|
||||
"dialog_title": "Redakti uzanton",
|
||||
"label_roles_for": "Roloj de {name}"
|
||||
},
|
||||
"edit_user_shared_libraries": {
|
||||
"button_cancel": "Nuligi",
|
||||
"button_confirm": "Konservi ŝanĝojn",
|
||||
"dialog_title": "Redakti komunajn bibliotekojn",
|
||||
"field_all_libraries": "Ĉiuj bibliotekoj",
|
||||
"label_shared_with": "Komuna kun {name}"
|
||||
},
|
||||
"file_browser": {
|
||||
"button_cancel": "Nuligi",
|
||||
"button_confirm_default": "Elekti",
|
||||
"dialog_title_default": "Dosieradministrilo",
|
||||
"parent_directory": "Patro"
|
||||
},
|
||||
"filename_chooser": {
|
||||
"button_choose": "Elekti",
|
||||
"field_destination_filename": "Cela dosiernomo",
|
||||
"label_source_filename": "Fonta Dosiernomo",
|
||||
"table": {
|
||||
"existing_file": "Ekzistanta Dosiero",
|
||||
"order": "Ordo"
|
||||
},
|
||||
"title": "Cela Dosiernomo"
|
||||
},
|
||||
"password_change": {
|
||||
"button_cancel": "Nuligi",
|
||||
"button_confirm": "Ŝanĝi pasvorton",
|
||||
"dialog_title": "Ŝanĝi pasvorton",
|
||||
"field_new_password": "Nova pasvorto",
|
||||
"field_new_password_error": "Nova pasvorto estas deviga.",
|
||||
"field_repeat_password": "Ripetu novan pasvorton",
|
||||
"field_repeat_password_error": "Pasvortoj devas esti identaj."
|
||||
},
|
||||
"series_picker": {
|
||||
"label_search_series": "Serĉi Serion",
|
||||
"title": "Elekti Serion"
|
||||
},
|
||||
"server_stop": {
|
||||
"button_cancel": "Nuligi",
|
||||
"button_confirm": "Ĉesigi",
|
||||
"confirmation_message": "Ĉu vi certe volas ĉesigi Komga?",
|
||||
"dialog_title": "Haltigi servilon"
|
||||
},
|
||||
"shortcut_help": {
|
||||
"label_description": "Priskribo",
|
||||
"label_key": "Ŝlosilo"
|
||||
},
|
||||
"transient_book_details": {
|
||||
"label_candidate": "Kandidato",
|
||||
"label_existing": "Ekzistanta",
|
||||
"label_format": "Formo",
|
||||
"label_name": "Nomo",
|
||||
"label_pages": "Paĝoj",
|
||||
"label_size": "Grando",
|
||||
"pages_table": {
|
||||
"filename": "Nomo de dosiero",
|
||||
"height": "Alto",
|
||||
"index": "Indekso",
|
||||
"media_type": "Tipo",
|
||||
"width": "Larĝo"
|
||||
},
|
||||
"title": "Detaloj pri Libro",
|
||||
"title_comparison": "Komparo inter Libroj"
|
||||
},
|
||||
"transient_book_viewer": {
|
||||
"label_candidate": "Kandidato",
|
||||
"label_existing": "Ekzistanta",
|
||||
"page_of_pages": "{page} el {pages}",
|
||||
"title": "Inspekti Libron",
|
||||
"title_comparison": "Komparo inter Libroj"
|
||||
}
|
||||
},
|
||||
"enums": {
|
||||
"copy_mode": {
|
||||
"HARDLINK": "Rekte Ligi/Kopii Dosierojn",
|
||||
"MOVE": "Movi Dosierojn"
|
||||
},
|
||||
"media_status": {
|
||||
"ERROR": "Eraro",
|
||||
"OUTDATED": "Ne ĝisdata",
|
||||
"READY": "Preta",
|
||||
"UNKNOWN": "Nekonata",
|
||||
"UNSUPPORTED": "Ne subtenata"
|
||||
},
|
||||
"reading_direction": {
|
||||
"LEFT_TO_RIGHT": "Demaldekstre dekstren",
|
||||
"RIGHT_TO_LEFT": "Dedekstre maldekstren",
|
||||
"VERTICAL": "Vertikala",
|
||||
"WEBTOON": "Retkomikso"
|
||||
},
|
||||
"series_status": {
|
||||
"ABANDONED": "Forlasita",
|
||||
"ENDED": "Finita",
|
||||
"HIATUS": "Paŭzinta",
|
||||
"ONGOING": "Eldonata"
|
||||
}
|
||||
},
|
||||
"error_codes": {
|
||||
"ERR_1000": "La dosiero ne estis atingebla por analizo",
|
||||
"ERR_1001": "La dosiertipo ne estas subtenata",
|
||||
"ERR_1002": "Ĉifrita RAR-arkivo ne estas subtenata",
|
||||
"ERR_1003": "Solida RAR-arkivo ne estas subtenata",
|
||||
"ERR_1004": "Plurvoluma RAR-arkivo ne estas subtenata",
|
||||
"ERR_1005": "Nekonata eraro pri analizado de libro",
|
||||
"ERR_1006": "Libro enhavas neniujn paĝojn",
|
||||
"ERR_1007": "Kelkaj rikordoj ne estis analizeblaj",
|
||||
"ERR_1008": "Nekonata eraro pri atingado de libro",
|
||||
"ERR_1009": "Jam ekzistas legolisto de tiu nomo",
|
||||
"ERR_1010": "Neniu kongrua libro en la legolisto",
|
||||
"ERR_1011": "Kongrua serio ne estas unika",
|
||||
"ERR_1012": "Neniu kongrua serio",
|
||||
"ERR_1013": "Kongrua numero de libro ne estas unika en serio",
|
||||
"ERR_1014": "Neniu kongrua numero de libro en serio",
|
||||
"ERR_1015": "Eraro pri enportado de legolisto ComicsRack"
|
||||
},
|
||||
"filter": {
|
||||
"age_rating": "aĝolimo",
|
||||
"age_rating_none": "Nenio",
|
||||
"genre": "ĝenro",
|
||||
"language": "lingvo",
|
||||
"library": "biblioteko",
|
||||
"publisher": "eldonejo",
|
||||
"release_date": "dato de eldono",
|
||||
"status": "stato",
|
||||
"tag": "etikedo",
|
||||
"unread": "Ne legita"
|
||||
},
|
||||
"filter_drawer": {
|
||||
"filter": "filtri",
|
||||
"sort": "ordigi"
|
||||
},
|
||||
"home": {
|
||||
"theme": "Etoso",
|
||||
"translation": "Traduko"
|
||||
},
|
||||
"library_navigation": {
|
||||
"browse": "Foliumi",
|
||||
"collections": "Kolektoj",
|
||||
"readlists": "Legolistoj"
|
||||
},
|
||||
"login": {
|
||||
"create_user_account": "Krei konton de uzanto",
|
||||
"login": "Saluti",
|
||||
"unclaimed_html": "Ĉi tiu Komga-servilo ankoraŭ ne aktivas; vi devas krei konton de uzanto por uzi ĝin.<br><br>Elektu <strong>retpoŝtan adreson</strong> kaj <strong>pasvorton</strong> kaj alklaku la butonon <strong>Krei konton de uzanto</strong>."
|
||||
},
|
||||
"media_analysis": {
|
||||
"comment": "Komento",
|
||||
"media_analysis": "Analizo",
|
||||
"media_type": "Tipo",
|
||||
"name": "Nomo",
|
||||
"size": "Grando",
|
||||
"status": "Stato",
|
||||
"url": "Reta adreso"
|
||||
},
|
||||
"menu": {
|
||||
"add_to_collection": "Aldoni en kolekton",
|
||||
"add_to_readlist": "Aldoni en legoliston",
|
||||
"analyze": "Analizi",
|
||||
"delete": "Forigi",
|
||||
"download_series": "Elŝuti serion",
|
||||
"edit": "Redakti",
|
||||
"edit_metadata": "Redakti metadatenojn",
|
||||
"mark_read": "Marki kiel legitan",
|
||||
"mark_unread": "Marki kiel ne legitan",
|
||||
"refresh_metadata": "Reŝargi metadatenojn",
|
||||
"scan_library_files": "Skani bibliotekajn dosierojn"
|
||||
},
|
||||
"navigation": {
|
||||
"home": "Hejmo",
|
||||
"libraries": "Bibliotekoj",
|
||||
"logout": "Adiaŭi"
|
||||
},
|
||||
"page_not_found": {
|
||||
"go_back_to_home_page": "Hejmen",
|
||||
"page_does_not_exist": "La serĉata paĝo ne ekzistas.",
|
||||
"page_not_found": "La paĝo ne troviĝis"
|
||||
},
|
||||
"read_more": {
|
||||
"less": "Montri malpli",
|
||||
"more": "Montri pli"
|
||||
},
|
||||
"readlists_expansion_panel": {
|
||||
"manage_readlist": "Mastrumi legoliston",
|
||||
"title": "Legolisto {name}"
|
||||
},
|
||||
"search": {
|
||||
"no_results": "La serĉo ne donis rezultojn",
|
||||
"search": "Serĉi",
|
||||
"search_for_something_else": "Provu serĉi ion alian",
|
||||
"search_results_for": "Serĉi \"{name}\" en rezultoj"
|
||||
},
|
||||
"searchbox": {
|
||||
"no_results": "Neniuj rezultoj",
|
||||
"search_all": "Serĉi ĉion…"
|
||||
},
|
||||
"server": {
|
||||
"server_management": {
|
||||
"button_shutdown": "Haltigi",
|
||||
"section_title": "Administrado de Servilo"
|
||||
},
|
||||
"tab_title": "Servilo"
|
||||
},
|
||||
"server_settings": {
|
||||
"server_settings": "Agordoj pri Servilo"
|
||||
},
|
||||
"settings_user": {
|
||||
"edit_shared_libraries": "Redakti komunajn bibliotekojn",
|
||||
"edit_user": "Redakti uzanton",
|
||||
"role_administrator": "Administranto",
|
||||
"role_user": "Uzanto"
|
||||
},
|
||||
"sort": {
|
||||
"books_count": "Nombro de libroj",
|
||||
"date_added": "Dato de aldono",
|
||||
"date_updated": "Dato de ĝisdatigo",
|
||||
"file_name": "Nomo de dosiero",
|
||||
"file_size": "Grando de dosiero",
|
||||
"folder_name": "Nomo de dosierujo",
|
||||
"name": "Nomo",
|
||||
"number": "Numero",
|
||||
"release_date": "Dato de eldono"
|
||||
},
|
||||
"theme": {
|
||||
"dark": "Malhela",
|
||||
"light": "Hela",
|
||||
"system": "Sistema"
|
||||
},
|
||||
"user_roles": {
|
||||
"ADMIN": "Administranto",
|
||||
"FILE_DOWNLOAD": "Elŝutanto de dosieroj",
|
||||
"PAGE_STREAMING": "Popaĝa leganto",
|
||||
"USER": "Uzanto"
|
||||
},
|
||||
"users": {
|
||||
"users": "Uzantoj"
|
||||
},
|
||||
"welcome": {
|
||||
"add_library": "Aldoni bibliotekon",
|
||||
"no_libraries_yet": "Ankoraŭ neniuj bibliotekoj estis aldonitaj!",
|
||||
"welcome_message": "Bonvenon al Komga"
|
||||
}
|
||||
}
|
||||
|
|
@ -5,7 +5,7 @@
|
|||
},
|
||||
"dataTable": {
|
||||
"itemsPerPageText": "Filas por página:",
|
||||
"sortBy": "Ordenado por"
|
||||
"sortBy": "Ordenar por"
|
||||
},
|
||||
"fileInput": {
|
||||
"counter": "{0} archivos",
|
||||
|
|
@ -15,297 +15,509 @@
|
|||
},
|
||||
"account_settings": {
|
||||
"account_settings": "Configuración de cuenta",
|
||||
"change_password": "Cambiar contraseña"
|
||||
"change_password": "cambiar contraseña"
|
||||
},
|
||||
"author_roles": {
|
||||
"colorist": "coloristas",
|
||||
"cover": "cobertura",
|
||||
"cover": "cubierta",
|
||||
"editor": "editores",
|
||||
"inker": "entintadores",
|
||||
"letterer": "letristas",
|
||||
"penciller": "dibujantes",
|
||||
"writer": "escritores"
|
||||
},
|
||||
"book_card": {
|
||||
"error": "Error",
|
||||
"unknown": "Análisis pendiente",
|
||||
"unsupported": "No soportado"
|
||||
},
|
||||
"bookreader": {
|
||||
"beginning_of_book": "Estás al inicio del libro.",
|
||||
"changing_reading_direction": "Cambiar dirección de lectura a",
|
||||
"cycling_page_layout": "Disposición de la página",
|
||||
"cycling_scale": "Formato de la Página",
|
||||
"cycling_page_layout": "Cambiar disposición de página",
|
||||
"cycling_scale": "Cambiar escalado de página",
|
||||
"cycling_side_padding": "Cambiar el borde lateral",
|
||||
"end_of_book": "Had llegado al final del libro.",
|
||||
"from_series_metadata": "desde serie metadatos",
|
||||
"move_next": "Has click o presiona siguiente de nuevo para moverte al siguiente libro.",
|
||||
"move_next_exit": "Has click o presiona siguiente de nuevo para salir del lector.",
|
||||
"move_previous": "Has click o presiona anterior de nuevo para moverte al libro anterior.",
|
||||
"download_current_page": "Descargar página actual",
|
||||
"end_of_book": "Has llegado al final del libro.",
|
||||
"from_series_metadata": "desde metadatos de serie",
|
||||
"move_next": "Haz clic o presiona «Siguiente» de nuevo para moverte al siguiente libro.",
|
||||
"move_next_exit": "Haz clic o presiona «Siguiente» de nuevo para salir del lector.",
|
||||
"move_previous": "Haz clic o presiona «Anterior» de nuevo para moverte al libro anterior.",
|
||||
"paged_reader_layout": {
|
||||
"double": "doble página",
|
||||
"double_no_cover": "Doble página (sin cobertura)",
|
||||
"double": "Doble página",
|
||||
"double_no_cover": "Doble página (sin portada)",
|
||||
"single": "Página única"
|
||||
},
|
||||
"reader_settings": "Configuraciones del lector",
|
||||
"reader_settings": "Configuración de Lector",
|
||||
"scale_type": {
|
||||
"continuous_original": "Original",
|
||||
"continuous_width": "Estiramiento horizontal",
|
||||
"height": "Estiramiento vertical",
|
||||
"continuous_width": "Ajustar a ancho",
|
||||
"height": "Ajustar a altura",
|
||||
"original": "Original",
|
||||
"screen": "Pantalla",
|
||||
"width": "Estiramiento horizontal"
|
||||
"width": "Ajustar a ancho"
|
||||
},
|
||||
"settings": {
|
||||
"animate_page_transitions": "Animar transición de página",
|
||||
"animate_page_transitions": "Animar las transiciones de página",
|
||||
"background_color": "Color de fondo",
|
||||
"background_colors": {
|
||||
"black": "Oscuro",
|
||||
"black": "Negro",
|
||||
"white": "Blanco"
|
||||
},
|
||||
"display": "Pantalla",
|
||||
"display": "Visualización",
|
||||
"general": "General",
|
||||
"gestures": "Gestos",
|
||||
"page_layout": "Diseño de página",
|
||||
"paged": "Opción de lector",
|
||||
"page_layout": "Disposición de página",
|
||||
"paged": "Opciones de Paginado",
|
||||
"reading_mode": "Modo de lectura",
|
||||
"scale_type": "Tipo de escala",
|
||||
"scale_type": "Tipo de escalado",
|
||||
"side_padding": "Borde lateral",
|
||||
"side_padding_none": "Ningún",
|
||||
"webtoon": "Opciones del lector Webtoon"
|
||||
"side_padding_none": "Ninguno",
|
||||
"webtoon": "Opciones del Lector de Webtoon"
|
||||
},
|
||||
"shortcuts": {
|
||||
"close": "Cerrar",
|
||||
"cycle_page_layout": "Cambiar la disposición de páginas",
|
||||
"cycle_scale": "Escalado de páginas",
|
||||
"cycle_page_layout": "Cambiar disposición de página",
|
||||
"cycle_scale": "Cambiar escalado de página",
|
||||
"cycle_side_padding": "Cambiar el borde lateral",
|
||||
"first_page": "Primera página",
|
||||
"last_page": "Última página",
|
||||
"left_to_right": "De izquierda a derecha",
|
||||
"left_to_right": "Izquierda a Derecha",
|
||||
"menus": "Menús",
|
||||
"next_page": "Página siguiente",
|
||||
"previous_page": "Página anterior",
|
||||
"reader_navigation": "Navegador en el lector",
|
||||
"right_to_left": "De derecha a izquierda",
|
||||
"settings": "Configuración",
|
||||
"reader_navigation": "Navegación en Lector",
|
||||
"right_to_left": "Derecha a Izquierda",
|
||||
"settings": "Ajustes",
|
||||
"show_hide_help": "Mostrar/ocultar ayuda",
|
||||
"show_hide_settings": "Mostrar/ocultar configuración",
|
||||
"show_hide_thumbnails": "Mostrar/ocultar imágenes en miniatura",
|
||||
"show_hide_toolbars": "Mostrar/ocultar barra de herramientas",
|
||||
"show_hide_settings": "Mostrar/ocultar menú de configuración",
|
||||
"show_hide_thumbnails": "Mostrar/ocultar miniaturas",
|
||||
"show_hide_toolbars": "Mostrar/ocultar barras de herramientas",
|
||||
"vertical": "Vertical",
|
||||
"webtoon": "Webtoon"
|
||||
}
|
||||
},
|
||||
"browse_book": {
|
||||
"comment": "COMENTARIO",
|
||||
"download_file": "Descargar el archivo",
|
||||
"file": "Archivo",
|
||||
"format": "FORMAT",
|
||||
"download_file": "Descargar archivo",
|
||||
"file": "ARCHIVO",
|
||||
"format": "FORMATO",
|
||||
"isbn": "ISBN",
|
||||
"navigation_within_readlist": "Navegación en la lista: {name}",
|
||||
"read_book": "Leer el libro",
|
||||
"size": "TALLA"
|
||||
"read_book": "Leer libro",
|
||||
"size": "TAMAÑO"
|
||||
},
|
||||
"browse_collection": {
|
||||
"edit_collection": "Editar la colección",
|
||||
"edit_elements": "Cambiar los elementos",
|
||||
"manual_ordering": "clasificación manual"
|
||||
"edit_collection": "Editar colección",
|
||||
"edit_elements": "Editar elementos",
|
||||
"manual_ordering": "orden manual"
|
||||
},
|
||||
"browse_readlist": {
|
||||
"edit_elements": "Cambiar los elementos",
|
||||
"edit_readlist": "Modificar lista de lectura"
|
||||
"edit_elements": "Editar elementos",
|
||||
"edit_readlist": "Editar lista de lectura"
|
||||
},
|
||||
"browse_series": {
|
||||
"earliest_year_from_release_dates": "Este es el año más antiguo que figura entre unos de los libros de la serie",
|
||||
"series_no_summary": "Esta serie no tiene resumen, hemos seleccionado uno para usted !",
|
||||
"earliest_year_from_release_dates": "Este es el año de lanzamiento del libro más antiguo de la serie",
|
||||
"series_no_summary": "Esta serie no tiene resumen, ¡así que escogimos uno para usted!",
|
||||
"summary_from_book": "Resumen del libro {number}:"
|
||||
},
|
||||
"collections_expansion_panel": {
|
||||
"manage_collection": "Gestionar la colección",
|
||||
"title": "{name} colección"
|
||||
"manage_collection": "Administrar colección",
|
||||
"title": "Colección {name}"
|
||||
},
|
||||
"common": {
|
||||
"all_libraries": "Todas las bibliotecas",
|
||||
"all_libraries": "Todas las Bibliotecas",
|
||||
"books": "Libros",
|
||||
"books_n": "Sin libros | 1 libro | {count} libros",
|
||||
"cancel": "Cancelar",
|
||||
"close": "Cerrar",
|
||||
"collections": "Colecciones",
|
||||
"create": "Crear",
|
||||
"delete": "Borrar",
|
||||
"delete": "Eliminar",
|
||||
"download": "Descargar",
|
||||
"email": "Email",
|
||||
"filter_no_matches": "Los filtros activos no tienen ninguna coincidencia",
|
||||
"email": "Correo electrónico",
|
||||
"filter_no_matches": "El filtro activo no tiene coincidencias",
|
||||
"genre": "Género",
|
||||
"go_to_library": "Ir a la biblioteca",
|
||||
"locale_name": "Español",
|
||||
"locale_rtl": "false",
|
||||
"n_selected": "{count} seleccionado",
|
||||
"n_selected": "Nada seleccionado | 1 seleccionado | {count} seleccionados",
|
||||
"nothing_to_show": "Nada que mostrar",
|
||||
"pages": "páginas",
|
||||
"pages_n": "Sin páginas | 1 página | {count} páginas",
|
||||
"password": "Contraseña",
|
||||
"publisher": "Editor",
|
||||
"read": "Leer",
|
||||
"readlists": "Listas de lectura",
|
||||
"readlists": "Listas de Lectura",
|
||||
"required": "Requerido",
|
||||
"roles": "Rol",
|
||||
"series": "Series",
|
||||
"roles": "Funciones",
|
||||
"series": "Serie",
|
||||
"tags": "Etiquetas",
|
||||
"use_filter_panel_to_change_filter": "Uso el panel de filtros para cambiar los filtros activos",
|
||||
"use_filter_panel_to_change_filter": "Use el panel de filtros para cambiar el filtro activo",
|
||||
"year": "año"
|
||||
},
|
||||
"dashboard": {
|
||||
"keep_reading": "Seguir leyendo",
|
||||
"keep_reading": "Seguir Leyendo",
|
||||
"on_deck": "En curso",
|
||||
"recently_added_books": "Libros recién agregados",
|
||||
"recently_added_series": "Series agregadas recientemente",
|
||||
"recently_updated_series": "Series recientemente actualizadas"
|
||||
"recently_added_books": "Libros Recientemente Agregados",
|
||||
"recently_added_series": "Series Recientemente Agregadas",
|
||||
"recently_updated_series": "Series Recientemente Actualizadas"
|
||||
},
|
||||
"data_import": {
|
||||
"book_number": "Número de libro: {name}",
|
||||
"book_series": "Serie: {name}",
|
||||
"button_import": "Importar",
|
||||
"comicrack_preambule_html": "Puedes importar Listas de Lectura de ComicRack en formato <code>.cbl</code>.<br>Komga intentará hacer coincidir la serie y número de libro suministrados con los existentes en tus bibliotecas.",
|
||||
"field_files_label": "Listas de Lectura de ComicRack (.cbl)",
|
||||
"import_read_lists": "Importar Listas de Lectura",
|
||||
"imported_as": "Importado como {name}",
|
||||
"results_preambule": "El resultado de la importación se muestra abajo. También puedes revisar los libros de cada archivo suministrado para los cuales no se encontró coincidencia.",
|
||||
"size_limit": "El tamaño debe ser menor a {size} MB",
|
||||
"tab_title": "Importar Datos"
|
||||
},
|
||||
"dialog": {
|
||||
"add_to_collection": {
|
||||
"button_create": "Crear",
|
||||
"card_collection_subtitle": "No series | 1 series | {count} series",
|
||||
"dialog_title": "Añadir a la colección",
|
||||
"card_collection_subtitle": "Sin series | 1 serie | {count} series",
|
||||
"dialog_title": "Añadir a colección",
|
||||
"field_search_create": "Buscar o crear una colección",
|
||||
"field_search_create_error": "Ya existe una colección con este nombre",
|
||||
"label_no_matching_collection": "Ninguna colección correspondiente"
|
||||
"label_no_matching_collection": "Ninguna colección coincide"
|
||||
},
|
||||
"add_to_readlist": {
|
||||
"button_create": "Crear",
|
||||
"card_readlist_subtitle": "No book | 1 book | {count} books",
|
||||
"dialog_title": "Añadir a la lista de lectura",
|
||||
"card_readlist_subtitle": "Sin libros | 1 libro | {count} libros",
|
||||
"dialog_title": "Añadir a lista de lectura",
|
||||
"field_search_create": "Buscar o crear una lista de lectura",
|
||||
"field_search_create_error": "Ya existe una lista de lectura con este nombre",
|
||||
"label_no_matching_readlist": "No hay lista de lectura correspondiente"
|
||||
"label_no_matching_readlist": "Ninguna lista de lectura coincide"
|
||||
},
|
||||
"add_user": {
|
||||
"button_cancel": "Anular",
|
||||
"button_confirm": "Añadir",
|
||||
"dialog_title": "Añadir un usuario",
|
||||
"field_email": "Email",
|
||||
"field_email_error": "Debe ser una dirección de email válida",
|
||||
"field_password": "Código de acceso",
|
||||
"button_cancel": "Cancelar",
|
||||
"button_confirm": "Agregar",
|
||||
"dialog_title": "Añadir usuario",
|
||||
"field_email": "Correo electrónico",
|
||||
"field_email_error": "Debe ser una dirección de correo electrónico válida",
|
||||
"field_password": "Contraseña",
|
||||
"field_role_administrator": "Administrador",
|
||||
"field_role_file_download": "Descarga de archivos",
|
||||
"field_role_page_streaming": "Lectura en línea",
|
||||
"label_roles": "Funciones"
|
||||
},
|
||||
"delete_collection": {
|
||||
"button_cancel": "Anular",
|
||||
"button_confirm": "Suprimir",
|
||||
"confirm_delete": "Si, suprimir la colección \"{name}\"",
|
||||
"dialog_title": "Suprimir la colección",
|
||||
"warning_html": "La colección <b>{name}</b> será removida de este servidor. Sus archivos nunca serán eliminados. Esta acción <b> es irreversible </b> ¿Desea continuar?"
|
||||
"button_cancel": "Cancelar",
|
||||
"button_confirm": "Eliminar",
|
||||
"confirm_delete": "Sí, eliminar la colección «{name}»",
|
||||
"dialog_title": "Eliminar Colección",
|
||||
"warning_html": "La colección <b>{name}</b> será removida de este servidor. Los archivos no serán eliminados. Esta acción <b>es irreversible</b> ¿Desea continuar?"
|
||||
},
|
||||
"delete_library": {
|
||||
"button_cancel": "Anular",
|
||||
"button_confirm": "Suprimir",
|
||||
"confirm_delete": "Si, eliminar la biblioteca \"{name}\"",
|
||||
"title": "Eliminar la biblioteca",
|
||||
"warning_html": "La colección <b>{name}</b> será removida de este servidor. Sus archivos nunca serán eliminados. Esta acción <b> es irreversible </b> ¿Desea continuar?"
|
||||
"button_cancel": "Cancelar",
|
||||
"button_confirm": "Eliminar",
|
||||
"confirm_delete": "Sí, eliminar la biblioteca «{name}»",
|
||||
"title": "Eliminar Biblioteca",
|
||||
"warning_html": "La biblioteca <b>{name}</b> será removida de este servidor. Los archivos no serán eliminados. Esta acción <b>es irreversible</b> ¿Desea continuar?"
|
||||
},
|
||||
"delete_readlist": {
|
||||
"button_cancel": "Anular",
|
||||
"button_confirm": "Suprimir",
|
||||
"confirm_delete": "Si, eliminar la lista de lectura \"{name}\"",
|
||||
"dialog_title": "Eliminar la lista de lectura",
|
||||
"warning_html": "La lista de lecura <b>{name}</b> será removida de este servidor. Sus archivos nunca serán eliminados. Esta acción <b>es irreversible</b> ¿Desea continuar?"
|
||||
"button_cancel": "Cancelar",
|
||||
"button_confirm": "Eliminar",
|
||||
"confirm_delete": "Sí, eliminar la lista de lectura «{name}»",
|
||||
"dialog_title": "Eliminar Lista de lectura",
|
||||
"warning_html": "La lista de lectura <b>{name}</b> será removida de este servidor. Los archivos no serán eliminados. Esta acción <b>es irreversible</b> ¿Desea continuar?"
|
||||
},
|
||||
"delete_user": {
|
||||
"button_cancel": "Anular",
|
||||
"button_confirm": "Suprimir",
|
||||
"confirm_delete": "Si, Suprimir al usuario \"{name}\"",
|
||||
"dialog_title": "Suprimir el usuario",
|
||||
"warning_html": "El usuario <b>{name}</b> será removido de este servidor. Esta acción<b>es irreversible</b> ¿Desea continuar?"
|
||||
"button_cancel": "Cancelar",
|
||||
"button_confirm": "Eliminar",
|
||||
"confirm_delete": "Sí, eliminar el usuario «{name}»",
|
||||
"dialog_title": "Eliminar usuario",
|
||||
"warning_html": "El usuario <b>{name}</b> será removido de este servidor. Esta acción <b>es irreversible</b> ¿Desea continuar?"
|
||||
},
|
||||
"edit_books": {
|
||||
"authors_notice_multiple_edit": "Estás editando autores para varios libros. Esto suprimira a los autores existentes para cada uno de los libros.",
|
||||
"button_cancel": "Anular",
|
||||
"button_confirm": "Guardar los cambios",
|
||||
"dialog_title_multiple": "Cambiar {count} livre | Cambiar {count} livros",
|
||||
"dialog_title_single": "Cambiar {book}",
|
||||
"authors_notice_multiple_edit": "Estás editando los autores de varios libros. Esto sobreescribirá los autores existentes para cada libro.",
|
||||
"button_cancel": "Cancelar",
|
||||
"button_confirm": "Guardar cambios",
|
||||
"dialog_title_multiple": "Editar {count} libro | Editar {count} libros",
|
||||
"dialog_title_single": "Editar {book}",
|
||||
"field_isbn": "ISBN",
|
||||
"field_isbn_error": "Debe ser un código ISBN 13 válido",
|
||||
"field_number": "Numero",
|
||||
"field_number_sort": "Ordenar por números",
|
||||
"field_number": "Número",
|
||||
"field_number_sort": "Ordenar por Número",
|
||||
"field_number_sort_hint": "Puede utilizar números decimales",
|
||||
"field_release_date": "Fecha de publicación",
|
||||
"field_release_date_error": "Debe ser una fecha en formato YYYY-MM-DD",
|
||||
"field_release_date": "Fecha de Publicación",
|
||||
"field_release_date_error": "Debe ser una fecha válida en formato AAAA-MM-DD",
|
||||
"field_summary": "Resumen",
|
||||
"field_tags": "Etiquetas",
|
||||
"field_title": "Título",
|
||||
"tab_authors": "Autores",
|
||||
"tab_general": "General",
|
||||
"tab_tags": "Etiquetas",
|
||||
"tags_notice_multiple_edit": "Usted está cambiando las etiquetas para varios libros. Esto va a suprimir las etiquetas por cada livro."
|
||||
"tags_notice_multiple_edit": "Estás editando las etiquetas de varios libros. Esto sobreescribirá las etiquetas existentes en cada uno."
|
||||
},
|
||||
"edit_collection": {
|
||||
"button_cancel": "Anular",
|
||||
"button_confirm": "Guardar los cambios",
|
||||
"dialog_title": "Editar la colección",
|
||||
"field_manual_ordering": "Clasificación manual",
|
||||
"label_ordering": "De forma predeterminada, las series de una colección se ordenarán por nombre. Puede habilitar la ordenación manual para definir su propria ordenación."
|
||||
"button_cancel": "Cancelar",
|
||||
"button_confirm": "Guardar cambios",
|
||||
"dialog_title": "Editar colección",
|
||||
"field_manual_ordering": "Orden manual",
|
||||
"label_ordering": "Por defecto, las series de una colección se ordenan por nombre. Puedes habilitar el orden manual para definir tu propio orden."
|
||||
},
|
||||
"edit_library": {
|
||||
"button_browse": "Navegar",
|
||||
"button_cancel": "Anular",
|
||||
"button_confirm_add": "Añadir",
|
||||
"button_browse": "Explorar",
|
||||
"button_cancel": "Cancelar",
|
||||
"button_confirm_add": "Agregar",
|
||||
"button_confirm_edit": "Editar",
|
||||
"dialog_title_add": "Añadir biblioteca",
|
||||
"dialot_title_edit": "Modificar la biblioteca",
|
||||
"dialog_title_add": "Agregar Biblioteca",
|
||||
"dialot_title_edit": "Modificar Biblioteca",
|
||||
"field_import_barcode_isbn": "Código de barras ISBN",
|
||||
"field_import_comicinfo_book": "Metadatos del libro",
|
||||
"field_import_comicinfo_book": "Metadatos de libro",
|
||||
"field_import_comicinfo_collections": "Colecciones",
|
||||
"field_import_comicinfo_readlists": "Playlists",
|
||||
"field_import_comicinfo_series": "Metadatos de series",
|
||||
"field_import_epub_book": "Metadatos del libro",
|
||||
"field_import_epub_series": "Metadatos de series",
|
||||
"field_import_local_artwork": "Cobertura local",
|
||||
"field_import_comicinfo_readlists": "Listas de lectura",
|
||||
"field_import_comicinfo_series": "Metadatos de serie",
|
||||
"field_import_epub_book": "Metadatos de libro",
|
||||
"field_import_epub_series": "Metadatos de serie",
|
||||
"field_import_local_artwork": "Portada local",
|
||||
"field_name": "Nombre",
|
||||
"field_root_folder": "Directorio raíz",
|
||||
"field_root_folder": "Carpeta raíz",
|
||||
"field_scanner_deep_scan": "Análisis en profundidad",
|
||||
"field_scanner_force_directory_modified_time": "Forzar hora de edición del directorio",
|
||||
"file_browser_dialog_button_confirm": "Seleccionar",
|
||||
"file_browser_dialog_title": "directorio raíz de la librería",
|
||||
"label_import_barcode_isbn": "Importar ISBN con código de barras",
|
||||
"file_browser_dialog_button_confirm": "Elegir",
|
||||
"file_browser_dialog_title": "Directorio raíz de librería",
|
||||
"label_import_barcode_isbn": "Importar ISBN en código de barras",
|
||||
"label_import_comicinfo": "Importar metadatos para archivos CBR/CBZ que contengan un archivo ComicInfo.xml",
|
||||
"label_import_epub": "Importar metadatos de archivos EPUB",
|
||||
"label_import_local": "Importar medios de comunicación locales",
|
||||
"label_import_epub": "Importar metadatos desde archivos EPUB",
|
||||
"label_import_local": "Importar medios locales",
|
||||
"label_scanner": "Escáner",
|
||||
"tab_general": "General",
|
||||
"tab_options": "Opciones"
|
||||
},
|
||||
"edit_readlist": {
|
||||
"button_cancel": "Anular",
|
||||
"button_confirm": "Guardar los cambios",
|
||||
"dialog_title": "Modificar la playlist",
|
||||
"button_cancel": "Cancelar",
|
||||
"button_confirm": "Guardar cambios",
|
||||
"dialog_title": "Modificar lista de lectura",
|
||||
"field_name": "Nombre"
|
||||
},
|
||||
"edit_series": {
|
||||
"button_cancel": "Anular",
|
||||
"button_confirm": "Guardar los cambios",
|
||||
"dialog_title_multiple": "Rectifica {count} la serie | Rectifica {count} las series",
|
||||
"dialog_title_single": "Rectifica {series}",
|
||||
"button_cancel": "Cancelar",
|
||||
"button_confirm": "Guardar cambios",
|
||||
"dialog_title_multiple": "Editar {count} serie | Editar {count} series",
|
||||
"dialog_title_single": "Editar {series}",
|
||||
"field_age_rating": "Edad mínima",
|
||||
"field_age_rating_error": "La edad mínima debe ser superior o igual a 0",
|
||||
"field_genres": "Género",
|
||||
"field_language": "Lengua",
|
||||
"field_genres": "Géneros",
|
||||
"field_language": "Idioma",
|
||||
"field_publisher": "Editor",
|
||||
"field_reading_direction": "Dirección de lectura",
|
||||
"field_sort_title": "Título para la ordenación",
|
||||
"field_status": "Estatuto",
|
||||
"field_sort_title": "Título para el orden",
|
||||
"field_status": "Estado",
|
||||
"field_summary": "Resumen",
|
||||
"field_tags": "Etiquetas",
|
||||
"field_title": "Título",
|
||||
"mixed": "MIXTO",
|
||||
"tab_general": "General",
|
||||
"tab_tags": "Etiquetas",
|
||||
"tags_notice_multiple_edit": "Está editando etiquetas para varias series. Esto sobrescribirá las etiquetas de cada una de las series."
|
||||
"tags_notice_multiple_edit": "Estás editando las etiquetas de varias series. Esto sobreescribirá las etiquetas existentes en cada una."
|
||||
},
|
||||
"edit_user": {
|
||||
"button_cancel": "Cancelar",
|
||||
"button_confirm": "Guardar cambios",
|
||||
"dialog_title": "Editar usuario",
|
||||
"label_roles_for": "Funciones para {name}"
|
||||
},
|
||||
"edit_user_shared_libraries": {
|
||||
"button_cancel": "Cancelar",
|
||||
"button_confirm": "Guardar cambios",
|
||||
"dialog_title": "Editar bibliotecas compartidas",
|
||||
"field_all_libraries": "Todas las bibliotecas",
|
||||
"label_shared_with": "Compartida con {name}"
|
||||
},
|
||||
"file_browser": {
|
||||
"button_cancel": "Cancelar",
|
||||
"button_confirm_default": "Elegir",
|
||||
"dialog_title_default": "Explorador de Archivos",
|
||||
"parent_directory": "Padre"
|
||||
},
|
||||
"password_change": {
|
||||
"button_cancel": "Cancelar",
|
||||
"button_confirm": "Cambiar contraseña",
|
||||
"dialog_title": "Cambiar contraseña",
|
||||
"field_new_password": "Contraseña nueva",
|
||||
"field_new_password_error": "Se requiere una contraseña nueva.",
|
||||
"field_repeat_password": "Confirmar nueva contraseña",
|
||||
"field_repeat_password_error": "Las contraseñas deben ser idénticas."
|
||||
},
|
||||
"server_stop": {
|
||||
"confirmation_message": "¿Estás seguro de querer apagar Komga?"
|
||||
"button_cancel": "Cancelar",
|
||||
"button_confirm": "Detener",
|
||||
"confirmation_message": "¿Estás seguro de que quieres detener Komga?",
|
||||
"dialog_title": "Apagar servidor"
|
||||
},
|
||||
"shortcut_help": {
|
||||
"label_description": "Descripción",
|
||||
"label_key": "Tecla"
|
||||
}
|
||||
},
|
||||
"enums": {
|
||||
"media_status": {
|
||||
"ERROR": "Error",
|
||||
"OUTDATED": "Desactualizado",
|
||||
"READY": "Listo",
|
||||
"UNKNOWN": "Desconocido",
|
||||
"UNSUPPORTED": "No soportado"
|
||||
},
|
||||
"reading_direction": {
|
||||
"LEFT_TO_RIGHT": "De izquierda a derecha",
|
||||
"RIGHT_TO_LEFT": "De derecha a izquierda",
|
||||
"VERTICAL": "Vertical",
|
||||
"WEBTOON": "Webtoon"
|
||||
},
|
||||
"series_status": {
|
||||
"ABANDONED": "Abandonado",
|
||||
"ENDED": "Finalizada",
|
||||
"HIATUS": "En hiato",
|
||||
"ONGOING": "En curso"
|
||||
}
|
||||
},
|
||||
"error_codes": {
|
||||
"ERR_1000": "No se pudo acceder al archivo durante el análisis",
|
||||
"ERR_1001": "Tipo de medios no soportado",
|
||||
"ERR_1002": "Archivos RAR encriptados no están soportados",
|
||||
"ERR_1003": "Archivos RAR sólidos no están soportados",
|
||||
"ERR_1004": "Archivos RAR multi-volúmen no están soportados",
|
||||
"ERR_1005": "Error desconocido durante análisis de libro",
|
||||
"ERR_1006": "El libro no contiene ninguna página",
|
||||
"ERR_1007": "Algunas entradas no pudieron ser analizadas",
|
||||
"ERR_1008": "Error desconocido al obtener las entradas del libro",
|
||||
"ERR_1009": "Ya existe una lista de lectura con este nombre",
|
||||
"ERR_1010": "No se encontró coincidencia para ningún libro dentro de la lista de lectura",
|
||||
"ERR_1011": "No existe una única coincidencia para la serie",
|
||||
"ERR_1012": "No existe coincidencia para la serie",
|
||||
"ERR_1013": "No existe una única coincidencia para el número de libro dentro de la serie",
|
||||
"ERR_1014": "No existe coincidencia para el número de libro dentro de la serie",
|
||||
"ERR_1015": "Error al deserializar la Lista de lectura ComicRack"
|
||||
},
|
||||
"filter": {
|
||||
"age_rating": "edad mínima",
|
||||
"age_rating_none": "Ninguna",
|
||||
"genre": "género",
|
||||
"language": "idioma",
|
||||
"library": "biblioteca",
|
||||
"publisher": "editor",
|
||||
"release_date": "fecha de lanzamiento",
|
||||
"status": "estado",
|
||||
"tag": "etiqueta",
|
||||
"unread": "Sin leer"
|
||||
},
|
||||
"filter_drawer": {
|
||||
"filter": "filtro",
|
||||
"sort": "orden"
|
||||
},
|
||||
"home": {
|
||||
"theme": "Tema",
|
||||
"translation": "Traducción"
|
||||
},
|
||||
"library_navigation": {
|
||||
"browse": "Explorar",
|
||||
"collections": "Colecciones",
|
||||
"readlists": "Listas de lectura"
|
||||
},
|
||||
"login": {
|
||||
"unclaimed_html": "Este Komga server no está respondiendo, Primero necesitas crear una cuenta de usuario para poderse conectar.<br><br>Elegir un <strong>email</strong> y<strong>password</strong> y haga clic en <strong>Crear una cuenta de usuario</strong>."
|
||||
"create_user_account": "Crear cuenta de usuario",
|
||||
"login": "Iniciar sesión",
|
||||
"unclaimed_html": "Este servidor Komga no está activo aún, debes crear una cuenta de usuario para poder acceder a él.<br><br>Escoge un <strong>correo eleactrónico</strong> y <strong>contraseña</strong>, y haz clic en <strong>Crear cuenta de usuario</strong>."
|
||||
},
|
||||
"media_analysis": {
|
||||
"comment": "Comentario",
|
||||
"media_analysis": "Análisis de medios",
|
||||
"media_type": "Tipo de medio",
|
||||
"name": "Nombre",
|
||||
"size": "Tamaño",
|
||||
"status": "Estado",
|
||||
"url": "URL"
|
||||
},
|
||||
"menu": {
|
||||
"add_to_collection": "Añadir a colección",
|
||||
"add_to_readlist": "Añadir a lista de lectura",
|
||||
"analyze": "Analizar",
|
||||
"delete": "Eliminar",
|
||||
"download_series": "Descargar serie",
|
||||
"edit": "Editar",
|
||||
"edit_metadata": "Editar metadatos",
|
||||
"mark_read": "Marcar como leído",
|
||||
"mark_unread": "Marcar como no leído",
|
||||
"refresh_metadata": "Actualizar metadatos",
|
||||
"scan_library_files": "Escanear archivos de biblioteca"
|
||||
},
|
||||
"navigation": {
|
||||
"home": "Inicio",
|
||||
"libraries": "Bibliotecas",
|
||||
"logout": "Cerrar sesión"
|
||||
},
|
||||
"page_not_found": {
|
||||
"go_back_to_home_page": "Volver a la página principal",
|
||||
"page_does_not_exist": "La página que buscas no existe.",
|
||||
"page_not_found": "No se encontró la página"
|
||||
},
|
||||
"read_more": {
|
||||
"less": "Leer menos",
|
||||
"more": "Leer más"
|
||||
},
|
||||
"readlists_expansion_panel": {
|
||||
"manage_readlist": "Administrar lista de lectura",
|
||||
"title": "Lista de lectura {name}"
|
||||
},
|
||||
"search": {
|
||||
"no_results": "La búsqueda no arrojó resultados",
|
||||
"search": "Buscar",
|
||||
"search_for_something_else": "Intenta buscar otra cosa",
|
||||
"search_results_for": "Resultados de búsqueda para «{name}»"
|
||||
},
|
||||
"searchbox": {
|
||||
"no_results": "No hay resultados",
|
||||
"search_all": "Buscar todo…"
|
||||
},
|
||||
"server": {
|
||||
"server_management": {
|
||||
"button_shutdown": "Apagar",
|
||||
"section_title": "Administración de Servidor"
|
||||
},
|
||||
"tab_title": "Servidor"
|
||||
},
|
||||
"server_settings": {
|
||||
"server_settings": "Configuración de Servidor"
|
||||
},
|
||||
"settings_user": {
|
||||
"edit_shared_libraries": "Editar bibliotecas compartidas",
|
||||
"edit_user": "Editar usuario",
|
||||
"role_administrator": "Administrador",
|
||||
"role_user": "Usuario"
|
||||
},
|
||||
"sort": {
|
||||
"books_count": "Número de libros",
|
||||
"date_added": "Fecha de adición",
|
||||
"date_updated": "Fecha de actualización",
|
||||
"file_name": "Nombre de archivo",
|
||||
"file_size": "Tamaño de archivo",
|
||||
"folder_name": "Nombre de carpeta",
|
||||
"name": "Nombre",
|
||||
"number": "Número",
|
||||
"release_date": "Fecha de lanzamiento"
|
||||
},
|
||||
"theme": {
|
||||
"dark": "Oscuro",
|
||||
"light": "Claro",
|
||||
"system": "Sistema"
|
||||
},
|
||||
"user_roles": {
|
||||
"ADMIN": "Administrador",
|
||||
"FILE_DOWNLOAD": "Descarga de archivos",
|
||||
"PAGE_STREAMING": "Lectura en línea",
|
||||
"USER": "Usuario"
|
||||
},
|
||||
"users": {
|
||||
"users": "Usuarios"
|
||||
},
|
||||
"welcome": {
|
||||
"welcome_message": "Bienvenidos a Komga"
|
||||
"add_library": "Añadir biblioteca",
|
||||
"no_libraries_yet": "¡Aún no has agregado bibliotecas!",
|
||||
"welcome_message": "Bienvenido a Komga"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
523
komga-webui/src/locales/fi.json
Normal file
523
komga-webui/src/locales/fi.json
Normal file
|
|
@ -0,0 +1,523 @@
|
|||
{
|
||||
"$vuetify": {
|
||||
"dataFooter": {
|
||||
"pageText": "{0}-{1}/{2}"
|
||||
},
|
||||
"dataTable": {
|
||||
"itemsPerPageText": "Riviä/sivu:",
|
||||
"sortBy": "Lajittelu"
|
||||
},
|
||||
"fileInput": {
|
||||
"counter": "{0} tiedostoa",
|
||||
"counterSize": "{0} tiedostoa ({1} yhteensä)"
|
||||
},
|
||||
"noDataText": "Tietoja ei ole saatavilla"
|
||||
},
|
||||
"account_settings": {
|
||||
"account_settings": "Tilin asetukset",
|
||||
"change_password": "Vaihda salasana"
|
||||
},
|
||||
"author_roles": {
|
||||
"colorist": "värittäjät",
|
||||
"cover": "kansi",
|
||||
"editor": "toimittajat",
|
||||
"inker": "ääriviivatyöntekijät",
|
||||
"letterer": "kirjoittajat",
|
||||
"penciller": "piirtäjät",
|
||||
"writer": "kirjailijat"
|
||||
},
|
||||
"book_card": {
|
||||
"error": "Virhe",
|
||||
"unknown": "Ei vielä analysoitu",
|
||||
"unsupported": "Ei tuettu"
|
||||
},
|
||||
"bookreader": {
|
||||
"beginning_of_book": "Olet kirjan alussa.",
|
||||
"changing_reading_direction": "Vaihda lukusuunta",
|
||||
"cycling_page_layout": "Vaihda sivun asettelua",
|
||||
"cycling_scale": "Vaihda skaalaa",
|
||||
"cycling_side_padding": "Vaihda sivuntäyttöä",
|
||||
"download_current_page": "Lataa tämänhetkinen sivu",
|
||||
"end_of_book": "Olet saavuttanut kirjan lopun.",
|
||||
"from_series_metadata": "sarjan metatiedoista",
|
||||
"move_next": "Siirry seuraavaan kirjaan napsauttamalla tai painamalla ”Seuraava” uudelleen.",
|
||||
"move_next_exit": "Poistu lukijasta napsauttamalla tai painamalla ”Seuraava” uudelleen.",
|
||||
"move_previous": "Siirry edelliseen kirjaan napsauttamalla tai painamalla ”Edellinen” uudelleen.",
|
||||
"paged_reader_layout": {
|
||||
"double": "Kaksoissivut",
|
||||
"double_no_cover": "Kaksoissivut (ei kantta)",
|
||||
"single": "Yksi sivu"
|
||||
},
|
||||
"reader_settings": "Lukijan asetukset",
|
||||
"scale_type": {
|
||||
"continuous_original": "Alkuperäinen",
|
||||
"continuous_width": "Sovita leveys",
|
||||
"height": "Sovita korkeus",
|
||||
"original": "Alkuperäinen",
|
||||
"screen": "Kuvaruutu",
|
||||
"width": "Sovita leveys"
|
||||
},
|
||||
"settings": {
|
||||
"animate_page_transitions": "Animoi sivusiirtymät",
|
||||
"background_color": "Taustan väri",
|
||||
"background_colors": {
|
||||
"black": "Musta",
|
||||
"white": "Valkoinen"
|
||||
},
|
||||
"display": "Näytä",
|
||||
"general": "Yleiset",
|
||||
"gestures": "Eleet",
|
||||
"page_layout": "Sivun asettelu",
|
||||
"paged": "Sivutetun lukijan asetukset",
|
||||
"reading_mode": "Lukutila",
|
||||
"scale_type": "Skaalauksen tyyppi",
|
||||
"side_padding": "Sivuntäyttö",
|
||||
"side_padding_none": "Ei mitään",
|
||||
"webtoon": "Webtoon-lukijan asetukset"
|
||||
},
|
||||
"shortcuts": {
|
||||
"close": "Sulje",
|
||||
"cycle_page_layout": "Vaihda sivun asettelua",
|
||||
"cycle_scale": "Vaihda skaalaa",
|
||||
"cycle_side_padding": "Vaihda sivuntäyttöä",
|
||||
"first_page": "Ensimmäinen sivu",
|
||||
"last_page": "Viimeinen sivu",
|
||||
"left_to_right": "Vasemmalta oikealle",
|
||||
"menus": "Valikot",
|
||||
"next_page": "Seuraava sivu",
|
||||
"previous_page": "Edellinen sivu",
|
||||
"reader_navigation": "Lukijan navigointi",
|
||||
"right_to_left": "Oikealta vasemmalle",
|
||||
"settings": "Asetukset",
|
||||
"show_hide_help": "Näytä/piilota apu",
|
||||
"show_hide_settings": "Näytä/piilota asetusvalikko",
|
||||
"show_hide_thumbnails": "Näytä/piilota pikkukuvaselain",
|
||||
"show_hide_toolbars": "Näytä/piilota työkalupalkit",
|
||||
"vertical": "Pystysuora",
|
||||
"webtoon": "Webtoon-tyylinen"
|
||||
}
|
||||
},
|
||||
"browse_book": {
|
||||
"comment": "KOMMENTTI",
|
||||
"download_file": "Lataa tiedosto",
|
||||
"file": "TIEDOSTO",
|
||||
"format": "MUOTO",
|
||||
"isbn": "ISBN",
|
||||
"navigation_within_readlist": "Navigointi lukulistan sisällä: {name}",
|
||||
"read_book": "Lue kirja",
|
||||
"size": "KOKO"
|
||||
},
|
||||
"browse_collection": {
|
||||
"edit_collection": "Muokkaa kokoelmaa",
|
||||
"edit_elements": "Muokkaa elementtejä",
|
||||
"manual_ordering": "manuaalinen järjestely"
|
||||
},
|
||||
"browse_readlist": {
|
||||
"edit_elements": "Muokkaa elementtejä",
|
||||
"edit_readlist": "Muokkaa lukulistaa"
|
||||
},
|
||||
"browse_series": {
|
||||
"earliest_year_from_release_dates": "Tämä on aikaisin vuosi kaikista sarjassa olevien kirjojen julkaisuvuosista",
|
||||
"series_no_summary": "Tällä sarjalla ei ole yhteenvetoa, joten valitsimme sinulle sellaisen!",
|
||||
"summary_from_book": "Kirjan {number} yhteenveto:"
|
||||
},
|
||||
"collections_expansion_panel": {
|
||||
"manage_collection": "Hallitse kokoelmaa",
|
||||
"title": "{name} kokoelma"
|
||||
},
|
||||
"common": {
|
||||
"all_libraries": "Kaikki kirjastot",
|
||||
"books": "Kirjat",
|
||||
"books_n": "Ei kirjaa | Yksi kirja | {count} kirjaa",
|
||||
"cancel": "Peruuta",
|
||||
"close": "Sulje",
|
||||
"collections": "Kokoelmat",
|
||||
"create": "Luo",
|
||||
"delete": "Poista",
|
||||
"download": "Lataa",
|
||||
"email": "Sähköposti",
|
||||
"filter_no_matches": "Aktiiviselle suodattimelle ei löytynyt tuloksia",
|
||||
"genre": "Tyylilaji",
|
||||
"go_to_library": "Siirry kirjastoon",
|
||||
"locale_name": "Suomi",
|
||||
"locale_rtl": "false",
|
||||
"n_selected": "{count} valittu",
|
||||
"nothing_to_show": "Ei näytettävää",
|
||||
"pages": "sivua",
|
||||
"pages_n": "Ei sivuja | Yksi sivu | {count} sivua",
|
||||
"password": "Salasana",
|
||||
"publisher": "Julkaisija",
|
||||
"read": "Lue",
|
||||
"readlists": "Lukulistat",
|
||||
"required": "Pakollinen",
|
||||
"roles": "Roolit",
|
||||
"series": "Sarja",
|
||||
"tags": "Tunnisteet",
|
||||
"use_filter_panel_to_change_filter": "Käytä suodatinpaneelia aktiivisen suodattimen vaihtamiseen",
|
||||
"year": "vuosi"
|
||||
},
|
||||
"dashboard": {
|
||||
"keep_reading": "Jatka lukemista",
|
||||
"on_deck": "Kannella",
|
||||
"recently_added_books": "Viimeksi lisätyt kirjat",
|
||||
"recently_added_series": "Viimeksi lisätyt sarjat",
|
||||
"recently_updated_series": "Viimeksi päivitetyt sarjat"
|
||||
},
|
||||
"data_import": {
|
||||
"book_number": "Kirjan numero: {name}",
|
||||
"book_series": "Sarja: {name}",
|
||||
"button_import": "Tuo",
|
||||
"comicrack_preambule_html": "Voit tuoda valmiita ComicRack -lukulistoja <code>.cbl</code> -formaatissa.<br>Komga yrittää sovittaa annetun sarjan ja kirjan numeron kirjastossasi oleviin sarjoihin ja kirjoihin.",
|
||||
"field_files_label": "ComicRack lukulistat (.cbl)",
|
||||
"import_read_lists": "Tuo lukulistat",
|
||||
"imported_as": "Tuotu nimellä {name}",
|
||||
"results_preambule": "Tuonnin tulokset on näytetty alla. Voit myös tarkistaa sovittamattomat kirjat jokaiselle annetulle tiedostolle.",
|
||||
"size_limit": "Koon pitäisi olla alle {size} MB",
|
||||
"tab_title": "Tietojen tuonti"
|
||||
},
|
||||
"dialog": {
|
||||
"add_to_collection": {
|
||||
"button_create": "Luo",
|
||||
"card_collection_subtitle": "Ei sarjaa | Yksi sarja | {count} sarjaa",
|
||||
"dialog_title": "Lisää kokoelmaan",
|
||||
"field_search_create": "Etsi tai luo kokoelma",
|
||||
"field_search_create_error": "Tämänniminen kokoelma on jo olemassa",
|
||||
"label_no_matching_collection": "Ei vastaavia kokoelmia"
|
||||
},
|
||||
"add_to_readlist": {
|
||||
"button_create": "Luo",
|
||||
"card_readlist_subtitle": "Ei kirjaa | Yksi kirja | {count} kirjaa",
|
||||
"dialog_title": "Lisää lukulistaan",
|
||||
"field_search_create": "Etsi tai luo lukulista",
|
||||
"field_search_create_error": "Tämänniminen lukulista on jo olemassa",
|
||||
"label_no_matching_readlist": "Ei vastaavaa lukulistaa"
|
||||
},
|
||||
"add_user": {
|
||||
"button_cancel": "Peruuta",
|
||||
"button_confirm": "Lisää",
|
||||
"dialog_title": "Lisää käyttäjä",
|
||||
"field_email": "Sähköposti",
|
||||
"field_email_error": "Täytyy olla pätevä sähköpostiosoite",
|
||||
"field_password": "Salasana",
|
||||
"field_role_administrator": "Järjestelmänvalvoja",
|
||||
"field_role_file_download": "Tiedostojen lataus",
|
||||
"field_role_page_streaming": "Sivujen suoratoisto",
|
||||
"label_roles": "Roolit"
|
||||
},
|
||||
"delete_collection": {
|
||||
"button_cancel": "Peruuta",
|
||||
"button_confirm": "Poista",
|
||||
"confirm_delete": "Kyllä, poista kokoelma ”{name}”",
|
||||
"dialog_title": "Poista kokoelma",
|
||||
"warning_html": "Kokoelma <b>{name}</b> poistetaan. Tämä ei vaikuta mediatiedostoihisi. Tätä <b>ei voi</b> kumota. Jatka?"
|
||||
},
|
||||
"delete_library": {
|
||||
"button_cancel": "Peruuta",
|
||||
"button_confirm": "Poista",
|
||||
"confirm_delete": "Kyllä, poista kirjasto ”{name}”",
|
||||
"title": "Poista kirjasto",
|
||||
"warning_html": "Kirjasto <b>{name}</b> poistetaan palvelimelta. Tämä ei vaikuta mediatiedostoihisi. Tätä <b>ei voi</b> kumota. Jatka?"
|
||||
},
|
||||
"delete_readlist": {
|
||||
"button_cancel": "Peruuta",
|
||||
"button_confirm": "Poista",
|
||||
"confirm_delete": "Kyllä, poista lukulista ”{name}”",
|
||||
"dialog_title": "Poista lukulista",
|
||||
"warning_html": "Lukulista <b>{name}</b> poistetaan palvelimelta. Tämä ei vaikuta mediatiedostoihisi. Tätä <b>ei voi</b> kumota. Jatka?"
|
||||
},
|
||||
"delete_user": {
|
||||
"button_cancel": "Peruuta",
|
||||
"button_confirm": "Poista",
|
||||
"confirm_delete": "Kyllä, poista käyttäjä ”{name}”",
|
||||
"dialog_title": "Poista käyttäjä",
|
||||
"warning_html": "Käyttäjä <b>{name}</b> poistetaan palvelimelta. Tätä <b>ei voi</b> kumota. Jatka?"
|
||||
},
|
||||
"edit_books": {
|
||||
"authors_notice_multiple_edit": "Olet muokkaamassa useiden kirjojen kirjailijoita. Tämä ohittaa jokaisen kirjan nykyiset kirjailijat.",
|
||||
"button_cancel": "Peruuta",
|
||||
"button_confirm": "Tallenna muutokset",
|
||||
"dialog_title_multiple": "Muokkaa {count} kirjaa | Muokkaa {count} kirjaa",
|
||||
"dialog_title_single": "Muokkaa {book}",
|
||||
"field_isbn": "ISBN",
|
||||
"field_isbn_error": "Täytyy olla kelvollinen ISBN 13",
|
||||
"field_number": "Numero",
|
||||
"field_number_sort": "Lajittelunumero",
|
||||
"field_number_sort_hint": "Voit käyttää desimaalilukuja",
|
||||
"field_release_date": "Julkaisupäivämäärä",
|
||||
"field_release_date_error": "Täytyy olla kelvollinen päivämäärä VVVV-KK-PP -muodossa",
|
||||
"field_summary": "Yhteenveto",
|
||||
"field_tags": "Tunnisteet",
|
||||
"field_title": "Nimi",
|
||||
"tab_authors": "Kirjailijat",
|
||||
"tab_general": "Yleiset",
|
||||
"tab_tags": "Tunnisteet",
|
||||
"tags_notice_multiple_edit": "Olet muokkaamassa useiden kirjojen tunnisteita. Tämä ohittaa jokaisen kirjan nykyiset tunnisteet."
|
||||
},
|
||||
"edit_collection": {
|
||||
"button_cancel": "Peruuta",
|
||||
"button_confirm": "Tallenna muutokset",
|
||||
"dialog_title": "Muokkaa kokoelmaa",
|
||||
"field_manual_ordering": "Manuaalinen järjestely",
|
||||
"label_ordering": "Kokoelman sarjat lajitellaan oletusarvoisesti nimen mukaan. Voit ottaa manuaalisen järjestelyn käyttöön määritelläksesi oman järjestyksesi."
|
||||
},
|
||||
"edit_library": {
|
||||
"button_browse": "Selaa",
|
||||
"button_cancel": "Peruuta",
|
||||
"button_confirm_add": "Lisää",
|
||||
"button_confirm_edit": "Muokkaa",
|
||||
"dialog_title_add": "Lisää kirjasto",
|
||||
"dialot_title_edit": "Muokkaa kirjastoa",
|
||||
"field_import_barcode_isbn": "ISBN-viivakoodi",
|
||||
"field_import_comicinfo_book": "Kirjan metatiedot",
|
||||
"field_import_comicinfo_collections": "Kokoelmat",
|
||||
"field_import_comicinfo_readlists": "Lukulistat",
|
||||
"field_import_comicinfo_series": "Sarjan metatiedot",
|
||||
"field_import_epub_book": "Kirjan metatiedot",
|
||||
"field_import_epub_series": "Sarjan metatiedot",
|
||||
"field_import_local_artwork": "Paikallinen kuvamateriaali",
|
||||
"field_name": "Nimi",
|
||||
"field_root_folder": "Juurikansio",
|
||||
"field_scanner_deep_scan": "Syvä skannaus",
|
||||
"field_scanner_force_directory_modified_time": "Pakota hakemiston muokattu aika",
|
||||
"file_browser_dialog_button_confirm": "Valitse",
|
||||
"file_browser_dialog_title": "Kirjaston juurikansio",
|
||||
"label_import_barcode_isbn": "Tuo ISBN viivakoodin sisällä",
|
||||
"label_import_comicinfo": "Tuo ComedInfo.xml -tiedoston sisältävien CBR/CBZ -tiedostojen metatiedot",
|
||||
"label_import_epub": "Tuo metatiedot EPUB-tiedostoista",
|
||||
"label_import_local": "Tuo paikalliset mediatiedot",
|
||||
"label_scanner": "Skanneri",
|
||||
"tab_general": "Yleiset",
|
||||
"tab_options": "Vaihtoehdot"
|
||||
},
|
||||
"edit_readlist": {
|
||||
"button_cancel": "Peruuta",
|
||||
"button_confirm": "Tallenna muutokset",
|
||||
"dialog_title": "Muokkaa lukulistaa",
|
||||
"field_name": "Nimi"
|
||||
},
|
||||
"edit_series": {
|
||||
"button_cancel": "Peruuta",
|
||||
"button_confirm": "Tallenna muutokset",
|
||||
"dialog_title_multiple": "Muokkaa {count} sarjaa | Muokkaa {count} sarjaa",
|
||||
"dialog_title_single": "Muokkaa {series}",
|
||||
"field_age_rating": "Ikäluokitus",
|
||||
"field_age_rating_error": "Ikäluokituksen on oltava vähintään 0",
|
||||
"field_genres": "Tyylilajit",
|
||||
"field_language": "Kieli",
|
||||
"field_publisher": "Julkaisija",
|
||||
"field_reading_direction": "Lukusuunta",
|
||||
"field_sort_title": "Lajitteluotsikko",
|
||||
"field_status": "Tila",
|
||||
"field_summary": "Yhteenveto",
|
||||
"field_tags": "Tunnisteet",
|
||||
"field_title": "Nimi",
|
||||
"mixed": "SEKALAINEN",
|
||||
"tab_general": "Yleiset",
|
||||
"tab_tags": "Tunnisteet",
|
||||
"tags_notice_multiple_edit": "Olet muokkaamassa usean sarjan tunnisteita. Tämä ohittaa jokaisen sarjan nykyiset tunnisteet."
|
||||
},
|
||||
"edit_user": {
|
||||
"button_cancel": "Peruuta",
|
||||
"button_confirm": "Tallenna muutokset",
|
||||
"dialog_title": "Muokkaa käyttäjää",
|
||||
"label_roles_for": "Roolit käyttäjälle {name}"
|
||||
},
|
||||
"edit_user_shared_libraries": {
|
||||
"button_cancel": "Peruuta",
|
||||
"button_confirm": "Tallenna muutokset",
|
||||
"dialog_title": "Muokkaa jaettuja kirjastoja",
|
||||
"field_all_libraries": "Kaikki kirjastot",
|
||||
"label_shared_with": "Jaettu käyttäjän {name} kanssa"
|
||||
},
|
||||
"file_browser": {
|
||||
"button_cancel": "Peruuta",
|
||||
"button_confirm_default": "Valitse",
|
||||
"dialog_title_default": "Tiedostoselain",
|
||||
"parent_directory": "Emohakemisto"
|
||||
},
|
||||
"password_change": {
|
||||
"button_cancel": "Peruuta",
|
||||
"button_confirm": "Vaihda salasana",
|
||||
"dialog_title": "Vaihda salasana",
|
||||
"field_new_password": "Uusi salasana",
|
||||
"field_new_password_error": "Uusi salasana vaaditaan.",
|
||||
"field_repeat_password": "Toista uusi salasana",
|
||||
"field_repeat_password_error": "Salasanojen on oltava identtisiä."
|
||||
},
|
||||
"server_stop": {
|
||||
"button_cancel": "Peruuta",
|
||||
"button_confirm": "Pysäytä",
|
||||
"confirmation_message": "Oletko varma, että haluat lopettaa Komgan?",
|
||||
"dialog_title": "Sammuta palvelin"
|
||||
},
|
||||
"shortcut_help": {
|
||||
"label_description": "Kuvaus",
|
||||
"label_key": "Avain"
|
||||
}
|
||||
},
|
||||
"enums": {
|
||||
"media_status": {
|
||||
"ERROR": "Virhe",
|
||||
"OUTDATED": "Vanhentunut",
|
||||
"READY": "Valmis",
|
||||
"UNKNOWN": "Tuntematon",
|
||||
"UNSUPPORTED": "Ei tuettu"
|
||||
},
|
||||
"reading_direction": {
|
||||
"LEFT_TO_RIGHT": "Vasemmalta oikealle",
|
||||
"RIGHT_TO_LEFT": "Oikealta vasemmalle",
|
||||
"VERTICAL": "Pystysuora",
|
||||
"WEBTOON": "Webtoon-tyylinen"
|
||||
},
|
||||
"series_status": {
|
||||
"ABANDONED": "Hylätty",
|
||||
"ENDED": "Päättynyt",
|
||||
"HIATUS": "Tauolla",
|
||||
"ONGOING": "Jatkuva"
|
||||
}
|
||||
},
|
||||
"error_codes": {
|
||||
"ERR_1000": "Tiedostoa ei voitu käyttää analyysin aikana",
|
||||
"ERR_1001": "Mediatyyppiä ei tueta",
|
||||
"ERR_1002": "Salattuja RAR-arkistoja ei tueta",
|
||||
"ERR_1003": "Kiinteitä RAR-arkistoja ei tueta",
|
||||
"ERR_1004": "Moniosaisia RAR-arkistoja ei tueta",
|
||||
"ERR_1005": "Tuntematon virhe kirjaa analysoitaessa",
|
||||
"ERR_1006": "Kirja ei sisällä yhtään sivua",
|
||||
"ERR_1007": "Joitain merkintöjä ei voitu analysoida",
|
||||
"ERR_1008": "Tuntematon virhe kirjan merkintöjä haettaessa",
|
||||
"ERR_1009": "Tämänniminen lukulista on jo olemassa",
|
||||
"ERR_1010": "Yksikään kirja ei vastannut lukuluetteloa",
|
||||
"ERR_1011": "Ei yksiselitteistä vastinetta sarjalle",
|
||||
"ERR_1012": "Ei vastinetta sarjalle",
|
||||
"ERR_1013": "Kirjan numerolle ei löytynyt yksiselitteistä vastinetta sarjan sisältä",
|
||||
"ERR_1014": "Kirjan numerolle ei löytynyt vastinetta sarjan sisältä",
|
||||
"ERR_1015": "Virhe deserialisoidessa ComicRack lukulistaa"
|
||||
},
|
||||
"filter": {
|
||||
"age_rating": "ikäluokitus",
|
||||
"age_rating_none": "Ei mitään",
|
||||
"genre": "tyylilaji",
|
||||
"language": "kieli",
|
||||
"library": "kirjasto",
|
||||
"publisher": "julkaisija",
|
||||
"release_date": "julkaisupäivämäärä",
|
||||
"status": "tila",
|
||||
"tag": "tunniste",
|
||||
"unread": "Lukemattomat"
|
||||
},
|
||||
"filter_drawer": {
|
||||
"filter": "suodata",
|
||||
"sort": "lajittele"
|
||||
},
|
||||
"home": {
|
||||
"theme": "Teema",
|
||||
"translation": "Käännös"
|
||||
},
|
||||
"library_navigation": {
|
||||
"browse": "Selaa",
|
||||
"collections": "Kokoelmat",
|
||||
"readlists": "Lukulistat"
|
||||
},
|
||||
"login": {
|
||||
"create_user_account": "Luo käyttäjätili",
|
||||
"login": "Kirjaudu",
|
||||
"unclaimed_html": "Tämä Komga-palvelin ei ole vielä aktiivinen. Sinun täytyy luoda käyttäjätili voidaksesi käyttää sitä.<br><br>Valitse <strong>sähköpostiosoite</strong> ja <strong>salasana</strong> ja klikkaa <strong>Luo käyttäjätunnus</strong>."
|
||||
},
|
||||
"media_analysis": {
|
||||
"comment": "Kommentti",
|
||||
"media_analysis": "Media-analyysi",
|
||||
"media_type": "Mediatyyppi",
|
||||
"name": "Nimi",
|
||||
"size": "Koko",
|
||||
"status": "Tila",
|
||||
"url": "URL"
|
||||
},
|
||||
"menu": {
|
||||
"add_to_collection": "Lisää kokoelmaan",
|
||||
"add_to_readlist": "Lisää lukulistaan",
|
||||
"analyze": "Analysoi",
|
||||
"delete": "Poista",
|
||||
"download_series": "Lataa sarja",
|
||||
"edit": "Muokkaa",
|
||||
"edit_metadata": "Muokkaa metatietoja",
|
||||
"mark_read": "Merkitse luetuksi",
|
||||
"mark_unread": "Merkitse lukemattomaksi",
|
||||
"refresh_metadata": "Päivitä metatiedot",
|
||||
"scan_library_files": "Skannaa kirjastotiedostot"
|
||||
},
|
||||
"navigation": {
|
||||
"home": "Koti",
|
||||
"libraries": "Kirjastot",
|
||||
"logout": "Kirjaudu ulos"
|
||||
},
|
||||
"page_not_found": {
|
||||
"go_back_to_home_page": "Takaisin etusivulle",
|
||||
"page_does_not_exist": "Etsimääsi sivua ei ole olemassa.",
|
||||
"page_not_found": "Sivua ei löytynyt"
|
||||
},
|
||||
"read_more": {
|
||||
"less": "Lue vähemmän",
|
||||
"more": "Lue lisää"
|
||||
},
|
||||
"readlists_expansion_panel": {
|
||||
"manage_readlist": "Hallitse lukulistaa",
|
||||
"title": "{name} lukulista"
|
||||
},
|
||||
"search": {
|
||||
"no_results": "Haku ei tuottanut yhtään tulosta",
|
||||
"search": "Etsi",
|
||||
"search_for_something_else": "Yritä etsiä jotain muuta",
|
||||
"search_results_for": "Tulokset haulle ”{name}”"
|
||||
},
|
||||
"searchbox": {
|
||||
"no_results": "Ei tuloksia",
|
||||
"search_all": "Hae kaikista…"
|
||||
},
|
||||
"server": {
|
||||
"server_management": {
|
||||
"button_shutdown": "Sammuta",
|
||||
"section_title": "Palvelimen hallinta"
|
||||
},
|
||||
"tab_title": "Palvelin"
|
||||
},
|
||||
"server_settings": {
|
||||
"server_settings": "Palvelimen asetukset"
|
||||
},
|
||||
"settings_user": {
|
||||
"edit_shared_libraries": "Muokkaa jaettuja kirjastoja",
|
||||
"edit_user": "Muokkaa käyttäjää",
|
||||
"role_administrator": "Järjestelmänvalvoja",
|
||||
"role_user": "Käyttäjä"
|
||||
},
|
||||
"sort": {
|
||||
"books_count": "Kirjojen määrä",
|
||||
"date_added": "Lisäyspäivämäärä",
|
||||
"date_updated": "Päivitetty päivämäärä",
|
||||
"file_name": "Tiedostonimi",
|
||||
"file_size": "Tiedoston koko",
|
||||
"folder_name": "Kansion nimi",
|
||||
"name": "Nimi",
|
||||
"number": "Numero",
|
||||
"release_date": "Julkaisupäivämäärä"
|
||||
},
|
||||
"theme": {
|
||||
"dark": "Tumma",
|
||||
"light": "Vaalea",
|
||||
"system": "Järjestelmä"
|
||||
},
|
||||
"user_roles": {
|
||||
"ADMIN": "Järjestelmänvalvoja",
|
||||
"FILE_DOWNLOAD": "Tiedostojen lataus",
|
||||
"PAGE_STREAMING": "Sivujen suoratoisto",
|
||||
"USER": "Käyttäjä"
|
||||
},
|
||||
"users": {
|
||||
"users": "Käyttäjät"
|
||||
},
|
||||
"welcome": {
|
||||
"add_library": "Lisää kirjasto",
|
||||
"no_libraries_yet": "Kirjastoja ei ole vielä lisätty!",
|
||||
"welcome_message": "Tervetuloa Komgaan"
|
||||
}
|
||||
}
|
||||
|
|
@ -26,17 +26,23 @@
|
|||
"penciller": "dessinateurs",
|
||||
"writer": "scénaristes"
|
||||
},
|
||||
"book_card": {
|
||||
"error": "Erreur",
|
||||
"unknown": "À analyser",
|
||||
"unsupported": "Non pris en charge"
|
||||
},
|
||||
"bookreader": {
|
||||
"beginning_of_book": "Vous êtes au début du livre.",
|
||||
"changing_reading_direction": "Modifier le sens de lecture vers",
|
||||
"cycling_page_layout": "Changer la disposition des pages",
|
||||
"cycling_scale": "Changer la mise à l'échelle",
|
||||
"cycling_side_padding": "Changer la bordure latérale",
|
||||
"download_current_page": "Télécharger la page courante",
|
||||
"end_of_book": "Vous avez atteint la fin du livre.",
|
||||
"from_series_metadata": "à partir des métadonnées de la série",
|
||||
"move_next": "Cliquez ou appuyez à nouveau sur \"Suivant\" pour passer au livre suivant.",
|
||||
"move_next_exit": "Cliquez ou appuyez à nouveau sur \"Suivant\" pour quitter la liseuse.",
|
||||
"move_previous": "Cliquez ou appuyez à nouveau sur \"Précédent\" pour passer au livre précédent.",
|
||||
"move_next": "Cliquez ou appuyez à nouveau sur « Suivant » pour passer au livre suivant.",
|
||||
"move_next_exit": "Cliquez ou appuyez à nouveau sur « Suivant » pour quitter la liseuse.",
|
||||
"move_previous": "Cliquez ou appuyez à nouveau sur « Précédent » pour passer au livre précédent.",
|
||||
"paged_reader_layout": {
|
||||
"double": "Double pages",
|
||||
"double_no_cover": "Double pages (sans couverture)",
|
||||
|
|
@ -161,7 +167,7 @@
|
|||
"book_number": "Nombre de livres : {name}",
|
||||
"book_series": "Série : {name}",
|
||||
"button_import": "Importer",
|
||||
"comicrack_preambule_html": "Vous pouvez importer des listes de lecture ComicRack au format <code>.cbl</code>.<br/>Komga tentera de faire correspondre les séries fournies et le numéro des livres avec les séries et livres de votre bibliothèque.",
|
||||
"comicrack_preambule_html": "Vous pouvez importer des listes de lecture ComicRack au format <code>.cbl</code>.<br>Komga tentera de faire correspondre les séries fournies et le numéro des livres avec les séries et livres de votre bibliothèque.",
|
||||
"field_files_label": "Listes de lecture ComicRack (.cbl)",
|
||||
"import_read_lists": "Importer des listes de lecture",
|
||||
"imported_as": "Importé comme {name}",
|
||||
|
|
@ -191,7 +197,7 @@
|
|||
"button_confirm": "Ajouter",
|
||||
"dialog_title": "Ajouter un utilisateur",
|
||||
"field_email": "E-mail",
|
||||
"field_email_error": "Doit être une adresse e-mail valide",
|
||||
"field_email_error": "Doit être une adresse électronique valide",
|
||||
"field_password": "Mot de passe",
|
||||
"field_role_administrator": "Administrateur",
|
||||
"field_role_file_download": "Téléchargement de fichier",
|
||||
|
|
@ -238,7 +244,7 @@
|
|||
"field_number_sort": "Numéro pour le tri",
|
||||
"field_number_sort_hint": "Vous pouvez utiliser des nombres décimaux",
|
||||
"field_release_date": "Date de publication",
|
||||
"field_release_date_error": "Doit être une date au format YYYY-MM-DD",
|
||||
"field_release_date_error": "Doit être une date au format AAAA-MM-JJ",
|
||||
"field_summary": "Résumé",
|
||||
"field_tags": "Étiquettes",
|
||||
"field_title": "Titre",
|
||||
|
|
@ -341,7 +347,7 @@
|
|||
"server_stop": {
|
||||
"button_cancel": "Annuler",
|
||||
"button_confirm": "Arrêter",
|
||||
"confirmation_message": "Êtes-vous sûr de vouloir arrêter Komga ?",
|
||||
"confirmation_message": "Êtes-vous sûr de vouloir arrêter Komga ?",
|
||||
"dialog_title": "Arrêter le serveur"
|
||||
},
|
||||
"shortcut_help": {
|
||||
|
|
|
|||
|
|
@ -8,35 +8,572 @@
|
|||
"sortBy": "Ordina per"
|
||||
},
|
||||
"fileInput": {
|
||||
"counter": "{0} files",
|
||||
"counter": "{0} file",
|
||||
"counterSize": "{0} files ({1} in totale)"
|
||||
},
|
||||
"noDataText": "Nessun elemento disponibile"
|
||||
"noDataText": "Nessun dato disponibile"
|
||||
},
|
||||
"account_settings": {
|
||||
"account_settings": "Impostazioni Account",
|
||||
"account_settings": "Impostazioni profilo",
|
||||
"change_password": "cambia password"
|
||||
},
|
||||
"author_roles": {
|
||||
"colorist": "coloristi",
|
||||
"cover": "copertina",
|
||||
"editor": "editori",
|
||||
"inker": "inchiostratori",
|
||||
"letterer": "letteristi",
|
||||
"penciller": "disegnatori",
|
||||
"writer": "scrittori"
|
||||
},
|
||||
"book_card": {
|
||||
"error": "Errore",
|
||||
"unknown": "Da analizzare",
|
||||
"unsupported": "Non supportato"
|
||||
},
|
||||
"book_import": {
|
||||
"button_browse": "Sfoglia",
|
||||
"button_import": "Importa",
|
||||
"button_scan": "Scansiona",
|
||||
"button_select_series": "Seleziona Serie",
|
||||
"field_import_path": "Importa da cartella",
|
||||
"table": {
|
||||
"destination_name": "Nome destinazione",
|
||||
"file_name": "Nome File",
|
||||
"number": "Numero",
|
||||
"series": "Serie"
|
||||
},
|
||||
"title": "Importa"
|
||||
},
|
||||
"bookreader": {
|
||||
"beginning_of_book": "Sei all'inizio del libro.",
|
||||
"changing_reading_direction": "Cambia la direzione di lettura in",
|
||||
"cycling_page_layout": "Cambia Layout Pagina",
|
||||
"cycling_scale": "Cambia scala",
|
||||
"cycling_side_padding": "Cambia Distanziamento Laterale",
|
||||
"download_current_page": "Scarica pagina corrente",
|
||||
"end_of_book": "Hai raggiunto la fine del libro.",
|
||||
"from_series_metadata": "dai metadati della serie",
|
||||
"move_next": "Clicca o premi nuovamente «Avanti» per passare al libro successivo.",
|
||||
"move_next_exit": "Clicca o premi nuovamente «Avanti» per uscire dal lettore.",
|
||||
"move_previous": "Clicca o premi nuovamente «Indietro» per passare al libro precedente.",
|
||||
"paged_reader_layout": {
|
||||
"double": "Pagine doppie",
|
||||
"double_no_cover": "Pagine doppie (senza copertina)",
|
||||
"single": "Pagina singola"
|
||||
},
|
||||
"reader_settings": "Impostazioni Lettore",
|
||||
"scale_type": {
|
||||
"continuous_original": "Originale",
|
||||
"continuous_width": "Adatta a larghezza"
|
||||
"continuous_width": "Adatta a larghezza",
|
||||
"height": "Adatta ad altezza",
|
||||
"original": "Originale",
|
||||
"screen": "Schermo",
|
||||
"width": "Adatta a larghezza"
|
||||
},
|
||||
"settings": {
|
||||
"animate_page_transitions": "Anima le transizioni di pagina",
|
||||
"background_color": "Colore sfondo",
|
||||
"background_colors": {
|
||||
"black": "Nero",
|
||||
"white": "Bianco"
|
||||
},
|
||||
"display": "Visualizzazione",
|
||||
"general": "Generale",
|
||||
"gestures": "Gesti",
|
||||
"page_layout": "Formato pagina",
|
||||
"paged": "Opzioni Impaginazione",
|
||||
"reading_mode": "Modalità di lettura",
|
||||
"scale_type": "Scala",
|
||||
"side_padding": "Distanziamento laterale",
|
||||
"side_padding_none": "Nessuno",
|
||||
"webtoon": "Opzioni Lettore Webtoon"
|
||||
},
|
||||
"shortcuts": {
|
||||
"close": "Chiudi",
|
||||
"cycle_page_layout": "Cambia layout pagina",
|
||||
"cycle_scale": "Cambia scala",
|
||||
"cycle_side_padding": "Cambia distanziamento laterale",
|
||||
"first_page": "Prima pagina",
|
||||
"last_page": "Ultima pagina",
|
||||
"left_to_right": "Da sinistra a destra",
|
||||
"menus": "Menu",
|
||||
"next_page": "Pagina successiva",
|
||||
"previous_page": "Pagina precedente",
|
||||
"reader_navigation": "Navigazione Lettore",
|
||||
"right_to_left": "Da destra a sinistra",
|
||||
"settings": "Impostazioni",
|
||||
"show_hide_help": "Mostra/nascondi aiuto",
|
||||
"show_hide_settings": "Mostra/nascondi le impostazioni",
|
||||
"show_hide_thumbnails": "Mostra/nascondi miniature",
|
||||
"show_hide_toolbars": "Mostra/nascondi barre strumenti",
|
||||
"vertical": "Verticale",
|
||||
"webtoon": "Webtoon"
|
||||
}
|
||||
},
|
||||
"browse_book": {
|
||||
"comment": "COMMENTO",
|
||||
"download_file": "Scarica file",
|
||||
"file": "FILE",
|
||||
"format": "FORMATO",
|
||||
"isbn": "ISBN",
|
||||
"navigation_within_readlist": "Navigazione all'interno della lista di lettura: {name}",
|
||||
"read_book": "Leggi libro",
|
||||
"size": "DIMENSIONE"
|
||||
},
|
||||
"browse_collection": {
|
||||
"edit_collection": "Modifica raccolta",
|
||||
"edit_elements": "Modifica elementi",
|
||||
"manual_ordering": "ordinamento manuale"
|
||||
},
|
||||
"browse_readlist": {
|
||||
"edit_elements": "Modifica elementi",
|
||||
"edit_readlist": "Modifica elenco lettura"
|
||||
},
|
||||
"browse_series": {
|
||||
"earliest_year_from_release_dates": "Questo è la data di uscita del primo fumetto della serie",
|
||||
"series_no_summary": "Questa serie non ha riassunto, quindi ne ho scelto uno per te!",
|
||||
"summary_from_book": "Riassunto dal libro {number}:"
|
||||
},
|
||||
"collections_expansion_panel": {
|
||||
"manage_collection": "Gestisci raccolta",
|
||||
"title": "Raccolta {name}"
|
||||
},
|
||||
"common": {
|
||||
"locale_name": "Italiano"
|
||||
"all_libraries": "Tutte le librerie",
|
||||
"books": "Fumetti",
|
||||
"books_n": "Nessun fumetto | 1 fumetto | {count} fumetti",
|
||||
"cancel": "Annulla",
|
||||
"close": "Chiudi",
|
||||
"collections": "Raccolte",
|
||||
"create": "Crea",
|
||||
"delete": "Elimina",
|
||||
"download": "Scarica",
|
||||
"email": "E-mail",
|
||||
"filter_no_matches": "Il filtro attivo non ha corrispondenze",
|
||||
"genre": "Genere",
|
||||
"go_to_library": "Vai alla libreria",
|
||||
"locale_name": "Italiano",
|
||||
"locale_rtl": "false",
|
||||
"n_selected": "{count} selezionati",
|
||||
"nothing_to_show": "Niente da mostrare",
|
||||
"pages": "pagine",
|
||||
"pages_n": "Nessuna pagina | 1 pagina | {count} pagine",
|
||||
"password": "Password",
|
||||
"publisher": "Editore",
|
||||
"read": "Leggi",
|
||||
"readlists": "Liste di lettura",
|
||||
"required": "Richiesto",
|
||||
"roles": "Ruoli",
|
||||
"series": "Serie",
|
||||
"tags": "Etichette",
|
||||
"use_filter_panel_to_change_filter": "Usa il pannello dei filtri per cambiare il filtro attivo",
|
||||
"year": "anno"
|
||||
},
|
||||
"dashboard": {
|
||||
"keep_reading": "Continua a leggere",
|
||||
"on_deck": "Primo Piano",
|
||||
"recently_added_books": "Fumetti aggiunti di recente",
|
||||
"recently_added_series": "Serie aggiunte di recente",
|
||||
"recently_updated_series": "Serie aggiornate di recente"
|
||||
},
|
||||
"data_import": {
|
||||
"book_number": "Libro numero: {name}",
|
||||
"book_series": "Serie: {name}",
|
||||
"button_import": "Importa",
|
||||
"comicrack_preambule_html": "Puoi importare liste di lettura ComicRack esistenti in formato <code>.cbl</code>.<br>Komga cercherà di far corrispondere la serie e il numero di fumetti forniti con le serie e i fumetti nelle tue librerie.",
|
||||
"field_files_label": "Liste di lettura ComicRack (.cbl)",
|
||||
"import_read_lists": "Importa Liste Lettura",
|
||||
"imported_as": "Importato come {name}",
|
||||
"results_preambule": "Il risultato dell'importazione è mostrato qui sotto. Puoi anche controllare i fumetti senza corrispondenza per ogni file fornito.",
|
||||
"size_limit": "Deve essere meno di {size} MB",
|
||||
"tab_title": "Importa Dati"
|
||||
},
|
||||
"dialog": {
|
||||
"add_to_collection": {
|
||||
"button_create": "Crea",
|
||||
"card_collection_subtitle": "Nessuna serie | 1 serie | {count} serie",
|
||||
"dialog_title": "Aggiungi alla raccolta",
|
||||
"field_search_create": "Cerca o crea raccolta",
|
||||
"field_search_create_error": "Esiste già una raccolta con questo nome",
|
||||
"label_no_matching_collection": "Nessuna raccolta corrispondente"
|
||||
},
|
||||
"add_to_readlist": {
|
||||
"button_create": "Crea",
|
||||
"card_readlist_subtitle": "Nessun fumetto | 1 fumetto | {count} fumetti",
|
||||
"dialog_title": "Aggiungi all'elenco di lettura",
|
||||
"field_search_create": "Cerca o crea una lista di lettura",
|
||||
"field_search_create_error": "Una lista di lettura con questo nome esiste già",
|
||||
"label_no_matching_readlist": "Nessuna lista di lettura corrispondente"
|
||||
},
|
||||
"add_user": {
|
||||
"button_cancel": "Annulla",
|
||||
"button_confirm": "Aggiungi",
|
||||
"dialog_title": "Aggiungi un utente",
|
||||
"field_email": "E-mail",
|
||||
"field_email_error": "Deve essere un indirizzo e-mail valido",
|
||||
"field_password": "Password",
|
||||
"field_role_administrator": "Amministratore",
|
||||
"field_role_file_download": "Scaricamento file",
|
||||
"field_role_page_streaming": "Lettura online",
|
||||
"label_roles": "Ruoli"
|
||||
},
|
||||
"delete_collection": {
|
||||
"button_cancel": "Annulla",
|
||||
"button_confirm": "Elimina",
|
||||
"confirm_delete": "Sì, elimina la raccolta «{name}»",
|
||||
"dialog_title": "Elimina Raccolta",
|
||||
"warning_html": "La raccolta <b>{name}</b> sarà rimossa da questo server. I tuoi file non saranno interessati. Questo <b>non</b> può essere annullato. Continuare?"
|
||||
},
|
||||
"delete_library": {
|
||||
"button_cancel": "Annulla",
|
||||
"button_confirm": "Elimina",
|
||||
"confirm_delete": "Sì, elimina la libreria «{name}»",
|
||||
"title": "Elimina Libreria",
|
||||
"warning_html": "La libreria <b>{name}</b> sarà rimossa da questo server. I tuoi file multimediali non saranno interessati. Questo <b> non può</b> essere annullato. Continuare?"
|
||||
},
|
||||
"delete_readlist": {
|
||||
"button_cancel": "Annulla",
|
||||
"button_confirm": "Elimina",
|
||||
"confirm_delete": "Sì, cancella la lista di lettura «{name}»",
|
||||
"dialog_title": "Elimina Elenco Lettura",
|
||||
"warning_html": "La lista di lettura <b>{name}</b> sarà rimossa da questo server. I tuoi file multimediali non saranno interessati. Questo <b>non può</b> essere annullato. Continuare?"
|
||||
},
|
||||
"delete_user": {
|
||||
"button_cancel": "Annulla",
|
||||
"button_confirm": "Elimina",
|
||||
"confirm_delete": "Sì, elimina l'utente «{name}»",
|
||||
"dialog_title": "Elimina Utente",
|
||||
"warning_html": "L'utente <b>{name}</b> sarà cancellato da questo server. Questo <b>non può</b> essere annullato. Continuare?"
|
||||
},
|
||||
"edit_books": {
|
||||
"authors_notice_multiple_edit": "Stai modificando gli autori per più fumetti. Questo sovrascriverà gli autori esistenti di ogni fumetto.",
|
||||
"button_cancel": "Annulla",
|
||||
"button_confirm": "Salva modifiche",
|
||||
"dialog_title_multiple": "Modifica {count} fumetto | Modifica {count} fumetti",
|
||||
"dialog_title_single": "Modifica {book}",
|
||||
"field_isbn": "ISBN",
|
||||
"field_isbn_error": "Deve essere un ISBN 13 valido",
|
||||
"field_number": "Numero",
|
||||
"field_number_sort": "Numero di ordinamento",
|
||||
"field_number_sort_hint": "Puoi usare numeri decimali",
|
||||
"field_release_date": "Data di uscita",
|
||||
"field_release_date_error": "Deve essere una data valida nel formato AAAA-MM-GG",
|
||||
"field_summary": "Riassunto",
|
||||
"field_tags": "Etichette",
|
||||
"field_title": "Titolo",
|
||||
"tab_authors": "Autori",
|
||||
"tab_general": "Generale",
|
||||
"tab_tags": "Etichette",
|
||||
"tags_notice_multiple_edit": "Stai modificando le etichette per più fumetti. Questo sovrascriverà le etichette esistenti di ogni fumetto."
|
||||
},
|
||||
"edit_collection": {
|
||||
"button_cancel": "Annulla",
|
||||
"button_confirm": "Salva modifiche",
|
||||
"dialog_title": "Modifica raccolta",
|
||||
"field_manual_ordering": "Ordinamento manuale",
|
||||
"label_ordering": "Per impostazione predefinita, le serie in una collezione saranno ordinate per nome. Puoi abilitare l'ordinamento manuale per definire il proprio ordine."
|
||||
},
|
||||
"edit_library": {
|
||||
"button_browse": "Sfoglia",
|
||||
"button_cancel": "Annulla",
|
||||
"button_confirm_add": "Aggiungi",
|
||||
"button_confirm_edit": "Modifica",
|
||||
"dialog_title_add": "Aggiungi Libreria",
|
||||
"dialot_title_edit": "Modifica Libreria",
|
||||
"field_import_barcode_isbn": "Codice a barre ISBN",
|
||||
"field_import_comicinfo_book": "Metadati del libro",
|
||||
"field_import_comicinfo_collections": "Raccolte",
|
||||
"field_import_comicinfo_readlists": "Liste di lettura",
|
||||
"field_import_comicinfo_series": "Metadati della serie",
|
||||
"field_import_epub_book": "Metadati del libro",
|
||||
"field_import_epub_series": "Metadati della serie",
|
||||
"field_import_local_artwork": "Copertine locali",
|
||||
"field_name": "Nome",
|
||||
"field_root_folder": "Cartella principale",
|
||||
"field_scanner_deep_scan": "Scansione profonda",
|
||||
"field_scanner_force_directory_modified_time": "Usa data di modifica delle cartelle",
|
||||
"file_browser_dialog_button_confirm": "Scegli",
|
||||
"file_browser_dialog_title": "Cartella principale della libreria",
|
||||
"label_import_barcode_isbn": "Importa ISBN all'interno del codice a barre",
|
||||
"label_import_comicinfo": "Importa metadati per CBR/CBZ contenenti un file ComicInfo.xml",
|
||||
"label_import_epub": "Importa metadati dai file EPUB",
|
||||
"label_import_local": "Importa risorse multimediali locali",
|
||||
"label_scanner": "Scanner",
|
||||
"tab_general": "Generale",
|
||||
"tab_options": "Opzioni"
|
||||
},
|
||||
"edit_readlist": {
|
||||
"button_cancel": "Annulla",
|
||||
"button_confirm": "Salva modifiche",
|
||||
"dialog_title": "Modifica elenco di lettura",
|
||||
"field_name": "Nome"
|
||||
},
|
||||
"edit_series": {
|
||||
"button_cancel": "Annulla",
|
||||
"button_confirm": "Salva modifiche",
|
||||
"dialog_title_multiple": "Modifica {count} serie | Modifica {count} serie",
|
||||
"dialog_title_single": "Modifica {series}",
|
||||
"field_age_rating": "Fascia d'età",
|
||||
"field_age_rating_error": "La fascia d'età deve essere pari o superiore a 0",
|
||||
"field_genres": "Generi",
|
||||
"field_language": "Lingua",
|
||||
"field_publisher": "Editore",
|
||||
"field_reading_direction": "Direzione Lettura",
|
||||
"field_sort_title": "Titolo di Ordinamento",
|
||||
"field_status": "Stato",
|
||||
"field_summary": "Riassunto",
|
||||
"field_tags": "Etichette",
|
||||
"field_title": "Titolo",
|
||||
"mixed": "MISTO",
|
||||
"tab_general": "Generale",
|
||||
"tab_tags": "Etichette",
|
||||
"tags_notice_multiple_edit": "Stai modificando le etichette per più serie. Questo sovrascriverà le etichette esistenti di ogni serie."
|
||||
},
|
||||
"edit_user": {
|
||||
"button_cancel": "Annulla",
|
||||
"button_confirm": "Salva modifiche",
|
||||
"dialog_title": "Modifica utente",
|
||||
"label_roles_for": "Ruoli per {name}"
|
||||
},
|
||||
"edit_user_shared_libraries": {
|
||||
"button_cancel": "Annulla",
|
||||
"button_confirm": "Salva modifiche",
|
||||
"dialog_title": "Modifica librerie condivise",
|
||||
"field_all_libraries": "Tutte le librerie",
|
||||
"label_shared_with": "Condiviso con {name}"
|
||||
},
|
||||
"file_browser": {
|
||||
"button_cancel": "Annulla",
|
||||
"button_confirm_default": "Scegli",
|
||||
"dialog_title_default": "Browser di file",
|
||||
"parent_directory": "Genitore"
|
||||
},
|
||||
"filename_chooser": {
|
||||
"button_choose": "Scegli",
|
||||
"field_destination_filename": "Nome del file di destinazione",
|
||||
"label_source_filename": "Nome del file di origine",
|
||||
"table": {
|
||||
"existing_file": "File Esistente",
|
||||
"order": "Ordine"
|
||||
},
|
||||
"title": "Nome del file di destinazione"
|
||||
},
|
||||
"password_change": {
|
||||
"button_cancel": "Annulla",
|
||||
"button_confirm": "Cambia password",
|
||||
"dialog_title": "Cambia password",
|
||||
"field_new_password": "Nuova password",
|
||||
"field_new_password_error": "È richiesta una nuova password.",
|
||||
"field_repeat_password": "Ripeti la nuova password",
|
||||
"field_repeat_password_error": "Le password devono essere identiche."
|
||||
},
|
||||
"series_picker": {
|
||||
"label_search_series": "Cerca Serie",
|
||||
"title": "Seleziona Serie"
|
||||
},
|
||||
"server_stop": {
|
||||
"button_cancel": "Annulla",
|
||||
"button_confirm": "Spegni",
|
||||
"confirmation_message": "Sei sicuro/a di voler spegnere Komga?",
|
||||
"dialog_title": "Spegni il server"
|
||||
},
|
||||
"shortcut_help": {
|
||||
"label_description": "Descrizione",
|
||||
"label_key": "Chiave"
|
||||
},
|
||||
"transient_book_details": {
|
||||
"label_candidate": "Candidato",
|
||||
"label_existing": "Esistente",
|
||||
"label_format": "Formato",
|
||||
"label_name": "Nome",
|
||||
"label_pages": "Pagine",
|
||||
"label_size": "Dimensione",
|
||||
"pages_table": {
|
||||
"filename": "Nome File",
|
||||
"height": "Altezza",
|
||||
"index": "Indice",
|
||||
"media_type": "Tipo media",
|
||||
"width": "Larghezza"
|
||||
},
|
||||
"title": "Dettagli Fumetto",
|
||||
"title_comparison": "Confronto Fumetto"
|
||||
},
|
||||
"transient_book_viewer": {
|
||||
"label_candidate": "Candidato",
|
||||
"label_existing": "Esistente",
|
||||
"page_of_pages": "{page} / {pages}",
|
||||
"title": "Ispeziona Fumetto",
|
||||
"title_comparison": "Confronto Fumetto"
|
||||
}
|
||||
},
|
||||
"enums": {
|
||||
"copy_mode": {
|
||||
"HARDLINK": "Hardlink / Copia File",
|
||||
"MOVE": "Sposta File"
|
||||
},
|
||||
"media_status": {
|
||||
"ERROR": "Errore",
|
||||
"OUTDATED": "Scaduto",
|
||||
"READY": "Pronto",
|
||||
"UNKNOWN": "Sconosciuto",
|
||||
"UNSUPPORTED": "Non supportato"
|
||||
},
|
||||
"reading_direction": {
|
||||
"LEFT_TO_RIGHT": "Da sinistra a destra",
|
||||
"RIGHT_TO_LEFT": "Da destra a sinistra",
|
||||
"VERTICAL": "Verticale",
|
||||
"WEBTOON": "Webtoon"
|
||||
},
|
||||
"series_status": {
|
||||
"ABANDONED": "Abbandonato",
|
||||
"ENDED": "Concluso",
|
||||
"HIATUS": "Hiatus",
|
||||
"ONGOING": "In corso"
|
||||
}
|
||||
},
|
||||
"error_codes": {
|
||||
"ERR_1000": "Impossibile accedere al file durante l'analisi",
|
||||
"ERR_1001": "Tipo di media non supportato",
|
||||
"ERR_1002": "Gli archivi RAR crittografati non sono supportati",
|
||||
"ERR_1003": "Gli archivi RAR solidi non sono supportati",
|
||||
"ERR_1004": "Gli archivi RAR multivolume non sono supportati",
|
||||
"ERR_1005": "Errore sconosciuto durante l'analisi del libro",
|
||||
"ERR_1006": "Il libro non contiene alcuna pagina",
|
||||
"ERR_1007": "Non è stato possibile analizzare alcune voci",
|
||||
"ERR_1008": "Errore sconosciuto durante il recupero delle voci del libro",
|
||||
"ERR_1009": "Esiste già una lista di lettura con quel nome",
|
||||
"ERR_1010": "Nessun libro è stato abbinato all'interno della lista di lettura richiesta",
|
||||
"ERR_1011": "Nessuna corrispondenza unica per la serie",
|
||||
"ERR_1012": "Nessuna corrispondenza per la serie",
|
||||
"ERR_1013": "Nessuna corrispondenza per il numero del fumetto nella serie",
|
||||
"ERR_1014": "Nessuna corrispondenza per il numero del fumetto nella serie",
|
||||
"ERR_1015": "Errore durante la deserializzazione della lista di lettura di ComicRack"
|
||||
},
|
||||
"filter": {
|
||||
"age_rating": "Fascia d'età",
|
||||
"age_rating_none": "Nessuna",
|
||||
"genre": "genere",
|
||||
"language": "lingua",
|
||||
"library": "libreria",
|
||||
"publisher": "editore",
|
||||
"release_date": "data di pubblicazione",
|
||||
"status": "stato",
|
||||
"tag": "etichetta",
|
||||
"unread": "Non letti"
|
||||
},
|
||||
"filter_drawer": {
|
||||
"filter": "filtro",
|
||||
"sort": "ordina"
|
||||
},
|
||||
"home": {
|
||||
"theme": "Tema",
|
||||
"translation": "Traduzione"
|
||||
},
|
||||
"library_navigation": {
|
||||
"browse": "Esplora",
|
||||
"collections": "Raccolte",
|
||||
"readlists": "Liste Lettura"
|
||||
},
|
||||
"login": {
|
||||
"create_user_account": "Crea account utente",
|
||||
"login": "Entra",
|
||||
"unclaimed_html": "Questo server Komga non è ancora attivo, devi creare un account utente per potervi accedere.<br><br>Scegli un'<strong>e-mail</strong> e una <strong>password</strong> e clicca su <strong>Crea account utente</strong>."
|
||||
},
|
||||
"media_analysis": {
|
||||
"comment": "Commento",
|
||||
"media_analysis": "Analisi dei media",
|
||||
"media_type": "Tipo Media",
|
||||
"name": "Nome",
|
||||
"size": "Dimensione",
|
||||
"status": "Stato",
|
||||
"url": "URL"
|
||||
},
|
||||
"menu": {
|
||||
"add_to_collection": "Aggiungi alla raccolta",
|
||||
"add_to_readlist": "Aggiungi all'elenco di lettura",
|
||||
"analyze": "Analizza",
|
||||
"delete": "Elimina",
|
||||
"download_series": "Scarica serie",
|
||||
"edit": "Modifica",
|
||||
"edit_metadata": "Modifica metadati",
|
||||
"mark_read": "Segna come letto",
|
||||
"mark_unread": "Segna come non letto",
|
||||
"refresh_metadata": "Aggiorna metadati",
|
||||
"scan_library_files": "Scansiona i file della libreria"
|
||||
},
|
||||
"navigation": {
|
||||
"home": "Home",
|
||||
"libraries": "Librerie",
|
||||
"logout": "Esci"
|
||||
},
|
||||
"page_not_found": {
|
||||
"go_back_to_home_page": "Torna alla pagina iniziale",
|
||||
"page_does_not_exist": "La pagina che stai cercando non esiste.",
|
||||
"page_not_found": "Pagina non trovata"
|
||||
},
|
||||
"read_more": {
|
||||
"less": "Mostra meno",
|
||||
"more": "Mostra di più"
|
||||
},
|
||||
"readlists_expansion_panel": {
|
||||
"manage_readlist": "Gestire lista di lettura",
|
||||
"title": "{name} lista di lettura"
|
||||
},
|
||||
"search": {
|
||||
"no_results": "La ricerca non ha dato risultati",
|
||||
"search": "Cerca",
|
||||
"search_for_something_else": "Prova a cercare qualcos'altro",
|
||||
"search_results_for": "Risultati della ricerca per «{name}»"
|
||||
},
|
||||
"searchbox": {
|
||||
"no_results": "Nessun risultato",
|
||||
"search_all": "Cerca tutto…"
|
||||
},
|
||||
"server": {
|
||||
"server_management": {
|
||||
"button_shutdown": "Spegni",
|
||||
"section_title": "Gestione Server"
|
||||
},
|
||||
"tab_title": "Server"
|
||||
},
|
||||
"server_settings": {
|
||||
"server_settings": "Impostazioni Server"
|
||||
},
|
||||
"settings_user": {
|
||||
"edit_shared_libraries": "Modifica librerie condivise",
|
||||
"edit_user": "Modifica utente",
|
||||
"role_administrator": "Amministratore",
|
||||
"role_user": "Utente"
|
||||
},
|
||||
"sort": {
|
||||
"books_count": "Numero fumetti",
|
||||
"date_added": "Data di aggiunta",
|
||||
"date_updated": "Data di aggiornamento",
|
||||
"file_name": "Nome del file",
|
||||
"file_size": "Dimensioni del file",
|
||||
"folder_name": "Nome della cartella",
|
||||
"name": "Nome",
|
||||
"number": "Numero",
|
||||
"release_date": "Data di uscita"
|
||||
},
|
||||
"theme": {
|
||||
"dark": "Scuro",
|
||||
"light": "Chiaro",
|
||||
"system": "Sistema"
|
||||
},
|
||||
"user_roles": {
|
||||
"ADMIN": "Amministratore",
|
||||
"FILE_DOWNLOAD": "Scaricamento file",
|
||||
"PAGE_STREAMING": "Leggi online",
|
||||
"USER": "Utente"
|
||||
},
|
||||
"users": {
|
||||
"users": "Utenti"
|
||||
},
|
||||
"welcome": {
|
||||
"add_library": "Aggiungi libreria",
|
||||
"no_libraries_yet": "Non sono ancora state aggiunte librerie!",
|
||||
"welcome_message": "Benvenuto/a su Komga"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,12 +26,17 @@
|
|||
"penciller": "tegnere",
|
||||
"writer": "skribenter"
|
||||
},
|
||||
"book_card": {
|
||||
"error": "Feil",
|
||||
"unsupported": "Ustøttet"
|
||||
},
|
||||
"bookreader": {
|
||||
"beginning_of_book": "Du er på begynnelsen av boka.",
|
||||
"changing_reading_direction": "Endrer leseretning til",
|
||||
"cycling_page_layout": "Endrer sideutforming til",
|
||||
"cycling_scale": "Endrer skalering til",
|
||||
"cycling_side_padding": "Endrer sidefyll til",
|
||||
"download_current_page": "Last ned nåværende side",
|
||||
"end_of_book": "Du har nådd slutten på boka.",
|
||||
"from_series_metadata": "fra serie metadata",
|
||||
"move_next": "Klikk eller trykk «Neste» igjen for å gå til neste bok.",
|
||||
|
|
|
|||
523
komga-webui/src/locales/pl.json
Normal file
523
komga-webui/src/locales/pl.json
Normal file
|
|
@ -0,0 +1,523 @@
|
|||
{
|
||||
"$vuetify": {
|
||||
"dataFooter": {
|
||||
"pageText": "{0}-{1} z {2}"
|
||||
},
|
||||
"dataTable": {
|
||||
"itemsPerPageText": "Rekordów na stronę:",
|
||||
"sortBy": "Sortowanie po"
|
||||
},
|
||||
"fileInput": {
|
||||
"counter": "{0} plików",
|
||||
"counterSize": "{0} plików ({1} wszystkich)"
|
||||
},
|
||||
"noDataText": "Brak dostępnych danych"
|
||||
},
|
||||
"account_settings": {
|
||||
"account_settings": "Ustawienia konta",
|
||||
"change_password": "zmiana hasła"
|
||||
},
|
||||
"author_roles": {
|
||||
"colorist": "koloryści",
|
||||
"cover": "okładka",
|
||||
"editor": "edytorzy",
|
||||
"inker": "inkerzy",
|
||||
"letterer": "liternicy",
|
||||
"penciller": "rysownicy",
|
||||
"writer": "scenarzyści"
|
||||
},
|
||||
"book_card": {
|
||||
"error": "Błąd",
|
||||
"unknown": "Do analizy",
|
||||
"unsupported": "Nieobsługiwany"
|
||||
},
|
||||
"bookreader": {
|
||||
"beginning_of_book": "Jesteś na początku książki.",
|
||||
"changing_reading_direction": "Zmiana kierunku czytania",
|
||||
"cycling_page_layout": "Zmiana układu strony",
|
||||
"cycling_scale": "Zmiana powiększenia",
|
||||
"cycling_side_padding": "Zmiana wypełnienia bocznego",
|
||||
"download_current_page": "Pobierz aktualną stronę",
|
||||
"end_of_book": "Osiągnięto koniec książki.",
|
||||
"from_series_metadata": "z metadanych serii",
|
||||
"move_next": "Kliknij lub naciśnij \"Dalej\" ponownie aby przejść do następnej książki.",
|
||||
"move_next_exit": "Kliknij lub naciśnij \"Dalej\" ponownie aby zamknąć czytnik.",
|
||||
"move_previous": "Kliknij lub naciśnij \"Wstecz\" ponownie aby przejść do poprzedniej książki.",
|
||||
"paged_reader_layout": {
|
||||
"double": "Podwójne strony",
|
||||
"double_no_cover": "Podwójne strony (bez okładki)",
|
||||
"single": "Pojedyncza strona"
|
||||
},
|
||||
"reader_settings": "Ustawienia czytnika",
|
||||
"scale_type": {
|
||||
"continuous_original": "Bez zmian",
|
||||
"continuous_width": "Dopasuj do szerokości",
|
||||
"height": "Dopasuj do wysokości",
|
||||
"original": "Bez zmian",
|
||||
"screen": "Ekran",
|
||||
"width": "Dopasuj do szerokości"
|
||||
},
|
||||
"settings": {
|
||||
"animate_page_transitions": "Animowane przejścia stron",
|
||||
"background_color": "Kolor tła",
|
||||
"background_colors": {
|
||||
"black": "czarny",
|
||||
"white": "biały"
|
||||
},
|
||||
"display": "Wyświetlanie",
|
||||
"general": "Ogólne",
|
||||
"gestures": "Gesty",
|
||||
"page_layout": "Układ strony",
|
||||
"paged": "Konfiguracja stron w czytniku",
|
||||
"reading_mode": "Tryb czytania",
|
||||
"scale_type": "Tryb skalowania",
|
||||
"side_padding": "Wypełnienie boczne",
|
||||
"side_padding_none": "Brak",
|
||||
"webtoon": "Konfiguracja czytnika Webtoon"
|
||||
},
|
||||
"shortcuts": {
|
||||
"close": "Zamknij",
|
||||
"cycle_page_layout": "Zmień układ strony",
|
||||
"cycle_scale": "Zmień skalowanie",
|
||||
"cycle_side_padding": "Zmień wypełnienie boczne",
|
||||
"first_page": "Pierwsza strona",
|
||||
"last_page": "Ostatnia strona",
|
||||
"left_to_right": "Od lewej do prawej",
|
||||
"menus": "Menu",
|
||||
"next_page": "Następna strona",
|
||||
"previous_page": "Poprzednia strona",
|
||||
"reader_navigation": "Nawigacja w czytniku",
|
||||
"right_to_left": "Od prawej do lewej",
|
||||
"settings": "Ustawienia",
|
||||
"show_hide_help": "Pokaż/ukryj pomoc",
|
||||
"show_hide_settings": "Pokaż/ukryj menu ustawień",
|
||||
"show_hide_thumbnails": "Pokaż/ukryj przeglądarkę miniaturek",
|
||||
"show_hide_toolbars": "Pokaż/ukryj paski narzędzi",
|
||||
"vertical": "Pionowy",
|
||||
"webtoon": "Web-komiks"
|
||||
}
|
||||
},
|
||||
"browse_book": {
|
||||
"comment": "KOMENTARZ",
|
||||
"download_file": "Pobierz plik",
|
||||
"file": "PLIK",
|
||||
"format": "FORMAT",
|
||||
"isbn": "ISBN",
|
||||
"navigation_within_readlist": "Nawigacja w ramach listy: {name}",
|
||||
"read_book": "Czytaj",
|
||||
"size": "ROZMIAR"
|
||||
},
|
||||
"browse_collection": {
|
||||
"edit_collection": "Edycja kolekcji",
|
||||
"edit_elements": "Edycja elementów",
|
||||
"manual_ordering": "sortowanie manualne"
|
||||
},
|
||||
"browse_readlist": {
|
||||
"edit_elements": "Edycja elementów",
|
||||
"edit_readlist": "Edytuj listę"
|
||||
},
|
||||
"browse_series": {
|
||||
"earliest_year_from_release_dates": "To jest najwcześniejszy rok z dat publikacji wszystkich książek w serii",
|
||||
"series_no_summary": "Ta seria nie ma podsumowania, dlatego uzupełniliśmy je dla Ciebie!",
|
||||
"summary_from_book": "Podsumowanie z książki {number}:"
|
||||
},
|
||||
"collections_expansion_panel": {
|
||||
"manage_collection": "Zarządzaj kolekcją",
|
||||
"title": "kolekcja {name}"
|
||||
},
|
||||
"common": {
|
||||
"all_libraries": "Wszystkie biblioteki",
|
||||
"books": "Książki",
|
||||
"books_n": "Brak książek | 1 książka | {count} książki",
|
||||
"cancel": "Anuluj",
|
||||
"close": "Zamknij",
|
||||
"collections": "Kolekcje",
|
||||
"create": "Utwórz",
|
||||
"delete": "Usuń",
|
||||
"download": "Pobierz",
|
||||
"email": "Adres e-mail",
|
||||
"filter_no_matches": "Aktywny filtr nie zwrócił żadnych wyników",
|
||||
"genre": "Gatunek",
|
||||
"go_to_library": "Przejdź do biblioteki",
|
||||
"locale_name": "polski",
|
||||
"locale_rtl": "false",
|
||||
"n_selected": "wybrano {count}",
|
||||
"nothing_to_show": "Nic do wyświetlenia",
|
||||
"pages": "strony",
|
||||
"pages_n": "Brak stron | 1 strona | {count} stron",
|
||||
"password": "Hasło",
|
||||
"publisher": "Wydawca",
|
||||
"read": "Czytaj",
|
||||
"readlists": "Listy",
|
||||
"required": "Wymagane",
|
||||
"roles": "Role",
|
||||
"series": "Serie",
|
||||
"tags": "Tagi",
|
||||
"use_filter_panel_to_change_filter": "Użyj panelu filtrów aby zmienić aktywny filtr",
|
||||
"year": "rok"
|
||||
},
|
||||
"dashboard": {
|
||||
"keep_reading": "Kontynuuj czytanie",
|
||||
"on_deck": "Na stosie",
|
||||
"recently_added_books": "Ostatnio dodane książki",
|
||||
"recently_added_series": "Ostatnio dodane cykle",
|
||||
"recently_updated_series": "Ostatnio aktualizowane cykle"
|
||||
},
|
||||
"data_import": {
|
||||
"book_number": "Numer książki: {name}",
|
||||
"book_series": "Seria: {name}",
|
||||
"button_import": "Importuj",
|
||||
"comicrack_preambule_html": "Importowanie istniejących list z programu ComicRack w formacie <code>.cbl</code>.<br>Komga spróbuje dopasować cykle i tomy do cykli i tomów znajdujących się w bibliotekach.",
|
||||
"field_files_label": "Listy programu ComicRack",
|
||||
"import_read_lists": "Zaimportuj listy",
|
||||
"imported_as": "Zaimportowano jako {name}",
|
||||
"results_preambule": "Wynik importu wyświetlono poniżej. Wyświetlono także niedopasowane książki dla każdego przetworzonego pliku.",
|
||||
"size_limit": "Plik powinien być mniejszy niż {size} MB",
|
||||
"tab_title": "Import danych"
|
||||
},
|
||||
"dialog": {
|
||||
"add_to_collection": {
|
||||
"button_create": "Utwórz",
|
||||
"card_collection_subtitle": "Brak serii | 1 seria | {count} serie",
|
||||
"dialog_title": "Dodaj do kolekcji",
|
||||
"field_search_create": "Szukaj lub utwórz kolekcję",
|
||||
"field_search_create_error": "Kolekcja z taką nazwą już istnieje",
|
||||
"label_no_matching_collection": "Brak pasujących kolekcji"
|
||||
},
|
||||
"add_to_readlist": {
|
||||
"button_create": "Utwórz",
|
||||
"card_readlist_subtitle": "Brak tomów | 1 tom | {count} tomów",
|
||||
"dialog_title": "Dodaj do listy",
|
||||
"field_search_create": "Szukaj lub utwórz listę",
|
||||
"field_search_create_error": "Lista z taką nazwą już istnieje",
|
||||
"label_no_matching_readlist": "Brak pasujących list"
|
||||
},
|
||||
"add_user": {
|
||||
"button_cancel": "Anuluj",
|
||||
"button_confirm": "Dodaj",
|
||||
"dialog_title": "Dodaj użytkownika",
|
||||
"field_email": "E-mail",
|
||||
"field_email_error": "Adres e-mail musi być poprawny",
|
||||
"field_password": "Hasło",
|
||||
"field_role_administrator": "Administrator",
|
||||
"field_role_file_download": "Pobieranie pliku",
|
||||
"field_role_page_streaming": "Strumieniowanie stron",
|
||||
"label_roles": "Role"
|
||||
},
|
||||
"delete_collection": {
|
||||
"button_cancel": "Anuluj",
|
||||
"button_confirm": "Usuń",
|
||||
"confirm_delete": "Tak, usuń kolekcję „{name}”",
|
||||
"dialog_title": "Usuń kolekcję",
|
||||
"warning_html": "Kolekcja <b>{name}</b> zostanie usunięta z tego serwera. Nie będzie to miało wpływu na Twoje pliki. Ta czynność <b>nie może</b> być cofnięta. Kontynuować?"
|
||||
},
|
||||
"delete_library": {
|
||||
"button_cancel": "Anuluj",
|
||||
"button_confirm": "Usuń",
|
||||
"confirm_delete": "Tak, usuń bibliotekę \"{name}\"",
|
||||
"title": "Usuń bibliotekę",
|
||||
"warning_html": "Biblioteka <b>{name}</b> zostanie usunięta z tego serwera. Nie będzie to miało wpływu na Twoje pliki. Ta czynność <b>nie może</b> być cofnięta. Kontynuować?"
|
||||
},
|
||||
"delete_readlist": {
|
||||
"button_cancel": "Anuluj",
|
||||
"button_confirm": "Usuń",
|
||||
"confirm_delete": "Tak, usuń listę \"{name}\"",
|
||||
"dialog_title": "Usuwanie listy",
|
||||
"warning_html": "Lista <b>{name}</b> zostanie usunięta z tego serwera. Nie będzie to miało wpływu na Twoje pliki. Ta czynność <b>nie może</b> być cofnięta. Kontynuować?"
|
||||
},
|
||||
"delete_user": {
|
||||
"button_cancel": "Anuluj",
|
||||
"button_confirm": "Usuń",
|
||||
"confirm_delete": "Tak, usuń użytkownika \"{name}\"",
|
||||
"dialog_title": "Usuń użytkownika",
|
||||
"warning_html": "Użytkownik <b>{name}</b> zostanie usunięty z tego serwera. Ta czynność <b>nie może</b> zostać cofnięta. Kontynuować?"
|
||||
},
|
||||
"edit_books": {
|
||||
"authors_notice_multiple_edit": "Edytujesz autorów dla wielu książek. Spowoduje to zastąpienie istniejących autorów każdej książki.",
|
||||
"button_cancel": "Anuluj",
|
||||
"button_confirm": "Zapisz zmiany",
|
||||
"dialog_title_multiple": "Edycja książki | Edycja {count} książek",
|
||||
"dialog_title_single": "Edycja {book}",
|
||||
"field_isbn": "ISBN",
|
||||
"field_isbn_error": "Musi być prawidłowym numerem ISBN 13",
|
||||
"field_number": "Numer",
|
||||
"field_number_sort": "Numer sortowania",
|
||||
"field_number_sort_hint": "Można użyć liczb dziesiętnych",
|
||||
"field_release_date": "Data wydania",
|
||||
"field_release_date_error": "Musi być prawidłową datą w formacie RRRR-MM-DD",
|
||||
"field_summary": "Streszczenie",
|
||||
"field_tags": "Tagi",
|
||||
"field_title": "Tytuł",
|
||||
"tab_authors": "Autorzy",
|
||||
"tab_general": "Ogólne",
|
||||
"tab_tags": "Tagi",
|
||||
"tags_notice_multiple_edit": "Edytujesz tagi dla wielu książek. Spowoduje to zastąpienie istniejących tagów każdej książki."
|
||||
},
|
||||
"edit_collection": {
|
||||
"button_cancel": "Anuluj",
|
||||
"button_confirm": "Zapisz zmiany",
|
||||
"dialog_title": "Edycja kolekcji",
|
||||
"field_manual_ordering": "Ręczne sortowanie",
|
||||
"label_ordering": "Domyślnie cykle w kolekcji będą uporządkowane według nazwy. Można włączyć ręczne porządkowanie, aby zdefiniować własną kolejność."
|
||||
},
|
||||
"edit_library": {
|
||||
"button_browse": "Przeglądaj",
|
||||
"button_cancel": "Anuluj",
|
||||
"button_confirm_add": "Dodaj",
|
||||
"button_confirm_edit": "Edytuj",
|
||||
"dialog_title_add": "Dodawanie biblioteki",
|
||||
"dialot_title_edit": "Edycja biblioteki",
|
||||
"field_import_barcode_isbn": "Kod kreskowy ISBN",
|
||||
"field_import_comicinfo_book": "Metadane książki",
|
||||
"field_import_comicinfo_collections": "Kolekcje",
|
||||
"field_import_comicinfo_readlists": "Listy",
|
||||
"field_import_comicinfo_series": "Metadane cykli",
|
||||
"field_import_epub_book": "Metadane książki",
|
||||
"field_import_epub_series": "Metadane cykli",
|
||||
"field_import_local_artwork": "Grafika lokalna",
|
||||
"field_name": "Nazwa",
|
||||
"field_root_folder": "Folder główny",
|
||||
"field_scanner_deep_scan": "Głębokie skanowanie",
|
||||
"field_scanner_force_directory_modified_time": "Wymuś czas modyfikacji katalogu",
|
||||
"file_browser_dialog_button_confirm": "Wybierz",
|
||||
"file_browser_dialog_title": "Folder główny biblioteki",
|
||||
"label_import_barcode_isbn": "Importowanie ISBN z kodu kreskowego",
|
||||
"label_import_comicinfo": "Importowanie metadanych dla CBR/CBZ zawierających plik ComicInfo.xml",
|
||||
"label_import_epub": "Importowanie metadanych z plików EPUB",
|
||||
"label_import_local": "Importowanie lokalnych zasobów",
|
||||
"label_scanner": "Skaner",
|
||||
"tab_general": "Ogólne",
|
||||
"tab_options": "Opcje"
|
||||
},
|
||||
"edit_readlist": {
|
||||
"button_cancel": "Anuluj",
|
||||
"button_confirm": "Zapisz zmiany",
|
||||
"dialog_title": "Edycja listy",
|
||||
"field_name": "Nazwa"
|
||||
},
|
||||
"edit_series": {
|
||||
"button_cancel": "Anuluj",
|
||||
"button_confirm": "Zapisz zmiany",
|
||||
"dialog_title_multiple": "Edycja cyklu | Edycja {count} cykli",
|
||||
"dialog_title_single": "Edycja {series}",
|
||||
"field_age_rating": "Klasyfikacja wiekowa",
|
||||
"field_age_rating_error": "Klasyfikacja wiekowa musi wynosić 0 lub więcej",
|
||||
"field_genres": "Gatunki",
|
||||
"field_language": "Język",
|
||||
"field_publisher": "Wydawca",
|
||||
"field_reading_direction": "Kierunek czytania",
|
||||
"field_sort_title": "Tytuł sortowania",
|
||||
"field_status": "Status",
|
||||
"field_summary": "Streszczenie",
|
||||
"field_tags": "Tagi",
|
||||
"field_title": "Tytuł",
|
||||
"mixed": "MIESZANY",
|
||||
"tab_general": "Ogólne",
|
||||
"tab_tags": "Tagi",
|
||||
"tags_notice_multiple_edit": "Edytujesz tagi dla wielu cykli. Spowoduje to zastąpienie istniejących tagów w każdym cyklu."
|
||||
},
|
||||
"edit_user": {
|
||||
"button_cancel": "Anuluj",
|
||||
"button_confirm": "Zapisz zmiany",
|
||||
"dialog_title": "Edycja użytkownika",
|
||||
"label_roles_for": "Role dla {name}"
|
||||
},
|
||||
"edit_user_shared_libraries": {
|
||||
"button_cancel": "Anuluj",
|
||||
"button_confirm": "Zapisz zmiany",
|
||||
"dialog_title": "Edycja bibliotek udostępnionych",
|
||||
"field_all_libraries": "Wszystkie biblioteki",
|
||||
"label_shared_with": "Udostępnione {name}"
|
||||
},
|
||||
"file_browser": {
|
||||
"button_cancel": "Anuluj",
|
||||
"button_confirm_default": "Wybierz",
|
||||
"dialog_title_default": "Przeglądarka plików",
|
||||
"parent_directory": "Nadrzędny"
|
||||
},
|
||||
"password_change": {
|
||||
"button_cancel": "Anuluj",
|
||||
"button_confirm": "Zmień hasło",
|
||||
"dialog_title": "Zmiana hasła",
|
||||
"field_new_password": "Nowe hasło",
|
||||
"field_new_password_error": "Wymagane jest nowe hasło.",
|
||||
"field_repeat_password": "Powtórzenie nowego hasła",
|
||||
"field_repeat_password_error": "Hasła muszą być identyczne."
|
||||
},
|
||||
"server_stop": {
|
||||
"button_cancel": "Anuluj",
|
||||
"button_confirm": "Wyłącz",
|
||||
"confirmation_message": "Jesteś pewien, że chcesz wyłączyć Komgę?",
|
||||
"dialog_title": "Wyłączenie serwera"
|
||||
},
|
||||
"shortcut_help": {
|
||||
"label_description": "Opis",
|
||||
"label_key": "Klucz"
|
||||
}
|
||||
},
|
||||
"enums": {
|
||||
"media_status": {
|
||||
"ERROR": "Błąd",
|
||||
"OUTDATED": "Nieaktualny",
|
||||
"READY": "Gotowy",
|
||||
"UNKNOWN": "Nieznany",
|
||||
"UNSUPPORTED": "Nieobsługiwany"
|
||||
},
|
||||
"reading_direction": {
|
||||
"LEFT_TO_RIGHT": "Od lewej do prawej",
|
||||
"RIGHT_TO_LEFT": "Od prawej do lewej",
|
||||
"VERTICAL": "Pionowy",
|
||||
"WEBTOON": "Web-komiks"
|
||||
},
|
||||
"series_status": {
|
||||
"ABANDONED": "Porzucony",
|
||||
"ENDED": "Zakończony",
|
||||
"HIATUS": "Zawieszony",
|
||||
"ONGOING": "Trwający"
|
||||
}
|
||||
},
|
||||
"error_codes": {
|
||||
"ERR_1000": "Nie można uzyskać dostępu do pliku podczas analizy",
|
||||
"ERR_1001": "Typ mediów nie jest obsługiwany",
|
||||
"ERR_1002": "Zaszyfrowane archiwa RAR nie są obsługiwane",
|
||||
"ERR_1003": "Archiwa Solid RAR nie są obsługiwane",
|
||||
"ERR_1004": "Archiwa RAR o wielu woluminach nie są obsługiwane",
|
||||
"ERR_1005": "Nieznany błąd podczas analizowania książki",
|
||||
"ERR_1006": "Książka nie zawiera żadnej strony",
|
||||
"ERR_1007": "Niektóre wpisy nie mogły zostać przeanalizowane",
|
||||
"ERR_1008": "Nieznany błąd podczas pobierania pozycji książki",
|
||||
"ERR_1009": "Lista o tej nazwie już istnieje",
|
||||
"ERR_1010": "Żadne książki nie zostały dopasowane w żądaniu listy",
|
||||
"ERR_1011": "Brak unikalnego dopasowania dla cyklu",
|
||||
"ERR_1012": "Brak dopasowania dla cykli",
|
||||
"ERR_1013": "Brak unikalnego dopasowania dla numeru tomu w cyklu",
|
||||
"ERR_1014": "Brak dopasowania dla numeru tomu w cyklu",
|
||||
"ERR_1015": "Błąd podczas deserializacji listy ComicRack"
|
||||
},
|
||||
"filter": {
|
||||
"age_rating": "Klasyfikacja wiekowa",
|
||||
"age_rating_none": "Brak",
|
||||
"genre": "gatunek",
|
||||
"language": "język",
|
||||
"library": "biblioteka",
|
||||
"publisher": "wydawca",
|
||||
"release_date": "data wydania",
|
||||
"status": "status",
|
||||
"tag": "tag",
|
||||
"unread": "Nieprzeczytane"
|
||||
},
|
||||
"filter_drawer": {
|
||||
"filter": "filtr",
|
||||
"sort": "sortowanie"
|
||||
},
|
||||
"home": {
|
||||
"theme": "Motyw",
|
||||
"translation": "Tłumaczenie"
|
||||
},
|
||||
"library_navigation": {
|
||||
"browse": "Przeglądaj",
|
||||
"collections": "Kolekcje",
|
||||
"readlists": "Listy"
|
||||
},
|
||||
"login": {
|
||||
"create_user_account": "Tworzenie konta użytkownika",
|
||||
"login": "Zaloguj",
|
||||
"unclaimed_html": "Ten serwer Komga nie jest jeszcze aktywny, musisz utworzyć konto użytkownika, by móc uzyskać do niego dostęp.<br><br>Wybierz <strong>e-mail</strong> oraz <strong>hasło</strong> i kliknij na <strong>Utwórz konto użytkownika</strong>."
|
||||
},
|
||||
"media_analysis": {
|
||||
"comment": "Komentarz",
|
||||
"media_analysis": "Analiza mediów",
|
||||
"media_type": "Typ mediów",
|
||||
"name": "Nazwa",
|
||||
"size": "Rozmiar",
|
||||
"status": "Status",
|
||||
"url": "Adres URL"
|
||||
},
|
||||
"menu": {
|
||||
"add_to_collection": "Dodaj do kolekcji",
|
||||
"add_to_readlist": "Dodaj do listy",
|
||||
"analyze": "Analizuj",
|
||||
"delete": "Usuń",
|
||||
"download_series": "Pobierz cykl",
|
||||
"edit": "Edytuj",
|
||||
"edit_metadata": "Edytuj metadane",
|
||||
"mark_read": "Oznacz jako przeczytane",
|
||||
"mark_unread": "Oznacz jako nieprzeczytane",
|
||||
"refresh_metadata": "Odśwież metadane",
|
||||
"scan_library_files": "Skanuj pliki bibliotek"
|
||||
},
|
||||
"navigation": {
|
||||
"home": "Ekran główny",
|
||||
"libraries": "Biblioteki",
|
||||
"logout": "Wyloguj się"
|
||||
},
|
||||
"page_not_found": {
|
||||
"go_back_to_home_page": "Wróć do ekranu głównego",
|
||||
"page_does_not_exist": "Strona, której szukasz, nie istnieje.",
|
||||
"page_not_found": "Strona nie znaleziona"
|
||||
},
|
||||
"read_more": {
|
||||
"less": "Mniej",
|
||||
"more": "Więcej"
|
||||
},
|
||||
"readlists_expansion_panel": {
|
||||
"manage_readlist": "Zarządzanie listami",
|
||||
"title": "lista {name}"
|
||||
},
|
||||
"search": {
|
||||
"no_results": "Wyszukiwanie nie zwróciło żadnych wyników",
|
||||
"search": "Wyszukaj",
|
||||
"search_for_something_else": "Spróbuj wyszukać coś innego",
|
||||
"search_results_for": "Wyniki wyszukiwania dla \"{name}\""
|
||||
},
|
||||
"searchbox": {
|
||||
"no_results": "Brak Wyników",
|
||||
"search_all": "Przeszukaj wszystkie…"
|
||||
},
|
||||
"server": {
|
||||
"server_management": {
|
||||
"button_shutdown": "Wyłącz",
|
||||
"section_title": "Zarządzanie serwerem"
|
||||
},
|
||||
"tab_title": "Serwer"
|
||||
},
|
||||
"server_settings": {
|
||||
"server_settings": "Ustawienia serwera"
|
||||
},
|
||||
"settings_user": {
|
||||
"edit_shared_libraries": "Edycja bibliotek udostępnionych",
|
||||
"edit_user": "Edycja użytkownika",
|
||||
"role_administrator": "Administrator",
|
||||
"role_user": "Użytkownik"
|
||||
},
|
||||
"sort": {
|
||||
"books_count": "Liczba książek",
|
||||
"date_added": "Data dodania",
|
||||
"date_updated": "Data aktualizacji",
|
||||
"file_name": "Nazwa pliku",
|
||||
"file_size": "Rozmiar pliku",
|
||||
"folder_name": "Nazwa katalogu",
|
||||
"name": "Nazwa",
|
||||
"number": "Numer",
|
||||
"release_date": "Data wydania"
|
||||
},
|
||||
"theme": {
|
||||
"dark": "Ciemny",
|
||||
"light": "Jasny",
|
||||
"system": "Systemowy"
|
||||
},
|
||||
"user_roles": {
|
||||
"ADMIN": "Administrator",
|
||||
"FILE_DOWNLOAD": "Pobieranie plików",
|
||||
"PAGE_STREAMING": "Strumieniowanie stron",
|
||||
"USER": "Użytkownik"
|
||||
},
|
||||
"users": {
|
||||
"users": "Użytkownicy"
|
||||
},
|
||||
"welcome": {
|
||||
"add_library": "Dodaj bibliotekę",
|
||||
"no_libraries_yet": "Nie dodano jeszcze żadnych bibliotek!",
|
||||
"welcome_message": "Witamy w Komga"
|
||||
}
|
||||
}
|
||||
|
|
@ -26,21 +26,47 @@
|
|||
"penciller": "formgivare",
|
||||
"writer": "författare"
|
||||
},
|
||||
"book_card": {
|
||||
"error": "Fel",
|
||||
"unknown": "Ska analyseras",
|
||||
"unsupported": "Stödjs inte"
|
||||
},
|
||||
"book_import": {
|
||||
"button_browse": "Bläddra",
|
||||
"button_import": "Importera",
|
||||
"button_scan": "Skanna",
|
||||
"button_select_series": "Välj serie",
|
||||
"field_import_path": "Importera från mapp",
|
||||
"row": {
|
||||
"error_analyze_first": "Boken måste analyseras först",
|
||||
"error_choose_series": "Välj en serie",
|
||||
"error_only_import_no_errors": "Kan bara importera böcker utan fel",
|
||||
"warning_upgrade": "Befintlig bok kommer att uppgraderas"
|
||||
},
|
||||
"table": {
|
||||
"destination_name": "Målnamn",
|
||||
"file_name": "Filnamn",
|
||||
"number": "Nummer",
|
||||
"series": "Serie"
|
||||
},
|
||||
"title": "Importera"
|
||||
},
|
||||
"bookreader": {
|
||||
"beginning_of_book": "Du är i början av boken.",
|
||||
"changing_reading_direction": "Byt läsriktning till",
|
||||
"cycling_page_layout": "Växla Sidlayout",
|
||||
"cycling_scale": "Växla Skala",
|
||||
"cycling_side_padding": "Växla Spaltfyllnad",
|
||||
"download_current_page": "Ladda ner nuvarande sida",
|
||||
"end_of_book": "Du har kommit till slutet av boken.",
|
||||
"from_series_metadata": "från seriens metadata",
|
||||
"move_next": "Tryck \"Nästa\" igen för att gå till nästa bok.",
|
||||
"move_next_exit": "Tryck \"Nästa\" igen för att stänga läsaren.",
|
||||
"move_previous": "Tryck \"Föregående\" igen för att gå till föregående bok.",
|
||||
"move_next": "Tryck ”Nästa” igen för att gå till nästa bok.",
|
||||
"move_next_exit": "Tryck ”Nästa” igen för att stänga läsaren.",
|
||||
"move_previous": "Tryck ”Föregående” igen för att gå till föregående bok.",
|
||||
"paged_reader_layout": {
|
||||
"double": "Dubbla sidor",
|
||||
"double_no_cover": "Dubbla sidor (inget omslag)",
|
||||
"single": "En sida"
|
||||
"single": "Enkelsidig"
|
||||
},
|
||||
"reader_settings": "Läsinställningar",
|
||||
"scale_type": {
|
||||
|
|
@ -161,7 +187,7 @@
|
|||
"book_number": "Boknummer: {name}",
|
||||
"book_series": "Serie: {name}",
|
||||
"button_import": "Importera",
|
||||
"comicrack_preambule_html": "Du kan importera existerande ComicRack Läslistor i <code>.cbl</code>-format.<br/>Komga kommer att försöka matcha innehållet med serier och böker i dina bibliotek.",
|
||||
"comicrack_preambule_html": "Du kan importera existerande ComicRack Läslistor i <code>.cbl</code>-format.<br>Komga kommer att försöka matcha innehållet med serier och böker i dina bibliotek.",
|
||||
"field_files_label": "ComicRack Läslistor (.cbl)",
|
||||
"import_read_lists": "Importera Läslistor",
|
||||
"imported_as": "Importerad som {name}",
|
||||
|
|
@ -201,28 +227,28 @@
|
|||
"delete_collection": {
|
||||
"button_cancel": "Avbryt",
|
||||
"button_confirm": "Radera",
|
||||
"confirm_delete": "Ja, radera samlingen \"{name}\"",
|
||||
"confirm_delete": "Ja, radera samlingen ”{name}”",
|
||||
"dialog_title": "Radera samling",
|
||||
"warning_html": "Samlingen <b>{name}</b> tas bort från den här servern. Dina mediefiler påverkas inte. Detta <b>kan inte</b> ångras. Fortsätta?"
|
||||
},
|
||||
"delete_library": {
|
||||
"button_cancel": "Avbryt",
|
||||
"button_confirm": "Radera",
|
||||
"confirm_delete": "Ja, redera biblioteket \"{name}\"",
|
||||
"confirm_delete": "Ja, redera biblioteket ”{name}”",
|
||||
"title": "Redera bibliotek",
|
||||
"warning_html": "Biblioteket <b>{name}</b> kommer att tas bort från den här servern. Dina mediefiler påverkas inte. Detta <b>kan inte</b> ångras. Fortsätta?"
|
||||
},
|
||||
"delete_readlist": {
|
||||
"button_cancel": "Avbryt",
|
||||
"button_confirm": "Radera",
|
||||
"confirm_delete": "Ja, radera läslistan \"{name}\"",
|
||||
"confirm_delete": "Ja, radera läslistan ”{name}”",
|
||||
"dialog_title": "Radera läslista",
|
||||
"warning_html": "Läslistan <b>{name}</b> tas bort från den här servern. Dina mediefiler påverkas inte. Detta <b>kan inte</b> ångras. Fortsätta?"
|
||||
},
|
||||
"delete_user": {
|
||||
"button_cancel": "Avbryt",
|
||||
"button_confirm": "Radera",
|
||||
"confirm_delete": "Ja, radera användare \"{name}\"",
|
||||
"confirm_delete": "Ja, radera användare ”{name}”",
|
||||
"dialog_title": "Radera användare",
|
||||
"warning_html": "Användaren <b>{name}</b> kommer att tas bort från den här servern. Detta <b>kan inte</b> ångras. Fortsätta?"
|
||||
},
|
||||
|
|
@ -329,6 +355,16 @@
|
|||
"dialog_title_default": "Filhanterare",
|
||||
"parent_directory": "Ovanstående"
|
||||
},
|
||||
"filename_chooser": {
|
||||
"button_choose": "Välj",
|
||||
"field_destination_filename": "Namn på målfil",
|
||||
"label_source_filename": "Källfilens namn",
|
||||
"table": {
|
||||
"existing_file": "Befintlig fil",
|
||||
"order": "Ordning"
|
||||
},
|
||||
"title": "Namn på målfil"
|
||||
},
|
||||
"password_change": {
|
||||
"button_cancel": "Avbryt",
|
||||
"button_confirm": "Ändra lösenord",
|
||||
|
|
@ -338,6 +374,10 @@
|
|||
"field_repeat_password": "Upprepa nytt lösenord",
|
||||
"field_repeat_password_error": "Lösenorden måste vara identiska."
|
||||
},
|
||||
"series_picker": {
|
||||
"label_search_series": "Sök serie",
|
||||
"title": "Välj serie"
|
||||
},
|
||||
"server_stop": {
|
||||
"button_cancel": "Avbryt",
|
||||
"button_confirm": "Stopp",
|
||||
|
|
@ -347,9 +387,37 @@
|
|||
"shortcut_help": {
|
||||
"label_description": "Beskrivning",
|
||||
"label_key": "Nyckel"
|
||||
},
|
||||
"transient_book_details": {
|
||||
"label_candidate": "Kandidat",
|
||||
"label_existing": "Befintlig",
|
||||
"label_format": "Format",
|
||||
"label_name": "Namn",
|
||||
"label_pages": "Sidor",
|
||||
"label_size": "Storlek",
|
||||
"pages_table": {
|
||||
"filename": "Filnamn",
|
||||
"height": "Höjd",
|
||||
"index": "Index",
|
||||
"media_type": "Mediatyp",
|
||||
"width": "Bredd"
|
||||
},
|
||||
"title": "Bokinformation",
|
||||
"title_comparison": "Bokjämförelse"
|
||||
},
|
||||
"transient_book_viewer": {
|
||||
"label_candidate": "Kandidat",
|
||||
"label_existing": "Befintlig",
|
||||
"page_of_pages": "{page} / {pages}",
|
||||
"title": "Inspektera boken",
|
||||
"title_comparison": "Bokjämförelse"
|
||||
}
|
||||
},
|
||||
"enums": {
|
||||
"copy_mode": {
|
||||
"HARDLINK": "Hårdlänk/kopiera filer",
|
||||
"MOVE": "Flytta filer"
|
||||
},
|
||||
"media_status": {
|
||||
"ERROR": "Fel",
|
||||
"OUTDATED": "Föråldrad",
|
||||
|
|
@ -374,8 +442,8 @@
|
|||
"ERR_1000": "Filen kunde inte nås under analysen",
|
||||
"ERR_1001": "Mediatyp är inte supporterad",
|
||||
"ERR_1002": "Krypterade RAR-arkiv är inte supporterade",
|
||||
"ERR_1003": "\"Solid RAR\"-arkiv är inte supporterade",
|
||||
"ERR_1004": "\"Multi Volume RAR\"-arkiv är inte supporterade",
|
||||
"ERR_1003": "Solid RAR-arkiv är inte supporterade",
|
||||
"ERR_1004": "Multi Volume RAR-arkiv är inte supporterade",
|
||||
"ERR_1005": "Okänt fel vid analys av bok",
|
||||
"ERR_1006": "Boken innehåller inte några sidor",
|
||||
"ERR_1007": "Vissa poster kunde inte analyseras",
|
||||
|
|
@ -462,7 +530,7 @@
|
|||
"no_results": "Sökningen gav inga resultat",
|
||||
"search": "Sök",
|
||||
"search_for_something_else": "Försök söka efter något annat",
|
||||
"search_results_for": "Sökresultat för \"{name}\""
|
||||
"search_results_for": "Sökresultat för ”{name}”"
|
||||
},
|
||||
"searchbox": {
|
||||
"no_results": "Inga resultat",
|
||||
|
|
|
|||
|
|
@ -26,12 +26,38 @@
|
|||
"penciller": "铅稿",
|
||||
"writer": "作者"
|
||||
},
|
||||
"book_card": {
|
||||
"error": "错误",
|
||||
"unknown": "待分析",
|
||||
"unsupported": "不支持"
|
||||
},
|
||||
"book_import": {
|
||||
"button_browse": "浏览",
|
||||
"button_import": "导入",
|
||||
"button_scan": "扫描",
|
||||
"button_select_series": "选择系列",
|
||||
"field_import_path": "从文件夹导入",
|
||||
"row": {
|
||||
"error_analyze_first": "书籍需要先分析",
|
||||
"error_choose_series": "选择一个系列",
|
||||
"error_only_import_no_errors": "只能导入没有错误的书籍",
|
||||
"warning_upgrade": "现有书籍将升级"
|
||||
},
|
||||
"table": {
|
||||
"destination_name": "目标名称",
|
||||
"file_name": "文件名",
|
||||
"number": "编号",
|
||||
"series": "系列"
|
||||
},
|
||||
"title": "导入"
|
||||
},
|
||||
"bookreader": {
|
||||
"beginning_of_book": "你可以开始阅读这本书.",
|
||||
"changing_reading_direction": "将阅读方向更改为",
|
||||
"cycling_page_layout": "切换页模式",
|
||||
"cycling_scale": "切换缩放",
|
||||
"cycling_side_padding": "切换边距",
|
||||
"download_current_page": "下载当前页面",
|
||||
"end_of_book": "你已经阅读完这本书了.",
|
||||
"from_series_metadata": "来自系列元数据",
|
||||
"move_next": "再次单击或按“下一步”移动到下一本书。",
|
||||
|
|
@ -161,7 +187,7 @@
|
|||
"book_number": "书号: {name}",
|
||||
"book_series": "系列: {name}",
|
||||
"button_import": "导入",
|
||||
"comicrack_preambule_html": "您可以导入<code>.cbl</code>格式的阅读列表。<br/>Komga将尝试将你提供的系列和书号与你的库中的系列和书号相匹配。",
|
||||
"comicrack_preambule_html": "您可以导入<code>.cbl</code>格式的阅读列表.<br>Komga将尝试将你提供的系列和书号与你的库中的系列和书号相匹配.",
|
||||
"field_files_label": "阅读列表(.cbl)",
|
||||
"import_read_lists": "导入阅读列表",
|
||||
"imported_as": "导入为 {name}",
|
||||
|
|
@ -329,6 +355,16 @@
|
|||
"dialog_title_default": "文件浏览器",
|
||||
"parent_directory": "父目录"
|
||||
},
|
||||
"filename_chooser": {
|
||||
"button_choose": "选择",
|
||||
"field_destination_filename": "目标文件名",
|
||||
"label_source_filename": "源文件名",
|
||||
"table": {
|
||||
"existing_file": "现有文件",
|
||||
"order": "命令"
|
||||
},
|
||||
"title": "目标文件名"
|
||||
},
|
||||
"password_change": {
|
||||
"button_cancel": "取消",
|
||||
"button_confirm": "修改密码",
|
||||
|
|
@ -338,6 +374,10 @@
|
|||
"field_repeat_password": "确认新密码",
|
||||
"field_repeat_password_error": "密码必须相同."
|
||||
},
|
||||
"series_picker": {
|
||||
"label_search_series": "搜索系列",
|
||||
"title": "选择系列"
|
||||
},
|
||||
"server_stop": {
|
||||
"button_cancel": "取消",
|
||||
"button_confirm": "停止",
|
||||
|
|
@ -347,9 +387,37 @@
|
|||
"shortcut_help": {
|
||||
"label_description": "说明",
|
||||
"label_key": "按键"
|
||||
},
|
||||
"transient_book_details": {
|
||||
"label_candidate": "候选",
|
||||
"label_existing": "现有的",
|
||||
"label_format": "文件格式",
|
||||
"label_name": "名称",
|
||||
"label_pages": "页数",
|
||||
"label_size": "文件大小",
|
||||
"pages_table": {
|
||||
"filename": "文件名",
|
||||
"height": "高",
|
||||
"index": "索引",
|
||||
"media_type": "媒体类型",
|
||||
"width": "宽"
|
||||
},
|
||||
"title": "书籍详情",
|
||||
"title_comparison": "书籍比较"
|
||||
},
|
||||
"transient_book_viewer": {
|
||||
"label_candidate": "候选",
|
||||
"label_existing": "现有的",
|
||||
"page_of_pages": "{page} / {pages}",
|
||||
"title": "查验书籍",
|
||||
"title_comparison": "书籍比较"
|
||||
}
|
||||
},
|
||||
"enums": {
|
||||
"copy_mode": {
|
||||
"HARDLINK": "硬链接/复制文件",
|
||||
"MOVE": "移动文件"
|
||||
},
|
||||
"media_status": {
|
||||
"ERROR": "错误",
|
||||
"OUTDATED": "过期",
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import komgaLibraries from './plugins/komga-libraries.plugin'
|
|||
import komgaReferential from './plugins/komga-referential.plugin'
|
||||
import komgaSeries from './plugins/komga-series.plugin'
|
||||
import komgaUsers from './plugins/komga-users.plugin'
|
||||
import komgaTransientBooks from './plugins/komga-transientbooks.plugin'
|
||||
import vuetify from './plugins/vuetify'
|
||||
import './public-path'
|
||||
import router from './router'
|
||||
|
|
@ -35,6 +36,7 @@ Vue.use(komgaReadLists, {http: Vue.prototype.$http})
|
|||
Vue.use(komgaBooks, {http: Vue.prototype.$http})
|
||||
Vue.use(komgaReferential, {http: Vue.prototype.$http})
|
||||
Vue.use(komgaClaim, {http: Vue.prototype.$http})
|
||||
Vue.use(komgaTransientBooks, {http: Vue.prototype.$http})
|
||||
Vue.use(komgaUsers, {store: store, http: Vue.prototype.$http})
|
||||
Vue.use(komgaLibraries, {store: store, http: Vue.prototype.$http})
|
||||
Vue.use(actuator, {http: Vue.prototype.$http})
|
||||
|
|
|
|||
20
komga-webui/src/plugins/komga-transientbooks.plugin.ts
Normal file
20
komga-webui/src/plugins/komga-transientbooks.plugin.ts
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
import KomgaTransientBooksService from '@/services/komga-transientbooks.service'
|
||||
import {AxiosInstance} from 'axios'
|
||||
import _Vue from 'vue'
|
||||
|
||||
let service: KomgaTransientBooksService
|
||||
|
||||
export default {
|
||||
install (
|
||||
Vue: typeof _Vue,
|
||||
{ http }: { http: AxiosInstance }) {
|
||||
service = new KomgaTransientBooksService(http)
|
||||
Vue.prototype.$komgaTransientBooks = service
|
||||
},
|
||||
}
|
||||
|
||||
declare module 'vue/types/vue' {
|
||||
interface Vue {
|
||||
$komgaTransientBooks: KomgaTransientBooksService;
|
||||
}
|
||||
}
|
||||
|
|
@ -163,6 +163,12 @@ const router = new Router({
|
|||
name: 'search',
|
||||
component: () => import(/* webpackChunkName: "search" */ './views/Search.vue'),
|
||||
},
|
||||
{
|
||||
path: '/import',
|
||||
name: 'import',
|
||||
beforeEnter: adminGuard,
|
||||
component: () => import(/* webpackChunkName: "book-import" */ './views/BookImport.vue'),
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import {AxiosInstance} from 'axios'
|
||||
import {BookDto, BookMetadataUpdateDto, PageDto, ReadProgressUpdateDto} from '@/types/komga-books'
|
||||
import {BookDto, BookImportBatchDto, BookMetadataUpdateDto, PageDto, ReadProgressUpdateDto} from '@/types/komga-books'
|
||||
|
||||
const qs = require('qs')
|
||||
|
||||
|
|
@ -177,4 +177,16 @@ export default class KomgaBooksService {
|
|||
throw new Error(msg)
|
||||
}
|
||||
}
|
||||
|
||||
async importBooks(batch: BookImportBatchDto) {
|
||||
try {
|
||||
await this.http.post(`${API_BOOKS}/import`, batch)
|
||||
} catch (e) {
|
||||
let msg = `An error occurred while trying to submit book import batch`
|
||||
if (e.response.data.message) {
|
||||
msg += `: ${e.response.data.message}`
|
||||
}
|
||||
throw new Error(msg)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -70,19 +70,48 @@ export default class KomgaReferentialService {
|
|||
}
|
||||
}
|
||||
|
||||
async getTags(libraryId?: string, seriesId?: string, collectionId?: string): Promise<string[]> {
|
||||
async getTags(): Promise<string[]> {
|
||||
try {
|
||||
return (await this.http.get('/api/v1/tags')).data
|
||||
} catch (e) {
|
||||
let msg = 'An error occurred while trying to retrieve tags'
|
||||
if (e.response.data.message) {
|
||||
msg += `: ${e.response.data.message}`
|
||||
}
|
||||
throw new Error(msg)
|
||||
}
|
||||
}
|
||||
|
||||
async getSeriesTags(libraryId?: string, collectionId?: string): Promise<string[]> {
|
||||
try {
|
||||
const params = {} as any
|
||||
if (libraryId) params.library_id = libraryId
|
||||
if (seriesId) params.series_id = seriesId
|
||||
if (collectionId) params.collection_id = collectionId
|
||||
|
||||
return (await this.http.get('/api/v1/tags', {
|
||||
return (await this.http.get('/api/v1/tags/series', {
|
||||
params: params,
|
||||
paramsSerializer: params => qs.stringify(params, {indices: false}),
|
||||
})).data
|
||||
} catch (e) {
|
||||
let msg = 'An error occurred while trying to retrieve tags'
|
||||
let msg = 'An error occurred while trying to retrieve series tags'
|
||||
if (e.response.data.message) {
|
||||
msg += `: ${e.response.data.message}`
|
||||
}
|
||||
throw new Error(msg)
|
||||
}
|
||||
}
|
||||
|
||||
async getBookTags(seriesId?: string): Promise<string[]> {
|
||||
try {
|
||||
const params = {} as any
|
||||
if (seriesId) params.series_id = seriesId
|
||||
|
||||
return (await this.http.get('/api/v1/tags/book', {
|
||||
params: params,
|
||||
paramsSerializer: params => qs.stringify(params, {indices: false}),
|
||||
})).data
|
||||
} catch (e) {
|
||||
let msg = 'An error occurred while trying to retrieve book tags'
|
||||
if (e.response.data.message) {
|
||||
msg += `: ${e.response.data.message}`
|
||||
}
|
||||
|
|
|
|||
36
komga-webui/src/services/komga-transientbooks.service.ts
Normal file
36
komga-webui/src/services/komga-transientbooks.service.ts
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import {AxiosInstance} from 'axios'
|
||||
import {TransientBookDto} from "@/types/komga-transientbooks";
|
||||
|
||||
const API_TRANSIENT_BOOKS = '/api/v1/transient-books'
|
||||
|
||||
export default class KomgaTransientBooksService {
|
||||
private http: AxiosInstance
|
||||
|
||||
constructor(http: AxiosInstance) {
|
||||
this.http = http
|
||||
}
|
||||
|
||||
async scanForTransientBooks(path: string): Promise<TransientBookDto[]> {
|
||||
try {
|
||||
return (await this.http.post(API_TRANSIENT_BOOKS, {
|
||||
path: path,
|
||||
})).data
|
||||
} catch (e) {
|
||||
if (e.response.data.message) throw new Error(e.response.data.message)
|
||||
throw new Error('An error occurred while trying to scan for transient book')
|
||||
}
|
||||
}
|
||||
|
||||
async analyze(id: string): Promise<TransientBookDto> {
|
||||
try {
|
||||
return (await this.http.post(`${API_TRANSIENT_BOOKS}/${id}/analyze`)).data
|
||||
} catch (e) {
|
||||
let msg = `An error occurred while trying to analyze transient book`
|
||||
if (e.response.data.message) {
|
||||
msg += `: ${e.response.data.message}`
|
||||
}
|
||||
throw new Error(msg)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -18,3 +18,9 @@ export enum ReadStatus {
|
|||
IN_PROGRESS = 'IN_PROGRESS',
|
||||
READ = 'READ'
|
||||
}
|
||||
|
||||
export enum CopyMode {
|
||||
MOVE = 'MOVE',
|
||||
COPY = 'COPY',
|
||||
HARDLINK = 'HARDLINK',
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import {Context} from '@/types/context'
|
||||
import {CopyMode} from "@/types/enum-books";
|
||||
|
||||
export interface BookDto {
|
||||
id: string,
|
||||
|
|
@ -103,3 +104,15 @@ export interface BookFormat {
|
|||
type: string,
|
||||
color: string
|
||||
}
|
||||
|
||||
export interface BookImportBatchDto{
|
||||
books: BookImportDto[],
|
||||
copyMode: CopyMode,
|
||||
}
|
||||
|
||||
export interface BookImportDto {
|
||||
sourceFile: string,
|
||||
seriesId: string,
|
||||
upgradeBookId?: string,
|
||||
destinationName?: string,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,5 +31,5 @@ interface ReadListRequestResultBookDto {
|
|||
|
||||
interface ReadListRequestBookDto {
|
||||
series: string,
|
||||
number: number,
|
||||
number: string,
|
||||
}
|
||||
|
|
|
|||
19
komga-webui/src/types/komga-transientbooks.ts
Normal file
19
komga-webui/src/types/komga-transientbooks.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import {PageDto} from "@/types/komga-books";
|
||||
|
||||
export interface ScanRequestDto {
|
||||
path: string,
|
||||
}
|
||||
|
||||
export interface TransientBookDto {
|
||||
id: string,
|
||||
name: string,
|
||||
url: string,
|
||||
fileLastModified: string,
|
||||
sizeBytes: number,
|
||||
size: string,
|
||||
status: string,
|
||||
mediaType: string,
|
||||
pages: PageDto[],
|
||||
files: string[],
|
||||
comment: string,
|
||||
}
|
||||
197
komga-webui/src/views/BookImport.vue
Normal file
197
komga-webui/src/views/BookImport.vue
Normal file
|
|
@ -0,0 +1,197 @@
|
|||
<template>
|
||||
<v-container fluid class="pa-6">
|
||||
<v-alert type="warning" text>{{ $t('book_import.warning_early_feature') }}</v-alert>
|
||||
|
||||
<v-alert type="info" text class="body-2">
|
||||
<div>{{ $t('book_import.info_part1') }}</div>
|
||||
<div class="mt-2">{{ $t('book_import.info_part2') }}</div>
|
||||
</v-alert>
|
||||
|
||||
<v-row align="center">
|
||||
<v-col>
|
||||
<v-text-field
|
||||
v-model="importPath"
|
||||
:label="$t('book_import.field_import_path')"
|
||||
/>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="auto">
|
||||
<file-browser-dialog
|
||||
v-model="modalFileBrowser"
|
||||
:path.sync="importPath"
|
||||
/>
|
||||
<v-btn @click="modalFileBrowser = true">{{ $t('book_import.button_browse') }}</v-btn>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="auto">
|
||||
<v-btn
|
||||
color="primary"
|
||||
:disabled="!importPath"
|
||||
@click="scanBooks"
|
||||
>{{ $t('book_import.button_scan') }}
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
<template v-if="transientBooks.length > 0">
|
||||
<v-divider/>
|
||||
|
||||
<v-simple-table>
|
||||
<thead class="font-weight-medium">
|
||||
<tr>
|
||||
<td>
|
||||
<v-checkbox v-model="globalSelect" :indeterminate="globalSelect === 1"></v-checkbox>
|
||||
</td>
|
||||
<td>{{ $t('book_import.table.file_name') }}</td>
|
||||
<td></td>
|
||||
<td>{{ $t('book_import.table.series') }}</td>
|
||||
<td>{{ $t('book_import.table.number') }}</td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td>{{ $t('book_import.table.destination_name') }}</td>
|
||||
<td>
|
||||
<v-icon>mdi-alert-circle-outline</v-icon>
|
||||
</td>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody
|
||||
v-for="(book, i) in transientBooks"
|
||||
:key="i"
|
||||
>
|
||||
<file-import-row :book="book" :series="selected.includes(i) ? selectedSeries : undefined"
|
||||
:payload.sync="payloads[i]">
|
||||
<v-checkbox v-model="selected" :value="i"/>
|
||||
</file-import-row>
|
||||
</tbody>
|
||||
</v-simple-table>
|
||||
|
||||
<v-row align="center">
|
||||
<v-col cols="3">
|
||||
<v-select v-model="copyMode" :items="copyModes"></v-select>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="auto">
|
||||
<v-btn @click="modalSeriesPicker = true" :disabled="globalSelect === 0">
|
||||
{{ $t('book_import.button_select_series') }}
|
||||
</v-btn>
|
||||
<series-picker-dialog v-model="modalSeriesPicker" :series.sync="selectedSeries"></series-picker-dialog>
|
||||
</v-col>
|
||||
<v-spacer/>
|
||||
|
||||
<v-col cols="auto">
|
||||
<v-btn :color="importFinished ? 'success': 'primary'"
|
||||
:disabled="payloadBatch.books.length === 0"
|
||||
@click="performImport"
|
||||
>
|
||||
<v-icon left v-if="importFinished">mdi-check</v-icon>
|
||||
{{ $t('book_import.button_import') }}
|
||||
</v-btn>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</template>
|
||||
|
||||
<v-snackbar
|
||||
v-model="snackbar"
|
||||
bottom
|
||||
color="error"
|
||||
>
|
||||
{{ snackText }}
|
||||
<v-btn
|
||||
text
|
||||
@click="snackbar = false"
|
||||
>{{ $t('common.close') }}
|
||||
</v-btn>
|
||||
</v-snackbar>
|
||||
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue'
|
||||
import FileBrowserDialog from "@/components/dialogs/FileBrowserDialog.vue";
|
||||
import FileImportRow from "@/components/FileImportRow.vue";
|
||||
import {TransientBookDto} from "@/types/komga-transientbooks";
|
||||
import SeriesPickerDialog from "@/components/dialogs/SeriesPickerDialog.vue";
|
||||
import {SeriesDto} from "@/types/komga-series";
|
||||
import {BookImportBatchDto, BookImportDto} from "@/types/komga-books";
|
||||
import {CopyMode} from "@/types/enum-books";
|
||||
import {convertErrorCodes} from "@/functions/error-codes";
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'BookImport',
|
||||
components: {FileBrowserDialog, FileImportRow, SeriesPickerDialog},
|
||||
data: () => ({
|
||||
modalFileBrowser: false,
|
||||
modalSeriesPicker: false,
|
||||
selected: [] as number[],
|
||||
selectedSeries: undefined as SeriesDto | undefined,
|
||||
payloads: [] as BookImportDto[],
|
||||
importPath: '',
|
||||
transientBooks: [] as TransientBookDto[],
|
||||
copyMode: CopyMode.HARDLINK,
|
||||
importFinished: false,
|
||||
snackbar: false,
|
||||
snackText: '',
|
||||
}),
|
||||
computed: {
|
||||
globalSelect: {
|
||||
get: function (): number {
|
||||
if (this.selected.length === 0) return 0
|
||||
if (this.selected.length === this.transientBooks.length) return 2
|
||||
return 1
|
||||
},
|
||||
set: function (val: boolean): void {
|
||||
if (val) this.selected = this.$_.range(this.transientBooks.length)
|
||||
else this.selected = []
|
||||
},
|
||||
},
|
||||
copyModes(): object[] {
|
||||
return [
|
||||
{text: this.$t('enums.copy_mode.HARDLINK').toString(), value: CopyMode.HARDLINK},
|
||||
{text: this.$t('enums.copy_mode.MOVE').toString(), value: CopyMode.MOVE},
|
||||
]
|
||||
},
|
||||
payloadBatch(): BookImportBatchDto {
|
||||
return {
|
||||
books: this.selected.map(x => this.payloads[x]).filter(Boolean),
|
||||
copyMode: this.copyMode,
|
||||
}
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
selectedSeries(val) {
|
||||
if (val) setTimeout(() => {
|
||||
this.selectedSeries = undefined
|
||||
}, 100)
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async scanBooks() {
|
||||
this.transientBooks = []
|
||||
try {
|
||||
this.transientBooks = await this.$komgaTransientBooks.scanForTransientBooks(this.importPath)
|
||||
} catch (e) {
|
||||
this.showSnack(convertErrorCodes(e.message))
|
||||
}
|
||||
this.selected = this.$_.range(this.transientBooks.length)
|
||||
this.payloads = this.payloads.splice(this.transientBooks.length, this.payloads.length)
|
||||
this.importFinished = false
|
||||
},
|
||||
performImport() {
|
||||
if (!this.importFinished) {
|
||||
this.$komgaBooks.importBooks(this.payloadBatch)
|
||||
this.importFinished = true
|
||||
}
|
||||
},
|
||||
showSnack(message: string) {
|
||||
this.snackText = message
|
||||
this.snackbar = true
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
</style>
|
||||
|
|
@ -20,6 +20,13 @@
|
|||
<v-toolbar-title> {{ bookTitle }}</v-toolbar-title>
|
||||
<v-spacer></v-spacer>
|
||||
|
||||
<v-tooltip bottom v-if="incognito">
|
||||
<template v-slot:activator="{ on }">
|
||||
<v-icon v-on="on">mdi-incognito</v-icon>
|
||||
</template>
|
||||
<span>{{ $t('bookreader.tooltip_incognito') }}</span>
|
||||
</v-tooltip>
|
||||
|
||||
<v-btn
|
||||
icon
|
||||
@click="showHelp = !showHelp">
|
||||
|
|
@ -324,6 +331,7 @@ export default Vue.extend({
|
|||
series: {} as SeriesDto,
|
||||
context: {} as Context,
|
||||
contextName: '',
|
||||
incognito: false,
|
||||
siblingPrevious: {} as BookDto,
|
||||
siblingNext: {} as BookDto,
|
||||
jumpToNextBook: false,
|
||||
|
|
@ -427,7 +435,7 @@ export default Vue.extend({
|
|||
page(val) {
|
||||
this.updateRoute()
|
||||
this.goToPage = val
|
||||
this.markProgress(val)
|
||||
// this.markProgress(val)
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -587,6 +595,9 @@ export default Vue.extend({
|
|||
document.title = `Komga - ${getBookTitleCompact(this.book.metadata.title, this.series.metadata.title)}`
|
||||
}
|
||||
|
||||
// parse query params to get incognito mode
|
||||
this.incognito = !!(this.$route.query.incognito && this.$route.query.incognito.toString().toLowerCase() === 'true');
|
||||
|
||||
const pageDtos = (await this.$komgaBooks.getBookPages(bookId))
|
||||
pageDtos.forEach((p: any) => p['url'] = this.getPageUrl(p))
|
||||
this.pages = pageDtos as PageDtoWithUrl[]
|
||||
|
|
@ -654,7 +665,7 @@ export default Vue.extend({
|
|||
this.$router.push({
|
||||
name: 'read-book',
|
||||
params: {bookId: this.siblingPrevious.id.toString()},
|
||||
query: {context: this.context.origin, contextId: this.context.id},
|
||||
query: {context: this.context.origin, contextId: this.context.id, incognito: this.incognito.toString()},
|
||||
})
|
||||
}
|
||||
},
|
||||
|
|
@ -666,12 +677,13 @@ export default Vue.extend({
|
|||
this.$router.push({
|
||||
name: 'read-book',
|
||||
params: {bookId: this.siblingNext.id.toString()},
|
||||
query: {context: this.context.origin, contextId: this.context.id},
|
||||
query: {context: this.context.origin, contextId: this.context.id, incognito: this.incognito.toString()},
|
||||
})
|
||||
}
|
||||
},
|
||||
goTo(page: number) {
|
||||
this.page = page
|
||||
this.markProgress(page)
|
||||
},
|
||||
goToFirst() {
|
||||
this.goTo(1)
|
||||
|
|
@ -687,6 +699,7 @@ export default Vue.extend({
|
|||
page: this.page.toString(),
|
||||
context: this.context.origin,
|
||||
contextId: this.context.id,
|
||||
incognito: this.incognito.toString(),
|
||||
},
|
||||
} as Location)
|
||||
},
|
||||
|
|
@ -770,7 +783,8 @@ export default Vue.extend({
|
|||
this.notification.enabled = true
|
||||
},
|
||||
async markProgress(page: number) {
|
||||
await this.$komgaBooks.updateReadProgress(this.bookId, {page: page})
|
||||
if (!this.incognito)
|
||||
await this.$komgaBooks.updateReadProgress(this.bookId, {page: page})
|
||||
},
|
||||
downloadCurrentPage() {
|
||||
new jsFileDownloader({
|
||||
|
|
|
|||
|
|
@ -29,8 +29,7 @@
|
|||
:disabled="$_.isEmpty(siblingPrevious)"
|
||||
:to="{ name: 'browse-book', params: { bookId: previousId }, query: { context: context.origin, contextId: context.id} }"
|
||||
>
|
||||
<v-icon v-if="$vuetify.rtl">mdi-chevron-right</v-icon>
|
||||
<v-icon v-else>mdi-chevron-left</v-icon>
|
||||
<rtl-icon icon="mdi-chevron-left" rtl="mdi-chevron-right"/>
|
||||
</v-btn>
|
||||
|
||||
<!-- List of all books in context (series/readlist) for navigation -->
|
||||
|
|
@ -67,8 +66,7 @@
|
|||
:disabled="$_.isEmpty(siblingNext)"
|
||||
:to="{ name: 'browse-book', params: { bookId: nextId }, query: { context: context.origin, contextId: context.id} }"
|
||||
>
|
||||
<v-icon v-if="$vuetify.rtl">mdi-chevron-left</v-icon>
|
||||
<v-icon v-else>mdi-chevron-right</v-icon>
|
||||
<rtl-icon icon="mdi-chevron-right" rtl="mdi-chevron-left"/>
|
||||
</v-btn>
|
||||
</toolbar-sticky>
|
||||
|
||||
|
|
@ -104,7 +102,9 @@
|
|||
{{ book.metadata.number }} · {{ book.media.pagesCount }} {{ $t('common.pages') }}
|
||||
</v-col>
|
||||
<v-col cols="auto" v-if="book.metadata.releaseDate">
|
||||
{{ new Intl.DateTimeFormat($i18n.locale, { dateStyle: 'long' }).format(new Date(book.metadata.releaseDate)) }}
|
||||
{{
|
||||
new Intl.DateTimeFormat($i18n.locale, {dateStyle: 'long'}).format(new Date(book.metadata.releaseDate))
|
||||
}}
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
||||
|
|
@ -122,6 +122,18 @@
|
|||
{{ $t('common.read') }}
|
||||
</v-btn>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="auto">
|
||||
<v-btn small
|
||||
:title="$t('browse_book.read_book')"
|
||||
:to="{name: 'read-book', params: { bookId: bookId}, query: { context: context.origin, contextId: context.id, incognito: true}}"
|
||||
:disabled="book.media.status !== 'READY' || !canReadPages"
|
||||
>
|
||||
<v-icon left small>mdi-incognito</v-icon>
|
||||
{{ $t('common.read') }}
|
||||
</v-btn>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="auto">
|
||||
<v-btn :title="$t('browse_book.download_file')"
|
||||
small
|
||||
|
|
@ -155,6 +167,18 @@
|
|||
{{ $t('common.read') }}
|
||||
</v-btn>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="auto">
|
||||
<v-btn small
|
||||
:title="$t('browse_book.read_book')"
|
||||
:to="{name: 'read-book', params: { bookId: bookId}, query: { context: context.origin, contextId: context.id, incognito: true}}"
|
||||
:disabled="book.media.status !== 'READY' || !canReadPages"
|
||||
>
|
||||
<v-icon left small>mdi-incognito</v-icon>
|
||||
{{ $t('common.read') }}
|
||||
</v-btn>
|
||||
</v-col>
|
||||
|
||||
<v-col cols="auto">
|
||||
<v-btn :title="$t('browse_book.download_file')"
|
||||
small
|
||||
|
|
@ -295,10 +319,11 @@ import ReadMore from "@/components/ReadMore.vue";
|
|||
import VueHorizontal from "vue-horizontal";
|
||||
import {authorRoles} from "@/types/author-roles";
|
||||
import {convertErrorCodes} from "@/functions/error-codes";
|
||||
import RtlIcon from "@/components/RtlIcon.vue";
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'BrowseBook',
|
||||
components: {ReadMore, ToolbarSticky, ItemCard, BookActionsMenu, ReadListsExpansionPanels, VueHorizontal},
|
||||
components: {ReadMore, ToolbarSticky, ItemCard, BookActionsMenu, ReadListsExpansionPanels, VueHorizontal, RtlIcon},
|
||||
data: () => {
|
||||
return {
|
||||
authorRoles,
|
||||
|
|
|
|||
|
|
@ -121,7 +121,7 @@ import FilterPanels from '@/components/FilterPanels.vue'
|
|||
import FilterList from '@/components/FilterList.vue'
|
||||
import {Location} from 'vue-router'
|
||||
import EmptyState from '@/components/EmptyState.vue'
|
||||
import {parseQueryFilter} from '@/functions/query-params'
|
||||
import {parseQueryParam} from '@/functions/query-params'
|
||||
import {SeriesDto} from "@/types/komga-series";
|
||||
import {authorRoles} from "@/types/author-roles";
|
||||
import {groupAuthorsByRole} from "@/functions/authors";
|
||||
|
|
@ -258,7 +258,7 @@ export default Vue.extend({
|
|||
value: x.id,
|
||||
})))
|
||||
this.$set(this.filterOptions, 'genre', toNameValue(await this.$komgaReferential.getGenres(undefined, collectionId)))
|
||||
this.$set(this.filterOptions, 'tag', toNameValue(await this.$komgaReferential.getTags(undefined, undefined, collectionId)))
|
||||
this.$set(this.filterOptions, 'tag', toNameValue(await this.$komgaReferential.getSeriesTags(undefined, collectionId)))
|
||||
this.$set(this.filterOptions, 'publisher', toNameValue(await this.$komgaReferential.getPublishers(undefined, collectionId)))
|
||||
this.$set(this.filterOptions, 'language', (await this.$komgaReferential.getLanguages(undefined, collectionId)))
|
||||
this.$set(this.filterOptions, 'ageRating', toNameValue(await this.$komgaReferential.getAgeRatings(undefined, collectionId)))
|
||||
|
|
@ -268,24 +268,44 @@ export default Vue.extend({
|
|||
this.$set(this.filterOptions, role, role in grouped ? toNameValue(grouped[role]) : [])
|
||||
})
|
||||
|
||||
// filter query params with available filter values
|
||||
// get filter from query params or local storage and validate with available filter values
|
||||
let activeFilters: any
|
||||
if (route.query.status || route.query.readStatus || route.query.genre || route.query.tag || route.query.language || route.query.ageRating || route.query.library || route.query.publisher || authorRoles.some(role => role in route.query)) {
|
||||
this.$set(this.filters, 'status', parseQueryFilter(route.query.status, Object.keys(SeriesStatus)))
|
||||
this.$set(this.filters, 'readStatus', parseQueryFilter(route.query.readStatus, Object.keys(ReadStatus)))
|
||||
this.$set(this.filters, 'library', parseQueryFilter(route.query.library, this.filterOptions.library.map(x => x.value)))
|
||||
this.$set(this.filters, 'genre', parseQueryFilter(route.query.genre, this.filterOptions.genre.map(x => x.value)))
|
||||
this.$set(this.filters, 'tag', parseQueryFilter(route.query.tag, this.filterOptions.tag.map(x => x.value)))
|
||||
this.$set(this.filters, 'publisher', parseQueryFilter(route.query.publisher, this.filterOptions.publisher.map(x => x.value)))
|
||||
this.$set(this.filters, 'language', parseQueryFilter(route.query.language, this.filterOptions.language.map(x => x.value)))
|
||||
this.$set(this.filters, 'ageRating', parseQueryFilter(route.query.ageRating, this.filterOptions.ageRating.map(x => x.value)))
|
||||
this.$set(this.filters, 'releaseDate', parseQueryFilter(route.query.releaseDate, this.filterOptions.releaseDate.map(x => x.value)))
|
||||
activeFilters = {
|
||||
status: parseQueryParam(route.query.status),
|
||||
readStatus: parseQueryParam(route.query.readStatus),
|
||||
library: parseQueryParam(route.query.library),
|
||||
genre: parseQueryParam(route.query.genre),
|
||||
tag: parseQueryParam(route.query.tag),
|
||||
publisher: parseQueryParam(route.query.publisher),
|
||||
language: parseQueryParam(route.query.language),
|
||||
ageRating: parseQueryParam(route.query.ageRating),
|
||||
releaseDate: parseQueryParam(route.query.releaseDate),
|
||||
}
|
||||
authorRoles.forEach((role: string) => {
|
||||
//@ts-ignore
|
||||
this.$set(this.filters, role, parseQueryFilter(route.query[role], this.filterOptions[role].map((x: NameValue) => x.value)))
|
||||
activeFilters[role] = parseQueryParam(route.query[role])
|
||||
})
|
||||
} else {
|
||||
this.filters = this.$store.getters.getCollectionFilter(route.params.collectionId) || {} as FiltersActive
|
||||
activeFilters = this.$store.getters.getCollectionFilter(route.params.collectionId) || {} as FiltersActive
|
||||
}
|
||||
this.filters = this.validateFilters(activeFilters)
|
||||
},
|
||||
validateFilters(filters: FiltersActive): FiltersActive {
|
||||
const validFilter = {
|
||||
status: filters.status?.filter(x => Object.keys(SeriesStatus).includes(x)) || [],
|
||||
readStatus: filters.readStatus?.filter(x => Object.keys(ReadStatus).includes(x)) || [],
|
||||
library: filters.library?.filter(x => this.filterOptions.library.map(n => n.value).includes(x)) || [],
|
||||
genre: filters.genre?.filter(x => this.filterOptions.genre.map(n => n.value).includes(x)) || [],
|
||||
tag: filters.tag?.filter(x => this.filterOptions.tag.map(n => n.value).includes(x)) || [],
|
||||
publisher: filters.publisher?.filter(x => this.filterOptions.publisher.map(n => n.value).includes(x)) || [],
|
||||
language: filters.language?.filter(x => this.filterOptions.language.map(n => n.value).includes(x)) || [],
|
||||
ageRating: filters.ageRating?.filter(x => this.filterOptions.ageRating.map(n => n.value).includes(x)) || [],
|
||||
releaseDate: filters.releaseDate?.filter(x => this.filterOptions.releaseDate.map(n => n.value).includes(x)) || [],
|
||||
} as any
|
||||
authorRoles.forEach((role: string) => {
|
||||
validFilter[role] = filters[role]?.filter(x => ((this.filterOptions as any)[role] as NameValue[]).map(n => n.value).includes(x)) || []
|
||||
})
|
||||
return validFilter
|
||||
},
|
||||
setWatches() {
|
||||
this.filterUnwatch = this.$watch('filters', (val) => {
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@ import ItemBrowser from '@/components/ItemBrowser.vue'
|
|||
import LibraryNavigation from '@/components/LibraryNavigation.vue'
|
||||
import LibraryActionsMenu from '@/components/menus/LibraryActionsMenu.vue'
|
||||
import PageSizeSelect from '@/components/PageSizeSelect.vue'
|
||||
import {parseQueryFilter, parseQuerySort} from '@/functions/query-params'
|
||||
import {parseQueryParam, parseQuerySort} from '@/functions/query-params'
|
||||
import {ReadStatus} from '@/types/enum-books'
|
||||
import {SeriesStatus, SeriesStatusKeyValue} from '@/types/enum-series'
|
||||
import {LIBRARY_CHANGED, LIBRARY_DELETED, SERIES_CHANGED} from '@/types/events'
|
||||
|
|
@ -288,7 +288,7 @@ export default Vue.extend({
|
|||
|
||||
// load dynamic filters
|
||||
this.$set(this.filterOptions, 'genre', toNameValue(await this.$komgaReferential.getGenres(requestLibraryId)))
|
||||
this.$set(this.filterOptions, 'tag', toNameValue(await this.$komgaReferential.getTags(requestLibraryId)))
|
||||
this.$set(this.filterOptions, 'tag', toNameValue(await this.$komgaReferential.getSeriesTags(requestLibraryId)))
|
||||
this.$set(this.filterOptions, 'publisher', toNameValue(await this.$komgaReferential.getPublishers(requestLibraryId)))
|
||||
this.$set(this.filterOptions, 'language', (await this.$komgaReferential.getLanguages(requestLibraryId)))
|
||||
this.$set(this.filterOptions, 'ageRating', toNameValue(await this.$komgaReferential.getAgeRatings(requestLibraryId)))
|
||||
|
|
@ -298,23 +298,42 @@ export default Vue.extend({
|
|||
this.$set(this.filterOptions, role, role in grouped ? toNameValue(grouped[role]) : [])
|
||||
})
|
||||
|
||||
// filter query params with available filter values
|
||||
// get filter from query params or local storage and validate with available filter values
|
||||
let activeFilters: any
|
||||
if (route.query.status || route.query.readStatus || route.query.genre || route.query.tag || route.query.language || route.query.ageRating || route.query.publisher || authorRoles.some(role => role in route.query)) {
|
||||
this.$set(this.filters, 'status', parseQueryFilter(route.query.status, Object.keys(SeriesStatus)))
|
||||
this.$set(this.filters, 'readStatus', parseQueryFilter(route.query.readStatus, Object.keys(ReadStatus)))
|
||||
this.$set(this.filters, 'genre', parseQueryFilter(route.query.genre, this.filterOptions.genre.map(x => x.value)))
|
||||
this.$set(this.filters, 'tag', parseQueryFilter(route.query.tag, this.filterOptions.tag.map(x => x.value)))
|
||||
this.$set(this.filters, 'publisher', parseQueryFilter(route.query.publisher, this.filterOptions.publisher.map(x => x.value)))
|
||||
this.$set(this.filters, 'language', parseQueryFilter(route.query.language, this.filterOptions.language.map(x => x.value)))
|
||||
this.$set(this.filters, 'ageRating', parseQueryFilter(route.query.ageRating, this.filterOptions.ageRating.map(x => x.value)))
|
||||
this.$set(this.filters, 'releaseDate', parseQueryFilter(route.query.releaseDate, this.filterOptions.releaseDate.map(x => x.value)))
|
||||
activeFilters = {
|
||||
status: parseQueryParam(route.query.status),
|
||||
readStatus: parseQueryParam(route.query.readStatus),
|
||||
genre: parseQueryParam(route.query.genre),
|
||||
tag: parseQueryParam(route.query.tag),
|
||||
publisher: parseQueryParam(route.query.publisher),
|
||||
language: parseQueryParam(route.query.language),
|
||||
ageRating: parseQueryParam(route.query.ageRating),
|
||||
releaseDate: parseQueryParam(route.query.releaseDate),
|
||||
}
|
||||
authorRoles.forEach((role: string) => {
|
||||
//@ts-ignore
|
||||
this.$set(this.filters, role, parseQueryFilter(route.query[role], this.filterOptions[role].map((x: NameValue) => x.value)))
|
||||
activeFilters[role] = parseQueryParam(route.query[role])
|
||||
})
|
||||
} else {
|
||||
this.filters = this.$store.getters.getLibraryFilter(route.params.libraryId) || {} as FiltersActive
|
||||
activeFilters = this.$store.getters.getLibraryFilter(route.params.libraryId) || {} as FiltersActive
|
||||
}
|
||||
this.filters = this.validateFilters(activeFilters)
|
||||
},
|
||||
validateFilters(filters: FiltersActive): FiltersActive {
|
||||
const validFilter = {
|
||||
status: filters.status?.filter(x => Object.keys(SeriesStatus).includes(x)) || [],
|
||||
readStatus: filters.readStatus?.filter(x => Object.keys(ReadStatus).includes(x)) || [],
|
||||
genre: filters.genre?.filter(x => this.filterOptions.genre.map(n => n.value).includes(x)) || [],
|
||||
tag: filters.tag?.filter(x => this.filterOptions.tag.map(n => n.value).includes(x)) || [],
|
||||
publisher: filters.publisher?.filter(x => this.filterOptions.publisher.map(n => n.value).includes(x)) || [],
|
||||
language: filters.language?.filter(x => this.filterOptions.language.map(n => n.value).includes(x)) || [],
|
||||
ageRating: filters.ageRating?.filter(x => this.filterOptions.ageRating.map(n => n.value).includes(x)) || [],
|
||||
releaseDate: filters.releaseDate?.filter(x => this.filterOptions.releaseDate.map(n => n.value).includes(x)) || [],
|
||||
} as any
|
||||
authorRoles.forEach((role: string) => {
|
||||
validFilter[role] = filters[role]?.filter(x => ((this.filterOptions as any)[role] as NameValue[]).map(n => n.value).includes(x)) || []
|
||||
})
|
||||
return validFilter
|
||||
},
|
||||
libraryDeleted(event: EventLibraryDeleted) {
|
||||
if (event.id === this.libraryId) {
|
||||
|
|
|
|||
|
|
@ -6,8 +6,7 @@
|
|||
:title="$t('common.go_to_library')"
|
||||
:to="{name:'browse-libraries', params: {libraryId: series.libraryId }}"
|
||||
>
|
||||
<v-icon v-if="$vuetify.rtl">mdi-arrow-right</v-icon>
|
||||
<v-icon v-else>mdi-arrow-left</v-icon>
|
||||
<rtl-icon icon="mdi-arrow-left" rtl="mdi-arrow-right"/>
|
||||
</v-btn>
|
||||
|
||||
<series-actions-menu v-if="series"
|
||||
|
|
@ -92,7 +91,7 @@
|
|||
<v-tooltip right>
|
||||
<template v-slot:activator="{ on }">
|
||||
<span v-on="on">{{
|
||||
new Intl.DateTimeFormat($i18n.locale, {dateStyle: 'long'}).format(new Date(series.booksMetadata.releaseDate))
|
||||
new Intl.DateTimeFormat($i18n.locale, {year: 'numeric'}).format(new Date(series.booksMetadata.releaseDate))
|
||||
}}</span>
|
||||
</template>
|
||||
{{ $t('browse_series.earliest_year_from_release_dates') }}
|
||||
|
|
@ -371,7 +370,7 @@ import ItemBrowser from '@/components/ItemBrowser.vue'
|
|||
import ItemCard from '@/components/ItemCard.vue'
|
||||
import SeriesActionsMenu from '@/components/menus/SeriesActionsMenu.vue'
|
||||
import PageSizeSelect from '@/components/PageSizeSelect.vue'
|
||||
import {parseQueryFilter, parseQuerySort} from '@/functions/query-params'
|
||||
import {parseQueryParamAndFilter, parseQuerySort} from '@/functions/query-params'
|
||||
import {seriesFileUrl, seriesThumbnailUrl} from '@/functions/urls'
|
||||
import {ReadStatus} from '@/types/enum-books'
|
||||
import {BOOK_CHANGED, LIBRARY_DELETED, READLIST_CHANGED, SERIES_CHANGED} from '@/types/events'
|
||||
|
|
@ -389,6 +388,7 @@ import {groupAuthorsByRole} from "@/functions/authors";
|
|||
import ReadMore from "@/components/ReadMore.vue";
|
||||
import {authorRoles, authorRolesSeries} from "@/types/author-roles";
|
||||
import VueHorizontal from "vue-horizontal";
|
||||
import RtlIcon from "@/components/RtlIcon.vue";
|
||||
|
||||
const tags = require('language-tags')
|
||||
|
||||
|
|
@ -409,6 +409,7 @@ export default Vue.extend({
|
|||
SortList,
|
||||
ReadMore,
|
||||
VueHorizontal,
|
||||
RtlIcon,
|
||||
},
|
||||
data: function () {
|
||||
return {
|
||||
|
|
@ -566,18 +567,18 @@ export default Vue.extend({
|
|||
this.sortActive = this.parseQuerySortOrDefault(route.query.sort)
|
||||
|
||||
// load dynamic filters
|
||||
this.$set(this.filterOptions, 'tag', toNameValue(await this.$komgaReferential.getTags(undefined, seriesId)))
|
||||
this.$set(this.filterOptions, 'tag', toNameValue(await this.$komgaReferential.getBookTags(seriesId)))
|
||||
const grouped = groupAuthorsByRole(await this.$komgaReferential.getAuthors(undefined, undefined, undefined, seriesId))
|
||||
authorRoles.forEach((role: string) => {
|
||||
this.$set(this.filterOptions, role, role in grouped ? toNameValue(grouped[role]) : [])
|
||||
})
|
||||
|
||||
// filter query params with available filter values
|
||||
this.$set(this.filters, 'readStatus', parseQueryFilter(this.$route.query.readStatus, Object.keys(ReadStatus)))
|
||||
this.$set(this.filters, 'tag', parseQueryFilter(this.$route.query.tag, this.filterOptions.tag.map(x => x.value)))
|
||||
this.$set(this.filters, 'readStatus', parseQueryParamAndFilter(this.$route.query.readStatus, Object.keys(ReadStatus)))
|
||||
this.$set(this.filters, 'tag', parseQueryParamAndFilter(this.$route.query.tag, this.filterOptions.tag.map(x => x.value)))
|
||||
authorRoles.forEach((role: string) => {
|
||||
//@ts-ignore
|
||||
this.$set(this.filters, role, parseQueryFilter(route.query[role], this.filterOptions[role].map((x: NameValue) => x.value)))
|
||||
this.$set(this.filters, role, parseQueryParamAndFilter(route.query[role], this.filterOptions[role].map((x: NameValue) => x.value)))
|
||||
})
|
||||
},
|
||||
setWatches() {
|
||||
|
|
|
|||
|
|
@ -69,6 +69,15 @@
|
|||
</v-list-item-action>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item :to="{name: 'import'}" v-if="isAdmin">
|
||||
<v-list-item-icon>
|
||||
<v-icon>mdi-import</v-icon>
|
||||
</v-list-item-icon>
|
||||
<v-list-item-content>
|
||||
<v-list-item-title>{{ $t('book_import.title') }}</v-list-item-title>
|
||||
</v-list-item-content>
|
||||
</v-list-item>
|
||||
|
||||
<v-list-item :to="{name: 'settings'}" v-if="isAdmin">
|
||||
<v-list-item-action>
|
||||
<v-icon>mdi-cog</v-icon>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
module.exports = {
|
||||
env: {
|
||||
jest: true
|
||||
}
|
||||
jest: true,
|
||||
},
|
||||
}
|
||||
|
|
|
|||
81
komga-webui/tests/unit/functions/book-spreads.spec.ts
Normal file
81
komga-webui/tests/unit/functions/book-spreads.spec.ts
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
import {buildSpreads} from "@/functions/book-spreads";
|
||||
import {PagedReaderLayout} from "@/types/enum-reader";
|
||||
import {PageDtoWithUrl} from "@/types/komga-books";
|
||||
|
||||
describe("Single Page", () => {
|
||||
const pageLayout = PagedReaderLayout.SINGLE_PAGE
|
||||
|
||||
test("given no pages then it should return no spreads", () => {
|
||||
const pages = [] as PageDtoWithUrl[]
|
||||
|
||||
const spreads = buildSpreads(pages, pageLayout)
|
||||
|
||||
expect(spreads.length).toEqual(0)
|
||||
})
|
||||
|
||||
test("given single page then it should return single spread with single page", () => {
|
||||
const pages = [
|
||||
{
|
||||
number: 1,
|
||||
} as PageDtoWithUrl,
|
||||
] as PageDtoWithUrl[]
|
||||
|
||||
const spreads = buildSpreads(pages, pageLayout)
|
||||
|
||||
expect(spreads.length).toEqual(1)
|
||||
expect(spreads[0].length).toEqual(1)
|
||||
expect(spreads[0][0].number).toEqual(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe("Double Pages", () => {
|
||||
const pageLayout = PagedReaderLayout.DOUBLE_PAGES
|
||||
|
||||
test("given no pages then it should return no spreads", () => {
|
||||
const pages = [] as PageDtoWithUrl[]
|
||||
|
||||
const spreads = buildSpreads(pages, pageLayout)
|
||||
|
||||
expect(spreads.length).toEqual(0)
|
||||
})
|
||||
|
||||
test("given single page then it should return single spread with single page", () => {
|
||||
const pages = [
|
||||
{
|
||||
number: 1,
|
||||
} as PageDtoWithUrl,
|
||||
] as PageDtoWithUrl[]
|
||||
|
||||
const spreads = buildSpreads(pages, pageLayout)
|
||||
|
||||
expect(spreads.length).toEqual(1)
|
||||
expect(spreads[0].length).toEqual(1)
|
||||
expect(spreads[0][0].number).toEqual(1)
|
||||
})
|
||||
})
|
||||
|
||||
describe("Double Pages No Cover", () => {
|
||||
const pageLayout = PagedReaderLayout.DOUBLE_NO_COVER
|
||||
|
||||
test("given no pages then it should return no spreads", () => {
|
||||
const pages = [] as PageDtoWithUrl[]
|
||||
|
||||
const spreads = buildSpreads(pages, pageLayout)
|
||||
|
||||
expect(spreads.length).toEqual(0)
|
||||
})
|
||||
|
||||
test("given single page then it should return single spread with single page", () => {
|
||||
const pages = [
|
||||
{
|
||||
number: 1,
|
||||
} as PageDtoWithUrl,
|
||||
] as PageDtoWithUrl[]
|
||||
|
||||
const spreads = buildSpreads(pages, pageLayout)
|
||||
|
||||
expect(spreads.length).toEqual(1)
|
||||
expect(spreads[0].length).toEqual(1)
|
||||
expect(spreads[0][0].number).toEqual(1)
|
||||
})
|
||||
})
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
import com.github.benmanes.gradle.versions.updates.DependencyUpdatesTask
|
||||
import org.apache.tools.ant.taskdefs.condition.Os
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
|
||||
|
|
@ -68,9 +67,9 @@ dependencies {
|
|||
implementation("org.jsoup:jsoup:1.13.1")
|
||||
|
||||
implementation("net.coobird:thumbnailator:0.4.13")
|
||||
runtimeOnly("com.twelvemonkeys.imageio:imageio-jpeg:3.6.4")
|
||||
runtimeOnly("com.twelvemonkeys.imageio:imageio-tiff:3.6.4")
|
||||
runtimeOnly(files("$projectDir/libs/webp-imageio-decoder-plugin-0.2.jar"))
|
||||
runtimeOnly("com.twelvemonkeys.imageio:imageio-jpeg:3.7.0")
|
||||
runtimeOnly("com.twelvemonkeys.imageio:imageio-tiff:3.7.0")
|
||||
runtimeOnly("com.twelvemonkeys.imageio:imageio-webp:3.7.0")
|
||||
implementation("com.github.gotson:webp-imageio:0.2.0")
|
||||
// support for jpeg2000
|
||||
runtimeOnly("com.github.jai-imageio:jai-imageio-jpeg2000:1.4.0")
|
||||
|
|
@ -83,6 +82,8 @@ dependencies {
|
|||
|
||||
implementation("com.github.f4b6a3:tsid-creator:3.0.1")
|
||||
|
||||
implementation("com.github.ben-manes.caffeine:caffeine:2.9.0")
|
||||
|
||||
// While waiting for https://github.com/xerial/sqlite-jdbc/pull/491 and https://github.com/xerial/sqlite-jdbc/pull/494
|
||||
// runtimeOnly("org.xerial:sqlite-jdbc:3.32.3.2")
|
||||
// jooqGenerator("org.xerial:sqlite-jdbc:3.32.3.2")
|
||||
|
|
@ -107,7 +108,11 @@ tasks {
|
|||
withType<KotlinCompile> {
|
||||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
freeCompilerArgs = listOf("-Xjsr305=strict", "-Xopt-in=kotlin.time.ExperimentalTime")
|
||||
freeCompilerArgs = listOf(
|
||||
"-Xjsr305=strict",
|
||||
"-Xopt-in=kotlin.time.ExperimentalTime",
|
||||
"-Xopt-in=kotlin.io.path.ExperimentalPathApi"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -251,20 +256,6 @@ openApi {
|
|||
forkProperties.set("-Dspring.profiles.active=claim")
|
||||
}
|
||||
|
||||
fun isNonStable(version: String): Boolean {
|
||||
val stableKeyword = listOf("RELEASE", "FINAL", "GA").any { version.toUpperCase().contains(it) }
|
||||
val regex = "^[0-9,.v-]+(-r)?$".toRegex()
|
||||
val isStable = stableKeyword || regex.matches(version)
|
||||
return isStable.not()
|
||||
}
|
||||
tasks.named("dependencyUpdates", DependencyUpdatesTask::class.java).configure {
|
||||
// disallow release candidates as upgradable versions from stable versions
|
||||
rejectVersionIf {
|
||||
isNonStable(candidate.version) && !isNonStable(currentVersion)
|
||||
}
|
||||
gradleReleaseChannel = "current"
|
||||
}
|
||||
|
||||
configure<org.jlleitschuh.gradle.ktlint.KtlintExtension> {
|
||||
version.set("0.40.0")
|
||||
filter {
|
||||
|
|
|
|||
|
|
@ -303,6 +303,65 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"TransientBookDto": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"comment",
|
||||
"fileLastModified",
|
||||
"files",
|
||||
"id",
|
||||
"mediaType",
|
||||
"name",
|
||||
"pages",
|
||||
"size",
|
||||
"sizeBytes",
|
||||
"status",
|
||||
"url"
|
||||
],
|
||||
"properties": {
|
||||
"pages": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/PageDto"
|
||||
}
|
||||
},
|
||||
"size": {
|
||||
"type": "string"
|
||||
},
|
||||
"fileLastModified": {
|
||||
"format": "date-time",
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"files": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"mediaType": {
|
||||
"type": "string"
|
||||
},
|
||||
"comment": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"url": {
|
||||
"type": "string"
|
||||
},
|
||||
"sizeBytes": {
|
||||
"format": "int64",
|
||||
"type": "integer"
|
||||
},
|
||||
"status": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"Pageable": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
|
@ -728,8 +787,7 @@
|
|||
],
|
||||
"properties": {
|
||||
"number": {
|
||||
"format": "int32",
|
||||
"type": "integer"
|
||||
"type": "string"
|
||||
},
|
||||
"series": {
|
||||
"type": "string"
|
||||
|
|
@ -797,6 +855,29 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"BookImportBatchDto": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"books",
|
||||
"copyMode"
|
||||
],
|
||||
"properties": {
|
||||
"books": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/BookImportDto"
|
||||
}
|
||||
},
|
||||
"copyMode": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"MOVE",
|
||||
"COPY",
|
||||
"HARDLINK"
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"OpdsEntry": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
|
|
@ -1055,6 +1136,27 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"BookImportDto": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"seriesId",
|
||||
"sourceFile"
|
||||
],
|
||||
"properties": {
|
||||
"destinationName": {
|
||||
"type": "string"
|
||||
},
|
||||
"upgradeBookId": {
|
||||
"type": "string"
|
||||
},
|
||||
"sourceFile": {
|
||||
"type": "string"
|
||||
},
|
||||
"seriesId": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"OpdsFeed": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
|
|
@ -1536,6 +1638,17 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"ScanRequestDto": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"path"
|
||||
],
|
||||
"properties": {
|
||||
"path": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"BookMetadataAggregationDto": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
|
|
@ -1863,6 +1976,59 @@
|
|||
]
|
||||
}
|
||||
},
|
||||
"/api/v1/transient-books/{id}/pages/{pageNumber}": {
|
||||
"get": {
|
||||
"operationId": "getSourcePage",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"*/*": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"format": "byte",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"content": {
|
||||
"*/*": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ValidationErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"in": "path",
|
||||
"name": "id",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"schema": {
|
||||
"format": "int32",
|
||||
"type": "integer"
|
||||
},
|
||||
"in": "path",
|
||||
"name": "pageNumber",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"transient-books-controller"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v1/books/latest": {
|
||||
"get": {
|
||||
"description": "Return newly added or updated books.",
|
||||
|
|
@ -2356,6 +2522,50 @@
|
|||
]
|
||||
}
|
||||
},
|
||||
"/api/v1/tags/book": {
|
||||
"get": {
|
||||
"operationId": "getBookTags",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"uniqueItems": true,
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"content": {
|
||||
"*/*": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ValidationErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"in": "query",
|
||||
"name": "series_id",
|
||||
"required": false
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"referential-controller"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/opds/v1.2/books/latest": {
|
||||
"get": {
|
||||
"operationId": "getLatestBooks",
|
||||
|
|
@ -2477,7 +2687,7 @@
|
|||
},
|
||||
"/api/v1/books/{bookId}/analyze": {
|
||||
"post": {
|
||||
"operationId": "analyze_2",
|
||||
"operationId": "analyze_3",
|
||||
"responses": {
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
|
|
@ -2508,6 +2718,39 @@
|
|||
]
|
||||
}
|
||||
},
|
||||
"/api/v1/books/import": {
|
||||
"post": {
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/BookImportBatchDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"operationId": "importBooks",
|
||||
"responses": {
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"content": {
|
||||
"*/*": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ValidationErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"202": {
|
||||
"description": "Accepted"
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"book-controller"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v1/books/{bookId}/file/*": {
|
||||
"get": {
|
||||
"description": "Download the book file.",
|
||||
|
|
@ -2740,7 +2983,7 @@
|
|||
},
|
||||
"/api/v1/series/{seriesId}/analyze": {
|
||||
"post": {
|
||||
"operationId": "analyze",
|
||||
"operationId": "analyze_1",
|
||||
"responses": {
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
|
|
@ -4014,6 +4257,58 @@
|
|||
]
|
||||
}
|
||||
},
|
||||
"/api/v1/tags/series": {
|
||||
"get": {
|
||||
"operationId": "getSeriesTags",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"uniqueItems": true,
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"content": {
|
||||
"*/*": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ValidationErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"in": "query",
|
||||
"name": "library_id",
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"in": "query",
|
||||
"name": "collection_id",
|
||||
"required": false
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"referential-controller"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v1/age-ratings": {
|
||||
"get": {
|
||||
"operationId": "getAgeRatings",
|
||||
|
|
@ -4959,7 +5254,7 @@
|
|||
},
|
||||
"/api/v1/libraries/{libraryId}/analyze": {
|
||||
"post": {
|
||||
"operationId": "analyze_1",
|
||||
"operationId": "analyze_2",
|
||||
"responses": {
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
|
|
@ -5081,6 +5376,46 @@
|
|||
]
|
||||
}
|
||||
},
|
||||
"/api/v1/transient-books/{id}/analyze": {
|
||||
"post": {
|
||||
"operationId": "analyze",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/TransientBookDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"content": {
|
||||
"*/*": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ValidationErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"in": "path",
|
||||
"name": "id",
|
||||
"required": true
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
"transient-books-controller"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/opds/v1.2/catalog": {
|
||||
"get": {
|
||||
"operationId": "getCatalog",
|
||||
|
|
@ -5570,6 +5905,49 @@
|
|||
]
|
||||
}
|
||||
},
|
||||
"/api/v1/transient-books": {
|
||||
"post": {
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ScanRequestDto"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"operationId": "scanForTransientBooks",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/TransientBookDto"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"400": {
|
||||
"description": "Bad Request",
|
||||
"content": {
|
||||
"*/*": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ValidationErrorResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"transient-books-controller"
|
||||
]
|
||||
}
|
||||
},
|
||||
"/api/v1/books/{bookId}/pages": {
|
||||
"get": {
|
||||
"operationId": "getBookPages",
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -1,6 +1,7 @@
|
|||
package org.gotson.komga.application.tasks
|
||||
|
||||
import org.gotson.komga.domain.model.BookMetadataPatchCapability
|
||||
import org.gotson.komga.domain.model.CopyMode
|
||||
import java.io.Serializable
|
||||
|
||||
sealed class Task : Serializable {
|
||||
|
|
@ -29,4 +30,8 @@ sealed class Task : Serializable {
|
|||
data class AggregateSeriesMetadata(val seriesId: String) : Task() {
|
||||
override fun uniqueId() = "AGGREGATE_SERIES_METADATA_$seriesId"
|
||||
}
|
||||
|
||||
data class ImportBook(val sourceFile: String, val seriesId: String, val copyMode: CopyMode, val destinationName: String?, val upgradeBookId: String?) : Task() {
|
||||
override fun uniqueId(): String = "IMPORT_BOOK_${seriesId}_$sourceFile"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import mu.KotlinLogging
|
|||
import org.gotson.komga.domain.persistence.BookRepository
|
||||
import org.gotson.komga.domain.persistence.LibraryRepository
|
||||
import org.gotson.komga.domain.persistence.SeriesRepository
|
||||
import org.gotson.komga.domain.service.BookImporter
|
||||
import org.gotson.komga.domain.service.BookLifecycle
|
||||
import org.gotson.komga.domain.service.LibraryContentLifecycle
|
||||
import org.gotson.komga.domain.service.MetadataLifecycle
|
||||
|
|
@ -11,6 +12,7 @@ import org.gotson.komga.infrastructure.jms.QUEUE_TASKS
|
|||
import org.gotson.komga.infrastructure.jms.QUEUE_TASKS_SELECTOR
|
||||
import org.springframework.jms.annotation.JmsListener
|
||||
import org.springframework.stereotype.Service
|
||||
import java.nio.file.Paths
|
||||
import kotlin.time.measureTime
|
||||
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
|
@ -23,7 +25,8 @@ class TaskHandler(
|
|||
private val seriesRepository: SeriesRepository,
|
||||
private val libraryContentLifecycle: LibraryContentLifecycle,
|
||||
private val bookLifecycle: BookLifecycle,
|
||||
private val metadataLifecycle: MetadataLifecycle
|
||||
private val metadataLifecycle: MetadataLifecycle,
|
||||
private val bookImporter: BookImporter,
|
||||
) {
|
||||
|
||||
@JmsListener(destination = QUEUE_TASKS, selector = QUEUE_TASKS_SELECTOR)
|
||||
|
|
@ -68,6 +71,11 @@ class TaskHandler(
|
|||
seriesRepository.findByIdOrNull(task.seriesId)?.let {
|
||||
metadataLifecycle.aggregateMetadata(it)
|
||||
} ?: logger.warn { "Cannot execute task $task: Series does not exist" }
|
||||
|
||||
is Task.ImportBook ->
|
||||
seriesRepository.findByIdOrNull(task.seriesId)?.let { series ->
|
||||
bookImporter.importBook(Paths.get(task.sourceFile), series, task.copyMode, task.destinationName, task.upgradeBookId)
|
||||
} ?: logger.warn { "Cannot execute task $task: Series does not exist" }
|
||||
}
|
||||
}.also {
|
||||
logger.info { "Task $task executed in $it" }
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import mu.KotlinLogging
|
|||
import org.gotson.komga.domain.model.Book
|
||||
import org.gotson.komga.domain.model.BookMetadataPatchCapability
|
||||
import org.gotson.komga.domain.model.BookSearch
|
||||
import org.gotson.komga.domain.model.CopyMode
|
||||
import org.gotson.komga.domain.model.Library
|
||||
import org.gotson.komga.domain.model.Media
|
||||
import org.gotson.komga.domain.persistence.BookRepository
|
||||
|
|
@ -77,6 +78,10 @@ class TaskReceiver(
|
|||
submitTask(Task.AggregateSeriesMetadata(seriesId))
|
||||
}
|
||||
|
||||
fun importBook(sourceFile: String, seriesId: String, copyMode: CopyMode, destinationName: String?, upgradeBookId: String?) {
|
||||
submitTask(Task.ImportBook(sourceFile, seriesId, copyMode, destinationName, upgradeBookId))
|
||||
}
|
||||
|
||||
private fun submitTask(task: Task) {
|
||||
logger.info { "Sending task: $task" }
|
||||
jmsTemplate.convertAndSend(QUEUE_TASKS, task) {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
package org.gotson.komga.domain.model
|
||||
|
||||
data class BookWithMedia(
|
||||
val book: Book,
|
||||
val media: Media,
|
||||
)
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package org.gotson.komga.domain.model
|
||||
|
||||
enum class CopyMode {
|
||||
MOVE,
|
||||
COPY,
|
||||
HARDLINK,
|
||||
}
|
||||
|
|
@ -16,4 +16,8 @@ data class Media(
|
|||
enum class Status {
|
||||
UNKNOWN, ERROR, READY, UNSUPPORTED, OUTDATED
|
||||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return "Media(status=$status, mediaType=$mediaType, comment=$comment, bookId='$bookId', createdDate=$createdDate, lastModifiedDate=$lastModifiedDate)"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ data class ReadListRequest(
|
|||
|
||||
data class ReadListRequestBook(
|
||||
val series: String,
|
||||
val number: Int,
|
||||
val number: String,
|
||||
)
|
||||
|
||||
data class ReadListRequestResult(
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ interface ReadProgressRepository {
|
|||
fun findAll(): Collection<ReadProgress>
|
||||
fun findByBookIdAndUserId(bookId: String, userId: String): ReadProgress?
|
||||
fun findByUserId(userId: String): Collection<ReadProgress>
|
||||
fun findByBookId(bookId: String): Collection<ReadProgress>
|
||||
|
||||
fun save(readProgress: ReadProgress)
|
||||
|
||||
|
|
|
|||
|
|
@ -4,36 +4,37 @@ import org.gotson.komga.domain.model.Author
|
|||
import java.time.LocalDate
|
||||
|
||||
interface ReferentialRepository {
|
||||
fun findAuthorsByName(search: String): List<Author>
|
||||
fun findAuthorsByNameAndLibrary(search: String, libraryId: String): List<Author>
|
||||
fun findAuthorsByNameAndCollection(search: String, collectionId: String): List<Author>
|
||||
fun findAuthorsByNameAndSeries(search: String, seriesId: String): List<Author>
|
||||
fun findAuthorsNamesByName(search: String): List<String>
|
||||
fun findAuthorsRoles(): List<String>
|
||||
fun findAuthorsByName(search: String, filterOnLibraryIds: Collection<String>?): List<Author>
|
||||
fun findAuthorsByNameAndLibrary(search: String, libraryId: String, filterOnLibraryIds: Collection<String>?): List<Author>
|
||||
fun findAuthorsByNameAndCollection(search: String, collectionId: String, filterOnLibraryIds: Collection<String>?): List<Author>
|
||||
fun findAuthorsByNameAndSeries(search: String, seriesId: String, filterOnLibraryIds: Collection<String>?): List<Author>
|
||||
fun findAuthorsNamesByName(search: String, filterOnLibraryIds: Collection<String>?): List<String>
|
||||
fun findAuthorsRoles(filterOnLibraryIds: Collection<String>?): List<String>
|
||||
|
||||
fun findAllGenres(): Set<String>
|
||||
fun findAllGenresByLibrary(libraryId: String): Set<String>
|
||||
fun findAllGenresByCollection(collectionId: String): Set<String>
|
||||
fun findAllGenres(filterOnLibraryIds: Collection<String>?): Set<String>
|
||||
fun findAllGenresByLibrary(libraryId: String, filterOnLibraryIds: Collection<String>?): Set<String>
|
||||
fun findAllGenresByCollection(collectionId: String, filterOnLibraryIds: Collection<String>?): Set<String>
|
||||
|
||||
fun findAllTags(): Set<String>
|
||||
fun findAllTagsByLibrary(libraryId: String): Set<String>
|
||||
fun findAllTagsBySeries(seriesId: String): Set<String>
|
||||
fun findAllTagsByCollection(collectionId: String): Set<String>
|
||||
fun findAllSeriesAndBookTags(filterOnLibraryIds: Collection<String>?): Set<String>
|
||||
fun findAllSeriesTags(filterOnLibraryIds: Collection<String>?): Set<String>
|
||||
fun findAllSeriesTagsByLibrary(libraryId: String, filterOnLibraryIds: Collection<String>?): Set<String>
|
||||
fun findAllSeriesTagsByCollection(collectionId: String, filterOnLibraryIds: Collection<String>?): Set<String>
|
||||
fun findAllBookTags(filterOnLibraryIds: Collection<String>?): Set<String>
|
||||
fun findAllBookTagsBySeries(seriesId: String, filterOnLibraryIds: Collection<String>?): Set<String>
|
||||
|
||||
fun findAllLanguages(): Set<String>
|
||||
fun findAllLanguagesByLibrary(libraryId: String): Set<String>
|
||||
fun findAllLanguagesByCollection(collectionId: String): Set<String>
|
||||
fun findAllLanguages(filterOnLibraryIds: Collection<String>?): Set<String>
|
||||
fun findAllLanguagesByLibrary(libraryId: String, filterOnLibraryIds: Collection<String>?): Set<String>
|
||||
fun findAllLanguagesByCollection(collectionId: String, filterOnLibraryIds: Collection<String>?): Set<String>
|
||||
|
||||
fun findAllPublishers(): Set<String>
|
||||
fun findAllPublishersByLibrary(libraryId: String): Set<String>
|
||||
fun findAllPublishersByLibraries(libraryIds: Set<String>): Set<String>
|
||||
fun findAllPublishersByCollection(collectionId: String): Set<String>
|
||||
fun findAllPublishers(filterOnLibraryIds: Collection<String>?): Set<String>
|
||||
fun findAllPublishersByLibrary(libraryId: String, filterOnLibraryIds: Collection<String>?): Set<String>
|
||||
fun findAllPublishersByCollection(collectionId: String, filterOnLibraryIds: Collection<String>?): Set<String>
|
||||
|
||||
fun findAllAgeRatings(): Set<Int?>
|
||||
fun findAllAgeRatingsByLibrary(libraryId: String): Set<Int?>
|
||||
fun findAllAgeRatingsByCollection(collectionId: String): Set<Int?>
|
||||
fun findAllAgeRatings(filterOnLibraryIds: Collection<String>?): Set<Int?>
|
||||
fun findAllAgeRatingsByLibrary(libraryId: String, filterOnLibraryIds: Collection<String>?): Set<Int?>
|
||||
fun findAllAgeRatingsByCollection(collectionId: String, filterOnLibraryIds: Collection<String>?): Set<Int?>
|
||||
|
||||
fun findAllSeriesReleaseDates(): Set<LocalDate>
|
||||
fun findAllSeriesReleaseDatesByLibrary(libraryId: String): Set<LocalDate>
|
||||
fun findAllSeriesReleaseDatesByCollection(collectionId: String): Set<LocalDate>
|
||||
fun findAllSeriesReleaseDates(filterOnLibraryIds: Collection<String>?): Set<LocalDate>
|
||||
fun findAllSeriesReleaseDatesByLibrary(libraryId: String, filterOnLibraryIds: Collection<String>?): Set<LocalDate>
|
||||
fun findAllSeriesReleaseDatesByCollection(collectionId: String, filterOnLibraryIds: Collection<String>?): Set<LocalDate>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,9 @@
|
|||
package org.gotson.komga.domain.persistence
|
||||
|
||||
import org.gotson.komga.domain.model.BookWithMedia
|
||||
|
||||
interface TransientBookRepository {
|
||||
fun findById(transientBookId: String): BookWithMedia?
|
||||
fun save(transientBook: BookWithMedia)
|
||||
fun saveAll(transientBooks: Collection<BookWithMedia>)
|
||||
}
|
||||
|
|
@ -3,15 +3,16 @@ package org.gotson.komga.domain.service
|
|||
import mu.KotlinLogging
|
||||
import org.gotson.komga.domain.model.Book
|
||||
import org.gotson.komga.domain.model.BookPage
|
||||
import org.gotson.komga.domain.model.BookWithMedia
|
||||
import org.gotson.komga.domain.model.Media
|
||||
import org.gotson.komga.domain.model.MediaNotReadyException
|
||||
import org.gotson.komga.domain.model.MediaUnsupportedException
|
||||
import org.gotson.komga.domain.model.ThumbnailBook
|
||||
import org.gotson.komga.domain.persistence.MediaRepository
|
||||
import org.gotson.komga.infrastructure.image.ImageConverter
|
||||
import org.gotson.komga.infrastructure.mediacontainer.ContentDetector
|
||||
import org.gotson.komga.infrastructure.mediacontainer.MediaContainerExtractor
|
||||
import org.springframework.stereotype.Service
|
||||
import java.nio.file.AccessDeniedException
|
||||
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
|
|
@ -19,8 +20,7 @@ private val logger = KotlinLogging.logger {}
|
|||
class BookAnalyzer(
|
||||
private val contentDetector: ContentDetector,
|
||||
extractors: List<MediaContainerExtractor>,
|
||||
private val imageConverter: ImageConverter,
|
||||
private val mediaRepository: MediaRepository
|
||||
private val imageConverter: ImageConverter
|
||||
) {
|
||||
|
||||
val supportedMediaTypes = extractors
|
||||
|
|
@ -32,61 +32,66 @@ class BookAnalyzer(
|
|||
|
||||
fun analyze(book: Book): Media {
|
||||
logger.info { "Trying to analyze book: $book" }
|
||||
try {
|
||||
val mediaType = contentDetector.detectMediaType(book.path())
|
||||
logger.info { "Detected media type: $mediaType" }
|
||||
if (!supportedMediaTypes.containsKey(mediaType))
|
||||
return Media(mediaType = mediaType, status = Media.Status.UNSUPPORTED, comment = "ERR_1001", bookId = book.id)
|
||||
|
||||
val mediaType = contentDetector.detectMediaType(book.path())
|
||||
logger.info { "Detected media type: $mediaType" }
|
||||
if (!supportedMediaTypes.containsKey(mediaType))
|
||||
return Media(mediaType = mediaType, status = Media.Status.UNSUPPORTED, comment = "ERR_1001")
|
||||
|
||||
val entries = try {
|
||||
supportedMediaTypes.getValue(mediaType).getEntries(book.path())
|
||||
} catch (ex: MediaUnsupportedException) {
|
||||
return Media(mediaType = mediaType, status = Media.Status.UNSUPPORTED, comment = ex.code)
|
||||
} catch (ex: Exception) {
|
||||
logger.error(ex) { "Error while analyzing book: $book" }
|
||||
return Media(mediaType = mediaType, status = Media.Status.ERROR, comment = "ERR_1008")
|
||||
}
|
||||
|
||||
val (pages, others) = entries
|
||||
.partition { entry ->
|
||||
entry.mediaType?.let { contentDetector.isImage(it) } ?: false
|
||||
}.let { (images, others) ->
|
||||
Pair(
|
||||
images.map { BookPage(it.name, it.mediaType!!, it.dimension) },
|
||||
others
|
||||
)
|
||||
val entries = try {
|
||||
supportedMediaTypes.getValue(mediaType).getEntries(book.path())
|
||||
} catch (ex: MediaUnsupportedException) {
|
||||
return Media(mediaType = mediaType, status = Media.Status.UNSUPPORTED, comment = ex.code, bookId = book.id)
|
||||
} catch (ex: Exception) {
|
||||
logger.error(ex) { "Error while analyzing book: $book" }
|
||||
return Media(mediaType = mediaType, status = Media.Status.ERROR, comment = "ERR_1008", bookId = book.id)
|
||||
}
|
||||
|
||||
val entriesErrorSummary = others
|
||||
.filter { it.mediaType.isNullOrBlank() }
|
||||
.map { it.name }
|
||||
.ifEmpty { null }
|
||||
?.joinToString(prefix = "ERR_1007 [", postfix = "]") { it }
|
||||
val (pages, others) = entries
|
||||
.partition { entry ->
|
||||
entry.mediaType?.let { contentDetector.isImage(it) } ?: false
|
||||
}.let { (images, others) ->
|
||||
Pair(
|
||||
images.map { BookPage(it.name, it.mediaType!!, it.dimension) },
|
||||
others
|
||||
)
|
||||
}
|
||||
|
||||
if (pages.isEmpty()) {
|
||||
logger.warn { "Book $book does not contain any pages" }
|
||||
return Media(mediaType = mediaType, status = Media.Status.ERROR, comment = "ERR_1006")
|
||||
val entriesErrorSummary = others
|
||||
.filter { it.mediaType.isNullOrBlank() }
|
||||
.map { it.name }
|
||||
.ifEmpty { null }
|
||||
?.joinToString(prefix = "ERR_1007 [", postfix = "]") { it }
|
||||
|
||||
if (pages.isEmpty()) {
|
||||
logger.warn { "Book $book does not contain any pages" }
|
||||
return Media(mediaType = mediaType, status = Media.Status.ERROR, comment = "ERR_1006", bookId = book.id)
|
||||
}
|
||||
logger.info { "Book has ${pages.size} pages" }
|
||||
|
||||
val files = others.map { it.name }
|
||||
|
||||
return Media(mediaType = mediaType, status = Media.Status.READY, pages = pages, files = files, comment = entriesErrorSummary, bookId = book.id)
|
||||
} catch (ade: AccessDeniedException) {
|
||||
logger.error(ade) { "Error while analyzing book: $book" }
|
||||
return Media(status = Media.Status.ERROR, comment = "ERR_1000", bookId = book.id)
|
||||
} catch (ex: Exception) {
|
||||
logger.error(ex) { "Error while analyzing book: $book" }
|
||||
return Media(status = Media.Status.ERROR, comment = "ERR_1005", bookId = book.id)
|
||||
}
|
||||
logger.info { "Book has ${pages.size} pages" }
|
||||
|
||||
val files = others.map { it.name }
|
||||
|
||||
return Media(mediaType = mediaType, status = Media.Status.READY, pages = pages, files = files, comment = entriesErrorSummary)
|
||||
}
|
||||
|
||||
@Throws(MediaNotReadyException::class)
|
||||
fun generateThumbnail(book: Book): ThumbnailBook {
|
||||
fun generateThumbnail(book: BookWithMedia): ThumbnailBook {
|
||||
logger.info { "Generate thumbnail for book: $book" }
|
||||
|
||||
val media = mediaRepository.findById(book.id)
|
||||
|
||||
if (media.status != Media.Status.READY) {
|
||||
if (book.media.status != Media.Status.READY) {
|
||||
logger.warn { "Book media is not ready, cannot generate thumbnail. Book: $book" }
|
||||
throw MediaNotReadyException()
|
||||
}
|
||||
|
||||
val thumbnail = try {
|
||||
supportedMediaTypes.getValue(media.mediaType!!).getEntryStream(book.path(), media.pages.first().fileName).let { cover ->
|
||||
supportedMediaTypes.getValue(book.media.mediaType!!).getEntryStream(book.book.path(), book.media.pages.first().fileName).let { cover ->
|
||||
imageConverter.resizeImage(cover, thumbnailFormat, thumbnailSize)
|
||||
}
|
||||
} catch (ex: Exception) {
|
||||
|
|
@ -97,7 +102,7 @@ class BookAnalyzer(
|
|||
return ThumbnailBook(
|
||||
thumbnail = thumbnail,
|
||||
type = ThumbnailBook.Type.GENERATED,
|
||||
bookId = book.id
|
||||
bookId = book.book.id
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -105,37 +110,33 @@ class BookAnalyzer(
|
|||
MediaNotReadyException::class,
|
||||
IndexOutOfBoundsException::class
|
||||
)
|
||||
fun getPageContent(book: Book, number: Int): ByteArray {
|
||||
fun getPageContent(book: BookWithMedia, number: Int): ByteArray {
|
||||
logger.info { "Get page #$number for book: $book" }
|
||||
|
||||
val media = mediaRepository.findById(book.id)
|
||||
|
||||
if (media.status != Media.Status.READY) {
|
||||
if (book.media.status != Media.Status.READY) {
|
||||
logger.warn { "Book media is not ready, cannot get pages" }
|
||||
throw MediaNotReadyException()
|
||||
}
|
||||
|
||||
if (number > media.pages.size || number <= 0) {
|
||||
logger.error { "Page number #$number is out of bounds. Book has ${media.pages.size} pages" }
|
||||
if (number > book.media.pages.size || number <= 0) {
|
||||
logger.error { "Page number #$number is out of bounds. Book has ${book.media.pages.size} pages" }
|
||||
throw IndexOutOfBoundsException("Page $number does not exist")
|
||||
}
|
||||
|
||||
return supportedMediaTypes.getValue(media.mediaType!!).getEntryStream(book.path(), media.pages[number - 1].fileName)
|
||||
return supportedMediaTypes.getValue(book.media.mediaType!!).getEntryStream(book.book.path(), book.media.pages[number - 1].fileName)
|
||||
}
|
||||
|
||||
@Throws(
|
||||
MediaNotReadyException::class
|
||||
)
|
||||
fun getFileContent(book: Book, fileName: String): ByteArray {
|
||||
fun getFileContent(book: BookWithMedia, fileName: String): ByteArray {
|
||||
logger.info { "Get file $fileName for book: $book" }
|
||||
|
||||
val media = mediaRepository.findById(book.id)
|
||||
|
||||
if (media.status != Media.Status.READY) {
|
||||
if (book.media.status != Media.Status.READY) {
|
||||
logger.warn { "Book media is not ready, cannot get files" }
|
||||
throw MediaNotReadyException()
|
||||
}
|
||||
|
||||
return supportedMediaTypes.getValue(media.mediaType!!).getEntryStream(book.path(), fileName)
|
||||
return supportedMediaTypes.getValue(book.media.mediaType!!).getEntryStream(book.book.path(), fileName)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,152 @@
|
|||
package org.gotson.komga.domain.service
|
||||
|
||||
import mu.KotlinLogging
|
||||
import org.gotson.komga.application.tasks.TaskReceiver
|
||||
import org.gotson.komga.domain.model.CopyMode
|
||||
import org.gotson.komga.domain.model.Media
|
||||
import org.gotson.komga.domain.model.PathContainedInPath
|
||||
import org.gotson.komga.domain.model.Series
|
||||
import org.gotson.komga.domain.persistence.BookMetadataRepository
|
||||
import org.gotson.komga.domain.persistence.BookRepository
|
||||
import org.gotson.komga.domain.persistence.LibraryRepository
|
||||
import org.gotson.komga.domain.persistence.MediaRepository
|
||||
import org.gotson.komga.domain.persistence.ReadListRepository
|
||||
import org.gotson.komga.domain.persistence.ReadProgressRepository
|
||||
import org.gotson.komga.infrastructure.language.toIndexedMap
|
||||
import org.springframework.stereotype.Service
|
||||
import java.io.FileNotFoundException
|
||||
import java.nio.file.FileAlreadyExistsException
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.NoSuchFileException
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
import kotlin.io.path.copyTo
|
||||
import kotlin.io.path.deleteExisting
|
||||
import kotlin.io.path.deleteIfExists
|
||||
import kotlin.io.path.exists
|
||||
import kotlin.io.path.extension
|
||||
import kotlin.io.path.moveTo
|
||||
import kotlin.io.path.notExists
|
||||
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
@Service
|
||||
class BookImporter(
|
||||
private val bookLifecycle: BookLifecycle,
|
||||
private val fileSystemScanner: FileSystemScanner,
|
||||
private val seriesLifecycle: SeriesLifecycle,
|
||||
private val bookRepository: BookRepository,
|
||||
private val mediaRepository: MediaRepository,
|
||||
private val metadataRepository: BookMetadataRepository,
|
||||
private val readProgressRepository: ReadProgressRepository,
|
||||
private val readListRepository: ReadListRepository,
|
||||
private val taskReceiver: TaskReceiver,
|
||||
private val libraryRepository: LibraryRepository,
|
||||
) {
|
||||
|
||||
fun importBook(sourceFile: Path, series: Series, copyMode: CopyMode, destinationName: String? = null, upgradeBookId: String? = null) {
|
||||
if (sourceFile.notExists()) throw FileNotFoundException("File not found: $sourceFile")
|
||||
|
||||
libraryRepository.findAll().forEach { library ->
|
||||
if (sourceFile.startsWith(library.path())) throw PathContainedInPath("Cannot import file that is part of an existing library")
|
||||
}
|
||||
|
||||
val destFile = series.path().resolve(
|
||||
if (destinationName != null) Paths.get("$destinationName.${sourceFile.extension}").fileName.toString()
|
||||
else sourceFile.fileName.toString()
|
||||
)
|
||||
|
||||
val upgradedBookId =
|
||||
if (upgradeBookId != null) {
|
||||
bookRepository.findByIdOrNull(upgradeBookId)?.let {
|
||||
if (it.seriesId != series.id) throw IllegalArgumentException("Book to upgrade ($upgradeBookId) does not belong to series: $series")
|
||||
it.id
|
||||
}
|
||||
} else null
|
||||
val upgradedBookPath =
|
||||
if (upgradedBookId != null)
|
||||
bookRepository.findByIdOrNull(upgradedBookId)?.path()
|
||||
else null
|
||||
|
||||
var deletedUpgradedFile = false
|
||||
when {
|
||||
upgradedBookPath != null && destFile == upgradedBookPath -> {
|
||||
logger.info { "Deleting existing file: $upgradedBookPath" }
|
||||
try {
|
||||
upgradedBookPath.deleteExisting()
|
||||
deletedUpgradedFile = true
|
||||
} catch (e: NoSuchFileException) {
|
||||
logger.warn { "Could not delete upgraded book: $upgradedBookPath" }
|
||||
}
|
||||
}
|
||||
destFile.exists() -> throw FileAlreadyExistsException("Destination file already exists: $destFile")
|
||||
}
|
||||
|
||||
when (copyMode) {
|
||||
CopyMode.MOVE -> {
|
||||
logger.info { "Moving file $sourceFile to $destFile" }
|
||||
sourceFile.moveTo(destFile)
|
||||
}
|
||||
CopyMode.COPY -> {
|
||||
logger.info { "Copying file $sourceFile to $destFile" }
|
||||
sourceFile.copyTo(destFile)
|
||||
}
|
||||
CopyMode.HARDLINK -> try {
|
||||
logger.info { "Hardlink file $sourceFile to $destFile" }
|
||||
Files.createLink(destFile, sourceFile)
|
||||
} catch (e: Exception) {
|
||||
logger.warn(e) { "Filesystem does not support hardlinks, copying instead" }
|
||||
sourceFile.copyTo(destFile)
|
||||
}
|
||||
}
|
||||
|
||||
val importedBook = fileSystemScanner.scanFile(destFile)
|
||||
?.copy(libraryId = series.libraryId)
|
||||
?: throw IllegalStateException("Newly imported book could not be scanned: $destFile")
|
||||
|
||||
seriesLifecycle.addBooks(series, listOf(importedBook))
|
||||
|
||||
if (upgradedBookId != null) {
|
||||
// copy media and mark it as outdated
|
||||
mediaRepository.findById(upgradedBookId).let {
|
||||
mediaRepository.update(
|
||||
it.copy(
|
||||
bookId = importedBook.id,
|
||||
status = Media.Status.OUTDATED,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// copy metadata
|
||||
metadataRepository.findById(upgradedBookId).let {
|
||||
metadataRepository.update(it.copy(bookId = importedBook.id))
|
||||
}
|
||||
|
||||
// copy read progress
|
||||
readProgressRepository.findByBookId(upgradedBookId)
|
||||
.map { it.copy(bookId = importedBook.id) }
|
||||
.forEach { readProgressRepository.save(it) }
|
||||
|
||||
// replace upgraded book by imported book in read lists
|
||||
readListRepository.findAllByBook(upgradedBookId, filterOnLibraryIds = null)
|
||||
.forEach { rl ->
|
||||
readListRepository.update(
|
||||
rl.copy(
|
||||
bookIds = rl.bookIds.values.map { if (it == upgradedBookId) importedBook.id else it }.toIndexedMap()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// delete upgraded book file on disk if it has not been replaced earlier
|
||||
if (upgradedBookPath != null && !deletedUpgradedFile && upgradedBookPath.deleteIfExists())
|
||||
logger.info { "Deleted existing file: $upgradedBookPath" }
|
||||
|
||||
// delete upgraded book
|
||||
bookLifecycle.deleteOne(upgradedBookId)
|
||||
}
|
||||
|
||||
seriesLifecycle.sortBooks(series)
|
||||
|
||||
taskReceiver.analyzeBook(importedBook)
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ package org.gotson.komga.domain.service
|
|||
import mu.KotlinLogging
|
||||
import org.gotson.komga.domain.model.Book
|
||||
import org.gotson.komga.domain.model.BookPageContent
|
||||
import org.gotson.komga.domain.model.BookWithMedia
|
||||
import org.gotson.komga.domain.model.ImageConversionException
|
||||
import org.gotson.komga.domain.model.KomgaUser
|
||||
import org.gotson.komga.domain.model.Media
|
||||
|
|
@ -19,7 +20,6 @@ import org.gotson.komga.infrastructure.image.ImageConverter
|
|||
import org.gotson.komga.infrastructure.image.ImageType
|
||||
import org.springframework.stereotype.Service
|
||||
import java.io.File
|
||||
import java.nio.file.AccessDeniedException
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Paths
|
||||
|
||||
|
|
@ -39,15 +39,7 @@ class BookLifecycle(
|
|||
|
||||
fun analyzeAndPersist(book: Book): Boolean {
|
||||
logger.info { "Analyze and persist book: $book" }
|
||||
val media = try {
|
||||
bookAnalyzer.analyze(book)
|
||||
} catch (ade: AccessDeniedException) {
|
||||
logger.error(ade) { "Error while analyzing book: $book" }
|
||||
Media(status = Media.Status.ERROR, comment = "ERR_1000")
|
||||
} catch (ex: Exception) {
|
||||
logger.error(ex) { "Error while analyzing book: $book" }
|
||||
Media(status = Media.Status.ERROR, comment = "ERR_1005")
|
||||
}.copy(bookId = book.id)
|
||||
val media = bookAnalyzer.analyze(book)
|
||||
|
||||
// if the number of pages has changed, delete all read progress for that book
|
||||
mediaRepository.findById(book.id).let { previous ->
|
||||
|
|
@ -63,7 +55,7 @@ class BookLifecycle(
|
|||
fun generateThumbnailAndPersist(book: Book) {
|
||||
logger.info { "Generate thumbnail and persist for book: $book" }
|
||||
try {
|
||||
addThumbnailForBook(bookAnalyzer.generateThumbnail(book))
|
||||
addThumbnailForBook(bookAnalyzer.generateThumbnail(BookWithMedia(book, mediaRepository.findById(book.id))))
|
||||
} catch (ex: Exception) {
|
||||
logger.error(ex) { "Error while creating thumbnail" }
|
||||
}
|
||||
|
|
@ -155,7 +147,7 @@ class BookLifecycle(
|
|||
)
|
||||
fun getBookPage(book: Book, number: Int, convertTo: ImageType? = null, resizeTo: Int? = null): BookPageContent {
|
||||
val media = mediaRepository.findById(book.id)
|
||||
val pageContent = bookAnalyzer.getPageContent(book, number)
|
||||
val pageContent = bookAnalyzer.getPageContent(BookWithMedia(book, mediaRepository.findById(book.id)), number)
|
||||
val pageMediaType = media.pages[number - 1].mediaType
|
||||
|
||||
if (resizeTo != null) {
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@ import java.nio.file.attribute.BasicFileAttributes
|
|||
import java.nio.file.attribute.FileTime
|
||||
import java.time.LocalDateTime
|
||||
import java.time.ZoneId
|
||||
import kotlin.io.path.exists
|
||||
import kotlin.io.path.readAttributes
|
||||
import kotlin.time.measureTime
|
||||
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
|
@ -35,7 +37,7 @@ class FileSystemScanner(
|
|||
logger.info { "Force directory modified time: $forceDirectoryModifiedTime" }
|
||||
|
||||
if (!(Files.isDirectory(root) && Files.isReadable(root)))
|
||||
throw DirectoryNotFoundException("Library root is not accessible: $root")
|
||||
throw DirectoryNotFoundException("Folder is not accessible: $root", "ERR_1016")
|
||||
|
||||
val scannedSeries = mutableMapOf<Series, List<Book>>()
|
||||
|
||||
|
|
@ -69,12 +71,7 @@ class FileSystemScanner(
|
|||
supportedExtensions.contains(FilenameUtils.getExtension(file.fileName.toString()).toLowerCase()) &&
|
||||
!file.fileName.toString().startsWith(".")
|
||||
) {
|
||||
val book = Book(
|
||||
name = FilenameUtils.getBaseName(file.fileName.toString()),
|
||||
url = file.toUri().toURL(),
|
||||
fileLastModified = attrs.getUpdatedTime(),
|
||||
fileSize = attrs.size()
|
||||
)
|
||||
val book = pathToBook(file, attrs)
|
||||
file.parent.let { key ->
|
||||
if (pathToBooks.containsKey(key)) pathToBooks[key]!!.add(book)
|
||||
else pathToBooks[key] = mutableListOf(book)
|
||||
|
|
@ -118,6 +115,20 @@ class FileSystemScanner(
|
|||
|
||||
return scannedSeries
|
||||
}
|
||||
|
||||
fun scanFile(path: Path): Book? {
|
||||
if (!path.exists()) return null
|
||||
|
||||
return pathToBook(path, path.readAttributes())
|
||||
}
|
||||
|
||||
private fun pathToBook(path: Path, attrs: BasicFileAttributes): Book =
|
||||
Book(
|
||||
name = FilenameUtils.getBaseName(path.fileName.toString()),
|
||||
url = path.toUri().toURL(),
|
||||
fileLastModified = attrs.getUpdatedTime(),
|
||||
fileSize = attrs.size()
|
||||
)
|
||||
}
|
||||
|
||||
fun BasicFileAttributes.getUpdatedTime(): LocalDateTime =
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import mu.KotlinLogging
|
|||
import org.gotson.komga.domain.model.Book
|
||||
import org.gotson.komga.domain.model.BookMetadataPatch
|
||||
import org.gotson.komga.domain.model.BookMetadataPatchCapability
|
||||
import org.gotson.komga.domain.model.BookWithMedia
|
||||
import org.gotson.komga.domain.model.ReadList
|
||||
import org.gotson.komga.domain.model.Series
|
||||
import org.gotson.komga.domain.model.SeriesCollection
|
||||
|
|
@ -65,7 +66,7 @@ class MetadataLifecycle(
|
|||
logger.info { "Library is not set to import book metadata from Barcode ISBN, skipping" }
|
||||
else -> {
|
||||
logger.debug { "Provider: $provider" }
|
||||
val patch = provider.getBookMetadataFromBook(book, media)
|
||||
val patch = provider.getBookMetadataFromBook(BookWithMedia(book, media))
|
||||
|
||||
if (
|
||||
(provider is ComicInfoProvider && library.importComicInfoBook) ||
|
||||
|
|
@ -156,7 +157,7 @@ class MetadataLifecycle(
|
|||
else -> {
|
||||
logger.debug { "Provider: $provider" }
|
||||
val patches = bookRepository.findBySeriesId(series.id)
|
||||
.mapNotNull { provider.getSeriesMetadataFromBook(it, mediaRepository.findById(it.id)) }
|
||||
.mapNotNull { provider.getSeriesMetadataFromBook(BookWithMedia(it, mediaRepository.findById(it.id))) }
|
||||
|
||||
if (
|
||||
(provider is ComicInfoProvider && library.importComicInfoSeries) ||
|
||||
|
|
|
|||
|
|
@ -40,7 +40,7 @@ class ReadListMatcher(
|
|||
val seriesId = seriesMatches.first().id
|
||||
val seriesBooks = bookRepository.findBySeriesId(seriesId)
|
||||
val bookMatches = bookMetadataRepository.findByIds(seriesBooks.map { it.id })
|
||||
.filter { it.number.toIntOrNull()?.equals(book.number) ?: false }
|
||||
.filter { (it.number.trimStart('0') == book.number.trimStart('0')) }
|
||||
.map { it.bookId }
|
||||
when {
|
||||
bookMatches.size > 1 -> unmatchedBooks += ReadListRequestResultBook(book, "ERR_1013")
|
||||
|
|
|
|||
|
|
@ -0,0 +1,54 @@
|
|||
package org.gotson.komga.domain.service
|
||||
|
||||
import org.gotson.komga.domain.model.BookPageContent
|
||||
import org.gotson.komga.domain.model.BookWithMedia
|
||||
import org.gotson.komga.domain.model.Media
|
||||
import org.gotson.komga.domain.model.MediaNotReadyException
|
||||
import org.gotson.komga.domain.model.PathContainedInPath
|
||||
import org.gotson.komga.domain.persistence.LibraryRepository
|
||||
import org.gotson.komga.domain.persistence.TransientBookRepository
|
||||
import org.springframework.stereotype.Service
|
||||
import java.nio.file.Paths
|
||||
|
||||
@Service
|
||||
class TransientBookLifecycle(
|
||||
private val transientBookRepository: TransientBookRepository,
|
||||
private val bookAnalyzer: BookAnalyzer,
|
||||
private val fileSystemScanner: FileSystemScanner,
|
||||
private val libraryRepository: LibraryRepository,
|
||||
) {
|
||||
|
||||
fun scanAndPersist(filePath: String): List<BookWithMedia> {
|
||||
val folderToScan = Paths.get(filePath)
|
||||
|
||||
libraryRepository.findAll().forEach { library ->
|
||||
if (folderToScan.startsWith(library.path())) throw PathContainedInPath("Cannot scan folder that is part of an existing library", "ERR_1017")
|
||||
}
|
||||
|
||||
val books = fileSystemScanner.scanRootFolder(folderToScan).values.flatten().map { BookWithMedia(it, Media()) }
|
||||
|
||||
transientBookRepository.saveAll(books)
|
||||
|
||||
return books
|
||||
}
|
||||
|
||||
fun analyzeAndPersist(transientBook: BookWithMedia): BookWithMedia {
|
||||
val media = bookAnalyzer.analyze(transientBook.book)
|
||||
|
||||
val updated = transientBook.copy(media = media)
|
||||
transientBookRepository.save(updated)
|
||||
|
||||
return updated
|
||||
}
|
||||
|
||||
@Throws(
|
||||
MediaNotReadyException::class,
|
||||
IndexOutOfBoundsException::class
|
||||
)
|
||||
fun getBookPage(transientBook: BookWithMedia, number: Int): BookPageContent {
|
||||
val pageContent = bookAnalyzer.getPageContent(transientBook, number)
|
||||
val pageMediaType = transientBook.media.pages[number - 1].mediaType
|
||||
|
||||
return BookPageContent(number, pageContent, pageMediaType)
|
||||
}
|
||||
}
|
||||
27
komga/src/main/kotlin/org/gotson/komga/infrastructure/cache/TransientBookCache.kt
vendored
Normal file
27
komga/src/main/kotlin/org/gotson/komga/infrastructure/cache/TransientBookCache.kt
vendored
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
package org.gotson.komga.infrastructure.cache
|
||||
|
||||
import com.github.benmanes.caffeine.cache.Caffeine
|
||||
import mu.KotlinLogging
|
||||
import org.gotson.komga.domain.model.BookWithMedia
|
||||
import org.gotson.komga.domain.persistence.TransientBookRepository
|
||||
import org.springframework.stereotype.Service
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
@Service
|
||||
class TransientBookCache : TransientBookRepository {
|
||||
private val cache = Caffeine.newBuilder()
|
||||
.expireAfterAccess(1, TimeUnit.HOURS)
|
||||
.build<String, BookWithMedia>()
|
||||
|
||||
override fun findById(transientBookId: String): BookWithMedia? = cache.getIfPresent(transientBookId)
|
||||
|
||||
override fun save(transientBook: BookWithMedia) {
|
||||
cache.put(transientBook.book.id, transientBook)
|
||||
}
|
||||
|
||||
override fun saveAll(transientBooks: Collection<BookWithMedia>) {
|
||||
cache.putAll(transientBooks.associateBy { it.book.id })
|
||||
}
|
||||
}
|
||||
|
|
@ -23,8 +23,16 @@ class ImageConverter {
|
|||
|
||||
init {
|
||||
val registry = IIORegistry.getDefaultInstance()
|
||||
val nativeWebp = registry.getServiceProviderByClass(Class.forName("com.luciad.imageio.webp.WebPImageReaderSpi")) as ImageReaderSpi?
|
||||
val javaWebp = registry.getServiceProviderByClass(Class.forName("net.sf.javavp8decoder.imageio.WebPImageReaderSpi")) as ImageReaderSpi?
|
||||
val nativeWebp = try {
|
||||
registry.getServiceProviderByClass(Class.forName("com.luciad.imageio.webp.WebPImageReaderSpi"))
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
} as ImageReaderSpi?
|
||||
val javaWebp = try {
|
||||
registry.getServiceProviderByClass(Class.forName("com.twelvemonkeys.imageio.plugins.webp.WebPImageReaderSpi"))
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
} as ImageReaderSpi?
|
||||
|
||||
if (nativeWebp != null) {
|
||||
if (!WebP.loadNativeLibrary()) {
|
||||
|
|
|
|||
|
|
@ -33,6 +33,12 @@ class ReadProgressDao(
|
|||
.fetchInto(r)
|
||||
.map { it.toDomain() }
|
||||
|
||||
override fun findByBookId(bookId: String): Collection<ReadProgress> =
|
||||
dsl.selectFrom(r)
|
||||
.where(r.BOOK_ID.eq(bookId))
|
||||
.fetchInto(r)
|
||||
.map { it.toDomain() }
|
||||
|
||||
override fun save(readProgress: ReadProgress) {
|
||||
dsl.insertInto(r, r.BOOK_ID, r.USER_ID, r.PAGE, r.COMPLETED)
|
||||
.values(readProgress.bookId, readProgress.userId, readProgress.page, readProgress.completed)
|
||||
|
|
|
|||
|
|
@ -27,215 +27,295 @@ class ReferentialDao(
|
|||
private val st = Tables.SERIES_METADATA_TAG
|
||||
private val cs = Tables.COLLECTION_SERIES
|
||||
|
||||
override fun findAuthorsByName(search: String): List<Author> =
|
||||
override fun findAuthorsByName(search: String, filterOnLibraryIds: Collection<String>?): List<Author> =
|
||||
dsl.selectDistinct(a.NAME, a.ROLE)
|
||||
.from(a)
|
||||
.apply { filterOnLibraryIds?.let { leftJoin(b).on(a.BOOK_ID.eq(b.ID)) } }
|
||||
.where(a.NAME.containsIgnoreCase(search))
|
||||
.apply { filterOnLibraryIds?.let { and(b.LIBRARY_ID.`in`(it)) } }
|
||||
.orderBy(a.NAME, a.ROLE)
|
||||
.fetchInto(a)
|
||||
.map { it.toDomain() }
|
||||
|
||||
override fun findAuthorsByNameAndLibrary(search: String, libraryId: String): List<Author> =
|
||||
override fun findAuthorsByNameAndLibrary(search: String, libraryId: String, filterOnLibraryIds: Collection<String>?): List<Author> =
|
||||
dsl.selectDistinct(bmaa.NAME, bmaa.ROLE)
|
||||
.from(bmaa)
|
||||
.leftJoin(s).on(bmaa.SERIES_ID.eq(s.ID))
|
||||
.where(bmaa.NAME.containsIgnoreCase(search))
|
||||
.and(s.LIBRARY_ID.eq(libraryId))
|
||||
.apply { filterOnLibraryIds?.let { and(s.LIBRARY_ID.`in`(it)) } }
|
||||
.orderBy(bmaa.NAME, bmaa.ROLE)
|
||||
.fetchInto(bmaa)
|
||||
.map { it.toDomain() }
|
||||
|
||||
override fun findAuthorsByNameAndCollection(search: String, collectionId: String): List<Author> =
|
||||
override fun findAuthorsByNameAndCollection(search: String, collectionId: String, filterOnLibraryIds: Collection<String>?): List<Author> =
|
||||
dsl.selectDistinct(bmaa.NAME, bmaa.ROLE)
|
||||
.from(bmaa)
|
||||
.leftJoin(cs).on(bmaa.SERIES_ID.eq(cs.SERIES_ID))
|
||||
.apply { filterOnLibraryIds?.let { leftJoin(s).on(bmaa.SERIES_ID.eq(s.ID)) } }
|
||||
.where(bmaa.NAME.containsIgnoreCase(search))
|
||||
.and(cs.COLLECTION_ID.eq(collectionId))
|
||||
.apply { filterOnLibraryIds?.let { and(s.LIBRARY_ID.`in`(it)) } }
|
||||
.orderBy(bmaa.NAME, bmaa.ROLE)
|
||||
.fetchInto(bmaa)
|
||||
.map { it.toDomain() }
|
||||
|
||||
override fun findAuthorsByNameAndSeries(search: String, seriesId: String): List<Author> =
|
||||
override fun findAuthorsByNameAndSeries(search: String, seriesId: String, filterOnLibraryIds: Collection<String>?): List<Author> =
|
||||
dsl.selectDistinct(bmaa.NAME, bmaa.ROLE)
|
||||
.from(bmaa)
|
||||
.apply { filterOnLibraryIds?.let { leftJoin(s).on(bmaa.SERIES_ID.eq(s.ID)) } }
|
||||
.where(bmaa.NAME.containsIgnoreCase(search))
|
||||
.and(bmaa.SERIES_ID.eq(seriesId))
|
||||
.apply { filterOnLibraryIds?.let { and(s.LIBRARY_ID.`in`(it)) } }
|
||||
.orderBy(bmaa.NAME, bmaa.ROLE)
|
||||
.fetchInto(bmaa)
|
||||
.map { it.toDomain() }
|
||||
|
||||
override fun findAuthorsNamesByName(search: String): List<String> =
|
||||
override fun findAuthorsNamesByName(search: String, filterOnLibraryIds: Collection<String>?): List<String> =
|
||||
dsl.selectDistinct(a.NAME)
|
||||
.from(a)
|
||||
.apply { filterOnLibraryIds?.let { leftJoin(b).on(a.BOOK_ID.eq(b.ID)) } }
|
||||
.where(a.NAME.containsIgnoreCase(search))
|
||||
.apply { filterOnLibraryIds?.let { and(b.LIBRARY_ID.`in`(it)) } }
|
||||
.orderBy(a.NAME)
|
||||
.fetch(a.NAME)
|
||||
|
||||
override fun findAuthorsRoles(): List<String> =
|
||||
override fun findAuthorsRoles(filterOnLibraryIds: Collection<String>?): List<String> =
|
||||
dsl.selectDistinct(a.ROLE)
|
||||
.from(a)
|
||||
.apply {
|
||||
filterOnLibraryIds?.let {
|
||||
leftJoin(b).on(a.BOOK_ID.eq(b.ID))
|
||||
.where(b.LIBRARY_ID.`in`(it))
|
||||
}
|
||||
}
|
||||
.orderBy(a.ROLE)
|
||||
.fetch(a.ROLE)
|
||||
|
||||
override fun findAllGenres(): Set<String> =
|
||||
override fun findAllGenres(filterOnLibraryIds: Collection<String>?): Set<String> =
|
||||
dsl.selectDistinct(g.GENRE)
|
||||
.from(g)
|
||||
.apply {
|
||||
filterOnLibraryIds?.let {
|
||||
leftJoin(s).on(g.SERIES_ID.eq(s.ID))
|
||||
.where(s.LIBRARY_ID.`in`(it))
|
||||
}
|
||||
}
|
||||
.orderBy(lower(g.GENRE))
|
||||
.fetchSet(g.GENRE)
|
||||
|
||||
override fun findAllGenresByLibrary(libraryId: String): Set<String> =
|
||||
override fun findAllGenresByLibrary(libraryId: String, filterOnLibraryIds: Collection<String>?): Set<String> =
|
||||
dsl.selectDistinct(g.GENRE)
|
||||
.from(g)
|
||||
.leftJoin(s).on(g.SERIES_ID.eq(s.ID))
|
||||
.where(s.LIBRARY_ID.eq(libraryId))
|
||||
.apply { filterOnLibraryIds?.let { and(s.LIBRARY_ID.`in`(it)) } }
|
||||
.orderBy(lower(g.GENRE))
|
||||
.fetchSet(g.GENRE)
|
||||
|
||||
override fun findAllGenresByCollection(collectionId: String): Set<String> =
|
||||
override fun findAllGenresByCollection(collectionId: String, filterOnLibraryIds: Collection<String>?): Set<String> =
|
||||
dsl.selectDistinct(g.GENRE)
|
||||
.from(g)
|
||||
.leftJoin(cs).on(g.SERIES_ID.eq(cs.SERIES_ID))
|
||||
.apply { filterOnLibraryIds?.let { leftJoin(s).on(g.SERIES_ID.eq(s.ID)) } }
|
||||
.where(cs.COLLECTION_ID.eq(collectionId))
|
||||
.apply { filterOnLibraryIds?.let { and(s.LIBRARY_ID.`in`(it)) } }
|
||||
.orderBy(lower(g.GENRE))
|
||||
.fetchSet(g.GENRE)
|
||||
|
||||
override fun findAllTags(): Set<String> =
|
||||
override fun findAllSeriesAndBookTags(filterOnLibraryIds: Collection<String>?): Set<String> =
|
||||
dsl.select(bt.TAG.`as`("tag"))
|
||||
.from(bt)
|
||||
.apply {
|
||||
filterOnLibraryIds?.let {
|
||||
leftJoin(b).on(bt.BOOK_ID.eq(b.ID))
|
||||
.where(b.LIBRARY_ID.`in`(it))
|
||||
}
|
||||
}
|
||||
.union(
|
||||
select(st.TAG.`as`("tag")).from(st)
|
||||
select(st.TAG.`as`("tag"))
|
||||
.from(st)
|
||||
.apply {
|
||||
filterOnLibraryIds?.let {
|
||||
leftJoin(s).on(st.SERIES_ID.eq(s.ID))
|
||||
.where(s.LIBRARY_ID.`in`(it))
|
||||
}
|
||||
}
|
||||
)
|
||||
.fetchSet(0, String::class.java)
|
||||
.sortedBy { it.toLowerCase() }
|
||||
.toSet()
|
||||
|
||||
override fun findAllTagsByLibrary(libraryId: String): Set<String> =
|
||||
override fun findAllSeriesTags(filterOnLibraryIds: Collection<String>?): Set<String> =
|
||||
dsl.select(st.TAG)
|
||||
.from(st)
|
||||
.apply {
|
||||
filterOnLibraryIds?.let {
|
||||
leftJoin(s).on(st.SERIES_ID.eq(s.ID))
|
||||
.where(s.LIBRARY_ID.`in`(it))
|
||||
}
|
||||
}
|
||||
.orderBy(lower(st.TAG))
|
||||
.fetchSet(st.TAG)
|
||||
|
||||
override fun findAllSeriesTagsByLibrary(libraryId: String, filterOnLibraryIds: Collection<String>?): Set<String> =
|
||||
dsl.select(st.TAG)
|
||||
.from(st)
|
||||
.leftJoin(s).on(st.SERIES_ID.eq(s.ID))
|
||||
.where(s.LIBRARY_ID.eq(libraryId))
|
||||
.apply { filterOnLibraryIds?.let { and(s.LIBRARY_ID.`in`(it)) } }
|
||||
.orderBy(lower(st.TAG))
|
||||
.fetchSet(st.TAG)
|
||||
|
||||
override fun findAllTagsBySeries(seriesId: String): Set<String> =
|
||||
override fun findAllBookTagsBySeries(seriesId: String, filterOnLibraryIds: Collection<String>?): Set<String> =
|
||||
dsl.select(bt.TAG)
|
||||
.from(bt)
|
||||
.leftJoin(b).on(bt.BOOK_ID.eq(b.ID))
|
||||
.where(b.SERIES_ID.eq(seriesId))
|
||||
.apply { filterOnLibraryIds?.let { and(b.LIBRARY_ID.`in`(it)) } }
|
||||
.orderBy(lower(bt.TAG))
|
||||
.fetchSet(bt.TAG)
|
||||
|
||||
override fun findAllTagsByCollection(collectionId: String): Set<String> =
|
||||
override fun findAllSeriesTagsByCollection(collectionId: String, filterOnLibraryIds: Collection<String>?): Set<String> =
|
||||
dsl.select(st.TAG)
|
||||
.from(st)
|
||||
.leftJoin(cs).on(st.SERIES_ID.eq(cs.SERIES_ID))
|
||||
.apply { filterOnLibraryIds?.let { leftJoin(s).on(st.SERIES_ID.eq(s.ID)) } }
|
||||
.where(cs.COLLECTION_ID.eq(collectionId))
|
||||
.apply { filterOnLibraryIds?.let { and(s.LIBRARY_ID.`in`(it)) } }
|
||||
.orderBy(lower(st.TAG))
|
||||
.fetchSet(st.TAG)
|
||||
|
||||
override fun findAllLanguages(): Set<String> =
|
||||
override fun findAllBookTags(filterOnLibraryIds: Collection<String>?): Set<String> =
|
||||
dsl.select(bt.TAG)
|
||||
.from(bt)
|
||||
.apply {
|
||||
filterOnLibraryIds?.let {
|
||||
leftJoin(b).on(bt.BOOK_ID.eq(b.ID))
|
||||
.where(b.LIBRARY_ID.`in`(it))
|
||||
}
|
||||
}
|
||||
.orderBy(lower(st.TAG))
|
||||
.fetchSet(st.TAG)
|
||||
|
||||
override fun findAllLanguages(filterOnLibraryIds: Collection<String>?): Set<String> =
|
||||
dsl.selectDistinct(sd.LANGUAGE)
|
||||
.from(sd)
|
||||
.apply { filterOnLibraryIds?.let { leftJoin(s).on(sd.SERIES_ID.eq(s.ID)) } }
|
||||
.where(sd.LANGUAGE.ne(""))
|
||||
.apply { filterOnLibraryIds?.let { and(s.LIBRARY_ID.`in`(it)) } }
|
||||
.orderBy(sd.LANGUAGE)
|
||||
.fetchSet(sd.LANGUAGE)
|
||||
|
||||
override fun findAllLanguagesByLibrary(libraryId: String): Set<String> =
|
||||
override fun findAllLanguagesByLibrary(libraryId: String, filterOnLibraryIds: Collection<String>?): Set<String> =
|
||||
dsl.selectDistinct(sd.LANGUAGE)
|
||||
.from(sd)
|
||||
.leftJoin(s).on(sd.SERIES_ID.eq(s.ID))
|
||||
.where(sd.LANGUAGE.ne(""))
|
||||
.and(s.LIBRARY_ID.eq(libraryId))
|
||||
.apply { filterOnLibraryIds?.let { and(s.LIBRARY_ID.`in`(it)) } }
|
||||
.orderBy(sd.LANGUAGE)
|
||||
.fetchSet(sd.LANGUAGE)
|
||||
|
||||
override fun findAllLanguagesByCollection(collectionId: String): Set<String> =
|
||||
override fun findAllLanguagesByCollection(collectionId: String, filterOnLibraryIds: Collection<String>?): Set<String> =
|
||||
dsl.selectDistinct(sd.LANGUAGE)
|
||||
.from(sd)
|
||||
.leftJoin(cs).on(sd.SERIES_ID.eq(cs.SERIES_ID))
|
||||
.apply { filterOnLibraryIds?.let { leftJoin(s).on(sd.SERIES_ID.eq(s.ID)) } }
|
||||
.where(sd.LANGUAGE.ne(""))
|
||||
.and(cs.COLLECTION_ID.eq(collectionId))
|
||||
.apply { filterOnLibraryIds?.let { and(s.LIBRARY_ID.`in`(it)) } }
|
||||
.orderBy(sd.LANGUAGE)
|
||||
.fetchSet(sd.LANGUAGE)
|
||||
|
||||
override fun findAllPublishers(): Set<String> =
|
||||
override fun findAllPublishers(filterOnLibraryIds: Collection<String>?): Set<String> =
|
||||
dsl.selectDistinct(sd.PUBLISHER)
|
||||
.from(sd)
|
||||
.apply { filterOnLibraryIds?.let { leftJoin(s).on(sd.SERIES_ID.eq(s.ID)) } }
|
||||
.where(sd.PUBLISHER.ne(""))
|
||||
.apply { filterOnLibraryIds?.let { and(s.LIBRARY_ID.`in`(it)) } }
|
||||
.orderBy(sd.PUBLISHER)
|
||||
.fetchSet(sd.PUBLISHER)
|
||||
|
||||
override fun findAllPublishersByLibrary(libraryId: String): Set<String> =
|
||||
override fun findAllPublishersByLibrary(libraryId: String, filterOnLibraryIds: Collection<String>?): Set<String> =
|
||||
dsl.selectDistinct(sd.PUBLISHER)
|
||||
.from(sd)
|
||||
.leftJoin(s).on(sd.SERIES_ID.eq(s.ID))
|
||||
.where(sd.PUBLISHER.ne(""))
|
||||
.and(s.LIBRARY_ID.eq(libraryId))
|
||||
.apply { filterOnLibraryIds?.let { and(s.LIBRARY_ID.`in`(it)) } }
|
||||
.orderBy(sd.PUBLISHER)
|
||||
.fetchSet(sd.PUBLISHER)
|
||||
|
||||
override fun findAllPublishersByLibraries(libraryIds: Set<String>): Set<String> =
|
||||
dsl.selectDistinct(sd.PUBLISHER)
|
||||
.from(sd)
|
||||
.leftJoin(s).on(sd.SERIES_ID.eq(s.ID))
|
||||
.where(sd.PUBLISHER.ne(""))
|
||||
.and(s.LIBRARY_ID.`in`(libraryIds))
|
||||
.orderBy(sd.PUBLISHER)
|
||||
.fetchSet(sd.PUBLISHER)
|
||||
|
||||
override fun findAllPublishersByCollection(collectionId: String): Set<String> =
|
||||
override fun findAllPublishersByCollection(collectionId: String, filterOnLibraryIds: Collection<String>?): Set<String> =
|
||||
dsl.selectDistinct(sd.PUBLISHER)
|
||||
.from(sd)
|
||||
.leftJoin(cs).on(sd.SERIES_ID.eq(cs.SERIES_ID))
|
||||
.apply { filterOnLibraryIds?.let { leftJoin(s).on(sd.SERIES_ID.eq(s.ID)) } }
|
||||
.where(sd.PUBLISHER.ne(""))
|
||||
.and(cs.COLLECTION_ID.eq(collectionId))
|
||||
.apply { filterOnLibraryIds?.let { and(s.LIBRARY_ID.`in`(it)) } }
|
||||
.orderBy(sd.PUBLISHER)
|
||||
.fetchSet(sd.PUBLISHER)
|
||||
|
||||
override fun findAllAgeRatings(): Set<Int> =
|
||||
override fun findAllAgeRatings(filterOnLibraryIds: Collection<String>?): Set<Int> =
|
||||
dsl.selectDistinct(sd.AGE_RATING)
|
||||
.from(sd)
|
||||
.apply {
|
||||
filterOnLibraryIds?.let {
|
||||
leftJoin(s).on(sd.SERIES_ID.eq(s.ID))
|
||||
.where(s.LIBRARY_ID.`in`(it))
|
||||
}
|
||||
}
|
||||
.orderBy(sd.AGE_RATING)
|
||||
.fetchSet(sd.AGE_RATING)
|
||||
|
||||
override fun findAllAgeRatingsByLibrary(libraryId: String): Set<Int> =
|
||||
override fun findAllAgeRatingsByLibrary(libraryId: String, filterOnLibraryIds: Collection<String>?): Set<Int> =
|
||||
dsl.selectDistinct(sd.AGE_RATING)
|
||||
.from(sd)
|
||||
.leftJoin(s).on(sd.SERIES_ID.eq(s.ID))
|
||||
.where(s.LIBRARY_ID.eq(libraryId))
|
||||
.apply { filterOnLibraryIds?.let { and(s.LIBRARY_ID.`in`(it)) } }
|
||||
.orderBy(sd.AGE_RATING)
|
||||
.fetchSet(sd.AGE_RATING)
|
||||
|
||||
override fun findAllAgeRatingsByCollection(collectionId: String): Set<Int?> =
|
||||
override fun findAllAgeRatingsByCollection(collectionId: String, filterOnLibraryIds: Collection<String>?): Set<Int?> =
|
||||
dsl.selectDistinct(sd.AGE_RATING)
|
||||
.from(sd)
|
||||
.leftJoin(cs).on(sd.SERIES_ID.eq(cs.SERIES_ID))
|
||||
.apply { filterOnLibraryIds?.let { leftJoin(s).on(sd.SERIES_ID.eq(s.ID)) } }
|
||||
.where(cs.COLLECTION_ID.eq(collectionId))
|
||||
.apply { filterOnLibraryIds?.let { and(s.LIBRARY_ID.`in`(it)) } }
|
||||
.orderBy(sd.AGE_RATING)
|
||||
.fetchSet(sd.AGE_RATING)
|
||||
|
||||
override fun findAllSeriesReleaseDates(): Set<LocalDate> =
|
||||
override fun findAllSeriesReleaseDates(filterOnLibraryIds: Collection<String>?): Set<LocalDate> =
|
||||
dsl.selectDistinct(bma.RELEASE_DATE)
|
||||
.from(bma)
|
||||
.apply { filterOnLibraryIds?.let { leftJoin(s).on(bma.SERIES_ID.eq(s.ID)) } }
|
||||
.where(bma.RELEASE_DATE.isNotNull)
|
||||
.apply { filterOnLibraryIds?.let { and(s.LIBRARY_ID.`in`(it)) } }
|
||||
.orderBy(bma.RELEASE_DATE.desc())
|
||||
.fetchSet(bma.RELEASE_DATE)
|
||||
|
||||
override fun findAllSeriesReleaseDatesByLibrary(libraryId: String): Set<LocalDate> =
|
||||
override fun findAllSeriesReleaseDatesByLibrary(libraryId: String, filterOnLibraryIds: Collection<String>?): Set<LocalDate> =
|
||||
dsl.selectDistinct(bma.RELEASE_DATE)
|
||||
.from(bma)
|
||||
.leftJoin(s).on(bma.SERIES_ID.eq(s.ID))
|
||||
.where(s.LIBRARY_ID.eq(libraryId))
|
||||
.and(bma.RELEASE_DATE.isNotNull)
|
||||
.apply { filterOnLibraryIds?.let { and(s.LIBRARY_ID.`in`(it)) } }
|
||||
.orderBy(bma.RELEASE_DATE.desc())
|
||||
.fetchSet(bma.RELEASE_DATE)
|
||||
|
||||
override fun findAllSeriesReleaseDatesByCollection(collectionId: String): Set<LocalDate> =
|
||||
override fun findAllSeriesReleaseDatesByCollection(collectionId: String, filterOnLibraryIds: Collection<String>?): Set<LocalDate> =
|
||||
dsl.selectDistinct(bma.RELEASE_DATE)
|
||||
.from(bma)
|
||||
.leftJoin(cs).on(bma.SERIES_ID.eq(cs.SERIES_ID))
|
||||
.apply { filterOnLibraryIds?.let { leftJoin(s).on(bma.SERIES_ID.eq(s.ID)) } }
|
||||
.where(cs.COLLECTION_ID.eq(collectionId))
|
||||
.and(bma.RELEASE_DATE.isNotNull)
|
||||
.apply { filterOnLibraryIds?.let { and(s.LIBRARY_ID.`in`(it)) } }
|
||||
.orderBy(bma.RELEASE_DATE.desc())
|
||||
.fetchSet(bma.RELEASE_DATE)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
package org.gotson.komga.infrastructure.metadata
|
||||
|
||||
import org.gotson.komga.domain.model.Book
|
||||
import org.gotson.komga.domain.model.BookMetadataPatch
|
||||
import org.gotson.komga.domain.model.BookMetadataPatchCapability
|
||||
import org.gotson.komga.domain.model.Media
|
||||
import org.gotson.komga.domain.model.BookWithMedia
|
||||
|
||||
interface BookMetadataProvider {
|
||||
fun getCapabilities(): List<BookMetadataPatchCapability>
|
||||
fun getBookMetadataFromBook(book: Book, media: Media): BookMetadataPatch?
|
||||
fun getBookMetadataFromBook(book: BookWithMedia): BookMetadataPatch?
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
package org.gotson.komga.infrastructure.metadata
|
||||
|
||||
import org.gotson.komga.domain.model.Book
|
||||
import org.gotson.komga.domain.model.Media
|
||||
import org.gotson.komga.domain.model.BookWithMedia
|
||||
import org.gotson.komga.domain.model.SeriesMetadataPatch
|
||||
|
||||
interface SeriesMetadataProvider {
|
||||
fun getSeriesMetadataFromBook(book: Book, media: Media): SeriesMetadataPatch?
|
||||
fun getSeriesMetadataFromBook(book: BookWithMedia): SeriesMetadataPatch?
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,10 +8,9 @@ import com.google.zxing.RGBLuminanceSource
|
|||
import com.google.zxing.common.HybridBinarizer
|
||||
import mu.KotlinLogging
|
||||
import org.apache.commons.validator.routines.ISBNValidator
|
||||
import org.gotson.komga.domain.model.Book
|
||||
import org.gotson.komga.domain.model.BookMetadataPatch
|
||||
import org.gotson.komga.domain.model.BookMetadataPatchCapability
|
||||
import org.gotson.komga.domain.model.Media
|
||||
import org.gotson.komga.domain.model.BookWithMedia
|
||||
import org.gotson.komga.domain.service.BookAnalyzer
|
||||
import org.gotson.komga.infrastructure.metadata.BookMetadataProvider
|
||||
import org.springframework.stereotype.Service
|
||||
|
|
@ -37,8 +36,8 @@ class IsbnBarcodeProvider(
|
|||
override fun getCapabilities(): List<BookMetadataPatchCapability> =
|
||||
listOf(BookMetadataPatchCapability.ISBN)
|
||||
|
||||
override fun getBookMetadataFromBook(book: Book, media: Media): BookMetadataPatch? {
|
||||
val pagesToTry = (1..media.pages.size).toList().let {
|
||||
override fun getBookMetadataFromBook(book: BookWithMedia): BookMetadataPatch? {
|
||||
val pagesToTry = (1..book.media.pages.size).toList().let {
|
||||
(it.takeLast(PAGES_LAST).reversed() + it.take(PAGES_FIRST)).distinct()
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,10 +3,9 @@ package org.gotson.komga.infrastructure.metadata.comicrack
|
|||
import com.fasterxml.jackson.dataformat.xml.XmlMapper
|
||||
import mu.KotlinLogging
|
||||
import org.gotson.komga.domain.model.Author
|
||||
import org.gotson.komga.domain.model.Book
|
||||
import org.gotson.komga.domain.model.BookMetadataPatch
|
||||
import org.gotson.komga.domain.model.BookMetadataPatchCapability
|
||||
import org.gotson.komga.domain.model.Media
|
||||
import org.gotson.komga.domain.model.BookWithMedia
|
||||
import org.gotson.komga.domain.model.SeriesMetadata
|
||||
import org.gotson.komga.domain.model.SeriesMetadataPatch
|
||||
import org.gotson.komga.domain.service.BookAnalyzer
|
||||
|
|
@ -40,8 +39,8 @@ class ComicInfoProvider(
|
|||
BookMetadataPatchCapability.READ_LISTS,
|
||||
)
|
||||
|
||||
override fun getBookMetadataFromBook(book: Book, media: Media): BookMetadataPatch? {
|
||||
getComicInfo(book, media)?.let { comicInfo ->
|
||||
override fun getBookMetadataFromBook(book: BookWithMedia): BookMetadataPatch? {
|
||||
getComicInfo(book)?.let { comicInfo ->
|
||||
val releaseDate = comicInfo.year?.let {
|
||||
LocalDate.of(comicInfo.year!!, comicInfo.month ?: 1, comicInfo.day ?: 1)
|
||||
}
|
||||
|
|
@ -83,8 +82,8 @@ class ComicInfoProvider(
|
|||
return null
|
||||
}
|
||||
|
||||
override fun getSeriesMetadataFromBook(book: Book, media: Media): SeriesMetadataPatch? {
|
||||
getComicInfo(book, media)?.let { comicInfo ->
|
||||
override fun getSeriesMetadataFromBook(book: BookWithMedia): SeriesMetadataPatch? {
|
||||
getComicInfo(book)?.let { comicInfo ->
|
||||
val readingDirection = when (comicInfo.manga) {
|
||||
Manga.NO -> SeriesMetadata.ReadingDirection.LEFT_TO_RIGHT
|
||||
Manga.YES_AND_RIGHT_TO_LEFT -> SeriesMetadata.ReadingDirection.RIGHT_TO_LEFT
|
||||
|
|
@ -110,9 +109,9 @@ class ComicInfoProvider(
|
|||
return null
|
||||
}
|
||||
|
||||
private fun getComicInfo(book: Book, media: Media): ComicInfo? {
|
||||
private fun getComicInfo(book: BookWithMedia): ComicInfo? {
|
||||
try {
|
||||
if (media.files.none { it == COMIC_INFO }) {
|
||||
if (book.media.files.none { it == COMIC_INFO }) {
|
||||
logger.debug { "Book does not contain any $COMIC_INFO file: $book" }
|
||||
return null
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ class ReadListProvider(
|
|||
val books = readingList.books.mapNotNull {
|
||||
val series = computeSeriesFromSeriesAndVolume(it.series, it.volume)
|
||||
if (!series.isNullOrBlank() && it.number != null)
|
||||
ReadListRequestBook(series, it.number!!)
|
||||
ReadListRequestBook(series, it.number!!.trim())
|
||||
else {
|
||||
logger.warn { "Book is missing series or number, skipping: $it" }
|
||||
null
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ class Book {
|
|||
var series: String? = null
|
||||
|
||||
@JsonProperty(value = "Number")
|
||||
var number: Int? = null
|
||||
var number: String? = null
|
||||
|
||||
@JsonProperty(value = "Volume")
|
||||
var volume: Int? = null
|
||||
|
|
|
|||
|
|
@ -2,10 +2,9 @@ package org.gotson.komga.infrastructure.metadata.epub
|
|||
|
||||
import org.apache.commons.validator.routines.ISBNValidator
|
||||
import org.gotson.komga.domain.model.Author
|
||||
import org.gotson.komga.domain.model.Book
|
||||
import org.gotson.komga.domain.model.BookMetadataPatch
|
||||
import org.gotson.komga.domain.model.BookMetadataPatchCapability
|
||||
import org.gotson.komga.domain.model.Media
|
||||
import org.gotson.komga.domain.model.BookWithMedia
|
||||
import org.gotson.komga.domain.model.SeriesMetadata
|
||||
import org.gotson.komga.domain.model.SeriesMetadataPatch
|
||||
import org.gotson.komga.infrastructure.mediacontainer.EpubExtractor
|
||||
|
|
@ -41,9 +40,9 @@ class EpubMetadataProvider(
|
|||
BookMetadataPatchCapability.ISBN,
|
||||
)
|
||||
|
||||
override fun getBookMetadataFromBook(book: Book, media: Media): BookMetadataPatch? {
|
||||
if (media.mediaType != "application/epub+zip") return null
|
||||
epubExtractor.getPackageFile(book.path())?.let { packageFile ->
|
||||
override fun getBookMetadataFromBook(book: BookWithMedia): BookMetadataPatch? {
|
||||
if (book.media.mediaType != "application/epub+zip") return null
|
||||
epubExtractor.getPackageFile(book.book.path())?.let { packageFile ->
|
||||
val opf = Jsoup.parse(packageFile)
|
||||
|
||||
val title = opf.selectFirst("metadata > dc|title")?.text()?.ifBlank { null }
|
||||
|
|
@ -80,9 +79,9 @@ class EpubMetadataProvider(
|
|||
return null
|
||||
}
|
||||
|
||||
override fun getSeriesMetadataFromBook(book: Book, media: Media): SeriesMetadataPatch? {
|
||||
if (media.mediaType != "application/epub+zip") return null
|
||||
epubExtractor.getPackageFile(book.path())?.let { packageFile ->
|
||||
override fun getSeriesMetadataFromBook(book: BookWithMedia): SeriesMetadataPatch? {
|
||||
if (book.media.mediaType != "application/epub+zip") return null
|
||||
epubExtractor.getPackageFile(book.book.path())?.let { packageFile ->
|
||||
val opf = Jsoup.parse(packageFile)
|
||||
|
||||
val series = opf.selectFirst("metadata > meta[property=belongs-to-collection]")?.text()?.ifBlank { null }
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package org.gotson.komga.infrastructure.web
|
||||
|
||||
import org.springframework.http.CacheControl
|
||||
import org.springframework.http.MediaType
|
||||
import org.springframework.http.ResponseEntity
|
||||
import java.net.URL
|
||||
import java.nio.file.Paths
|
||||
|
|
@ -20,3 +21,13 @@ val cachePrivate = CacheControl
|
|||
.noTransform()
|
||||
.cachePrivate()
|
||||
.mustRevalidate()
|
||||
|
||||
fun getMediaTypeOrDefault(mediaTypeString: String?): MediaType {
|
||||
mediaTypeString?.let {
|
||||
try {
|
||||
return MediaType.parseMediaType(mediaTypeString)
|
||||
} catch (ex: Exception) {
|
||||
}
|
||||
}
|
||||
return MediaType.APPLICATION_OCTET_STREAM
|
||||
}
|
||||
|
|
|
|||
|
|
@ -338,9 +338,7 @@ class OpdsController(
|
|||
fun getPublishers(
|
||||
@AuthenticationPrincipal principal: KomgaPrincipal
|
||||
): OpdsFeed {
|
||||
val publishers =
|
||||
if (principal.user.sharedAllLibraries) referentialRepository.findAllPublishers()
|
||||
else referentialRepository.findAllPublishersByLibraries(principal.user.sharedLibrariesIds)
|
||||
val publishers = referentialRepository.findAllPublishers(principal.user.getAuthorizedLibraryIds(null))
|
||||
|
||||
return OpdsFeedNavigation(
|
||||
id = ID_PUBLISHERS_ALL,
|
||||
|
|
|
|||
|
|
@ -28,8 +28,10 @@ import org.gotson.komga.infrastructure.mediacontainer.ContentDetector
|
|||
import org.gotson.komga.infrastructure.security.KomgaPrincipal
|
||||
import org.gotson.komga.infrastructure.swagger.PageableAsQueryParam
|
||||
import org.gotson.komga.infrastructure.swagger.PageableWithoutSortAsQueryParam
|
||||
import org.gotson.komga.infrastructure.web.getMediaTypeOrDefault
|
||||
import org.gotson.komga.infrastructure.web.setCachePrivate
|
||||
import org.gotson.komga.interfaces.rest.dto.BookDto
|
||||
import org.gotson.komga.interfaces.rest.dto.BookImportBatchDto
|
||||
import org.gotson.komga.interfaces.rest.dto.BookMetadataUpdateDto
|
||||
import org.gotson.komga.interfaces.rest.dto.PageDto
|
||||
import org.gotson.komga.interfaces.rest.dto.ReadListDto
|
||||
|
|
@ -502,19 +504,30 @@ class BookController(
|
|||
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||
}
|
||||
|
||||
@PostMapping("api/v1/books/import")
|
||||
@PreAuthorize("hasRole('$ROLE_ADMIN')")
|
||||
@ResponseStatus(HttpStatus.ACCEPTED)
|
||||
fun importBooks(
|
||||
@RequestBody bookImportBatch: BookImportBatchDto,
|
||||
) {
|
||||
bookImportBatch.books.forEach {
|
||||
try {
|
||||
taskReceiver.importBook(
|
||||
sourceFile = it.sourceFile,
|
||||
seriesId = it.seriesId,
|
||||
copyMode = bookImportBatch.copyMode,
|
||||
destinationName = it.destinationName,
|
||||
upgradeBookId = it.upgradeBookId,
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
logger.error(e) { "Error while creating import task for: $it" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun ResponseEntity.BodyBuilder.setNotModified(media: Media) =
|
||||
this.setCachePrivate().lastModified(getBookLastModified(media))
|
||||
|
||||
private fun getBookLastModified(media: Media) =
|
||||
media.lastModifiedDate.toInstant(ZoneOffset.UTC).toEpochMilli()
|
||||
|
||||
private fun getMediaTypeOrDefault(mediaTypeString: String?): MediaType {
|
||||
mediaTypeString?.let {
|
||||
try {
|
||||
return MediaType.parseMediaType(mediaTypeString)
|
||||
} catch (ex: Exception) {
|
||||
}
|
||||
}
|
||||
return MediaType.APPLICATION_OCTET_STREAM
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
package org.gotson.komga.interfaces.rest
|
||||
|
||||
import org.gotson.komga.domain.persistence.ReferentialRepository
|
||||
import org.gotson.komga.infrastructure.security.KomgaPrincipal
|
||||
import org.gotson.komga.interfaces.rest.dto.AuthorDto
|
||||
import org.gotson.komga.interfaces.rest.dto.toDto
|
||||
import org.springframework.http.MediaType
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.RequestMapping
|
||||
import org.springframework.web.bind.annotation.RequestParam
|
||||
|
|
@ -17,6 +19,7 @@ class ReferentialController(
|
|||
|
||||
@GetMapping("/authors")
|
||||
fun getAuthors(
|
||||
@AuthenticationPrincipal principal: KomgaPrincipal,
|
||||
@RequestParam(name = "search", defaultValue = "") search: String,
|
||||
@RequestParam(name = "library_id", required = false) libraryId: String?,
|
||||
@RequestParam(name = "collection_id", required = false) collectionId: String?,
|
||||
|
|
@ -24,87 +27,119 @@ class ReferentialController(
|
|||
): List<AuthorDto> =
|
||||
|
||||
when {
|
||||
libraryId != null -> referentialRepository.findAuthorsByNameAndLibrary(search, libraryId)
|
||||
collectionId != null -> referentialRepository.findAuthorsByNameAndCollection(search, collectionId)
|
||||
seriesId != null -> referentialRepository.findAuthorsByNameAndSeries(search, seriesId)
|
||||
else -> referentialRepository.findAuthorsByName(search)
|
||||
libraryId != null -> referentialRepository.findAuthorsByNameAndLibrary(search, libraryId, principal.user.getAuthorizedLibraryIds(null))
|
||||
collectionId != null -> referentialRepository.findAuthorsByNameAndCollection(search, collectionId, principal.user.getAuthorizedLibraryIds(null))
|
||||
seriesId != null -> referentialRepository.findAuthorsByNameAndSeries(search, seriesId, principal.user.getAuthorizedLibraryIds(null))
|
||||
else -> referentialRepository.findAuthorsByName(search, principal.user.getAuthorizedLibraryIds(null))
|
||||
}.map { it.toDto() }
|
||||
|
||||
@GetMapping("/authors/names")
|
||||
fun getAuthorsNames(
|
||||
@AuthenticationPrincipal principal: KomgaPrincipal,
|
||||
@RequestParam(name = "search", defaultValue = "") search: String
|
||||
): List<String> =
|
||||
referentialRepository.findAuthorsNamesByName(search)
|
||||
referentialRepository.findAuthorsNamesByName(search, principal.user.getAuthorizedLibraryIds(null))
|
||||
|
||||
@GetMapping("/authors/roles")
|
||||
fun getAuthorsRoles(): List<String> =
|
||||
referentialRepository.findAuthorsRoles()
|
||||
fun getAuthorsRoles(
|
||||
@AuthenticationPrincipal principal: KomgaPrincipal,
|
||||
): List<String> =
|
||||
referentialRepository.findAuthorsRoles(principal.user.getAuthorizedLibraryIds(null))
|
||||
|
||||
@GetMapping("/genres")
|
||||
fun getGenres(
|
||||
@AuthenticationPrincipal principal: KomgaPrincipal,
|
||||
@RequestParam(name = "library_id", required = false) libraryId: String?,
|
||||
@RequestParam(name = "collection_id", required = false) collectionId: String?
|
||||
): Set<String> =
|
||||
when {
|
||||
libraryId != null -> referentialRepository.findAllGenresByLibrary(libraryId)
|
||||
collectionId != null -> referentialRepository.findAllGenresByCollection(collectionId)
|
||||
else -> referentialRepository.findAllGenres()
|
||||
libraryId != null -> referentialRepository.findAllGenresByLibrary(libraryId, principal.user.getAuthorizedLibraryIds(null))
|
||||
collectionId != null -> referentialRepository.findAllGenresByCollection(collectionId, principal.user.getAuthorizedLibraryIds(null))
|
||||
else -> referentialRepository.findAllGenres(principal.user.getAuthorizedLibraryIds(null))
|
||||
}
|
||||
|
||||
@GetMapping("/tags")
|
||||
fun getTags(
|
||||
@AuthenticationPrincipal principal: KomgaPrincipal,
|
||||
// TODO: remove those parameters once Tachiyomi Extension is using the new /tags/series endpoint
|
||||
@RequestParam(name = "library_id", required = false) libraryId: String?,
|
||||
@RequestParam(name = "series_id", required = false) seriesId: String?,
|
||||
@RequestParam(name = "collection_id", required = false) collectionId: String?
|
||||
): Set<String> =
|
||||
when {
|
||||
libraryId != null -> referentialRepository.findAllTagsByLibrary(libraryId)
|
||||
seriesId != null -> referentialRepository.findAllTagsBySeries(seriesId)
|
||||
collectionId != null -> referentialRepository.findAllTagsByCollection(collectionId)
|
||||
else -> referentialRepository.findAllTags()
|
||||
libraryId != null -> referentialRepository.findAllSeriesTagsByLibrary(libraryId, principal.user.getAuthorizedLibraryIds(null))
|
||||
seriesId != null -> referentialRepository.findAllBookTagsBySeries(seriesId, principal.user.getAuthorizedLibraryIds(null))
|
||||
collectionId != null -> referentialRepository.findAllSeriesTagsByCollection(collectionId, principal.user.getAuthorizedLibraryIds(null))
|
||||
else -> referentialRepository.findAllSeriesAndBookTags(principal.user.getAuthorizedLibraryIds(null))
|
||||
}
|
||||
|
||||
@GetMapping("/tags/book")
|
||||
fun getBookTags(
|
||||
@AuthenticationPrincipal principal: KomgaPrincipal,
|
||||
@RequestParam(name = "series_id", required = false) seriesId: String?,
|
||||
): Set<String> =
|
||||
when {
|
||||
seriesId != null -> referentialRepository.findAllBookTagsBySeries(seriesId, principal.user.getAuthorizedLibraryIds(null))
|
||||
else -> referentialRepository.findAllBookTags(principal.user.getAuthorizedLibraryIds(null))
|
||||
}
|
||||
|
||||
@GetMapping("/tags/series")
|
||||
fun getSeriesTags(
|
||||
@AuthenticationPrincipal principal: KomgaPrincipal,
|
||||
@RequestParam(name = "library_id", required = false) libraryId: String?,
|
||||
@RequestParam(name = "collection_id", required = false) collectionId: String?
|
||||
): Set<String> =
|
||||
when {
|
||||
libraryId != null -> referentialRepository.findAllSeriesTagsByLibrary(libraryId, principal.user.getAuthorizedLibraryIds(null))
|
||||
collectionId != null -> referentialRepository.findAllSeriesTagsByCollection(collectionId, principal.user.getAuthorizedLibraryIds(null))
|
||||
else -> referentialRepository.findAllSeriesTags(principal.user.getAuthorizedLibraryIds(null))
|
||||
}
|
||||
|
||||
@GetMapping("/languages")
|
||||
fun getLanguages(
|
||||
@AuthenticationPrincipal principal: KomgaPrincipal,
|
||||
@RequestParam(name = "library_id", required = false) libraryId: String?,
|
||||
@RequestParam(name = "collection_id", required = false) collectionId: String?
|
||||
): Set<String> =
|
||||
when {
|
||||
libraryId != null -> referentialRepository.findAllLanguagesByLibrary(libraryId)
|
||||
collectionId != null -> referentialRepository.findAllLanguagesByCollection(collectionId)
|
||||
else -> referentialRepository.findAllLanguages()
|
||||
libraryId != null -> referentialRepository.findAllLanguagesByLibrary(libraryId, principal.user.getAuthorizedLibraryIds(null))
|
||||
collectionId != null -> referentialRepository.findAllLanguagesByCollection(collectionId, principal.user.getAuthorizedLibraryIds(null))
|
||||
else -> referentialRepository.findAllLanguages(principal.user.getAuthorizedLibraryIds(null))
|
||||
}
|
||||
|
||||
@GetMapping("/publishers")
|
||||
fun getPublishers(
|
||||
@AuthenticationPrincipal principal: KomgaPrincipal,
|
||||
@RequestParam(name = "library_id", required = false) libraryId: String?,
|
||||
@RequestParam(name = "collection_id", required = false) collectionId: String?
|
||||
): Set<String> =
|
||||
when {
|
||||
libraryId != null -> referentialRepository.findAllPublishersByLibrary(libraryId)
|
||||
collectionId != null -> referentialRepository.findAllPublishersByCollection(collectionId)
|
||||
else -> referentialRepository.findAllPublishers()
|
||||
libraryId != null -> referentialRepository.findAllPublishersByLibrary(libraryId, principal.user.getAuthorizedLibraryIds(null))
|
||||
collectionId != null -> referentialRepository.findAllPublishersByCollection(collectionId, principal.user.getAuthorizedLibraryIds(null))
|
||||
else -> referentialRepository.findAllPublishers(principal.user.getAuthorizedLibraryIds(null))
|
||||
}
|
||||
|
||||
@GetMapping("/age-ratings")
|
||||
fun getAgeRatings(
|
||||
@AuthenticationPrincipal principal: KomgaPrincipal,
|
||||
@RequestParam(name = "library_id", required = false) libraryId: String?,
|
||||
@RequestParam(name = "collection_id", required = false) collectionId: String?
|
||||
): Set<String> =
|
||||
when {
|
||||
libraryId != null -> referentialRepository.findAllAgeRatingsByLibrary(libraryId)
|
||||
collectionId != null -> referentialRepository.findAllAgeRatingsByCollection(collectionId)
|
||||
else -> referentialRepository.findAllAgeRatings()
|
||||
libraryId != null -> referentialRepository.findAllAgeRatingsByLibrary(libraryId, principal.user.getAuthorizedLibraryIds(null))
|
||||
collectionId != null -> referentialRepository.findAllAgeRatingsByCollection(collectionId, principal.user.getAuthorizedLibraryIds(null))
|
||||
else -> referentialRepository.findAllAgeRatings(principal.user.getAuthorizedLibraryIds(null))
|
||||
}.map { it?.toString() ?: "None" }.toSet()
|
||||
|
||||
@GetMapping("/series/release-dates")
|
||||
fun getSeriesReleaseDates(
|
||||
@AuthenticationPrincipal principal: KomgaPrincipal,
|
||||
@RequestParam(name = "library_id", required = false) libraryId: String?,
|
||||
@RequestParam(name = "collection_id", required = false) collectionId: String?
|
||||
): Set<String> =
|
||||
when {
|
||||
libraryId != null -> referentialRepository.findAllSeriesReleaseDatesByLibrary(libraryId)
|
||||
collectionId != null -> referentialRepository.findAllSeriesReleaseDatesByCollection(collectionId)
|
||||
else -> referentialRepository.findAllSeriesReleaseDates()
|
||||
libraryId != null -> referentialRepository.findAllSeriesReleaseDatesByLibrary(libraryId, principal.user.getAuthorizedLibraryIds(null))
|
||||
collectionId != null -> referentialRepository.findAllSeriesReleaseDatesByCollection(collectionId, principal.user.getAuthorizedLibraryIds(null))
|
||||
else -> referentialRepository.findAllSeriesReleaseDates(principal.user.getAuthorizedLibraryIds(null))
|
||||
}.map { it.year.toString() }.toSet()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,121 @@
|
|||
package org.gotson.komga.interfaces.rest
|
||||
|
||||
import com.jakewharton.byteunits.BinaryByteUnit
|
||||
import mu.KotlinLogging
|
||||
import org.gotson.komga.domain.model.BookWithMedia
|
||||
import org.gotson.komga.domain.model.CodedException
|
||||
import org.gotson.komga.domain.model.MediaNotReadyException
|
||||
import org.gotson.komga.domain.model.ROLE_ADMIN
|
||||
import org.gotson.komga.domain.persistence.TransientBookRepository
|
||||
import org.gotson.komga.domain.service.TransientBookLifecycle
|
||||
import org.gotson.komga.infrastructure.web.getMediaTypeOrDefault
|
||||
import org.gotson.komga.infrastructure.web.toFilePath
|
||||
import org.gotson.komga.interfaces.rest.dto.PageDto
|
||||
import org.springframework.http.HttpStatus
|
||||
import org.springframework.http.MediaType
|
||||
import org.springframework.http.ResponseEntity
|
||||
import org.springframework.security.access.prepost.PreAuthorize
|
||||
import org.springframework.web.bind.annotation.GetMapping
|
||||
import org.springframework.web.bind.annotation.PathVariable
|
||||
import org.springframework.web.bind.annotation.PostMapping
|
||||
import org.springframework.web.bind.annotation.RequestBody
|
||||
import org.springframework.web.bind.annotation.RequestMapping
|
||||
import org.springframework.web.bind.annotation.RestController
|
||||
import org.springframework.web.server.ResponseStatusException
|
||||
import java.nio.file.NoSuchFileException
|
||||
import java.time.LocalDateTime
|
||||
|
||||
private val logger = KotlinLogging.logger {}
|
||||
|
||||
@RestController
|
||||
@RequestMapping("api/v1/transient-books", produces = [MediaType.APPLICATION_JSON_VALUE])
|
||||
@PreAuthorize("hasRole('$ROLE_ADMIN')")
|
||||
class TransientBooksController(
|
||||
private val transientBookLifecycle: TransientBookLifecycle,
|
||||
private val transientBookRepository: TransientBookRepository,
|
||||
) {
|
||||
|
||||
@PostMapping
|
||||
fun scanForTransientBooks(
|
||||
@RequestBody request: ScanRequestDto
|
||||
): List<TransientBookDto> =
|
||||
try {
|
||||
transientBookLifecycle.scanAndPersist(request.path)
|
||||
.sortedBy { it.book.path() }
|
||||
.map { it.toDto() }
|
||||
} catch (e: CodedException) {
|
||||
throw ResponseStatusException(HttpStatus.BAD_REQUEST, e.code)
|
||||
}
|
||||
|
||||
@PostMapping("{id}/analyze")
|
||||
fun analyze(
|
||||
@PathVariable id: String,
|
||||
): TransientBookDto = transientBookRepository.findById(id)?.let {
|
||||
transientBookLifecycle.analyzeAndPersist(it).toDto()
|
||||
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||
|
||||
@GetMapping(
|
||||
value = ["{id}/pages/{pageNumber}"],
|
||||
produces = [MediaType.ALL_VALUE]
|
||||
)
|
||||
fun getSourcePage(
|
||||
@PathVariable id: String,
|
||||
@PathVariable pageNumber: Int,
|
||||
): ResponseEntity<ByteArray> =
|
||||
transientBookRepository.findById(id)?.let {
|
||||
try {
|
||||
val pageContent = transientBookLifecycle.getBookPage(it, pageNumber)
|
||||
|
||||
ResponseEntity.ok()
|
||||
.contentType(getMediaTypeOrDefault(pageContent.mediaType))
|
||||
.body(pageContent.content)
|
||||
} catch (ex: IndexOutOfBoundsException) {
|
||||
throw ResponseStatusException(HttpStatus.BAD_REQUEST, "Page number does not exist")
|
||||
} catch (ex: MediaNotReadyException) {
|
||||
throw ResponseStatusException(HttpStatus.NOT_FOUND, "Book analysis failed")
|
||||
} catch (ex: NoSuchFileException) {
|
||||
logger.warn(ex) { "File not found}" }
|
||||
throw ResponseStatusException(HttpStatus.NOT_FOUND, "File not found, it may have moved")
|
||||
}
|
||||
} ?: throw ResponseStatusException(HttpStatus.NOT_FOUND)
|
||||
}
|
||||
|
||||
private fun BookWithMedia.toDto() =
|
||||
TransientBookDto(
|
||||
id = book.id,
|
||||
name = book.name,
|
||||
url = book.url.toFilePath(),
|
||||
fileLastModified = book.fileLastModified,
|
||||
sizeBytes = book.fileSize,
|
||||
status = media.status.toString(),
|
||||
mediaType = media.mediaType ?: "",
|
||||
pages = media.pages.mapIndexed { index, bookPage ->
|
||||
PageDto(
|
||||
number = index + 1,
|
||||
fileName = bookPage.fileName,
|
||||
mediaType = bookPage.mediaType,
|
||||
width = bookPage.dimension?.width,
|
||||
height = bookPage.dimension?.height,
|
||||
)
|
||||
},
|
||||
files = media.files,
|
||||
comment = media.comment ?: "",
|
||||
)
|
||||
|
||||
data class ScanRequestDto(
|
||||
val path: String,
|
||||
)
|
||||
|
||||
data class TransientBookDto(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val url: String,
|
||||
val fileLastModified: LocalDateTime,
|
||||
val sizeBytes: Long,
|
||||
val size: String = BinaryByteUnit.format(sizeBytes),
|
||||
val status: String,
|
||||
val mediaType: String,
|
||||
val pages: List<PageDto>,
|
||||
val files: List<String>,
|
||||
val comment: String,
|
||||
)
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package org.gotson.komga.interfaces.rest.dto
|
||||
|
||||
import org.gotson.komga.domain.model.CopyMode
|
||||
|
||||
data class BookImportBatchDto(
|
||||
val books: List<BookImportDto> = emptyList(),
|
||||
val copyMode: CopyMode,
|
||||
)
|
||||
|
||||
data class BookImportDto(
|
||||
val sourceFile: String,
|
||||
val seriesId: String,
|
||||
val upgradeBookId: String? = null,
|
||||
val destinationName: String? = null,
|
||||
)
|
||||
|
|
@ -6,7 +6,7 @@ import org.gotson.komga.domain.model.ReadListRequestResultBook
|
|||
|
||||
data class ReadListRequestBookDto(
|
||||
val series: String,
|
||||
val number: Int,
|
||||
val number: String,
|
||||
)
|
||||
|
||||
data class ReadListRequestResultDto(
|
||||
|
|
|
|||
|
|
@ -4,29 +4,33 @@ import com.github.f4b6a3.tsid.TsidCreator
|
|||
import java.net.URL
|
||||
import java.time.LocalDateTime
|
||||
|
||||
fun makeBook(name: String, fileLastModified: LocalDateTime = LocalDateTime.now(), libraryId: String = "", seriesId: String = ""): Book {
|
||||
fun makeBook(name: String, fileLastModified: LocalDateTime = LocalDateTime.now(), libraryId: String = "", seriesId: String = "", url: URL? = null): Book {
|
||||
Thread.sleep(5)
|
||||
return Book(
|
||||
name = name,
|
||||
url = URL("file:/$name"),
|
||||
url = url ?: URL("file:/$name"),
|
||||
fileLastModified = fileLastModified,
|
||||
libraryId = libraryId,
|
||||
seriesId = seriesId
|
||||
)
|
||||
}
|
||||
|
||||
fun makeSeries(name: String, libraryId: String = ""): Series {
|
||||
fun makeSeries(name: String, libraryId: String = "", url: URL? = null): Series {
|
||||
Thread.sleep(5)
|
||||
return Series(
|
||||
name = name,
|
||||
url = URL("file:/$name"),
|
||||
url = url ?: URL("file:/$name"),
|
||||
fileLastModified = LocalDateTime.now(),
|
||||
libraryId = libraryId
|
||||
)
|
||||
}
|
||||
|
||||
fun makeLibrary(name: String = "default", url: String = "file:/$name", id: String = TsidCreator.getTsid256().toString()): Library {
|
||||
return Library(name, URL(url), id = id)
|
||||
fun makeLibrary(name: String = "default", path: String = "file:/$name", id: String = TsidCreator.getTsid256().toString(), url: URL? = null): Library {
|
||||
return Library(
|
||||
name = name,
|
||||
root = url ?: URL(path),
|
||||
id = id
|
||||
)
|
||||
}
|
||||
|
||||
fun makeBookPage(name: String) =
|
||||
|
|
|
|||
|
|
@ -0,0 +1,456 @@
|
|||
package org.gotson.komga.domain.service
|
||||
|
||||
import com.google.common.jimfs.Configuration
|
||||
import com.google.common.jimfs.Jimfs
|
||||
import com.ninjasquad.springmockk.MockkBean
|
||||
import io.mockk.Runs
|
||||
import io.mockk.every
|
||||
import io.mockk.just
|
||||
import io.mockk.verify
|
||||
import org.assertj.core.api.Assertions
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.gotson.komga.application.tasks.TaskReceiver
|
||||
import org.gotson.komga.domain.model.Book
|
||||
import org.gotson.komga.domain.model.BookPage
|
||||
import org.gotson.komga.domain.model.CopyMode
|
||||
import org.gotson.komga.domain.model.KomgaUser
|
||||
import org.gotson.komga.domain.model.Media
|
||||
import org.gotson.komga.domain.model.PathContainedInPath
|
||||
import org.gotson.komga.domain.model.ReadList
|
||||
import org.gotson.komga.domain.model.makeBook
|
||||
import org.gotson.komga.domain.model.makeLibrary
|
||||
import org.gotson.komga.domain.model.makeSeries
|
||||
import org.gotson.komga.domain.persistence.BookMetadataRepository
|
||||
import org.gotson.komga.domain.persistence.BookRepository
|
||||
import org.gotson.komga.domain.persistence.KomgaUserRepository
|
||||
import org.gotson.komga.domain.persistence.LibraryRepository
|
||||
import org.gotson.komga.domain.persistence.MediaRepository
|
||||
import org.gotson.komga.domain.persistence.ReadListRepository
|
||||
import org.gotson.komga.domain.persistence.ReadProgressRepository
|
||||
import org.gotson.komga.domain.persistence.SeriesRepository
|
||||
import org.gotson.komga.infrastructure.language.toIndexedMap
|
||||
import org.junit.jupiter.api.AfterAll
|
||||
import org.junit.jupiter.api.AfterEach
|
||||
import org.junit.jupiter.api.BeforeAll
|
||||
import org.junit.jupiter.api.BeforeEach
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.extension.ExtendWith
|
||||
import org.springframework.beans.factory.annotation.Autowired
|
||||
import org.springframework.boot.test.context.SpringBootTest
|
||||
import org.springframework.test.context.junit.jupiter.SpringExtension
|
||||
import java.io.FileNotFoundException
|
||||
import java.nio.file.FileAlreadyExistsException
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Paths
|
||||
import kotlin.io.path.createDirectories
|
||||
import kotlin.io.path.createDirectory
|
||||
import kotlin.io.path.createFile
|
||||
|
||||
@ExtendWith(SpringExtension::class)
|
||||
@SpringBootTest
|
||||
class BookImporterTest(
|
||||
@Autowired private val bookImporter: BookImporter,
|
||||
@Autowired private val bookRepository: BookRepository,
|
||||
@Autowired private val bookLifecycle: BookLifecycle,
|
||||
@Autowired private val readProgressRepository: ReadProgressRepository,
|
||||
@Autowired private val libraryRepository: LibraryRepository,
|
||||
@Autowired private val seriesRepository: SeriesRepository,
|
||||
@Autowired private val seriesLifecycle: SeriesLifecycle,
|
||||
@Autowired private val mediaRepository: MediaRepository,
|
||||
@Autowired private val metadataRepository: BookMetadataRepository,
|
||||
@Autowired private val userRepository: KomgaUserRepository,
|
||||
@Autowired private val readListRepository: ReadListRepository,
|
||||
@Autowired private val readListLifecycle: ReadListLifecycle,
|
||||
) {
|
||||
|
||||
@MockkBean
|
||||
private lateinit var mockTaskReceiver: TaskReceiver
|
||||
|
||||
private val library = makeLibrary("lib", "file:/library")
|
||||
private val user1 = KomgaUser("user1@example.org", "", false)
|
||||
private val user2 = KomgaUser("user2@example.org", "", false)
|
||||
|
||||
@BeforeAll
|
||||
fun init() {
|
||||
libraryRepository.insert(library)
|
||||
|
||||
userRepository.insert(user1)
|
||||
userRepository.insert(user2)
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
fun beforeEach() {
|
||||
every { mockTaskReceiver.analyzeBook(any<Book>()) } just Runs
|
||||
every { mockTaskReceiver.refreshBookMetadata(any<String>(), any()) } just Runs
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
fun teardown() {
|
||||
libraryRepository.deleteAll()
|
||||
userRepository.deleteAll()
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun `clear repository`() {
|
||||
seriesLifecycle.deleteMany(seriesRepository.findAll().map { it.id })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given non-existent source file when importing then exception is thrown`() {
|
||||
// given
|
||||
val sourceFile = Paths.get("/non-existent")
|
||||
|
||||
// when
|
||||
val thrown = Assertions.catchThrowable {
|
||||
bookImporter.importBook(sourceFile, makeSeries("a series"), CopyMode.COPY)
|
||||
}
|
||||
|
||||
// then
|
||||
assertThat(thrown).isInstanceOf(FileNotFoundException::class.java)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given existing target when importing then exception is thrown`() {
|
||||
Jimfs.newFileSystem(Configuration.unix()).use { fs ->
|
||||
// given
|
||||
val sourceDir = fs.getPath("/source").createDirectory()
|
||||
val sourceFile = sourceDir.resolve("source.cbz").createFile()
|
||||
val destDir = fs.getPath("/dest").createDirectory()
|
||||
destDir.resolve("source.cbz").createFile()
|
||||
|
||||
val series = makeSeries("dest", url = destDir.toUri().toURL())
|
||||
|
||||
// when
|
||||
val thrown = Assertions.catchThrowable {
|
||||
bookImporter.importBook(sourceFile, series, CopyMode.COPY)
|
||||
}
|
||||
|
||||
// then
|
||||
assertThat(thrown).isInstanceOf(FileAlreadyExistsException::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given existing target when importing with destination name then exception is thrown`() {
|
||||
Jimfs.newFileSystem(Configuration.unix()).use { fs ->
|
||||
// given
|
||||
val sourceDir = fs.getPath("/source").createDirectory()
|
||||
val sourceFile = sourceDir.resolve("source.cbz").createFile()
|
||||
val destDir = fs.getPath("/dest").createDirectory()
|
||||
destDir.resolve("dest.cbz").createFile()
|
||||
|
||||
val series = makeSeries("dest").copy(url = destDir.toUri().toURL())
|
||||
|
||||
// when
|
||||
val thrown = Assertions.catchThrowable {
|
||||
bookImporter.importBook(sourceFile, series, CopyMode.COPY, destinationName = "dest")
|
||||
}
|
||||
|
||||
// then
|
||||
assertThat(thrown).isInstanceOf(FileAlreadyExistsException::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given source file parf of a Komga library when importing then exception is thrown`() {
|
||||
Jimfs.newFileSystem(Configuration.unix()).use { fs ->
|
||||
// given
|
||||
val sourceDir = fs.getPath("/source").createDirectory()
|
||||
val sourceFile = sourceDir.resolve("source.cbz").createFile()
|
||||
val destDir = fs.getPath("/dest").createDirectory()
|
||||
|
||||
val series = makeSeries("dest").copy(url = destDir.toUri().toURL())
|
||||
|
||||
val libraryJimfs = makeLibrary("jimfs", url = sourceDir.toUri().toURL())
|
||||
libraryRepository.insert(libraryJimfs)
|
||||
|
||||
// when
|
||||
val thrown = Assertions.catchThrowable {
|
||||
bookImporter.importBook(sourceFile, series, CopyMode.COPY)
|
||||
}
|
||||
|
||||
// then
|
||||
assertThat(thrown).isInstanceOf(PathContainedInPath::class.java)
|
||||
|
||||
libraryRepository.delete(libraryJimfs.id)
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given book when importing then book is imported and series is sorted`() {
|
||||
Jimfs.newFileSystem(Configuration.unix()).use { fs ->
|
||||
// given
|
||||
val sourceDir = fs.getPath("/source").createDirectory()
|
||||
val sourceFile = sourceDir.resolve("2.cbz").createFile()
|
||||
val destDir = fs.getPath("/library/series").createDirectories()
|
||||
|
||||
val existingBooks = listOf(
|
||||
makeBook("1", libraryId = library.id),
|
||||
makeBook("3", libraryId = library.id),
|
||||
)
|
||||
val series = makeSeries("series", url = destDir.toUri().toURL(), libraryId = library.id)
|
||||
.also { series ->
|
||||
seriesLifecycle.createSeries(series)
|
||||
seriesLifecycle.addBooks(series, existingBooks)
|
||||
seriesLifecycle.sortBooks(series)
|
||||
}
|
||||
|
||||
// when
|
||||
bookImporter.importBook(sourceFile, series, CopyMode.COPY)
|
||||
|
||||
// then
|
||||
val books = bookRepository.findBySeriesId(series.id).sortedBy { it.number }
|
||||
assertThat(books).hasSize(3)
|
||||
assertThat(books[0].id).isEqualTo(existingBooks[0].id)
|
||||
assertThat(books[2].id).isEqualTo(existingBooks[1].id)
|
||||
|
||||
with(books[1]) {
|
||||
assertThat(id)
|
||||
.isNotEqualTo(existingBooks[0].id)
|
||||
.isNotEqualTo(existingBooks[1].id)
|
||||
assertThat(number).isEqualTo(2)
|
||||
assertThat(name).isEqualTo("2")
|
||||
|
||||
val newMedia = mediaRepository.findById(id)
|
||||
assertThat(newMedia.status).isEqualTo(Media.Status.UNKNOWN)
|
||||
}
|
||||
|
||||
verify(exactly = 1) { mockTaskReceiver.analyzeBook(any<Book>()) }
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given existing book when importing with upgrade then existing book is deleted`() {
|
||||
Jimfs.newFileSystem(Configuration.unix()).use { fs ->
|
||||
// given
|
||||
val sourceDir = fs.getPath("/source").createDirectory()
|
||||
val sourceFile = sourceDir.resolve("source.cbz").createFile()
|
||||
val destDir = fs.getPath("/library/series").createDirectories()
|
||||
val existingFile = destDir.resolve("4.cbz").createFile()
|
||||
|
||||
val bookToUpgrade = makeBook("2", libraryId = library.id, url = existingFile.toUri().toURL())
|
||||
val otherBooks = listOf(
|
||||
makeBook("1", libraryId = library.id),
|
||||
makeBook("3", libraryId = library.id),
|
||||
)
|
||||
val series = makeSeries("series", url = destDir.toUri().toURL(), libraryId = library.id)
|
||||
.also { series ->
|
||||
seriesLifecycle.createSeries(series)
|
||||
seriesLifecycle.addBooks(series, listOf(bookToUpgrade) + otherBooks)
|
||||
seriesLifecycle.sortBooks(series)
|
||||
}
|
||||
|
||||
// when
|
||||
bookImporter.importBook(sourceFile, series, CopyMode.MOVE, upgradeBookId = bookToUpgrade.id)
|
||||
|
||||
// then
|
||||
val books = bookRepository.findBySeriesId(series.id).sortedBy { it.number }
|
||||
assertThat(books).hasSize(3)
|
||||
assertThat(books[0].id).isEqualTo(otherBooks[0].id)
|
||||
assertThat(books[1].id).isEqualTo(otherBooks[1].id)
|
||||
assertThat(books[2].id).isNotEqualTo(bookToUpgrade.id)
|
||||
|
||||
assertThat(bookRepository.findByIdOrNull(bookToUpgrade.id)).isNull()
|
||||
|
||||
val upgradedMedia = mediaRepository.findById(books[2].id)
|
||||
assertThat(upgradedMedia.status).isEqualTo(Media.Status.OUTDATED)
|
||||
|
||||
assertThat(Files.notExists(sourceFile)).isTrue
|
||||
|
||||
verify(exactly = 1) { mockTaskReceiver.analyzeBook(any<Book>()) }
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given existing book with metadata when importing with upgrade then metadata is kept`() {
|
||||
Jimfs.newFileSystem(Configuration.unix()).use { fs ->
|
||||
// given
|
||||
val sourceDir = fs.getPath("/source").createDirectory()
|
||||
val sourceFile = sourceDir.resolve("source.cbz").createFile()
|
||||
val destDir = fs.getPath("/library/series").createDirectories()
|
||||
val existingFile = destDir.resolve("4.cbz").createFile()
|
||||
|
||||
val bookToUpgrade = makeBook("2", libraryId = library.id, url = existingFile.toUri().toURL())
|
||||
val otherBooks = listOf(
|
||||
makeBook("1", libraryId = library.id),
|
||||
makeBook("3", libraryId = library.id),
|
||||
)
|
||||
val series = makeSeries("series", url = destDir.toUri().toURL(), libraryId = library.id)
|
||||
.also { series ->
|
||||
seriesLifecycle.createSeries(series)
|
||||
seriesLifecycle.addBooks(series, listOf(bookToUpgrade) + otherBooks)
|
||||
seriesLifecycle.sortBooks(series)
|
||||
}
|
||||
|
||||
metadataRepository.findById(bookToUpgrade.id).let {
|
||||
metadataRepository.update(
|
||||
it.copy(
|
||||
summary = "a summary",
|
||||
number = "HS",
|
||||
numberLock = true,
|
||||
numberSort = 100F,
|
||||
numberSortLock = true,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// when
|
||||
bookImporter.importBook(sourceFile, series, CopyMode.MOVE, upgradeBookId = bookToUpgrade.id)
|
||||
|
||||
// then
|
||||
val books = bookRepository.findBySeriesId(series.id).sortedBy { it.number }
|
||||
assertThat(books).hasSize(3)
|
||||
assertThat(books[0].id).isEqualTo(otherBooks[0].id)
|
||||
assertThat(books[1].id).isEqualTo(otherBooks[1].id)
|
||||
assertThat(books[2].id).isNotEqualTo(bookToUpgrade.id)
|
||||
|
||||
assertThat(bookRepository.findByIdOrNull(bookToUpgrade.id)).isNull()
|
||||
|
||||
val upgradedMedia = mediaRepository.findById(books[2].id)
|
||||
assertThat(upgradedMedia.status).isEqualTo(Media.Status.OUTDATED)
|
||||
|
||||
with(metadataRepository.findById(books[2].id)) {
|
||||
assertThat(summary).isEqualTo("a summary")
|
||||
assertThat(number).isEqualTo("HS")
|
||||
assertThat(numberLock).isTrue
|
||||
assertThat(numberSort).isEqualTo(100F)
|
||||
assertThat(numberSortLock).isTrue
|
||||
}
|
||||
|
||||
assertThat(Files.notExists(sourceFile)).isTrue
|
||||
|
||||
verify(exactly = 1) { mockTaskReceiver.analyzeBook(any<Book>()) }
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given existing book when importing with upgrade and same name then existing book is replaced`() {
|
||||
Jimfs.newFileSystem(Configuration.unix()).use { fs ->
|
||||
// given
|
||||
val sourceDir = fs.getPath("/source").createDirectory()
|
||||
val sourceFile = sourceDir.resolve("source.cbz").createFile()
|
||||
val destDir = fs.getPath("/library/series").createDirectories()
|
||||
val existingFile = destDir.resolve("2.cbz").createFile()
|
||||
|
||||
val bookToUpgrade = makeBook("2", libraryId = library.id, url = existingFile.toUri().toURL())
|
||||
val otherBooks = listOf(
|
||||
makeBook("1", libraryId = library.id),
|
||||
makeBook("3", libraryId = library.id),
|
||||
)
|
||||
val series = makeSeries("series", url = destDir.toUri().toURL(), libraryId = library.id)
|
||||
.also { series ->
|
||||
seriesLifecycle.createSeries(series)
|
||||
seriesLifecycle.addBooks(series, listOf(bookToUpgrade) + otherBooks)
|
||||
seriesLifecycle.sortBooks(series)
|
||||
}
|
||||
|
||||
// when
|
||||
bookImporter.importBook(sourceFile, series, CopyMode.COPY, destinationName = "2", upgradeBookId = bookToUpgrade.id)
|
||||
|
||||
// then
|
||||
val books = bookRepository.findBySeriesId(series.id).sortedBy { it.number }
|
||||
assertThat(books).hasSize(3)
|
||||
assertThat(books[0].id).isEqualTo(otherBooks[0].id)
|
||||
assertThat(books[1].id).isNotEqualTo(bookToUpgrade.id)
|
||||
assertThat(books[2].id).isEqualTo(otherBooks[1].id)
|
||||
|
||||
assertThat(bookRepository.findByIdOrNull(bookToUpgrade.id)).isNull()
|
||||
|
||||
val upgradedMedia = mediaRepository.findById(books[1].id)
|
||||
assertThat(upgradedMedia.status).isEqualTo(Media.Status.OUTDATED)
|
||||
|
||||
assertThat(Files.exists(sourceFile)).isTrue
|
||||
|
||||
verify(exactly = 1) { mockTaskReceiver.analyzeBook(any<Book>()) }
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given book with read progress when importing with upgrade then read progress is kept`() {
|
||||
Jimfs.newFileSystem(Configuration.unix()).use { fs ->
|
||||
// given
|
||||
val sourceDir = fs.getPath("/source").createDirectory()
|
||||
val sourceFile = sourceDir.resolve("source.cbz").createFile()
|
||||
val destDir = fs.getPath("/library/series").createDirectories()
|
||||
val existingFile = destDir.resolve("1.cbz").createFile()
|
||||
|
||||
val bookToUpgrade = makeBook("1", libraryId = library.id, url = existingFile.toUri().toURL())
|
||||
val series = makeSeries("series", url = destDir.toUri().toURL(), libraryId = library.id)
|
||||
.also { series ->
|
||||
seriesLifecycle.createSeries(series)
|
||||
seriesLifecycle.addBooks(series, listOf(bookToUpgrade))
|
||||
seriesLifecycle.sortBooks(series)
|
||||
}
|
||||
|
||||
mediaRepository.findById(bookToUpgrade.id).let { media ->
|
||||
mediaRepository.update(
|
||||
media.copy(
|
||||
status = Media.Status.READY,
|
||||
pages = (1..10).map { BookPage("$it", "image/jpeg") }
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
bookLifecycle.markReadProgressCompleted(bookToUpgrade.id, user1)
|
||||
bookLifecycle.markReadProgress(bookToUpgrade, user2, 4)
|
||||
|
||||
// when
|
||||
bookImporter.importBook(sourceFile, series, CopyMode.MOVE, upgradeBookId = bookToUpgrade.id)
|
||||
|
||||
// then
|
||||
val books = bookRepository.findBySeriesId(series.id).sortedBy { it.number }
|
||||
assertThat(books).hasSize(1)
|
||||
|
||||
val progress = readProgressRepository.findByBookId(books[0].id)
|
||||
assertThat(progress).hasSize(2)
|
||||
with(progress.find { it.userId == user1.id }!!) {
|
||||
assertThat(completed).isTrue
|
||||
}
|
||||
with(progress.find { it.userId == user2.id }!!) {
|
||||
assertThat(completed).isFalse
|
||||
assertThat(page).isEqualTo(4)
|
||||
}
|
||||
|
||||
verify(exactly = 1) { mockTaskReceiver.analyzeBook(any<Book>()) }
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `given book part of a read list when importing with upgrade then imported book replaces upgraded book in the read list`() {
|
||||
Jimfs.newFileSystem(Configuration.unix()).use { fs ->
|
||||
// given
|
||||
val sourceDir = fs.getPath("/source").createDirectory()
|
||||
val sourceFile = sourceDir.resolve("source.cbz").createFile()
|
||||
val destDir = fs.getPath("/library/series").createDirectories()
|
||||
val existingFile = destDir.resolve("1.cbz").createFile()
|
||||
|
||||
val bookToUpgrade = makeBook("1", libraryId = library.id, url = existingFile.toUri().toURL())
|
||||
val series = makeSeries("series", url = destDir.toUri().toURL(), libraryId = library.id)
|
||||
.also { series ->
|
||||
seriesLifecycle.createSeries(series)
|
||||
seriesLifecycle.addBooks(series, listOf(bookToUpgrade))
|
||||
seriesLifecycle.sortBooks(series)
|
||||
}
|
||||
|
||||
val readList = ReadList(
|
||||
name = "readlist",
|
||||
bookIds = listOf(bookToUpgrade.id).toIndexedMap(),
|
||||
)
|
||||
readListLifecycle.addReadList(readList)
|
||||
|
||||
// when
|
||||
bookImporter.importBook(sourceFile, series, CopyMode.MOVE, upgradeBookId = bookToUpgrade.id)
|
||||
|
||||
// then
|
||||
val books = bookRepository.findBySeriesId(series.id).sortedBy { it.number }
|
||||
assertThat(books).hasSize(1)
|
||||
|
||||
with(readListRepository.findByIdOrNull(readList.id)!!) {
|
||||
assertThat(bookIds).hasSize(1)
|
||||
assertThat(bookIds[0]).isEqualTo(books[0].id)
|
||||
}
|
||||
|
||||
verify(exactly = 1) { mockTaskReceiver.analyzeBook(any<Book>()) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -35,7 +35,7 @@ class BookLifecycleTest(
|
|||
@Autowired private val seriesLifecycle: SeriesLifecycle,
|
||||
@Autowired private val readProgressRepository: ReadProgressRepository,
|
||||
@Autowired private val mediaRepository: MediaRepository,
|
||||
@Autowired private val userRepository: KomgaUserRepository
|
||||
@Autowired private val userRepository: KomgaUserRepository,
|
||||
) {
|
||||
|
||||
@MockkBean
|
||||
|
|
@ -90,7 +90,7 @@ class BookLifecycleTest(
|
|||
assertThat(readProgressRepository.findAll()).hasSize(2)
|
||||
|
||||
// when
|
||||
every { mockAnalyzer.analyze(any()) } returns Media(status = Media.Status.READY, mediaType = "application/zip", pages = mutableListOf(makeBookPage("1.jpg"), makeBookPage("2.jpg")))
|
||||
every { mockAnalyzer.analyze(any()) } returns Media(status = Media.Status.READY, mediaType = "application/zip", pages = mutableListOf(makeBookPage("1.jpg"), makeBookPage("2.jpg")), bookId = book.id)
|
||||
bookLifecycle.analyzeAndPersist(book)
|
||||
|
||||
// then
|
||||
|
|
@ -123,7 +123,7 @@ class BookLifecycleTest(
|
|||
assertThat(readProgressRepository.findAll()).hasSize(2)
|
||||
|
||||
// when
|
||||
every { mockAnalyzer.analyze(any()) } returns Media(status = Media.Status.READY, mediaType = "application/zip", pages = (1..10).map { BookPage("$it", "image/jpeg") })
|
||||
every { mockAnalyzer.analyze(any()) } returns Media(status = Media.Status.READY, mediaType = "application/zip", pages = (1..10).map { BookPage("$it", "image/jpeg") }, bookId = book.id)
|
||||
bookLifecycle.analyzeAndPersist(book)
|
||||
|
||||
// then
|
||||
|
|
|
|||
|
|
@ -200,7 +200,7 @@ class LibraryContentLifecycleTest(
|
|||
)
|
||||
libraryContentLifecycle.scanRootFolder(library)
|
||||
|
||||
every { mockAnalyzer.analyze(any()) } returns Media(status = Media.Status.READY, mediaType = "application/zip", pages = mutableListOf(makeBookPage("1.jpg"), makeBookPage("2.jpg")))
|
||||
every { mockAnalyzer.analyze(any()) } returns Media(status = Media.Status.READY, mediaType = "application/zip", pages = mutableListOf(makeBookPage("1.jpg"), makeBookPage("2.jpg")), bookId = book1.id)
|
||||
bookRepository.findAll().map { bookLifecycle.analyzeAndPersist(it) }
|
||||
|
||||
// when
|
||||
|
|
@ -236,7 +236,7 @@ class LibraryContentLifecycleTest(
|
|||
)
|
||||
libraryContentLifecycle.scanRootFolder(library)
|
||||
|
||||
every { mockAnalyzer.analyze(any()) } returns Media(status = Media.Status.READY, mediaType = "application/zip", pages = mutableListOf(makeBookPage("1.jpg"), makeBookPage("2.jpg")))
|
||||
every { mockAnalyzer.analyze(any()) } returns Media(status = Media.Status.READY, mediaType = "application/zip", pages = mutableListOf(makeBookPage("1.jpg"), makeBookPage("2.jpg")), bookId = book1.id)
|
||||
bookRepository.findAll().map { bookLifecycle.analyzeAndPersist(it) }
|
||||
|
||||
// when
|
||||
|
|
|
|||
|
|
@ -78,17 +78,17 @@ class ReadListMatcherTest(
|
|||
seriesLifecycle.sortBooks(s)
|
||||
|
||||
bookMetadataRepository.findById(booksSeries2[0].id).let {
|
||||
bookMetadataRepository.update(it.copy(number = "025"))
|
||||
bookMetadataRepository.update(it.copy(number = "0025"))
|
||||
}
|
||||
}
|
||||
|
||||
val request = ReadListRequest(
|
||||
name = "readlist",
|
||||
books = listOf(
|
||||
ReadListRequestBook(series = "Batman: White Knight", number = 1),
|
||||
ReadListRequestBook(series = "joker", number = 2),
|
||||
ReadListRequestBook(series = "Batman: White Knight", number = 2),
|
||||
ReadListRequestBook(series = "joker", number = 25),
|
||||
ReadListRequestBook(series = "Batman: White Knight", number = "1"),
|
||||
ReadListRequestBook(series = "joker", number = "02"),
|
||||
ReadListRequestBook(series = "Batman: White Knight", number = "2"),
|
||||
ReadListRequestBook(series = "joker", number = "25"),
|
||||
)
|
||||
)
|
||||
|
||||
|
|
@ -99,7 +99,7 @@ class ReadListMatcherTest(
|
|||
with(result) {
|
||||
assertThat(readList).isNotNull
|
||||
assertThat(unmatchedBooks).isEmpty()
|
||||
assertThat(errorCode).isBlank()
|
||||
assertThat(errorCode).isBlank
|
||||
with(readList!!) {
|
||||
assertThat(name).isEqualTo(request.name)
|
||||
assertThat(bookIds).hasSize(4)
|
||||
|
|
@ -125,10 +125,10 @@ class ReadListMatcherTest(
|
|||
val request = ReadListRequest(
|
||||
name = "my readlist",
|
||||
books = listOf(
|
||||
ReadListRequestBook(series = "batman: white knight", number = 1),
|
||||
ReadListRequestBook(series = "joker", number = 2),
|
||||
ReadListRequestBook(series = "BATMAN: WHITE KNIGHT", number = 2),
|
||||
ReadListRequestBook(series = "joker", number = 25),
|
||||
ReadListRequestBook(series = "batman: white knight", number = "1"),
|
||||
ReadListRequestBook(series = "joker", number = "2"),
|
||||
ReadListRequestBook(series = "BATMAN: WHITE KNIGHT", number = "2"),
|
||||
ReadListRequestBook(series = "joker", number = "25"),
|
||||
)
|
||||
)
|
||||
|
||||
|
|
@ -176,10 +176,10 @@ class ReadListMatcherTest(
|
|||
val request = ReadListRequest(
|
||||
name = "readlist",
|
||||
books = listOf(
|
||||
ReadListRequestBook(series = "tokyo ghost", number = 1),
|
||||
ReadListRequestBook(series = "batman", number = 3),
|
||||
ReadListRequestBook(series = "joker", number = 3),
|
||||
ReadListRequestBook(series = "batman", number = 2),
|
||||
ReadListRequestBook(series = "tokyo ghost", number = "1"),
|
||||
ReadListRequestBook(series = "batman", number = "3"),
|
||||
ReadListRequestBook(series = "joker", number = "3"),
|
||||
ReadListRequestBook(series = "batman", number = "2"),
|
||||
)
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import io.mockk.mockk
|
|||
import org.apache.commons.validator.routines.ISBNValidator
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.gotson.komga.domain.model.BookPage
|
||||
import org.gotson.komga.domain.model.BookWithMedia
|
||||
import org.gotson.komga.domain.model.Media
|
||||
import org.gotson.komga.domain.model.makeBook
|
||||
import org.gotson.komga.domain.service.BookAnalyzer
|
||||
|
|
@ -25,7 +26,7 @@ class IsbnBarcodeProviderTest {
|
|||
val media = Media(pages = listOf(BookPage("page", "image/jpeg")))
|
||||
|
||||
// when
|
||||
val patch = isbnBarcodeProvider.getBookMetadataFromBook(book, media)
|
||||
val patch = isbnBarcodeProvider.getBookMetadataFromBook(BookWithMedia(book, media))
|
||||
|
||||
// then
|
||||
assertThat(patch?.isbn).isEqualTo("9782811632397")
|
||||
|
|
@ -40,7 +41,7 @@ class IsbnBarcodeProviderTest {
|
|||
val media = Media(pages = listOf(BookPage("page", "image/jpeg")))
|
||||
|
||||
// when
|
||||
val patch = isbnBarcodeProvider.getBookMetadataFromBook(book, media)
|
||||
val patch = isbnBarcodeProvider.getBookMetadataFromBook(BookWithMedia(book, media))
|
||||
|
||||
// then
|
||||
assertThat(patch).isNull()
|
||||
|
|
@ -56,7 +57,7 @@ class IsbnBarcodeProviderTest {
|
|||
val media = Media(pages = listOf(BookPage("page", "image/jpeg")))
|
||||
|
||||
// when
|
||||
val patch = isbnBarcodeProvider.getBookMetadataFromBook(book, media)
|
||||
val patch = isbnBarcodeProvider.getBookMetadataFromBook(BookWithMedia(book, media))
|
||||
|
||||
// then
|
||||
assertThat(patch).isNull()
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import com.fasterxml.jackson.dataformat.xml.XmlMapper
|
|||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.gotson.komga.domain.model.BookWithMedia
|
||||
import org.gotson.komga.domain.model.Media
|
||||
import org.gotson.komga.domain.model.SeriesMetadata
|
||||
import org.gotson.komga.domain.model.makeBook
|
||||
|
|
@ -53,7 +54,7 @@ class ComicInfoProviderTest {
|
|||
|
||||
every { mockMapper.readValue(any<ByteArray>(), ComicInfo::class.java) } returns comicInfo
|
||||
|
||||
val patch = comicInfoProvider.getBookMetadataFromBook(book, media)
|
||||
val patch = comicInfoProvider.getBookMetadataFromBook(BookWithMedia(book, media))
|
||||
|
||||
with(patch!!) {
|
||||
assertThat(title).isEqualTo("title")
|
||||
|
|
@ -86,7 +87,7 @@ class ComicInfoProviderTest {
|
|||
|
||||
every { mockMapper.readValue(any<ByteArray>(), ComicInfo::class.java) } returns comicInfo
|
||||
|
||||
val patch = comicInfoProvider.getBookMetadataFromBook(book, media)
|
||||
val patch = comicInfoProvider.getBookMetadataFromBook(BookWithMedia(book, media))
|
||||
|
||||
with(patch!!) {
|
||||
assertThat(title).isNull()
|
||||
|
|
@ -106,7 +107,7 @@ class ComicInfoProviderTest {
|
|||
|
||||
every { mockMapper.readValue(any<ByteArray>(), ComicInfo::class.java) } returns comicInfo
|
||||
|
||||
val patch = comicInfoProvider.getBookMetadataFromBook(book, media)
|
||||
val patch = comicInfoProvider.getBookMetadataFromBook(BookWithMedia(book, media))
|
||||
|
||||
with(patch!!) {
|
||||
assertThat(releaseDate).isNull()
|
||||
|
|
@ -121,7 +122,7 @@ class ComicInfoProviderTest {
|
|||
|
||||
every { mockMapper.readValue(any<ByteArray>(), ComicInfo::class.java) } returns comicInfo
|
||||
|
||||
val patch = comicInfoProvider.getBookMetadataFromBook(book, media)
|
||||
val patch = comicInfoProvider.getBookMetadataFromBook(BookWithMedia(book, media))
|
||||
|
||||
with(patch!!) {
|
||||
assertThat(releaseDate).isEqualTo(LocalDate.of(2020, 1, 1))
|
||||
|
|
@ -142,7 +143,7 @@ class ComicInfoProviderTest {
|
|||
|
||||
every { mockMapper.readValue(any<ByteArray>(), ComicInfo::class.java) } returns comicInfo
|
||||
|
||||
val patch = comicInfoProvider.getBookMetadataFromBook(book, media)
|
||||
val patch = comicInfoProvider.getBookMetadataFromBook(BookWithMedia(book, media))
|
||||
|
||||
with(patch!!) {
|
||||
assertThat(authors).hasSize(7)
|
||||
|
|
@ -165,7 +166,7 @@ class ComicInfoProviderTest {
|
|||
|
||||
every { mockMapper.readValue(any<ByteArray>(), ComicInfo::class.java) } returns comicInfo
|
||||
|
||||
val patch = comicInfoProvider.getBookMetadataFromBook(book, media)
|
||||
val patch = comicInfoProvider.getBookMetadataFromBook(BookWithMedia(book, media))
|
||||
|
||||
with(patch!!) {
|
||||
assertThat(authors).hasSize(14)
|
||||
|
|
@ -179,7 +180,7 @@ class ComicInfoProviderTest {
|
|||
val book = makeBook("book")
|
||||
val media = Media(Media.Status.READY)
|
||||
|
||||
val patch = comicInfoProvider.getBookMetadataFromBook(book, media)
|
||||
val patch = comicInfoProvider.getBookMetadataFromBook(BookWithMedia(book, media))
|
||||
|
||||
assertThat(patch).isNull()
|
||||
}
|
||||
|
|
@ -201,7 +202,7 @@ class ComicInfoProviderTest {
|
|||
|
||||
every { mockMapper.readValue(any<ByteArray>(), ComicInfo::class.java) } returns comicInfo
|
||||
|
||||
val patch = comicInfoProvider.getSeriesMetadataFromBook(book, media)!!
|
||||
val patch = comicInfoProvider.getSeriesMetadataFromBook(BookWithMedia(book, media))!!
|
||||
|
||||
with(patch) {
|
||||
assertThat(title).isEqualTo("series")
|
||||
|
|
@ -226,7 +227,7 @@ class ComicInfoProviderTest {
|
|||
|
||||
every { mockMapper.readValue(any<ByteArray>(), ComicInfo::class.java) } returns comicInfo
|
||||
|
||||
val patch = comicInfoProvider.getSeriesMetadataFromBook(book, media)!!
|
||||
val patch = comicInfoProvider.getSeriesMetadataFromBook(BookWithMedia(book, media))!!
|
||||
|
||||
with(patch) {
|
||||
assertThat(title).isEqualTo("series (2020)")
|
||||
|
|
@ -242,7 +243,7 @@ class ComicInfoProviderTest {
|
|||
|
||||
every { mockMapper.readValue(any<ByteArray>(), ComicInfo::class.java) } returns comicInfo
|
||||
|
||||
val patch = comicInfoProvider.getSeriesMetadataFromBook(book, media)!!
|
||||
val patch = comicInfoProvider.getSeriesMetadataFromBook(BookWithMedia(book, media))!!
|
||||
|
||||
with(patch) {
|
||||
assertThat(title).isEqualTo("series")
|
||||
|
|
@ -257,7 +258,7 @@ class ComicInfoProviderTest {
|
|||
|
||||
every { mockMapper.readValue(any<ByteArray>(), ComicInfo::class.java) } returns comicInfo
|
||||
|
||||
val patch = comicInfoProvider.getSeriesMetadataFromBook(book, media)!!
|
||||
val patch = comicInfoProvider.getSeriesMetadataFromBook(BookWithMedia(book, media))!!
|
||||
|
||||
with(patch) {
|
||||
assertThat(language).isNull()
|
||||
|
|
@ -277,7 +278,7 @@ class ComicInfoProviderTest {
|
|||
|
||||
every { mockMapper.readValue(any<ByteArray>(), ComicInfo::class.java) } returns comicInfo
|
||||
|
||||
val patch = comicInfoProvider.getSeriesMetadataFromBook(book, media)!!
|
||||
val patch = comicInfoProvider.getSeriesMetadataFromBook(BookWithMedia(book, media))!!
|
||||
|
||||
with(patch) {
|
||||
assertThat(title).isNull()
|
||||
|
|
|
|||
|
|
@ -21,12 +21,12 @@ class ReadListProviderTest {
|
|||
books = listOf(
|
||||
Book().apply {
|
||||
series = "series 1"
|
||||
number = 4
|
||||
number = " 4 "
|
||||
volume = 2005
|
||||
},
|
||||
Book().apply {
|
||||
series = "series 2"
|
||||
number = 1
|
||||
number = "1"
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -44,12 +44,12 @@ class ReadListProviderTest {
|
|||
|
||||
with(books[0]) {
|
||||
assertThat(series).isEqualTo("series 1 (2005)")
|
||||
assertThat(number).isEqualTo(4)
|
||||
assertThat(number).isEqualTo("4")
|
||||
}
|
||||
|
||||
with(books[1]) {
|
||||
assertThat(series).isEqualTo("series 2")
|
||||
assertThat(number).isEqualTo(1)
|
||||
assertThat(number).isEqualTo("1")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -62,12 +62,12 @@ class ReadListProviderTest {
|
|||
books = listOf(
|
||||
Book().apply {
|
||||
series = " "
|
||||
number = 4
|
||||
number = "4"
|
||||
volume = 2005
|
||||
},
|
||||
Book().apply {
|
||||
series = null
|
||||
number = 1
|
||||
number = "1"
|
||||
},
|
||||
Book().apply {
|
||||
series = "Series"
|
||||
|
|
@ -110,12 +110,12 @@ class ReadListProviderTest {
|
|||
books = listOf(
|
||||
Book().apply {
|
||||
series = "series 1"
|
||||
number = 4
|
||||
number = "4"
|
||||
volume = 2005
|
||||
},
|
||||
Book().apply {
|
||||
series = "series 2"
|
||||
number = 1
|
||||
number = "1"
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
@ -137,12 +137,12 @@ class ReadListProviderTest {
|
|||
books = listOf(
|
||||
Book().apply {
|
||||
series = "series 1"
|
||||
number = 4
|
||||
number = "4"
|
||||
volume = 2005
|
||||
},
|
||||
Book().apply {
|
||||
series = "series 2"
|
||||
number = 1
|
||||
number = "1"
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ class ReadingListTest {
|
|||
|
||||
with(books[0]) {
|
||||
assertThat(series).isEqualTo("Civil War")
|
||||
assertThat(number).isEqualTo(1)
|
||||
assertThat(number).isEqualTo("1")
|
||||
assertThat(volume).isEqualTo(2006)
|
||||
assertThat(year).isEqualTo(2006)
|
||||
assertThat(fileName).isEqualTo("Civil War Vol.2006 #01 (July, 2006)")
|
||||
|
|
@ -29,7 +29,7 @@ class ReadingListTest {
|
|||
|
||||
with(books[1]) {
|
||||
assertThat(series).isEqualTo("Wolverine")
|
||||
assertThat(number).isEqualTo(42)
|
||||
assertThat(number).isEqualTo("42")
|
||||
assertThat(volume).isEqualTo(2003)
|
||||
assertThat(year).isEqualTo(2006)
|
||||
assertThat(fileName).isEqualTo("Wolverine Vol.2003 #42 (July, 2006)")
|
||||
|
|
@ -37,7 +37,7 @@ class ReadingListTest {
|
|||
|
||||
with(books[2]) {
|
||||
assertThat(series).isEqualTo("X-Factor")
|
||||
assertThat(number).isEqualTo(8)
|
||||
assertThat(number).isEqualTo("HS")
|
||||
assertThat(volume).isEqualTo(2006)
|
||||
assertThat(year).isEqualTo(2006)
|
||||
assertThat(fileName).isEqualTo("X-Factor Vol.2006 #08 (August, 2006)")
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ class LibraryControllerTest(
|
|||
|
||||
private val route = "/api/v1/libraries"
|
||||
|
||||
private val library = makeLibrary(url = "file:/library1", id = "1")
|
||||
private val library = makeLibrary(path = "file:/library1", id = "1")
|
||||
|
||||
@BeforeAll
|
||||
fun `setup library`() {
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue