ytptube/ui/utils/index.js

488 lines
11 KiB
JavaScript

import { useStorage } from '@vueuse/core'
const toast = useToast()
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)
}
/**
* 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) {
toast.success('Text copied to clipboard.')
}
}).catch((error) => {
console.error('Failed to copy.', error)
if (notify) {
toast.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) {
toast.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 {RequestInit} options - The request options
*
* @returns {Promise<Response>} - The response from the API
*/
const request = (url, options = {}) => {
const runtimeConfig = useRuntimeConfig()
options = options || {}
options.method = options.method || 'GET'
options.headers = options.headers || {}
options.withCredentials = true
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'
}
if (url.startsWith('/')) {
options.credentials = 'same-origin'
}
return fetch(url.startsWith('/') ? eTrim(runtimeConfig.public.domain, '/') + '/' + sTrim(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 = 'api/download') => {
let baseDir = 'api/player/m3u8/video/';
if ('m3u8' !== base) {
baseDir = `${base}/`;
}
if (item.folder) {
item.folder = item.folder.replace(/#/g, '%23');
baseDir += item.folder + '/';
}
let url = `/${sTrim(baseDir, '/')}${encodePath(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]}`
}
/**
* Convert options to JSON
*
* @param {string} opts
*
* @returns {Promise<string>}
*/
const convertCliOptions = async opts => {
const response = await request('/api/yt-dlp/convert', {
method: 'POST',
body: JSON.stringify({ args: opts }),
});
const data = await response.json()
if (200 !== response.status) {
throw new Error(`Error: (${response.status}): ${data.error}`)
}
return data
}
/**
* URL Safe Base64 Encode.
*
* @param {String} data The data to encode
*
* @returns {String} The encoded data
*/
const base64UrlEncode = data => btoa(data).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
/**
* URL Safe Base64 Decode.
*
* @param {String} input The input string to decode
*
* @returns {String} The decoded data
*/
const base64UrlDecode = input => {
let base64 = input.replace(/-/g, '+').replace(/_/g, '/');
const pad = base64.length % 4;
if (pad) {
base64 += '='.repeat(4 - pad);
}
return atob(base64);
}
/**
* Check if array or object has data.
*
* @param {string} item - The item to check
* @returns {boolean} - True if the item has data, false otherwise.
*/
const has_data = item => {
if (!item) {
return false
}
if (typeof item === 'string') {
try {
item = JSON.parse(item)
} catch (e) {
return true
}
}
try {
if (typeof item === 'object') {
return Object.keys(item).length > 0
}
return item.length > 0
} catch (e) {
console.error(e)
}
return false
}
export {
ag_set,
ag,
awaitElement,
copyText,
dEvent,
makePagination,
request,
r,
encodePath,
removeANSIColors,
makeId,
basename,
iTrim,
eTrim,
sTrim,
ucFirst,
dirname,
getQueryParams,
makeDownload,
formatBytes,
convertCliOptions,
base64UrlEncode,
base64UrlDecode,
has_data,
}