2022-09-16 23:17:45 +00:00
|
|
|
import { getCanonicalLocale } from "@nextcloud/l10n";
|
2022-10-28 19:08:34 +00:00
|
|
|
import { getCurrentUser } from "@nextcloud/auth";
|
|
|
|
import { loadState } from "@nextcloud/initial-state";
|
2022-10-16 03:28:40 +00:00
|
|
|
import { IPhoto } from "../types";
|
2022-10-28 19:08:34 +00:00
|
|
|
import moment from "moment";
|
2022-09-16 23:17:45 +00:00
|
|
|
|
2022-10-15 18:12:54 +00:00
|
|
|
// Memoize the result of short date conversions
|
|
|
|
// These operations are surprisingly expensive
|
|
|
|
// and we do them a lot because of scroller hover
|
|
|
|
const shortDateStrMemo = new Map<number, string>();
|
|
|
|
|
2022-09-12 19:15:28 +00:00
|
|
|
/** Get JS date object from dayId */
|
2022-10-28 19:08:34 +00:00
|
|
|
export function dayIdToDate(dayId: number) {
|
|
|
|
return new Date(dayId * 86400 * 1000);
|
2022-09-12 19:15:28 +00:00
|
|
|
}
|
|
|
|
|
2022-10-15 18:12:54 +00:00
|
|
|
/** Get Day ID from JS date */
|
2022-10-28 19:08:34 +00:00
|
|
|
export function dateToDayId(date: Date) {
|
|
|
|
return Math.floor(date.getTime() / (86400 * 1000));
|
2022-10-15 18:12:54 +00:00
|
|
|
}
|
|
|
|
|
2022-09-12 19:15:28 +00:00
|
|
|
/** Get month name from number */
|
2022-09-16 23:17:45 +00:00
|
|
|
export function getShortDateStr(date: Date) {
|
2022-10-28 19:08:34 +00:00
|
|
|
const dayId = dateToDayId(date);
|
|
|
|
if (!shortDateStrMemo.has(dayId)) {
|
|
|
|
shortDateStrMemo.set(
|
|
|
|
dayId,
|
|
|
|
date.toLocaleDateString(getCanonicalLocale(), {
|
|
|
|
month: "short",
|
|
|
|
year: "numeric",
|
|
|
|
timeZone: "UTC",
|
|
|
|
})
|
|
|
|
);
|
|
|
|
}
|
|
|
|
return shortDateStrMemo.get(dayId);
|
2022-09-16 23:17:45 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/** Get long date string with optional year if same as current */
|
2022-10-28 19:08:34 +00:00
|
|
|
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,
|
|
|
|
});
|
2022-10-06 23:28:35 +00:00
|
|
|
}
|
|
|
|
|
2022-10-18 03:06:01 +00:00
|
|
|
/** Get text like "5 years ago" from a date */
|
|
|
|
export function getFromNowStr(date: Date) {
|
2022-10-28 19:08:34 +00:00
|
|
|
// Get fromNow in correct locale
|
|
|
|
const text = moment(date).locale(getCanonicalLocale()).fromNow();
|
2022-10-18 03:06:01 +00:00
|
|
|
|
2022-10-28 19:08:34 +00:00
|
|
|
// Title case
|
|
|
|
return text.charAt(0).toUpperCase() + text.slice(1);
|
2022-10-18 03:06:01 +00:00
|
|
|
}
|
|
|
|
|
2022-10-06 23:28:35 +00:00
|
|
|
/**
|
|
|
|
* Returns a hash code from a string
|
|
|
|
* @param {String} str The string to hash.
|
|
|
|
* @return {Number} A 32bit integer
|
|
|
|
* @see http://werxltd.com/wp/2010/05/13/javascript-implementation-of-javas-string-hashcode-method/
|
|
|
|
*/
|
2022-10-15 18:00:24 +00:00
|
|
|
export function hashCode(str: string): number {
|
2022-10-28 19:08:34 +00:00
|
|
|
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;
|
2022-10-06 23:28:35 +00:00
|
|
|
}
|
|
|
|
|
2022-10-15 18:00:24 +00:00
|
|
|
/**
|
|
|
|
* Search for elem in a sorted array of objects
|
|
|
|
* If the object is not found, return the index where it should be inserted
|
|
|
|
*
|
|
|
|
* @param arr Array of objects to search
|
|
|
|
* @param elem Element to search for
|
|
|
|
* @param key Key to use for comparison
|
|
|
|
*/
|
|
|
|
export function binarySearch(arr: any, elem: any, key?: string) {
|
2022-10-28 19:08:34 +00:00
|
|
|
let minIndex = 0;
|
|
|
|
let maxIndex = arr.length - 1;
|
|
|
|
let currentIndex: number;
|
|
|
|
let currentElement: any;
|
2022-10-15 18:00:24 +00:00
|
|
|
|
2022-10-28 19:08:34 +00:00
|
|
|
while (minIndex <= maxIndex) {
|
|
|
|
currentIndex = ((minIndex + maxIndex) / 2) | 0;
|
|
|
|
currentElement = key ? arr[currentIndex][key] : arr[currentIndex];
|
2022-10-15 18:00:24 +00:00
|
|
|
|
2022-10-28 19:08:34 +00:00
|
|
|
if (currentElement < elem) {
|
|
|
|
minIndex = currentIndex + 1;
|
|
|
|
} else if (currentElement > elem) {
|
|
|
|
maxIndex = currentIndex - 1;
|
|
|
|
} else {
|
|
|
|
return currentIndex;
|
2022-10-15 18:00:24 +00:00
|
|
|
}
|
2022-10-28 19:08:34 +00:00
|
|
|
}
|
2022-10-15 18:00:24 +00:00
|
|
|
|
2022-10-28 19:08:34 +00:00
|
|
|
return minIndex;
|
2022-10-15 18:00:24 +00:00
|
|
|
}
|
|
|
|
|
2022-10-16 02:55:53 +00:00
|
|
|
/**
|
|
|
|
* Round a number to N decimal places
|
|
|
|
* @param num Number to round
|
|
|
|
* @param places Number of decimal places
|
2022-10-16 03:33:07 +00:00
|
|
|
* @param floor If true, round down instead of to nearest
|
2022-10-16 02:55:53 +00:00
|
|
|
*/
|
2022-10-28 19:08:34 +00:00
|
|
|
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;
|
2022-10-16 02:55:53 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Round to nearest 0.5. Useful for pixels.
|
|
|
|
* @param num Number to round
|
|
|
|
*/
|
|
|
|
export function roundHalf(num: number) {
|
2022-10-28 19:08:34 +00:00
|
|
|
return Math.round(num * 2) / 2;
|
2022-10-16 02:55:53 +00:00
|
|
|
}
|
|
|
|
|
2022-10-18 17:52:44 +00:00
|
|
|
/** Choose a random element from an array */
|
|
|
|
export function randomChoice(arr: any[]) {
|
2022-10-28 19:08:34 +00:00
|
|
|
return arr[Math.floor(Math.random() * arr.length)];
|
2022-10-18 17:52:44 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Choose a random sub array from an array
|
|
|
|
* https://stackoverflow.com/a/11935263/4745239
|
|
|
|
*/
|
|
|
|
export function randomSubarray(arr: any[], size: number) {
|
2022-10-28 19:08:34 +00:00
|
|
|
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);
|
2022-10-18 17:52:44 +00:00
|
|
|
}
|
|
|
|
|
2022-10-16 03:28:40 +00:00
|
|
|
/**
|
|
|
|
* Convert server-side flags to bitmask
|
|
|
|
* @param photo Photo to process
|
|
|
|
*/
|
|
|
|
export function convertFlags(photo: IPhoto) {
|
2022-10-28 19:08:34 +00:00
|
|
|
if (typeof photo.flag === "undefined") {
|
|
|
|
photo.flag = 0; // flags
|
|
|
|
}
|
2022-10-23 18:53:00 +00:00
|
|
|
|
2022-10-28 19:08:34 +00:00
|
|
|
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;
|
|
|
|
}
|
2022-10-16 03:28:40 +00:00
|
|
|
}
|
|
|
|
|
2022-10-29 05:24:19 +00:00
|
|
|
/**
|
|
|
|
* Get the path of the folder on folders route
|
|
|
|
* This function does not check if this is the folder route
|
|
|
|
*/
|
|
|
|
export function getFolderRoutePath(basePath: string) {
|
|
|
|
let path: any = vuerouter.currentRoute.params.path || "/";
|
|
|
|
path = typeof path === "string" ? path : path.join("/");
|
|
|
|
path = basePath + "/" + path;
|
|
|
|
path = path.replace(/\/\/+/, "/"); // Remove double slashes
|
|
|
|
return path;
|
|
|
|
}
|
|
|
|
|
2022-10-25 01:34:46 +00:00
|
|
|
// Outside for set
|
|
|
|
const TagDayID = {
|
2022-10-28 19:08:34 +00:00
|
|
|
START: -(1 << 30),
|
|
|
|
FOLDERS: -(1 << 30) + 1,
|
|
|
|
TAGS: -(1 << 30) + 2,
|
|
|
|
FACES: -(1 << 30) + 3,
|
|
|
|
ALBUMS: -(1 << 30) + 4,
|
|
|
|
};
|
2022-10-25 01:34:46 +00:00
|
|
|
|
2022-10-15 18:00:24 +00:00
|
|
|
/** Global constants */
|
2022-10-06 23:28:35 +00:00
|
|
|
export const constants = {
|
2022-10-28 19:08:34 +00:00
|
|
|
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,
|
|
|
|
},
|
2022-10-06 23:28:35 +00:00
|
|
|
|
2022-10-28 19:08:34 +00:00
|
|
|
TagDayID: TagDayID,
|
|
|
|
TagDayIDValueSet: new Set(Object.values(TagDayID)),
|
|
|
|
};
|
2022-10-16 19:58:38 +00:00
|
|
|
|
|
|
|
/** Cache store */
|
2022-10-16 23:41:27 +00:00
|
|
|
let staticCache: Cache | null = null;
|
2022-10-28 19:08:34 +00:00
|
|
|
const cacheName = `memories-${loadState("memories", "version")}-${
|
2022-10-28 23:21:08 +00:00
|
|
|
getCurrentUser()?.uid
|
2022-10-28 19:08:34 +00:00
|
|
|
}`;
|
|
|
|
openCache().then((cache) => {
|
|
|
|
staticCache = cache;
|
|
|
|
});
|
2022-10-16 23:41:27 +00:00
|
|
|
|
2022-10-17 02:17:56 +00:00
|
|
|
// Clear all caches except the current one
|
2022-10-21 04:28:10 +00:00
|
|
|
window.caches?.keys().then((keys) => {
|
2022-10-28 19:08:34 +00:00
|
|
|
keys
|
|
|
|
.filter((key) => key.startsWith("memories-") && key !== cacheName)
|
|
|
|
.forEach((key) => {
|
|
|
|
window.caches.delete(key);
|
2022-10-17 02:17:56 +00:00
|
|
|
});
|
2022-10-23 18:01:53 +00:00
|
|
|
});
|
2022-10-17 02:17:56 +00:00
|
|
|
|
2022-10-16 23:41:27 +00:00
|
|
|
/** Open the cache */
|
|
|
|
export async function openCache() {
|
2022-10-28 19:08:34 +00:00
|
|
|
try {
|
|
|
|
return await window.caches?.open(cacheName);
|
|
|
|
} catch {
|
|
|
|
console.warn("Failed to get cache", cacheName);
|
|
|
|
return null;
|
|
|
|
}
|
2022-10-16 23:41:27 +00:00
|
|
|
}
|
2022-10-16 19:58:38 +00:00
|
|
|
|
|
|
|
/** Get data from the cache */
|
2022-10-18 18:35:38 +00:00
|
|
|
export async function getCachedData<T>(url: string): Promise<T> {
|
2022-10-28 19:08:34 +00:00
|
|
|
if (!window.caches) return null;
|
|
|
|
const cache = staticCache || (await openCache());
|
|
|
|
if (!cache) return null;
|
2022-10-23 18:01:53 +00:00
|
|
|
|
2022-10-28 19:08:34 +00:00
|
|
|
const cachedResponse = await cache.match(url);
|
|
|
|
if (!cachedResponse || !cachedResponse.ok) return undefined;
|
|
|
|
return await cachedResponse.json();
|
2022-10-16 19:58:38 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/** Store data in the cache */
|
2022-10-19 18:22:43 +00:00
|
|
|
export function cacheData(url: string, data: Object) {
|
2022-10-28 19:08:34 +00:00
|
|
|
if (!window.caches) return;
|
|
|
|
const str = JSON.stringify(data);
|
2022-10-23 18:01:53 +00:00
|
|
|
|
2022-10-28 19:08:34 +00:00
|
|
|
(async () => {
|
|
|
|
const cache = staticCache || (await openCache());
|
|
|
|
if (!cache) return;
|
2022-10-23 18:01:53 +00:00
|
|
|
|
2022-10-28 19:08:34 +00:00
|
|
|
const response = new Response(str);
|
|
|
|
response.headers.set("Content-Type", "application/json");
|
|
|
|
await cache.put(url, response);
|
|
|
|
})();
|
|
|
|
}
|