forked from miao-moe/QZMusic_PC
feat: 实现功能&播放器内核&实现页面
- AMLL MeshGradient背景 - 全屏播放页初始化 - 纯C音频播放器 - FFmpeg解码 - 编译FFmpeg静态库 - wasapi shared - IPC通信 - FFTW实时频谱计算 - 低频响度实时计算 - PCM缓存 - 数据缓存&解码缓存 - 弃用mpv,改用qzplayer
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
<template>
|
||||
<MainLayout />
|
||||
<FullScreenPlayer />
|
||||
<Settings v-if="showSettings" @close="showSettings = false" />
|
||||
</template>
|
||||
|
||||
@@ -7,6 +8,7 @@
|
||||
import { ref, provide, onMounted } from 'vue';
|
||||
import MainLayout from './layout/MainLayout.vue';
|
||||
import Settings from './components/Settings.vue';
|
||||
import FullScreenPlayer from './components/FullScreenPlayer.vue';
|
||||
|
||||
const showSettings = ref(false);
|
||||
|
||||
|
||||
382
src/renderer/components/FullScreenPlayer.vue
Normal file
382
src/renderer/components/FullScreenPlayer.vue
Normal file
@@ -0,0 +1,382 @@
|
||||
<template>
|
||||
<div class="fullscreen-player" :class="{ active: isPlayerFullScreen }">
|
||||
<div class="player-content">
|
||||
<div class="dismiss-area" @click="toggleFullScreen">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
|
||||
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<polyline points="6 9 12 15 18 9"></polyline>
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<div class="background-container">
|
||||
<BackgroundRender
|
||||
:album="currentSong?.picUrl"
|
||||
:lowFreqVolume="loudness"
|
||||
:hasLyric="true"
|
||||
:playing="isPlaying"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="music-info">
|
||||
<!-- Placeholder for song info, lyrics etc. -->
|
||||
<h1>{{ currentSong?.name || 'No Music' }}</h1>
|
||||
<p>{{ currentSong?.artist }}</p>
|
||||
</div>
|
||||
|
||||
<canvas ref="canvasRef" class="spectrum-canvas"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, watch } from 'vue';
|
||||
import { usePlayerStore } from '../stores/player';
|
||||
import {
|
||||
type AbstractBaseRenderer,
|
||||
type BaseRenderer,
|
||||
BackgroundRender as CoreBackgroundRender,
|
||||
MeshGradientRenderer,
|
||||
} from "@applemusic-like-lyrics/core";
|
||||
import {
|
||||
defineComponent,
|
||||
onMounted,
|
||||
onUnmounted,
|
||||
type PropType,
|
||||
type Ref,
|
||||
ref,
|
||||
type ShallowRef,
|
||||
useTemplateRef,
|
||||
watchEffect,
|
||||
h,
|
||||
} from "vue";
|
||||
|
||||
// --- BackgroundRender Component Definition (from User Request) ---
|
||||
|
||||
/**
|
||||
* 背景渲染组件的引用
|
||||
*/
|
||||
export interface BackgroundRenderRef {
|
||||
/**
|
||||
* 背景渲染实例引用
|
||||
*/
|
||||
bgRender?: Ref<AbstractBaseRenderer | undefined>;
|
||||
/**
|
||||
* 将背景渲染实例的元素包裹起来的 DIV 元素实例
|
||||
*/
|
||||
wrapperEl: Readonly<ShallowRef<HTMLDivElement | null>>;
|
||||
}
|
||||
|
||||
const backgroundRenderProps = {
|
||||
/**
|
||||
* 设置背景专辑资源
|
||||
*/
|
||||
album: {
|
||||
type: [String, Object] as PropType<
|
||||
string | HTMLImageElement | HTMLVideoElement
|
||||
>,
|
||||
required: false,
|
||||
},
|
||||
/**
|
||||
* 设置专辑资源是否为视频
|
||||
*/
|
||||
albumIsVideo: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
},
|
||||
/**
|
||||
* 设置当前背景动画帧率,如果为 `undefined` 则默认为 `30`
|
||||
*/
|
||||
fps: {
|
||||
type: Number,
|
||||
required: false,
|
||||
},
|
||||
/**
|
||||
* 设置当前播放状态,如果为 `undefined` 则默认为 `true`
|
||||
*/
|
||||
playing: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
},
|
||||
/**
|
||||
* 设置当前动画流动速度,如果为 `undefined` 则默认为 `2`
|
||||
*/
|
||||
flowSpeed: {
|
||||
type: Number,
|
||||
required: false,
|
||||
},
|
||||
/**
|
||||
* 设置背景是否根据“是否有歌词”这个特征调整自身效果,例如有歌词时会变得更加活跃
|
||||
*
|
||||
* 部分渲染器会根据这个特征调整自身效果
|
||||
*
|
||||
* 如果不确定是否需要赋值或无法知晓是否包含歌词,请传入 true 或不做任何处理(默认值为 true)
|
||||
*/
|
||||
hasLyric: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
},
|
||||
/**
|
||||
* 设置低频的音量大小,范围在 80hz-120hz 之间为宜,取值范围在 [0.0-1.0] 之间
|
||||
*
|
||||
* 部分渲染器会根据音量大小调整背景效果(例如根据鼓点跳动)
|
||||
*
|
||||
* 如果无法获取到类似的数据,请传入 undefined 或 1.0 作为默认值,或不做任何处理(默认值即 1.0)
|
||||
*/
|
||||
lowFreqVolume: {
|
||||
type: Number,
|
||||
required: false,
|
||||
},
|
||||
/**
|
||||
* 设置当前渲染缩放比例,如果为 `undefined` 则默认为 `0.5`
|
||||
*/
|
||||
renderScale: {
|
||||
type: Number,
|
||||
required: false,
|
||||
},
|
||||
/**
|
||||
* 设置渲染器,如果为 `undefined` 则默认为 `MeshGradientRenderer`
|
||||
* 默认渲染器有可能会随着版本更新而更换
|
||||
*/
|
||||
renderer: {
|
||||
type: Object as PropType<{ // Use constructor type
|
||||
new (...args: ConstructorParameters<typeof BaseRenderer>): BaseRenderer;
|
||||
}>,
|
||||
required: false,
|
||||
},
|
||||
} as const;
|
||||
|
||||
const BackgroundRender = defineComponent({
|
||||
name: "BackgroundRender",
|
||||
props: backgroundRenderProps,
|
||||
setup(props, { expose }) {
|
||||
const wrapperRef = useTemplateRef<HTMLDivElement>("wrapper-ref");
|
||||
const bgRenderRef = ref<AbstractBaseRenderer>();
|
||||
|
||||
onMounted(() => {
|
||||
if (wrapperRef.value) {
|
||||
// @ts-ignore
|
||||
bgRenderRef.value = CoreBackgroundRender.new(
|
||||
props.renderer ?? MeshGradientRenderer,
|
||||
);
|
||||
const el = bgRenderRef.value.getElement();
|
||||
el.style.width = "100%";
|
||||
el.style.height = "100%";
|
||||
wrapperRef.value.appendChild(el);
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (bgRenderRef.value) {
|
||||
bgRenderRef.value.dispose();
|
||||
}
|
||||
});
|
||||
|
||||
watchEffect(() => {
|
||||
if (props.album)
|
||||
bgRenderRef.value?.setAlbum(props.album, props.albumIsVideo);
|
||||
});
|
||||
|
||||
watchEffect(() => {
|
||||
if (props.fps) bgRenderRef.value?.setFPS(props.fps);
|
||||
});
|
||||
|
||||
watchEffect(() => {
|
||||
if (props.playing) bgRenderRef.value?.pause();
|
||||
else bgRenderRef.value?.resume();
|
||||
});
|
||||
|
||||
watchEffect(() => {
|
||||
if (props.flowSpeed) bgRenderRef.value?.setFlowSpeed(props.flowSpeed);
|
||||
});
|
||||
|
||||
watchEffect(() => {
|
||||
if (props.renderScale)
|
||||
bgRenderRef.value?.setRenderScale(props.renderScale);
|
||||
});
|
||||
|
||||
watchEffect(() => {
|
||||
if (props.lowFreqVolume !== undefined)
|
||||
bgRenderRef.value?.setLowFreqVolume(props.lowFreqVolume);
|
||||
});
|
||||
|
||||
watchEffect(() => {
|
||||
if (props.hasLyric !== undefined)
|
||||
bgRenderRef.value?.setHasLyric(props.hasLyric ?? true);
|
||||
});
|
||||
|
||||
expose<BackgroundRenderRef>({
|
||||
bgRender: bgRenderRef,
|
||||
wrapperEl: wrapperRef,
|
||||
});
|
||||
|
||||
return () => h("div", { style: "display: contents;", ref: "wrapper-ref" });
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
// --- FullScreenPlayer Logic ---
|
||||
|
||||
const playerStore = usePlayerStore();
|
||||
const isPlayerFullScreen = computed(() => playerStore.isPlayerFullScreen);
|
||||
const currentSong = computed(() => playerStore.currentSong);
|
||||
const isPlaying = computed(() => playerStore.isPlaying);
|
||||
const loudness = computed(() => playerStore.loudness);
|
||||
|
||||
const canvasRef = ref<HTMLCanvasElement | null>(null);
|
||||
let animationId: number | null = null;
|
||||
|
||||
const drawSpectrum = () => {
|
||||
if (!canvasRef.value) return;
|
||||
const canvas = canvasRef.value;
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) return;
|
||||
|
||||
// Adjust canvas size to display resolution
|
||||
const dpr = window.devicePixelRatio || 1;
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
canvas.width = rect.width * dpr;
|
||||
canvas.height = rect.height * dpr;
|
||||
ctx.scale(dpr, dpr);
|
||||
|
||||
const width = rect.width;
|
||||
const height = rect.height;
|
||||
const barCount = 32;
|
||||
const barWidth = (width / barCount) * 0.5; // Gap = Bar Width
|
||||
const gap = (width - (barCount * barWidth)) / (barCount + 1);
|
||||
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
|
||||
const data = playerStore.spectrum;
|
||||
if (!data || data.length === 0) return;
|
||||
|
||||
ctx.fillStyle = 'rgba(255, 255, 255, 0.8)';
|
||||
|
||||
for (let i = 0; i < Math.min(data.length, barCount); i++) {
|
||||
const value = data[i];
|
||||
const barHeight = value * height * 0.6; // Scale down a bit
|
||||
const x = gap + i * (barWidth + gap);
|
||||
const y = height - barHeight;
|
||||
|
||||
// Draw rounded bar
|
||||
ctx.beginPath();
|
||||
ctx.roundRect(x, y, barWidth, barHeight, 4);
|
||||
ctx.fill();
|
||||
}
|
||||
};
|
||||
|
||||
const loop = () => {
|
||||
if (isPlayerFullScreen.value) {
|
||||
drawSpectrum();
|
||||
animationId = requestAnimationFrame(loop);
|
||||
}
|
||||
};
|
||||
|
||||
watch(isPlayerFullScreen, (val) => {
|
||||
if (val) {
|
||||
loop();
|
||||
} else {
|
||||
if (animationId) cancelAnimationFrame(animationId);
|
||||
}
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
if (isPlayerFullScreen.value) loop();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
if (animationId) cancelAnimationFrame(animationId);
|
||||
});
|
||||
|
||||
const toggleFullScreen = () => {
|
||||
playerStore.toggleFullScreen();
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.fullscreen-player {
|
||||
position: fixed;
|
||||
top: 100%; /* Initially hidden below screen */
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background-color: #000;
|
||||
z-index: 9999;
|
||||
transition: top 0.4s cubic-bezier(0.2, 0.8, 0.2, 1);
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.fullscreen-player.active {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.player-content {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 10;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.background-container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: -1;
|
||||
pointer-events: none;
|
||||
opacity: 0.6; /* Dim background slightly */
|
||||
}
|
||||
|
||||
.dismiss-area {
|
||||
padding: 24px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
transition: color 0.2s;
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.dismiss-area:hover {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.music-info {
|
||||
margin-top: auto;
|
||||
padding: 40px;
|
||||
text-align: left;
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.music-info h1 {
|
||||
font-size: 2rem;
|
||||
font-weight: bold;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.music-info p {
|
||||
font-size: 1.2rem;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.spectrum-canvas {
|
||||
width: 100%;
|
||||
height: 200px;
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
pointer-events: none;
|
||||
z-index: 15;
|
||||
/* Optional: Fade out at top */
|
||||
mask-image: linear-gradient(to top, black 0%, transparent 100%);
|
||||
-webkit-mask-image: linear-gradient(to top, black 0%, transparent 100%);
|
||||
}
|
||||
|
||||
</style>
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<transition name="slide-up">
|
||||
<div class="player-bar" v-if="hasSongs">
|
||||
<div class="player-bar" v-if="hasSongs" @click="handleBarClick">
|
||||
<!-- Left: Vinyl & Info -->
|
||||
<div class="player-left">
|
||||
<div class="vinyl-wrapper" :class="{ 'playing': isPlaying }">
|
||||
@@ -164,6 +164,20 @@ const toggleMute = () => {
|
||||
else playerStore.setVolume(50);
|
||||
};
|
||||
|
||||
const handleBarClick = (e: MouseEvent) => {
|
||||
// Prevent triggering when clicking on controls/inputs
|
||||
const target = e.target as HTMLElement;
|
||||
if (
|
||||
target.closest('button') ||
|
||||
target.closest('input') ||
|
||||
target.closest('.icon-btn') ||
|
||||
target.closest('.play-btn')
|
||||
) {
|
||||
return;
|
||||
}
|
||||
playerStore.toggleFullScreen();
|
||||
};
|
||||
|
||||
// Utils
|
||||
const formatTime = (seconds: number) => {
|
||||
if (!seconds || isNaN(seconds)) return '00:00';
|
||||
|
||||
@@ -155,7 +155,7 @@
|
||||
<div class="app-logo">🎶</div>
|
||||
<h3>QZ Music</h3>
|
||||
<p class="version">版本 1.0.0</p>
|
||||
<p class="copyright">© 2024 QZ Music Team</p>
|
||||
<p class="copyright">©2026 QZ <DEVELOPERS></DEVELOPERS></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -96,4 +96,15 @@ body {
|
||||
.page-content::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--color-text-muted);
|
||||
}
|
||||
|
||||
/* Route Transition Utils */
|
||||
.fade-enter-active,
|
||||
.fade-leave-active {
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.fade-enter-from,
|
||||
.fade-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
@@ -50,12 +50,12 @@ const playerStore = usePlayerStore()
|
||||
playerStore.playMode = PlayMode.Single;
|
||||
|
||||
const testSong: Song = {
|
||||
id: '3337983421',
|
||||
name: '不死身ごっこ (feat. 初音ミク)',
|
||||
artist: 'ピノキオピー、初音ミク',
|
||||
picUrl: 'http://p2.music.126.net/7O6FcCraxldhFGz4CSPVlw==/109951172567380787.jpg?imageView=&thumbnail=371y371&type=webp&rotate=360&tostatic=0',
|
||||
url: 'http://m701.music.126.net/20260203120731/1480f3bdda9795d5e7cc25af304b6cae/jdymusic/obj/wo3DlMOGwrbDjj7DisKw/77635439540/89c6/162f/e373/62ded4a27758346e53f717f46ba2802c.flac',
|
||||
duration: '02:31',
|
||||
id: '2716424334',
|
||||
name: 'T氏の話を信じるな (feat. 初音ミク & 重音テト)',
|
||||
artist: 'ピノキオピー、初音ミク、重音テト',
|
||||
picUrl: 'http://p2.music.126.net/DmrEz0M4GwSeISIReCNNgw==/109951171319581237.jpg?param=130y130',
|
||||
url: '',
|
||||
duration: '02:43',
|
||||
source: 'wy',
|
||||
type: 'Remote'
|
||||
};
|
||||
|
||||
@@ -28,6 +28,13 @@ export const usePlayerStore = defineStore('player', () => {
|
||||
const duration = ref(0);
|
||||
const currentTime = ref(0);
|
||||
|
||||
// Audio Visualization State
|
||||
const loudness = ref(0);
|
||||
const spectrum = ref<number[]>([]);
|
||||
|
||||
// UI State
|
||||
const isPlayerFullScreen = ref(false);
|
||||
|
||||
// Playlist State
|
||||
const playlist = ref<Song[]>([]);
|
||||
const currentIndex = ref(-1);
|
||||
@@ -96,7 +103,7 @@ export const usePlayerStore = defineStore('player', () => {
|
||||
let playUrl = song.url;
|
||||
if (song.type === 'Remote' && song.source) {
|
||||
// Use Local Proxy
|
||||
const quality = 'hires';
|
||||
const quality = 'jymaster';
|
||||
playUrl = `http://localhost:5266/music?source=${song.source}&id=${song.id}&quality=${quality}`;
|
||||
console.log('[Player] Using Proxy:', playUrl);
|
||||
}
|
||||
@@ -106,8 +113,8 @@ export const usePlayerStore = defineStore('player', () => {
|
||||
// Reset retry flag for new playback attempt
|
||||
hasRetriedWithFreshUrl.value = false;
|
||||
try {
|
||||
await window.electronAPI.mpv.load(playUrl);
|
||||
await window.electronAPI.mpv.play();
|
||||
await window.electronAPI.qzplayer.load(playUrl);
|
||||
await window.electronAPI.qzplayer.play();
|
||||
isPlaying.value = true;
|
||||
song.url = playUrl;
|
||||
} catch (e) {
|
||||
@@ -131,8 +138,8 @@ export const usePlayerStore = defineStore('player', () => {
|
||||
artwork: song.picUrl ? [{ src: song.picUrl, sizes: '512x512', type: 'image/png' }] : []
|
||||
});
|
||||
|
||||
navigator.mediaSession.setActionHandler('play', () => window.electronAPI.mpv.play());
|
||||
navigator.mediaSession.setActionHandler('pause', () => window.electronAPI.mpv.pause());
|
||||
navigator.mediaSession.setActionHandler('play', () => window.electronAPI.qzplayer.play());
|
||||
navigator.mediaSession.setActionHandler('pause', () => window.electronAPI.qzplayer.pause());
|
||||
navigator.mediaSession.setActionHandler('previoustrack', () => prev());
|
||||
navigator.mediaSession.setActionHandler('nexttrack', () => next(true));
|
||||
|
||||
@@ -170,7 +177,7 @@ export const usePlayerStore = defineStore('player', () => {
|
||||
};
|
||||
|
||||
const handlePlayError = async () => {
|
||||
// Proxy handles refreshing internally, so we rely on MPV error/retry for now.
|
||||
// Proxy handles refreshing internally, so we rely on qzplayer error/retry for now.
|
||||
// Or we could implement a mechanism to tell proxy to invalidate cache if this fails repeatedly (future work).
|
||||
|
||||
// Normal error handling
|
||||
@@ -184,7 +191,7 @@ export const usePlayerStore = defineStore('player', () => {
|
||||
return;
|
||||
}
|
||||
if (playErrorCount.value >= MAX_RETRY_COUNT) {
|
||||
window.electronAPI.mpv.pause();
|
||||
window.electronAPI.qzplayer.pause();
|
||||
isPlaying.value = false;
|
||||
MessagePlugin.error('连续多次播放失败,已停止播放');
|
||||
playErrorCount.value = 0;
|
||||
@@ -197,16 +204,18 @@ export const usePlayerStore = defineStore('player', () => {
|
||||
|
||||
// Listeners
|
||||
if (window.electronAPI) {
|
||||
window.electronAPI.mpv.onEvent((_event, data) => {
|
||||
window.electronAPI.qzplayer.onEvent((_event, data) => {
|
||||
if (data.event === 'property-change') {
|
||||
if (data.name === 'pause') {
|
||||
const isPaused = data.data;
|
||||
isPlaying.value = !isPaused;
|
||||
// 核心:MPV 暂停 -> 同步暂停 Dummy -> 浏览器更新 SMTC 状态
|
||||
// 核心:qzplayer 暂停 -> 同步暂停 Dummy -> 浏览器更新 SMTC 状态
|
||||
syncDummyAudioState(!isPaused);
|
||||
}
|
||||
if (data.name === 'time-pos') currentTime.value = data.data;
|
||||
if (data.name === 'duration') duration.value = data.data;
|
||||
if (data.name === 'loudness') loudness.value = data.data;
|
||||
if (data.name === 'spectrum') spectrum.value = data.data;
|
||||
}
|
||||
|
||||
if (data.event === 'end-file') {
|
||||
@@ -221,16 +230,16 @@ export const usePlayerStore = defineStore('player', () => {
|
||||
}
|
||||
|
||||
const togglePlay = async () => {
|
||||
await window.electronAPI.mpv.togglePause();
|
||||
await window.electronAPI.qzplayer.togglePause();
|
||||
};
|
||||
|
||||
const setVolume = async (vol: number) => {
|
||||
volume.value = vol;
|
||||
await window.electronAPI.mpv.setVolume(vol);
|
||||
await window.electronAPI.qzplayer.setVolume(vol);
|
||||
};
|
||||
|
||||
const seek = async (time: number) => {
|
||||
await window.electronAPI.mpv.seek(time);
|
||||
await window.electronAPI.qzplayer.seek(time);
|
||||
};
|
||||
|
||||
const toggleMode = () => {
|
||||
@@ -239,6 +248,10 @@ export const usePlayerStore = defineStore('player', () => {
|
||||
else playMode.value = PlayMode.List;
|
||||
};
|
||||
|
||||
const toggleFullScreen = () => {
|
||||
isPlayerFullScreen.value = !isPlayerFullScreen.value;
|
||||
};
|
||||
|
||||
return {
|
||||
isPlaying,
|
||||
currentSong,
|
||||
@@ -247,6 +260,9 @@ export const usePlayerStore = defineStore('player', () => {
|
||||
currentTime,
|
||||
playlist,
|
||||
playMode,
|
||||
loudness,
|
||||
spectrum,
|
||||
isPlayerFullScreen,
|
||||
setPlaylist,
|
||||
playSong,
|
||||
next,
|
||||
@@ -254,6 +270,7 @@ export const usePlayerStore = defineStore('player', () => {
|
||||
togglePlay,
|
||||
setVolume,
|
||||
seek,
|
||||
toggleMode
|
||||
toggleMode,
|
||||
toggleFullScreen
|
||||
};
|
||||
});
|
||||
2
src/renderer/types/electron.d.ts
vendored
2
src/renderer/types/electron.d.ts
vendored
@@ -3,7 +3,7 @@ export interface IElectronAPI {
|
||||
maximizeWindow: () => void;
|
||||
closeWindow: () => void;
|
||||
isMaximized: () => Promise<boolean>;
|
||||
mpv: {
|
||||
qzplayer: {
|
||||
load: (url: string) => Promise<void>;
|
||||
play: () => Promise<void>;
|
||||
pause: () => Promise<void>;
|
||||
|
||||
Reference in New Issue
Block a user