Files
SPlayer/src/utils/helper.ts

431 lines
11 KiB
TypeScript
Raw Normal View History

import type { SongType, UpdateLogType } from "@/types/main";
2024-09-27 11:34:53 +08:00
import { NTooltip, SelectOption } from "naive-ui";
import { h, VNode } from "vue";
import { useClipboard } from "@vueuse/core";
import { getCacheData } from "./cache";
import { updateLog } from "@/api/other";
import { isEmpty } from "lodash-es";
import { convertToLocalTime } from "./time";
2024-09-27 11:34:53 +08:00
import { useSettingStore } from "@/stores";
import { marked } from "marked";
import SvgIcon from "@/components/Global/SvgIcon.vue";
type AnyObject = { [key: string]: any };
// 必要数据
let imageBlobURL: string = "";
// 环境判断
export const isDev = import.meta.env.MODE === "development" || import.meta.env.DEV;
// 系统判断
const userAgent = window.navigator.userAgent;
export const isWin = userAgent.includes("Windows");
export const isMac = userAgent.includes("Macintosh");
export const isLinux = userAgent.includes("Linux");
export const isElectron = userAgent.includes("Electron");
2025-10-20 18:22:40 +08:00
/**
*
* @param url
* @param target _self _blank
*/
export const openLink = (url: string, target: "_self" | "_blank" = "_blank") => {
window.open(url, target);
};
2025-10-20 18:22:40 +08:00
/**
*
* @param iconName
* @param option
* @returns
*/
export const renderIcon = (
iconName: string,
option: {
size?: number;
style?: AnyObject;
} = {},
) => {
const { size, style } = option;
return () => {
return h(SvgIcon, { name: iconName, size, style });
};
};
/**
*
* @param ms
*/
export const sleep = (ms: number): Promise<void> => {
return new Promise((resolve) => setTimeout(resolve, ms));
};
2025-10-20 18:22:40 +08:00
/**
*
* @param param0
* @returns
*/
export const renderOption = ({ node, option }: { node: VNode; option: SelectOption }) =>
h(
NTooltip,
{ placement: "left" },
{
trigger: () => node,
default: () => option.label,
},
);
2025-10-20 18:22:40 +08:00
/**
*
* @param keyword
* @param data
* @returns
*/
export const fuzzySearch = (keyword: string, data: SongType[]): SongType[] => {
try {
const result: SongType[] = [];
const regex = new RegExp(keyword, "i");
/**
*
* @param {Object} obj -
* @returns {boolean} - true false
*/
const searchInObject = (obj: AnyObject): boolean => {
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
const value = obj[key];
// 如果属性值是对象,则递归调用
if (typeof value === "object" && value !== null) {
if (searchInObject(value)) {
return true;
}
}
// 检查属性值是否是字符串并包含关键词
if (value && typeof value === "string" && regex.test(value)) {
return true;
}
}
}
return false;
};
if (!data) return [];
// 如果传入的是数组,遍历数组
if (Array.isArray(data)) {
for (const item of data) {
if (searchInObject(item)) {
result.push(item);
}
}
} else {
// 如果传入的是对象,直接调用递归函数
if (searchInObject(data)) {
result.push(data);
}
}
return result;
} catch (error) {
console.error("模糊搜索出现错误:", error);
return [];
}
};
/**
* 32 ARGB 24 RGB
*
* @param {number} x - 32ARGB颜色值
* @returns {number[]} - 绿24RGB颜色值数组0-255
*/
export const argbToRgb = (x: number): number[] => {
// 提取红色、绿色和蓝色分量
const r = (x >> 16) & 0xff;
const g = (x >> 8) & 0xff;
const b = x & 0xff;
// 返回24位RGB颜色值数组
return [r, g, b];
};
2025-10-20 18:22:40 +08:00
/**
* 1
* @param e
*/
export const coverLoaded = (e: Event) => {
const target = e.target as HTMLElement | null;
if (target && target.nodeType === Node.ELEMENT_NODE) {
target.style.opacity = "1";
}
};
2025-10-20 18:22:40 +08:00
/**
*
* @param num
* @returns
*/
export const formatNumber = (num: number): string => {
if (num < 10000) {
return num.toString();
} else if (num < 100000000) {
return `${(num / 10000).toFixed(1)}`;
} else {
return `${(num / 100000000).toFixed(1)}亿`;
}
};
2025-10-20 18:22:40 +08:00
/**
*
* @param bytes
* @returns
*/
export const formatFileSize = (bytes: number): string => {
if (bytes < 1024) {
return `${bytes} B`;
} else if (bytes < 1024 * 1024) {
return `${(bytes / 1024).toFixed(1)} KB`;
} else if (bytes < 1024 * 1024 * 1024) {
return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
} else {
return `${(bytes / 1024 / 1024 / 1024).toFixed(1)} GB`;
}
};
2025-10-20 18:22:40 +08:00
/**
* BlobUrl
* @param imageUrl
* @returns BlobUrl
*/
export const convertImageUrlToBlobUrl = async (imageUrl: string) => {
const response = await fetch(imageUrl);
if (!response.ok) {
throw new Error("Network response was not ok");
}
// 将响应数据转换为 Blob 对象
const blob = await response.blob();
// 撤销之前生成的对象 URL
if (imageBlobURL) URL.revokeObjectURL(imageBlobURL);
// 生成对象 URL
imageBlobURL = URL.createObjectURL(blob);
return imageBlobURL;
};
2025-10-20 18:22:40 +08:00
/**
*
* @param text
* @param message
* @returns
*/
export const copyData = async (text: any, message?: string) => {
const { copy, copied, isSupported } = useClipboard({ legacy: true });
if (!isSupported.value) {
window.$message.error("暂时无法使用复制功能");
return;
}
// 开始复制
try {
if (!text) return;
text = typeof text === "string" ? text.trim() : JSON.stringify(text, null, 2);
await copy(text);
if (copied.value) {
window.$message.success(message ?? "已复制到剪贴板");
} else {
window.$message.error("复制出错,请重试");
}
} catch (error) {
window.$message.error("复制出错,请重试");
console.error("复制出错:", error);
}
};
2025-10-20 18:22:40 +08:00
/*
*
* @returns null
*/
export const getClipboardData = async (): Promise<string | null> => {
try {
const text = await navigator.clipboard.readText();
return text;
} catch (error) {
console.error("Failed to read clipboard content:", error);
return null;
}
};
/**
* Electron
* @param shortcut
* @returns Accelerator
*/
export const formatForGlobalShortcut = (shortcut: string): string => {
return shortcut
.split("+")
.map((part) => {
// 字母
if (part.startsWith("Key")) {
return part.replace("Key", "");
}
// 数字
if (part.startsWith("Digit")) {
return part.replace("Digit", "num");
}
if (part.startsWith("Numpad")) {
return part.replace("Numpad", "num");
}
// 方向键
if (part.startsWith("Arrow")) {
return part.replace("Arrow", "");
}
return part;
})
.join("+");
};
2025-10-20 18:22:40 +08:00
/**
*
* @returns
*/
export const getUpdateLog = async (): Promise<UpdateLogType[]> => {
const result = await getCacheData(updateLog, { key: "updateLog", time: 10 });
if (!result || isEmpty(result)) return [];
const updateLogs = await Promise.all(
result.map(async (v: any) => ({
version: v.tag_name,
changelog: await marked(v.body),
time: convertToLocalTime(v.published_at),
url: v.html_url,
2024-09-27 16:14:38 +08:00
prerelease: v.prerelease,
})),
);
return updateLogs;
};
2024-09-27 11:34:53 +08:00
/**
*
* @param settingsKey key
* @param includeSubFolders
* @param errorConsole
* @param errorMessage
* @param needDefaultMusicPath
2024-09-27 11:34:53 +08:00
*/
const changeLocalPath =
(
settingsKey: string,
includeSubFolders: boolean,
errorConsole: string,
errorMessage: string,
needDefaultMusicPath: boolean = false,
) =>
async (delIndex?: number) => {
try {
if (!isElectron) return;
const settingStore = useSettingStore();
if (typeof delIndex === "number" && delIndex >= 0) {
settingStore[settingsKey].splice(delIndex, 1);
2024-09-27 11:34:53 +08:00
} else {
const selectedDir = await window.electron.ipcRenderer.invoke("choose-path");
if (!selectedDir) return;
// 动态获取默认路径
let allPath = [...settingStore[settingsKey]];
if (needDefaultMusicPath) {
const defaultDir = await window.electron.ipcRenderer.invoke("get-default-dir", "music");
if (defaultDir) allPath = [defaultDir, ...allPath];
}
// 检查是否为子文件夹
if (includeSubFolders) {
const isSubfolder = await window.electron.ipcRenderer.invoke(
"check-if-subfolder",
allPath,
selectedDir,
);
if (!isSubfolder) {
settingStore[settingsKey].push(selectedDir);
} else {
window.$message.error("添加的目录与现有目录有重叠,请重新选择");
}
} else {
if (allPath.includes(selectedDir)) {
window.$message.error("添加的目录已存在");
} else {
settingStore[settingsKey].push(selectedDir);
}
}
2024-09-27 11:34:53 +08:00
}
} catch (error) {
console.error(`${errorConsole}: `, error);
window.$message.error(errorMessage);
2024-09-27 11:34:53 +08:00
}
};
/**
*
* @param delIndex
*/
export const changeLocalMusicPath = changeLocalPath(
"localFilesPath",
true,
"Error changing local path",
"更改本地歌曲文件夹出错,请重试",
true,
);
/**
*
* @param delIndex
*/
export const changeLocalLyricPath = changeLocalPath(
"localLyricPath",
false,
"Error changing local lyric path",
"更改本地歌词文件夹出错,请重试",
false,
2025-10-22 13:59:56 +08:00
);
2025-10-20 18:22:40 +08:00
/**
* Fisher-Yates
*/
export const shuffleArray = <T>(arr: T[]): T[] => {
const copy = arr.slice();
for (let i = copy.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[copy[i], copy[j]] = [copy[j], copy[i]];
}
return copy;
};
2025-10-29 18:29:58 +08:00
/**
*
* @param task
*/
export const runIdle = (task: () => void) => {
try {
const ric = window?.requestIdleCallback as ((cb: () => void) => number) | undefined;
if (typeof ric === "function") {
ric(() => {
try {
task();
} catch {
/* empty */
}
});
} else {
setTimeout(() => {
try {
task();
} catch {
/* empty */
}
}, 0);
}
} catch {
setTimeout(() => {
try {
task();
} catch {
/* empty */
}
}, 0);
}
};