ytptube/ui/utils/index.js
2024-12-13 18:31:07 +03:00

444 lines
10 KiB
JavaScript

import { useNotification } from '@kyvg/vue3-notification'
import { useStorage } from '@vueuse/core'
const { notify } = useNotification()
const AG_SEPARATOR = '.'
/**
* Get value from object or function
*
* @param {Function|*} obj
* @returns {*}
*/
const getValue = (obj) => 'function' === typeof obj ? obj() : obj
/**
* Get value from object or function and return default value if it's undefined or null
*
* @param {Object|Array} obj The object to get the value from.
* @param {string} path The path to the value.
* @param {*} defaultValue The default value to return if the path is not found.
*
* @returns {*} The value at the path or the default value.
*/
const ag = (obj, path, defaultValue = null) => {
const keys = path.split(AG_SEPARATOR)
let at = obj
for (let key of keys) {
if (typeof at === 'object' && null !== at && key in at) {
at = at[key]
} else {
return getValue(defaultValue)
}
}
return getValue(null === at ? defaultValue : at)
}
/**
* Set value in object by path
*
* @param {Object} obj The object to set the value in.
* @param {string} path The path to the value.
* @param {*} value The value to set.
*
* @returns {Object} The object with the value set.
*/
const ag_set = (obj, path, value) => {
const keys = path.split(AG_SEPARATOR)
let at = obj
while (keys.length > 0) {
if (keys.length === 1) {
if (typeof at === 'object' && at !== null) {
at[keys.shift()] = value
} else {
throw new Error(`Cannot set value at this path (${path}) because it's not an object.`)
}
} else {
const key = keys.shift()
if (!at[key]) {
at[key] = {}
}
at = at[key]
}
}
return obj
}
/**
* Wait for an element to be loaded in the DOM
*
* @param {string} sel The selector of the element.
* @param {Function} callback The callback function.
*
* @returns {void}
*/
const awaitElement = (sel, callback) => {
let interval = undefined
let $elm = document.querySelector(sel)
if ($elm) {
callback($elm, sel)
return
}
interval = setInterval(() => {
let $elm = document.querySelector(sel)
if ($elm) {
clearInterval(interval)
callback($elm, sel)
}
}, 200)
}
/**
* Display a notification
*
* @param {string} type The type of the notification.
* @param {string} title The title of the notification.
* @param {string} text The text of the notification.
* @param {number} duration The duration of the notification.
*
* @returns {void}
*/
const notification = (type, title, text, duration = 3000) => {
let classes = ''
const notificationType = type.toLowerCase()
switch (notificationType) {
case 'info':
default:
classes = 'has-background-info has-text-white'
break
case 'success':
classes = 'has-background-success has-text-white'
break
case 'warning':
classes = 'has-background-warning has-text-white'
break
case 'error':
case 'crit':
classes = 'has-background-danger has-text-white'
if (3000 === duration) {
duration = 10000
}
if ('crit' === notificationType) {
log_error(`${title} ${text}`)
}
break
}
return notify({ title, text, type: classes, duration })
}
/**
* Replace tags in text with values from context
*
* @param {string} text The text with tags
* @param {object} context The context with values
*
* @returns {string} The text with replaced tags
*/
const r = (text, context = {}) => {
const tagLeft = '{'
const tagRight = '}'
if (!text.includes(tagLeft) || !text.includes(tagRight)) {
return text
}
const pattern = new RegExp(`${tagLeft}([\\w_.]+)${tagRight}`, 'g')
const matches = text.match(pattern)
if (!matches) {
return text
}
let replacements = {}
matches.forEach(match => replacements[match] = ag(context, match.slice(1, -1), ''))
for (let key in replacements) {
text = text.replace(new RegExp(key, 'g'), replacements[key])
}
return text
}
const copyText = (str, notify = true) => {
if (navigator.clipboard) {
navigator.clipboard.writeText(str).then(() => {
if (notify) {
notification('success', 'Success', 'Text copied to clipboard.')
}
}).catch((error) => {
console.error('Failed to copy.', error)
if (notify) {
notification('error', 'Error', 'Failed to copy to clipboard.')
}
})
return
}
const el = document.createElement('textarea')
el.value = str
document.body.appendChild(el)
el.select()
document.execCommand('copy')
document.body.removeChild(el)
if (notify) {
notification('success', 'Success', 'Text copied to clipboard.')
}
}
/**
* Dispatch custom event.
*
* @param {string} eventName The name of the event.
* @param {object} detail The detail object.
*
* @returns {Boolean} The return value of dispatchEvent.
*/
const dEvent = (eventName, detail = {}) => window.dispatchEvent(new CustomEvent(eventName, { detail }))
/**
* Make pagination
*
* @param {number} current The current page.
* @param {number} last The last page.
* @param {number} delta The delta.
*
* @returns {Array} The pagination array.
*/
const makePagination = (current, last, delta = 5) => {
let pagination = []
if (last < 2) {
return pagination
}
const strR = '-'.repeat(9 + `${last}`.length)
const left = current - delta, right = current + delta + 1
for (let i = 1; i <= last; i++) {
if (i === 1 || i === last || (i >= left && i < right)) {
if (i === left && i > 2) {
pagination.push({
page: 0, text: strR, selected: false,
})
}
pagination.push({
page: i, text: `Page #${i}`, selected: i === current
})
if (i === right - 1 && i < last - 1) {
pagination.push({
page: 0, text: strR, selected: false,
})
}
}
}
return pagination
}
/**
* Encodes a path string to be used in a URL
*
* @param {string} item - The path string to encode
*
* @returns {string} - The encoded path string
*/
const encodePath = item => {
// -- manually encode #
if (!item) {
return item
}
item = item.replace(/#/g, '%23')
return item.split('/').map(decodeURIComponent).map(encodeURIComponent).join('/')
}
/**
* Request content from the API. This function will automatically add the API token to the request headers.
* And prefix the URL with the API URL and path.
*
* @param {string} url - The URL to request
* @param {object} options - The request options
*
* @returns {Promise<Response>} - The response from the API
*/
const request = (url, options = {}) => {
const runtimeConfig = useRuntimeConfig()
const token = useStorage('token', null)
options = options || {}
options.method = options.method || 'GET'
options.headers = options.headers || {}
if (token && undefined === options.headers['Authorization']) {
options.headers['Authorization'] = 'Token ' + token.value
}
if (undefined === options.headers['Content-Type']) {
if (!(options?.body instanceof FormData)) {
options.headers['Content-Type'] = 'application/json'
}
}
if (undefined === options.headers['Accept']) {
options.headers['Accept'] = 'application/json'
}
return fetch(url.startsWith('/') ? runtimeConfig.public.domain + url : url, options)
}
/**
* Remove the ANSI colors from the text
*
* @param {string} text - The text to remove the colors from
*
* @returns {string} - The text without the colors.
*/
const removeANSIColors = text => text?.replace(/[\u001b\u009b][[()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-ORZcf-nqry=><]/g, '') ?? text
const dec2hex = dec => dec.toString(16).padStart(2, "0")
const makeId = len => Array.from(window.crypto.getRandomValues(new Uint8Array((len || 40) / 2)), dec2hex).join('')
const basename = (path, ext = '') => {
if (!path) {
return ''
}
const segments = path.replace(/\\/g, '/').split('/')
let base = segments.pop()
while (segments.length && base === '') {
base = segments.pop()
}
if (ext && base.endsWith(ext) && base !== ext) {
base = base.substring(0, base.length - ext.length)
}
return base
}
const dirname = filePath => {
const lastIndex = Math.max(filePath.lastIndexOf('/'), filePath.lastIndexOf('\\'))
if (-1 === lastIndex) {
return '.'
}
if (0 === lastIndex) {
return filePath[0]
}
return filePath.substring(0, lastIndex)
}
const iTrim = (str, delim, position = 'both') => {
if (!str) {
return str
}
if (!delim) {
throw new Error('Delimiter is required')
}
if ("]" === delim) {
delim = "\\]"
}
if ("\\" === delim) {
delim = "\\\\"
}
if (['both', 'start'].includes(position)) {
str = str.replace(new RegExp("^[" + delim + "]+", "g"), "")
}
if (['both', 'end'].includes(position)) {
str = str.replace(new RegExp("[" + delim + "]+$", "g"), "")
}
return str
}
const eTrim = (str, delim) => iTrim(str, delim, 'end')
const sTrim = (str, delim) => iTrim(str, delim, 'start')
const ucFirst = str => (!str) ? str : str.charAt(0).toUpperCase() + str.slice(1)
/**
* Get query parameters from the URL
*
* @param {string} url - The URL to get the query parameters from
*
* @returns {Object} - The query parameters
*/
const getQueryParams = (url = window.location.search) => Object.fromEntries(new URLSearchParams(url).entries())
/**
* Make download URL.
*
* @param {Object} config
* @param {Object} item
* @param {string} base
*
* @returns {string} The download URL
*/
const makeDownload = (config, item, base = 'download') => {
let baseDir = 'download' === base ? `${base}/` : 'player/playlist/';
if (item.folder) {
item.folder = item.folder.replace(/#/g, '%23');
baseDir += item.folder + '/';
}
let url = config.app.url_host + encodePath(config.app.url_prefix + baseDir + item.filename);
return ('m3u8' === base) ? url + '.m3u8' : url;
}
/**
* Format a file size
*
* @param {number} bytes
* @param {number} decimals
*
* @returns {string} The formatted file size
*/
const formatBytes = (bytes, decimals = 2) => {
if (!+bytes) return '0 Bytes'
const k = 1024
const dm = decimals < 0 ? 0 : decimals
const sizes = ['Bytes', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(dm))} ${sizes[i]}`
}
export {
ag_set,
ag,
awaitElement,
notification,
copyText,
dEvent,
makePagination,
request,
r,
encodePath,
removeANSIColors,
makeId,
basename,
iTrim,
eTrim,
sTrim,
ucFirst,
dirname,
getQueryParams,
makeDownload,
formatBytes,
}