2
import VueRouter from 'vue-router'
3
import { camelCase, startCase, capitalize, isFinite, upperFirst } from 'lodash-es'
4
import type { ApiConfig, TextSortOrder } from '@/store/config/types'
5
import { TinyColor } from '@ctrl/tinycolor'
6
import { DateFormats, Globals, TimeFormats, Waits, type DateTimeFormat } from '@/globals'
7
import i18n from '@/plugins/i18n'
8
import type { TranslateResult } from 'vue-i18n'
9
import store from '@/store'
10
import type { FileBrowserEntry } from '@/store/files/types'
11
import versionStringCompare from '@/util/version-string-compare'
14
* credit: taken from Vuetify source
16
const getNestedValue = (obj: any, path: (string | number)[], fallback?: any): any => {
17
const last = path.length - 1
19
if (last < 0) return obj === undefined ? fallback : obj
21
for (let i = 0; i < last; i++) {
28
if (obj == null) return fallback
30
return obj[path[last]] === undefined ? fallback : obj[path[last]]
34
* credit: taken from Vuetify source
36
const getObjectValueByPath = (obj: any, path: string, fallback?: any): any => {
37
// credit: http://stackoverflow.com/questions/6491463/accessing-nested-javascript-objects-with-string-key#comment55278413_6491621
38
if (obj == null || !path || typeof path !== 'string') return fallback
39
if (obj[path] !== undefined) return obj[path]
40
path = path.replace(/\[(\w+)\]/g, '.$1') // convert indexes to properties
41
path = path.replace(/^\./, '') // strip a leading dot
42
return getNestedValue(obj, path.split('.'), fallback)
45
export const Filters = {
48
* Formats a time to 00h 00m 00s
49
* Expects to be passed seconds.
51
formatCounterSeconds: (seconds: number | string) => {
53
if (isNaN(seconds) || !isFinite(seconds)) seconds = 0
56
seconds = Math.abs(seconds)
59
const h = Math.floor(seconds / 3600)
60
const m = Math.floor(seconds % 3600 / 60)
61
const s = Math.floor(seconds % 3600 % 60)
63
let r = s + 's' // always show seconds
64
r = m + 'm ' + r // always show minutes
65
if (h > 0) r = h + 'h ' + r // only show hours if relevent
67
return (isNeg) ? '-' + r : r
70
getNavigatorLocales: () => {
71
return navigator.languages ?? [navigator.language]
74
getAllLocales: () => {
77
...Filters.getNavigatorLocales()
81
getDateFormat: (override?: string): DateTimeFormat => {
83
locales: Filters.getAllLocales(),
84
...DateFormats[override ?? store.state.config.uiSettings.general.dateFormat]
88
getTimeFormat: (override?: string): DateTimeFormat => {
90
locales: Filters.getAllLocales(),
91
...TimeFormats[override ?? store.state.config.uiSettings.general.timeFormat]
95
formatDate: (value: number | string | Date, options?: Intl.DateTimeFormatOptions) => {
96
const date = new Date(value)
97
const dateFormat = Filters.getDateFormat()
99
return date.toLocaleDateString(dateFormat.locales, {
100
...dateFormat.options,
105
formatTime: (value: number | string | Date, options?: Intl.DateTimeFormatOptions) => {
106
const date = new Date(value)
107
const timeFormat = Filters.getTimeFormat()
109
return date.toLocaleTimeString(timeFormat.locales, {
110
...timeFormat.options,
115
formatTimeWithSeconds: (value: number | string | Date, options?: Intl.DateTimeFormatOptions) => {
116
return Filters.formatTime(value, {
122
formatDateTime: (value: number | string | Date, options?: Intl.DateTimeFormatOptions) => {
123
const timeFormat = Filters.getTimeFormat()
124
const dateFormat = Filters.getDateFormat()
126
if (timeFormat.locales !== dateFormat.locales) {
127
return Filters.formatDate(value, options) + ' ' + Filters.formatTime(value, options)
130
const date = new Date(value)
132
return date.toLocaleDateString(dateFormat.locales, {
133
...dateFormat.options,
134
...timeFormat.options,
139
formatRelativeTimeToNow (value: number | string | Date, options?: Intl.RelativeTimeFormatOptions) {
140
return Filters.formatRelativeTimeToDate(value, Date.now(), options)
143
formatRelativeTimeToDate (value: number | string | Date, value2: number | string | Date, options?: Intl.RelativeTimeFormatOptions) {
144
let v = Math.floor(+new Date(value) / 1000)
145
let v2 = Math.floor(+new Date(value2) / 1000)
147
const units: { unit: Intl.RelativeTimeFormatUnit, limit: number }[] = [
148
{ unit: 'second', limit: 60 },
149
{ unit: 'minute', limit: 60 },
150
{ unit: 'hour', limit: 24 },
151
{ unit: 'day', limit: 30 },
152
{ unit: 'month', limit: 12 },
153
{ unit: 'year', limit: -1 }
156
for (const { unit, limit } of units) {
157
if (limit === -1 || Math.abs(v - v2) < limit) {
158
return Filters.formatRelativeTime(v - v2, unit, options)
161
v = Math.floor(v / limit)
162
v2 = Math.floor(v2 / limit)
166
formatRelativeTime (value: number, unit: Intl.RelativeTimeFormatUnit, options?: Intl.RelativeTimeFormatOptions) {
167
const rtf = new Intl.RelativeTimeFormat(Filters.getAllLocales(), {
172
return rtf.format(value, unit)
175
formatAbsoluteDateTime: (value: number | string | Date, options?: Intl.RelativeTimeFormatOptions) => {
176
if (Filters.isToday(value)) {
177
return Filters.formatTime(value, options)
180
if (Filters.isThisYear(value)) {
181
return Filters.formatDateTime(value, {
187
return Filters.formatDateTime(value, options)
190
isToday: (value: number | string | Date) => {
191
const date = new Date(value)
192
const today = new Date()
194
return date.getDate() === today.getDate() &&
195
date.getMonth() === today.getMonth() &&
196
date.getFullYear() === today.getFullYear()
199
isThisMonth: (value: number | string | Date) => {
200
const date = new Date(value)
201
const today = new Date()
203
return date.getMonth() === today.getMonth() &&
204
date.getFullYear() === today.getFullYear()
207
isThisYear: (value: number | string | Date) => {
208
const date = new Date(value)
209
const today = new Date()
211
return date.getFullYear() === today.getFullYear()
214
upperFirst: (value: string) => {
215
return upperFirst(value)
218
prettyCase: (value: string) => {
223
.map(Filters.upperFirst)
228
* Formats a string into camel case.
230
camelCase: (string: string) => {
231
return camelCase(string)
235
* Formats a string into start case.
237
startCase: (string: string) => {
238
return startCase(string)
242
* Converts the first character to upper case and the rest lower case, removing underscores.
243
* TEST_STRING -> Teststring
245
capitalize: (string: string) => {
246
string = Filters.camelCase(string)
247
return capitalize(string)
251
* Formats a number (in bytes) to a human readable file size.
253
getReadableFileSizeString (fileSizeInBytes: number) {
255
const byteUnits = [' kB', ' MB', ' GB', ' TB', 'PB', 'EB', 'ZB', 'YB']
256
if (fileSizeInBytes === 0) return `0${byteUnits[0]}`
258
fileSizeInBytes = fileSizeInBytes / 1024
260
} while (fileSizeInBytes > 1024)
262
return Math.max(fileSizeInBytes, 0.1).toFixed(1) + byteUnits[i]
266
* Formats a number (in bytes/sec) to a human readable data rate.
268
getReadableDataRateString (dataRateInBytesPerSec: number) {
270
const byteUnits = [' kB', ' MB', ' GB', ' TB', 'PB', 'EB', 'ZB', 'YB']
271
if (dataRateInBytesPerSec === 0) return `0${byteUnits[0]}`
273
dataRateInBytesPerSec = dataRateInBytesPerSec / 1024
275
} while (dataRateInBytesPerSec > 1024)
277
return Math.max(dataRateInBytesPerSec, 0.2).toFixed(1) + byteUnits[i] + '/Sec'
281
* Formats a number representing mm to human readable distance.
283
getReadableLengthString (lengthInMm: number | undefined | null, showMicrons = false) {
284
if (lengthInMm === undefined || lengthInMm === null) {
288
if (lengthInMm >= 1000) return (lengthInMm / 1000).toFixed(2) + ' m'
289
if (lengthInMm > 100) return (lengthInMm / 10).toFixed(1) + ' cm'
290
if (lengthInMm < 0.1 && showMicrons) return (lengthInMm * 1000).toFixed(0) + ' μm'
291
return lengthInMm.toFixed(1) + ' mm'
295
* Formats a number representing g to human readable weight.
297
getReadableWeightString (weightInG: number | undefined | null) {
298
if (weightInG === undefined || weightInG === null) {
302
if (weightInG >= 1000) return (weightInG / 1000).toFixed(2) + ' kg'
303
return weightInG.toFixed(2) + ' g'
307
* Formats a number (in Hz) to a human readable frequency.
309
getReadableFrequencyString (frequencyInHz: number) {
311
const frequencyUnits = [' Hz', ' kHz', ' MHz', ' GHz', ' THz']
312
while (frequencyInHz >= 1000) {
313
frequencyInHz = frequencyInHz / 1000
316
return frequencyInHz.toFixed() + frequencyUnits[i]
320
* Formats a number (in ohms) to a human readable resistance.
322
getReadableResistanceString (resistanceInOhms: number) {
324
const resistanceUnits = [' Ω', ' kΩ', ' MΩ', ' GΩ', ' TΩ']
325
while (resistanceInOhms >= 1000) {
326
resistanceInOhms = resistanceInOhms / 1000
329
return resistanceInOhms.toFixed(1) + resistanceUnits[i]
333
* Formats a number (in hPa) to human readable atmospheric pressure.
335
getReadableAtmosphericPressureString (pressumeInHPa: number) {
336
return pressumeInHPa.toFixed(1) + ' hPa'
340
* The filesystem sorter. This is copied from vuetify, and modified to ensure our directories
341
* are always sorted to the top.
344
items: FileBrowserEntry[],
348
textSortOrder: TextSortOrder
350
if (sortBy === null || !sortBy.length) return items
351
const stringCollator = new Intl.Collator(locale, { sensitivity: 'accent', usage: 'sort' })
352
return items.sort((a, b) => {
353
if (a.type === 'directory' && (a.dirname === '..' || b.type !== 'directory')) return -1
354
if (b.type === 'directory' && (b.dirname === '..' || a.type !== 'directory')) return 1
356
for (let i = 0; i < sortBy.length; i++) {
357
const sortKey = sortBy[i]
360
getObjectValueByPath(a, sortKey),
361
getObjectValueByPath(b, sortKey)
364
// If values are equal, continue
365
if (sortValues[0] === sortValues[1]) {
369
// If sorting descending, reverse values
374
// If values are of type number, compare as number
375
if (sortValues.every(x => typeof (x) === 'number' && !isNaN(x))) {
376
return sortValues[0] - sortValues[1]
379
const sortValuesAsString = sortValues
380
.map(s => (s || '').toString() as string)
382
if (textSortOrder === 'numeric-prefix') {
383
const [sortA, sortB] = sortValuesAsString
384
.map(s => s.match(/^\d+/))
386
// If are number prefixed, compare prefixes as number
387
if (sortA && sortB && sortA[0] !== sortB[0]) {
388
return +sortA[0] - +sortB[0]
390
} else if (textSortOrder === 'version') {
391
return versionStringCompare(sortValuesAsString[0], sortValuesAsString[1])
394
return stringCollator.compare(sortValuesAsString[0], sortValuesAsString[1])
401
* Determines API urls from a base url
403
getApiUrls (apiUrl: string): ApiConfig {
405
!apiUrl.startsWith('http://') &&
406
!apiUrl.startsWith('https://')
408
apiUrl = `http://${apiUrl}`
411
if (apiUrl.endsWith('/')) {
412
apiUrl = apiUrl.slice(0, -1)
415
const socketUrl = new URL(apiUrl)
417
socketUrl.protocol = socketUrl.protocol === 'https:'
420
socketUrl.pathname += socketUrl.pathname.endsWith('/')
426
socketUrl: socketUrl.toString()
431
* Tells us if a color is considered dark or light
433
isColorDark (color: string) {
434
const t = new TinyColor(color).getBrightness()
435
return ((t / 255) * 100) <= 50
439
* Simple approach to route somewhere when we don't necessarily want
440
* route matching via :to
442
routeTo (router: VueRouter, path: string) {
443
if (router.currentRoute.fullPath !== path) router.push(path)
447
* Converts a given weight (in grams) to its corresponding length (in mm)
449
convertFilamentWeightToLength (weight: number, density: number, diameter: number) {
450
// l[mm] = m[g]/D[g/cm³]/A[mm²]*(1000mm³/cm³)
451
return weight / density / (Math.PI * (diameter / 2) ** 2) * 1000
455
export const Rules = {
456
required (v: unknown) {
457
return ((v ?? '') !== '') || i18n.t('app.general.simple_form.error.required')
460
numberValid (v: unknown) {
461
return !isNaN(+(v ?? NaN)) || i18n.t('app.general.simple_form.error.invalid_number')
464
numberGreaterThan (min: number) {
465
return (v: number) => v > min || i18n.t('app.general.simple_form.error.min', { min: `> ${min}` })
468
numberGreaterThanOrEqual (min: number) {
469
return (v: number) => v >= min || i18n.t('app.general.simple_form.error.min', { min })
472
numberGreaterThanOrEqualOrZero (min: number) {
473
return (v: number) => +v === 0 || v >= min || i18n.t('app.general.simple_form.error.min_or_0', { min })
476
numberGreaterThanOrZero (min: number) {
477
return (v: number) => +v === 0 || v > min || i18n.t('app.general.simple_form.error.min_or_0', { min: `> ${min}` })
480
numberLessThan (max: number) {
481
return (v: number) => v < max || i18n.t('app.general.simple_form.error.max', { max: `< ${max}` })
484
numberLessThanOrEqual (max: number) {
485
return (v: number) => v <= max || i18n.t('app.general.simple_form.error.max', { max })
488
numberLessThanOrEqualOrZero (max: number) {
489
return (v: number) => +v === 0 || v <= max || i18n.t('app.general.simple_form.error.max', { max })
492
numberLessThanOrZero (max: number) {
493
return (v: number) => +v === 0 || v < max || i18n.t('app.general.simple_form.error.max', { max })
496
lengthGreaterThanOrEqual (min: number) {
497
return (v: string | unknown[]) => v.length >= min || i18n.t('app.general.simple_form.error.min', { min })
500
lengthLessThanOrEqual (max: number) {
501
return (v: string | unknown[]) => v.length <= max || i18n.t('app.general.simple_form.error.max', { max })
504
numberArrayValid (v: unknown[]) {
505
return !v.some(i => i === '' || isNaN(+(i ?? NaN))) || i18n.t('app.general.simple_form.error.arrayofnums')
508
passwordNotEqualUsername (username?: string | null) {
509
return (v: string) => (v.toLowerCase() !== (username ?? '').toLowerCase()) || i18n.t('app.general.simple_form.error.password_username')
512
aspectRatioValid (v: string) {
513
return /^\d+\s*[:/]\s*\d+$/.test(v) || i18n.t('app.general.simple_form.error.invalid_aspect')
516
regExpPatternValid (v: string) {
518
// eslint-disable-next-line no-new
522
return i18n.t('app.general.simple_form.error.invalid_expression')
526
regExpValid (regExp: RegExp, errorMessage: string | TranslateResult) {
527
return (v: string) => regExp.test(v) || errorMessage || 'Invalid'
531
export const FiltersPlugin = {
532
install (Vue: typeof _Vue) {
533
Vue.prototype.$filters = Filters
534
Vue.prototype.$rules = Rules
535
Vue.prototype.$globals = Globals
536
Vue.prototype.$waits = Waits
537
Vue.$filters = Filters
539
Vue.$globals = Globals
544
declare module 'vue/types/vue' {
546
$filters: typeof Filters;
547
$rules: typeof Rules;
548
$globals: typeof Globals;
549
$waits: typeof Waits;
552
interface VueConstructor {
553
$filters: typeof Filters;
554
$rules: typeof Rules;
555
$globals: typeof Globals;
556
$waits: typeof Waits;