зеркало из https://github.com/nextcloud/photos.git
Added navigation, albums, init tags
Signed-off-by: John Molakvoæ (skjnldsv) <skjnldsv@protonmail.com>
This commit is contained in:
Родитель
9c94a3e10f
Коммит
ae28cc9b2d
|
@ -0,0 +1,24 @@
|
|||
<?php
|
||||
/**
|
||||
* @copyright Copyright (c) 2019 John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
use OCA\Photos\AppInfo\Application;
|
||||
\OC::$server->query(Application::class);
|
|
@ -0,0 +1,27 @@
|
|||
/**
|
||||
* @copyright Copyright (c) 2019 John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
.icon-folder.icon-dark {
|
||||
@include icon-color('folder', 'filetypes', $color-black, 1, true);
|
||||
}
|
||||
|
||||
@include icon-black-white('photos', 'photos', 1);
|
|
@ -1 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" height="32" width="32" version="1.0"><path fill="#fff" d="M2.69 4a.9.9 0 00-.69.88v22.25c0 .46.42.87.88.87h26.25c.45 0 .87-.42.87-.87V5.22C30 4.55 29.47 4 28.97 4zM4 6h24v10l-2-2-6 8-6-6-8 8H4zm5 2a3 3 0 100 6 3 3 0 000-6z"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="32" width="32" version="1"><path d="M2.8 4a1.3 1.3 0 00-1.3 1.3v22.4c0 .6.7 1.3 1.3 1.3h26.4c.6 0 1.3-.7 1.3-1.3V5.3c0-.6-.7-1.3-1.3-1.3zm.7 2h25v19h-25z" fill="#fff"/><circle cx="8.5" cy="11.2" r="3" fill="#fff"/><path d="M26.4 14.5l-4.7 6.2L20 23l-1.6-1.8-4.5-4.6-6 5.7-4.7 4.3h26.2v-8.7z" fill="#fff"/></svg>
|
До Ширина: | Высота: | Размер: 271 B После Ширина: | Высота: | Размер: 359 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" height="32" width="32" version="1"><g transform="translate(-11.5 2.5)"><path d="M20.5 7.5a1 1 0 00-1 1v17c0 .5.6 1 1 1h20c.5 0 1-.5 1-1v-17c0-.4-.5-1-1-1zM21 9h19v14.5H21z"/><circle cx="24.8" cy="13" r="2.3"/><path d="M38.4 15.5L35 20.2 33.6 22l-1.2-1.4-3.5-3.5-4.5 4.3-3.6 3.3h19.9v-6.6zM14.5 2.5a1 1 0 00-1 1v17c0 .5.6 1 1 1h6v-3H15V4h19v3.5h1.5v-4c0-.4-.5-1-1-1h-20z"/></g></svg>
|
После Ширина: | Высота: | Размер: 422 B |
|
@ -28,9 +28,9 @@ use OCP\AppFramework\App;
|
|||
|
||||
class Application extends App {
|
||||
|
||||
const appID = 'photos';
|
||||
const APP_ID = 'photos';
|
||||
|
||||
public function __construct() {
|
||||
parent::__construct(self::appID);
|
||||
parent::__construct(self::APP_ID);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,6 +30,7 @@ use OCP\AppFramework\Controller;
|
|||
use OCP\AppFramework\Http\TemplateResponse;
|
||||
use OCP\EventDispatcher\IEventDispatcher;
|
||||
use OCP\IRequest;
|
||||
use OCP\Util;
|
||||
|
||||
class PageController extends Controller {
|
||||
|
||||
|
@ -58,6 +59,10 @@ class PageController extends Controller {
|
|||
$this->eventDispatcher->dispatch(LoadSidebar::class, new LoadSidebar());
|
||||
$this->eventDispatcher->dispatch(LoadViewer::class, new LoadViewer());
|
||||
|
||||
|
||||
Util::addScript('photos', 'photos');
|
||||
Util::addStyle('photos', 'icons');
|
||||
|
||||
$response = new TemplateResponse($this->appName, 'main');
|
||||
return $response;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"name": "gallery",
|
||||
"name": "photos",
|
||||
"version": "19.0.0",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
|
@ -872,18 +872,18 @@
|
|||
}
|
||||
},
|
||||
"@nextcloud/vue": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@nextcloud/vue/-/vue-1.0.0.tgz",
|
||||
"integrity": "sha512-jYggwGf9so7g9uWP59cLspSo62uN7qX0+T096T/QBxyiEhoa+yspf9+Py/RIDylLacceKPDLTU4AdxELj63ZYQ==",
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@nextcloud/vue/-/vue-1.1.0.tgz",
|
||||
"integrity": "sha512-SGWrNTalT/59vElPnPJZ0xQb9sui1yq5fKRiIA2w81NlAK9JQGq+ADeqdVb8sciVnLGobQJ1qERhpo3pIyOhaQ==",
|
||||
"requires": {
|
||||
"@babel/polyfill": "^7.4.4",
|
||||
"@nextcloud/axios": "^0.4.0",
|
||||
"escape-html": "^1.0.3",
|
||||
"hammerjs": "^2.0.8",
|
||||
"md5": "^2.2.1",
|
||||
"v-click-outside": "^2.1.4",
|
||||
"v-tooltip": "^2.0.0-rc.33",
|
||||
"vue": "^2.6.7",
|
||||
"vue-click-outside": "^1.0.7",
|
||||
"vue-color": "^2.7.0",
|
||||
"vue-multiselect": "^2.1.3",
|
||||
"vue-visible": "^1.0.2",
|
||||
|
@ -9307,6 +9307,11 @@
|
|||
"integrity": "sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ==",
|
||||
"dev": true
|
||||
},
|
||||
"v-click-outside": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/v-click-outside/-/v-click-outside-2.1.5.tgz",
|
||||
"integrity": "sha512-VPNCOTZK6WZy73lcWc+R7IW1uaBFEO3/Csrs5CzWVOdvE30V8Y1+BE/BtTlcEmeDGx0eqdE7bSCg55Jj37PMJg=="
|
||||
},
|
||||
"v-tooltip": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/v-tooltip/-/v-tooltip-2.0.2.tgz",
|
||||
|
@ -9382,11 +9387,6 @@
|
|||
"resolved": "https://registry.npmjs.org/vue/-/vue-2.6.10.tgz",
|
||||
"integrity": "sha512-ImThpeNU9HbdZL3utgMCq0oiMzAkt1mcgy3/E6zWC/G6AaQoeuFdsl9nDhTDU3X1R6FK7nsIUuRACVcjI+A2GQ=="
|
||||
},
|
||||
"vue-click-outside": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/vue-click-outside/-/vue-click-outside-1.0.7.tgz",
|
||||
"integrity": "sha1-zdKxYF48SUR4TheU6uShKg9wC9Y="
|
||||
},
|
||||
"vue-color": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/vue-color/-/vue-color-2.7.0.tgz",
|
||||
|
@ -9506,9 +9506,9 @@
|
|||
"integrity": "sha512-yaX2its9XAJKGuQqf7LsiZHHSkxsIK8rmCOQOvEGEoF41blKRK8qr9my4qYoD6ikdLss4n8tKqYBecmaY0+WJg=="
|
||||
},
|
||||
"vue2-datepicker": {
|
||||
"version": "2.13.2",
|
||||
"resolved": "https://registry.npmjs.org/vue2-datepicker/-/vue2-datepicker-2.13.2.tgz",
|
||||
"integrity": "sha512-bgtCdSTpFJogL37A5n2HnNPkyKVi0WTiM2+H+fYTHVYbRpSyNaPQ1Kj86A6tx3T14cv6qq4Oo8MrCxXiarDx2w==",
|
||||
"version": "2.13.3",
|
||||
"resolved": "https://registry.npmjs.org/vue2-datepicker/-/vue2-datepicker-2.13.3.tgz",
|
||||
"integrity": "sha512-kAiTpCLlDC88mMTW5OqhlME0ZSB1fJKlHbKSEryPIi3lRJWHn4BlRSvGUTnSmBUr/5Qidma7Pxei9vih9Luicw==",
|
||||
"requires": {
|
||||
"fecha": "^2.3.3"
|
||||
}
|
||||
|
|
|
@ -35,7 +35,7 @@
|
|||
"@nextcloud/axios": "^0.5.0",
|
||||
"@nextcloud/l10n": "^0.2.1",
|
||||
"@nextcloud/router": "^0.1.0",
|
||||
"@nextcloud/vue": "^1.0.0",
|
||||
"@nextcloud/vue": "^1.1.0",
|
||||
"cdav-library": "git+https://github.com/nextcloud/cdav-library.git",
|
||||
"path-posix": "^1.0.0",
|
||||
"qs": "^6.9.0",
|
||||
|
|
|
@ -22,6 +22,17 @@
|
|||
|
||||
<template>
|
||||
<Content app-name="photos">
|
||||
<AppNavigation>
|
||||
<AppNavigationItem :to="{name: 'root'}"
|
||||
class="app-navigation__photos"
|
||||
:title="t('photos', 'Your photos')"
|
||||
icon="icon-photos" />
|
||||
<AppNavigationItem to="/favorites" :title="t('photos', 'Favorites')" icon="icon-favorite" />
|
||||
<AppNavigationItem :to="{name: 'albums'}" :title="t('photos', 'Your albums')" icon="icon-files-dark" />
|
||||
<AppNavigationItem :to="{name: 'shared'}" :title="t('photos', 'Shared albums')" icon="icon-share" />
|
||||
<AppNavigationItem :to="{name: 'tags'}" :title="t('photos', 'Tags')" icon="icon-tag" />
|
||||
<AppNavigationItem :to="{name: 'maps'}" :title="t('photos', 'Locations')" icon="icon-address" />
|
||||
</AppNavigation>
|
||||
<AppContent :class="{ 'icon-loading': loading }">
|
||||
<router-view v-show="!loading" :loading.sync="loading" />
|
||||
|
||||
|
@ -34,6 +45,8 @@
|
|||
<script>
|
||||
import Content from '@nextcloud/vue/dist/Components/Content'
|
||||
import AppContent from '@nextcloud/vue/dist/Components/AppContent'
|
||||
import AppNavigation from '@nextcloud/vue/dist/Components/AppNavigation'
|
||||
import AppNavigationItem from '@nextcloud/vue/dist/Components/AppNavigationItem'
|
||||
import svgplaceholder from './assets/img-placeholder.svg'
|
||||
|
||||
export default {
|
||||
|
@ -41,6 +54,8 @@ export default {
|
|||
components: {
|
||||
Content,
|
||||
AppContent,
|
||||
AppNavigation,
|
||||
AppNavigationItem,
|
||||
},
|
||||
data: function() {
|
||||
return {
|
||||
|
@ -50,3 +65,8 @@ export default {
|
|||
},
|
||||
}
|
||||
</script>
|
||||
<style lang="scss" scoped>
|
||||
.app-navigation__photos::v-deep .app-navigation-entry-icon.icon-photos {
|
||||
background-size: 20px;
|
||||
}
|
||||
</style>
|
|
@ -23,7 +23,7 @@
|
|||
<template>
|
||||
<router-link :class="{'folder--clear': isEmpty}"
|
||||
class="folder"
|
||||
:to="folder.filename"
|
||||
:to="to"
|
||||
:aria-label="ariaLabel">
|
||||
<transition name="fade">
|
||||
<div v-show="loaded"
|
||||
|
@ -39,8 +39,8 @@
|
|||
</transition>
|
||||
<div
|
||||
class="folder-name">
|
||||
<span :class="{'icon-white': !isEmpty}"
|
||||
class="folder-name__icon icon-folder"
|
||||
<span :class="[!isEmpty ? 'icon-white' : 'icon-dark', icon]"
|
||||
class="folder-name__icon"
|
||||
role="img" />
|
||||
<p :id="ariaUuid" class="folder-name__name">
|
||||
{{ folder.basename }}
|
||||
|
@ -66,6 +66,10 @@ export default {
|
|||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: 'icon-folder',
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
|
@ -106,6 +110,20 @@ export default {
|
|||
ariaLabel() {
|
||||
return t('photos', 'Open the "{name}" sub-directory', { name: this.folder.basename })
|
||||
},
|
||||
|
||||
/**
|
||||
* We do not want encoded slashes when browsing by folder
|
||||
* so we generate a new valid route object, get the final url back
|
||||
* decode it and use it as a direct string, which vue-router
|
||||
* does not encode afterwards
|
||||
*/
|
||||
to() {
|
||||
const route = Object.assign({}, this.$route, {
|
||||
// always remove first slash
|
||||
params: { path: this.folder.filename.substr(1) }
|
||||
});
|
||||
return decodeURIComponent(this.$router.resolve(route).resolved.path)
|
||||
},
|
||||
},
|
||||
|
||||
async created() {
|
||||
|
@ -215,6 +233,9 @@ $name-height: 1.2rem;
|
|||
.folder {
|
||||
// if no img, let's display the folder icon as default black
|
||||
&--clear {
|
||||
.folder-name__icon {
|
||||
opacity: .3;
|
||||
}
|
||||
.folder-name__name {
|
||||
color: var(--color-main-text);
|
||||
text-shadow: 0 0 8px var(--color-main-background);
|
||||
|
|
|
@ -0,0 +1,87 @@
|
|||
<!--
|
||||
- @copyright Copyright (c) 2019 John Molakvoæ <skjnldsv@protonmail.com>
|
||||
-
|
||||
- @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||
-
|
||||
- @license GNU AGPL version 3 or any later version
|
||||
-
|
||||
- This program is free software: you can redistribute it and/or modify
|
||||
- it under the terms of the GNU Affero General Public License as
|
||||
- published by the Free Software Foundation, either version 3 of the
|
||||
- License, or (at your option) any later version.
|
||||
-
|
||||
- This program is distributed in the hope that it will be useful,
|
||||
- but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
- GNU Affero General Public License for more details.
|
||||
-
|
||||
- You should have received a copy of the GNU Affero General Public License
|
||||
- along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-
|
||||
-->
|
||||
|
||||
<template>
|
||||
<!-- Folder content -->
|
||||
<transition-group
|
||||
class="photos-grid"
|
||||
role="grid"
|
||||
name="list"
|
||||
tag="div">
|
||||
<slot />
|
||||
<div key="footer" role="none" class="photos-grid__footer-spacer" />
|
||||
</transition-group>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'Grid',
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.photos-grid {
|
||||
display: grid;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
grid-template-columns: repeat(10, 1fr);
|
||||
position: relative;
|
||||
|
||||
// always put one more row of grid for the spacer
|
||||
&__footer-spacer {
|
||||
// always add one row, so placing it on the first
|
||||
// column will always add one more
|
||||
grid-column: 1;
|
||||
// same height as the width
|
||||
padding-bottom: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.list-move {
|
||||
transition: transform var(--animation-quick);
|
||||
}
|
||||
|
||||
// TODO: use mixins/GridSizes as soon as node-sass supports it
|
||||
// needs node-sass 5.0 (with libsass 3.6)
|
||||
// https://github.com/sass/node-sass/pull/2312
|
||||
$previous: 0;
|
||||
@each $size, $config in get('sizes') {
|
||||
$count: map-get($config, 'count');
|
||||
$marginTop: map-get($config, 'marginTop');
|
||||
$marginW: map-get($config, 'marginW');
|
||||
|
||||
// if this is the last entry, only use min-width
|
||||
$rule: '(min-width: #{$previous}px) and (max-width: #{$size}px)';
|
||||
@if $size == 'max' {
|
||||
$rule: '(min-width: #{$previous}px)';
|
||||
}
|
||||
|
||||
@media #{$rule} {
|
||||
.photos-grid {
|
||||
padding: #{$marginTop}px #{$marginW}px #{$marginW}px #{$marginW}px;
|
||||
grid-template-columns: repeat($count, 1fr);
|
||||
}
|
||||
}
|
||||
$previous: $size;
|
||||
}
|
||||
</style>
|
|
@ -89,11 +89,25 @@ export default {
|
|||
}
|
||||
return t('photos', 'Back to {folder}', { folder: this.parentName })
|
||||
},
|
||||
|
||||
/**
|
||||
* We do not want encoded slashes when browsing by folder
|
||||
* so we generate a new valid route object, get the final url back
|
||||
* decode it and use it as a direct string, which vue-router
|
||||
* does not encode afterwards
|
||||
*/
|
||||
to() {
|
||||
const route = Object.assign({}, this.$route, {
|
||||
// always remove first slash
|
||||
params: { path: this.parentPath.substr(1) }
|
||||
});
|
||||
return decodeURIComponent(this.$router.resolve(route).resolved.path)
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
folderUp() {
|
||||
this.$router.push(this.parentPath)
|
||||
this.$router.push(this.to)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -26,7 +26,7 @@ import { sync } from 'vuex-router-sync'
|
|||
import { translate, translatePlural } from '@nextcloud/l10n'
|
||||
import Vue from 'vue'
|
||||
|
||||
import Gallery from './Gallery'
|
||||
import Photos from './Photos'
|
||||
import router from './router'
|
||||
import store from './store'
|
||||
|
||||
|
@ -49,8 +49,8 @@ Vue.prototype.n = translatePlural
|
|||
export default new Vue({
|
||||
el: '#content',
|
||||
// eslint-disable-next-line vue/match-component-file-name
|
||||
name: 'GalleryRoot',
|
||||
name: 'PhotosRoot',
|
||||
router,
|
||||
store,
|
||||
render: h => h(Gallery),
|
||||
render: h => h(Photos),
|
||||
})
|
||||
|
|
|
@ -24,10 +24,17 @@ import { generateUrl } from '@nextcloud/router'
|
|||
import Router from 'vue-router'
|
||||
import Vue from 'vue'
|
||||
|
||||
import Grid from '../views/Grid'
|
||||
import Albums from '../views/Albums'
|
||||
import Tags from '../views/Tags'
|
||||
|
||||
Vue.use(Router)
|
||||
|
||||
// shortcut to properly format the path prop
|
||||
const props = route => ({
|
||||
// always lead current path with a slash
|
||||
path: `/${route.params.path ? route.params.path : ''}`,
|
||||
})
|
||||
|
||||
export default new Router({
|
||||
mode: 'history',
|
||||
// if index.php is in the url AND we got this far, then it's working:
|
||||
|
@ -37,20 +44,65 @@ export default new Router({
|
|||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
component: Grid,
|
||||
props: route => ({
|
||||
// always lead current path with a slash
|
||||
path: `/${route.params.path ? route.params.path : ''}`,
|
||||
}),
|
||||
component: Albums,
|
||||
name: 'root',
|
||||
},
|
||||
{
|
||||
path: '/albums',
|
||||
component: Albums,
|
||||
name: 'albums',
|
||||
props,
|
||||
children: [
|
||||
{
|
||||
path: ':path*',
|
||||
name: 'path',
|
||||
component: Grid,
|
||||
component: Albums,
|
||||
},
|
||||
],
|
||||
},
|
||||
{ path: '*', redirect: { name: 'root' } },
|
||||
{
|
||||
path: '/shared',
|
||||
component: Albums,
|
||||
name: 'shared',
|
||||
props,
|
||||
children: [
|
||||
{
|
||||
path: ':path*',
|
||||
name: 'path',
|
||||
component: Albums,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/favorites',
|
||||
component: Tags,
|
||||
name: 'favorites',
|
||||
props,
|
||||
children: [
|
||||
{
|
||||
path: ':path*',
|
||||
name: 'path',
|
||||
component: Tags,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/tags',
|
||||
component: Tags,
|
||||
name: 'tags',
|
||||
props,
|
||||
children: [
|
||||
{
|
||||
path: ':path*',
|
||||
name: 'path',
|
||||
component: Tags,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: '/maps',
|
||||
name: 'maps',
|
||||
redirect: '',
|
||||
},
|
||||
],
|
||||
})
|
||||
|
|
|
@ -24,14 +24,13 @@ import webdav from 'webdav'
|
|||
import axios from '@nextcloud/axios'
|
||||
import parseUrl from 'url-parse'
|
||||
import { generateRemoteUrl } from '@nextcloud/router'
|
||||
import { getCurrentUser } from '@nextcloud/auth'
|
||||
|
||||
// force our axios
|
||||
const patcher = webdav.getPatcher()
|
||||
patcher.patch('request', axios)
|
||||
|
||||
// init webdav client
|
||||
const remote = generateRemoteUrl(`dav/files/${getCurrentUser().uid}`)
|
||||
const remote = generateRemoteUrl(`dav`)
|
||||
const client = webdav.createClient(remote)
|
||||
|
||||
export const remotePath = parseUrl(remote).pathname
|
||||
|
|
|
@ -20,12 +20,14 @@
|
|||
*
|
||||
*/
|
||||
|
||||
import { getCurrentUser } from '@nextcloud/auth'
|
||||
import { getSingleValue, getValueForKey, parseXML, propsToStat } from 'webdav/dist/interface/dav'
|
||||
import { handleResponseCode, processResponsePayload } from 'webdav/dist/response'
|
||||
import { normaliseHREF, normalisePath } from 'webdav/dist/url'
|
||||
import client, { remotePath } from './DavClient'
|
||||
import pathPosix from 'path-posix'
|
||||
import request from './DavRequest'
|
||||
import parseFile from '../utils/ParseFile'
|
||||
|
||||
/**
|
||||
* List files from a folder and filter out unwanted mimes
|
||||
|
@ -46,6 +48,8 @@ export default async function(path, options) {
|
|||
details: true,
|
||||
}, options)
|
||||
|
||||
const prefixPath = `/files/${getCurrentUser().uid}`
|
||||
|
||||
/**
|
||||
* Fetch listing
|
||||
*
|
||||
|
@ -54,7 +58,7 @@ export default async function(path, options) {
|
|||
* see https://github.com/perry-mitchell/webdav-client/blob/baf858a4856d44ae19ac12cb10c469b3e6c41ae4/source/interface/directoryContents.js#L11
|
||||
*/
|
||||
let response = null
|
||||
const { data } = await client.customRequest(path, options)
|
||||
const { data } = await client.customRequest(prefixPath + path, options)
|
||||
.then(handleResponseCode)
|
||||
.then(res => {
|
||||
response = res
|
||||
|
@ -64,14 +68,7 @@ export default async function(path, options) {
|
|||
.then(result => getDirectoryFiles(result, remotePath, options.details))
|
||||
.then(files => processResponsePayload(response, files, options.details))
|
||||
|
||||
const list = data
|
||||
.map(entry => {
|
||||
return Object.assign({
|
||||
id: parseInt(entry.props.fileid),
|
||||
isFavorite: entry.props.favorite !== '0',
|
||||
hasPreview: entry.props['has-preview'] !== 'false',
|
||||
}, entry)
|
||||
})
|
||||
const list = data.map(data => parseFile(data, prefixPath))
|
||||
|
||||
// filter all the files and folders
|
||||
let folder = {}
|
||||
|
@ -91,6 +88,15 @@ export default async function(path, options) {
|
|||
return { folder, folders, files }
|
||||
}
|
||||
|
||||
/**
|
||||
* Modified function to include the root requested folder
|
||||
* Into the returned data
|
||||
*
|
||||
* @param {Object} result the request result
|
||||
* @param {string} serverBasePath server base path
|
||||
* @param {boolean} isDetailed detailed request
|
||||
* @returns {Array}
|
||||
*/
|
||||
function getDirectoryFiles(result, serverBasePath, isDetailed = false) {
|
||||
const serverBase = pathPosix.join(serverBasePath, '/')
|
||||
// Extract the response items (directory contents)
|
||||
|
|
|
@ -20,8 +20,10 @@
|
|||
*
|
||||
*/
|
||||
|
||||
import { getCurrentUser } from '@nextcloud/auth'
|
||||
import client from './DavClient'
|
||||
import request from './DavRequest'
|
||||
import parseFile from '../utils/ParseFile'
|
||||
|
||||
/**
|
||||
* List files from a folder and filter out unwanted mimes
|
||||
|
@ -33,17 +35,13 @@ export default async function(path) {
|
|||
// getDirectoryContents doesn't accept / for root
|
||||
const fixedPath = path === '/' ? '' : path
|
||||
|
||||
const prefixPath = `/files/${getCurrentUser().uid}`
|
||||
|
||||
// fetch listing
|
||||
const response = await client.stat(fixedPath, {
|
||||
const response = await client.stat(prefixPath + fixedPath, {
|
||||
data: request,
|
||||
details: true,
|
||||
})
|
||||
|
||||
const entry = response.data
|
||||
return Object.assign({
|
||||
id: parseInt(entry.props.fileid),
|
||||
isFavorite: entry.props.favorite !== '0',
|
||||
hasPreview: entry.props['has-preview'] !== 'false',
|
||||
}, entry)
|
||||
|
||||
return parseFile(response.data, prefixPath)
|
||||
}
|
||||
|
|
|
@ -20,8 +20,10 @@
|
|||
*
|
||||
*/
|
||||
|
||||
import client from './DavClient'
|
||||
import { generateRemoteUrl } from '@nextcloud/router'
|
||||
import { getCurrentUser } from '@nextcloud/auth'
|
||||
import client from './DavClient'
|
||||
import parseFile from '../utils/ParseFile'
|
||||
|
||||
/**
|
||||
* List files from a folder and filter out unwanted mimes
|
||||
|
@ -35,7 +37,7 @@ export default async function() {
|
|||
headers: {
|
||||
'content-Type': 'text/xml',
|
||||
},
|
||||
url: '/remote.php/dav/',
|
||||
url: generateRemoteUrl(`dav`),
|
||||
data: `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<d:searchrequest xmlns:d="DAV:"
|
||||
xmlns:oc="http://owncloud.org/ns"
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
/**
|
||||
* @copyright Copyright (c) 2019 John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
import client from './DavClient'
|
||||
import { generateRemoteUrl } from '@nextcloud/router'
|
||||
|
||||
/**
|
||||
* List files from a folder and filter out unwanted mimes
|
||||
*
|
||||
* @returns {Array} the file list
|
||||
*/
|
||||
export default async function() {
|
||||
const response = await client.getDirectoryContents('/systemtags/', {
|
||||
data: `<?xml version="1.0"?>
|
||||
<d:propfind xmlns:d="DAV:"
|
||||
xmlns:oc="http://owncloud.org/ns">
|
||||
<d:prop>
|
||||
<oc:id />
|
||||
<oc:display-name />
|
||||
<oc:user-visible />
|
||||
<oc:user-assignable />
|
||||
<oc:can-assign />
|
||||
</d:prop>
|
||||
</d:propfind>`,
|
||||
details: true,
|
||||
})
|
||||
|
||||
console.info(response)
|
||||
|
||||
const entry = response.data
|
||||
return Object.assign({
|
||||
id: parseInt(entry.props.fileid),
|
||||
isFavorite: entry.props.favorite !== '0',
|
||||
hasPreview: entry.props['has-preview'] !== 'false',
|
||||
}, entry)
|
||||
|
||||
}
|
|
@ -25,12 +25,14 @@ import Vuex, { Store } from 'vuex'
|
|||
|
||||
import files from './files'
|
||||
import folders from './folders'
|
||||
import systemtags from './systemtags'
|
||||
|
||||
Vue.use(Vuex)
|
||||
export default new Store({
|
||||
modules: {
|
||||
files,
|
||||
folders,
|
||||
systemtags,
|
||||
},
|
||||
|
||||
strict: process.env.NODE_ENV !== 'production',
|
||||
|
|
|
@ -0,0 +1,99 @@
|
|||
/**
|
||||
* @copyright Copyright (c) 2019 John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
import Vue from 'vue'
|
||||
|
||||
const state = {
|
||||
paths: {},
|
||||
tags: {},
|
||||
}
|
||||
|
||||
const mutations = {
|
||||
/**
|
||||
* Index folders paths and ids
|
||||
*
|
||||
* @param {Object} state vuex state
|
||||
* @param {Object} data destructuring object
|
||||
* @param {number} data.id current folder id
|
||||
* @param {Array} data.files list of files
|
||||
*/
|
||||
updateTags(state, { id, files }) {
|
||||
if (files.length > 0) {
|
||||
// sort by last modified
|
||||
const list = files.sort((a, b) => {
|
||||
return new Date(b.lastmod).getTime() - new Date(a.lastmod).getTime()
|
||||
})
|
||||
|
||||
// Set folder list
|
||||
Vue.set(state.tags, id, list.map(file => file.id))
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Index folders paths and ids
|
||||
*
|
||||
* @param {Object} state vuex state
|
||||
* @param {Object} data destructuring object
|
||||
* @param {string} data.path path of this folder
|
||||
* @param {number} data.id id of this folder
|
||||
*/
|
||||
addPath(state, { path, id }) {
|
||||
Vue.set(state.paths, path, id)
|
||||
},
|
||||
}
|
||||
|
||||
const getters = {
|
||||
tags: state => state.tags,
|
||||
tag: state => id => state.tags[id],
|
||||
tagId: state => path => state.paths[path],
|
||||
}
|
||||
|
||||
const actions = {
|
||||
/**
|
||||
* Update files and folders
|
||||
*
|
||||
* @param {Object} context vuex context
|
||||
* @param {Object} data destructuring object
|
||||
* @param {number} data.id current folder id
|
||||
* @param {Array} data.files list of files
|
||||
* @param {Array} data.folders list of folders
|
||||
*/
|
||||
updateTags(context, { id, files, folders }) {
|
||||
context.commit('updateTags', { id, files })
|
||||
|
||||
// then add each folders path indexes
|
||||
folders.forEach(folder => context.commit('addPath', { path: folder.filename, id: folder.id }))
|
||||
},
|
||||
|
||||
/**
|
||||
* Index folders paths and ids
|
||||
*
|
||||
* @param {Object} context vuex context
|
||||
* @param {Object} data destructuring object
|
||||
* @param {string} data.path path of this folder
|
||||
* @param {number} data.id id of this folder
|
||||
*/
|
||||
addPath(context, { path, id }) {
|
||||
context.commit('addPath', { path, id })
|
||||
},
|
||||
}
|
||||
|
||||
export default { state, mutations, getters, actions }
|
|
@ -0,0 +1,37 @@
|
|||
/**
|
||||
* @copyright Copyright (c) 2019 John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* Format a file into a usable fileinfo object
|
||||
*
|
||||
* @param {Object} fileData file data returned by the webdav lib
|
||||
* @param {String} prefixPath path to substract from the files
|
||||
* @returns {Object}
|
||||
*/
|
||||
export default function(fileData, prefixPath = '') {
|
||||
const filename = fileData.filename.replace(prefixPath, '/').replace(/^\/\//, '/')
|
||||
return Object.assign({
|
||||
id: parseInt(fileData.props.fileid),
|
||||
isFavorite: fileData.props.favorite !== '0',
|
||||
hasPreview: fileData.props['has-preview'] !== 'false',
|
||||
}, fileData, { filename })
|
||||
}
|
|
@ -33,16 +33,11 @@
|
|||
</EmptyContent>
|
||||
|
||||
<!-- Folder content -->
|
||||
<transition-group v-else
|
||||
class="photos-grid"
|
||||
role="grid"
|
||||
name="list"
|
||||
tag="div">
|
||||
<Grid v-else>
|
||||
<Navigation v-if="folder" key="navigation" v-bind="folder" />
|
||||
<Folder v-for="dir in folderList" :key="dir.id" :folder="dir" />
|
||||
<File v-for="file in fileList" :key="file.id" v-bind="file" />
|
||||
<div key="footer" role="none" class="photos-grid__footer-spacer" />
|
||||
</transition-group>
|
||||
</Grid>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
@ -55,16 +50,18 @@ import getPictures from '../services/FileList'
|
|||
import EmptyContent from './EmptyContent'
|
||||
import Folder from '../components/Folder'
|
||||
import File from '../components/File'
|
||||
import Grid from '../components/Grid'
|
||||
import Navigation from '../components/Navigation'
|
||||
|
||||
import cancelableRequest from '../utils/CancelableRequest'
|
||||
|
||||
export default {
|
||||
name: 'Grid',
|
||||
name: 'Albums',
|
||||
components: {
|
||||
EmptyContent,
|
||||
File,
|
||||
Folder,
|
||||
Grid,
|
||||
Navigation,
|
||||
},
|
||||
props: {
|
||||
|
@ -205,50 +202,3 @@ export default {
|
|||
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
.photos-grid {
|
||||
display: grid;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
grid-template-columns: repeat(10, 1fr);
|
||||
position: relative;
|
||||
// always put one more row of grid for the spacer
|
||||
&__footer-spacer {
|
||||
// always add one row, so placing it on the first
|
||||
// column will always add one more
|
||||
grid-column: 1;
|
||||
// same height as the width
|
||||
padding-bottom: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.list-move {
|
||||
transition: transform var(--animation-quick);
|
||||
}
|
||||
|
||||
// TODO: use mixins/GridSizes as soon as node-sass supports it
|
||||
// needs node-sass 5.0 (with libsass 3.6)
|
||||
// https://github.com/sass/node-sass/pull/2312
|
||||
$previous: 0;
|
||||
@each $size, $config in get('sizes') {
|
||||
$count: map-get($config, 'count');
|
||||
$marginTop: map-get($config, 'marginTop');
|
||||
$marginW: map-get($config, 'marginW');
|
||||
|
||||
// if this is the last entry, only use min-width
|
||||
$rule: '(min-width: #{$previous}px) and (max-width: #{$size}px)';
|
||||
@if $size == 'max' {
|
||||
$rule: '(min-width: #{$previous}px)';
|
||||
}
|
||||
|
||||
@media #{$rule} {
|
||||
.photos-grid {
|
||||
padding: #{$marginTop}px #{$marginW}px #{$marginW}px #{$marginW}px;
|
||||
grid-template-columns: repeat($count, 1fr);
|
||||
}
|
||||
}
|
||||
$previous: $size;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,111 @@
|
|||
<!--
|
||||
- @copyright Copyright (c) 2019 John Molakvoæ <skjnldsv@protonmail.com>
|
||||
-
|
||||
- @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||
-
|
||||
- @license GNU AGPL version 3 or any later version
|
||||
-
|
||||
- This program is free software: you can redistribute it and/or modify
|
||||
- it under the terms of the GNU Affero General Public License as
|
||||
- published by the Free Software Foundation, either version 3 of the
|
||||
- License, or (at your option) any later version.
|
||||
-
|
||||
- This program is distributed in the hope that it will be useful,
|
||||
- but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
- GNU Affero General Public License for more details.
|
||||
-
|
||||
- You should have received a copy of the GNU Affero General Public License
|
||||
- along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
-
|
||||
-->
|
||||
|
||||
<template>
|
||||
<!-- Errors handlers-->
|
||||
<!-- <EmptyContent v-if="error === 404" illustration-name="folder">
|
||||
{{ t('photos', 'This folder does not exists') }}
|
||||
</EmptyContent>
|
||||
<EmptyContent v-else-if="error">
|
||||
{{ t('photos', 'An error occurred') }}
|
||||
</EmptyContent>
|
||||
<EmptyContent v-else-if="!loading && isEmpty" illustration-name="empty">
|
||||
{{ t('photos', 'This folder does not contain pictures') }}
|
||||
</EmptyContent> -->
|
||||
|
||||
<!-- Folder content -->
|
||||
<!-- <Grid v-else>
|
||||
<Navigation v-if="folder" key="navigation" v-bind="folder" />
|
||||
<Folder v-for="dir in folderList" :key="dir.id" :folder="dir" />
|
||||
<File v-for="file in fileList" :key="file.id" v-bind="file" />
|
||||
</Grid> -->
|
||||
<span>Test</span>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex'
|
||||
|
||||
import getSystemTags from '../services/SystemTags'
|
||||
|
||||
import EmptyContent from './EmptyContent'
|
||||
import Folder from '../components/Folder'
|
||||
import File from '../components/File'
|
||||
import Grid from '../components/Grid'
|
||||
import Navigation from '../components/Navigation'
|
||||
|
||||
import cancelableRequest from '../utils/CancelableRequest'
|
||||
|
||||
export default {
|
||||
name: 'Tags',
|
||||
components: {
|
||||
EmptyContent,
|
||||
File,
|
||||
Folder,
|
||||
Grid,
|
||||
Navigation,
|
||||
},
|
||||
props: {
|
||||
path: {
|
||||
type: String,
|
||||
default: '/',
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
error: null,
|
||||
cancelRequest: () => {},
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
// global lists
|
||||
...mapGetters([
|
||||
'files',
|
||||
'tags',
|
||||
]),
|
||||
},
|
||||
|
||||
watch: {
|
||||
path(path) {
|
||||
console.debug('changed:', path)
|
||||
this.fetchFolderContent()
|
||||
},
|
||||
},
|
||||
|
||||
async beforeMount() {
|
||||
console.debug('beforemount: GRID')
|
||||
this.fetchFolderContent()
|
||||
},
|
||||
|
||||
methods: {
|
||||
async fetchFolderContent() {
|
||||
await getSystemTags()
|
||||
},
|
||||
},
|
||||
|
||||
}
|
||||
</script>
|
|
@ -1,4 +1,25 @@
|
|||
<?php
|
||||
script('photos', 'photos');
|
||||
/**
|
||||
* @copyright Copyright (c) 2019 John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*
|
||||
*/
|
||||
?>
|
||||
|
||||
<div id="content"></div>
|
||||
|
|
|
@ -14,13 +14,13 @@ module.exports = {
|
|||
path: path.resolve(__dirname, './js'),
|
||||
publicPath: '/js/',
|
||||
filename: `${appName}.js`,
|
||||
chunkFilename: 'chunks/[name]-[hash].js'
|
||||
chunkFilename: 'chunks/[name]-[hash].js',
|
||||
},
|
||||
module: {
|
||||
rules: [
|
||||
{
|
||||
test: /\.css$/,
|
||||
use: ['vue-style-loader', 'css-loader', 'postcss-loader']
|
||||
use: ['vue-style-loader', 'css-loader', 'postcss-loader'],
|
||||
},
|
||||
{
|
||||
test: /\.scss$/,
|
||||
|
@ -32,34 +32,34 @@ module.exports = {
|
|||
loader: 'sass-loader',
|
||||
options: {
|
||||
functions: {
|
||||
'get($keys)': SassGetGridConfig
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
'get($keys)': SassGetGridConfig,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
test: /\.(js|vue)$/,
|
||||
use: 'eslint-loader',
|
||||
exclude: /node_modules/,
|
||||
enforce: 'pre'
|
||||
enforce: 'pre',
|
||||
},
|
||||
{
|
||||
test: /\.vue$/,
|
||||
loader: 'vue-loader',
|
||||
exclude: /node_modules/
|
||||
exclude: /node_modules/,
|
||||
},
|
||||
{
|
||||
test: /\.js$/,
|
||||
loader: 'babel-loader',
|
||||
exclude: /node_modules(?!(\/|\\)(hot-patcher|webdav)(\/|\\))/
|
||||
exclude: /node_modules(?!(\/|\\)(hot-patcher|webdav)(\/|\\))/,
|
||||
},
|
||||
{
|
||||
test: /\.svg$/,
|
||||
// illustrations
|
||||
loader: 'svg-inline-loader'
|
||||
}
|
||||
]
|
||||
loader: 'svg-inline-loader',
|
||||
},
|
||||
],
|
||||
},
|
||||
plugins: [
|
||||
new VueLoaderPlugin(),
|
||||
|
@ -68,12 +68,12 @@ module.exports = {
|
|||
new ModuleReplaceWebpackPlugin({
|
||||
modules: [{
|
||||
test: /request.js/,
|
||||
replace: './src/patchedRequest.js'
|
||||
}]
|
||||
})
|
||||
replace: './src/patchedRequest.js',
|
||||
}],
|
||||
}),
|
||||
],
|
||||
resolve: {
|
||||
extensions: ['*', '.js', '.vue'],
|
||||
symlinks: false
|
||||
}
|
||||
symlinks: false,
|
||||
},
|
||||
}
|
||||
|
|
Загрузка…
Ссылка в новой задаче