diff options
| author | Jules Laplace <julescarbon@gmail.com> | 2018-06-03 02:25:34 +0200 |
|---|---|---|
| committer | Jules Laplace <julescarbon@gmail.com> | 2018-06-03 02:25:34 +0200 |
| commit | 46fbedf0ad7b167a28daf2030e06f34480576394 (patch) | |
| tree | 81877c97f777a8d705d47bb97ac96e5fa244bd2b /app/client | |
| parent | 2149eb581c35a93d41dbad6e3409c498b4bed804 (diff) | |
add results page
Diffstat (limited to 'app/client')
| -rw-r--r-- | app/client/common/fileList.component.js | 68 | ||||
| -rw-r--r-- | app/client/modules/samplernn/index.js | 3 | ||||
| -rw-r--r-- | app/client/modules/samplernn/samplernn.actions.js | 109 | ||||
| -rw-r--r-- | app/client/modules/samplernn/samplernn.datasets.js | 60 | ||||
| -rw-r--r-- | app/client/modules/samplernn/samplernn.loss.js | 10 | ||||
| -rw-r--r-- | app/client/modules/samplernn/samplernn.results.js | 108 | ||||
| -rw-r--r-- | app/client/util/index.js | 159 | ||||
| -rw-r--r-- | app/client/util/sort.js | 32 |
8 files changed, 413 insertions, 136 deletions
diff --git a/app/client/common/fileList.component.js b/app/client/common/fileList.component.js index 13eb300..c16928f 100644 --- a/app/client/common/fileList.component.js +++ b/app/client/common/fileList.component.js @@ -5,7 +5,7 @@ import { Link } from 'react-router-dom'; import moment from 'moment' import * as util from '../util' -const defaultFields = new Set(['date', 'size']) +const defaultFields = new Set(['name', 'date', 'size']) export const FileList = props => { const { @@ -17,19 +17,19 @@ export const FileList = props => { fileListClassName='filelist', rowClassName='row file' } = props - const { mapFn, sortFn } = orderByFn(orderBy) + const { mapFn, sortFn } = util.sort.orderByFn(orderBy) const fileList = (files || []) .map(mapFn) .sort(sortFn) - .map(pair => - <FileRow + .map(pair => { + return <FileRow file={pair[1]} fields={fieldSet(fields)} className={rowClassName} linkFiles onClick /> - ) + }) return ( <div className={'rows ' + className}> <div class='row heading'> @@ -45,39 +45,6 @@ export const FileList = props => { ) } -const numericSort = { - asc: (a,b) => a[0] - b[0], - desc: (a,b) => b[0] - a[0], -} -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 'epoch': - mapFn = a => [a.epoch || a.epochs, a] - sortFn = numericSort[direction] - break - case 'size': - mapFn = a => [a.size, a] - sortFn = numericSort[direction] - break - case 'date': - mapFn = a => [+new Date(a.date || a.created_at), a] - sortFn = numericSort[direction] - break - case 'name': - default: - mapFn = a => [a.id || a.name, a] - sortFn = stringSort[direction] - break - } - return { mapFn, sortFn } -} - export const fieldSet = fields => { if (fields) { if (fields instanceof Set) { @@ -94,23 +61,29 @@ export const FileRow = props => { const size = util.hush_size(file.size) const date = file.date || file.created_at + const epoch = file.epoch || file.epochs || 0 return ( <div class={className} key={file.name}> - <div className="filename" title={file.name || file.url}> - {file.persisted === false - ? <span>{file.name || file.url}</span> - : (linkFiles && file.url) - ? <a target='_blank' href={file.url}>{file.name || file.url}</a> - : <span class='link' onClick={() => onClick(file)}>{file.name || file.url}</span> - } - </div> + {fields.has('name') && + <div className="filename" title={file.name || file.url}> + {file.persisted === false + ? <span className='unpersisted'>{file.name || file.url}</span> + : (linkFiles && file.url) + ? <a target='_blank' href={file.url}>{file.name || file.url}</a> + : <span class='link' onClick={() => onClick(file)}>{file.name || file.url}</span> + } + </div> + } {fields.has('age') && <div className={"age " + util.carbon_date(date)}>{util.get_age(date)}</div> } {fields.has('username') && <div className={"username"}>{username}</div> } + {fields.has('epoch') && + <div className={"epoch " + util.hush_null(epoch)[0]}>{epoch > 0 ? 'ep. ' + epoch : ''}</div> + } {fields.has('date') && <div className={"date " + util.carbon_date(date)}>{moment(date).format("YYYY-MM-DD")}</div> } @@ -120,9 +93,6 @@ export const FileRow = props => { {fields.has('size') && <div className={"size " + size[0]}>{size[1]}</div> } - {fields.has('epoch') && - <div className="epoch">{file.epoch > 0 ? 'epoch ' + file.epoch : ' '}</div> - } {(fields.has('activity') || fields.has('module')) && <div className='activity'> {fields.has('activity') && file.activity} diff --git a/app/client/modules/samplernn/index.js b/app/client/modules/samplernn/index.js index 7066942..e1c41d5 100644 --- a/app/client/modules/samplernn/index.js +++ b/app/client/modules/samplernn/index.js @@ -1,6 +1,7 @@ import { h, Component } from 'preact' import { Route, Link } from 'react-router-dom' import SampleRNNDatasets from './samplernn.datasets' +import SampleRNNResults from './samplernn.results' import SampleRNNInspect from './samplernn.inspect' import SampleRNNLoss from './samplernn.loss' @@ -9,8 +10,10 @@ function router () { <div> <Route exact path='/samplernn/graph/' component={SampleRNNLoss} /> <Route exact path='/samplernn/inspect/' component={SampleRNNInspect} /> + <Route exact path='/samplernn/datasets/' component={SampleRNNDatasets} /> <Route exact path='/samplernn/datasets/:id/' component={SampleRNNDatasets} /> <Route exact path='/samplernn/datasets/new/' component={SampleRNNDatasets} /> + <Route exact path='/samplernn/results/' component={SampleRNNResults} /> </div> ) } diff --git a/app/client/modules/samplernn/samplernn.actions.js b/app/client/modules/samplernn/samplernn.actions.js index 1a60719..e8f8251 100644 --- a/app/client/modules/samplernn/samplernn.actions.js +++ b/app/client/modules/samplernn/samplernn.actions.js @@ -12,16 +12,38 @@ export const load_directories = (id) => (dispatch) => { actions.socket.list_directory({ module: 'samplernn', dir: 'datasets' }), actions.socket.list_directory({ module: 'samplernn', dir: 'results' }), actions.socket.list_directory({ module: 'samplernn', dir: 'output' }), + load_loss()(dispatch), ]).then(res => { // console.log(res) - const [folders, files, tasks, datasets, results, output] = res + const [folders, files, tasks, datasets, results, output, lossReport] = res - const empty_dataset = (name) => ({ - name, - input: [], - checkpoints: [], - output: [], - }) + const unsortedFolder = { + id: 0, + name: 'unsorted', + datasets: [], + } + + const datasetLookup = {} + + const get_dataset = (name, folder=unsortedFolder, date) => { + const dataset = datasetLookup[name] || empty_dataset(name, folder) + if (date) { + dataset.date = dataset.date ? Math.max(+new Date(date), dataset.date) : +new Date(date) + } + return dataset + } + + const empty_dataset = (name, folder=unsortedFolder) => { + const dataset = { + name, + input: [], + checkpoints: [], + output: [], + } + datasetLookup[dataset.name] = dataset + folder.datasets.push(dataset) + return dataset + } // take all of the folders and put them in a lookup const folderLookup = folders.reduce((folderLookup, folder) => { @@ -29,11 +51,7 @@ export const load_directories = (id) => (dispatch) => { folder.datasets = [] return folderLookup }, { - 'unsorted': { - id: 0, - name: 'unsorted', - datasets: [], - } + unsorted: unsortedFolder }) // prepare the files by splitting into two groups @@ -41,61 +59,34 @@ export const load_directories = (id) => (dispatch) => { const ungeneratedFiles = files.filter(file => !file.generated) // build the initial dataset lookup table using the ungenerated files - const datasetLookup = ungeneratedFiles.reduce((datasetLookup, file) => { - file.checkpoints = [] + ungeneratedFiles.reduce((datasetLookup, file) => { if (! file.name) { file.name = (file.opt || {}).token || file.url - // datasetLookup[] = file } const name = (file.name || 'unsorted').split('.')[0] - if (! datasetLookup[name]) { - const dataset = empty_dataset(name) - datasetLookup[dataset.name] = dataset - dataset.input.push(file) - folderLookup[file.folder_id].datasets.push(dataset) - } - else { - datasetLookup[name].input.push(file) - } + const dataset = get_dataset(name, folderLookup[file.folder_id], unsortedFolder, file.date) + dataset.input.push(file) return datasetLookup - }, { - unsorted: empty_dataset('unsorted') - }) + }, datasetLookup) // go over the generated files and add addl datasets (if the files were deleted) generatedFiles.map(file => { const pair = file.name.split('.')[0].split('-') - let dataset = datasetLookup[pair[0]] - if (!dataset) { - dataset = empty_dataset(pair[0]) - datasetLookup[dataset.name] = dataset - folderLookup[file.folder_id].datasets.push(dataset) - } + const dataset = get_dataset(pair[0], folderLookup[file.folder_id], unsortedFolder, file.date) dataset.output.push(file) file.epoch = file.epoch || pair[1] }) - // console.log(datasets) const flatDatasets = datasets.filter(s => s.name.match(/(wav|aiff?|flac|mp3)$/) && !s.dir) const builtDatasets = datasets.filter(s => s.dir) builtDatasets.forEach(dir => { - let dataset = datasetLookup[dir.name] - if (! dataset) { - dataset = empty_dataset(dir.name) - datasetLookup[dataset.name] = dataset - folderLookup.unsorted.datasets.push(dataset) - } + const dataset = get_dataset(dir.name) dataset.isBuilt = true }) flatDatasets.forEach(file => { const name = file.name.split('.')[0] - let dataset = datasetLookup[name] - if (! dataset) { - dataset = empty_dataset(name) - datasetLookup[dataset.name] = dataset - folderLookup.unsorted.datasets.push(dataset) - } + const dataset = get_dataset(name, unsortedFolder, file.date) file.persisted = false dataset.input.push(file) }) @@ -107,14 +98,15 @@ export const load_directories = (id) => (dispatch) => { .map(s => s.split(':')) .filter(b => b.length && b[1]) .reduce((a,b) => (a[b[0]] = b[1]) && a, {}) - checkpoint.name = checkpoint.dataset || checkpoint.exp + checkpoint.name = checkpoint.name || checkpoint.dataset || checkpoint.exp checkpoint.date = s.date checkpoint.dir = s - let dataset = datasetLookup[checkpoint.dataset] - if (! dataset) { - dataset = empty_dataset(checkpoint.dataset) - datasetLookup[dataset.name] = dataset - folderLookup.unsorted.datasets.push(dataset) + checkpoint.persisted = false + const dataset = get_dataset(checkpoint.name, unsortedFolder, checkpoint.date) + const loss = lossReport[checkpoint.name] + if (loss) { + dataset.epoch = checkpoint.epoch = loss.length + checkpoint.training_loss = loss } dataset.checkpoints.push(checkpoint) return checkpoint @@ -122,14 +114,10 @@ export const load_directories = (id) => (dispatch) => { output.map(file => { const pair = file.name.split('.')[0].split('-') - let dataset = datasetLookup[pair[0]] - if (!dataset) { - dataset = empty_dataset(pair[0]) - datasetLookup[dataset.name] = dataset - folderLookup.unsorted.datasets.push(dataset) - } + const dataset = get_dataset(pair[0], unsortedFolder, file.date) file.persisted = false - file.epoch = file.epoch || pair[1].replace(/^\D+/, '') + file.epoch = parseInt(file.epoch || pair[1].replace(/^\D+/, '')) || 0 + dataset.epoch = Math.max(file.epoch, dataset.epoch || 0) dataset.output.push(file) }) @@ -156,7 +144,7 @@ export const load_directories = (id) => (dispatch) => { } export const load_loss = () => dispatch => { - actions.socket.run_script({ module: 'samplernn', activity: 'report' }) + return actions.socket.run_script({ module: 'samplernn', activity: 'report' }) .then(report => { const lossReport = {} report.stdout.split('\n\n').filter(a=>!!a).forEach(data => { @@ -175,6 +163,7 @@ export const load_loss = () => dispatch => { type: types.samplernn.load_loss, lossReport }) + return lossReport }) } diff --git a/app/client/modules/samplernn/samplernn.datasets.js b/app/client/modules/samplernn/samplernn.datasets.js index b6a12cc..aa729a5 100644 --- a/app/client/modules/samplernn/samplernn.datasets.js +++ b/app/client/modules/samplernn/samplernn.datasets.js @@ -1,6 +1,7 @@ import { h, Component } from 'preact' import { bindActionCreators } from 'redux' import { connect } from 'react-redux' +import * as util from '../../util' import * as samplernnActions from './samplernn.actions' @@ -18,11 +19,15 @@ class SampleRNNDatasets extends Component { super() this.fileOptions = this.fileOptions.bind(this) this.pickFile = this.pickFile.bind(this) - let id = props.match.params.id + let id = props.match.params.id || localStorage.getItem('samplernn.last_id') if (! id && props.location.pathname.match(/\/new\//)) { id = 'new' + } else if (id) { + localStorage.setItem('samplernn.last_id', id) + } + if (id && (! props.samplernn.folder || props.samplernn.folder.id !== id)) { + props.actions.load_directories(id) } - props.actions.load_directories(id) } pickFile(file){ console.log('pick', file) @@ -50,28 +55,45 @@ class SampleRNNDatasets extends Component { </div> ) } - fetchURL(url) { - } render(){ const { samplernn } = this.props const folder = samplernn.folder if (!folder.name) return - console.log(folder) - const datasets = folder.datasets.map(dataset =>{ - console.log(dataset) + const { mapFn, sortFn } = util.sort.orderByFn('date desc') + const datasets = folder.datasets.map(mapFn).sort(sortFn).map(pair => { + const dataset = pair[1] return ( <div className='row dataset'> <div className='col'> - {!!dataset.input.length && <FileList files={dataset.input} className='input_files' fileListClassName='' rowClassName='input_file' />} + {!!dataset.input.length && + <FileList + files={dataset.input} + className='input_files' + fileListClassName='' + rowClassName='input_file' + /> + } </div> <div className='col quiet'> - <div>{dataset.isBuilt ? 'has dataset' : 'not built'}</div> + <div>{dataset.isBuilt ? 'cached' : ''}</div> </div> <div className='col checkpoint'> - {!!dataset.checkpoints.length && <FileRow file={dataset.checkpoints[0]} />} + {!!dataset.checkpoints.length && + <FileRow + file={dataset.checkpoints[0]} + fields={'name date epoch'} + className='row checkpoint' + /> + } </div> <div className='col'> - {!!dataset.output.length && <FileList files={dataset.output} orderBy='epoch desc' />} + {!!dataset.output.length && + <FileList + files={dataset.output} + orderBy='epoch desc' + fields={'name date epoch size'} + /> + } </div> </div> ) @@ -79,12 +101,12 @@ class SampleRNNDatasets extends Component { return ( <div className='app'> <div className='heading'> - <h3>SampleRNN</h3> + <h2>SampleRNN</h2> </div> <div class='rows params datasets'> <div class='row dataset'> <div class='col'>input</div> - <div class='col'>dataset</div> + <div class='col'></div> <div class='col'>checkpoint</div> <div class='col'>output</div> </div> @@ -95,18 +117,6 @@ class SampleRNNDatasets extends Component { } } -// <Dataset -// canRename -// canUpload -// canAddURL -// canDeleteFile -// linkFiles -// fileOptions={this.fileOptions} -// onPick={this.handlePick} -// folder={samplernn.folder} -// files={samplernn.folder.files} -// /> - const mapStateToProps = state => ({ samplernn: state.module.samplernn, runner: state.system.runner, diff --git a/app/client/modules/samplernn/samplernn.loss.js b/app/client/modules/samplernn/samplernn.loss.js index 3900c31..04d16c8 100644 --- a/app/client/modules/samplernn/samplernn.loss.js +++ b/app/client/modules/samplernn/samplernn.loss.js @@ -95,9 +95,15 @@ class SampleRNNLoss extends Component { // if ( (ii % 1) < 0.1) { // ctx.strokeStyle = 'rgba(255,255,255,1.0)' ctx.lineWidth = 2 + ctx.setLineDash([4, 4]) ctx.stroke() + ctx.stroke() ctx.stroke() - ctx.stroke() + ctx.setLineDash([0,0]) + const fontSize = 12 + ctx.font = 'italic ' + (fontSize * devicePixelRatio) + 'px "Georgia"' + ctx.fill = 'black' + ctx.fillText(ii.toFixed(1), w-50, (h-Y) + fontSize + (10 * devicePixelRatio)) // } } ctx.lineWidth = 1 @@ -106,7 +112,7 @@ class SampleRNNLoss extends Component { const loss = lossReport[key] const vf = parseFloat(loss[loss.length-1].training_loss) || 0 const vg = parseFloat(loss[0].training_loss) || 5 - console.log(vf) + // console.log(vf) const vv = 1 - norm(vf, scaleMin, scaleMax/2) ctx.lineWidth = (1-norm(vf, scaleMin, scaleMax)) * 5 ctx.strokeStyle = 'rgba(' + [randrange(30,190), randrange(30,150), randrange(60,120)].join(',') + ',' + 0.8+ ')' diff --git a/app/client/modules/samplernn/samplernn.results.js b/app/client/modules/samplernn/samplernn.results.js new file mode 100644 index 0000000..518864b --- /dev/null +++ b/app/client/modules/samplernn/samplernn.results.js @@ -0,0 +1,108 @@ +import { h, Component } from 'preact' +import { bindActionCreators } from 'redux' +import { connect } from 'react-redux' +import * as util from '../../util' + +import * as samplernnActions from './samplernn.actions' + +import Dataset from '../../dataset/dataset.component' + +import Group from '../../common/group.component' +import Slider from '../../common/slider.component' +import Select from '../../common/select.component' +import Button from '../../common/button.component' +import { FileList, FileRow } from '../../common/fileList.component' +import TextInput from '../../common/textInput.component' + +class SampleRNNResults extends Component { + constructor(props){ + super() + this.fileOptions = this.fileOptions.bind(this) + this.pickFile = this.pickFile.bind(this) + let id = props.match.params.id || localStorage.getItem('samplernn.last_id') + if (!props.samplernn.data) props.actions.load_directories() + } + pickFile(file){ + console.log('pick', file) + } + fileOptions(file){ + if (file.activity === 'url' && !file.dataset) { + if (this.props.runner.cpu.status !== 'IDLE') { + return ( + <div className='gray'> + fetching... + </div> + ) + } else { + return ( + <div className='link' onClick={() => this.fetchURL(file.url)}> + fetch + </div> + ) + } + } + return ( + <div> + <div className='link' onClick={() => this.train(file)}>train</div> + {file.epoch == 0 && <div className='epochs'>{file.epochs} ep.</div>} + </div> + ) + } + render(){ + if (this.props.samplernn.loading) return <span>Loading</span> + console.log(this.props.samplernn.data) + const { folderLookup } = this.props.samplernn.data + // const { folderLookup } = samplernn + console.log(this.props) + + const renders = Object.keys(folderLookup).sort(util.sort.stringSort.asc).map(key => { + const folder = folderLookup[key] + let { mapFn, sortFn } = util.sort.orderByFn('epoch desc') + console.log(mapFn, sortFn) + const datasetPairs = folder.datasets.map(mapFn).sort(sortFn) + const bestRenders = datasetPairs + .map(pair => pair[1]) + .filter(dataset => dataset.output.length) + .map(dataset => { + const { output } = dataset + return output.map(mapFn).sort(sortFn)[0][1] + }) + console.log(bestRenders.map(r => r.epoch)) + return ( + <div className='col'> + <h3>{key}</h3> + {!!bestRenders.length && + <FileList + files={bestRenders} + orderBy='epoch desc' + fields={'name date epoch size'} + /> + } + </div> + ) + }) + + return ( + <div className='app'> + <div className='heading'> + <h2>SampleRNN</h2> + </div> + <div class='rows params renders'> + {renders} + </div> + </div> + ) + } +} + +const mapStateToProps = state => ({ + samplernn: state.module.samplernn, + runner: state.system.runner, + task: state.task, +}) + +const mapDispatchToProps = (dispatch, ownProps) => ({ + actions: bindActionCreators(samplernnActions, dispatch), +}) + +export default connect(mapStateToProps, mapDispatchToProps)(SampleRNNResults) diff --git a/app/client/util/index.js b/app/client/util/index.js new file mode 100644 index 0000000..6ae1f64 --- /dev/null +++ b/app/client/util/index.js @@ -0,0 +1,159 @@ +import * as sort from './sort' + +export { + sort +} + +export const is_iphone = !!((navigator.userAgent.match(/iPhone/i)) || (navigator.userAgent.match(/iPod/i))) +export const is_ipad = !!(navigator.userAgent.match(/iPad/i)) +export const is_android = !!(navigator.userAgent.match(/Android/i)) +export const is_mobile = is_iphone || is_ipad || is_android +export const is_desktop = ! is_mobile; + +const htmlClassList = document.body.parentNode.classList +htmlClassList.add(is_desktop ? 'desktop' : 'mobile') +htmlClassList.remove('loading') + +// window.debug = false + +export function clamp(n,a,b) { return n<a?a:n<b?n:b } +export function norm(n,a,b) { return (n-a) / (b-a) } +export function lerp(n,a,b) { return (b-a)*n+a } +export function mix(n,a,b) { return a*(1-n)+b*n } +export function randint(n) { return Math.floor(Math.random()*n) } +export function randrange(a,b){ return Math.random() * (b-a) + a } + +document.body.style.backgroundImage = 'linear-gradient(' + (randint(40)+40) + 'deg, #fde, #ffe)' + +export function timeInSeconds(n){ + return (n / 10).toFixed(1) + ' s.' +} +export function gerund(s){ + return s.replace(/e?$/, 'ing') +} +export function commatize (n, radix) { + radix = radix || 1024 + var nums = [], i, counter = 0, r = Math.floor + if (n > radix) { + n /= radix + nums.unshift(r((n * 10) % 10)) + nums.unshift(".") + } + do { + i = n % 10 + n = r(n / 10) + if (n && ! (++counter % 3)) + { i = ' ' + r(i) } + nums.unshift(r(i)) + } + while (n) + return nums.join("") +} +export function carbon_date (date, no_bold) { + var span = (+new Date() - new Date(date)) / 1000, color + if (! no_bold && span < 86400) // modified today + { color = "new" } + else if (span < 604800) // modifed this week + { color = "recent" } + else if (span < 1209600) // modifed 2 weeks ago + { color = "med" } + else if (span < 3024000) // modifed 5 weeks ago + { color = "old" } + else if (span < 12315200) // modifed 6 months ago + { color = "older" } + else + { color = "quiet" } + return color +} +export function hush_views (n, bias, no_bold) { + var txt = commatize(n, 1000) + bias = bias || 1 + n = n || 0 + if (n < 30) { return["quiet", n + " v."] } + if (n < 200) { return ["quiet", txt + " v."] } + else if (n < 500) { return ["quiet", txt + " v."] } + else if (n < 1000) { return ["old", txt + " v."] } + else if (n < 5000) { return ["med", txt + " kv."] } + else if (no_bold || n < 10000) { return ["recent", txt + " kv."] } + else { return ["new", txt + " kv."] } +} +export function hush_threads (n, bias, no_bold) { + var txt = commatize(n, 1000) + bias = bias || 1 + n = n || 0 + if (n < 10) { return["quiet", n + " t."] } + else if (n < 25) { return ["old", txt + " t."] } + else if (n < 50) { return ["med", txt + " t."] } + else if (no_bold || n < 100) { return ["recent", txt + " t."] } + else { return ["new", txt + " t."] } +} +export function hush_size (n, bias, no_bold) { + var txt = commatize(Math.round(n / 1024)) + bias = 1 || bias + n = n || 0 + if (! n) { return ['', ''] } + if (n < 1000) { + return ["quiet", n + " b."] + } + if (n < 1000000) { + return ["quiet", txt + " kb."] + } + else if (n < (20000000/bias)) { + return ["quiet", txt + " mb."] + } + else if (n < (50000000/bias)) { + return ["old", txt + " mb."] + } + else if (n < (80000000/bias)) { + return ["med", txt + " mb."] + } + else if (no_bold || n < (170000000/bias)) { + return ["recent", txt + " mb."] + } + else { + return ["new", txt + " mb."] + } +} +export function hush_null (n, unit, no_bold) { + var s = unit ? n + " " + unit + "." : n + if (n < 3) { + return ["quiet", s] + } + else if (n < 6) { + return ["older", s] + } + else if (n < 10) { + return ["old", s] + } + else if (n < 16) { + return ["med", s] + } + else if (no_bold || n < 21) { + return ["recent", s] + } + else { + return ["new", s] + } +} +export function get_age (t) { + var age = Math.abs(+Date.now() - new Date(t))/1000 + var r = Math.floor + var m + if (age < 5) { return "now" } + if (age < 60) { return r(age) + "s" } + age /= 60 + if (age < 60) { return r(age) + "m" } + m = r(age % 60) + age /= 60 + if (m > 0 && age < 2) { return r(age) + "h" + m + "m" } + if (age < 24) { return r(age) + "h" } + age /= 24 + if (age < 7) { return r(age) + "d" } + age /= 7 + if (age < 12) { return r(age) + "w" } + age /= 4 + if (age < 12) { return r(age) + "m" } + age /= 12 + return r(age) + "y" +} +export function courtesy_s (n, s) { return n == 1 ? "" : (s || "s") } diff --git a/app/client/util/sort.js b/app/client/util/sort.js new file mode 100644 index 0000000..cc1b462 --- /dev/null +++ b/app/client/util/sort.js @@ -0,0 +1,32 @@ +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 'epoch': + mapFn = a => [parseInt(a.epoch || a.epochs) || 0, a] + sortFn = numericSort[direction] + break + case 'size': + mapFn = a => [a.size, a] + sortFn = numericSort[direction] + break + case 'date': + mapFn = a => [+new Date(a.date || a.created_at), a] + sortFn = numericSort[direction] + break + case 'name': + default: + mapFn = a => [a.id || a.name, a] + sortFn = stringSort[direction] + break + } + return { mapFn, sortFn } +} |
