From fc6a4fc244604377227c0ee5f617e9f379925a67 Mon Sep 17 00:00:00 2001 From: Varun Patil Date: Fri, 28 Oct 2022 12:08:34 -0700 Subject: [PATCH] Make the code prettier --- src/App.vue | 363 +-- src/components/FirstStart.vue | 285 +-- src/components/ScrollerManager.vue | 984 ++++---- src/components/SelectionManager.vue | 843 ++++--- src/components/Settings.vue | 136 +- src/components/Timeline.vue | 2144 +++++++++-------- src/components/frame/Folder.vue | 346 +-- src/components/frame/Photo.vue | 427 ++-- src/components/frame/Tag.vue | 417 ++-- src/components/modal/AddToAlbumModal.vue | 126 +- src/components/modal/AlbumCollaborators.vue | 865 ++++--- src/components/modal/AlbumCreateModal.vue | 125 +- src/components/modal/AlbumDeleteModal.vue | 132 +- src/components/modal/AlbumForm.vue | 448 ++-- src/components/modal/AlbumPicker.vue | 277 ++- src/components/modal/AlbumShareModal.vue | 125 +- src/components/modal/EditDate.vue | 756 +++--- src/components/modal/FaceDeleteModal.vue | 126 +- src/components/modal/FaceEditModal.vue | 156 +- src/components/modal/FaceList.vue | 112 +- src/components/modal/FaceMergeModal.vue | 234 +- src/components/modal/FaceMoveModal.vue | 208 +- src/components/modal/Modal.vue | 69 +- src/components/top-matter/AlbumTopMatter.vue | 200 +- src/components/top-matter/FaceTopMatter.vue | 195 +- src/components/top-matter/FolderTopMatter.vue | 92 +- src/components/top-matter/OnThisDay.vue | 492 ++-- src/components/top-matter/TagTopMatter.vue | 84 +- src/components/top-matter/TopMatter.vue | 79 +- src/main.ts | 40 +- src/mixins/GlobalMixin.ts | 18 +- src/mixins/UserConfig.ts | 94 +- src/router.ts | 210 +- src/services/DavClient.ts | 42 +- src/services/DavRequests.ts | 18 +- src/services/FileUtils.ts | 219 +- src/services/Layout.ts | 421 ++-- src/services/NumberUtils.ts | 14 +- src/services/Utils.ts | 301 +-- src/services/Viewer.ts | 155 +- src/services/dav/albums.ts | 292 ++- src/services/dav/archive.ts | 54 +- src/services/dav/base.ts | 199 +- src/services/dav/download.ts | 60 +- src/services/dav/face.ts | 111 +- src/services/dav/favorites.ts | 99 +- src/services/dav/folders.ts | 61 +- src/services/dav/onthisday.ts | 86 +- src/services/dav/tags.ts | 61 +- src/types.ts | 328 +-- src/vue-shims.d.ts | 14 +- 51 files changed, 7276 insertions(+), 6467 deletions(-) diff --git a/src/App.vue b/src/App.vue index 93ebfe8f..3e11725a 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,221 +1,254 @@ diff --git a/src/components/FirstStart.vue b/src/components/FirstStart.vue index 9a1a6a7b..d5b7ea4b 100644 --- a/src/components/FirstStart.vue +++ b/src/components/FirstStart.vue @@ -1,176 +1,189 @@ \ No newline at end of file diff --git a/src/components/ScrollerManager.vue b/src/components/ScrollerManager.vue index 670858f4..990c1362 100644 --- a/src/components/ScrollerManager.vue +++ b/src/components/ScrollerManager.vue @@ -1,532 +1,586 @@ \ No newline at end of file diff --git a/src/components/SelectionManager.vue b/src/components/SelectionManager.vue index 1de723a2..1d22810a 100644 --- a/src/components/SelectionManager.vue +++ b/src/components/SelectionManager.vue @@ -1,444 +1,513 @@ diff --git a/src/components/Settings.vue b/src/components/Settings.vue index 4a679be2..43839041 100644 --- a/src/components/Settings.vue +++ b/src/components/Settings.vue @@ -21,89 +21,103 @@ --> \ No newline at end of file diff --git a/src/components/Timeline.vue b/src/components/Timeline.vue index 578affd5..b4c5bb09 100644 --- a/src/components/Timeline.vue +++ b/src/components/Timeline.vue @@ -1,1170 +1,1258 @@ \ No newline at end of file diff --git a/src/components/frame/Folder.vue b/src/components/frame/Folder.vue index 0f083d66..017a70e1 100644 --- a/src/components/frame/Folder.vue +++ b/src/components/frame/Folder.vue @@ -1,202 +1,238 @@ \ No newline at end of file diff --git a/src/components/frame/Photo.vue b/src/components/frame/Photo.vue index 57630637..0d7bb5cb 100644 --- a/src/components/frame/Photo.vue +++ b/src/components/frame/Photo.vue @@ -1,194 +1,210 @@ \ No newline at end of file diff --git a/src/components/frame/Tag.vue b/src/components/frame/Tag.vue index 23aaba7b..42c6aca7 100644 --- a/src/components/frame/Tag.vue +++ b/src/components/frame/Tag.vue @@ -1,255 +1,276 @@ \ No newline at end of file diff --git a/src/components/modal/AddToAlbumModal.vue b/src/components/modal/AddToAlbumModal.vue index adbb9a9e..063f02a3 100644 --- a/src/components/modal/AddToAlbumModal.vue +++ b/src/components/modal/AddToAlbumModal.vue @@ -1,84 +1,92 @@ \ No newline at end of file diff --git a/src/components/modal/AlbumCollaborators.vue b/src/components/modal/AlbumCollaborators.vue index b1593c6b..3db9e8f0 100644 --- a/src/components/modal/AlbumCollaborators.vue +++ b/src/components/modal/AlbumCollaborators.vue @@ -20,452 +20,533 @@ - --> \ No newline at end of file diff --git a/src/components/modal/AlbumCreateModal.vue b/src/components/modal/AlbumCreateModal.vue index f76be8d5..a3d5bca6 100644 --- a/src/components/modal/AlbumCreateModal.vue +++ b/src/components/modal/AlbumCreateModal.vue @@ -1,87 +1,90 @@ \ No newline at end of file diff --git a/src/components/modal/AlbumDeleteModal.vue b/src/components/modal/AlbumDeleteModal.vue index e2a04c5d..92b49343 100644 --- a/src/components/modal/AlbumDeleteModal.vue +++ b/src/components/modal/AlbumDeleteModal.vue @@ -1,81 +1,91 @@ \ No newline at end of file diff --git a/src/components/modal/AlbumForm.vue b/src/components/modal/AlbumForm.vue index e9a7a99e..49672401 100644 --- a/src/components/modal/AlbumForm.vue +++ b/src/components/modal/AlbumForm.vue @@ -20,244 +20,274 @@ - --> \ No newline at end of file diff --git a/src/components/modal/AlbumPicker.vue b/src/components/modal/AlbumPicker.vue index 604be79c..44adf8c0 100644 --- a/src/components/modal/AlbumPicker.vue +++ b/src/components/modal/AlbumPicker.vue @@ -20,168 +20,185 @@ - --> \ No newline at end of file diff --git a/src/components/modal/AlbumShareModal.vue b/src/components/modal/AlbumShareModal.vue index 143b3606..06dde4a3 100644 --- a/src/components/modal/AlbumShareModal.vue +++ b/src/components/modal/AlbumShareModal.vue @@ -1,76 +1,83 @@ \ No newline at end of file diff --git a/src/components/modal/EditDate.vue b/src/components/modal/EditDate.vue index 48ea7fc0..da5723d8 100644 --- a/src/components/modal/EditDate.vue +++ b/src/components/modal/EditDate.vue @@ -1,425 +1,463 @@ diff --git a/src/components/modal/FaceDeleteModal.vue b/src/components/modal/FaceDeleteModal.vue index ba2d9823..7d8e23cc 100644 --- a/src/components/modal/FaceDeleteModal.vue +++ b/src/components/modal/FaceDeleteModal.vue @@ -1,79 +1,87 @@ \ No newline at end of file diff --git a/src/components/modal/FaceEditModal.vue b/src/components/modal/FaceEditModal.vue index 6ba72bcb..53f97fc2 100644 --- a/src/components/modal/FaceEditModal.vue +++ b/src/components/modal/FaceEditModal.vue @@ -1,97 +1,109 @@ \ No newline at end of file diff --git a/src/components/modal/FaceList.vue b/src/components/modal/FaceList.vue index e819d553..683a41b3 100644 --- a/src/components/modal/FaceList.vue +++ b/src/components/modal/FaceList.vue @@ -1,83 +1,83 @@ \ No newline at end of file diff --git a/src/components/modal/FaceMergeModal.vue b/src/components/modal/FaceMergeModal.vue index 309416de..d1d677f0 100644 --- a/src/components/modal/FaceMergeModal.vue +++ b/src/components/modal/FaceMergeModal.vue @@ -1,136 +1,156 @@ \ No newline at end of file diff --git a/src/components/modal/FaceMoveModal.vue b/src/components/modal/FaceMoveModal.vue index 6890515e..68fe58b7 100644 --- a/src/components/modal/FaceMoveModal.vue +++ b/src/components/modal/FaceMoveModal.vue @@ -1,129 +1,141 @@ \ No newline at end of file diff --git a/src/components/modal/Modal.vue b/src/components/modal/Modal.vue index f48cf951..4f91a982 100644 --- a/src/components/modal/Modal.vue +++ b/src/components/modal/Modal.vue @@ -1,56 +1,53 @@ \ No newline at end of file diff --git a/src/components/top-matter/AlbumTopMatter.vue b/src/components/top-matter/AlbumTopMatter.vue index 8e602b19..57bd1026 100644 --- a/src/components/top-matter/AlbumTopMatter.vue +++ b/src/components/top-matter/AlbumTopMatter.vue @@ -1,123 +1,139 @@ \ No newline at end of file diff --git a/src/components/top-matter/FaceTopMatter.vue b/src/components/top-matter/FaceTopMatter.vue index f66f243d..edba9a1b 100644 --- a/src/components/top-matter/FaceTopMatter.vue +++ b/src/components/top-matter/FaceTopMatter.vue @@ -1,116 +1,135 @@ \ No newline at end of file diff --git a/src/components/top-matter/FolderTopMatter.vue b/src/components/top-matter/FolderTopMatter.vue index af7deaa2..dff3ca7b 100644 --- a/src/components/top-matter/FolderTopMatter.vue +++ b/src/components/top-matter/FolderTopMatter.vue @@ -1,60 +1,66 @@ \ No newline at end of file diff --git a/src/components/top-matter/OnThisDay.vue b/src/components/top-matter/OnThisDay.vue index 9d1fc0e2..4efad140 100644 --- a/src/components/top-matter/OnThisDay.vue +++ b/src/components/top-matter/OnThisDay.vue @@ -1,190 +1,210 @@ @@ -193,97 +213,107 @@ $height: 200px; $mobHeight: 150px; .outer { - width: calc(100% - 50px); - height: $height; - overflow: hidden; - position: relative; - padding: 0 calc(28px * 0.6); + width: calc(100% - 50px); + height: $height; + overflow: hidden; + position: relative; + padding: 0 calc(28px * 0.6); - // Sloppy: ideally this should be done in Timeline - // to put a gap between the title and this - margin-top: 10px; + // Sloppy: ideally this should be done in Timeline + // to put a gap between the title and this + margin-top: 10px; + .inner { + height: calc(100% + 20px); + white-space: nowrap; + overflow-x: scroll; + scroll-behavior: smooth; + border-radius: 10px; + } + + :deep .dir-btn button { + transform: scale(0.6); + box-shadow: var(--color-main-text) 0 0 3px 0 !important; + background-color: var(--color-main-background) !important; + } + + .left-btn { + position: absolute; + top: 50%; + left: 0; + transform: translate(-10%, -50%); + } + + .right-btn { + position: absolute; + top: 50%; + right: 0; + transform: translate(10%, -50%); + } + + @media (max-width: 768px) { + width: 98%; + padding: 0; .inner { - height: calc(100% + 20px); - white-space: nowrap; - overflow-x: scroll; - scroll-behavior: smooth; - border-radius: 10px; + padding: 0 8px; } - - :deep .dir-btn button { - transform: scale(0.6); - box-shadow: var(--color-main-text) 0 0 3px 0 !important; - background-color: var(--color-main-background) !important; - } - - .left-btn { - position: absolute; - top: 50%; left: 0; - transform: translate(-10%, -50%); - } - - .right-btn { - position: absolute; - top: 50%; right: 0; - transform: translate(10%, -50%); - } - - @media (max-width: 768px) { - width: 98%; - padding: 0; - .inner { padding: 0 8px; } - .dir-btn { display: none; } - } - @media (max-width: 600px) { - height: $mobHeight; + .dir-btn { + display: none; } + } + @media (max-width: 600px) { + height: $mobHeight; + } } .group { - height: $height; - aspect-ratio: 4/3; - display: inline-block; - position: relative; - cursor: pointer; + height: $height; + aspect-ratio: 4/3; + display: inline-block; + position: relative; + cursor: pointer; - &:not(:last-of-type) { margin-right: 8px; } + &:not(:last-of-type) { + margin-right: 8px; + } - img { - cursor: inherit; - object-fit: cover; - border-radius: 10px; - background-color: var(--color-background-dark); - background-clip: padding-box, content-box; - } + img { + cursor: inherit; + object-fit: cover; + border-radius: 10px; + background-color: var(--color-background-dark); + background-clip: padding-box, content-box; + } + .overlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.2); + border-radius: 10px; + display: flex; + align-items: end; + justify-content: center; + color: white; + font-size: 1.2em; + padding: 5%; + white-space: normal; + cursor: inherit; + transition: background-color 0.2s ease-in-out; + } + + &:hover .overlay { + background-color: transparent; + } + + @media (max-width: 600px) { + aspect-ratio: 3/4; + height: $mobHeight; .overlay { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - background-color: rgba(0, 0, 0, 0.2); - border-radius: 10px; - display: flex; - align-items: end; - justify-content: center; - color: white; - font-size: 1.2em; - padding: 5%; - white-space: normal; - cursor: inherit; - transition: background-color 0.2s ease-in-out; - } - - &:hover .overlay { - background-color: transparent; - } - - @media (max-width: 600px) { - aspect-ratio: 3/4; - height: $mobHeight; - .overlay { font-size: 1.1em; } + font-size: 1.1em; } + } } \ No newline at end of file diff --git a/src/components/top-matter/TagTopMatter.vue b/src/components/top-matter/TagTopMatter.vue index 4cec0900..25fb9d2e 100644 --- a/src/components/top-matter/TagTopMatter.vue +++ b/src/components/top-matter/TagTopMatter.vue @@ -1,63 +1,63 @@ \ No newline at end of file diff --git a/src/components/top-matter/TopMatter.vue b/src/components/top-matter/TopMatter.vue index 2ef3a88c..ebd33ff5 100644 --- a/src/components/top-matter/TopMatter.vue +++ b/src/components/top-matter/TopMatter.vue @@ -1,53 +1,62 @@ \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index 091ad466..ce81067f 100644 --- a/src/main.ts +++ b/src/main.ts @@ -19,30 +19,34 @@ * along with this program. If not, see . * */ -import 'reflect-metadata' -import Vue from 'vue' -import VueVirtualScroller from 'vue-virtual-scroller' -import 'vue-virtual-scroller/dist/vue-virtual-scroller.css' +import "reflect-metadata"; +import Vue from "vue"; +import VueVirtualScroller from "vue-virtual-scroller"; +import "vue-virtual-scroller/dist/vue-virtual-scroller.css"; -import App from './App.vue' -import router from './router' +import App from "./App.vue"; +import router from "./router"; -Vue.use(VueVirtualScroller) +Vue.use(VueVirtualScroller); // https://github.com/nextcloud/photos/blob/156f280c0476c483cb9ce81769ccb0c1c6500a4e/src/main.js // TODO: remove when we have a proper fileinfo standalone library // original scripts are loaded from // https://github.com/nextcloud/server/blob/5bf3d1bb384da56adbf205752be8f840aac3b0c5/lib/private/legacy/template.php#L120-L122 -window.addEventListener('DOMContentLoaded', () => { - if (!globalThis.OCA.Files) { - globalThis.OCA.Files = {} - } - // register unused client for the sidebar to have access to its parser methods - Object.assign(globalThis.OCA.Files, { App: { fileList: { filesClient: globalThis.OC.Files.getClient() } } }, globalThis.OCA.Files) -}) +window.addEventListener("DOMContentLoaded", () => { + if (!globalThis.OCA.Files) { + globalThis.OCA.Files = {}; + } + // register unused client for the sidebar to have access to its parser methods + Object.assign( + globalThis.OCA.Files, + { App: { fileList: { filesClient: globalThis.OC.Files.getClient() } } }, + globalThis.OCA.Files + ); +}); export default new Vue({ - el: '#content', - router, - render: h => h(App), -}) + el: "#content", + router, + render: (h) => h(App), +}); diff --git a/src/mixins/GlobalMixin.ts b/src/mixins/GlobalMixin.ts index 99733cb3..6aeb3977 100644 --- a/src/mixins/GlobalMixin.ts +++ b/src/mixins/GlobalMixin.ts @@ -1,13 +1,13 @@ -import { Component, Vue } from 'vue-property-decorator'; -import { translate as t, translatePlural as n } from '@nextcloud/l10n' -import { constants } from '../services/Utils'; +import { Component, Vue } from "vue-property-decorator"; +import { translate as t, translatePlural as n } from "@nextcloud/l10n"; +import { constants } from "../services/Utils"; @Component export default class GlobalMixin extends Vue { - public readonly t = t; - public readonly n = n; + public readonly t = t; + public readonly n = n; - public readonly c = constants.c; - public readonly TagDayID = constants.TagDayID; - public readonly TagDayIDValueSet = constants.TagDayIDValueSet; -} \ No newline at end of file + public readonly c = constants.c; + public readonly TagDayID = constants.TagDayID; + public readonly TagDayIDValueSet = constants.TagDayIDValueSet; +} diff --git a/src/mixins/UserConfig.ts b/src/mixins/UserConfig.ts index 1cfb74b1..f920168d 100644 --- a/src/mixins/UserConfig.ts +++ b/src/mixins/UserConfig.ts @@ -20,60 +20,60 @@ * */ -import { Component, Vue } from 'vue-property-decorator'; -import { emit, subscribe, unsubscribe } from '@nextcloud/event-bus' -import { generateUrl } from '@nextcloud/router' -import { loadState } from '@nextcloud/initial-state' -import axios from '@nextcloud/axios' +import { Component, Vue } from "vue-property-decorator"; +import { emit, subscribe, unsubscribe } from "@nextcloud/event-bus"; +import { generateUrl } from "@nextcloud/router"; +import { loadState } from "@nextcloud/initial-state"; +import axios from "@nextcloud/axios"; -const eventName = 'memories:user-config-changed' -const localSettings = ['squareThumbs', 'showFaceRect']; +const eventName = "memories:user-config-changed"; +const localSettings = ["squareThumbs", "showFaceRect"]; @Component export default class UserConfig extends Vue { - config_timelinePath: string = loadState('memories', 'timelinePath') || ''; - config_foldersPath: string = loadState('memories', 'foldersPath') || '/'; - config_showHidden = loadState('memories', 'showHidden') === "true"; + config_timelinePath: string = loadState("memories", "timelinePath") || ""; + config_foldersPath: string = loadState("memories", "foldersPath") || "/"; + config_showHidden = loadState("memories", "showHidden") === "true"; - config_tagsEnabled = Boolean(loadState('memories', 'systemtags')); - config_recognizeEnabled = Boolean(loadState('memories', 'recognize')); - config_mapsEnabled = Boolean(loadState('memories', 'maps')); - config_albumsEnabled = Boolean(loadState('memories', 'albums')); + config_tagsEnabled = Boolean(loadState("memories", "systemtags")); + config_recognizeEnabled = Boolean(loadState("memories", "recognize")); + config_mapsEnabled = Boolean(loadState("memories", "maps")); + config_albumsEnabled = Boolean(loadState("memories", "albums")); - config_squareThumbs = localStorage.getItem('memories_squareThumbs') === '1'; - config_showFaceRect = localStorage.getItem('memories_showFaceRect') === '1'; + config_squareThumbs = localStorage.getItem("memories_squareThumbs") === "1"; + config_showFaceRect = localStorage.getItem("memories_showFaceRect") === "1"; - config_eventName = eventName; + config_eventName = eventName; - created() { - subscribe(eventName, this.updateLocalSetting) + created() { + subscribe(eventName, this.updateLocalSetting); + } + + beforeDestroy() { + unsubscribe(eventName, this.updateLocalSetting); + } + + updateLocalSetting({ setting, value }) { + this["config_" + setting] = value; + } + + async updateSetting(setting: string) { + const value = this["config_" + setting]; + + if (localSettings.includes(setting)) { + if (typeof value === "boolean") { + localStorage.setItem("memories_" + setting, value ? "1" : "0"); + } else { + localStorage.setItem("memories_" + setting, value); + } + } else { + // Long time save setting + await axios.put(generateUrl("apps/memories/api/config/" + setting), { + value: value.toString(), + }); } - beforeDestroy() { - unsubscribe(eventName, this.updateLocalSetting) - } - - updateLocalSetting({ setting, value }) { - this['config_' + setting] = value - } - - async updateSetting(setting: string) { - const value = this['config_' + setting] - - if (localSettings.includes(setting)) { - if (typeof value === 'boolean') { - localStorage.setItem('memories_' + setting, value ? '1' : '0') - } else { - localStorage.setItem('memories_' + setting, value) - } - } else { - // Long time save setting - await axios.put(generateUrl('apps/memories/api/config/' + setting), { - value: value.toString(), - }); - } - - // Visible elements update setting - emit(eventName, { setting, value }); - } -} \ No newline at end of file + // Visible elements update setting + emit(eventName, { setting, value }); + } +} diff --git a/src/router.ts b/src/router.ts index 6409e5ad..192408ed 100644 --- a/src/router.ts +++ b/src/router.ts @@ -20,120 +20,120 @@ * */ - import { generateUrl } from '@nextcloud/router' - import { translate as t, translatePlural as n } from '@nextcloud/l10n' - import Router from 'vue-router' - import Vue from 'vue' - import Timeline from './components/Timeline.vue'; +import { generateUrl } from "@nextcloud/router"; +import { translate as t, translatePlural as n } from "@nextcloud/l10n"; +import Router from "vue-router"; +import Vue from "vue"; +import Timeline from "./components/Timeline.vue"; - Vue.use(Router) +Vue.use(Router); - /** - * Parse the path of a route : join the elements of the array and return a single string with slashes - * + always lead current path with a slash - * - * @param {string | Array} path path arguments to parse - * @return {string} - */ - const parsePathParams = (path) => { - return `/${Array.isArray(path) ? path.join('/') : path || ''}` - } +/** + * Parse the path of a route : join the elements of the array and return a single string with slashes + * + always lead current path with a slash + * + * @param {string | Array} path path arguments to parse + * @return {string} + */ +const parsePathParams = (path) => { + return `/${Array.isArray(path) ? path.join("/") : path || ""}`; +}; - export default new Router({ - 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', - routes: [ - { - path: '/', - component: Timeline, - name: 'timeline', - props: route => ({ - rootTitle: t('memories', 'Timeline'), - }), - }, +export default new Router({ + 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", + routes: [ + { + path: "/", + component: Timeline, + name: "timeline", + props: (route) => ({ + rootTitle: t("memories", "Timeline"), + }), + }, - { - path: '/folders/:path*', - component: Timeline, - name: 'folders', - props: route => ({ - rootTitle: t('memories', 'Folders'), - }), - }, + { + path: "/folders/:path*", + component: Timeline, + name: "folders", + props: (route) => ({ + rootTitle: t("memories", "Folders"), + }), + }, - { - path: '/favorites', - component: Timeline, - name: 'favorites', - props: route => ({ - rootTitle: t('memories', 'Favorites'), - }), - }, + { + path: "/favorites", + component: Timeline, + name: "favorites", + props: (route) => ({ + rootTitle: t("memories", "Favorites"), + }), + }, - { - path: '/videos', - component: Timeline, - name: 'videos', - props: route => ({ - rootTitle: t('memories', 'Videos'), - }), - }, + { + path: "/videos", + component: Timeline, + name: "videos", + props: (route) => ({ + rootTitle: t("memories", "Videos"), + }), + }, - { - path: '/albums/:user?/:name?', - component: Timeline, - name: 'albums', - props: route => ({ - rootTitle: t('memories', 'Albums'), - }), - }, + { + path: "/albums/:user?/:name?", + component: Timeline, + name: "albums", + props: (route) => ({ + rootTitle: t("memories", "Albums"), + }), + }, - { - path: '/archive', - component: Timeline, - name: 'archive', - props: route => ({ - rootTitle: t('memories', 'Archive'), - }), - }, + { + path: "/archive", + component: Timeline, + name: "archive", + props: (route) => ({ + rootTitle: t("memories", "Archive"), + }), + }, - { - path: '/thisday', - component: Timeline, - name: 'thisday', - props: route => ({ - rootTitle: t('memories', 'On this day'), - }), - }, + { + path: "/thisday", + component: Timeline, + name: "thisday", + props: (route) => ({ + rootTitle: t("memories", "On this day"), + }), + }, - { - path: '/people/:user?/:name?', - component: Timeline, - name: 'people', - props: route => ({ - rootTitle: t('memories', 'People'), - }), - }, + { + path: "/people/:user?/:name?", + component: Timeline, + name: "people", + props: (route) => ({ + rootTitle: t("memories", "People"), + }), + }, - { - path: '/tags/:name*', - component: Timeline, - name: 'tags', - props: route => ({ - rootTitle: t('memories', 'Tags'), - }), - }, + { + path: "/tags/:name*", + component: Timeline, + name: "tags", + props: (route) => ({ + rootTitle: t("memories", "Tags"), + }), + }, - { - path: '/maps', - name: 'maps', - // router-link doesn't support external url, let's force the redirect - beforeEnter() { - window.open(generateUrl('/apps/maps'), '_blank') - }, - }, - ], - }) \ No newline at end of file + { + path: "/maps", + name: "maps", + // router-link doesn't support external url, let's force the redirect + beforeEnter() { + window.open(generateUrl("/apps/maps"), "_blank"); + }, + }, + ], +}); diff --git a/src/services/DavClient.ts b/src/services/DavClient.ts index 37577bcc..e3769a6a 100644 --- a/src/services/DavClient.ts +++ b/src/services/DavClient.ts @@ -20,26 +20,30 @@ * */ - 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'; - (rq).prepareRequestOptionsOld = rq.prepareRequestOptions.bind(rq); - (rq).prepareRequestOptions = (function(requestOptions, context, userOptions) { - requestOptions.method = userOptions.method || requestOptions.method; - return this.prepareRequestOptionsOld(requestOptions, context, userOptions); - }).bind(rq); +// Monkey business +import * as rq from "webdav/dist/node/request"; +(rq).prepareRequestOptionsOld = rq.prepareRequestOptions.bind(rq); +(rq).prepareRequestOptions = function ( + requestOptions, + context, + userOptions +) { + requestOptions.method = userOptions.method || requestOptions.method; + return this.prepareRequestOptionsOld(requestOptions, context, userOptions); +}.bind(rq); - // force our axios - const patcher = webdav.getPatcher() - patcher.patch('request', axios) +// force our axios +const patcher = webdav.getPatcher(); +patcher.patch("request", axios); - // init webdav client on default dav endpoint - const remote = generateRemoteUrl('dav') - const client = webdav.createClient(remote) +// init webdav client on default dav endpoint +const remote = generateRemoteUrl("dav"); +const client = webdav.createClient(remote); - export const remotePath = parseUrl(remote).pathname - export default client \ No newline at end of file +export const remotePath = parseUrl(remote).pathname; +export default client; diff --git a/src/services/DavRequests.ts b/src/services/DavRequests.ts index 2b14c9f1..00332907 100644 --- a/src/services/DavRequests.ts +++ b/src/services/DavRequests.ts @@ -1,9 +1,9 @@ -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'; \ No newline at end of file +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"; diff --git a/src/services/FileUtils.ts b/src/services/FileUtils.ts index 16572a24..8c76884d 100644 --- a/src/services/FileUtils.ts +++ b/src/services/FileUtils.ts @@ -19,113 +19,136 @@ * along with this program. If not, see . * */ - import camelcase from 'camelcase' - import { isNumber } from './NumberUtils' - import { generateUrl } from '@nextcloud/router' +import camelcase from "camelcase"; +import { isNumber } from "./NumberUtils"; +import { generateUrl } from "@nextcloud/router"; - /** - * Get an url encoded path - * - * @param {string} path the full path - * @return {string} url encoded file path - */ - const encodeFilePath = function(path) { - const pathSections = (path.startsWith('/') ? path : `/${path}`).split('/') - let relativePath = '' - pathSections.forEach((section) => { - if (section !== '') { - relativePath += '/' + encodeURIComponent(section) - } - }) - return relativePath - } +/** + * Get an url encoded path + * + * @param {string} path the full path + * @return {string} url encoded file path + */ +const encodeFilePath = function (path) { + const pathSections = (path.startsWith("/") ? path : `/${path}`).split("/"); + let relativePath = ""; + pathSections.forEach((section) => { + if (section !== "") { + relativePath += "/" + encodeURIComponent(section); + } + }); + return relativePath; +}; - /** - * Extract dir and name from file path - * - * @param {string} path the full path - * @return {string[]} [dirPath, fileName] - */ - const extractFilePaths = function(path) { - const pathSections = path.split('/') - const fileName = pathSections[pathSections.length - 1] - const dirPath = pathSections.slice(0, pathSections.length - 1).join('/') - return [dirPath, fileName] - } +/** + * Extract dir and name from file path + * + * @param {string} path the full path + * @return {string[]} [dirPath, fileName] + */ +const extractFilePaths = function (path) { + const pathSections = path.split("/"); + const fileName = pathSections[pathSections.length - 1]; + const dirPath = pathSections.slice(0, pathSections.length - 1).join("/"); + return [dirPath, fileName]; +}; - /** - * Sorting comparison function - * - * @param {object} fileInfo1 file 1 fileinfo - * @param {object} fileInfo2 file 2 fileinfo - * @param {string} key key to sort with - * @param {boolean} [asc=true] sort ascending? - * @return {number} - */ - const sortCompare = function(fileInfo1, fileInfo2, key, asc = true) { +/** + * Sorting comparison function + * + * @param {object} fileInfo1 file 1 fileinfo + * @param {object} fileInfo2 file 2 fileinfo + * @param {string} key key to sort with + * @param {boolean} [asc=true] sort ascending? + * @return {number} + */ +const sortCompare = function (fileInfo1, fileInfo2, key, asc = true) { + // favorite always first + if (fileInfo1.isFavorite && !fileInfo2.isFavorite) { + return -1; + } else if (!fileInfo1.isFavorite && fileInfo2.isFavorite) { + return 1; + } - // favorite always first - if (fileInfo1.isFavorite && !fileInfo2.isFavorite) { - return -1 - } else if (!fileInfo1.isFavorite && fileInfo2.isFavorite) { - return 1 - } + // 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]); + } - // 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]) - } + // else we sort by string, so let's sort directories first + if (fileInfo1.type !== "file" && fileInfo2.type === "file") { + return asc ? -1 : 1; + } else if (fileInfo1.type === "file" && fileInfo2.type !== "file") { + return asc ? 1 : -1; + } - // else we sort by string, so let's sort directories first - if (fileInfo1.type !== 'file' && fileInfo2.type === 'file') { - return asc ? -1 : 1 - } 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()) + ) { + return asc + ? new Date(fileInfo2[key]).getTime() - new Date(fileInfo1[key]).getTime() + : new Date(fileInfo1[key]).getTime() - new Date(fileInfo2[key]).getTime(); + } - // if this is a date, let's sort by date - 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() - } + // 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; +}; - // 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 - } +const genFileInfo = function (obj) { + const fileInfo = {}; - const genFileInfo = function(obj) { - const fileInfo = {} + Object.keys(obj).forEach((key) => { + const data = obj[key]; - Object.keys(obj).forEach(key => { - const data = obj[key] + // flatten object if any + if (!!data && typeof data === "object") { + Object.assign(fileInfo, genFileInfo(data)); + } else { + // format key and add it to the fileInfo + if (data === "false") { + fileInfo[camelcase(key)] = false; + } else if (data === "true") { + fileInfo[camelcase(key)] = true; + } else { + fileInfo[camelcase(key)] = isNumber(data) ? Number(data) : data; + } + } + }); + return fileInfo; +}; - // flatten object if any - if (!!data && typeof data === 'object') { - Object.assign(fileInfo, genFileInfo(data)) - } else { - // format key and add it to the fileInfo - if (data === 'false') { - fileInfo[camelcase(key)] = false - } else if (data === 'true') { - fileInfo[camelcase(key)] = true - } else { - fileInfo[camelcase(key)] = isNumber(data) - ? Number(data) - : data - } - } - }) - return fileInfo - } +const getPreviewUrl = function ( + fileid: number, + etag: string, + square: boolean, + size: number +): string { + const a = square ? "0" : "1"; + return generateUrl( + `/core/preview?fileId=${fileid}&c=${etag}&x=${size}&y=${size}&forceIcon=0&a=${a}` + ); +}; - const getPreviewUrl = function(fileid: number, etag: string, square: boolean, size: number): string { - const a = square ? '0' : '1' - return generateUrl(`/core/preview?fileId=${fileid}&c=${etag}&x=${size}&y=${size}&forceIcon=0&a=${a}`); - } - - export { encodeFilePath, extractFilePaths, sortCompare, genFileInfo, getPreviewUrl } \ No newline at end of file +export { + encodeFilePath, + extractFilePaths, + sortCompare, + genFileInfo, + getPreviewUrl, +}; diff --git a/src/services/Layout.ts b/src/services/Layout.ts index 1ec284ed..edc42063 100644 --- a/src/services/Layout.ts +++ b/src/services/Layout.ts @@ -7,221 +7,236 @@ import justifiedLayout from "justified-layout"; * Otherwise, use flickr/justified-layout (at least for now). */ export function getLayout( - input: { - width: number, - height: number, - forceSquare: boolean, - }[], - opts: { - rowWidth: number, - rowHeight: number, - squareMode: boolean, - numCols: number, - allowBreakout: boolean, - seed: number, - } + input: { + width: number; + height: number; + forceSquare: boolean; + }[], + opts: { + rowWidth: number; + rowHeight: number; + squareMode: boolean; + numCols: number; + allowBreakout: boolean; + seed: number; + } ): { - top: number, - left: number, - width: number, - height: number, - rowHeight?: number, + top: number; + left: number; + width: number; + height: number; + rowHeight?: number; }[] { - if (input.length === 0) return []; + if (input.length === 0) return []; - if (!opts.squareMode) { - return justifiedLayout((input), { - containerPadding: 0, - boxSpacing: 0, - containerWidth: opts.rowWidth, - targetRowHeight: opts.rowHeight, - targetRowHeightTolerance: 0.1, - }).boxes; + if (!opts.squareMode) { + return justifiedLayout(input, { + containerPadding: 0, + boxSpacing: 0, + containerWidth: opts.rowWidth, + targetRowHeight: opts.rowHeight, + targetRowHeightTolerance: 0.1, + }).boxes; + } + + // RNG + const rand = mulberry32(opts.seed); + + // Binary flags + const FLAG_USE = 1 << 0; + const FLAG_USED = 1 << 1; + const FLAG_USE4 = 1 << 2; + const FLAG_USE6 = 1 << 3; + const FLAG_BREAKOUT = 1 << 4; + + // Create 2d matrix to work in + const matrix: number[][] = []; + + // Fill in the matrix + let row = 0; + let col = 0; + let photoId = 0; + while (photoId < input.length) { + // Check if we reached the end of row + if (col >= opts.numCols) { + row++; + col = 0; } - // RNG - const rand = mulberry32(opts.seed); - - // Binary flags - const FLAG_USE = 1 << 0; - const FLAG_USED = 1 << 1; - const FLAG_USE4 = 1 << 2; - const FLAG_USE6 = 1 << 3; - const FLAG_BREAKOUT = 1 << 4; - - // Create 2d matrix to work in - const matrix: number[][] = []; - - // Fill in the matrix - let row = 0; - let col = 0; - let photoId = 0; - while (photoId < input.length) { - // Check if we reached the end of row - if (col >= opts.numCols) { - row++; col = 0; - } - - // Make sure we have this and the next few rows - while (row + 3 >= matrix.length) { - matrix.push(new Array(opts.numCols).fill(0)); - } - - // Check if already used - if (matrix[row][col] & FLAG_USED) { - col++; continue; - } - - // Use this slot - matrix[row][col] |= FLAG_USE; - - // Check if previous row has something used - // or something beside this is used - // We don't do these one after another - if (!opts.allowBreakout || - (row > 0 && matrix[row-1].some(v => v & FLAG_USED)) || - (col > 0 && matrix[row][col-1] & FLAG_USED) - ) { - photoId++; col++; continue; - } - - // 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)); - - let canUse4 = - // We have enough space - (row + 1 < matrix.length && col+1 < opts.numCols) && - // This cannot end up being a widow (conservative) - // Also make sure the next row gets fully filled, otherwise looks weird - (numLeft === needFill(4) || numLeft >= needFill(4)+opts.numCols); - - let canUse6 = - // Image is portrait - input[photoId].height > input[photoId].width && - // We have enough space - (row + 2 < matrix.length && col+1 < opts.numCols) && - // This cannot end up being a widow (conservative) - // Also make sure the next row gets fully filled, otherwise looks weird - (numLeft === needFill(6) || numLeft >= needFill(6)+2*opts.numCols); - - let canBreakout = - // First column only - col === 0 && - // Image is landscape - input[photoId].width > input[photoId].height && - // The next row gets filled - (numLeft === 0 || numLeft >= opts.numCols); - - // Probably folders or tags or faces - if (input[photoId].forceSquare) { - // We are square already. Everything below is else-if. - } - - // Full width breakout - else if (canBreakout && rand() < (input.length > 0 ? 0.25 : 0.1)) { - matrix[row][col] |= FLAG_BREAKOUT; - for (let i = 1; i < opts.numCols; i++) { - matrix[row][i] |= FLAG_USED; - } - } - - // Use 6 vertically - else if (canUse6 && rand() < 0.2) { - matrix[row][col] |= FLAG_USE6; - matrix[row+1][col] |= FLAG_USED; - matrix[row+2][col] |= FLAG_USED; - matrix[row][col+1] |= FLAG_USED; - matrix[row+1][col+1] |= FLAG_USED; - matrix[row+2][col+1] |= FLAG_USED; - } - - // Use 4 box - else if (canUse4 && rand() < 0.35) { - matrix[row][col] |= FLAG_USE4; - matrix[row+1][col] |= FLAG_USED; - matrix[row][col+1] |= FLAG_USED; - matrix[row+1][col+1] |= FLAG_USED; - } - - // Go ahead - photoId++; col++; + // Make sure we have this and the next few rows + while (row + 3 >= matrix.length) { + matrix.push(new Array(opts.numCols).fill(0)); } - // Square layout matrix - const absMatrix: { - top: number, - left: number, - width: number, - height: number, - }[] = []; - - let currTop = 0; - row = 0; col = 0; photoId = 0; - while (photoId < input.length) { - // Check if we reached the end of row - if (col >= opts.numCols) { - row++; col = 0; - currTop += opts.rowHeight; - continue; - } - - // Skip if used - if (!(matrix[row][col] & FLAG_USE)) { - col++; continue; - } - - // Create basic object - const sqsize = opts.rowHeight; - const p = { - top: currTop, - left: col * sqsize, - width: sqsize, - height: sqsize, - rowHeight: opts.rowHeight, - } - - // Use twice the space - const v = matrix[row][col]; - if (v & FLAG_USE4) { - p.width *= 2; - p.height *= 2; - col += 2; - } else if (v & FLAG_USE6) { - p.width *= 2; - p.height *= 3; - col += 2; - } else if (v & FLAG_BREAKOUT) { - p.width *= opts.numCols; - p.height = input[photoId].height * p.width / input[photoId].width; - p.rowHeight = p.height; - col += opts.numCols; - } else { - col++; - } - - absMatrix.push(p); - photoId++; + // Check if already used + if (matrix[row][col] & FLAG_USED) { + col++; + continue; } - return absMatrix; + // Use this slot + matrix[row][col] |= FLAG_USE; + + // Check if previous row has something used + // or something beside this is used + // We don't do these one after another + if ( + !opts.allowBreakout || + (row > 0 && matrix[row - 1].some((v) => v & FLAG_USED)) || + (col > 0 && matrix[row][col - 1] & FLAG_USED) + ) { + photoId++; + col++; + continue; + } + + // 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); + + let canUse4 = + // We have enough space + row + 1 < matrix.length && + col + 1 < opts.numCols && + // This cannot end up being a widow (conservative) + // Also make sure the next row gets fully filled, otherwise looks weird + (numLeft === needFill(4) || numLeft >= needFill(4) + opts.numCols); + + let canUse6 = + // Image is portrait + input[photoId].height > input[photoId].width && + // We have enough space + row + 2 < matrix.length && + col + 1 < opts.numCols && + // This cannot end up being a widow (conservative) + // Also make sure the next row gets fully filled, otherwise looks weird + (numLeft === needFill(6) || numLeft >= needFill(6) + 2 * opts.numCols); + + let canBreakout = + // First column only + col === 0 && + // Image is landscape + input[photoId].width > input[photoId].height && + // The next row gets filled + (numLeft === 0 || numLeft >= opts.numCols); + + // Probably folders or tags or faces + if (input[photoId].forceSquare) { + // We are square already. Everything below is else-if. + } + + // Full width breakout + else if (canBreakout && rand() < (input.length > 0 ? 0.25 : 0.1)) { + matrix[row][col] |= FLAG_BREAKOUT; + for (let i = 1; i < opts.numCols; i++) { + matrix[row][i] |= FLAG_USED; + } + } + + // Use 6 vertically + else if (canUse6 && rand() < 0.2) { + matrix[row][col] |= FLAG_USE6; + matrix[row + 1][col] |= FLAG_USED; + matrix[row + 2][col] |= FLAG_USED; + matrix[row][col + 1] |= FLAG_USED; + matrix[row + 1][col + 1] |= FLAG_USED; + matrix[row + 2][col + 1] |= FLAG_USED; + } + + // Use 4 box + else if (canUse4 && rand() < 0.35) { + matrix[row][col] |= FLAG_USE4; + matrix[row + 1][col] |= FLAG_USED; + matrix[row][col + 1] |= FLAG_USED; + matrix[row + 1][col + 1] |= FLAG_USED; + } + + // Go ahead + photoId++; + col++; + } + + // Square layout matrix + const absMatrix: { + top: number; + left: number; + width: number; + height: number; + }[] = []; + + let currTop = 0; + row = 0; + col = 0; + photoId = 0; + while (photoId < input.length) { + // Check if we reached the end of row + if (col >= opts.numCols) { + row++; + col = 0; + currTop += opts.rowHeight; + continue; + } + + // Skip if used + if (!(matrix[row][col] & FLAG_USE)) { + col++; + continue; + } + + // Create basic object + const sqsize = opts.rowHeight; + const p = { + top: currTop, + left: col * sqsize, + width: sqsize, + height: sqsize, + rowHeight: opts.rowHeight, + }; + + // Use twice the space + const v = matrix[row][col]; + if (v & FLAG_USE4) { + p.width *= 2; + p.height *= 2; + col += 2; + } else if (v & FLAG_USE6) { + p.width *= 2; + p.height *= 3; + col += 2; + } else if (v & FLAG_BREAKOUT) { + p.width *= opts.numCols; + p.height = (input[photoId].height * p.width) / input[photoId].width; + p.rowHeight = p.height; + col += opts.numCols; + } else { + col++; + } + + absMatrix.push(p); + photoId++; + } + + return absMatrix; } function flagMatrixStr(matrix: number[][], numFlag: number) { - 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'; - } - return 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"; + } + return str; } function mulberry32(a: number) { - return function() { - var t = a += 0x6D2B79F5; - t = Math.imul(t ^ t >>> 15, t | 1); - t ^= t + Math.imul(t ^ t >>> 7, t | 61); - return ((t ^ t >>> 14) >>> 0) / 4294967296; - } -} \ No newline at end of file + return function () { + var t = (a += 0x6d2b79f5); + t = Math.imul(t ^ (t >>> 15), t | 1); + t ^= t + Math.imul(t ^ (t >>> 7), t | 61); + return ((t ^ (t >>> 14)) >>> 0) / 4294967296; + }; +} diff --git a/src/services/NumberUtils.ts b/src/services/NumberUtils.ts index 56e90729..2a9d10b9 100644 --- a/src/services/NumberUtils.ts +++ b/src/services/NumberUtils.ts @@ -20,11 +20,11 @@ * */ - const isNumber = function(num: any) { - if (!num) { - return false - } - return Number(num).toString() === num.toString() -} +const isNumber = function (num: any) { + if (!num) { + return false; + } + return Number(num).toString() === num.toString(); +}; -export { isNumber } \ No newline at end of file +export { isNumber }; diff --git a/src/services/Utils.ts b/src/services/Utils.ts index 222dc8b6..c59a8c04 100644 --- a/src/services/Utils.ts +++ b/src/services/Utils.ts @@ -1,8 +1,8 @@ import { getCanonicalLocale } from "@nextcloud/l10n"; -import { getCurrentUser } from '@nextcloud/auth' -import { loadState } from '@nextcloud/initial-state' +import { getCurrentUser } from "@nextcloud/auth"; +import { loadState } from "@nextcloud/initial-state"; import { IPhoto } from "../types"; -import moment from 'moment'; +import moment from "moment"; // Memoize the result of short date conversions // These operations are surprisingly expensive @@ -10,49 +10,54 @@ import moment from 'moment'; const shortDateStrMemo = new Map(); /** Get JS date object from dayId */ -export function dayIdToDate(dayId: number){ - return new Date(dayId*86400*1000); +export function dayIdToDate(dayId: number) { + return new Date(dayId * 86400 * 1000); } /** Get Day ID from JS date */ -export function dateToDayId(date: Date){ - return Math.floor(date.getTime() / (86400*1000)); +export function dateToDayId(date: Date) { + return Math.floor(date.getTime() / (86400 * 1000)); } /** Get month name from number */ export function getShortDateStr(date: Date) { - const dayId = dateToDayId(date); - if (!shortDateStrMemo.has(dayId)) { - shortDateStrMemo.set(dayId, - date.toLocaleDateString(getCanonicalLocale(), { - month: 'short', - year: 'numeric', - timeZone: 'UTC', - })); - } - return shortDateStrMemo.get(dayId); + const dayId = dateToDayId(date); + if (!shortDateStrMemo.has(dayId)) { + shortDateStrMemo.set( + dayId, + date.toLocaleDateString(getCanonicalLocale(), { + month: "short", + year: "numeric", + timeZone: "UTC", + }) + ); + } + return shortDateStrMemo.get(dayId); } /** 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, - }); +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, + }); } /** Get text like "5 years ago" from a date */ export function getFromNowStr(date: Date) { - // Get fromNow in correct locale - const text = moment(date).locale(getCanonicalLocale()).fromNow(); + // Get fromNow in correct locale + const text = moment(date).locale(getCanonicalLocale()).fromNow(); - // Title case - return text.charAt(0).toUpperCase() + text.slice(1); + // Title case + return text.charAt(0).toUpperCase() + text.slice(1); } /** @@ -62,13 +67,13 @@ export function getFromNowStr(date: Date) { * @see http://werxltd.com/wp/2010/05/13/javascript-implementation-of-javas-string-hashcode-method/ */ export function hashCode(str: string): number { - let hash = 0; - for (let i = 0, len = str.length; i < len; i++) { - let chr = str.charCodeAt(i); - hash = (hash << 5) - hash + chr; - hash |= 0; // Convert to 32bit integer - } - return hash; + let hash = 0; + for (let i = 0, len = str.length; i < len; i++) { + let chr = str.charCodeAt(i); + hash = (hash << 5) - hash + chr; + hash |= 0; // Convert to 32bit integer + } + return hash; } /** @@ -80,27 +85,25 @@ export function hashCode(str: string): number { * @param key Key to use for comparison */ export function binarySearch(arr: any, elem: any, key?: string) { - let minIndex = 0; - let maxIndex = arr.length - 1; - let currentIndex: number; - let currentElement: any; + let minIndex = 0; + let maxIndex = arr.length - 1; + let currentIndex: number; + let currentElement: any; - while (minIndex <= maxIndex) { - currentIndex = (minIndex + maxIndex) / 2 | 0; - currentElement = key ? arr[currentIndex][key] : arr[currentIndex]; + while (minIndex <= maxIndex) { + currentIndex = ((minIndex + maxIndex) / 2) | 0; + currentElement = key ? arr[currentIndex][key] : arr[currentIndex]; - if (currentElement < elem) { - minIndex = currentIndex + 1; - } - else if (currentElement > elem) { - maxIndex = currentIndex - 1; - } - else { - return currentIndex; - } + if (currentElement < elem) { + minIndex = currentIndex + 1; + } else if (currentElement > elem) { + maxIndex = currentIndex - 1; + } else { + return currentIndex; } + } - return minIndex; + return minIndex; } /** @@ -109,10 +112,10 @@ export function binarySearch(arr: any, elem: any, key?: string) { * @param places Number of decimal places * @param floor If true, round down instead of to nearest */ -export function round(num: number, places: number, floor=false) { - const pow = Math.pow(10, places); - const int = num * pow; - return (floor ? Math.floor : Math.round)(int) / pow; +export function round(num: number, places: number, floor = false) { + const pow = Math.pow(10, places); + const int = num * pow; + return (floor ? Math.floor : Math.round)(int) / pow; } /** @@ -120,12 +123,12 @@ export function round(num: number, places: number, floor=false) { * @param num Number to round */ export function roundHalf(num: number) { - return Math.round(num * 2) / 2; + return Math.round(num * 2) / 2; } /** Choose a random element from an array */ export function randomChoice(arr: any[]) { - return arr[Math.floor(Math.random() * arr.length)]; + return arr[Math.floor(Math.random() * arr.length)]; } /** @@ -133,15 +136,19 @@ export function randomChoice(arr: any[]) { * https://stackoverflow.com/a/11935263/4745239 */ export function randomSubarray(arr: any[], size: number) { - if (arr.length <= size) return arr; - var shuffled = arr.slice(0), i = arr.length, min = i - size, temp, index; - while (i-- > min) { - index = Math.floor((i + 1) * Math.random()); - temp = shuffled[index]; - shuffled[index] = shuffled[i]; - shuffled[i] = temp; - } - return shuffled.slice(min); + if (arr.length <= size) return arr; + var shuffled = arr.slice(0), + i = arr.length, + min = i - size, + temp, + index; + while (i-- > min) { + index = Math.floor((i + 1) * Math.random()); + temp = shuffled[index]; + shuffled[index] = shuffled[i]; + shuffled[i] = temp; + } + return shuffled.slice(min); } /** @@ -149,108 +156,114 @@ export function randomSubarray(arr: any[], size: number) { * @param photo Photo to process */ export function convertFlags(photo: IPhoto) { - if (typeof photo.flag === "undefined") { - photo.flag = 0; // flags - } + if (typeof photo.flag === "undefined") { + photo.flag = 0; // flags + } - if (photo.isvideo) { - photo.flag |= constants.c.FLAG_IS_VIDEO; - delete photo.isvideo; - } - if (photo.isfavorite) { - photo.flag |= constants.c.FLAG_IS_FAVORITE; - delete photo.isfavorite; - } - if (photo.isfolder) { - photo.flag |= constants.c.FLAG_IS_FOLDER; - delete photo.isfolder; - } - if (photo.isface) { - photo.flag |= constants.c.FLAG_IS_FACE; - delete photo.isface; - } - if (photo.istag) { - photo.flag |= constants.c.FLAG_IS_TAG; - delete photo.istag; - } - if (photo.isalbum) { - photo.flag |= constants.c.FLAG_IS_ALBUM; - delete photo.isalbum; - } + if (photo.isvideo) { + photo.flag |= constants.c.FLAG_IS_VIDEO; + delete photo.isvideo; + } + if (photo.isfavorite) { + photo.flag |= constants.c.FLAG_IS_FAVORITE; + delete photo.isfavorite; + } + if (photo.isfolder) { + photo.flag |= constants.c.FLAG_IS_FOLDER; + delete photo.isfolder; + } + if (photo.isface) { + photo.flag |= constants.c.FLAG_IS_FACE; + delete photo.isface; + } + if (photo.istag) { + photo.flag |= constants.c.FLAG_IS_TAG; + delete photo.istag; + } + if (photo.isalbum) { + photo.flag |= constants.c.FLAG_IS_ALBUM; + delete photo.isalbum; + } } // Outside for set const TagDayID = { - START: -(1 << 30), - FOLDERS: -(1 << 30) + 1, - TAGS: -(1 << 30) + 2, - FACES: -(1 << 30) + 3, - ALBUMS: -(1 << 30) + 4, -} + START: -(1 << 30), + FOLDERS: -(1 << 30) + 1, + TAGS: -(1 << 30) + 2, + FACES: -(1 << 30) + 3, + ALBUMS: -(1 << 30) + 4, +}; /** Global constants */ export const constants = { - c: { - FLAG_PLACEHOLDER: 1 << 0, - FLAG_LOAD_FAIL: 1 << 1, - FLAG_IS_VIDEO: 1 << 2, - FLAG_IS_FAVORITE: 1 << 3, - FLAG_IS_FOLDER: 1 << 4, - FLAG_IS_TAG: 1 << 5, - FLAG_IS_FACE: 1 << 6, - FLAG_IS_ALBUM: 1 << 7, - FLAG_SELECTED: 1 << 8, - FLAG_LEAVING: 1 << 9, - }, + c: { + FLAG_PLACEHOLDER: 1 << 0, + FLAG_LOAD_FAIL: 1 << 1, + FLAG_IS_VIDEO: 1 << 2, + FLAG_IS_FAVORITE: 1 << 3, + FLAG_IS_FOLDER: 1 << 4, + FLAG_IS_TAG: 1 << 5, + FLAG_IS_FACE: 1 << 6, + FLAG_IS_ALBUM: 1 << 7, + FLAG_SELECTED: 1 << 8, + FLAG_LEAVING: 1 << 9, + }, - TagDayID: TagDayID, - TagDayIDValueSet: new Set(Object.values(TagDayID)), -} + TagDayID: TagDayID, + TagDayIDValueSet: new Set(Object.values(TagDayID)), +}; /** Cache store */ let staticCache: Cache | null = null; -const cacheName = `memories-${loadState('memories', 'version')}-${getCurrentUser()!.uid}`; -openCache().then((cache) => { staticCache = cache }); +const cacheName = `memories-${loadState("memories", "version")}-${ + getCurrentUser()!.uid +}`; +openCache().then((cache) => { + staticCache = cache; +}); // Clear all caches except the current one window.caches?.keys().then((keys) => { - keys.filter((key) => key.startsWith('memories-') && key !== cacheName).forEach((key) => { - window.caches.delete(key); + keys + .filter((key) => key.startsWith("memories-") && key !== cacheName) + .forEach((key) => { + window.caches.delete(key); }); }); /** Open the cache */ export async function openCache() { - try { - return await window.caches?.open(cacheName); - } catch { - console.warn('Failed to get cache', cacheName); - return null; - } + try { + return await window.caches?.open(cacheName); + } catch { + console.warn("Failed to get cache", cacheName); + return null; + } } /** Get data from the cache */ export async function getCachedData(url: string): Promise { - if (!window.caches) return null; - const cache = staticCache || await openCache(); - if (!cache) return null; + if (!window.caches) return null; + const cache = staticCache || (await openCache()); + if (!cache) return null; - const cachedResponse = await cache.match(url); - if (!cachedResponse || !cachedResponse.ok) return undefined; - return await cachedResponse.json(); + const cachedResponse = await cache.match(url); + if (!cachedResponse || !cachedResponse.ok) return undefined; + return await cachedResponse.json(); } /** Store data in the cache */ export function cacheData(url: string, data: Object) { - if (!window.caches) return; - const str = JSON.stringify(data); + if (!window.caches) return; + const str = JSON.stringify(data); - (async () => { - const cache = staticCache || await openCache(); - if (!cache) return; + (async () => { + const cache = staticCache || (await openCache()); + if (!cache) return; - const response = new Response(str); - response.headers.set('Content-Type', 'application/json'); - await cache.put(url, response); - })(); -} \ No newline at end of file + const response = new Response(str); + response.headers.set("Content-Type", "application/json"); + await cache.put(url, response); + })(); +} diff --git a/src/services/Viewer.ts b/src/services/Viewer.ts index 699c9945..17323a09 100644 --- a/src/services/Viewer.ts +++ b/src/services/Viewer.ts @@ -1,87 +1,88 @@ import { IFileInfo, IPhoto } from "../types"; -import { showError } from '@nextcloud/dialogs' -import { subscribe } from '@nextcloud/event-bus'; -import { translate as t, translatePlural as n } from '@nextcloud/l10n' +import { showError } from "@nextcloud/dialogs"; +import { subscribe } from "@nextcloud/event-bus"; +import { translate as t, translatePlural as n } from "@nextcloud/l10n"; import * as dav from "./DavRequests"; // Key to store sidebar state -const SIDEBAR_KEY = 'memories:sidebar-open'; +const SIDEBAR_KEY = "memories:sidebar-open"; export class ViewerManager { - /** Map from fileid to Photo */ - private photoMap = new Map(); + /** Map from fileid to Photo */ + private photoMap = new Map(); - constructor( - ondelete: (photos: IPhoto[]) => void, - private updateLoading: (delta: number) => void, - ) { - subscribe('files:file:deleted', ({ fileid }: { fileid: number }) => { - const photo = this.photoMap.get(fileid); - ondelete([photo]); - }); + constructor( + ondelete: (photos: IPhoto[]) => void, + private updateLoading: (delta: number) => void + ) { + subscribe("files:file:deleted", ({ fileid }: { fileid: number }) => { + const photo = this.photoMap.get(fileid); + ondelete([photo]); + }); + } + + public async open(photo: IPhoto, list?: IPhoto[]) { + list = list || photo.d?.detail; + if (!list) return; + + // Repopulate map + this.photoMap.clear(); + for (const p of list) { + this.photoMap.set(p.fileid, p); } - public async open(photo: IPhoto, list?: IPhoto[]) { - list = list || photo.d?.detail; - if (!list) return; - - // Repopulate map - this.photoMap.clear(); - for (const p of list) { - this.photoMap.set(p.fileid, p); - } - - // Get file infos - let fileInfos: IFileInfo[]; - const ids = list.map(p => p.fileid); - try { - this.updateLoading(1); - fileInfos = await dav.getFiles(ids); - } catch (e) { - console.error('Failed to load fileInfos', e); - showError('Failed to load fileInfos'); - return; - } finally { - this.updateLoading(-1); - } - if (fileInfos.length === 0) { - return; - } - - // Fix sorting of the fileInfos - const itemPositions = {}; - for (const [index, id] of ids.entries()) { - itemPositions[id] = index; - } - fileInfos.sort(function (a, b) { - return itemPositions[a.fileid] - itemPositions[b.fileid]; - }); - - // Get this photo in the fileInfos - const fInfo = fileInfos.find(d => Number(d.fileid) === photo.fileid); - if (!fInfo) { - showError(t('memories', 'Cannot find this photo anymore!')); - return; - } - - // Open Nextcloud viewer - globalThis.OCA.Viewer.open({ - path: fInfo.filename, // path - list: fileInfos, // file list - canLoop: false, // don't loop - onClose: () => { // on viewer close - if (globalThis.OCA.Files.Sidebar.file) { - localStorage.setItem(SIDEBAR_KEY, '1'); - } else { - localStorage.removeItem(SIDEBAR_KEY); - } - globalThis.OCA.Files.Sidebar.close(); - }, - }); - - // Restore sidebar state - if (localStorage.getItem(SIDEBAR_KEY) === '1') { - globalThis.OCA.Files.Sidebar.open(fInfo.filename); - } + // Get file infos + let fileInfos: IFileInfo[]; + const ids = list.map((p) => p.fileid); + try { + this.updateLoading(1); + fileInfos = await dav.getFiles(ids); + } catch (e) { + console.error("Failed to load fileInfos", e); + showError("Failed to load fileInfos"); + return; + } finally { + this.updateLoading(-1); } -} \ No newline at end of file + if (fileInfos.length === 0) { + return; + } + + // Fix sorting of the fileInfos + const itemPositions = {}; + for (const [index, id] of ids.entries()) { + itemPositions[id] = index; + } + fileInfos.sort(function (a, b) { + return itemPositions[a.fileid] - itemPositions[b.fileid]; + }); + + // Get this photo in the fileInfos + const fInfo = fileInfos.find((d) => Number(d.fileid) === photo.fileid); + if (!fInfo) { + showError(t("memories", "Cannot find this photo anymore!")); + return; + } + + // Open Nextcloud viewer + globalThis.OCA.Viewer.open({ + path: fInfo.filename, // path + list: fileInfos, // file list + canLoop: false, // don't loop + onClose: () => { + // on viewer close + if (globalThis.OCA.Files.Sidebar.file) { + localStorage.setItem(SIDEBAR_KEY, "1"); + } else { + localStorage.removeItem(SIDEBAR_KEY); + } + globalThis.OCA.Files.Sidebar.close(); + }, + }); + + // Restore sidebar state + if (localStorage.getItem(SIDEBAR_KEY) === "1") { + globalThis.OCA.Files.Sidebar.open(fInfo.filename); + } + } +} diff --git a/src/services/dav/albums.ts b/src/services/dav/albums.ts index 9747ca8f..fd864a83 100644 --- a/src/services/dav/albums.ts +++ b/src/services/dav/albums.ts @@ -1,51 +1,58 @@ import * as base from "./base"; -import { getCurrentUser } from '@nextcloud/auth' -import { generateUrl } from '@nextcloud/router' -import { showError } from '@nextcloud/dialogs' -import { translate as t, translatePlural as n } from '@nextcloud/l10n' -import { IAlbum, IDay, ITag } from '../../types'; -import { constants } from '../Utils'; -import axios from '@nextcloud/axios' -import client from '../DavClient'; +import { getCurrentUser } from "@nextcloud/auth"; +import { generateUrl } from "@nextcloud/router"; +import { showError } from "@nextcloud/dialogs"; +import { translate as t, translatePlural as n } from "@nextcloud/l10n"; +import { IAlbum, IDay, ITag } from "../../types"; +import { constants } from "../Utils"; +import axios from "@nextcloud/axios"; +import client from "../DavClient"; /** * Get DAV path for album */ - export function getAlbumPath(user: string, name: string) { - // Folder in the dav collection for user - const cuid = getCurrentUser().uid; - if (user === cuid) { - return `/photos/${cuid}/albums/${name}`; - } else { - return `/photos/${cuid}/sharedalbums/${name} (${user})`; - } +export function getAlbumPath(user: string, name: string) { + // Folder in the dav collection for user + const cuid = getCurrentUser().uid; + if (user === cuid) { + return `/photos/${cuid}/albums/${name}`; + } else { + return `/photos/${cuid}/sharedalbums/${name} (${user})`; + } } /** * Get list of albums and convert to Days response * @param type Type of albums to get; 1 = personal, 2 = shared, 3 = all */ -export async function getAlbumsData(type: '1' | '2' | '3'): Promise { - let data: IAlbum[] = []; - try { - const res = await axios.get(generateUrl(`/apps/memories/api/albums?t=${type}`)); - data = res.data; - } catch (e) { - throw e; - } +export async function getAlbumsData(type: "1" | "2" | "3"): Promise { + let data: IAlbum[] = []; + try { + const res = await axios.get( + generateUrl(`/apps/memories/api/albums?t=${type}`) + ); + data = res.data; + } catch (e) { + throw e; + } - // Convert to days response - return [{ - dayid: constants.TagDayID.ALBUMS, - count: data.length, - detail: data.map((album) => ({ + // Convert to days response + return [ + { + dayid: constants.TagDayID.ALBUMS, + count: data.length, + detail: data.map( + (album) => + ({ ...album, fileid: album.album_id, flag: constants.c.FLAG_IS_TAG & constants.c.FLAG_IS_ALBUM, istag: true, isalbum: true, - } as ITag)), - }] + } as ITag) + ), + }, + ]; } /** @@ -56,34 +63,37 @@ export async function getAlbumsData(type: '1' | '2' | '3'): Promise { * @param fileIds List of file IDs to add * @returns Generator */ -export async function* addToAlbum(user: string, name: string, fileIds: number[]) { - // Get files data - let fileInfos = await base.getFiles(fileIds.filter(f => f)); +export async function* addToAlbum( + user: string, + name: string, + fileIds: number[] +) { + // Get files data + let fileInfos = await base.getFiles(fileIds.filter((f) => f)); - const albumPath = getAlbumPath(user, name); + const albumPath = getAlbumPath(user, name); - // Add each file - const calls = fileInfos.map((f) => async () => { - try { - await client.copyFile( - f.originalFilename, - `${albumPath}/${f.basename}`, - ) - return f.fileid; - } catch (e) { - if (e.response?.status === 409) { - // File already exists, all good - return f.fileid; - } + // Add each file + const calls = fileInfos.map((f) => async () => { + try { + await client.copyFile(f.originalFilename, `${albumPath}/${f.basename}`); + return f.fileid; + } catch (e) { + if (e.response?.status === 409) { + // File already exists, all good + return f.fileid; + } - showError(t('memories', 'Failed to add {filename} to album.', { - filename: f.filename, - })); - return 0; - } - }); + showError( + t("memories", "Failed to add {filename} to album.", { + filename: f.filename, + }) + ); + return 0; + } + }); - yield* base.runInParallel(calls, 10); + yield* base.runInParallel(calls, 10); } /** @@ -94,38 +104,46 @@ export async function* addToAlbum(user: string, name: string, fileIds: number[]) * @param fileIds List of file IDs to remove * @returns Generator */ -export async function* removeFromAlbum(user: string, name: string, fileIds: number[]) { - // Get files data - let fileInfos = await base.getFiles(fileIds.filter(f => f)); +export async function* removeFromAlbum( + user: string, + name: string, + fileIds: number[] +) { + // Get files data + let fileInfos = await base.getFiles(fileIds.filter((f) => f)); - // Add each file - const calls = fileInfos.map((f) => async () => { - try { - await client.deleteFile( - `/photos/${user}/albums/${name}/${f.fileid}-${f.basename}`, - ) - return f.fileid; - } catch (e) { - showError(t('memories', 'Failed to remove {filename}.', { - filename: f.filename, - })); - return 0; - } - }); + // Add each file + const calls = fileInfos.map((f) => async () => { + try { + await client.deleteFile( + `/photos/${user}/albums/${name}/${f.fileid}-${f.basename}` + ); + return f.fileid; + } catch (e) { + showError( + t("memories", "Failed to remove {filename}.", { + filename: f.filename, + }) + ); + return 0; + } + }); - yield* base.runInParallel(calls, 10); + yield* base.runInParallel(calls, 10); } /** * Create an album. */ export async function createAlbum(albumName: string) { - try { - await client.createDirectory(`/photos/${getCurrentUser()?.uid}/albums/${albumName}`) - } catch (error) { - console.error(error); - showError(t('photos', 'Failed to create {albumName}.', { albumName })) - } + try { + await client.createDirectory( + `/photos/${getCurrentUser()?.uid}/albums/${albumName}` + ); + } catch (error) { + console.error(error); + showError(t("photos", "Failed to create {albumName}.", { albumName })); + } } /** @@ -137,26 +155,23 @@ export async function createAlbum(albumName: string) { * @param {object} data.properties - The properties to update. */ export async function updateAlbum(album: any, { albumName, properties }: any) { - const stringifiedProperties = Object - .entries(properties) - .map(([name, value]) => { - switch (typeof value) { - case 'string': - return `${value}` - case 'object': - return `${JSON.stringify(value)}` - default: - return '' - } - }) - .join() + const stringifiedProperties = Object.entries(properties) + .map(([name, value]) => { + switch (typeof value) { + case "string": + return `${value}`; + case "object": + return `${JSON.stringify(value)}`; + default: + return ""; + } + }) + .join(); - try { - await client.customRequest( - album.filename, - { - method: 'PROPPATCH', - data: ` + try { + await client.customRequest(album.filename, { + method: "PROPPATCH", + data: ` `, - } - ); + }); - return album; - } catch (error) { - console.error(error); - showError(t('photos', 'Failed to update properties of {albumName} with {properties}.', { albumName, properties: JSON.stringify(properties) })) - return album - } + return album; + } catch (error) { + console.error(error); + showError( + t( + "photos", + "Failed to update properties of {albumName} with {properties}.", + { albumName, properties: JSON.stringify(properties) } + ) + ); + return album; + } } /** @@ -184,7 +204,7 @@ export async function updateAlbum(album: any, { albumName, properties }: any) { * @param name Name of album (or ID) */ export async function getAlbum(user: string, name: string, extraProps = {}) { - const req = ` + const req = ` `; - let album = await client.stat(`/photos/${user}/albums/${name}`, { - data: req, - details: true, - }) as any; + let album = (await client.stat(`/photos/${user}/albums/${name}`, { + data: req, + details: true, + })) as any; - // Post processing - album = { - ...album.data, - ...album.data.props, - }; - const c = album?.collaborators?.collaborator; - album.collaborators = c ? (Array.isArray(c) ? c : [c]) : []; - return album; + // Post processing + album = { + ...album.data, + ...album.data.props, + }; + const c = album?.collaborators?.collaborator; + album.collaborators = c ? (Array.isArray(c) ? c : [c]) : []; + return album; } /** Rename an album */ -export async function renameAlbum(album: any, { currentAlbumName, newAlbumName }) { - const newAlbum = { ...album, basename: newAlbumName } - try { - await client.moveFile( - `/photos/${getCurrentUser()?.uid}/albums/${currentAlbumName}`, - `/photos/${getCurrentUser()?.uid}/albums/${newAlbumName}`, - ) - return newAlbum - } catch (error) { - console.error(error); - showError(t('photos', 'Failed to rename {currentAlbumName} to {newAlbumName}.', { currentAlbumName, newAlbumName })) - return album - } +export async function renameAlbum( + album: any, + { currentAlbumName, newAlbumName } +) { + const newAlbum = { ...album, basename: newAlbumName }; + try { + await client.moveFile( + `/photos/${getCurrentUser()?.uid}/albums/${currentAlbumName}`, + `/photos/${getCurrentUser()?.uid}/albums/${newAlbumName}` + ); + return newAlbum; + } catch (error) { + console.error(error); + showError( + t("photos", "Failed to rename {currentAlbumName} to {newAlbumName}.", { + currentAlbumName, + newAlbumName, + }) + ); + return album; + } } diff --git a/src/services/dav/archive.ts b/src/services/dav/archive.ts index 0ec4861c..c4cbff44 100644 --- a/src/services/dav/archive.ts +++ b/src/services/dav/archive.ts @@ -1,8 +1,8 @@ -import * as base from './base'; -import { generateUrl } from '@nextcloud/router' -import { showError } from '@nextcloud/dialogs' -import { translate as t, translatePlural as n } from '@nextcloud/l10n' -import axios from '@nextcloud/axios' +import * as base from "./base"; +import { generateUrl } from "@nextcloud/router"; +import { showError } from "@nextcloud/dialogs"; +import { translate as t, translatePlural as n } from "@nextcloud/l10n"; +import axios from "@nextcloud/axios"; /** * Archive or unarchive a single file @@ -10,8 +10,11 @@ import axios from '@nextcloud/axios' * @param fileid File id * @param archive Archive or unarchive */ - export async function archiveFile(fileid: number, archive: boolean) { - return await axios.patch(generateUrl('/apps/memories/api/archive/{fileid}', { fileid }), { archive }); +export async function archiveFile(fileid: number, archive: boolean) { + return await axios.patch( + generateUrl("/apps/memories/api/archive/{fileid}", { fileid }), + { archive } + ); } /** @@ -21,23 +24,24 @@ import axios from '@nextcloud/axios' * @param archive Archive or unarchive * @returns list of file ids that were deleted */ - export async function* archiveFilesByIds(fileIds: number[], archive: boolean) { - if (fileIds.length === 0) { - return; +export async function* archiveFilesByIds(fileIds: number[], archive: boolean) { + if (fileIds.length === 0) { + return; + } + + // Archive each file + const calls = fileIds.map((id) => async () => { + try { + 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 })); + return 0; } + }); - // Archive each file - const calls = fileIds.map((id) => async () => { - try { - 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 })); - return 0; - } - }); - - yield* base.runInParallel(calls, 10); -} \ No newline at end of file + yield* base.runInParallel(calls, 10); +} diff --git a/src/services/dav/base.ts b/src/services/dav/base.ts index cd2ac05a..26902da9 100644 --- a/src/services/dav/base.ts +++ b/src/services/dav/base.ts @@ -1,9 +1,9 @@ -import { getCurrentUser } from '@nextcloud/auth'; -import { showError } from '@nextcloud/dialogs'; -import { translate as t } from '@nextcloud/l10n'; -import { IFileInfo } from '../../types'; -import client from '../DavClient'; -import { genFileInfo } from '../FileUtils'; +import { getCurrentUser } from "@nextcloud/auth"; +import { showError } from "@nextcloud/dialogs"; +import { translate as t } from "@nextcloud/l10n"; +import { IFileInfo } from "../../types"; +import client from "../DavClient"; +import { genFileInfo } from "../FileUtils"; export const props = ` @@ -17,18 +17,18 @@ export const props = ` `; 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; @@ -38,17 +38,17 @@ const GET_FILE_CHUNK_SIZE = 50; * @param fileIds list of file ids * @returns list of file infos */ - export async function getFiles(fileIds: number[]): Promise { - // Divide fileIds into chunks of GET_FILE_CHUNK_SIZE - const chunks = []; - for (let i = 0; i < fileIds.length; i += GET_FILE_CHUNK_SIZE) { - chunks.push(fileIds.slice(i, i + GET_FILE_CHUNK_SIZE)); - } +export async function getFiles(fileIds: number[]): Promise { + // Divide fileIds into chunks of GET_FILE_CHUNK_SIZE + const chunks = []; + for (let i = 0; i < fileIds.length; i += GET_FILE_CHUNK_SIZE) { + chunks.push(fileIds.slice(i, i + GET_FILE_CHUNK_SIZE)); + } - // Get file infos for each chunk - const fileInfos = await Promise.all(chunks.map(getFilesInternal)); - return fileInfos.flat(); - } + // Get file infos for each chunk + const fileInfos = await Promise.all(chunks.map(getFilesInternal)); + return fileInfos.flat(); +} /** * Get file infos for list of files given Ids @@ -56,29 +56,33 @@ const GET_FILE_CHUNK_SIZE = 50; * @returns list of file infos */ async function getFilesInternal(fileIds: number[]): Promise { - const prefixPath = `/files/${getCurrentUser()!.uid}`; + const prefixPath = `/files/${getCurrentUser()!.uid}`; - // IMPORTANT: if this isn't there, then a blank - // returns EVERYTHING on the server! - if (fileIds.length === 0) { - return []; - } + // IMPORTANT: if this isn't there, then a blank + // returns EVERYTHING on the server! + if (fileIds.length === 0) { + return []; + } - const filter = fileIds.map(fileId => ` + const filter = fileIds + .map( + (fileId) => ` ${fileId} - `).join(''); + ` + ) + .join(""); - const options = { - method: 'SEARCH', - headers: { - 'content-Type': 'text/xml', - }, - data: ` + const options = { + method: "SEARCH", + headers: { + "content-Type": "text/xml", + }, + data: ` { `, - deep: true, - details: true, - responseType: 'text', - }; + deep: true, + details: true, + responseType: "text", + }; - 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, '') - })); + 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, ""), + }) + ); } - - /** * Run promises in parallel, but only n at a time * @param promises Array of promise generator funnction (async functions) * @param n Number of promises to run in parallel */ -export async function* runInParallel(promises: (() => Promise)[], n: number) { - while (promises.length > 0) { - const promisesToRun = promises.splice(0, n); - const resultsForThisBatch = await Promise.all(promisesToRun.map(p => p())); - yield resultsForThisBatch; - } - return; +export async function* runInParallel( + promises: (() => Promise)[], + n: number +) { + while (promises.length > 0) { + const promisesToRun = promises.splice(0, n); + const resultsForThisBatch = await Promise.all( + promisesToRun.map((p) => p()) + ); + yield resultsForThisBatch; + } + return; } /** @@ -139,8 +148,8 @@ export async function* runInParallel(promises: (() => Promise)[], n: numbe * @param path path to the file */ export async function deleteFile(path: string) { - const prefixPath = `/files/${getCurrentUser()!.uid}`; - return await client.deleteFile(`${prefixPath}${path}`); + const prefixPath = `/files/${getCurrentUser()!.uid}`; + return await client.deleteFile(`${prefixPath}${path}`); } /** @@ -150,46 +159,34 @@ export async function deleteFile(path: string) { * @returns list of file ids that were deleted */ export async function* deleteFilesByIds(fileIds: number[]) { - const fileIdsSet = new Set(fileIds); + const fileIdsSet = new Set(fileIds); - if (fileIds.length === 0) { - return; - } + if (fileIds.length === 0) { + return; + } - // Get files data - let fileInfos: any[] = []; + // Get files data + let fileInfos: any[] = []; + try { + fileInfos = await getFiles(fileIds.filter((f) => f)); + } catch (e) { + console.error("Failed to get file info for files to delete", fileIds, e); + showError(t("memories", "Failed to delete files.")); + return; + } + + // Delete each file + fileInfos = fileInfos.filter((f) => fileIdsSet.has(f.fileid)); + const calls = fileInfos.map((fileInfo) => async () => { try { - fileInfos = await getFiles(fileIds.filter(f => f)); - } catch (e) { - console.error('Failed to get file info for files to delete', fileIds, e); - showError(t('memories', 'Failed to delete files.')); - return; + await deleteFile(fileInfo.filename); + return fileInfo.fileid as number; + } catch (error) { + console.error("Failed to delete", fileInfo, error); + showError(t("memories", "Failed to delete {fileName}.", fileInfo)); + return 0; } + }); - // Delete each file - fileInfos = fileInfos.filter((f) => fileIdsSet.has(f.fileid)); - const calls = fileInfos.map((fileInfo) => async () => { - try { - await deleteFile(fileInfo.filename); - return fileInfo.fileid as number; - } catch (error) { - console.error('Failed to delete', fileInfo, error); - showError(t('memories', 'Failed to delete {fileName}.', fileInfo)); - return 0; - } - }); - - yield* runInParallel(calls, 10); + yield* runInParallel(calls, 10); } - - - - - - - - - - - - diff --git a/src/services/dav/download.ts b/src/services/dav/download.ts index e4d8fa1c..da0fd675 100644 --- a/src/services/dav/download.ts +++ b/src/services/dav/download.ts @@ -1,5 +1,5 @@ -import * as base from './base'; -import { generateUrl } from '@nextcloud/router' +import * as base from "./base"; +import { generateUrl } from "@nextcloud/router"; /** * Download a file @@ -7,32 +7,32 @@ import { generateUrl } from '@nextcloud/router' * @param fileNames - The file's names */ export async function downloadFiles(fileNames: string[]): Promise { - const randomToken = Math.random().toString(36).substring(2) + const randomToken = Math.random().toString(36).substring(2); - const params = new URLSearchParams() - params.append('files', JSON.stringify(fileNames)) - params.append('downloadStartSecret', randomToken) + const params = new URLSearchParams(); + params.append("files", JSON.stringify(fileNames)); + params.append("downloadStartSecret", randomToken); - const downloadURL = generateUrl(`/apps/files/ajax/download.php?${params}`) + const downloadURL = generateUrl(`/apps/files/ajax/download.php?${params}`); - window.location.href = `${downloadURL}downloadStartSecret=${randomToken}` + window.location.href = `${downloadURL}downloadStartSecret=${randomToken}`; - return new Promise((resolve) => { - const waitForCookieInterval = setInterval( - () => { - const cookieIsSet = document.cookie - .split(';') - .map(cookie => cookie.split('=')) - .findIndex(([cookieName, cookieValue]) => cookieName === 'ocDownloadStarted' && cookieValue === randomToken) + return new Promise((resolve) => { + const waitForCookieInterval = setInterval(() => { + const cookieIsSet = document.cookie + .split(";") + .map((cookie) => cookie.split("=")) + .findIndex( + ([cookieName, cookieValue]) => + cookieName === "ocDownloadStarted" && cookieValue === randomToken + ); - if (cookieIsSet) { - clearInterval(waitForCookieInterval) - resolve(true) - } - }, - 50 - ) - }) + if (cookieIsSet) { + clearInterval(waitForCookieInterval); + resolve(true); + } + }, 50); + }); } /** @@ -40,11 +40,11 @@ export async function downloadFiles(fileNames: string[]): Promise { * @param fileIds list of file ids */ export async function downloadFilesByIds(fileIds: number[]) { - if (fileIds.length === 0) { - return; - } + if (fileIds.length === 0) { + return; + } - // Get files to download - const fileInfos = await base.getFiles(fileIds); - await downloadFiles(fileInfos.map(f => f.filename)); -} \ No newline at end of file + // Get files to download + const fileInfos = await base.getFiles(fileIds); + await downloadFiles(fileInfos.map((f) => f.filename)); +} diff --git a/src/services/dav/face.ts b/src/services/dav/face.ts index f55894fc..f7308605 100644 --- a/src/services/dav/face.ts +++ b/src/services/dav/face.ts @@ -1,44 +1,51 @@ -import axios from '@nextcloud/axios'; -import { showError } from '@nextcloud/dialogs'; -import { translate as t } from '@nextcloud/l10n'; -import { generateUrl } from '@nextcloud/router'; -import { IDay, IPhoto } from '../../types'; -import client from '../DavClient'; -import { constants } from '../Utils'; -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 { IDay, IPhoto } from "../../types"; +import client from "../DavClient"; +import { constants } from "../Utils"; +import * as base from "./base"; /** * Get list of tags and convert to Days response */ export async function getPeopleData(): Promise { - // Query for photos - let data: { - id: number; - count: number; - name: string; - previews: IPhoto[]; - }[] = []; - try { - const res = await axios.get(generateUrl('/apps/memories/api/faces')); - data = res.data; - } catch (e) { - throw e; - } + // Query for photos + let data: { + id: number; + count: number; + name: string; + previews: IPhoto[]; + }[] = []; + try { + const res = await axios.get( + generateUrl("/apps/memories/api/faces") + ); + data = res.data; + } catch (e) { + throw e; + } - // Add flag to previews - data.forEach(t => t.previews?.forEach((preview) => preview.flag = 0)); + // Add flag to previews + data.forEach((t) => t.previews?.forEach((preview) => (preview.flag = 0))); - // Convert to days response - return [{ - dayid: constants.TagDayID.FACES, - count: data.length, - detail: data.map((face) => ({ + // Convert to days response + return [ + { + dayid: constants.TagDayID.FACES, + count: data.length, + detail: data.map( + (face) => + ({ ...face, fileid: face.id, istag: true, isface: true, - } as any)), - }] + } as any) + ), + }, + ]; } /** @@ -49,23 +56,31 @@ export async function getPeopleData(): Promise { * @param fileIds List of file IDs to remove * @returns Generator */ -export async function* removeFaceImages(user: string, name: string, fileIds: number[]) { - // Get files data - let fileInfos = await base.getFiles(fileIds.filter(f => f)); +export async function* removeFaceImages( + user: string, + name: string, + fileIds: number[] +) { + // Get files data + let fileInfos = await base.getFiles(fileIds.filter((f) => f)); - // Remove each file - const calls = fileInfos.map((f) => async () => { - try { - 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.', { - filename: f.filename, - })); - return 0; - } - }); + // Remove each file + const calls = fileInfos.map((f) => async () => { + try { + 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.", { + filename: f.filename, + }) + ); + return 0; + } + }); - yield* base.runInParallel(calls, 10); -} \ No newline at end of file + yield* base.runInParallel(calls, 10); +} diff --git a/src/services/dav/favorites.ts b/src/services/dav/favorites.ts index c6672c16..4c88b78a 100644 --- a/src/services/dav/favorites.ts +++ b/src/services/dav/favorites.ts @@ -1,9 +1,9 @@ -import * as base from './base'; -import { generateUrl } from '@nextcloud/router' -import { encodePath } from '@nextcloud/paths' -import { showError } from '@nextcloud/dialogs' -import { translate as t, translatePlural as n } from '@nextcloud/l10n' -import axios from '@nextcloud/axios' +import * as base from "./base"; +import { generateUrl } from "@nextcloud/router"; +import { encodePath } from "@nextcloud/paths"; +import { showError } from "@nextcloud/dialogs"; +import { translate as t, translatePlural as n } from "@nextcloud/l10n"; +import axios from "@nextcloud/axios"; /** * Favorite a file @@ -13,22 +13,22 @@ import axios from '@nextcloud/axios' * @param favoriteState - The new favorite state */ export async function favoriteFile(fileName: string, favoriteState: boolean) { - let encodedPath = encodePath(fileName) - while (encodedPath[0] === '/') { - encodedPath = encodedPath.substring(1) - } + let encodedPath = encodePath(fileName); + while (encodedPath[0] === "/") { + encodedPath = encodedPath.substring(1); + } - try { - return axios.post( - `${generateUrl('/apps/files/api/v1/files/')}${encodedPath}`, - { - tags: favoriteState ? ['_$!!$_'] : [], - }, - ) - } catch (error) { - console.error('Failed to favorite', fileName, error) - showError(t('memories', 'Failed to favorite {fileName}.', { fileName })) - } + try { + return axios.post( + `${generateUrl("/apps/files/api/v1/files/")}${encodedPath}`, + { + tags: favoriteState ? ["_$!!$_"] : [], + } + ); + } catch (error) { + console.error("Failed to favorite", fileName, error); + showError(t("memories", "Failed to favorite {fileName}.", { fileName })); + } } /** @@ -38,35 +38,38 @@ export async 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* favoriteFilesByIds(fileIds: number[], favoriteState: boolean) { - const fileIdsSet = new Set(fileIds); +export async function* favoriteFilesByIds( + fileIds: number[], + favoriteState: boolean +) { + const fileIdsSet = new Set(fileIds); - if (fileIds.length === 0) { - return; - } + if (fileIds.length === 0) { + return; + } - // Get files data - let fileInfos: any[] = []; + // Get files data + let fileInfos: any[] = []; + try { + fileInfos = await base.getFiles(fileIds.filter((f) => f)); + } catch (e) { + console.error("Failed to get file info", fileIds, e); + showError(t("memories", "Failed to favorite files.")); + return; + } + + // Favorite each file + fileInfos = fileInfos.filter((f) => fileIdsSet.has(f.fileid)); + const calls = fileInfos.map((fileInfo) => async () => { try { - fileInfos = await base.getFiles(fileIds.filter(f => f)); - } catch (e) { - console.error('Failed to get file info', fileIds, e); - showError(t('memories', 'Failed to favorite files.')); - return; + await favoriteFile(fileInfo.filename, favoriteState); + return fileInfo.fileid as number; + } catch (error) { + console.error("Failed to favorite", fileInfo, error); + showError(t("memories", "Failed to favorite {fileName}.", fileInfo)); + return 0; } + }); - // Favorite each file - fileInfos = fileInfos.filter((f) => fileIdsSet.has(f.fileid)); - const calls = fileInfos.map((fileInfo) => async () => { - try { - await favoriteFile(fileInfo.filename, favoriteState); - return fileInfo.fileid as number; - } catch (error) { - console.error('Failed to favorite', fileInfo, error); - showError(t('memories', 'Failed to favorite {fileName}.', fileInfo)); - return 0; - } - }); - - yield* base.runInParallel(calls, 10); -} \ No newline at end of file + yield* base.runInParallel(calls, 10); +} diff --git a/src/services/dav/folders.ts b/src/services/dav/folders.ts index 97303814..1c954724 100644 --- a/src/services/dav/folders.ts +++ b/src/services/dav/folders.ts @@ -1,32 +1,37 @@ -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 { - const prefixPath = `/files/${getCurrentUser()!.uid}`; +export async function getFolderPreviewFileIds( + folderPath: string, + limit: number +): Promise { + const prefixPath = `/files/${getCurrentUser()!.uid}`; - const filter = base.IMAGE_MIME_TYPES.map(mime => ` + const filter = base.IMAGE_MIME_TYPES.map( + (mime) => ` ${mime} - `).join(''); + ` + ).join(""); - const options = { - method: 'SEARCH', - headers: { - 'content-Type': 'text/xml', - }, - data: ` + const options = { + method: "SEARCH", + headers: { + "content-Type": "text/xml", + }, + data: ` `, - deep: true, - details: true, - responseType: 'text', - }; + deep: true, + details: true, + responseType: "text", + }; - 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(/"/g, ''), // remove quotes - })); -} \ No newline at end of file + 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(/"/g, ""), // remove quotes + }) + ); +} diff --git a/src/services/dav/onthisday.ts b/src/services/dav/onthisday.ts index 1b7a43bb..93beb8cd 100644 --- a/src/services/dav/onthisday.ts +++ b/src/services/dav/onthisday.ts @@ -1,30 +1,32 @@ -import { generateUrl } from '@nextcloud/router' -import { IDay, IPhoto } from '../../types'; -import axios from '@nextcloud/axios' +import { generateUrl } from "@nextcloud/router"; +import { IDay, IPhoto } from "../../types"; +import axios from "@nextcloud/axios"; /** * Get original onThisDay response. */ export async function getOnThisDayRaw() { - const dayIds: number[] = []; - const now = new Date(); - const nowUTC = new Date(now.getTime() - now.getTimezoneOffset() * 60000); + const dayIds: number[] = []; + const now = new Date(); + const nowUTC = new Date(now.getTime() - now.getTimezoneOffset() * 60000); - // Populate dayIds - for (let i = 1; i <= 120; i++) { - // +- 3 days from this day - for (let j = -3; j <= 3; j++) { - const d = new Date(nowUTC); - d.setFullYear(d.getFullYear() - i); - d.setDate(d.getDate() + j); - const dayId = Math.floor(d.getTime() / 1000 / 86400) - dayIds.push(dayId); - } + // Populate dayIds + for (let i = 1; i <= 120; i++) { + // +- 3 days from this day + for (let j = -3; j <= 3; j++) { + const d = new Date(nowUTC); + d.setFullYear(d.getFullYear() - i); + d.setDate(d.getDate() + j); + const dayId = Math.floor(d.getTime() / 1000 / 86400); + dayIds.push(dayId); } + } - return (await axios.post(generateUrl('/apps/memories/api/days'), { - body_ids: dayIds.join(','), - })).data; + return ( + await axios.post(generateUrl("/apps/memories/api/days"), { + body_ids: dayIds.join(","), + }) + ).data; } /** @@ -32,30 +34,30 @@ export async function getOnThisDayRaw() { * Query for last 120 years; should be enough */ export async function getOnThisDayData(): Promise { - // Query for photos - let data = await getOnThisDayRaw(); + // Query for photos + let data = await getOnThisDayRaw(); - // Group photos by day - const ans: IDay[] = []; - let prevDayId = Number.MIN_SAFE_INTEGER; - for (const photo of data) { - if (!photo.dayid) continue; + // Group photos by day + const ans: IDay[] = []; + let prevDayId = Number.MIN_SAFE_INTEGER; + for (const photo of data) { + if (!photo.dayid) continue; - // This works because the response is sorted by date taken - if (photo.dayid !== prevDayId) { - ans.push({ - dayid: photo.dayid, - count: 0, - detail: [], - }); - prevDayId = photo.dayid; - } - - // Add to last day - const day = ans[ans.length - 1]; - day.detail.push(photo); - day.count++; + // This works because the response is sorted by date taken + if (photo.dayid !== prevDayId) { + ans.push({ + dayid: photo.dayid, + count: 0, + detail: [], + }); + prevDayId = photo.dayid; } - return ans; -} \ No newline at end of file + // Add to last day + const day = ans[ans.length - 1]; + day.detail.push(photo); + day.count++; + } + + return ans; +} diff --git a/src/services/dav/tags.ts b/src/services/dav/tags.ts index d2f2b1ee..7ae7a446 100644 --- a/src/services/dav/tags.ts +++ b/src/services/dav/tags.ts @@ -1,38 +1,45 @@ -import { generateUrl } from '@nextcloud/router' -import { IDay, IPhoto, ITag } from '../../types'; -import { constants, hashCode } from '../Utils'; -import axios from '@nextcloud/axios' +import { generateUrl } from "@nextcloud/router"; +import { IDay, IPhoto, ITag } from "../../types"; +import { constants, hashCode } from "../Utils"; +import axios from "@nextcloud/axios"; /** * Get list of tags and convert to Days response */ export async function getTagsData(): Promise { - // Query for photos - let data: { - id: number; - count: number; - name: string; - previews: IPhoto[]; - }[] = []; - try { - const res = await axios.get(generateUrl('/apps/memories/api/tags')); - data = res.data; - } catch (e) { - throw e; - } + // Query for photos + let data: { + id: number; + count: number; + name: string; + previews: IPhoto[]; + }[] = []; + try { + const res = await axios.get( + generateUrl("/apps/memories/api/tags") + ); + data = res.data; + } catch (e) { + throw e; + } - // Add flag to previews - data.forEach(t => t.previews?.forEach((preview) => preview.flag = 0)); + // Add flag to previews + data.forEach((t) => t.previews?.forEach((preview) => (preview.flag = 0))); - // Convert to days response - return [{ - dayid: constants.TagDayID.TAGS, - count: data.length, - detail: data.map((tag) => ({ + // Convert to days response + return [ + { + dayid: constants.TagDayID.TAGS, + count: data.length, + detail: data.map( + (tag) => + ({ ...tag, fileid: hashCode(tag.name), flag: constants.c.FLAG_IS_TAG, istag: true, - } as ITag)), - }] -} \ No newline at end of file + } as ITag) + ), + }, + ]; +} diff --git a/src/types.ts b/src/types.ts index 752aa8d5..f0de0773 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,201 +1,201 @@ import { VueConstructor } from "vue"; export type IFileInfo = { - /** Database file ID */ - fileid: number; - /** Full file name, e.g. /pi/test/Qx0dq7dvEXA.jpg */ - filename: string; - /** Original file name, e.g. /files/admin/pi/test/Qx0dq7dvEXA.jpg */ - originalFilename: string; - /** Base name of file e.g. Qx0dq7dvEXA.jpg */ - basename: string; - /** Etag identifier */ - etag: string; - /** File has preview available */ - hasPreview: boolean; - /** File is marked favorite */ - favorite: boolean; - /** Vue flags */ - flag?: number; -} + /** Database file ID */ + fileid: number; + /** Full file name, e.g. /pi/test/Qx0dq7dvEXA.jpg */ + filename: string; + /** Original file name, e.g. /files/admin/pi/test/Qx0dq7dvEXA.jpg */ + originalFilename: string; + /** Base name of file e.g. Qx0dq7dvEXA.jpg */ + basename: string; + /** Etag identifier */ + etag: string; + /** File has preview available */ + hasPreview: boolean; + /** File is marked favorite */ + favorite: boolean; + /** Vue flags */ + flag?: number; +}; export type IDay = { - /** Day ID */ - dayid: number; - /** Number of photos in this day */ - count: number; - /** Rows in the day */ - rows?: IRow[]; - /** List of photos for this day */ - detail?: IPhoto[]; -} + /** Day ID */ + dayid: number; + /** Number of photos in this day */ + count: number; + /** Rows in the day */ + rows?: IRow[]; + /** List of photos for this day */ + detail?: IPhoto[]; +}; export type IPhoto = { - /** Nextcloud ID of file */ - fileid: number; - /** Etag from server */ - etag?: string; - /** Bit flags */ - flag: number; - /** DayID from server */ - dayid?: number; - /** Width of full image */ - w?: number; - /** Height of full image */ - h?: number; + /** Nextcloud ID of file */ + fileid: number; + /** Etag from server */ + etag?: string; + /** Bit flags */ + flag: number; + /** DayID from server */ + dayid?: number; + /** Width of full image */ + w?: number; + /** Height of full image */ + h?: number; - /** Grid display width px */ - dispW?: number; - /** Grid display height px */ - dispH?: number; - /** Grid display X px */ - dispX?: number; - /** Grid display Y px */ - dispY?: number; - /** Grid display row id (relative to head) */ - dispRowNum?: number; + /** Grid display width px */ + dispW?: number; + /** Grid display height px */ + dispH?: number; + /** Grid display X px */ + dispX?: number; + /** Grid display Y px */ + dispY?: number; + /** Grid display row id (relative to head) */ + dispRowNum?: number; - /** Reference to day object */ - d?: IDay; + /** Reference to day object */ + d?: IDay; - /** Face dimensions */ - facerect?: IFaceRect; + /** Face dimensions */ + facerect?: IFaceRect; - /** Video flag from server */ - isvideo?: boolean; - /** Favorite flag from server */ - isfavorite?: boolean; - /** Is this a folder */ - isfolder?: boolean; - /** Is this a tag */ - istag?: boolean; - /** Is this an album */ - isalbum?: boolean; - /** Is this a face */ - isface?: boolean; - /** Optional datetaken epoch */ - datetaken?: number; -} + /** Video flag from server */ + isvideo?: boolean; + /** Favorite flag from server */ + isfavorite?: boolean; + /** Is this a folder */ + isfolder?: boolean; + /** Is this a tag */ + istag?: boolean; + /** Is this an album */ + isalbum?: boolean; + /** Is this a face */ + isface?: boolean; + /** Optional datetaken epoch */ + datetaken?: number; +}; export interface IFolder extends IPhoto { - /** Path to folder */ - path: string; - /** FileInfos for preview images */ - previewFileInfos?: IFileInfo[]; - /** Name of folder */ - name: string; + /** Path to folder */ + path: string; + /** FileInfos for preview images */ + previewFileInfos?: IFileInfo[]; + /** Name of folder */ + name: string; } export interface ITag extends IPhoto { - /** Name of tag */ - name: string; - /** Number of images in this tag */ - count: number; - /** User for face if face */ - user_id?: string; - /** Cache of previews */ - previews?: IPhoto[]; + /** Name of tag */ + name: string; + /** Number of images in this tag */ + count: number; + /** User for face if face */ + user_id?: string; + /** Cache of previews */ + previews?: IPhoto[]; } export interface IAlbum extends ITag { - /** ID of album */ - album_id: number; - /** Owner of album */ - user: string; - /** Created timestamp */ - created: number; - /** Location string */ - location: string; - /** File ID of last added photo */ - last_added_photo: number; + /** ID of album */ + album_id: number; + /** Owner of album */ + user: string; + /** Created timestamp */ + created: number; + /** Location string */ + location: string; + /** File ID of last added photo */ + last_added_photo: number; } export interface IFaceRect { - w: number; - h: number; - x: number; - y: number; + w: number; + h: number; + x: number; + y: number; } export type IRow = { - /** Vue Recycler identifier */ - id?: string; - /** Row ID from head */ - num: number; - /** Day ID */ - dayId: number; - /** Refrence to day object */ - day: IDay; - /** Whether this is a head row */ - type: IRowType; - /** [Head only] Title of the header */ - name?: string; - /** [Head only] Boolean if the entire day is selected */ - selected?: boolean; - /** Main list of photo items */ - photos?: IPhoto[]; - /** Height in px of the row */ - size?: number; - /** Count of placeholders to create */ - pct?: number; -} + /** Vue Recycler identifier */ + id?: string; + /** Row ID from head */ + num: number; + /** Day ID */ + dayId: number; + /** Refrence to day object */ + day: IDay; + /** Whether this is a head row */ + type: IRowType; + /** [Head only] Title of the header */ + name?: string; + /** [Head only] Boolean if the entire day is selected */ + selected?: boolean; + /** Main list of photo items */ + photos?: IPhoto[]; + /** Height in px of the row */ + size?: number; + /** Count of placeholders to create */ + pct?: number; +}; export type IHeadRow = IRow & { - type: IRowType.HEAD; - selected: boolean; - super?: string; -} + type: IRowType.HEAD; + selected: boolean; + super?: string; +}; export enum IRowType { - HEAD = 0, - PHOTOS = 1, - FOLDERS = 2, + HEAD = 0, + PHOTOS = 1, + FOLDERS = 2, } export type ITick = { - /** Day ID */ - dayId: number; - /** Display top position */ - topF: number; - /** Display top position (truncated to 1 decimal pt) */ - top: number; - /** Y coordinate on recycler */ - y: number; - /** Cumulative number of photos before this tick */ - count: number; - /** Is a new month */ - isMonth: boolean; - /** Text if any (e.g. year) */ - text?: string | number; - /** Whether this tick should be shown */ - s?: boolean; - /** Key for vue component */ - key?: number -} + /** Day ID */ + dayId: number; + /** Display top position */ + topF: number; + /** Display top position (truncated to 1 decimal pt) */ + top: number; + /** Y coordinate on recycler */ + y: number; + /** Cumulative number of photos before this tick */ + count: number; + /** Is a new month */ + isMonth: boolean; + /** Text if any (e.g. year) */ + text?: string | number; + /** Whether this tick should be shown */ + s?: boolean; + /** Key for vue component */ + key?: number; +}; export type TopMatter = { - type: TopMatterType; -} + type: TopMatterType; +}; export enum TopMatterType { - NONE = 0, - FOLDER = 1, - TAG = 2, - FACE = 3, - ALBUM = 4, + NONE = 0, + FOLDER = 1, + TAG = 2, + FACE = 3, + ALBUM = 4, } export type TopMatterFolder = TopMatter & { - type: TopMatterType.FOLDER; - list: { - text: string; - path: string; - }[]; -} + type: TopMatterType.FOLDER; + list: { + text: string; + path: string; + }[]; +}; export type ISelectionAction = { - /** Display text */ - name: string; - /** Icon component */ - icon: VueConstructor; - /** Action to perform */ - callback: (selection: Map) => Promise; - /** Condition to check for including */ - if?: (self?: any) => boolean; -} + /** Display text */ + name: string; + /** Icon component */ + icon: VueConstructor; + /** Action to perform */ + callback: (selection: Map) => Promise; + /** Condition to check for including */ + if?: (self?: any) => boolean; +}; diff --git a/src/vue-shims.d.ts b/src/vue-shims.d.ts index 98c4f9e4..60142589 100644 --- a/src/vue-shims.d.ts +++ b/src/vue-shims.d.ts @@ -1,10 +1,10 @@ declare module "*.vue" { - import Vue from "vue" - export default Vue + import Vue from "vue"; + export default Vue; } -declare module '*.svg' { - import Vue, {VueConstructor} from 'vue'; - const content: VueConstructor; - export default content; -} \ No newline at end of file +declare module "*.svg" { + import Vue, { VueConstructor } from "vue"; + const content: VueConstructor; + export default content; +}