mirror of
https://github.com/timeshiftsauce/CeruMusic.git
synced 2025-11-25 03:15:07 +08:00
fix:修复了一些已知问题
This commit is contained in:
275
.vitepress/cache/deps/@theme_index.js
vendored
Normal file
275
.vitepress/cache/deps/@theme_index.js
vendored
Normal file
@@ -0,0 +1,275 @@
|
||||
import {
|
||||
useMediaQuery
|
||||
} from "./chunk-B6YPYVPP.js";
|
||||
import {
|
||||
computed,
|
||||
ref,
|
||||
shallowRef,
|
||||
watch
|
||||
} from "./chunk-I4O5PVBA.js";
|
||||
|
||||
// node_modules/vitepress/dist/client/theme-default/index.js
|
||||
import "D:/code/CeruMuisc/node_modules/vitepress/dist/client/theme-default/styles/fonts.css";
|
||||
|
||||
// node_modules/vitepress/dist/client/theme-default/without-fonts.js
|
||||
import "D:/code/CeruMuisc/node_modules/vitepress/dist/client/theme-default/styles/vars.css";
|
||||
import "D:/code/CeruMuisc/node_modules/vitepress/dist/client/theme-default/styles/base.css";
|
||||
import "D:/code/CeruMuisc/node_modules/vitepress/dist/client/theme-default/styles/icons.css";
|
||||
import "D:/code/CeruMuisc/node_modules/vitepress/dist/client/theme-default/styles/utils.css";
|
||||
import "D:/code/CeruMuisc/node_modules/vitepress/dist/client/theme-default/styles/components/custom-block.css";
|
||||
import "D:/code/CeruMuisc/node_modules/vitepress/dist/client/theme-default/styles/components/vp-code.css";
|
||||
import "D:/code/CeruMuisc/node_modules/vitepress/dist/client/theme-default/styles/components/vp-code-group.css";
|
||||
import "D:/code/CeruMuisc/node_modules/vitepress/dist/client/theme-default/styles/components/vp-doc.css";
|
||||
import "D:/code/CeruMuisc/node_modules/vitepress/dist/client/theme-default/styles/components/vp-sponsor.css";
|
||||
import VPBadge from "D:/code/CeruMuisc/node_modules/vitepress/dist/client/theme-default/components/VPBadge.vue";
|
||||
import Layout from "D:/code/CeruMuisc/node_modules/vitepress/dist/client/theme-default/Layout.vue";
|
||||
import { default as default2 } from "D:/code/CeruMuisc/node_modules/vitepress/dist/client/theme-default/components/VPBadge.vue";
|
||||
import { default as default3 } from "D:/code/CeruMuisc/node_modules/vitepress/dist/client/theme-default/components/VPButton.vue";
|
||||
import { default as default4 } from "D:/code/CeruMuisc/node_modules/vitepress/dist/client/theme-default/components/VPDocAsideSponsors.vue";
|
||||
import { default as default5 } from "D:/code/CeruMuisc/node_modules/vitepress/dist/client/theme-default/components/VPFeatures.vue";
|
||||
import { default as default6 } from "D:/code/CeruMuisc/node_modules/vitepress/dist/client/theme-default/components/VPHomeContent.vue";
|
||||
import { default as default7 } from "D:/code/CeruMuisc/node_modules/vitepress/dist/client/theme-default/components/VPHomeFeatures.vue";
|
||||
import { default as default8 } from "D:/code/CeruMuisc/node_modules/vitepress/dist/client/theme-default/components/VPHomeHero.vue";
|
||||
import { default as default9 } from "D:/code/CeruMuisc/node_modules/vitepress/dist/client/theme-default/components/VPHomeSponsors.vue";
|
||||
import { default as default10 } from "D:/code/CeruMuisc/node_modules/vitepress/dist/client/theme-default/components/VPImage.vue";
|
||||
import { default as default11 } from "D:/code/CeruMuisc/node_modules/vitepress/dist/client/theme-default/components/VPLink.vue";
|
||||
import { default as default12 } from "D:/code/CeruMuisc/node_modules/vitepress/dist/client/theme-default/components/VPNavBarSearch.vue";
|
||||
import { default as default13 } from "D:/code/CeruMuisc/node_modules/vitepress/dist/client/theme-default/components/VPSocialLink.vue";
|
||||
import { default as default14 } from "D:/code/CeruMuisc/node_modules/vitepress/dist/client/theme-default/components/VPSocialLinks.vue";
|
||||
import { default as default15 } from "D:/code/CeruMuisc/node_modules/vitepress/dist/client/theme-default/components/VPSponsors.vue";
|
||||
import { default as default16 } from "D:/code/CeruMuisc/node_modules/vitepress/dist/client/theme-default/components/VPTeamMembers.vue";
|
||||
import { default as default17 } from "D:/code/CeruMuisc/node_modules/vitepress/dist/client/theme-default/components/VPTeamPage.vue";
|
||||
import { default as default18 } from "D:/code/CeruMuisc/node_modules/vitepress/dist/client/theme-default/components/VPTeamPageSection.vue";
|
||||
import { default as default19 } from "D:/code/CeruMuisc/node_modules/vitepress/dist/client/theme-default/components/VPTeamPageTitle.vue";
|
||||
|
||||
// node_modules/vitepress/dist/client/theme-default/composables/local-nav.js
|
||||
import { onContentUpdated } from "vitepress";
|
||||
|
||||
// node_modules/vitepress/dist/client/theme-default/composables/outline.js
|
||||
import { getScrollOffset } from "vitepress";
|
||||
|
||||
// node_modules/vitepress/dist/client/theme-default/support/utils.js
|
||||
import { withBase } from "vitepress";
|
||||
|
||||
// node_modules/vitepress/dist/client/theme-default/composables/data.js
|
||||
import { useData as useData$ } from "vitepress";
|
||||
var useData = useData$;
|
||||
|
||||
// node_modules/vitepress/dist/client/theme-default/support/utils.js
|
||||
function ensureStartingSlash(path) {
|
||||
return path.startsWith("/") ? path : `/${path}`;
|
||||
}
|
||||
|
||||
// node_modules/vitepress/dist/client/theme-default/support/sidebar.js
|
||||
function getSidebar(_sidebar, path) {
|
||||
if (Array.isArray(_sidebar))
|
||||
return addBase(_sidebar);
|
||||
if (_sidebar == null)
|
||||
return [];
|
||||
path = ensureStartingSlash(path);
|
||||
const dir = Object.keys(_sidebar).sort((a, b) => {
|
||||
return b.split("/").length - a.split("/").length;
|
||||
}).find((dir2) => {
|
||||
return path.startsWith(ensureStartingSlash(dir2));
|
||||
});
|
||||
const sidebar = dir ? _sidebar[dir] : [];
|
||||
return Array.isArray(sidebar) ? addBase(sidebar) : addBase(sidebar.items, sidebar.base);
|
||||
}
|
||||
function getSidebarGroups(sidebar) {
|
||||
const groups = [];
|
||||
let lastGroupIndex = 0;
|
||||
for (const index in sidebar) {
|
||||
const item = sidebar[index];
|
||||
if (item.items) {
|
||||
lastGroupIndex = groups.push(item);
|
||||
continue;
|
||||
}
|
||||
if (!groups[lastGroupIndex]) {
|
||||
groups.push({ items: [] });
|
||||
}
|
||||
groups[lastGroupIndex].items.push(item);
|
||||
}
|
||||
return groups;
|
||||
}
|
||||
function addBase(items, _base) {
|
||||
return [...items].map((_item) => {
|
||||
const item = { ..._item };
|
||||
const base = item.base || _base;
|
||||
if (base && item.link)
|
||||
item.link = base + item.link;
|
||||
if (item.items)
|
||||
item.items = addBase(item.items, base);
|
||||
return item;
|
||||
});
|
||||
}
|
||||
|
||||
// node_modules/vitepress/dist/client/theme-default/composables/sidebar.js
|
||||
function useSidebar() {
|
||||
const { frontmatter, page, theme: theme2 } = useData();
|
||||
const is960 = useMediaQuery("(min-width: 960px)");
|
||||
const isOpen = ref(false);
|
||||
const _sidebar = computed(() => {
|
||||
const sidebarConfig = theme2.value.sidebar;
|
||||
const relativePath = page.value.relativePath;
|
||||
return sidebarConfig ? getSidebar(sidebarConfig, relativePath) : [];
|
||||
});
|
||||
const sidebar = ref(_sidebar.value);
|
||||
watch(_sidebar, (next, prev) => {
|
||||
if (JSON.stringify(next) !== JSON.stringify(prev))
|
||||
sidebar.value = _sidebar.value;
|
||||
});
|
||||
const hasSidebar = computed(() => {
|
||||
return frontmatter.value.sidebar !== false && sidebar.value.length > 0 && frontmatter.value.layout !== "home";
|
||||
});
|
||||
const leftAside = computed(() => {
|
||||
if (hasAside)
|
||||
return frontmatter.value.aside == null ? theme2.value.aside === "left" : frontmatter.value.aside === "left";
|
||||
return false;
|
||||
});
|
||||
const hasAside = computed(() => {
|
||||
if (frontmatter.value.layout === "home")
|
||||
return false;
|
||||
if (frontmatter.value.aside != null)
|
||||
return !!frontmatter.value.aside;
|
||||
return theme2.value.aside !== false;
|
||||
});
|
||||
const isSidebarEnabled = computed(() => hasSidebar.value && is960.value);
|
||||
const sidebarGroups = computed(() => {
|
||||
return hasSidebar.value ? getSidebarGroups(sidebar.value) : [];
|
||||
});
|
||||
function open() {
|
||||
isOpen.value = true;
|
||||
}
|
||||
function close() {
|
||||
isOpen.value = false;
|
||||
}
|
||||
function toggle() {
|
||||
isOpen.value ? close() : open();
|
||||
}
|
||||
return {
|
||||
isOpen,
|
||||
sidebar,
|
||||
sidebarGroups,
|
||||
hasSidebar,
|
||||
hasAside,
|
||||
leftAside,
|
||||
isSidebarEnabled,
|
||||
open,
|
||||
close,
|
||||
toggle
|
||||
};
|
||||
}
|
||||
|
||||
// node_modules/vitepress/dist/client/theme-default/composables/outline.js
|
||||
var ignoreRE = /\b(?:VPBadge|header-anchor|footnote-ref|ignore-header)\b/;
|
||||
var resolvedHeaders = [];
|
||||
function getHeaders(range) {
|
||||
const headers = [
|
||||
...document.querySelectorAll(".VPDoc :where(h1,h2,h3,h4,h5,h6)")
|
||||
].filter((el) => el.id && el.hasChildNodes()).map((el) => {
|
||||
const level = Number(el.tagName[1]);
|
||||
return {
|
||||
element: el,
|
||||
title: serializeHeader(el),
|
||||
link: "#" + el.id,
|
||||
level
|
||||
};
|
||||
});
|
||||
return resolveHeaders(headers, range);
|
||||
}
|
||||
function serializeHeader(h) {
|
||||
let ret = "";
|
||||
for (const node of h.childNodes) {
|
||||
if (node.nodeType === 1) {
|
||||
if (ignoreRE.test(node.className))
|
||||
continue;
|
||||
ret += node.textContent;
|
||||
} else if (node.nodeType === 3) {
|
||||
ret += node.textContent;
|
||||
}
|
||||
}
|
||||
return ret.trim();
|
||||
}
|
||||
function resolveHeaders(headers, range) {
|
||||
if (range === false) {
|
||||
return [];
|
||||
}
|
||||
const levelsRange = (typeof range === "object" && !Array.isArray(range) ? range.level : range) || 2;
|
||||
const [high, low] = typeof levelsRange === "number" ? [levelsRange, levelsRange] : levelsRange === "deep" ? [2, 6] : levelsRange;
|
||||
return buildTree(headers, high, low);
|
||||
}
|
||||
function buildTree(data, min, max) {
|
||||
resolvedHeaders.length = 0;
|
||||
const result = [];
|
||||
const stack = [];
|
||||
data.forEach((item) => {
|
||||
const node = { ...item, children: [] };
|
||||
let parent = stack[stack.length - 1];
|
||||
while (parent && parent.level >= node.level) {
|
||||
stack.pop();
|
||||
parent = stack[stack.length - 1];
|
||||
}
|
||||
if (node.element.classList.contains("ignore-header") || parent && "shouldIgnore" in parent) {
|
||||
stack.push({ level: node.level, shouldIgnore: true });
|
||||
return;
|
||||
}
|
||||
if (node.level > max || node.level < min)
|
||||
return;
|
||||
resolvedHeaders.push({ element: node.element, link: node.link });
|
||||
if (parent)
|
||||
parent.children.push(node);
|
||||
else
|
||||
result.push(node);
|
||||
stack.push(node);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
// node_modules/vitepress/dist/client/theme-default/composables/local-nav.js
|
||||
function useLocalNav() {
|
||||
const { theme: theme2, frontmatter } = useData();
|
||||
const headers = shallowRef([]);
|
||||
const hasLocalNav = computed(() => {
|
||||
return headers.value.length > 0;
|
||||
});
|
||||
onContentUpdated(() => {
|
||||
headers.value = getHeaders(frontmatter.value.outline ?? theme2.value.outline);
|
||||
});
|
||||
return {
|
||||
headers,
|
||||
hasLocalNav
|
||||
};
|
||||
}
|
||||
|
||||
// node_modules/vitepress/dist/client/theme-default/without-fonts.js
|
||||
var theme = {
|
||||
Layout,
|
||||
enhanceApp: ({ app }) => {
|
||||
app.component("Badge", VPBadge);
|
||||
}
|
||||
};
|
||||
var without_fonts_default = theme;
|
||||
export {
|
||||
default2 as VPBadge,
|
||||
default3 as VPButton,
|
||||
default4 as VPDocAsideSponsors,
|
||||
default5 as VPFeatures,
|
||||
default6 as VPHomeContent,
|
||||
default7 as VPHomeFeatures,
|
||||
default8 as VPHomeHero,
|
||||
default9 as VPHomeSponsors,
|
||||
default10 as VPImage,
|
||||
default11 as VPLink,
|
||||
default12 as VPNavBarSearch,
|
||||
default13 as VPSocialLink,
|
||||
default14 as VPSocialLinks,
|
||||
default15 as VPSponsors,
|
||||
default16 as VPTeamMembers,
|
||||
default17 as VPTeamPage,
|
||||
default18 as VPTeamPageSection,
|
||||
default19 as VPTeamPageTitle,
|
||||
without_fonts_default as default,
|
||||
useLocalNav,
|
||||
useSidebar
|
||||
};
|
||||
//# sourceMappingURL=@theme_index.js.map
|
||||
7
.vitepress/cache/deps/@theme_index.js.map
vendored
Normal file
7
.vitepress/cache/deps/@theme_index.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
40
.vitepress/cache/deps/_metadata.json
vendored
Normal file
40
.vitepress/cache/deps/_metadata.json
vendored
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"hash": "99cf66da",
|
||||
"configHash": "acc3a95b",
|
||||
"lockfileHash": "6f0f9736",
|
||||
"browserHash": "6e863def",
|
||||
"optimized": {
|
||||
"vue": {
|
||||
"src": "../../../node_modules/vue/dist/vue.runtime.esm-bundler.js",
|
||||
"file": "vue.js",
|
||||
"fileHash": "4f939392",
|
||||
"needsInterop": false
|
||||
},
|
||||
"vitepress > @vue/devtools-api": {
|
||||
"src": "../../../node_modules/@vue/devtools-api/dist/index.js",
|
||||
"file": "vitepress___@vue_devtools-api.js",
|
||||
"fileHash": "fcdf6679",
|
||||
"needsInterop": false
|
||||
},
|
||||
"vitepress > @vueuse/core": {
|
||||
"src": "../../../node_modules/@vueuse/core/index.mjs",
|
||||
"file": "vitepress___@vueuse_core.js",
|
||||
"fileHash": "f6cccf57",
|
||||
"needsInterop": false
|
||||
},
|
||||
"@theme/index": {
|
||||
"src": "../../../node_modules/vitepress/dist/client/theme-default/index.js",
|
||||
"file": "@theme_index.js",
|
||||
"fileHash": "1995bc33",
|
||||
"needsInterop": false
|
||||
}
|
||||
},
|
||||
"chunks": {
|
||||
"chunk-B6YPYVPP": {
|
||||
"file": "chunk-B6YPYVPP.js"
|
||||
},
|
||||
"chunk-I4O5PVBA": {
|
||||
"file": "chunk-I4O5PVBA.js"
|
||||
}
|
||||
}
|
||||
}
|
||||
9719
.vitepress/cache/deps/chunk-B6YPYVPP.js
vendored
Normal file
9719
.vitepress/cache/deps/chunk-B6YPYVPP.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
7
.vitepress/cache/deps/chunk-B6YPYVPP.js.map
vendored
Normal file
7
.vitepress/cache/deps/chunk-B6YPYVPP.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
12683
.vitepress/cache/deps/chunk-I4O5PVBA.js
vendored
Normal file
12683
.vitepress/cache/deps/chunk-I4O5PVBA.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
7
.vitepress/cache/deps/chunk-I4O5PVBA.js.map
vendored
Normal file
7
.vitepress/cache/deps/chunk-I4O5PVBA.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
3
.vitepress/cache/deps/package.json
vendored
Normal file
3
.vitepress/cache/deps/package.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"type": "module"
|
||||
}
|
||||
4505
.vitepress/cache/deps/vitepress___@vue_devtools-api.js
vendored
Normal file
4505
.vitepress/cache/deps/vitepress___@vue_devtools-api.js
vendored
Normal file
File diff suppressed because it is too large
Load Diff
7
.vitepress/cache/deps/vitepress___@vue_devtools-api.js.map
vendored
Normal file
7
.vitepress/cache/deps/vitepress___@vue_devtools-api.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
583
.vitepress/cache/deps/vitepress___@vueuse_core.js
vendored
Normal file
583
.vitepress/cache/deps/vitepress___@vueuse_core.js
vendored
Normal file
@@ -0,0 +1,583 @@
|
||||
import {
|
||||
DefaultMagicKeysAliasMap,
|
||||
StorageSerializers,
|
||||
TransitionPresets,
|
||||
assert,
|
||||
breakpointsAntDesign,
|
||||
breakpointsBootstrapV5,
|
||||
breakpointsElement,
|
||||
breakpointsMasterCss,
|
||||
breakpointsPrimeFlex,
|
||||
breakpointsQuasar,
|
||||
breakpointsSematic,
|
||||
breakpointsTailwind,
|
||||
breakpointsVuetify,
|
||||
breakpointsVuetifyV2,
|
||||
breakpointsVuetifyV3,
|
||||
bypassFilter,
|
||||
camelize,
|
||||
clamp,
|
||||
cloneFnJSON,
|
||||
computedAsync,
|
||||
computedEager,
|
||||
computedInject,
|
||||
computedWithControl,
|
||||
containsProp,
|
||||
controlledRef,
|
||||
createEventHook,
|
||||
createFetch,
|
||||
createFilterWrapper,
|
||||
createGlobalState,
|
||||
createInjectionState,
|
||||
createRef,
|
||||
createReusableTemplate,
|
||||
createSharedComposable,
|
||||
createSingletonPromise,
|
||||
createTemplatePromise,
|
||||
createUnrefFn,
|
||||
customStorageEventName,
|
||||
debounceFilter,
|
||||
defaultDocument,
|
||||
defaultLocation,
|
||||
defaultNavigator,
|
||||
defaultWindow,
|
||||
executeTransition,
|
||||
extendRef,
|
||||
formatDate,
|
||||
formatTimeAgo,
|
||||
get,
|
||||
getLifeCycleTarget,
|
||||
getSSRHandler,
|
||||
hasOwn,
|
||||
hyphenate,
|
||||
identity,
|
||||
increaseWithUnit,
|
||||
injectLocal,
|
||||
invoke,
|
||||
isClient,
|
||||
isDef,
|
||||
isDefined,
|
||||
isIOS,
|
||||
isObject,
|
||||
isWorker,
|
||||
makeDestructurable,
|
||||
mapGamepadToXbox360Controller,
|
||||
noop,
|
||||
normalizeDate,
|
||||
notNullish,
|
||||
now,
|
||||
objectEntries,
|
||||
objectOmit,
|
||||
objectPick,
|
||||
onClickOutside,
|
||||
onElementRemoval,
|
||||
onKeyDown,
|
||||
onKeyPressed,
|
||||
onKeyStroke,
|
||||
onKeyUp,
|
||||
onLongPress,
|
||||
onStartTyping,
|
||||
pausableFilter,
|
||||
promiseTimeout,
|
||||
provideLocal,
|
||||
provideSSRWidth,
|
||||
pxValue,
|
||||
rand,
|
||||
reactify,
|
||||
reactifyObject,
|
||||
reactiveComputed,
|
||||
reactiveOmit,
|
||||
reactivePick,
|
||||
refAutoReset,
|
||||
refDebounced,
|
||||
refDefault,
|
||||
refThrottled,
|
||||
refWithControl,
|
||||
resolveRef,
|
||||
resolveUnref,
|
||||
set,
|
||||
setSSRHandler,
|
||||
syncRef,
|
||||
syncRefs,
|
||||
templateRef,
|
||||
throttleFilter,
|
||||
timestamp,
|
||||
toArray,
|
||||
toReactive,
|
||||
toRef,
|
||||
toRefs,
|
||||
toValue,
|
||||
tryOnBeforeMount,
|
||||
tryOnBeforeUnmount,
|
||||
tryOnMounted,
|
||||
tryOnScopeDispose,
|
||||
tryOnUnmounted,
|
||||
unrefElement,
|
||||
until,
|
||||
useActiveElement,
|
||||
useAnimate,
|
||||
useArrayDifference,
|
||||
useArrayEvery,
|
||||
useArrayFilter,
|
||||
useArrayFind,
|
||||
useArrayFindIndex,
|
||||
useArrayFindLast,
|
||||
useArrayIncludes,
|
||||
useArrayJoin,
|
||||
useArrayMap,
|
||||
useArrayReduce,
|
||||
useArraySome,
|
||||
useArrayUnique,
|
||||
useAsyncQueue,
|
||||
useAsyncState,
|
||||
useBase64,
|
||||
useBattery,
|
||||
useBluetooth,
|
||||
useBreakpoints,
|
||||
useBroadcastChannel,
|
||||
useBrowserLocation,
|
||||
useCached,
|
||||
useClipboard,
|
||||
useClipboardItems,
|
||||
useCloned,
|
||||
useColorMode,
|
||||
useConfirmDialog,
|
||||
useCountdown,
|
||||
useCounter,
|
||||
useCssVar,
|
||||
useCurrentElement,
|
||||
useCycleList,
|
||||
useDark,
|
||||
useDateFormat,
|
||||
useDebounceFn,
|
||||
useDebouncedRefHistory,
|
||||
useDeviceMotion,
|
||||
useDeviceOrientation,
|
||||
useDevicePixelRatio,
|
||||
useDevicesList,
|
||||
useDisplayMedia,
|
||||
useDocumentVisibility,
|
||||
useDraggable,
|
||||
useDropZone,
|
||||
useElementBounding,
|
||||
useElementByPoint,
|
||||
useElementHover,
|
||||
useElementSize,
|
||||
useElementVisibility,
|
||||
useEventBus,
|
||||
useEventListener,
|
||||
useEventSource,
|
||||
useEyeDropper,
|
||||
useFavicon,
|
||||
useFetch,
|
||||
useFileDialog,
|
||||
useFileSystemAccess,
|
||||
useFocus,
|
||||
useFocusWithin,
|
||||
useFps,
|
||||
useFullscreen,
|
||||
useGamepad,
|
||||
useGeolocation,
|
||||
useIdle,
|
||||
useImage,
|
||||
useInfiniteScroll,
|
||||
useIntersectionObserver,
|
||||
useInterval,
|
||||
useIntervalFn,
|
||||
useKeyModifier,
|
||||
useLastChanged,
|
||||
useLocalStorage,
|
||||
useMagicKeys,
|
||||
useManualRefHistory,
|
||||
useMediaControls,
|
||||
useMediaQuery,
|
||||
useMemoize,
|
||||
useMemory,
|
||||
useMounted,
|
||||
useMouse,
|
||||
useMouseInElement,
|
||||
useMousePressed,
|
||||
useMutationObserver,
|
||||
useNavigatorLanguage,
|
||||
useNetwork,
|
||||
useNow,
|
||||
useObjectUrl,
|
||||
useOffsetPagination,
|
||||
useOnline,
|
||||
usePageLeave,
|
||||
useParallax,
|
||||
useParentElement,
|
||||
usePerformanceObserver,
|
||||
usePermission,
|
||||
usePointer,
|
||||
usePointerLock,
|
||||
usePointerSwipe,
|
||||
usePreferredColorScheme,
|
||||
usePreferredContrast,
|
||||
usePreferredDark,
|
||||
usePreferredLanguages,
|
||||
usePreferredReducedMotion,
|
||||
usePreferredReducedTransparency,
|
||||
usePrevious,
|
||||
useRafFn,
|
||||
useRefHistory,
|
||||
useResizeObserver,
|
||||
useSSRWidth,
|
||||
useScreenOrientation,
|
||||
useScreenSafeArea,
|
||||
useScriptTag,
|
||||
useScroll,
|
||||
useScrollLock,
|
||||
useSessionStorage,
|
||||
useShare,
|
||||
useSorted,
|
||||
useSpeechRecognition,
|
||||
useSpeechSynthesis,
|
||||
useStepper,
|
||||
useStorage,
|
||||
useStorageAsync,
|
||||
useStyleTag,
|
||||
useSupported,
|
||||
useSwipe,
|
||||
useTemplateRefsList,
|
||||
useTextDirection,
|
||||
useTextSelection,
|
||||
useTextareaAutosize,
|
||||
useThrottleFn,
|
||||
useThrottledRefHistory,
|
||||
useTimeAgo,
|
||||
useTimeout,
|
||||
useTimeoutFn,
|
||||
useTimeoutPoll,
|
||||
useTimestamp,
|
||||
useTitle,
|
||||
useToNumber,
|
||||
useToString,
|
||||
useToggle,
|
||||
useTransition,
|
||||
useUrlSearchParams,
|
||||
useUserMedia,
|
||||
useVModel,
|
||||
useVModels,
|
||||
useVibrate,
|
||||
useVirtualList,
|
||||
useWakeLock,
|
||||
useWebNotification,
|
||||
useWebSocket,
|
||||
useWebWorker,
|
||||
useWebWorkerFn,
|
||||
useWindowFocus,
|
||||
useWindowScroll,
|
||||
useWindowSize,
|
||||
watchArray,
|
||||
watchAtMost,
|
||||
watchDebounced,
|
||||
watchDeep,
|
||||
watchIgnorable,
|
||||
watchImmediate,
|
||||
watchOnce,
|
||||
watchPausable,
|
||||
watchThrottled,
|
||||
watchTriggerable,
|
||||
watchWithFilter,
|
||||
whenever
|
||||
} from "./chunk-B6YPYVPP.js";
|
||||
import "./chunk-I4O5PVBA.js";
|
||||
export {
|
||||
DefaultMagicKeysAliasMap,
|
||||
StorageSerializers,
|
||||
TransitionPresets,
|
||||
assert,
|
||||
computedAsync as asyncComputed,
|
||||
refAutoReset as autoResetRef,
|
||||
breakpointsAntDesign,
|
||||
breakpointsBootstrapV5,
|
||||
breakpointsElement,
|
||||
breakpointsMasterCss,
|
||||
breakpointsPrimeFlex,
|
||||
breakpointsQuasar,
|
||||
breakpointsSematic,
|
||||
breakpointsTailwind,
|
||||
breakpointsVuetify,
|
||||
breakpointsVuetifyV2,
|
||||
breakpointsVuetifyV3,
|
||||
bypassFilter,
|
||||
camelize,
|
||||
clamp,
|
||||
cloneFnJSON,
|
||||
computedAsync,
|
||||
computedEager,
|
||||
computedInject,
|
||||
computedWithControl,
|
||||
containsProp,
|
||||
computedWithControl as controlledComputed,
|
||||
controlledRef,
|
||||
createEventHook,
|
||||
createFetch,
|
||||
createFilterWrapper,
|
||||
createGlobalState,
|
||||
createInjectionState,
|
||||
reactify as createReactiveFn,
|
||||
createRef,
|
||||
createReusableTemplate,
|
||||
createSharedComposable,
|
||||
createSingletonPromise,
|
||||
createTemplatePromise,
|
||||
createUnrefFn,
|
||||
customStorageEventName,
|
||||
debounceFilter,
|
||||
refDebounced as debouncedRef,
|
||||
watchDebounced as debouncedWatch,
|
||||
defaultDocument,
|
||||
defaultLocation,
|
||||
defaultNavigator,
|
||||
defaultWindow,
|
||||
computedEager as eagerComputed,
|
||||
executeTransition,
|
||||
extendRef,
|
||||
formatDate,
|
||||
formatTimeAgo,
|
||||
get,
|
||||
getLifeCycleTarget,
|
||||
getSSRHandler,
|
||||
hasOwn,
|
||||
hyphenate,
|
||||
identity,
|
||||
watchIgnorable as ignorableWatch,
|
||||
increaseWithUnit,
|
||||
injectLocal,
|
||||
invoke,
|
||||
isClient,
|
||||
isDef,
|
||||
isDefined,
|
||||
isIOS,
|
||||
isObject,
|
||||
isWorker,
|
||||
makeDestructurable,
|
||||
mapGamepadToXbox360Controller,
|
||||
noop,
|
||||
normalizeDate,
|
||||
notNullish,
|
||||
now,
|
||||
objectEntries,
|
||||
objectOmit,
|
||||
objectPick,
|
||||
onClickOutside,
|
||||
onElementRemoval,
|
||||
onKeyDown,
|
||||
onKeyPressed,
|
||||
onKeyStroke,
|
||||
onKeyUp,
|
||||
onLongPress,
|
||||
onStartTyping,
|
||||
pausableFilter,
|
||||
watchPausable as pausableWatch,
|
||||
promiseTimeout,
|
||||
provideLocal,
|
||||
provideSSRWidth,
|
||||
pxValue,
|
||||
rand,
|
||||
reactify,
|
||||
reactifyObject,
|
||||
reactiveComputed,
|
||||
reactiveOmit,
|
||||
reactivePick,
|
||||
refAutoReset,
|
||||
refDebounced,
|
||||
refDefault,
|
||||
refThrottled,
|
||||
refWithControl,
|
||||
resolveRef,
|
||||
resolveUnref,
|
||||
set,
|
||||
setSSRHandler,
|
||||
syncRef,
|
||||
syncRefs,
|
||||
templateRef,
|
||||
throttleFilter,
|
||||
refThrottled as throttledRef,
|
||||
watchThrottled as throttledWatch,
|
||||
timestamp,
|
||||
toArray,
|
||||
toReactive,
|
||||
toRef,
|
||||
toRefs,
|
||||
toValue,
|
||||
tryOnBeforeMount,
|
||||
tryOnBeforeUnmount,
|
||||
tryOnMounted,
|
||||
tryOnScopeDispose,
|
||||
tryOnUnmounted,
|
||||
unrefElement,
|
||||
until,
|
||||
useActiveElement,
|
||||
useAnimate,
|
||||
useArrayDifference,
|
||||
useArrayEvery,
|
||||
useArrayFilter,
|
||||
useArrayFind,
|
||||
useArrayFindIndex,
|
||||
useArrayFindLast,
|
||||
useArrayIncludes,
|
||||
useArrayJoin,
|
||||
useArrayMap,
|
||||
useArrayReduce,
|
||||
useArraySome,
|
||||
useArrayUnique,
|
||||
useAsyncQueue,
|
||||
useAsyncState,
|
||||
useBase64,
|
||||
useBattery,
|
||||
useBluetooth,
|
||||
useBreakpoints,
|
||||
useBroadcastChannel,
|
||||
useBrowserLocation,
|
||||
useCached,
|
||||
useClipboard,
|
||||
useClipboardItems,
|
||||
useCloned,
|
||||
useColorMode,
|
||||
useConfirmDialog,
|
||||
useCountdown,
|
||||
useCounter,
|
||||
useCssVar,
|
||||
useCurrentElement,
|
||||
useCycleList,
|
||||
useDark,
|
||||
useDateFormat,
|
||||
refDebounced as useDebounce,
|
||||
useDebounceFn,
|
||||
useDebouncedRefHistory,
|
||||
useDeviceMotion,
|
||||
useDeviceOrientation,
|
||||
useDevicePixelRatio,
|
||||
useDevicesList,
|
||||
useDisplayMedia,
|
||||
useDocumentVisibility,
|
||||
useDraggable,
|
||||
useDropZone,
|
||||
useElementBounding,
|
||||
useElementByPoint,
|
||||
useElementHover,
|
||||
useElementSize,
|
||||
useElementVisibility,
|
||||
useEventBus,
|
||||
useEventListener,
|
||||
useEventSource,
|
||||
useEyeDropper,
|
||||
useFavicon,
|
||||
useFetch,
|
||||
useFileDialog,
|
||||
useFileSystemAccess,
|
||||
useFocus,
|
||||
useFocusWithin,
|
||||
useFps,
|
||||
useFullscreen,
|
||||
useGamepad,
|
||||
useGeolocation,
|
||||
useIdle,
|
||||
useImage,
|
||||
useInfiniteScroll,
|
||||
useIntersectionObserver,
|
||||
useInterval,
|
||||
useIntervalFn,
|
||||
useKeyModifier,
|
||||
useLastChanged,
|
||||
useLocalStorage,
|
||||
useMagicKeys,
|
||||
useManualRefHistory,
|
||||
useMediaControls,
|
||||
useMediaQuery,
|
||||
useMemoize,
|
||||
useMemory,
|
||||
useMounted,
|
||||
useMouse,
|
||||
useMouseInElement,
|
||||
useMousePressed,
|
||||
useMutationObserver,
|
||||
useNavigatorLanguage,
|
||||
useNetwork,
|
||||
useNow,
|
||||
useObjectUrl,
|
||||
useOffsetPagination,
|
||||
useOnline,
|
||||
usePageLeave,
|
||||
useParallax,
|
||||
useParentElement,
|
||||
usePerformanceObserver,
|
||||
usePermission,
|
||||
usePointer,
|
||||
usePointerLock,
|
||||
usePointerSwipe,
|
||||
usePreferredColorScheme,
|
||||
usePreferredContrast,
|
||||
usePreferredDark,
|
||||
usePreferredLanguages,
|
||||
usePreferredReducedMotion,
|
||||
usePreferredReducedTransparency,
|
||||
usePrevious,
|
||||
useRafFn,
|
||||
useRefHistory,
|
||||
useResizeObserver,
|
||||
useSSRWidth,
|
||||
useScreenOrientation,
|
||||
useScreenSafeArea,
|
||||
useScriptTag,
|
||||
useScroll,
|
||||
useScrollLock,
|
||||
useSessionStorage,
|
||||
useShare,
|
||||
useSorted,
|
||||
useSpeechRecognition,
|
||||
useSpeechSynthesis,
|
||||
useStepper,
|
||||
useStorage,
|
||||
useStorageAsync,
|
||||
useStyleTag,
|
||||
useSupported,
|
||||
useSwipe,
|
||||
useTemplateRefsList,
|
||||
useTextDirection,
|
||||
useTextSelection,
|
||||
useTextareaAutosize,
|
||||
refThrottled as useThrottle,
|
||||
useThrottleFn,
|
||||
useThrottledRefHistory,
|
||||
useTimeAgo,
|
||||
useTimeout,
|
||||
useTimeoutFn,
|
||||
useTimeoutPoll,
|
||||
useTimestamp,
|
||||
useTitle,
|
||||
useToNumber,
|
||||
useToString,
|
||||
useToggle,
|
||||
useTransition,
|
||||
useUrlSearchParams,
|
||||
useUserMedia,
|
||||
useVModel,
|
||||
useVModels,
|
||||
useVibrate,
|
||||
useVirtualList,
|
||||
useWakeLock,
|
||||
useWebNotification,
|
||||
useWebSocket,
|
||||
useWebWorker,
|
||||
useWebWorkerFn,
|
||||
useWindowFocus,
|
||||
useWindowScroll,
|
||||
useWindowSize,
|
||||
watchArray,
|
||||
watchAtMost,
|
||||
watchDebounced,
|
||||
watchDeep,
|
||||
watchIgnorable,
|
||||
watchImmediate,
|
||||
watchOnce,
|
||||
watchPausable,
|
||||
watchThrottled,
|
||||
watchTriggerable,
|
||||
watchWithFilter,
|
||||
whenever
|
||||
};
|
||||
//# sourceMappingURL=vitepress___@vueuse_core.js.map
|
||||
7
.vitepress/cache/deps/vitepress___@vueuse_core.js.map
vendored
Normal file
7
.vitepress/cache/deps/vitepress___@vueuse_core.js.map
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"version": 3,
|
||||
"sources": [],
|
||||
"sourcesContent": [],
|
||||
"mappings": "",
|
||||
"names": []
|
||||
}
|
||||
343
.vitepress/cache/deps/vue.js
vendored
Normal file
343
.vitepress/cache/deps/vue.js
vendored
Normal file
@@ -0,0 +1,343 @@
|
||||
import {
|
||||
BaseTransition,
|
||||
BaseTransitionPropsValidators,
|
||||
Comment,
|
||||
DeprecationTypes,
|
||||
EffectScope,
|
||||
ErrorCodes,
|
||||
ErrorTypeStrings,
|
||||
Fragment,
|
||||
KeepAlive,
|
||||
ReactiveEffect,
|
||||
Static,
|
||||
Suspense,
|
||||
Teleport,
|
||||
Text,
|
||||
TrackOpTypes,
|
||||
Transition,
|
||||
TransitionGroup,
|
||||
TriggerOpTypes,
|
||||
VueElement,
|
||||
assertNumber,
|
||||
callWithAsyncErrorHandling,
|
||||
callWithErrorHandling,
|
||||
camelize,
|
||||
capitalize,
|
||||
cloneVNode,
|
||||
compatUtils,
|
||||
compile,
|
||||
computed,
|
||||
createApp,
|
||||
createBaseVNode,
|
||||
createBlock,
|
||||
createCommentVNode,
|
||||
createElementBlock,
|
||||
createHydrationRenderer,
|
||||
createPropsRestProxy,
|
||||
createRenderer,
|
||||
createSSRApp,
|
||||
createSlots,
|
||||
createStaticVNode,
|
||||
createTextVNode,
|
||||
createVNode,
|
||||
customRef,
|
||||
defineAsyncComponent,
|
||||
defineComponent,
|
||||
defineCustomElement,
|
||||
defineEmits,
|
||||
defineExpose,
|
||||
defineModel,
|
||||
defineOptions,
|
||||
defineProps,
|
||||
defineSSRCustomElement,
|
||||
defineSlots,
|
||||
devtools,
|
||||
effect,
|
||||
effectScope,
|
||||
getCurrentInstance,
|
||||
getCurrentScope,
|
||||
getCurrentWatcher,
|
||||
getTransitionRawChildren,
|
||||
guardReactiveProps,
|
||||
h,
|
||||
handleError,
|
||||
hasInjectionContext,
|
||||
hydrate,
|
||||
hydrateOnIdle,
|
||||
hydrateOnInteraction,
|
||||
hydrateOnMediaQuery,
|
||||
hydrateOnVisible,
|
||||
initCustomFormatter,
|
||||
initDirectivesForSSR,
|
||||
inject,
|
||||
isMemoSame,
|
||||
isProxy,
|
||||
isReactive,
|
||||
isReadonly,
|
||||
isRef,
|
||||
isRuntimeOnly,
|
||||
isShallow,
|
||||
isVNode,
|
||||
markRaw,
|
||||
mergeDefaults,
|
||||
mergeModels,
|
||||
mergeProps,
|
||||
nextTick,
|
||||
normalizeClass,
|
||||
normalizeProps,
|
||||
normalizeStyle,
|
||||
onActivated,
|
||||
onBeforeMount,
|
||||
onBeforeUnmount,
|
||||
onBeforeUpdate,
|
||||
onDeactivated,
|
||||
onErrorCaptured,
|
||||
onMounted,
|
||||
onRenderTracked,
|
||||
onRenderTriggered,
|
||||
onScopeDispose,
|
||||
onServerPrefetch,
|
||||
onUnmounted,
|
||||
onUpdated,
|
||||
onWatcherCleanup,
|
||||
openBlock,
|
||||
popScopeId,
|
||||
provide,
|
||||
proxyRefs,
|
||||
pushScopeId,
|
||||
queuePostFlushCb,
|
||||
reactive,
|
||||
readonly,
|
||||
ref,
|
||||
registerRuntimeCompiler,
|
||||
render,
|
||||
renderList,
|
||||
renderSlot,
|
||||
resolveComponent,
|
||||
resolveDirective,
|
||||
resolveDynamicComponent,
|
||||
resolveFilter,
|
||||
resolveTransitionHooks,
|
||||
setBlockTracking,
|
||||
setDevtoolsHook,
|
||||
setTransitionHooks,
|
||||
shallowReactive,
|
||||
shallowReadonly,
|
||||
shallowRef,
|
||||
ssrContextKey,
|
||||
ssrUtils,
|
||||
stop,
|
||||
toDisplayString,
|
||||
toHandlerKey,
|
||||
toHandlers,
|
||||
toRaw,
|
||||
toRef,
|
||||
toRefs,
|
||||
toValue,
|
||||
transformVNodeArgs,
|
||||
triggerRef,
|
||||
unref,
|
||||
useAttrs,
|
||||
useCssModule,
|
||||
useCssVars,
|
||||
useHost,
|
||||
useId,
|
||||
useModel,
|
||||
useSSRContext,
|
||||
useShadowRoot,
|
||||
useSlots,
|
||||
useTemplateRef,
|
||||
useTransitionState,
|
||||
vModelCheckbox,
|
||||
vModelDynamic,
|
||||
vModelRadio,
|
||||
vModelSelect,
|
||||
vModelText,
|
||||
vShow,
|
||||
version,
|
||||
warn,
|
||||
watch,
|
||||
watchEffect,
|
||||
watchPostEffect,
|
||||
watchSyncEffect,
|
||||
withAsyncContext,
|
||||
withCtx,
|
||||
withDefaults,
|
||||
withDirectives,
|
||||
withKeys,
|
||||
withMemo,
|
||||
withModifiers,
|
||||
withScopeId
|
||||
} from "./chunk-I4O5PVBA.js";
|
||||
export {
|
||||
BaseTransition,
|
||||
BaseTransitionPropsValidators,
|
||||
Comment,
|
||||
DeprecationTypes,
|
||||
EffectScope,
|
||||
ErrorCodes,
|
||||
ErrorTypeStrings,
|
||||
Fragment,
|
||||
KeepAlive,
|
||||
ReactiveEffect,
|
||||
Static,
|
||||
Suspense,
|
||||
Teleport,
|
||||
Text,
|
||||
TrackOpTypes,
|
||||
Transition,
|
||||
TransitionGroup,
|
||||
TriggerOpTypes,
|
||||
VueElement,
|
||||
assertNumber,
|
||||
callWithAsyncErrorHandling,
|
||||
callWithErrorHandling,
|
||||
camelize,
|
||||
capitalize,
|
||||
cloneVNode,
|
||||
compatUtils,
|
||||
compile,
|
||||
computed,
|
||||
createApp,
|
||||
createBlock,
|
||||
createCommentVNode,
|
||||
createElementBlock,
|
||||
createBaseVNode as createElementVNode,
|
||||
createHydrationRenderer,
|
||||
createPropsRestProxy,
|
||||
createRenderer,
|
||||
createSSRApp,
|
||||
createSlots,
|
||||
createStaticVNode,
|
||||
createTextVNode,
|
||||
createVNode,
|
||||
customRef,
|
||||
defineAsyncComponent,
|
||||
defineComponent,
|
||||
defineCustomElement,
|
||||
defineEmits,
|
||||
defineExpose,
|
||||
defineModel,
|
||||
defineOptions,
|
||||
defineProps,
|
||||
defineSSRCustomElement,
|
||||
defineSlots,
|
||||
devtools,
|
||||
effect,
|
||||
effectScope,
|
||||
getCurrentInstance,
|
||||
getCurrentScope,
|
||||
getCurrentWatcher,
|
||||
getTransitionRawChildren,
|
||||
guardReactiveProps,
|
||||
h,
|
||||
handleError,
|
||||
hasInjectionContext,
|
||||
hydrate,
|
||||
hydrateOnIdle,
|
||||
hydrateOnInteraction,
|
||||
hydrateOnMediaQuery,
|
||||
hydrateOnVisible,
|
||||
initCustomFormatter,
|
||||
initDirectivesForSSR,
|
||||
inject,
|
||||
isMemoSame,
|
||||
isProxy,
|
||||
isReactive,
|
||||
isReadonly,
|
||||
isRef,
|
||||
isRuntimeOnly,
|
||||
isShallow,
|
||||
isVNode,
|
||||
markRaw,
|
||||
mergeDefaults,
|
||||
mergeModels,
|
||||
mergeProps,
|
||||
nextTick,
|
||||
normalizeClass,
|
||||
normalizeProps,
|
||||
normalizeStyle,
|
||||
onActivated,
|
||||
onBeforeMount,
|
||||
onBeforeUnmount,
|
||||
onBeforeUpdate,
|
||||
onDeactivated,
|
||||
onErrorCaptured,
|
||||
onMounted,
|
||||
onRenderTracked,
|
||||
onRenderTriggered,
|
||||
onScopeDispose,
|
||||
onServerPrefetch,
|
||||
onUnmounted,
|
||||
onUpdated,
|
||||
onWatcherCleanup,
|
||||
openBlock,
|
||||
popScopeId,
|
||||
provide,
|
||||
proxyRefs,
|
||||
pushScopeId,
|
||||
queuePostFlushCb,
|
||||
reactive,
|
||||
readonly,
|
||||
ref,
|
||||
registerRuntimeCompiler,
|
||||
render,
|
||||
renderList,
|
||||
renderSlot,
|
||||
resolveComponent,
|
||||
resolveDirective,
|
||||
resolveDynamicComponent,
|
||||
resolveFilter,
|
||||
resolveTransitionHooks,
|
||||
setBlockTracking,
|
||||
setDevtoolsHook,
|
||||
setTransitionHooks,
|
||||
shallowReactive,
|
||||
shallowReadonly,
|
||||
shallowRef,
|
||||
ssrContextKey,
|
||||
ssrUtils,
|
||||
stop,
|
||||
toDisplayString,
|
||||
toHandlerKey,
|
||||
toHandlers,
|
||||
toRaw,
|
||||
toRef,
|
||||
toRefs,
|
||||
toValue,
|
||||
transformVNodeArgs,
|
||||
triggerRef,
|
||||
unref,
|
||||
useAttrs,
|
||||
useCssModule,
|
||||
useCssVars,
|
||||
useHost,
|
||||
useId,
|
||||
useModel,
|
||||
useSSRContext,
|
||||
useShadowRoot,
|
||||
useSlots,
|
||||
useTemplateRef,
|
||||
useTransitionState,
|
||||
vModelCheckbox,
|
||||
vModelDynamic,
|
||||
vModelRadio,
|
||||
vModelSelect,
|
||||
vModelText,
|
||||
vShow,
|
||||
version,
|
||||
warn,
|
||||
watch,
|
||||
watchEffect,
|
||||
watchPostEffect,
|
||||
watchSyncEffect,
|
||||
withAsyncContext,
|
||||
withCtx,
|
||||
withDefaults,
|
||||
withDirectives,
|
||||
withKeys,
|
||||
withMemo,
|
||||
withModifiers,
|
||||
withScopeId
|
||||
};
|
||||
//# sourceMappingURL=vue.js.map
|
||||
7
.vitepress/cache/deps/vue.js.map
vendored
Normal file
7
.vitepress/cache/deps/vue.js.map
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"version": 3,
|
||||
"sources": [],
|
||||
"sourcesContent": [],
|
||||
"mappings": "",
|
||||
"names": []
|
||||
}
|
||||
@@ -1,105 +1,122 @@
|
||||
---
|
||||
layout: doc
|
||||
---
|
||||
|
||||
# CeruMusic 插件开发文档
|
||||
# CeruMusic 插件开发指南
|
||||
|
||||
## 概述
|
||||
|
||||
本文档介绍如何为 CeruMusic 开发音乐源插件。CeruMusic 插件是运行在沙箱环境中的 JavaScript 模块,用于从各种音乐平台获取音乐资源。
|
||||
CeruMusic 支持两种类型的插件:
|
||||
1. **CeruMusic 原生插件**:基于 CeruMusic API 的插件格式
|
||||
2. **LX 兼容插件**:兼容 LX Music 的事件驱动插件格式
|
||||
|
||||
## 插件结构
|
||||
本文档将详细介绍如何开发这两种类型的插件。
|
||||
|
||||
### 基本结构
|
||||
## 文件要求
|
||||
|
||||
每个 CeruMusic 插件必须导出以下三个核心组件:
|
||||
- **编码格式**:UTF-8
|
||||
- **编程语言**:JavaScript (支持 ES6+ 语法)
|
||||
- **文件扩展名**:`.js`
|
||||
|
||||
```javascript
|
||||
module.exports = {
|
||||
pluginInfo, // 插件信息
|
||||
sources, // 支持的音源
|
||||
musicUrl // 获取音乐链接的函数
|
||||
}
|
||||
```
|
||||
## 插件信息注释
|
||||
|
||||
# 完整示例
|
||||
所有插件文件的开头必须包含以下注释格式:
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* 示例音乐插件
|
||||
* @author 开发者名称
|
||||
* @name 插件名称
|
||||
* @description 插件描述
|
||||
* @version 1.0.0
|
||||
* @author 作者名称
|
||||
* @homepage https://example.com
|
||||
*/
|
||||
```
|
||||
|
||||
### 注释字段说明
|
||||
|
||||
- `@name`:插件名称,建议不超过 24 个字符
|
||||
- `@description`:插件描述,建议不超过 36 个字符(可选)
|
||||
- `@version`:版本号(可选)
|
||||
- `@author`:作者名称(可选)
|
||||
- `@homepage`:主页地址(可选)
|
||||
|
||||
---
|
||||
|
||||
## CeruMusic 原生插件开发
|
||||
|
||||
首先 `澜音` 插件是面向 方法的 这意味着你直接导出方法即可为播放器提供音源
|
||||
|
||||
### 基本结构
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* @name 示例音乐源
|
||||
* @description CeruMusic 原生插件示例
|
||||
* @version 1.0.0
|
||||
* @author CeruMusic Team
|
||||
*/
|
||||
|
||||
// 1. 插件信息
|
||||
// 插件信息
|
||||
const pluginInfo = {
|
||||
name: '示例音源插件',
|
||||
version: '1.0.0',
|
||||
author: '开发者名称',
|
||||
description: '这是一个示例音乐源插件'
|
||||
}
|
||||
name: "示例音乐源",
|
||||
version: "1.0.0",
|
||||
author: "CeruMusic Team",
|
||||
description: "这是一个示例插件"
|
||||
};
|
||||
|
||||
// 2. 支持的音源配置
|
||||
// 支持的音源配置
|
||||
const sources = {
|
||||
demo: {
|
||||
name: '示例音源',
|
||||
type: 'music',
|
||||
qualitys: ['128k', '320k', 'flac']
|
||||
kw:{
|
||||
name: "酷我音乐",
|
||||
qualities: ['128k', '320k', 'flac', 'flac24bit']
|
||||
},
|
||||
demo2: {
|
||||
name: '示例音源2',
|
||||
type: 'music',
|
||||
qualitys: ['128k', '320k']
|
||||
tx:{
|
||||
name: "QQ音乐",
|
||||
qualities: ['128k', '320k', 'flac']
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 3. 获取音乐URL的核心函数
|
||||
// 获取音乐链接的主要方法
|
||||
async function musicUrl(source, musicInfo, quality) {
|
||||
// 从 cerumusic 对象获取 API
|
||||
const { request, env, version } = cerumusic
|
||||
try {
|
||||
// 使用 cerumusic API 发送 HTTP 请求
|
||||
const result = await cerumusic.request('https://api.example.com/music', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
...你的其他参数 可以 是密钥或者其他...
|
||||
},
|
||||
body: JSON.stringify({
|
||||
id: musicInfo.id,
|
||||
quality: quality
|
||||
})
|
||||
});
|
||||
|
||||
// 构建请求参数
|
||||
const songId = musicInfo.hash ?? musicInfo.songmid
|
||||
const apiUrl = `https://api.example.com/music/${source}/${songId}/${quality}`
|
||||
|
||||
console.log(`[${pluginInfo.name}] 请求音乐链接: ${apiUrl}`)
|
||||
|
||||
// 发起网络请求
|
||||
const { body, statusCode } = await request(apiUrl, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': `cerumusic-${env}/${version}`
|
||||
if (result.statusCode === 200 && result.body.url) {
|
||||
return result.body.url;
|
||||
} else {
|
||||
throw new Error('获取音乐链接失败');
|
||||
}
|
||||
})
|
||||
|
||||
// 处理响应
|
||||
if (statusCode !== 200 || body.code !== 200) {
|
||||
const errorMessage = body.msg || `接口错误 (HTTP: ${statusCode})`
|
||||
console.error(`[${pluginInfo.name}] Error: ${errorMessage}`)
|
||||
throw new Error(errorMessage)
|
||||
} catch (error) {
|
||||
console.error('获取音乐链接时发生错误:', error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
console.log(`[${pluginInfo.name}] 获取成功: ${body.url}`)
|
||||
return body.url
|
||||
}
|
||||
|
||||
// 4. 可选:获取封面图片
|
||||
// 获取歌曲封面(可选)
|
||||
async function getPic(source, musicInfo) {
|
||||
const { request } = cerumusic
|
||||
const songId = musicInfo.hash ?? musicInfo.songmid
|
||||
|
||||
const { body } = await request(`https://api.example.com/pic/${source}/${songId}`)
|
||||
return body.picUrl
|
||||
try {
|
||||
const result = await cerumusic.request(`https://api.example.com/pic/${musicInfo.id}`);
|
||||
return result.body.picUrl;
|
||||
} catch (error) {
|
||||
throw new Error('获取封面失败: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// 5. 可选:获取歌词
|
||||
// 获取歌词(可选)
|
||||
async function getLyric(source, musicInfo) {
|
||||
const { request } = cerumusic
|
||||
const songId = musicInfo.hash ?? musicInfo.songmid
|
||||
|
||||
const { body } = await request(`https://api.example.com/lyric/${source}/${songId}`)
|
||||
return body.lyric
|
||||
try {
|
||||
const result = await cerumusic.request(`https://api.example.com/lyric/${musicInfo.id}`);
|
||||
return result.body.lyric;
|
||||
} catch (error) {
|
||||
throw new Error('获取歌词失败: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// 导出插件
|
||||
@@ -107,279 +124,549 @@ module.exports = {
|
||||
pluginInfo,
|
||||
sources,
|
||||
musicUrl,
|
||||
getPic, // 可选
|
||||
getLyric // 可选
|
||||
getPic, // 可选
|
||||
getLyric // 可选
|
||||
};
|
||||
```
|
||||
|
||||
> #### PS:
|
||||
>
|
||||
> - `sources key` 取值
|
||||
>
|
||||
> - wy 网易云音乐 |
|
||||
> - tx QQ音乐 |
|
||||
> - kg 酷狗音乐 |
|
||||
> - mg 咪咕音乐 |
|
||||
> - kw 酷我音乐
|
||||
>
|
||||
> - 导出
|
||||
>
|
||||
> ```javascript
|
||||
> module.exports = {
|
||||
> sources, // 你的音源支持
|
||||
> };
|
||||
> ```
|
||||
>
|
||||
> - 支持的音质 ` sources.qualities: ['128k', '320k', 'flac']`
|
||||
>
|
||||
> - `128k`: 128kbps
|
||||
> - `320k`: 320kbps
|
||||
> - `flac`: FLAC 无损
|
||||
> - `flac24bit`: 24bit FLAC
|
||||
> - `hires`: Hi-Res 高解析度
|
||||
> - `atmos`: 杜比全景声
|
||||
> - `master`: 母带音质
|
||||
|
||||
|
||||
|
||||
### CeruMusic API 参考
|
||||
|
||||
#### cerumusic.request(url, options)
|
||||
|
||||
HTTP 请求方法,返回 Promise。
|
||||
|
||||
**参数:**
|
||||
- `url` (string): 请求地址
|
||||
- `options` (object): 请求选项
|
||||
- `method`: 请求方法 (GET, POST, PUT, DELETE 等)
|
||||
- `headers`: 请求头对象
|
||||
- `body`: 请求体
|
||||
- `timeout`: 超时时间(毫秒)
|
||||
|
||||
**返回值:**
|
||||
```javascript
|
||||
{
|
||||
statusCode: 200,
|
||||
headers: {...},
|
||||
body: {...} // 自动解析的响应体
|
||||
}
|
||||
```
|
||||
|
||||
## 详细说明
|
||||
#### cerumusic.utils
|
||||
|
||||
### 1. pluginInfo 对象
|
||||
|
||||
插件的基本信息,必须包含以下字段:
|
||||
工具方法集合:
|
||||
|
||||
```javascript
|
||||
const pluginInfo = {
|
||||
name: '插件名称', // 必需:插件显示名称
|
||||
version: '1.0.0', // 必需:版本号
|
||||
author: '作者名', // 必需:作者信息
|
||||
description: '插件描述' // 必需:功能描述
|
||||
}
|
||||
// Buffer 操作
|
||||
cerumusic.utils.buffer.from(data, encoding)
|
||||
cerumusic.utils.buffer.bufToString(buffer, encoding)
|
||||
|
||||
// 加密工具
|
||||
cerumusic.utils.crypto.md5(str)
|
||||
cerumusic.utils.crypto.randomBytes(size)
|
||||
cerumusic.utils.crypto.aesEncrypt(data, mode, key, iv)
|
||||
cerumusic.utils.crypto.rsaEncrypt(data, key)
|
||||
```
|
||||
|
||||
### 2. sources 对象
|
||||
#### cerumusic.NoticeCenter(type, data)
|
||||
|
||||
定义插件支持的音源,键为音源标识,值为音源配置:
|
||||
发送通知到用户界面:
|
||||
|
||||
```javascript
|
||||
const sources = {
|
||||
// 音源标识(用于API调用)
|
||||
source_id: {
|
||||
name: '音源显示名称', // 必需:用户看到的名称
|
||||
type: 'music', // 必需:固定为 'music'
|
||||
qualitys: [
|
||||
// 必需:支持的音质列表
|
||||
'128k', // 标准音质
|
||||
'320k', // 高音质
|
||||
'flac', // 无损音质
|
||||
'flac24bit', // 24位无损
|
||||
'hires' // 高解析度
|
||||
]
|
||||
cerumusic.NoticeCenter('info', {
|
||||
title: '通知标题',
|
||||
content: '通知内容',
|
||||
url: 'https://example.com', // 可选 当通知为update 版本跟新可传
|
||||
version: '版本号', // 当通知为update 版本跟新可传
|
||||
pluginInfo: {
|
||||
name: '插件名称',
|
||||
type: 'cr', // 固定唯一标识
|
||||
}// 当通知为update 版本跟新可传
|
||||
});
|
||||
```
|
||||
|
||||
**通知类型:**
|
||||
- `'info'`: 信息通知
|
||||
- `'success'`: 成功通知
|
||||
- `'warn'`: 警告通知
|
||||
- `'error'`: 错误通知
|
||||
- `'update'`: 更新通知
|
||||
|
||||
---
|
||||
|
||||
## LX 兼容插件开发 引用于落雪官网改编
|
||||
|
||||
CeruMusic 完全兼容 LX Music 的插件格式,支持事件驱动的开发模式。
|
||||
|
||||
### 基本结构
|
||||
|
||||
```javascript
|
||||
/**
|
||||
* @name 测试音乐源
|
||||
* @description 我只是一个测试音乐源哦
|
||||
* @version 1.0.0
|
||||
* @author xxx
|
||||
* @homepage http://xxx
|
||||
*/
|
||||
|
||||
const { EVENT_NAMES, request, on, send } = globalThis.lx
|
||||
|
||||
// 音质配置
|
||||
const qualitys = {
|
||||
kw: {
|
||||
'128k': '128',
|
||||
'320k': '320',
|
||||
flac: 'flac',
|
||||
flac24bit: 'flac24bit',
|
||||
},
|
||||
local: {},
|
||||
}
|
||||
|
||||
// HTTP 请求封装
|
||||
const httpRequest = (url, options) => new Promise((resolve, reject) => {
|
||||
request(url, options, (err, resp) => {
|
||||
if (err) return reject(err)
|
||||
resolve(resp.body)
|
||||
})
|
||||
})
|
||||
|
||||
// API 实现
|
||||
const apis = {
|
||||
kw: {
|
||||
musicUrl({ songmid }, quality) {
|
||||
return httpRequest('http://xxx').then(data => {
|
||||
return data.url
|
||||
})
|
||||
},
|
||||
},
|
||||
local: {
|
||||
musicUrl(info) {
|
||||
return httpRequest('http://xxx').then(data => {
|
||||
return data.url
|
||||
})
|
||||
},
|
||||
pic(info) {
|
||||
return httpRequest('http://xxx').then(data => {
|
||||
return data.url
|
||||
})
|
||||
},
|
||||
lyric(info) {
|
||||
return httpRequest('http://xxx').then(data => {
|
||||
return {
|
||||
lyric: '...', // 歌曲歌词
|
||||
tlyric: '...', // 翻译歌词,没有可为 null
|
||||
rlyric: '...', // 罗马音歌词,没有可为 null
|
||||
lxlyric: '...', // lx 逐字歌词,没有可为 null
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. musicUrl 函数
|
||||
|
||||
获取音乐播放链接的核心函数:
|
||||
|
||||
```javascript
|
||||
async function musicUrl(source, musicInfo, quality) {
|
||||
// source: 音源标识(sources 对象的键)
|
||||
// musicInfo: 歌曲信息对象
|
||||
// quality: 请求的音质
|
||||
// 返回: Promise<string> - 音乐播放链接
|
||||
}
|
||||
```
|
||||
|
||||
#### musicInfo 对象结构
|
||||
|
||||
```javascript
|
||||
const musicInfo = {
|
||||
songmid: '歌曲ID', // 歌曲标识符
|
||||
hash: '歌曲哈希', // 备用标识符
|
||||
title: '歌曲标题', // 歌曲名称
|
||||
artist: '艺术家', // 演唱者
|
||||
album: '专辑名' // 专辑信息
|
||||
// ... 其他可能的字段
|
||||
}
|
||||
```
|
||||
|
||||
## 可用 API
|
||||
|
||||
### cerumusic 对象
|
||||
|
||||
插件运行时可以访问 `cerumusic` 全局对象:
|
||||
|
||||
```javascript
|
||||
const { request, env, version, utils } = cerumusic
|
||||
```
|
||||
|
||||
#### request 函数
|
||||
|
||||
用于发起 HTTP 请求:
|
||||
|
||||
```javascript
|
||||
// Promise 模式
|
||||
const response = await request(url, options)
|
||||
|
||||
// Callback 模式
|
||||
request(url, options, (error, response) => {
|
||||
if (error) {
|
||||
console.error('请求失败:', error)
|
||||
return
|
||||
// 注册 API 请求事件
|
||||
on(EVENT_NAMES.request, ({ source, action, info }) => {
|
||||
switch (action) {
|
||||
case 'musicUrl':
|
||||
return apis[source].musicUrl(info.musicInfo, qualitys[source][info.type])
|
||||
case 'lyric':
|
||||
return apis[source].lyric(info.musicInfo)
|
||||
case 'pic':
|
||||
return apis[source].pic(info.musicInfo)
|
||||
}
|
||||
console.log('响应:', response)
|
||||
})
|
||||
|
||||
// 发送初始化完成事件
|
||||
send(EVENT_NAMES.inited, {
|
||||
openDevTools: false, // 是否打开开发者工具
|
||||
sources: {
|
||||
kw: {
|
||||
name: '酷我音乐',
|
||||
type: 'music',
|
||||
actions: ['musicUrl'],
|
||||
qualitys: ['128k', '320k', 'flac', 'flac24bit'],
|
||||
},
|
||||
local: {
|
||||
name: '本地音乐',
|
||||
type: 'music',
|
||||
actions: ['musicUrl', 'lyric', 'pic'],
|
||||
qualitys: [],
|
||||
},
|
||||
},
|
||||
})
|
||||
```
|
||||
|
||||
**参数说明:**
|
||||
### LX API 参考
|
||||
|
||||
- `url` (string): 请求地址
|
||||
- `options` (Object): 请求选项
|
||||
- `method`: HTTP 方法 ('GET', 'POST', 等)
|
||||
- `headers`: 请求头对象
|
||||
- `body`: 请求体(POST 请求时)
|
||||
#### globalThis.lx.EVENT_NAMES
|
||||
|
||||
**响应格式:**
|
||||
事件名称常量:
|
||||
|
||||
- `inited`: 初始化完成事件
|
||||
- `request`: API 请求事件
|
||||
- `updateAlert`: 更新提示事件
|
||||
|
||||
#### globalThis.lx.on(eventName, handler)
|
||||
|
||||
注册事件监听器:
|
||||
|
||||
```javascript
|
||||
{
|
||||
body: {}, // 解析后的响应体
|
||||
statusCode: 200, // HTTP 状态码
|
||||
headers: {} // 响应头
|
||||
}
|
||||
lx.on(lx.EVENT_NAMES.request, ({ source, action, info }) => {
|
||||
// 必须返回 Promise
|
||||
return Promise.resolve(result);
|
||||
});
|
||||
```
|
||||
|
||||
#### utils 对象
|
||||
#### globalThis.lx.send(eventName, data)
|
||||
|
||||
提供实用工具函数:
|
||||
发送事件:
|
||||
|
||||
```javascript
|
||||
const { utils } = cerumusic
|
||||
// 发送初始化事件
|
||||
lx.send(lx.EVENT_NAMES.inited, {
|
||||
openDevTools: false,
|
||||
sources: {...}
|
||||
});
|
||||
|
||||
// 发送更新提示
|
||||
lx.send(lx.EVENT_NAMES.updateAlert, {
|
||||
log: '更新日志\n修复了一些问题',
|
||||
updateUrl: 'https://example.com/update'
|
||||
});
|
||||
```
|
||||
|
||||
#### globalThis.lx.request(url, options, callback)
|
||||
|
||||
HTTP 请求方法:
|
||||
|
||||
```javascript
|
||||
lx.request('https://api.example.com', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(data),
|
||||
timeout: 10000
|
||||
}, (err, resp) => {
|
||||
if (err) {
|
||||
console.error('请求失败:', err);
|
||||
return;
|
||||
}
|
||||
console.log('响应:', resp.body);
|
||||
});
|
||||
```
|
||||
|
||||
#### globalThis.lx.utils
|
||||
|
||||
工具方法:
|
||||
|
||||
```javascript
|
||||
// Buffer 操作
|
||||
const buffer = utils.buffer.from('hello', 'utf8')
|
||||
const string = utils.buffer.bufToString(buffer, 'utf8')
|
||||
lx.utils.buffer.from(data, encoding)
|
||||
lx.utils.buffer.bufToString(buffer, encoding)
|
||||
|
||||
// 加密工具
|
||||
lx.utils.crypto.md5(str)
|
||||
lx.utils.crypto.aesEncrypt(buffer, mode, key, iv)
|
||||
lx.utils.crypto.randomBytes(size)
|
||||
lx.utils.crypto.rsaEncrypt(buffer, key)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 音源配置
|
||||
|
||||
### 支持的音源 ID
|
||||
|
||||
- `kw`: 酷我音乐
|
||||
- `kg`: 酷狗音乐
|
||||
- `tx`: QQ音乐
|
||||
- `wy`: 网易云音乐
|
||||
- `mg`: 咪咕音乐
|
||||
- `local`: 本地音乐
|
||||
|
||||
### 支持的音质
|
||||
|
||||
- `128k`: 128kbps
|
||||
- `320k`: 320kbps
|
||||
- `flac`: FLAC 无损
|
||||
- `flac24bit`: 24bit FLAC
|
||||
- `hires`: Hi-Res 高解析度
|
||||
- `atmos`: 杜比全景声
|
||||
- `master`: 母带音质
|
||||
|
||||
---
|
||||
|
||||
## 错误处理
|
||||
|
||||
### 最佳实践
|
||||
|
||||
1. **总是检查 API 响应状态**
|
||||
```javascript
|
||||
async function musicUrl(source, musicInfo, quality) {
|
||||
try {
|
||||
// 参数验证
|
||||
if (!musicInfo || !musicInfo.id) {
|
||||
throw new Error('音乐信息不完整');
|
||||
}
|
||||
|
||||
```javascript
|
||||
if (statusCode !== 200 || body.code !== 200) {
|
||||
throw new Error(`请求失败: ${body.msg || '未知错误'}`)
|
||||
}
|
||||
```
|
||||
// API 调用
|
||||
const result = await cerumusic.request(url, options);
|
||||
|
||||
// 结果验证
|
||||
if (!result || result.statusCode !== 200) {
|
||||
throw new Error(`API 请求失败: ${result?.statusCode || 'Unknown'}`);
|
||||
}
|
||||
|
||||
2. **提供有意义的错误信息**
|
||||
if (!result.body || !result.body.url) {
|
||||
throw new Error('返回数据格式错误');
|
||||
}
|
||||
|
||||
```javascript
|
||||
console.error(`[${pluginInfo.name}] Error: ${errorMessage}`)
|
||||
throw new Error(errorMessage)
|
||||
```
|
||||
|
||||
3. **处理网络异常**
|
||||
```javascript
|
||||
try {
|
||||
const response = await request(url, options)
|
||||
// 处理响应
|
||||
} catch (error) {
|
||||
console.error(`[${pluginInfo.name}] 网络请求失败:`, error.message)
|
||||
throw new Error(`网络错误: ${error.message}`)
|
||||
}
|
||||
```
|
||||
return result.body.url;
|
||||
} catch (error) {
|
||||
// 记录错误日志
|
||||
console.error(`[${source}] 获取音乐链接失败:`, error.message);
|
||||
|
||||
// 重新抛出错误供上层处理
|
||||
throw new Error(`获取 ${source} 音乐链接失败: ${error.message}`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 常见错误类型
|
||||
|
||||
- **网络错误**: 无法连接到 API 服务器
|
||||
- **认证错误**: API 密钥无效或过期
|
||||
- **参数错误**: 请求参数格式不正确
|
||||
- **资源不存在**: 请求的歌曲不存在
|
||||
- **限流错误**: 请求过于频繁
|
||||
1. **网络错误**: 请求超时、连接失败
|
||||
2. **API 错误**: 接口返回错误状态码
|
||||
3. **数据错误**: 返回数据格式不正确
|
||||
4. **参数错误**: 传入参数不完整或格式错误
|
||||
|
||||
## 事件驱动插件
|
||||
|
||||
对于使用 `lx.on(EVENT_NAMES.request)` 模式的插件,可以使用转换器:
|
||||
|
||||
```javascript
|
||||
// 使用转换器转换事件驱动插件
|
||||
node converter-event-driven.js input-plugin.js output-plugin.js
|
||||
```
|
||||
|
||||
转换后的插件将兼容 CeruMusicPluginHost。
|
||||
---
|
||||
|
||||
## 调试技巧
|
||||
|
||||
### 1. 使用 console.log
|
||||
|
||||
```javascript
|
||||
console.log(`[${pluginInfo.name}] 调试信息:`, data)
|
||||
console.error(`[${pluginInfo.name}] 错误:`, error)
|
||||
console.log('[插件名] 调试信息:', data);
|
||||
console.warn('[插件名] 警告信息:', warning);
|
||||
console.error('[插件名] 错误信息:', error);
|
||||
```
|
||||
|
||||
### 2. 检查请求和响应
|
||||
### 2. LX 插件开发者工具
|
||||
|
||||
```javascript
|
||||
console.log('请求URL:', url)
|
||||
console.log('请求选项:', options)
|
||||
console.log('响应状态:', statusCode)
|
||||
console.log('响应内容:', body)
|
||||
send(EVENT_NAMES.inited, {
|
||||
openDevTools: true, // 开启开发者工具
|
||||
sources: {...}
|
||||
});
|
||||
```
|
||||
|
||||
### 3. 测试插件
|
||||
|
||||
创建测试文件:
|
||||
### 3. 错误捕获
|
||||
|
||||
```javascript
|
||||
const CeruMusicPluginHost = require('./CeruMusicPluginHost')
|
||||
process.on('unhandledRejection', (reason, promise) => {
|
||||
console.error('未处理的 Promise 拒绝:', reason);
|
||||
});
|
||||
```
|
||||
|
||||
async function testPlugin() {
|
||||
const host = new CeruMusicPluginHost()
|
||||
await host.loadPlugin('./my-plugin.js')
|
||||
---
|
||||
|
||||
const musicInfo = {
|
||||
songmid: 'test123',
|
||||
title: '测试歌曲'
|
||||
## 性能优化
|
||||
|
||||
### 1. 请求缓存
|
||||
|
||||
```javascript
|
||||
const cache = new Map();
|
||||
|
||||
async function getCachedData(key, fetcher, ttl = 300000) {
|
||||
const cached = cache.get(key);
|
||||
if (cached && Date.now() - cached.timestamp < ttl) {
|
||||
return cached.data;
|
||||
}
|
||||
|
||||
const data = await fetcher();
|
||||
cache.set(key, { data, timestamp: Date.now() });
|
||||
return data;
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 请求超时控制
|
||||
|
||||
```javascript
|
||||
const result = await cerumusic.request(url, {
|
||||
timeout: 10000 // 10秒超时
|
||||
});
|
||||
```
|
||||
|
||||
### 3. 并发控制
|
||||
|
||||
```javascript
|
||||
// 限制并发请求数量
|
||||
const semaphore = new Semaphore(3); // 最多3个并发请求
|
||||
|
||||
async function limitedRequest(url, options) {
|
||||
await semaphore.acquire();
|
||||
try {
|
||||
const url = await host.getMusicUrl('demo', musicInfo, '320k')
|
||||
console.log('成功获取URL:', url)
|
||||
} catch (error) {
|
||||
console.error('测试失败:', error.message)
|
||||
return await cerumusic.request(url, options);
|
||||
} finally {
|
||||
semaphore.release();
|
||||
}
|
||||
}
|
||||
|
||||
testPlugin()
|
||||
```
|
||||
|
||||
## 发布和分发
|
||||
---
|
||||
|
||||
### 文件结构
|
||||
## 安全注意事项
|
||||
|
||||
```
|
||||
my-plugin/
|
||||
├── plugin.js # 主插件文件
|
||||
├── package.json # 包信息(可选)
|
||||
├── README.md # 说明文档
|
||||
└── test.js # 测试文件(可选)
|
||||
### 1. 输入验证
|
||||
|
||||
```javascript
|
||||
function validateMusicInfo(musicInfo) {
|
||||
if (!musicInfo || typeof musicInfo !== 'object') {
|
||||
throw new Error('音乐信息格式错误');
|
||||
}
|
||||
|
||||
if (!musicInfo.id || typeof musicInfo.id !== 'string') {
|
||||
throw new Error('音乐 ID 无效');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
### 版本管理
|
||||
### 2. URL 验证
|
||||
|
||||
遵循语义化版本规范:
|
||||
```javascript
|
||||
function isValidUrl(url) {
|
||||
try {
|
||||
const urlObj = new URL(url);
|
||||
return urlObj.protocol === 'http:' || urlObj.protocol === 'https:';
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- `1.0.0` - 主版本.次版本.修订版本
|
||||
### 3. 敏感信息保护
|
||||
|
||||
```javascript
|
||||
// 不要在日志中输出敏感信息
|
||||
console.log('请求参数:', {
|
||||
...params,
|
||||
token: '***', // 隐藏敏感信息
|
||||
password: '***'
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 插件发布
|
||||
|
||||
### 1. 代码检查清单
|
||||
|
||||
- [ ] 插件信息注释完整
|
||||
- [ ] 错误处理完善
|
||||
- [ ] 性能优化合理
|
||||
- [ ] 安全验证到位
|
||||
- [ ] 测试覆盖充分
|
||||
|
||||
### 2. 测试建议
|
||||
|
||||
```javascript
|
||||
// 单元测试示例
|
||||
async function testMusicUrl() {
|
||||
const testMusicInfo = {
|
||||
id: 'test123',
|
||||
name: '测试歌曲',
|
||||
artist: '测试歌手'
|
||||
};
|
||||
|
||||
try {
|
||||
const url = await musicUrl('kw', testMusicInfo, '320k');
|
||||
console.log('测试通过:', url);
|
||||
} catch (error) {
|
||||
console.error('测试失败:', error);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 版本管理
|
||||
|
||||
使用语义化版本号:
|
||||
- `1.0.0`: 主版本.次版本.修订版本
|
||||
- 主版本:不兼容的 API 修改
|
||||
- 次版本:向下兼容的功能性新增
|
||||
- 修订版本:向下兼容的问题修正
|
||||
|
||||
## 示例插件
|
||||
|
||||
查看项目中的示例:
|
||||
|
||||
- `example-plugin.js` - 基础插件示例
|
||||
- `plugin.js` - 事件驱动插件示例
|
||||
- `fm.js` - 复杂插件示例
|
||||
---
|
||||
|
||||
## 常见问题
|
||||
|
||||
**Q: 如何处理需要登录的 API?**
|
||||
### Q: 插件加载失败怎么办?
|
||||
|
||||
A: 在请求头中添加认证信息,或使用 Cookie。
|
||||
A: 检查以下几点:
|
||||
1. 文件编码是否为 UTF-8
|
||||
2. 插件信息注释格式是否正确
|
||||
3. JavaScript 语法是否有错误
|
||||
4. 是否正确导出了必需的方法
|
||||
|
||||
**Q: 如何处理加密的 API 响应?**
|
||||
### Q: 如何处理跨域请求?
|
||||
|
||||
A: 在插件中实现解密逻辑,使用 `utils` 对象提供的工具函数。
|
||||
A: CeruMusic 的请求方法不受浏览器跨域限制,可以直接请求任何域名的 API。
|
||||
|
||||
**Q: 插件可以访问文件系统吗?**
|
||||
### Q: 插件如何更新?
|
||||
|
||||
A: 不可以,插件运行在受限的沙箱环境中,无法直接访问文件系统。
|
||||
A: 使用 `cerumusic.NoticeCenter` 事件通知用户更新:
|
||||
|
||||
**Q: 如何优化插件性能?**
|
||||
```javascript
|
||||
cerumusic.NoticeCenter('update',{
|
||||
title:'新版本更新',
|
||||
content:'xxxx',
|
||||
version: 'v1.0.3',
|
||||
url:'https://shiqianjiang.cn',
|
||||
pluginInfo:{
|
||||
type:'cr'
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
A: 减少不必要的网络请求,使用适当的缓存策略,避免阻塞操作。
|
||||
### Q: 如何调试插件?
|
||||
|
||||
## 贡献指南
|
||||
A:
|
||||
1. 使用 `console.log` 输出调试信息 可在设置—>插件管理—>日志 查看调试
|
||||
2. LX 插件可以设置 `openDevTools: true` 打开开发者工具
|
||||
3. 查看 CeruMusic 的插件日志
|
||||
|
||||
1. Fork 项目仓库
|
||||
2. 创建功能分支
|
||||
3. 编写插件代码和测试
|
||||
4. 提交 Pull Request
|
||||
5. 等待代码审查
|
||||
---
|
||||
|
||||
欢迎贡献新的音源插件!
|
||||
## 技术支持
|
||||
|
||||
如有问题或建议,请通过以下方式联系:
|
||||
- GitHub Issues: [CeruMusic Issues](https://github.com/timeshiftsauce/CeruMusic/issues)
|
||||
- Blog (最好登录,否则需要审核): [CeruMusic Blog](https://shiqianjiang.cn/blog/4966904626407280640)
|
||||
|
||||
@@ -4,7 +4,22 @@
|
||||
|
||||
## 日志
|
||||
|
||||
- 2025-9-17 **(V1.3.1)**
|
||||
- ###### 2025-9-17 **(V1.3.2)**
|
||||
|
||||
1. 目录结构调整
|
||||
|
||||
2. **支持插件更新提示**
|
||||
|
||||
**洛雪** 插件请手动重装适配
|
||||
|
||||
3. **debug**
|
||||
|
||||
- SMTC 问题
|
||||
|
||||
- 歌曲缓存播放多次请求和多次缓存问题
|
||||
|
||||
- ###### 2025-9-17 **(V1.3.1)**
|
||||
|
||||
1. **设置功能页**
|
||||
- 缓存路径支持自定义
|
||||
- 下载路径支持自定义
|
||||
|
||||
84
docs/plugin-notice-test.js
Normal file
84
docs/plugin-notice-test.js
Normal file
@@ -0,0 +1,84 @@
|
||||
// 测试插件通知功能的示例插件
|
||||
// 这个文件可以用来测试 NoticeCenter 功能
|
||||
|
||||
const pluginInfo = {
|
||||
name: "测试通知插件",
|
||||
version: "1.0.0",
|
||||
author: "CeruMusic Team",
|
||||
description: "用于测试插件通知功能的示例插件",
|
||||
type: "cr"
|
||||
}
|
||||
|
||||
const sources = [
|
||||
{
|
||||
name: "test",
|
||||
qualities: ["128k", "320k"]
|
||||
}
|
||||
]
|
||||
|
||||
// 模拟音乐URL获取函数
|
||||
async function musicUrl(source, musicInfo, quality) {
|
||||
console.log('测试插件:获取音乐URL')
|
||||
|
||||
// 测试不同类型的通知
|
||||
setTimeout(() => {
|
||||
// 测试信息通知
|
||||
this.cerumusic.NoticeCenter('info', {
|
||||
title: '信息通知',
|
||||
message: '这是一个信息通知测试',
|
||||
content: '插件正在正常工作'
|
||||
})
|
||||
}, 1000)
|
||||
|
||||
setTimeout(() => {
|
||||
// 测试警告通知
|
||||
this.cerumusic.NoticeCenter('warning', {
|
||||
title: '警告通知',
|
||||
message: '这是一个警告通知测试',
|
||||
content: '请注意某些设置'
|
||||
})
|
||||
}, 2000)
|
||||
|
||||
setTimeout(() => {
|
||||
// 测试成功通知
|
||||
this.cerumusic.NoticeCenter('success', {
|
||||
title: '成功通知',
|
||||
message: '操作已成功完成',
|
||||
content: '音乐URL获取成功'
|
||||
})
|
||||
}, 3000)
|
||||
|
||||
setTimeout(() => {
|
||||
// 测试更新通知
|
||||
this.cerumusic.NoticeCenter('update', {
|
||||
title: '插件更新',
|
||||
message: '发现新版本 v2.0.0,是否立即更新?',
|
||||
url: 'https://example.com/plugin-update.js',
|
||||
version: '2.0.0',
|
||||
pluginInfo: {
|
||||
name: '测试通知插件',
|
||||
type: 'cr',
|
||||
forcedUpdate: false
|
||||
}
|
||||
})
|
||||
}, 4000)
|
||||
|
||||
setTimeout(() => {
|
||||
// 测试错误通知
|
||||
this.cerumusic.NoticeCenter('error', {
|
||||
title: '错误通知',
|
||||
message: '这是一个错误通知测试',
|
||||
error: '模拟的错误信息'
|
||||
})
|
||||
}, 5000)
|
||||
|
||||
// 返回一个测试URL
|
||||
return 'https://example.com/test-music.mp3'
|
||||
}
|
||||
|
||||
// 导出插件
|
||||
module.exports = {
|
||||
pluginInfo,
|
||||
sources,
|
||||
musicUrl
|
||||
}
|
||||
215
docs/plugin-notice-usage.md
Normal file
215
docs/plugin-notice-usage.md
Normal file
@@ -0,0 +1,215 @@
|
||||
# 插件通知系统使用说明
|
||||
|
||||
## 概述
|
||||
|
||||
CeruMusic 插件通知系统允许插件向用户显示各种类型的通知对话框,包括信息、警告、错误、成功和更新通知。
|
||||
|
||||
## 功能特性
|
||||
|
||||
### 🎯 支持的通知类型
|
||||
|
||||
1. **信息通知 (info)** - 显示一般信息
|
||||
2. **警告通知 (warning)** - 显示警告信息
|
||||
3. **错误通知 (error)** - 显示错误信息
|
||||
4. **成功通知 (success)** - 显示成功信息
|
||||
5. **更新通知 (update)** - 显示插件更新信息,支持一键更新
|
||||
|
||||
### 🎨 界面特性
|
||||
|
||||
- 使用 TDesign 组件库,界面美观统一
|
||||
- 支持深色主题适配
|
||||
- 响应式设计,移动端友好
|
||||
- 不同通知类型有对应的图标和颜色
|
||||
|
||||
### ⚡ 技术特性
|
||||
|
||||
- 基于 Electron IPC 通信
|
||||
- TypeScript 类型安全
|
||||
- 异步操作支持
|
||||
- 错误处理完善
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 在插件中调用通知
|
||||
|
||||
```javascript
|
||||
// 基本用法
|
||||
this.cerumusic.NoticeCenter(type, data)
|
||||
|
||||
// 信息通知
|
||||
this.cerumusic.NoticeCenter('info', {
|
||||
title: '插件信息',
|
||||
message: '这是一条信息通知',
|
||||
content: '详细的信息内容'
|
||||
})
|
||||
|
||||
// 警告通知
|
||||
this.cerumusic.NoticeCenter('warning', {
|
||||
title: '注意',
|
||||
message: '这是一条警告信息',
|
||||
content: '请检查相关设置'
|
||||
})
|
||||
|
||||
// 错误通知
|
||||
this.cerumusic.NoticeCenter('error', {
|
||||
title: '错误',
|
||||
message: '操作失败',
|
||||
error: '具体的错误信息'
|
||||
})
|
||||
|
||||
// 成功通知
|
||||
this.cerumusic.NoticeCenter('success', {
|
||||
title: '成功',
|
||||
message: '操作已成功完成'
|
||||
})
|
||||
|
||||
// 更新通知(特殊)
|
||||
this.cerumusic.NoticeCenter('update', {
|
||||
title: '插件更新',
|
||||
message: '发现新版本,是否立即更新?',
|
||||
url: 'https://example.com/plugin-update.js',
|
||||
version: '2.0.0',
|
||||
pluginInfo: {
|
||||
name: '插件名称',
|
||||
type: 'cr', // 'cr' 或 'lx'
|
||||
forcedUpdate: false
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### 参数说明
|
||||
|
||||
#### 通用参数 (data 对象)
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| title | string | 否 | 通知标题,不提供时使用默认标题 |
|
||||
| message | string | 否 | 通知消息内容 |
|
||||
| content | string | 否 | 详细内容(与 message 二选一) |
|
||||
|
||||
#### 更新通知特有参数
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| url | string | 是 | 插件更新下载链接 |
|
||||
| version | string | 否 | 新版本号 |
|
||||
| pluginInfo.name | string | 否 | 插件名称 |
|
||||
| pluginInfo.type | 'cr' \| 'lx' | 否 | 插件类型 |
|
||||
| pluginInfo.forcedUpdate | boolean | 否 | 是否强制更新 |
|
||||
|
||||
#### 错误通知特有参数
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| error | string | 否 | 具体错误信息 |
|
||||
|
||||
## 实现原理
|
||||
|
||||
### 架构图
|
||||
|
||||
```
|
||||
插件代码
|
||||
↓ (调用 NoticeCenter)
|
||||
CeruMusicPluginHost
|
||||
↓ (sendPluginNotice)
|
||||
pluginNotice.ts (主进程)
|
||||
↓ (IPC 通信)
|
||||
PluginNoticeDialog.vue (渲染进程)
|
||||
↓ (显示对话框)
|
||||
用户界面
|
||||
```
|
||||
|
||||
### 文件结构
|
||||
|
||||
```
|
||||
src/
|
||||
├── main/
|
||||
│ ├── events/
|
||||
│ │ └── pluginNotice.ts # 主进程通知处理
|
||||
│ └── services/plugin/manager/
|
||||
│ └── CeruMusicPluginHost.ts # 插件主机
|
||||
├── renderer/src/
|
||||
│ ├── components/
|
||||
│ │ └── PluginNoticeDialog.vue # 通知对话框组件
|
||||
│ └── App.vue # 主应用(注册组件)
|
||||
└── preload/
|
||||
└── index.ts # IPC API 定义
|
||||
```
|
||||
|
||||
## 测试
|
||||
|
||||
### 使用测试插件
|
||||
|
||||
1. 将 `docs/plugin-notice-test.js` 作为插件加载
|
||||
2. 调用插件的 `musicUrl` 方法
|
||||
3. 观察不同类型的通知是否正确显示
|
||||
|
||||
### 测试场景
|
||||
|
||||
- [x] 信息通知显示
|
||||
- [x] 警告通知显示
|
||||
- [x] 错误通知显示
|
||||
- [x] 成功通知显示
|
||||
- [x] 更新通知显示(带更新按钮)
|
||||
- [x] 更新按钮功能
|
||||
- [x] 对话框关闭功能
|
||||
- [x] 响应式布局
|
||||
- [x] 深色主题适配
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **URL 验证**: 更新通知的 URL 必须是有效的 HTTP/HTTPS 链接
|
||||
2. **错误处理**: 所有通知操作都有完善的错误处理机制
|
||||
3. **性能考虑**: 避免频繁发送通知,可能影响用户体验
|
||||
4. **类型安全**: 使用 TypeScript 确保参数类型正确
|
||||
|
||||
## 扩展功能
|
||||
|
||||
### 未来可能的增强
|
||||
|
||||
- [ ] 通知历史记录
|
||||
- [ ] 通知优先级系统
|
||||
- [ ] 批量通知管理
|
||||
- [ ] 自定义通知样式
|
||||
- [ ] 通知声音提醒
|
||||
- [ ] 通知位置自定义
|
||||
|
||||
## 故障排除
|
||||
|
||||
### 常见问题
|
||||
|
||||
1. **通知不显示**
|
||||
- 检查主窗口是否存在
|
||||
- 确认 IPC 通信是否正常
|
||||
- 查看控制台错误信息
|
||||
|
||||
2. **更新按钮无响应**
|
||||
- 确认更新 URL 是否有效
|
||||
- 检查网络连接
|
||||
- 查看主进程日志
|
||||
|
||||
3. **样式显示异常**
|
||||
- 确认 TDesign 组件库已正确加载
|
||||
- 检查 CSS 样式是否冲突
|
||||
- 验证主题配置
|
||||
|
||||
### 调试方法
|
||||
|
||||
```javascript
|
||||
// 在插件中添加调试日志
|
||||
console.log('[Plugin] 发送通知:', type, data)
|
||||
|
||||
// 在渲染进程中监听通知
|
||||
window.api.on('plugin-notice', (_, notice) => {
|
||||
console.log('[Renderer] 收到通知:', notice)
|
||||
})
|
||||
```
|
||||
|
||||
## 更新日志
|
||||
|
||||
### v1.0.0 (2025-09-20)
|
||||
- ✨ 初始版本发布
|
||||
- ✨ 支持 5 种通知类型
|
||||
- ✨ 完整的 TypeScript 类型定义
|
||||
- ✨ 响应式设计和深色主题支持
|
||||
- ✨ 完善的错误处理机制
|
||||
@@ -36,14 +36,16 @@ export default defineConfig({
|
||||
TDesignResolver({
|
||||
library: 'vue-next'
|
||||
})
|
||||
]
|
||||
],
|
||||
dts: true
|
||||
}),
|
||||
Components({
|
||||
resolvers: [
|
||||
TDesignResolver({
|
||||
library: 'vue-next'
|
||||
})
|
||||
]
|
||||
],
|
||||
dts: true
|
||||
})
|
||||
],
|
||||
base: './',
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ceru-music",
|
||||
"version": "1.3.1",
|
||||
"version": "1.3.2",
|
||||
"description": "一款简洁优雅的音乐播放器",
|
||||
"main": "./out/main/index.js",
|
||||
"author": "sqj,wldss,star",
|
||||
|
||||
161
src/main/events/pluginNotice.ts
Normal file
161
src/main/events/pluginNotice.ts
Normal file
@@ -0,0 +1,161 @@
|
||||
/*
|
||||
* Copyright (c) 2025. 时迁酱 Inc. All rights reserved.
|
||||
*
|
||||
* This software is the confidential and proprietary information of 时迁酱.
|
||||
* Unauthorized copying of this file, via any medium is strictly prohibited.
|
||||
*
|
||||
* @author 时迁酱,无聊的霜霜,Star
|
||||
* @since 2025-9-20
|
||||
* @version 1.0
|
||||
*/
|
||||
|
||||
import { BrowserWindow } from 'electron'
|
||||
|
||||
export interface PluginNoticeData {
|
||||
type: 'error' | 'info' | 'success' | 'warn' | 'update'
|
||||
data: {
|
||||
title?: string
|
||||
content?: string
|
||||
message?: string
|
||||
url?: string
|
||||
version?: string
|
||||
pluginInfo?: {
|
||||
name?: string
|
||||
type?: 'lx' | 'cr'
|
||||
forcedUpdate?: boolean
|
||||
}
|
||||
}
|
||||
currentVersion?: string
|
||||
timestamp?: number
|
||||
pluginName?: string
|
||||
}
|
||||
|
||||
export interface DialogNotice {
|
||||
type: string
|
||||
data: any
|
||||
timestamp: number
|
||||
pluginName: string
|
||||
dialogType: 'update' | 'info' | 'error' | 'warning' | 'success'
|
||||
title: string
|
||||
message: string
|
||||
updateUrl?: string
|
||||
pluginType?: 'lx' | 'cr'
|
||||
currentVersion?: string
|
||||
newVersion?: string
|
||||
actions: Array<{
|
||||
text: string
|
||||
type: 'cancel' | 'update' | 'confirm'
|
||||
primary?: boolean
|
||||
}>
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证 URL 是否有效
|
||||
*/
|
||||
function isValidUrl(url: string): boolean {
|
||||
try {
|
||||
const urlObj = new URL(url)
|
||||
return urlObj.protocol === 'http:' || urlObj.protocol === 'https:'
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据通知类型获取标题
|
||||
*/
|
||||
function getNoticeTitle(type: string): string {
|
||||
const titleMap: Record<string, string> = {
|
||||
update: '插件更新',
|
||||
error: '插件错误',
|
||||
warning: '插件警告',
|
||||
info: '插件信息',
|
||||
success: '操作成功'
|
||||
}
|
||||
return titleMap[type] || '插件通知'
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据通知类型获取默认消息
|
||||
*/
|
||||
function getDefaultMessage(type: string, data: any, pluginName: string): string {
|
||||
switch (type) {
|
||||
case 'error':
|
||||
return `插件 "${pluginName}" 发生错误: ${data?.error || data?.message || '未知错误'}`
|
||||
case 'warning':
|
||||
return `插件 "${pluginName}" 警告: ${data?.warning || data?.message || '需要注意'}`
|
||||
case 'success':
|
||||
return `插件 "${pluginName}" 操作成功: ${data?.message || ''}`
|
||||
case 'info':
|
||||
default:
|
||||
return data?.message || `插件 "${pluginName}" 信息: ${JSON.stringify(data)}`
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送插件通知到渲染进程
|
||||
*/
|
||||
export function sendPluginNotice(noticeData: PluginNoticeData, pluginName?: string): void {
|
||||
try {
|
||||
console.log(`[CeruMusic] 插件通知: ${noticeData.type}`, noticeData.data)
|
||||
|
||||
// 获取主窗口实例
|
||||
const mainWindow = BrowserWindow.getAllWindows().find((win) => !win.isDestroyed())
|
||||
if (!mainWindow) {
|
||||
console.warn('[CeruMusic] 未找到主窗口,无法发送通知')
|
||||
return
|
||||
}
|
||||
|
||||
// 构建通知数据
|
||||
const baseNoticeData = {
|
||||
type: noticeData.type,
|
||||
data: noticeData.data,
|
||||
timestamp: noticeData.timestamp || Date.now(),
|
||||
pluginName: pluginName || noticeData.pluginName || 'Unknown Plugin'
|
||||
}
|
||||
|
||||
// 根据通知类型处理不同的逻辑
|
||||
if (noticeData.type === 'update' && noticeData.data?.url && isValidUrl(noticeData.data.url)) {
|
||||
// 更新通知 - 显示带更新按钮的对话框
|
||||
const updateNotice: DialogNotice = {
|
||||
...baseNoticeData,
|
||||
dialogType: 'update',
|
||||
title: noticeData.data.title || '插件更新',
|
||||
message: noticeData.data.content || `插件 "${baseNoticeData.pluginName}" 有新版本可用`,
|
||||
updateUrl: noticeData.data.url,
|
||||
pluginType: noticeData.data.pluginInfo?.type,
|
||||
currentVersion: noticeData.currentVersion || '未知', // 这个需要从插件实例获取
|
||||
newVersion: noticeData.data.version,
|
||||
actions: [
|
||||
{ text: '稍后更新', type: 'cancel' },
|
||||
{ text: '立即更新', type: 'update', primary: true }
|
||||
]
|
||||
}
|
||||
|
||||
mainWindow.webContents.send('plugin-notice', updateNotice)
|
||||
} else {
|
||||
// 普通通知 - 显示信息对话框
|
||||
const infoNotice: DialogNotice = {
|
||||
...baseNoticeData,
|
||||
dialogType:
|
||||
noticeData.type === 'error'
|
||||
? 'error'
|
||||
: noticeData.type === 'warn'
|
||||
? 'warning'
|
||||
: noticeData.type === 'success'
|
||||
? 'success'
|
||||
: 'info',
|
||||
title: noticeData.data.title || getNoticeTitle(noticeData.type),
|
||||
message:
|
||||
noticeData.data.message ||
|
||||
noticeData.data.content ||
|
||||
getDefaultMessage(noticeData.type, noticeData.data, baseNoticeData.pluginName),
|
||||
actions: [{ text: '我知道了', type: 'confirm', primary: true }]
|
||||
}
|
||||
|
||||
mainWindow.webContents.send('plugin-notice', infoNotice)
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('[CeruMusic] 发送插件通知失败:', error.message)
|
||||
}
|
||||
}
|
||||
@@ -218,6 +218,7 @@ aiEvents(mainWindow)
|
||||
import './events/musicCache'
|
||||
import './events/songList'
|
||||
import './events/directorySettings'
|
||||
import './events/pluginNotice'
|
||||
import { registerAutoUpdateEvents, initAutoUpdateForWindow } from './events/autoUpdate'
|
||||
|
||||
// This method will be called when Electron has finished
|
||||
|
||||
@@ -82,9 +82,9 @@ export class MusicCacheService {
|
||||
return path.join(this.cacheDir, `${cacheKey}${ext}`)
|
||||
}
|
||||
|
||||
async getCachedMusicUrl(songId: string, originalUrlPromise: Promise<string>): Promise<string> {
|
||||
async getCachedMusicUrl(songId: string): Promise<string | null> {
|
||||
const cacheKey = this.generateCacheKey(songId)
|
||||
console.log('hash', cacheKey)
|
||||
console.log('检查缓存 hash:', cacheKey)
|
||||
|
||||
// 检查是否已缓存
|
||||
if (this.cacheIndex.has(cacheKey)) {
|
||||
@@ -97,14 +97,29 @@ export class MusicCacheService {
|
||||
return `file://${cachedFilePath}`
|
||||
} catch (error) {
|
||||
// 文件不存在,从缓存索引中移除
|
||||
console.warn(`缓存文件不存在,移除索引: ${cachedFilePath}`)
|
||||
this.cacheIndex.delete(cacheKey)
|
||||
await this.saveCacheIndex()
|
||||
}
|
||||
}
|
||||
|
||||
// 下载并缓存文件 先返回源链接不等待结果优化体验
|
||||
this.downloadAndCache(songId, await originalUrlPromise, cacheKey)
|
||||
return await originalUrlPromise
|
||||
return null
|
||||
}
|
||||
|
||||
async cacheMusic(songId: string, url: string): Promise<void> {
|
||||
const cacheKey = this.generateCacheKey(songId)
|
||||
|
||||
// 如果已经缓存,跳过
|
||||
if (this.cacheIndex.has(cacheKey)) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
await this.downloadAndCache(songId, url, cacheKey)
|
||||
} catch (error) {
|
||||
console.error(`缓存歌曲失败: ${songId}`, error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
private async downloadAndCache(songId: string, url: string, cacheKey: string): Promise<string> {
|
||||
|
||||
@@ -35,21 +35,23 @@ function main(source: string) {
|
||||
const usePlugin = pluginService.getPluginById(pluginId)
|
||||
if (!pluginId || !usePlugin) return { error: '请配置音源来播放歌曲' }
|
||||
|
||||
// 获取原始URL
|
||||
const originalUrlPromise = usePlugin.getMusicUrl(source, songInfo, quality)
|
||||
|
||||
// 生成歌曲唯一标识
|
||||
const songId = `${songInfo.name}-${songInfo.singer}-${source}-${quality}`
|
||||
|
||||
// 尝试获取缓存的URL
|
||||
try {
|
||||
const cachedUrl = await musicCacheService.getCachedMusicUrl(songId, originalUrlPromise)
|
||||
// 先检查缓存
|
||||
const cachedUrl = await musicCacheService.getCachedMusicUrl(songId)
|
||||
if (cachedUrl) {
|
||||
return cachedUrl
|
||||
} catch (cacheError) {
|
||||
console.warn('缓存获取失败,使用原始URL:', cacheError)
|
||||
const originalUrl = await originalUrlPromise
|
||||
return originalUrl
|
||||
}
|
||||
|
||||
// 没有缓存时才发起网络请求
|
||||
const originalUrl = await usePlugin.getMusicUrl(source, songInfo, quality)
|
||||
// 异步缓存,不阻塞返回
|
||||
musicCacheService.cacheMusic(songId, originalUrl).catch((error) => {
|
||||
console.warn('缓存歌曲失败:', error)
|
||||
})
|
||||
|
||||
return originalUrl
|
||||
} catch (e: any) {
|
||||
return {
|
||||
error: '获取歌曲失败 ' + e.error || e
|
||||
|
||||
@@ -13,8 +13,18 @@ import * as vm from 'vm'
|
||||
import fetch from 'node-fetch'
|
||||
import * as fs from 'fs'
|
||||
import { MusicItem } from '../../musicSdk/type'
|
||||
import { sendPluginNotice } from '../../../events/pluginNotice'
|
||||
|
||||
// 定义插件结构接口
|
||||
// ==================== 常量定义 ====================
|
||||
const CONSTANTS = {
|
||||
DEFAULT_TIMEOUT: 10000, // 10秒超时
|
||||
API_VERSION: '1.0.3',
|
||||
ENVIRONMENT: 'nodejs',
|
||||
NOTICE_DELAY: 100, // 通知延迟时间
|
||||
LOG_PREFIX: '[CeruMusic]'
|
||||
} as const
|
||||
|
||||
// ==================== 类型定义 ====================
|
||||
export interface PluginInfo {
|
||||
name: string
|
||||
version: string
|
||||
@@ -44,7 +54,7 @@ interface MusicInfo extends MusicItem {
|
||||
interface RequestResult {
|
||||
body: any
|
||||
statusCode: number
|
||||
headers: Record<string, string[]>
|
||||
headers: Record<string, string>
|
||||
}
|
||||
|
||||
interface CeruMusicApiUtils {
|
||||
@@ -63,12 +73,26 @@ interface CeruMusicApi {
|
||||
options?: RequestOptions | RequestCallback,
|
||||
callback?: RequestCallback
|
||||
) => Promise<RequestResult> | void
|
||||
NoticeCenter: (
|
||||
type: 'error' | 'info' | 'success' | 'warn' | 'update',
|
||||
data: {
|
||||
title: string
|
||||
content?: string
|
||||
url?: string
|
||||
version?: string
|
||||
pluginInfo: {
|
||||
name?: string // 插件名
|
||||
type: 'lx' | 'cr' //插件类型
|
||||
}
|
||||
}
|
||||
) => void
|
||||
}
|
||||
|
||||
type RequestOptions = {
|
||||
method?: string
|
||||
headers?: Record<string, string>
|
||||
body?: any
|
||||
timeout?: number
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
@@ -77,8 +101,21 @@ type RequestCallback = (error: Error | null, result: RequestResult | null) => vo
|
||||
type Logger = {
|
||||
log: (...args: any[]) => void
|
||||
error: (...args: any[]) => void
|
||||
warn?: (...args: any[]) => void
|
||||
info?: (...args: any[]) => void
|
||||
warn: (...args: any[]) => void
|
||||
info: (...args: any[]) => void
|
||||
}
|
||||
|
||||
type PluginMethodName = 'musicUrl' | 'getPic' | 'getLyric'
|
||||
|
||||
// ==================== 错误类定义 ====================
|
||||
class PluginError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
public readonly method?: string
|
||||
) {
|
||||
super(message)
|
||||
this.name = 'PluginError'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -96,160 +133,27 @@ class CeruMusicPluginHost {
|
||||
*/
|
||||
constructor(pluginCode: string | null = null, logger: Logger = console) {
|
||||
this.pluginCode = pluginCode
|
||||
this.plugin = null // 存储插件导出的对象
|
||||
this.plugin = null
|
||||
|
||||
if (pluginCode) {
|
||||
this._initialize(logger)
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 公共方法 ====================
|
||||
|
||||
/**
|
||||
* 从文件加载插件
|
||||
* @param pluginPath 插件文件路径
|
||||
* @param logger 日志记录器
|
||||
*/
|
||||
async loadPlugin(pluginPath: string, logger: Logger = console): Promise<CeruMusicPlugin> {
|
||||
this.pluginCode = fs.readFileSync(pluginPath, 'utf-8')
|
||||
this._initialize(logger)
|
||||
return this.plugin as CeruMusicPlugin
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化沙箱环境,加载并验证插件
|
||||
* @private
|
||||
*/
|
||||
_initialize(console: Logger): void {
|
||||
// 提供给插件的API
|
||||
const cerumusicApi: CeruMusicApi = {
|
||||
env: 'nodejs',
|
||||
version: '1.0.0',
|
||||
utils: {
|
||||
buffer: {
|
||||
from: (data: string | Buffer | ArrayBuffer, encoding?: BufferEncoding) => {
|
||||
if (typeof data === 'string') {
|
||||
return Buffer.from(data, encoding)
|
||||
} else if (data instanceof Buffer) {
|
||||
return data
|
||||
} else if (data instanceof ArrayBuffer) {
|
||||
return Buffer.from(new Uint8Array(data))
|
||||
} else {
|
||||
return Buffer.from(data as any)
|
||||
}
|
||||
},
|
||||
bufToString: (buffer: Buffer, encoding?: BufferEncoding) => buffer.toString(encoding)
|
||||
}
|
||||
},
|
||||
request: (url, options, callback) => {
|
||||
// 支持 Promise 和 callback 两种调用方式
|
||||
if (typeof options === 'function') {
|
||||
callback = options as RequestCallback
|
||||
options = { method: 'GET' }
|
||||
}
|
||||
|
||||
const makeRequest = async (): Promise<RequestResult> => {
|
||||
try {
|
||||
console.log(`[CeruMusic] 发起请求: ${url}`)
|
||||
|
||||
// 添加超时设置
|
||||
const controller = new AbortController()
|
||||
const timeoutId = setTimeout(() => controller.abort(), 10000) // 10秒超时
|
||||
|
||||
const requestOptions = {
|
||||
method: 'GET',
|
||||
...(options as RequestOptions),
|
||||
signal: controller.signal
|
||||
}
|
||||
|
||||
const response = await fetch(url, requestOptions)
|
||||
clearTimeout(timeoutId)
|
||||
|
||||
console.log(`[CeruMusic] 请求响应状态: ${response.status}`)
|
||||
|
||||
// 尝试解析JSON,如果失败则返回文本
|
||||
let body: any
|
||||
try {
|
||||
body = await response.json()
|
||||
} catch (parseError: any) {
|
||||
console.error(`[CeruMusic] 解析响应失败: ${parseError.message}`)
|
||||
// 解析失败时创建错误body
|
||||
body = {
|
||||
code: response.status,
|
||||
msg: `Failed to parse response: ${parseError.message}`
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[CeruMusic] 请求响应内容:`, body)
|
||||
|
||||
const result: RequestResult = {
|
||||
body,
|
||||
statusCode: response.status,
|
||||
headers: response.headers.raw()
|
||||
}
|
||||
|
||||
if (callback) {
|
||||
callback(null, result)
|
||||
}
|
||||
return result
|
||||
} catch (error: any) {
|
||||
console.error(`[CeruMusic] Request failed: ${error.message}`)
|
||||
|
||||
if (callback) {
|
||||
// 网络错误时,调用 callback(error, null)
|
||||
callback(error, null)
|
||||
// 需要返回一个值以满足 Promise<RequestResult> 类型
|
||||
return {
|
||||
body: { error: error.message },
|
||||
statusCode: 500,
|
||||
headers: {}
|
||||
}
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (callback) {
|
||||
makeRequest().catch((error) => {
|
||||
console.error(`[CeruMusic] Unhandled request error in callback mode: ${error.message}`)
|
||||
}) // 确保错误被正确处理
|
||||
return undefined
|
||||
} else {
|
||||
return makeRequest()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const sandbox = {
|
||||
module: { exports: {} },
|
||||
cerumusic: cerumusicApi,
|
||||
console: console,
|
||||
setTimeout: setTimeout,
|
||||
clearTimeout: clearTimeout,
|
||||
setInterval: setInterval,
|
||||
clearInterval: clearInterval,
|
||||
Buffer: Buffer,
|
||||
JSON: JSON,
|
||||
require: () => ({}),
|
||||
global: {},
|
||||
process: { env: {} }
|
||||
}
|
||||
|
||||
try {
|
||||
// 在沙箱中执行插件代码
|
||||
if (this.pluginCode) {
|
||||
vm.runInNewContext(this.pluginCode, sandbox)
|
||||
this.plugin = sandbox.module.exports as CeruMusicPlugin
|
||||
console.log(`[CeruMusic] Plugin "${this.plugin.pluginInfo.name}" loaded successfully.`)
|
||||
} else {
|
||||
throw new Error('No plugin code provided.')
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error('[CeruMusic] Error executing plugin code:', e)
|
||||
throw new Error('Failed to initialize plugin.')
|
||||
}
|
||||
|
||||
// 验证插件结构
|
||||
if (!this.plugin?.pluginInfo || !this.plugin.sources || !this.plugin.musicUrl) {
|
||||
throw new Error('Invalid plugin structure. Required fields: pluginInfo, sources, musicUrl.')
|
||||
this.pluginCode = fs.readFileSync(pluginPath, 'utf-8')
|
||||
this._initialize(logger)
|
||||
return this.plugin as CeruMusicPlugin
|
||||
} catch (error: any) {
|
||||
throw new PluginError(`无法加载插件 ${pluginPath}: ${error.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -257,10 +161,8 @@ class CeruMusicPluginHost {
|
||||
* 获取插件信息
|
||||
*/
|
||||
getPluginInfo(): PluginInfo {
|
||||
if (!this.plugin) {
|
||||
throw new Error('Plugin not initialized')
|
||||
}
|
||||
return this.plugin.pluginInfo
|
||||
this._ensurePluginInitialized()
|
||||
return this.plugin!.pluginInfo
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -274,10 +176,8 @@ class CeruMusicPluginHost {
|
||||
* 获取支持的音源和音质信息
|
||||
*/
|
||||
getSupportedSources(): PluginSource[] {
|
||||
if (!this.plugin) {
|
||||
throw new Error('Plugin not initialized')
|
||||
}
|
||||
return this.plugin.sources
|
||||
this._ensurePluginInitialized()
|
||||
return this.plugin!.sources
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -287,148 +187,7 @@ class CeruMusicPluginHost {
|
||||
* @param quality 音质
|
||||
*/
|
||||
async getMusicUrl(source: string, musicInfo: MusicInfo, quality: string): Promise<string> {
|
||||
try {
|
||||
if (!this.plugin || typeof this.plugin.musicUrl !== 'function') {
|
||||
throw new Error(`Action "musicUrl" is not implemented in plugin.`)
|
||||
}
|
||||
|
||||
console.log(`[CeruMusic] 开始调用插件的 musicUrl 方法...`)
|
||||
|
||||
// 将 cerumusic API 绑定到函数调用的 this 上下文
|
||||
const result = await this.plugin.musicUrl.call(
|
||||
{ cerumusic: this._getCerumusicApi() },
|
||||
source,
|
||||
musicInfo,
|
||||
quality
|
||||
)
|
||||
|
||||
console.log(`[CeruMusic] 插件 musicUrl 方法调用成功`)
|
||||
return result
|
||||
} catch (error: any) {
|
||||
console.error(`[CeruMusic] getMusicUrl 方法执行失败:`, error.message)
|
||||
console.error(`[CeruMusic] 错误堆栈:`, error.stack)
|
||||
|
||||
// 重新抛出错误,确保外部可以捕获
|
||||
throw new Error(`Plugin getMusicUrl failed: ${error.message}`)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 cerumusic API 对象
|
||||
* @private
|
||||
*/
|
||||
_getCerumusicApi(): CeruMusicApi {
|
||||
return {
|
||||
env: 'nodejs',
|
||||
version: '1.0.0',
|
||||
utils: {
|
||||
buffer: {
|
||||
from: (data: string | Buffer | ArrayBuffer, encoding?: BufferEncoding) => {
|
||||
if (typeof data === 'string') {
|
||||
return Buffer.from(data, encoding)
|
||||
} else if (data instanceof Buffer) {
|
||||
return data
|
||||
} else if (data instanceof ArrayBuffer) {
|
||||
return Buffer.from(new Uint8Array(data))
|
||||
} else {
|
||||
return Buffer.from(data as any)
|
||||
}
|
||||
},
|
||||
bufToString: (buffer: Buffer, encoding?: BufferEncoding) => buffer.toString(encoding)
|
||||
}
|
||||
},
|
||||
request: (url, options, callback) => {
|
||||
// 支持 Promise 和 callback 两种调用方式
|
||||
if (typeof options === 'function') {
|
||||
callback = options as RequestCallback
|
||||
options = { method: 'GET' }
|
||||
}
|
||||
|
||||
const makeRequest = async (): Promise<RequestResult> => {
|
||||
try {
|
||||
console.log(`[CeruMusic] 发起请求: ${url}`)
|
||||
|
||||
// 添加超时设置
|
||||
const controller = new AbortController()
|
||||
const timeoutId = setTimeout(() => controller.abort(), 10000) // 10秒超时
|
||||
|
||||
const requestOptions = {
|
||||
method: 'GET',
|
||||
...(options as RequestOptions),
|
||||
signal: controller.signal
|
||||
}
|
||||
|
||||
const response = await fetch(url, requestOptions)
|
||||
clearTimeout(timeoutId)
|
||||
|
||||
console.log(`[CeruMusic] 请求响应状态: ${response.status}`)
|
||||
|
||||
// 尝试解析JSON,如果失败则返回文本
|
||||
let body: any
|
||||
const contentType = response.headers.get('content-type')
|
||||
|
||||
try {
|
||||
if (contentType && contentType.includes('application/json')) {
|
||||
body = await response.json()
|
||||
} else {
|
||||
const text = await response.text()
|
||||
console.log(`[CeruMusic] 响应不是JSON格式,内容: ${text.substring(0, 200)}...`)
|
||||
// 对于非JSON响应,创建一个错误状态的body
|
||||
body = {
|
||||
code: response.status,
|
||||
msg: `Expected JSON response but got: ${contentType || 'unknown content type'}`,
|
||||
data: text
|
||||
}
|
||||
}
|
||||
} catch (parseError: any) {
|
||||
console.error(`[CeruMusic] 解析响应失败: ${parseError.message}`)
|
||||
// 解析失败时创建错误body
|
||||
body = {
|
||||
code: response.status,
|
||||
msg: `Failed to parse response: ${parseError.message}`
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[CeruMusic] 请求响应内容:`, body)
|
||||
|
||||
const result: RequestResult = {
|
||||
body,
|
||||
statusCode: response.status,
|
||||
headers: response.headers.raw()
|
||||
}
|
||||
|
||||
if (callback) {
|
||||
callback(null, result)
|
||||
}
|
||||
return result
|
||||
} catch (error: any) {
|
||||
console.error(`[CeruMusic] Request failed: ${error.message}`)
|
||||
|
||||
if (callback) {
|
||||
// 网络错误时,调用 callback(error, null)
|
||||
callback(error, null)
|
||||
// 需要返回一个值以满足 Promise<RequestResult> 类型
|
||||
return {
|
||||
body: { error: error.message },
|
||||
statusCode: 500,
|
||||
headers: {}
|
||||
}
|
||||
} else {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (callback) {
|
||||
makeRequest().catch((error) => {
|
||||
console.error(`[CeruMusic] Unhandled request error in callback mode: ${error.message}`)
|
||||
}) // 确保错误被正确处理
|
||||
return undefined
|
||||
} else {
|
||||
return makeRequest()
|
||||
}
|
||||
}
|
||||
}
|
||||
return this._callPluginMethod('musicUrl', source, musicInfo, quality)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -437,25 +196,7 @@ class CeruMusicPluginHost {
|
||||
* @param musicInfo 音乐信息
|
||||
*/
|
||||
async getPic(source: string, musicInfo: MusicInfo): Promise<string> {
|
||||
try {
|
||||
if (!this.plugin || typeof this.plugin.getPic !== 'function') {
|
||||
throw new Error(`Action "getPic" is not implemented in plugin.`)
|
||||
}
|
||||
|
||||
console.log(`[CeruMusic] 开始调用插件的 getPic 方法...`)
|
||||
|
||||
const result = await this.plugin.getPic.call(
|
||||
{ cerumusic: this._getCerumusicApi() },
|
||||
source,
|
||||
musicInfo
|
||||
)
|
||||
|
||||
console.log(`[CeruMusic] 插件 getPic 方法调用成功`)
|
||||
return result
|
||||
} catch (error: any) {
|
||||
console.error(`[CeruMusic] getPic 方法执行失败:`, error.message)
|
||||
throw new Error(`Plugin getPic failed: ${error.message}`)
|
||||
}
|
||||
return this._callPluginMethod('getPic', source, musicInfo)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -464,24 +205,364 @@ class CeruMusicPluginHost {
|
||||
* @param musicInfo 音乐信息
|
||||
*/
|
||||
async getLyric(source: string, musicInfo: MusicInfo): Promise<string> {
|
||||
return this._callPluginMethod('getLyric', source, musicInfo)
|
||||
}
|
||||
|
||||
// ==================== 私有方法 ====================
|
||||
|
||||
/**
|
||||
* 初始化沙箱环境,加载并验证插件
|
||||
* @private
|
||||
*/
|
||||
private _initialize(logger: Logger): void {
|
||||
if (!this.pluginCode) {
|
||||
throw new PluginError('No plugin code provided.')
|
||||
}
|
||||
|
||||
const sandbox = this._createSandbox(logger)
|
||||
|
||||
try {
|
||||
if (!this.plugin || typeof this.plugin.getLyric !== 'function') {
|
||||
throw new Error(`Action "getLyric" is not implemented in plugin.`)
|
||||
}
|
||||
vm.runInNewContext(this.pluginCode, sandbox)
|
||||
this.plugin = sandbox.module.exports as CeruMusicPlugin
|
||||
|
||||
console.log(`[CeruMusic] 开始调用插件的 getLyric 方法...`)
|
||||
this._validatePlugin()
|
||||
|
||||
const result = await this.plugin.getLyric.call(
|
||||
{ cerumusic: this._getCerumusicApi() },
|
||||
source,
|
||||
musicInfo
|
||||
logger.log(
|
||||
`${CONSTANTS.LOG_PREFIX} Plugin "${this.plugin.pluginInfo.name}" loaded successfully.`
|
||||
)
|
||||
} catch (error: any) {
|
||||
logger.error(`${CONSTANTS.LOG_PREFIX} Error executing plugin code:`, error)
|
||||
throw new PluginError('Failed to initialize plugin.')
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`[CeruMusic] 插件 getLyric 方法调用成功`)
|
||||
/**
|
||||
* 创建沙箱环境
|
||||
* @private
|
||||
*/
|
||||
private _createSandbox(logger: Logger): any {
|
||||
return {
|
||||
module: { exports: {} },
|
||||
cerumusic: this._getCerumusicApi(),
|
||||
console: logger,
|
||||
setTimeout,
|
||||
clearTimeout,
|
||||
setInterval,
|
||||
clearInterval,
|
||||
Buffer,
|
||||
JSON,
|
||||
require: () => ({}),
|
||||
global: {},
|
||||
process: { env: {} }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证插件结构
|
||||
* @private
|
||||
*/
|
||||
private _validatePlugin(): void {
|
||||
if (!this.plugin?.pluginInfo || !this.plugin.sources || !this.plugin.musicUrl) {
|
||||
throw new PluginError(
|
||||
'Invalid plugin structure. Required fields: pluginInfo, sources, musicUrl.'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 确保插件已初始化
|
||||
* @private
|
||||
*/
|
||||
private _ensurePluginInitialized(): void {
|
||||
if (!this.plugin) {
|
||||
throw new PluginError('Plugin not initialized')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一的插件方法调用逻辑
|
||||
* @private
|
||||
*/
|
||||
private async _callPluginMethod(
|
||||
methodName: PluginMethodName,
|
||||
...args: readonly any[]
|
||||
): Promise<string> {
|
||||
this._ensurePluginInitialized()
|
||||
const method = this.plugin![methodName] as any
|
||||
if (typeof method !== 'function') {
|
||||
throw new PluginError(`Action "${methodName}" is not implemented in plugin.`, methodName)
|
||||
}
|
||||
try {
|
||||
console.log(`${CONSTANTS.LOG_PREFIX} 开始调用插件的 ${methodName} 方法...`)
|
||||
|
||||
const result = await method.call(...[{ cerumusic: this._getCerumusicApi() }], ...args)
|
||||
|
||||
console.log(`${CONSTANTS.LOG_PREFIX} 插件 ${methodName} 方法调用成功`)
|
||||
return result
|
||||
} catch (error: any) {
|
||||
console.error(`[CeruMusic] getLyric 方法执行失败:`, error.message)
|
||||
throw new Error(`Plugin getLyric failed: ${error.message}`)
|
||||
console.error(`${CONSTANTS.LOG_PREFIX} ${methodName} 方法执行失败:`, error.message)
|
||||
if (methodName === 'musicUrl') {
|
||||
console.error(`${CONSTANTS.LOG_PREFIX} 错误堆栈:`, error.stack)
|
||||
}
|
||||
throw new PluginError(`Plugin ${methodName} failed: ${error.message}`, methodName)
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 工具方法 ====================
|
||||
|
||||
// /**
|
||||
// * 验证 URL 是否有效
|
||||
// * @private
|
||||
// */
|
||||
// private _isValidUrl(url: string): boolean {
|
||||
// try {
|
||||
// const urlObj = new URL(url)
|
||||
// return urlObj.protocol === 'http:' || urlObj.protocol === 'https:'
|
||||
// } catch {
|
||||
// return false
|
||||
// }
|
||||
// }
|
||||
|
||||
// /**
|
||||
// * 根据通知类型获取标题
|
||||
// * @private
|
||||
// */
|
||||
// private _getNoticeTitle(type: string): string {
|
||||
// const titleMap: Record<string, string> = {
|
||||
// update: '插件更新',
|
||||
// error: '插件错误',
|
||||
// warning: '插件警告',
|
||||
// info: '插件信息',
|
||||
// success: '操作成功'
|
||||
// }
|
||||
// return titleMap[type] || '插件通知'
|
||||
// }
|
||||
|
||||
// /**
|
||||
// * 根据通知类型获取默认消息
|
||||
// * @private
|
||||
// */
|
||||
// private _getDefaultMessage(type: string, data: any): string {
|
||||
// const pluginName = this.plugin?.pluginInfo?.name || '未知插件'
|
||||
|
||||
// switch (type) {
|
||||
// case 'error':
|
||||
// return `插件 "${pluginName}" 发生错误: ${data?.error || '未知错误'}`
|
||||
// case 'warning':
|
||||
// return `插件 "${pluginName}" 警告: ${data?.warning || '需要注意'}`
|
||||
// case 'success':
|
||||
// return `插件 "${pluginName}" 操作成功`
|
||||
// case 'info':
|
||||
// default:
|
||||
// return `插件 "${pluginName}" 信息: ${JSON.stringify(data)}`
|
||||
// }
|
||||
// }
|
||||
|
||||
/**
|
||||
* 解析响应体
|
||||
* @private
|
||||
*/
|
||||
private async _parseResponseBody(response: any): Promise<any> {
|
||||
const contentType = response.headers.get('content-type') || ''
|
||||
|
||||
try {
|
||||
if (contentType.includes('application/json')) {
|
||||
return await response.json()
|
||||
} else if (contentType.includes('text/')) {
|
||||
return await response.text()
|
||||
} else {
|
||||
// 对于其他类型,尝试解析为 JSON,失败则返回文本
|
||||
const text = await response.text()
|
||||
try {
|
||||
return JSON.parse(text)
|
||||
} catch {
|
||||
return text
|
||||
}
|
||||
}
|
||||
} catch (parseError: any) {
|
||||
console.error(`${CONSTANTS.LOG_PREFIX} 解析响应失败: ${parseError.message}`)
|
||||
return {
|
||||
error: 'Parse failed',
|
||||
message: parseError.message,
|
||||
statusCode: response.status
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建错误结果
|
||||
* @private
|
||||
*/
|
||||
private _createErrorResult(error: any, url: string): RequestResult {
|
||||
const isTimeout = error.name === 'AbortError'
|
||||
return {
|
||||
body: {
|
||||
error: error.name || 'RequestError',
|
||||
message: error.message,
|
||||
url
|
||||
},
|
||||
statusCode: isTimeout ? 408 : 500,
|
||||
headers: {}
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== API 构建方法 ====================
|
||||
|
||||
/**
|
||||
* 获取 cerumusic API 对象
|
||||
* @private
|
||||
*/
|
||||
private _getCerumusicApi(): CeruMusicApi {
|
||||
return {
|
||||
env: CONSTANTS.ENVIRONMENT,
|
||||
version: CONSTANTS.API_VERSION,
|
||||
utils: this._createApiUtils(),
|
||||
request: this._createRequestFunction(),
|
||||
NoticeCenter: this._createNoticeCenter()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建 API 工具对象
|
||||
* @private
|
||||
*/
|
||||
private _createApiUtils(): CeruMusicApiUtils {
|
||||
return {
|
||||
buffer: {
|
||||
from: (data: string | Buffer | ArrayBuffer, encoding?: BufferEncoding) => {
|
||||
if (typeof data === 'string') {
|
||||
return Buffer.from(data, encoding)
|
||||
} else if (data instanceof Buffer) {
|
||||
return data
|
||||
} else if (data instanceof ArrayBuffer) {
|
||||
return Buffer.from(new Uint8Array(data))
|
||||
} else {
|
||||
return Buffer.from(data as any)
|
||||
}
|
||||
},
|
||||
bufToString: (buffer: Buffer, encoding?: BufferEncoding) => buffer.toString(encoding)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建请求函数
|
||||
* @private
|
||||
*/
|
||||
private _createRequestFunction() {
|
||||
return (
|
||||
url: string,
|
||||
options?: RequestOptions | RequestCallback,
|
||||
callback?: RequestCallback
|
||||
) => {
|
||||
// 支持 Promise 和 callback 两种调用方式
|
||||
if (typeof options === 'function') {
|
||||
callback = options as RequestCallback
|
||||
options = { method: 'GET' }
|
||||
}
|
||||
|
||||
const requestOptions = options as RequestOptions
|
||||
const makeRequest = () => this._makeHttpRequest(url, requestOptions)
|
||||
|
||||
// 执行请求
|
||||
if (callback) {
|
||||
makeRequest()
|
||||
.then((result) => callback(null, result))
|
||||
.catch((error) => {
|
||||
const errorResult = this._createErrorResult(error, url)
|
||||
callback(error, errorResult)
|
||||
})
|
||||
return undefined
|
||||
} else {
|
||||
return makeRequest()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行 HTTP 请求
|
||||
* @private
|
||||
*/
|
||||
private async _makeHttpRequest(url: string, options: RequestOptions): Promise<RequestResult> {
|
||||
const controller = new AbortController()
|
||||
const timeout = options.timeout || CONSTANTS.DEFAULT_TIMEOUT
|
||||
|
||||
const timeoutId = setTimeout(() => {
|
||||
controller.abort()
|
||||
console.warn(`${CONSTANTS.LOG_PREFIX} 请求超时: ${url}`)
|
||||
}, timeout)
|
||||
|
||||
try {
|
||||
console.log(`${CONSTANTS.LOG_PREFIX} 发起请求: ${options.method || 'GET'} ${url}`)
|
||||
|
||||
const fetchOptions = {
|
||||
method: 'GET',
|
||||
...options,
|
||||
signal: controller.signal
|
||||
}
|
||||
|
||||
const response = await fetch(url, fetchOptions)
|
||||
clearTimeout(timeoutId)
|
||||
|
||||
console.log(`${CONSTANTS.LOG_PREFIX} 请求响应: ${response.status} ${response.statusText}`)
|
||||
|
||||
const body = await this._parseResponseBody(response)
|
||||
const headers = this._extractHeaders(response)
|
||||
|
||||
const result: RequestResult = {
|
||||
body,
|
||||
statusCode: response.status,
|
||||
headers
|
||||
}
|
||||
|
||||
console.log(`${CONSTANTS.LOG_PREFIX} 请求完成:`, {
|
||||
url,
|
||||
status: response.status,
|
||||
bodyType: typeof body
|
||||
})
|
||||
|
||||
return result
|
||||
} catch (error: any) {
|
||||
clearTimeout(timeoutId)
|
||||
|
||||
const errorMessage =
|
||||
error.name === 'AbortError' ? `请求超时: ${url}` : `请求失败: ${error.message}`
|
||||
|
||||
console.error(`${CONSTANTS.LOG_PREFIX} ${errorMessage}`)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 提取响应头
|
||||
* @private
|
||||
*/
|
||||
private _extractHeaders(response: any): Record<string, string> {
|
||||
const headers: Record<string, string> = {}
|
||||
response.headers.forEach((value: string, key: string) => {
|
||||
headers[key] = value
|
||||
})
|
||||
return headers
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建通知中心
|
||||
* @private
|
||||
*/
|
||||
private _createNoticeCenter() {
|
||||
return (type: string, data: any) => {
|
||||
const sendNotice = () => {
|
||||
if (this.plugin?.pluginInfo) {
|
||||
sendPluginNotice(
|
||||
{ type: type as any, data, currentVersion: this.plugin.pluginInfo.version },
|
||||
this.plugin.pluginInfo.name
|
||||
)
|
||||
} else {
|
||||
// 如果插件还未初始化,延迟执行
|
||||
setTimeout(sendNotice, CONSTANTS.NOTICE_DELAY)
|
||||
}
|
||||
}
|
||||
sendNotice()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,7 +68,6 @@ function extractDefaultSources() {
|
||||
};
|
||||
});
|
||||
|
||||
console.log('提取的音源配置:', extractedSources);
|
||||
return extractedSources;
|
||||
} catch (e) {
|
||||
console.log('解析 MUSIC_QUALITY 失败:', e.message);
|
||||
@@ -94,6 +93,70 @@ sources = extractDefaultSources();
|
||||
let isInitialized = false;
|
||||
let pluginSources = {};
|
||||
let requestHandler = null;
|
||||
let updateAlertSent = false; // 防止重复发送更新提示
|
||||
|
||||
// 处理更新提示事件
|
||||
function handleUpdateAlert(data, cerumusicApi) {
|
||||
// 每次运行脚本只能调用一次
|
||||
if (updateAlertSent) {
|
||||
console.warn(\`[${pluginName}] updateAlert 事件每次运行脚本只能调用一次,忽略重复调用\`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!data || !data.log) {
|
||||
console.error(\`[${pluginName}] updateAlert 事件缺少必需的 log 参数\`);
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证和处理参数
|
||||
let log = String(data.log);
|
||||
let updateUrl = data.updateUrl ? String(data.updateUrl) : undefined;
|
||||
|
||||
// 限制 log 长度为 1024 字符
|
||||
if (log.length > 1024) {
|
||||
log = log.substring(0, 1024);
|
||||
console.warn(\`[${pluginName}] 更新日志超过 1024 字符,已截断\`);
|
||||
}
|
||||
|
||||
// 验证 updateUrl 格式
|
||||
if (updateUrl) {
|
||||
if (updateUrl.length > 1024) {
|
||||
updateUrl = updateUrl.substring(0, 1024);
|
||||
console.warn(\`[${pluginName}] 更新地址超过 1024 字符,已截断\`);
|
||||
}
|
||||
|
||||
if (!updateUrl.startsWith('http://') && !updateUrl.startsWith('https://')) {
|
||||
console.error(\`[${pluginName}] updateUrl 必须是 HTTP 协议的 URL 地址\`);
|
||||
updateUrl = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// 标记已发送
|
||||
updateAlertSent = true;
|
||||
|
||||
// 通过 CeruMusic 的通知系统发送更新提示
|
||||
try {
|
||||
// 使用传入的 cerumusic API 对象发送通知
|
||||
if (cerumusicApi && cerumusicApi.NoticeCenter) {
|
||||
cerumusicApi.NoticeCenter('update', {
|
||||
title: \`${pluginName} 有新版本可用\`,
|
||||
content: log,
|
||||
url: updateUrl,
|
||||
pluginInfo: {
|
||||
name: '${pluginName}',
|
||||
type: 'lx',
|
||||
forcedUpdate: false
|
||||
}
|
||||
});
|
||||
|
||||
console.log(\`[${pluginName}] 更新提示已发送\`, { log: log.substring(0, 100) + '...', updateUrl });
|
||||
} else {
|
||||
console.error(\`[${pluginName}] CeruMusic API 不可用,无法发送更新提示\`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(\`[${pluginName}] 发送更新提示失败:\`, error.message);
|
||||
}
|
||||
}
|
||||
initializePlugin()
|
||||
function initializePlugin() {
|
||||
if (isInitialized) return;
|
||||
@@ -133,9 +196,9 @@ function initializePlugin() {
|
||||
qualitys: sourceInfo.qualitys || originalQualitys || ['128k', '320k']
|
||||
};
|
||||
});
|
||||
|
||||
console.log('[${pluginName + ' by Ceru插件' || 'ceru插件'}] 音源注册完成:', Object.keys(pluginSources));
|
||||
console.log('[${pluginName + ' by Ceru插件' || 'ceru插件'}] 动态音源信息已更新:', sources);
|
||||
} else if (event === 'updateAlert' && data) {
|
||||
// 处理更新提示事件,传入 cerumusic API
|
||||
handleUpdateAlert(data, cerumusic);
|
||||
}
|
||||
},
|
||||
request: request,
|
||||
|
||||
5
src/preload/index.d.ts
vendored
5
src/preload/index.d.ts
vendored
@@ -119,10 +119,15 @@ interface CustomAPI {
|
||||
size: number
|
||||
formatted: string
|
||||
}>
|
||||
|
||||
}
|
||||
|
||||
// 用户配置API
|
||||
getUserConfig: () => Promise<any>
|
||||
|
||||
pluginNotice: {
|
||||
onPluginNotice: (listener: (...args: any[]) => void) => ()=>void
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -178,6 +178,17 @@ const api = {
|
||||
ipcRenderer.invoke('directory-settings:open-directory', dirPath),
|
||||
getDirectorySize: (dirPath: string) =>
|
||||
ipcRenderer.invoke('directory-settings:get-directory-size', dirPath)
|
||||
},
|
||||
|
||||
// 插件通知相关
|
||||
pluginNotice: {
|
||||
onPluginNotice(callback: (data: string) => any) {
|
||||
function listener(_: any, data: any) {
|
||||
callback(data)
|
||||
}
|
||||
ipcRenderer.on('plugin-notice', listener)
|
||||
return () => ipcRenderer.removeListener('plugin-notice', listener)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
3
src/renderer/components.d.ts
vendored
3
src/renderer/components.d.ts
vendored
@@ -14,14 +14,15 @@ declare module 'vue' {
|
||||
FloatBall: typeof import('./src/components/AI/FloatBall.vue')['default']
|
||||
FullPlay: typeof import('./src/components/Play/FullPlay.vue')['default']
|
||||
GlobalAudio: typeof import('./src/components/Play/GlobalAudio.vue')['default']
|
||||
HomeLayout: typeof import('./src/components/layout/HomeLayout.vue')['default']
|
||||
MusicCache: typeof import('./src/components/Settings/MusicCache.vue')['default']
|
||||
PlaylistActions: typeof import('./src/components/Play/PlaylistActions.vue')['default']
|
||||
PlaylistDrawer: typeof import('./src/components/Play/PlaylistDrawer.vue')['default']
|
||||
PlaylistSettings: typeof import('./src/components/Settings/PlaylistSettings.vue')['default']
|
||||
PlayMusic: typeof import('./src/components/Play/PlayMusic.vue')['default']
|
||||
PluginNoticeDialog: typeof import('./src/components/PluginNoticeDialog.vue')['default']
|
||||
RouterLink: typeof import('vue-router')['RouterLink']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
SearchComponent: typeof import('./src/components/Search/SearchComponent.vue')['default']
|
||||
ShaderBackground: typeof import('./src/components/Play/ShaderBackground.vue')['default']
|
||||
SongVirtualList: typeof import('./src/components/Music/SongVirtualList.vue')['default']
|
||||
TAlert: typeof import('tdesign-vue-next')['Alert']
|
||||
|
||||
@@ -11,8 +11,6 @@
|
||||
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from 'vue'
|
||||
import GlobalAudio from './components/Play/GlobalAudio.vue'
|
||||
import FloatBall from './components/AI/FloatBall.vue'
|
||||
import { LocalUserDetailStore } from '@renderer/store/LocalUserDetail'
|
||||
import { useAutoUpdate } from './composables/useAutoUpdate'
|
||||
|
||||
@@ -79,6 +77,7 @@ const applyTheme = (themeName) => {
|
||||
</router-view>
|
||||
<GlobalAudio />
|
||||
<FloatBall />
|
||||
<PluginNoticeDialog />
|
||||
<UpdateProgress />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -180,20 +180,12 @@ const playSong = async (song: SongList) => {
|
||||
|
||||
let urlToPlay = ''
|
||||
|
||||
// 如果没有URL,需要获取URL
|
||||
if (!urlToPlay) {
|
||||
// eslint-disable-next-line no-useless-catch
|
||||
try {
|
||||
urlToPlay = await getSongRealUrl(toRaw(song))
|
||||
|
||||
// 同时更新播放列表中对应歌曲的URL
|
||||
const playlistIndex = list.value.findIndex((item) => item.songmid === song.songmid)
|
||||
if (playlistIndex !== -1) {
|
||||
;(list.value[playlistIndex] as any).url = urlToPlay
|
||||
}
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
// 获取URL
|
||||
// eslint-disable-next-line no-useless-catch
|
||||
try {
|
||||
urlToPlay = await getSongRealUrl(toRaw(song))
|
||||
} catch (error) {
|
||||
throw error
|
||||
}
|
||||
|
||||
// 先停止当前播放
|
||||
|
||||
324
src/renderer/src/components/PluginNoticeDialog.vue
Normal file
324
src/renderer/src/components/PluginNoticeDialog.vue
Normal file
@@ -0,0 +1,324 @@
|
||||
<template>
|
||||
<t-dialog
|
||||
v-model:visible="visible"
|
||||
:header="dialogTitle"
|
||||
:width="dialogWidth"
|
||||
:close-btn="true"
|
||||
:close-on-overlay-click="false"
|
||||
:destroy-on-close="true"
|
||||
placement="center"
|
||||
@close="handleClose"
|
||||
>
|
||||
<template #body>
|
||||
<div class="plugin-notice-content">
|
||||
<!-- 通知消息 -->
|
||||
<div class="notice-message">
|
||||
<p class="message-text">{{ notice?.message }}</p>
|
||||
|
||||
<!-- 更新通知的额外信息 -->
|
||||
<div v-if="notice?.dialogType === 'update'" class="update-info">
|
||||
<div class="version-info">
|
||||
<span class="version-label">当前版本:</span>
|
||||
<span class="version-value">{{ notice?.currentVersion || 'Unknown' }}</span>
|
||||
</div>
|
||||
<div class="version-info">
|
||||
<span class="version-label">新版本:</span>
|
||||
<span class="version-value new-version">{{ notice?.newVersion || 'Unknown' }}</span>
|
||||
</div>
|
||||
<div v-if="notice?.pluginType" class="plugin-type">
|
||||
<span class="type-label">插件类型:</span>
|
||||
<t-tag :theme="notice.pluginType === 'cr' ? 'primary' : 'success'" size="small">
|
||||
{{ notice.pluginType === 'cr' ? 'CeruMusic' : 'LX Music' }}
|
||||
</t-tag>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<div class="dialog-actions">
|
||||
<t-button
|
||||
v-for="action in notice?.actions || []"
|
||||
:key="action.type"
|
||||
:theme="action.primary ? 'primary' : 'default'"
|
||||
:loading="actionLoading === action.type"
|
||||
@click="handleAction(action.type)"
|
||||
>
|
||||
{{ action.text }}
|
||||
</t-button>
|
||||
</div>
|
||||
</template>
|
||||
</t-dialog>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { MessagePlugin } from 'tdesign-vue-next'
|
||||
|
||||
interface DialogNotice {
|
||||
type: string
|
||||
data: any
|
||||
timestamp: number
|
||||
pluginName: string
|
||||
dialogType: 'update' | 'info' | 'error' | 'warning' | 'success'
|
||||
title: string
|
||||
message: string
|
||||
updateUrl?: string
|
||||
pluginType?: 'lx' | 'cr'
|
||||
currentVersion?: string
|
||||
newVersion?: string
|
||||
actions: Array<{
|
||||
text: string
|
||||
type: 'cancel' | 'update' | 'confirm'
|
||||
primary?: boolean
|
||||
}>
|
||||
}
|
||||
|
||||
// 响应式数据
|
||||
const visible = ref(false)
|
||||
const notice = ref<DialogNotice | null>(null)
|
||||
const actionLoading = ref<string | null>(null)
|
||||
const noticeQueue = ref<DialogNotice[]>([]) // 通知队列
|
||||
|
||||
// 计算属性
|
||||
const dialogWidth = computed(() => {
|
||||
return notice.value?.dialogType === 'update' ? '500px' : '400px'
|
||||
})
|
||||
|
||||
// 对话框标题(包含队列信息)
|
||||
const dialogTitle = computed(() => {
|
||||
const baseTitle = notice.value?.title || '插件通知'
|
||||
const queueLength = noticeQueue.value.length
|
||||
|
||||
if (queueLength > 0) {
|
||||
return `${baseTitle} (还有 ${queueLength} 个通知)`
|
||||
}
|
||||
|
||||
return baseTitle
|
||||
})
|
||||
|
||||
// 显示通知对话框
|
||||
const showNotice = (noticeData: DialogNotice) => {
|
||||
// 添加到队列
|
||||
noticeQueue.value.push(noticeData)
|
||||
console.log('[PluginNotice] 添加通知到队列:', noticeData, '队列长度:', noticeQueue.value.length)
|
||||
|
||||
// 如果当前没有显示对话框,立即显示
|
||||
if (!visible.value) {
|
||||
showNextNotice()
|
||||
}
|
||||
}
|
||||
|
||||
// 显示队列中的下一个通知
|
||||
const showNextNotice = () => {
|
||||
if (noticeQueue.value.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const nextNotice = noticeQueue.value.shift()
|
||||
if (nextNotice) {
|
||||
notice.value = nextNotice
|
||||
visible.value = true
|
||||
console.log(
|
||||
'[PluginNotice] 显示下一个通知:',
|
||||
nextNotice,
|
||||
'剩余队列长度:',
|
||||
noticeQueue.value.length
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 处理操作按钮点击
|
||||
const handleAction = async (actionType: string) => {
|
||||
if (!notice.value) return
|
||||
|
||||
actionLoading.value = actionType
|
||||
|
||||
try {
|
||||
console.log('[PluginNotice] 处理操作:', actionType, notice.value)
|
||||
|
||||
if (actionType === 'update' && notice.value.updateUrl) {
|
||||
window.open(notice.value.updateUrl)
|
||||
handleClose()
|
||||
} else if (actionType === 'cancel') {
|
||||
// 取消操作直接关闭
|
||||
handleClose()
|
||||
} else {
|
||||
// 其他操作直接关闭对话框
|
||||
handleClose()
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('[PluginNotice] 处理操作失败:', error)
|
||||
MessagePlugin.error(`操作失败: ${error.message}`)
|
||||
} finally {
|
||||
actionLoading.value = null
|
||||
}
|
||||
}
|
||||
|
||||
// 处理对话框关闭
|
||||
const handleClose = () => {
|
||||
visible.value = false
|
||||
notice.value = null
|
||||
actionLoading.value = null
|
||||
|
||||
// 延迟一点时间后显示下一个通知,避免对话框切换过快
|
||||
setTimeout(() => {
|
||||
showNextNotice()
|
||||
}, 300)
|
||||
}
|
||||
|
||||
// 监听插件通知事件
|
||||
const handlePluginNotice = (noticeData: DialogNotice) => {
|
||||
showNotice(noticeData)
|
||||
}
|
||||
let event: () => void
|
||||
// 生命周期
|
||||
onMounted(() => {
|
||||
// 监听来自主进程的插件通知
|
||||
event = window.api.pluginNotice.onPluginNotice(handlePluginNotice)
|
||||
})
|
||||
onUnmounted(() => {
|
||||
event()
|
||||
// 清空队列
|
||||
noticeQueue.value = []
|
||||
})
|
||||
// 暴露方法给父组件
|
||||
defineExpose({
|
||||
showNotice,
|
||||
getQueueLength: () => noticeQueue.value.length,
|
||||
clearQueue: () => {
|
||||
noticeQueue.value = []
|
||||
console.log('[PluginNotice] 清空通知队列')
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.plugin-notice-content {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
padding: 16px 0;
|
||||
|
||||
.notice-icon {
|
||||
flex-shrink: 0;
|
||||
|
||||
.icon-update {
|
||||
color: #0052d9;
|
||||
}
|
||||
|
||||
.icon-error {
|
||||
color: #e34d59;
|
||||
}
|
||||
|
||||
.icon-warning {
|
||||
color: #ed7b2f;
|
||||
}
|
||||
|
||||
.icon-success {
|
||||
color: #00a870;
|
||||
}
|
||||
|
||||
.icon-info {
|
||||
color: #0052d9;
|
||||
}
|
||||
}
|
||||
|
||||
.notice-message {
|
||||
flex: 1;
|
||||
|
||||
.message-text {
|
||||
margin: 0 0 16px 0;
|
||||
font-size: 14px;
|
||||
line-height: 1.5;
|
||||
color: var(--td-text-color-primary);
|
||||
}
|
||||
|
||||
.update-info {
|
||||
background: var(--td-bg-color-container);
|
||||
border-radius: 6px;
|
||||
padding: 20px;
|
||||
margin: 0 10px;
|
||||
border: 1px solid var(--td-border-level-1-color);
|
||||
|
||||
.version-info {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.version-label {
|
||||
font-size: 12px;
|
||||
color: var(--td-text-color-secondary);
|
||||
}
|
||||
|
||||
.version-value {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--td-text-color-primary);
|
||||
|
||||
&.new-version {
|
||||
color: var(--td-brand-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.plugin-type {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-top: 8px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid var(--td-border-level-2-color);
|
||||
|
||||
.type-label {
|
||||
font-size: 12px;
|
||||
color: var(--td-text-color-secondary);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
// 响应式设计
|
||||
@media (max-width: 768px) {
|
||||
.plugin-notice-content {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
|
||||
.notice-icon {
|
||||
align-self: center;
|
||||
}
|
||||
}
|
||||
|
||||
.dialog-actions {
|
||||
flex-direction: column-reverse;
|
||||
|
||||
:deep(.t-button) {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 深色主题适配
|
||||
:deep(.t-dialog) {
|
||||
.t-dialog__header {
|
||||
border-bottom: 1px solid var(--td-border-level-1-color);
|
||||
}
|
||||
|
||||
.t-dialog__footer {
|
||||
border-top: 1px solid var(--td-border-level-1-color);
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,86 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const router = useRouter()
|
||||
const keyword = ref('')
|
||||
const isSearching = ref(false)
|
||||
|
||||
// 搜索类型:1: 单曲, 10: 专辑, 100: 歌手, 1000: 歌单
|
||||
|
||||
// 处理搜索事件
|
||||
const handleSearch = async () => {
|
||||
if (!keyword.value.trim()) return
|
||||
|
||||
isSearching.value = true
|
||||
try {
|
||||
// 调用搜索API
|
||||
|
||||
// 跳转到搜索结果页面,并传递搜索结果和关键词
|
||||
router.push({
|
||||
path: '/home/search',
|
||||
query: { keyword: keyword.value }
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('搜索失败:', error)
|
||||
} finally {
|
||||
isSearching.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 处理按键事件,按下回车键时触发搜索
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Enter') {
|
||||
handleSearch()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="search-component">
|
||||
<t-input
|
||||
v-model="keyword"
|
||||
placeholder="搜索音乐、歌手"
|
||||
:loading="isSearching"
|
||||
@keydown="handleKeyDown"
|
||||
>
|
||||
<template #suffix>
|
||||
<t-button
|
||||
theme="primary"
|
||||
variant="text"
|
||||
shape="square"
|
||||
:disabled="isSearching"
|
||||
@click="handleSearch"
|
||||
>
|
||||
<i class="iconfont icon-sousuo"></i>
|
||||
</t-button>
|
||||
</template>
|
||||
</t-input>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.search-component {
|
||||
width: 100%;
|
||||
|
||||
:deep(.t-input) {
|
||||
border-radius: 0rem !important;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
:deep(.t-input__suffix) {
|
||||
padding-right: 0.5rem;
|
||||
}
|
||||
|
||||
.iconfont {
|
||||
font-size: 1rem;
|
||||
color: #6b7280;
|
||||
transition: color 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
color: #f97316;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
7
src/renderer/src/env.d.ts
vendored
7
src/renderer/src/env.d.ts
vendored
@@ -1 +1,8 @@
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
declare module '*.vue' {
|
||||
import type { DefineComponent } from 'vue'
|
||||
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
|
||||
const component: DefineComponent<{}, {}, any>
|
||||
export default component
|
||||
}
|
||||
|
||||
@@ -74,15 +74,11 @@ export async function addToPlaylistAndPlay(
|
||||
playSongCallback: (song: SongList) => Promise<void>
|
||||
) {
|
||||
try {
|
||||
// 获取真实播放URL
|
||||
|
||||
await getSongRealUrl(song)
|
||||
const playResult = playSongCallback(song)
|
||||
|
||||
// 使用store的方法添加歌曲到第一位
|
||||
localUserStore.addSongToFirst(song)
|
||||
|
||||
// 播放歌曲 - 确保正确处理Promise
|
||||
const playResult = playSongCallback(song)
|
||||
if (playResult && typeof playResult.then === 'function') {
|
||||
await playResult
|
||||
}
|
||||
@@ -152,9 +148,7 @@ export async function replacePlaylist(
|
||||
|
||||
// 播放第一首歌曲
|
||||
if (songs[0]) {
|
||||
await getSongRealUrl(songs[0])
|
||||
const playResult = playSongCallback(songs[0])
|
||||
|
||||
if (playResult && typeof playResult.then === 'function') {
|
||||
await playResult
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import PlayMusic from '@renderer/components/Play/PlayMusic.vue'
|
||||
import HomeLayout from '@renderer//layout/index.vue'
|
||||
// import HomeLayout from '@renderer/layout/index.vue'
|
||||
// Trigger auto-import regeneration
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
@@ -815,7 +815,7 @@ onMounted(() => {
|
||||
v-if="playlists.length > 0"
|
||||
:options="playlists.map((p) => ({ content: p.name, value: p.id }))"
|
||||
@click="
|
||||
(playlistId) => addToPlaylist(song, playlists.find((p) => p.id === playlistId)!)
|
||||
(option) => addToPlaylist(song, playlists.find((p) => p.id === option.value)!)
|
||||
"
|
||||
>
|
||||
<t-button
|
||||
|
||||
@@ -829,8 +829,6 @@ onMounted(async () => {
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-size: 12px;
|
||||
user-select: text;
|
||||
|
||||
.console-prompt {
|
||||
color: var(--td-brand-color-5);
|
||||
font-weight: bold;
|
||||
@@ -953,6 +951,7 @@ onMounted(async () => {
|
||||
flex: 1;
|
||||
word-break: break-all;
|
||||
white-space: pre-wrap;
|
||||
user-select: text !important;
|
||||
}
|
||||
|
||||
/* 不同日志级别的颜色 */
|
||||
@@ -1025,7 +1024,6 @@ onMounted(async () => {
|
||||
}
|
||||
|
||||
.console-container {
|
||||
|
||||
height: 50vh;
|
||||
max-height: 400px;
|
||||
min-height: 250px;
|
||||
|
||||
@@ -56,7 +56,6 @@ onMounted(async () => {
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
.welcome-container {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
"src/renderer/src/env.d.ts",
|
||||
"src/renderer/src/**/*",
|
||||
"src/renderer/src/**/*.vue",
|
||||
"src/renderer/auto-imports.d.ts",
|
||||
"src/renderer/components.d.ts",
|
||||
"src/preload/*.d.ts",
|
||||
"src/types/**/*",
|
||||
"src/common/**/*"
|
||||
|
||||
Reference in New Issue
Block a user