Compare commits

...

1 Commits
master ... vue3

Author SHA1 Message Date
Varun Patil c87134b16d Migrate to vue 3 (broken) 2022-12-10 09:18:58 -08:00
49 changed files with 1931 additions and 6063 deletions

6122
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -32,21 +32,22 @@
"@nextcloud/l10n": "^1.6.0",
"@nextcloud/paths": "^2.1.0",
"@nextcloud/sharing": "^0.1.0",
"@nextcloud/vue": "7.1.0",
"@nextcloud/vue": "7.2.0",
"camelcase": "^7.0.0",
"filerobot-image-editor": "^4.3.7",
"justified-layout": "^4.1.0",
"moment": "^2.29.4",
"node-polyfill-webpack-plugin": "^2.0.1",
"path-posix": "^1.0.0",
"photoswipe": "^5.3.4",
"plyr": "^3.7.3",
"reflect-metadata": "^0.1.13",
"video.js": "^7.20.3",
"videojs-contrib-quality-levels": "^2.2.0",
"vue": "^2.7.10",
"videojs-contrib-quality-levels": "^2.2.1",
"vue": "^3.2.45",
"vue-material-design-icons": "^5.1.2",
"vue-router": "^3.5.4",
"vue-virtual-scroller": "1.1.2",
"vue-router": "^4.1.6",
"vue-virtual-scroller": "2.0.0-beta.5",
"webdav": "^4.11.2"
},
"browserslist": [
@ -59,13 +60,19 @@
"devDependencies": {
"@nextcloud/babel-config": "^1.0.0",
"@nextcloud/browserslist-config": "^2.3.0",
"@nextcloud/webpack-vue-config": "^5.4.0",
"@playwright/test": "^1.28.0",
"@playwright/test": "^1.28.1",
"@types/url-parse": "^1.4.8",
"@types/video.js": "^7.3.49",
"playwright": "^1.28.0",
"ts-loader": "^9.4.1",
"typescript": "^4.9.3",
"@types/video.js": "^7.3.50",
"babel-loader": "^9.1.0",
"css-loader": "^6.7.2",
"playwright": "^1.28.1",
"sass": "^1.56.2",
"sass-loader": "^13.2.0",
"style-loader": "^3.3.1",
"ts-loader": "^9.4.2",
"typescript": "^4.9.4",
"vue-loader": "^17.0.1",
"webpack-cli": "^5.0.1",
"workbox-webpack-plugin": "^6.5.4"
}
}

View File

@ -2,8 +2,8 @@
<FirstStart v-if="isFirstStart" />
<NcContent
v-else-if="false"
app-name="memories"
v-else
:class="{
'remove-gap': removeOuterGap,
}"
@ -35,10 +35,14 @@
</div>
</NcAppContent>
</NcContent>
<div class="outer" v-else>
<router-view />
</div>
</template>
<script lang="ts">
import Vue, { defineComponent } from "vue";
import { defineComponent } from "vue";
import NcContent from "@nextcloud/vue/dist/Components/NcContent";
import NcAppContent from "@nextcloud/vue/dist/Components/NcAppContent";
@ -95,16 +99,16 @@ export default defineComponent({
data() {
return {
navItems: [],
metadataComponent: null as Metadata,
metadataComponent: null as any,
};
},
computed: {
ncVersion() {
ncVersion(): number {
const version = (<any>window.OC).config.version.split(".");
return Number(version[0]);
},
recognize() {
recognize(): string | boolean {
if (!this.config_recognizeEnabled) {
return false;
}
@ -115,7 +119,7 @@ export default defineComponent({
return t("memories", "People");
},
facerecognition() {
facerecognition(): string | boolean {
if (!this.config_facerecognitionInstalled) {
return false;
}
@ -126,16 +130,16 @@ export default defineComponent({
return t("memories", "People");
},
isFirstStart() {
isFirstStart(): boolean {
return this.config_timelinePath === "EMPTY";
},
showAlbums() {
showAlbums(): boolean {
return this.config_albumsEnabled;
},
removeOuterGap() {
removeOuterGap(): boolean {
return this.ncVersion >= 25;
},
showNavigation() {
showNavigation(): boolean {
return this.$route.name !== "folder-share";
},
},
@ -174,7 +178,7 @@ export default defineComponent({
if (this.metadataComponent) {
this.metadataComponent.$destroy();
}
this.metadataComponent = new Vue(Metadata);
this.metadataComponent = new Metadata();
// Only mount after we have all the info we need
await this.metadataComponent.update(fileInfo);
this.metadataComponent.$mount(el);
@ -281,9 +285,9 @@ export default defineComponent({
if (globalThis.windowInnerWidth <= 1024) nav?.toggleNavigation(false);
},
doRouteChecks() {
doRouteChecks(): void {
if (this.$route.name === "folder-share") {
this.putFolderShareToken(this.$route.params.token);
this.putFolderShareToken(<string>this.$route.params.token);
}
},

View File

@ -9,7 +9,7 @@
<div class="text">
{{ t("memories", "A better photos experience awaits you") }} <br />
{{
t("memories", "Choose the root folder of your timeline to begin")
t("memories", "Choose the root folder of your timeline to begin")
}}
</div>
@ -160,6 +160,7 @@ export default defineComponent({
transition: opacity 1s ease;
opacity: 0;
&.show {
opacity: 1;
}
@ -174,7 +175,7 @@ export default defineComponent({
width: 100%;
filter: var(--background-invert-if-dark);
> img {
>img {
max-width: calc(100vw - 40px);
}
}

View File

@ -27,12 +27,11 @@
<div class="edit" v-if="field.edit">
<NcActions :inline="1">
<NcActionButton
:aria-label="t('memories', 'Edit')"
@click="field.edit()"
>
<NcActionButton :aria-label="t('memories', 'Edit')" @click="field.edit()">
{{ t("memories", "Edit") }}
<template #icon> <EditIcon :size="20" /> </template>
<template #icon>
<EditIcon :size="20" />
</template>
</NcActionButton>
</NcActions>
</div>
@ -67,6 +66,14 @@ import InfoIcon from "vue-material-design-icons/InformationOutline.vue";
import LocationIcon from "vue-material-design-icons/MapMarker.vue";
import { API } from "../services/API";
interface TopField {
title: string;
subtitle: string[];
icon: any;
href?: string;
edit?: () => void;
};
export default defineComponent({
name: "Metadata",
components: {
@ -78,7 +85,7 @@ export default defineComponent({
data() {
return {
fileInfo: null as IFileInfo,
exif: {} as { [prop: string]: any },
exif: {} as { [prop: string]: any; },
baseInfo: {} as any,
nominatim: null as any,
state: 0,
@ -94,14 +101,8 @@ export default defineComponent({
},
computed: {
topFields() {
let list: {
title: string;
subtitle: string[];
icon: any;
href?: string;
edit?: () => void;
}[] = [];
topFields(): TopField[] {
let list: TopField[] = [];
if (this.dateOriginal) {
list.push({
@ -152,7 +153,7 @@ export default defineComponent({
},
/** Date taken info */
dateOriginal() {
dateOriginal(): moment.Moment | null {
const dt = this.exif["DateTimeOriginal"] || this.exif["CreateDate"];
if (!dt) return null;
@ -162,12 +163,12 @@ export default defineComponent({
return m;
},
dateOriginalStr() {
dateOriginalStr(): string | null {
if (!this.dateOriginal) return null;
return utils.getLongDateStr(this.dateOriginal.toDate(), true);
},
dateOriginalTime() {
dateOriginalTime(): string[] | null {
if (!this.dateOriginal) return null;
// Try to get timezone
@ -182,7 +183,7 @@ export default defineComponent({
},
/** Camera make and model info */
camera() {
camera(): string | null {
const make = this.exif["Make"];
const model = this.exif["Model"];
if (!make || !model) return null;
@ -190,7 +191,7 @@ export default defineComponent({
return `${make} ${model}`;
},
cameraSub() {
cameraSub(): string[] | null {
const f = this.exif["FNumber"] || this.exif["Aperture"];
const s = this.shutterSpeed;
const len = this.exif["FocalLength"];
@ -205,11 +206,11 @@ export default defineComponent({
},
/** Convert shutter speed decimal to 1/x format */
shutterSpeed() {
shutterSpeed(): string | null {
const speed = Number(
this.exif["ShutterSpeedValue"] ||
this.exif["ShutterSpeed"] ||
this.exif["ExposureTime"]
this.exif["ShutterSpeed"] ||
this.exif["ExposureTime"]
);
if (!speed) return null;
@ -221,11 +222,11 @@ export default defineComponent({
},
/** Image info */
imageInfo() {
imageInfo(): string | null {
return this.fileInfo.basename || (<any>this.fileInfo).name;
},
imageInfoSub() {
imageInfoSub(): string[] | null {
let parts = [];
let mp = Number(this.exif["Megapixels"]);
@ -244,7 +245,7 @@ export default defineComponent({
return parts;
},
address() {
address(): string | null {
if (!this.lat || !this.lon) return null;
if (!this.nominatim) return this.t("memories", "Loading …");
@ -263,15 +264,15 @@ export default defineComponent({
}
},
lat() {
lat(): number | null {
return this.exif["GPSLatitude"];
},
lon() {
lon(): number | null {
return this.exif["GPSLongitude"];
},
mapUrl() {
mapUrl(): string | null {
const boxSize = 0.0075;
const bbox = [
this.lon - boxSize,
@ -283,7 +284,7 @@ export default defineComponent({
return `https://www.openstreetmap.org/export/embed.html?bbox=${bbox.join()}&marker=${m}`;
},
mapFullUrl() {
mapFullUrl(): string | null {
return `https://www.openstreetmap.org/?mlat=${this.lat}&mlon=${this.lon}#map=18/${this.lat}/${this.lon}`;
},
},
@ -345,9 +346,11 @@ export default defineComponent({
color: var(--color-text-lighter);
}
}
.edit {
transform: translateX(10px);
}
.text {
display: inline-block;
word-break: break-word;
@ -355,6 +358,7 @@ export default defineComponent({
.subtitle {
font-size: 0.95em;
.part {
margin-right: 5px;
}

View File

@ -1,48 +1,25 @@
<template>
<div
class="scroller"
ref="scroller"
v-bind:class="{
'scrolling-recycler-now': scrollingRecyclerNowTimer,
'scrolling-recycler': scrollingRecyclerTimer,
'scrolling-now': scrollingNowTimer,
scrolling: scrollingTimer,
}"
@mousemove.passive="mousemove"
@mouseleave.passive="mouseleave"
@mousedown.passive="mousedown"
@mouseup.passive="interactend"
@touchmove.prevent="touchmove"
@touchstart.passive="interactstart"
@touchend.passive="interactend"
@touchcancel.passive="interactend"
>
<span
class="cursor st"
ref="cursorSt"
:style="{ transform: `translateY(${cursorY}px)` }"
>
<div class="scroller" ref="scroller" v-bind:class="{
'scrolling-recycler-now': scrollingRecyclerNowTimer,
'scrolling-recycler': scrollingRecyclerTimer,
'scrolling-now': scrollingNowTimer,
scrolling: scrollingTimer,
}" @mousemove.passive="mousemove" @mouseleave.passive="mouseleave" @mousedown.passive="mousedown"
@mouseup.passive="interactend" @touchmove.prevent="touchmove" @touchstart.passive="interactstart"
@touchend.passive="interactend" @touchcancel.passive="interactend">
<span class="cursor st" ref="cursorSt" :style="{ transform: `translateY(${cursorY}px)` }">
</span>
<span
class="cursor hv"
:style="{ transform: `translateY(${hoverCursorY}px)` }"
@touchmove.prevent="touchmove"
@touchstart.passive="interactstart"
@touchend.passive="interactend"
@touchcancel.passive="interactend"
>
<span class="cursor hv" :style="{ transform: `translateY(${hoverCursorY}px)` }" @touchmove.prevent="touchmove"
@touchstart.passive="interactstart" @touchend.passive="interactend" @touchcancel.passive="interactend">
<div class="text">{{ hoverCursorText }}</div>
<div class="icon"><ScrollIcon :size="22" /></div>
<div class="icon">
<ScrollIcon :size="22" />
</div>
</span>
<div
v-for="tick of visibleTicks"
:key="tick.key"
class="tick"
:class="{ dash: !tick.text }"
:style="{ transform: `translateY(calc(${tick.top}px - 50%))` }"
>
<div v-for="tick of visibleTicks" :key="tick.key" class="tick" :class="{ dash: !tick.text }"
:style="{ transform: `translateY(calc(${tick.top}px - 50%))` }">
<span v-if="tick.text">{{ tick.text }}</span>
</div>
</div>
@ -152,7 +129,7 @@ export default defineComponent({
},
/** Recycler scroll event, must be called by timeline */
recyclerScrolled() {
recyclerScrolled(event: Event | null) {
// This isn't a renewing timer, it's a scheduled task
if (this.scrollingRecyclerUpdateTimer) return;
this.scrollingRecyclerUpdateTimer = window.setTimeout(() => {
@ -510,7 +487,7 @@ export default defineComponent({
interactend() {
this.interacting = false;
this.recyclerScrolled(); // make sure final position is correct
this.recyclerScrolled(null); // make sure final position is correct
},
/** Update scroller is being used to scroll recycler */
@ -547,7 +524,7 @@ export default defineComponent({
opacity: 1;
}
> .tick {
>.tick {
pointer-events: none;
position: absolute;
font-size: 0.75em;
@ -566,6 +543,7 @@ export default defineComponent({
background-color: var(--color-main-text);
opacity: 0.15;
display: block;
@include phone {
display: none;
}
@ -578,7 +556,7 @@ export default defineComponent({
}
}
> .cursor {
>.cursor {
position: absolute;
pointer-events: none;
right: 0;
@ -603,17 +581,20 @@ export default defineComponent({
font-size: 0.95em;
font-weight: 600;
> .icon {
>.icon {
display: none;
transform: translate(-16px, 6px);
}
}
}
&.scrolling-recycler-now:not(.scrolling-now) > .cursor {
&.scrolling-recycler-now:not(.scrolling-now)>.cursor {
transition: transform 0.1s linear;
}
&:hover > .cursor {
&:hover>.cursor {
transition: none !important;
&.st {
opacity: 1;
}
@ -623,15 +604,17 @@ export default defineComponent({
@include phone {
// Shift pointer events to hover cursor
pointer-events: none;
.cursor.hv {
pointer-events: all;
}
> .tick {
>.tick {
right: 40px;
}
&:not(.scrolling) {
> .tick {
>.tick {
display: none;
}
}
@ -643,10 +626,12 @@ export default defineComponent({
height: 40px;
width: 70px;
border-radius: 20px;
> .text {
>.text {
display: none;
}
> .icon {
>.icon {
display: block;
}
}

View File

@ -2,31 +2,25 @@
<div>
<div v-if="show" class="top-bar">
<NcActions :inline="1">
<NcActionButton
:aria-label="t('memories', 'Cancel')"
@click="clearSelection()"
>
<NcActionButton :aria-label="t('memories', 'Cancel')" @click="clearSelection()">
{{ t("memories", "Cancel") }}
<template #icon> <CloseIcon :size="20" /> </template>
<template #icon>
<CloseIcon :size="20" />
</template>
</NcActionButton>
</NcActions>
<div class="text">
{{
n("memories", "{n} selected", "{n} selected", size, {
n: size,
})
n("memories", "{n} selected", "{n} selected", size, {
n: size,
})
}}
</div>
<NcActions :inline="1">
<NcActionButton
v-for="action of getActions()"
:key="action.name"
:aria-label="action.name"
close-after-click
@click="click(action)"
>
<NcActionButton v-for="action of getActions()" :key="action.name" :aria-label="action.name" close-after-click
@click="click(action)">
{{ action.name }}
<template #icon>
<component :is="action.icon" :size="20" />
@ -38,11 +32,7 @@
<!-- Selection Modals -->
<EditDate ref="editDate" @refresh="refresh" />
<EditExif ref="editExif" @refresh="refresh" />
<FaceMoveModal
ref="faceMoveModal"
@moved="deletePhotos"
:updateLoading="updateLoading"
/>
<FaceMoveModal ref="faceMoveModal" @moved="deletePhotos" :updateLoading="updateLoading" />
<AddToAlbumModal ref="addToAlbumModal" @added="clearSelection" />
</div>
</template>
@ -103,7 +93,7 @@ export default defineComponent({
},
props: {
heads: Object as PropType<{ [dayid: number]: IHeadRow }>,
heads: Object as PropType<{ [dayid: number]: IHeadRow; }>,
/** List of rows for multi selection */
rows: Array as PropType<IRow[]>,
/** Rows are in ascending order (desc is normal) */
@ -840,8 +830,8 @@ export default defineComponent({
// Run query
for await (let delIds of dav.removeFaceImages(
user,
name,
<string>user,
<string>name,
Array.from(selection.values())
)) {
const delPhotos = delIds
@ -879,7 +869,7 @@ export default defineComponent({
vertical-align: middle;
z-index: 100;
> .text {
>.text {
flex-grow: 1;
line-height: 42px;
padding-left: 8px;

View File

@ -23,42 +23,20 @@
<template>
<div>
<label for="timeline-path">{{ t("memories", "Timeline Path") }}</label>
<input
id="timeline-path"
@click="chooseTimelinePath"
v-model="config_timelinePath"
type="text"
/>
<input id="timeline-path" @click="chooseTimelinePath" v-model="config_timelinePath" type="text" />
<label for="folders-path">{{ t("memories", "Folders Path") }}</label>
<input
id="folders-path"
@click="chooseFoldersPath"
v-model="config_foldersPath"
type="text"
/>
<input id="folders-path" @click="chooseFoldersPath" v-model="config_foldersPath" type="text" />
<NcCheckboxRadioSwitch
:checked.sync="config_showHidden"
@update:checked="updateShowHidden"
type="switch"
>
<NcCheckboxRadioSwitch :checked.sync="config_showHidden" @update:checked="updateShowHidden" type="switch">
{{ t("memories", "Show hidden folders") }}
</NcCheckboxRadioSwitch>
<NcCheckboxRadioSwitch
:checked.sync="config_squareThumbs"
@update:checked="updateSquareThumbs"
type="switch"
>
<NcCheckboxRadioSwitch :checked.sync="config_squareThumbs" @update:checked="updateSquareThumbs" type="switch">
{{ t("memories", "Square grid mode") }}
</NcCheckboxRadioSwitch>
<MultiPathSelectionModal
ref="multiPathModal"
:title="pathSelTitle"
@close="saveTimelinePath"
/>
<MultiPathSelectionModal ref="multiPathModal" :title="pathSelTitle" @close="saveTimelinePath" />
</div>
</template>
@ -86,7 +64,7 @@ export default defineComponent({
},
computed: {
pathSelTitle() {
pathSelTitle(): string {
return this.t("memories", "Choose Timeline Paths");
},
},

View File

@ -1,14 +1,10 @@
<template>
<div
class="container"
ref="container"
:class="{ 'icon-loading': loading > 0 }"
>
<div class="container" ref="container" :class="{ 'icon-loading': loading > 0 }">
<!-- Static top matter -->
<TopMatter ref="topmatter" />
<!-- No content found and nothing is loading -->
<NcEmptyContent
<!-- <NcEmptyContent
title="Nothing to show here"
:description="emptyViewDescription"
v-if="loading === 0 && list.length === 0"
@ -18,49 +14,28 @@
<ArchiveIcon v-else-if="routeIsArchive" />
<ImageMultipleIcon v-else />
</template>
</NcEmptyContent>
</NcEmptyContent> -->
<!-- Main recycler view for rows -->
<RecycleScroller
ref="recycler"
class="recycler"
:class="{ empty: list.length === 0 }"
:items="list"
:emit-update="true"
:buffer="800"
:skipHover="true"
key-field="id"
size-field="size"
type-field="type"
:updateInterval="100"
@update="scrollChange"
@resize="handleResizeWithDelay"
>
<RecycleScroller ref="recycler" class="recycler" :class="{ empty: list.length === 0 }" :items="list"
:emit-update="true" :buffer="800" :skipHover="true" key-field="id" size-field="size" type-field="type"
:updateInterval="100" @update="scrollChange" @resize="handleResizeWithDelay">
<template #before>
<!-- Show dynamic top matter, name of the view -->
<div class="recycler-before" ref="recyclerBefore">
<div class="text" v-show="!$refs.topmatter.type && list.length > 0">
<div class="text" v-show="!(<any>$refs.topmatter)?.type && list.length > 0">
{{ viewName }}
</div>
<OnThisDay
v-if="routeIsBase"
:key="config_timelinePath"
:viewer="$refs.viewer"
@load="scrollerManager.adjust()"
>
<OnThisDay v-if="routeIsBase" :key="config_timelinePath" :viewer="$refs.viewer"
@load="scrollerManager.adjust()">
</OnThisDay>
</div>
</template>
<template v-slot="{ item, index }">
<div
v-if="item.type === 0"
class="head-row"
:class="{ selected: item.selected }"
:style="{ height: item.size + 'px' }"
:key="item.id"
>
<div v-if="item.type === 0" class="head-row" :class="{ selected: item.selected }"
:style="{ height: item.size + 'px' }" :key="item.id">
<div class="super" v-if="item.super !== undefined">
{{ item.super }}
</div>
@ -71,72 +46,34 @@
</div>
<template v-else>
<div
class="photo"
v-for="photo of item.photos"
:key="photo.key"
:style="{
height: photo.dispH + 'px',
width: photo.dispW + 'px',
transform: `translate(${photo.dispX}px, ${photo.dispY}px`,
}"
>
<Folder
v-if="photo.flag & c.FLAG_IS_FOLDER"
:data="photo"
:key="photo.fileid"
/>
<div class="photo" v-for="photo of item.photos" :key="photo.key" :style="{
height: photo.dispH + 'px',
width: photo.dispW + 'px',
transform: `translate(${photo.dispX}px, ${photo.dispY}px`,
}">
<Folder v-if="photo.flag & c.FLAG_IS_FOLDER" :data="photo" />
<Tag
v-else-if="photo.flag & c.FLAG_IS_TAG"
:data="photo"
:key="photo.fileid"
/>
<Tag v-else-if="photo.flag & c.FLAG_IS_TAG" :data="photo" />
<Photo
v-else
:data="photo"
:day="item.day"
:key="photo.fileid"
@select="selectionManager.selectPhoto"
@pointerdown="selectionManager.clickPhoto(photo, $event, index)"
@touchstart="
<Photo v-else :data="photo" :day="item.day" @select="selectionManager.selectPhoto"
@pointerdown="selectionManager.clickPhoto(photo, $event, index)" @touchstart="
selectionManager.touchstartPhoto(photo, $event, index)
"
@touchend="selectionManager.touchendPhoto(photo, $event, index)"
@touchmove="selectionManager.touchmovePhoto(photo, $event, index)"
/>
" @touchend="selectionManager.touchendPhoto(photo, $event, index)"
@touchmove="selectionManager.touchmovePhoto(photo, $event, index)" />
</div>
</template>
</template>
</RecycleScroller>
<!-- Managers -->
<ScrollerManager
ref="scrollerManager"
:rows="list"
:height="scrollerHeight"
:recycler="$refs.recycler"
:recyclerBefore="$refs.recyclerBefore"
/>
<ScrollerManager ref="scrollerManager" :rows="list" :height="scrollerHeight" :recycler="$refs.recycler"
:recyclerBefore="($refs.recyclerBefore as any)" />
<SelectionManager
ref="selectionManager"
:heads="heads"
:rows="list"
:isreverse="isMonthView"
:recycler="$refs.recycler"
@refresh="softRefresh"
@delete="deleteFromViewWithAnimation"
@updateLoading="updateLoading"
/>
<SelectionManager ref="selectionManager" :heads="heads" :rows="list" :isreverse="isMonthView"
:recycler="$refs.recycler" @refresh="softRefresh" @delete="deleteFromViewWithAnimation"
@updateLoading="updateLoading" />
<Viewer
ref="viewer"
@deleted="deleteFromViewWithAnimation"
@fetchDay="fetchDay"
@updateLoading="updateLoading"
/>
<Viewer ref="viewer" @deleted="deleteFromViewWithAnimation" @fetchDay="fetchDay" @updateLoading="updateLoading" />
</div>
</template>
@ -201,7 +138,7 @@ export default defineComponent({
/** Computed number of columns */
numCols: 0,
/** Header rows for dayId key */
heads: {} as { [dayid: number]: IHeadRow },
heads: {} as { [dayid: number]: IHeadRow; },
/** Computed row height */
rowHeight: 100,
@ -230,15 +167,15 @@ export default defineComponent({
state: Math.random(),
/** Selection manager component */
selectionManager: null as SelectionManager & any,
selectionManager: null as InstanceType<typeof SelectionManager>,
/** Scroller manager component */
scrollerManager: null as ScrollerManager & any,
scrollerManager: null as InstanceType<typeof ScrollerManager>,
};
},
mounted() {
this.selectionManager = this.$refs.selectionManager;
this.scrollerManager = this.$refs.scrollerManager;
this.selectionManager = <any>this.$refs.selectionManager;
this.scrollerManager = <any>this.$refs.scrollerManager;
this.routeChange(this.$route);
},
@ -265,20 +202,20 @@ export default defineComponent({
},
computed: {
routeIsBase() {
routeIsBase(): boolean {
return this.$route.name === "timeline";
},
routeIsPeople() {
return ["recognize", "facerecognition"].includes(this.$route.name);
routeIsPeople(): boolean {
return ["recognize", "facerecognition"].includes(<string>this.$route.name);
},
routeIsArchive() {
routeIsArchive(): boolean {
return this.$route.name === "archive";
},
isMonthView() {
isMonthView(): boolean {
return this.$route.name === "albums";
},
/** Get view name for dynamic top matter */
viewName() {
viewName(): string {
switch (this.$route.name) {
case "timeline":
return this.t("memories", "Your Timeline");
@ -301,7 +238,7 @@ export default defineComponent({
return "";
}
},
emptyViewDescription() {
emptyViewDescription(): string {
switch (this.$route.name) {
case "facerecognition":
if (this.config_facerecognitionEnabled)
@ -655,7 +592,7 @@ export default defineComponent({
this.$route.params.name
) {
query.set(
this.$route.name, // "recognize" or "facerecognition"
<string>this.$route.name, // "recognize" or "facerecognition"
`${this.$route.params.user}/${this.$route.params.name}`
);
@ -667,7 +604,7 @@ export default defineComponent({
// Tags
if (this.$route.name === "tags" && this.$route.params.name) {
query.set("tag", this.$route.params.name);
query.set("tag", <string>this.$route.params.name);
}
// Albums
@ -718,7 +655,7 @@ export default defineComponent({
/** Fetch timeline main call */
async fetchDays(noCache = false) {
const url = API.Q(API.DAYS(), this.getQuery());
const cacheUrl = this.$route.name + url;
const cacheUrl = <string>this.$route.name + url;
// Try cache first
let cache: IDay[];
@ -1349,13 +1286,15 @@ export default defineComponent({
padding-left: 3px;
font-size: 0.9em;
> div {
>div {
position: relative;
&.super {
font-size: 1.4em;
font-weight: bold;
margin-bottom: 4px;
}
&.main {
display: inline-block;
font-weight: 600;
@ -1373,6 +1312,7 @@ export default defineComponent({
border-radius: 50%;
cursor: pointer;
}
.name {
display: block;
transition: transform 0.2s ease;
@ -1386,10 +1326,12 @@ export default defineComponent({
display: flex;
opacity: 0.7;
}
.name {
transform: translateX(24px);
}
}
&.selected .select {
opacity: 1;
color: var(--color-primary);
@ -1403,16 +1345,20 @@ export default defineComponent({
/** Static and dynamic top matter */
.top-matter {
padding-top: 4px;
@include phone {
padding-left: 40px;
}
}
.recycler-before {
width: 100%;
> .text {
>.text {
font-size: 1.2em;
padding-top: 13px;
padding-left: 8px;
@include phone {
padding-left: 48px;
}

View File

@ -1,14 +1,9 @@
<template>
<router-link
draggable="false"
class="folder fill-block"
:class="{
hasPreview: previews.length > 0,
onePreview: previews.length === 1,
hasError: error,
}"
:to="target"
>
<router-link draggable="false" class="folder fill-block" :class="{
hasPreview: previews.length > 0,
onePreview: previews.length === 1,
hasError: error,
}" :to="target">
<div class="big-icon fill-block">
<FolderIcon class="icon" />
<div class="name">{{ data.name }}</div>
@ -17,11 +12,7 @@
<div class="previews fill-block">
<div class="preview-container fill-block">
<div class="img-outer" v-for="info of previews" :key="info.fileid">
<img
class="fill-block"
:src="getPreviewUrl(info, true, 256)"
@error="$event.target.classList.add('error')"
/>
<img class="fill-block" :src="getPreviewUrl(info, true, 256)" @error="$event.target.classList.add('error')" />
</div>
</div>
</div>
@ -132,7 +123,7 @@ export default defineComponent({
height: 50%;
}
> .name {
>.name {
cursor: pointer;
width: 100%;
padding: 0 5%;
@ -151,36 +142,38 @@ export default defineComponent({
}
// Make it white if there is a preview
.folder.hasPreview > & {
.folder.hasPreview>& {
.folder-icon {
opacity: 1;
filter: invert(1) brightness(100);
}
.name {
color: white;
}
}
// Show it on hover if not a preview
.folder:hover > & > .folder-icon {
.folder:hover>&>.folder-icon {
opacity: 0.8;
}
.folder.hasPreview:hover > & {
.folder.hasPreview:hover>& {
opacity: 0;
}
// Make it red if has an error
.folder.hasError > & {
.folder.hasError>& {
.folder-icon {
filter: invert(12%) sepia(62%) saturate(5862%) hue-rotate(8deg)
brightness(103%) contrast(128%);
filter: invert(12%) sepia(62%) saturate(5862%) hue-rotate(8deg) brightness(103%) contrast(128%);
}
.name {
color: #bb0000;
}
}
> .folder-icon {
>.folder-icon {
cursor: pointer;
height: 90%;
width: 100%;
@ -208,12 +201,12 @@ export default defineComponent({
height: 50%;
display: inline-block;
.folder.onePreview > & {
.folder.onePreview>& {
width: 100%;
height: 100%;
}
> img {
>img {
object-fit: cover;
padding: 0;
filter: brightness(50%);
@ -222,6 +215,7 @@ export default defineComponent({
&.error {
display: none;
}
.folder:hover & {
filter: brightness(100%);
}

View File

@ -1,19 +1,11 @@
<template>
<div
class="p-outer fill-block"
:class="{
selected: data.flag & c.FLAG_SELECTED,
placeholder: data.flag & c.FLAG_PLACEHOLDER,
leaving: data.flag & c.FLAG_LEAVING,
error: data.flag & c.FLAG_LOAD_FAIL,
}"
>
<CheckCircle
:size="18"
class="select"
v-if="!(data.flag & c.FLAG_PLACEHOLDER)"
@click="toggleSelect"
/>
<div class="p-outer fill-block" :class="{
selected: data.flag & c.FLAG_SELECTED,
placeholder: data.flag & c.FLAG_PLACEHOLDER,
leaving: data.flag & c.FLAG_LEAVING,
error: data.flag & c.FLAG_LOAD_FAIL,
}">
<CheckCircle :size="18" class="select" v-if="!(data.flag & c.FLAG_PLACEHOLDER)" @click="toggleSelect" />
<div class="video" v-if="data.flag & c.FLAG_IS_VIDEO">
<span v-if="data.video_duration" class="time">
@ -22,45 +14,20 @@
<Video :size="22" />
</div>
<div
class="livephoto"
@mouseenter.passive="playVideo"
@mouseleave.passive="stopVideo"
>
<div class="livephoto" @mouseenter.passive="playVideo" @mouseleave.passive="stopVideo">
<LivePhoto :size="22" v-if="data.liveid" />
</div>
<Star :size="22" v-if="data.flag & c.FLAG_IS_FAVORITE" />
<div
class="img-outer fill-block"
:class="{
'memories-livephoto': data.liveid,
}"
@contextmenu="contextmenu"
@pointerdown.passive="$emit('pointerdown', $event)"
@touchstart.passive="$emit('touchstart', $event)"
@touchmove="$emit('touchmove', $event)"
@touchend.passive="$emit('touchend', $event)"
@touchcancel.passive="$emit('touchend', $event)"
>
<img
ref="img"
:class="['fill-block', `memories-thumb-${data.key}`]"
draggable="false"
:src="src"
:key="data.fileid"
@load="load"
@error="error"
/>
<video
ref="video"
v-if="videoUrl"
:src="videoUrl"
preload="none"
muted
playsinline
/>
<div class="img-outer fill-block" :class="{
'memories-livephoto': data.liveid,
}" @contextmenu="contextmenu" @pointerdown.passive="$emit('pointerdown', $event)"
@touchstart.passive="$emit('touchstart', $event)" @touchmove="$emit('touchmove', $event)"
@touchend.passive="$emit('touchend', $event)" @touchcancel.passive="$emit('touchend', $event)">
<img ref="img" :class="['fill-block', `memories-thumb-${data.key}`]" draggable="false" :src="src"
:key="data.fileid" @load="load" @error="error" />
<video ref="video" v-if="videoUrl" :src="videoUrl" preload="none" muted playsinline />
<div class="overlay fill-block" />
</div>
</div>
@ -193,7 +160,7 @@ export default defineComponent({
size =
Math.floor(
(base * Math.max(this.data.w, this.data.h)) /
Math.min(this.data.w, this.data.h)
Math.min(this.data.w, this.data.h)
) - 1;
}
@ -275,12 +242,14 @@ export default defineComponent({
/* Container and selection */
.p-outer {
padding: 2px;
@media (max-width: 768px) {
padding: 1px;
}
transition: background-color 0.15s ease, opacity 0.2s ease-in,
transform 0.2s ease-in;
transition: background-color 0.15s ease,
opacity 0.2s ease-in,
transform 0.2s ease-in;
&.leaving {
transform: scale(0.9);
@ -293,6 +262,7 @@ export default defineComponent({
}
--icon-dist: 8px;
@media (max-width: 768px) {
--icon-dist: 4px;
}
@ -312,13 +282,15 @@ $icon-size: $icon-half-size * 2;
cursor: pointer;
display: none;
@media (hover: hover) {
.p-outer:hover > & {
.p-outer:hover>& {
display: flex;
}
}
opacity: 0.7;
&:hover,
.p-outer.selected & {
opacity: 1;
@ -331,13 +303,15 @@ $icon-size: $icon-half-size * 2;
}
filter: invert(1) brightness(100);
.p-outer.selected > & {
.p-outer.selected>& {
display: flex;
filter: invert(0);
background-color: white;
color: var(--color-primary);
}
}
.video,
.star-icon,
.livephoto {
@ -347,12 +321,14 @@ $icon-size: $icon-half-size * 2;
transition: transform 0.15s ease;
filter: invert(1) brightness(100);
}
.video,
.livephoto {
position: absolute;
top: var(--icon-dist);
right: var(--icon-dist);
.p-outer.selected > & {
.p-outer.selected>& {
transform: translate(-$icon-size, $icon-size);
}
@ -366,13 +342,16 @@ $icon-size: $icon-half-size * 2;
margin-right: 3px;
}
}
.livephoto {
pointer-events: auto;
}
.star-icon {
bottom: var(--icon-dist);
left: var(--icon-dist);
.p-outer.selected > & {
.p-outer.selected>& {
transform: translate($icon-size, -$icon-size);
}
}
@ -384,16 +363,17 @@ div.img-outer {
padding: 0;
transition: padding 0.15s ease;
.p-outer.selected > & {
.p-outer.selected>& {
padding: calc(var(--icon-dist) + $icon-half-size);
}
.p-outer.placeholder > & {
.p-outer.placeholder>& {
background-color: var(--color-background-dark);
background-clip: content-box, padding-box;
}
> img {
>img {
filter: contrast(1.05); // most real world images are a bit overexposed
background-clip: content-box;
object-fit: cover;
@ -405,20 +385,21 @@ div.img-outer {
user-select: none;
transition: border-radius 0.1s ease-in, var(--livephoto-img-transition);
.p-outer.placeholder > & {
.p-outer.placeholder>& {
display: none;
}
.p-outer.error & {
object-fit: contain;
}
}
> video {
>video {
pointer-events: none;
object-fit: cover;
}
> .overlay {
>.overlay {
pointer-events: none;
position: absolute;
top: 0;
@ -427,16 +408,17 @@ div.img-outer {
display: none;
transition: border-radius 0.1s ease-in;
@media (hover: hover) {
.p-outer:not(.selected):hover > & {
.p-outer:not(.selected):hover>& {
display: block;
}
}
}
> * {
>* {
@media (max-width: 768px) {
.selected > & {
.selected>& {
border-radius: $icon-size;
border-top-left-radius: 0;
}

View File

@ -1,11 +1,6 @@
<template>
<router-link
draggable="false"
class="tag fill-block"
:class="{ face, error }"
:to="target"
@click.native="openTag(data)"
>
<router-link draggable="false" class="tag fill-block" :class="{ face, error }" :to="target"
@click.native="openTag(data)">
<div class="bbl">
<NcCounterBubble> {{ data.count }} </NcCounterBubble>
</div>
@ -16,13 +11,8 @@
<div class="previews fill-block" ref="previews">
<div class="img-outer">
<img
draggable="false"
class="fill-block"
:class="{ error }"
:src="previewUrl"
@error="data.flag |= c.FLAG_LOAD_FAIL"
/>
<img draggable="false" class="fill-block" :class="{ error }" :src="previewUrl"
@error="data.flag |= c.FLAG_LOAD_FAIL" />
</div>
</div>
</router-link>
@ -163,19 +153,19 @@ img {
text-overflow: ellipsis;
line-height: 1em;
> .subtitle {
>.subtitle {
font-size: 0.7em;
margin-top: 2px;
display: block;
}
.tag.face > & {
.tag.face>& {
top: unset;
bottom: 10%;
transform: unset;
}
.tag.error > & {
.tag.error>& {
color: unset;
}
@ -198,7 +188,7 @@ img {
padding: 2px;
box-sizing: border-box;
> .img-outer {
>.img-outer {
background-color: var(--color-background-dark);
border-radius: 10px;
padding: 0;
@ -209,7 +199,7 @@ img {
display: inline-block;
cursor: pointer;
> img {
>img {
object-fit: cover;
padding: 0;
filter: brightness(60%);
@ -219,6 +209,7 @@ img {
&.error {
display: none;
}
.tag:hover & {
filter: brightness(100%);
}

View File

@ -9,10 +9,10 @@
<div v-if="processing" class="info-pad">
{{
t("memories", "Processing … {n}/{m}", {
n: photosDone,
m: photos.length,
})
t("memories", "Processing … {n}/{m}", {
n: photosDone,
m: photos.length,
})
}}
</div>
</div>

View File

@ -7,36 +7,22 @@
<form class="manage-collaborators__form" @submit.prevent>
<NcPopover ref="popover" :auto-size="true" :distance="0">
<label slot="trigger" class="manage-collaborators__form__input">
<NcTextField
:value.sync="searchText"
autocomplete="off"
type="search"
name="search"
:aria-label="t('photos', 'Search for collaborators')"
aria-autocomplete="list"
<NcTextField :value.sync="searchText" autocomplete="off" type="search" name="search"
:aria-label="t('photos', 'Search for collaborators')" aria-autocomplete="list"
:aria-controls="`manage-collaborators__form__selection-${randomId} manage-collaborators__form__list-${randomId}`"
:placeholder="t('photos', 'Search people or groups')"
@input="searchCollaborators"
>
:placeholder="t('photos', 'Search people or groups')" @input="searchCollaborators">
<Magnify :size="16" />
</NcTextField>
<NcLoadingIcon v-if="loadingCollaborators" />
</label>
<ul
v-if="searchResults.length !== 0"
:id="`manage-collaborators__form__list-${randomId}`"
class="manage-collaborators__form__list"
>
<ul v-if="searchResults.length !== 0" :id="`manage-collaborators__form__list-${randomId}`"
class="manage-collaborators__form__list">
<li v-for="collaboratorKey of searchResults" :key="collaboratorKey">
<NcListItemIcon
:id="availableCollaborators[collaboratorKey].id"
class="manage-collaborators__form__list__result"
:title="availableCollaborators[collaboratorKey].id"
:search="searchText"
:user="availableCollaborators[collaboratorKey].id"
:display-name="availableCollaborators[collaboratorKey].label"
:aria-label="
<NcListItemIcon :id="availableCollaborators[collaboratorKey].id"
class="manage-collaborators__form__list__result" :title="availableCollaborators[collaboratorKey].id"
:search="searchText" :user="availableCollaborators[collaboratorKey].id"
:display-name="availableCollaborators[collaboratorKey].label" :aria-label="
t(
'photos',
'Add {collaboratorLabel} to the collaborators list',
@ -45,48 +31,32 @@
availableCollaborators[collaboratorKey].label,
}
)
"
@click="selectEntity(collaboratorKey)"
/>
" @click="selectEntity(collaboratorKey)" />
</li>
</ul>
<NcEmptyContent
v-else
key="emptycontent"
class="manage-collaborators__form__list--empty"
:title="t('photos', 'No collaborators available')"
>
<NcEmptyContent v-else key="emptycontent" class="manage-collaborators__form__list--empty"
:title="t('photos', 'No collaborators available')">
<AccountGroup slot="icon" />
</NcEmptyContent>
</NcPopover>
</form>
<ul class="manage-collaborators__selection">
<li
v-for="collaboratorKey of listableSelectedCollaboratorsKeys"
:key="collaboratorKey"
class="manage-collaborators__selection__item"
>
<NcListItemIcon
:id="availableCollaborators[collaboratorKey].id"
<li v-for="collaboratorKey of listableSelectedCollaboratorsKeys" :key="collaboratorKey"
class="manage-collaborators__selection__item">
<NcListItemIcon :id="availableCollaborators[collaboratorKey].id"
:display-name="availableCollaborators[collaboratorKey].label"
:title="availableCollaborators[collaboratorKey].id"
:user="availableCollaborators[collaboratorKey].id"
>
<NcButton
type="tertiary"
:aria-label="
t(
'photos',
'Remove {collaboratorLabel} from the collaborators list',
{
collaboratorLabel:
availableCollaborators[collaboratorKey].label,
}
)
"
@click="unselectEntity(collaboratorKey)"
>
:title="availableCollaborators[collaboratorKey].id" :user="availableCollaborators[collaboratorKey].id">
<NcButton type="tertiary" :aria-label="
t(
'photos',
'Remove {collaboratorLabel} from the collaborators list',
{
collaboratorLabel:
availableCollaborators[collaboratorKey].label,
}
)
" @click="unselectEntity(collaboratorKey)">
<Close slot="icon" :size="20" />
</NcButton>
</NcListItemIcon>
@ -96,12 +66,8 @@
<div class="actions">
<div v-if="allowPublicLink" class="actions__public-link">
<template v-if="isPublicLinkSelected">
<NcButton
class="manage-collaborators__public-link-button"
:aria-label="t('photos', 'Copy the public link')"
:disabled="publicLink.id === ''"
@click="copyPublicLink"
>
<NcButton class="manage-collaborators__public-link-button" :aria-label="t('photos', 'Copy the public link')"
:disabled="publicLink.id === ''" @click="copyPublicLink">
<template v-if="publicLinkCopied">
{{ t("photos", "Public link copied!") }}
</template>
@ -113,21 +79,13 @@
<ContentCopy v-else />
</template>
</NcButton>
<NcButton
type="tertiary"
:aria-label="t('photos', 'Delete the public link')"
:disabled="publicLink.id === ''"
@click="deletePublicLink"
>
<NcButton type="tertiary" :aria-label="t('photos', 'Delete the public link')" :disabled="publicLink.id === ''"
@click="deletePublicLink">
<NcLoadingIcon v-if="publicLink.id === ''" slot="icon" />
<Close v-else slot="icon" />
</NcButton>
</template>
<NcButton
v-else
class="manage-collaborators__public-link-button"
@click="createPublicLinkForAlbum"
>
<NcButton v-else class="manage-collaborators__public-link-button" @click="createPublicLinkForAlbum">
<Earth slot="icon" />
{{ t("photos", "Share via public link") }}
</NcButton>
@ -207,7 +165,7 @@ export default defineComponent({
data() {
return {
searchText: "",
availableCollaborators: {} as { [key: string]: Collaborator },
availableCollaborators: {} as { [key: string]: Collaborator; },
selectedCollaboratorsKeys: [] as string[],
currentSearchResults: [] as Collaborator[],
loadingAlbum: false,
@ -353,14 +311,13 @@ export default defineComponent({
* @param {Collaborator} collaborator - A collaborator
*/
indexCollaborators(
collaborators: { [s: string]: Collaborator },
collaborators: { [s: string]: Collaborator; },
collaborator: Collaborator
) {
return {
...collaborators,
[`${collaborator.type}${
collaborator.type === Type.SHARE_TYPE_LINK ? "" : ":"
}${collaborator.type === Type.SHARE_TYPE_LINK ? "" : collaborator.id}`]:
[`${collaborator.type}${collaborator.type === Type.SHARE_TYPE_LINK ? "" : ":"
}${collaborator.type === Type.SHARE_TYPE_LINK ? "" : collaborator.id}`]:
collaborator,
};
},

View File

@ -10,12 +10,7 @@
</template>
<div class="outer">
<AlbumForm
:album="album"
:display-back-button="false"
:title="t('photos', 'New album')"
@done="done"
/>
<AlbumForm :album="album" :display-back-button="false" :title="t('photos', 'New album')" @done="done" />
</div>
</Modal>
</template>
@ -52,8 +47,8 @@ export default defineComponent({
if (edit) {
try {
this.album = await dav.getAlbum(
this.$route.params.user,
this.$route.params.name
<string>this.$route.params.user,
<string>this.$route.params.name
);
} catch (e) {
console.error(e);

View File

@ -6,11 +6,11 @@
<span>
{{
t(
"memories",
'Are you sure you want to permanently remove album "{name}"?',
{ name }
)
t(
"memories",
'Are you sure you want to permanently remove album "{name}"?',
{ name }
)
}}
</span>
@ -78,8 +78,8 @@ export default defineComponent({
},
refreshParams() {
this.user = this.$route.params.user || "";
this.name = this.$route.params.name || "";
this.user = <string>this.$route.params.user || "";
this.name = <string>this.$route.params.name || "";
},
async save() {

View File

@ -1,58 +1,29 @@
<template>
<form
v-if="!showCollaboratorView"
class="album-form"
@submit.prevent="submit"
>
<form v-if="!showCollaboratorView" class="album-form" @submit.prevent="submit">
<div class="form-inputs">
<NcTextField
ref="nameInput"
:value.sync="albumName"
type="text"
name="name"
:required="true"
autofocus="true"
:placeholder="t('photos', 'Name of the album')"
/>
<NcTextField ref="nameInput" :value.sync="albumName" type="text" name="name" :required="true" autofocus="true"
:placeholder="t('photos', 'Name of the album')" />
<label>
<NcTextField
:value.sync="albumLocation"
name="location"
type="text"
:placeholder="t('photos', 'Location of the album')"
/>
<NcTextField :value.sync="albumLocation" name="location" type="text"
:placeholder="t('photos', 'Location of the album')" />
</label>
</div>
<div class="form-buttons">
<span class="left-buttons">
<NcButton
v-if="displayBackButton"
:aria-label="t('photos', 'Go back to the previous view.')"
type="tertiary"
@click="back"
>
<NcButton v-if="displayBackButton" :aria-label="t('photos', 'Go back to the previous view.')" type="tertiary"
@click="back">
{{ t("photos", "Back") }}
</NcButton>
</span>
<span class="right-buttons">
<NcButton
v-if="sharingEnabled && !editMode"
:aria-label="t('photos', 'Go to the add collaborators view.')"
type="secondary"
:disabled="albumName.trim() === '' || loading"
@click="showCollaboratorView = true"
>
<NcButton v-if="sharingEnabled && !editMode" :aria-label="t('photos', 'Go to the add collaborators view.')"
type="secondary" :disabled="albumName.trim() === '' || loading" @click="showCollaboratorView = true">
<template #icon>
<AccountMultiplePlus />
</template>
{{ t("photos", "Add collaborators") }}
</NcButton>
<NcButton
:aria-label="saveText"
type="primary"
:disabled="albumName === '' || loading"
@click="submit()"
>
<NcButton :aria-label="saveText" type="primary" :disabled="albumName === '' || loading" @click="submit()">
<template #icon>
<NcLoadingIcon v-if="loading" />
<Send v-else />
@ -62,29 +33,17 @@
</span>
</div>
</form>
<AlbumCollaborators
v-else
:album-name="albumName"
:allow-public-link="false"
:collaborators="[]"
>
<AlbumCollaborators v-else :album-name="albumName" :allow-public-link="false" :collaborators="[]">
<template slot-scope="{ collaborators }">
<span class="left-buttons">
<NcButton
:aria-label="t('photos', 'Back to the new album form.')"
type="tertiary"
@click="showCollaboratorView = false"
>
<NcButton :aria-label="t('photos', 'Back to the new album form.')" type="tertiary"
@click="showCollaboratorView = false">
{{ t("photos", "Back") }}
</NcButton>
</span>
<span class="right-buttons">
<NcButton
:aria-label="saveText"
type="primary"
:disabled="albumName.trim() === '' || loading"
@click="submit(collaborators)"
>
<NcButton :aria-label="saveText" type="primary" :disabled="albumName.trim() === '' || loading"
@click="submit(collaborators)">
<template #icon>
<NcLoadingIcon v-if="loading" />
<Send v-else />
@ -136,6 +95,7 @@ export default defineComponent({
data() {
return {
collaborators: [],
showCollaboratorView: false,
albumName: "",
albumLocation: "",
@ -176,7 +136,7 @@ export default defineComponent({
},
methods: {
submit(collaborators = []) {
submit(collaborators: any = []) {
if (this.albumName === "" || this.loading) {
return;
}
@ -251,41 +211,52 @@ export default defineComponent({
flex-direction: column;
height: 350px;
padding: 16px;
.form-title {
font-weight: bold;
}
.form-subtitle {
color: var(--color-text-lighter);
}
.form-inputs {
flex-grow: 1;
justify-items: flex-end;
input {
width: 100%;
}
label {
display: flex;
margin-top: 16px;
:deep svg {
margin-right: 12px;
}
}
}
.form-buttons {
display: flex;
justify-content: space-between;
.left-buttons,
.right-buttons {
display: flex;
}
.right-buttons {
justify-content: flex-end;
}
button {
margin-right: 16px;
}
}
}
.left-buttons {
flex-grow: 1;
}

View File

@ -3,24 +3,13 @@
<NcLoadingIcon v-if="loadingAlbums" class="loading-icon" />
<ul class="albums-container">
<NcListItem
v-for="album in albums"
:key="album.album_id"
class="album"
:title="getAlbumName(album)"
:aria-label="
t('photos', 'Add selection to album {albumName}', {
albumName: getAlbumName(album),
})
"
@click="pickAlbum(album)"
>
<NcListItem v-for="album in albums" :key="album.album_id" class="album" :title="getAlbumName(album)" :aria-label="
t('photos', 'Add selection to album {albumName}', {
albumName: getAlbumName(album),
})
" @click="pickAlbum(album)">
<template slot="icon">
<img
v-if="album.last_added_photo !== -1"
class="album__image"
:src="album.last_added_photo | toCoverUrl"
/>
<img v-if="album.last_added_photo !== -1" class="album__image" :src="toCoverUrl(album.last_added_photo)" />
<div v-else class="album__image album__image--placeholder">
<ImageMultiple :size="32" />
</div>
@ -34,12 +23,8 @@
</NcListItem>
</ul>
<NcButton
:aria-label="t('photos', 'Create a new album.')"
class="new-album-button"
type="tertiary"
@click="showAlbumCreationForm = true"
>
<NcButton :aria-label="t('photos', 'Create a new album.')" class="new-album-button" type="tertiary"
@click="showAlbumCreationForm = true">
<template #icon>
<Plus />
</template>
@ -47,13 +32,8 @@
</NcButton>
</div>
<AlbumForm
v-else
:display-back-button="true"
:title="t('photos', 'New album')"
@back="showAlbumCreationForm = false"
@done="albumCreatedHandler"
/>
<AlbumForm v-else :display-back-button="true" :title="t('photos', 'New album')" @back="showAlbumCreationForm = false"
@done="albumCreatedHandler" />
</template>
<script lang="ts">
@ -85,18 +65,6 @@ export default defineComponent({
NcLoadingIcon,
},
filters: {
toCoverUrl(fileId: string) {
return getPreviewUrl(
{
fileid: Number(fileId),
} as IPhoto,
true,
256
);
},
},
data() {
return {
showAlbumCreationForm: false,
@ -110,6 +78,16 @@ export default defineComponent({
},
methods: {
toCoverUrl(fileId: number | string) {
return getPreviewUrl(
{
fileid: Number(fileId),
} as IPhoto,
true,
256
);
},
albumCreatedHandler() {
this.showAlbumCreationForm = false;
this.loadAlbums();

View File

@ -4,19 +4,11 @@
{{ t("memories", "Share Album") }}
</template>
<AlbumCollaborators
v-if="album"
:album-name="album.basename"
:collaborators="album.collaborators"
:public-link="album.publicLink"
>
<AlbumCollaborators v-if="album" :album-name="album.basename" :collaborators="album.collaborators"
:public-link="album.publicLink">
<template slot-scope="{ collaborators }">
<NcButton
:aria-label="t('photos', 'Save collaborators for this album.')"
type="primary"
:disabled="loadingAddCollaborators"
@click="handleSetCollaborators(collaborators)"
>
<NcButton :aria-label="t('photos', 'Save collaborators for this album.')" type="primary"
:disabled="loadingAddCollaborators" @click="handleSetCollaborators(collaborators)">
<template #icon>
<NcLoadingIcon v-if="loadingAddCollaborators" />
</template>
@ -52,6 +44,7 @@ export default defineComponent({
album: null as any,
show: false,
loadingAddCollaborators: false,
collaborators: [] as any[],
};
},
@ -65,8 +58,8 @@ export default defineComponent({
async open() {
this.show = true;
this.loadingAddCollaborators = true;
const user = this.$route.params.user || "";
const name = this.$route.params.name || "";
const user = <string>this.$route.params.user || "";
const name = <string>this.$route.params.name || "";
this.album = await dav.getAlbum(user, name);
this.loadingAddCollaborators = false;
},

View File

@ -15,45 +15,16 @@
{{ longDateStr }}
<div class="fields">
<NcTextField
:value.sync="year"
class="field"
@input="newestChange()"
:label="t('memories', 'Year')"
:label-visible="true"
:placeholder="t('memories', 'Year')"
/>
<NcTextField
:value.sync="month"
class="field"
@input="newestChange()"
:label="t('memories', 'Month')"
:label-visible="true"
:placeholder="t('memories', 'Month')"
/>
<NcTextField
:value.sync="day"
class="field"
@input="newestChange()"
:label="t('memories', 'Day')"
:label-visible="true"
:placeholder="t('memories', 'Day')"
/>
<NcTextField
:value.sync="hour"
class="field"
@input="newestChange(true)"
:label="t('memories', 'Time')"
:label-visible="true"
:placeholder="t('memories', 'Hour')"
/>
<NcTextField
:value.sync="minute"
class="field"
@input="newestChange(true)"
:label="t('memories', 'Minute')"
:placeholder="t('memories', 'Minute')"
/>
<NcTextField :value.sync="year" class="field" @input="newestChange()" :label="t('memories', 'Year')"
:label-visible="true" :placeholder="t('memories', 'Year')" />
<NcTextField :value.sync="month" class="field" @input="newestChange()" :label="t('memories', 'Month')"
:label-visible="true" :placeholder="t('memories', 'Month')" />
<NcTextField :value.sync="day" class="field" @input="newestChange()" :label="t('memories', 'Day')"
:label-visible="true" :placeholder="t('memories', 'Day')" />
<NcTextField :value.sync="hour" class="field" @input="newestChange(true)" :label="t('memories', 'Time')"
:label-visible="true" :placeholder="t('memories', 'Hour')" />
<NcTextField :value.sync="minute" class="field" @input="newestChange(true)" :label="t('memories', 'Minute')"
:placeholder="t('memories', 'Minute')" />
</div>
<div v-if="photos.length > 1" class="oldest">
@ -61,59 +32,35 @@
{{ longDateStrLast }}
<div class="fields">
<NcTextField
:value.sync="yearLast"
class="field"
:label="t('memories', 'Year')"
:label-visible="true"
:placeholder="t('memories', 'Year')"
/>
<NcTextField
:value.sync="monthLast"
class="field"
:label="t('memories', 'Month')"
:label-visible="true"
:placeholder="t('memories', 'Month')"
/>
<NcTextField
:value.sync="dayLast"
class="field"
:label="t('memories', 'Day')"
:label-visible="true"
:placeholder="t('memories', 'Day')"
/>
<NcTextField
:value.sync="hourLast"
class="field"
:label="t('memories', 'Time')"
:label-visible="true"
:placeholder="t('memories', 'Hour')"
/>
<NcTextField
:value.sync="minuteLast"
class="field"
:label="t('memories', 'Minute')"
:placeholder="t('memories', 'Minute')"
/>
<NcTextField :value.sync="yearLast" class="field" :label="t('memories', 'Year')" :label-visible="true"
:placeholder="t('memories', 'Year')" />
<NcTextField :value.sync="monthLast" class="field" :label="t('memories', 'Month')" :label-visible="true"
:placeholder="t('memories', 'Month')" />
<NcTextField :value.sync="dayLast" class="field" :label="t('memories', 'Day')" :label-visible="true"
:placeholder="t('memories', 'Day')" />
<NcTextField :value.sync="hourLast" class="field" :label="t('memories', 'Time')" :label-visible="true"
:placeholder="t('memories', 'Hour')" />
<NcTextField :value.sync="minuteLast" class="field" :label="t('memories', 'Minute')"
:placeholder="t('memories', 'Minute')" />
</div>
</div>
<div v-if="processing" class="info-pad">
{{
t("memories", "Processing … {n}/{m}", {
n: photosDone,
m: photos.length,
})
t("memories", "Processing … {n}/{m}", {
n: photosDone,
m: photos.length,
})
}}
</div>
</div>
<div v-else>
{{
t("memories", "Loading data … {n}/{m}", {
n: photosDone,
m: photos.length,
})
t("memories", "Loading data … {n}/{m}", {
n: photosDone,
m: photos.length,
})
}}
</div>
</Modal>
@ -254,7 +201,7 @@ export default defineComponent({
this.minuteLast = dateLastNew.getUTCMinutes().toString();
this.secondLast = dateLastNew.getUTCSeconds().toString();
}
} catch (error) {}
} catch (error) { }
},
close() {

View File

@ -5,28 +5,15 @@
</template>
<template #buttons>
<NcButton
@click="save"
class="button"
type="error"
v-if="exif"
:disabled="processing"
>
<NcButton @click="save" class="button" type="error" v-if="exif" :disabled="processing">
{{ t("memories", "Update Exif") }}
</NcButton>
</template>
<div v-if="exif">
<div class="fields">
<NcTextField
v-for="field of fields"
:key="field.field"
:value.sync="exif[field.field]"
class="field"
:label="field.label"
:label-visible="true"
:placeholder="field.label"
/>
<NcTextField v-for="field of fields" :key="field.field" :value.sync="exif[field.field]" class="field"
:label="field.label" :label-visible="true" :placeholder="field.label" />
</div>
</div>
</Modal>
@ -168,6 +155,7 @@ export default defineComponent({
.field {
margin-bottom: 8px;
}
:deep label {
font-size: 0.8em;
padding: 0 !important;

View File

@ -5,7 +5,7 @@
</template>
<span>{{
t("memories", "Are you sure you want to remove {name}?", { name })
t("memories", "Are you sure you want to remove {name}?", { name })
}}</span>
<template #buttons>
@ -74,8 +74,8 @@ export default defineComponent({
},
refreshParams() {
this.user = this.$route.params.user || "";
this.name = this.$route.params.name || "";
this.user = <string>this.$route.params.user || "";
this.name = <string>this.$route.params.name || "";
},
async save() {

View File

@ -5,14 +5,8 @@
</template>
<div class="fields">
<NcTextField
:value.sync="name"
class="field"
:label="t('memories', 'Name')"
:label-visible="false"
:placeholder="t('memories', 'Name')"
@keypress.enter="save()"
/>
<NcTextField :value.sync="name" class="field" :label="t('memories', 'Name')" :label-visible="false"
:placeholder="t('memories', 'Name')" @keypress.enter="save()" />
</div>
<template #buttons>
@ -82,9 +76,9 @@ export default defineComponent({
},
refreshParams() {
this.user = this.$route.params.user || "";
this.name = this.$route.params.name || "";
this.oldName = this.$route.params.name || "";
this.user = <string>this.$route.params.user || "";
this.name = <string>this.$route.params.name || "";
this.oldName = <string>this.$route.params.name || "";
},
async save() {

View File

@ -26,7 +26,7 @@ export default defineComponent({
return {
user: "",
name: "",
detail: null as IPhoto[] | null,
detail: null as ITag[] | null,
};
},
@ -46,8 +46,8 @@ export default defineComponent({
},
async refreshParams() {
this.user = this.$route.params.user || "";
this.name = this.$route.params.name || "";
this.user = <string>this.$route.params.user || "";
this.name = <string>this.$route.params.name || "";
this.detail = null;
let data = [];
@ -85,6 +85,7 @@ export default defineComponent({
overflow-x: hidden;
overflow-y: auto;
}
.photo {
display: inline-block;
position: relative;

View File

@ -2,7 +2,7 @@
<Modal @close="close" size="large" v-if="show">
<template #title>
{{
t("memories", "Merge {name} with person", { name: $route.params.name })
t("memories", "Merge {name} with person", { name: $route.params.name })
}}
</template>
@ -11,10 +11,10 @@
<div v-if="procesingTotal > 0" class="info-pad">
{{
t("memories", "Processing … {n}/{m}", {
n: processing,
m: procesingTotal,
})
t("memories", "Processing … {n}/{m}", {
n: processing,
m: procesingTotal,
})
}}
</div>
</div>
@ -157,6 +157,7 @@ export default defineComponent({
.outer {
margin-top: 15px;
}
.info-pad {
margin-top: 6px;
margin-bottom: 2px;

View File

@ -1,10 +1,5 @@
<template>
<Modal
@close="close"
size="normal"
v-if="show"
:sidebar="!isRoot ? this.folderPath : null"
>
<Modal @close="close" size="normal" v-if="show" :sidebar="!isRoot ? folderPath : null">
<template #title>
{{ t("memories", "Share Folder") }}
</template>
@ -15,21 +10,15 @@
<div v-else>
{{ t("memories", "Use the sidebar to share this folder.") }} <br />
{{
t(
"memories",
"If you create a public link share, click on refresh and a corresponding link to Memories will be shown below."
)
t(
"memories",
"If you create a public link share, click on refresh and a corresponding link to Memories will be shown below."
)
}}
</div>
<div class="links">
<a
v-for="link of links"
:key="link.url"
:href="link.url"
target="_blank"
rel="noopener noreferrer"
>
<a v-for="link of links" :key="link.url" :href="link.url" target="_blank" rel="noopener noreferrer">
{{ link.url }}
</a>
</div>
@ -64,12 +53,12 @@ export default defineComponent({
return {
show: false,
folderPath: "",
links: [] as { url: string }[],
links: [] as { url: string; }[],
};
},
computed: {
isRoot() {
isRoot(): boolean {
return this.folderPath === "/" || this.folderPath === "";
},
},
@ -111,6 +100,7 @@ export default defineComponent({
<style lang="scss" scoped>
.links {
margin-top: 1em;
a {
display: block;
margin-bottom: 0.2em;

View File

@ -1,14 +1,11 @@
<template>
<NcModal
:size="size"
@close="close"
:outTransition="true"
:style="{ width: isSidebarShown ? `calc(100% - ${sidebarWidth}px)` : null }"
:additionalTrapElements="trapElements"
>
<NcModal :size="size" @close="close" :outTransition="true"
:style="{ width: isSidebarShown ? `calc(100% - ${sidebarWidth}px)` : null }" :additionalTrapElements="trapElements">
<div class="container">
<div class="head">
<span> <slot name="title"></slot> </span>
<span>
<slot name="title"></slot>
</span>
</div>
<slot></slot>
@ -125,7 +122,7 @@ export default defineComponent({
margin-top: 10px;
text-align: right;
> button {
>button {
display: inline-block !important;
}
}

View File

@ -9,12 +9,11 @@
{{ path }}
<NcActions :inline="1">
<NcActionButton
:aria-label="t('memories', 'Remove')"
@click="remove(index)"
>
<NcActionButton :aria-label="t('memories', 'Remove')" @click="remove(index)">
{{ t("memories", "Remove") }}
<template #icon> <CloseIcon :size="20" /> </template>
<template #icon>
<CloseIcon :size="20" />
</template>
</NcActionButton>
</NcActions>
</li>

View File

@ -3,7 +3,9 @@
<NcActions v-if="!isAlbumList">
<NcActionButton :aria-label="t('memories', 'Back')" @click="back()">
{{ t("memories", "Back") }}
<template #icon> <BackIcon :size="20" /> </template>
<template #icon>
<BackIcon :size="20" />
</template>
</NcActionButton>
</NcActions>
@ -11,50 +13,40 @@
<div class="right-actions">
<NcActions :inline="1">
<NcActionButton
:aria-label="t('memories', 'Create new album')"
@click="$refs.createModal.open(false)"
close-after-click
v-if="isAlbumList"
>
<NcActionButton :aria-label="t('memories', 'Create new album')" @click="$refs.createModal.open(false)"
close-after-click v-if="isAlbumList">
{{ t("memories", "Create new album") }}
<template #icon> <PlusIcon :size="20" /> </template>
<template #icon>
<PlusIcon :size="20" />
</template>
</NcActionButton>
<NcActionButton
:aria-label="t('memories', 'Share album')"
@click="$refs.shareModal.open(false)"
close-after-click
v-if="canEditAlbum"
>
<NcActionButton :aria-label="t('memories', 'Share album')" @click="$refs.shareModal.open(false)"
close-after-click v-if="canEditAlbum">
{{ t("memories", "Share album") }}
<template #icon> <ShareIcon :size="20" /> </template>
<template #icon>
<ShareIcon :size="20" />
</template>
</NcActionButton>
<NcActionButton
:aria-label="t('memories', 'Download album')"
@click="downloadAlbum()"
close-after-click
v-if="!isAlbumList"
>
<NcActionButton :aria-label="t('memories', 'Download album')" @click="downloadAlbum()" close-after-click
v-if="!isAlbumList">
{{ t("memories", "Download album") }}
<template #icon> <DownloadIcon :size="20" /> </template>
<template #icon>
<DownloadIcon :size="20" />
</template>
</NcActionButton>
<NcActionButton
:aria-label="t('memories', 'Edit album details')"
@click="$refs.createModal.open(true)"
close-after-click
v-if="canEditAlbum"
>
<NcActionButton :aria-label="t('memories', 'Edit album details')" @click="$refs.createModal.open(true)"
close-after-click v-if="canEditAlbum">
{{ t("memories", "Edit album details") }}
<template #icon> <EditIcon :size="20" /> </template>
<template #icon>
<EditIcon :size="20" />
</template>
</NcActionButton>
<NcActionButton
:aria-label="t('memories', 'Delete album')"
@click="$refs.deleteModal.open()"
close-after-click
v-if="canEditAlbum"
>
<NcActionButton :aria-label="t('memories', 'Delete album')" @click="$refs.deleteModal.open()" close-after-click
v-if="canEditAlbum">
{{ t("memories", "Delete album") }}
<template #icon> <DeleteIcon :size="20" /> </template>
<template #icon>
<DeleteIcon :size="20" />
</template>
</NcActionButton>
</NcActions>
</div>
@ -114,11 +106,11 @@ export default defineComponent({
},
computed: {
isAlbumList() {
isAlbumList(): boolean {
return !Boolean(this.$route.params.name);
},
canEditAlbum() {
canEditAlbum(): boolean {
return (
!this.isAlbumList && this.$route.params.user === getCurrentUser()?.uid
);
@ -137,7 +129,7 @@ export default defineComponent({
methods: {
createMatter() {
this.name = this.$route.params.name || this.t("memories", "Albums");
this.name = <string>this.$route.params.name || this.t("memories", "Albums");
},
back() {
@ -146,7 +138,7 @@ export default defineComponent({
async downloadAlbum() {
const res = await axios.post(
API.ALBUM_DOWNLOAD(this.$route.params.user, this.$route.params.name)
API.ALBUM_DOWNLOAD(<string>this.$route.params.user, <string>this.$route.params.name)
);
if (res.status === 200 && res.data.handle) {
downloadWithHandle(res.data.handle);
@ -172,6 +164,7 @@ export default defineComponent({
.right-actions {
margin-right: 40px;
z-index: 50;
@media (max-width: 768px) {
margin-right: 10px;
}

View File

@ -3,7 +3,9 @@
<NcActions>
<NcActionButton :aria-label="t('memories', 'Back')" @click="back()">
{{ t("memories", "Back") }}
<template #icon> <BackIcon :size="20" /> </template>
<template #icon>
<BackIcon :size="20" />
</template>
</NcActionButton>
</NcActions>
@ -11,36 +13,29 @@
<div class="right-actions">
<NcActions :inline="1">
<NcActionButton
:aria-label="t('memories', 'Rename person')"
@click="$refs.editModal.open()"
close-after-click
>
<NcActionButton :aria-label="t('memories', 'Rename person')" @click="$refs.editModal.open()" close-after-click>
{{ t("memories", "Rename person") }}
<template #icon> <EditIcon :size="20" /> </template>
<template #icon>
<EditIcon :size="20" />
</template>
</NcActionButton>
<NcActionButton
:aria-label="t('memories', 'Merge with different person')"
@click="$refs.mergeModal.open()"
close-after-click
>
<NcActionButton :aria-label="t('memories', 'Merge with different person')" @click="$refs.mergeModal.open()"
close-after-click>
{{ t("memories", "Merge with different person") }}
<template #icon> <MergeIcon :size="20" /> </template>
<template #icon>
<MergeIcon :size="20" />
</template>
</NcActionButton>
<NcActionCheckbox
:aria-label="t('memories', 'Mark person in preview')"
:checked.sync="config_showFaceRect"
@change="changeShowFaceRect"
>
<NcActionCheckbox :aria-label="t('memories', 'Mark person in preview')" :checked.sync="config_showFaceRect"
@change="changeShowFaceRect">
{{ t("memories", "Mark person in preview") }}
</NcActionCheckbox>
<NcActionButton
:aria-label="t('memories', 'Remove person')"
@click="$refs.deleteModal.open()"
close-after-click
>
<NcActionButton :aria-label="t('memories', 'Remove person')" @click="$refs.deleteModal.open()"
close-after-click>
{{ t("memories", "Remove person") }}
<template #icon> <DeleteIcon :size="20" /> </template>
<template #icon>
<DeleteIcon :size="20" />
</template>
</NcActionButton>
</NcActions>
</div>
@ -99,7 +94,7 @@ export default defineComponent({
methods: {
createMatter() {
this.name = this.$route.params.name || "";
this.name = <string>this.$route.params.name || "";
},
back() {
@ -135,6 +130,7 @@ export default defineComponent({
.right-actions {
margin-right: 40px;
z-index: 50;
@media (max-width: 768px) {
margin-right: 10px;
}

View File

@ -6,23 +6,18 @@
<HomeIcon :size="20" />
</template>
</NcBreadcrumb>
<NcBreadcrumb
v-for="folder in topMatter.list"
:key="folder.path"
:title="folder.text"
:to="{ name: 'folders', params: { path: folder.path } }"
/>
<NcBreadcrumb v-for="folder in topMatter.list" :key="folder.path" :title="folder.text"
:to="{ name: 'folders', params: { path: folder.path } }" />
</NcBreadcrumbs>
<div class="right-actions">
<NcActions :inline="1">
<NcActionButton
:aria-label="t('memories', 'Share folder')"
@click="$refs.shareModal.open(false)"
close-after-click
>
<NcActionButton :aria-label="t('memories', 'Share folder')" @click="$refs.shareModal.open(false)"
close-after-click>
{{ t("memories", "Share folder") }}
<template #icon> <ShareIcon :size="20" /> </template>
<template #icon>
<ShareIcon :size="20" />
</template>
</NcActionButton>
</NcActions>
</div>
@ -110,6 +105,7 @@ export default defineComponent({
.right-actions {
margin-right: 40px;
z-index: 50;
@media (max-width: 768px) {
margin-right: 10px;
}

View File

@ -1,12 +1,7 @@
<template>
<div class="outer" v-show="years.length > 0">
<div class="inner" ref="inner">
<div
v-for="year of years"
class="group"
:key="year.year"
@click="click(year)"
>
<div v-for="year of years" class="group" :key="year.year" @click="click(year)">
<img class="fill-block" :src="year.url" />
<div class="overlay">
@ -17,23 +12,21 @@
<div class="left-btn dir-btn" v-if="hasLeft">
<NcActions>
<NcActionButton
:aria-label="t('memories', 'Move left')"
@click="moveLeft"
>
<NcActionButton :aria-label="t('memories', 'Move left')" @click="moveLeft">
{{ t("memories", "Move left") }}
<template #icon> <LeftMoveIcon :size="28" /> </template>
<template #icon>
<LeftMoveIcon :size="28" />
</template>
</NcActionButton>
</NcActions>
</div>
<div class="right-btn dir-btn" v-if="hasRight">
<NcActions>
<NcActionButton
:aria-label="t('memories', 'Move right')"
@click="moveRight"
>
<NcActionButton :aria-label="t('memories', 'Move right')" @click="moveRight">
{{ t("memories", "Move right") }}
<template #icon> <RightMoveIcon :size="28" /> </template>
<template #icon>
<RightMoveIcon :size="28" />
</template>
</NcActionButton>
</NcActions>
</div>
@ -255,13 +248,16 @@ $mobHeight: 150px;
@media (max-width: 768px) {
width: 98%;
padding: 0;
.inner {
padding: 0 8px;
}
.dir-btn {
display: none;
}
}
@media (max-width: 600px) {
height: $mobHeight;
}
@ -312,6 +308,7 @@ $mobHeight: 150px;
@media (max-width: 600px) {
aspect-ratio: 3/4;
height: $mobHeight;
.overlay {
font-size: 1.1em;
}

View File

@ -3,7 +3,9 @@
<NcActions>
<NcActionButton :aria-label="t('memories', 'Back')" @click="back()">
{{ t("memories", "Back") }}
<template #icon> <BackIcon :size="20" /> </template>
<template #icon>
<BackIcon :size="20" />
</template>
</NcActionButton>
</NcActions>
<span class="name">{{ name }}</span>
@ -44,7 +46,7 @@ export default defineComponent({
methods: {
createMatter() {
this.name = this.$route.params.name || "";
this.name = <string>this.$route.params.name || "";
},
back() {

View File

@ -1,10 +1,5 @@
<template>
<div
ref="editor"
class="viewer__image-editor"
:class="{ loading: !imageEditor }"
v-bind="themeDataAttr"
/>
<div ref="editor" class="viewer__image-editor" :class="{ loading: !imageEditor }" v-bind="themeDataAttr" />
</template>
<script lang="ts">
@ -58,7 +53,7 @@ export default defineComponent({
},
computed: {
config(): FilerobotImageEditorConfig & { theme: any } {
config(): FilerobotImageEditorConfig & { theme: any; } {
let src: string;
if (["image/png", "image/jpeg", "image/webp"].includes(this.mime)) {
src = this.src;
@ -275,8 +270,8 @@ export default defineComponent({
onExitWithoutSaving() {
(<any>OC.dialogs).confirmDestructive(
translations.changesLoseConfirmation +
"\n\n" +
translations.changesLoseConfirmationHint,
"\n\n" +
translations.changesLoseConfirmationHint,
this.t("memories", "Unsaved changes"),
{
type: (<any>OC.dialogs).YES_NO_BUTTONS,
@ -361,7 +356,8 @@ export default defineComponent({
label,
button {
color: var(--color-main-text);
> span {
>span {
font-size: var(--default-font-size) !important;
}
}
@ -381,6 +377,7 @@ export default defineComponent({
.SfxInput-root {
height: auto !important;
padding: 0 !important;
.SfxInput-Base {
margin: 0 !important;
}
@ -396,18 +393,22 @@ export default defineComponent({
min-height: 44px !important;
margin: 0 !important;
border: transparent !important;
&[color="error"] {
color: white !important;
background-color: var(--color-error) !important;
&:hover,
&:focus {
border-color: white !important;
background-color: var(--color-error-hover) !important;
}
}
&[color="primary"] {
color: var(--color-primary-text) !important;
background-color: var(--color-primary-element) !important;
&:hover,
&:focus {
background-color: var(--color-primary-element-hover) !important;
@ -419,8 +420,9 @@ export default defineComponent({
.SfxMenuItem-root {
height: 44px;
padding-left: 8px !important;
// Center the menu entry icon and fix width
> div {
>div {
margin-right: 0;
padding: 14px;
// Minus the parent padding-left
@ -446,9 +448,11 @@ export default defineComponent({
justify-content: center;
color: var(--color-main-text);
}
.SfxModalTitle-Icon {
margin-bottom: 22px !important;
background: none !important;
// Fit EmptyContent styling
svg {
width: 64px;
@ -460,10 +464,12 @@ export default defineComponent({
--color-error: var(--color-main-text);
}
}
// Hide close icon (use cancel button)
.SfxModalTitle-Close {
display: none !important;
}
// Modal actions buttons display
.SfxModalActions-root {
justify-content: space-evenly !important;
@ -471,8 +477,8 @@ export default defineComponent({
}
// Header buttons
.FIE_topbar-center-options > button,
.FIE_topbar-center-options > label {
.FIE_topbar-center-options>button,
.FIE_topbar-center-options>label {
margin-left: 6px !important;
}
@ -488,10 +494,12 @@ export default defineComponent({
height: 80px !important;
padding: 8px;
border-radius: var(--border-radius-large) !important;
svg {
width: 16px;
height: 16px;
}
&-label {
margin-top: 8px !important;
overflow: hidden;
@ -520,8 +528,8 @@ export default defineComponent({
}
// Matching buttons tools
& > div[class$="-tool-button"],
& > div[class$="-tool"] {
&>div[class$="-tool-button"],
&>div[class$="-tool"] {
display: flex;
align-items: center;
justify-content: center;
@ -567,6 +575,7 @@ export default defineComponent({
content: attr(title);
font-weight: normal;
}
svg {
display: none;
}
@ -578,6 +587,7 @@ export default defineComponent({
color: var(--color-primary-text) !important;
border: none !important;
background-color: var(--color-primary-element) !important;
&:hover,
&:focus {
background-color: var(--color-primary-element-hover) !important;
@ -586,6 +596,7 @@ export default defineComponent({
// Save Modal fixes
.FIE_resize-tool-options {
.FIE_resize-width-option,
.FIE_resize-height-option {
flex: 1 1;
@ -596,10 +607,12 @@ export default defineComponent({
// Resize lock
.FIE_resize-ratio-locker {
margin-right: 8px !important;
// Icon is very thin
svg {
width: 20px;
height: 20px;
path {
stroke-width: 1;
stroke: var(--color-main-text);

View File

@ -1,141 +1,85 @@
<template>
<div
class="memories_viewer outer"
v-if="show"
:class="{ fullyOpened, slideshowTimer }"
:style="{ width: outerWidth }"
@fullscreenchange="fullscreenChange"
>
<ImageEditor
v-if="editorOpen"
:mime="currentPhoto.mimetype"
:src="currentDownloadLink"
:fileid="currentPhoto.fileid"
@close="editorOpen = false"
/>
<div class="memories_viewer outer" v-if="show" :class="{ fullyOpened, slideshowTimer }" :style="{ width: outerWidth }"
@fullscreenchange="fullscreenChange">
<ImageEditor v-if="editorOpen" :mime="currentPhoto.mimetype" :src="currentDownloadLink"
:fileid="currentPhoto.fileid" @close="editorOpen = false" />
<div
class="inner"
ref="inner"
v-show="!editorOpen"
@pointermove.passive="setUiVisible"
@pointerdown.passive="setUiVisible"
>
<div class="inner" ref="inner" v-show="!editorOpen" @pointermove.passive="setUiVisible"
@pointerdown.passive="setUiVisible">
<div class="top-bar" v-if="photoswipe" :class="{ showControls }">
<NcActions
:inline="numInlineActions"
container=".memories_viewer .pswp"
>
<NcActionButton
v-if="canShare"
:aria-label="t('memories', 'Share')"
@click="shareCurrent"
:close-after-click="true"
>
<NcActions :inline="numInlineActions" container=".memories_viewer .pswp">
<NcActionButton v-if="canShare" :aria-label="t('memories', 'Share')" @click="shareCurrent"
:close-after-click="true">
{{ t("memories", "Share") }}
<template #icon> <ShareIcon :size="24" /> </template>
<template #icon>
<ShareIcon :size="24" />
</template>
</NcActionButton>
<NcActionButton
v-if="!routeIsPublic && !routeIsAlbum"
:aria-label="t('memories', 'Delete')"
@click="deleteCurrent"
:close-after-click="true"
>
<NcActionButton v-if="!routeIsPublic && !routeIsAlbum" :aria-label="t('memories', 'Delete')"
@click="deleteCurrent" :close-after-click="true">
{{ t("memories", "Delete") }}
<template #icon> <DeleteIcon :size="24" /> </template>
<template #icon>
<DeleteIcon :size="24" />
</template>
</NcActionButton>
<NcActionButton
v-if="!routeIsPublic && routeIsAlbum"
:aria-label="t('memories', 'Remove from album')"
@click="deleteCurrent"
:close-after-click="true"
>
<NcActionButton v-if="!routeIsPublic && routeIsAlbum" :aria-label="t('memories', 'Remove from album')"
@click="deleteCurrent" :close-after-click="true">
{{ t("memories", "Remove from album") }}
<template #icon> <AlbumRemoveIcon :size="24" /> </template>
<template #icon>
<AlbumRemoveIcon :size="24" />
</template>
</NcActionButton>
<NcActionButton
v-if="!routeIsPublic"
:aria-label="t('memories', 'Favorite')"
@click="favoriteCurrent"
:close-after-click="true"
>
<NcActionButton v-if="!routeIsPublic" :aria-label="t('memories', 'Favorite')" @click="favoriteCurrent"
:close-after-click="true">
{{ t("memories", "Favorite") }}
<template #icon>
<StarIcon v-if="isFavorite()" :size="24" />
<StarOutlineIcon v-else :size="24" />
</template>
</NcActionButton>
<NcActionButton
v-if="!routeIsPublic"
:aria-label="t('memories', 'Sidebar')"
@click="toggleSidebar"
:close-after-click="true"
>
<NcActionButton v-if="!routeIsPublic" :aria-label="t('memories', 'Sidebar')" @click="toggleSidebar"
:close-after-click="true">
{{ t("memories", "Sidebar") }}
<template #icon>
<InfoIcon :size="24" />
</template>
</NcActionButton>
<NcActionButton
v-if="canEdit && !routeIsPublic"
:aria-label="t('memories', 'Edit')"
@click="openEditor"
:close-after-click="true"
>
<NcActionButton v-if="canEdit && !routeIsPublic" :aria-label="t('memories', 'Edit')" @click="openEditor"
:close-after-click="true">
{{ t("memories", "Edit") }}
<template #icon>
<TuneIcon :size="24" />
</template>
</NcActionButton>
<NcActionButton
:aria-label="t('memories', 'Download')"
@click="downloadCurrent"
:close-after-click="true"
v-if="!this.state_noDownload"
>
<NcActionButton :aria-label="t('memories', 'Download')" @click="downloadCurrent" :close-after-click="true"
v-if="!state_noDownload">
{{ t("memories", "Download") }}
<template #icon>
<DownloadIcon :size="24" />
</template>
</NcActionButton>
<NcActionButton
v-if="!this.state_noDownload && currentPhoto?.liveid"
:aria-label="t('memories', 'Download Video')"
@click="downloadCurrentLiveVideo"
:close-after-click="true"
>
<NcActionButton v-if="!state_noDownload && currentPhoto?.liveid" :aria-label="t('memories', 'Download Video')"
@click="downloadCurrentLiveVideo" :close-after-click="true">
{{ t("memories", "Download Video") }}
<template #icon>
<DownloadIcon :size="24" />
</template>
</NcActionButton>
<NcActionButton
v-if="!routeIsPublic && !routeIsAlbum"
:aria-label="t('memories', 'View in folder')"
@click="viewInFolder"
:close-after-click="true"
>
<NcActionButton v-if="!routeIsPublic && !routeIsAlbum" :aria-label="t('memories', 'View in folder')"
@click="viewInFolder" :close-after-click="true">
{{ t("memories", "View in folder") }}
<template #icon>
<OpenInNewIcon :size="24" />
</template>
</NcActionButton>
<NcActionButton
:aria-label="t('memories', 'Slideshow')"
@click="startSlideshow"
:close-after-click="true"
>
<NcActionButton :aria-label="t('memories', 'Slideshow')" @click="startSlideshow" :close-after-click="true">
{{ t("memories", "Slideshow") }}
<template #icon>
<SlideshowIcon :size="24" />
</template>
</NcActionButton>
<NcActionButton
:aria-label="t('memories', 'Edit EXIF Data')"
v-if="!routeIsPublic"
@click="editExif"
:close-after-click="true"
>
<NcActionButton :aria-label="t('memories', 'Edit EXIF Data')" v-if="!routeIsPublic" @click="editExif"
:close-after-click="true">
{{ t("memories", "Edit EXIF Data") }}
<template #icon>
<EditFileIcon :size="24" />
@ -144,18 +88,11 @@
</NcActions>
</div>
<div
class="bottom-bar"
v-if="photoswipe"
:class="{ showControls, showBottomBar }"
>
<div class="bottom-bar" v-if="photoswipe" :class="{ showControls, showBottomBar }">
<div class="exif title" v-if="currentPhoto?.imageInfo?.exif?.Title">
{{ currentPhoto.imageInfo.exif.Title }}
</div>
<div
class="exif description"
v-if="currentPhoto?.imageInfo?.exif?.Description"
>
<div class="exif description" v-if="currentPhoto?.imageInfo?.exif?.Description">
{{ currentPhoto.imageInfo.exif.Description }}
</div>
<div class="exif date" v-if="currentDateTaken">
@ -270,7 +207,7 @@ export default defineComponent({
computed: {
/** Number of buttons to show inline */
numInlineActions() {
numInlineActions(): number {
let base = 3;
if (this.canShare) base++;
if (this.canEdit) base++;
@ -283,17 +220,17 @@ export default defineComponent({
},
/** Route is public */
routeIsPublic() {
routeIsPublic(): boolean {
return this.$route.name === "folder-share";
},
/** Route is album */
routeIsAlbum() {
routeIsAlbum(): boolean {
return this.$route.name === "albums";
},
/** Get the currently open photo */
currentPhoto() {
currentPhoto(): IPhoto | null {
if (!this.list.length || !this.photoswipe) {
return null;
}
@ -305,31 +242,31 @@ export default defineComponent({
},
/** Is the current slide a video */
isVideo() {
return this.currentPhoto?.flag & this.c.FLAG_IS_VIDEO;
isVideo(): boolean {
return Boolean(this.currentPhoto?.flag & this.c.FLAG_IS_VIDEO);
},
/** Show bottom bar info such as date taken */
showBottomBar() {
return !this.isVideo && this.fullyOpened && this.currentPhoto?.imageInfo;
showBottomBar(): boolean {
return !this.isVideo && this.fullyOpened && Boolean(this.currentPhoto?.imageInfo);
},
/** Get date taken string */
currentDateTaken() {
currentDateTaken(): string | null {
const date = this.currentPhoto?.imageInfo?.datetaken;
if (!date) return null;
return utils.getLongDateStr(new Date(date * 1000), false, true);
},
/** Get download link for current photo */
currentDownloadLink() {
currentDownloadLink(): string | null {
return this.currentPhoto
? window.location.origin + getDownloadLink(this.currentPhoto)
: null;
},
/** Allow opening editor */
canEdit() {
canEdit(): boolean {
return (
this.currentPhoto?.mimetype?.startsWith("image/") &&
!this.currentPhoto.liveid
@ -337,7 +274,7 @@ export default defineComponent({
},
/** Does the browser support native share API */
canShare() {
canShare(): boolean {
return "share" in navigator && this.currentPhoto && !this.isVideo;
},
},
@ -369,7 +306,7 @@ export default defineComponent({
},
/** Event on file changed */
handleFileUpdated({ fileid }: { fileid: number }) {
handleFileUpdated({ fileid }: { fileid: number; }) {
if (this.currentPhoto && this.currentPhoto.fileid === fileid) {
this.currentPhoto.etag += "_";
this.currentPhoto.imageInfo = null;
@ -547,14 +484,14 @@ export default defineComponent({
});
// Video support
new PsVideo(this.photoswipe, {
new PsVideo(<PhotoSwipe>this.photoswipe, {
videoAttributes: { controls: "", playsinline: "", preload: "none" },
autoplay: true,
preventDragOffset: 40,
});
// Live photo support
new PsLivePhoto(this.photoswipe, {});
new PsLivePhoto(<PhotoSwipe>this.photoswipe, {});
// Patch the close button to stop the slideshow
const _close = this.photoswipe.close.bind(this.photoswipe);
@ -1170,6 +1107,7 @@ export default defineComponent({
transition: opacity 0.2s ease-in-out;
opacity: 0;
pointer-events: none;
&.showControls {
opacity: 1;
pointer-events: auto;
@ -1188,6 +1126,7 @@ export default defineComponent({
transition: opacity 0.2s ease-in-out;
opacity: 0;
&.showControls.showBottomBar {
opacity: 1;
}
@ -1197,6 +1136,7 @@ export default defineComponent({
font-weight: bold;
font-size: 0.9em;
}
&.description {
margin-top: -2px;
margin-bottom: 2px;
@ -1228,6 +1168,7 @@ export default defineComponent({
}
:deep .plyr__volume {
// Cannot be vertical yet :(
@media (max-width: 768px) {
display: none;
@ -1255,6 +1196,7 @@ export default defineComponent({
cursor: pointer;
}
}
.pswp__icn-shadow {
display: none;
}

View File

@ -1,7 +1,7 @@
/// <reference types="@nextcloud/typings" />
import "reflect-metadata";
import Vue from "vue";
import { createApp } from "vue";
import VueVirtualScroller from "vue-virtual-scroller";
import "vue-virtual-scroller/dist/vue-virtual-scroller.css";
import GlobalMixin from "./mixins/GlobalMixin";
@ -66,10 +66,6 @@ if (!globalThis.videoClientIdPersistent) {
);
}
Vue.mixin(GlobalMixin);
Vue.mixin(UserConfig);
Vue.use(VueVirtualScroller);
// https://github.com/nextcloud/photos/blob/156f280c0476c483cb9ce81769ccb0c1c6500a4e/src/main.js
// TODO: remove when we have a proper fileinfo standalone library
// original scripts are loaded from
@ -90,8 +86,12 @@ window.addEventListener("DOMContentLoaded", () => {
);
});
export default new Vue({
el: "#content",
router,
render: (h) => h(App),
});
const Vue = createApp(App);
Vue.use(router);
Vue.use(VueVirtualScroller);
Vue.mixin(GlobalMixin);
Vue.mixin(UserConfig);
Vue.mount("#content");
export default Vue;

View File

@ -2,11 +2,12 @@ import { emit, subscribe, unsubscribe } from "@nextcloud/event-bus";
import { loadState } from "@nextcloud/initial-state";
import axios from "@nextcloud/axios";
import { API } from "../services/API";
import { defineComponent } from "vue";
const eventName = "memories:user-config-changed";
const localSettings = ["squareThumbs", "showFaceRect"];
export default {
export default defineComponent({
name: "UserConfig",
data() {
@ -83,4 +84,4 @@ export default {
emit(eventName, { setting, value });
},
},
};
});

View File

@ -1,16 +1,13 @@
import { generateUrl } from "@nextcloud/router";
import { translate as t, translatePlural as n } from "@nextcloud/l10n";
import Router from "vue-router";
import Vue from "vue";
import { createRouter, createWebHistory } from "vue-router";
import Timeline from "./components/Timeline.vue";
Vue.use(Router);
export default new Router({
mode: "history",
export default createRouter({
// if index.php is in the url AND we got this far, then it's working:
// let's keep using index.php in the url
base: generateUrl("/apps/memories"),
history: createWebHistory(generateUrl("/apps/memories")),
linkActiveClass: "active",
routes: [
{
@ -23,7 +20,7 @@ export default new Router({
},
{
path: "/folders/:path*",
path: "/folders/:path*", // REMOVED IN VUE 3
component: Timeline,
name: "folders",
props: (route) => ({
@ -95,7 +92,7 @@ export default new Router({
},
{
path: "/tags/:name*",
path: "/tags/:name*", // REMOVED IN VUE 3
component: Timeline,
name: "tags",
props: (route) => ({
@ -106,9 +103,8 @@ export default new Router({
{
path: "/maps",
name: "maps",
// router-link doesn't support external url, let's force the redirect
beforeEnter() {
window.open(generateUrl("/apps/maps"), "_blank");
redirect: () => {
return generateUrl("/apps/maps");
},
},

View File

@ -6,8 +6,11 @@ const gen = generateUrl;
/** Add auth token to this URL */
function tok(url: string) {
if (vuerouter.currentRoute.name === "folder-share") {
url = API.Q(url, `folder_share=${vuerouter.currentRoute.params.token}`);
if (vuerouter.currentRoute.value.name === "folder-share") {
url = API.Q(
url,
`folder_share=${vuerouter.currentRoute.value.params.token}`
);
}
return url;
}

View File

@ -27,15 +27,11 @@ import { generateRemoteUrl } from "@nextcloud/router";
// Monkey business
import * as rq from "webdav/dist/node/request";
(<any>rq).prepareRequestOptionsOld = rq.prepareRequestOptions.bind(rq);
(<any>rq).prepareRequestOptions = function (
requestOptions,
context,
userOptions
) {
const prepareRequestOptionsOld = rq.prepareRequestOptions.bind(rq);
(<any>rq).prepareRequestOptions = (requestOptions, context, userOptions) => {
requestOptions.method = userOptions.method || requestOptions.method;
return this.prepareRequestOptionsOld(requestOptions, context, userOptions);
}.bind(rq);
return prepareRequestOptionsOld(requestOptions, context, userOptions);
};
// force our axios
const patcher = webdav.getPatcher();

View File

@ -235,7 +235,7 @@ export function convertFlags(photo: IPhoto) {
* This function does not check if this is the folder route
*/
export function getFolderRoutePath(basePath: string) {
let path: any = vuerouter.currentRoute.params.path || "/";
let path: any = vuerouter.currentRoute.value.params.path || "/";
path = typeof path === "string" ? path : path.join("/");
path = basePath + "/" + path;
path = path.replace(/\/\/+/, "/"); // Remove double slashes

View File

@ -44,9 +44,13 @@ const GET_FILE_CHUNK_SIZE = 50;
*/
export async function getFiles(photos: IPhoto[]): Promise<IFileInfo[]> {
// Check if albums
const route = vuerouter.currentRoute;
const route = vuerouter.currentRoute.value;
if (route.name === "albums") {
return getAlbumFileInfos(photos, route.params.user, route.params.name);
return getAlbumFileInfos(
photos,
<string>route.params.user,
<string>route.params.name
);
}
// Get file infos

View File

@ -48,8 +48,8 @@ export async function downloadFilesByPhotos(photos: IPhoto[]) {
/** Get URL to download one file (e.g. for video streaming) */
export function getDownloadLink(photo: IPhoto) {
// Check if public
if (vuerouter.currentRoute.name === "folder-share") {
const token = window.vuerouter.currentRoute.params.token;
if (vuerouter.currentRoute.value.name === "folder-share") {
const token = window.vuerouter.currentRoute.value.params.token;
// TODO: allow proper dav access without the need of basic auth
// https://github.com/nextcloud/server/issues/19700
return generateUrl(`/s/${token}/download?path={dirname}&files={basename}`, {
@ -59,12 +59,12 @@ export function getDownloadLink(photo: IPhoto) {
}
// Check if albums
const route = vuerouter.currentRoute;
const route = vuerouter.currentRoute.value;
if (route.name === "albums") {
const fInfos = getAlbumFileInfos(
[photo],
route.params.user,
route.params.name
route.params.user as string,
route.params.name as string
);
if (fInfos.length) {
return generateUrl(`/remote.php/dav${fInfos[0].originalFilename}`);

View File

@ -1,5 +1,3 @@
import { VueConstructor } from "vue";
export type IFileInfo = {
/** Same as fileid */
id: number;
@ -229,7 +227,7 @@ export type ISelectionAction = {
/** Display text */
name: string;
/** Icon component */
icon: VueConstructor;
icon: any;
/** Action to perform */
callback: (selection: Map<number, IPhoto>) => Promise<void>;
/** Condition to check for including */

41
src/vue-globals.d.ts vendored 100644
View File

@ -0,0 +1,41 @@
import { constants } from "./services/Utils";
import { translate as t, translatePlural as n } from "@nextcloud/l10n";
declare module "vue" {
interface ComponentCustomProperties {
// GlobalMixin.ts
t: typeof t;
n: typeof n;
c: typeof constants.c;
TagDayID: typeof constants.TagDayID;
TagDayIDValueSet: typeof constants.TagDayIDValueSet;
state_noDownload: boolean;
// UserConfig.ts
config_timelinePath: string;
config_foldersPath: string;
config_showHidden: boolean;
config_tagsEnabled: boolean;
config_recognizeEnabled: boolean;
config_facerecognitionInstalled: boolean;
config_facerecognitionEnabled: boolean;
config_mapsEnabled: boolean;
config_albumsEnabled: boolean;
config_squareThumbs: boolean;
config_showFaceRect: boolean;
config_eventName: string;
updateSetting(setting: string): Promise<void>;
updateLocalSetting({
setting,
value,
}: {
setting: string;
value: any;
}): void;
}
}
export {};

8
src/vue-shims.d.ts vendored
View File

@ -1,10 +1,10 @@
declare module "*.vue" {
import Vue from "vue";
export default Vue;
import { defineComponent } from "vue";
const Component: ReturnType<typeof defineComponent>;
export default Component;
}
declare module "*.svg" {
import Vue, { VueConstructor } from "vue";
const content: VueConstructor<Vue>;
const content: any;
export default content;
}

View File

@ -1,13 +1,15 @@
{
"compilerOptions": {
"module": "ESNext",
"moduleResolution": "Node",
"resolveJsonModule": true,
"useDefineForClassFields": true,
"jsx": "preserve",
"lib": ["dom", "es2017"],
"target": "ES2017",
"module": "es2020",
"moduleResolution": "node",
"sourceMap": true,
"allowSyntheticDefaultImports": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"jsx": "preserve"
"noImplicitThis": true,
"esModuleInterop": true
}
}

144
webpack-base.js 100644
View File

@ -0,0 +1,144 @@
/**
* @copyright Copyright (c) 2020 John Molakvoæ <skjnldsv@protonmail.com>
*
* @author John Molakvoæ <skjnldsv@protonmail.com>
*
* @license AGPL-3.0-or-later
*
* 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/>.
*
*/
const path = require("path");
const webpack = require("webpack");
const { VueLoaderPlugin } = require("vue-loader");
const NodePolyfillPlugin = require('node-polyfill-webpack-plugin')
const TerserPlugin = require("terser-webpack-plugin");
const appName = process.env.npm_package_name;
const appVersion = process.env.npm_package_version;
const buildMode = process.env.NODE_ENV;
const isDev = buildMode === "development";
console.info("Building", appName, appVersion, "\n");
const rules = {
RULE_CSS: {
test: /\.css$/,
use: ["style-loader", "css-loader"],
},
RULE_SCSS: {
test: /\.scss$/,
use: ["style-loader", "css-loader", "sass-loader"],
},
RULE_VUE: {
test: /\.vue$/,
loader: "vue-loader",
},
RULE_JS: {
test: /\.js$/,
loader: "babel-loader",
exclude: /node_modules/,
},
RULE_ASSETS: {
test: /\.(png|jpe?g|gif|svg|woff2?|eot|ttf)$/,
type: "asset/inline",
},
};
module.exports = {
target: "web",
mode: buildMode,
devtool: isDev ? "cheap-source-map" : "source-map",
entry: {
main: path.resolve(path.join("src", "main.js")),
},
output: {
path: path.resolve("./js"),
publicPath: path.join("/apps/", appName, "/js/"),
// Output file names
filename: `${appName}-[name].js?v=[contenthash]`,
chunkFilename: `${appName}-[name].js?v=[contenthash]`,
// Clean output before each build
clean: true,
// Make sure sourcemaps have a proper path and do not
// leak local paths https://github.com/webpack/webpack/issues/3603
devtoolNamespace: appName,
devtoolModuleFilenameTemplate(info) {
const rootDir = process.cwd();
const rel = path.relative(rootDir, info.absoluteResourcePath);
return `webpack:///${appName}/${rel}`;
},
},
devServer: {
hot: true,
host: "127.0.0.1",
port: 3000,
client: {
overlay: false,
},
devMiddleware: {
writeToDisk: true,
},
headers: {
"Access-Control-Allow-Origin": "*",
},
},
cache: !isDev,
optimization: {
chunkIds: "named",
splitChunks: {
automaticNameDelimiter: "-",
},
minimize: !isDev,
minimizer: [
new TerserPlugin({
terserOptions: {
output: {
comments: false,
},
},
extractComments: true,
}),
],
},
module: {
rules: Object.values(rules),
},
plugins: [
new VueLoaderPlugin(),
// Make sure we auto-inject node polyfills on demand
// https://webpack.js.org/blog/2020-10-10-webpack-5-release/#automatic-nodejs-polyfills-removed
new NodePolyfillPlugin(),
// Make appName & appVersion available as a constant
new webpack.DefinePlugin({ appName: JSON.stringify(appName) }),
new webpack.DefinePlugin({ appVersion: JSON.stringify(appVersion) }),
],
resolve: {
extensions: ["*", ".js", ".vue"],
symlinks: false,
},
};

View File

@ -1,4 +1,4 @@
const webpackConfig = require('@nextcloud/webpack-vue-config')
const webpackConfig = require('./webpack-base')
const WorkboxPlugin = require('workbox-webpack-plugin')
const path = require('path')
@ -14,9 +14,6 @@ webpackConfig.module.rules.push({
},
});
webpackConfig.resolve.extensions.push('.ts');
webpackConfig.resolve.alias = {
'vue$': 'vue/dist/vue.esm.js',
}
webpackConfig.entry.main = path.resolve(path.join('src', 'main'));
webpackConfig.watchOptions = {
@ -24,11 +21,13 @@ webpackConfig.watchOptions = {
aggregateTimeout: 300,
};
webpackConfig.plugins.push(
new WorkboxPlugin.InjectManifest({
swSrc: path.resolve(path.join('src', 'service-worker.js')),
swDest: 'memories-service-worker.js',
})
);
if (!isDev) {
webpackConfig.plugins.push(
new WorkboxPlugin.InjectManifest({
swSrc: path.resolve(path.join('src', 'service-worker.js')),
swDest: 'memories-service-worker.js',
})
);
}
module.exports = webpackConfig