summaryrefslogtreecommitdiff
path: root/frontend/app/utils/index.js
diff options
context:
space:
mode:
Diffstat (limited to 'frontend/app/utils/index.js')
-rw-r--r--frontend/app/utils/index.js331
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