fluidd

Форк
0
/
filters.ts 
558 строк · 16.2 Кб
1
import _Vue from 'vue'
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'
12

13
/**
14
 * credit: taken from Vuetify source
15
 */
16
const getNestedValue = (obj: any, path: (string | number)[], fallback?: any): any => {
17
  const last = path.length - 1
18

19
  if (last < 0) return obj === undefined ? fallback : obj
20

21
  for (let i = 0; i < last; i++) {
22
    if (obj == null) {
23
      return fallback
24
    }
25
    obj = obj[path[i]]
26
  }
27

28
  if (obj == null) return fallback
29

30
  return obj[path[last]] === undefined ? fallback : obj[path[last]]
31
}
32

33
/**
34
 * credit: taken from Vuetify source
35
 */
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)
43
}
44

45
export const Filters = {
46

47
  /**
48
   * Formats a time to 00h 00m 00s
49
   * Expects to be passed seconds.
50
   */
51
  formatCounterSeconds: (seconds: number | string) => {
52
    seconds = +seconds
53
    if (isNaN(seconds) || !isFinite(seconds)) seconds = 0
54
    let isNeg = false
55
    if (seconds < 0) {
56
      seconds = Math.abs(seconds)
57
      isNeg = true
58
    }
59
    const h = Math.floor(seconds / 3600)
60
    const m = Math.floor(seconds % 3600 / 60)
61
    const s = Math.floor(seconds % 3600 % 60)
62

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
66

67
    return (isNeg) ? '-' + r : r
68
  },
69

70
  getNavigatorLocales: () => {
71
    return navigator.languages ?? [navigator.language]
72
  },
73

74
  getAllLocales: () => {
75
    return [
76
      i18n.locale,
77
      ...Filters.getNavigatorLocales()
78
    ]
79
  },
80

81
  getDateFormat: (override?: string): DateTimeFormat => {
82
    return {
83
      locales: Filters.getAllLocales(),
84
      ...DateFormats[override ?? store.state.config.uiSettings.general.dateFormat]
85
    }
86
  },
87

88
  getTimeFormat: (override?: string): DateTimeFormat => {
89
    return {
90
      locales: Filters.getAllLocales(),
91
      ...TimeFormats[override ?? store.state.config.uiSettings.general.timeFormat]
92
    }
93
  },
94

95
  formatDate: (value: number | string | Date, options?: Intl.DateTimeFormatOptions) => {
96
    const date = new Date(value)
97
    const dateFormat = Filters.getDateFormat()
98

99
    return date.toLocaleDateString(dateFormat.locales, {
100
      ...dateFormat.options,
101
      ...options
102
    })
103
  },
104

105
  formatTime: (value: number | string | Date, options?: Intl.DateTimeFormatOptions) => {
106
    const date = new Date(value)
107
    const timeFormat = Filters.getTimeFormat()
108

109
    return date.toLocaleTimeString(timeFormat.locales, {
110
      ...timeFormat.options,
111
      ...options
112
    })
113
  },
114

115
  formatTimeWithSeconds: (value: number | string | Date, options?: Intl.DateTimeFormatOptions) => {
116
    return Filters.formatTime(value, {
117
      second: '2-digit',
118
      ...options
119
    })
120
  },
121

122
  formatDateTime: (value: number | string | Date, options?: Intl.DateTimeFormatOptions) => {
123
    const timeFormat = Filters.getTimeFormat()
124
    const dateFormat = Filters.getDateFormat()
125

126
    if (timeFormat.locales !== dateFormat.locales) {
127
      return Filters.formatDate(value, options) + ' ' + Filters.formatTime(value, options)
128
    }
129

130
    const date = new Date(value)
131

132
    return date.toLocaleDateString(dateFormat.locales, {
133
      ...dateFormat.options,
134
      ...timeFormat.options,
135
      ...options
136
    })
137
  },
138

139
  formatRelativeTimeToNow (value: number | string | Date, options?: Intl.RelativeTimeFormatOptions) {
140
    return Filters.formatRelativeTimeToDate(value, Date.now(), options)
141
  },
142

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)
146

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 }
154
    ]
155

156
    for (const { unit, limit } of units) {
157
      if (limit === -1 || Math.abs(v - v2) < limit) {
158
        return Filters.formatRelativeTime(v - v2, unit, options)
159
      }
160

161
      v = Math.floor(v / limit)
162
      v2 = Math.floor(v2 / limit)
163
    }
164
  },
165

166
  formatRelativeTime (value: number, unit: Intl.RelativeTimeFormatUnit, options?: Intl.RelativeTimeFormatOptions) {
167
    const rtf = new Intl.RelativeTimeFormat(Filters.getAllLocales(), {
168
      numeric: 'auto',
169
      ...options
170
    })
171

172
    return rtf.format(value, unit)
173
  },
174

175
  formatAbsoluteDateTime: (value: number | string | Date, options?: Intl.RelativeTimeFormatOptions) => {
176
    if (Filters.isToday(value)) {
177
      return Filters.formatTime(value, options)
178
    }
179

180
    if (Filters.isThisYear(value)) {
181
      return Filters.formatDateTime(value, {
182
        year: undefined,
183
        ...options
184
      })
185
    }
186

187
    return Filters.formatDateTime(value, options)
188
  },
189

190
  isToday: (value: number | string | Date) => {
191
    const date = new Date(value)
192
    const today = new Date()
193

194
    return date.getDate() === today.getDate() &&
195
      date.getMonth() === today.getMonth() &&
196
      date.getFullYear() === today.getFullYear()
197
  },
198

199
  isThisMonth: (value: number | string | Date) => {
200
    const date = new Date(value)
201
    const today = new Date()
202

203
    return date.getMonth() === today.getMonth() &&
204
      date.getFullYear() === today.getFullYear()
205
  },
206

207
  isThisYear: (value: number | string | Date) => {
208
    const date = new Date(value)
209
    const today = new Date()
210

211
    return date.getFullYear() === today.getFullYear()
212
  },
213

214
  upperFirst: (value: string) => {
215
    return upperFirst(value)
216
  },
217

218
  prettyCase: (value: string) => {
219
    return value
220
      .replace(/_/g, ' ')
221
      .split(' ')
222
      .filter(x => x)
223
      .map(Filters.upperFirst)
224
      .join(' ')
225
  },
226

227
  /**
228
   * Formats a string into camel case.
229
   */
230
  camelCase: (string: string) => {
231
    return camelCase(string)
232
  },
233

234
  /**
235
   * Formats a string into start case.
236
   */
237
  startCase: (string: string) => {
238
    return startCase(string)
239
  },
240

241
  /**
242
   * Converts the first character to upper case and the rest lower case, removing underscores.
243
   * TEST_STRING -> Teststring
244
   */
245
  capitalize: (string: string) => {
246
    string = Filters.camelCase(string)
247
    return capitalize(string)
248
  },
249

250
  /**
251
   * Formats a number (in bytes) to a human readable file size.
252
   */
253
  getReadableFileSizeString (fileSizeInBytes: number) {
254
    let i = -1
255
    const byteUnits = [' kB', ' MB', ' GB', ' TB', 'PB', 'EB', 'ZB', 'YB']
256
    if (fileSizeInBytes === 0) return `0${byteUnits[0]}`
257
    do {
258
      fileSizeInBytes = fileSizeInBytes / 1024
259
      i++
260
    } while (fileSizeInBytes > 1024)
261

262
    return Math.max(fileSizeInBytes, 0.1).toFixed(1) + byteUnits[i]
263
  },
264

265
  /**
266
   * Formats a number (in bytes/sec) to a human readable data rate.
267
   */
268
  getReadableDataRateString (dataRateInBytesPerSec: number) {
269
    let i = -1
270
    const byteUnits = [' kB', ' MB', ' GB', ' TB', 'PB', 'EB', 'ZB', 'YB']
271
    if (dataRateInBytesPerSec === 0) return `0${byteUnits[0]}`
272
    do {
273
      dataRateInBytesPerSec = dataRateInBytesPerSec / 1024
274
      i++
275
    } while (dataRateInBytesPerSec > 1024)
276

277
    return Math.max(dataRateInBytesPerSec, 0.2).toFixed(1) + byteUnits[i] + '/Sec'
278
  },
279

280
  /**
281
   * Formats a number representing mm to human readable distance.
282
   */
283
  getReadableLengthString (lengthInMm: number | undefined | null, showMicrons = false) {
284
    if (lengthInMm === undefined || lengthInMm === null) {
285
      return '-'
286
    }
287

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'
292
  },
293

294
  /**
295
   * Formats a number representing g to human readable weight.
296
   */
297
  getReadableWeightString (weightInG: number | undefined | null) {
298
    if (weightInG === undefined || weightInG === null) {
299
      return '-'
300
    }
301

302
    if (weightInG >= 1000) return (weightInG / 1000).toFixed(2) + ' kg'
303
    return weightInG.toFixed(2) + ' g'
304
  },
305

306
  /**
307
   * Formats a number (in Hz) to a human readable frequency.
308
   */
309
  getReadableFrequencyString (frequencyInHz: number) {
310
    let i = 0
311
    const frequencyUnits = [' Hz', ' kHz', ' MHz', ' GHz', ' THz']
312
    while (frequencyInHz >= 1000) {
313
      frequencyInHz = frequencyInHz / 1000
314
      i++
315
    }
316
    return frequencyInHz.toFixed() + frequencyUnits[i]
317
  },
318

319
  /**
320
   * Formats a number (in ohms) to a human readable resistance.
321
   */
322
  getReadableResistanceString (resistanceInOhms: number) {
323
    let i = 0
324
    const resistanceUnits = [' Ω', ' kΩ', ' MΩ', ' GΩ', ' TΩ']
325
    while (resistanceInOhms >= 1000) {
326
      resistanceInOhms = resistanceInOhms / 1000
327
      i++
328
    }
329
    return resistanceInOhms.toFixed(1) + resistanceUnits[i]
330
  },
331

332
  /**
333
   * Formats a number (in hPa) to human readable atmospheric pressure.
334
   */
335
  getReadableAtmosphericPressureString (pressumeInHPa: number) {
336
    return pressumeInHPa.toFixed(1) + ' hPa'
337
  },
338

339
  /**
340
   * The filesystem sorter. This is copied from vuetify, and modified to ensure our directories
341
   * are always sorted to the top.
342
   */
343
  fileSystemSort (
344
    items: FileBrowserEntry[],
345
    sortBy: string[],
346
    sortDesc: boolean[],
347
    locale: string,
348
    textSortOrder: TextSortOrder
349
  ) {
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
355

356
      for (let i = 0; i < sortBy.length; i++) {
357
        const sortKey = sortBy[i]
358

359
        const sortValues = [
360
          getObjectValueByPath(a, sortKey),
361
          getObjectValueByPath(b, sortKey)
362
        ]
363

364
        // If values are equal, continue
365
        if (sortValues[0] === sortValues[1]) {
366
          continue
367
        }
368

369
        // If sorting descending, reverse values
370
        if (sortDesc[i]) {
371
          sortValues.reverse()
372
        }
373

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]
377
        }
378

379
        const sortValuesAsString = sortValues
380
          .map(s => (s || '').toString() as string)
381

382
        if (textSortOrder === 'numeric-prefix') {
383
          const [sortA, sortB] = sortValuesAsString
384
            .map(s => s.match(/^\d+/))
385

386
          // If are number prefixed, compare prefixes as number
387
          if (sortA && sortB && sortA[0] !== sortB[0]) {
388
            return +sortA[0] - +sortB[0]
389
          }
390
        } else if (textSortOrder === 'version') {
391
          return versionStringCompare(sortValuesAsString[0], sortValuesAsString[1])
392
        }
393

394
        return stringCollator.compare(sortValuesAsString[0], sortValuesAsString[1])
395
      }
396
      return 0
397
    })
398
  },
399

400
  /**
401
   * Determines API urls from a base url
402
   */
403
  getApiUrls (apiUrl: string): ApiConfig {
404
    if (
405
      !apiUrl.startsWith('http://') &&
406
      !apiUrl.startsWith('https://')
407
    ) {
408
      apiUrl = `http://${apiUrl}`
409
    }
410

411
    if (apiUrl.endsWith('/')) {
412
      apiUrl = apiUrl.slice(0, -1)
413
    }
414

415
    const socketUrl = new URL(apiUrl)
416

417
    socketUrl.protocol = socketUrl.protocol === 'https:'
418
      ? 'wss://'
419
      : 'ws://'
420
    socketUrl.pathname += socketUrl.pathname.endsWith('/')
421
      ? 'websocket'
422
      : '/websocket'
423

424
    return {
425
      apiUrl,
426
      socketUrl: socketUrl.toString()
427
    }
428
  },
429

430
  /**
431
   * Tells us if a color is considered dark or light
432
   */
433
  isColorDark (color: string) {
434
    const t = new TinyColor(color).getBrightness()
435
    return ((t / 255) * 100) <= 50
436
  },
437

438
  /**
439
   * Simple approach to route somewhere when we don't necessarily want
440
   * route matching via :to
441
   */
442
  routeTo (router: VueRouter, path: string) {
443
    if (router.currentRoute.fullPath !== path) router.push(path)
444
  },
445

446
  /**
447
   * Converts a given weight (in grams) to its corresponding length (in mm)
448
   */
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
452
  }
453
}
454

455
export const Rules = {
456
  required (v: unknown) {
457
    return ((v ?? '') !== '') || i18n.t('app.general.simple_form.error.required')
458
  },
459

460
  numberValid (v: unknown) {
461
    return !isNaN(+(v ?? NaN)) || i18n.t('app.general.simple_form.error.invalid_number')
462
  },
463

464
  numberGreaterThan (min: number) {
465
    return (v: number) => v > min || i18n.t('app.general.simple_form.error.min', { min: `> ${min}` })
466
  },
467

468
  numberGreaterThanOrEqual (min: number) {
469
    return (v: number) => v >= min || i18n.t('app.general.simple_form.error.min', { min })
470
  },
471

472
  numberGreaterThanOrEqualOrZero (min: number) {
473
    return (v: number) => +v === 0 || v >= min || i18n.t('app.general.simple_form.error.min_or_0', { min })
474
  },
475

476
  numberGreaterThanOrZero (min: number) {
477
    return (v: number) => +v === 0 || v > min || i18n.t('app.general.simple_form.error.min_or_0', { min: `> ${min}` })
478
  },
479

480
  numberLessThan (max: number) {
481
    return (v: number) => v < max || i18n.t('app.general.simple_form.error.max', { max: `< ${max}` })
482
  },
483

484
  numberLessThanOrEqual (max: number) {
485
    return (v: number) => v <= max || i18n.t('app.general.simple_form.error.max', { max })
486
  },
487

488
  numberLessThanOrEqualOrZero (max: number) {
489
    return (v: number) => +v === 0 || v <= max || i18n.t('app.general.simple_form.error.max', { max })
490
  },
491

492
  numberLessThanOrZero (max: number) {
493
    return (v: number) => +v === 0 || v < max || i18n.t('app.general.simple_form.error.max', { max })
494
  },
495

496
  lengthGreaterThanOrEqual (min: number) {
497
    return (v: string | unknown[]) => v.length >= min || i18n.t('app.general.simple_form.error.min', { min })
498
  },
499

500
  lengthLessThanOrEqual (max: number) {
501
    return (v: string | unknown[]) => v.length <= max || i18n.t('app.general.simple_form.error.max', { max })
502
  },
503

504
  numberArrayValid (v: unknown[]) {
505
    return !v.some(i => i === '' || isNaN(+(i ?? NaN))) || i18n.t('app.general.simple_form.error.arrayofnums')
506
  },
507

508
  passwordNotEqualUsername (username?: string | null) {
509
    return (v: string) => (v.toLowerCase() !== (username ?? '').toLowerCase()) || i18n.t('app.general.simple_form.error.password_username')
510
  },
511

512
  aspectRatioValid (v: string) {
513
    return /^\d+\s*[:/]\s*\d+$/.test(v) || i18n.t('app.general.simple_form.error.invalid_aspect')
514
  },
515

516
  regExpPatternValid (v: string) {
517
    try {
518
      // eslint-disable-next-line no-new
519
      new RegExp(v)
520
      return true
521
    } catch (e) {
522
      return i18n.t('app.general.simple_form.error.invalid_expression')
523
    }
524
  },
525

526
  regExpValid (regExp: RegExp, errorMessage: string | TranslateResult) {
527
    return (v: string) => regExp.test(v) || errorMessage || 'Invalid'
528
  }
529
}
530

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
538
    Vue.$rules = Rules
539
    Vue.$globals = Globals
540
    Vue.$waits = Waits
541
  }
542
}
543

544
declare module 'vue/types/vue' {
545
  interface Vue {
546
    $filters: typeof Filters;
547
    $rules: typeof Rules;
548
    $globals: typeof Globals;
549
    $waits: typeof Waits;
550
  }
551

552
  interface VueConstructor {
553
    $filters: typeof Filters;
554
    $rules: typeof Rules;
555
    $globals: typeof Globals;
556
    $waits: typeof Waits;
557
  }
558
}
559

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.