Migrate to vue 3 (broken)

vue3
Varun Patil 2022-12-10 04:42:00 -08:00
parent 555697a404
commit c87134b16d
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/l10n": "^1.6.0",
"@nextcloud/paths": "^2.1.0", "@nextcloud/paths": "^2.1.0",
"@nextcloud/sharing": "^0.1.0", "@nextcloud/sharing": "^0.1.0",
"@nextcloud/vue": "7.1.0", "@nextcloud/vue": "7.2.0",
"camelcase": "^7.0.0", "camelcase": "^7.0.0",
"filerobot-image-editor": "^4.3.7", "filerobot-image-editor": "^4.3.7",
"justified-layout": "^4.1.0", "justified-layout": "^4.1.0",
"moment": "^2.29.4", "moment": "^2.29.4",
"node-polyfill-webpack-plugin": "^2.0.1",
"path-posix": "^1.0.0", "path-posix": "^1.0.0",
"photoswipe": "^5.3.4", "photoswipe": "^5.3.4",
"plyr": "^3.7.3", "plyr": "^3.7.3",
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"video.js": "^7.20.3", "video.js": "^7.20.3",
"videojs-contrib-quality-levels": "^2.2.0", "videojs-contrib-quality-levels": "^2.2.1",
"vue": "^2.7.10", "vue": "^3.2.45",
"vue-material-design-icons": "^5.1.2", "vue-material-design-icons": "^5.1.2",
"vue-router": "^3.5.4", "vue-router": "^4.1.6",
"vue-virtual-scroller": "1.1.2", "vue-virtual-scroller": "2.0.0-beta.5",
"webdav": "^4.11.2" "webdav": "^4.11.2"
}, },
"browserslist": [ "browserslist": [
@ -59,13 +60,19 @@
"devDependencies": { "devDependencies": {
"@nextcloud/babel-config": "^1.0.0", "@nextcloud/babel-config": "^1.0.0",
"@nextcloud/browserslist-config": "^2.3.0", "@nextcloud/browserslist-config": "^2.3.0",
"@nextcloud/webpack-vue-config": "^5.4.0", "@playwright/test": "^1.28.1",
"@playwright/test": "^1.28.0",
"@types/url-parse": "^1.4.8", "@types/url-parse": "^1.4.8",
"@types/video.js": "^7.3.49", "@types/video.js": "^7.3.50",
"playwright": "^1.28.0", "babel-loader": "^9.1.0",
"ts-loader": "^9.4.1", "css-loader": "^6.7.2",
"typescript": "^4.9.3", "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" "workbox-webpack-plugin": "^6.5.4"
} }
} }

View File

@ -2,8 +2,8 @@
<FirstStart v-if="isFirstStart" /> <FirstStart v-if="isFirstStart" />
<NcContent <NcContent
v-else-if="false"
app-name="memories" app-name="memories"
v-else
:class="{ :class="{
'remove-gap': removeOuterGap, 'remove-gap': removeOuterGap,
}" }"
@ -35,10 +35,14 @@
</div> </div>
</NcAppContent> </NcAppContent>
</NcContent> </NcContent>
<div class="outer" v-else>
<router-view />
</div>
</template> </template>
<script lang="ts"> <script lang="ts">
import Vue, { defineComponent } from "vue"; import { defineComponent } from "vue";
import NcContent from "@nextcloud/vue/dist/Components/NcContent"; import NcContent from "@nextcloud/vue/dist/Components/NcContent";
import NcAppContent from "@nextcloud/vue/dist/Components/NcAppContent"; import NcAppContent from "@nextcloud/vue/dist/Components/NcAppContent";
@ -95,16 +99,16 @@ export default defineComponent({
data() { data() {
return { return {
navItems: [], navItems: [],
metadataComponent: null as Metadata, metadataComponent: null as any,
}; };
}, },
computed: { computed: {
ncVersion() { ncVersion(): number {
const version = (<any>window.OC).config.version.split("."); const version = (<any>window.OC).config.version.split(".");
return Number(version[0]); return Number(version[0]);
}, },
recognize() { recognize(): string | boolean {
if (!this.config_recognizeEnabled) { if (!this.config_recognizeEnabled) {
return false; return false;
} }
@ -115,7 +119,7 @@ export default defineComponent({
return t("memories", "People"); return t("memories", "People");
}, },
facerecognition() { facerecognition(): string | boolean {
if (!this.config_facerecognitionInstalled) { if (!this.config_facerecognitionInstalled) {
return false; return false;
} }
@ -126,16 +130,16 @@ export default defineComponent({
return t("memories", "People"); return t("memories", "People");
}, },
isFirstStart() { isFirstStart(): boolean {
return this.config_timelinePath === "EMPTY"; return this.config_timelinePath === "EMPTY";
}, },
showAlbums() { showAlbums(): boolean {
return this.config_albumsEnabled; return this.config_albumsEnabled;
}, },
removeOuterGap() { removeOuterGap(): boolean {
return this.ncVersion >= 25; return this.ncVersion >= 25;
}, },
showNavigation() { showNavigation(): boolean {
return this.$route.name !== "folder-share"; return this.$route.name !== "folder-share";
}, },
}, },
@ -174,7 +178,7 @@ export default defineComponent({
if (this.metadataComponent) { if (this.metadataComponent) {
this.metadataComponent.$destroy(); this.metadataComponent.$destroy();
} }
this.metadataComponent = new Vue(Metadata); this.metadataComponent = new Metadata();
// Only mount after we have all the info we need // Only mount after we have all the info we need
await this.metadataComponent.update(fileInfo); await this.metadataComponent.update(fileInfo);
this.metadataComponent.$mount(el); this.metadataComponent.$mount(el);
@ -281,9 +285,9 @@ export default defineComponent({
if (globalThis.windowInnerWidth <= 1024) nav?.toggleNavigation(false); if (globalThis.windowInnerWidth <= 1024) nav?.toggleNavigation(false);
}, },
doRouteChecks() { doRouteChecks(): void {
if (this.$route.name === "folder-share") { 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"> <div class="text">
{{ t("memories", "A better photos experience awaits you") }} <br /> {{ 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> </div>
@ -160,6 +160,7 @@ export default defineComponent({
transition: opacity 1s ease; transition: opacity 1s ease;
opacity: 0; opacity: 0;
&.show { &.show {
opacity: 1; opacity: 1;
} }
@ -174,7 +175,7 @@ export default defineComponent({
width: 100%; width: 100%;
filter: var(--background-invert-if-dark); filter: var(--background-invert-if-dark);
> img { >img {
max-width: calc(100vw - 40px); max-width: calc(100vw - 40px);
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
/// <reference types="@nextcloud/typings" /> /// <reference types="@nextcloud/typings" />
import "reflect-metadata"; import "reflect-metadata";
import Vue from "vue"; import { createApp } from "vue";
import VueVirtualScroller from "vue-virtual-scroller"; import VueVirtualScroller from "vue-virtual-scroller";
import "vue-virtual-scroller/dist/vue-virtual-scroller.css"; import "vue-virtual-scroller/dist/vue-virtual-scroller.css";
import GlobalMixin from "./mixins/GlobalMixin"; 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 // https://github.com/nextcloud/photos/blob/156f280c0476c483cb9ce81769ccb0c1c6500a4e/src/main.js
// TODO: remove when we have a proper fileinfo standalone library // TODO: remove when we have a proper fileinfo standalone library
// original scripts are loaded from // original scripts are loaded from
@ -90,8 +86,12 @@ window.addEventListener("DOMContentLoaded", () => {
); );
}); });
export default new Vue({ const Vue = createApp(App);
el: "#content", Vue.use(router);
router, Vue.use(VueVirtualScroller);
render: (h) => h(App),
}); 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 { loadState } from "@nextcloud/initial-state";
import axios from "@nextcloud/axios"; import axios from "@nextcloud/axios";
import { API } from "../services/API"; import { API } from "../services/API";
import { defineComponent } from "vue";
const eventName = "memories:user-config-changed"; const eventName = "memories:user-config-changed";
const localSettings = ["squareThumbs", "showFaceRect"]; const localSettings = ["squareThumbs", "showFaceRect"];
export default { export default defineComponent({
name: "UserConfig", name: "UserConfig",
data() { data() {
@ -83,4 +84,4 @@ export default {
emit(eventName, { setting, value }); emit(eventName, { setting, value });
}, },
}, },
}; });

View File

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

View File

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

View File

@ -27,15 +27,11 @@ import { generateRemoteUrl } from "@nextcloud/router";
// Monkey business // Monkey business
import * as rq from "webdav/dist/node/request"; import * as rq from "webdav/dist/node/request";
(<any>rq).prepareRequestOptionsOld = rq.prepareRequestOptions.bind(rq); const prepareRequestOptionsOld = rq.prepareRequestOptions.bind(rq);
(<any>rq).prepareRequestOptions = function ( (<any>rq).prepareRequestOptions = (requestOptions, context, userOptions) => {
requestOptions,
context,
userOptions
) {
requestOptions.method = userOptions.method || requestOptions.method; requestOptions.method = userOptions.method || requestOptions.method;
return this.prepareRequestOptionsOld(requestOptions, context, userOptions); return prepareRequestOptionsOld(requestOptions, context, userOptions);
}.bind(rq); };
// force our axios // force our axios
const patcher = webdav.getPatcher(); 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 * This function does not check if this is the folder route
*/ */
export function getFolderRoutePath(basePath: string) { 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 = typeof path === "string" ? path : path.join("/");
path = basePath + "/" + path; path = basePath + "/" + path;
path = path.replace(/\/\/+/, "/"); // Remove double slashes 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[]> { export async function getFiles(photos: IPhoto[]): Promise<IFileInfo[]> {
// Check if albums // Check if albums
const route = vuerouter.currentRoute; const route = vuerouter.currentRoute.value;
if (route.name === "albums") { 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 // 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) */ /** Get URL to download one file (e.g. for video streaming) */
export function getDownloadLink(photo: IPhoto) { export function getDownloadLink(photo: IPhoto) {
// Check if public // Check if public
if (vuerouter.currentRoute.name === "folder-share") { if (vuerouter.currentRoute.value.name === "folder-share") {
const token = window.vuerouter.currentRoute.params.token; const token = window.vuerouter.currentRoute.value.params.token;
// TODO: allow proper dav access without the need of basic auth // TODO: allow proper dav access without the need of basic auth
// https://github.com/nextcloud/server/issues/19700 // https://github.com/nextcloud/server/issues/19700
return generateUrl(`/s/${token}/download?path={dirname}&files={basename}`, { return generateUrl(`/s/${token}/download?path={dirname}&files={basename}`, {
@ -59,12 +59,12 @@ export function getDownloadLink(photo: IPhoto) {
} }
// Check if albums // Check if albums
const route = vuerouter.currentRoute; const route = vuerouter.currentRoute.value;
if (route.name === "albums") { if (route.name === "albums") {
const fInfos = getAlbumFileInfos( const fInfos = getAlbumFileInfos(
[photo], [photo],
route.params.user, route.params.user as string,
route.params.name route.params.name as string
); );
if (fInfos.length) { if (fInfos.length) {
return generateUrl(`/remote.php/dav${fInfos[0].originalFilename}`); return generateUrl(`/remote.php/dav${fInfos[0].originalFilename}`);

View File

@ -1,5 +1,3 @@
import { VueConstructor } from "vue";
export type IFileInfo = { export type IFileInfo = {
/** Same as fileid */ /** Same as fileid */
id: number; id: number;
@ -229,7 +227,7 @@ export type ISelectionAction = {
/** Display text */ /** Display text */
name: string; name: string;
/** Icon component */ /** Icon component */
icon: VueConstructor; icon: any;
/** Action to perform */ /** Action to perform */
callback: (selection: Map<number, IPhoto>) => Promise<void>; callback: (selection: Map<number, IPhoto>) => Promise<void>;
/** Condition to check for including */ /** 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" { declare module "*.vue" {
import Vue from "vue"; import { defineComponent } from "vue";
export default Vue; const Component: ReturnType<typeof defineComponent>;
export default Component;
} }
declare module "*.svg" { declare module "*.svg" {
import Vue, { VueConstructor } from "vue"; const content: any;
const content: VueConstructor<Vue>;
export default content; export default content;
} }

View File

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

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