import { api as api_type } from "app/types"; // import { format, formatDistance } from 'date-fns' import format from "date-fns/format"; import formatDistance from "date-fns/formatDistance"; export const formatDateTime = (dateStr) => format(new Date(dateStr), "d MMM yyyy H:mm"); export const formatDate = (dateStr) => format(new Date(dateStr), "d MMM yyyy"); export const formatTime = (dateStr) => format(new Date(dateStr), "H:mm"); export const formatAge = (dateStr) => formatDistance(new Date(), new Date(dateStr)) + " ago."; export const unslugify = (fn) => fn.replace(/-/g, " ").replace(/_/g, " ").replace(".mp3", ""); /* Mobile check */ export const isiPhone = !!( navigator.userAgent.match(/iPhone/i) || navigator.userAgent.match(/iPod/i) ); export const isiPad = !!navigator.userAgent.match(/iPad/i); export const isAndroid = !!navigator.userAgent.match(/Android/i); export const isMobile = isiPhone || isiPad || isAndroid; export const isDesktop = !isMobile; const htmlClassList = document.body.parentNode.classList; htmlClassList.add(isDesktop ? "desktop" : "mobile"); /* Default image dimensions */ export const widths = { th: 160, sm: 320, md: 640, lg: 1280, }; /* Formatting functions */ const acronyms = "id url cc sa fp md5 sha256".split(" ").map((s) => "_" + s); const acronymsUpperCase = acronyms.map((s) => s.toUpperCase()); export const formatName = (s) => { acronyms.forEach( (acronym, i) => (s = s.replace(acronym, acronymsUpperCase[i])) ); return s.replace(/_/g, " "); }; // Use to pad frame numbers with zeroes export const pad = (n, m) => { let s = String(n || 0); while (s.length < m) { s = "0" + s; } return s; }; export const courtesyS = (n, s) => n + " " + (n === 1 ? s : s + "s"); export const capitalize = (s) => s.split(" ").map(capitalizeWord).join(" "); export const capitalizeWord = (s) => s.substr(0, 1).toUpperCase() + s.substr(1); export const padSeconds = (n) => (n < 10 ? "0" + n : n); export const commatize = (n, radix) => { radix = radix || -1; var nums = [], i, counter = 0, r = Math.floor; if (radix !== -1 && n > radix) { n /= radix; nums.unshift(r((n * 10) % 10)); nums.unshift("."); } do { i = r(n % 10); n = r(n / 10); counter += 1; if (n && !(counter % 3)) { i = "," + r(i); } nums.unshift(i); } while (n); return nums.join(""); }; export const timestamp = (n = 0, fps = 25) => { n /= fps; let s = padSeconds(Math.round(n) % 60); n = Math.floor(n / 60); if (n > 60) { return Math.floor(n / 60) + ":" + padSeconds(n % 60) + ":" + s; } return (n % 60) + ":" + s; }; export const timestampToSeconds = (time_str) => { const time_str_parts = (time_str || "") .trim() .split(":") .map((s) => parseFloat(s)); if (time_str_parts.length === 3) { return ( (time_str_parts[0] * 60 + time_str_parts[1]) * 60 + time_str_parts[2] ); } if (time_str_parts.length === 2) { return time_str_parts[0] * 60 + time_str_parts[1]; } return time_str_parts[0]; }; export const percent = (n) => (n * 100).toFixed(1) + "%"; export const px = (n, w) => Math.round(n * w) + "px"; export const clamp = (n, a = 0, b = 1) => (n < a ? a : n < b ? n : b); export const dist = (x1, y1, x2, y2) => Math.sqrt(Math.pow(x1 - x2, 2) + Math.pow(y1 - y2, 2)); export const mod = (n, m) => n - m * Math.floor(n / m); export const angle = (x1, y1, x2, y2) => Math.atan2(y2 - y1, x2 - x1); export const randint = (n) => Math.floor(Math.random() * n); /* URLs */ export const sha256_tree = (sha256, branch_size = 2, tree_depth = 2) => { const tree_size = tree_depth * branch_size; let tree = ""; for (var i = 0; i < tree_size; i += branch_size) { tree += "/" + sha256.substr(i, branch_size); } return tree; }; export const imageUrl = (sha256, frame, size = "th") => [ "https://" + process.env.S3_HOST + "/v1/media/keyframes", sha256_tree(sha256), pad(frame, 6), size, "index.jpg", ] .filter((s) => !!s) .join("/"); export const uploadUri = ({ sha256, ext }) => "/static/data/uploads" + sha256_tree(sha256) + "/" + sha256 + ext; export const metadataUri = (sha256, tag) => "/metadata/" + sha256 + "/" + tag + "/"; export const keyframeUri = (sha256, frame) => "/metadata/" + sha256 + "/keyframe/" + pad(frame, 6) + "/"; export const preloadImage = (url) => new Promise((resolve, reject) => { const image = new Image(); let loaded = false; image.onload = () => { if (loaded) return; loaded = true; image.onload = null; image.onerror = null; console.log(image); resolve(image); }; image.onerror = () => { if (loaded) return; image.onload = null; image.onerror = null; resolve(image); }; // console.log(img.src) // image.crossOrigin = 'anonymous' image.src = url; if (image.complete) { image.onload(); } }); export const preloadVideo = (url, options = {}) => new Promise((resolve, reject) => { const { canplaythrough } = options; const video = document.createElement("video"); let loaded = false; const bind = () => { video.addEventListener( canplaythrough ? "canplaythrough" : "loadedmetadata", onload ); video.addEventListener("error", onerror); }; const unbind = () => { video.removeEventListener( canplaythrough ? "canplaythrough" : "loadedmetadata", onload ); video.removeEventListener("error", onerror); }; const onload = () => { if (loaded) return; loaded = true; unbind(); resolve(video); }; const onerror = (error) => { if (loaded) return; loaded = true; unbind(); reject(error); }; bind(); video.preload = "auto"; video.src = url; }); export const preloadAudio = (url) => new Promise((resolve, reject) => { const audio = document.createElement("audio"); let loaded = false; const bind = () => { audio.addEventListener("loadedmetadata", onload); audio.addEventListener("error", onerror); }; const unbind = () => { audio.removeEventListener("loadedmetadata", onload); audio.removeEventListener("error", onerror); }; const onload = () => { if (loaded) return; loaded = true; unbind(); resolve(audio); }; const onerror = (error) => { if (loaded) return; loaded = true; unbind(); reject(error); }; bind(); audio.preload = "auto"; audio.src = url; }); export const cropImage = (url, crop) => { return new Promise((resolve, reject) => { let { x, y, w, h } = crop; const image = new Image(); let loaded = false; x = parseFloat(x); y = parseFloat(y); w = parseFloat(w); h = parseFloat(h); image.onload = () => { if (loaded) return; loaded = true; image.onload = null; const canvas = document.createElement("canvas"); const ctx = canvas.getContext("2d"); const width = image.naturalWidth; const height = image.naturalHeight; canvas.width = w * width; canvas.height = h * height; ctx.drawImage( image, Math.round(x * width), Math.round(y * height), Math.round(w * width), Math.round(h * height), 0, 0, canvas.width, canvas.height ); resolve(canvas); }; image.onerror = () => { console.log("image error"); reject(); }; // console.log(img.src) image.crossOrigin = "anonymous"; image.src = url; if (image.complete) { image.onload(); } }); }; export const urlSearchParamsToDict = (search) => { const params = new URLSearchParams(search); const dict = {}; params.forEach((value, key) => { // ??? dict[key] = value; }); return dict; }; /* AJAX */ let cachedAuth = null; let token = ""; let username = ""; export const post = (dispatch, type = api_type, tag, url, data) => { let headers; if (data instanceof FormData) { headers = { Accept: "application/json", }; } else if (data) { headers = { Accept: "application/json", "Content-Type": "application/json; charset=utf-8", }; data = JSON.stringify(data); } dispatch({ type: type.loading, tag, }); return fetch(url, { method: "POST", body: data, headers, }) .then((res) => res.json()) .then((res) => dispatch({ type: type.loaded, tag, data: res, }) ) .catch((err) => dispatch({ type: type.error, tag, err, }) ); }; export const api = (dispatch, type = api_type, tag, url, data) => { dispatch({ type: type.loading, tag, }); if (url.indexOf("http") !== 0) { url = window.location.origin + url; } url = new URL(url); if (data) { url.search = new URLSearchParams(data).toString(); } return fetch(url, { method: "GET", // mode: 'cors', }) .then((res) => res.json()) .then((res) => dispatch({ type: type.loaded, tag, data: res, }) ) .catch((err) => dispatch({ type: type.error, tag, err, }) ); }; /* sorting */ export const numericSort = { asc: (a, b) => a[0] - b[0], desc: (a, b) => b[0] - a[0], }; export const stringSort = { asc: (a, b) => a[0].localeCompare(b[0]), desc: (a, b) => b[0].localeCompare(a[0]), }; export const orderByFn = (s = "name asc") => { const [field = "name", direction = "asc"] = s.split(" "); let mapFn, sortFn; switch (field) { case "id": mapFn = (a) => [parseInt(a.id) || 0, a]; sortFn = numericSort[direction]; break; case "epoch": mapFn = (a) => [parseInt(a.epoch || a.epochs) || 0, a]; sortFn = numericSort[direction]; break; case "size": mapFn = (a) => [parseInt(a.size) || 0, a]; sortFn = numericSort[direction]; break; case "date": mapFn = (a) => [+new Date(a.date || a.created_at), a]; sortFn = numericSort[direction]; break; case "updated_at": mapFn = (a) => [+new Date(a.updated_at), a]; sortFn = numericSort[direction]; break; case "priority": mapFn = (a) => [parseInt(a.priority) || parseInt(a.id) || 1000, a]; sortFn = numericSort[direction]; case "name": default: mapFn = (a) => [a.name || "", a]; sortFn = stringSort[direction]; break; } return { mapFn, sortFn }; }; export const getOrderedIds = (objects, sort, prepend = []) => { const { mapFn, sortFn } = orderByFn(sort); return prepend.concat( objects .map(mapFn) .sort(sortFn) .map((a) => a[1].id) ); }; export const getOrderedIdsFromLookup = (lookup, sort) => { return getOrderedIds( Object.keys(lookup).map((key) => lookup[key]), sort ); }; /* parallel promises */ export const allProgress = (promises, progress_cb) => { let d = 0; progress_cb(0, 0, promises.length); promises.forEach((p) => { p.then((s) => { d += 1; progress_cb(Math.floor((d * 100) / promises.length), d, promises.length); return s; }); }); return Promise.all(promises); }; /* Clipboard */ export function writeToClipboard(str) { return new Promise((resolve, reject) => { let success = false; function listener(e) { e.clipboardData.setData("text/plain", str); e.preventDefault(); success = true; } document.addEventListener("copy", listener); document.execCommand("copy"); document.removeEventListener("copy", listener); if (success) { resolve(); } else { reject(); } }); }