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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,44 +1,41 @@
<template>
<div class="admin-section">
<h2>{{ t("memories", "Media Indexing") }}</h2>
<h2>{{ t('memories', 'Media Indexing') }}</h2>
<template v-if="status">
<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,
})
}}
</NcNoteCard>
<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,
})
}}
</NcNoteCard>
<NcNoteCard
v-if="status.last_index_job_start"
:type="status.last_index_job_duration ? 'success' : 'warning'"
>
<NcNoteCard 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,
})
}}
{{
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("memories", "It is still running or was interrupted.")
: t('memories', 'It is still running or was interrupted.')
}}
</NcNoteCard>
<NcNoteCard type="error" v-if="status.bad_encryption">
{{
t(
"memories",
"Only server-side encryption (OC_DEFAULT_MODULE) is supported, but another encryption module is enabled."
'memories',
'Only server-side encryption (OC_DEFAULT_MODULE) is supported, but another encryption module is enabled.'
)
}}
</NcNoteCard>
@ -47,23 +44,18 @@
<p>
{{
t(
"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."
)
}}
{{
t(
"memories",
'Folders with a ".nomedia" file are always excluded from indexing.'
'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.'
)
}}
{{ t('memories', 'Folders with a ".nomedia" file are always excluded from indexing.') }}
<NcCheckboxRadioSwitch
:checked.sync="config['memories.index.mode']"
value="1"
name="idxm_radio"
type="radio"
@update:checked="update('memories.index.mode')"
>{{ t("memories", "Index all media automatically (recommended)") }}
>{{ t('memories', 'Index all media automatically (recommended)') }}
</NcCheckboxRadioSwitch>
<NcCheckboxRadioSwitch
:checked.sync="config['memories.index.mode']"
@ -71,9 +63,7 @@
name="idxm_radio"
type="radio"
@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
:checked.sync="config['memories.index.mode']"
@ -81,7 +71,7 @@
name="idxm_radio"
type="radio"
@update:checked="update('memories.index.mode')"
>{{ t("memories", "Index a fixed relative path") }}
>{{ t('memories', 'Index a fixed relative path') }}
</NcCheckboxRadioSwitch>
<NcCheckboxRadioSwitch
:checked.sync="config['memories.index.mode']"
@ -89,7 +79,7 @@
name="idxm_radio"
type="radio"
@update:checked="update('memories.index.mode')"
>{{ t("memories", "Disable background indexing") }}
>{{ t('memories', 'Disable background indexing') }}
</NcCheckboxRadioSwitch>
<NcTextField
@ -101,57 +91,46 @@
/>
</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 />
<code>occ memories:index</code>
<br />
{{ t("memories", "Run index in parallel with 4 threads:") }}
{{ t('memories', 'Run index in parallel with 4 threads:') }}
<br />
<code>bash -c 'for i in {1..4}; do (occ memories:index &amp;); done'</code>
<br />
{{ t("memories", "Force re-indexing of all files:") }}
{{ t('memories', 'Force re-indexing of all files:') }}
<br />
<code>occ memories:index --force</code>
<br />
{{ t("memories", "You can limit indexing by user and/or folder:") }}
{{ t('memories', 'You can limit indexing by user and/or folder:') }}
<br />
<code>occ memories:index --user=admin --folder=/Photos/</code>
<br />
{{ t("memories", "Clear all existing index tables:") }}
{{ t('memories', 'Clear all existing index tables:') }}
<br />
<code>occ memories:index --clear</code>
<br />
<br />
{{
t(
"memories",
"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") }}
{{ t('memories', '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>
<br />
<code v-if="status"
><template v-for="mime in status.mimes"
>{{ mime }}<br :key="mime" /></template
><template v-for="mime in status.mimes">{{ mime }}<br :key="mime" /></template
></code>
</div>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import { defineComponent } from 'vue';
import AdminMixin from "../AdminMixin";
import AdminMixin from '../AdminMixin';
export default defineComponent({
name: "Indexing",
name: 'Indexing',
mixins: [AdminMixin],
});
</script>

View File

@ -1,29 +1,25 @@
<template>
<div class="admin-section">
<h2>{{ t("memories", "Performance") }}</h2>
<h2>{{ t('memories', 'Performance') }}</h2>
<p>
<NcNoteCard :type="isHttps ? 'success' : 'warning'">
{{
isHttps
? t("memories", "HTTPS is enabled")
? t('memories', 'HTTPS is enabled')
: t(
"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."
'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.'
)
}}
</NcNoteCard>
<NcNoteCard :type="httpVerOk ? 'success' : 'warning'">
{{
httpVerOk
? t("memories", "HTTP/2 or HTTP/3 is enabled")
: t(
"memories",
"HTTP/2 or HTTP/3 is strongly recommended ({httpVer} detected)",
{
httpVer,
}
)
? t('memories', 'HTTP/2 or HTTP/3 is enabled')
: t('memories', 'HTTP/2 or HTTP/3 is strongly recommended ({httpVer} detected)', {
httpVer,
})
}}
</NcNoteCard>
</p>
@ -31,28 +27,26 @@
</template>
<script lang="ts">
import { defineComponent } from "vue";
import { defineComponent } from 'vue';
import AdminMixin from "../AdminMixin";
import AdminMixin from '../AdminMixin';
export default defineComponent({
name: "Performance",
name: 'Performance',
mixins: [AdminMixin],
computed: {
isHttps(): boolean {
return window.location.protocol === "https:";
return window.location.protocol === 'https:';
},
httpVer(): string {
const entry = window.performance?.getEntriesByType?.(
"navigation"
)?.[0] as any;
return entry?.nextHopProtocol || this.t("memories", "Unknown");
const entry = window.performance?.getEntriesByType?.('navigation')?.[0] as any;
return entry?.nextHopProtocol || this.t('memories', 'Unknown');
},
httpVerOk(): boolean {
return this.httpVer === "h2" || this.httpVer === "h3";
return this.httpVer === 'h2' || this.httpVer === 'h3';
},
},
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -29,16 +29,16 @@
</template>
<script lang="ts">
import { defineComponent, PropType } from "vue";
import { IFolder, IPhoto } from "../../types";
import { defineComponent, PropType } from 'vue';
import { IFolder, IPhoto } from '../../types';
import { getPreviewUrl } from "../../services/utils/helpers";
import { getPreviewUrl } from '../../services/utils/helpers';
import UserConfig from "../../mixins/UserConfig";
import FolderIcon from "vue-material-design-icons/Folder.vue";
import UserConfig from '../../mixins/UserConfig';
import FolderIcon from 'vue-material-design-icons/Folder.vue';
export default defineComponent({
name: "Folder",
name: 'Folder',
components: {
FolderIcon,
},
@ -65,20 +65,17 @@ export default defineComponent({
/** Open folder */
target() {
const path = this.data.path
.split("/")
.split('/')
.filter((x) => x)
.slice(2) as string[];
// Remove base path if present
const basePath = this.config_foldersPath.split("/").filter((x) => x);
if (
path.length >= basePath.length &&
path.slice(0, basePath.length).every((x, i) => x === basePath[i])
) {
const basePath = this.config_foldersPath.split('/').filter((x) => x);
if (path.length >= basePath.length && path.slice(0, basePath.length).every((x, i) => x === basePath[i])) {
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
.folder.hasError > & {
.folder-icon {
filter: invert(12%) sepia(62%) saturate(5862%) hue-rotate(8deg)
brightness(103%) contrast(128%);
filter: invert(12%) sepia(62%) saturate(5862%) hue-rotate(8deg) brightness(103%) contrast(128%);
}
.name {
color: #bb0000;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -7,13 +7,13 @@
*/
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[] = [];
self.onmessage = function (val: any) {
missedQueue.push(val);
};
import("./XImgWorker").then(function () {
import('./XImgWorker').then(function () {
missedQueue.forEach((data: any) => self.onmessage?.(data));
});

View File

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

View File

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

View File

@ -2,35 +2,30 @@
<Modal @close="close" size="normal" v-if="show">
<template #title>
<template v-if="!album">
{{ t("memories", "Create new album") }}
{{ t('memories', 'Create new album') }}
</template>
<template v-else>
{{ t("memories", "Edit album details") }}
{{ t('memories', 'Edit album details') }}
</template>
</template>
<div class="outer">
<AlbumForm
:album="album"
:display-back-button="false"
:title="t('photos', 'New album')"
@done="done"
/>
<AlbumForm :album="album" :display-back-button="false" :title="t('photos', 'New album')" @done="done" />
</div>
</Modal>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import { defineComponent } from 'vue';
import { showError } from "@nextcloud/dialogs";
import * as dav from "../../services/DavRequests";
import { showError } from '@nextcloud/dialogs';
import * as dav from '../../services/DavRequests';
import Modal from "./Modal.vue";
import AlbumForm from "./AlbumForm.vue";
import Modal from './Modal.vue';
import AlbumForm from './AlbumForm.vue';
export default defineComponent({
name: "AlbumCreateModal",
name: 'AlbumCreateModal',
components: {
Modal,
AlbumForm,
@ -49,13 +44,10 @@ export default defineComponent({
async open(edit: boolean) {
if (edit) {
try {
this.album = await dav.getAlbum(
<string>this.$route.params.user,
<string>this.$route.params.name
);
this.album = await dav.getAlbum(<string>this.$route.params.user, <string>this.$route.params.name);
} catch (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;
}
} else {
@ -67,14 +59,14 @@ export default defineComponent({
close() {
this.show = false;
this.$emit("close");
this.$emit('close');
},
done({ album }: any) {
if (!this.album || album.basename !== this.album.basename) {
const user = album.filename.split("/")[2];
const user = album.filename.split('/')[2];
const name = album.basename;
this.$router.push({ name: "albums", params: { user, name } });
this.$router.push({ name: 'albums', params: { user, name } });
}
this.close();
},
@ -90,4 +82,4 @@ export default defineComponent({
.info-pad {
margin-top: 6px;
}
</style>
</style>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,8 +1,5 @@
<template>
<NcEmptyContent
:title="t('memories', 'Nothing to show here')"
:description="emptyViewDescription"
>
<NcEmptyContent :title="t('memories', 'Nothing to show here')" :description="emptyViewDescription">
<template #icon>
<PeopleIcon v-if="routeIsPeople" />
<ArchiveIcon v-else-if="routeIsArchive" />
@ -12,18 +9,18 @@
</template>
<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 ImageMultipleIcon from "vue-material-design-icons/ImageMultiple.vue";
import ArchiveIcon from "vue-material-design-icons/PackageDown.vue";
import PeopleIcon from 'vue-material-design-icons/AccountMultiple.vue';
import ImageMultipleIcon from 'vue-material-design-icons/ImageMultiple.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({
name: "EmptyContent",
name: 'EmptyContent',
components: {
NcEmptyContent,
@ -39,14 +36,11 @@ export default defineComponent({
},
routeIsPeople(): boolean {
return (
this.$route.name === "recognize" ||
this.$route.name === "facerecognition"
);
return this.$route.name === 'recognize' || this.$route.name === 'facerecognition';
},
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">
<NcActions>
<NcActionButton :aria-label="t('memories', 'Back')" @click="back()">
{{ t("memories", "Back") }}
{{ t('memories', 'Back') }}
<template #icon> <BackIcon :size="20" /> </template>
</NcActionButton>
</NcActions>
@ -11,12 +11,8 @@
<div class="right-actions">
<NcActions :inline="1">
<NcActionButton
:aria-label="t('memories', 'Rename person')"
@click="$refs.editModal?.open()"
close-after-click
>
{{ t("memories", "Rename person") }}
<NcActionButton :aria-label="t('memories', 'Rename person')" @click="$refs.editModal?.open()" close-after-click>
{{ t('memories', 'Rename person') }}
<template #icon> <EditIcon :size="20" /> </template>
</NcActionButton>
<NcActionButton
@ -24,7 +20,7 @@
@click="$refs.mergeModal?.open()"
close-after-click
>
{{ t("memories", "Merge with different person") }}
{{ t('memories', 'Merge with different person') }}
<template #icon> <MergeIcon :size="20" /> </template>
</NcActionButton>
<NcActionCheckbox
@ -32,14 +28,14 @@
:checked.sync="config_showFaceRect"
@change="changeShowFaceRect"
>
{{ t("memories", "Mark person in preview") }}
{{ t('memories', 'Mark person in preview') }}
</NcActionCheckbox>
<NcActionButton
:aria-label="t('memories', 'Remove person')"
@click="$refs.deleteModal?.open()"
close-after-click
>
{{ t("memories", "Remove person") }}
{{ t('memories', 'Remove person') }}
<template #icon> <DeleteIcon :size="20" /> </template>
</NcActionButton>
</NcActions>
@ -52,23 +48,23 @@
</template>
<script lang="ts">
import { defineComponent } from "vue";
import { defineComponent } from 'vue';
import UserConfig from "../../mixins/UserConfig";
import NcActions from "@nextcloud/vue/dist/Components/NcActions";
import NcActionButton from "@nextcloud/vue/dist/Components/NcActionButton";
import NcActionCheckbox from "@nextcloud/vue/dist/Components/NcActionCheckbox";
import UserConfig from '../../mixins/UserConfig';
import NcActions from '@nextcloud/vue/dist/Components/NcActions';
import NcActionButton from '@nextcloud/vue/dist/Components/NcActionButton';
import NcActionCheckbox from '@nextcloud/vue/dist/Components/NcActionCheckbox';
import FaceEditModal from "../modal/FaceEditModal.vue";
import FaceDeleteModal from "../modal/FaceDeleteModal.vue";
import FaceMergeModal from "../modal/FaceMergeModal.vue";
import BackIcon from "vue-material-design-icons/ArrowLeft.vue";
import EditIcon from "vue-material-design-icons/Pencil.vue";
import DeleteIcon from "vue-material-design-icons/Close.vue";
import MergeIcon from "vue-material-design-icons/Merge.vue";
import FaceEditModal from '../modal/FaceEditModal.vue';
import FaceDeleteModal from '../modal/FaceDeleteModal.vue';
import FaceMergeModal from '../modal/FaceMergeModal.vue';
import BackIcon from 'vue-material-design-icons/ArrowLeft.vue';
import EditIcon from 'vue-material-design-icons/Pencil.vue';
import DeleteIcon from 'vue-material-design-icons/Close.vue';
import MergeIcon from 'vue-material-design-icons/Merge.vue';
export default defineComponent({
name: "FaceTopMatter",
name: 'FaceTopMatter',
components: {
NcActions,
NcActionButton,
@ -85,7 +81,7 @@ export default defineComponent({
mixins: [UserConfig],
data: () => ({
name: "",
name: '',
}),
watch: {
@ -100,7 +96,7 @@ export default defineComponent({
methods: {
createMatter() {
this.name = <string>this.$route.params.name || "";
this.name = <string>this.$route.params.name || '';
},
back() {
@ -108,10 +104,7 @@ export default defineComponent({
},
changeShowFaceRect() {
localStorage.setItem(
"memories_showFaceRect",
this.config_showFaceRect ? "1" : "0"
);
localStorage.setItem('memories_showFaceRect', this.config_showFaceRect ? '1' : '0');
setTimeout(() => {
this.$router.go(0); // refresh page
}, 500);

View File

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

View File

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

View File

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

View File

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

View File

@ -1,35 +1,30 @@
<template>
<div
v-bind="themeDataAttr"
ref="editor"
class="viewer__image-editor"
:class="{ loading: !imageEditor }"
/>
<div v-bind="themeDataAttr" ref="editor" class="viewer__image-editor" :class="{ loading: !imageEditor }" />
</template>
<script lang="ts">
import { defineComponent, PropType } from "vue";
import { defineComponent, PropType } from 'vue';
import { emit } from "@nextcloud/event-bus";
import { showError, showSuccess } from "@nextcloud/dialogs";
import axios from "@nextcloud/axios";
import { emit } from '@nextcloud/event-bus';
import { showError, showSuccess } from '@nextcloud/dialogs';
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 { IImageInfo, IPhoto } from "../../types";
import * as utils from "../../services/Utils";
import { fetchImage } from "../frame/XImgCache";
import { API } from '../../services/API';
import { IImageInfo, IPhoto } from '../../types';
import * as utils from '../../services/Utils';
import { fetchImage } from '../frame/XImgCache';
let TABS, TOOLS: any;
type FilerobotImageEditor = import("filerobot-image-editor").default;
let FilerobotImageEditor: typeof import("filerobot-image-editor").default;
type FilerobotImageEditor = import('filerobot-image-editor').default;
let FilerobotImageEditor: typeof import('filerobot-image-editor').default;
async function loadFilerobot() {
if (!FilerobotImageEditor) {
FilerobotImageEditor = (await import("filerobot-image-editor")).default;
FilerobotImageEditor = (await import('filerobot-image-editor')).default;
TABS = (<any>FilerobotImageEditor).TABS;
TOOLS = (<any>FilerobotImageEditor).TOOLS;
}
@ -54,7 +49,7 @@ export default defineComponent({
return {
source:
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),
defaultSavedImageName: this.defaultSavedImageName,
@ -81,7 +76,7 @@ export default defineComponent({
Rotate: {
angle: 90,
componentType: "buttons",
componentType: 'buttons',
},
// Translations
@ -89,24 +84,24 @@ export default defineComponent({
theme: {
palette: {
"bg-secondary": "var(--color-main-background)",
"bg-primary": "var(--color-background-dark)",
'bg-secondary': 'var(--color-main-background)',
'bg-primary': 'var(--color-background-dark)',
// Accent
"accent-primary": "var(--color-primary)",
'accent-primary': 'var(--color-primary)',
// Use by the slider
"border-active-bottom": "var(--color-primary)",
"icons-primary": "var(--color-main-text)",
'border-active-bottom': 'var(--color-primary)',
'icons-primary': 'var(--color-main-text)',
// Active state
"bg-primary-active": "var(--color-background-dark)",
"bg-primary-hover": "var(--color-background-hover)",
"accent-primary-active": "var(--color-main-text)",
'bg-primary-active': 'var(--color-background-dark)',
'bg-primary-hover': 'var(--color-background-hover)',
'accent-primary-active': 'var(--color-main-text)',
// 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: {
fontFamily: "var(--font-face)",
fontFamily: 'var(--font-face)',
},
},
@ -116,31 +111,29 @@ export default defineComponent({
},
defaultSavedImageName(): string {
return this.photo.basename || "";
return this.photo.basename || '';
},
defaultSavedImageType(): "jpeg" | "png" | "webp" {
if (
["image/jpeg", "image/png", "image/webp"].includes(this.photo.mimetype!)
) {
return this.photo.mimetype!.split("/")[1] as any;
defaultSavedImageType(): 'jpeg' | 'png' | 'webp' {
if (['image/jpeg', 'image/png', 'image/webp'].includes(this.photo.mimetype!)) {
return this.photo.mimetype!.split('/')[1] as any;
}
return "jpeg";
return 'jpeg';
},
hasHighContrastEnabled(): boolean {
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> {
if (this.hasHighContrastEnabled) {
return {
"data-theme-dark-highcontrast": true,
'data-theme-dark-highcontrast': true,
};
}
return {
"data-theme-dark": true,
'data-theme-dark': true,
};
},
},
@ -155,7 +148,7 @@ export default defineComponent({
this.imageEditor.render();
// Handle keyboard
window.addEventListener("keydown", this.handleKeydown, true);
window.addEventListener('keydown', this.handleKeydown, true);
},
beforeDestroy() {
@ -163,7 +156,7 @@ export default defineComponent({
this.imageEditor.terminate();
}
globalThis._fileRobotOverrideImage = undefined;
window.removeEventListener("keydown", this.handleKeydown, true);
window.removeEventListener('keydown', this.handleKeydown, true);
},
methods: {
@ -189,8 +182,8 @@ export default defineComponent({
this.onExitWithoutSaving();
return;
}
window.removeEventListener("keydown", this.handleKeydown, true);
this.$emit("close");
window.removeEventListener('keydown', this.handleKeydown, true);
this.$emit('close');
},
/**
@ -229,36 +222,33 @@ export default defineComponent({
// Make sure we have an extension
let name = data.name;
const nameLower = name.toLowerCase();
if (!nameLower.endsWith(data.extension) && !nameLower.endsWith(".jpg")) {
name += "." + data.extension;
if (!nameLower.endsWith(data.extension) && !nameLower.endsWith('.jpg')) {
name += '.' + data.extension;
}
try {
const res = await axios.put<IImageInfo>(
API.IMAGE_EDIT(this.photo.fileid),
{
name: name,
width: data.width,
height: data.height,
quality: data.quality,
extension: data.extension,
state: state,
}
);
const res = await axios.put<IImageInfo>(API.IMAGE_EDIT(this.photo.fileid), {
name: name,
width: data.width,
height: data.height,
quality: data.quality,
extension: data.extension,
state: state,
});
const fileid = res.data.fileid;
// Success, emit an appropriate event
showSuccess(this.t("memories", "Image saved successfully"));
showSuccess(this.t('memories', 'Image saved successfully'));
if (fileid !== this.photo.fileid) {
emit("files:file:created", { fileid });
emit('files:file:created', { fileid });
} else {
utils.updatePhotoFromImageInfo(this.photo, res.data);
emit("files:file:updated", { fileid });
emit('files:file:updated', { fileid });
}
this.onClose(undefined, false);
} catch (err) {
showError(this.t("memories", "Error saving image"));
showError(this.t('memories', 'Error saving image'));
console.error(err);
}
},
@ -268,21 +258,19 @@ export default defineComponent({
*/
onExitWithoutSaving() {
(<any>OC.dialogs).confirmDestructive(
translations.changesLoseConfirmation +
"\n\n" +
translations.changesLoseConfirmationHint,
this.t("memories", "Unsaved changes"),
translations.changesLoseConfirmation + '\n\n' + translations.changesLoseConfirmationHint,
this.t('memories', 'Unsaved changes'),
{
type: (<any>OC.dialogs).YES_NO_BUTTONS,
confirm: this.t("memories", "Drop changes"),
confirmClasses: "error",
confirm: this.t('memories', 'Drop changes'),
confirmClasses: 'error',
cancel: translations.cancel,
},
(decision) => {
if (!decision) {
return;
}
this.onClose("warning-ignored", false);
this.onClose('warning-ignored', false);
}
);
},
@ -291,29 +279,23 @@ export default defineComponent({
handleKeydown(event) {
event.stopImmediatePropagation();
// escape key
if (event.key === "Escape") {
if (event.key === 'Escape') {
// Since we cannot call the closeMethod and know if there
// are unsaved changes, let's fake a close button trigger.
event.preventDefault();
(
document.querySelector(".FIE_topbar-close-button") as HTMLElement
).click();
(document.querySelector('.FIE_topbar-close-button') as HTMLElement).click();
}
// ctrl + S = save
if (event.ctrlKey && event.key === "s") {
if (event.ctrlKey && event.key === 's') {
event.preventDefault();
(
document.querySelector(".FIE_topbar-save-button") as HTMLElement
).click();
(document.querySelector('.FIE_topbar-save-button') as HTMLElement).click();
}
// ctrl + Z = undo
if (event.ctrlKey && event.key === "z") {
if (event.ctrlKey && event.key === 'z') {
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;
margin: 0 !important;
border: transparent !important;
&[color="error"] {
&[color='error'] {
color: white !important;
background-color: var(--color-error) !important;
&:hover,
@ -399,7 +381,7 @@ export default defineComponent({
background-color: var(--color-error-hover) !important;
}
}
&[color="primary"] {
&[color='primary'] {
color: var(--color-primary-text) !important;
background-color: var(--color-primary-element) !important;
&:hover,
@ -423,7 +405,7 @@ export default defineComponent({
}
// Disable jpeg saving (jpg is already here)
&[value="jpeg"] {
&[value='jpeg'] {
display: none;
}
}
@ -500,7 +482,7 @@ export default defineComponent({
background-color: var(--color-background-hover) !important;
}
&[aria-selected="true"] {
&[aria-selected='true'] {
color: var(--color-main-text);
background-color: var(--color-background-dark);
box-shadow: 0 0 0 2px var(--color-primary-element);
@ -514,8 +496,8 @@ export default defineComponent({
}
// Matching buttons tools
& > div[class$="-tool-button"],
& > div[class$="-tool"] {
& > div[class$='-tool-button'],
& > div[class$='-tool'] {
display: flex;
align-items: center;
justify-content: center;
@ -634,7 +616,7 @@ export default defineComponent({
width: 28px;
height: 28px;
margin: -16px 0 0 -16px;
content: "";
content: '';
-webkit-transform-origin: center;
-ms-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
@ -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
*/
export default {
name: t("memories", "Name"),
save: t("memories", "Save"),
saveAs: t("memories", "Save as"),
back: t("memories", "Back"),
loading: t("memories", "Loading …"),
name: t('memories', 'Name'),
save: t('memories', 'Save'),
saveAs: t('memories', 'Save as'),
back: t('memories', 'Back'),
loading: t('memories', 'Loading …'),
// resetOperations: 'Reset/delete all operations',
resetOperations: t("memories", "Reset"),
changesLoseConfirmation: t("memories", "All changes will be lost."),
changesLoseConfirmationHint: t(
"memories",
"Are you sure you want to continue?"
),
cancel: t("memories", "Cancel"),
continue: t("memories", "Continue"),
undoTitle: t("memories", "Undo"),
redoTitle: t("memories", "Redo"),
showImageTitle: t("memories", "Show original image"),
zoomInTitle: t("memories", "Zoom in"),
zoomOutTitle: t("memories", "Zoom out"),
toggleZoomMenuTitle: t("memories", "Toggle zoom menu"),
adjustTab: t("memories", "Adjust"),
finetuneTab: t("memories", "Fine-tune"),
filtersTab: t("memories", "Filters"),
watermarkTab: t("memories", "Watermark"),
annotateTab: t("memories", "Draw"),
resize: t("memories", "Resize"),
resizeTab: t("memories", "Resize"),
invalidImageError: t("memories", "Invalid image."),
uploadImageError: t("memories", "Error while uploading the image."),
areNotImages: t("memories", "are not images"),
isNotImage: t("memories", "is not an image"),
toBeUploaded: t("memories", "to be uploaded"),
cropTool: t("memories", "Crop"),
original: t("memories", "Original"),
custom: t("memories", "Custom"),
square: t("memories", "Square"),
landscape: t("memories", "Landscape"),
portrait: t("memories", "Portrait"),
ellipse: t("memories", "Ellipse"),
classicTv: t("memories", "Classic TV"),
cinemascope: t("memories", "CinemaScope"),
arrowTool: t("memories", "Arrow"),
blurTool: t("memories", "Blur"),
brightnessTool: t("memories", "Brightness"),
contrastTool: t("memories", "Contrast"),
ellipseTool: t("memories", "Ellipse"),
unFlipX: t("memories", "Un-flip X"),
flipX: t("memories", "Flip X"),
unFlipY: t("memories", "Un-flip Y"),
flipY: t("memories", "Flip Y"),
hsvTool: t("memories", "HSV"),
hue: t("memories", "Hue"),
saturation: t("memories", "Saturation"),
value: t("memories", "Value"),
imageTool: t("memories", "Image"),
importing: t("memories", "Importing …"),
addImage: t("memories", "+ Add image"),
lineTool: t("memories", "Line"),
penTool: t("memories", "Pen"),
polygonTool: t("memories", "Polygon"),
sides: t("memories", "Sides"),
rectangleTool: t("memories", "Rectangle"),
cornerRadius: t("memories", "Corner Radius"),
resizeWidthTitle: t("memories", "Width in pixels"),
resizeHeightTitle: t("memories", "Height in pixels"),
toggleRatioLockTitle: t("memories", "Toggle ratio lock"),
reset: t("memories", "Reset"),
resetSize: t("memories", "Reset to original image size"),
rotateTool: t("memories", "Rotate"),
textTool: t("memories", "Text"),
textSpacings: t("memories", "Text spacing"),
textAlignment: t("memories", "Text alignment"),
fontFamily: t("memories", "Font family"),
size: t("memories", "Size"),
letterSpacing: t("memories", "Letter spacing"),
lineHeight: t("memories", "Line height"),
warmthTool: t("memories", "Warmth"),
addWatermark: t("memories", "+ Add watermark"),
addWatermarkTitle: t("memories", "Choose watermark type"),
uploadWatermark: t("memories", "Upload watermark"),
addWatermarkAsText: t("memories", "Add as text"),
padding: t("memories", "Padding"),
shadow: t("memories", "Shadow"),
horizontal: t("memories", "Horizontal"),
vertical: t("memories", "Vertical"),
blur: t("memories", "Blur"),
opacity: t("memories", "Opacity"),
position: t("memories", "Position"),
stroke: t("memories", "Stroke"),
saveAsModalLabel: t("memories", "Save image as"),
extension: t("memories", "Extension"),
nameIsRequired: t("memories", "Name is required."),
quality: t("memories", "Quality"),
imageDimensionsHoverTitle: t("memories", "Saved image size (width x height)"),
resetOperations: t('memories', 'Reset'),
changesLoseConfirmation: t('memories', 'All changes will be lost.'),
changesLoseConfirmationHint: t('memories', 'Are you sure you want to continue?'),
cancel: t('memories', 'Cancel'),
continue: t('memories', 'Continue'),
undoTitle: t('memories', 'Undo'),
redoTitle: t('memories', 'Redo'),
showImageTitle: t('memories', 'Show original image'),
zoomInTitle: t('memories', 'Zoom in'),
zoomOutTitle: t('memories', 'Zoom out'),
toggleZoomMenuTitle: t('memories', 'Toggle zoom menu'),
adjustTab: t('memories', 'Adjust'),
finetuneTab: t('memories', 'Fine-tune'),
filtersTab: t('memories', 'Filters'),
watermarkTab: t('memories', 'Watermark'),
annotateTab: t('memories', 'Draw'),
resize: t('memories', 'Resize'),
resizeTab: t('memories', 'Resize'),
invalidImageError: t('memories', 'Invalid image.'),
uploadImageError: t('memories', 'Error while uploading the image.'),
areNotImages: t('memories', 'are not images'),
isNotImage: t('memories', 'is not an image'),
toBeUploaded: t('memories', 'to be uploaded'),
cropTool: t('memories', 'Crop'),
original: t('memories', 'Original'),
custom: t('memories', 'Custom'),
square: t('memories', 'Square'),
landscape: t('memories', 'Landscape'),
portrait: t('memories', 'Portrait'),
ellipse: t('memories', 'Ellipse'),
classicTv: t('memories', 'Classic TV'),
cinemascope: t('memories', 'CinemaScope'),
arrowTool: t('memories', 'Arrow'),
blurTool: t('memories', 'Blur'),
brightnessTool: t('memories', 'Brightness'),
contrastTool: t('memories', 'Contrast'),
ellipseTool: t('memories', 'Ellipse'),
unFlipX: t('memories', 'Un-flip X'),
flipX: t('memories', 'Flip X'),
unFlipY: t('memories', 'Un-flip Y'),
flipY: t('memories', 'Flip Y'),
hsvTool: t('memories', 'HSV'),
hue: t('memories', 'Hue'),
saturation: t('memories', 'Saturation'),
value: t('memories', 'Value'),
imageTool: t('memories', 'Image'),
importing: t('memories', 'Importing …'),
addImage: t('memories', '+ Add image'),
lineTool: t('memories', 'Line'),
penTool: t('memories', 'Pen'),
polygonTool: t('memories', 'Polygon'),
sides: t('memories', 'Sides'),
rectangleTool: t('memories', 'Rectangle'),
cornerRadius: t('memories', 'Corner Radius'),
resizeWidthTitle: t('memories', 'Width in pixels'),
resizeHeightTitle: t('memories', 'Height in pixels'),
toggleRatioLockTitle: t('memories', 'Toggle ratio lock'),
reset: t('memories', 'Reset'),
resetSize: t('memories', 'Reset to original image size'),
rotateTool: t('memories', 'Rotate'),
textTool: t('memories', 'Text'),
textSpacings: t('memories', 'Text spacing'),
textAlignment: t('memories', 'Text alignment'),
fontFamily: t('memories', 'Font family'),
size: t('memories', 'Size'),
letterSpacing: t('memories', 'Letter spacing'),
lineHeight: t('memories', 'Line height'),
warmthTool: t('memories', 'Warmth'),
addWatermark: t('memories', '+ Add watermark'),
addWatermarkTitle: t('memories', 'Choose watermark type'),
uploadWatermark: t('memories', 'Upload watermark'),
addWatermarkAsText: t('memories', 'Add as text'),
padding: t('memories', 'Padding'),
shadow: t('memories', 'Shadow'),
horizontal: t('memories', 'Horizontal'),
vertical: t('memories', 'Vertical'),
blur: t('memories', 'Blur'),
opacity: t('memories', 'Opacity'),
position: t('memories', 'Position'),
stroke: t('memories', 'Stroke'),
saveAsModalLabel: t('memories', 'Save image as'),
extension: t('memories', 'Extension'),
nameIsRequired: t('memories', 'Name is required.'),
quality: t('memories', 'Quality'),
imageDimensionsHoverTitle: t('memories', 'Saved image size (width x height)'),
cropSizeLowerThanResizedWarning: t(
"memories",
"Note that the selected crop area is lower than the applied resize which might cause quality decrease"
'memories',
'Note that the selected crop area is lower than the applied resize which might cause quality decrease'
),
actualSize: t("memories", "Actual size (100%)"),
fitSize: t("memories", "Fit size"),
actualSize: t('memories', 'Actual size (100%)'),
fitSize: t('memories', 'Fit size'),
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,26 +1,26 @@
import "reflect-metadata";
import Vue from "vue";
import VueVirtualScroller from "vue-virtual-scroller";
import "vue-virtual-scroller/dist/vue-virtual-scroller.css";
import XImg from "./components/frame/XImg.vue";
import GlobalMixin from "./mixins/GlobalMixin";
import 'reflect-metadata';
import Vue from 'vue';
import VueVirtualScroller from 'vue-virtual-scroller';
import 'vue-virtual-scroller/dist/vue-virtual-scroller.css';
import XImg from './components/frame/XImg.vue';
import GlobalMixin from './mixins/GlobalMixin';
import App from "./App.vue";
import Admin from "./components/admin/AdminMain.vue";
import router from "./router";
import { generateFilePath } from "@nextcloud/router";
import { getRequestToken } from "@nextcloud/auth";
import App from './App.vue';
import Admin from './components/admin/AdminMain.vue';
import router from './router';
import { generateFilePath } from '@nextcloud/router';
import { getRequestToken } from '@nextcloud/auth';
import type { Route } from "vue-router";
import type { IPhoto } from "./types";
import type PlyrType from "plyr";
import type videojsType from "video.js";
import type { Route } from 'vue-router';
import type { IPhoto } from './types';
import type PlyrType from 'plyr';
import type videojsType from 'video.js';
import "./global.scss";
import './global.scss';
// Global exposed variables
declare global {
var mode: "admin" | "user";
var mode: 'admin' | 'user';
var vueroute: () => Route;
var OC: Nextcloud.v24.OC;
@ -59,35 +59,30 @@ globalThis.windowInnerWidth = window.innerWidth;
globalThis.windowInnerHeight = window.innerHeight;
// 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
// OC.linkTo matches the apps folders
// OC.generateUrl ensure the index.php (or not)
// 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
// Does not need to be cryptographically secure
const getClientId = (): string =>
Math.random().toString(36).substring(2, 15).padEnd(12, "0");
const getClientId = (): string => Math.random().toString(36).substring(2, 15).padEnd(12, '0');
globalThis.videoClientId = getClientId();
globalThis.videoClientIdPersistent =
localStorage.getItem("videoClientIdPersistent") ?? getClientId();
localStorage.setItem(
"videoClientIdPersistent",
globalThis.videoClientIdPersistent
);
globalThis.videoClientIdPersistent = localStorage.getItem('videoClientIdPersistent') ?? getClientId();
localStorage.setItem('videoClientIdPersistent', globalThis.videoClientIdPersistent);
Vue.mixin(GlobalMixin as any);
Vue.use(VueVirtualScroller);
Vue.component("XImg", XImg);
Vue.component('XImg', XImg);
// https://github.com/nextcloud/photos/blob/156f280c0476c483cb9ce81769ccb0c1c6500a4e/src/main.js
// TODO: remove when we have a proper fileinfo standalone library
// original scripts are loaded from
// https://github.com/nextcloud/server/blob/5bf3d1bb384da56adbf205752be8f840aac3b0c5/lib/private/legacy/template.php#L120-L122
window.addEventListener("DOMContentLoaded", () => {
window.addEventListener('DOMContentLoaded', () => {
if (!globalThis.OCA.Files) {
globalThis.OCA.Files = {};
}
@ -105,20 +100,20 @@ window.addEventListener("DOMContentLoaded", () => {
let app = null;
const adminSection = document.getElementById("memories-admin-content");
const adminSection = document.getElementById('memories-admin-content');
if (adminSection) {
app = new Vue({
el: "#memories-admin-content",
el: '#memories-admin-content',
render: (h) => h(Admin),
});
globalThis.mode = "admin";
globalThis.mode = 'admin';
} else {
app = new Vue({
el: "#content",
el: '#content',
router,
render: (h) => h(App),
});
globalThis.mode = "user";
globalThis.mode = 'user';
}
export default app;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -19,10 +19,10 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
*/
import camelcase from "camelcase";
import { IPhoto } from "../types";
import { API } from "./API";
import { isNumber } from "./utils/algo";
import camelcase from 'camelcase';
import { IPhoto } from '../types';
import { API } from './API';
import { isNumber } from './utils/algo';
/**
* Get an url encoded path
@ -31,11 +31,11 @@ import { isNumber } from "./utils/algo";
* @return {string} url encoded file path
*/
const encodeFilePath = function (path) {
const pathSections = (path.startsWith("/") ? path : `/${path}`).split("/");
let relativePath = "";
const pathSections = (path.startsWith('/') ? path : `/${path}`).split('/');
let relativePath = '';
pathSections.forEach((section) => {
if (section !== "") {
relativePath += "/" + encodeURIComponent(section);
if (section !== '') {
relativePath += '/' + encodeURIComponent(section);
}
});
return relativePath;
@ -48,9 +48,9 @@ const encodeFilePath = function (path) {
* @return {string[]} [dirPath, fileName]
*/
const extractFilePaths = function (path) {
const pathSections = path.split("/");
const pathSections = path.split('/');
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];
};
@ -73,23 +73,18 @@ const sortCompare = function (fileInfo1, fileInfo2, key, asc = true) {
// if this is a number, let's sort by integer
if (isNumber(fileInfo1[key]) && isNumber(fileInfo2[key])) {
return asc
? Number(fileInfo2[key]) - Number(fileInfo1[key])
: Number(fileInfo1[key]) - Number(fileInfo2[key]);
return asc ? Number(fileInfo2[key]) - Number(fileInfo1[key]) : Number(fileInfo1[key]) - Number(fileInfo2[key]);
}
// 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;
} else if (fileInfo1.type === "file" && fileInfo2.type !== "file") {
} else if (fileInfo1.type === 'file' && fileInfo2.type !== 'file') {
return asc ? 1 : -1;
}
// if this is a date, let's sort by date
if (
isNumber(new Date(fileInfo1[key]).getTime()) &&
isNumber(new Date(fileInfo2[key]).getTime())
) {
if (isNumber(new Date(fileInfo1[key]).getTime()) && isNumber(new Date(fileInfo2[key]).getTime())) {
return asc
? new Date(fileInfo2[key]).getTime() - new Date(fileInfo1[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
return asc
? fileInfo1[key]
?.toString()
?.localeCompare(
fileInfo2[key].toString(),
globalThis.OC.getLanguage()
) || 1
: -fileInfo1[key]
?.toString()
?.localeCompare(
fileInfo2[key].toString(),
globalThis.OC.getLanguage()
) || -1;
? fileInfo1[key]?.toString()?.localeCompare(fileInfo2[key].toString(), globalThis.OC.getLanguage()) || 1
: -fileInfo1[key]?.toString()?.localeCompare(fileInfo2[key].toString(), globalThis.OC.getLanguage()) || -1;
};
const genFileInfo = function (obj) {
@ -118,13 +103,13 @@ const genFileInfo = function (obj) {
const data = obj[key];
// flatten object if any
if (!!data && typeof data === "object") {
if (!!data && typeof data === 'object') {
Object.assign(fileInfo, genFileInfo(data));
} else {
// format key and add it to the fileInfo
if (data === "false") {
if (data === 'false') {
fileInfo[camelcase(key)] = false;
} else if (data === "true") {
} else if (data === 'true') {
fileInfo[camelcase(key)] = true;
} else {
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.
@ -93,8 +93,7 @@ export function getLayout(
// Number of photos left
const numLeft = input.length - photoId - 1;
// Number of photos needed for perfect fill after using n
const needFill = (n: number) =>
opts.numCols - col - 2 + (n / 2 - 1) * (opts.numCols - 2);
const needFill = (n: number) => opts.numCols - col - 2 + (n / 2 - 1) * (opts.numCols - 2);
let canUse4 =
// We have enough space
@ -222,12 +221,10 @@ export function getLayout(
}
function flagMatrixStr(matrix: number[][], numFlag: number) {
let str = "";
let str = '';
for (let i = 0; i < matrix.length; i++) {
const rstr = matrix[i]
.map((v) => v.toString(2).padStart(numFlag, "0"))
.join(" ");
str += i.toString().padStart(2) + " | " + rstr + "\n";
const rstr = matrix[i].map((v) => v.toString(2).padStart(numFlag, '0')).join(' ');
str += i.toString().padStart(2) + ' | ' + rstr + '\n';
}
return str;
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,27 +1,23 @@
import { IImageInfo, IPhoto } from "../../types";
import { API } from "../API";
import { IImageInfo, IPhoto } from '../../types';
import { API } from '../API';
/** Get preview URL from photo object */
export function getPreviewUrl(
photo: IPhoto,
square: boolean,
size: number | [number, number] | "screen"
) {
export function getPreviewUrl(photo: IPhoto, square: boolean, size: number | [number, number] | 'screen') {
// Screen-appropriate size
if (size === "screen") {
if (size === 'screen') {
const sw = Math.floor(screen.width * devicePixelRatio);
const sh = Math.floor(screen.height * devicePixelRatio);
size = [sw, sh];
}
// 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), {
c: photo.etag,
x,
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
*/
export function getFolderRoutePath(basePath: string) {
let path: any = vueroute().params.path || "/";
path = typeof path === "string" ? path : path.join("/");
path = basePath + "/" + path;
path = path.replace(/\/\/+/, "/"); // Remove double slashes
let path: any = vueroute().params.path || '/';
path = typeof path === 'string' ? path : path.join('/');
path = basePath + '/' + path;
path = path.replace(/\/\/+/, '/'); // Remove double slashes
return path;
}
@ -68,15 +64,15 @@ export function getLivePhotoVideoUrl(p: IPhoto, transcode: boolean) {
* @param video Video element
*/
export function setupLivePhotoHooks(video: HTMLVideoElement) {
const div = video.closest(".memories-livephoto") as HTMLDivElement;
const div = video.closest('.memories-livephoto') as HTMLDivElement;
video.onplay = () => {
div.classList.add("playing");
div.classList.add('playing');
};
video.oncanplay = () => {
div.classList.add("canplay");
div.classList.add('canplay');
};
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;
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;
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.blankVideo = "";
(<any>Plyr).defaults.blankVideo = '';

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