diff options
Diffstat (limited to 'animism-align/frontend/app/utils/index.js')
| -rw-r--r-- | animism-align/frontend/app/utils/index.js | 371 |
1 files changed, 371 insertions, 0 deletions
diff --git a/animism-align/frontend/app/utils/index.js b/animism-align/frontend/app/utils/index.js new file mode 100644 index 0000000..c2dd464 --- /dev/null +++ b/animism-align/frontend/app/utils/index.js @@ -0,0 +1,371 @@ +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 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 timestamp = (n = 0, fps = 1, ms = false) => { + if (n < 0) return '' + let s = '' + n /= fps + if (ms) { + const mantissa = Math.round((n % 1) * 10) + s = '.' + mantissa + } + n = Math.round(n) + s = padSeconds(n % 60) + s + 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) +export const floatLT = (a,b) => ((a*10|0) < (b*10|0)) +export const floatLTE = (a,b) => ((a*10|0) === (b*10|0) || floatLT(a,b)) + +/* 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 preloadImage = (url, anonymous=false) => ( + new Promise((resolve, reject) => { + if (typeof url === 'object' && url instanceof Image) { + return resolve(url) + } + 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) + if (anonymous) { + image.crossOrigin = 'anonymous' + } + image.src = url + if (image.complete) { + image.onload() + } + }) +) + +export const cropImage = (url, crop, maxSide) => { + return new Promise((resolve, reject) => { + preloadImage(url, true) + .then(image => { + let { x, y, w, h } = crop + x = parseFloat(x) + y = parseFloat(y) + w = parseFloat(w) + h = parseFloat(h) + const canvas = document.createElement('canvas') + const ctx = canvas.getContext('2d') + const { naturalWidth, naturalHeight } = image + + let width, height + let cropWidth = naturalWidth * w + let cropHeight = naturalHeight * h + + if (maxSide > 0) { + if (cropWidth > cropHeight) { + width = Math.min(maxSide, cropWidth) + height = cropHeight * width / cropWidth + } else { + height = Math.min(maxSide, cropHeight) + width = cropWidth * height / cropHeight + } + } else { + width = cropWidth + height = cropHeight + } + + canvas.width = w * width + canvas.height = h * height + + ctx.drawImage( + image, + Math.round(x * naturalWidth), + Math.round(y * naturalHeight), + Math.round(w * naturalWidth), + Math.round(h * naturalHeight), + 0, 0, canvas.width, canvas.height + ) + // console.log(x, y, w, h) + // console.log(naturalWidth, naturalHeight) + // console.log(width, height) + resolve(canvas) + }) + }) +} +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() + } + let req = fetch(url, { + method: 'GET', + // mode: 'cors', + }) + // console.log(tag) + if (tag === 'text') { + req = req.then(res => res.text()) + } else { + req = req.then(res => res.json()) + } + req = req.then(res => { + dispatch({ + type: type.loaded, + tag, + data: res, + }) + return res + }) + .catch(err => dispatch({ + type: type.error, + tag, + err, + })) + return req +} + +/* 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 'start_ts': + mapFn = a => [parseFloat(a.start_ts) || 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] + break + case 'title': + mapFn = a => [a.title || "", a] + sortFn = stringSort[direction] + break + case 'author': + mapFn = a => { + let author = (a.author || "").split(' and ')[0].split(' ') + author.unshift(author.pop()) + author = author.join(' ') + return [author, a] + } + sortFn = stringSort[direction] + break + case 'name': + default: + mapFn = a => [a.name || "", a] + sortFn = stringSort[direction] + break + } + return { mapFn, sortFn } +} +export const getOrderedIds = (objects, sort, prepend) => { + if (!prepend) { + 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 |
