feat: Add Tauri support and enhance client functionality

- Introduced Tauri as a new EventFetcherType in api-models.
- Enhanced ClientFetcher.vue to support forced mode switching for Danmaku client.
- Updated ClientLayout.vue to restrict usage outside Tauri environment with appropriate alerts.
- Improved ClientSettings.vue to fetch and display the current version of the application.
- Modified initialization logic in initialize.ts to handle minimized startup for Tauri.
- Updated QueryBiliAPI function to conditionally use cookies based on a new parameter.
- Added bootAsMinimized setting to useSettings store for better user experience.
- Refactored logging in useWebFetcher to use console instead of logError/logInfo for clarity.
- Created a new LabelItem component for better label handling in forms.
- Enhanced EventFetcherStatusCard.vue to display version information based on EventFetcherType.
This commit is contained in:
2025-04-07 19:14:39 +08:00
parent 277497420c
commit 0195e7b01a
14 changed files with 536 additions and 328 deletions

View File

@@ -1,4 +1,4 @@
import { useStorage } from '@vueuse/core'
import { useStorage } from '@vueuse/core';
import {
ConfigProviderProps,
NButton,
@@ -10,114 +10,114 @@ import {
dateZhCN,
useOsTheme,
zhCN
} from 'naive-ui'
import { SongFrom, SongsInfo, ThemeType } from './api/api-models'
import { computed } from 'vue'
import { VTSURU_API_URL } from './data/constants'
import { DiscreteApiType } from 'naive-ui/es/discrete/src/interface'
} from 'naive-ui';
import { SongFrom, SongsInfo, ThemeType } from './api/api-models';
import { computed } from 'vue';
import { VTSURU_API_URL } from './data/constants';
import { DiscreteApiType } from 'naive-ui/es/discrete/src/interface';
import { SquareArrowForward24Filled } from '@vicons/fluent';
import FiveSingIcon from '@/svgs/fivesing.svg'
import NeteaseIcon from '@/svgs/netease.svg'
import FiveSingIcon from '@/svgs/fivesing.svg';
import NeteaseIcon from '@/svgs/netease.svg';
const { message } = createDiscreteApi(['message'])
const { message } = createDiscreteApi(['message']);
const osThemeRef = useOsTheme() //获取当前系统主题
const themeType = useStorage('Settings.Theme', ThemeType.Auto)
const osThemeRef = useOsTheme(); //获取当前系统主题
const themeType = useStorage('Settings.Theme', ThemeType.Auto);
export const theme = computed(() => {
if (themeType.value == ThemeType.Auto) {
var osThemeRef = useOsTheme() //获取当前系统主题
return osThemeRef.value === 'dark' ? darkTheme : null
var osThemeRef = useOsTheme(); //获取当前系统主题
return osThemeRef.value === 'dark' ? darkTheme : null;
} else {
return themeType.value == ThemeType.Dark ? darkTheme : null
return themeType.value == ThemeType.Dark ? darkTheme : null;
}
})
});
export const configProviderPropsRef = computed<ConfigProviderProps>(() => ({
theme: theme.value,
locale: zhCN,
dateLocale: dateZhCN,
}))
}));
export function createNaiveUIApi(types: DiscreteApiType[]) {
return createDiscreteApi(types, {
configProviderProps: configProviderPropsRef
})
});
}
export function NavigateToNewTab(url: string) {
window.open(url, '_blank')
window.open(url, '_blank');
}
export const isDarkMode = computed(() => {
if (themeType.value == ThemeType.Auto) return osThemeRef.value === 'dark'
else return themeType.value == ThemeType.Dark
})
if (themeType.value == ThemeType.Auto) return osThemeRef.value === 'dark';
else return themeType.value == ThemeType.Dark;
});
export function copyToClipboard(text: string) {
if (navigator.clipboard) {
navigator.clipboard.writeText(text)
message.success('已复制到剪切板')
navigator.clipboard.writeText(text);
message.success('已复制到剪切板');
} else {
message.warning('当前环境不支持自动复制, 请手动选择并复制')
message.warning('当前环境不支持自动复制, 请手动选择并复制');
}
}
export function objectsToCSV(arr: any[]) {
const array = [Object.keys(arr[0])].concat(arr)
const array = [Object.keys(arr[0])].concat(arr);
return array
.map((row) => {
return Object.values(row)
.map((value) => {
return typeof value === 'string' ? JSON.stringify(value) : value
return typeof value === 'string' ? JSON.stringify(value) : value;
})
.toString()
.toString();
})
.join('\n')
.join('\n');
}
export function GetGuardColor(level: number | null | undefined): string {
if (level) {
switch (level) {
case 1: {
return 'rgb(122, 4, 35)'
return 'rgb(122, 4, 35)';
}
case 2: {
return 'rgb(157, 155, 255)'
return 'rgb(157, 155, 255)';
}
case 3: {
return 'rgb(104, 136, 241)'
return 'rgb(104, 136, 241)';
}
}
}
return ''
return '';
}
export function downloadImage(imageSrc: string, filename: string) {
const image = new Image()
image.crossOrigin = 'Anonymous' // This might be needed depending on the image's server
const image = new Image();
image.crossOrigin = 'Anonymous'; // This might be needed depending on the image's server
image.onload = () => {
const canvas = document.createElement('canvas')
canvas.width = image.width
canvas.height = image.height
const ctx = canvas.getContext('2d')
ctx!.drawImage(image, 0, 0)
const canvas = document.createElement('canvas');
canvas.width = image.width;
canvas.height = image.height;
const ctx = canvas.getContext('2d');
ctx!.drawImage(image, 0, 0);
canvas.toBlob((blob) => {
if (blob) {
const link = document.createElement('a')
link.href = URL.createObjectURL(blob)
link.download = filename
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
}) // Omitted the 'image/jpeg' to use the original image format
}
image.src = imageSrc
}); // Omitted the 'image/jpeg' to use the original image format
};
image.src = imageSrc;
}
export function getBase64(
file: File | undefined | null
): Promise<string | undefined> {
if (!file) return new Promise((resolve) => resolve(undefined))
if (!file) return new Promise((resolve) => resolve(undefined));
return new Promise((resolve, reject) => {
const reader = new FileReader()
reader.readAsDataURL(file)
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () =>
resolve(reader.result?.toString().split(',')[1] || undefined)
reader.onerror = (error) => reject(error)
})
resolve(reader.result?.toString().split(',')[1] || undefined);
reader.onerror = (error) => reject(error);
});
}
export async function getImageUploadModel(
files: UploadFileInfo[] | undefined | null,
@@ -126,83 +126,83 @@ export async function getImageUploadModel(
const result = {
existImages: [],
newImagesBase64: []
} as { existImages: string[]; newImagesBase64: string[] }
if (!files) return result
} as { existImages: string[]; newImagesBase64: string[]; };
if (!files) return result;
for (let i = 0; i < files.length; i++) {
const file = files[i]
const file = files[i];
if ((file.file?.size ?? 0) > maxSize) {
message.error('文件大小不能超过 ' + maxSize / 1024 / 1024 + 'MB')
return result
message.error('文件大小不能超过 ' + maxSize / 1024 / 1024 + 'MB');
return result;
}
if (!file.file) {
result.existImages.push(file.id) //用id绝对路径当的文件名
result.existImages.push(file.id); //用id绝对路径当的文件名
} else {
const base64 = await getBase64(file.file)
const base64 = await getBase64(file.file);
if (base64) {
result.newImagesBase64.push(base64)
result.newImagesBase64.push(base64);
}
}
}
return result
return result;
}
export function getUserAvatarUrl(userId: number | undefined | null) {
if (!userId) return ''
return VTSURU_API_URL + 'user-face/' + userId
if (!userId) return '';
return VTSURU_API_URL + 'user-face/' + userId;
}
export function getOUIdAvatarUrl(ouid: string) {
return VTSURU_API_URL + 'face/' + ouid
return VTSURU_API_URL + 'face/' + ouid;
}
export class GuidUtils {
// 将数字转换为GUID
public static numToGuid(value: number): string {
const buffer = new ArrayBuffer(16)
const view = new DataView(buffer)
view.setBigUint64(8, BigInt(value)) // 将数字写入后8个字节
return GuidUtils.bufferToGuid(buffer)
const buffer = new ArrayBuffer(16);
const view = new DataView(buffer);
view.setBigUint64(8, BigInt(value)); // 将数字写入后8个字节
return GuidUtils.bufferToGuid(buffer);
}
// 检查GUID是否由数字生成
public static isGuidFromUserId(guid: string): boolean {
const buffer = GuidUtils.guidToBuffer(guid)
const view = new DataView(buffer)
const buffer = GuidUtils.guidToBuffer(guid);
const view = new DataView(buffer);
for (let i = 0; i < 8; i++) {
if (view.getUint8(i) !== 0) return false // 检查前8个字节是否为0
if (view.getUint8(i) !== 0) return false; // 检查前8个字节是否为0
}
return true
return true;
}
// 将GUID转换为数字
public static guidToLong(guid: string): number {
const buffer = GuidUtils.guidToBuffer(guid)
const view = new DataView(buffer)
return Number(view.getBigUint64(8))
const buffer = GuidUtils.guidToBuffer(guid);
const view = new DataView(buffer);
return Number(view.getBigUint64(8));
}
// 辅助方法将ArrayBuffer转换为GUID字符串
private static bufferToGuid(buffer: ArrayBuffer): string {
const bytes = new Uint8Array(buffer)
const bytes = new Uint8Array(buffer);
const guid = bytes.reduce((str, byte, idx) => {
const pair = byte.toString(16).padStart(2, '0')
const pair = byte.toString(16).padStart(2, '0');
return (
str +
pair +
(idx === 3 || idx === 5 || idx === 7 || idx === 9 ? '-' : '')
)
}, '')
return guid
);
}, '');
return guid;
}
// 辅助方法将GUID字符串转换为ArrayBuffer
private static guidToBuffer(guid: string): ArrayBuffer {
const hex = guid.replace(/-/g, '')
if (hex.length !== 32) throw new Error('Invalid GUID format.')
const buffer = new ArrayBuffer(16)
const view = new DataView(buffer)
const hex = guid.replace(/-/g, '');
if (hex.length !== 32) throw new Error('Invalid GUID format.');
const buffer = new ArrayBuffer(16);
const view = new DataView(buffer);
for (let i = 0; i < 16; i++) {
view.setUint8(i, parseInt(hex.substr(i * 2, 2), 16))
view.setUint8(i, parseInt(hex.substr(i * 2, 2), 16));
}
return buffer
return buffer;
}
}
export function GetPlayButton(song: SongsInfo) {
@@ -218,7 +218,7 @@ export function GetPlayButton(song: SongsInfo) {
color: '#00BBB3',
ghost: true,
onClick: () => {
window.open(`http://5sing.kugou.com/bz/${song.id}.html`)
window.open(`http://5sing.kugou.com/bz/${song.id}.html`);
},
},
{
@@ -227,7 +227,7 @@ export function GetPlayButton(song: SongsInfo) {
),
),
default: () => '在5sing打开',
})
});
}
case SongFrom.Netease:
return h(NTooltip, null, {
@@ -239,7 +239,7 @@ export function GetPlayButton(song: SongsInfo) {
color: '#C20C0C',
ghost: true,
onClick: () => {
window.open(`https://music.163.com/#/song?id=${song.id}`)
window.open(`https://music.163.com/#/song?id=${song.id}`);
},
},
{
@@ -247,7 +247,7 @@ export function GetPlayButton(song: SongsInfo) {
},
),
default: () => '在网易云打开',
})
});
case SongFrom.Custom:
return song.url
? h(NTooltip, null, {
@@ -259,7 +259,7 @@ export function GetPlayButton(song: SongsInfo) {
color: '#6b95bd',
ghost: true,
onClick: () => {
window.open(song.url)
window.open(song.url);
},
},
{
@@ -268,6 +268,33 @@ export function GetPlayButton(song: SongsInfo) {
),
default: () => '打开链接',
})
: null
: null;
}
}
export function getBrowserName() {
var userAgent = navigator.userAgent;
if (userAgent.indexOf("Opera") > -1 || userAgent.indexOf("OPR") > -1) {
return 'Opera';
}
else if (userAgent.indexOf("compatible") > -1 && userAgent.indexOf("MSIE") > -1) {
return 'IE';
}
else if (userAgent.indexOf("Edge") > -1) {
return 'Edge';
}
else if (userAgent.indexOf("Firefox") > -1) {
return 'Firefox';
}
else if (userAgent.indexOf("Safari") > -1 && userAgent.indexOf("Chrome") == -1) {
return 'Safari';
}
else if (userAgent.indexOf("Chrome") > -1 && userAgent.indexOf("Safari") > -1) {
return 'Chrome';
}
else if (!!window.ActiveXObject || "ActiveXObject" in window) {
return 'IE>=11';
}
else {
return 'Unkonwn';
}
}