refactor: update prettier config

Signed-off-by: Varun Patil <radialapps@gmail.com>
pull/602/head
Varun Patil 2023-04-19 16:14:30 -07:00
parent 9da0e87a7b
commit aa81acd139
104 changed files with 2362 additions and 3349 deletions

4
.prettierrc 100644
View File

@ -0,0 +1,4 @@
{
"printWidth": 120,
"singleQuote": true
}

View File

@ -23,10 +23,7 @@
</template> </template>
<template #footer> <template #footer>
<NcAppNavigationItem <NcAppNavigationItem :name="t('memories', 'Settings')" @click="showSettings">
:name="t('memories', 'Settings')"
@click="showSettings"
>
<CogIcon slot="icon" :size="20" /> <CogIcon slot="icon" :size="20" />
</NcAppNavigationItem> </NcAppNavigationItem>
</template> </template>
@ -53,41 +50,40 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import Vue, { defineComponent } from "vue"; import Vue, { 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';
import NcAppNavigation from "@nextcloud/vue/dist/Components/NcAppNavigation"; import NcAppNavigation from '@nextcloud/vue/dist/Components/NcAppNavigation';
const NcAppNavigationItem = () => const NcAppNavigationItem = () => import('@nextcloud/vue/dist/Components/NcAppNavigationItem');
import("@nextcloud/vue/dist/Components/NcAppNavigationItem");
import { generateUrl } from "@nextcloud/router"; import { generateUrl } from '@nextcloud/router';
import { translate as t } from "@nextcloud/l10n"; import { translate as t } from '@nextcloud/l10n';
import { emit } from "@nextcloud/event-bus"; import { emit } from '@nextcloud/event-bus';
import * as utils from "./services/Utils"; import * as utils from './services/Utils';
import UserConfig from "./mixins/UserConfig"; import UserConfig from './mixins/UserConfig';
import Timeline from "./components/Timeline.vue"; import Timeline from './components/Timeline.vue';
import Settings from "./components/Settings.vue"; import Settings from './components/Settings.vue';
import FirstStart from "./components/FirstStart.vue"; import FirstStart from './components/FirstStart.vue';
import Metadata from "./components/Metadata.vue"; import Metadata from './components/Metadata.vue';
import Sidebar from "./components/Sidebar.vue"; import Sidebar from './components/Sidebar.vue';
import EditMetadataModal from "./components/modal/EditMetadataModal.vue"; import EditMetadataModal from './components/modal/EditMetadataModal.vue';
import NodeShareModal from "./components/modal/NodeShareModal.vue"; import NodeShareModal from './components/modal/NodeShareModal.vue';
import ShareModal from "./components/modal/ShareModal.vue"; import ShareModal from './components/modal/ShareModal.vue';
import ImageMultiple from "vue-material-design-icons/ImageMultiple.vue"; import ImageMultiple from 'vue-material-design-icons/ImageMultiple.vue';
import FolderIcon from "vue-material-design-icons/Folder.vue"; import FolderIcon from 'vue-material-design-icons/Folder.vue';
import Star from "vue-material-design-icons/Star.vue"; import Star from 'vue-material-design-icons/Star.vue';
import Video from "vue-material-design-icons/PlayCircle.vue"; import Video from 'vue-material-design-icons/PlayCircle.vue';
import AlbumIcon from "vue-material-design-icons/ImageAlbum.vue"; import AlbumIcon from 'vue-material-design-icons/ImageAlbum.vue';
import ArchiveIcon from "vue-material-design-icons/PackageDown.vue"; import ArchiveIcon from 'vue-material-design-icons/PackageDown.vue';
import CalendarIcon from "vue-material-design-icons/Calendar.vue"; import CalendarIcon from 'vue-material-design-icons/Calendar.vue';
import PeopleIcon from "vue-material-design-icons/AccountBoxMultiple.vue"; import PeopleIcon from 'vue-material-design-icons/AccountBoxMultiple.vue';
import MarkerIcon from "vue-material-design-icons/MapMarker.vue"; import MarkerIcon from 'vue-material-design-icons/MapMarker.vue';
import TagsIcon from "vue-material-design-icons/Tag.vue"; import TagsIcon from 'vue-material-design-icons/Tag.vue';
import MapIcon from "vue-material-design-icons/Map.vue"; import MapIcon from 'vue-material-design-icons/Map.vue';
import CogIcon from "vue-material-design-icons/Cog.vue"; import CogIcon from 'vue-material-design-icons/Cog.vue';
type NavItem = { type NavItem = {
name: string; name: string;
@ -97,7 +93,7 @@ type NavItem = {
}; };
export default defineComponent({ export default defineComponent({
name: "App", name: 'App',
components: { components: {
NcContent, NcContent,
NcAppContent, NcAppContent,
@ -136,7 +132,7 @@ export default defineComponent({
computed: { computed: {
ncVersion(): number { 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]);
}, },
@ -146,10 +142,10 @@ export default defineComponent({
} }
if (this.config_facerecognitionInstalled) { if (this.config_facerecognitionInstalled) {
return t("memories", "People (Recognize)"); return t('memories', 'People (Recognize)');
} }
return t("memories", "People"); return t('memories', 'People');
}, },
facerecognition(): string | false { facerecognition(): string | false {
@ -158,14 +154,14 @@ export default defineComponent({
} }
if (this.config_recognizeEnabled) { if (this.config_recognizeEnabled) {
return t("memories", "People (Face Recognition)"); return t('memories', 'People (Face Recognition)');
} }
return t("memories", "People"); return t('memories', 'People');
}, },
isFirstStart(): boolean { isFirstStart(): boolean {
return this.config_timelinePath === "EMPTY"; return this.config_timelinePath === 'EMPTY';
}, },
showAlbums(): boolean { showAlbums(): boolean {
@ -177,11 +173,11 @@ export default defineComponent({
}, },
showNavigation(): boolean { showNavigation(): boolean {
return !this.$route.name?.endsWith("-share"); return !this.$route.name?.endsWith('-share');
}, },
removeNavGap(): boolean { removeNavGap(): boolean {
return this.$route.name === "map"; return this.$route.name === 'map';
}, },
}, },
@ -196,10 +192,10 @@ export default defineComponent({
const onResize = () => { const onResize = () => {
globalThis.windowInnerWidth = window.innerWidth; globalThis.windowInnerWidth = window.innerWidth;
globalThis.windowInnerHeight = window.innerHeight; globalThis.windowInnerHeight = window.innerHeight;
emit("memories:window:resize", {}); emit('memories:window:resize', {});
}; };
window.addEventListener("resize", () => { window.addEventListener('resize', () => {
utils.setRenewingTimeout(this, "resizeTimer", onResize, 100); utils.setRenewingTimeout(this, 'resizeTimer', onResize, 100);
}); });
}, },
@ -207,25 +203,22 @@ export default defineComponent({
this.doRouteChecks(); this.doRouteChecks();
// Populate navigation // Populate navigation
this.navItems = this.navItemsAll().filter( this.navItems = this.navItemsAll().filter((item) => typeof item.if === 'undefined' || Boolean(item.if));
(item) => typeof item.if === "undefined" || Boolean(item.if)
);
// Store CSS variables modified // Store CSS variables modified
const root = document.documentElement; const root = document.documentElement;
const colorPrimary = const colorPrimary = getComputedStyle(root).getPropertyValue('--color-primary');
getComputedStyle(root).getPropertyValue("--color-primary"); root.style.setProperty('--color-primary-select-light', `${colorPrimary}40`);
root.style.setProperty("--color-primary-select-light", `${colorPrimary}40`); root.style.setProperty('--plyr-color-main', colorPrimary);
root.style.setProperty("--plyr-color-main", colorPrimary);
// Register sidebar metadata tab // Register sidebar metadata tab
const OCA = globalThis.OCA; const OCA = globalThis.OCA;
if (OCA.Files && OCA.Files.Sidebar) { if (OCA.Files && OCA.Files.Sidebar) {
OCA.Files.Sidebar.registerTab( OCA.Files.Sidebar.registerTab(
new OCA.Files.Sidebar.Tab({ new OCA.Files.Sidebar.Tab({
id: "memories-metadata", id: 'memories-metadata',
name: this.t("memories", "Info"), name: this.t('memories', 'Info'),
icon: "icon-details", icon: 'icon-details',
mount(el, fileInfo, context) { mount(el, fileInfo, context) {
this.metadataComponent?.$destroy?.(); this.metadataComponent?.$destroy?.();
@ -246,21 +239,21 @@ export default defineComponent({
}, },
async beforeMount() { async beforeMount() {
if ("serviceWorker" in navigator) { if ('serviceWorker' in navigator) {
// Use the window load event to keep the page load performant // Use the window load event to keep the page load performant
window.addEventListener("load", async () => { window.addEventListener('load', async () => {
try { try {
const url = generateUrl("/apps/memories/service-worker.js"); const url = generateUrl('/apps/memories/service-worker.js');
const registration = await navigator.serviceWorker.register(url, { const registration = await navigator.serviceWorker.register(url, {
scope: generateUrl("/apps/memories"), scope: generateUrl('/apps/memories'),
}); });
console.log("SW registered: ", registration); console.log('SW registered: ', registration);
} catch (error) { } catch (error) {
console.error("SW registration failed: ", error); console.error('SW registration failed: ', error);
} }
}); });
} else { } else {
console.debug("Service Worker is not enabled on this browser."); console.debug('Service Worker is not enabled on this browser.');
} }
}, },
@ -268,68 +261,68 @@ export default defineComponent({
navItemsAll(): NavItem[] { navItemsAll(): NavItem[] {
return [ return [
{ {
name: "timeline", name: 'timeline',
icon: ImageMultiple, icon: ImageMultiple,
title: t("memories", "Timeline"), title: t('memories', 'Timeline'),
}, },
{ {
name: "folders", name: 'folders',
icon: FolderIcon, icon: FolderIcon,
title: t("memories", "Folders"), title: t('memories', 'Folders'),
}, },
{ {
name: "favorites", name: 'favorites',
icon: Star, icon: Star,
title: t("memories", "Favorites"), title: t('memories', 'Favorites'),
}, },
{ {
name: "videos", name: 'videos',
icon: Video, icon: Video,
title: t("memories", "Videos"), title: t('memories', 'Videos'),
}, },
{ {
name: "albums", name: 'albums',
icon: AlbumIcon, icon: AlbumIcon,
title: t("memories", "Albums"), title: t('memories', 'Albums'),
if: this.showAlbums, if: this.showAlbums,
}, },
{ {
name: "recognize", name: 'recognize',
icon: PeopleIcon, icon: PeopleIcon,
title: this.recognize || "", title: this.recognize || '',
if: this.recognize, if: this.recognize,
}, },
{ {
name: "facerecognition", name: 'facerecognition',
icon: PeopleIcon, icon: PeopleIcon,
title: this.facerecognition || "", title: this.facerecognition || '',
if: this.facerecognition, if: this.facerecognition,
}, },
{ {
name: "archive", name: 'archive',
icon: ArchiveIcon, icon: ArchiveIcon,
title: t("memories", "Archive"), title: t('memories', 'Archive'),
}, },
{ {
name: "thisday", name: 'thisday',
icon: CalendarIcon, icon: CalendarIcon,
title: t("memories", "On this day"), title: t('memories', 'On this day'),
}, },
{ {
name: "places", name: 'places',
icon: MarkerIcon, icon: MarkerIcon,
title: t("memories", "Places"), title: t('memories', 'Places'),
if: this.config_placesGis > 0, if: this.config_placesGis > 0,
}, },
{ {
name: "map", name: 'map',
icon: MapIcon, icon: MapIcon,
title: t("memories", "Map"), title: t('memories', 'Map'),
}, },
{ {
name: "tags", name: 'tags',
icon: TagsIcon, icon: TagsIcon,
title: t("memories", "Tags"), title: t('memories', 'Tags'),
if: this.config_tagsEnabled, if: this.config_tagsEnabled,
}, },
]; ];
@ -341,7 +334,7 @@ export default defineComponent({
}, },
doRouteChecks() { doRouteChecks() {
if (this.$route.name?.endsWith("-share")) { if (this.$route.name?.endsWith('-share')) {
this.putShareToken(<string>this.$route.params.token); this.putShareToken(<string>this.$route.params.token);
} }
}, },
@ -350,14 +343,12 @@ export default defineComponent({
// Viewer looks for an input with ID sharingToken with the value as the token // Viewer looks for an input with ID sharingToken with the value as the token
// Create this element or update it otherwise files not gonna open // Create this element or update it otherwise files not gonna open
// https://github.com/nextcloud/viewer/blob/a8c46050fb687dcbb48a022a15a5d1275bf54a8e/src/utils/davUtils.js#L61 // https://github.com/nextcloud/viewer/blob/a8c46050fb687dcbb48a022a15a5d1275bf54a8e/src/utils/davUtils.js#L61
let tokenInput = document.getElementById( let tokenInput = document.getElementById('sharingToken') as HTMLInputElement;
"sharingToken"
) as HTMLInputElement;
if (!tokenInput) { if (!tokenInput) {
tokenInput = document.createElement("input"); tokenInput = document.createElement('input');
tokenInput.id = "sharingToken"; tokenInput.id = 'sharingToken';
tokenInput.type = "hidden"; tokenInput.type = 'hidden';
tokenInput.style.display = "none"; tokenInput.style.display = 'none';
document.body.appendChild(tokenInput); document.body.appendChild(tokenInput);
} }

View File

@ -21,12 +21,12 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from "vue"; import { defineComponent } from 'vue';
import Cluster from "./frame/Cluster.vue"; import Cluster from './frame/Cluster.vue';
import type { ICluster } from "../types"; import type { ICluster } from '../types';
export default defineComponent({ export default defineComponent({
name: "ClusterGrid", name: 'ClusterGrid',
components: { components: {
Cluster, Cluster,
@ -58,7 +58,7 @@ export default defineComponent({
methods: { methods: {
click(item: ICluster) { click(item: ICluster) {
this.$emit("click", item); this.$emit('click', item);
}, },
resize() { resize() {

View File

@ -11,20 +11,20 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from "vue"; import { defineComponent } from 'vue';
import UserConfig from "../mixins/UserConfig"; import UserConfig from '../mixins/UserConfig';
import TopMatter from "./top-matter/TopMatter.vue"; import TopMatter from './top-matter/TopMatter.vue';
import ClusterGrid from "./ClusterGrid.vue"; import ClusterGrid from './ClusterGrid.vue';
import Timeline from "./Timeline.vue"; import Timeline from './Timeline.vue';
import EmptyContent from "./top-matter/EmptyContent.vue"; import EmptyContent from './top-matter/EmptyContent.vue';
import * as dav from "../services/DavRequests"; import * as dav from '../services/DavRequests';
import type { ICluster } from "../types"; import type { ICluster } from '../types';
export default defineComponent({ export default defineComponent({
name: "ClusterView", name: 'ClusterView',
components: { components: {
TopMatter, TopMatter,
@ -63,13 +63,13 @@ export default defineComponent({
this.items = []; this.items = [];
this.loading++; this.loading++;
if (route === "albums") { if (route === 'albums') {
this.items = await dav.getAlbums(3, this.config_albumListSort); this.items = await dav.getAlbums(3, this.config_albumListSort);
} else if (route === "tags") { } else if (route === 'tags') {
this.items = await dav.getTags(); this.items = await dav.getTags();
} else if (route === "recognize" || route === "facerecognition") { } else if (route === 'recognize' || route === 'facerecognition') {
this.items = await dav.getFaceList(route); this.items = await dav.getFaceList(route);
} else if (route === "places") { } else if (route === 'places') {
this.items = await dav.getPlaces(); this.items = await dav.getPlaces();
} }
} finally { } finally {

View File

@ -7,14 +7,12 @@
</div> </div>
<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>
<div class="admin-text" v-if="isAdmin"> <div class="admin-text" v-if="isAdmin">
{{ t("memories", "If you just installed Memories, run:") }} {{ t('memories', 'If you just installed Memories, run:') }}
<br /> <br />
<code>occ memories:index</code> <code>occ memories:index</code>
</div> </div>
@ -27,19 +25,19 @@
{{ info }} <br /> {{ info }} <br />
<NcButton @click="finish" class="button" type="primary"> <NcButton @click="finish" class="button" type="primary">
{{ t("memories", "Continue to Memories") }} {{ t('memories', 'Continue to Memories') }}
</NcButton> </NcButton>
</div> </div>
<NcButton @click="begin" class="button" v-if="info"> <NcButton @click="begin" class="button" v-if="info">
{{ t("memories", "Choose again") }} {{ t('memories', 'Choose again') }}
</NcButton> </NcButton>
<NcButton @click="begin" class="button" type="primary" v-else> <NcButton @click="begin" class="button" type="primary" v-else>
{{ t("memories", "Click here to start") }} {{ t('memories', 'Click here to start') }}
</NcButton> </NcButton>
<div class="footer"> <div class="footer">
{{ t("memories", "You can always change this later in settings") }} {{ t('memories', 'You can always change this later in settings') }}
</div> </div>
</div> </div>
</NcAppContent> </NcAppContent>
@ -47,23 +45,23 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from "vue"; import { defineComponent } from 'vue';
import UserConfig from "../mixins/UserConfig"; import UserConfig from '../mixins/UserConfig';
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';
import NcButton from "@nextcloud/vue/dist/Components/NcButton"; import NcButton from '@nextcloud/vue/dist/Components/NcButton';
import { getFilePickerBuilder } from "@nextcloud/dialogs"; import { getFilePickerBuilder } from '@nextcloud/dialogs';
import { getCurrentUser } from "@nextcloud/auth"; import { getCurrentUser } from '@nextcloud/auth';
import axios from "@nextcloud/axios"; import axios from '@nextcloud/axios';
import banner from "../assets/banner.svg"; import banner from '../assets/banner.svg';
import type { IDay } from "../types"; import type { IDay } from '../types';
import { API } from "../services/API"; import { API } from '../services/API';
export default defineComponent({ export default defineComponent({
name: "FirstStart", name: 'FirstStart',
components: { components: {
NcContent, NcContent,
NcAppContent, NcAppContent,
@ -74,10 +72,10 @@ export default defineComponent({
data: () => ({ data: () => ({
banner, banner,
error: "", error: '',
info: "", info: '',
show: false, show: false,
chosenPath: "", chosenPath: '',
}), }),
mounted() { mounted() {
@ -94,38 +92,26 @@ export default defineComponent({
methods: { methods: {
async begin() { async begin() {
const path = await this.chooseFolder( const path = await this.chooseFolder(this.t('memories', 'Choose the root of your timeline'), '/');
this.t("memories", "Choose the root of your timeline"),
"/"
);
// Get folder days // Get folder days
this.error = ""; this.error = '';
this.info = ""; this.info = '';
let url = API.Q(API.DAYS(), { timelinePath: path }); let url = API.Q(API.DAYS(), { timelinePath: path });
const res = await axios.get<IDay[]>(url); const res = await axios.get<IDay[]>(url);
// Check response // Check response
if (res.status !== 200) { if (res.status !== 200) {
this.error = this.t( this.error = this.t('memories', 'The selected folder does not seem to be valid. Try again.');
"memories",
"The selected folder does not seem to be valid. Try again."
);
return; return;
} }
// Count total photos // Count total photos
const n = res.data.reduce((acc, day) => acc + day.count, 0); const n = res.data.reduce((acc, day) => acc + day.count, 0);
this.info = this.n( this.info = this.n('memories', 'Found {n} item in {path}', 'Found {n} items in {path}', n, {
"memories",
"Found {n} item in {path}",
"Found {n} items in {path}",
n,
{
n, n,
path, path,
} });
);
this.chosenPath = path; this.chosenPath = path;
}, },
@ -133,7 +119,7 @@ export default defineComponent({
this.show = false; this.show = false;
await new Promise((resolve) => setTimeout(resolve, 500)); await new Promise((resolve) => setTimeout(resolve, 500));
this.config_timelinePath = this.chosenPath; this.config_timelinePath = this.chosenPath;
await this.updateSetting("timelinePath"); await this.updateSetting('timelinePath');
}, },
async chooseFolder(title: string, initial: string) { async chooseFolder(title: string, initial: string) {
@ -141,7 +127,7 @@ export default defineComponent({
.setMultiSelect(false) .setMultiSelect(false)
.setModal(true) .setModal(true)
.setType(1) .setType(1)
.addMimeTypeFilter("httpd/unix-directory") .addMimeTypeFilter('httpd/unix-directory')
.allowDirectories() .allowDirectories()
.startAt(initial) .startAt(initial)
.build(); .build();

View File

@ -7,15 +7,15 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from "vue"; import { defineComponent } from 'vue';
import UserConfig from "../mixins/UserConfig"; import UserConfig from '../mixins/UserConfig';
import Folder from "./frame/Folder.vue"; import Folder from './frame/Folder.vue';
import type { IFolder } from "../types"; import type { IFolder } from '../types';
export default defineComponent({ export default defineComponent({
name: "ClusterGrid", name: 'ClusterGrid',
components: { components: {
Folder, Folder,

View File

@ -27,11 +27,8 @@
<div class="edit" v-if="canEdit && field.edit"> <div class="edit" v-if="canEdit && field.edit">
<NcActions :inline="1"> <NcActions :inline="1">
<NcActionButton <NcActionButton :aria-label="t('memories', 'Edit')" @click="field.edit?.()">
:aria-label="t('memories', 'Edit')" {{ t('memories', 'Edit') }}
@click="field.edit?.()"
>
{{ t("memories", "Edit") }}
<template #icon> <EditIcon :size="20" /> </template> <template #icon> <EditIcon :size="20" /> </template>
</NcActionButton> </NcActionButton>
</NcActions> </NcActions>
@ -49,30 +46,30 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from "vue"; import { defineComponent } from 'vue';
import NcActions from "@nextcloud/vue/dist/Components/NcActions"; import NcActions from '@nextcloud/vue/dist/Components/NcActions';
import NcActionButton from "@nextcloud/vue/dist/Components/NcActionButton"; import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton';
import NcLoadingIcon from "@nextcloud/vue/dist/Components/NcLoadingIcon"; import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon';
import axios from "@nextcloud/axios"; import axios from '@nextcloud/axios';
import { subscribe, unsubscribe } from "@nextcloud/event-bus"; import { subscribe, unsubscribe } from '@nextcloud/event-bus';
import { getCanonicalLocale } from "@nextcloud/l10n"; import { getCanonicalLocale } from '@nextcloud/l10n';
import moment from "moment"; import moment from 'moment';
import "moment-timezone"; import 'moment-timezone';
import * as utils from "../services/Utils"; import * as utils from '../services/Utils';
import EditIcon from "vue-material-design-icons/Pencil.vue"; import EditIcon from 'vue-material-design-icons/Pencil.vue';
import CalendarIcon from "vue-material-design-icons/Calendar.vue"; import CalendarIcon from 'vue-material-design-icons/Calendar.vue';
import CameraIrisIcon from "vue-material-design-icons/CameraIris.vue"; import CameraIrisIcon from 'vue-material-design-icons/CameraIris.vue';
import ImageIcon from "vue-material-design-icons/Image.vue"; import ImageIcon from 'vue-material-design-icons/Image.vue';
import InfoIcon from "vue-material-design-icons/InformationOutline.vue"; 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 TagIcon from "vue-material-design-icons/Tag.vue"; import TagIcon from 'vue-material-design-icons/Tag.vue';
import { API } from "../services/API"; import { API } from '../services/API';
import type { IImageInfo } from "../types"; import type { IImageInfo } from '../types';
interface TopField { interface TopField {
title: string; title: string;
@ -83,7 +80,7 @@ interface TopField {
} }
export default defineComponent({ export default defineComponent({
name: "Metadata", name: 'Metadata',
components: { components: {
NcActions, NcActions,
NcActionButton, NcActionButton,
@ -99,11 +96,11 @@ export default defineComponent({
}), }),
mounted() { mounted() {
subscribe("files:file:updated", this.handleFileUpdated); subscribe('files:file:updated', this.handleFileUpdated);
}, },
beforeDestroy() { beforeDestroy() {
unsubscribe("files:file:updated", this.handleFileUpdated); unsubscribe('files:file:updated', this.handleFileUpdated);
}, },
computed: { computed: {
@ -115,8 +112,7 @@ export default defineComponent({
title: this.dateOriginalStr!, title: this.dateOriginalStr!,
subtitle: this.dateOriginalTime!, subtitle: this.dateOriginalTime!,
icon: CalendarIcon, icon: CalendarIcon,
edit: () => edit: () => globalThis.editMetadata([globalThis.currentViewerPhoto], [1]),
globalThis.editMetadata([globalThis.currentViewerPhoto], [1]),
}); });
} }
@ -136,15 +132,14 @@ export default defineComponent({
}); });
} }
const title = this.exif?.["Title"]; const title = this.exif?.['Title'];
const desc = this.exif?.["Description"]; const desc = this.exif?.['Description'];
if (title || desc) { if (title || desc) {
list.push({ list.push({
title: title || this.t("memories", "No title"), title: title || this.t('memories', 'No title'),
subtitle: [desc || this.t("memories", "No description")], subtitle: [desc || this.t('memories', 'No description')],
icon: InfoIcon, icon: InfoIcon,
edit: () => edit: () => globalThis.editMetadata([globalThis.currentViewerPhoto], [3]),
globalThis.editMetadata([globalThis.currentViewerPhoto], [3]),
}); });
} }
@ -153,27 +148,23 @@ export default defineComponent({
title: this.tagNamesStr, title: this.tagNamesStr,
subtitle: [], subtitle: [],
icon: TagIcon, icon: TagIcon,
edit: () => edit: () => globalThis.editMetadata([globalThis.currentViewerPhoto], [2]),
globalThis.editMetadata([globalThis.currentViewerPhoto], [2]),
}); });
} }
list.push({ list.push({
title: this.address || this.t("memories", "No coordinates"), title: this.address || this.t('memories', 'No coordinates'),
subtitle: this.address subtitle: this.address ? [] : [this.t('memories', 'Click edit to set location')],
? []
: [this.t("memories", "Click edit to set location")],
icon: LocationIcon, icon: LocationIcon,
href: this.address ? this.mapFullUrl : undefined, href: this.address ? this.mapFullUrl : undefined,
edit: () => edit: () => globalThis.editMetadata([globalThis.currentViewerPhoto], [4]),
globalThis.editMetadata([globalThis.currentViewerPhoto], [4]),
}); });
return list; return list;
}, },
canEdit(): boolean { canEdit(): boolean {
return this.baseInfo?.permissions?.includes("U"); return this.baseInfo?.permissions?.includes('U');
}, },
/** Date taken info */ /** Date taken info */
@ -186,9 +177,8 @@ export default defineComponent({
// The fallback to datetaken can be eventually removed // The fallback to datetaken can be eventually removed
// and then this can be discarded // and then this can be discarded
if (this.exif.DateTimeEpoch) { if (this.exif.DateTimeEpoch) {
const tzOffset = const tzOffset = this.exif['OffsetTimeOriginal'] || this.exif['OffsetTime'];
this.exif["OffsetTimeOriginal"] || this.exif["OffsetTime"]; const tzId = this.exif['LocationTZID'];
const tzId = this.exif["LocationTZID"];
if (tzOffset) { if (tzOffset) {
m.utcOffset(tzOffset); m.utcOffset(tzOffset);
@ -201,19 +191,16 @@ export default defineComponent({
}, },
dateOriginalStr(): string | null { dateOriginalStr(): string | null {
return utils.getLongDateStr( return utils.getLongDateStr(new Date(this.baseInfo.datetaken * 1000), true);
new Date(this.baseInfo.datetaken * 1000),
true
);
}, },
dateOriginalTime(): string[] | null { dateOriginalTime(): string[] | null {
if (!this.dateOriginal) return null; if (!this.dateOriginal) return null;
let format = "h:mm A"; let format = 'h:mm A';
const fields = ["OffsetTimeOriginal", "OffsetTime", "LocationTZID"]; const fields = ['OffsetTimeOriginal', 'OffsetTime', 'LocationTZID'];
if (fields.some((key) => this.exif[key])) { if (fields.some((key) => this.exif[key])) {
format += " Z"; format += ' Z';
} }
return [this.dateOriginal.format(format)]; return [this.dateOriginal.format(format)];
@ -221,18 +208,18 @@ export default defineComponent({
/** Camera make and model info */ /** Camera make and model info */
camera(): string | null { 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;
if (model.startsWith(make)) return model; if (model.startsWith(make)) return model;
return `${make} ${model}`; return `${make} ${model}`;
}, },
cameraSub(): string[] { cameraSub(): string[] {
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'];
const iso = this.exif["ISO"]; const iso = this.exif['ISO'];
const parts: string[] = []; const parts: string[] = [];
if (f) parts.push(`f/${f}`); if (f) parts.push(`f/${f}`);
@ -244,11 +231,7 @@ export default defineComponent({
/** Convert shutter speed decimal to 1/x format */ /** Convert shutter speed decimal to 1/x format */
shutterSpeed(): string | null { shutterSpeed(): string | null {
const speed = Number( const speed = Number(this.exif['ShutterSpeedValue'] || this.exif['ShutterSpeed'] || this.exif['ExposureTime']);
this.exif["ShutterSpeedValue"] ||
this.exif["ShutterSpeed"] ||
this.exif["ExposureTime"]
);
if (!speed) return null; if (!speed) return null;
if (speed < 1) { if (speed < 1) {
@ -265,7 +248,7 @@ export default defineComponent({
imageInfoSub(): string[] { imageInfoSub(): string[] {
let parts: string[] = []; let parts: string[] = [];
let mp = Number(this.exif["Megapixels"]); let mp = Number(this.exif['Megapixels']);
if (this.baseInfo.w && this.baseInfo.h) { if (this.baseInfo.w && this.baseInfo.h) {
parts.push(`${this.baseInfo.w}x${this.baseInfo.h}`); parts.push(`${this.baseInfo.w}x${this.baseInfo.h}`);
@ -287,11 +270,11 @@ export default defineComponent({
}, },
lat(): number { lat(): number {
return this.exif["GPSLatitude"]; return this.exif['GPSLatitude'];
}, },
lon(): number { lon(): number {
return this.exif["GPSLongitude"]; return this.exif['GPSLongitude'];
}, },
tagNames(): string[] { tagNames(): string[] {
@ -299,17 +282,12 @@ export default defineComponent({
}, },
tagNamesStr(): string | null { tagNamesStr(): string | null {
return this.tagNames.length > 0 ? this.tagNames.join(", ") : null; return this.tagNames.length > 0 ? this.tagNames.join(', ') : null;
}, },
mapUrl(): string { mapUrl(): string {
const boxSize = 0.0075; const boxSize = 0.0075;
const bbox = [ const bbox = [this.lon - boxSize, this.lat - boxSize, this.lon + boxSize, this.lat + boxSize];
this.lon - boxSize,
this.lat - boxSize,
this.lon + boxSize,
this.lat + boxSize,
];
const m = `${this.lat},${this.lon}`; const m = `${this.lat},${this.lon}`;
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}`;
}, },

View File

@ -17,12 +17,7 @@
@touchend.passive="interactend" @touchend.passive="interactend"
@touchcancel.passive="interactend" @touchcancel.passive="interactend"
> >
<span <span class="cursor st" ref="cursorSt" :style="{ transform: `translateY(${cursorY}px)` }"> </span>
class="cursor st"
ref="cursorSt"
:style="{ transform: `translateY(${cursorY}px)` }"
>
</span>
<span <span
class="cursor hv" class="cursor hv"
@ -49,17 +44,17 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, PropType } from "vue"; import { defineComponent, PropType } from 'vue';
import { IRow, IRowType, ITick } from "../types"; import { IRow, IRowType, ITick } from '../types';
import ScrollIcon from "vue-material-design-icons/UnfoldMoreHorizontal.vue"; import ScrollIcon from 'vue-material-design-icons/UnfoldMoreHorizontal.vue';
import * as utils from "../services/Utils"; import * as utils from '../services/Utils';
// Pixels to snap at // Pixels to snap at
const SNAP_OFFSET = -35; const SNAP_OFFSET = -35;
export default defineComponent({ export default defineComponent({
name: "ScrollerManager", name: 'ScrollerManager',
components: { components: {
ScrollIcon, ScrollIcon,
}, },
@ -101,7 +96,7 @@ export default defineComponent({
/** Hover cursor top */ /** Hover cursor top */
hoverCursorY: -5, hoverCursorY: -5,
/** Hover cursor text */ /** Hover cursor text */
hoverCursorText: "", hoverCursorText: '',
/** Scrolling using the scroller */ /** Scrolling using the scroller */
scrollingTimer: 0, scrollingTimer: 0,
/** Scrolling now using the scroller */ /** Scrolling now using the scroller */
@ -145,7 +140,7 @@ export default defineComponent({
this.ticks = []; this.ticks = [];
this.cursorY = 0; this.cursorY = 0;
this.hoverCursorY = -5; this.hoverCursorY = -5;
this.hoverCursorText = ""; this.hoverCursorText = '';
this.reflowRequest = false; this.reflowRequest = false;
// Clear all timers // Clear all timers
@ -171,8 +166,8 @@ export default defineComponent({
}, 100); }, 100);
// Update that we're scrolling with the recycler // Update that we're scrolling with the recycler
utils.setRenewingTimeout(this, "scrollingRecyclerNowTimer", null, 200); utils.setRenewingTimeout(this, 'scrollingRecyclerNowTimer', null, 200);
utils.setRenewingTimeout(this, "scrollingRecyclerTimer", null, 1500); utils.setRenewingTimeout(this, 'scrollingRecyclerTimer', null, 1500);
}, },
/** Update cursor position from recycler scroll position */ /** Update cursor position from recycler scroll position */
@ -184,7 +179,7 @@ export default defineComponent({
const scroll = this.recycler?.$el?.scrollTop || 0; const scroll = this.recycler?.$el?.scrollTop || 0;
// Get cursor px position // Get cursor px position
const { top1, top2, y1, y2 } = this.getCoords(scroll, "y"); const { top1, top2, y1, y2 } = this.getCoords(scroll, 'y');
const topfrac = (scroll - y1) / (y2 - y1); const topfrac = (scroll - y1) / (y2 - y1);
const rtop = top1 + (top2 - top1) * (topfrac || 0); const rtop = top1 + (top2 - top1) * (topfrac || 0);
@ -193,7 +188,7 @@ export default defineComponent({
// Move hover cursor to same position unless hovering // Move hover cursor to same position unless hovering
// Regardless, we need this call because the internal mapping might have changed // Regardless, we need this call because the internal mapping might have changed
if ((<HTMLElement>this.$refs.scroller).matches(":hover")) { if ((<HTMLElement>this.$refs.scroller).matches(':hover')) {
this.moveHoverCursor(this.hoverCursorY); this.moveHoverCursor(this.hoverCursorY);
} else { } else {
this.moveHoverCursor(rtop); this.moveHoverCursor(rtop);
@ -234,11 +229,7 @@ export default defineComponent({
let prevMonth = 0; let prevMonth = 0;
// Get a new tick // Get a new tick
const getTick = ( const getTick = (dayId: number, isMonth = false, text?: string | number): ITick => {
dayId: number,
isMonth = false,
text?: string | number
): ITick => {
return { return {
dayId, dayId,
isMonth, isMonth,
@ -336,16 +327,12 @@ export default defineComponent({
/** Mark ticks as visible or invisible */ /** Mark ticks as visible or invisible */
computeVisibleTicks() { computeVisibleTicks() {
// Kind of unrelated here, but refresh rect // Kind of unrelated here, but refresh rect
this.scrollerRect = ( this.scrollerRect = (this.$refs.scroller as HTMLElement).getBoundingClientRect();
this.$refs.scroller as HTMLElement
).getBoundingClientRect();
// Do another pass to figure out which points are visible // Do another pass to figure out which points are visible
// This is not as bad as it looks, it's actually 12*O(n) // This is not as bad as it looks, it's actually 12*O(n)
// because there are only 12 months in a year // because there are only 12 months in a year
const fontSizePx = parseFloat( const fontSizePx = parseFloat(getComputedStyle(this.$refs.cursorSt as any).fontSize);
getComputedStyle(this.$refs.cursorSt as any).fontSize
);
const minGap = fontSizePx + (globalThis.windowInnerWidth <= 768 ? 5 : 2); const minGap = fontSizePx + (globalThis.windowInnerWidth <= 768 ? 5 : 2);
let prevShow = -9999; let prevShow = -9999;
for (const [idx, tick] of this.ticks.entries()) { for (const [idx, tick] of this.ticks.entries()) {
@ -380,10 +367,7 @@ export default defineComponent({
if (i < this.ticks.length) { if (i < this.ticks.length) {
// A labelled tick was found // A labelled tick was found
const nextLabelledTick = this.ticks[i]; const nextLabelledTick = this.ticks[i];
if ( if (tick.top + minGap > nextLabelledTick.top && nextLabelledTick.top < this.height - minGap) {
tick.top + minGap > nextLabelledTick.top &&
nextLabelledTick.top < this.height - minGap
) {
// make sure this will be shown // make sure this will be shown
continue; continue;
} }
@ -410,7 +394,7 @@ export default defineComponent({
this.hoverCursorY = y; this.hoverCursorY = y;
// Get index of previous tick // Get index of previous tick
let idx = utils.binarySearch(this.ticks, y, "topF"); let idx = utils.binarySearch(this.ticks, y, 'topF');
if (idx === 0) { if (idx === 0) {
// use this tick // use this tick
} else if (idx >= 1 && idx <= this.ticks.length) { } else if (idx >= 1 && idx <= this.ticks.length) {
@ -424,12 +408,12 @@ export default defineComponent({
// Special days // Special days
if (dayId === undefined) { if (dayId === undefined) {
this.hoverCursorText = ""; this.hoverCursorText = '';
return; return;
} }
const date = utils.dayIdToDate(dayId); const date = utils.dayIdToDate(dayId);
this.hoverCursorText = utils.getShortDateStr(date) ?? ""; this.hoverCursorText = utils.getShortDateStr(date) ?? '';
}, },
/** Handle mouse hover */ /** Handle mouse hover */
@ -447,7 +431,7 @@ export default defineComponent({
}, },
/** Binary search and get coords surrounding position */ /** Binary search and get coords surrounding position */
getCoords(y: number, field: "topF" | "y") { getCoords(y: number, field: 'topF' | 'y') {
// Top of first and second ticks // Top of first and second ticks
let top1 = 0, let top1 = 0,
top2 = 0, top2 = 0,
@ -485,7 +469,7 @@ export default defineComponent({
this.cursorY = y; this.cursorY = y;
this.hoverCursorY = y; this.hoverCursorY = y;
const { top1, top2, y1, y2 } = this.getCoords(y, "topF"); const { top1, top2, y1, y2 } = this.getCoords(y, 'topF');
const yfrac = (y - top1) / (top2 - top1); const yfrac = (y - top1) / (top2 - top1);
const ry = y1 + (y2 - y1) * (yfrac || 0); const ry = y1 + (y2 - y1) * (yfrac || 0);
const targetY = snap ? y1 + SNAP_OFFSET : ry; const targetY = snap ? y1 + SNAP_OFFSET : ry;
@ -519,13 +503,13 @@ export default defineComponent({
interactend() { interactend() {
this.interacting = false; this.interacting = false;
this.recyclerScrolled(null); // make sure final position is correct this.recyclerScrolled(null); // make sure final position is correct
this.$emit("interactend"); // tell recycler to load stuff this.$emit('interactend'); // tell recycler to load stuff
}, },
/** Update scroller is being used to scroll recycler */ /** Update scroller is being used to scroll recycler */
handleScroll() { handleScroll() {
utils.setRenewingTimeout(this, "scrollingNowTimer", null, 200); utils.setRenewingTimeout(this, 'scrollingNowTimer', null, 200);
utils.setRenewingTimeout(this, "scrollingTimer", null, 1500); utils.setRenewingTimeout(this, 'scrollingTimer', null, 1500);
}, },
}, },
}); });

View File

@ -2,18 +2,15 @@
<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')" {{ t('memories', 'Cancel') }}
@click="clearSelection()"
>
{{ 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,
}) })
}} }}
@ -36,60 +33,49 @@
</div> </div>
<!-- Selection Modals --> <!-- Selection Modals -->
<FaceMoveModal <FaceMoveModal ref="faceMoveModal" @moved="deletePhotos" :updateLoading="updateLoading" />
ref="faceMoveModal"
@moved="deletePhotos"
:updateLoading="updateLoading"
/>
<AddToAlbumModal ref="addToAlbumModal" @added="clearSelection" /> <AddToAlbumModal ref="addToAlbumModal" @added="clearSelection" />
<MoveToFolderModal ref="moveToFolderModal" @moved="refresh" /> <MoveToFolderModal ref="moveToFolderModal" @moved="refresh" />
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, PropType } from "vue"; import { defineComponent, PropType } from 'vue';
import { showError } from "@nextcloud/dialogs"; import { showError } from '@nextcloud/dialogs';
import UserConfig from "../mixins/UserConfig"; import UserConfig from '../mixins/UserConfig';
import NcActions from "@nextcloud/vue/dist/Components/NcActions"; import NcActions from '@nextcloud/vue/dist/Components/NcActions';
import NcActionButton from "@nextcloud/vue/dist/Components/NcActionButton"; import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton';
import { translate as t, translatePlural as n } from "@nextcloud/l10n"; import { translate as t, translatePlural as n } from '@nextcloud/l10n';
import { import { IDay, IHeadRow, IPhoto, IRow, IRowType, ISelectionAction } from '../types';
IDay, import { getCurrentUser } from '@nextcloud/auth';
IHeadRow,
IPhoto,
IRow,
IRowType,
ISelectionAction,
} from "../types";
import { getCurrentUser } from "@nextcloud/auth";
import * as dav from "../services/DavRequests"; import * as dav from '../services/DavRequests';
import * as utils from "../services/Utils"; import * as utils from '../services/Utils';
import FaceMoveModal from "./modal/FaceMoveModal.vue"; import FaceMoveModal from './modal/FaceMoveModal.vue';
import AddToAlbumModal from "./modal/AddToAlbumModal.vue"; import AddToAlbumModal from './modal/AddToAlbumModal.vue';
import MoveToFolderModal from "./modal/MoveToFolderModal.vue"; import MoveToFolderModal from './modal/MoveToFolderModal.vue';
import StarIcon from "vue-material-design-icons/Star.vue"; import StarIcon from 'vue-material-design-icons/Star.vue';
import DownloadIcon from "vue-material-design-icons/Download.vue"; import DownloadIcon from 'vue-material-design-icons/Download.vue';
import DeleteIcon from "vue-material-design-icons/TrashCanOutline.vue"; import DeleteIcon from 'vue-material-design-icons/TrashCanOutline.vue';
import EditFileIcon from "vue-material-design-icons/FileEdit.vue"; import EditFileIcon from 'vue-material-design-icons/FileEdit.vue';
import ArchiveIcon from "vue-material-design-icons/PackageDown.vue"; import ArchiveIcon from 'vue-material-design-icons/PackageDown.vue';
import UnarchiveIcon from "vue-material-design-icons/PackageUp.vue"; import UnarchiveIcon from 'vue-material-design-icons/PackageUp.vue';
import OpenInNewIcon from "vue-material-design-icons/OpenInNew.vue"; import OpenInNewIcon from 'vue-material-design-icons/OpenInNew.vue';
import CloseIcon from "vue-material-design-icons/Close.vue"; import CloseIcon from 'vue-material-design-icons/Close.vue';
import MoveIcon from "vue-material-design-icons/ImageMove.vue"; import MoveIcon from 'vue-material-design-icons/ImageMove.vue';
import AlbumsIcon from "vue-material-design-icons/ImageAlbum.vue"; import AlbumsIcon from 'vue-material-design-icons/ImageAlbum.vue';
import AlbumRemoveIcon from "vue-material-design-icons/BookRemove.vue"; import AlbumRemoveIcon from 'vue-material-design-icons/BookRemove.vue';
import FolderMoveIcon from "vue-material-design-icons/FolderMove.vue"; import FolderMoveIcon from 'vue-material-design-icons/FolderMove.vue';
type Selection = Map<number, IPhoto>; type Selection = Map<number, IPhoto>;
export default defineComponent({ export default defineComponent({
name: "SelectionManager", name: 'SelectionManager',
components: { components: {
NcActions, NcActions,
NcActionButton, NcActionButton,
@ -144,82 +130,82 @@ export default defineComponent({
// Make default actions // Make default actions
this.defaultActions = [ this.defaultActions = [
{ {
name: t("memories", "Delete"), name: t('memories', 'Delete'),
icon: DeleteIcon, icon: DeleteIcon,
callback: this.deleteSelection.bind(this), callback: this.deleteSelection.bind(this),
if: () => !this.routeIsAlbum(), if: () => !this.routeIsAlbum(),
}, },
{ {
name: t("memories", "Remove from album"), name: t('memories', 'Remove from album'),
icon: AlbumRemoveIcon, icon: AlbumRemoveIcon,
callback: this.deleteSelection.bind(this), callback: this.deleteSelection.bind(this),
if: () => this.routeIsAlbum(), if: () => this.routeIsAlbum(),
}, },
{ {
name: t("memories", "Download"), name: t('memories', 'Download'),
icon: DownloadIcon, icon: DownloadIcon,
callback: this.downloadSelection.bind(this), callback: this.downloadSelection.bind(this),
allowPublic: true, allowPublic: true,
if: () => !this.allowDownload(), if: () => !this.allowDownload(),
}, },
{ {
name: t("memories", "Favorite"), name: t('memories', 'Favorite'),
icon: StarIcon, icon: StarIcon,
callback: this.favoriteSelection.bind(this), callback: this.favoriteSelection.bind(this),
}, },
{ {
name: t("memories", "Archive"), name: t('memories', 'Archive'),
icon: ArchiveIcon, icon: ArchiveIcon,
callback: this.archiveSelection.bind(this), callback: this.archiveSelection.bind(this),
if: () => !this.routeIsArchive() && !this.routeIsAlbum(), if: () => !this.routeIsArchive() && !this.routeIsAlbum(),
}, },
{ {
name: t("memories", "Unarchive"), name: t('memories', 'Unarchive'),
icon: UnarchiveIcon, icon: UnarchiveIcon,
callback: this.archiveSelection.bind(this), callback: this.archiveSelection.bind(this),
if: () => this.routeIsArchive(), if: () => this.routeIsArchive(),
}, },
{ {
name: t("memories", "Edit metadata"), name: t('memories', 'Edit metadata'),
icon: EditFileIcon, icon: EditFileIcon,
callback: this.editMetadataSelection.bind(this), callback: this.editMetadataSelection.bind(this),
}, },
{ {
name: t("memories", "View in folder"), name: t('memories', 'View in folder'),
icon: OpenInNewIcon, icon: OpenInNewIcon,
callback: this.viewInFolder.bind(this), callback: this.viewInFolder.bind(this),
if: () => this.selection.size === 1 && !this.routeIsAlbum(), if: () => this.selection.size === 1 && !this.routeIsAlbum(),
}, },
{ {
name: t("memories", "Move to folder"), name: t('memories', 'Move to folder'),
icon: FolderMoveIcon, icon: FolderMoveIcon,
callback: this.moveToFolder.bind(this), callback: this.moveToFolder.bind(this),
if: () => !this.routeIsAlbum() && !this.routeIsArchive(), if: () => !this.routeIsAlbum() && !this.routeIsArchive(),
}, },
{ {
name: t("memories", "Add to album"), name: t('memories', 'Add to album'),
icon: AlbumsIcon, icon: AlbumsIcon,
callback: this.addToAlbum.bind(this), callback: this.addToAlbum.bind(this),
if: (self: any) => self.config_albumsEnabled && !self.routeIsAlbum(), if: (self: any) => self.config_albumsEnabled && !self.routeIsAlbum(),
}, },
{ {
name: t("memories", "Move to person"), name: t('memories', 'Move to person'),
icon: MoveIcon, icon: MoveIcon,
callback: this.moveSelectionToPerson.bind(this), callback: this.moveSelectionToPerson.bind(this),
if: () => this.$route.name === "recognize", if: () => this.$route.name === 'recognize',
}, },
{ {
name: t("memories", "Remove from person"), name: t('memories', 'Remove from person'),
icon: CloseIcon, icon: CloseIcon,
callback: this.removeSelectionFromPerson.bind(this), callback: this.removeSelectionFromPerson.bind(this),
if: () => this.$route.name === "recognize", if: () => this.$route.name === 'recognize',
}, },
]; ];
}, },
watch: { watch: {
show() { show() {
const klass = "has-top-bar"; const klass = 'has-top-bar';
if (this.show) { if (this.show) {
document.body.classList.add(klass); document.body.classList.add(klass);
} else { } else {
@ -230,23 +216,19 @@ export default defineComponent({
methods: { methods: {
refresh() { refresh() {
this.$emit("refresh"); this.$emit('refresh');
}, },
deletePhotos(photos: IPhoto[]) { deletePhotos(photos: IPhoto[]) {
this.$emit("delete", photos); this.$emit('delete', photos);
}, },
deleteSelectedPhotosById(delIds: number[], selection: Selection) { deleteSelectedPhotosById(delIds: number[], selection: Selection) {
return this.deletePhotos( return this.deletePhotos(delIds.map((id) => selection.get(id)).filter((p): p is IPhoto => p !== undefined));
delIds
.map((id) => selection.get(id))
.filter((p): p is IPhoto => p !== undefined)
);
}, },
updateLoading(delta: number) { updateLoading(delta: number) {
this.$emit("updateLoading", delta); this.$emit('updateLoading', delta);
}, },
/** Download is not allowed on some public shares */ /** Download is not allowed on some public shares */
@ -257,15 +239,15 @@ export default defineComponent({
/** Is archive route */ /** Is archive route */
routeIsArchive() { routeIsArchive() {
// Check if the route itself is archive // Check if the route itself is archive
if (this.$route.name === "archive") { if (this.$route.name === 'archive') {
return true; return true;
} }
// Check if route is folder and the path contains .archive // Check if route is folder and the path contains .archive
if (this.$route.name === "folders") { if (this.$route.name === 'folders') {
let path = this.$route.params.path || ""; let path = this.$route.params.path || '';
if (Array.isArray(path)) path = path.join("/"); if (Array.isArray(path)) path = path.join('/');
return ("/" + path + "/").includes("/.archive/"); return ('/' + path + '/').includes('/.archive/');
} }
return false; return false;
@ -273,12 +255,12 @@ export default defineComponent({
/** Is album route */ /** Is album route */
routeIsAlbum() { routeIsAlbum() {
return this.config_albumsEnabled && this.$route.name === "albums"; return this.config_albumsEnabled && this.$route.name === 'albums';
}, },
/** Public route that can't modify anything */ /** Public route that can't modify anything */
routeIsPublic() { routeIsPublic() {
return this.$route.name?.endsWith("-share"); return this.$route.name?.endsWith('-share');
}, },
/** Trigger to update props from selection set */ /** Trigger to update props from selection set */
@ -298,10 +280,7 @@ export default defineComponent({
/** Get the actions list */ /** Get the actions list */
getActions(): ISelectionAction[] { getActions(): ISelectionAction[] {
return ( return (
this.defaultActions?.filter( this.defaultActions?.filter((a) => (!a.if || a.if(this)) && (!this.routeIsPublic() || a.allowPublic)) || []
(a) =>
(!a.if || a.if(this)) && (!this.routeIsPublic() || a.allowPublic)
) || []
); );
}, },
@ -320,7 +299,7 @@ export default defineComponent({
/** Clicking on photo */ /** Clicking on photo */
clickPhoto(photo: IPhoto, event: PointerEvent, rowIdx: number) { clickPhoto(photo: IPhoto, event: PointerEvent, rowIdx: number) {
if (photo.flag & this.c.FLAG_PLACEHOLDER) return; if (photo.flag & this.c.FLAG_PLACEHOLDER) return;
if (event.pointerType === "touch") return; // let touch events handle this if (event.pointerType === 'touch') return; // let touch events handle this
if (this.has()) { if (this.has()) {
if (event.shiftKey) { if (event.shiftKey) {
@ -431,8 +410,7 @@ export default defineComponent({
if (scrollUp) { if (scrollUp) {
this.touchScrollDelta = (-1 * (110 - touch.clientY)) / 3; this.touchScrollDelta = (-1 * (110 - touch.clientY)) / 3;
} else { } else {
this.touchScrollDelta = this.touchScrollDelta = (touch.clientY - globalThis.windowInnerHeight + 60) / 3;
(touch.clientY - globalThis.windowInnerHeight + 60) / 3;
} }
if (this.touchAnchor && !this.touchScrollInterval) { if (this.touchAnchor && !this.touchScrollInterval) {
@ -467,12 +445,9 @@ export default defineComponent({
if (!this.touchAnchor) return; if (!this.touchAnchor) return;
// Which photo is the cursor over, if any // Which photo is the cursor over, if any
const elem: any = document const elem: any = document.elementFromPoint(touch.clientX, touch.clientY)?.closest('.p-outer-super');
.elementFromPoint(touch.clientX, touch.clientY)
?.closest(".p-outer-super");
let overPhoto: IPhoto | null = elem?.__vue__?.data; let overPhoto: IPhoto | null = elem?.__vue__?.data;
if (overPhoto && overPhoto.flag & this.c.FLAG_PLACEHOLDER) if (overPhoto && overPhoto.flag & this.c.FLAG_PLACEHOLDER) overPhoto = null;
overPhoto = null;
// Do multi-selection "till" overPhoto "from" anchor // Do multi-selection "till" overPhoto "from" anchor
// This logic is completely different from the desktop because of the // This logic is completely different from the desktop because of the
@ -619,16 +594,13 @@ export default defineComponent({
// Clear everything in front in this day // Clear everything in front in this day
const pdIdx = detail.indexOf(photo); const pdIdx = detail.indexOf(photo);
for (let i = pdIdx + 1; i < detail.length; i++) { for (let i = pdIdx + 1; i < detail.length; i++) {
if (detail[i].flag & this.c.FLAG_SELECTED) if (detail[i].flag & this.c.FLAG_SELECTED) this.selectPhoto(detail[i], false, true);
this.selectPhoto(detail[i], false, true);
} }
// Clear everything else in front // Clear everything else in front
Array.from(this.selection.values()) Array.from(this.selection.values())
.filter((p: IPhoto) => { .filter((p: IPhoto) => {
return this.isreverse return this.isreverse ? p.dayid > photo.dayid : p.dayid < photo.dayid;
? p.dayid > photo.dayid
: p.dayid < photo.dayid;
}) })
.forEach((photo: IPhoto) => { .forEach((photo: IPhoto) => {
this.selectPhoto(photo, false, true); this.selectPhoto(photo, false, true);
@ -722,14 +694,7 @@ export default defineComponent({
*/ */
async downloadSelection(selection: Selection) { async downloadSelection(selection: Selection) {
if (selection.size >= 100) { if (selection.size >= 100) {
if ( if (!confirm(this.t('memories', 'You are about to download a large number of files. Are you sure?'))) {
!confirm(
this.t(
"memories",
"You are about to download a large number of files. Are you sure?"
)
)
) {
return; return;
} }
} }
@ -740,9 +705,7 @@ export default defineComponent({
* Check if all files selected currently are favorites * Check if all files selected currently are favorites
*/ */
allSelectedFavorites(selection: Selection) { allSelectedFavorites(selection: Selection) {
return Array.from(selection.values()).every( return Array.from(selection.values()).every((p) => p.flag & this.c.FLAG_IS_FAVORITE);
(p) => p.flag & this.c.FLAG_IS_FAVORITE
);
}, },
/** /**
@ -750,10 +713,7 @@ export default defineComponent({
*/ */
async favoriteSelection(selection: Selection) { async favoriteSelection(selection: Selection) {
const val = !this.allSelectedFavorites(selection); const val = !this.allSelectedFavorites(selection);
for await (const favIds of dav.favoritePhotos( for await (const favIds of dav.favoritePhotos(Array.from(selection.values()), val)) {
Array.from(selection.values()),
val
)) {
} }
this.clearSelection(); this.clearSelection();
}, },
@ -763,21 +723,12 @@ export default defineComponent({
*/ */
async deleteSelection(selection: Selection) { async deleteSelection(selection: Selection) {
if (selection.size >= 100) { if (selection.size >= 100) {
if ( if (!confirm(this.t('memories', 'You are about to delete a large number of files. Are you sure?'))) {
!confirm(
this.t(
"memories",
"You are about to delete a large number of files. Are you sure?"
)
)
) {
return; return;
} }
} }
for await (const delIds of dav.deletePhotos( for await (const delIds of dav.deletePhotos(Array.from(selection.values()))) {
Array.from(selection.values())
)) {
this.deleteSelectedPhotosById(delIds, selection); this.deleteSelectedPhotosById(delIds, selection);
} }
}, },
@ -803,22 +754,12 @@ export default defineComponent({
*/ */
async archiveSelection(selection: Selection) { async archiveSelection(selection: Selection) {
if (selection.size >= 100) { if (selection.size >= 100) {
if ( if (!confirm(this.t('memories', 'You are about to touch a large number of files. Are you sure?'))) {
!confirm(
this.t(
"memories",
"You are about to touch a large number of files. Are you sure?"
)
)
) {
return; return;
} }
} }
for await (let delIds of dav.archiveFilesByIds( for await (let delIds of dav.archiveFilesByIds(Array.from(selection.keys()), !this.routeIsArchive())) {
Array.from(selection.keys()),
!this.routeIsArchive()
)) {
this.deleteSelectedPhotosById(delIds, selection); this.deleteSelectedPhotosById(delIds, selection);
} }
}, },
@ -842,12 +783,7 @@ export default defineComponent({
*/ */
async moveSelectionToPerson(selection: Selection) { async moveSelectionToPerson(selection: Selection) {
if (!this.config_showFaceRect) { if (!this.config_showFaceRect) {
showError( showError(this.t('memories', 'You must enable "Mark person in preview" to use this feature'));
this.t(
"memories",
'You must enable "Mark person in preview" to use this feature'
)
);
return; return;
} }
(<any>this.$refs.faceMoveModal).open(Array.from(selection.values())); (<any>this.$refs.faceMoveModal).open(Array.from(selection.values()));
@ -859,14 +795,14 @@ export default defineComponent({
async removeSelectionFromPerson(selection: Selection) { async removeSelectionFromPerson(selection: Selection) {
// Make sure route is valid // Make sure route is valid
const { user, name } = this.$route.params; const { user, name } = this.$route.params;
if (this.$route.name !== "recognize" || !user || !name) { if (this.$route.name !== 'recognize' || !user || !name) {
return; return;
} }
// Check photo ownership // Check photo ownership
if (this.$route.params.user !== getCurrentUser()?.uid) { if (this.$route.params.user !== getCurrentUser()?.uid) {
showError( showError(
this.t("memories", 'Only user "{user}" can update this person', { this.t('memories', 'Only user "{user}" can update this person', {
user, user,
}) })
); );
@ -874,11 +810,7 @@ export default defineComponent({
} }
// Run query // Run query
for await (let delIds of dav.removeFaceImages( for await (let delIds of dav.removeFaceImages(<string>user, <string>name, Array.from(selection.values()))) {
<string>user,
<string>name,
Array.from(selection.values())
)) {
this.deleteSelectedPhotosById(delIds, selection); this.deleteSelectedPhotosById(delIds, selection);
} }
}, },

View File

@ -28,24 +28,12 @@
:title="t('memories', 'Memories Settings')" :title="t('memories', 'Memories Settings')"
@update:open="onClose" @update:open="onClose"
> >
<NcAppSettingsSection <NcAppSettingsSection id="general-settings" :title="t('memories', 'General')">
id="general-settings" <label for="timeline-path">{{ t('memories', 'Timeline Path') }}</label>
:title="t('memories', 'General')" <input id="timeline-path" @click="chooseTimelinePath" v-model="config_timelinePath" type="text" />
>
<label for="timeline-path">{{ t("memories", "Timeline Path") }}</label>
<input
id="timeline-path"
@click="chooseTimelinePath"
v-model="config_timelinePath"
type="text"
/>
<NcCheckboxRadioSwitch <NcCheckboxRadioSwitch :checked.sync="config_squareThumbs" @update:checked="updateSquareThumbs" type="switch">
:checked.sync="config_squareThumbs" {{ t('memories', 'Square grid mode') }}
@update:checked="updateSquareThumbs"
type="switch"
>
{{ t("memories", "Square grid mode") }}
</NcCheckboxRadioSwitch> </NcCheckboxRadioSwitch>
<NcCheckboxRadioSwitch <NcCheckboxRadioSwitch
@ -53,44 +41,24 @@
@update:checked="updateEnableTopMemories" @update:checked="updateEnableTopMemories"
type="switch" type="switch"
> >
{{ t("memories", "Show past photos on top of timeline") }} {{ t('memories', 'Show past photos on top of timeline') }}
</NcCheckboxRadioSwitch> </NcCheckboxRadioSwitch>
<NcCheckboxRadioSwitch <NcCheckboxRadioSwitch :checked.sync="config_fullResOnZoom" @update:checked="updateFullResOnZoom" type="switch">
:checked.sync="config_fullResOnZoom" {{ t('memories', 'Load full size image on zoom') }}
@update:checked="updateFullResOnZoom"
type="switch"
>
{{ t("memories", "Load full size image on zoom") }}
</NcCheckboxRadioSwitch> </NcCheckboxRadioSwitch>
<NcCheckboxRadioSwitch <NcCheckboxRadioSwitch :checked.sync="config_fullResAlways" @update:checked="updateFullResAlways" type="switch">
:checked.sync="config_fullResAlways" {{ t('memories', 'Always load full size image (not recommended)') }}
@update:checked="updateFullResAlways"
type="switch"
>
{{ t("memories", "Always load full size image (not recommended)") }}
</NcCheckboxRadioSwitch> </NcCheckboxRadioSwitch>
</NcAppSettingsSection> </NcAppSettingsSection>
<NcAppSettingsSection <NcAppSettingsSection id="folders-settings" :title="t('memories', 'Folders')">
id="folders-settings" <label for="folders-path">{{ t('memories', 'Folders Path') }}</label>
:title="t('memories', 'Folders')" <input id="folders-path" @click="chooseFoldersPath" v-model="config_foldersPath" type="text" />
>
<label for="folders-path">{{ t("memories", "Folders Path") }}</label>
<input
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" {{ t('memories', 'Show hidden folders') }}
@update:checked="updateShowHidden"
type="switch"
>
{{ t("memories", "Show hidden folders") }}
</NcCheckboxRadioSwitch> </NcCheckboxRadioSwitch>
<NcCheckboxRadioSwitch <NcCheckboxRadioSwitch
@ -98,55 +66,45 @@
@update:checked="updateSortFolderMonth" @update:checked="updateSortFolderMonth"
type="switch" type="switch"
> >
{{ t("memories", "Sort folders oldest-first") }} {{ t('memories', 'Sort folders oldest-first') }}
</NcCheckboxRadioSwitch> </NcCheckboxRadioSwitch>
</NcAppSettingsSection> </NcAppSettingsSection>
<NcAppSettingsSection <NcAppSettingsSection id="albums-settings" :title="t('memories', 'Albums')">
id="albums-settings"
:title="t('memories', 'Albums')"
>
<NcCheckboxRadioSwitch <NcCheckboxRadioSwitch
:checked.sync="config_sortAlbumMonth" :checked.sync="config_sortAlbumMonth"
@update:checked="updateSortAlbumMonth" @update:checked="updateSortAlbumMonth"
type="switch" type="switch"
> >
{{ t("memories", "Sort albums oldest-first") }} {{ t('memories', 'Sort albums oldest-first') }}
</NcCheckboxRadioSwitch> </NcCheckboxRadioSwitch>
</NcAppSettingsSection> </NcAppSettingsSection>
</NcAppSettingsDialog> </NcAppSettingsDialog>
<MultiPathSelectionModal <MultiPathSelectionModal ref="multiPathModal" :title="pathSelTitle" @close="saveTimelinePath" />
ref="multiPathModal"
:title="pathSelTitle"
@close="saveTimelinePath"
/>
</div> </div>
</template> </template>
<style scoped> <style scoped>
input[type="text"] { input[type='text'] {
width: 100%; width: 100%;
} }
</style> </style>
<script lang="ts"> <script lang="ts">
import { defineComponent } from "vue"; import { defineComponent } from 'vue';
import { getFilePickerBuilder } from "@nextcloud/dialogs"; import { getFilePickerBuilder } from '@nextcloud/dialogs';
import UserConfig from "../mixins/UserConfig"; import UserConfig from '../mixins/UserConfig';
const NcAppSettingsDialog = () => const NcAppSettingsDialog = () => import('@nextcloud/vue/dist/Components/NcAppSettingsDialog');
import("@nextcloud/vue/dist/Components/NcAppSettingsDialog"); const NcAppSettingsSection = () => import('@nextcloud/vue/dist/Components/NcAppSettingsSection');
const NcAppSettingsSection = () => const NcCheckboxRadioSwitch = () => import('@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch');
import("@nextcloud/vue/dist/Components/NcAppSettingsSection");
const NcCheckboxRadioSwitch = () =>
import("@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch");
import MultiPathSelectionModal from "./modal/MultiPathSelectionModal.vue"; import MultiPathSelectionModal from './modal/MultiPathSelectionModal.vue';
export default defineComponent({ export default defineComponent({
name: "Settings", name: 'Settings',
components: { components: {
NcAppSettingsDialog, NcAppSettingsDialog,
@ -166,13 +124,13 @@ export default defineComponent({
computed: { computed: {
pathSelTitle(): string { pathSelTitle(): string {
return this.t("memories", "Choose Timeline Paths"); return this.t('memories', 'Choose Timeline Paths');
}, },
}, },
methods: { methods: {
onClose() { onClose() {
this.$emit("update:open", false); this.$emit('update:open', false);
}, },
async chooseFolder(title: string, initial: string) { async chooseFolder(title: string, initial: string) {
@ -180,7 +138,7 @@ export default defineComponent({
.setMultiSelect(false) .setMultiSelect(false)
.setModal(true) .setModal(true)
.setType(1) .setType(1)
.addMimeTypeFilter("httpd/unix-directory") .addMimeTypeFilter('httpd/unix-directory')
.allowDirectories() .allowDirectories()
.startAt(initial) .startAt(initial)
.build(); .build();
@ -189,59 +147,57 @@ export default defineComponent({
}, },
async chooseTimelinePath() { async chooseTimelinePath() {
(<any>this.$refs.multiPathModal).open( (<any>this.$refs.multiPathModal).open(this.config_timelinePath.split(';'));
this.config_timelinePath.split(";")
);
}, },
async saveTimelinePath(paths: string[]) { async saveTimelinePath(paths: string[]) {
if (!paths || !paths.length) return; if (!paths || !paths.length) return;
const newPath = paths.join(";"); const newPath = paths.join(';');
if (newPath !== this.config_timelinePath) { if (newPath !== this.config_timelinePath) {
this.config_timelinePath = newPath; this.config_timelinePath = newPath;
await this.updateSetting("timelinePath"); await this.updateSetting('timelinePath');
} }
}, },
async chooseFoldersPath() { async chooseFoldersPath() {
let newPath = await this.chooseFolder( let newPath = await this.chooseFolder(
this.t("memories", "Choose the root for the folders view"), this.t('memories', 'Choose the root for the folders view'),
this.config_foldersPath this.config_foldersPath
); );
if (newPath === "") newPath = "/"; if (newPath === '') newPath = '/';
if (newPath !== this.config_foldersPath) { if (newPath !== this.config_foldersPath) {
this.config_foldersPath = newPath; this.config_foldersPath = newPath;
await this.updateSetting("foldersPath"); await this.updateSetting('foldersPath');
} }
}, },
async updateSquareThumbs() { async updateSquareThumbs() {
await this.updateSetting("squareThumbs"); await this.updateSetting('squareThumbs');
}, },
async updateFullResOnZoom() { async updateFullResOnZoom() {
await this.updateSetting("fullResOnZoom"); await this.updateSetting('fullResOnZoom');
}, },
async updateFullResAlways() { async updateFullResAlways() {
await this.updateSetting("fullResAlways"); await this.updateSetting('fullResAlways');
}, },
async updateEnableTopMemories() { async updateEnableTopMemories() {
await this.updateSetting("enableTopMemories"); await this.updateSetting('enableTopMemories');
}, },
async updateShowHidden() { async updateShowHidden() {
await this.updateSetting("showHidden"); await this.updateSetting('showHidden');
}, },
async updateSortFolderMonth() { async updateSortFolderMonth() {
await this.updateSetting("sortFolderMonth"); await this.updateSetting('sortFolderMonth');
}, },
async updateSortAlbumMonth() { async updateSortAlbumMonth() {
await this.updateSetting("sortAlbumMonth"); await this.updateSetting('sortAlbumMonth');
}, },
}, },
}); });

View File

@ -5,7 +5,7 @@
<NcActions :inline="1"> <NcActions :inline="1">
<NcActionButton :aria-label="t('memories', 'Close')" @click="close()"> <NcActionButton :aria-label="t('memories', 'Close')" @click="close()">
{{ t("memories", "Close") }} {{ t('memories', 'Close') }}
<template #icon> <CloseIcon :size="20" /> </template> <template #icon> <CloseIcon :size="20" /> </template>
</NcActionButton> </NcActionButton>
</NcActions> </NcActions>
@ -16,19 +16,19 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from "vue"; import { defineComponent } from 'vue';
import { subscribe, unsubscribe, emit } from "@nextcloud/event-bus"; import { subscribe, unsubscribe, emit } from '@nextcloud/event-bus';
import NcActions from "@nextcloud/vue/dist/Components/NcActions"; import NcActions from '@nextcloud/vue/dist/Components/NcActions';
import NcActionButton from "@nextcloud/vue/dist/Components/NcActionButton"; import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton';
import Metadata from "./Metadata.vue"; import Metadata from './Metadata.vue';
import { IImageInfo } from "../types"; import { IImageInfo } from '../types';
import CloseIcon from "vue-material-design-icons/Close.vue"; import CloseIcon from 'vue-material-design-icons/Close.vue';
export default defineComponent({ export default defineComponent({
name: "Sidebar", name: 'Sidebar',
components: { components: {
Metadata, Metadata,
NcActions, NcActions,
@ -40,7 +40,7 @@ export default defineComponent({
return { return {
nativeOpen: false, nativeOpen: false,
reducedOpen: false, reducedOpen: false,
basename: "", basename: '',
lastKnownWidth: 0, lastKnownWidth: 0,
}; };
}, },
@ -52,8 +52,8 @@ export default defineComponent({
}, },
mounted() { mounted() {
subscribe("files:sidebar:opened", this.handleNativeOpen); subscribe('files:sidebar:opened', this.handleNativeOpen);
subscribe("files:sidebar:closed", this.handleNativeClose); subscribe('files:sidebar:closed', this.handleNativeClose);
globalThis.mSidebar = { globalThis.mSidebar = {
open: this.open.bind(this), open: this.open.bind(this),
@ -64,8 +64,8 @@ export default defineComponent({
}, },
beforeDestroy() { beforeDestroy() {
unsubscribe("files:sidebar:opened", this.handleNativeOpen); unsubscribe('files:sidebar:opened', this.handleNativeOpen);
unsubscribe("files:sidebar:closed", this.handleNativeClose); unsubscribe('files:sidebar:closed', this.handleNativeClose);
}, },
methods: { methods: {
@ -104,23 +104,23 @@ export default defineComponent({
}, },
getWidth() { getWidth() {
const sidebar = document.getElementById("app-sidebar-vue"); const sidebar = document.getElementById('app-sidebar-vue');
this.lastKnownWidth = sidebar?.offsetWidth || this.lastKnownWidth; this.lastKnownWidth = sidebar?.offsetWidth || this.lastKnownWidth;
return (this.lastKnownWidth || 2) - 2; return (this.lastKnownWidth || 2) - 2;
}, },
handleClose() { handleClose() {
emit("memories:sidebar:closed", {}); emit('memories:sidebar:closed', {});
}, },
handleOpen() { handleOpen() {
// Stop sidebar typing from leaking outside // Stop sidebar typing from leaking outside
const sidebar = document.getElementById("app-sidebar-vue"); const sidebar = document.getElementById('app-sidebar-vue');
sidebar?.addEventListener("keydown", (e) => { sidebar?.addEventListener('keydown', (e) => {
if (e.key.length === 1) e.stopPropagation(); if (e.key.length === 1) e.stopPropagation();
}); });
emit("memories:sidebar:opened", {}); emit('memories:sidebar:opened', {});
}, },
handleNativeOpen() { handleNativeOpen() {

View File

@ -17,7 +17,7 @@
<div class="timeline-header" ref="timelineHeader"> <div class="timeline-header" ref="timelineHeader">
<div class="swiper"></div> <div class="swiper"></div>
<div class="title"> <div class="title">
{{ t("memories", "{photoCount} photos", { photoCount }) }} {{ t('memories', '{photoCount} photos', { photoCount }) }}
</div> </div>
</div> </div>
<div class="timeline-inner"> <div class="timeline-inner">
@ -28,14 +28,14 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from "vue"; import { defineComponent } from 'vue';
import Timeline from "./Timeline.vue"; import Timeline from './Timeline.vue';
const MapSplitMatter = () => import("./top-matter/MapSplitMatter.vue"); const MapSplitMatter = () => import('./top-matter/MapSplitMatter.vue');
import { emit } from "@nextcloud/event-bus"; import { emit } from '@nextcloud/event-bus';
import Hammer from "hammerjs"; import Hammer from 'hammerjs';
export default defineComponent({ export default defineComponent({
name: "SplitTimeline", name: 'SplitTimeline',
components: { components: {
Timeline, Timeline,
@ -53,21 +53,21 @@ export default defineComponent({
computed: { computed: {
primary() { primary() {
switch (this.$route.name) { switch (this.$route.name) {
case "map": case 'map':
return MapSplitMatter; return MapSplitMatter;
default: default:
return "None"; return 'None';
} }
}, },
headerClass() { headerClass() {
switch (this.mobileOpen) { switch (this.mobileOpen) {
case 0: case 0:
return "m-zero"; return 'm-zero';
case 1: case 1:
return "m-one"; return 'm-one';
case 2: case 2:
return "m-two"; return 'm-two';
} }
}, },
}, },
@ -75,12 +75,12 @@ export default defineComponent({
mounted() { mounted() {
// Set up hammerjs hooks // Set up hammerjs hooks
this.hammer = new Hammer(this.$refs.timelineHeader as HTMLElement); this.hammer = new Hammer(this.$refs.timelineHeader as HTMLElement);
this.hammer.get("swipe").set({ this.hammer.get('swipe').set({
direction: Hammer.DIRECTION_VERTICAL, direction: Hammer.DIRECTION_VERTICAL,
threshold: 3, threshold: 3,
}); });
this.hammer.on("swipeup", this.mobileSwipeUp); this.hammer.on('swipeup', this.mobileSwipeUp);
this.hammer.on("swipedown", this.mobileSwipeDown); this.hammer.on('swipedown', this.mobileSwipeDown);
}, },
beforeDestroy() { beforeDestroy() {
@ -107,11 +107,11 @@ export default defineComponent({
this.containerSize = this.isVertical() ? cRect.height : cRect.width; this.containerSize = this.isVertical() ? cRect.height : cRect.width;
// Let touch handle itself // Let touch handle itself
if (event.pointerType === "touch") return; if (event.pointerType === 'touch') return;
// Otherwise, handle pointer events on document // Otherwise, handle pointer events on document
document.addEventListener("pointermove", this.documentPointerMove); document.addEventListener('pointermove', this.documentPointerMove);
document.addEventListener("pointerup", this.pointerUp); document.addEventListener('pointerup', this.pointerUp);
// Prevent text selection // Prevent text selection
event.preventDefault(); event.preventDefault();
@ -131,9 +131,9 @@ export default defineComponent({
pointerUp() { pointerUp() {
// Get rid of listeners on document quickly // Get rid of listeners on document quickly
this.pointerDown = false; this.pointerDown = false;
document.removeEventListener("pointermove", this.documentPointerMove); document.removeEventListener('pointermove', this.documentPointerMove);
document.removeEventListener("pointerup", this.pointerUp); document.removeEventListener('pointerup', this.pointerUp);
emit("memories:window:resize", {}); emit('memories:window:resize', {});
}, },
setFlexBasis(pos: { clientX: number; clientY: number }) { setFlexBasis(pos: { clientX: number; clientY: number }) {
@ -154,7 +154,7 @@ export default defineComponent({
// so that we can prepare in advance for showing more photos // so that we can prepare in advance for showing more photos
// on the timeline // on the timeline
await this.$nextTick(); await this.$nextTick();
emit("memories:window:resize", {}); emit('memories:window:resize', {});
}, },
async mobileSwipeDown() { async mobileSwipeDown() {
@ -165,7 +165,7 @@ export default defineComponent({
// ends. Note that this is necesary: the height of the timeline inner // ends. Note that this is necesary: the height of the timeline inner
// div is also animated to the smaller size. // div is also animated to the smaller size.
await new Promise((resolve) => setTimeout(resolve, 300)); await new Promise((resolve) => setTimeout(resolve, 300));
emit("memories:window:resize", {}); emit('memories:window:resize', {});
}, },
}, },
}); });

View File

@ -25,10 +25,7 @@
<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 <div class="text" v-show="!$refs.topmatter.type && list.length && viewName">
class="text"
v-show="!$refs.topmatter.type && list.length && viewName"
>
{{ viewName }} {{ viewName }}
</div> </div>
@ -94,42 +91,37 @@
@updateLoading="updateLoading" @updateLoading="updateLoading"
/> />
<Viewer <Viewer ref="viewer" @deleted="deleteFromViewWithAnimation" @fetchDay="fetchDay" @updateLoading="updateLoading" />
ref="viewer"
@deleted="deleteFromViewWithAnimation"
@fetchDay="fetchDay"
@updateLoading="updateLoading"
/>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from "vue"; import { defineComponent } from 'vue';
import axios from "@nextcloud/axios"; import axios from '@nextcloud/axios';
import { showError } from "@nextcloud/dialogs"; import { showError } from '@nextcloud/dialogs';
import { subscribe, unsubscribe } from "@nextcloud/event-bus"; import { subscribe, unsubscribe } from '@nextcloud/event-bus';
import { getLayout } from "../services/Layout"; import { getLayout } from '../services/Layout';
import { IDay, IFolder, IHeadRow, IPhoto, IRow, IRowType } from "../types"; import { IDay, IFolder, IHeadRow, IPhoto, IRow, IRowType } from '../types';
import UserConfig from "../mixins/UserConfig"; import UserConfig from '../mixins/UserConfig';
import FolderGrid from "./FolderGrid.vue"; import FolderGrid from './FolderGrid.vue';
import RowHead from "./frame/RowHead.vue"; import RowHead from './frame/RowHead.vue';
import Photo from "./frame/Photo.vue"; import Photo from './frame/Photo.vue';
import ScrollerManager from "./ScrollerManager.vue"; import ScrollerManager from './ScrollerManager.vue';
import SelectionManager from "./SelectionManager.vue"; import SelectionManager from './SelectionManager.vue';
import Viewer from "./viewer/Viewer.vue"; import Viewer from './viewer/Viewer.vue';
import EmptyContent from "./top-matter/EmptyContent.vue"; import EmptyContent from './top-matter/EmptyContent.vue';
import OnThisDay from "./top-matter/OnThisDay.vue"; import OnThisDay from './top-matter/OnThisDay.vue';
import TopMatter from "./top-matter/TopMatter.vue"; import TopMatter from './top-matter/TopMatter.vue';
import * as dav from "../services/DavRequests"; import * as dav from '../services/DavRequests';
import * as utils from "../services/Utils"; import * as utils from '../services/Utils';
import * as strings from "../services/strings"; import * as strings from '../services/strings';
import { API, DaysFilterType } from "../services/API"; import { API, DaysFilterType } from '../services/API';
const SCROLL_LOAD_DELAY = 250; // Delay in loading data when scrolling const SCROLL_LOAD_DELAY = 250; // Delay in loading data when scrolling
const DESKTOP_ROW_HEIGHT = 200; // Height of row on desktop const DESKTOP_ROW_HEIGHT = 200; // Height of row on desktop
@ -137,7 +129,7 @@ const MOBILE_ROW_HEIGHT = 120; // Approx row height on mobile
const ROW_NUM_LPAD = 16; // Number of rows to load before and after viewport const ROW_NUM_LPAD = 16; // Number of rows to load before and after viewport
export default defineComponent({ export default defineComponent({
name: "Timeline", name: 'Timeline',
components: { components: {
FolderGrid, FolderGrid,
@ -213,41 +205,37 @@ export default defineComponent({
created() { created() {
subscribe(this.config_eventName, this.softRefresh); subscribe(this.config_eventName, this.softRefresh);
subscribe("files:file:created", this.softRefresh); subscribe('files:file:created', this.softRefresh);
subscribe("memories:window:resize", this.handleResizeWithDelay); subscribe('memories:window:resize', this.handleResizeWithDelay);
}, },
beforeDestroy() { beforeDestroy() {
unsubscribe(this.config_eventName, this.softRefresh); unsubscribe(this.config_eventName, this.softRefresh);
unsubscribe("files:file:created", this.softRefresh); unsubscribe('files:file:created', this.softRefresh);
unsubscribe("memories:window:resize", this.handleResizeWithDelay); unsubscribe('memories:window:resize', this.handleResizeWithDelay);
this.resetState(); this.resetState();
}, },
computed: { computed: {
routeIsBase(): boolean { routeIsBase(): boolean {
return this.$route.name === "timeline"; return this.$route.name === 'timeline';
}, },
routeIsPeople(): boolean { routeIsPeople(): boolean {
return ["recognize", "facerecognition"].includes( return ['recognize', 'facerecognition'].includes(<string>this.$route.name);
<string>this.$route.name
);
}, },
routeIsArchive(): boolean { routeIsArchive(): boolean {
return this.$route.name === "archive"; return this.$route.name === 'archive';
}, },
routeIsFolders(): boolean { routeIsFolders(): boolean {
return this.$route.name === "folders"; return this.$route.name === 'folders';
}, },
isMonthView(): boolean { isMonthView(): boolean {
if (this.$route.query.sort === "timeline") return false; if (this.$route.query.sort === 'timeline') return false;
return ( return (
this.$route.query.sort === "album" || this.$route.query.sort === 'album' ||
(this.config_sortAlbumMonth && (this.config_sortAlbumMonth && (this.$route.name === 'albums' || this.$route.name === 'album-share')) ||
(this.$route.name === "albums" || (this.config_sortFolderMonth && this.$route.name === 'folders')
this.$route.name === "album-share")) ||
(this.config_sortFolderMonth && this.$route.name === "folders")
); );
}, },
@ -279,13 +267,9 @@ export default defineComponent({
// Check if hash has changed // Check if hash has changed
const viewerIsOpen = (this.$refs.viewer as any)?.isOpen; const viewerIsOpen = (this.$refs.viewer as any)?.isOpen;
if ( if (from?.hash !== to.hash && to.hash?.startsWith('#v') && !viewerIsOpen) {
from?.hash !== to.hash &&
to.hash?.startsWith("#v") &&
!viewerIsOpen
) {
// Open viewer // Open viewer
const parts = to.hash.split("/"); const parts = to.hash.split('/');
if (parts.length !== 3) return; if (parts.length !== 3) return;
// Get params // Get params
@ -307,20 +291,14 @@ export default defineComponent({
// Scroll to photo if initializing // Scroll to photo if initializing
if (!from) { if (!from) {
const index = this.list.findIndex( const index = this.list.findIndex((r) => r.day.dayid === dayid && r.photos?.includes(photo));
(r) => r.day.dayid === dayid && r.photos?.includes(photo)
);
if (index !== -1) { if (index !== -1) {
(this.$refs.recycler as any).scrollToItem(index); (this.$refs.recycler as any).scrollToItem(index);
} }
} }
(this.$refs.viewer as any).open(photo, this.list); (this.$refs.viewer as any).open(photo, this.list);
} else if ( } else if (from?.hash?.startsWith('#v') && !to.hash?.startsWith('#v') && viewerIsOpen) {
from?.hash?.startsWith("#v") &&
!to.hash?.startsWith("#v") &&
viewerIsOpen
) {
// Close viewer // Close viewer
(this.$refs.viewer as any).close(); (this.$refs.viewer as any).close();
} }
@ -350,11 +328,7 @@ export default defineComponent({
this.recomputeSizes(); this.recomputeSizes();
// Timeline recycler init // Timeline recycler init
(this.$refs.recycler as any).$el.addEventListener( (this.$refs.recycler as any).$el.addEventListener('scroll', this.scrollPositionChange, { passive: true });
"scroll",
this.scrollPositionChange,
{ passive: true }
);
// Get data // Get data
await this.fetchDays(); await this.fetchDays();
@ -393,7 +367,7 @@ export default defineComponent({
/** Do resize after some time */ /** Do resize after some time */
handleResizeWithDelay() { handleResizeWithDelay() {
utils.setRenewingTimeout(this, "resizeTimer", this.recomputeSizes, 100); utils.setRenewingTimeout(this, 'resizeTimer', this.recomputeSizes, 100);
}, },
/** Recompute static sizes of containers */ /** Recompute static sizes of containers */
@ -419,7 +393,7 @@ export default defineComponent({
const widthChanged = this.rowWidth !== targetWidth; const widthChanged = this.rowWidth !== targetWidth;
if (heightChanged) { if (heightChanged) {
recycler.$el.style.height = targetHeight + "px"; recycler.$el.style.height = targetHeight + 'px';
} }
if (widthChanged) { if (widthChanged) {
@ -434,18 +408,12 @@ export default defineComponent({
if (this.isMobileLayout()) { if (this.isMobileLayout()) {
// Mobile // Mobile
this.numCols = Math.max( this.numCols = Math.max(3, Math.floor(this.rowWidth / MOBILE_ROW_HEIGHT));
3,
Math.floor(this.rowWidth / MOBILE_ROW_HEIGHT)
);
this.rowHeight = Math.floor(this.rowWidth / this.numCols); this.rowHeight = Math.floor(this.rowWidth / this.numCols);
} else { } else {
// Desktop // Desktop
if (this.config_squareThumbs) { if (this.config_squareThumbs) {
this.numCols = Math.max( this.numCols = Math.max(3, Math.floor(this.rowWidth / DESKTOP_ROW_HEIGHT));
3,
Math.floor(this.rowWidth / DESKTOP_ROW_HEIGHT)
);
this.rowHeight = Math.floor(this.rowWidth / this.numCols); this.rowHeight = Math.floor(this.rowWidth / this.numCols);
} else { } else {
// As a heuristic, assume all images are 4:3 landscape // As a heuristic, assume all images are 4:3 landscape
@ -527,12 +495,7 @@ export default defineComponent({
const delay = force || !scrolling ? 0 : SCROLL_LOAD_DELAY; const delay = force || !scrolling ? 0 : SCROLL_LOAD_DELAY;
// Debounce; only execute the newest call after delay // Debounce; only execute the newest call after delay
utils.setRenewingTimeout( utils.setRenewingTimeout(this, '_scrollChangeTimer', this.loadScrollView, delay);
this,
"_scrollChangeTimer",
this.loadScrollView,
delay
);
}, },
/** Load image data for given view (index based) */ /** Load image data for given view (index based) */
@ -583,17 +546,17 @@ export default defineComponent({
const query: { [key: string]: string } = {}; const query: { [key: string]: string } = {};
// Favorites // Favorites
if (this.$route.name === "favorites") { if (this.$route.name === 'favorites') {
API.DAYS_FILTER(query, DaysFilterType.FAVORITES); API.DAYS_FILTER(query, DaysFilterType.FAVORITES);
} }
// Videos // Videos
if (this.$route.name === "videos") { if (this.$route.name === 'videos') {
API.DAYS_FILTER(query, DaysFilterType.VIDEOS); API.DAYS_FILTER(query, DaysFilterType.VIDEOS);
} }
// Folder // Folder
if (this.$route.name === "folders") { if (this.$route.name === 'folders') {
const path = utils.getFolderRoutePath(this.config_foldersPath); const path = utils.getFolderRoutePath(this.config_foldersPath);
API.DAYS_FILTER(query, DaysFilterType.FOLDER, path); API.DAYS_FILTER(query, DaysFilterType.FOLDER, path);
if (this.$route.query.recursive) { if (this.$route.query.recursive) {
@ -602,16 +565,16 @@ export default defineComponent({
} }
// Archive // Archive
if (this.$route.name === "archive") { if (this.$route.name === 'archive') {
API.DAYS_FILTER(query, DaysFilterType.ARCHIVE); API.DAYS_FILTER(query, DaysFilterType.ARCHIVE);
} }
// Albums // Albums
const user = <string>this.$route.params.user; const user = <string>this.$route.params.user;
const name = <string>this.$route.params.name; const name = <string>this.$route.params.name;
if (this.$route.name === "albums") { if (this.$route.name === 'albums') {
if (!user || !name) { if (!user || !name) {
throw new Error("Invalid album route"); throw new Error('Invalid album route');
} }
API.DAYS_FILTER(query, DaysFilterType.ALBUM, `${user}/${name}`); API.DAYS_FILTER(query, DaysFilterType.ALBUM, `${user}/${name}`);
} }
@ -619,7 +582,7 @@ export default defineComponent({
// People // People
if (this.routeIsPeople) { if (this.routeIsPeople) {
if (!user || !name) { if (!user || !name) {
throw new Error("Invalid album route"); throw new Error('Invalid album route');
} }
const filter = <DaysFilterType>this.$route.name; const filter = <DaysFilterType>this.$route.name;
@ -632,28 +595,28 @@ export default defineComponent({
} }
// Places // Places
if (this.$route.name === "places") { if (this.$route.name === 'places') {
if (!name || !name.includes("-")) { if (!name || !name.includes('-')) {
throw new Error("Invalid place route"); throw new Error('Invalid place route');
} }
const id = <string>name.split("-", 1)[0]; const id = <string>name.split('-', 1)[0];
API.DAYS_FILTER(query, DaysFilterType.PLACE, id); API.DAYS_FILTER(query, DaysFilterType.PLACE, id);
} }
// Tags // Tags
if (this.$route.name === "tags") { if (this.$route.name === 'tags') {
if (!name) { if (!name) {
throw new Error("Invalid tag route"); throw new Error('Invalid tag route');
} }
API.DAYS_FILTER(query, DaysFilterType.TAG, name); API.DAYS_FILTER(query, DaysFilterType.TAG, name);
} }
// Map Bounds // Map Bounds
if (this.$route.name === "map") { if (this.$route.name === 'map') {
const bounds = <string>this.$route.query.b; const bounds = <string>this.$route.query.b;
if (!bounds) { if (!bounds) {
throw new Error("Missing map bounds"); throw new Error('Missing map bounds');
} }
API.DAYS_FILTER(query, DaysFilterType.MAP_BOUNDS, bounds); API.DAYS_FILTER(query, DaysFilterType.MAP_BOUNDS, bounds);
@ -692,9 +655,7 @@ export default defineComponent({
// Filter out hidden folders // Filter out hidden folders
if (!this.config_showHidden) { if (!this.config_showHidden) {
this.folders = this.folders.filter( this.folders = this.folders.filter((f) => !f.name.startsWith('.') && f.previews?.length);
(f) => !f.name.startsWith(".") && f.previews?.length
);
} }
}, },
@ -727,7 +688,7 @@ export default defineComponent({
const startState = this.state; const startState = this.state;
let data: IDay[] = []; let data: IDay[] = [];
if (this.$route.name === "thisday") { if (this.$route.name === 'thisday') {
data = await dav.getOnThisDayData(); data = await dav.getOnThisDayData();
} else if (dav.isSingleItem()) { } else if (dav.isSingleItem()) {
data = await dav.getSingleItemData(); data = await dav.getSingleItemData();
@ -813,10 +774,7 @@ export default defineComponent({
}; };
// Special headers // Special headers
if ( if (this.$route.name === 'thisday' && (!prevDay || Math.abs(prevDay.dayid - day.dayid) > 30)) {
this.$route.name === "thisday" &&
(!prevDay || Math.abs(prevDay.dayid - day.dayid) > 30)
) {
// thisday view with new year title // thisday view with new year title
head.size = 67; head.size = 67;
head.super = utils.getFromNowStr(utils.dayIdToDate(day.dayid)); head.super = utils.getFromNowStr(utils.dayIdToDate(day.dayid));
@ -868,7 +826,7 @@ export default defineComponent({
} }
// Notify parent components about stats // Notify parent components about stats
this.$emit("daysLoaded", { this.$emit('daysLoaded', {
count: data.reduce((acc, day) => acc + day.count, 0), count: data.reduce((acc, day) => acc + day.count, 0),
}); });
@ -921,7 +879,7 @@ export default defineComponent({
if (this.fetchDayQueue.length === 0) return; if (this.fetchDayQueue.length === 0) return;
// Construct URL // Construct URL
const dayStr = this.fetchDayQueue.join(","); const dayStr = this.fetchDayQueue.join(',');
const url = this.getDayUrl(dayStr); const url = this.getDayUrl(dayStr);
this.fetchDayQueue = []; this.fetchDayQueue = [];
@ -961,10 +919,7 @@ export default defineComponent({
if (head?.day?.detail?.length) { if (head?.day?.detail?.length) {
if ( if (
head.day.detail.length === photos.length && head.day.detail.length === photos.length &&
head.day.detail.every( head.day.detail.every((p, i) => p.fileid === photos[i].fileid && p.etag === photos[i].etag)
(p, i) =>
p.fileid === photos[i].fileid && p.etag === photos[i].etag
)
) { ) {
continue; continue;
} }
@ -974,7 +929,7 @@ export default defineComponent({
this.processDay(dayId, photos); this.processDay(dayId, photos);
} }
} catch (e) { } catch (e) {
showError(this.t("memories", "Failed to load some photos")); showError(this.t('memories', 'Failed to load some photos'));
console.error(e); console.error(e);
} }
}, },
@ -1058,10 +1013,7 @@ export default defineComponent({
let dataIdx = 0; let dataIdx = 0;
while (dataIdx < data.length) { while (dataIdx < data.length) {
// Check if we ran out of rows // Check if we ran out of rows
if ( if (rowIdx >= this.list.length || this.list[rowIdx].type === IRowType.HEAD) {
rowIdx >= this.list.length ||
this.list[rowIdx].type === IRowType.HEAD
) {
const newRow = this.addRow(day); const newRow = this.addRow(day);
addedRows.push(newRow); addedRows.push(newRow);
this.list.splice(rowIdx, 0, newRow); this.list.splice(rowIdx, 0, newRow);
@ -1172,11 +1124,7 @@ export default defineComponent({
// Get rid of any extra rows // Get rid of any extra rows
let spliceCount = 0; let spliceCount = 0;
for ( for (let i = rowIdx + 1; i < this.list.length && this.list[i].type !== IRowType.HEAD; i++) {
let i = rowIdx + 1;
i < this.list.length && this.list[i].type !== IRowType.HEAD;
i++
) {
spliceCount++; spliceCount++;
} }
if (spliceCount > 0) { if (spliceCount > 0) {
@ -1323,8 +1271,7 @@ export default defineComponent({
left: 0; left: 0;
cursor: pointer; cursor: pointer;
height: 100%; height: 100%;
transition: width 0.2s ease-in-out, height 0.2s ease-in-out, transition: width 0.2s ease-in-out, height 0.2s ease-in-out, transform 0.2s ease-in-out; // reflow
transform 0.2s ease-in-out; // reflow
} }
/** Dynamic top matter */ /** Dynamic top matter */

View File

@ -2,40 +2,33 @@
<div class="outer" v-if="loaded"> <div class="outer" v-if="loaded">
<NcLoadingIcon class="loading-icon" v-show="loading" /> <NcLoadingIcon class="loading-icon" v-show="loading" />
<component <component v-for="c in components" :key="c.__name" :is="c" :status="status" :config="config" @update="update" />
v-for="c in components"
:key="c.__name"
:is="c"
:status="status"
:config="config"
@update="update"
/>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from "vue"; import { defineComponent } from 'vue';
import axios from "@nextcloud/axios"; import axios from '@nextcloud/axios';
import { showError } from "@nextcloud/dialogs"; import { showError } from '@nextcloud/dialogs';
import { API } from "../../services/API"; import { API } from '../../services/API';
import * as utils from "../../services/Utils"; import * as utils from '../../services/Utils';
import Exif from "./sections/Exif.vue"; import Exif from './sections/Exif.vue';
import Indexing from "./sections/Indexing.vue"; import Indexing from './sections/Indexing.vue';
import Performance from "./sections/Performance.vue"; import Performance from './sections/Performance.vue';
import Places from "./sections/Places.vue"; import Places from './sections/Places.vue';
import Video from "./sections/Video.vue"; import Video from './sections/Video.vue';
import VideoTranscoder from "./sections/VideoTranscoder.vue"; import VideoTranscoder from './sections/VideoTranscoder.vue';
import VideoAccel from "./sections/VideoAccel.vue"; import VideoAccel from './sections/VideoAccel.vue';
import { ISystemConfig, ISystemStatus } from "./AdminTypes"; import { ISystemConfig, ISystemStatus } from './AdminTypes';
import NcLoadingIcon from "@nextcloud/vue/dist/Components/NcLoadingIcon"; import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon';
export default defineComponent({ export default defineComponent({
name: "Admin", name: 'Admin',
components: { components: {
NcLoadingIcon, NcLoadingIcon,
}, },
@ -47,15 +40,7 @@ export default defineComponent({
status: null as ISystemStatus | null, status: null as ISystemStatus | null,
config: null as ISystemConfig | null, config: null as ISystemConfig | null,
components: [ components: [Exif, Indexing, Performance, Places, Video, VideoTranscoder, VideoAccel],
Exif,
Indexing,
Performance,
Places,
Video,
VideoTranscoder,
VideoAccel,
],
}), }),
mounted() { mounted() {
@ -91,7 +76,7 @@ export default defineComponent({
async update(key: keyof ISystemConfig, value: any = null) { async update(key: keyof ISystemConfig, value: any = null) {
if (!this.config?.hasOwnProperty(key)) { if (!this.config?.hasOwnProperty(key)) {
console.error("Unknown setting", key); console.error('Unknown setting', key);
return; return;
} }
@ -105,15 +90,10 @@ export default defineComponent({
value: value, value: value,
}); });
utils.setRenewingTimeout( utils.setRenewingTimeout(this, '_refreshTimer', this.refreshStatus.bind(this), 500);
this,
"_refreshTimer",
this.refreshStatus.bind(this),
500
);
} catch (err) { } catch (err) {
console.error(err); console.error(err);
showError(this.t("memories", "Failed to update setting")); showError(this.t('memories', 'Failed to update setting'));
} finally { } finally {
this.loading--; this.loading--;
} }

View File

@ -1,16 +1,15 @@
import { defineComponent, PropType } from "vue"; import { defineComponent, PropType } from 'vue';
import { ISystemStatus, ISystemConfig, IBinaryStatus } from "./AdminTypes"; import { ISystemStatus, ISystemConfig, IBinaryStatus } from './AdminTypes';
import axios from "@nextcloud/axios"; import axios from '@nextcloud/axios';
const NcCheckboxRadioSwitch = () => const NcCheckboxRadioSwitch = () => import('@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch');
import("@nextcloud/vue/dist/Components/NcCheckboxRadioSwitch"); const NcNoteCard = () => import('@nextcloud/vue/dist/Components/NcNoteCard');
const NcNoteCard = () => import("@nextcloud/vue/dist/Components/NcNoteCard"); const NcTextField = () => import('@nextcloud/vue/dist/Components/NcTextField');
const NcTextField = () => import("@nextcloud/vue/dist/Components/NcTextField"); import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon';
import NcLoadingIcon from "@nextcloud/vue/dist/Components/NcLoadingIcon"; import NcButton from '@nextcloud/vue/dist/Components/NcButton';
import NcButton from "@nextcloud/vue/dist/Components/NcButton";
export default defineComponent({ export default defineComponent({
name: "AdminMixin", name: 'AdminMixin',
components: { components: {
NcCheckboxRadioSwitch, NcCheckboxRadioSwitch,
@ -33,7 +32,7 @@ export default defineComponent({
methods: { methods: {
update(key: keyof ISystemConfig, value: any = null) { update(key: keyof ISystemConfig, value: any = null) {
this.$emit("update", key, value); this.$emit('update', key, value);
}, },
binaryStatus(name: string, status: IBinaryStatus): string { binaryStatus(name: string, status: IBinaryStatus): string {
@ -41,20 +40,20 @@ export default defineComponent({
escape: false, escape: false,
sanitize: false, sanitize: false,
}; };
if (status === "ok") { if (status === 'ok') {
return this.t("memories", "{name} binary exists and is executable.", { return this.t('memories', '{name} binary exists and is executable.', {
name, name,
}); });
} else if (status === "not_found") { } else if (status === 'not_found') {
return this.t("memories", "{name} binary not found.", { name }); return this.t('memories', '{name} binary not found.', { name });
} else if (status === "not_executable") { } else if (status === 'not_executable') {
return this.t("memories", "{name} binary is not executable.", { return this.t('memories', '{name} binary is not executable.', {
name, name,
}); });
} else if (status.startsWith("test_fail")) { } else if (status.startsWith('test_fail')) {
return this.t( return this.t(
"memories", 'memories',
"{name} failed test: {info}.", '{name} failed test: {info}.',
{ {
name, name,
info: status.substring(10), info: status.substring(10),
@ -62,10 +61,10 @@ export default defineComponent({
0, 0,
noescape noescape
); );
} else if (status.startsWith("test_ok")) { } else if (status.startsWith('test_ok')) {
return this.t( return this.t(
"memories", 'memories',
"{name} binary exists and is usable ({info}).", '{name} binary exists and is usable ({info}).',
{ {
name, name,
info: status.substring(8), info: status.substring(8),
@ -74,7 +73,7 @@ export default defineComponent({
noescape noescape
); );
} else { } else {
return this.t("memories", "{name} binary status: {status}.", { return this.t('memories', '{name} binary status: {status}.', {
name, name,
status, status,
}); });
@ -82,16 +81,12 @@ export default defineComponent({
}, },
binaryStatusType(status: IBinaryStatus, critical = true): string { binaryStatusType(status: IBinaryStatus, critical = true): string {
if (status === "ok" || status.startsWith("test_ok")) { if (status === 'ok' || status.startsWith('test_ok')) {
return "success"; return 'success';
} else if ( } else if (status === 'not_found' || status === 'not_executable' || status.startsWith('test_fail')) {
status === "not_found" || return critical ? 'error' : 'warning';
status === "not_executable" ||
status.startsWith("test_fail")
) {
return critical ? "error" : "warning";
} else { } else {
return "warning"; return 'warning';
} }
}, },
}, },
@ -102,16 +97,16 @@ export default defineComponent({
}, },
actionToken() { actionToken() {
return this.status?.action_token || ""; return this.status?.action_token || '';
}, },
/** Reverse of memories.vod.disable, unfortunately */ /** Reverse of memories.vod.disable, unfortunately */
enableTranscoding: { enableTranscoding: {
get() { get() {
return !this.config["memories.vod.disable"]; return !this.config['memories.vod.disable'];
}, },
set(value: boolean) { set(value: boolean) {
this.config["memories.vod.disable"] = !value; this.config['memories.vod.disable'] = !value;
}, },
}, },
}, },

View File

@ -1,35 +1,30 @@
/** System configuration */ /** System configuration */
export type ISystemConfig = { export type ISystemConfig = {
"memories.exiftool": string; 'memories.exiftool': string;
"memories.exiftool_no_local": boolean; 'memories.exiftool_no_local': boolean;
"memories.index.mode": string; 'memories.index.mode': string;
"memories.index.path": string; 'memories.index.path': string;
"memories.gis_type": number; 'memories.gis_type': number;
"memories.vod.disable": boolean; 'memories.vod.disable': boolean;
"memories.vod.ffmpeg": string; 'memories.vod.ffmpeg': string;
"memories.vod.ffprobe": string; 'memories.vod.ffprobe': string;
"memories.vod.path": string; 'memories.vod.path': string;
"memories.vod.bind": string; 'memories.vod.bind': string;
"memories.vod.connect": string; 'memories.vod.connect': string;
"memories.vod.external": boolean; 'memories.vod.external': boolean;
"memories.video_default_quality": string; 'memories.video_default_quality': string;
"memories.vod.vaapi": boolean; 'memories.vod.vaapi': boolean;
"memories.vod.vaapi.low_power": boolean; 'memories.vod.vaapi.low_power': boolean;
"memories.vod.nvenc": boolean; 'memories.vod.nvenc': boolean;
"memories.vod.nvenc.temporal_aq": boolean; 'memories.vod.nvenc.temporal_aq': boolean;
"memories.vod.nvenc.scale": string; 'memories.vod.nvenc.scale': string;
}; };
export type IBinaryStatus = export type IBinaryStatus = 'ok' | 'not_found' | 'not_executable' | 'test_ok' | string;
| "ok"
| "not_found"
| "not_executable"
| "test_ok"
| string;
export type ISystemStatus = { export type ISystemStatus = {
last_index_job_start: number; last_index_job_start: number;
@ -47,7 +42,7 @@ export type ISystemStatus = {
ffmpeg: IBinaryStatus; ffmpeg: IBinaryStatus;
ffprobe: IBinaryStatus; ffprobe: IBinaryStatus;
govod: IBinaryStatus; govod: IBinaryStatus;
vaapi_dev: "ok" | "not_found" | "not_readable"; vaapi_dev: 'ok' | 'not_found' | 'not_readable';
action_token: string; action_token: string;
}; };

View File

@ -1,10 +1,10 @@
<template> <template>
<div class="admin-section"> <div class="admin-section">
<h2>{{ t("memories", "EXIF Extraction") }}</h2> <h2>{{ t('memories', 'EXIF Extraction') }}</h2>
<template v-if="status"> <template v-if="status">
<NcNoteCard :type="binaryStatusType(status.exiftool)"> <NcNoteCard :type="binaryStatusType(status.exiftool)">
{{ binaryStatus("exiftool", status.exiftool) }} {{ binaryStatus('exiftool', status.exiftool) }}
</NcNoteCard> </NcNoteCard>
</template> </template>
@ -18,13 +18,8 @@
<template v-if="status"> <template v-if="status">
<NcNoteCard :type="binaryStatusType(status.perl, false)"> <NcNoteCard :type="binaryStatusType(status.perl, false)">
{{ binaryStatus("perl", status.perl) }} {{ binaryStatus('perl', status.perl) }}
{{ {{ t('memories', 'You need perl only if the packaged exiftool binary does not work for some reason.') }}
t(
"memories",
"You need perl only if the packaged exiftool binary does not work for some reason."
)
}}
</NcNoteCard> </NcNoteCard>
</template> </template>
@ -33,20 +28,18 @@
@update:checked="update('memories.exiftool_no_local')" @update:checked="update('memories.exiftool_no_local')"
type="switch" type="switch"
> >
{{ {{ t('memories', 'Use system perl (only if exiftool binary does not work)') }}
t("memories", "Use system perl (only if exiftool binary does not work)")
}}
</NcCheckboxRadioSwitch> </NcCheckboxRadioSwitch>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from "vue"; import { defineComponent } from 'vue';
import AdminMixin from "../AdminMixin"; import AdminMixin from '../AdminMixin';
export default defineComponent({ export default defineComponent({
name: "Exif", name: 'Exif',
mixins: [AdminMixin], mixins: [AdminMixin],
}); });
</script> </script>

View File

@ -1,44 +1,41 @@
<template> <template>
<div class="admin-section"> <div class="admin-section">
<h2>{{ t("memories", "Media Indexing") }}</h2> <h2>{{ t('memories', 'Media Indexing') }}</h2>
<template v-if="status"> <template v-if="status">
<NcNoteCard :type="status.indexed_count > 0 ? 'success' : 'warning'"> <NcNoteCard :type="status.indexed_count > 0 ? 'success' : 'warning'">
{{ {{
t("memories", "{n} media files have been indexed", { t('memories', '{n} media files have been indexed', {
n: status.indexed_count, n: status.indexed_count,
}) })
}} }}
</NcNoteCard> </NcNoteCard>
<NcNoteCard :type="status.last_index_job_status_type"> <NcNoteCard :type="status.last_index_job_status_type">
{{ {{
t("memories", "Automatic Indexing status: {status}", { t('memories', 'Automatic Indexing status: {status}', {
status: status.last_index_job_status, status: status.last_index_job_status,
}) })
}} }}
</NcNoteCard> </NcNoteCard>
<NcNoteCard <NcNoteCard v-if="status.last_index_job_start" :type="status.last_index_job_duration ? 'success' : 'warning'">
v-if="status.last_index_job_start"
:type="status.last_index_job_duration ? 'success' : 'warning'"
>
{{ {{
t("memories", "Last index job was run {t} seconds ago.", { t('memories', 'Last index job was run {t} seconds ago.', {
t: status.last_index_job_start, t: status.last_index_job_start,
}) })
}} }}
{{ {{
status.last_index_job_duration status.last_index_job_duration
? t("memories", "It took {t} seconds to complete.", { ? t('memories', 'It took {t} seconds to complete.', {
t: status.last_index_job_duration, t: status.last_index_job_duration,
}) })
: t("memories", "It is still running or was interrupted.") : t('memories', 'It is still running or was interrupted.')
}} }}
</NcNoteCard> </NcNoteCard>
<NcNoteCard type="error" v-if="status.bad_encryption"> <NcNoteCard type="error" v-if="status.bad_encryption">
{{ {{
t( t(
"memories", 'memories',
"Only server-side encryption (OC_DEFAULT_MODULE) is supported, but another encryption module is enabled." 'Only server-side encryption (OC_DEFAULT_MODULE) is supported, but another encryption module is enabled.'
) )
}} }}
</NcNoteCard> </NcNoteCard>
@ -47,23 +44,18 @@
<p> <p>
{{ {{
t( t(
"memories", 'memories',
"The EXIF indexes are built and checked in a periodic background task. Be careful when selecting anything other than automatic indexing. For example, setting the indexing to only timeline folders may cause delays before media becomes available to users, since the user configures the timeline only after logging in." 'The EXIF indexes are built and checked in a periodic background task. Be careful when selecting anything other than automatic indexing. For example, setting the indexing to only timeline folders may cause delays before media becomes available to users, since the user configures the timeline only after logging in.'
)
}}
{{
t(
"memories",
'Folders with a ".nomedia" file are always excluded from indexing.'
) )
}} }}
{{ t('memories', 'Folders with a ".nomedia" file are always excluded from indexing.') }}
<NcCheckboxRadioSwitch <NcCheckboxRadioSwitch
:checked.sync="config['memories.index.mode']" :checked.sync="config['memories.index.mode']"
value="1" value="1"
name="idxm_radio" name="idxm_radio"
type="radio" type="radio"
@update:checked="update('memories.index.mode')" @update:checked="update('memories.index.mode')"
>{{ t("memories", "Index all media automatically (recommended)") }} >{{ t('memories', 'Index all media automatically (recommended)') }}
</NcCheckboxRadioSwitch> </NcCheckboxRadioSwitch>
<NcCheckboxRadioSwitch <NcCheckboxRadioSwitch
:checked.sync="config['memories.index.mode']" :checked.sync="config['memories.index.mode']"
@ -71,9 +63,7 @@
name="idxm_radio" name="idxm_radio"
type="radio" type="radio"
@update:checked="update('memories.index.mode')" @update:checked="update('memories.index.mode')"
>{{ >{{ t('memories', 'Index per-user timeline folders (not recommended)') }}
t("memories", "Index per-user timeline folders (not recommended)")
}}
</NcCheckboxRadioSwitch> </NcCheckboxRadioSwitch>
<NcCheckboxRadioSwitch <NcCheckboxRadioSwitch
:checked.sync="config['memories.index.mode']" :checked.sync="config['memories.index.mode']"
@ -81,7 +71,7 @@
name="idxm_radio" name="idxm_radio"
type="radio" type="radio"
@update:checked="update('memories.index.mode')" @update:checked="update('memories.index.mode')"
>{{ t("memories", "Index a fixed relative path") }} >{{ t('memories', 'Index a fixed relative path') }}
</NcCheckboxRadioSwitch> </NcCheckboxRadioSwitch>
<NcCheckboxRadioSwitch <NcCheckboxRadioSwitch
:checked.sync="config['memories.index.mode']" :checked.sync="config['memories.index.mode']"
@ -89,7 +79,7 @@
name="idxm_radio" name="idxm_radio"
type="radio" type="radio"
@update:checked="update('memories.index.mode')" @update:checked="update('memories.index.mode')"
>{{ t("memories", "Disable background indexing") }} >{{ t('memories', 'Disable background indexing') }}
</NcCheckboxRadioSwitch> </NcCheckboxRadioSwitch>
<NcTextField <NcTextField
@ -101,57 +91,46 @@
/> />
</p> </p>
{{ {{ t('memories', 'For advanced usage, perform a run of indexing by running:') }}
t("memories", "For advanced usage, perform a run of indexing by running:")
}}
<br /> <br />
<code>occ memories:index</code> <code>occ memories:index</code>
<br /> <br />
{{ t("memories", "Run index in parallel with 4 threads:") }} {{ t('memories', 'Run index in parallel with 4 threads:') }}
<br /> <br />
<code>bash -c 'for i in {1..4}; do (occ memories:index &amp;); done'</code> <code>bash -c 'for i in {1..4}; do (occ memories:index &amp;); done'</code>
<br /> <br />
{{ t("memories", "Force re-indexing of all files:") }} {{ t('memories', 'Force re-indexing of all files:') }}
<br /> <br />
<code>occ memories:index --force</code> <code>occ memories:index --force</code>
<br /> <br />
{{ t("memories", "You can limit indexing by user and/or folder:") }} {{ t('memories', 'You can limit indexing by user and/or folder:') }}
<br /> <br />
<code>occ memories:index --user=admin --folder=/Photos/</code> <code>occ memories:index --user=admin --folder=/Photos/</code>
<br /> <br />
{{ t("memories", "Clear all existing index tables:") }} {{ t('memories', 'Clear all existing index tables:') }}
<br /> <br />
<code>occ memories:index --clear</code> <code>occ memories:index --clear</code>
<br /> <br />
<br /> <br />
{{ {{ t('memories', 'The following MIME types are configured for preview generation correctly. More documentation:') }}
t( <a href="https://github.com/pulsejet/memories/wiki/File-Type-Support" target="_blank">
"memories", {{ t('memories', 'External Link') }}
"The following MIME types are configured for preview generation correctly. More documentation:"
)
}}
<a
href="https://github.com/pulsejet/memories/wiki/File-Type-Support"
target="_blank"
>
{{ t("memories", "External Link") }}
</a> </a>
<br /> <br />
<code v-if="status" <code v-if="status"
><template v-for="mime in status.mimes" ><template v-for="mime in status.mimes">{{ mime }}<br :key="mime" /></template
>{{ mime }}<br :key="mime" /></template
></code> ></code>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from "vue"; import { defineComponent } from 'vue';
import AdminMixin from "../AdminMixin"; import AdminMixin from '../AdminMixin';
export default defineComponent({ export default defineComponent({
name: "Indexing", name: 'Indexing',
mixins: [AdminMixin], mixins: [AdminMixin],
}); });
</script> </script>

View File

@ -1,29 +1,25 @@
<template> <template>
<div class="admin-section"> <div class="admin-section">
<h2>{{ t("memories", "Performance") }}</h2> <h2>{{ t('memories', 'Performance') }}</h2>
<p> <p>
<NcNoteCard :type="isHttps ? 'success' : 'warning'"> <NcNoteCard :type="isHttps ? 'success' : 'warning'">
{{ {{
isHttps isHttps
? t("memories", "HTTPS is enabled") ? t('memories', 'HTTPS is enabled')
: t( : t(
"memories", 'memories',
"You are accessing this page over an insecure context. Several browser APIs are not available, which will make Memories very slow. Enable HTTPS on your server to improve performance." 'You are accessing this page over an insecure context. Several browser APIs are not available, which will make Memories very slow. Enable HTTPS on your server to improve performance.'
) )
}} }}
</NcNoteCard> </NcNoteCard>
<NcNoteCard :type="httpVerOk ? 'success' : 'warning'"> <NcNoteCard :type="httpVerOk ? 'success' : 'warning'">
{{ {{
httpVerOk httpVerOk
? t("memories", "HTTP/2 or HTTP/3 is enabled") ? t('memories', 'HTTP/2 or HTTP/3 is enabled')
: t( : t('memories', 'HTTP/2 or HTTP/3 is strongly recommended ({httpVer} detected)', {
"memories",
"HTTP/2 or HTTP/3 is strongly recommended ({httpVer} detected)",
{
httpVer, httpVer,
} })
)
}} }}
</NcNoteCard> </NcNoteCard>
</p> </p>
@ -31,28 +27,26 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from "vue"; import { defineComponent } from 'vue';
import AdminMixin from "../AdminMixin"; import AdminMixin from '../AdminMixin';
export default defineComponent({ export default defineComponent({
name: "Performance", name: 'Performance',
mixins: [AdminMixin], mixins: [AdminMixin],
computed: { computed: {
isHttps(): boolean { isHttps(): boolean {
return window.location.protocol === "https:"; return window.location.protocol === 'https:';
}, },
httpVer(): string { httpVer(): string {
const entry = window.performance?.getEntriesByType?.( const entry = window.performance?.getEntriesByType?.('navigation')?.[0] as any;
"navigation" return entry?.nextHopProtocol || this.t('memories', 'Unknown');
)?.[0] as any;
return entry?.nextHopProtocol || this.t("memories", "Unknown");
}, },
httpVerOk(): boolean { httpVerOk(): boolean {
return this.httpVer === "h2" || this.httpVer === "h3"; return this.httpVer === 'h2' || this.httpVer === 'h3';
}, },
}, },
}); });

View File

@ -1,6 +1,6 @@
<template> <template>
<div class="admin-section"> <div class="admin-section">
<h2>{{ t("memories", "Reverse Geocoding") }}</h2> <h2>{{ t('memories', 'Reverse Geocoding') }}</h2>
<p> <p>
<template v-if="status"> <template v-if="status">
@ -13,118 +13,89 @@
> >
{{ {{
status.gis_count > 0 status.gis_count > 0
? t("memories", "Database is populated with {n} geometries.", { ? t('memories', 'Database is populated with {n} geometries.', {
n: status.gis_count, n: status.gis_count,
}) })
: t("memories", "Geometry table has not been created.") : t('memories', 'Geometry table has not been created.')
}} }}
{{ {{
status.gis_count > 0 && status.gis_count <= 500000 status.gis_count > 0 && status.gis_count <= 500000
? t("memories", "Looks like the planet data is incomplete.") ? t('memories', 'Looks like the planet data is incomplete.')
: "" : ''
}} }}
</NcNoteCard> </NcNoteCard>
<NcNoteCard <NcNoteCard
v-if=" v-if="typeof config['memories.gis_type'] !== 'number' || config['memories.gis_type'] < 0"
typeof config['memories.gis_type'] !== 'number' ||
config['memories.gis_type'] < 0
"
type="warning" type="warning"
> >
{{ {{
t( t('memories', 'Reverse geocoding has not been configured ({status}).', {
"memories", status: config['memories.gis_type'],
"Reverse geocoding has not been configured ({status}).", })
{ status: config["memories.gis_type"] }
)
}} }}
</NcNoteCard> </NcNoteCard>
</template> </template>
{{ {{
t( t(
"memories", 'memories',
"Memories supports offline reverse geocoding using the OpenStreetMaps data on MySQL and Postgres." 'Memories supports offline reverse geocoding using the OpenStreetMaps data on MySQL and Postgres.'
) )
}} }}
<br /> <br />
{{ {{
t( t(
"memories", 'memories',
"You need to download the planet data into your database. This is highly recommended and has low overhead." 'You need to download the planet data into your database. This is highly recommended and has low overhead.'
) )
}} }}
<br /> <br />
{{ {{ t('memories', 'If the button below does not work for importing the planet data, use the following command:') }}
t(
"memories",
"If the button below does not work for importing the planet data, use the following command:"
)
}}
<br /> <br />
<code>occ memories:places-setup</code> <code>occ memories:places-setup</code>
<br /> <br />
{{ {{ t('memories', 'Note: the geometry data is stored in the memories_planet_geometry table, with no prefix.') }}
t(
"memories",
"Note: the geometry data is stored in the memories_planet_geometry table, with no prefix."
)
}}
</p> </p>
<form <form :action="placesSetupUrl" method="post" @submit="placesSetup" target="_blank">
:action="placesSetupUrl"
method="post"
@submit="placesSetup"
target="_blank"
>
<input name="requesttoken" type="hidden" :value="requestToken" /> <input name="requesttoken" type="hidden" :value="requestToken" />
<input name="actiontoken" type="hidden" :value="actionToken" /> <input name="actiontoken" type="hidden" :value="actionToken" />
<NcButton nativeType="submit" type="warning"> <NcButton nativeType="submit" type="warning">
{{ t("memories", "Download planet database") }} {{ t('memories', 'Download planet database') }}
</NcButton> </NcButton>
</form> </form>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from "vue"; import { defineComponent } from 'vue';
import { API } from "../../../services/API"; import { API } from '../../../services/API';
import AdminMixin from "../AdminMixin"; import AdminMixin from '../AdminMixin';
export default defineComponent({ export default defineComponent({
name: "Places", name: 'Places',
mixins: [AdminMixin], mixins: [AdminMixin],
computed: { computed: {
gisStatus() { gisStatus() {
if (!this.status) return ""; if (!this.status) return '';
if (typeof this.status.gis_type !== "number") { if (typeof this.status.gis_type !== 'number') {
return this.status.gis_type; return this.status.gis_type;
} }
if (this.status.gis_type <= 0) { if (this.status.gis_type <= 0) {
return this.t( return this.t('memories', 'Geometry support was not detected in your database');
"memories",
"Geometry support was not detected in your database"
);
} else if (this.status.gis_type === 1) { } else if (this.status.gis_type === 1) {
return this.t("memories", "MySQL-like geometry support was detected "); return this.t('memories', 'MySQL-like geometry support was detected ');
} else if (this.status.gis_type === 2) { } else if (this.status.gis_type === 2) {
return this.t( return this.t('memories', 'Postgres native geometry support was detected');
"memories",
"Postgres native geometry support was detected"
);
} }
}, },
gisStatusType() { gisStatusType() {
return typeof this.status?.gis_type !== "number" || return typeof this.status?.gis_type !== 'number' || this.status.gis_type <= 0 ? 'error' : 'success';
this.status.gis_type <= 0
? "error"
: "success";
}, },
placesSetupUrl() { placesSetupUrl() {
@ -135,19 +106,12 @@ export default defineComponent({
methods: { methods: {
placesSetup(event: Event) { placesSetup(event: Event) {
const warnSetup = this.t( const warnSetup = this.t(
"memories", 'memories',
"Looks like the database is already setup. Are you sure you want to redownload planet data?" 'Looks like the database is already setup. Are you sure you want to redownload planet data?'
); );
const warnLong = this.t( const warnLong = this.t('memories', 'You are about to download the planet database. This may take a while.');
"memories", const warnReindex = this.t('memories', 'This may also cause all photos to be re-indexed!');
"You are about to download the planet database. This may take a while." const msg = (this.status?.gis_count ? warnSetup : warnLong) + ' ' + warnReindex;
);
const warnReindex = this.t(
"memories",
"This may also cause all photos to be re-indexed!"
);
const msg =
(this.status?.gis_count ? warnSetup : warnLong) + " " + warnReindex;
if (!confirm(msg)) { if (!confirm(msg)) {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();

View File

@ -1,19 +1,14 @@
<template> <template>
<div class="admin-section"> <div class="admin-section">
<h2>{{ t("memories", "Video Streaming") }}</h2> <h2>{{ t('memories', 'Video Streaming') }}</h2>
<p> <p>
{{ {{ t('memories', 'Live transcoding provides for adaptive streaming of videos using HLS.') }}
t(
"memories",
"Live transcoding provides for adaptive streaming of videos using HLS."
)
}}
<br /> <br />
{{ {{
t( t(
"memories", 'memories',
"Note that this may be very CPU intensive without hardware acceleration, and transcoding will not be used for external storage." 'Note that this may be very CPU intensive without hardware acceleration, and transcoding will not be used for external storage.'
) )
}} }}
@ -22,15 +17,15 @@
@update:checked="update('memories.vod.disable', !enableTranscoding)" @update:checked="update('memories.vod.disable', !enableTranscoding)"
type="switch" type="switch"
> >
{{ t("memories", "Enable Transcoding") }} {{ t('memories', 'Enable Transcoding') }}
</NcCheckboxRadioSwitch> </NcCheckboxRadioSwitch>
<template v-if="status"> <template v-if="status">
<NcNoteCard :type="binaryStatusType(status.ffmpeg)"> <NcNoteCard :type="binaryStatusType(status.ffmpeg)">
{{ binaryStatus("ffmpeg", status.ffmpeg) }} {{ binaryStatus('ffmpeg', status.ffmpeg) }}
</NcNoteCard> </NcNoteCard>
<NcNoteCard :type="binaryStatusType(status.ffprobe)"> <NcNoteCard :type="binaryStatusType(status.ffprobe)">
{{ binaryStatus("ffprobe", status.ffprobe) }} {{ binaryStatus('ffprobe', status.ffprobe) }}
</NcNoteCard> </NcNoteCard>
</template> </template>
@ -51,7 +46,7 @@
/> />
<br /> <br />
{{ t("memories", "Global default video quality (user may override)") }} {{ t('memories', 'Global default video quality (user may override)') }}
<NcCheckboxRadioSwitch <NcCheckboxRadioSwitch
:disabled="!enableTranscoding" :disabled="!enableTranscoding"
:checked.sync="config['memories.video_default_quality']" :checked.sync="config['memories.video_default_quality']"
@ -59,7 +54,7 @@
name="vdq_radio" name="vdq_radio"
type="radio" type="radio"
@update:checked="update('memories.video_default_quality')" @update:checked="update('memories.video_default_quality')"
>{{ t("memories", "Auto (adaptive transcode)") }} >{{ t('memories', 'Auto (adaptive transcode)') }}
</NcCheckboxRadioSwitch> </NcCheckboxRadioSwitch>
<NcCheckboxRadioSwitch <NcCheckboxRadioSwitch
:disabled="!enableTranscoding" :disabled="!enableTranscoding"
@ -68,7 +63,7 @@
name="vdq_radio" name="vdq_radio"
type="radio" type="radio"
@update:checked="update('memories.video_default_quality')" @update:checked="update('memories.video_default_quality')"
>{{ t("memories", "Original (transcode with max quality)") }} >{{ t('memories', 'Original (transcode with max quality)') }}
</NcCheckboxRadioSwitch> </NcCheckboxRadioSwitch>
<NcCheckboxRadioSwitch <NcCheckboxRadioSwitch
:disabled="!enableTranscoding" :disabled="!enableTranscoding"
@ -77,19 +72,19 @@
name="vdq_radio" name="vdq_radio"
type="radio" type="radio"
@update:checked="update('memories.video_default_quality')" @update:checked="update('memories.video_default_quality')"
>{{ t("memories", "Direct (original video file without transcode)") }} >{{ t('memories', 'Direct (original video file without transcode)') }}
</NcCheckboxRadioSwitch> </NcCheckboxRadioSwitch>
</p> </p>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from "vue"; import { defineComponent } from 'vue';
import AdminMixin from "../AdminMixin"; import AdminMixin from '../AdminMixin';
export default defineComponent({ export default defineComponent({
name: "Video", name: 'Video',
mixins: [AdminMixin], mixins: [AdminMixin],
}); });
</script> </script>

View File

@ -1,49 +1,26 @@
<template> <template>
<div class="admin-section"> <div class="admin-section">
<h3>{{ t("memories", "Hardware Acceleration") }}</h3> <h3>{{ t('memories', 'Hardware Acceleration') }}</h3>
<p> <p>
{{ {{ t('memories', 'You must first make sure the correct drivers are installed before configuring acceleration.') }}
t(
"memories",
"You must first make sure the correct drivers are installed before configuring acceleration."
)
}}
<br /> <br />
{{ {{ t('memories', 'Make sure you test hardware acceleration with various options after enabling.') }}
t(
"memories",
"Make sure you test hardware acceleration with various options after enabling."
)
}}
<br /> <br />
{{ {{ t('memories', 'Do not enable multiple types of hardware acceleration simultaneously.') }}
t(
"memories",
"Do not enable multiple types of hardware acceleration simultaneously."
)
}}
<br /> <br />
<br /> <br />
{{ {{
t( t(
"memories", 'memories',
"Intel processors supporting QuickSync Video (QSV) as well as some AMD GPUs can be used for transcoding using VA-API acceleration." 'Intel processors supporting QuickSync Video (QSV) as well as some AMD GPUs can be used for transcoding using VA-API acceleration.'
) )
}} }}
{{ {{ t('memories', 'For more details on driver installation, check the documentation:') }}
t( <a target="_blank" href="https://github.com/pulsejet/memories/wiki/HW-Transcoding#va-api">
"memories", {{ t('memories', 'External Link') }}
"For more details on driver installation, check the documentation:"
)
}}
<a
target="_blank"
href="https://github.com/pulsejet/memories/wiki/HW-Transcoding#va-api"
>
{{ t("memories", "External Link") }}
</a> </a>
<NcNoteCard :type="vaapiStatusType" v-if="status"> <NcNoteCard :type="vaapiStatusType" v-if="status">
@ -56,7 +33,7 @@
@update:checked="update('memories.vod.vaapi')" @update:checked="update('memories.vod.vaapi')"
type="switch" type="switch"
> >
{{ t("memories", "Enable acceleration with VA-API") }} {{ t('memories', 'Enable acceleration with VA-API') }}
</NcCheckboxRadioSwitch> </NcCheckboxRadioSwitch>
<NcCheckboxRadioSwitch <NcCheckboxRadioSwitch
@ -65,30 +42,20 @@
@update:checked="update('memories.vod.vaapi.low_power')" @update:checked="update('memories.vod.vaapi.low_power')"
type="switch" type="switch"
> >
{{ t("memories", "Enable low-power mode (QSV)") }} {{ t('memories', 'Enable low-power mode (QSV)') }}
</NcCheckboxRadioSwitch> </NcCheckboxRadioSwitch>
{{ {{ t('memories', 'NVIDIA GPUs can be used for transcoding using the NVENC encoder with the proper drivers.') }}
t(
"memories",
"NVIDIA GPUs can be used for transcoding using the NVENC encoder with the proper drivers."
)
}}
<br /> <br />
{{ {{
t( t(
"memories", 'memories',
"Depending on the versions of the installed SDK and ffmpeg, you need to specify the scaler to use" 'Depending on the versions of the installed SDK and ffmpeg, you need to specify the scaler to use'
) )
}} }}
<NcNoteCard type="warning"> <NcNoteCard type="warning">
{{ {{ t('memories', 'No automated tests are available for NVIDIA acceleration.') }}
t(
"memories",
"No automated tests are available for NVIDIA acceleration."
)
}}
</NcNoteCard> </NcNoteCard>
<NcCheckboxRadioSwitch <NcCheckboxRadioSwitch
@ -97,7 +64,7 @@
@update:checked="update('memories.vod.nvenc')" @update:checked="update('memories.vod.nvenc')"
type="switch" type="switch"
> >
{{ t("memories", "Enable acceleration with NVENC") }} {{ t('memories', 'Enable acceleration with NVENC') }}
</NcCheckboxRadioSwitch> </NcCheckboxRadioSwitch>
<NcCheckboxRadioSwitch <NcCheckboxRadioSwitch
:disabled="!enableTranscoding || !config['memories.vod.nvenc']" :disabled="!enableTranscoding || !config['memories.vod.nvenc']"
@ -105,7 +72,7 @@
@update:checked="update('memories.vod.nvenc.temporal_aq')" @update:checked="update('memories.vod.nvenc.temporal_aq')"
type="switch" type="switch"
> >
{{ t("memories", "Enable NVENC Temporal AQ") }} {{ t('memories', 'Enable NVENC Temporal AQ') }}
</NcCheckboxRadioSwitch> </NcCheckboxRadioSwitch>
<NcCheckboxRadioSwitch <NcCheckboxRadioSwitch
@ -116,7 +83,7 @@
type="radio" type="radio"
@update:checked="update('memories.vod.nvenc.scale')" @update:checked="update('memories.vod.nvenc.scale')"
class="m-radio" class="m-radio"
>{{ t("memories", "NPP scaler") }} >{{ t('memories', 'NPP scaler') }}
</NcCheckboxRadioSwitch> </NcCheckboxRadioSwitch>
<NcCheckboxRadioSwitch <NcCheckboxRadioSwitch
:disabled="!enableTranscoding || !config['memories.vod.nvenc']" :disabled="!enableTranscoding || !config['memories.vod.nvenc']"
@ -126,45 +93,41 @@
type="radio" type="radio"
class="m-radio" class="m-radio"
@update:checked="update('memories.vod.nvenc.scale')" @update:checked="update('memories.vod.nvenc.scale')"
>{{ t("memories", "CUDA scaler") }} >{{ t('memories', 'CUDA scaler') }}
</NcCheckboxRadioSwitch> </NcCheckboxRadioSwitch>
</p> </p>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from "vue"; import { defineComponent } from 'vue';
import AdminMixin from "../AdminMixin"; import AdminMixin from '../AdminMixin';
export default defineComponent({ export default defineComponent({
name: "VideoAccel", name: 'VideoAccel',
mixins: [AdminMixin], mixins: [AdminMixin],
computed: { computed: {
vaapiStatusText(): string { vaapiStatusText(): string {
if (!this.status) return ""; if (!this.status) return '';
const dev = "/dev/dri/renderD128"; const dev = '/dev/dri/renderD128';
if (this.status.vaapi_dev === "ok") { if (this.status.vaapi_dev === 'ok') {
return this.t("memories", "VA-API device ({dev}) is readable", { dev }); return this.t('memories', 'VA-API device ({dev}) is readable', { dev });
} else if (this.status.vaapi_dev === "not_found") { } else if (this.status.vaapi_dev === 'not_found') {
return this.t("memories", "VA-API device ({dev}) not found", { dev }); return this.t('memories', 'VA-API device ({dev}) not found', { dev });
} else if (this.status.vaapi_dev === "not_readable") { } else if (this.status.vaapi_dev === 'not_readable') {
return this.t( return this.t('memories', 'VA-API device ({dev}) has incorrect permissions', { dev });
"memories",
"VA-API device ({dev}) has incorrect permissions",
{ dev }
);
} else { } else {
return this.t("memories", "VA-API device status: {status}", { return this.t('memories', 'VA-API device status: {status}', {
status: this.status.vaapi_dev, status: this.status.vaapi_dev,
}); });
} }
}, },
vaapiStatusType(): string { vaapiStatusType(): string {
return this.status?.vaapi_dev === "ok" ? "success" : "error"; return this.status?.vaapi_dev === 'ok' ? 'success' : 'error';
}, },
}, },
}); });

View File

@ -1,23 +1,20 @@
<template> <template>
<div class="admin-section"> <div class="admin-section">
<h3>{{ t("memories", "Transcoder configuration") }}</h3> <h3>{{ t('memories', 'Transcoder configuration') }}</h3>
<p> <p>
{{ {{
t( t(
"memories", 'memories',
"Memories uses the go-vod transcoder. You can run go-vod exernally (e.g. in a separate Docker container for hardware acceleration) or use the built-in transcoder. To use an external transcoder, enable the following option and follow the instructions in the documentation:" 'Memories uses the go-vod transcoder. You can run go-vod exernally (e.g. in a separate Docker container for hardware acceleration) or use the built-in transcoder. To use an external transcoder, enable the following option and follow the instructions in the documentation:'
) )
}} }}
<a <a target="_blank" href="https://github.com/pulsejet/memories/wiki/HW-Transcoding">
target="_blank" {{ t('memories', 'External Link') }}
href="https://github.com/pulsejet/memories/wiki/HW-Transcoding"
>
{{ t("memories", "External Link") }}
</a> </a>
<template v-if="status"> <template v-if="status">
<NcNoteCard :type="binaryStatusType(status.govod)"> <NcNoteCard :type="binaryStatusType(status.govod)">
{{ binaryStatus("go-vod", status.govod) }} {{ binaryStatus('go-vod', status.govod) }}
</NcNoteCard> </NcNoteCard>
</template> </template>
@ -27,7 +24,7 @@
@update:checked="update('memories.vod.external')" @update:checked="update('memories.vod.external')"
type="switch" type="switch"
> >
{{ t("memories", "Enable external transcoder (go-vod)") }} {{ t('memories', 'Enable external transcoder (go-vod)') }}
</NcCheckboxRadioSwitch> </NcCheckboxRadioSwitch>
<NcTextField <NcTextField
@ -58,12 +55,12 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from "vue"; import { defineComponent } from 'vue';
import AdminMixin from "../AdminMixin"; import AdminMixin from '../AdminMixin';
export default defineComponent({ export default defineComponent({
name: "VideoTranscoder", name: 'VideoTranscoder',
mixins: [AdminMixin], mixins: [AdminMixin],
}); });
</script> </script>

View File

@ -3,12 +3,12 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from "vue"; import { defineComponent } from 'vue';
import AdminMixin from "../AdminMixin"; import AdminMixin from '../AdminMixin';
export default defineComponent({ export default defineComponent({
name: "Template", name: 'Template',
mixins: [AdminMixin], mixins: [AdminMixin],
}); });
</script> </script>

View File

@ -1,11 +1,5 @@
<template> <template>
<router-link <router-link draggable="false" class="cluster fill-block" :class="{ error }" :to="target" @click.native="click">
draggable="false"
class="cluster fill-block"
:class="{ error }"
:to="target"
@click.native="click"
>
<div class="bbl"> <div class="bbl">
<NcCounterBubble> {{ data.count }} </NcCounterBubble> <NcCounterBubble> {{ data.count }} </NcCounterBubble>
</div> </div>
@ -31,21 +25,21 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, PropType } from "vue"; import { defineComponent, PropType } from 'vue';
import { getCurrentUser } from "@nextcloud/auth"; import { getCurrentUser } from '@nextcloud/auth';
import NcCounterBubble from "@nextcloud/vue/dist/Components/NcCounterBubble"; import NcCounterBubble from '@nextcloud/vue/dist/Components/NcCounterBubble';
import type { IAlbum, ICluster, IFace, IPhoto } from "../../types"; import type { IAlbum, ICluster, IFace, IPhoto } from '../../types';
import { getPreviewUrl } from "../../services/utils/helpers"; import { getPreviewUrl } from '../../services/utils/helpers';
import errorsvg from "../../assets/error.svg"; import errorsvg from '../../assets/error.svg';
import { API } from "../../services/API"; import { API } from '../../services/API';
import Vue from "vue"; import Vue from 'vue';
export default defineComponent({ export default defineComponent({
name: "Cluster", name: 'Cluster',
components: { components: {
NcCounterBubble, NcCounterBubble,
}, },
@ -79,7 +73,7 @@ export default defineComponent({
title() { title() {
if (this.tag) { if (this.tag) {
return this.t("recognize", this.tag.name); return this.t('recognize', this.tag.name);
} }
return this.data.name; return this.data.name;
@ -90,27 +84,25 @@ export default defineComponent({
return `(${this.album.user})`; return `(${this.album.user})`;
} }
return ""; return '';
}, },
tag() { tag() {
return this.data.cluster_type === "tags" && this.data; return this.data.cluster_type === 'tags' && this.data;
}, },
face() { face() {
return ( return (
(this.data.cluster_type === "recognize" || (this.data.cluster_type === 'recognize' || this.data.cluster_type === 'facerecognition') && (this.data as IFace)
this.data.cluster_type === "facerecognition") &&
(this.data as IFace)
); );
}, },
place() { place() {
return this.data.cluster_type === "places" && this.data; return this.data.cluster_type === 'places' && this.data;
}, },
album() { album() {
return this.data.cluster_type === "albums" && (this.data as IAlbum); return this.data.cluster_type === 'albums' && (this.data as IAlbum);
}, },
/** Target URL to navigate to */ /** Target URL to navigate to */
@ -120,7 +112,7 @@ export default defineComponent({
if (this.album) { if (this.album) {
const user = this.album.user; const user = this.album.user;
const name = this.album.name; const name = this.album.name;
return { name: "albums", params: { user, name } }; return { name: 'albums', params: { user, name } };
} }
if (this.face) { if (this.face) {
@ -133,25 +125,22 @@ export default defineComponent({
const id = this.place.cluster_id; const id = this.place.cluster_id;
const placeName = this.place.name || id; const placeName = this.place.name || id;
const name = `${id}-${placeName}`; const name = `${id}-${placeName}`;
return { name: "places", params: { name } }; return { name: 'places', params: { name } };
} }
return { name: "tags", params: { name: this.data.name } }; return { name: 'tags', params: { name: this.data.name } };
}, },
error() { error() {
return ( return Boolean(this.data.previewError) || Boolean(this.album && this.album.last_added_photo <= 0);
Boolean(this.data.previewError) ||
Boolean(this.album && this.album.last_added_photo <= 0)
);
}, },
}, },
methods: { methods: {
failed() { failed() {
Vue.set(this.data, "previewError", true); Vue.set(this.data, 'previewError', true);
}, },
click() { click() {
this.$emit("click", this.data); this.$emit('click', this.data);
}, },
}, },
}); });
@ -235,11 +224,7 @@ img {
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
background: linear-gradient( background: linear-gradient(0deg, rgba(0, 0, 0, 0.7) 10%, transparent 35%);
0deg,
rgba(0, 0, 0, 0.7) 10%,
transparent 35%
);
.cluster.error & { .cluster.error & {
display: none; display: none;

View File

@ -29,16 +29,16 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, PropType } from "vue"; import { defineComponent, PropType } from 'vue';
import { IFolder, IPhoto } from "../../types"; import { IFolder, IPhoto } from '../../types';
import { getPreviewUrl } from "../../services/utils/helpers"; import { getPreviewUrl } from '../../services/utils/helpers';
import UserConfig from "../../mixins/UserConfig"; import UserConfig from '../../mixins/UserConfig';
import FolderIcon from "vue-material-design-icons/Folder.vue"; import FolderIcon from 'vue-material-design-icons/Folder.vue';
export default defineComponent({ export default defineComponent({
name: "Folder", name: 'Folder',
components: { components: {
FolderIcon, FolderIcon,
}, },
@ -65,20 +65,17 @@ export default defineComponent({
/** Open folder */ /** Open folder */
target() { target() {
const path = this.data.path const path = this.data.path
.split("/") .split('/')
.filter((x) => x) .filter((x) => x)
.slice(2) as string[]; .slice(2) as string[];
// Remove base path if present // Remove base path if present
const basePath = this.config_foldersPath.split("/").filter((x) => x); const basePath = this.config_foldersPath.split('/').filter((x) => x);
if ( if (path.length >= basePath.length && path.slice(0, basePath.length).every((x, i) => x === basePath[i])) {
path.length >= basePath.length &&
path.slice(0, basePath.length).every((x, i) => x === basePath[i])
) {
path.splice(0, basePath.length); path.splice(0, basePath.length);
} }
return { name: "folders", params: { path: path as any } }; return { name: 'folders', params: { path: path as any } };
}, },
}, },
@ -176,8 +173,7 @@ export default defineComponent({
// 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;

View File

@ -18,11 +18,7 @@
<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>
@ -50,15 +46,7 @@
@load="load" @load="load"
@error="error" @error="error"
/> />
<video <video ref="video" v-if="videoUrl" :src="videoUrl" preload="none" muted playsinline disableRemotePlayback />
ref="video"
v-if="videoUrl"
:src="videoUrl"
preload="none"
muted
playsinline
disableRemotePlayback
/>
<div class="overlay fill-block" /> <div class="overlay fill-block" />
</div> </div>
</div> </div>
@ -66,19 +54,19 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, PropType } from "vue"; import { defineComponent, PropType } from 'vue';
import { IDay, IPhoto } from "../../types"; import { IDay, IPhoto } from '../../types';
import * as utils from "../../services/Utils"; import * as utils from '../../services/Utils';
import errorsvg from "../../assets/error.svg"; import errorsvg from '../../assets/error.svg';
import CheckCircle from "vue-material-design-icons/CheckCircle.vue"; import CheckCircle from 'vue-material-design-icons/CheckCircle.vue';
import Star from "vue-material-design-icons/Star.vue"; import Star from 'vue-material-design-icons/Star.vue';
import Video from "vue-material-design-icons/PlayCircleOutline.vue"; import Video from 'vue-material-design-icons/PlayCircleOutline.vue';
import LivePhoto from "vue-material-design-icons/MotionPlayOutline.vue"; import LivePhoto from 'vue-material-design-icons/MotionPlayOutline.vue';
export default defineComponent({ export default defineComponent({
name: "Photo", name: 'Photo',
components: { components: {
CheckCircle, CheckCircle,
Video, Video,
@ -106,8 +94,7 @@ export default defineComponent({
data(newData: IPhoto, oldData: IPhoto) { data(newData: IPhoto, oldData: IPhoto) {
// Copy flags relevant to this component // Copy flags relevant to this component
if (oldData && newData) { if (oldData && newData) {
newData.flag |= newData.flag |= oldData.flag & (this.c.FLAG_SELECTED | this.c.FLAG_LOAD_FAIL);
oldData.flag & (this.c.FLAG_SELECTED | this.c.FLAG_LOAD_FAIL);
} }
}, },
}, },
@ -164,7 +151,7 @@ export default defineComponent({
methods: { methods: {
emitSelect(data: IPhoto) { emitSelect(data: IPhoto) {
this.$emit("select", data); this.$emit('select', data);
}, },
/** Get url of the photo */ /** Get url of the photo */
@ -184,11 +171,7 @@ export default defineComponent({
// Make the shorter dimension equal to base // Make the shorter dimension equal to base
let size = base; let size = base;
if (this.data.w && this.data.h) { if (this.data.w && this.data.h) {
size = size = Math.floor((base * Math.max(this.data.w, this.data.h)) / Math.min(this.data.w, this.data.h)) - 1;
Math.floor(
(base * Math.max(this.data.w, this.data.h)) /
Math.min(this.data.w, this.data.h)
) - 1;
} }
return utils.getPreviewUrl(this.data, false, size); return utils.getPreviewUrl(this.data, false, size);
@ -207,14 +190,14 @@ export default defineComponent({
// so there's no point in trying to draw the face rect // so there's no point in trying to draw the face rect
if (!img || img.naturalWidth < 5) return; if (!img || img.naturalWidth < 5) return;
const canvas = document.createElement("canvas"); const canvas = document.createElement('canvas');
const context = canvas.getContext("2d"); const context = canvas.getContext('2d');
if (!context) return; // failed to create canvas if (!context) return; // failed to create canvas
canvas.width = img.naturalWidth; canvas.width = img.naturalWidth;
canvas.height = img.naturalHeight; canvas.height = img.naturalHeight;
context.drawImage(img, 0, 0); context.drawImage(img, 0, 0);
context.strokeStyle = "#00ff00"; context.strokeStyle = '#00ff00';
context.lineWidth = 2; context.lineWidth = 2;
context.strokeRect( context.strokeRect(
this.data.facerect.x * img.naturalWidth, this.data.facerect.x * img.naturalWidth,
@ -228,7 +211,7 @@ export default defineComponent({
if (!blob) return; if (!blob) return;
this.faceSrc = URL.createObjectURL(blob); this.faceSrc = URL.createObjectURL(blob);
}, },
"image/jpeg", 'image/jpeg',
0.95 0.95
); );
}, },
@ -281,8 +264,7 @@ export default defineComponent({
padding: 1px; padding: 1px;
} }
transition: background-color 0.15s ease, opacity 0.2s ease-in, transition: background-color 0.15s ease, opacity 0.2s ease-in, transform 0.2s ease-in;
transform 0.2s ease-in;
&.leaving { &.leaving {
transform: scale(0.9); transform: scale(0.9);

View File

@ -1,9 +1,5 @@
<template> <template>
<div <div class="head-row" :class="{ selected: item.selected }" :style="{ height: `${item.size}px` }">
class="head-row"
:class="{ selected: item.selected }"
:style="{ height: `${item.size}px` }"
>
<div class="super" v-if="item.super !== undefined"> <div class="super" v-if="item.super !== undefined">
{{ item.super }} {{ item.super }}
</div> </div>
@ -15,15 +11,15 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, PropType } from "vue"; import { defineComponent, PropType } from 'vue';
import { IHeadRow } from "../../types"; import { IHeadRow } from '../../types';
import CheckCircle from "vue-material-design-icons/CheckCircle.vue"; import CheckCircle from 'vue-material-design-icons/CheckCircle.vue';
import * as utils from "../../services/Utils"; import * as utils from '../../services/Utils';
export default defineComponent({ export default defineComponent({
name: "RowHead", name: 'RowHead',
components: { components: {
CheckCircle, CheckCircle,
@ -66,7 +62,7 @@ export default defineComponent({
methods: { methods: {
click() { click() {
this.$emit("click", this.item); this.$emit('click', this.item);
}, },
}, },
}); });

View File

@ -3,14 +3,13 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from "vue"; import { defineComponent } from 'vue';
import { fetchImage, sticky } from "./XImgCache"; import { fetchImage, sticky } from './XImgCache';
const BLANK_IMG = const BLANK_IMG = 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
"data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7";
export default defineComponent({ export default defineComponent({
name: "XImg", name: 'XImg',
props: { props: {
src: { src: {
type: String, type: String,
@ -18,7 +17,7 @@ export default defineComponent({
}, },
alt: { alt: {
type: String, type: String,
default: "", default: '',
}, },
}, },
@ -55,7 +54,7 @@ export default defineComponent({
this.freeBlob(); this.freeBlob();
// Just set src if not http // Just set src if not http
if (this.src.startsWith("data:") || this.src.startsWith("blob:")) { if (this.src.startsWith('data:') || this.src.startsWith('blob:')) {
this.dataSrc = this.src; this.dataSrc = this.src;
return; return;
} }
@ -72,14 +71,14 @@ export default defineComponent({
this.lockBlob(); this.lockBlob();
} catch (error) { } catch (error) {
this.dataSrc = BLANK_IMG; this.dataSrc = BLANK_IMG;
this.$emit("error", error); this.$emit('error', error);
console.error("Failed to load XImg", error); console.error('Failed to load XImg', error);
} }
}, },
load() { load() {
if (this.dataSrc === BLANK_IMG) return; if (this.dataSrc === BLANK_IMG) return;
this.$emit("load", this.dataSrc); this.$emit('load', this.dataSrc);
}, },
lockBlob() { lockBlob() {

View File

@ -1,6 +1,6 @@
import { API } from "../../services/API"; import { API } from '../../services/API';
import { workerImporter } from "../../worker"; import { workerImporter } from '../../worker';
import type * as w from "./XImgWorker"; import type * as w from './XImgWorker';
// Global web worker to fetch images // Global web worker to fetch images
let worker: Worker; let worker: Worker;
@ -12,21 +12,21 @@ const BLOB_STICKY = new Map<string, number>();
// Start and configure the worker // Start and configure the worker
function startWorker() { function startWorker() {
if (worker || globalThis.mode !== "user") return; if (worker || globalThis.mode !== 'user') return;
// Start worker // Start worker
worker = new Worker(new URL("./XImgWorkerStub.ts", import.meta.url)); worker = new Worker(new URL('./XImgWorkerStub.ts', import.meta.url));
importer = workerImporter(worker); importer = workerImporter(worker);
// Configure worker // Configure worker
importer<typeof w.configure>("configure")({ importer<typeof w.configure>('configure')({
multiUrl: API.IMAGE_MULTIPREVIEW(), multiUrl: API.IMAGE_MULTIPREVIEW(),
}); });
} }
// Configure worker on startup // Configure worker on startup
document.addEventListener("DOMContentLoaded", () => { document.addEventListener('DOMContentLoaded', () => {
if (globalThis.mode !== "user") return; if (globalThis.mode !== 'user') return;
// Periodic blob cache cleaner // Periodic blob cache cleaner
window.setInterval(() => { window.setInterval(() => {
@ -66,7 +66,7 @@ export async function fetchImage(url: string) {
if (entry) return entry[1]; if (entry) return entry[1];
// Fetch image // Fetch image
const blobUrl = await importer<typeof w.fetchImageSrc>("fetchImageSrc")(url); const blobUrl = await importer<typeof w.fetchImageSrc>('fetchImageSrc')(url);
// Check memcache entry again and revoke if it was added in the meantime // Check memcache entry again and revoke if it was added in the meantime
if ((entry = BLOB_CACHE.get(url))) { if ((entry = BLOB_CACHE.get(url))) {

View File

@ -1,5 +1,5 @@
import { CacheExpiration } from "workbox-expiration"; import { CacheExpiration } from 'workbox-expiration';
import { workerExport } from "../../worker"; import { workerExport } from '../../worker';
interface BlobCallback { interface BlobCallback {
resolve: (blob: Blob) => void; resolve: (blob: Blob) => void;
@ -20,13 +20,13 @@ let fetchPreviewQueue: FetchPreviewObject[] = [];
const pendingUrls = new Map<string, BlobCallback[]>(); const pendingUrls = new Map<string, BlobCallback[]>();
// Cache for preview images // Cache for preview images
const cacheName = "images"; const cacheName = 'images';
let imageCache: Cache; let imageCache: Cache;
self.caches self.caches
?.open(cacheName) ?.open(cacheName)
.then((c) => (imageCache = c)) .then((c) => (imageCache = c))
.catch((e) => { .catch((e) => {
console.warn("Failed to open cache in worker", e); console.warn('Failed to open cache in worker', e);
}); });
// Expiration for cache // Expiration for cache
@ -98,21 +98,20 @@ async function flushPreviewQueue() {
// Create aggregated request body // Create aggregated request body
const files = fetchPreviewQueueCopy.map((p) => ({ const files = fetchPreviewQueueCopy.map((p) => ({
fileid: p.fileid, fileid: p.fileid,
x: Number(p.url.searchParams.get("x")), x: Number(p.url.searchParams.get('x')),
y: Number(p.url.searchParams.get("y")), y: Number(p.url.searchParams.get('y')),
a: p.url.searchParams.get("a"), a: p.url.searchParams.get('a'),
reqid: p.reqid, reqid: p.reqid,
})); }));
try { try {
// Fetch multipreview // Fetch multipreview
const res = await fetchMultipreview(files); const res = await fetchMultipreview(files);
if (res.status !== 200 || !res.body) if (res.status !== 200 || !res.body) throw new Error('Error fetching multi-preview');
throw new Error("Error fetching multi-preview");
// Create fake headers for 7-day expiry // Create fake headers for 7-day expiry
const headers = { const headers = {
"cache-control": "max-age=604800", 'cache-control': 'max-age=604800',
expires: new Date(Date.now() + 604800000).toUTCString(), expires: new Date(Date.now() + 604800000).toUTCString(),
}; };
@ -151,7 +150,7 @@ async function flushPreviewQueue() {
const newBuffer = new Uint8Array(buffer.length * 2); const newBuffer = new Uint8Array(buffer.length * 2);
newBuffer.set(buffer); newBuffer.set(buffer);
buffer = newBuffer; buffer = newBuffer;
console.warn("Doubling multipreview buffer size", buffer.length); console.warn('Doubling multipreview buffer size', buffer.length);
} }
// Copy data into buffer // Copy data into buffer
@ -201,7 +200,7 @@ async function flushPreviewQueue() {
} }
} }
} catch (e) { } catch (e) {
console.error("Multipreview error", e); console.error('Multipreview error', e);
} }
// Initiate callbacks for failed requests // Initiate callbacks for failed requests
@ -216,7 +215,7 @@ async function fetchImage(url: string): Promise<Blob> {
// Get file id from URL // Get file id from URL
const urlObj = new URL(url, self.location.origin); const urlObj = new URL(url, self.location.origin);
const fileid = Number(urlObj.pathname.split("/").pop()); const fileid = Number(urlObj.pathname.split('/').pop());
// Just fetch if not a preview // Just fetch if not a preview
const regex = /^.*\/apps\/memories\/api\/image\/preview\/.*/; const regex = /^.*\/apps\/memories\/api\/image\/preview\/.*/;
@ -270,7 +269,7 @@ function cacheResponse(url: string, res: Response) {
expirationManager.expireEntries(); expirationManager.expireEntries();
} }
} catch (e) { } catch (e) {
console.error("Error caching response", e); console.error('Error caching response', e);
} }
} }
@ -279,9 +278,9 @@ function getResponse(blob: Blob, type: string | null, headers: any = {}) {
return new Response(blob, { return new Response(blob, {
status: 200, status: 200,
headers: { headers: {
"Content-Type": type || headers["content-type"], 'Content-Type': type || headers['content-type'],
"Content-Length": blob.size.toString(), 'Content-Length': blob.size.toString(),
"Cache-Control": headers["cache-control"], 'Cache-Control': headers['cache-control'],
Expires: headers.expires, Expires: headers.expires,
}, },
}); });
@ -295,10 +294,10 @@ async function fetchOneImage(url: string) {
/** Fetch multipreview with axios */ /** Fetch multipreview with axios */
async function fetchMultipreview(files: any[]) { async function fetchMultipreview(files: any[]) {
return await fetch(config.multiUrl, { return await fetch(config.multiUrl, {
method: "POST", method: 'POST',
body: JSON.stringify(files), body: JSON.stringify(files),
headers: { headers: {
"Content-Type": "application/json", 'Content-Type': 'application/json',
}, },
}); });
} }

View File

@ -7,13 +7,13 @@
*/ */
const pathname = self.location.pathname; const pathname = self.location.pathname;
__webpack_public_path__ = pathname.substring(0, pathname.lastIndexOf("/") + 1); __webpack_public_path__ = pathname.substring(0, pathname.lastIndexOf('/') + 1);
const missedQueue: any[] = []; const missedQueue: any[] = [];
self.onmessage = function (val: any) { self.onmessage = function (val: any) {
missedQueue.push(val); missedQueue.push(val);
}; };
import("./XImgWorker").then(function () { import('./XImgWorker').then(function () {
missedQueue.forEach((data: any) => self.onmessage?.(data)); missedQueue.forEach((data: any) => self.onmessage?.(data));
}); });

View File

@ -1,37 +1,33 @@
<template> <template>
<Modal @close="close" size="normal" v-if="show"> <Modal @close="close" size="normal" v-if="show">
<template #title> <template #title>
{{ t("memories", "Add to album") }} {{ t('memories', 'Add to album') }}
</template> </template>
<div class="outer"> <div class="outer">
<AlbumPicker @select="selectAlbum" /> <AlbumPicker @select="selectAlbum" />
<div v-if="processing"> <div v-if="processing">
<NcProgressBar <NcProgressBar :value="Math.round((photosDone * 100) / photos.length)" :error="true" />
:value="Math.round((photosDone * 100) / photos.length)"
:error="true"
/>
</div> </div>
</div> </div>
</Modal> </Modal>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from "vue"; import { defineComponent } from 'vue';
import * as dav from "../../services/DavRequests"; import * as dav from '../../services/DavRequests';
import { showInfo } from "@nextcloud/dialogs"; import { showInfo } from '@nextcloud/dialogs';
import { IAlbum, IPhoto } from "../../types"; import { IAlbum, IPhoto } from '../../types';
const NcProgressBar = () => const NcProgressBar = () => import('@nextcloud/vue/dist/Components/NcProgressBar');
import("@nextcloud/vue/dist/Components/NcProgressBar");
import Modal from "./Modal.vue"; import Modal from './Modal.vue';
import AlbumPicker from "./AlbumPicker.vue"; import AlbumPicker from './AlbumPicker.vue';
export default defineComponent({ export default defineComponent({
name: "AddToAlbumModal", name: 'AddToAlbumModal',
components: { components: {
NcProgressBar, NcProgressBar,
Modal, Modal,
@ -54,14 +50,14 @@ export default defineComponent({
}, },
added(photos: IPhoto[]) { added(photos: IPhoto[]) {
this.$emit("added", photos); this.$emit('added', photos);
}, },
close() { close() {
this.photos = []; this.photos = [];
this.processing = false; this.processing = false;
this.show = false; this.show = false;
this.$emit("close"); this.$emit('close');
}, },
async selectAlbum(album: IAlbum) { async selectAlbum(album: IAlbum) {
@ -77,15 +73,7 @@ export default defineComponent({
} }
const n = this.photosDone; const n = this.photosDone;
showInfo( showInfo(this.n('memories', '{n} item added to album', '{n} items added to album', n, { n }));
this.n(
"memories",
"{n} item added to album",
"{n} items added to album",
n,
{ n }
)
);
this.close(); this.close();
}, },
}, },

View File

@ -1,7 +1,7 @@
<template> <template>
<div class="manage-collaborators"> <div class="manage-collaborators">
<div class="manage-collaborators__subtitle"> <div class="manage-collaborators__subtitle">
{{ t("photos", "Add people or groups who can edit your album") }} {{ t('photos', 'Add people or groups who can edit your album') }}
</div> </div>
<form class="manage-collaborators__form" @submit.prevent> <form class="manage-collaborators__form" @submit.prevent>
@ -37,14 +37,9 @@
:user="availableCollaborators[collaboratorKey].id" :user="availableCollaborators[collaboratorKey].id"
:display-name="availableCollaborators[collaboratorKey].label" :display-name="availableCollaborators[collaboratorKey].label"
:aria-label=" :aria-label="
t( t('photos', 'Add {collaboratorLabel} to the collaborators list', {
'photos', collaboratorLabel: availableCollaborators[collaboratorKey].label,
'Add {collaboratorLabel} to the collaborators list', })
{
collaboratorLabel:
availableCollaborators[collaboratorKey].label,
}
)
" "
@click="selectEntity(collaboratorKey)" @click="selectEntity(collaboratorKey)"
/> />
@ -76,14 +71,9 @@
<NcButton <NcButton
type="tertiary" type="tertiary"
:aria-label=" :aria-label="
t( t('photos', 'Remove {collaboratorLabel} from the collaborators list', {
'photos', collaboratorLabel: availableCollaborators[collaboratorKey].label,
'Remove {collaboratorLabel} from the collaborators list', })
{
collaboratorLabel:
availableCollaborators[collaboratorKey].label,
}
)
" "
@click="unselectEntity(collaboratorKey)" @click="unselectEntity(collaboratorKey)"
> >
@ -103,10 +93,10 @@
@click="copyPublicLink" @click="copyPublicLink"
> >
<template v-if="publicLinkCopied"> <template v-if="publicLinkCopied">
{{ t("photos", "Public link copied!") }} {{ t('photos', 'Public link copied!') }}
</template> </template>
<template v-else> <template v-else>
{{ t("photos", "Copy public link") }} {{ t('photos', 'Copy public link') }}
</template> </template>
<template #icon> <template #icon>
<Check v-if="publicLinkCopied" /> <Check v-if="publicLinkCopied" />
@ -123,13 +113,9 @@
<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>
</div> </div>
@ -141,30 +127,29 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, PropType } from "vue"; import { defineComponent, PropType } from 'vue';
import Magnify from "vue-material-design-icons/Magnify.vue"; import Magnify from 'vue-material-design-icons/Magnify.vue';
import Close from "vue-material-design-icons/Close.vue"; import Close from 'vue-material-design-icons/Close.vue';
import Check from "vue-material-design-icons/Check.vue"; import Check from 'vue-material-design-icons/Check.vue';
import ContentCopy from "vue-material-design-icons/ContentCopy.vue"; import ContentCopy from 'vue-material-design-icons/ContentCopy.vue';
import AccountGroup from "vue-material-design-icons/AccountGroup.vue"; import AccountGroup from 'vue-material-design-icons/AccountGroup.vue';
import Earth from "vue-material-design-icons/Earth.vue"; import Earth from 'vue-material-design-icons/Earth.vue';
import axios from "@nextcloud/axios"; import axios from '@nextcloud/axios';
import * as dav from "../../services/DavRequests"; import * as dav from '../../services/DavRequests';
import { showError } from "@nextcloud/dialogs"; import { showError } from '@nextcloud/dialogs';
import { getCurrentUser } from "@nextcloud/auth"; import { getCurrentUser } from '@nextcloud/auth';
import { generateOcsUrl, generateUrl } from "@nextcloud/router"; import { generateOcsUrl, generateUrl } from '@nextcloud/router';
import NcButton from "@nextcloud/vue/dist/Components/NcButton"; import NcButton from '@nextcloud/vue/dist/Components/NcButton';
import NcLoadingIcon from "@nextcloud/vue/dist/Components/NcLoadingIcon"; import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon';
import NcPopover from "@nextcloud/vue/dist/Components/NcPopover"; import NcPopover from '@nextcloud/vue/dist/Components/NcPopover';
import NcEmptyContent from "@nextcloud/vue/dist/Components/NcEmptyContent"; import NcEmptyContent from '@nextcloud/vue/dist/Components/NcEmptyContent';
const NcTextField = () => import("@nextcloud/vue/dist/Components/NcTextField"); const NcTextField = () => import('@nextcloud/vue/dist/Components/NcTextField');
const NcListItemIcon = () => const NcListItemIcon = () => import('@nextcloud/vue/dist/Components/NcListItemIcon');
import("@nextcloud/vue/dist/Components/NcListItemIcon");
import { Type } from "@nextcloud/sharing"; import { Type } from '@nextcloud/sharing';
type Collaborator = { type Collaborator = {
id: string; id: string;
@ -173,7 +158,7 @@ type Collaborator = {
}; };
export default defineComponent({ export default defineComponent({
name: "AddToAlbumModal", name: 'AddToAlbumModal',
components: { components: {
Magnify, Magnify,
Close, Close,
@ -205,7 +190,7 @@ export default defineComponent({
}, },
data: () => ({ data: () => ({
searchText: "", searchText: '',
availableCollaborators: {} as { [key: string]: Collaborator }, availableCollaborators: {} as { [key: string]: Collaborator },
selectedCollaboratorsKeys: [] as string[], selectedCollaboratorsKeys: [] as string[],
currentSearchResults: [] as Collaborator[], currentSearchResults: [] as Collaborator[],
@ -216,8 +201,7 @@ export default defineComponent({
randomId: Math.random().toString().substring(2, 10), randomId: Math.random().toString().substring(2, 10),
publicLinkCopied: false, publicLinkCopied: false,
config: { config: {
minSearchStringLength: minSearchStringLength: parseInt(window.OC.config['sharing.minSearchStringLength'], 10) || 0,
parseInt(window.OC.config["sharing.minSearchStringLength"], 10) || 0,
}, },
}), }),
@ -226,24 +210,17 @@ export default defineComponent({
return this.currentSearchResults return this.currentSearchResults
.filter(({ id }) => id !== getCurrentUser()?.uid) .filter(({ id }) => id !== getCurrentUser()?.uid)
.map(({ type, id }) => `${type}:${id}`) .map(({ type, id }) => `${type}:${id}`)
.filter( .filter((collaboratorKey) => !this.selectedCollaboratorsKeys.includes(collaboratorKey));
(collaboratorKey) =>
!this.selectedCollaboratorsKeys.includes(collaboratorKey)
);
}, },
listableSelectedCollaboratorsKeys(): string[] { listableSelectedCollaboratorsKeys(): string[] {
return this.selectedCollaboratorsKeys.filter( return this.selectedCollaboratorsKeys.filter(
(collaboratorKey) => (collaboratorKey) => this.availableCollaborators[collaboratorKey].type !== Type.SHARE_TYPE_LINK
this.availableCollaborators[collaboratorKey].type !==
Type.SHARE_TYPE_LINK
); );
}, },
selectedCollaborators(): Collaborator[] { selectedCollaborators(): Collaborator[] {
return this.selectedCollaboratorsKeys.map( return this.selectedCollaboratorsKeys.map((collaboratorKey) => this.availableCollaborators[collaboratorKey]);
(collaboratorKey) => this.availableCollaborators[collaboratorKey]
);
}, },
isPublicLinkSelected(): boolean { isPublicLinkSelected(): boolean {
@ -280,39 +257,32 @@ export default defineComponent({
} }
this.loadingCollaborators = true; this.loadingCollaborators = true;
const response = await axios.get( const response = await axios.get(generateOcsUrl('core/autocomplete/get'), {
generateOcsUrl("core/autocomplete/get"),
{
params: { params: {
search: this.searchText, search: this.searchText,
itemType: "share-recipients", itemType: 'share-recipients',
shareTypes: [Type.SHARE_TYPE_USER, Type.SHARE_TYPE_GROUP], shareTypes: [Type.SHARE_TYPE_USER, Type.SHARE_TYPE_GROUP],
}, },
} });
);
this.currentSearchResults = response.data.ocs.data.map( this.currentSearchResults = response.data.ocs.data.map((collaborator) => {
(collaborator) => {
switch (collaborator.source) { switch (collaborator.source) {
case "users": case 'users':
return { return {
id: collaborator.id, id: collaborator.id,
label: collaborator.label, label: collaborator.label,
type: Type.SHARE_TYPE_USER, type: Type.SHARE_TYPE_USER,
}; };
case "groups": case 'groups':
return { return {
id: collaborator.id, id: collaborator.id,
label: collaborator.label, label: collaborator.label,
type: Type.SHARE_TYPE_GROUP, type: Type.SHARE_TYPE_GROUP,
}; };
default: default:
throw new Error( throw new Error(`Invalid collaborator source ${collaborator.source}`);
`Invalid collaborator source ${collaborator.source}`
);
} }
} });
);
this.availableCollaborators = { this.availableCollaborators = {
...this.availableCollaborators, ...this.availableCollaborators,
@ -320,7 +290,7 @@ export default defineComponent({
}; };
} catch (error) { } catch (error) {
this.errorFetchingCollaborators = error; this.errorFetchingCollaborators = error;
showError(this.t("photos", "Failed to fetch collaborators list.")); showError(this.t('photos', 'Failed to fetch collaborators list.'));
} finally { } finally {
this.loadingCollaborators = false; this.loadingCollaborators = false;
} }
@ -330,15 +300,12 @@ export default defineComponent({
* Populate selectedCollaboratorsKeys and availableCollaborators. * Populate selectedCollaboratorsKeys and availableCollaborators.
*/ */
populateCollaborators(collaborators: Collaborator[]) { populateCollaborators(collaborators: Collaborator[]) {
const initialCollaborators = collaborators.reduce( const initialCollaborators = collaborators.reduce(this.indexCollaborators, {});
this.indexCollaborators,
{}
);
this.selectedCollaboratorsKeys = Object.keys(initialCollaborators); this.selectedCollaboratorsKeys = Object.keys(initialCollaborators);
this.availableCollaborators = { this.availableCollaborators = {
3: { 3: {
id: "", id: '',
label: this.t("photos", "Public link"), label: this.t('photos', 'Public link'),
type: Type.SHARE_TYPE_LINK, type: Type.SHARE_TYPE_LINK,
}, },
...this.availableCollaborators, ...this.availableCollaborators,
@ -350,16 +317,12 @@ export default defineComponent({
* @param {Object<string, Collaborator>} collaborators - Index of collaborators * @param {Object<string, Collaborator>} collaborators - Index of collaborators
* @param {Collaborator} collaborator - A collaborator * @param {Collaborator} collaborator - A collaborator
*/ */
indexCollaborators( indexCollaborators(collaborators: { [s: string]: Collaborator }, collaborator: Collaborator) {
collaborators: { [s: string]: 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,
}; };
}, },
@ -381,7 +344,7 @@ export default defineComponent({
this.errorFetchingAlbum = error; this.errorFetchingAlbum = error;
} }
showError(this.t("photos", "Failed to fetch album.")); showError(this.t('photos', 'Failed to fetch album.'));
} finally { } finally {
this.loadingAlbum = false; this.loadingAlbum = false;
} }
@ -390,8 +353,8 @@ export default defineComponent({
async deletePublicLink() { async deletePublicLink() {
this.unselectEntity(`${Type.SHARE_TYPE_LINK}`); this.unselectEntity(`${Type.SHARE_TYPE_LINK}`);
this.availableCollaborators[3] = { this.availableCollaborators[3] = {
id: "", id: '',
label: this.t("photos", "Public link"), label: this.t('photos', 'Public link'),
type: Type.SHARE_TYPE_LINK, type: Type.SHARE_TYPE_LINK,
}; };
this.publicLinkCopied = false; this.publicLinkCopied = false;
@ -410,7 +373,7 @@ export default defineComponent({
}, },
}); });
} catch (error) { } catch (error) {
showError(this.t("photos", "Failed to update album.")); showError(this.t('photos', 'Failed to update album.'));
} finally { } finally {
this.loadingAlbum = false; this.loadingAlbum = false;
} }
@ -418,9 +381,7 @@ export default defineComponent({
async copyPublicLink() { async copyPublicLink() {
await navigator.clipboard.writeText( await navigator.clipboard.writeText(
`${window.location.protocol}//${window.location.host}${generateUrl( `${window.location.protocol}//${window.location.host}${generateUrl(`apps/memories/a/${this.publicLink.id}`)}`
`apps/memories/a/${this.publicLink.id}`
)}`
); );
this.publicLinkCopied = true; this.publicLinkCopied = true;
setTimeout(() => { setTimeout(() => {

View File

@ -2,35 +2,30 @@
<Modal @close="close" size="normal" v-if="show"> <Modal @close="close" size="normal" v-if="show">
<template #title> <template #title>
<template v-if="!album"> <template v-if="!album">
{{ t("memories", "Create new album") }} {{ t('memories', 'Create new album') }}
</template> </template>
<template v-else> <template v-else>
{{ t("memories", "Edit album details") }} {{ t('memories', 'Edit album details') }}
</template> </template>
</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>
<script lang="ts"> <script lang="ts">
import { defineComponent } from "vue"; import { defineComponent } from 'vue';
import { showError } from "@nextcloud/dialogs"; import { showError } from '@nextcloud/dialogs';
import * as dav from "../../services/DavRequests"; import * as dav from '../../services/DavRequests';
import Modal from "./Modal.vue"; import Modal from './Modal.vue';
import AlbumForm from "./AlbumForm.vue"; import AlbumForm from './AlbumForm.vue';
export default defineComponent({ export default defineComponent({
name: "AlbumCreateModal", name: 'AlbumCreateModal',
components: { components: {
Modal, Modal,
AlbumForm, AlbumForm,
@ -49,13 +44,10 @@ export default defineComponent({
async open(edit: boolean) { async open(edit: boolean) {
if (edit) { if (edit) {
try { try {
this.album = await dav.getAlbum( this.album = await dav.getAlbum(<string>this.$route.params.user, <string>this.$route.params.name);
<string>this.$route.params.user,
<string>this.$route.params.name
);
} catch (e) { } catch (e) {
console.error(e); console.error(e);
showError(this.t("photos", "Could not load the selected album")); showError(this.t('photos', 'Could not load the selected album'));
return; return;
} }
} else { } else {
@ -67,14 +59,14 @@ export default defineComponent({
close() { close() {
this.show = false; this.show = false;
this.$emit("close"); this.$emit('close');
}, },
done({ album }: any) { done({ album }: any) {
if (!this.album || album.basename !== this.album.basename) { if (!this.album || album.basename !== this.album.basename) {
const user = album.filename.split("/")[2]; const user = album.filename.split('/')[2];
const name = album.basename; const name = album.basename;
this.$router.push({ name: "albums", params: { user, name } }); this.$router.push({ name: 'albums', params: { user, name } });
} }
this.close(); this.close();
}, },

View File

@ -1,39 +1,33 @@
<template> <template>
<Modal @close="close" v-if="show"> <Modal @close="close" v-if="show">
<template #title> <template #title>
{{ t("memories", "Remove Album") }} {{ t('memories', 'Remove Album') }}
</template> </template>
<span> <span>
{{ {{ t('memories', 'Are you sure you want to permanently remove album "{name}"?', { name }) }}
t(
"memories",
'Are you sure you want to permanently remove album "{name}"?',
{ name }
)
}}
</span> </span>
<template #buttons> <template #buttons>
<NcButton @click="save" class="button" type="error"> <NcButton @click="save" class="button" type="error">
{{ t("memories", "Delete") }} {{ t('memories', 'Delete') }}
</NcButton> </NcButton>
</template> </template>
</Modal> </Modal>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from "vue"; import { defineComponent } from 'vue';
import NcButton from "@nextcloud/vue/dist/Components/NcButton"; import NcButton from '@nextcloud/vue/dist/Components/NcButton';
const NcTextField = () => import("@nextcloud/vue/dist/Components/NcTextField"); const NcTextField = () => import('@nextcloud/vue/dist/Components/NcTextField');
import { showError } from "@nextcloud/dialogs"; import { showError } from '@nextcloud/dialogs';
import { getCurrentUser } from "@nextcloud/auth"; import { getCurrentUser } from '@nextcloud/auth';
import Modal from "./Modal.vue"; import Modal from './Modal.vue';
import client from "../../services/DavClient"; import client from '../../services/DavClient';
export default defineComponent({ export default defineComponent({
name: "AlbumDeleteModal", name: 'AlbumDeleteModal',
components: { components: {
NcButton, NcButton,
NcTextField, NcTextField,
@ -42,8 +36,8 @@ export default defineComponent({
data: () => ({ data: () => ({
show: false, show: false,
user: "", user: '',
name: "", name: '',
}), }),
watch: { watch: {
@ -59,14 +53,14 @@ export default defineComponent({
methods: { methods: {
close() { close() {
this.show = false; this.show = false;
this.$emit("close"); this.$emit('close');
}, },
open() { open() {
const user = this.$route.params.user || ""; const user = this.$route.params.user || '';
if (this.$route.params.user !== getCurrentUser()?.uid) { if (this.$route.params.user !== getCurrentUser()?.uid) {
showError( showError(
this.t("memories", 'Only user "{user}" can delete this album', { this.t('memories', 'Only user "{user}" can delete this album', {
user, user,
}) })
); );
@ -76,19 +70,19 @@ export default defineComponent({
}, },
refreshParams() { refreshParams() {
this.user = <string>this.$route.params.user || ""; this.user = <string>this.$route.params.user || '';
this.name = <string>this.$route.params.name || ""; this.name = <string>this.$route.params.name || '';
}, },
async save() { async save() {
try { try {
await client.deleteFile(`/photos/${this.user}/albums/${this.name}`); await client.deleteFile(`/photos/${this.user}/albums/${this.name}`);
this.$router.push({ name: "albums" }); this.$router.push({ name: 'albums' });
this.close(); this.close();
} catch (error) { } catch (error) {
console.log(error); console.log(error);
showError( showError(
this.t("photos", "Failed to delete {name}.", { this.t('photos', 'Failed to delete {name}.', {
name: this.name, name: this.name,
}) })
); );

View File

@ -1,9 +1,5 @@
<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" ref="nameInput"
@ -31,7 +27,7 @@
type="tertiary" type="tertiary"
@click="back" @click="back"
> >
{{ t("photos", "Back") }} {{ t('photos', 'Back') }}
</NcButton> </NcButton>
</span> </span>
<span class="right-buttons"> <span class="right-buttons">
@ -45,14 +41,9 @@
<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 />
@ -75,7 +66,7 @@
type="tertiary" type="tertiary"
@click="showCollaboratorView = false" @click="showCollaboratorView = false"
> >
{{ t("photos", "Back") }} {{ t('photos', 'Back') }}
</NcButton> </NcButton>
</span> </span>
<span class="right-buttons"> <span class="right-buttons">
@ -96,24 +87,24 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, PropType } from "vue"; import { defineComponent, PropType } from 'vue';
import { getCurrentUser } from "@nextcloud/auth"; import { getCurrentUser } from '@nextcloud/auth';
import { showError } from "@nextcloud/dialogs"; import { showError } from '@nextcloud/dialogs';
import NcButton from "@nextcloud/vue/dist/Components/NcButton"; import NcButton from '@nextcloud/vue/dist/Components/NcButton';
import NcLoadingIcon from "@nextcloud/vue/dist/Components/NcLoadingIcon"; import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon';
const NcTextField = () => import("@nextcloud/vue/dist/Components/NcTextField"); const NcTextField = () => import('@nextcloud/vue/dist/Components/NcTextField');
import moment from "moment"; import moment from 'moment';
import * as dav from "../../services/DavRequests"; import * as dav from '../../services/DavRequests';
import AlbumCollaborators from "./AlbumCollaborators.vue"; import AlbumCollaborators from './AlbumCollaborators.vue';
import Send from "vue-material-design-icons/Send.vue"; import Send from 'vue-material-design-icons/Send.vue';
import AccountMultiplePlus from "vue-material-design-icons/AccountMultiplePlus.vue"; import AccountMultiplePlus from 'vue-material-design-icons/AccountMultiplePlus.vue';
export default defineComponent({ export default defineComponent({
name: "AlbumForm", name: 'AlbumForm',
components: { components: {
NcButton, NcButton,
NcLoadingIcon, NcLoadingIcon,
@ -138,8 +129,8 @@ export default defineComponent({
data: () => ({ data: () => ({
collaborators: [], collaborators: [],
showCollaboratorView: false, showCollaboratorView: false,
albumName: "", albumName: '',
albumLocation: "", albumLocation: '',
loading: false, loading: false,
}), }),
@ -152,9 +143,7 @@ export default defineComponent({
}, },
saveText(): string { saveText(): string {
return this.editMode return this.editMode ? this.t('photos', 'Save') : this.t('photos', 'Create album');
? this.t("photos", "Save")
: this.t("photos", "Create album");
}, },
/** /**
@ -171,24 +160,19 @@ export default defineComponent({
this.albumLocation = this.album.location; this.albumLocation = this.album.location;
} }
this.$nextTick(() => { this.$nextTick(() => {
(<any>this.$refs.nameInput)?.$el.getElementsByTagName("input")[0].focus(); (<any>this.$refs.nameInput)?.$el.getElementsByTagName('input')[0].focus();
}); });
}, },
methods: { methods: {
submit(collaborators: any = []) { submit(collaborators: any = []) {
if (this.albumName === "" || this.loading) { if (this.albumName === '' || this.loading) {
return; return;
} }
// Validate the album name, it shouldn't contain any slash // Validate the album name, it shouldn't contain any slash
if (this.albumName.includes("/")) { if (this.albumName.includes('/')) {
showError( showError(this.t('memories', 'Invalid album name; should not contain any slashes.'));
this.t(
"memories",
"Invalid album name; should not contain any slashes."
)
);
return; return;
} }
@ -208,12 +192,12 @@ export default defineComponent({
nbItems: 0, nbItems: 0,
location: this.albumLocation, location: this.albumLocation,
lastPhoto: -1, lastPhoto: -1,
date: moment().format("MMMM YYYY"), date: moment().format('MMMM YYYY'),
collaborators, collaborators,
}; };
await dav.createAlbum(album.basename); await dav.createAlbum(album.basename);
if (this.albumLocation !== "" || collaborators.length !== 0) { if (this.albumLocation !== '' || collaborators.length !== 0) {
album = await dav.updateAlbum(album, { album = await dav.updateAlbum(album, {
albumName: this.albumName, albumName: this.albumName,
properties: { properties: {
@ -223,7 +207,7 @@ export default defineComponent({
}); });
} }
this.$emit("done", { album }); this.$emit('done', { album });
} finally { } finally {
this.loading = false; this.loading = false;
} }
@ -245,14 +229,14 @@ export default defineComponent({
properties: { location: this.albumLocation }, properties: { location: this.albumLocation },
}); });
} }
this.$emit("done", { album }); this.$emit('done', { album });
} finally { } finally {
this.loading = false; this.loading = false;
} }
}, },
back() { back() {
this.$emit("back"); this.$emit('back');
}, },
}, },
}); });

View File

@ -16,18 +16,14 @@
@click="pickAlbum(album)" @click="pickAlbum(album)"
> >
<template v-slot:icon="{}"> <template v-slot:icon="{}">
<XImg <XImg 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="toCoverUrl(album.last_added_photo)"
/>
<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>
</template> </template>
<template v-slot:subtitle="{}"> <template v-slot:subtitle="{}">
{{ n("photos", "%n item", "%n items", album.count) }} {{ n('photos', '%n item', '%n items', album.count) }}
<!-- TODO: finish collaboration --> <!-- TODO: finish collaboration -->
<!-- {{ n('photos', 'Share with %n user', 'Share with %n users', album.isShared) }}--> <!-- {{ n('photos', 'Share with %n user', 'Share with %n users', album.isShared) }}-->
</template> </template>
@ -43,7 +39,7 @@
<template #icon> <template #icon>
<Plus /> <Plus />
</template> </template>
{{ t("photos", "Create new album") }} {{ t('photos', 'Create new album') }}
</NcButton> </NcButton>
</div> </div>
@ -57,25 +53,25 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from "vue"; import { defineComponent } from 'vue';
import { getCurrentUser } from "@nextcloud/auth"; import { getCurrentUser } from '@nextcloud/auth';
import AlbumForm from "./AlbumForm.vue"; import AlbumForm from './AlbumForm.vue';
import Plus from "vue-material-design-icons/Plus.vue"; import Plus from 'vue-material-design-icons/Plus.vue';
import ImageMultiple from "vue-material-design-icons/ImageMultiple.vue"; import ImageMultiple from 'vue-material-design-icons/ImageMultiple.vue';
import NcButton from "@nextcloud/vue/dist/Components/NcButton"; import NcButton from '@nextcloud/vue/dist/Components/NcButton';
import NcLoadingIcon from "@nextcloud/vue/dist/Components/NcLoadingIcon"; import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon';
const NcListItem = () => import("@nextcloud/vue/dist/Components/NcListItem"); const NcListItem = () => import('@nextcloud/vue/dist/Components/NcListItem');
import { getPreviewUrl } from "../../services/utils/helpers"; import { getPreviewUrl } from '../../services/utils/helpers';
import { IAlbum, IPhoto } from "../../types"; import { IAlbum, IPhoto } from '../../types';
import axios from "@nextcloud/axios"; import axios from '@nextcloud/axios';
import { API } from "../../services/API"; import { API } from '../../services/API';
export default defineComponent({ export default defineComponent({
name: "AlbumPicker", name: 'AlbumPicker',
components: { components: {
AlbumForm, AlbumForm,
Plus, Plus,
@ -130,7 +126,7 @@ export default defineComponent({
}, },
pickAlbum(album: IAlbum) { pickAlbum(album: IAlbum) {
this.$emit("select", album); this.$emit('select', album);
}, },
}, },
}); });

View File

@ -1,7 +1,7 @@
<template> <template>
<Modal @close="close" v-if="show"> <Modal @close="close" v-if="show">
<template #title> <template #title>
{{ t("memories", "Share Album") }} {{ t('memories', 'Share Album') }}
</template> </template>
<AlbumCollaborators <AlbumCollaborators
@ -21,25 +21,25 @@
<template #icon> <template #icon>
<NcLoadingIcon v-if="loadingAddCollaborators" /> <NcLoadingIcon v-if="loadingAddCollaborators" />
</template> </template>
{{ t("photos", "Save") }} {{ t('photos', 'Save') }}
</NcButton> </NcButton>
</AlbumCollaborators> </AlbumCollaborators>
</Modal> </Modal>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from "vue"; import { defineComponent } from 'vue';
import NcButton from "@nextcloud/vue/dist/Components/NcButton"; import NcButton from '@nextcloud/vue/dist/Components/NcButton';
import NcLoadingIcon from "@nextcloud/vue/dist/Components/NcLoadingIcon"; import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon';
import * as dav from "../../services/DavRequests"; import * as dav from '../../services/DavRequests';
import Modal from "./Modal.vue"; import Modal from './Modal.vue';
import AlbumCollaborators from "./AlbumCollaborators.vue"; import AlbumCollaborators from './AlbumCollaborators.vue';
export default defineComponent({ export default defineComponent({
name: "AlbumShareModal", name: 'AlbumShareModal',
components: { components: {
NcButton, NcButton,
NcLoadingIcon, NcLoadingIcon,
@ -58,14 +58,14 @@ export default defineComponent({
close() { close() {
this.show = false; this.show = false;
this.album = null; this.album = null;
this.$emit("close"); this.$emit('close');
}, },
async open() { async open() {
this.show = true; this.show = true;
this.loadingAddCollaborators = true; this.loadingAddCollaborators = true;
const user = <string>this.$route.params.user || ""; const user = <string>this.$route.params.user || '';
const name = <string>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

@ -1,9 +1,9 @@
<template> <template>
<div> <div>
<div class="title-text"> <div class="title-text">
<span v-if="photos.length > 1"> [{{ t("memories", "Newest") }}] </span> <span v-if="photos.length > 1"> [{{ t('memories', 'Newest') }}] </span>
{{ longDateStr }} {{ longDateStr }}
{{ newestDirty ? "*" : "" }} {{ newestDirty ? '*' : '' }}
</div> </div>
<div class="fields"> <div class="fields">
@ -50,9 +50,9 @@
<div v-if="photos.length > 1" class="oldest"> <div v-if="photos.length > 1" class="oldest">
<div class="title-text"> <div class="title-text">
<span> [{{ t("memories", "Oldest") }}] </span> <span> [{{ t('memories', 'Oldest') }}] </span>
{{ longDateStrLast }} {{ longDateStrLast }}
{{ oldestDirty ? "*" : "" }} {{ oldestDirty ? '*' : '' }}
</div> </div>
<div class="fields"> <div class="fields">
@ -101,15 +101,15 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from "vue"; import { defineComponent } from 'vue';
import { IPhoto } from "../../types"; import { IPhoto } from '../../types';
const NcTextField = () => import("@nextcloud/vue/dist/Components/NcTextField"); const NcTextField = () => import('@nextcloud/vue/dist/Components/NcTextField');
import * as utils from "../../services/Utils"; import * as utils from '../../services/Utils';
export default defineComponent({ export default defineComponent({
name: "EditDate", name: 'EditDate',
components: { components: {
NcTextField, NcTextField,
}, },
@ -124,19 +124,19 @@ export default defineComponent({
data: () => ({ data: () => ({
sortedPhotos: [] as IPhoto[], sortedPhotos: [] as IPhoto[],
year: "0", year: '0',
month: "0", month: '0',
day: "0", day: '0',
hour: "0", hour: '0',
minute: "0", minute: '0',
second: "0", second: '0',
yearLast: "0", yearLast: '0',
monthLast: "0", monthLast: '0',
dayLast: "0", dayLast: '0',
hourLast: "0", hourLast: '0',
minuteLast: "0", minuteLast: '0',
secondLast: "0", secondLast: '0',
newestDirty: false, newestDirty: false,
oldestDirty: false, oldestDirty: false,
@ -154,14 +154,7 @@ export default defineComponent({
computed: { computed: {
date() { date() {
return this.makeDate( return this.makeDate(this.year, this.month, this.day, this.hour, this.minute, this.second);
this.year,
this.month,
this.day,
this.hour,
this.minute,
this.second
);
}, },
dateLast() { dateLast() {
@ -176,9 +169,7 @@ export default defineComponent({
}, },
dateDiff() { dateDiff() {
return this.date && this.dateLast return this.date && this.dateLast ? this.date.getTime() - this.dateLast.getTime() : 0;
? this.date.getTime() - this.dateLast.getTime()
: 0;
}, },
origDateNewest() { origDateNewest() {
@ -186,9 +177,7 @@ export default defineComponent({
}, },
origDateOldest() { origDateOldest() {
return new Date( return new Date(this.sortedPhotos[this.sortedPhotos.length - 1].datetaken!);
this.sortedPhotos[this.sortedPhotos.length - 1].datetaken!
);
}, },
origDateDiff() { origDateDiff() {
@ -200,24 +189,18 @@ export default defineComponent({
}, },
longDateStr() { longDateStr() {
return this.date return this.date ? utils.getLongDateStr(this.date, false, true) : this.t('memories', 'Invalid Date');
? utils.getLongDateStr(this.date, false, true)
: this.t("memories", "Invalid Date");
}, },
longDateStrLast() { longDateStrLast() {
return this.dateLast return this.dateLast ? utils.getLongDateStr(this.dateLast, false, true) : this.t('memories', 'Invalid Date');
? utils.getLongDateStr(this.dateLast, false, true)
: this.t("memories", "Invalid Date");
}, },
}, },
methods: { methods: {
init() { init() {
// Filter out only photos that have a datetaken // Filter out only photos that have a datetaken
const photos = (this.sortedPhotos = this.photos.filter( const photos = (this.sortedPhotos = this.photos.filter((photo) => photo.datetaken !== undefined));
(photo) => photo.datetaken !== undefined
));
// Sort photos by datetaken descending // Sort photos by datetaken descending
photos.sort((a, b) => b.datetaken! - a.datetaken!); photos.sort((a, b) => b.datetaken! - a.datetaken!);
@ -245,19 +228,17 @@ export default defineComponent({
validate() { validate() {
if (!this.date) { if (!this.date) {
throw new Error(this.t("memories", "Invalid Date")); throw new Error(this.t('memories', 'Invalid Date'));
} }
if (this.photos.length > 1) { if (this.photos.length > 1) {
if (!this.dateLast) { if (!this.dateLast) {
throw new Error(this.t("memories", "Invalid Date")); throw new Error(this.t('memories', 'Invalid Date'));
} }
if (this.dateDiff < -60000) { if (this.dateDiff < -60000) {
// 1 minute // 1 minute
throw new Error( throw new Error(this.t('memories', 'Newest date is older than oldest date'));
this.t("memories", "Newest date is older than oldest date")
);
} }
} }
}, },
@ -312,23 +293,16 @@ export default defineComponent({
}, },
getExifFormat(date: Date) { getExifFormat(date: Date) {
const year = date.getUTCFullYear().toString().padStart(4, "0"); const year = date.getUTCFullYear().toString().padStart(4, '0');
const month = (date.getUTCMonth() + 1).toString().padStart(2, "0"); const month = (date.getUTCMonth() + 1).toString().padStart(2, '0');
const day = date.getUTCDate().toString().padStart(2, "0"); const day = date.getUTCDate().toString().padStart(2, '0');
const hour = date.getUTCHours().toString().padStart(2, "0"); const hour = date.getUTCHours().toString().padStart(2, '0');
const minute = date.getUTCMinutes().toString().padStart(2, "0"); const minute = date.getUTCMinutes().toString().padStart(2, '0');
const second = date.getUTCSeconds().toString().padStart(2, "0"); const second = date.getUTCSeconds().toString().padStart(2, '0');
return `${year}:${month}:${day} ${hour}:${minute}:${second}`; return `${year}:${month}:${day} ${hour}:${minute}:${second}`;
}, },
makeDate( makeDate(yearS: string, monthS: string, dayS: string, hourS: string, minuteS: string, secondS: string) {
yearS: string,
monthS: string,
dayS: string,
hourS: string,
minuteS: string,
secondS: string
) {
const date = new Date(); const date = new Date();
const year = parseInt(yearS, 10); const year = parseInt(yearS, 10);
const month = parseInt(monthS, 10) - 1; const month = parseInt(monthS, 10) - 1;

View File

@ -20,12 +20,12 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from "vue"; import { defineComponent } from 'vue';
import { IPhoto } from "../../types"; import { IPhoto } from '../../types';
const NcTextField = () => import("@nextcloud/vue/dist/Components/NcTextField"); const NcTextField = () => import('@nextcloud/vue/dist/Components/NcTextField');
import { translate as t } from "@nextcloud/l10n"; import { translate as t } from '@nextcloud/l10n';
export default defineComponent({ export default defineComponent({
components: { components: {
@ -45,32 +45,32 @@ export default defineComponent({
fields: [ fields: [
{ {
field: "Title", field: 'Title',
label: t("memories", "Title"), label: t('memories', 'Title'),
}, },
{ {
field: "Description", field: 'Description',
label: t("memories", "Description"), label: t('memories', 'Description'),
}, },
{ {
field: "Label", field: 'Label',
label: t("memories", "Label"), label: t('memories', 'Label'),
}, },
{ {
field: "Make", field: 'Make',
label: t("memories", "Camera Make"), label: t('memories', 'Camera Make'),
}, },
{ {
field: "Model", field: 'Model',
label: t("memories", "Camera Model"), label: t('memories', 'Camera Model'),
}, },
{ {
field: "LensModel", field: 'LensModel',
label: t("memories", "Lens Model"), label: t('memories', 'Lens Model'),
}, },
{ {
field: "Copyright", field: 'Copyright',
label: t("memories", "Copyright"), label: t('memories', 'Copyright'),
}, },
], ],
}), }),
@ -94,7 +94,7 @@ export default defineComponent({
if (ePhoto && (eCurr === null || ePhoto === eCurr)) { if (ePhoto && (eCurr === null || ePhoto === eCurr)) {
exif[field.field] = String(ePhoto); exif[field.field] = String(ePhoto);
} else { } else {
exif[field.field] = ""; exif[field.field] = '';
} }
} }
} }
@ -114,17 +114,15 @@ export default defineComponent({
}, },
label(field: any) { label(field: any) {
return field.label + (this.dirty[field.field] ? "*" : ""); return field.label + (this.dirty[field.field] ? '*' : '');
}, },
placeholder(field: any) { placeholder(field: any) {
return this.dirty[field.field] return this.dirty[field.field] ? t('memories', 'Empty') : t('memories', 'Unchanged');
? t("memories", "Empty")
: t("memories", "Unchanged");
}, },
reset(field: any) { reset(field: any) {
this.exif[field.field] = ""; this.exif[field.field] = '';
this.dirty[field.field] = false; this.dirty[field.field] = false;
}, },
}, },

View File

@ -2,26 +2,18 @@
<div class="outer"> <div class="outer">
<div class="lat-lon"> <div class="lat-lon">
<div class="coords"> <div class="coords">
<span>{{ loc }}</span> {{ dirty ? "*" : "" }} <span>{{ loc }}</span> {{ dirty ? '*' : '' }}
</div> </div>
<div class="action"> <div class="action">
<NcActions :inline="2"> <NcActions :inline="2">
<NcActionButton <NcActionButton v-if="dirty" :aria-label="t('memories', 'Reset')" @click="reset()">
v-if="dirty" {{ t('memories', 'Reset') }}
:aria-label="t('memories', 'Reset')"
@click="reset()"
>
{{ t("memories", "Reset") }}
<template #icon> <UndoIcon :size="20" /> </template> <template #icon> <UndoIcon :size="20" /> </template>
</NcActionButton> </NcActionButton>
<NcActionButton <NcActionButton v-if="lat && lon" :aria-label="t('memories', 'Remove location')" @click="clear()">
v-if="lat && lon" {{ t('memories', 'Remove location') }}
:aria-label="t('memories', 'Remove location')"
@click="clear()"
>
{{ t("memories", "Remove location") }}
<template #icon> <CloseIcon :size="20" /> </template> <template #icon> <CloseIcon :size="20" /> </template>
</NcActionButton> </NcActionButton>
</NcActions> </NcActions>
@ -41,13 +33,9 @@
<div class="osm-attribution"> <div class="osm-attribution">
Powered by Powered by
<a href="https://nominatim.openstreetmap.org" target="_blank" <a href="https://nominatim.openstreetmap.org" target="_blank">Nominatim</a>
>Nominatim</a
>
&copy; &copy;
<a href="https://www.openstreetmap.org/copyright" target="_blank" <a href="https://www.openstreetmap.org/copyright" target="_blank">OpenStreetMap</a>
>OpenStreetMap</a
>
contributors contributors
</div> </div>
@ -68,21 +56,21 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from "vue"; import { defineComponent } from 'vue';
import { IPhoto } from "../../types"; import { IPhoto } from '../../types';
import axios from "@nextcloud/axios"; import axios from '@nextcloud/axios';
import { showError } from "@nextcloud/dialogs"; import { showError } from '@nextcloud/dialogs';
import NcActions from "@nextcloud/vue/dist/Components/NcActions"; import NcActions from '@nextcloud/vue/dist/Components/NcActions';
import NcActionButton from "@nextcloud/vue/dist/Components/NcActionButton"; import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton';
const NcTextField = () => import("@nextcloud/vue/dist/Components/NcTextField"); const NcTextField = () => import('@nextcloud/vue/dist/Components/NcTextField');
const NcListItem = () => import("@nextcloud/vue/dist/Components/NcListItem"); const NcListItem = () => import('@nextcloud/vue/dist/Components/NcListItem');
import NcLoadingIcon from "@nextcloud/vue/dist/Components/NcLoadingIcon"; import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon';
import MagnifyIcon from "vue-material-design-icons/Magnify.vue"; import MagnifyIcon from 'vue-material-design-icons/Magnify.vue';
import CloseIcon from "vue-material-design-icons/Close.vue"; import CloseIcon from 'vue-material-design-icons/Close.vue';
import UndoIcon from "vue-material-design-icons/UndoVariant.vue"; import UndoIcon from 'vue-material-design-icons/UndoVariant.vue';
type NLocation = { type NLocation = {
osm_id: number; osm_id: number;
@ -116,7 +104,7 @@ export default defineComponent({
dirty: false, dirty: false,
lat: null as number | null, lat: null as number | null,
lon: null as number | null, lon: null as number | null,
searchBar: "", searchBar: '',
loading: false, loading: false,
options: [] as NLocation[], options: [] as NLocation[],
@ -127,7 +115,7 @@ export default defineComponent({
if (this.lat && this.lon) { if (this.lat && this.lon) {
return `${this.lat.toFixed(6)}, ${this.lon.toFixed(6)}`; return `${this.lat.toFixed(6)}, ${this.lon.toFixed(6)}`;
} }
return this.t("memories", "No coordinates"); return this.t('memories', 'No coordinates');
}, },
}, },
@ -170,9 +158,7 @@ export default defineComponent({
this.loading = true; this.loading = true;
const q = window.encodeURIComponent(this.searchBar); const q = window.encodeURIComponent(this.searchBar);
axios axios
.get( .get(`https://nominatim.openstreetmap.org/search.php?q=${q}&format=jsonv2`)
`https://nominatim.openstreetmap.org/search.php?q=${q}&format=jsonv2`
)
.then((response) => { .then((response) => {
this.loading = false; this.loading = false;
this.options = response.data.filter((x: NLocation) => { this.options = response.data.filter((x: NLocation) => {
@ -182,9 +168,7 @@ export default defineComponent({
.catch((error) => { .catch((error) => {
this.loading = false; this.loading = false;
console.error(error); console.error(error);
showError( showError(this.t('memories', 'Failed to search for location with Nominatim.'));
this.t("memories", "Failed to search for location with Nominatim.")
);
}); });
}, },
@ -199,7 +183,7 @@ export default defineComponent({
this.lat = Number(option.lat); this.lat = Number(option.lat);
this.lon = Number(option.lon); this.lon = Number(option.lon);
this.options = []; this.options = [];
this.searchBar = ""; this.searchBar = '';
}, },
result() { result() {

View File

@ -1,32 +1,26 @@
<template> <template>
<Modal v-if="show" @close="close"> <Modal v-if="show" @close="close">
<template #title> <template #title>
{{ t("memories", "Edit metadata") }} {{ t('memories', 'Edit metadata') }}
</template> </template>
<template #buttons> <template #buttons>
<NcButton <NcButton @click="save" class="button" type="error" v-if="photos" :disabled="processing">
@click="save" {{ t('memories', 'Save') }}
class="button"
type="error"
v-if="photos"
:disabled="processing"
>
{{ t("memories", "Save") }}
</NcButton> </NcButton>
</template> </template>
<div v-if="photos"> <div v-if="photos">
<div v-if="sections.includes(1)"> <div v-if="sections.includes(1)">
<div class="title-text"> <div class="title-text">
{{ t("memories", "Date / Time") }} {{ t('memories', 'Date / Time') }}
</div> </div>
<EditDate ref="editDate" :photos="photos" /> <EditDate ref="editDate" :photos="photos" />
</div> </div>
<div v-if="config_tagsEnabled && sections.includes(2)"> <div v-if="config_tagsEnabled && sections.includes(2)">
<div class="title-text"> <div class="title-text">
{{ t("memories", "Collaborative Tags") }} {{ t('memories', 'Collaborative Tags') }}
</div> </div>
<EditTags ref="editTags" :photos="photos" /> <EditTags ref="editTags" :photos="photos" />
<div class="tag-padding" v-if="sections.length === 1"></div> <div class="tag-padding" v-if="sections.length === 1"></div>
@ -34,14 +28,14 @@
<div v-if="sections.includes(3)"> <div v-if="sections.includes(3)">
<div class="title-text"> <div class="title-text">
{{ t("memories", "EXIF Fields") }} {{ t('memories', 'EXIF Fields') }}
</div> </div>
<EditExif ref="editExif" :photos="photos" /> <EditExif ref="editExif" :photos="photos" />
</div> </div>
<div v-if="sections.includes(4)"> <div v-if="sections.includes(4)">
<div class="title-text"> <div class="title-text">
{{ t("memories", "Geolocation") }} {{ t('memories', 'Geolocation') }}
</div> </div>
<EditLocation ref="editLocation" :photos="photos" /> <EditLocation ref="editLocation" :photos="photos" />
</div> </div>
@ -54,27 +48,26 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from "vue"; import { defineComponent } from 'vue';
import { IPhoto } from "../../types"; import { IPhoto } from '../../types';
import UserConfig from "../../mixins/UserConfig"; import UserConfig from '../../mixins/UserConfig';
import NcButton from "@nextcloud/vue/dist/Components/NcButton"; import NcButton from '@nextcloud/vue/dist/Components/NcButton';
const NcTextField = () => import("@nextcloud/vue/dist/Components/NcTextField"); const NcTextField = () => import('@nextcloud/vue/dist/Components/NcTextField');
const NcProgressBar = () => const NcProgressBar = () => import('@nextcloud/vue/dist/Components/NcProgressBar');
import("@nextcloud/vue/dist/Components/NcProgressBar"); import Modal from './Modal.vue';
import Modal from "./Modal.vue";
import EditDate from "./EditDate.vue"; import EditDate from './EditDate.vue';
import EditTags from "./EditTags.vue"; import EditTags from './EditTags.vue';
import EditExif from "./EditExif.vue"; import EditExif from './EditExif.vue';
import EditLocation from "./EditLocation.vue"; import EditLocation from './EditLocation.vue';
import { showError } from "@nextcloud/dialogs"; import { showError } from '@nextcloud/dialogs';
import { emit } from "@nextcloud/event-bus"; import { emit } from '@nextcloud/event-bus';
import axios from "@nextcloud/axios"; import axios from '@nextcloud/axios';
import * as dav from "../../services/DavRequests"; import * as dav from '../../services/DavRequests';
import { API } from "../../services/API"; import { API } from '../../services/API';
export default defineComponent({ export default defineComponent({
components: { components: {
@ -122,14 +115,14 @@ export default defineComponent({
// Validate response // Validate response
p.imageInfo = null; p.imageInfo = null;
if (typeof res.data.datetaken !== "number") { if (typeof res.data.datetaken !== 'number') {
console.error("Invalid date for", p.fileid); console.error('Invalid date for', p.fileid);
return; return;
} }
p.datetaken = res.data.datetaken * 1000; p.datetaken = res.data.datetaken * 1000;
p.imageInfo = res.data; p.imageInfo = res.data;
} catch (error) { } catch (error) {
console.error("Failed to get date info for", p.fileid, error); console.error('Failed to get date info for', p.fileid, error);
} finally { } finally {
done++; done++;
this.progress = Math.round((done * 100) / photos.length); this.progress = Math.round((done * 100) / photos.length);
@ -212,10 +205,10 @@ export default defineComponent({
// Refresh UX // Refresh UX
if (dirty) { if (dirty) {
p.imageInfo = null; p.imageInfo = null;
emit("files:file:updated", { fileid }); emit('files:file:updated', { fileid });
} }
} catch (e) { } catch (e) {
console.error("Failed to save metadata for", p.fileid, e); console.error('Failed to save metadata for', p.fileid, e);
if (e.response?.data?.message) { if (e.response?.data?.message) {
showError(e.response.data.message); showError(e.response.data.message);
} else { } else {
@ -235,7 +228,7 @@ export default defineComponent({
this.close(); this.close();
// Trigger a soft refresh // Trigger a soft refresh
emit("files:file:created", { fileid: 0 }); emit('files:file:created', { fileid: 0 });
}, },
filterValid(photos: IPhoto[]) { filterValid(photos: IPhoto[]) {
@ -243,25 +236,19 @@ export default defineComponent({
const valid = photos.filter((p) => p.imageInfo); const valid = photos.filter((p) => p.imageInfo);
if (valid.length !== photos.length) { if (valid.length !== photos.length) {
showError( showError(
this.t("memories", "Failed to load metadata for {n} photos.", { this.t('memories', 'Failed to load metadata for {n} photos.', {
n: photos.length - valid.length, n: photos.length - valid.length,
}) })
); );
} }
// Check if photos are updatable // Check if photos are updatable
const updatable = valid.filter((p) => const updatable = valid.filter((p) => p.imageInfo?.permissions?.includes('U'));
p.imageInfo?.permissions?.includes("U")
);
if (updatable.length !== valid.length) { if (updatable.length !== valid.length) {
showError( showError(
this.t( this.t('memories', '{n} photos cannot be edited (permissions error).', {
"memories",
"{n} photos cannot be edited (permissions error).",
{
n: valid.length - updatable.length, n: valid.length - updatable.length,
} })
)
); );
} }

View File

@ -1,23 +1,17 @@
<template> <template>
<div class="outer"> <div class="outer">
<NcSelectTags <NcSelectTags class="nc-comp" v-model="tagSelection" :limit="null" :options-filter="tagFilter" />
class="nc-comp"
v-model="tagSelection"
:limit="null"
:options-filter="tagFilter"
/>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from "vue"; import { defineComponent } from 'vue';
import { IPhoto } from "../../types"; import { IPhoto } from '../../types';
const NcSelectTags = () => const NcSelectTags = () => import('@nextcloud/vue/dist/Components/NcSelectTags');
import("@nextcloud/vue/dist/Components/NcSelectTags");
export default defineComponent({ export default defineComponent({
name: "EditTags", name: 'EditTags',
components: { components: {
NcSelectTags, NcSelectTags,
}, },
@ -45,9 +39,7 @@ export default defineComponent({
// Find common tags in all selected photos // Find common tags in all selected photos
for (const photo of this.photos) { for (const photo of this.photos) {
const s = new Set<number>(); const s = new Set<number>();
for (const tag of Object.keys(photo.imageInfo?.tags || {}).map( for (const tag of Object.keys(photo.imageInfo?.tags || {}).map(Number)) {
Number
)) {
s.add(tag); s.add(tag);
} }
tagIds = tagIds ? [...tagIds].filter((x) => s.has(x)) : [...s]; tagIds = tagIds ? [...tagIds].filter((x) => s.has(x)) : [...s];
@ -60,7 +52,7 @@ export default defineComponent({
tagFilter(element, index) { tagFilter(element, index) {
return ( return (
element.id >= 2 && element.id >= 2 &&
element.displayName !== "" && element.displayName !== '' &&
element.canAssign && element.canAssign &&
element.userAssignable && element.userAssignable &&
element.userVisible element.userVisible
@ -69,9 +61,7 @@ export default defineComponent({
result() { result() {
const add = this.tagSelection.filter((x) => !this.origIds.has(x)); const add = this.tagSelection.filter((x) => !this.origIds.has(x));
const remove = [...this.origIds].filter( const remove = [...this.origIds].filter((x) => !this.tagSelection.includes(x));
(x) => !this.tagSelection.includes(x)
);
if (add.length === 0 && remove.length === 0) { if (add.length === 0 && remove.length === 0) {
return null; return null;

View File

@ -1,35 +1,33 @@
<template> <template>
<Modal @close="close" v-if="show"> <Modal @close="close" v-if="show">
<template #title> <template #title>
{{ t("memories", "Remove person") }} {{ t('memories', 'Remove person') }}
</template> </template>
<span>{{ <span>{{ t('memories', 'Are you sure you want to remove {name}?', { name }) }}</span>
t("memories", "Are you sure you want to remove {name}?", { name })
}}</span>
<template #buttons> <template #buttons>
<NcButton @click="save" class="button" type="error"> <NcButton @click="save" class="button" type="error">
{{ t("memories", "Delete") }} {{ t('memories', 'Delete') }}
</NcButton> </NcButton>
</template> </template>
</Modal> </Modal>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from "vue"; import { defineComponent } from 'vue';
import NcButton from "@nextcloud/vue/dist/Components/NcButton"; import NcButton from '@nextcloud/vue/dist/Components/NcButton';
const NcTextField = () => import("@nextcloud/vue/dist/Components/NcTextField"); const NcTextField = () => import('@nextcloud/vue/dist/Components/NcTextField');
import { showError } from "@nextcloud/dialogs"; import { showError } from '@nextcloud/dialogs';
import { getCurrentUser } from "@nextcloud/auth"; import { getCurrentUser } from '@nextcloud/auth';
import Modal from "./Modal.vue"; import Modal from './Modal.vue';
import client from "../../services/DavClient"; import client from '../../services/DavClient';
import * as dav from "../../services/DavRequests"; import * as dav from '../../services/DavRequests';
export default defineComponent({ export default defineComponent({
name: "FaceDeleteModal", name: 'FaceDeleteModal',
components: { components: {
NcButton, NcButton,
NcTextField, NcTextField,
@ -38,8 +36,8 @@ export default defineComponent({
data: () => ({ data: () => ({
show: false, show: false,
user: "", user: '',
name: "", name: '',
}), }),
mounted() { mounted() {
@ -55,14 +53,14 @@ export default defineComponent({
methods: { methods: {
close() { close() {
this.show = false; this.show = false;
this.$emit("close"); this.$emit('close');
}, },
open() { open() {
const user = this.$route.params.user || ""; const user = this.$route.params.user || '';
if (this.$route.params.user !== getCurrentUser()?.uid) { if (this.$route.params.user !== getCurrentUser()?.uid) {
showError( showError(
this.t("memories", 'Only user "{user}" can delete this person', { this.t('memories', 'Only user "{user}" can delete this person', {
user, user,
}) })
); );
@ -72,13 +70,13 @@ export default defineComponent({
}, },
refreshParams() { refreshParams() {
this.user = <string>this.$route.params.user || ""; this.user = <string>this.$route.params.user || '';
this.name = <string>this.$route.params.name || ""; this.name = <string>this.$route.params.name || '';
}, },
async save() { async save() {
try { try {
if (this.$route.name === "recognize") { if (this.$route.name === 'recognize') {
await client.deleteFile(`/recognize/${this.user}/faces/${this.name}`); await client.deleteFile(`/recognize/${this.user}/faces/${this.name}`);
} else { } else {
await dav.setVisibilityPeopleFaceRecognition(this.name, false); await dav.setVisibilityPeopleFaceRecognition(this.name, false);
@ -88,7 +86,7 @@ export default defineComponent({
} catch (error) { } catch (error) {
console.log(error); console.log(error);
showError( showError(
this.t("photos", "Failed to delete {name}.", { this.t('photos', 'Failed to delete {name}.', {
name: this.name, name: this.name,
}) })
); );

View File

@ -1,7 +1,7 @@
<template> <template>
<Modal @close="close" v-if="show"> <Modal @close="close" v-if="show">
<template #title> <template #title>
{{ t("memories", "Rename person") }} {{ t('memories', 'Rename person') }}
</template> </template>
<div class="fields"> <div class="fields">
@ -18,26 +18,26 @@
<template #buttons> <template #buttons>
<NcButton @click="save" class="button" type="primary"> <NcButton @click="save" class="button" type="primary">
{{ t("memories", "Update") }} {{ t('memories', 'Update') }}
</NcButton> </NcButton>
</template> </template>
</Modal> </Modal>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from "vue"; import { defineComponent } from 'vue';
import NcButton from "@nextcloud/vue/dist/Components/NcButton"; import NcButton from '@nextcloud/vue/dist/Components/NcButton';
const NcTextField = () => import("@nextcloud/vue/dist/Components/NcTextField"); const NcTextField = () => import('@nextcloud/vue/dist/Components/NcTextField');
import { showError } from "@nextcloud/dialogs"; import { showError } from '@nextcloud/dialogs';
import { getCurrentUser } from "@nextcloud/auth"; import { getCurrentUser } from '@nextcloud/auth';
import Modal from "./Modal.vue"; import Modal from './Modal.vue';
import client from "../../services/DavClient"; import client from '../../services/DavClient';
import * as dav from "../../services/DavRequests"; import * as dav from '../../services/DavRequests';
export default defineComponent({ export default defineComponent({
name: "FaceEditModal", name: 'FaceEditModal',
components: { components: {
NcButton, NcButton,
NcTextField, NcTextField,
@ -46,9 +46,9 @@ export default defineComponent({
data: () => ({ data: () => ({
show: false, show: false,
user: "", user: '',
name: "", name: '',
oldName: "", oldName: '',
}), }),
mounted() { mounted() {
@ -64,14 +64,14 @@ export default defineComponent({
methods: { methods: {
close() { close() {
this.show = false; this.show = false;
this.$emit("close"); this.$emit('close');
}, },
open() { open() {
const user = this.$route.params.user || ""; const user = this.$route.params.user || '';
if (this.$route.params.user !== getCurrentUser()?.uid) { if (this.$route.params.user !== getCurrentUser()?.uid) {
showError( showError(
this.t("memories", 'Only user "{user}" can update this person', { this.t('memories', 'Only user "{user}" can update this person', {
user, user,
}) })
); );
@ -81,14 +81,14 @@ export default defineComponent({
}, },
refreshParams() { refreshParams() {
this.user = <string>this.$route.params.user || ""; this.user = <string>this.$route.params.user || '';
this.name = <string>this.$route.params.name || ""; this.name = <string>this.$route.params.name || '';
this.oldName = <string>this.$route.params.name || ""; this.oldName = <string>this.$route.params.name || '';
}, },
async save() { async save() {
try { try {
if (this.$route.name === "recognize") { if (this.$route.name === 'recognize') {
await client.moveFile( await client.moveFile(
`/recognize/${this.user}/faces/${this.oldName}`, `/recognize/${this.user}/faces/${this.oldName}`,
`/recognize/${this.user}/faces/${this.name}` `/recognize/${this.user}/faces/${this.name}`
@ -104,7 +104,7 @@ export default defineComponent({
} catch (error) { } catch (error) {
console.log(error); console.log(error);
showError( showError(
this.t("photos", "Failed to rename {oldName} to {name}.", { this.t('photos', 'Failed to rename {oldName} to {name}.', {
oldName: this.oldName, oldName: this.oldName,
name: this.name, name: this.name,
}) })

View File

@ -11,33 +11,27 @@
</NcTextField> </NcTextField>
</div> </div>
<ClusterGrid <ClusterGrid v-if="list" :items="filteredList" :link="false" :maxSize="120" @click="click" />
v-if="list"
:items="filteredList"
:link="false"
:maxSize="120"
@click="click"
/>
<div v-else> <div v-else>
{{ t("memories", "Loading …") }} {{ t('memories', 'Loading …') }}
</div> </div>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from "vue"; import { defineComponent } from 'vue';
import { ICluster, IFace } from "../../types"; import { ICluster, IFace } from '../../types';
import ClusterGrid from "../ClusterGrid.vue"; import ClusterGrid from '../ClusterGrid.vue';
import NcTextField from "@nextcloud/vue/dist/Components/NcTextField"; import NcTextField from '@nextcloud/vue/dist/Components/NcTextField';
import * as dav from "../../services/DavRequests"; import * as dav from '../../services/DavRequests';
import Fuse from "fuse.js"; import Fuse from 'fuse.js';
import Magnify from "vue-material-design-icons/Magnify.vue"; import Magnify from 'vue-material-design-icons/Magnify.vue';
export default defineComponent({ export default defineComponent({
name: "FaceList", name: 'FaceList',
components: { components: {
ClusterGrid, ClusterGrid,
NcTextField, NcTextField,
@ -45,11 +39,11 @@ export default defineComponent({
}, },
data: () => ({ data: () => ({
user: "", user: '',
name: "", name: '',
list: null as ICluster[] | null, list: null as ICluster[] | null,
fuse: null as Fuse<ICluster> | null, fuse: null as Fuse<ICluster> | null,
search: "", search: '',
}), }),
watch: { watch: {
@ -71,27 +65,25 @@ export default defineComponent({
methods: { methods: {
close() { close() {
this.$emit("close"); this.$emit('close');
}, },
async refreshParams() { async refreshParams() {
this.user = <string>this.$route.params.user || ""; this.user = <string>this.$route.params.user || '';
this.name = <string>this.$route.params.name || ""; this.name = <string>this.$route.params.name || '';
this.list = null; this.list = null;
this.search = ""; this.search = '';
this.list = (await dav.getFaceList(this.$route.name as any)).filter( this.list = (await dav.getFaceList(this.$route.name as any)).filter((c: IFace) => {
(c: IFace) => {
const clusterName = String(c.name || c.cluster_id); const clusterName = String(c.name || c.cluster_id);
return c.user_id === this.user && clusterName !== this.name; return c.user_id === this.user && clusterName !== this.name;
} });
);
this.fuse = new Fuse(this.list, { keys: ["name"] }); this.fuse = new Fuse(this.list, { keys: ['name'] });
}, },
async click(face: IFace) { async click(face: IFace) {
this.$emit("select", face); this.$emit('select', face);
}, },
}, },
}); });

View File

@ -1,50 +1,44 @@
<template> <template>
<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>
<div class="outer"> <div class="outer">
<FaceList @select="clickFace" /> <FaceList @select="clickFace" />
<div v-if="processingTotal > 0"> <div v-if="processingTotal > 0">
<NcProgressBar <NcProgressBar :value="Math.round((processing * 100) / processingTotal)" :error="true" />
:value="Math.round((processing * 100) / processingTotal)"
:error="true"
/>
</div> </div>
</div> </div>
<template #buttons> <template #buttons>
<NcButton @click="close" class="button" type="error"> <NcButton @click="close" class="button" type="error">
{{ t("memories", "Cancel") }} {{ t('memories', 'Cancel') }}
</NcButton> </NcButton>
</template> </template>
</Modal> </Modal>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from "vue"; import { defineComponent } from 'vue';
import NcButton from "@nextcloud/vue/dist/Components/NcButton"; import NcButton from '@nextcloud/vue/dist/Components/NcButton';
const NcTextField = () => import("@nextcloud/vue/dist/Components/NcTextField"); const NcTextField = () => import('@nextcloud/vue/dist/Components/NcTextField');
const NcProgressBar = () => const NcProgressBar = () => import('@nextcloud/vue/dist/Components/NcProgressBar');
import("@nextcloud/vue/dist/Components/NcProgressBar");
import { showError } from "@nextcloud/dialogs"; import { showError } from '@nextcloud/dialogs';
import { getCurrentUser } from "@nextcloud/auth"; import { getCurrentUser } from '@nextcloud/auth';
import { IFileInfo, IFace } from "../../types"; import { IFileInfo, IFace } from '../../types';
import Cluster from "../frame/Cluster.vue"; import Cluster from '../frame/Cluster.vue';
import FaceList from "./FaceList.vue"; import FaceList from './FaceList.vue';
import Modal from "./Modal.vue"; import Modal from './Modal.vue';
import client from "../../services/DavClient"; import client from '../../services/DavClient';
import * as dav from "../../services/DavRequests"; import * as dav from '../../services/DavRequests';
export default defineComponent({ export default defineComponent({
name: "FaceMergeModal", name: 'FaceMergeModal',
components: { components: {
NcButton, NcButton,
NcTextField, NcTextField,
@ -63,14 +57,14 @@ export default defineComponent({
methods: { methods: {
close() { close() {
this.show = false; this.show = false;
this.$emit("close"); this.$emit('close');
}, },
open() { open() {
const user = this.$route.params.user || ""; const user = this.$route.params.user || '';
if (this.$route.params.user !== getCurrentUser()?.uid) { if (this.$route.params.user !== getCurrentUser()?.uid) {
showError( showError(
this.t("memories", 'Only user "{user}" can update this person', { this.t('memories', 'Only user "{user}" can update this person', {
user, user,
}) })
); );
@ -80,28 +74,17 @@ export default defineComponent({
}, },
async clickFace(face: IFace) { async clickFace(face: IFace) {
const user = this.$route.params.user || ""; const user = this.$route.params.user || '';
const name = this.$route.params.name || ""; const name = this.$route.params.name || '';
const newName = String(face.name || face.cluster_id); const newName = String(face.name || face.cluster_id);
if ( if (!confirm(this.t('memories', 'Are you sure you want to merge {name} with {newName}?', { name, newName }))) {
!confirm(
this.t(
"memories",
"Are you sure you want to merge {name} with {newName}?",
{ name, newName }
)
)
) {
return; return;
} }
try { try {
// Get all files for current face // Get all files for current face
let res = (await client.getDirectoryContents( let res = (await client.getDirectoryContents(`/recognize/${user}/faces/${name}`, { details: true })) as any;
`/recognize/${user}/faces/${name}`,
{ details: true }
)) as any;
let data: IFileInfo[] = res.data; let data: IFileInfo[] = res.data;
this.processingTotal = data.length; this.processingTotal = data.length;
@ -112,7 +95,7 @@ export default defineComponent({
const calls = data.map((p) => async () => { const calls = data.map((p) => async () => {
// Short circuit if we have too many failures // Short circuit if we have too many failures
if (failures === 10) { if (failures === 10) {
showError(this.t("memories", "Too many failures, aborting")); showError(this.t('memories', 'Too many failures, aborting'));
failures++; failures++;
} }
if (failures >= 10) return; if (failures >= 10) return;
@ -125,9 +108,7 @@ export default defineComponent({
); );
} catch (e) { } catch (e) {
console.error(e); console.error(e);
showError( showError(this.t('memories', 'Error while moving {basename}', <any>p));
this.t("memories", "Error while moving {basename}", <any>p)
);
failures++; failures++;
} finally { } finally {
this.processing++; this.processing++;
@ -140,14 +121,14 @@ export default defineComponent({
// Go to new face // Go to new face
if (failures === 0) { if (failures === 0) {
this.$router.push({ this.$router.push({
name: "recognize", name: 'recognize',
params: { user: face.user_id, name: newName }, params: { user: face.user_id, name: newName },
}); });
this.close(); this.close();
} }
} catch (error) { } catch (error) {
console.error(error); console.error(error);
showError(this.t("photos", "Failed to move {name}.", { name })); showError(this.t('photos', 'Failed to move {name}.', { name }));
} }
}, },
}, },

View File

@ -1,7 +1,7 @@
<template> <template>
<Modal @close="close" size="large" v-if="show"> <Modal @close="close" size="large" v-if="show">
<template #title> <template #title>
{{ t("memories", "Move selected photos to person") }} {{ t('memories', 'Move selected photos to person') }}
</template> </template>
<div class="outer"> <div class="outer">
@ -10,30 +10,30 @@
<template #buttons> <template #buttons>
<NcButton @click="close" class="button" type="error"> <NcButton @click="close" class="button" type="error">
{{ t("memories", "Cancel") }} {{ t('memories', 'Cancel') }}
</NcButton> </NcButton>
</template> </template>
</Modal> </Modal>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from "vue"; import { defineComponent } from 'vue';
import NcButton from "@nextcloud/vue/dist/Components/NcButton"; import NcButton from '@nextcloud/vue/dist/Components/NcButton';
const NcTextField = () => import("@nextcloud/vue/dist/Components/NcTextField"); const NcTextField = () => import('@nextcloud/vue/dist/Components/NcTextField');
import { showError } from "@nextcloud/dialogs"; import { showError } from '@nextcloud/dialogs';
import { getCurrentUser } from "@nextcloud/auth"; import { getCurrentUser } from '@nextcloud/auth';
import { IPhoto, IFace } from "../../types"; import { IPhoto, IFace } from '../../types';
import Cluster from "../frame/Cluster.vue"; import Cluster from '../frame/Cluster.vue';
import FaceList from "./FaceList.vue"; import FaceList from './FaceList.vue';
import Modal from "./Modal.vue"; import Modal from './Modal.vue';
import client from "../../services/DavClient"; import client from '../../services/DavClient';
import * as dav from "../../services/DavRequests"; import * as dav from '../../services/DavRequests';
export default defineComponent({ export default defineComponent({
name: "FaceMoveModal", name: 'FaceMoveModal',
components: { components: {
NcButton, NcButton,
NcTextField, NcTextField,
@ -62,10 +62,10 @@ export default defineComponent({
} }
// check ownership // check ownership
const user = this.$route.params.user || ""; const user = this.$route.params.user || '';
if (this.$route.params.user !== getCurrentUser()?.uid) { if (this.$route.params.user !== getCurrentUser()?.uid) {
showError( showError(
this.t("memories", 'Only user "{user}" can update this person', { this.t('memories', 'Only user "{user}" can update this person', {
user, user,
}) })
); );
@ -79,26 +79,25 @@ export default defineComponent({
close() { close() {
this.photos = []; this.photos = [];
this.show = false; this.show = false;
this.$emit("close"); this.$emit('close');
}, },
moved(list: IPhoto[]) { moved(list: IPhoto[]) {
this.$emit("moved", list); this.$emit('moved', list);
}, },
async clickFace(face: IFace) { async clickFace(face: IFace) {
const user = this.$route.params.user || ""; const user = this.$route.params.user || '';
const name = this.$route.params.name || ""; const name = this.$route.params.name || '';
const newName = String(face.name || face.cluster_id); const newName = String(face.name || face.cluster_id);
if ( if (
!confirm( !confirm(
this.t( this.t('memories', 'Are you sure you want to move the selected photos from {name} to {newName}?', {
"memories", name,
"Are you sure you want to move the selected photos from {name} to {newName}?", newName,
{ name, newName } })
)
) )
) { ) {
return; return;
@ -124,9 +123,7 @@ export default defineComponent({
return photoMap.get(p.fileid); return photoMap.get(p.fileid);
} catch (e) { } catch (e) {
console.error(e); console.error(e);
showError( showError(this.t('memories', 'Error while moving {basename}', <any>p));
this.t("memories", "Error while moving {basename}", <any>p)
);
} }
}); });
for await (const resp of dav.runInParallel(calls, 10)) { for await (const resp of dav.runInParallel(calls, 10)) {
@ -135,7 +132,7 @@ export default defineComponent({
} }
} catch (error) { } catch (error) {
console.error(error); console.error(error);
showError(this.t("photos", "Failed to move {name}.", { name })); showError(this.t('photos', 'Failed to move {name}.', { name }));
} finally { } finally {
this.updateLoading(-1); this.updateLoading(-1);
this.close(); this.close();

View File

@ -21,13 +21,13 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from "vue"; import { defineComponent } from 'vue';
const NcModal = () => import("@nextcloud/vue/dist/Components/NcModal"); const NcModal = () => import('@nextcloud/vue/dist/Components/NcModal');
import { subscribe, unsubscribe } from "@nextcloud/event-bus"; import { subscribe, unsubscribe } from '@nextcloud/event-bus';
export default defineComponent({ export default defineComponent({
name: "Modal", name: 'Modal',
components: { components: {
NcModal, NcModal,
}, },
@ -35,7 +35,7 @@ export default defineComponent({
props: { props: {
size: { size: {
type: String, type: String,
default: "small", default: 'small',
}, },
sidebar: { sidebar: {
type: String, type: String,
@ -52,8 +52,8 @@ export default defineComponent({
beforeMount() { beforeMount() {
if (this.sidebar) { if (this.sidebar) {
subscribe("memories:sidebar:opened", this.handleAppSidebarOpen); subscribe('memories:sidebar:opened', this.handleAppSidebarOpen);
subscribe("memories:sidebar:closed", this.handleAppSidebarClose); subscribe('memories:sidebar:closed', this.handleAppSidebarClose);
} }
this._mutationObserver = new MutationObserver(this.handleBodyMutation); this._mutationObserver = new MutationObserver(this.handleBodyMutation);
this._mutationObserver.observe(document.body, { childList: true }); this._mutationObserver.observe(document.body, { childList: true });
@ -61,8 +61,8 @@ export default defineComponent({
beforeDestroy() { beforeDestroy() {
if (this.sidebar) { if (this.sidebar) {
unsubscribe("memories:sidebar:opened", this.handleAppSidebarOpen); unsubscribe('memories:sidebar:opened', this.handleAppSidebarOpen);
unsubscribe("memories:sidebar:closed", this.handleAppSidebarClose); unsubscribe('memories:sidebar:closed', this.handleAppSidebarClose);
globalThis.mSidebar.close(); globalThis.mSidebar.close();
} }
this._mutationObserver.disconnect(); this._mutationObserver.disconnect();
@ -79,7 +79,7 @@ export default defineComponent({
methods: { methods: {
close() { close() {
this.$emit("close"); this.$emit('close');
}, },
/** /**
@ -88,28 +88,22 @@ export default defineComponent({
*/ */
handleBodyMutation(mutations: MutationRecord[]) { handleBodyMutation(mutations: MutationRecord[]) {
const test = (node: Node): node is HTMLElement => const test = (node: Node): node is HTMLElement =>
node instanceof HTMLElement && node instanceof HTMLElement && node?.classList?.contains('v-popper__popper');
node?.classList?.contains("v-popper__popper");
mutations.forEach((mutation) => { mutations.forEach((mutation) => {
if (mutation.type === "childList") { if (mutation.type === 'childList') {
Array.from(mutation.addedNodes) Array.from(mutation.addedNodes)
.filter(test) .filter(test)
.forEach((node) => this.trapElements.push(node)); .forEach((node) => this.trapElements.push(node));
Array.from(mutation.removedNodes) Array.from(mutation.removedNodes)
.filter(test) .filter(test)
.forEach( .forEach((node) => (this.trapElements = this.trapElements.filter((el) => el !== node)));
(node) =>
(this.trapElements = this.trapElements.filter(
(el) => el !== node
))
);
} }
}); });
}, },
handleAppSidebarOpen() { handleAppSidebarOpen() {
const sidebar = document.getElementById("app-sidebar-vue"); const sidebar = document.getElementById('app-sidebar-vue');
if (sidebar) { if (sidebar) {
this.isSidebarShown = true; this.isSidebarShown = true;
this.sidebarWidth = sidebar.offsetWidth; this.sidebarWidth = sidebar.offsetWidth;

View File

@ -1,34 +1,30 @@
<template> <template>
<Modal @close="close" size="normal" v-if="processing"> <Modal @close="close" size="normal" v-if="processing">
<template #title> <template #title>
{{ t("memories", "Move to folder") }} {{ t('memories', 'Move to folder') }}
</template> </template>
<div class="outer"> <div class="outer">
<NcProgressBar <NcProgressBar :value="Math.round((photosDone * 100) / photos.length)" :error="true" />
:value="Math.round((photosDone * 100) / photos.length)"
:error="true"
/>
</div> </div>
</Modal> </Modal>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from "vue"; import { defineComponent } from 'vue';
import * as dav from "../../services/DavRequests"; import * as dav from '../../services/DavRequests';
import { getFilePickerBuilder, FilePickerType } from "@nextcloud/dialogs"; import { getFilePickerBuilder, FilePickerType } from '@nextcloud/dialogs';
import { showInfo } from "@nextcloud/dialogs"; import { showInfo } from '@nextcloud/dialogs';
import { IPhoto } from "../../types"; import { IPhoto } from '../../types';
const NcProgressBar = () => const NcProgressBar = () => import('@nextcloud/vue/dist/Components/NcProgressBar');
import("@nextcloud/vue/dist/Components/NcProgressBar");
import UserConfig from "../../mixins/UserConfig"; import UserConfig from '../../mixins/UserConfig';
import Modal from "./Modal.vue"; import Modal from './Modal.vue';
export default defineComponent({ export default defineComponent({
name: "MoveToFolderModal", name: 'MoveToFolderModal',
components: { components: {
NcProgressBar, NcProgressBar,
Modal, Modal,
@ -52,13 +48,13 @@ export default defineComponent({
}, },
moved(photos: IPhoto[]) { moved(photos: IPhoto[]) {
this.$emit("moved", photos); this.$emit('moved', photos);
}, },
close() { close() {
this.photos = []; this.photos = [];
this.processing = false; this.processing = false;
this.$emit("close"); this.$emit('close');
}, },
async chooseFolderModal(title: string, initial: string) { async chooseFolderModal(title: string, initial: string) {
@ -66,7 +62,7 @@ export default defineComponent({
.setMultiSelect(false) .setMultiSelect(false)
.setModal(false) .setModal(false)
.setType(FilePickerType.Move) .setType(FilePickerType.Move)
.addMimeTypeFilter("httpd/unix-directory") .addMimeTypeFilter('httpd/unix-directory')
.allowDirectories() .allowDirectories()
.startAt(initial) .startAt(initial)
.build(); .build();
@ -75,10 +71,7 @@ export default defineComponent({
}, },
async chooseFolderPath() { async chooseFolderPath() {
let destination = await this.chooseFolderModal( let destination = await this.chooseFolderModal(this.t('memories', 'Choose a folder'), this.config_foldersPath);
this.t("memories", "Choose a folder"),
this.config_foldersPath
);
// Fails if the target exists, same behavior with Nextcloud files implementation. // Fails if the target exists, same behavior with Nextcloud files implementation.
const gen = dav.movePhotos(this.photos, destination, false); const gen = dav.movePhotos(this.photos, destination, false);
this.processing = true; this.processing = true;
@ -89,15 +82,7 @@ export default defineComponent({
} }
const n = this.photosDone; const n = this.photosDone;
showInfo( showInfo(this.n('memories', '{n} item moved to folder', '{n} items moved to folder', n, { n }));
this.n(
"memories",
"{n} item moved to folder",
"{n} items moved to folder",
n,
{ n }
)
);
this.close(); this.close();
}, },
}, },

View File

@ -9,11 +9,8 @@
{{ path }} {{ path }}
<NcActions :inline="1"> <NcActions :inline="1">
<NcActionButton <NcActionButton :aria-label="t('memories', 'Remove')" @click="remove(index)">
:aria-label="t('memories', 'Remove')" {{ t('memories', 'Remove') }}
@click="remove(index)"
>
{{ t("memories", "Remove") }}
<template #icon> <CloseIcon :size="20" /> </template> <template #icon> <CloseIcon :size="20" /> </template>
</NcActionButton> </NcActionButton>
</NcActions> </NcActions>
@ -22,29 +19,29 @@
<template #buttons> <template #buttons>
<NcButton @click="add" class="button" type="secondary"> <NcButton @click="add" class="button" type="secondary">
{{ t("memories", "Add Path") }} {{ t('memories', 'Add Path') }}
</NcButton> </NcButton>
<NcButton @click="save" class="button" type="primary"> <NcButton @click="save" class="button" type="primary">
{{ t("memories", "Save") }} {{ t('memories', 'Save') }}
</NcButton> </NcButton>
</template> </template>
</Modal> </Modal>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from "vue"; import { defineComponent } from 'vue';
import Modal from "./Modal.vue"; import Modal from './Modal.vue';
import { getFilePickerBuilder } from "@nextcloud/dialogs"; import { getFilePickerBuilder } from '@nextcloud/dialogs';
import NcActions from "@nextcloud/vue/dist/Components/NcActions"; import NcActions from '@nextcloud/vue/dist/Components/NcActions';
import NcActionButton from "@nextcloud/vue/dist/Components/NcActionButton"; import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton';
import NcButton from "@nextcloud/vue/dist/Components/NcButton"; import NcButton from '@nextcloud/vue/dist/Components/NcButton';
import CloseIcon from "vue-material-design-icons/Close.vue"; import CloseIcon from 'vue-material-design-icons/Close.vue';
export default defineComponent({ export default defineComponent({
name: "MultiPathSelectionModal", name: 'MultiPathSelectionModal',
components: { components: {
Modal, Modal,
NcActions, NcActions,
@ -68,7 +65,7 @@ export default defineComponent({
methods: { methods: {
close(list: string[]) { close(list: string[]) {
this.show = false; this.show = false;
this.$emit("close", list); this.$emit('close', list);
}, },
open(paths: string[]) { open(paths: string[]) {
@ -85,7 +82,7 @@ export default defineComponent({
.setMultiSelect(false) .setMultiSelect(false)
.setModal(true) .setModal(true)
.setType(1) .setType(1)
.addMimeTypeFilter("httpd/unix-directory") .addMimeTypeFilter('httpd/unix-directory')
.allowDirectories() .allowDirectories()
.startAt(initial) .startAt(initial)
.build(); .build();
@ -94,11 +91,8 @@ export default defineComponent({
}, },
async add() { async add() {
let newPath = await this.chooseFolder( let newPath = await this.chooseFolder(this.t('memories', 'Add a root to your timeline'), '/');
this.t("memories", "Add a root to your timeline"), if (newPath === '') newPath = '/';
"/"
);
if (newPath === "") newPath = "/";
this.paths.push(newPath); this.paths.push(newPath);
}, },

View File

@ -1,33 +1,18 @@
<template> <template>
<Modal <Modal @close="close" size="normal" v-if="show" :sidebar="!isRoot && !isMobile ? this.filename : null">
@close="close"
size="normal"
v-if="show"
:sidebar="!isRoot && !isMobile ? this.filename : null"
>
<template #title> <template #title>
{{ t("memories", "Link Sharing") }} {{ t('memories', 'Link Sharing') }}
</template> </template>
<div v-if="isRoot"> <div v-if="isRoot">
{{ t("memories", "You cannot share the root folder") }} {{ t('memories', 'You cannot share the root folder') }}
</div> </div>
<div v-else> <div v-else>
{{ {{ t('memories', 'Public link shares are available to people outside Nextcloud.') }}
t(
"memories",
"Public link shares are available to people outside Nextcloud."
)
}}
<br /> <br />
{{ {{ t('memories', 'You may create or update permissions on public links using the sidebar.') }}
t(
"memories",
"You may create or update permissions on public links using the sidebar."
)
}}
<br /> <br />
{{ t("memories", "Click a link to copy to clipboard.") }} {{ t('memories', 'Click a link to copy to clipboard.') }}
</div> </div>
<div class="links"> <div class="links">
@ -49,7 +34,7 @@
</template> </template>
<template #actions> <template #actions>
<NcActionButton @click="deleteLink(share)" :disabled="loading"> <NcActionButton @click="deleteLink(share)" :disabled="loading">
{{ t("memories", "Remove") }} {{ t('memories', 'Remove') }}
<template #icon> <template #icon>
<CloseIcon :size="20" /> <CloseIcon :size="20" />
@ -64,34 +49,34 @@
<template #buttons> <template #buttons>
<NcButton class="primary" :disabled="loading" @click="createLink"> <NcButton class="primary" :disabled="loading" @click="createLink">
{{ t("memories", "Create Link") }} {{ t('memories', 'Create Link') }}
</NcButton> </NcButton>
<NcButton class="primary" :disabled="loading" @click="refreshUrls"> <NcButton class="primary" :disabled="loading" @click="refreshUrls">
{{ t("memories", "Refresh") }} {{ t('memories', 'Refresh') }}
</NcButton> </NcButton>
</template> </template>
</Modal> </Modal>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from "vue"; import { defineComponent } from 'vue';
import axios from "@nextcloud/axios"; import axios from '@nextcloud/axios';
import { showSuccess } from "@nextcloud/dialogs"; import { showSuccess } from '@nextcloud/dialogs';
import UserConfig from "../../mixins/UserConfig"; import UserConfig from '../../mixins/UserConfig';
import NcButton from "@nextcloud/vue/dist/Components/NcButton"; import NcButton from '@nextcloud/vue/dist/Components/NcButton';
import NcLoadingIcon from "@nextcloud/vue/dist/Components/NcLoadingIcon"; import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon';
const NcListItem = () => import("@nextcloud/vue/dist/Components/NcListItem"); const NcListItem = () => import('@nextcloud/vue/dist/Components/NcListItem');
import NcActionButton from "@nextcloud/vue/dist/Components/NcActionButton"; import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton';
import * as utils from "../../services/Utils"; import * as utils from '../../services/Utils';
import Modal from "./Modal.vue"; import Modal from './Modal.vue';
import { API } from "../../services/API"; import { API } from '../../services/API';
import CloseIcon from "vue-material-design-icons/Close.vue"; import CloseIcon from 'vue-material-design-icons/Close.vue';
import LinkIcon from "vue-material-design-icons/LinkVariant.vue"; import LinkIcon from 'vue-material-design-icons/LinkVariant.vue';
type IShare = { type IShare = {
id: string; id: string;
@ -104,7 +89,7 @@ type IShare = {
}; };
export default defineComponent({ export default defineComponent({
name: "NodeShareModal", name: 'NodeShareModal',
components: { components: {
Modal, Modal,
NcButton, NcButton,
@ -120,14 +105,14 @@ export default defineComponent({
data: () => ({ data: () => ({
show: false, show: false,
filename: "", filename: '',
loading: false, loading: false,
shares: [] as IShare[], shares: [] as IShare[],
}), }),
computed: { computed: {
isRoot(): boolean { isRoot(): boolean {
return this.filename === "/" || this.filename === ""; return this.filename === '/' || this.filename === '';
}, },
isMobile(): boolean { isMobile(): boolean {
@ -144,7 +129,7 @@ export default defineComponent({
this.filename = path; this.filename = path;
this.show = true; this.show = true;
this.shares = []; this.shares = [];
globalThis.mSidebar.setTab("sharing"); globalThis.mSidebar.setTab('sharing');
// Get current shares // Get current shares
await this.refreshUrls(); await this.refreshUrls();
@ -154,11 +139,10 @@ export default defineComponent({
// not password protected. Otherwise create a new share. // not password protected. Otherwise create a new share.
if (immediate) { if (immediate) {
let share = let share =
this.shares.find((s) => !s.hasPassword) || this.shares.find((s) => !s.hasPassword) || (this.shares.length === 0 ? await this.createLink(false) : null);
(this.shares.length === 0 ? await this.createLink(false) : null);
if (share) { if (share) {
if ("share" in window.navigator) { if ('share' in window.navigator) {
window.navigator.share({ window.navigator.share({
title: this.filename, title: this.filename,
url: share.url, url: share.url,
@ -172,15 +156,13 @@ export default defineComponent({
close() { close() {
this.show = false; this.show = false;
this.$emit("close"); this.$emit('close');
}, },
async refreshUrls() { async refreshUrls() {
this.loading = true; this.loading = true;
try { try {
this.shares = ( this.shares = (await axios.get(API.Q(API.SHARE_LINKS(), { path: this.filename }))).data;
await axios.get(API.Q(API.SHARE_LINKS(), { path: this.filename }))
).data;
} catch (e) { } catch (e) {
this.shares = []; this.shares = [];
} finally { } finally {
@ -191,24 +173,24 @@ export default defineComponent({
getShareLabels(share: IShare): string { getShareLabels(share: IShare): string {
const labels: string[] = []; const labels: string[] = [];
if (share.hasPassword) { if (share.hasPassword) {
labels.push(this.t("memories", "Password protected")); labels.push(this.t('memories', 'Password protected'));
} }
if (share.expiration) { if (share.expiration) {
const exp = utils.getLongDateStr(new Date(share.expiration * 1000)); const exp = utils.getLongDateStr(new Date(share.expiration * 1000));
const kw = this.t("memories", "Expires"); const kw = this.t('memories', 'Expires');
labels.push(`${kw} ${exp}`); labels.push(`${kw} ${exp}`);
} }
if (share.editable) { if (share.editable) {
labels.push(this.t("memories", "Editable")); labels.push(this.t('memories', 'Editable'));
} }
if (labels.length > 0) { if (labels.length > 0) {
return `${labels.join(", ")}`; return `${labels.join(', ')}`;
} }
return this.t("memories", "Read only"); return this.t('memories', 'Read only');
}, },
async createLink(copy = true): Promise<IShare> { async createLink(copy = true): Promise<IShare> {
@ -244,7 +226,7 @@ export default defineComponent({
copy(url: string) { copy(url: string) {
window.navigator.clipboard.writeText(url); window.navigator.clipboard.writeText(url);
showSuccess(this.t("memories", "Link copied to clipboard")); showSuccess(this.t('memories', 'Link copied to clipboard'));
}, },
refreshSidebar() { refreshSidebar() {

View File

@ -1,7 +1,7 @@
<template> <template>
<Modal @close="close" size="normal" v-if="photo"> <Modal @close="close" size="normal" v-if="photo">
<template #title> <template #title>
{{ t("memories", "Share File") }} {{ t('memories', 'Share File') }}
</template> </template>
<div class="loading-icon fill-block" v-if="loading > 0"> <div class="loading-icon fill-block" v-if="loading > 0">
@ -19,7 +19,7 @@
<PhotoIcon class="avatar" :size="24" /> <PhotoIcon class="avatar" :size="24" />
</template> </template>
<template #subtitle> <template #subtitle>
{{ t("memories", "Share a lower resolution image preview") }} {{ t('memories', 'Share a lower resolution image preview') }}
</template> </template>
</NcListItem> </NcListItem>
@ -35,8 +35,8 @@
<template #subtitle> <template #subtitle>
{{ {{
isVideo isVideo
? t("memories", "Share the video as a high quality MOV") ? t('memories', 'Share the video as a high quality MOV')
: t("memories", "Share the image as a high quality JPEG") : t('memories', 'Share the image as a high quality JPEG')
}} }}
</template> </template>
</NcListItem> </NcListItem>
@ -51,21 +51,16 @@
<FileIcon class="avatar" :size="24" /> <FileIcon class="avatar" :size="24" />
</template> </template>
<template #subtitle> <template #subtitle>
{{ t("memories", "Share the original image / video file") }} {{ t('memories', 'Share the original image / video file') }}
</template> </template>
</NcListItem> </NcListItem>
<NcListItem <NcListItem v-if="canShareLink" :title="t('memories', 'Public Link')" :bold="false" @click.prevent="shareLink()">
v-if="canShareLink"
:title="t('memories', 'Public Link')"
:bold="false"
@click.prevent="shareLink()"
>
<template #icon> <template #icon>
<LinkIcon class="avatar" :size="24" /> <LinkIcon class="avatar" :size="24" />
</template> </template>
<template #subtitle> <template #subtitle>
{{ t("memories", "Share an external Nextcloud link") }} {{ t('memories', 'Share an external Nextcloud link') }}
</template> </template>
</NcListItem> </NcListItem>
</ul> </ul>
@ -73,31 +68,31 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from "vue"; import { defineComponent } from 'vue';
import { showError } from "@nextcloud/dialogs"; import { showError } from '@nextcloud/dialogs';
import { loadState } from "@nextcloud/initial-state"; import { loadState } from '@nextcloud/initial-state';
import axios from "@nextcloud/axios"; import axios from '@nextcloud/axios';
import NcListItem from "@nextcloud/vue/dist/Components/NcListItem"; import NcListItem from '@nextcloud/vue/dist/Components/NcListItem';
import NcLoadingIcon from "@nextcloud/vue/dist/Components/NcLoadingIcon"; import NcLoadingIcon from '@nextcloud/vue/dist/Components/NcLoadingIcon';
import Modal from "./Modal.vue"; import Modal from './Modal.vue';
import { IPhoto } from "../../types"; import { IPhoto } from '../../types';
import { API } from "../../services/API"; import { API } from '../../services/API';
import * as dav from "../../services/DavRequests"; import * as dav from '../../services/DavRequests';
import * as utils from "../../services/Utils"; import * as utils from '../../services/Utils';
import PhotoIcon from "vue-material-design-icons/Image.vue"; import PhotoIcon from 'vue-material-design-icons/Image.vue';
import LargePhotoIcon from "vue-material-design-icons/ImageArea.vue"; import LargePhotoIcon from 'vue-material-design-icons/ImageArea.vue';
import LinkIcon from "vue-material-design-icons/LinkVariant.vue"; import LinkIcon from 'vue-material-design-icons/LinkVariant.vue';
import FileIcon from "vue-material-design-icons/File.vue"; import FileIcon from 'vue-material-design-icons/File.vue';
// Is video transcoding enabled? // Is video transcoding enabled?
const config_vodDisable = loadState("memories", "vod_disable", true); const config_vodDisable = loadState('memories', 'vod_disable', true);
export default defineComponent({ export default defineComponent({
name: "ShareModal", name: 'ShareModal',
components: { components: {
NcListItem, NcListItem,
@ -125,15 +120,11 @@ export default defineComponent({
computed: { computed: {
isVideo() { isVideo() {
return ( return this.photo && (this.photo.mimetype?.startsWith('video/') || this.photo.flag & this.c.FLAG_IS_VIDEO);
this.photo &&
(this.photo.mimetype?.startsWith("video/") ||
this.photo.flag & this.c.FLAG_IS_VIDEO)
);
}, },
canShareNative() { canShareNative() {
return "share" in navigator; return 'share' in navigator;
}, },
canShareHighRes() { canShareHighRes() {
@ -141,7 +132,7 @@ export default defineComponent({
}, },
canShareLink() { canShareLink() {
return this.photo?.imageInfo?.permissions?.includes("S"); return this.photo?.imageInfo?.permissions?.includes('S');
}, },
}, },
@ -166,9 +157,7 @@ export default defineComponent({
async shareHighRes() { async shareHighRes() {
const fileid = this.photo!.fileid; const fileid = this.photo!.fileid;
const src = this.isVideo const src = this.isVideo ? API.VIDEO_TRANSCODE(fileid, 'max.mov') : API.IMAGE_DECODABLE(fileid, this.photo!.etag);
? API.VIDEO_TRANSCODE(fileid, "max.mov")
: API.IMAGE_DECODABLE(fileid, this.photo!.etag);
this.shareWithHref(src, !this.isVideo); this.shareWithHref(src, !this.isVideo);
}, },
@ -187,30 +176,30 @@ export default defineComponent({
async shareWithHref(href: string, replaceExt = false) { async shareWithHref(href: string, replaceExt = false) {
let blob: Blob | undefined; let blob: Blob | undefined;
await this.l(async () => { await this.l(async () => {
const res = await axios.get(href, { responseType: "blob" }); const res = await axios.get(href, { responseType: 'blob' });
blob = res.data; blob = res.data;
}); });
if (!blob) { if (!blob) {
showError(this.t("memories", "Failed to download file")); showError(this.t('memories', 'Failed to download file'));
return; return;
} }
let basename = this.photo?.basename ?? "blank"; let basename = this.photo?.basename ?? 'blank';
if (replaceExt) { if (replaceExt) {
// Fix basename extension // Fix basename extension
let targetExts: string[] = []; let targetExts: string[] = [];
if (blob.type === "image/png") { if (blob.type === 'image/png') {
targetExts = ["png"]; targetExts = ['png'];
} else { } else {
targetExts = ["jpg", "jpeg"]; targetExts = ['jpg', 'jpeg'];
} }
// Append extension if not found // Append extension if not found
const baseExt = basename.split(".").pop()?.toLowerCase() ?? ""; const baseExt = basename.split('.').pop()?.toLowerCase() ?? '';
if (!targetExts.includes(baseExt)) { if (!targetExts.includes(baseExt)) {
basename += "." + targetExts[0]; basename += '.' + targetExts[0];
} }
} }
@ -223,7 +212,7 @@ export default defineComponent({
}; };
if (!(<any>navigator).canShare(data)) { if (!(<any>navigator).canShare(data)) {
showError(this.t("memories", "Cannot share this type of data")); showError(this.t('memories', 'Cannot share this type of data'));
} }
try { try {

View File

@ -2,7 +2,7 @@
<div class="top-matter"> <div class="top-matter">
<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>
@ -22,7 +22,7 @@
@change="changeSort(1)" @change="changeSort(1)"
close-after-click close-after-click
> >
{{ t("memories", "Sort by date") }} {{ t('memories', 'Sort by date') }}
<template #icon> <SortDateIcon :size="20" /> </template> <template #icon> <SortDateIcon :size="20" /> </template>
</NcActionRadio> </NcActionRadio>
@ -33,7 +33,7 @@
@change="changeSort(2)" @change="changeSort(2)"
close-after-click close-after-click
> >
{{ t("memories", "Sort by name") }} {{ t('memories', 'Sort by name') }}
<template #icon> <SlotAlphabeticalIcon :size="20" /> </template> <template #icon> <SlotAlphabeticalIcon :size="20" /> </template>
</NcActionRadio> </NcActionRadio>
</NcActions> </NcActions>
@ -45,7 +45,7 @@
close-after-click close-after-click
v-if="isAlbumList" 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
@ -54,7 +54,7 @@
close-after-click close-after-click
v-if="canEditAlbum" 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
@ -63,7 +63,7 @@
close-after-click close-after-click
v-if="!isAlbumList" 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
@ -72,7 +72,7 @@
close-after-click close-after-click
v-if="canEditAlbum" 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
@ -81,7 +81,7 @@
close-after-click close-after-click
v-if="canEditAlbum" 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>
@ -94,36 +94,36 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from "vue"; import { defineComponent } from 'vue';
import UserConfig from "../../mixins/UserConfig"; import UserConfig from '../../mixins/UserConfig';
import NcActions from "@nextcloud/vue/dist/Components/NcActions"; import NcActions from '@nextcloud/vue/dist/Components/NcActions';
import NcActionButton from "@nextcloud/vue/dist/Components/NcActionButton"; import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton';
import NcActionCheckbox from "@nextcloud/vue/dist/Components/NcActionCheckbox"; import NcActionCheckbox from '@nextcloud/vue/dist/Components/NcActionCheckbox';
import NcActionRadio from "@nextcloud/vue/dist/Components/NcActionRadio"; import NcActionRadio from '@nextcloud/vue/dist/Components/NcActionRadio';
import { getCurrentUser } from "@nextcloud/auth"; import { getCurrentUser } from '@nextcloud/auth';
import axios from "@nextcloud/axios"; import axios from '@nextcloud/axios';
import AlbumCreateModal from "../modal/AlbumCreateModal.vue"; import AlbumCreateModal from '../modal/AlbumCreateModal.vue';
import AlbumDeleteModal from "../modal/AlbumDeleteModal.vue"; import AlbumDeleteModal from '../modal/AlbumDeleteModal.vue';
import AlbumShareModal from "../modal/AlbumShareModal.vue"; import AlbumShareModal from '../modal/AlbumShareModal.vue';
import { downloadWithHandle } from "../../services/dav/download"; import { downloadWithHandle } from '../../services/dav/download';
import BackIcon from "vue-material-design-icons/ArrowLeft.vue"; import BackIcon from 'vue-material-design-icons/ArrowLeft.vue';
import DownloadIcon from "vue-material-design-icons/Download.vue"; import DownloadIcon from 'vue-material-design-icons/Download.vue';
import EditIcon from "vue-material-design-icons/Pencil.vue"; import EditIcon from 'vue-material-design-icons/Pencil.vue';
import DeleteIcon from "vue-material-design-icons/Close.vue"; import DeleteIcon from 'vue-material-design-icons/Close.vue';
import PlusIcon from "vue-material-design-icons/Plus.vue"; import PlusIcon from 'vue-material-design-icons/Plus.vue';
import ShareIcon from "vue-material-design-icons/ShareVariant.vue"; import ShareIcon from 'vue-material-design-icons/ShareVariant.vue';
import SortIcon from "vue-material-design-icons/SortVariant.vue"; import SortIcon from 'vue-material-design-icons/SortVariant.vue';
import SlotAlphabeticalIcon from "vue-material-design-icons/SortAlphabeticalAscending.vue"; import SlotAlphabeticalIcon from 'vue-material-design-icons/SortAlphabeticalAscending.vue';
import SortDateIcon from "vue-material-design-icons/SortCalendarDescending.vue"; import SortDateIcon from 'vue-material-design-icons/SortCalendarDescending.vue';
import { API } from "../../services/API"; import { API } from '../../services/API';
export default defineComponent({ export default defineComponent({
name: "AlbumTopMatter", name: 'AlbumTopMatter',
components: { components: {
NcActions, NcActions,
NcActionButton, NcActionButton,
@ -148,7 +148,7 @@ export default defineComponent({
mixins: [UserConfig], mixins: [UserConfig],
data: () => ({ data: () => ({
name: "", name: '',
}), }),
computed: { computed: {
@ -157,9 +157,7 @@ export default defineComponent({
}, },
canEditAlbum(): boolean { canEditAlbum(): boolean {
return ( return !this.isAlbumList && this.$route.params.user === getCurrentUser()?.uid;
!this.isAlbumList && this.$route.params.user === getCurrentUser()?.uid
);
}, },
}, },
@ -175,20 +173,16 @@ export default defineComponent({
methods: { methods: {
createMatter() { createMatter() {
this.name = this.name = <string>this.$route.params.name || this.t('memories', 'Albums');
<string>this.$route.params.name || this.t("memories", "Albums");
}, },
back() { back() {
this.$router.push({ name: "albums" }); this.$router.push({ name: 'albums' });
}, },
async downloadAlbum() { async downloadAlbum() {
const res = await axios.post( const res = await axios.post(
API.ALBUM_DOWNLOAD( API.ALBUM_DOWNLOAD(<string>this.$route.params.user, <string>this.$route.params.name)
<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);
@ -201,7 +195,7 @@ export default defineComponent({
*/ */
changeSort(order: 1 | 2) { changeSort(order: 1 | 2) {
this.config_albumListSort = order; this.config_albumListSort = order;
this.updateSetting("albumListSort"); this.updateSetting('albumListSort');
}, },
}, },
}); });

View File

@ -2,7 +2,7 @@
<div class="top-matter"> <div class="top-matter">
<NcActions v-if="name"> <NcActions v-if="name">
<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,16 +11,16 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from "vue"; import { defineComponent } from 'vue';
import NcActions from "@nextcloud/vue/dist/Components/NcActions"; import NcActions from '@nextcloud/vue/dist/Components/NcActions';
import NcActionButton from "@nextcloud/vue/dist/Components/NcActionButton"; import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton';
import * as strings from "../../services/strings"; import * as strings from '../../services/strings';
import BackIcon from "vue-material-design-icons/ArrowLeft.vue"; import BackIcon from 'vue-material-design-icons/ArrowLeft.vue';
export default defineComponent({ export default defineComponent({
name: "TagTopMatter", name: 'TagTopMatter',
components: { components: {
NcActions, NcActions,
NcActionButton, NcActionButton,
@ -34,10 +34,10 @@ export default defineComponent({
name(): string | null { name(): string | null {
switch (this.$route.name) { switch (this.$route.name) {
case "tags": case 'tags':
return this.$route.params.name; return this.$route.params.name;
case "places": case 'places':
return this.$route.params.name?.split("-").slice(1).join("-"); return this.$route.params.name?.split('-').slice(1).join('-');
default: default:
return null; return null;
} }

View File

@ -1,8 +1,5 @@
<template> <template>
<NcEmptyContent <NcEmptyContent :title="t('memories', 'Nothing to show here')" :description="emptyViewDescription">
:title="t('memories', 'Nothing to show here')"
:description="emptyViewDescription"
>
<template #icon> <template #icon>
<PeopleIcon v-if="routeIsPeople" /> <PeopleIcon v-if="routeIsPeople" />
<ArchiveIcon v-else-if="routeIsArchive" /> <ArchiveIcon v-else-if="routeIsArchive" />
@ -12,18 +9,18 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from "vue"; import { defineComponent } from 'vue';
import NcEmptyContent from "@nextcloud/vue/dist/Components/NcEmptyContent"; import NcEmptyContent from '@nextcloud/vue/dist/Components/NcEmptyContent';
import PeopleIcon from "vue-material-design-icons/AccountMultiple.vue"; import PeopleIcon from 'vue-material-design-icons/AccountMultiple.vue';
import ImageMultipleIcon from "vue-material-design-icons/ImageMultiple.vue"; import ImageMultipleIcon from 'vue-material-design-icons/ImageMultiple.vue';
import ArchiveIcon from "vue-material-design-icons/PackageDown.vue"; import ArchiveIcon from 'vue-material-design-icons/PackageDown.vue';
import * as strings from "../../services/strings"; import * as strings from '../../services/strings';
export default defineComponent({ export default defineComponent({
name: "EmptyContent", name: 'EmptyContent',
components: { components: {
NcEmptyContent, NcEmptyContent,
@ -39,14 +36,11 @@ export default defineComponent({
}, },
routeIsPeople(): boolean { routeIsPeople(): boolean {
return ( return this.$route.name === 'recognize' || this.$route.name === 'facerecognition';
this.$route.name === "recognize" ||
this.$route.name === "facerecognition"
);
}, },
routeIsArchive(): boolean { routeIsArchive(): boolean {
return this.$route.name === "archive"; return this.$route.name === 'archive';
}, },
}, },
}); });

View File

@ -2,7 +2,7 @@
<div v-if="name" class="face-top-matter"> <div v-if="name" class="face-top-matter">
<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,12 +11,8 @@
<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')" {{ t('memories', 'Rename person') }}
@click="$refs.editModal?.open()"
close-after-click
>
{{ t("memories", "Rename person") }}
<template #icon> <EditIcon :size="20" /> </template> <template #icon> <EditIcon :size="20" /> </template>
</NcActionButton> </NcActionButton>
<NcActionButton <NcActionButton
@ -24,7 +20,7 @@
@click="$refs.mergeModal?.open()" @click="$refs.mergeModal?.open()"
close-after-click 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
@ -32,14 +28,14 @@
:checked.sync="config_showFaceRect" :checked.sync="config_showFaceRect"
@change="changeShowFaceRect" @change="changeShowFaceRect"
> >
{{ t("memories", "Mark person in preview") }} {{ t('memories', 'Mark person in preview') }}
</NcActionCheckbox> </NcActionCheckbox>
<NcActionButton <NcActionButton
:aria-label="t('memories', 'Remove person')" :aria-label="t('memories', 'Remove person')"
@click="$refs.deleteModal?.open()" @click="$refs.deleteModal?.open()"
close-after-click 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>
@ -52,23 +48,23 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from "vue"; import { defineComponent } from 'vue';
import UserConfig from "../../mixins/UserConfig"; import UserConfig from '../../mixins/UserConfig';
import NcActions from "@nextcloud/vue/dist/Components/NcActions"; import NcActions from '@nextcloud/vue/dist/Components/NcActions';
import NcActionButton from "@nextcloud/vue/dist/Components/NcActionButton"; import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton';
import NcActionCheckbox from "@nextcloud/vue/dist/Components/NcActionCheckbox"; import NcActionCheckbox from '@nextcloud/vue/dist/Components/NcActionCheckbox';
import FaceEditModal from "../modal/FaceEditModal.vue"; import FaceEditModal from '../modal/FaceEditModal.vue';
import FaceDeleteModal from "../modal/FaceDeleteModal.vue"; import FaceDeleteModal from '../modal/FaceDeleteModal.vue';
import FaceMergeModal from "../modal/FaceMergeModal.vue"; import FaceMergeModal from '../modal/FaceMergeModal.vue';
import BackIcon from "vue-material-design-icons/ArrowLeft.vue"; import BackIcon from 'vue-material-design-icons/ArrowLeft.vue';
import EditIcon from "vue-material-design-icons/Pencil.vue"; import EditIcon from 'vue-material-design-icons/Pencil.vue';
import DeleteIcon from "vue-material-design-icons/Close.vue"; import DeleteIcon from 'vue-material-design-icons/Close.vue';
import MergeIcon from "vue-material-design-icons/Merge.vue"; import MergeIcon from 'vue-material-design-icons/Merge.vue';
export default defineComponent({ export default defineComponent({
name: "FaceTopMatter", name: 'FaceTopMatter',
components: { components: {
NcActions, NcActions,
NcActionButton, NcActionButton,
@ -85,7 +81,7 @@ export default defineComponent({
mixins: [UserConfig], mixins: [UserConfig],
data: () => ({ data: () => ({
name: "", name: '',
}), }),
watch: { watch: {
@ -100,7 +96,7 @@ export default defineComponent({
methods: { methods: {
createMatter() { createMatter() {
this.name = <string>this.$route.params.name || ""; this.name = <string>this.$route.params.name || '';
}, },
back() { back() {
@ -108,10 +104,7 @@ export default defineComponent({
}, },
changeShowFaceRect() { changeShowFaceRect() {
localStorage.setItem( localStorage.setItem('memories_showFaceRect', this.config_showFaceRect ? '1' : '0');
"memories_showFaceRect",
this.config_showFaceRect ? "1" : "0"
);
setTimeout(() => { setTimeout(() => {
this.$router.go(0); // refresh page this.$router.go(0); // refresh page
}, 500); }, 500);

View File

@ -16,26 +16,15 @@
<div class="right-actions"> <div class="right-actions">
<NcActions :inline="2"> <NcActions :inline="2">
<NcActionRouter <NcActionRouter :to="{ query: recursive ? {} : { recursive: '1' } }" close-after-click>
:to="{ query: recursive ? {} : { recursive: '1' } }" {{ recursive ? t('memories', 'Folder View') : t('memories', 'Timeline View') }}
close-after-click
>
{{
recursive
? t("memories", "Folder View")
: t("memories", "Timeline View")
}}
<template #icon> <template #icon>
<FoldersIcon v-if="recursive" :size="20" /> <FoldersIcon v-if="recursive" :size="20" />
<TimelineIcon v-else :size="20" /> <TimelineIcon v-else :size="20" />
</template> </template>
</NcActionRouter> </NcActionRouter>
<NcActionButton <NcActionButton :aria-label="t('memories', 'Share folder')" @click="share()" close-after-click>
:aria-label="t('memories', 'Share folder')" {{ t('memories', 'Share folder') }}
@click="share()"
close-after-click
>
{{ t("memories", "Share folder") }}
<template #icon> <ShareIcon :size="20" /> </template> <template #icon> <ShareIcon :size="20" /> </template>
</NcActionButton> </NcActionButton>
</NcActions> </NcActions>
@ -44,27 +33,25 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from "vue"; import { defineComponent } from 'vue';
import { TopMatterFolder, TopMatterType } from "../../types"; import { TopMatterFolder, TopMatterType } from '../../types';
import UserConfig from "../../mixins/UserConfig"; import UserConfig from '../../mixins/UserConfig';
const NcBreadcrumbs = () => const NcBreadcrumbs = () => import('@nextcloud/vue/dist/Components/NcBreadcrumbs');
import("@nextcloud/vue/dist/Components/NcBreadcrumbs"); const NcBreadcrumb = () => import('@nextcloud/vue/dist/Components/NcBreadcrumb');
const NcBreadcrumb = () => import NcActions from '@nextcloud/vue/dist/Components/NcActions';
import("@nextcloud/vue/dist/Components/NcBreadcrumb"); import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton';
import NcActions from "@nextcloud/vue/dist/Components/NcActions"; import NcActionRouter from '@nextcloud/vue/dist/Components/NcActionRouter';
import NcActionButton from "@nextcloud/vue/dist/Components/NcActionButton";
import NcActionRouter from "@nextcloud/vue/dist/Components/NcActionRouter";
import * as utils from "../../services/Utils"; import * as utils from '../../services/Utils';
import HomeIcon from "vue-material-design-icons/Home.vue"; import HomeIcon from 'vue-material-design-icons/Home.vue';
import ShareIcon from "vue-material-design-icons/ShareVariant.vue"; import ShareIcon from 'vue-material-design-icons/ShareVariant.vue';
import TimelineIcon from "vue-material-design-icons/ImageMultiple.vue"; import TimelineIcon from 'vue-material-design-icons/ImageMultiple.vue';
import FoldersIcon from "vue-material-design-icons/FolderMultiple.vue"; import FoldersIcon from 'vue-material-design-icons/FolderMultiple.vue';
export default defineComponent({ export default defineComponent({
name: "FolderTopMatter", name: 'FolderTopMatter',
components: { components: {
NcBreadcrumbs, NcBreadcrumbs,
NcBreadcrumb, NcBreadcrumb,
@ -96,10 +83,10 @@ export default defineComponent({
methods: { methods: {
createMatter() { createMatter() {
if (this.$route.name === "folders") { if (this.$route.name === 'folders') {
let path: any = this.$route.params.path || ""; let path: any = this.$route.params.path || '';
if (typeof path === "string") { if (typeof path === 'string') {
path = path.split("/"); path = path.split('/');
} }
this.topMatter = { this.topMatter = {
@ -109,11 +96,11 @@ export default defineComponent({
.map((x, idx, arr) => { .map((x, idx, arr) => {
return { return {
text: x, text: x,
path: arr.slice(0, idx + 1).join("/"), path: arr.slice(0, idx + 1).join('/'),
}; };
}), }),
}; };
this.recursive = this.$route.query.recursive === "1"; this.recursive = this.$route.query.recursive === '1';
} else { } else {
this.topMatter = null; this.topMatter = null;
this.recursive = false; this.recursive = false;
@ -121,9 +108,7 @@ export default defineComponent({
}, },
share() { share() {
globalThis.shareNodeLink( globalThis.shareNodeLink(utils.getFolderRoutePath(this.config_foldersPath));
utils.getFolderRoutePath(this.config_foldersPath)
);
}, },
}, },
}); });

View File

@ -16,12 +16,7 @@
:options="mapOptions" :options="mapOptions"
> >
<LTileLayer :url="tileurl" :attribution="attribution" :noWrap="true" /> <LTileLayer :url="tileurl" :attribution="attribution" :noWrap="true" />
<LMarker <LMarker v-for="cluster of clusters" :key="cluster.id" :lat-lng="cluster.center" @click="zoomTo(cluster)">
v-for="cluster of clusters"
:key="cluster.id"
:lat-lng="cluster.center"
@click="zoomTo(cluster)"
>
<LIcon :icon-anchor="[24, 24]" :className="clusterIconClass(cluster)"> <LIcon :icon-anchor="[24, 24]" :className="clusterIconClass(cluster)">
<div class="preview"> <div class="preview">
<div class="count" v-if="cluster.count > 1"> <div class="count" v-if="cluster.count > 1">
@ -30,10 +25,7 @@
<XImg <XImg
v-once v-once
:src="clusterPreviewUrl(cluster)" :src="clusterPreviewUrl(cluster)"
:class="[ :class="['thumb-important', `memories-thumb-${cluster.preview.fileid}`]"
'thumb-important',
`memories-thumb-${cluster.preview.fileid}`,
]"
/> />
</div> </div>
</LIcon> </LIcon>
@ -43,23 +35,22 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from "vue"; import { defineComponent } from 'vue';
import { LMap, LTileLayer, LMarker, LPopup, LIcon } from "vue2-leaflet"; import { LMap, LTileLayer, LMarker, LPopup, LIcon } from 'vue2-leaflet';
import { latLngBounds, Icon } from "leaflet"; import { latLngBounds, Icon } from 'leaflet';
import { IPhoto } from "../../types"; import { IPhoto } from '../../types';
import axios from "@nextcloud/axios"; import axios from '@nextcloud/axios';
import { subscribe, unsubscribe } from "@nextcloud/event-bus"; import { subscribe, unsubscribe } from '@nextcloud/event-bus';
import { API } from "../../services/API"; import { API } from '../../services/API';
import * as utils from "../../services/Utils"; import * as utils from '../../services/Utils';
import "leaflet/dist/leaflet.css"; import 'leaflet/dist/leaflet.css';
import "leaflet-edgebuffer"; import 'leaflet-edgebuffer';
const OSM_TILE_URL = "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"; const OSM_TILE_URL = 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png';
const OSM_ATTRIBUTION = const OSM_ATTRIBUTION = '&copy; <a target="_blank" href="http://osm.org/copyright">OpenStreetMap</a> contributors';
'&copy; <a target="_blank" href="http://osm.org/copyright">OpenStreetMap</a> contributors';
// CSS transition time for zooming in/out cluster animation // CSS transition time for zooming in/out cluster animation
const CLUSTER_TRANSITION_TIME = 300; const CLUSTER_TRANSITION_TIME = 300;
@ -75,13 +66,13 @@ type IMarkerCluster = {
delete (<any>Icon.Default.prototype)._getIconUrl; delete (<any>Icon.Default.prototype)._getIconUrl;
Icon.Default.mergeOptions({ Icon.Default.mergeOptions({
iconRetinaUrl: require("leaflet/dist/images/marker-icon-2x.png"), iconRetinaUrl: require('leaflet/dist/images/marker-icon-2x.png'),
iconUrl: require("leaflet/dist/images/marker-icon.png"), iconUrl: require('leaflet/dist/images/marker-icon.png'),
shadowUrl: require("leaflet/dist/images/marker-shadow.png"), shadowUrl: require('leaflet/dist/images/marker-shadow.png'),
}); });
export default defineComponent({ export default defineComponent({
name: "MapSplitMatter", name: 'MapSplitMatter',
components: { components: {
LMap, LMap,
LTileLayer, LTileLayer,
@ -106,23 +97,22 @@ export default defineComponent({
const map = this.$refs.map as LMap; const map = this.$refs.map as LMap;
// Make sure the zoom control doesn't overlap with the navbar // Make sure the zoom control doesn't overlap with the navbar
map.mapObject.zoomControl.setPosition("topright"); map.mapObject.zoomControl.setPosition('topright');
// Initialize // Initialize
this.initialize(); this.initialize();
// If currently dark mode, set isDark // If currently dark mode, set isDark
const pane = document.querySelector(".leaflet-tile-pane"); const pane = document.querySelector('.leaflet-tile-pane');
this.isDark = this.isDark = !pane || window.getComputedStyle(pane)?.['filter']?.includes('invert');
!pane || window.getComputedStyle(pane)?.["filter"]?.includes("invert");
}, },
created() { created() {
subscribe("memories:window:resize", this.handleContainerResize); subscribe('memories:window:resize', this.handleContainerResize);
}, },
beforeDestroy() { beforeDestroy() {
unsubscribe("memories:window:resize", this.handleContainerResize); unsubscribe('memories:window:resize', this.handleContainerResize);
}, },
computed: { computed: {
@ -164,7 +154,7 @@ export default defineComponent({
const map = this.$refs.map as LMap; const map = this.$refs.map as LMap;
const pos = init?.data?.pos; const pos = init?.data?.pos;
if (!pos?.lat || !pos?.lon) { if (!pos?.lat || !pos?.lon) {
throw new Error("No position data"); throw new Error('No position data');
} }
// This will trigger route change -> fetchClusters // This will trigger route change -> fetchClusters
@ -177,7 +167,7 @@ export default defineComponent({
}, },
async refreshDebounced() { async refreshDebounced() {
utils.setRenewingTimeout(this, "refreshTimer", this.refresh, 250); utils.setRenewingTimeout(this, 'refreshTimer', this.refresh, 250);
}, },
async refresh() { async refresh() {
@ -219,8 +209,7 @@ export default defineComponent({
const oldZoom = this.oldZoom; const oldZoom = this.oldZoom;
const qbounds = this.$route.query.b; const qbounds = this.$route.query.b;
const zoom = this.$route.query.z; const zoom = this.$route.query.z;
const paramsChanged = () => const paramsChanged = () => this.$route.query.b !== qbounds || this.$route.query.z !== zoom;
this.$route.query.b !== qbounds || this.$route.query.z !== zoom;
let { minLat, maxLat, minLon, maxLon } = this.boundsFromQuery(); let { minLat, maxLat, minLon, maxLon } = this.boundsFromQuery();
@ -258,7 +247,7 @@ export default defineComponent({
}, },
boundsFromQuery() { boundsFromQuery() {
const bounds = (this.$route.query.b as string).split(","); const bounds = (this.$route.query.b as string).split(',');
return { return {
minLat: parseFloat(bounds[0]), minLat: parseFloat(bounds[0]),
maxLat: parseFloat(bounds[1]), maxLat: parseFloat(bounds[1]),
@ -296,7 +285,7 @@ export default defineComponent({
}, },
clusterIconClass(cluster: IMarkerCluster) { clusterIconClass(cluster: IMarkerCluster) {
return cluster.dummy ? "dummy" : ""; return cluster.dummy ? 'dummy' : '';
}, },
zoomTo(cluster: IMarkerCluster) { zoomTo(cluster: IMarkerCluster) {

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)"
>
<XImg class="fill-block" :src="year.url" /> <XImg class="fill-block" :src="year.url" />
<div class="overlay"> <div class="overlay">
@ -17,22 +12,16 @@
<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')" {{ t('memories', 'Move left') }}
@click="moveLeft"
>
{{ t("memories", "Move left") }}
<template #icon> <LeftMoveIcon v-once :size="28" /> </template> <template #icon> <LeftMoveIcon v-once :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')" {{ t('memories', 'Move right') }}
@click="moveRight"
>
{{ t("memories", "Move right") }}
<template #icon> <RightMoveIcon v-once :size="28" /> </template> <template #icon> <RightMoveIcon v-once :size="28" /> </template>
</NcActionButton> </NcActionButton>
</NcActions> </NcActions>
@ -41,17 +30,17 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from "vue"; import { defineComponent } from 'vue';
import NcActions from "@nextcloud/vue/dist/Components/NcActions"; import NcActions from '@nextcloud/vue/dist/Components/NcActions';
import NcActionButton from "@nextcloud/vue/dist/Components/NcActionButton"; import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton';
import * as utils from "../../services/Utils"; import * as utils from '../../services/Utils';
import * as dav from "../../services/DavRequests"; import * as dav from '../../services/DavRequests';
import { IPhoto } from "../../types"; import { IPhoto } from '../../types';
import LeftMoveIcon from "vue-material-design-icons/ChevronLeft.vue"; import LeftMoveIcon from 'vue-material-design-icons/ChevronLeft.vue';
import RightMoveIcon from "vue-material-design-icons/ChevronRight.vue"; import RightMoveIcon from 'vue-material-design-icons/ChevronRight.vue';
interface IYear { interface IYear {
year: number; year: number;
@ -62,7 +51,7 @@ interface IYear {
} }
export default defineComponent({ export default defineComponent({
name: "OnThisDay", name: 'OnThisDay',
components: { components: {
NcActions, NcActions,
NcActionButton, NcActionButton,
@ -87,7 +76,7 @@ export default defineComponent({
mounted() { mounted() {
const inner = this.$refs.inner as HTMLElement; const inner = this.$refs.inner as HTMLElement;
inner.addEventListener("scroll", this.onScroll.bind(this), { inner.addEventListener('scroll', this.onScroll.bind(this), {
passive: true, passive: true,
}); });
@ -103,7 +92,7 @@ export default defineComponent({
methods: { methods: {
onload() { onload() {
this.$emit("load"); this.$emit('load');
}, },
async refresh() { async refresh() {
@ -118,11 +107,7 @@ export default defineComponent({
utils.cacheData(cacheUrl, photos); utils.cacheData(cacheUrl, photos);
// Check if exactly same as cache // Check if exactly same as cache
if ( if (cache?.length === photos.length && cache.every((p, i) => p.fileid === photos[i].fileid)) return;
cache?.length === photos.length &&
cache.every((p, i) => p.fileid === photos[i].fileid)
)
return;
this.process(photos); this.process(photos);
}, },
@ -130,7 +115,7 @@ export default defineComponent({
this.years = []; this.years = [];
let currentYear = 9999; let currentYear = 9999;
let currentText = ""; let currentText = '';
for (const photo of photos) { for (const photo of photos) {
const dateTaken = utils.dayIdToDate(photo.dayid); const dateTaken = utils.dayIdToDate(photo.dayid);
@ -145,7 +130,7 @@ export default defineComponent({
this.years.push({ this.years.push({
year, year,
text, text,
url: "", url: '',
preview: null!, preview: null!,
photos: [], photos: [],
}); });
@ -193,13 +178,8 @@ export default defineComponent({
.map((c) => c.getBoundingClientRect()) .map((c) => c.getBoundingClientRect())
.find((rect) => rect.right > innerRect.right); .find((rect) => rect.right > innerRect.right);
let scroll = nextChild let scroll = nextChild ? nextChild.left - innerRect.left : inner.clientWidth;
? nextChild.left - innerRect.left scroll = Math.min(inner.scrollWidth - inner.scrollLeft - inner.clientWidth, scroll);
: inner.clientWidth;
scroll = Math.min(
inner.scrollWidth - inner.scrollLeft - inner.clientWidth,
scroll
);
this.scrollStack.push(scroll); this.scrollStack.push(scroll);
inner.scrollBy(scroll, 0); inner.scrollBy(scroll, 0);
}, },
@ -208,8 +188,7 @@ export default defineComponent({
const inner = this.$refs.inner as HTMLElement; const inner = this.$refs.inner as HTMLElement;
if (!inner) return; if (!inner) return;
this.hasLeft = inner.scrollLeft > 0; this.hasLeft = inner.scrollLeft > 0;
this.hasRight = this.hasRight = inner.clientWidth + inner.scrollLeft < inner.scrollWidth - 20;
inner.clientWidth + inner.scrollLeft < inner.scrollWidth - 20;
}, },
click(year: IYear) { click(year: IYear) {

View File

@ -8,17 +8,17 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from "vue"; import { defineComponent } from 'vue';
import FolderTopMatter from "./FolderTopMatter.vue"; import FolderTopMatter from './FolderTopMatter.vue';
import ClusterTopMatter from "./ClusterTopMatter.vue"; import ClusterTopMatter from './ClusterTopMatter.vue';
import FaceTopMatter from "./FaceTopMatter.vue"; import FaceTopMatter from './FaceTopMatter.vue';
import AlbumTopMatter from "./AlbumTopMatter.vue"; import AlbumTopMatter from './AlbumTopMatter.vue';
import { TopMatterType } from "../../types"; import { TopMatterType } from '../../types';
export default defineComponent({ export default defineComponent({
name: "TopMatter", name: 'TopMatter',
components: { components: {
FolderTopMatter, FolderTopMatter,
ClusterTopMatter, ClusterTopMatter,
@ -45,18 +45,16 @@ export default defineComponent({
setTopMatter() { setTopMatter() {
this.type = (() => { this.type = (() => {
switch (this.$route.name) { switch (this.$route.name) {
case "folders": case 'folders':
return TopMatterType.FOLDER; return TopMatterType.FOLDER;
case "albums": case 'albums':
return TopMatterType.ALBUM; return TopMatterType.ALBUM;
case "tags": case 'tags':
case "places": case 'places':
return TopMatterType.CLUSTER; return TopMatterType.CLUSTER;
case "recognize": case 'recognize':
case "facerecognition": case 'facerecognition':
return this.$route.params.name return this.$route.params.name ? TopMatterType.FACE : TopMatterType.CLUSTER;
? TopMatterType.FACE
: TopMatterType.CLUSTER;
default: default:
return TopMatterType.NONE; return TopMatterType.NONE;
} }

View File

@ -1,35 +1,30 @@
<template> <template>
<div <div v-bind="themeDataAttr" ref="editor" class="viewer__image-editor" :class="{ loading: !imageEditor }" />
v-bind="themeDataAttr"
ref="editor"
class="viewer__image-editor"
:class="{ loading: !imageEditor }"
/>
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent, PropType } from "vue"; import { defineComponent, PropType } from 'vue';
import { emit } from "@nextcloud/event-bus"; import { emit } from '@nextcloud/event-bus';
import { showError, showSuccess } from "@nextcloud/dialogs"; import { showError, showSuccess } from '@nextcloud/dialogs';
import axios from "@nextcloud/axios"; import axios from '@nextcloud/axios';
import { FilerobotImageEditorConfig } from "react-filerobot-image-editor"; import { FilerobotImageEditorConfig } from 'react-filerobot-image-editor';
import translations from "./ImageEditorTranslations"; import translations from './ImageEditorTranslations';
import { API } from "../../services/API"; import { API } from '../../services/API';
import { IImageInfo, IPhoto } from "../../types"; import { IImageInfo, IPhoto } from '../../types';
import * as utils from "../../services/Utils"; import * as utils from '../../services/Utils';
import { fetchImage } from "../frame/XImgCache"; import { fetchImage } from '../frame/XImgCache';
let TABS, TOOLS: any; let TABS, TOOLS: any;
type FilerobotImageEditor = import("filerobot-image-editor").default; type FilerobotImageEditor = import('filerobot-image-editor').default;
let FilerobotImageEditor: typeof import("filerobot-image-editor").default; let FilerobotImageEditor: typeof import('filerobot-image-editor').default;
async function loadFilerobot() { async function loadFilerobot() {
if (!FilerobotImageEditor) { if (!FilerobotImageEditor) {
FilerobotImageEditor = (await import("filerobot-image-editor")).default; FilerobotImageEditor = (await import('filerobot-image-editor')).default;
TABS = (<any>FilerobotImageEditor).TABS; TABS = (<any>FilerobotImageEditor).TABS;
TOOLS = (<any>FilerobotImageEditor).TOOLS; TOOLS = (<any>FilerobotImageEditor).TOOLS;
} }
@ -54,7 +49,7 @@ export default defineComponent({
return { return {
source: source:
this.photo.h && this.photo.w this.photo.h && this.photo.w
? utils.getPreviewUrl(this.photo, false, "screen") ? utils.getPreviewUrl(this.photo, false, 'screen')
: API.IMAGE_DECODABLE(this.photo.fileid, this.photo.etag), : API.IMAGE_DECODABLE(this.photo.fileid, this.photo.etag),
defaultSavedImageName: this.defaultSavedImageName, defaultSavedImageName: this.defaultSavedImageName,
@ -81,7 +76,7 @@ export default defineComponent({
Rotate: { Rotate: {
angle: 90, angle: 90,
componentType: "buttons", componentType: 'buttons',
}, },
// Translations // Translations
@ -89,24 +84,24 @@ export default defineComponent({
theme: { theme: {
palette: { palette: {
"bg-secondary": "var(--color-main-background)", 'bg-secondary': 'var(--color-main-background)',
"bg-primary": "var(--color-background-dark)", 'bg-primary': 'var(--color-background-dark)',
// Accent // Accent
"accent-primary": "var(--color-primary)", 'accent-primary': 'var(--color-primary)',
// Use by the slider // Use by the slider
"border-active-bottom": "var(--color-primary)", 'border-active-bottom': 'var(--color-primary)',
"icons-primary": "var(--color-main-text)", 'icons-primary': 'var(--color-main-text)',
// Active state // Active state
"bg-primary-active": "var(--color-background-dark)", 'bg-primary-active': 'var(--color-background-dark)',
"bg-primary-hover": "var(--color-background-hover)", 'bg-primary-hover': 'var(--color-background-hover)',
"accent-primary-active": "var(--color-main-text)", 'accent-primary-active': 'var(--color-main-text)',
// Used by the save button // Used by the save button
"accent-primary-hover": "var(--color-primary)", 'accent-primary-hover': 'var(--color-primary)',
warning: "var(--color-error)", warning: 'var(--color-error)',
}, },
typography: { typography: {
fontFamily: "var(--font-face)", fontFamily: 'var(--font-face)',
}, },
}, },
@ -116,31 +111,29 @@ export default defineComponent({
}, },
defaultSavedImageName(): string { defaultSavedImageName(): string {
return this.photo.basename || ""; return this.photo.basename || '';
}, },
defaultSavedImageType(): "jpeg" | "png" | "webp" { defaultSavedImageType(): 'jpeg' | 'png' | 'webp' {
if ( if (['image/jpeg', 'image/png', 'image/webp'].includes(this.photo.mimetype!)) {
["image/jpeg", "image/png", "image/webp"].includes(this.photo.mimetype!) return this.photo.mimetype!.split('/')[1] as any;
) {
return this.photo.mimetype!.split("/")[1] as any;
} }
return "jpeg"; return 'jpeg';
}, },
hasHighContrastEnabled(): boolean { hasHighContrastEnabled(): boolean {
const themes = globalThis.OCA?.Theming?.enabledThemes || []; const themes = globalThis.OCA?.Theming?.enabledThemes || [];
return themes.find((theme) => theme.indexOf("highcontrast") !== -1); return themes.find((theme) => theme.indexOf('highcontrast') !== -1);
}, },
themeDataAttr(): Record<string, boolean> { themeDataAttr(): Record<string, boolean> {
if (this.hasHighContrastEnabled) { if (this.hasHighContrastEnabled) {
return { return {
"data-theme-dark-highcontrast": true, 'data-theme-dark-highcontrast': true,
}; };
} }
return { return {
"data-theme-dark": true, 'data-theme-dark': true,
}; };
}, },
}, },
@ -155,7 +148,7 @@ export default defineComponent({
this.imageEditor.render(); this.imageEditor.render();
// Handle keyboard // Handle keyboard
window.addEventListener("keydown", this.handleKeydown, true); window.addEventListener('keydown', this.handleKeydown, true);
}, },
beforeDestroy() { beforeDestroy() {
@ -163,7 +156,7 @@ export default defineComponent({
this.imageEditor.terminate(); this.imageEditor.terminate();
} }
globalThis._fileRobotOverrideImage = undefined; globalThis._fileRobotOverrideImage = undefined;
window.removeEventListener("keydown", this.handleKeydown, true); window.removeEventListener('keydown', this.handleKeydown, true);
}, },
methods: { methods: {
@ -189,8 +182,8 @@ export default defineComponent({
this.onExitWithoutSaving(); this.onExitWithoutSaving();
return; return;
} }
window.removeEventListener("keydown", this.handleKeydown, true); window.removeEventListener('keydown', this.handleKeydown, true);
this.$emit("close"); this.$emit('close');
}, },
/** /**
@ -229,36 +222,33 @@ export default defineComponent({
// Make sure we have an extension // Make sure we have an extension
let name = data.name; let name = data.name;
const nameLower = name.toLowerCase(); const nameLower = name.toLowerCase();
if (!nameLower.endsWith(data.extension) && !nameLower.endsWith(".jpg")) { if (!nameLower.endsWith(data.extension) && !nameLower.endsWith('.jpg')) {
name += "." + data.extension; name += '.' + data.extension;
} }
try { try {
const res = await axios.put<IImageInfo>( const res = await axios.put<IImageInfo>(API.IMAGE_EDIT(this.photo.fileid), {
API.IMAGE_EDIT(this.photo.fileid),
{
name: name, name: name,
width: data.width, width: data.width,
height: data.height, height: data.height,
quality: data.quality, quality: data.quality,
extension: data.extension, extension: data.extension,
state: state, state: state,
} });
);
const fileid = res.data.fileid; const fileid = res.data.fileid;
// Success, emit an appropriate event // Success, emit an appropriate event
showSuccess(this.t("memories", "Image saved successfully")); showSuccess(this.t('memories', 'Image saved successfully'));
if (fileid !== this.photo.fileid) { if (fileid !== this.photo.fileid) {
emit("files:file:created", { fileid }); emit('files:file:created', { fileid });
} else { } else {
utils.updatePhotoFromImageInfo(this.photo, res.data); utils.updatePhotoFromImageInfo(this.photo, res.data);
emit("files:file:updated", { fileid }); emit('files:file:updated', { fileid });
} }
this.onClose(undefined, false); this.onClose(undefined, false);
} catch (err) { } catch (err) {
showError(this.t("memories", "Error saving image")); showError(this.t('memories', 'Error saving image'));
console.error(err); console.error(err);
} }
}, },
@ -268,21 +258,19 @@ export default defineComponent({
*/ */
onExitWithoutSaving() { onExitWithoutSaving() {
(<any>OC.dialogs).confirmDestructive( (<any>OC.dialogs).confirmDestructive(
translations.changesLoseConfirmation + translations.changesLoseConfirmation + '\n\n' + translations.changesLoseConfirmationHint,
"\n\n" + this.t('memories', 'Unsaved changes'),
translations.changesLoseConfirmationHint,
this.t("memories", "Unsaved changes"),
{ {
type: (<any>OC.dialogs).YES_NO_BUTTONS, type: (<any>OC.dialogs).YES_NO_BUTTONS,
confirm: this.t("memories", "Drop changes"), confirm: this.t('memories', 'Drop changes'),
confirmClasses: "error", confirmClasses: 'error',
cancel: translations.cancel, cancel: translations.cancel,
}, },
(decision) => { (decision) => {
if (!decision) { if (!decision) {
return; return;
} }
this.onClose("warning-ignored", false); this.onClose('warning-ignored', false);
} }
); );
}, },
@ -291,29 +279,23 @@ export default defineComponent({
handleKeydown(event) { handleKeydown(event) {
event.stopImmediatePropagation(); event.stopImmediatePropagation();
// escape key // escape key
if (event.key === "Escape") { if (event.key === 'Escape') {
// Since we cannot call the closeMethod and know if there // Since we cannot call the closeMethod and know if there
// are unsaved changes, let's fake a close button trigger. // are unsaved changes, let's fake a close button trigger.
event.preventDefault(); event.preventDefault();
( (document.querySelector('.FIE_topbar-close-button') as HTMLElement).click();
document.querySelector(".FIE_topbar-close-button") as HTMLElement
).click();
} }
// ctrl + S = save // ctrl + S = save
if (event.ctrlKey && event.key === "s") { if (event.ctrlKey && event.key === 's') {
event.preventDefault(); event.preventDefault();
( (document.querySelector('.FIE_topbar-save-button') as HTMLElement).click();
document.querySelector(".FIE_topbar-save-button") as HTMLElement
).click();
} }
// ctrl + Z = undo // ctrl + Z = undo
if (event.ctrlKey && event.key === "z") { if (event.ctrlKey && event.key === 'z') {
event.preventDefault(); event.preventDefault();
( (document.querySelector('.FIE_topbar-undo-button') as HTMLElement).click();
document.querySelector(".FIE_topbar-undo-button") as HTMLElement
).click();
} }
}, },
}, },
@ -390,7 +372,7 @@ 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,
@ -399,7 +381,7 @@ export default defineComponent({
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,
@ -423,7 +405,7 @@ export default defineComponent({
} }
// Disable jpeg saving (jpg is already here) // Disable jpeg saving (jpg is already here)
&[value="jpeg"] { &[value='jpeg'] {
display: none; display: none;
} }
} }
@ -500,7 +482,7 @@ export default defineComponent({
background-color: var(--color-background-hover) !important; background-color: var(--color-background-hover) !important;
} }
&[aria-selected="true"] { &[aria-selected='true'] {
color: var(--color-main-text); color: var(--color-main-text);
background-color: var(--color-background-dark); background-color: var(--color-background-dark);
box-shadow: 0 0 0 2px var(--color-primary-element); box-shadow: 0 0 0 2px var(--color-primary-element);
@ -514,8 +496,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;
@ -634,7 +616,7 @@ export default defineComponent({
width: 28px; width: 28px;
height: 28px; height: 28px;
margin: -16px 0 0 -16px; margin: -16px 0 0 -16px;
content: ""; content: '';
-webkit-transform-origin: center; -webkit-transform-origin: center;
-ms-transform-origin: center; -ms-transform-origin: center;
transform-origin: center; transform-origin: center;

View File

@ -1,4 +1,4 @@
import { translate as t } from "@nextcloud/l10n"; import { translate as t } from '@nextcloud/l10n';
/** /**
* Translations file from library source * Translations file from library source
@ -8,104 +8,101 @@ import { translate as t } from "@nextcloud/l10n";
* @see https://raw.githubusercontent.com/scaleflex/filerobot-image-editor/v4/packages/react-filerobot-image-editor/src/context/defaultTranslations.js * @see https://raw.githubusercontent.com/scaleflex/filerobot-image-editor/v4/packages/react-filerobot-image-editor/src/context/defaultTranslations.js
*/ */
export default { export default {
name: t("memories", "Name"), name: t('memories', 'Name'),
save: t("memories", "Save"), save: t('memories', 'Save'),
saveAs: t("memories", "Save as"), saveAs: t('memories', 'Save as'),
back: t("memories", "Back"), back: t('memories', 'Back'),
loading: t("memories", "Loading …"), loading: t('memories', 'Loading …'),
// resetOperations: 'Reset/delete all operations', // resetOperations: 'Reset/delete all operations',
resetOperations: t("memories", "Reset"), resetOperations: t('memories', 'Reset'),
changesLoseConfirmation: t("memories", "All changes will be lost."), changesLoseConfirmation: t('memories', 'All changes will be lost.'),
changesLoseConfirmationHint: t( changesLoseConfirmationHint: t('memories', 'Are you sure you want to continue?'),
"memories", cancel: t('memories', 'Cancel'),
"Are you sure you want to continue?" continue: t('memories', 'Continue'),
), undoTitle: t('memories', 'Undo'),
cancel: t("memories", "Cancel"), redoTitle: t('memories', 'Redo'),
continue: t("memories", "Continue"), showImageTitle: t('memories', 'Show original image'),
undoTitle: t("memories", "Undo"), zoomInTitle: t('memories', 'Zoom in'),
redoTitle: t("memories", "Redo"), zoomOutTitle: t('memories', 'Zoom out'),
showImageTitle: t("memories", "Show original image"), toggleZoomMenuTitle: t('memories', 'Toggle zoom menu'),
zoomInTitle: t("memories", "Zoom in"), adjustTab: t('memories', 'Adjust'),
zoomOutTitle: t("memories", "Zoom out"), finetuneTab: t('memories', 'Fine-tune'),
toggleZoomMenuTitle: t("memories", "Toggle zoom menu"), filtersTab: t('memories', 'Filters'),
adjustTab: t("memories", "Adjust"), watermarkTab: t('memories', 'Watermark'),
finetuneTab: t("memories", "Fine-tune"), annotateTab: t('memories', 'Draw'),
filtersTab: t("memories", "Filters"), resize: t('memories', 'Resize'),
watermarkTab: t("memories", "Watermark"), resizeTab: t('memories', 'Resize'),
annotateTab: t("memories", "Draw"), invalidImageError: t('memories', 'Invalid image.'),
resize: t("memories", "Resize"), uploadImageError: t('memories', 'Error while uploading the image.'),
resizeTab: t("memories", "Resize"), areNotImages: t('memories', 'are not images'),
invalidImageError: t("memories", "Invalid image."), isNotImage: t('memories', 'is not an image'),
uploadImageError: t("memories", "Error while uploading the image."), toBeUploaded: t('memories', 'to be uploaded'),
areNotImages: t("memories", "are not images"), cropTool: t('memories', 'Crop'),
isNotImage: t("memories", "is not an image"), original: t('memories', 'Original'),
toBeUploaded: t("memories", "to be uploaded"), custom: t('memories', 'Custom'),
cropTool: t("memories", "Crop"), square: t('memories', 'Square'),
original: t("memories", "Original"), landscape: t('memories', 'Landscape'),
custom: t("memories", "Custom"), portrait: t('memories', 'Portrait'),
square: t("memories", "Square"), ellipse: t('memories', 'Ellipse'),
landscape: t("memories", "Landscape"), classicTv: t('memories', 'Classic TV'),
portrait: t("memories", "Portrait"), cinemascope: t('memories', 'CinemaScope'),
ellipse: t("memories", "Ellipse"), arrowTool: t('memories', 'Arrow'),
classicTv: t("memories", "Classic TV"), blurTool: t('memories', 'Blur'),
cinemascope: t("memories", "CinemaScope"), brightnessTool: t('memories', 'Brightness'),
arrowTool: t("memories", "Arrow"), contrastTool: t('memories', 'Contrast'),
blurTool: t("memories", "Blur"), ellipseTool: t('memories', 'Ellipse'),
brightnessTool: t("memories", "Brightness"), unFlipX: t('memories', 'Un-flip X'),
contrastTool: t("memories", "Contrast"), flipX: t('memories', 'Flip X'),
ellipseTool: t("memories", "Ellipse"), unFlipY: t('memories', 'Un-flip Y'),
unFlipX: t("memories", "Un-flip X"), flipY: t('memories', 'Flip Y'),
flipX: t("memories", "Flip X"), hsvTool: t('memories', 'HSV'),
unFlipY: t("memories", "Un-flip Y"), hue: t('memories', 'Hue'),
flipY: t("memories", "Flip Y"), saturation: t('memories', 'Saturation'),
hsvTool: t("memories", "HSV"), value: t('memories', 'Value'),
hue: t("memories", "Hue"), imageTool: t('memories', 'Image'),
saturation: t("memories", "Saturation"), importing: t('memories', 'Importing …'),
value: t("memories", "Value"), addImage: t('memories', '+ Add image'),
imageTool: t("memories", "Image"), lineTool: t('memories', 'Line'),
importing: t("memories", "Importing …"), penTool: t('memories', 'Pen'),
addImage: t("memories", "+ Add image"), polygonTool: t('memories', 'Polygon'),
lineTool: t("memories", "Line"), sides: t('memories', 'Sides'),
penTool: t("memories", "Pen"), rectangleTool: t('memories', 'Rectangle'),
polygonTool: t("memories", "Polygon"), cornerRadius: t('memories', 'Corner Radius'),
sides: t("memories", "Sides"), resizeWidthTitle: t('memories', 'Width in pixels'),
rectangleTool: t("memories", "Rectangle"), resizeHeightTitle: t('memories', 'Height in pixels'),
cornerRadius: t("memories", "Corner Radius"), toggleRatioLockTitle: t('memories', 'Toggle ratio lock'),
resizeWidthTitle: t("memories", "Width in pixels"), reset: t('memories', 'Reset'),
resizeHeightTitle: t("memories", "Height in pixels"), resetSize: t('memories', 'Reset to original image size'),
toggleRatioLockTitle: t("memories", "Toggle ratio lock"), rotateTool: t('memories', 'Rotate'),
reset: t("memories", "Reset"), textTool: t('memories', 'Text'),
resetSize: t("memories", "Reset to original image size"), textSpacings: t('memories', 'Text spacing'),
rotateTool: t("memories", "Rotate"), textAlignment: t('memories', 'Text alignment'),
textTool: t("memories", "Text"), fontFamily: t('memories', 'Font family'),
textSpacings: t("memories", "Text spacing"), size: t('memories', 'Size'),
textAlignment: t("memories", "Text alignment"), letterSpacing: t('memories', 'Letter spacing'),
fontFamily: t("memories", "Font family"), lineHeight: t('memories', 'Line height'),
size: t("memories", "Size"), warmthTool: t('memories', 'Warmth'),
letterSpacing: t("memories", "Letter spacing"), addWatermark: t('memories', '+ Add watermark'),
lineHeight: t("memories", "Line height"), addWatermarkTitle: t('memories', 'Choose watermark type'),
warmthTool: t("memories", "Warmth"), uploadWatermark: t('memories', 'Upload watermark'),
addWatermark: t("memories", "+ Add watermark"), addWatermarkAsText: t('memories', 'Add as text'),
addWatermarkTitle: t("memories", "Choose watermark type"), padding: t('memories', 'Padding'),
uploadWatermark: t("memories", "Upload watermark"), shadow: t('memories', 'Shadow'),
addWatermarkAsText: t("memories", "Add as text"), horizontal: t('memories', 'Horizontal'),
padding: t("memories", "Padding"), vertical: t('memories', 'Vertical'),
shadow: t("memories", "Shadow"), blur: t('memories', 'Blur'),
horizontal: t("memories", "Horizontal"), opacity: t('memories', 'Opacity'),
vertical: t("memories", "Vertical"), position: t('memories', 'Position'),
blur: t("memories", "Blur"), stroke: t('memories', 'Stroke'),
opacity: t("memories", "Opacity"), saveAsModalLabel: t('memories', 'Save image as'),
position: t("memories", "Position"), extension: t('memories', 'Extension'),
stroke: t("memories", "Stroke"), nameIsRequired: t('memories', 'Name is required.'),
saveAsModalLabel: t("memories", "Save image as"), quality: t('memories', 'Quality'),
extension: t("memories", "Extension"), imageDimensionsHoverTitle: t('memories', 'Saved image size (width x height)'),
nameIsRequired: t("memories", "Name is required."),
quality: t("memories", "Quality"),
imageDimensionsHoverTitle: t("memories", "Saved image size (width x height)"),
cropSizeLowerThanResizedWarning: t( cropSizeLowerThanResizedWarning: t(
"memories", 'memories',
"Note that the selected crop area is lower than the applied resize which might cause quality decrease" 'Note that the selected crop area is lower than the applied resize which might cause quality decrease'
), ),
actualSize: t("memories", "Actual size (100%)"), actualSize: t('memories', 'Actual size (100%)'),
fitSize: t("memories", "Fit size"), fitSize: t('memories', 'Fit size'),
}; };

View File

@ -1,20 +1,20 @@
import PhotoSwipe from "photoswipe"; import PhotoSwipe from 'photoswipe';
import { isVideoContent } from "./PsVideo"; import { isVideoContent } from './PsVideo';
import { isLiveContent } from "./PsLivePhoto"; import { isLiveContent } from './PsLivePhoto';
import { fetchImage } from "../frame/XImgCache"; import { fetchImage } from '../frame/XImgCache';
import { PsContent, PsEvent, PsSlide } from "./types"; import { PsContent, PsEvent, PsSlide } from './types';
export default class ImageContentSetup { export default class ImageContentSetup {
private loading = 0; private loading = 0;
constructor(private lightbox: PhotoSwipe) { constructor(private lightbox: PhotoSwipe) {
lightbox.on("contentLoad", this.onContentLoad.bind(this)); lightbox.on('contentLoad', this.onContentLoad.bind(this));
lightbox.on("contentLoadImage", this.onContentLoadImage.bind(this)); lightbox.on('contentLoadImage', this.onContentLoadImage.bind(this));
lightbox.on("zoomPanUpdate", this.zoomPanUpdate.bind(this)); lightbox.on('zoomPanUpdate', this.zoomPanUpdate.bind(this));
lightbox.on("slideActivate", this.slideActivate.bind(this)); lightbox.on('slideActivate', this.slideActivate.bind(this));
lightbox.addFilter("isContentLoading", this.isContentLoading.bind(this)); lightbox.addFilter('isContentLoading', this.isContentLoading.bind(this));
lightbox.addFilter("placeholderSrc", this.placeholderSrc.bind(this)); lightbox.addFilter('placeholderSrc', this.placeholderSrc.bind(this));
} }
isContentLoading(isLoading: boolean, content: PsContent) { isContentLoading(isLoading: boolean, content: PsContent) {
@ -39,7 +39,7 @@ export default class ImageContentSetup {
// since these requests are not cached, leading to race conditions // since these requests are not cached, leading to race conditions
// with the loading of the actual images. // with the loading of the actual images.
// Sample is for OnThisDay, where msrc isn't blob // Sample is for OnThisDay, where msrc isn't blob
if (content.data.msrc?.startsWith("blob:")) { if (content.data.msrc?.startsWith('blob:')) {
return content.data.msrc; return content.data.msrc;
} }
@ -47,9 +47,9 @@ export default class ImageContentSetup {
} }
getXImgElem(content: PsContent, onLoad: () => void): HTMLImageElement { getXImgElem(content: PsContent, onLoad: () => void): HTMLImageElement {
const img = document.createElement("img"); const img = document.createElement('img');
img.classList.add("pswp__img", "ximg"); img.classList.add('pswp__img', 'ximg');
img.style.visibility = "hidden"; img.style.visibility = 'hidden';
// Fetch with Axios // Fetch with Axios
fetchImage(content.data.src).then((blobSrc) => { fetchImage(content.data.src).then((blobSrc) => {
@ -59,7 +59,7 @@ export default class ImageContentSetup {
// Insert image // Insert image
img.onerror = img.onload = () => { img.onerror = img.onload = () => {
img.onerror = img.onload = null; img.onerror = img.onload = null;
img.style.visibility = "visible"; img.style.visibility = 'visible';
onLoad(); onLoad();
this.slideActivate(); this.slideActivate();
}; };
@ -70,7 +70,7 @@ export default class ImageContentSetup {
} }
zoomPanUpdate({ slide }: { slide: PsSlide }) { zoomPanUpdate({ slide }: { slide: PsSlide }) {
if (!slide.data.highSrc || slide.data.highSrcCond !== "zoom") return; if (!slide.data.highSrc || slide.data.highSrcCond !== 'zoom') return;
if (slide.currZoomLevel >= slide.zoomLevels.secondary) { if (slide.currZoomLevel >= slide.zoomLevels.secondary) {
this.loadFullImage(slide); this.loadFullImage(slide);
@ -79,7 +79,7 @@ export default class ImageContentSetup {
slideActivate() { slideActivate() {
const slide = this.lightbox.currSlide; const slide = this.lightbox.currSlide;
if (slide?.data.highSrcCond === "always") { if (slide?.data.highSrcCond === 'always') {
this.loadFullImage(slide as PsSlide); this.loadFullImage(slide as PsSlide);
} }
} }
@ -88,13 +88,11 @@ export default class ImageContentSetup {
if (!slide.data.highSrc) return; if (!slide.data.highSrc) return;
// Get ximg element // Get ximg element
const img = slide.holderElement?.querySelector( const img = slide.holderElement?.querySelector('.ximg:not(.ximg--full)') as HTMLImageElement;
".ximg:not(.ximg--full)"
) as HTMLImageElement;
if (!img) return; if (!img) return;
// Load full image at secondary zoom level // Load full image at secondary zoom level
img.classList.add("ximg--full"); img.classList.add('ximg--full');
this.loading++; this.loading++;
this.lightbox.ui?.updatePreloaderVisibility(); this.lightbox.ui?.updatePreloaderVisibility();
@ -108,7 +106,7 @@ export default class ImageContentSetup {
img.src = blobSrc; img.src = blobSrc;
// Don't load again // Don't load again
slide.data.highSrcCond = "never"; slide.data.highSrcCond = 'never';
}) })
.finally(() => { .finally(() => {
this.loading--; this.loading--;

View File

@ -1,7 +1,7 @@
import PhotoSwipe from "photoswipe"; import PhotoSwipe from 'photoswipe';
import PsImage from "./PsImage"; import PsImage from './PsImage';
import * as utils from "../../services/Utils"; import * as utils from '../../services/Utils';
import { PsContent, PsEvent } from "./types"; import { PsContent, PsEvent } from './types';
export function isLiveContent(content: PsContent): boolean { export function isLiveContent(content: PsContent): boolean {
// Do not play Live Photo if the slideshow is // Do not play Live Photo if the slideshow is
@ -15,10 +15,10 @@ export function isLiveContent(content: PsContent): boolean {
class LivePhotoContentSetup { class LivePhotoContentSetup {
constructor(lightbox: PhotoSwipe, private psImage: PsImage) { constructor(lightbox: PhotoSwipe, private psImage: PsImage) {
lightbox.on("contentLoad", this.onContentLoad.bind(this)); lightbox.on('contentLoad', this.onContentLoad.bind(this));
lightbox.on("contentActivate", this.onContentActivate.bind(this)); lightbox.on('contentActivate', this.onContentActivate.bind(this));
lightbox.on("contentDeactivate", this.onContentDeactivate.bind(this)); lightbox.on('contentDeactivate', this.onContentDeactivate.bind(this));
lightbox.on("contentDestroy", this.onContentDestroy.bind(this)); lightbox.on('contentDestroy', this.onContentDestroy.bind(this));
} }
onContentLoad(e) { onContentLoad(e) {
@ -30,16 +30,16 @@ class LivePhotoContentSetup {
const photo = content?.data?.photo; const photo = content?.data?.photo;
const video = document.createElement("video"); const video = document.createElement('video');
video.preload = "none"; video.preload = 'none';
video.muted = true; video.muted = true;
video.playsInline = true; video.playsInline = true;
video.disableRemotePlayback = true; video.disableRemotePlayback = true;
video.autoplay = false; video.autoplay = false;
video.src = utils.getLivePhotoVideoUrl(photo, true); video.src = utils.getLivePhotoVideoUrl(photo, true);
const div = document.createElement("div"); const div = document.createElement('div');
div.className = "memories-livephoto"; div.className = 'memories-livephoto';
div.appendChild(video); div.appendChild(video);
content.element = div; content.element = div;
@ -53,7 +53,7 @@ class LivePhotoContentSetup {
onContentActivate({ content }: { content: PsContent }) { onContentActivate({ content }: { content: PsContent }) {
if (isLiveContent(content)) { if (isLiveContent(content)) {
const video = content.element?.querySelector("video"); const video = content.element?.querySelector('video');
if (video) { if (video) {
video.currentTime = 0; video.currentTime = 0;
video.play(); video.play();
@ -63,7 +63,7 @@ class LivePhotoContentSetup {
onContentDeactivate({ content }: PsEvent) { onContentDeactivate({ content }: PsEvent) {
if (isLiveContent(content)) { if (isLiveContent(content)) {
content.element?.querySelector("video")?.pause(); content.element?.querySelector('video')?.pause();
} }
} }

View File

@ -1,15 +1,15 @@
import PhotoSwipe from "photoswipe"; import PhotoSwipe from 'photoswipe';
import { loadState } from "@nextcloud/initial-state"; import { loadState } from '@nextcloud/initial-state';
import { showError } from "@nextcloud/dialogs"; import { showError } from '@nextcloud/dialogs';
import { translate as t } from "@nextcloud/l10n"; import { translate as t } from '@nextcloud/l10n';
import { getCurrentUser } from "@nextcloud/auth"; import { getCurrentUser } from '@nextcloud/auth';
import axios from "@nextcloud/axios"; import axios from '@nextcloud/axios';
import { API } from "../../services/API"; import { API } from '../../services/API';
import type { PsContent, PsEvent } from "./types"; import type { PsContent, PsEvent } from './types';
import Player from "video.js/dist/types/player"; import Player from 'video.js/dist/types/player';
import { QualityLevelList } from "videojs-contrib-quality-levels"; import { QualityLevelList } from 'videojs-contrib-quality-levels';
type VideoContent = PsContent & { type VideoContent = PsContent & {
videoElement: HTMLVideoElement | null; videoElement: HTMLVideoElement | null;
@ -25,17 +25,15 @@ type PsVideoEvent = PsEvent & {
content: VideoContent; content: VideoContent;
}; };
const config_vodDisable = loadState("memories", "vod_disable", true); const config_vodDisable = loadState('memories', 'vod_disable', true);
const config_video_default_quality = Number( const config_video_default_quality = Number(loadState('memories', 'video_default_quality', <string>'0') as string);
loadState("memories", "video_default_quality", <string>"0") as string
);
/** /**
* Check if slide has video content * Check if slide has video content
*/ */
export function isVideoContent(content: any): content is VideoContent { export function isVideoContent(content: any): content is VideoContent {
return content?.data?.type === "video"; return content?.data?.type === 'video';
} }
class VideoContentSetup { class VideoContentSetup {
@ -46,43 +44,34 @@ class VideoContentSetup {
} }
) { ) {
this.initLightboxEvents(lightbox); this.initLightboxEvents(lightbox);
lightbox.on("init", () => { lightbox.on('init', () => {
this.initPswpEvents(lightbox); this.initPswpEvents(lightbox);
}); });
} }
initLightboxEvents(lightbox: PhotoSwipe) { initLightboxEvents(lightbox: PhotoSwipe) {
lightbox.on("contentLoad", this.onContentLoad.bind(this)); lightbox.on('contentLoad', this.onContentLoad.bind(this));
lightbox.on("contentDestroy", this.onContentDestroy.bind(this)); lightbox.on('contentDestroy', this.onContentDestroy.bind(this));
lightbox.on("contentActivate", this.onContentActivate.bind(this)); lightbox.on('contentActivate', this.onContentActivate.bind(this));
lightbox.on("contentDeactivate", this.onContentDeactivate.bind(this)); lightbox.on('contentDeactivate', this.onContentDeactivate.bind(this));
lightbox.on("contentResize", this.onContentResize.bind(this)); lightbox.on('contentResize', this.onContentResize.bind(this));
lightbox.addFilter( lightbox.addFilter('isKeepingPlaceholder', this.isKeepingPlaceholder.bind(this));
"isKeepingPlaceholder", lightbox.addFilter('isContentZoomable', this.isContentZoomable.bind(this));
this.isKeepingPlaceholder.bind(this) lightbox.addFilter('useContentPlaceholder', this.useContentPlaceholder.bind(this));
);
lightbox.addFilter("isContentZoomable", this.isContentZoomable.bind(this));
lightbox.addFilter(
"useContentPlaceholder",
this.useContentPlaceholder.bind(this)
);
} }
initPswpEvents(pswp: PhotoSwipe) { initPswpEvents(pswp: PhotoSwipe) {
// Prevent draggin when pointer is in bottom part of the video // Prevent draggin when pointer is in bottom part of the video
// todo: add option for this // todo: add option for this
pswp.on("pointerDown", (e) => { pswp.on('pointerDown', (e) => {
const slide = pswp.currSlide; const slide = pswp.currSlide;
if (isVideoContent(slide) && this.options.preventDragOffset) { if (isVideoContent(slide) && this.options.preventDragOffset) {
const origEvent = e.originalEvent; const origEvent = e.originalEvent;
if (origEvent.type === "pointerdown") { if (origEvent.type === 'pointerdown') {
// Check if directly over the videojs control bar // Check if directly over the videojs control bar
const elems = document.elementsFromPoint( const elems = document.elementsFromPoint(origEvent.clientX, origEvent.clientY);
origEvent.clientX, if (elems.some((el) => el.classList.contains('plyr__controls'))) {
origEvent.clientY
);
if (elems.some((el) => el.classList.contains("plyr__controls"))) {
e.preventDefault(); e.preventDefault();
return; return;
} }
@ -90,10 +79,7 @@ class VideoContentSetup {
const videoHeight = Math.ceil(slide.height * slide.currZoomLevel); const videoHeight = Math.ceil(slide.height * slide.currZoomLevel);
const verticalEnding = videoHeight + slide.bounds.center.y; const verticalEnding = videoHeight + slide.bounds.center.y;
const pointerYPos = origEvent.pageY - pswp.offset.y; const pointerYPos = origEvent.pageY - pswp.offset.y;
if ( if (pointerYPos > verticalEnding - this.options.preventDragOffset && pointerYPos < verticalEnding) {
pointerYPos > verticalEnding - this.options.preventDragOffset &&
pointerYPos < verticalEnding
) {
e.preventDefault(); e.preventDefault();
} }
} }
@ -101,7 +87,7 @@ class VideoContentSetup {
}); });
// do not append video on nearby slides // do not append video on nearby slides
pswp.on("appendHeavy", (e) => { pswp.on('appendHeavy', (e) => {
if (isVideoContent(e.slide)) { if (isVideoContent(e.slide)) {
const content = <any>e.slide.content; const content = <any>e.slide.content;
if (e.slide.isActive && content.videoElement) { if (e.slide.isActive && content.videoElement) {
@ -110,12 +96,12 @@ class VideoContentSetup {
} }
}); });
pswp.on("close", () => { pswp.on('close', () => {
this.destroyVideo(pswp.currSlide?.content as VideoContent); this.destroyVideo(pswp.currSlide?.content as VideoContent);
}); });
// Prevent closing when video fullscreen is active // Prevent closing when video fullscreen is active
pswp.on("pointerMove", (e) => { pswp.on('pointerMove', (e) => {
const plyr = (<VideoContent>pswp.currSlide?.content)?.plyr; const plyr = (<VideoContent>pswp.currSlide?.content)?.plyr;
if (plyr?.fullscreen.active) { if (plyr?.fullscreen.active) {
e.preventDefault(); e.preventDefault();
@ -124,11 +110,10 @@ class VideoContentSetup {
} }
getDirectSrc(content: VideoContent) { getDirectSrc(content: VideoContent) {
const numChunks = const numChunks = Math.ceil((content.data.photo?.video_duration || 0) / 3) || undefined;
Math.ceil((content.data.photo?.video_duration || 0) / 3) || undefined;
return { return {
src: API.Q(content.data.src, { numChunks }), src: API.Q(content.data.src, { numChunks }),
type: "video/mp4", // chrome refuses to play video/quicktime, so fool it type: 'video/mp4', // chrome refuses to play video/quicktime, so fool it
}; };
} }
@ -137,7 +122,7 @@ class VideoContentSetup {
const fileid = content.data.photo.fileid; const fileid = content.data.photo.fileid;
return { return {
src: API.VIDEO_TRANSCODE(fileid), src: API.VIDEO_TRANSCODE(fileid),
type: "application/x-mpegURL", type: 'application/x-mpegURL',
}; };
} }
@ -151,16 +136,16 @@ class VideoContentSetup {
// Load videojs scripts // Load videojs scripts
if (!globalThis.vidjs) { if (!globalThis.vidjs) {
await import("../../services/videojs"); await import('../../services/videojs');
} }
// Create video element // Create video element
content.videoElement = document.createElement("video"); content.videoElement = document.createElement('video');
content.videoElement.className = "video-js"; content.videoElement.className = 'video-js';
content.videoElement.setAttribute("poster", content.data.msrc); content.videoElement.setAttribute('poster', content.data.msrc);
content.videoElement.setAttribute("preload", "none"); content.videoElement.setAttribute('preload', 'none');
content.videoElement.setAttribute("controls", ""); content.videoElement.setAttribute('controls', '');
content.videoElement.setAttribute("playsinline", ""); content.videoElement.setAttribute('playsinline', '');
// Add the video element to the actual container // Add the video element to the actual container
content.element?.appendChild(content.videoElement); content.element?.appendChild(content.videoElement);
@ -183,7 +168,7 @@ class VideoContentSetup {
autoplay: true, autoplay: true,
controls: false, controls: false,
sources: sources, sources: sources,
preload: "metadata", preload: 'metadata',
playbackRates: [0.5, 1, 1.5, 2], playbackRates: [0.5, 1, 1.5, 2],
responsive: true, responsive: true,
html5: { html5: {
@ -203,26 +188,26 @@ class VideoContentSetup {
let directFailed = false; let directFailed = false;
let hlsFailed = false; let hlsFailed = false;
vjs.on("error", () => { vjs.on('error', () => {
if (vjs.src(undefined)?.includes("m3u8")) { if (vjs.src(undefined)?.includes('m3u8')) {
hlsFailed = true; hlsFailed = true;
console.warn("PsVideo: HLS stream could not be opened."); console.warn('PsVideo: HLS stream could not be opened.');
if (getCurrentUser()?.isAdmin) { if (getCurrentUser()?.isAdmin) {
showError(t("memories", "Transcoding failed, check Nextcloud logs.")); showError(t('memories', 'Transcoding failed, check Nextcloud logs.'));
} }
if (!directFailed) { if (!directFailed) {
console.warn("PsVideo: Trying direct video stream"); console.warn('PsVideo: Trying direct video stream');
vjs.src(this.getDirectSrc(content)); vjs.src(this.getDirectSrc(content));
this.updateRotation(content, 0); this.updateRotation(content, 0);
} }
} else { } else {
directFailed = true; directFailed = true;
console.warn("PsVideo: Direct video stream could not be opened."); console.warn('PsVideo: Direct video stream could not be opened.');
if (!hlsFailed && !config_vodDisable) { if (!hlsFailed && !config_vodDisable) {
console.warn("PsVideo: Trying HLS stream"); console.warn('PsVideo: Trying HLS stream');
vjs.src(this.getHLSsrc(content)); vjs.src(this.getHLSsrc(content));
} }
} }
@ -233,7 +218,7 @@ class VideoContentSetup {
playWithDelay(); playWithDelay();
let canPlay = false; let canPlay = false;
content.videojs.on("canplay", () => { content.videojs.on('canplay', () => {
canPlay = true; canPlay = true;
this.updateRotation(content); // also gets the correct video elem as a side effect this.updateRotation(content); // also gets the correct video elem as a side effect
@ -241,14 +226,14 @@ class VideoContentSetup {
window.setTimeout(() => this.initPlyr(content), 0); window.setTimeout(() => this.initPlyr(content), 0);
// Hide the preview image // Hide the preview image
content.placeholder?.element?.setAttribute("hidden", "true"); content.placeholder?.element?.setAttribute('hidden', 'true');
// Another attempt to play the video // Another attempt to play the video
playWithDelay(); playWithDelay();
}); });
content.videojs.qualityLevels?.()?.on("addqualitylevel", (e) => { content.videojs.qualityLevels?.()?.on('addqualitylevel', (e) => {
if (e.qualityLevel?.label?.includes("max.m3u8")) { if (e.qualityLevel?.label?.includes('max.m3u8')) {
// This is the highest quality level // This is the highest quality level
// and guaranteed to be the last one // and guaranteed to be the last one
this.initPlyr(content); this.initPlyr(content);
@ -295,14 +280,14 @@ class VideoContentSetup {
content.videoElement = null; content.videoElement = null;
// Restore placeholder image // Restore placeholder image
content.placeholder?.element?.removeAttribute("hidden"); content.placeholder?.element?.removeAttribute('hidden');
} }
} }
initPlyr(content: VideoContent) { initPlyr(content: VideoContent) {
if (content.plyr || !content.videojs || !content.element) return; if (content.plyr || !content.videojs || !content.element) return;
content.videoElement = content.videojs?.el()?.querySelector("video"); content.videoElement = content.videojs?.el()?.querySelector('video');
if (!content.videoElement) return; if (!content.videoElement) return;
// Retain original parent for video element // Retain original parent for video element
@ -318,7 +303,7 @@ class VideoContentSetup {
const { width, height, label } = qualityList[i]; const { width, height, label } = qualityList[i];
s.add(Math.min(width!, height!)); s.add(Math.min(width!, height!));
if (label?.includes("max.m3u8")) { if (label?.includes('max.m3u8')) {
hasMax = true; hasMax = true;
} }
} }
@ -335,9 +320,9 @@ class VideoContentSetup {
const opts: Plyr.Options = { const opts: Plyr.Options = {
i18n: { i18n: {
qualityLabel: { qualityLabel: {
"-2": t("memories", "Direct"), '-2': t('memories', 'Direct'),
"-1": t("memories", "Original"), '-1': t('memories', 'Original'),
"0": t("memories", "Auto"), '0': t('memories', 'Auto'),
}, },
}, },
fullscreen: { fullscreen: {
@ -359,7 +344,7 @@ class VideoContentSetup {
qualityList = content.videojs?.qualityLevels?.(); qualityList = content.videojs?.qualityLevels?.();
if (!qualityList || !content.videojs) return; if (!qualityList || !content.videojs) return;
const isHLS = content.videojs.src(undefined)?.includes("m3u8"); const isHLS = content.videojs.src(undefined)?.includes('m3u8');
if (quality === -2) { if (quality === -2) {
// Direct playback // Direct playback
@ -387,7 +372,7 @@ class VideoContentSetup {
qualityList[i].enabled = qualityList[i].enabled =
!quality || // auto !quality || // auto
pixels === quality || // exact match pixels === quality || // exact match
(label?.includes("max.m3u8") && quality === -1); // max (label?.includes('max.m3u8') && quality === -1); // max
} }
}, },
}; };
@ -397,16 +382,12 @@ class VideoContentSetup {
const plyr = new Plyr(content.videoElement, opts); const plyr = new Plyr(content.videoElement, opts);
const container = plyr.elements.container!; const container = plyr.elements.container!;
container.style.height = "100%"; container.style.height = '100%';
container.style.width = "100%"; container.style.width = '100%';
container container.querySelectorAll('button').forEach((el) => el.classList.add('button-vue'));
.querySelectorAll("button") container.querySelectorAll('progress').forEach((el) => el.classList.add('vue'));
.forEach((el) => el.classList.add("button-vue")); container.style.backgroundColor = 'transparent';
container plyr.elements.wrapper!.style.backgroundColor = 'transparent';
.querySelectorAll("progress")
.forEach((el) => el.classList.add("vue"));
container.style.backgroundColor = "transparent";
plyr.elements.wrapper!.style.backgroundColor = "transparent";
// Set the fullscreen element to the container // Set the fullscreen element to the container
plyr.elements.fullscreen = content.slide?.holderElement || null; plyr.elements.fullscreen = content.slide?.holderElement || null;
@ -415,9 +396,9 @@ class VideoContentSetup {
content.plyr = plyr; content.plyr = plyr;
// Wait for animation to end before showing Plyr // Wait for animation to end before showing Plyr
container.style.opacity = "0"; container.style.opacity = '0';
setTimeout(() => { setTimeout(() => {
container.style.opacity = "1"; container.style.opacity = '1';
}, 250); }, 250);
// Restore original parent of video element // Restore original parent of video element
@ -433,7 +414,7 @@ class VideoContentSetup {
let previousOrientation: OrientationLockType | undefined; let previousOrientation: OrientationLockType | undefined;
// Lock orientation when entering fullscreen // Lock orientation when entering fullscreen
plyr.on("enterfullscreen", async (event) => { plyr.on('enterfullscreen', async (event) => {
const rotation = this.updateRotation(content); const rotation = this.updateRotation(content);
const exif = content.data.photo.imageInfo?.exif; const exif = content.data.photo.imageInfo?.exif;
const h = Number(exif?.ImageHeight || 0); const h = Number(exif?.ImageHeight || 0);
@ -441,7 +422,7 @@ class VideoContentSetup {
if (h && w) { if (h && w) {
previousOrientation ||= screen.orientation.type; previousOrientation ||= screen.orientation.type;
const orientation = h < w && !rotation ? "landscape" : "portrait"; const orientation = h < w && !rotation ? 'landscape' : 'portrait';
try { try {
await screen.orientation.lock(orientation); await screen.orientation.lock(orientation);
@ -452,7 +433,7 @@ class VideoContentSetup {
}); });
// Unlock orientation when exiting fullscreen // Unlock orientation when exiting fullscreen
plyr.on("exitfullscreen", async (event) => { plyr.on('exitfullscreen', async (event) => {
try { try {
if (previousOrientation) { if (previousOrientation) {
await screen.orientation.lock(previousOrientation); await screen.orientation.lock(previousOrientation);
@ -470,13 +451,13 @@ class VideoContentSetup {
updateRotation(content: VideoContent, val?: number): boolean { updateRotation(content: VideoContent, val?: number): boolean {
if (!content.videojs) return false; if (!content.videojs) return false;
content.videoElement = content.videojs.el()?.querySelector("video"); content.videoElement = content.videojs.el()?.querySelector('video');
if (!content.videoElement) return false; if (!content.videoElement) return false;
const photo = content.data.photo; const photo = content.data.photo;
const exif = photo.imageInfo?.exif; const exif = photo.imageInfo?.exif;
const rotation = val ?? Number(exif?.Rotation || 0); const rotation = val ?? Number(exif?.Rotation || 0);
const shouldRotate = content.videojs?.src(undefined)?.includes("m3u8"); const shouldRotate = content.videojs?.src(undefined)?.includes('m3u8');
if (rotation && shouldRotate) { if (rotation && shouldRotate) {
let transform = `rotate(${rotation}deg)`; let transform = `rotate(${rotation}deg)`;
@ -487,16 +468,16 @@ class VideoContentSetup {
content.videoElement.style.height = content.element!.style.width; content.videoElement.style.height = content.element!.style.width;
transform = `translateY(-${content.element!.style.width}) ${transform}`; transform = `translateY(-${content.element!.style.width}) ${transform}`;
content.videoElement.style.transformOrigin = "bottom left"; content.videoElement.style.transformOrigin = 'bottom left';
} }
content.videoElement.style.transform = transform; content.videoElement.style.transform = transform;
return hasRotation; return hasRotation;
} else { } else {
content.videoElement.style.transform = "none"; content.videoElement.style.transform = 'none';
content.videoElement.style.width = "100%"; content.videoElement.style.width = '100%';
content.videoElement.style.height = "100%"; content.videoElement.style.height = '100%';
} }
return false; return false;
@ -515,16 +496,16 @@ class VideoContentSetup {
const content = e.content; const content = e.content;
if (content.element) { if (content.element) {
content.element.style.width = width + "px"; content.element.style.width = width + 'px';
content.element.style.height = height + "px"; content.element.style.height = height + 'px';
} }
if (content.slide && content.slide.placeholder) { if (content.slide && content.slide.placeholder) {
// override placeholder size, so it more accurately matches the video // override placeholder size, so it more accurately matches the video
const placeholderElStyle = content.slide.placeholder.element.style; const placeholderElStyle = content.slide.placeholder.element.style;
placeholderElStyle.transform = "none"; placeholderElStyle.transform = 'none';
placeholderElStyle.width = width + "px"; placeholderElStyle.width = width + 'px';
placeholderElStyle.height = height + "px"; placeholderElStyle.height = height + 'px';
} }
this.updateRotation(content); this.updateRotation(content);
@ -556,13 +537,13 @@ class VideoContentSetup {
// Stop default content load // Stop default content load
e.preventDefault(); e.preventDefault();
content.type = "video"; content.type = 'video';
if (content.element) return; if (content.element) return;
// Create DIV // Create DIV
content.element = document.createElement("div"); content.element = document.createElement('div');
content.element.classList.add("video-container"); content.element.classList.add('video-container');
content.onLoaded(); content.onLoaded();
} }

View File

@ -7,11 +7,7 @@
@fullscreenchange="fullscreenChange" @fullscreenchange="fullscreenChange"
@keydown="keydown" @keydown="keydown"
> >
<ImageEditor <ImageEditor v-if="editorOpen" :photo="currentPhoto" @close="editorOpen = false" />
v-if="editorOpen"
:photo="currentPhoto"
@close="editorOpen = false"
/>
<div <div
class="inner" class="inner"
@ -21,17 +17,14 @@
@pointerdown.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"
container=".memories_viewer .pswp"
>
<NcActionButton <NcActionButton
v-if="canShare" v-if="canShare"
:aria-label="t('memories', 'Share')" :aria-label="t('memories', 'Share')"
@click="shareCurrent" @click="shareCurrent"
:close-after-click="true" :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
@ -40,7 +33,7 @@
@click="deleteCurrent" @click="deleteCurrent"
:close-after-click="true" :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
@ -49,7 +42,7 @@
@click="deleteCurrent" @click="deleteCurrent"
:close-after-click="true" :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
@ -58,7 +51,7 @@
@click="playLivePhoto" @click="playLivePhoto"
:close-after-click="true" :close-after-click="true"
> >
{{ t("memories", "Play Live Photo") }} {{ t('memories', 'Play Live Photo') }}
<template #icon> <LivePhotoIcon :size="24" /> </template> <template #icon> <LivePhotoIcon :size="24" /> </template>
</NcActionButton> </NcActionButton>
<NcActionButton <NcActionButton
@ -67,18 +60,14 @@
@click="favoriteCurrent" @click="favoriteCurrent"
:close-after-click="true" :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 :aria-label="t('memories', 'Sidebar')" @click="toggleSidebar" :close-after-click="true">
:aria-label="t('memories', 'Sidebar')" {{ t('memories', 'Sidebar') }}
@click="toggleSidebar"
:close-after-click="true"
>
{{ t("memories", "Sidebar") }}
<template #icon> <template #icon>
<InfoIcon :size="24" /> <InfoIcon :size="24" />
</template> </template>
@ -89,7 +78,7 @@
@click="openEditor" @click="openEditor"
:close-after-click="true" :close-after-click="true"
> >
{{ t("memories", "Edit") }} {{ t('memories', 'Edit') }}
<template #icon> <template #icon>
<TuneIcon :size="24" /> <TuneIcon :size="24" />
</template> </template>
@ -100,7 +89,7 @@
:close-after-click="true" :close-after-click="true"
v-if="!this.state_noDownload" v-if="!this.state_noDownload"
> >
{{ t("memories", "Download") }} {{ t('memories', 'Download') }}
<template #icon> <template #icon>
<DownloadIcon :size="24" /> <DownloadIcon :size="24" />
</template> </template>
@ -111,7 +100,7 @@
@click="downloadCurrentLiveVideo" @click="downloadCurrentLiveVideo"
:close-after-click="true" :close-after-click="true"
> >
{{ t("memories", "Download Video") }} {{ t('memories', 'Download Video') }}
<template #icon> <template #icon>
<DownloadIcon :size="24" /> <DownloadIcon :size="24" />
</template> </template>
@ -122,17 +111,13 @@
@click="viewInFolder" @click="viewInFolder"
:close-after-click="true" :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')" {{ t('memories', 'Slideshow') }}
@click="startSlideshow"
:close-after-click="true"
>
{{ t("memories", "Slideshow") }}
<template #icon> <template #icon>
<SlideshowIcon :size="24" /> <SlideshowIcon :size="24" />
</template> </template>
@ -143,7 +128,7 @@
@click="editMetadata" @click="editMetadata"
:close-after-click="true" :close-after-click="true"
> >
{{ t("memories", "Edit metadata") }} {{ t('memories', 'Edit metadata') }}
<template #icon> <template #icon>
<EditFileIcon :size="24" /> <EditFileIcon :size="24" />
</template> </template>
@ -151,18 +136,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">
@ -174,47 +152,47 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineComponent } from "vue"; import { defineComponent } from 'vue';
import { IDay, IImageInfo, IPhoto, IRow, IRowType } from "../../types"; import { IDay, IImageInfo, IPhoto, IRow, IRowType } from '../../types';
import { PsSlide } from "./types"; import { PsSlide } from './types';
import UserConfig from "../../mixins/UserConfig"; import UserConfig from '../../mixins/UserConfig';
import NcActions from "@nextcloud/vue/dist/Components/NcActions"; import NcActions from '@nextcloud/vue/dist/Components/NcActions';
import NcActionButton from "@nextcloud/vue/dist/Components/NcActionButton"; import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton';
import { subscribe, unsubscribe } from "@nextcloud/event-bus"; import { subscribe, unsubscribe } from '@nextcloud/event-bus';
import { showError } from "@nextcloud/dialogs"; import { showError } from '@nextcloud/dialogs';
import axios from "@nextcloud/axios"; import axios from '@nextcloud/axios';
import { getDownloadLink } from "../../services/DavRequests"; import { getDownloadLink } from '../../services/DavRequests';
import { API } from "../../services/API"; import { API } from '../../services/API';
import * as dav from "../../services/DavRequests"; import * as dav from '../../services/DavRequests';
import * as utils from "../../services/Utils"; import * as utils from '../../services/Utils';
import ImageEditor from "./ImageEditor.vue"; import ImageEditor from './ImageEditor.vue';
import PhotoSwipe, { PhotoSwipeOptions } from "photoswipe"; import PhotoSwipe, { PhotoSwipeOptions } from 'photoswipe';
import "photoswipe/style.css"; import 'photoswipe/style.css';
import PsImage from "./PsImage"; import PsImage from './PsImage';
import PsVideo from "./PsVideo"; import PsVideo from './PsVideo';
import PsLivePhoto from "./PsLivePhoto"; import PsLivePhoto from './PsLivePhoto';
import ShareIcon from "vue-material-design-icons/ShareVariant.vue"; import ShareIcon from 'vue-material-design-icons/ShareVariant.vue';
import DeleteIcon from "vue-material-design-icons/TrashCanOutline.vue"; import DeleteIcon from 'vue-material-design-icons/TrashCanOutline.vue';
import StarIcon from "vue-material-design-icons/Star.vue"; import StarIcon from 'vue-material-design-icons/Star.vue';
import StarOutlineIcon from "vue-material-design-icons/StarOutline.vue"; import StarOutlineIcon from 'vue-material-design-icons/StarOutline.vue';
import DownloadIcon from "vue-material-design-icons/Download.vue"; import DownloadIcon from 'vue-material-design-icons/Download.vue';
import InfoIcon from "vue-material-design-icons/InformationOutline.vue"; import InfoIcon from 'vue-material-design-icons/InformationOutline.vue';
import OpenInNewIcon from "vue-material-design-icons/OpenInNew.vue"; import OpenInNewIcon from 'vue-material-design-icons/OpenInNew.vue';
import TuneIcon from "vue-material-design-icons/Tune.vue"; import TuneIcon from 'vue-material-design-icons/Tune.vue';
import SlideshowIcon from "vue-material-design-icons/PlayBox.vue"; import SlideshowIcon from 'vue-material-design-icons/PlayBox.vue';
import EditFileIcon from "vue-material-design-icons/FileEdit.vue"; import EditFileIcon from 'vue-material-design-icons/FileEdit.vue';
import AlbumRemoveIcon from "vue-material-design-icons/BookRemove.vue"; import AlbumRemoveIcon from 'vue-material-design-icons/BookRemove.vue';
import LivePhotoIcon from "vue-material-design-icons/MotionPlayOutline.vue"; import LivePhotoIcon from 'vue-material-design-icons/MotionPlayOutline.vue';
const SLIDESHOW_MS = 5000; const SLIDESHOW_MS = 5000;
export default defineComponent({ export default defineComponent({
name: "Viewer", name: 'Viewer',
components: { components: {
NcActions, NcActions,
NcActionButton, NcActionButton,
@ -239,14 +217,14 @@ export default defineComponent({
isOpen: false, isOpen: false,
originalTitle: null as string | null, originalTitle: null as string | null,
editorOpen: false, editorOpen: false,
editorSrc: "", editorSrc: '',
show: false, show: false,
showControls: false, showControls: false,
fullyOpened: false, fullyOpened: false,
sidebarOpen: false, sidebarOpen: false,
sidebarWidth: 400, sidebarWidth: 400,
outerWidth: "100vw", outerWidth: '100vw',
/** User interaction detection */ /** User interaction detection */
activityTimer: 0, activityTimer: 0,
@ -270,19 +248,19 @@ export default defineComponent({
}), }),
mounted() { mounted() {
subscribe("memories:sidebar:opened", this.handleAppSidebarOpen); subscribe('memories:sidebar:opened', this.handleAppSidebarOpen);
subscribe("memories:sidebar:closed", this.handleAppSidebarClose); subscribe('memories:sidebar:closed', this.handleAppSidebarClose);
subscribe("files:file:created", this.handleFileUpdated); subscribe('files:file:created', this.handleFileUpdated);
subscribe("files:file:updated", this.handleFileUpdated); subscribe('files:file:updated', this.handleFileUpdated);
subscribe("memories:window:resize", this.handleWindowResize); subscribe('memories:window:resize', this.handleWindowResize);
}, },
beforeDestroy() { beforeDestroy() {
unsubscribe("memories:sidebar:opened", this.handleAppSidebarOpen); unsubscribe('memories:sidebar:opened', this.handleAppSidebarOpen);
unsubscribe("memories:sidebar:closed", this.handleAppSidebarClose); unsubscribe('memories:sidebar:closed', this.handleAppSidebarClose);
unsubscribe("files:file:created", this.handleFileUpdated); unsubscribe('files:file:created', this.handleFileUpdated);
unsubscribe("files:file:updated", this.handleFileUpdated); unsubscribe('files:file:updated', this.handleFileUpdated);
unsubscribe("memories:window:resize", this.handleWindowResize); unsubscribe('memories:window:resize', this.handleWindowResize);
}, },
computed: { computed: {
@ -301,12 +279,12 @@ export default defineComponent({
/** Route is public */ /** Route is public */
routeIsPublic(): boolean { routeIsPublic(): boolean {
return this.$route.name?.endsWith("-share") ?? false; return this.$route.name?.endsWith('-share') ?? false;
}, },
/** Route is album */ /** Route is album */
routeIsAlbum(): boolean { routeIsAlbum(): boolean {
return this.$route.name === "albums"; return this.$route.name === 'albums';
}, },
/** Get the currently open photo */ /** Get the currently open photo */
@ -333,11 +311,7 @@ export default defineComponent({
/** Show bottom bar info such as date taken */ /** Show bottom bar info such as date taken */
showBottomBar(): boolean { showBottomBar(): boolean {
return ( return !this.isVideo && this.fullyOpened && Boolean(this.currentPhoto?.imageInfo);
!this.isVideo &&
this.fullyOpened &&
Boolean(this.currentPhoto?.imageInfo)
);
}, },
/** Allow closing the viewer */ /** Allow closing the viewer */
@ -354,12 +328,12 @@ export default defineComponent({
/** Show edit buttons */ /** Show edit buttons */
canEdit(): boolean { canEdit(): boolean {
return this.currentPhoto?.imageInfo?.permissions?.includes("U") ?? false; return this.currentPhoto?.imageInfo?.permissions?.includes('U') ?? false;
}, },
/** Show delete button */ /** Show delete button */
canDelete(): boolean { canDelete(): boolean {
return this.currentPhoto?.imageInfo?.permissions?.includes("D") ?? false; return this.currentPhoto?.imageInfo?.permissions?.includes('D') ?? false;
}, },
/** Show share button */ /** Show share button */
@ -370,15 +344,15 @@ export default defineComponent({
methods: { methods: {
deleted(photos: IPhoto[]) { deleted(photos: IPhoto[]) {
this.$emit("deleted", photos); this.$emit('deleted', photos);
}, },
fetchDay(dayId: number) { fetchDay(dayId: number) {
this.$emit("fetchDay", dayId); this.$emit('fetchDay', dayId);
}, },
updateLoading(delta: number) { updateLoading(delta: number) {
this.$emit("updateLoading", delta); this.$emit('updateLoading', delta);
}, },
/** Update the document title */ /** Update the document title */
@ -413,20 +387,20 @@ export default defineComponent({
// On touch devices, tapAction directly handles the ui visibility // On touch devices, tapAction directly handles the ui visibility
// through Photoswipe. // through Photoswipe.
const isPointer = evt instanceof PointerEvent; const isPointer = evt instanceof PointerEvent;
const isMouse = isPointer && evt.pointerType !== "touch"; const isMouse = isPointer && evt.pointerType !== 'touch';
if (this.isOpen && (!isPointer || isMouse)) { if (this.isOpen && (!isPointer || isMouse)) {
this.photoswipe?.template?.classList.add("pswp--ui-visible"); this.photoswipe?.template?.classList.add('pswp--ui-visible');
if (isMouse) { if (isMouse) {
this.activityTimer = window.setTimeout(() => { this.activityTimer = window.setTimeout(() => {
if (this.isOpen) { if (this.isOpen) {
this.photoswipe?.template?.classList.remove("pswp--ui-visible"); this.photoswipe?.template?.classList.remove('pswp--ui-visible');
} }
}, 2000); }, 2000);
} }
} }
} else { } else {
this.photoswipe?.template?.classList.remove("pswp--ui-visible"); this.photoswipe?.template?.classList.remove('pswp--ui-visible');
} }
}, },
@ -443,20 +417,20 @@ export default defineComponent({
bgOpacity: 1, bgOpacity: 1,
appendToEl: this.$refs.inner as HTMLElement, appendToEl: this.$refs.inner as HTMLElement,
preload: [2, 2], preload: [2, 2],
bgClickAction: "toggle-controls", bgClickAction: 'toggle-controls',
clickToCloseNonZoomable: false, clickToCloseNonZoomable: false,
pinchToClose: this.allowClose, pinchToClose: this.allowClose,
closeOnVerticalDrag: this.allowClose, closeOnVerticalDrag: this.allowClose,
easing: "cubic-bezier(.49,.85,.55,1)", easing: 'cubic-bezier(.49,.85,.55,1)',
showHideAnimationType: "zoom", showHideAnimationType: 'zoom',
showAnimationDuration: 250, showAnimationDuration: 250,
hideAnimationDuration: 250, hideAnimationDuration: 250,
closeTitle: this.t("memories", "Close"), closeTitle: this.t('memories', 'Close'),
arrowPrevTitle: this.t("memories", "Previous"), arrowPrevTitle: this.t('memories', 'Previous'),
arrowNextTitle: this.t("memories", "Next"), arrowNextTitle: this.t('memories', 'Next'),
getViewportSizeFn: () => { getViewportSizeFn: () => {
// Ignore the sidebar if mobile or fullscreen // Ignore the sidebar if mobile or fullscreen
const isMobile = globalThis.windowInnerWidth < 768; const isMobile = globalThis.windowInnerWidth < 768;
@ -479,56 +453,53 @@ export default defineComponent({
globalThis.photoswipe = this.photoswipe; globalThis.photoswipe = this.photoswipe;
// Monkey patch for focus trapping in sidebar // Monkey patch for focus trapping in sidebar
const _onFocusIn = this.photoswipe.keyboard["_onFocusIn"]; const _onFocusIn = this.photoswipe.keyboard['_onFocusIn'];
this.photoswipe.keyboard["_onFocusIn"] = (e: FocusEvent) => { this.photoswipe.keyboard['_onFocusIn'] = (e: FocusEvent) => {
if ( if (e.target instanceof HTMLElement && e.target.closest('#app-sidebar-vue, .v-popper__popper, .modal-mask')) {
e.target instanceof HTMLElement &&
e.target.closest("#app-sidebar-vue, .v-popper__popper, .modal-mask")
) {
return; return;
} }
_onFocusIn.call(this.photoswipe!.keyboard, e); _onFocusIn.call(this.photoswipe!.keyboard, e);
}; };
// Refresh sidebar on change // Refresh sidebar on change
this.photoswipe.on("change", () => { this.photoswipe.on('change', () => {
if (this.sidebarOpen) { if (this.sidebarOpen) {
this.openSidebar(); this.openSidebar();
} }
}); });
// Make sure buttons are styled properly // Make sure buttons are styled properly
this.photoswipe.addFilter("uiElement", (element, data) => { this.photoswipe.addFilter('uiElement', (element, data) => {
// add button-vue class if button // add button-vue class if button
if (element.classList.contains("pswp__button")) { if (element.classList.contains('pswp__button')) {
element.classList.add("button-vue"); element.classList.add('button-vue');
} }
return element; return element;
}); });
// Total number of photos in this view // Total number of photos in this view
this.photoswipe.addFilter("numItems", (numItems) => { this.photoswipe.addFilter('numItems', (numItems) => {
return this.globalCount; return this.globalCount;
}); });
// Put viewer over everything else // Put viewer over everything else
const navElem = document.getElementById("app-navigation-vue"); const navElem = document.getElementById('app-navigation-vue');
const klass = "has-viewer"; const klass = 'has-viewer';
this.photoswipe.on("beforeOpen", () => { this.photoswipe.on('beforeOpen', () => {
document.body.classList.add(klass); document.body.classList.add(klass);
if (navElem) navElem.style.zIndex = "0"; if (navElem) navElem.style.zIndex = '0';
}); });
this.photoswipe.on("openingAnimationStart", () => { this.photoswipe.on('openingAnimationStart', () => {
this.isOpen = true; this.isOpen = true;
this.fullyOpened = false; this.fullyOpened = false;
if (this.sidebarOpen) { if (this.sidebarOpen) {
this.openSidebar(); this.openSidebar();
} }
}); });
this.photoswipe.on("openingAnimationEnd", () => { this.photoswipe.on('openingAnimationEnd', () => {
this.fullyOpened = true; this.fullyOpened = true;
}); });
this.photoswipe.on("close", () => { this.photoswipe.on('close', () => {
this.isOpen = false; this.isOpen = false;
this.fullyOpened = false; this.fullyOpened = false;
this.setUiVisible(false); this.setUiVisible(false);
@ -536,9 +507,9 @@ export default defineComponent({
this.setRouteHash(undefined); this.setRouteHash(undefined);
this.updateTitle(undefined); this.updateTitle(undefined);
}); });
this.photoswipe.on("destroy", () => { this.photoswipe.on('destroy', () => {
document.body.classList.remove(klass); document.body.classList.remove(klass);
if (navElem) navElem.style.zIndex = ""; if (navElem) navElem.style.zIndex = '';
// reset everything // reset everything
this.show = false; this.show = false;
@ -555,7 +526,7 @@ export default defineComponent({
}); });
// Update vue route for deep linking // Update vue route for deep linking
this.photoswipe.on("slideActivate", (e) => { this.photoswipe.on('slideActivate', (e) => {
this.currIndex = this.photoswipe!.currIndex; this.currIndex = this.photoswipe!.currIndex;
const photo = e.slide?.data?.photo; const photo = e.slide?.data?.photo;
this.setRouteHash(photo); this.setRouteHash(photo);
@ -564,17 +535,15 @@ export default defineComponent({
}); });
// Show and hide controls // Show and hide controls
this.photoswipe.on("uiRegister", (e) => { this.photoswipe.on('uiRegister', (e) => {
if (this.photoswipe?.template) { if (this.photoswipe?.template) {
new MutationObserver((mutations) => { new MutationObserver((mutations) => {
mutations.forEach((mutationRecord) => { mutations.forEach((mutationRecord) => {
this.showControls = (<HTMLElement>( this.showControls = (<HTMLElement>mutationRecord.target)?.classList.contains('pswp--ui-visible');
mutationRecord.target
))?.classList.contains("pswp--ui-visible");
}); });
}).observe(this.photoswipe.template, { }).observe(this.photoswipe.template, {
attributes: true, attributes: true,
attributeFilter: ["class"], attributeFilter: ['class'],
}); });
} }
}); });
@ -588,10 +557,7 @@ export default defineComponent({
this.psImage = new PsImage(<PhotoSwipe>this.photoswipe); this.psImage = new PsImage(<PhotoSwipe>this.photoswipe);
// Live Photo support // Live Photo support
this.psLivePhoto = new PsLivePhoto( this.psLivePhoto = new PsLivePhoto(<PhotoSwipe>this.photoswipe, <PsImage>this.psImage);
<PhotoSwipe>this.photoswipe,
<PsImage>this.psImage
);
// 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);
@ -622,7 +588,7 @@ export default defineComponent({
async open(anchorPhoto: IPhoto, rows: IRow[]) { async open(anchorPhoto: IPhoto, rows: IRow[]) {
const detail = anchorPhoto.d?.detail; const detail = anchorPhoto.d?.detail;
if (!detail) { if (!detail) {
console.error("Attempted to open viewer with no detail list!"); console.error('Attempted to open viewer with no detail list!');
return; return;
} }
@ -649,7 +615,7 @@ export default defineComponent({
// Lazy-generate item data. // Lazy-generate item data.
// Load the next two days in the timeline. // Load the next two days in the timeline.
photoswipe.addFilter("itemData", (itemData, index) => { photoswipe.addFilter('itemData', (itemData, index) => {
// Get photo object from list // Get photo object from list
let idx = index - this.globalAnchor; let idx = index - this.globalAnchor;
if (idx < 0) { if (idx < 0) {
@ -663,7 +629,7 @@ export default defineComponent({
const prevDayId = this.dayIds[firstDayIdx - 1]; const prevDayId = this.dayIds[firstDayIdx - 1];
const prevDay = this.days.get(prevDayId); const prevDay = this.days.get(prevDayId);
if (!prevDay?.detail) { if (!prevDay?.detail) {
console.error("[BUG] No detail for previous day"); console.error('[BUG] No detail for previous day');
return {}; return {};
} }
this.list.unshift(...prevDay.detail); this.list.unshift(...prevDay.detail);
@ -679,7 +645,7 @@ export default defineComponent({
const nextDayId = this.dayIds[lastDayIdx + 1]; const nextDayId = this.dayIds[lastDayIdx + 1];
const nextDay = this.days.get(nextDayId); const nextDay = this.days.get(nextDayId);
if (!nextDay?.detail) { if (!nextDay?.detail) {
console.error("[BUG] No detail for next day"); console.error('[BUG] No detail for next day');
return {}; return {};
} }
this.list.push(...nextDay.detail); this.list.push(...nextDay.detail);
@ -696,11 +662,7 @@ export default defineComponent({
// Preload next and previous 3 days // Preload next and previous 3 days
const dayIdx = utils.binarySearch(this.dayIds, photo.dayid); const dayIdx = utils.binarySearch(this.dayIds, photo.dayid);
const preload = (idx: number) => { const preload = (idx: number) => {
if ( if (idx > 0 && idx < this.dayIds.length && !this.days.get(this.dayIds[idx])?.detail) {
idx > 0 &&
idx < this.dayIds.length &&
!this.days.get(this.dayIds[idx])?.detail
) {
this.fetchDay(this.dayIds[idx]); this.fetchDay(this.dayIds[idx]);
} }
}; };
@ -712,9 +674,7 @@ export default defineComponent({
preload(dayIdx + 3); preload(dayIdx + 3);
// Get thumb image // Get thumb image
const thumbSrc: string = const thumbSrc: string = this.thumbElem(photo)?.getAttribute('src') || utils.getPreviewUrl(photo, false, 256);
this.thumbElem(photo)?.getAttribute("src") ||
utils.getPreviewUrl(photo, false, 256);
// Get full image // Get full image
return { return {
@ -724,32 +684,27 @@ export default defineComponent({
}); });
// Get the thumbnail image // Get the thumbnail image
photoswipe.addFilter("thumbEl", (thumbEl, data, index) => { photoswipe.addFilter('thumbEl', (thumbEl, data, index) => {
const photo = this.list[index - this.globalAnchor]; const photo = this.list[index - this.globalAnchor];
if (!photo || !photo.w || !photo.h) return thumbEl as HTMLElement; if (!photo || !photo.w || !photo.h) return thumbEl as HTMLElement;
return this.thumbElem(photo) ?? (thumbEl as HTMLElement); // bug in PhotoSwipe types return this.thumbElem(photo) ?? (thumbEl as HTMLElement); // bug in PhotoSwipe types
}); });
photoswipe.on("slideActivate", (e) => { photoswipe.on('slideActivate', (e) => {
// Scroll to keep the thumbnail in view // Scroll to keep the thumbnail in view
const thumb = this.thumbElem(e.slide.data?.photo); const thumb = this.thumbElem(e.slide.data?.photo);
if (thumb && this.fullyOpened) { if (thumb && this.fullyOpened) {
const rect = thumb.getBoundingClientRect(); const rect = thumb.getBoundingClientRect();
if ( if (rect.bottom < 50 || rect.top > globalThis.windowInnerHeight - 50) {
rect.bottom < 50 ||
rect.top > globalThis.windowInnerHeight - 50
) {
thumb.scrollIntoView({ thumb.scrollIntoView({
block: "center", block: 'center',
}); });
} }
} }
// Remove active class from others and add to this one // Remove active class from others and add to this one
photoswipe.element photoswipe.element?.querySelectorAll('.pswp__item').forEach((el) => el.classList.remove('active'));
?.querySelectorAll(".pswp__item") e.slide.holderElement?.classList.add('active');
.forEach((el) => el.classList.remove("active"));
e.slide.holderElement?.classList.add("active");
}); });
photoswipe.init(); photoswipe.init();
@ -770,11 +725,9 @@ export default defineComponent({
this.globalCount = list.length; this.globalCount = list.length;
this.globalAnchor = 0; this.globalAnchor = 0;
photoswipe.addFilter("itemData", (itemData, index) => ({ photoswipe.addFilter('itemData', (itemData, index) => ({
...this.getItemData(this.list[index]), ...this.getItemData(this.list[index]),
msrc: thumbSize msrc: thumbSize ? utils.getPreviewUrl(photo, false, thumbSize) : undefined,
? utils.getPreviewUrl(photo, false, thumbSize)
: undefined,
})); }));
this.isOpen = true; this.isOpen = true;
@ -783,11 +736,11 @@ export default defineComponent({
/** Get base data object */ /** Get base data object */
getItemData(photo: IPhoto) { getItemData(photo: IPhoto) {
let previewUrl = utils.getPreviewUrl(photo, false, "screen"); let previewUrl = utils.getPreviewUrl(photo, false, 'screen');
const isvideo = photo.flag & this.c.FLAG_IS_VIDEO; const isvideo = photo.flag & this.c.FLAG_IS_VIDEO;
// Preview aren't animated // Preview aren't animated
if (isvideo || photo.mimetype === "image/gif") { if (isvideo || photo.mimetype === 'image/gif') {
previewUrl = getDownloadLink(photo); previewUrl = getDownloadLink(photo);
} }
@ -816,14 +769,8 @@ export default defineComponent({
} }
// Get full image URL // Get full image URL
const fullUrl = isvideo const fullUrl = isvideo ? null : API.IMAGE_DECODABLE(photo.fileid, photo.etag);
? null const fullLoadCond = this.config_fullResAlways ? 'always' : this.config_fullResOnZoom ? 'zoom' : 'never';
: API.IMAGE_DECODABLE(photo.fileid, photo.etag);
const fullLoadCond = this.config_fullResAlways
? "always"
: this.config_fullResOnZoom
? "zoom"
: "never";
return { return {
src: previewUrl, src: previewUrl,
@ -833,24 +780,20 @@ export default defineComponent({
height: h || undefined, height: h || undefined,
thumbCropped: true, thumbCropped: true,
photo: photo, photo: photo,
type: isvideo ? "video" : "image", type: isvideo ? 'video' : 'image',
}; };
}, },
/** Get element for thumbnail if it exists */ /** Get element for thumbnail if it exists */
thumbElem(photo: IPhoto): HTMLImageElement | undefined { thumbElem(photo: IPhoto): HTMLImageElement | undefined {
if (!photo) return; if (!photo) return;
const elems = Array.from( const elems = Array.from(document.querySelectorAll(`.memories-thumb-${photo.key}`));
document.querySelectorAll(`.memories-thumb-${photo.key}`)
);
if (elems.length === 0) return; if (elems.length === 0) return;
if (elems.length === 1) return elems[0] as HTMLImageElement; if (elems.length === 1) return elems[0] as HTMLImageElement;
// Find if any element has the thumb-important class // Find if any element has the thumb-important class
const important = elems.filter((e) => const important = elems.filter((e) => e.classList.contains('thumb-important'));
e.classList.contains("thumb-important")
);
if (important.length > 0) return important[0] as HTMLImageElement; if (important.length > 0) return important[0] as HTMLImageElement;
// Find element within 500px of the screen top // Find element within 500px of the screen top
@ -868,20 +811,20 @@ export default defineComponent({
/** Set the route hash to the given photo */ /** Set the route hash to the given photo */
setRouteHash(photo: IPhoto | undefined) { setRouteHash(photo: IPhoto | undefined) {
if (!photo) { if (!photo) {
if (!this.isOpen && this.$route.hash?.startsWith("#v")) { if (!this.isOpen && this.$route.hash?.startsWith('#v')) {
this.$router.back(); this.$router.back();
// Ensure this does not have the hash, otherwise replace it // Ensure this does not have the hash, otherwise replace it
if (this.$route.hash?.startsWith("#v")) { if (this.$route.hash?.startsWith('#v')) {
this.$router.replace({ this.$router.replace({
hash: "", hash: '',
query: this.$route.query, query: this.$route.query,
}); });
} }
} }
return; return;
} }
const hash = photo ? utils.getViewerHash(photo) : ""; const hash = photo ? utils.getViewerHash(photo) : '';
const route = { const route = {
path: this.$route.path, path: this.$route.path,
query: this.$route.query, query: this.$route.query,
@ -902,9 +845,7 @@ export default defineComponent({
// Prevent editing Live Photos // Prevent editing Live Photos
if (this.isLivePhoto) { if (this.isLivePhoto) {
showError( showError(this.t('memories', 'Editing is currently disabled for Live Photos'));
this.t("memories", "Editing is currently disabled for Live Photos")
);
return; return;
} }
@ -920,9 +861,9 @@ export default defineComponent({
/** Key press events */ /** Key press events */
keydown(e: KeyboardEvent) { keydown(e: KeyboardEvent) {
if ( if (
e.key === "Delete" && e.key === 'Delete' &&
!this.routeIsPublic && !this.routeIsPublic &&
confirm(this.t("memories", "Are you sure you want to delete?")) confirm(this.t('memories', 'Are you sure you want to delete?'))
) { ) {
this.deleteCurrent(); this.deleteCurrent();
} }
@ -970,9 +911,7 @@ export default defineComponent({
/** Play the current live photo */ /** Play the current live photo */
playLivePhoto() { playLivePhoto() {
this.psLivePhoto?.onContentActivate( this.psLivePhoto?.onContentActivate(this.photoswipe!.currSlide as PsSlide);
this.photoswipe!.currSlide as PsSlide
);
}, },
/** Is the current photo a favorite */ /** Is the current photo a favorite */
@ -1020,14 +959,14 @@ export default defineComponent({
/** Open the sidebar */ /** Open the sidebar */
async openSidebar(photo?: IPhoto) { async openSidebar(photo?: IPhoto) {
globalThis.mSidebar.setTab("memories-metadata"); globalThis.mSidebar.setTab('memories-metadata');
photo ??= this.currentPhoto!; photo ??= this.currentPhoto!;
if (this.routeIsPublic) { if (this.routeIsPublic) {
globalThis.mSidebar.open(photo.fileid); globalThis.mSidebar.open(photo.fileid);
} else { } else {
const fileInfo = (await dav.getFiles([photo]))[0]; const fileInfo = (await dav.getFiles([photo]))[0];
const forceNative = fileInfo?.originalFilename?.startsWith("/files/"); const forceNative = fileInfo?.originalFilename?.startsWith('/files/');
globalThis.mSidebar.open(photo.fileid, fileInfo?.filename, forceNative); globalThis.mSidebar.open(photo.fileid, fileInfo?.filename, forceNative);
} }
}, },
@ -1091,10 +1030,7 @@ export default defineComponent({
this.setUiVisible(false); this.setUiVisible(false);
// Start slideshow // Start slideshow
this.slideshowTimer = window.setTimeout( this.slideshowTimer = window.setTimeout(this.slideshowTimerFired, SLIDESHOW_MS);
this.slideshowTimerFired,
SLIDESHOW_MS
);
}, },
/** /**
@ -1108,15 +1044,13 @@ export default defineComponent({
// If this is a video, wait for it to finish // If this is a video, wait for it to finish
if (this.isVideo) { if (this.isVideo) {
// Get active video element // Get active video element
const video = this.photoswipe?.element?.querySelector<HTMLVideoElement>( const video = this.photoswipe?.element?.querySelector<HTMLVideoElement>('.pswp__item.active video');
".pswp__item.active video"
);
// If no video tag is found by now, something likely went wrong. Just skip ahead. // If no video tag is found by now, something likely went wrong. Just skip ahead.
// Otherwise check if video is not ended yet // Otherwise check if video is not ended yet
if (video && video.currentTime < video.duration - 0.1) { if (video && video.currentTime < video.duration - 0.1) {
// Wait for video to finish // Wait for video to finish
video.addEventListener("ended", this.slideshowTimerFired); video.addEventListener('ended', this.slideshowTimerFired);
return; return;
} }
} }
@ -1132,10 +1066,7 @@ export default defineComponent({
resetSlideshowTimer() { resetSlideshowTimer() {
if (this.slideshowTimer) { if (this.slideshowTimer) {
window.clearTimeout(this.slideshowTimer); window.clearTimeout(this.slideshowTimer);
this.slideshowTimer = window.setTimeout( this.slideshowTimer = window.setTimeout(this.slideshowTimerFired, SLIDESHOW_MS);
this.slideshowTimerFired,
SLIDESHOW_MS
);
} }
}, },

View File

@ -1,6 +1,6 @@
import Content from "photoswipe/dist/types/slide/content"; import Content from 'photoswipe/dist/types/slide/content';
import Slide, { _SlideData } from "photoswipe/dist/types/slide/slide"; import Slide, { _SlideData } from 'photoswipe/dist/types/slide/slide';
import { IPhoto } from "../../types"; import { IPhoto } from '../../types';
type PsAugment = { type PsAugment = {
data: _SlideData & { data: _SlideData & {

View File

@ -71,8 +71,7 @@ body.has-viewer header {
height: 100%; height: 100%;
display: block; display: block;
z-index: 1; z-index: 1;
transition: transform 0.3s ease-in-out, visibility 0.3s ease-in-out, transition: transform 0.3s ease-in-out, visibility 0.3s ease-in-out, opacity 0.3s ease-in-out;
opacity 0.3s ease-in-out;
} }
video { video {
@ -123,7 +122,7 @@ body.has-viewer header {
} }
// Make sure empty content is full width // Make sure empty content is full width
[role="note"].empty-content { [role='note'].empty-content {
width: 100%; width: 100%;
} }

View File

@ -1,26 +1,26 @@
import "reflect-metadata"; import 'reflect-metadata';
import Vue from "vue"; import Vue 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 XImg from "./components/frame/XImg.vue"; import XImg from './components/frame/XImg.vue';
import GlobalMixin from "./mixins/GlobalMixin"; import GlobalMixin from './mixins/GlobalMixin';
import App from "./App.vue"; import App from './App.vue';
import Admin from "./components/admin/AdminMain.vue"; import Admin from './components/admin/AdminMain.vue';
import router from "./router"; import router from './router';
import { generateFilePath } from "@nextcloud/router"; import { generateFilePath } from '@nextcloud/router';
import { getRequestToken } from "@nextcloud/auth"; import { getRequestToken } from '@nextcloud/auth';
import type { Route } from "vue-router"; import type { Route } from 'vue-router';
import type { IPhoto } from "./types"; import type { IPhoto } from './types';
import type PlyrType from "plyr"; import type PlyrType from 'plyr';
import type videojsType from "video.js"; import type videojsType from 'video.js';
import "./global.scss"; import './global.scss';
// Global exposed variables // Global exposed variables
declare global { declare global {
var mode: "admin" | "user"; var mode: 'admin' | 'user';
var vueroute: () => Route; var vueroute: () => Route;
var OC: Nextcloud.v24.OC; var OC: Nextcloud.v24.OC;
@ -59,35 +59,30 @@ globalThis.windowInnerWidth = window.innerWidth;
globalThis.windowInnerHeight = window.innerHeight; globalThis.windowInnerHeight = window.innerHeight;
// CSP config for webpack dynamic chunk loading // CSP config for webpack dynamic chunk loading
__webpack_nonce__ = window.btoa(getRequestToken() ?? ""); __webpack_nonce__ = window.btoa(getRequestToken() ?? '');
// Correct the root of the app for chunk loading // Correct the root of the app for chunk loading
// OC.linkTo matches the apps folders // OC.linkTo matches the apps folders
// OC.generateUrl ensure the index.php (or not) // OC.generateUrl ensure the index.php (or not)
// We do not want the index.php since we're loading files // We do not want the index.php since we're loading files
__webpack_public_path__ = generateFilePath("memories", "", "js/"); __webpack_public_path__ = generateFilePath('memories', '', 'js/');
// Generate client id for this instance // Generate client id for this instance
// Does not need to be cryptographically secure // Does not need to be cryptographically secure
const getClientId = (): string => const getClientId = (): string => Math.random().toString(36).substring(2, 15).padEnd(12, '0');
Math.random().toString(36).substring(2, 15).padEnd(12, "0");
globalThis.videoClientId = getClientId(); globalThis.videoClientId = getClientId();
globalThis.videoClientIdPersistent = globalThis.videoClientIdPersistent = localStorage.getItem('videoClientIdPersistent') ?? getClientId();
localStorage.getItem("videoClientIdPersistent") ?? getClientId(); localStorage.setItem('videoClientIdPersistent', globalThis.videoClientIdPersistent);
localStorage.setItem(
"videoClientIdPersistent",
globalThis.videoClientIdPersistent
);
Vue.mixin(GlobalMixin as any); Vue.mixin(GlobalMixin as any);
Vue.use(VueVirtualScroller); Vue.use(VueVirtualScroller);
Vue.component("XImg", XImg); Vue.component('XImg', XImg);
// 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
// https://github.com/nextcloud/server/blob/5bf3d1bb384da56adbf205752be8f840aac3b0c5/lib/private/legacy/template.php#L120-L122 // https://github.com/nextcloud/server/blob/5bf3d1bb384da56adbf205752be8f840aac3b0c5/lib/private/legacy/template.php#L120-L122
window.addEventListener("DOMContentLoaded", () => { window.addEventListener('DOMContentLoaded', () => {
if (!globalThis.OCA.Files) { if (!globalThis.OCA.Files) {
globalThis.OCA.Files = {}; globalThis.OCA.Files = {};
} }
@ -105,20 +100,20 @@ window.addEventListener("DOMContentLoaded", () => {
let app = null; let app = null;
const adminSection = document.getElementById("memories-admin-content"); const adminSection = document.getElementById('memories-admin-content');
if (adminSection) { if (adminSection) {
app = new Vue({ app = new Vue({
el: "#memories-admin-content", el: '#memories-admin-content',
render: (h) => h(Admin), render: (h) => h(Admin),
}); });
globalThis.mode = "admin"; globalThis.mode = 'admin';
} else { } else {
app = new Vue({ app = new Vue({
el: "#content", el: '#content',
router, router,
render: (h) => h(App), render: (h) => h(App),
}); });
globalThis.mode = "user"; globalThis.mode = 'user';
} }
export default app; export default app;

View File

@ -1,15 +1,15 @@
import { translate as t, translatePlural as n } from "@nextcloud/l10n"; import { translate as t, translatePlural as n } from '@nextcloud/l10n';
import { constants } from "../services/Utils"; import { constants } from '../services/Utils';
import { loadState } from "@nextcloud/initial-state"; import { loadState } from '@nextcloud/initial-state';
import { defineComponent } from "vue"; import { defineComponent } from 'vue';
export default defineComponent({ export default defineComponent({
name: "GlobalMixin", name: 'GlobalMixin',
data: () => ({ data: () => ({
...constants, ...constants,
state_noDownload: loadState("memories", "no_download", false) !== false, state_noDownload: loadState('memories', 'no_download', false) !== false,
}), }),
methods: { methods: {

View File

@ -1,67 +1,37 @@
import { emit, subscribe, unsubscribe } from "@nextcloud/event-bus"; 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"; import { defineComponent } from 'vue';
const eventName = "memories:user-config-changed"; const eventName = 'memories:user-config-changed';
const localSettings = [ const localSettings = ['squareThumbs', 'fullResOnZoom', 'fullResAlways', 'showFaceRect', 'albumListSort'];
"squareThumbs",
"fullResOnZoom",
"fullResAlways",
"showFaceRect",
"albumListSort",
];
export default defineComponent({ export default defineComponent({
name: "UserConfig", name: 'UserConfig',
data: () => ({ data: () => ({
config_timelinePath: loadState( config_timelinePath: loadState('memories', 'timelinePath', <string>'') as string,
"memories", config_foldersPath: loadState('memories', 'foldersPath', <string>'/') as string,
"timelinePath",
<string>""
) as string,
config_foldersPath: loadState(
"memories",
"foldersPath",
<string>"/"
) as string,
config_showHidden: config_showHidden: loadState('memories', 'showHidden', <string>'false') === 'true',
loadState("memories", "showHidden", <string>"false") === "true", config_sortFolderMonth: loadState('memories', 'sortFolderMonth', <string>'false') === 'true',
config_sortFolderMonth: config_sortAlbumMonth: loadState('memories', 'sortAlbumMonth', <string>'true') === 'true',
loadState("memories", "sortFolderMonth", <string>"false") === "true", config_enableTopMemories: loadState('memories', 'enableTopMemories', <string>'false') === 'true',
config_sortAlbumMonth:
loadState("memories", "sortAlbumMonth", <string>"true") === "true",
config_enableTopMemories:
loadState("memories", "enableTopMemories", <string>"false") === "true",
config_tagsEnabled: Boolean( config_tagsEnabled: Boolean(loadState('memories', 'systemtags', <string>'')),
loadState("memories", "systemtags", <string>"") config_recognizeEnabled: Boolean(loadState('memories', 'recognize', <string>'')),
), config_facerecognitionInstalled: Boolean(loadState('memories', 'facerecognitionInstalled', <string>'')),
config_recognizeEnabled: Boolean( config_facerecognitionEnabled: Boolean(loadState('memories', 'facerecognitionEnabled', <string>'')),
loadState("memories", "recognize", <string>"") config_albumsEnabled: Boolean(loadState('memories', 'albums', <string>'')),
),
config_facerecognitionInstalled: Boolean(
loadState("memories", "facerecognitionInstalled", <string>"")
),
config_facerecognitionEnabled: Boolean(
loadState("memories", "facerecognitionEnabled", <string>"")
),
config_albumsEnabled: Boolean(loadState("memories", "albums", <string>"")),
config_placesGis: Number(loadState("memories", "places_gis", <string>"-1")), config_placesGis: Number(loadState('memories', 'places_gis', <string>'-1')),
config_squareThumbs: localStorage.getItem("memories_squareThumbs") === "1", config_squareThumbs: localStorage.getItem('memories_squareThumbs') === '1',
config_fullResOnZoom: config_fullResOnZoom: localStorage.getItem('memories_fullResOnZoom') !== '0',
localStorage.getItem("memories_fullResOnZoom") !== "0", config_fullResAlways: localStorage.getItem('memories_fullResAlways') === '1',
config_fullResAlways: config_showFaceRect: localStorage.getItem('memories_showFaceRect') === '1',
localStorage.getItem("memories_fullResAlways") === "1", config_albumListSort: Number(localStorage.getItem('memories_albumListSort') || 1),
config_showFaceRect: localStorage.getItem("memories_showFaceRect") === "1",
config_albumListSort: Number(
localStorage.getItem("memories_albumListSort") || 1
),
config_eventName: eventName, config_eventName: eventName,
}), }),
@ -76,17 +46,17 @@ export default defineComponent({
methods: { methods: {
updateLocalSetting({ setting, value }) { updateLocalSetting({ setting, value }) {
this["config_" + setting] = value; this['config_' + setting] = value;
}, },
async updateSetting(setting: string) { async updateSetting(setting: string) {
const value = this["config_" + setting]; const value = this['config_' + setting];
if (localSettings.includes(setting)) { if (localSettings.includes(setting)) {
if (typeof value === "boolean") { if (typeof value === 'boolean') {
localStorage.setItem("memories_" + setting, value ? "1" : "0"); localStorage.setItem('memories_' + setting, value ? '1' : '0');
} else { } else {
localStorage.setItem("memories_" + setting, value); localStorage.setItem('memories_' + setting, value);
} }
} else { } else {
// Long time save setting // Long time save setting

View File

@ -1,152 +1,152 @@
import { generateUrl } from "@nextcloud/router"; import { generateUrl } from '@nextcloud/router';
import { translate as t } from "@nextcloud/l10n"; import { translate as t } from '@nextcloud/l10n';
import Router from "vue-router"; import Router from 'vue-router';
import Vue from "vue"; import Vue from 'vue';
import Timeline from "./components/Timeline.vue"; import Timeline from './components/Timeline.vue';
import SplitTimeline from "./components/SplitTimeline.vue"; import SplitTimeline from './components/SplitTimeline.vue';
import ClusterView from "./components/ClusterView.vue"; import ClusterView from './components/ClusterView.vue';
Vue.use(Router); Vue.use(Router);
export default new Router({ export default new Router({
mode: "history", 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"), base: generateUrl('/apps/memories'),
linkActiveClass: "active", linkActiveClass: 'active',
routes: [ routes: [
{ {
path: "/", path: '/',
component: Timeline, component: Timeline,
name: "timeline", name: 'timeline',
props: (route) => ({ props: (route) => ({
rootTitle: t("memories", "Timeline"), rootTitle: t('memories', 'Timeline'),
}), }),
}, },
{ {
path: "/folders/:path*", path: '/folders/:path*',
component: Timeline, component: Timeline,
name: "folders", name: 'folders',
props: (route) => ({ props: (route) => ({
rootTitle: t("memories", "Folders"), rootTitle: t('memories', 'Folders'),
}), }),
}, },
{ {
path: "/favorites", path: '/favorites',
component: Timeline, component: Timeline,
name: "favorites", name: 'favorites',
props: (route) => ({ props: (route) => ({
rootTitle: t("memories", "Favorites"), rootTitle: t('memories', 'Favorites'),
}), }),
}, },
{ {
path: "/videos", path: '/videos',
component: Timeline, component: Timeline,
name: "videos", name: 'videos',
props: (route) => ({ props: (route) => ({
rootTitle: t("memories", "Videos"), rootTitle: t('memories', 'Videos'),
}), }),
}, },
{ {
path: "/albums/:user?/:name?", path: '/albums/:user?/:name?',
component: ClusterView, component: ClusterView,
name: "albums", name: 'albums',
props: (route) => ({ props: (route) => ({
rootTitle: t("memories", "Albums"), rootTitle: t('memories', 'Albums'),
}), }),
}, },
{ {
path: "/archive", path: '/archive',
component: Timeline, component: Timeline,
name: "archive", name: 'archive',
props: (route) => ({ props: (route) => ({
rootTitle: t("memories", "Archive"), rootTitle: t('memories', 'Archive'),
}), }),
}, },
{ {
path: "/thisday", path: '/thisday',
component: Timeline, component: Timeline,
name: "thisday", name: 'thisday',
props: (route) => ({ props: (route) => ({
rootTitle: t("memories", "On this day"), rootTitle: t('memories', 'On this day'),
}), }),
}, },
{ {
path: "/recognize/:user?/:name?", path: '/recognize/:user?/:name?',
component: ClusterView, component: ClusterView,
name: "recognize", name: 'recognize',
props: (route) => ({ props: (route) => ({
rootTitle: t("memories", "People"), rootTitle: t('memories', 'People'),
}), }),
}, },
{ {
path: "/facerecognition/:user?/:name?", path: '/facerecognition/:user?/:name?',
component: ClusterView, component: ClusterView,
name: "facerecognition", name: 'facerecognition',
props: (route) => ({ props: (route) => ({
rootTitle: t("memories", "People"), rootTitle: t('memories', 'People'),
}), }),
}, },
{ {
path: "/places/:name*", path: '/places/:name*',
component: ClusterView, component: ClusterView,
name: "places", name: 'places',
props: (route) => ({ props: (route) => ({
rootTitle: t("memories", "Places"), rootTitle: t('memories', 'Places'),
}), }),
}, },
{ {
path: "/tags/:name*", path: '/tags/:name*',
component: ClusterView, component: ClusterView,
name: "tags", name: 'tags',
props: (route) => ({ props: (route) => ({
rootTitle: t("memories", "Tags"), rootTitle: t('memories', 'Tags'),
}), }),
}, },
{ {
path: "/maps", path: '/maps',
name: "maps", name: 'maps',
// router-link doesn't support external url, let's force the redirect // router-link doesn't support external url, let's force the redirect
beforeEnter() { beforeEnter() {
window.open(generateUrl("/apps/maps"), "_blank"); window.open(generateUrl('/apps/maps'), '_blank');
}, },
}, },
{ {
path: "/s/:token", path: '/s/:token',
component: Timeline, component: Timeline,
name: "folder-share", name: 'folder-share',
props: (route) => ({ props: (route) => ({
rootTitle: t("memories", "Shared Folder"), rootTitle: t('memories', 'Shared Folder'),
}), }),
}, },
{ {
path: "/a/:token", path: '/a/:token',
component: Timeline, component: Timeline,
name: "album-share", name: 'album-share',
props: (route) => ({ props: (route) => ({
rootTitle: t("memories", "Shared Album"), rootTitle: t('memories', 'Shared Album'),
}), }),
}, },
{ {
path: "/map", path: '/map',
component: SplitTimeline, component: SplitTimeline,
name: "map", name: 'map',
props: (route) => ({ props: (route) => ({
rootTitle: t("memories", "Map"), rootTitle: t('memories', 'Map'),
}), }),
}, },
], ],

View File

@ -1,14 +1,14 @@
import { precacheAndRoute } from "workbox-precaching"; import { precacheAndRoute } from 'workbox-precaching';
import { NetworkFirst, CacheFirst } from "workbox-strategies"; import { NetworkFirst, CacheFirst } from 'workbox-strategies';
import { registerRoute } from "workbox-routing"; import { registerRoute } from 'workbox-routing';
import { ExpirationPlugin } from "workbox-expiration"; import { ExpirationPlugin } from 'workbox-expiration';
precacheAndRoute(self.__WB_MANIFEST); precacheAndRoute(self.__WB_MANIFEST);
registerRoute( registerRoute(
/^.*\/apps\/memories\/api\/video\/livephoto\/.*/, /^.*\/apps\/memories\/api\/video\/livephoto\/.*/,
new CacheFirst({ new CacheFirst({
cacheName: "livephotos", cacheName: 'livephotos',
plugins: [ plugins: [
new ExpirationPlugin({ new ExpirationPlugin({
maxAgeSeconds: 3600 * 24 * 7, // days maxAgeSeconds: 3600 * 24 * 7, // days
@ -27,11 +27,9 @@ const networkOnly = [/^.*\/apps\/memories\/api\/.*/];
// Cache pages for same-origin requests only // Cache pages for same-origin requests only
registerRoute( registerRoute(
({ url }) => ({ url }) => url.origin === self.location.origin && !networkOnly.some((regex) => regex.test(url.href)),
url.origin === self.location.origin &&
!networkOnly.some((regex) => regex.test(url.href)),
new NetworkFirst({ new NetworkFirst({
cacheName: "pages", cacheName: 'pages',
plugins: [ plugins: [
new ExpirationPlugin({ new ExpirationPlugin({
maxAgeSeconds: 3600 * 24 * 7, // days maxAgeSeconds: 3600 * 24 * 7, // days
@ -41,7 +39,7 @@ registerRoute(
}) })
); );
self.addEventListener("activate", (event) => { self.addEventListener('activate', (event) => {
// Take control of all pages under this SW's scope immediately, // Take control of all pages under this SW's scope immediately,
// instead of waiting for reload/navigation. // instead of waiting for reload/navigation.
event.waitUntil(self.clients.claim()); event.waitUntil(self.clients.claim());

View File

@ -1,17 +1,17 @@
import { generateUrl } from "@nextcloud/router"; import { generateUrl } from '@nextcloud/router';
import { ClusterTypes } from "../types"; import { ClusterTypes } from '../types';
const BASE = "/apps/memories/api"; const BASE = '/apps/memories/api';
const gen = generateUrl; const gen = generateUrl;
/** Add auth token to this URL */ /** Add auth token to this URL */
function tok(url: string) { function tok(url: string) {
const route = vueroute(); const route = vueroute();
if (route.name === "folder-share") { if (route.name === 'folder-share') {
const token = <string>route.params.token; const token = <string>route.params.token;
url = API.Q(url, { token }); url = API.Q(url, { token });
} else if (route.name === "album-share") { } else if (route.name === 'album-share') {
const token = <string>route.params.token; const token = <string>route.params.token;
url = API.Q(url, { token, albums: token }); url = API.Q(url, { token, albums: token });
} }
@ -19,31 +19,28 @@ function tok(url: string) {
} }
export enum DaysFilterType { export enum DaysFilterType {
FAVORITES = "fav", FAVORITES = 'fav',
VIDEOS = "vid", VIDEOS = 'vid',
FOLDER = "folder", FOLDER = 'folder',
ARCHIVE = "archive", ARCHIVE = 'archive',
ALBUM = "albums", ALBUM = 'albums',
RECOGNIZE = "recognize", RECOGNIZE = 'recognize',
FACERECOGNITION = "facerecognition", FACERECOGNITION = 'facerecognition',
PLACE = "places", PLACE = 'places',
TAG = "tags", TAG = 'tags',
MAP_BOUNDS = "mapbounds", MAP_BOUNDS = 'mapbounds',
FACE_RECT = "facerect", FACE_RECT = 'facerect',
RECURSIVE = "recursive", RECURSIVE = 'recursive',
MONTH_VIEW = "monthView", MONTH_VIEW = 'monthView',
REVERSE = "reverse", REVERSE = 'reverse',
} }
export class API { export class API {
static Q( static Q(url: string, query: string | URLSearchParams | Object | undefined | null) {
url: string,
query: string | URLSearchParams | Object | undefined | null
) {
if (!query) return url; if (!query) return url;
if (typeof query === "object") { if (typeof query === 'object') {
// Clean up undefined and null // Clean up undefined and null
for (const key of Object.keys(query)) { for (const key of Object.keys(query)) {
if (query[key] === undefined || query[key] === null) { if (query[key] === undefined || query[key] === null) {
@ -64,7 +61,7 @@ export class API {
if (!query) return url; if (!query) return url;
if (url.indexOf("?") > -1) { if (url.indexOf('?') > -1) {
return `${url}&${query}`; return `${url}&${query}`;
} else { } else {
return `${url}?${query}`; return `${url}?${query}`;
@ -79,7 +76,7 @@ export class API {
return tok(gen(`${BASE}/days/{id}`, { id })); return tok(gen(`${BASE}/days/{id}`, { id }));
} }
static DAYS_FILTER(query: any, filter: DaysFilterType, value: string = "1") { static DAYS_FILTER(query: any, filter: DaysFilterType, value: string = '1') {
query[filter] = value; query[filter] = value;
} }
@ -110,7 +107,7 @@ export class API {
return gen(`${BASE}/tags/set/{fileid}`, { fileid }); return gen(`${BASE}/tags/set/{fileid}`, { fileid });
} }
static FACE_LIST(app: "recognize" | "facerecognition") { static FACE_LIST(app: 'recognize' | 'facerecognition') {
return gen(`${BASE}/clusters/${app}`); return gen(`${BASE}/clusters/${app}`);
} }
@ -146,7 +143,7 @@ export class API {
return gen(`${BASE}/image/edit/{id}`, { id }); return gen(`${BASE}/image/edit/{id}`, { id });
} }
static VIDEO_TRANSCODE(fileid: number, file = "index.m3u8") { static VIDEO_TRANSCODE(fileid: number, file = 'index.m3u8') {
return tok( return tok(
gen(`${BASE}/video/transcode/{videoClientId}/{fileid}/{file}`, { gen(`${BASE}/video/transcode/{videoClientId}/{fileid}/{file}`, {
videoClientId, videoClientId,
@ -189,9 +186,7 @@ export class API {
} }
static SYSTEM_CONFIG(setting: string | null) { static SYSTEM_CONFIG(setting: string | null) {
return setting return setting ? gen(`${BASE}/system-config/{setting}`, { setting }) : gen(`${BASE}/system-config`);
? gen(`${BASE}/system-config/{setting}`, { setting })
: gen(`${BASE}/system-config`);
} }
static SYSTEM_STATUS() { static SYSTEM_STATUS() {

View File

@ -20,13 +20,13 @@
* *
*/ */
import * as webdav from "webdav"; import * as webdav from 'webdav';
import axios from "@nextcloud/axios"; import axios from '@nextcloud/axios';
import parseUrl from "url-parse"; import parseUrl from 'url-parse';
import { generateRemoteUrl } from "@nextcloud/router"; 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';
const prepareRequestOptionsOld = rq.prepareRequestOptions.bind(rq); const prepareRequestOptionsOld = rq.prepareRequestOptions.bind(rq);
(<any>rq).prepareRequestOptions = (requestOptions, context, userOptions) => { (<any>rq).prepareRequestOptions = (requestOptions, context, userOptions) => {
requestOptions.method = userOptions.method || requestOptions.method; requestOptions.method = userOptions.method || requestOptions.method;
@ -35,10 +35,10 @@ const prepareRequestOptionsOld = rq.prepareRequestOptions.bind(rq);
// force our axios // force our axios
const patcher = webdav.getPatcher(); const patcher = webdav.getPatcher();
patcher.patch("request", axios); patcher.patch('request', axios);
// init webdav client on default dav endpoint // init webdav client on default dav endpoint
const remote = generateRemoteUrl("dav"); const remote = generateRemoteUrl('dav');
const client = webdav.createClient(remote); const client = webdav.createClient(remote);
export const remotePath = parseUrl(remote).pathname; export const remotePath = parseUrl(remote).pathname;

View File

@ -1,12 +1,12 @@
export * from "./dav/base"; export * from './dav/base';
export * from "./dav/albums"; export * from './dav/albums';
export * from "./dav/archive"; export * from './dav/archive';
export * from "./dav/download"; export * from './dav/download';
export * from "./dav/face"; export * from './dav/face';
export * from "./dav/favorites"; export * from './dav/favorites';
export * from "./dav/folders"; export * from './dav/folders';
export * from "./dav/onthisday"; export * from './dav/onthisday';
export * from "./dav/tags"; export * from './dav/tags';
export * from "./dav/other"; export * from './dav/other';
export * from "./dav/places"; export * from './dav/places';
export * from "./dav/single-item"; export * from './dav/single-item';

View File

@ -19,10 +19,10 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
* *
*/ */
import camelcase from "camelcase"; import camelcase from 'camelcase';
import { IPhoto } from "../types"; import { IPhoto } from '../types';
import { API } from "./API"; import { API } from './API';
import { isNumber } from "./utils/algo"; import { isNumber } from './utils/algo';
/** /**
* Get an url encoded path * Get an url encoded path
@ -31,11 +31,11 @@ import { isNumber } from "./utils/algo";
* @return {string} url encoded file path * @return {string} url encoded file path
*/ */
const encodeFilePath = function (path) { const encodeFilePath = function (path) {
const pathSections = (path.startsWith("/") ? path : `/${path}`).split("/"); const pathSections = (path.startsWith('/') ? path : `/${path}`).split('/');
let relativePath = ""; let relativePath = '';
pathSections.forEach((section) => { pathSections.forEach((section) => {
if (section !== "") { if (section !== '') {
relativePath += "/" + encodeURIComponent(section); relativePath += '/' + encodeURIComponent(section);
} }
}); });
return relativePath; return relativePath;
@ -48,9 +48,9 @@ const encodeFilePath = function (path) {
* @return {string[]} [dirPath, fileName] * @return {string[]} [dirPath, fileName]
*/ */
const extractFilePaths = function (path) { const extractFilePaths = function (path) {
const pathSections = path.split("/"); const pathSections = path.split('/');
const fileName = pathSections[pathSections.length - 1]; const fileName = pathSections[pathSections.length - 1];
const dirPath = pathSections.slice(0, pathSections.length - 1).join("/"); const dirPath = pathSections.slice(0, pathSections.length - 1).join('/');
return [dirPath, fileName]; return [dirPath, fileName];
}; };
@ -73,23 +73,18 @@ const sortCompare = function (fileInfo1, fileInfo2, key, asc = true) {
// if this is a number, let's sort by integer // if this is a number, let's sort by integer
if (isNumber(fileInfo1[key]) && isNumber(fileInfo2[key])) { if (isNumber(fileInfo1[key]) && isNumber(fileInfo2[key])) {
return asc return asc ? Number(fileInfo2[key]) - Number(fileInfo1[key]) : Number(fileInfo1[key]) - Number(fileInfo2[key]);
? Number(fileInfo2[key]) - Number(fileInfo1[key])
: Number(fileInfo1[key]) - Number(fileInfo2[key]);
} }
// else we sort by string, so let's sort directories first // else we sort by string, so let's sort directories first
if (fileInfo1.type !== "file" && fileInfo2.type === "file") { if (fileInfo1.type !== 'file' && fileInfo2.type === 'file') {
return asc ? -1 : 1; return asc ? -1 : 1;
} else if (fileInfo1.type === "file" && fileInfo2.type !== "file") { } else if (fileInfo1.type === 'file' && fileInfo2.type !== 'file') {
return asc ? 1 : -1; return asc ? 1 : -1;
} }
// if this is a date, let's sort by date // if this is a date, let's sort by date
if ( if (isNumber(new Date(fileInfo1[key]).getTime()) && isNumber(new Date(fileInfo2[key]).getTime())) {
isNumber(new Date(fileInfo1[key]).getTime()) &&
isNumber(new Date(fileInfo2[key]).getTime())
) {
return asc return asc
? new Date(fileInfo2[key]).getTime() - new Date(fileInfo1[key]).getTime() ? new Date(fileInfo2[key]).getTime() - new Date(fileInfo1[key]).getTime()
: new Date(fileInfo1[key]).getTime() - new Date(fileInfo2[key]).getTime(); : new Date(fileInfo1[key]).getTime() - new Date(fileInfo2[key]).getTime();
@ -97,18 +92,8 @@ const sortCompare = function (fileInfo1, fileInfo2, key, asc = true) {
// finally sort by name // finally sort by name
return asc return asc
? fileInfo1[key] ? fileInfo1[key]?.toString()?.localeCompare(fileInfo2[key].toString(), globalThis.OC.getLanguage()) || 1
?.toString() : -fileInfo1[key]?.toString()?.localeCompare(fileInfo2[key].toString(), globalThis.OC.getLanguage()) || -1;
?.localeCompare(
fileInfo2[key].toString(),
globalThis.OC.getLanguage()
) || 1
: -fileInfo1[key]
?.toString()
?.localeCompare(
fileInfo2[key].toString(),
globalThis.OC.getLanguage()
) || -1;
}; };
const genFileInfo = function (obj) { const genFileInfo = function (obj) {
@ -118,13 +103,13 @@ const genFileInfo = function (obj) {
const data = obj[key]; const data = obj[key];
// flatten object if any // flatten object if any
if (!!data && typeof data === "object") { if (!!data && typeof data === 'object') {
Object.assign(fileInfo, genFileInfo(data)); Object.assign(fileInfo, genFileInfo(data));
} else { } else {
// format key and add it to the fileInfo // format key and add it to the fileInfo
if (data === "false") { if (data === 'false') {
fileInfo[camelcase(key)] = false; fileInfo[camelcase(key)] = false;
} else if (data === "true") { } else if (data === 'true') {
fileInfo[camelcase(key)] = true; fileInfo[camelcase(key)] = true;
} else { } else {
fileInfo[camelcase(key)] = isNumber(data) ? Number(data) : data; fileInfo[camelcase(key)] = isNumber(data) ? Number(data) : data;

View File

@ -1,4 +1,4 @@
import justifiedLayout from "justified-layout"; import justifiedLayout from 'justified-layout';
/** /**
* Generate the layout matrix. * Generate the layout matrix.
@ -93,8 +93,7 @@ export function getLayout(
// Number of photos left // Number of photos left
const numLeft = input.length - photoId - 1; const numLeft = input.length - photoId - 1;
// Number of photos needed for perfect fill after using n // Number of photos needed for perfect fill after using n
const needFill = (n: number) => const needFill = (n: number) => opts.numCols - col - 2 + (n / 2 - 1) * (opts.numCols - 2);
opts.numCols - col - 2 + (n / 2 - 1) * (opts.numCols - 2);
let canUse4 = let canUse4 =
// We have enough space // We have enough space
@ -222,12 +221,10 @@ export function getLayout(
} }
function flagMatrixStr(matrix: number[][], numFlag: number) { function flagMatrixStr(matrix: number[][], numFlag: number) {
let str = ""; let str = '';
for (let i = 0; i < matrix.length; i++) { for (let i = 0; i < matrix.length; i++) {
const rstr = matrix[i] const rstr = matrix[i].map((v) => v.toString(2).padStart(numFlag, '0')).join(' ');
.map((v) => v.toString(2).padStart(numFlag, "0")) str += i.toString().padStart(2) + ' | ' + rstr + '\n';
.join(" ");
str += i.toString().padStart(2) + " | " + rstr + "\n";
} }
return str; return str;
} }

View File

@ -1,5 +1,5 @@
export * from "./utils/algo"; export * from './utils/algo';
export * from "./utils/cache"; export * from './utils/cache';
export * from "./utils/const"; export * from './utils/const';
export * from "./utils/date"; export * from './utils/date';
export * from "./utils/helpers"; export * from './utils/helpers';

View File

@ -1,11 +1,11 @@
import * as base from "./base"; import * as base from './base';
import { getCurrentUser } from "@nextcloud/auth"; import { getCurrentUser } from '@nextcloud/auth';
import { showError } from "@nextcloud/dialogs"; import { showError } from '@nextcloud/dialogs';
import { translate as t } from "@nextcloud/l10n"; import { translate as t } from '@nextcloud/l10n';
import { IAlbum, IFileInfo, IPhoto } from "../../types"; import { IAlbum, IFileInfo, IPhoto } from '../../types';
import { API } from "../API"; import { API } from '../API';
import axios from "@nextcloud/axios"; import axios from '@nextcloud/axios';
import client from "../DavClient"; import client from '../DavClient';
/** /**
* Get DAV path for album * Get DAV path for album
@ -30,9 +30,7 @@ export async function getAlbums(type: 1 | 2 | 3, sortOrder: 1 | 2) {
// Response is already sorted by date, sort otherwise // Response is already sorted by date, sort otherwise
if (sortOrder === 2) { if (sortOrder === 2) {
data.sort((a, b) => data.sort((a, b) => a.name.localeCompare(b.name, undefined, { sensitivity: 'base' }));
a.name.localeCompare(b.name, undefined, { sensitivity: "base" })
);
} }
return data; return data;
@ -46,11 +44,7 @@ export async function getAlbums(type: 1 | 2 | 3, sortOrder: 1 | 2) {
* @param photos List of photos to add * @param photos List of photos to add
* @returns Generator * @returns Generator
*/ */
export async function* addToAlbum( export async function* addToAlbum(user: string, name: string, photos: IPhoto[]) {
user: string,
name: string,
photos: IPhoto[]
) {
// Get files data // Get files data
let fileInfos = await base.getFiles(photos); let fileInfos = await base.getFiles(photos);
@ -68,12 +62,12 @@ export async function* addToAlbum(
} }
showError( showError(
t("memories", "Failed to add {filename} to album.", { t('memories', 'Failed to add {filename} to album.', {
filename: f.filename, filename: f.filename,
}) })
); );
console.error("DAV COPY error", e.response?.data); console.error('DAV COPY error', e.response?.data);
return 0; return 0;
} }
}); });
@ -89,21 +83,15 @@ export async function* addToAlbum(
* @param photos List of photos to remove * @param photos List of photos to remove
* @returns Generator * @returns Generator
*/ */
export async function* removeFromAlbum( export async function* removeFromAlbum(user: string, name: string, photos: IPhoto[]) {
user: string,
name: string,
photos: IPhoto[]
) {
// Add each file // Add each file
const calls = photos.map((f) => async () => { const calls = photos.map((f) => async () => {
try { try {
await client.deleteFile( await client.deleteFile(`/photos/${user}/albums/${name}/${f.fileid}-${f.basename}`);
`/photos/${user}/albums/${name}/${f.fileid}-${f.basename}`
);
return f.fileid; return f.fileid;
} catch (e) { } catch (e) {
showError( showError(
t("memories", "Failed to remove {filename}.", { t('memories', 'Failed to remove {filename}.', {
filename: f.basename ?? f.fileid, filename: f.basename ?? f.fileid,
}) })
); );
@ -119,12 +107,10 @@ export async function* removeFromAlbum(
*/ */
export async function createAlbum(albumName: string) { export async function createAlbum(albumName: string) {
try { try {
await client.createDirectory( await client.createDirectory(`/photos/${getCurrentUser()?.uid}/albums/${albumName}`);
`/photos/${getCurrentUser()?.uid}/albums/${albumName}`
);
} catch (error) { } catch (error) {
console.error(error); console.error(error);
showError(t("photos", "Failed to create {albumName}.", { albumName })); showError(t('photos', 'Failed to create {albumName}.', { albumName }));
} }
} }
@ -140,19 +126,19 @@ export async function updateAlbum(album: any, { albumName, properties }: any) {
const stringifiedProperties = Object.entries(properties) const stringifiedProperties = Object.entries(properties)
.map(([name, value]) => { .map(([name, value]) => {
switch (typeof value) { switch (typeof value) {
case "string": case 'string':
return `<nc:${name}>${value}</nc:${name}>`; return `<nc:${name}>${value}</nc:${name}>`;
case "object": case 'object':
return `<nc:${name}>${JSON.stringify(value)}</nc:${name}>`; return `<nc:${name}>${JSON.stringify(value)}</nc:${name}>`;
default: default:
return ""; return '';
} }
}) })
.join(); .join();
try { try {
await client.customRequest(album.filename, { await client.customRequest(album.filename, {
method: "PROPPATCH", method: 'PROPPATCH',
data: `<?xml version="1.0"?> data: `<?xml version="1.0"?>
<d:propertyupdate xmlns:d="DAV:" <d:propertyupdate xmlns:d="DAV:"
xmlns:oc="http://owncloud.org/ns" xmlns:oc="http://owncloud.org/ns"
@ -170,11 +156,10 @@ export async function updateAlbum(album: any, { albumName, properties }: any) {
} catch (error) { } catch (error) {
console.error(error); console.error(error);
showError( showError(
t( t('photos', 'Failed to update properties of {albumName} with {properties}.', {
"photos", albumName,
"Failed to update properties of {albumName} with {properties}.", properties: JSON.stringify(properties),
{ albumName, properties: JSON.stringify(properties) } })
)
); );
return album; return album;
} }
@ -216,10 +201,7 @@ export async function getAlbum(user: string, name: string, extraProps = {}) {
} }
/** Rename an album */ /** Rename an album */
export async function renameAlbum( export async function renameAlbum(album: any, { currentAlbumName, newAlbumName }) {
album: any,
{ currentAlbumName, newAlbumName }
) {
const newAlbum = { ...album, basename: newAlbumName }; const newAlbum = { ...album, basename: newAlbumName };
try { try {
await client.moveFile( await client.moveFile(
@ -230,7 +212,7 @@ export async function renameAlbum(
} catch (error) { } catch (error) {
console.error(error); console.error(error);
showError( showError(
t("photos", "Failed to rename {currentAlbumName} to {newAlbumName}.", { t('photos', 'Failed to rename {currentAlbumName} to {newAlbumName}.', {
currentAlbumName, currentAlbumName,
newAlbumName, newAlbumName,
}) })
@ -240,11 +222,7 @@ export async function renameAlbum(
} }
/** Get fileinfo objects from album photos */ /** Get fileinfo objects from album photos */
export function getAlbumFileInfos( export function getAlbumFileInfos(photos: IPhoto[], albumUser: string, albumName: string): IFileInfo[] {
photos: IPhoto[],
albumUser: string,
albumName: string
): IFileInfo[] {
const uid = getCurrentUser()?.uid; const uid = getCurrentUser()?.uid;
const collection = const collection =
albumUser === uid albumUser === uid

View File

@ -1,8 +1,8 @@
import * as base from "./base"; import * as base from './base';
import { showError } from "@nextcloud/dialogs"; import { showError } from '@nextcloud/dialogs';
import { translate as t, translatePlural as n } from "@nextcloud/l10n"; import { translate as t, translatePlural as n } from '@nextcloud/l10n';
import axios from "@nextcloud/axios"; import axios from '@nextcloud/axios';
import { API } from "../API"; import { API } from '../API';
/** /**
* Archive or unarchive a single file * Archive or unarchive a single file
@ -32,10 +32,9 @@ export async function* archiveFilesByIds(fileIds: number[], archive: boolean) {
await archiveFile(id, archive); await archiveFile(id, archive);
return id as number; return id as number;
} catch (error) { } catch (error) {
console.error("Failed to (un)archive", id, error); console.error('Failed to (un)archive', id, error);
const msg = const msg = error?.response?.data?.message || t('memories', 'General Failure');
error?.response?.data?.message || t("memories", "General Failure"); showError(t('memories', 'Error: {msg}', { msg }));
showError(t("memories", "Error: {msg}", { msg }));
return 0; return 0;
} }
}); });

View File

@ -1,13 +1,13 @@
import { getCurrentUser } from "@nextcloud/auth"; import { getCurrentUser } from '@nextcloud/auth';
import { showError } from "@nextcloud/dialogs"; import { showError } from '@nextcloud/dialogs';
import { translate as t } from "@nextcloud/l10n"; import { translate as t } from '@nextcloud/l10n';
import axios from "@nextcloud/axios"; import axios from '@nextcloud/axios';
import { IFileInfo, IPhoto } from "../../types"; import { IFileInfo, IPhoto } from '../../types';
import { genFileInfo } from "../FileUtils"; import { genFileInfo } from '../FileUtils';
import { getAlbumFileInfos } from "./albums"; import { getAlbumFileInfos } from './albums';
import * as utils from "../Utils"; import * as utils from '../Utils';
import client from "../DavClient"; import client from '../DavClient';
export const props = ` export const props = `
<oc:fileid /> <oc:fileid />
@ -21,18 +21,18 @@ export const props = `
<d:resourcetype />`; <d:resourcetype />`;
export const IMAGE_MIME_TYPES = [ export const IMAGE_MIME_TYPES = [
"image/png", 'image/png',
"image/jpeg", 'image/jpeg',
"image/heic", 'image/heic',
"image/png", 'image/png',
"image/tiff", 'image/tiff',
"image/gif", 'image/gif',
"image/bmp", 'image/bmp',
"video/mpeg", 'video/mpeg',
"video/webm", 'video/webm',
"video/mp4", 'video/mp4',
"video/quicktime", 'video/quicktime',
"video/x-matroska", 'video/x-matroska',
]; ];
const GET_FILE_CHUNK_SIZE = 50; const GET_FILE_CHUNK_SIZE = 50;
@ -45,12 +45,8 @@ 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 = vueroute(); const route = vueroute();
if (route.name === "albums") { if (route.name === 'albums') {
return getAlbumFileInfos( return getAlbumFileInfos(photos, <string>route.params.user, <string>route.params.name);
photos,
<string>route.params.user,
<string>route.params.name
);
} }
// Get file infos // Get file infos
@ -97,12 +93,12 @@ async function getFilesInternal(fileIds: number[]): Promise<IFileInfo[]> {
</d:eq> </d:eq>
` `
) )
.join(""); .join('');
const options = { const options = {
method: "SEARCH", method: 'SEARCH',
headers: { headers: {
"content-Type": "text/xml", 'content-Type': 'text/xml',
}, },
data: `<?xml version="1.0" encoding="UTF-8"?> data: `<?xml version="1.0" encoding="UTF-8"?>
<d:searchrequest xmlns:d="DAV:" <d:searchrequest xmlns:d="DAV:"
@ -131,16 +127,16 @@ async function getFilesInternal(fileIds: number[]): Promise<IFileInfo[]> {
</d:searchrequest>`, </d:searchrequest>`,
deep: true, deep: true,
details: true, details: true,
responseType: "text", responseType: 'text',
}; };
let response: any = await client.getDirectoryContents("", options); let response: any = await client.getDirectoryContents('', options);
return response.data return response.data
.map((data: any) => genFileInfo(data)) .map((data: any) => genFileInfo(data))
.map((data: any) => .map((data: any) =>
Object.assign({}, data, { Object.assign({}, data, {
originalFilename: data.filename, originalFilename: data.filename,
filename: data.filename.replace(prefixPath, ""), filename: data.filename.replace(prefixPath, ''),
}) })
); );
} }
@ -150,15 +146,10 @@ async function getFilesInternal(fileIds: number[]): Promise<IFileInfo[]> {
* @param promises Array of promise generator funnction (async functions) * @param promises Array of promise generator funnction (async functions)
* @param n Number of promises to run in parallel * @param n Number of promises to run in parallel
*/ */
export async function* runInParallel<T>( export async function* runInParallel<T>(promises: (() => Promise<T>)[], n: number) {
promises: (() => Promise<T>)[],
n: number
) {
while (promises.length > 0) { while (promises.length > 0) {
const promisesToRun = promises.splice(0, n); const promisesToRun = promises.splice(0, n);
const resultsForThisBatch = await Promise.all( const resultsForThisBatch = await Promise.all(promisesToRun.map((p) => p()));
promisesToRun.map((p) => p())
);
yield resultsForThisBatch; yield resultsForThisBatch;
} }
return; return;
@ -174,9 +165,9 @@ async function extendWithLivePhotos(photos: IPhoto[]) {
const livePhotos = ( const livePhotos = (
await Promise.all( await Promise.all(
photos photos
.filter((p) => p.liveid && !p.liveid.startsWith("self__")) .filter((p) => p.liveid && !p.liveid.startsWith('self__'))
.map(async (p) => { .map(async (p) => {
const url = utils.getLivePhotoVideoUrl(p, false) + "&format=json"; const url = utils.getLivePhotoVideoUrl(p, false) + '&format=json';
try { try {
const response = await axios.get(url); const response = await axios.get(url);
const data = response.data; const data = response.data;
@ -206,7 +197,7 @@ export async function* deletePhotos(photos: IPhoto[]) {
} }
// Extend with Live Photos unless this is an album // Extend with Live Photos unless this is an album
if (window.vueroute().name !== "albums") { if (window.vueroute().name !== 'albums') {
photos = await extendWithLivePhotos(photos); photos = await extendWithLivePhotos(photos);
} }
@ -218,8 +209,8 @@ export async function* deletePhotos(photos: IPhoto[]) {
try { try {
fileInfos = await getFiles(photos); fileInfos = await getFiles(photos);
} catch (e) { } catch (e) {
console.error("Failed to get file info for files to delete", photos, e); console.error('Failed to get file info for files to delete', photos, e);
showError(t("memories", "Failed to delete files.")); showError(t('memories', 'Failed to delete files.'));
return; return;
} }
@ -230,9 +221,9 @@ export async function* deletePhotos(photos: IPhoto[]) {
await client.deleteFile(fileInfo.originalFilename); await client.deleteFile(fileInfo.originalFilename);
return fileInfo.fileid; return fileInfo.fileid;
} catch (error) { } catch (error) {
console.error("Failed to delete", fileInfo, error); console.error('Failed to delete', fileInfo, error);
showError( showError(
t("memories", "Failed to delete {fileName}.", { t('memories', 'Failed to delete {fileName}.', {
fileName: fileInfo.filename, fileName: fileInfo.filename,
}) })
); );
@ -251,11 +242,7 @@ export async function* deletePhotos(photos: IPhoto[]) {
* @param overwrite behaviour if the target exists. `true` overwrites, `false` fails. * @param overwrite behaviour if the target exists. `true` overwrites, `false` fails.
* @returns list of file ids that were moved * @returns list of file ids that were moved
*/ */
export async function* movePhotos( export async function* movePhotos(photos: IPhoto[], destination: string, overwrite: boolean) {
photos: IPhoto[],
destination: string,
overwrite: boolean
) {
if (photos.length === 0) { if (photos.length === 0) {
return; return;
} }
@ -263,8 +250,8 @@ export async function* movePhotos(
// Set absolute target path // Set absolute target path
const prefixPath = `files/${getCurrentUser()?.uid}`; const prefixPath = `files/${getCurrentUser()?.uid}`;
let targetPath = prefixPath + destination; let targetPath = prefixPath + destination;
if (!targetPath.endsWith("/")) { if (!targetPath.endsWith('/')) {
targetPath += "/"; targetPath += '/';
} }
// Also move the Live Photo videos // Also move the Live Photo videos
@ -276,8 +263,8 @@ export async function* movePhotos(
try { try {
fileInfos = await getFiles(photos); fileInfos = await getFiles(photos);
} catch (e) { } catch (e) {
console.error("Failed to get file info for files to move", photos, e); console.error('Failed to get file info for files to move', photos, e);
showError(t("memories", "Failed to move files.")); showError(t('memories', 'Failed to move files.'));
return; return;
} }
@ -289,15 +276,15 @@ export async function* movePhotos(
fileInfo.originalFilename, fileInfo.originalFilename,
targetPath + fileInfo.basename, targetPath + fileInfo.basename,
// @ts-ignore - https://github.com/perry-mitchell/webdav-client/issues/329 // @ts-ignore - https://github.com/perry-mitchell/webdav-client/issues/329
{ headers: { Overwrite: overwrite ? "T" : "F" } } { headers: { Overwrite: overwrite ? 'T' : 'F' } }
); );
return fileInfo.fileid; return fileInfo.fileid;
} catch (error) { } catch (error) {
console.error("Failed to move", fileInfo, error); console.error('Failed to move', fileInfo, error);
if (error.response?.status === 412) { if (error.response?.status === 412) {
// Precondition failed (only if `overwrite` flag set to false) // Precondition failed (only if `overwrite` flag set to false)
showError( showError(
t("memories", "Could not move {fileName}, target exists.", { t('memories', 'Could not move {fileName}, target exists.', {
fileName: fileInfo.filename, fileName: fileInfo.filename,
}) })
); );
@ -305,7 +292,7 @@ export async function* movePhotos(
} }
showError( showError(
t("memories", "Failed to move {fileName}.", { t('memories', 'Failed to move {fileName}.', {
fileName: fileInfo.filename, fileName: fileInfo.filename,
}) })
); );

View File

@ -1,8 +1,8 @@
import axios from "@nextcloud/axios"; import axios from '@nextcloud/axios';
import { showError } from "@nextcloud/dialogs"; import { showError } from '@nextcloud/dialogs';
import { translate as t } from "@nextcloud/l10n"; import { translate as t } from '@nextcloud/l10n';
import { IPhoto } from "../../types"; import { IPhoto } from '../../types';
import { API } from "../API"; import { API } from '../API';
/** /**
* Download files * Download files
@ -12,7 +12,7 @@ export async function downloadFiles(fileIds: number[]) {
const res = await axios.post(API.DOWNLOAD_REQUEST(), { files: fileIds }); const res = await axios.post(API.DOWNLOAD_REQUEST(), { files: fileIds });
if (res.status !== 200 || !res.data.handle) { if (res.status !== 200 || !res.data.handle) {
showError(t("memories", "Failed to download files")); showError(t('memories', 'Failed to download files'));
return; return;
} }

View File

@ -1,46 +1,31 @@
import axios from "@nextcloud/axios"; import axios from '@nextcloud/axios';
import { showError } from "@nextcloud/dialogs"; import { showError } from '@nextcloud/dialogs';
import { translate as t } from "@nextcloud/l10n"; import { translate as t } from '@nextcloud/l10n';
import { generateUrl } from "@nextcloud/router"; import { generateUrl } from '@nextcloud/router';
import { IFace, IPhoto } from "../../types"; import { IFace, IPhoto } from '../../types';
import { API } from "../API"; import { API } from '../API';
import client from "../DavClient"; import client from '../DavClient';
import * as base from "./base"; import * as base from './base';
export async function getFaceList(app: "recognize" | "facerecognition") { export async function getFaceList(app: 'recognize' | 'facerecognition') {
return (await axios.get<IFace[]>(API.FACE_LIST(app))).data; return (await axios.get<IFace[]>(API.FACE_LIST(app))).data;
} }
export async function updatePeopleFaceRecognition( export async function updatePeopleFaceRecognition(name: string, params: object) {
name: string,
params: object
) {
if (Number.isInteger(Number(name))) { if (Number.isInteger(Number(name))) {
return await axios.put( return await axios.put(generateUrl(`/apps/facerecognition/api/2.0/cluster/${name}`), params);
generateUrl(`/apps/facerecognition/api/2.0/cluster/${name}`),
params
);
} else { } else {
return await axios.put( return await axios.put(generateUrl(`/apps/facerecognition/api/2.0/person/${name}`), params);
generateUrl(`/apps/facerecognition/api/2.0/person/${name}`),
params
);
} }
} }
export async function renamePeopleFaceRecognition( export async function renamePeopleFaceRecognition(name: string, newName: string) {
name: string,
newName: string
) {
return await updatePeopleFaceRecognition(name, { return await updatePeopleFaceRecognition(name, {
name: newName, name: newName,
}); });
} }
export async function setVisibilityPeopleFaceRecognition( export async function setVisibilityPeopleFaceRecognition(name: string, visibility: boolean) {
name: string,
visibility: boolean
) {
return await updatePeopleFaceRecognition(name, { return await updatePeopleFaceRecognition(name, {
visible: visibility, visible: visibility,
}); });
@ -54,22 +39,16 @@ export async function setVisibilityPeopleFaceRecognition(
* @param photos List of photos to remove * @param photos List of photos to remove
* @returns Generator * @returns Generator
*/ */
export async function* removeFaceImages( export async function* removeFaceImages(user: string, name: string, photos: IPhoto[]) {
user: string,
name: string,
photos: IPhoto[]
) {
// Remove each file // Remove each file
const calls = photos.map((f) => async () => { const calls = photos.map((f) => async () => {
try { try {
await client.deleteFile( await client.deleteFile(`/recognize/${user}/faces/${name}/${f.fileid}-${f.basename}`);
`/recognize/${user}/faces/${name}/${f.fileid}-${f.basename}`
);
return f.fileid; return f.fileid;
} catch (e) { } catch (e) {
console.error(e); console.error(e);
showError( showError(
t("memories", "Failed to remove {filename} from face.", { t('memories', 'Failed to remove {filename} from face.', {
filename: f.basename ?? f.fileid, filename: f.basename ?? f.fileid,
}) })
); );

View File

@ -1,9 +1,9 @@
import { showError } from "@nextcloud/dialogs"; import { showError } from '@nextcloud/dialogs';
import { translate as t } from "@nextcloud/l10n"; import { translate as t } from '@nextcloud/l10n';
import { IFileInfo, IPhoto } from "../../types"; import { IFileInfo, IPhoto } from '../../types';
import client from "../DavClient"; import client from '../DavClient';
import * as base from "./base"; import * as base from './base';
import * as utils from "../Utils"; import * as utils from '../Utils';
/** /**
* Favorite a file * Favorite a file
@ -14,7 +14,7 @@ import * as utils from "../Utils";
*/ */
export function favoriteFile(fileName: string, favoriteState: boolean) { export function favoriteFile(fileName: string, favoriteState: boolean) {
return client.customRequest(fileName, { return client.customRequest(fileName, {
method: "PROPPATCH", method: 'PROPPATCH',
data: `<?xml version="1.0"?> data: `<?xml version="1.0"?>
<d:propertyupdate xmlns:d="DAV:" <d:propertyupdate xmlns:d="DAV:"
xmlns:oc="http://owncloud.org/ns" xmlns:oc="http://owncloud.org/ns"
@ -22,7 +22,7 @@ export function favoriteFile(fileName: string, favoriteState: boolean) {
xmlns:ocs="http://open-collaboration-services.org/ns"> xmlns:ocs="http://open-collaboration-services.org/ns">
<d:set> <d:set>
<d:prop> <d:prop>
<oc:favorite>${favoriteState ? "1" : "0"}</oc:favorite> <oc:favorite>${favoriteState ? '1' : '0'}</oc:favorite>
</d:prop> </d:prop>
</d:set> </d:set>
</d:propertyupdate>`, </d:propertyupdate>`,
@ -36,10 +36,7 @@ export function favoriteFile(fileName: string, favoriteState: boolean) {
* @param favoriteState the new favorite state * @param favoriteState the new favorite state
* @returns generator of lists of file ids that were state-changed * @returns generator of lists of file ids that were state-changed
*/ */
export async function* favoritePhotos( export async function* favoritePhotos(photos: IPhoto[], favoriteState: boolean) {
photos: IPhoto[],
favoriteState: boolean
) {
if (photos.length === 0) { if (photos.length === 0) {
return; return;
} }
@ -49,13 +46,13 @@ export async function* favoritePhotos(
try { try {
fileInfos = await base.getFiles(photos); fileInfos = await base.getFiles(photos);
} catch (e) { } catch (e) {
console.error("Failed to get file info", photos, e); console.error('Failed to get file info', photos, e);
showError(t("memories", "Failed to favorite files.")); showError(t('memories', 'Failed to favorite files.'));
return; return;
} }
if (fileInfos.length !== photos.length) { if (fileInfos.length !== photos.length) {
showError(t("memories", "Failed to favorite some files.")); showError(t('memories', 'Failed to favorite some files.'));
} }
// Favorite each file // Favorite each file
@ -70,9 +67,9 @@ export async function* favoritePhotos(
} }
return fileInfo.fileid as number; return fileInfo.fileid as number;
} catch (error) { } catch (error) {
console.error("Failed to favorite", fileInfo, error); console.error('Failed to favorite', fileInfo, error);
showError( showError(
t("memories", "Failed to favorite {fileName}.", { t('memories', 'Failed to favorite {fileName}.', {
fileName: fileInfo.originalFilename, fileName: fileInfo.originalFilename,
}) })
); );

View File

@ -1,18 +1,15 @@
import * as base from "./base"; import * as base from './base';
import { getCurrentUser } from "@nextcloud/auth"; import { getCurrentUser } from '@nextcloud/auth';
import { genFileInfo } from "../FileUtils"; import { genFileInfo } from '../FileUtils';
import { IFileInfo } from "../../types"; import { IFileInfo } from '../../types';
import client from "../DavClient"; import client from '../DavClient';
/** /**
* Get file infos for files in folder path * Get file infos for files in folder path
* @param folderPath Path to folder * @param folderPath Path to folder
* @param limit Max number of files to return * @param limit Max number of files to return
*/ */
export async function getFolderPreviewFileIds( export async function getFolderPreviewFileIds(folderPath: string, limit: number): Promise<IFileInfo[]> {
folderPath: string,
limit: number
): Promise<IFileInfo[]> {
const prefixPath = `/files/${getCurrentUser()?.uid}`; const prefixPath = `/files/${getCurrentUser()?.uid}`;
const filter = base.IMAGE_MIME_TYPES.map( const filter = base.IMAGE_MIME_TYPES.map(
@ -24,12 +21,12 @@ export async function getFolderPreviewFileIds(
<d:literal>${mime}</d:literal> <d:literal>${mime}</d:literal>
</d:like> </d:like>
` `
).join(""); ).join('');
const options = { const options = {
method: "SEARCH", method: 'SEARCH',
headers: { headers: {
"content-Type": "text/xml", 'content-Type': 'text/xml',
}, },
data: `<?xml version="1.0" encoding="UTF-8"?> data: `<?xml version="1.0" encoding="UTF-8"?>
<d:searchrequest xmlns:d="DAV:" <d:searchrequest xmlns:d="DAV:"
@ -61,16 +58,16 @@ export async function getFolderPreviewFileIds(
</d:searchrequest>`, </d:searchrequest>`,
deep: true, deep: true,
details: true, details: true,
responseType: "text", responseType: 'text',
}; };
let response: any = await client.getDirectoryContents("", options); let response: any = await client.getDirectoryContents('', options);
return response.data return response.data
.map((data: any) => genFileInfo(data)) .map((data: any) => genFileInfo(data))
.map((data: any) => .map((data: any) =>
Object.assign({}, data, { Object.assign({}, data, {
filename: data.filename.replace(prefixPath, ""), filename: data.filename.replace(prefixPath, ''),
etag: data.etag.replace(/&quot;/g, ""), // remove quotes etag: data.etag.replace(/&quot;/g, ''), // remove quotes
}) })
); );
} }

View File

@ -1,7 +1,7 @@
import { IDay, IPhoto } from "../../types"; import { IDay, IPhoto } from '../../types';
import axios from "@nextcloud/axios"; import axios from '@nextcloud/axios';
import * as utils from "../Utils"; import * as utils from '../Utils';
import { API } from "../API"; import { API } from '../API';
/** /**
* Get original onThisDay response. * Get original onThisDay response.
@ -24,7 +24,7 @@ export async function getOnThisDayRaw() {
} }
const res = await axios.post<IPhoto[]>(API.DAYS(), { const res = await axios.post<IPhoto[]>(API.DAYS(), {
body_ids: dayIds.join(","), body_ids: dayIds.join(','),
}); });
res.data.forEach(utils.convertFlags); res.data.forEach(utils.convertFlags);

View File

@ -1,6 +1,6 @@
import { getFiles } from "./base"; import { getFiles } from './base';
import { generateUrl } from "@nextcloud/router"; import { generateUrl } from '@nextcloud/router';
import { IPhoto } from "../../types"; import { IPhoto } from '../../types';
/** /**
* Open the files app with the given photo * Open the files app with the given photo
@ -11,9 +11,7 @@ export async function viewInFolder(photo: IPhoto) {
if (f.length === 0) return; if (f.length === 0) return;
const file = f[0]; const file = f[0];
const dirPath = file.filename.split("/").slice(0, -1).join("/"); const dirPath = file.filename.split('/').slice(0, -1).join('/');
const url = generateUrl( const url = generateUrl(`/apps/files/?dir=${dirPath}&scrollto=${file.fileid}&openfile=${file.fileid}`);
`/apps/files/?dir=${dirPath}&scrollto=${file.fileid}&openfile=${file.fileid}` window.open(url, '_blank');
);
window.open(url, "_blank");
} }

View File

@ -1,6 +1,6 @@
import { ICluster } from "../../types"; import { ICluster } from '../../types';
import { API } from "../API"; import { API } from '../API';
import axios from "@nextcloud/axios"; import axios from '@nextcloud/axios';
export async function getPlaces() { export async function getPlaces() {
return (await axios.get<ICluster[]>(API.PLACE_LIST())).data; return (await axios.get<ICluster[]>(API.PLACE_LIST())).data;

View File

@ -1,11 +1,11 @@
import { IDay } from "../../types"; import { IDay } from '../../types';
import { loadState } from "@nextcloud/initial-state"; import { loadState } from '@nextcloud/initial-state';
let singleItem: any; let singleItem: any;
try { try {
singleItem = loadState("memories", "single_item", {}); singleItem = loadState('memories', 'single_item', {});
} catch (e) { } catch (e) {
console.error("Could not load single item", e); console.error('Could not load single item', e);
} }
export function isSingleItem(): boolean { export function isSingleItem(): boolean {

View File

@ -1,6 +1,6 @@
import { ICluster } from "../../types"; import { ICluster } from '../../types';
import { API } from "../API"; import { API } from '../API';
import axios from "@nextcloud/axios"; import axios from '@nextcloud/axios';
/** /**
* Get list of tags. * Get list of tags.

View File

@ -1,74 +1,63 @@
import { loadState } from "@nextcloud/initial-state"; import { loadState } from '@nextcloud/initial-state';
import { translate as t } from "@nextcloud/l10n"; import { translate as t } from '@nextcloud/l10n';
const config_facerecognitionEnabled = Boolean( const config_facerecognitionEnabled = Boolean(loadState('memories', 'facerecognitionEnabled', <string>''));
loadState("memories", "facerecognitionEnabled", <string>"")
);
type RouteNameType = string | null | undefined; type RouteNameType = string | null | undefined;
export function emptyDescription(routeName: RouteNameType): string { export function emptyDescription(routeName: RouteNameType): string {
switch (routeName) { switch (routeName) {
case "timeline": case 'timeline':
return t( return t('memories', 'Upload some photos and make sure the timeline path is configured');
"memories", case 'favorites':
"Upload some photos and make sure the timeline path is configured" return t('memories', 'Mark photos as favorite to find them easily');
); case 'thisday':
case "favorites": return t('memories', 'Memories from past years will appear here');
return t("memories", "Mark photos as favorite to find them easily"); case 'facerecognition':
case "thisday":
return t("memories", "Memories from past years will appear here");
case "facerecognition":
return config_facerecognitionEnabled return config_facerecognitionEnabled
? t("memories", "You will find your friends soon. Please be patient") ? t('memories', 'You will find your friends soon. Please be patient')
: t( : t('memories', 'Face Recognition is disabled. Enable in settings to find your friends');
"memories", case 'videos':
"Face Recognition is disabled. Enable in settings to find your friends" return t('memories', 'Your videos will appear here');
); case 'albums':
case "videos":
return t("memories", "Your videos will appear here");
case "albums":
return vueroute().params.name return vueroute().params.name
? t("memories", "No photos in this album yet") ? t('memories', 'No photos in this album yet')
: t("memories", "Create an album to get started"); : t('memories', 'Create an album to get started');
case "archive": case 'archive':
return t( return t('memories', "Archive photos you don't want to see in your timeline");
"memories", case 'tags':
"Archive photos you don't want to see in your timeline" return t('memories', 'Tag photos to find them easily');
); case 'recognize':
case "tags": return t('memories', 'Recognize is still working on your photos');
return t("memories", "Tag photos to find them easily"); case 'places':
case "recognize": return t('memories', 'Places you have been to will appear here');
return t("memories", "Recognize is still working on your photos");
case "places":
return t("memories", "Places you have been to will appear here");
default: default:
return ""; return '';
} }
} }
export function viewName(routeName: RouteNameType): string { export function viewName(routeName: RouteNameType): string {
switch (routeName) { switch (routeName) {
case "timeline": case 'timeline':
return t("memories", "Your Timeline"); return t('memories', 'Your Timeline');
case "favorites": case 'favorites':
return t("memories", "Favorites"); return t('memories', 'Favorites');
case "recognize": case 'recognize':
case "facerecognition": case 'facerecognition':
return t("memories", "People"); return t('memories', 'People');
case "videos": case 'videos':
return t("memories", "Videos"); return t('memories', 'Videos');
case "albums": case 'albums':
return t("memories", "Albums"); return t('memories', 'Albums');
case "archive": case 'archive':
return t("memories", "Archive"); return t('memories', 'Archive');
case "thisday": case 'thisday':
return t("memories", "On this day"); return t('memories', 'On this day');
case "tags": case 'tags':
return t("memories", "Tags"); return t('memories', 'Tags');
case "places": case 'places':
return t("memories", "Places"); return t('memories', 'Places');
default: default:
return ""; return '';
} }
} }

View File

@ -9,9 +9,7 @@
export function binarySearch(arr: any, elem: any, key?: string) { export function binarySearch(arr: any, elem: any, key?: string) {
if (arr.length === 0) return 0; if (arr.length === 0) return 0;
const desc = key const desc = key ? arr[0][key] > arr[arr.length - 1][key] : arr[0] > arr[arr.length - 1];
? arr[0][key] > arr[arr.length - 1][key]
: arr[0] > arr[arr.length - 1];
let minIndex = 0; let minIndex = 0;
let maxIndex = arr.length - 1; let maxIndex = arr.length - 1;
@ -83,12 +81,7 @@ export function randomSubarray(arr: any[], size: number) {
} }
/** Set a timer that renews if existing */ /** Set a timer that renews if existing */
export function setRenewingTimeout( export function setRenewingTimeout(ctx: any, name: string, callback: (() => void) | null, delay: number) {
ctx: any,
name: string,
callback: (() => void) | null,
delay: number
) {
if (ctx[name]) window.clearTimeout(ctx[name]); if (ctx[name]) window.clearTimeout(ctx[name]);
ctx[name] = window.setTimeout(() => { ctx[name] = window.setTimeout(() => {
ctx[name] = 0; ctx[name] = 0;

View File

@ -1,20 +1,20 @@
import { getCurrentUser } from "@nextcloud/auth"; import { getCurrentUser } from '@nextcloud/auth';
import { loadState } from "@nextcloud/initial-state"; import { loadState } from '@nextcloud/initial-state';
/** Cache keys */ /** Cache keys */
const memoriesVersion: string = loadState("memories", "version", ""); const memoriesVersion: string = loadState('memories', 'version', '');
const uid = getCurrentUser()?.uid || "guest"; const uid = getCurrentUser()?.uid || 'guest';
const cacheName = `memories-${memoriesVersion}-${uid}`; const cacheName = `memories-${memoriesVersion}-${uid}`;
// Clear all caches except the current one // Clear all caches except the current one
(async function clearCaches() { (async function clearCaches() {
if (!memoriesVersion || uid === "guest") return; if (!memoriesVersion || uid === 'guest') return;
const keys = await window.caches?.keys(); const keys = await window.caches?.keys();
if (!keys?.length) return; if (!keys?.length) return;
for (const key of keys) { for (const key of keys) {
if (key.startsWith("memories-") && key !== cacheName) { if (key.startsWith('memories-') && key !== cacheName) {
window.caches.delete(key); window.caches.delete(key);
} }
} }
@ -54,10 +54,10 @@ export function cacheData(url: string, data: Object) {
const response = new Response(str); const response = new Response(str);
const encoded = new TextEncoder().encode(str); const encoded = new TextEncoder().encode(str);
response.headers.set("Content-Type", "application/json"); response.headers.set('Content-Type', 'application/json');
response.headers.set("Content-Length", encoded.length.toString()); response.headers.set('Content-Length', encoded.length.toString());
response.headers.set("Cache-Control", "max-age=604800"); // 1 week response.headers.set('Cache-Control', 'max-age=604800'); // 1 week
response.headers.set("Vary", "Accept-Encoding"); response.headers.set('Vary', 'Accept-Encoding');
await cache.put(url, response); await cache.put(url, response);
})(); })();
} }

View File

@ -1,4 +1,4 @@
import { IPhoto } from "../../types"; import { IPhoto } from '../../types';
/** Global constants */ /** Global constants */
export const constants = { export const constants = {
@ -17,7 +17,7 @@ export const constants = {
* @param photo Photo to process * @param photo Photo to process
*/ */
export function convertFlags(photo: IPhoto) { export function convertFlags(photo: IPhoto) {
if (typeof photo.flag === "undefined") { if (typeof photo.flag === 'undefined') {
photo.flag = 0; // flags photo.flag = 0; // flags
photo.imageInfo = null; // make it reactive photo.imageInfo = null; // make it reactive
} }

View File

@ -1,5 +1,5 @@
import { getCanonicalLocale } from "@nextcloud/l10n"; import { getCanonicalLocale } from '@nextcloud/l10n';
import moment from "moment"; import moment from 'moment';
// Memoize the result of short date conversions // Memoize the result of short date conversions
// These operations are surprisingly expensive // These operations are surprisingly expensive
@ -23,9 +23,9 @@ export function getShortDateStr(date: Date) {
shortDateStrMemo.set( shortDateStrMemo.set(
dayId, dayId,
date.toLocaleDateString(getCanonicalLocale(), { date.toLocaleDateString(getCanonicalLocale(), {
month: "short", month: 'short',
year: "numeric", year: 'numeric',
timeZone: "UTC", timeZone: 'UTC',
}) })
); );
} }
@ -35,25 +35,22 @@ export function getShortDateStr(date: Date) {
/** Get long date string with optional year if same as current */ /** Get long date string with optional year if same as current */
export function getLongDateStr(date: Date, skipYear = false, time = false) { export function getLongDateStr(date: Date, skipYear = false, time = false) {
return date.toLocaleDateString(getCanonicalLocale(), { return date.toLocaleDateString(getCanonicalLocale(), {
weekday: "short", weekday: 'short',
month: "short", month: 'short',
day: "numeric", day: 'numeric',
year: year: skipYear && date.getUTCFullYear() === new Date().getUTCFullYear() ? undefined : 'numeric',
skipYear && date.getUTCFullYear() === new Date().getUTCFullYear() timeZone: 'UTC',
? undefined hour: time ? 'numeric' : undefined,
: "numeric", minute: time ? 'numeric' : undefined,
timeZone: "UTC",
hour: time ? "numeric" : undefined,
minute: time ? "numeric" : undefined,
}); });
} }
/** Get month and year string */ /** Get month and year string */
export function getMonthDateStr(date: Date) { export function getMonthDateStr(date: Date) {
return date.toLocaleDateString(getCanonicalLocale(), { return date.toLocaleDateString(getCanonicalLocale(), {
month: "long", month: 'long',
year: "numeric", year: 'numeric',
timeZone: "UTC", timeZone: 'UTC',
}); });
} }
@ -73,12 +70,12 @@ export function getDurationStr(sec: number) {
let seconds: number | string = sec - hours * 3600 - minutes * 60; let seconds: number | string = sec - hours * 3600 - minutes * 60;
if (seconds < 10) { if (seconds < 10) {
seconds = "0" + seconds; seconds = '0' + seconds;
} }
if (hours > 0) { if (hours > 0) {
if (minutes < 10) { if (minutes < 10) {
minutes = "0" + minutes; minutes = '0' + minutes;
} }
return `${hours}:${minutes}:${seconds}`; return `${hours}:${minutes}:${seconds}`;
} }

View File

@ -1,27 +1,23 @@
import { IImageInfo, IPhoto } from "../../types"; import { IImageInfo, IPhoto } from '../../types';
import { API } from "../API"; import { API } from '../API';
/** Get preview URL from photo object */ /** Get preview URL from photo object */
export function getPreviewUrl( export function getPreviewUrl(photo: IPhoto, square: boolean, size: number | [number, number] | 'screen') {
photo: IPhoto,
square: boolean,
size: number | [number, number] | "screen"
) {
// Screen-appropriate size // Screen-appropriate size
if (size === "screen") { if (size === 'screen') {
const sw = Math.floor(screen.width * devicePixelRatio); const sw = Math.floor(screen.width * devicePixelRatio);
const sh = Math.floor(screen.height * devicePixelRatio); const sh = Math.floor(screen.height * devicePixelRatio);
size = [sw, sh]; size = [sw, sh];
} }
// Convert to array // Convert to array
const [x, y] = typeof size === "number" ? [size, size] : size; const [x, y] = typeof size === 'number' ? [size, size] : size;
return API.Q(API.IMAGE_PREVIEW(photo.fileid), { return API.Q(API.IMAGE_PREVIEW(photo.fileid), {
c: photo.etag, c: photo.etag,
x, x,
y, y,
a: square ? "0" : "1", a: square ? '0' : '1',
}); });
} }
@ -45,10 +41,10 @@ export function updatePhotoFromImageInfo(photo: IPhoto, imageInfo: IImageInfo) {
* 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 = vueroute().params.path || "/"; let path: any = vueroute().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
return path; return path;
} }
@ -68,15 +64,15 @@ export function getLivePhotoVideoUrl(p: IPhoto, transcode: boolean) {
* @param video Video element * @param video Video element
*/ */
export function setupLivePhotoHooks(video: HTMLVideoElement) { export function setupLivePhotoHooks(video: HTMLVideoElement) {
const div = video.closest(".memories-livephoto") as HTMLDivElement; const div = video.closest('.memories-livephoto') as HTMLDivElement;
video.onplay = () => { video.onplay = () => {
div.classList.add("playing"); div.classList.add('playing');
}; };
video.oncanplay = () => { video.oncanplay = () => {
div.classList.add("canplay"); div.classList.add('canplay');
}; };
video.onended = video.onpause = () => { video.onended = video.onpause = () => {
div.classList.remove("playing"); div.classList.remove('playing');
}; };
} }

View File

@ -1,12 +1,12 @@
import videojs from "video.js"; import videojs from 'video.js';
globalThis.vidjs = videojs; globalThis.vidjs = videojs;
import "video.js/dist/video-js.min.css"; import 'video.js/dist/video-js.min.css';
import Plyr from "plyr"; import Plyr from 'plyr';
(<any>globalThis).Plyr = Plyr; (<any>globalThis).Plyr = Plyr;
import "plyr/dist/plyr.css"; import 'plyr/dist/plyr.css';
import plyrsvg from "../assets/plyr.svg"; import plyrsvg from '../assets/plyr.svg';
(<any>Plyr).defaults.iconUrl = plyrsvg; (<any>Plyr).defaults.iconUrl = plyrsvg;
(<any>Plyr).defaults.blankVideo = ""; (<any>Plyr).defaults.blankVideo = '';

Some files were not shown because too many files have changed in this diff Show More