diff options
Diffstat (limited to 'frontend/app/utils/index.js')
| -rw-r--r-- | frontend/app/utils/index.js | 331 |
1 files changed, 331 insertions, 0 deletions
diff --git a/frontend/app/utils/index.js b/frontend/app/utils/index.js new file mode 100644 index 0000000..bb5e01d --- /dev/null +++ b/frontend/app/utils/index.js @@ -0,0 +1,331 @@ +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.' + +/* 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 padSeconds = n => n < 10 ? '0' + n : n + +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 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) + +/* 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 + 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 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() + } + }) +}
\ No newline at end of file |
