first version of the library browser

npm upgrade
remove e2e test dependencies to speed up builds as it's not used
This commit is contained in:
Gauthier Roebroeck 2019-11-18 18:03:49 +08:00
parent 8970f192f3
commit f992a2193c
17 changed files with 2123 additions and 1605 deletions

View file

@ -1,3 +0,0 @@
{
"pluginsFile": "tests/e2e/plugins/index.js"
}

File diff suppressed because it is too large Load diff

View file

@ -6,28 +6,27 @@
"serve": "vue-cli-service serve --port 8081",
"build": "vue-cli-service build",
"lint": "vue-cli-service lint",
"test:e2e": "vue-cli-service test:e2e",
"test:unit": "vue-cli-service test:unit"
},
"dependencies": {
"axios": "^0.19.0",
"core-js": "^2.6.5",
"core-js": "^2.6.10",
"qs": "^6.9.1",
"vue": "^2.6.10",
"vue-router": "^3.0.3",
"vuelidate": "^0.7.4",
"vuetify": "^2.1.2",
"vuex": "^3.0.1",
"vuetify": "^2.1.10",
"vuex": "^3.1.2",
"vuex-router-sync": "^5.0.0"
},
"devDependencies": {
"@types/jest": "^23.1.4",
"@types/vuelidate": "^0.7.7",
"@vue/cli-plugin-babel": "^3.11.0",
"@vue/cli-plugin-e2e-cypress": "^3.11.0",
"@vue/cli-plugin-eslint": "^3.11.0",
"@vue/cli-plugin-typescript": "^3.11.0",
"@vue/cli-plugin-unit-jest": "^3.11.0",
"@vue/cli-service": "^3.11.0",
"@types/vuelidate": "^0.7.9",
"@vue/cli-plugin-babel": "^3.12.1",
"@vue/cli-plugin-eslint": "^3.12.1",
"@vue/cli-plugin-typescript": "^3.12.1",
"@vue/cli-plugin-unit-jest": "^3.12.1",
"@vue/cli-service": "^3.12.1",
"@vue/eslint-config-standard": "^4.0.0",
"@vue/eslint-config-typescript": "^4.0.0",
"@vue/test-utils": "1.0.0-beta.29",
@ -35,12 +34,12 @@
"babel-eslint": "^10.0.1",
"eslint": "^5.16.0",
"eslint-plugin-vue": "^5.0.0",
"sass": "^1.23.0",
"sass": "^1.23.6",
"sass-loader": "^7.1.0",
"ts-jest": "^23.0.0",
"typescript": "^3.6.4",
"typescript": "^3.7.2",
"vue-cli-plugin-vuetify": "^0.6.3",
"vue-template-compiler": "^2.6.10",
"vuetify-loader": "^1.2.2"
"vuetify-loader": "^1.3.1"
}
}

View file

@ -0,0 +1,144 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:osb="http://www.openswatchbook.org/uri/2009/osb"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
height="297mm"
viewBox="0 0 793.70081 1122.5197"
width="210mm"
version="1.1"
id="svg4586"
sodipodi:docname="cover.svg"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)">
<metadata
id="metadata4592">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage"/>
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs4590">
<linearGradient
id="linearGradient6082"
osb:paint="solid">
<stop
style="stop-color:#000000;stop-opacity:1;"
offset="0"
id="stop6080"/>
</linearGradient>
<linearGradient
id="linearGradient6076"
osb:paint="solid">
<stop
style="stop-color:#000000;stop-opacity:1;"
offset="0"
id="stop6074"/>
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient6082"
id="linearGradient6084"
x1="77.866814"
y1="386.00677"
x2="217.20259"
y2="386.00677"
gradientUnits="userSpaceOnUse"/>
<filter
style="color-interpolation-filters:sRGB"
inkscape:label="Greyscale"
id="filter4541">
<feColorMatrix
values="0.21 0.72 0.072 0 0 0.21 0.72 0.072 0 0 0.21 0.72 0.072 0 0 0 0 0 1 0 "
id="feColorMatrix4539"/>
</filter>
<filter
style="color-interpolation-filters:sRGB"
inkscape:label="Greyscale"
id="filter4545">
<feColorMatrix
values="0.21 0.72 0.072 0 0 0.21 0.72 0.072 0 0 0.21 0.72 0.072 0 0 0 0 0 1 0 "
id="feColorMatrix4543"/>
</filter>
</defs>
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1656"
inkscape:window-height="1368"
id="namedview4588"
showgrid="false"
inkscape:zoom="0.31281188"
inkscape:cx="546.28827"
inkscape:cy="1456.7169"
inkscape:window-x="-7"
inkscape:window-y="0"
inkscape:window-maximized="0"
inkscape:current-layer="svg4586"
inkscape:snap-page="true"
units="mm"
showborder="true"
viewbox-width="1"
viewbox-height="1"
scale-x="0.60001"/>
<rect
style="fill:#005ed3;fill-opacity:1;stroke:none;stroke-width:2.21274972;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;filter:url(#filter4541)"
id="rect826"
width="595.27557"
height="841.88977"
x="1.7763568e-14"
y="-2.3789293e-05"
transform="matrix(1.3333334,0,0,1.3333333,0,7.9297629e-6)"/>
<g
id="g4537"
transform="matrix(1.3333334,0,0,1.3333333,23.517062,16.073517)"
style="filter:url(#filter4545)">
<path
style="fill:#ff0335"
inkscape:connector-curvature="0"
id="path4560"
d="m 280,239.63194 37.10937,63.73828 70.57422,-31.41406 -10.52734,71.71875 77.07812,12.91015 -54.14453,52.30469 54.14453,52.30469 -77.07812,12.91015 10.52734,71.71875 L 317.10937,514.40928 280,578.14756 l -37.10938,-63.73828 -70.57421,31.41406 10.52734,-71.71875 -77.07813,-12.91015 54.14454,-52.30469 -54.14454,-52.30469 77.07813,-12.91015 -10.52734,-71.71875 70.57421,31.41406 z m 0,0"/>
<path
style="fill:#c2001b"
inkscape:connector-curvature="0"
id="path4562"
d="m 454.23047,461.19053 -77.07031,12.91016 10.51953,71.71875 L 317.10938,514.40928 280,578.15147 V 239.62803 l 37.10938,63.74219 70.57031,-31.41016 -6.75781,46.10156 -3.76172,25.61719 58.80078,9.85156 18.26953,3.0586 -13.39063,12.92969 -40.75,39.37109 11.37891,10.98828 z m 0,0"/>
<path
style="fill:#ffdf47"
inkscape:connector-curvature="0"
id="path4564"
d="M 280,607.95616 236.69531,533.58506 153.51953,570.6085 165.86719,486.46787 73.949219,471.07334 138.32031,408.88975 73.949219,346.70616 165.86719,331.31162 153.51953,247.171 236.69922,284.19444 280,209.82335 l 43.30469,74.37109 83.17578,-37.02344 -12.34766,84.14062 91.91797,15.39454 -64.37109,62.18359 64.37109,62.18359 -91.91797,15.39844 12.34766,84.13672 -83.17578,-37.02344 z M 249.08203,495.2335 280,548.33506 310.91797,495.2335 368.88281,521.03428 360.17969,461.74131 422.41797,451.31553 378.5,408.88975 422.41797,366.46397 360.17969,356.03819 368.88281,296.74522 310.91797,322.546 280,269.44444 249.08203,322.546 l -57.96484,-25.80078 8.70312,59.29297 -62.23828,10.42578 43.91797,42.42578 -43.91797,42.42578 62.23828,10.42578 -8.70312,59.29297 z m 0,0"/>
<path
style="fill:#fec000"
inkscape:connector-curvature="0"
id="path4566"
d="m 427.30859,414.33116 -5.6289,-5.44141 25.16015,-24.30078 39.21094,-37.87891 -55.75,-9.33984 -36.17187,-6.0586 2.80078,-19.09375 9.55078,-65.04687 -83.17969,37.01953 L 280,209.81944 v 59.62109 l 30.92188,53.10938 57.95703,-25.8086 -3.91016,26.66797 -2.54687,17.37891 -2.24219,15.25 2.48047,0.42187 59.76172,10.00781 -43.92188,42.42188 16.96875,16.39062 26.95313,26.03125 -62.24219,10.42969 8.69922,59.29688 -57.95703,-25.8086 L 280,548.33897 v 59.62109 l 43.30078,-74.37109 83.17969,37.01953 -12.35156,-84.14063 91.92187,-15.39843 z m 0,0"/>
<g
id="text4596"
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:296.55969238px;line-height:125%;font-family:Impact;-inkscape-font-specification:Impact;letter-spacing:0px;word-spacing:0px;fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1.54528999;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
transform="matrix(1.1590846,-0.34467221,0.22789693,0.794981,24,152.88975)"
aria-label="K">
<path
inkscape:connector-curvature="0"
id="path824"
style="font-size:296.55969238px;fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:1.54528999;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 220.91497,266.9035 -34.89789,105.85211 38.2284,128.58643 H 161.2555 L 136.63873,400.84769 V 501.34204 H 75.676021 V 266.9035 h 60.962709 v 91.08205 l 27.07845,-91.08205 z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.7 KiB

View file

@ -0,0 +1,67 @@
<template>
<v-container fluid>
<v-row justify="start">
<card-series v-for="s in series"
:key="s.id"
justify-self="start"
:series="s"
class="ma-3"
></card-series>
</v-row>
</v-container>
</template>
<script lang="ts">
import CardSeries from '@/components/CardSeries.vue'
import Vue from 'vue'
export default Vue.extend({
name: 'BrowseLibraries',
components: { CardSeries },
data: () => {
return {
series: [] as SeriesDto[],
seriesPage: {} as Page<SeriesDto>
}
},
props: {
libraryId: {
type: Number,
required: true
}
},
watch: {
libraryId (val) {
this.series = []
this.seriesPage = {} as Page<SeriesDto>
this.loadSeries()
}
},
mounted (): void {
this.loadSeries()
},
methods: {
async loadSeries () {
this.seriesPage = await this.$komgaSeries.getSeries(this.libraryId)
this.series = this.seriesPage.content
},
async loadNextPage () {
if (!this.seriesPage.last) {
const pageRequest = {
page: this.seriesPage.number + 1
} as PageRequest
this.seriesPage = await this.$komgaSeries.getSeries(this.libraryId, pageRequest)
this.series = this.series.concat(this.seriesPage.content)
}
}
}
})
</script>
<style scoped>
</style>

View file

@ -0,0 +1,43 @@
<template>
<v-card width="210px"
>
<v-img
:src="getThumbnailUrl()"
lazy-src="../assets/cover.png"
max-width="210px"
height="300px"
>
</v-img>
<v-card-title>{{ series.name }}</v-card-title>
</v-card>
</template>
<script lang="ts">
import Vue from 'vue'
export default Vue.extend({
name: 'CardSeries',
data: () => {
return {
baseURL: process.env.VUE_APP_KOMGA_API_URL ? process.env.VUE_APP_KOMGA_API_URL : window.location.origin
}
},
props: {
series: {
type: Object as () => SeriesDto,
required: true
}
},
methods: {
getThumbnailUrl () {
return `${this.baseURL}/api/v1/series/${this.series.id}/thumbnail`
}
}
})
</script>
<style scoped>
</style>

View file

@ -5,6 +5,7 @@ import App from './App.vue'
import httpPlugin from './plugins/http.plugin'
import komgaFileSystem from './plugins/komga-filesystem.plugin'
import komgaLibraries from './plugins/komga-libraries.plugin'
import komgaSeries from './plugins/komga-series.plugin'
import komgaUsers from './plugins/komga-users.plugin'
import vuetify from './plugins/vuetify'
import router from './router'
@ -14,6 +15,7 @@ Vue.use(Vuelidate)
Vue.use(httpPlugin)
Vue.use(komgaFileSystem, { http: Vue.prototype.$http })
Vue.use(komgaSeries, { http: Vue.prototype.$http })
Vue.use(komgaUsers, { store: store, http: Vue.prototype.$http })
Vue.use(komgaLibraries, { store: store, http: Vue.prototype.$http })

View file

@ -0,0 +1,17 @@
import KomgaSeriesService from '@/services/komga-series.service'
import { AxiosInstance } from 'axios'
import _Vue from 'vue'
export default {
install (
Vue: typeof _Vue,
{ http }: { http: AxiosInstance }) {
Vue.prototype.$komgaSeries = new KomgaSeriesService(http)
}
}
declare module 'vue/types/vue' {
interface Vue {
$komgaSeries: KomgaSeriesService;
}
}

View file

@ -54,6 +54,12 @@ export default new Router({
path: '/account',
name: 'account',
component: () => import(/* webpackChunkName: "account" */ './components/AccountSettings.vue')
},
{
path: '/browse/:libraryId',
name: 'browse-library',
component: () => import(/* webpackChunkName: "browse" */ './components/BrowseLibraries.vue'),
props: (route) => ({ libraryId: Number(route.params.libraryId) })
}
]
},

View file

@ -0,0 +1,28 @@
import { AxiosInstance } from 'axios'
const qs = require('qs')
const API_SERIES = '/api/v1/series'
export default class KomgaSeriesService {
private http: AxiosInstance;
constructor (http: AxiosInstance) {
this.http = http
}
async getSeries (libraryId?: number, pageRequest?: PageRequest): Promise<Page<SeriesDto>> {
try {
return (await this.http.get(API_SERIES, {
params: { library_id: libraryId, ...pageRequest },
paramsSerializer: params => qs.stringify(params, { indices: false })
})).data
} catch (e) {
let msg = 'An error occurred while trying to retrieve libraries'
if (e.response.data.message) {
msg += `: ${e.response.data.message}`
}
throw new Error(msg)
}
}
}

View file

@ -0,0 +1,6 @@
interface SeriesDto {
id: number,
name: string,
url: string,
lastModified: string
}

View file

@ -0,0 +1,34 @@
interface Page<T> {
content: T[],
pageable: Pageable,
size: number,
number: number,
totalPages: number,
first: boolean,
last: boolean,
numberOfElements: number,
totalElements: number,
empty: boolean
sort: Sort,
}
interface Pageable {
sort: Sort,
offset: number,
pageNumber: number,
pageSize: number,
unpaged: boolean,
paged: boolean
}
interface Sort {
sorted: boolean,
unsorted: boolean,
empty: boolean
}
interface PageRequest {
size?: number,
page?: number,
sort?: string[]
}

View file

@ -54,13 +54,18 @@
</v-list-item-action>
</v-list-item>
<v-list-item v-for="(l, index) in libraries" :key="index" dense>
<v-list-item v-for="(l, index) in libraries"
:key="index"
dense
:to="{name:'browse-library', params: {libraryId: l.id}}"
>
<v-list-item-icon>
</v-list-item-icon>
<v-list-item-content>
<v-tooltip bottom :disabled="!isAdmin">
<template v-slot:activator="{ on }">
<v-list-item-title v-on="on">{{ l.name }}</v-list-item-title>
<v-list-item-title v-on="on">{{ l.name }}
</v-list-item-title>
</template>
<span>{{ l.root }}</span>
</v-tooltip>

View file

@ -1,12 +0,0 @@
module.exports = {
plugins: [
'cypress'
],
env: {
mocha: true,
'cypress/globals': true
},
rules: {
strict: 'off'
}
}

View file

@ -1,24 +0,0 @@
// https://docs.cypress.io/guides/guides/plugins-guide.html
// if you need a custom webpack configuration you can uncomment the following import
// and then use the `file:preprocessor` event
// as explained in the cypress docs
// https://docs.cypress.io/api/plugins/preprocessors-api.html#Examples
/* eslint-disable import/no-extraneous-dependencies, global-require, arrow-body-style */
// const webpack = require('@cypress/webpack-preprocessor')
module.exports = (on, config) => {
// on('file:preprocessor', webpack({
// webpackOptions: require('@vue/cli-service/webpack.config'),
// watchOptions: {}
// }))
return Object.assign({}, config, {
fixturesFolder: 'tests/e2e/fixtures',
integrationFolder: 'tests/e2e/specs',
screenshotsFolder: 'tests/e2e/screenshots',
videosFolder: 'tests/e2e/videos',
supportFile: 'tests/e2e/support/index.js'
})
}

View file

@ -1,25 +0,0 @@
// ***********************************************
// This example commands.js shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add("login", (email, password) => { ... })
//
//
// -- This is a child command --
// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... })
//
//
// -- This is a dual command --
// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... })
//
//
// -- This is will overwrite an existing command --
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })

View file

@ -1,20 +0,0 @@
// ***********************************************************
// This example support/index.js is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
import './commands'
// Alternatively you can use CommonJS syntax:
// require('./commands')