Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 88 additions & 2 deletions src/components/Player/FullPlayerMobile.vue
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,18 @@ let savedPageType: MobilePageType = "info";

<div v-if="hasLyric" class="page lyric-page">
<div class="lyric-header">
<s-image :src="musicStore.getSongCover('s')" cache-type="covers" class="lyric-cover" />
<div
class="lyric-cover"
data-no-page-swipe
@pointerdown.stop
@pointerup="onLyricCoverPointerUp"
>
<s-image
:src="musicStore.getSongCover('s')"
cache-type="covers"
class="lyric-cover-image"
/>
</div>
<div class="lyric-info">
<div class="name text-hidden">
{{
Expand Down Expand Up @@ -301,6 +312,66 @@ const pageIndex = ref(resolveSavedPageIndex(savedPageType));
const pageTransitionDisabled = ref(false);
const pageSwipeBlocked = ref(false);
let pageTransitionTimer = 0;
let lastLyricCoverTapAt = 0;
let lastLyricCoverTapX = 0;
let lastLyricCoverTapY = 0;

const LYRIC_COVER_DOUBLE_TAP_DELAY = 320;
const LYRIC_COVER_DOUBLE_TAP_DISTANCE = 24;

const applyGlobalLyricOffsetToCurrentSong = () => {
if (!settingStore.globalLyricOffsetEnabled || !settingStore.globalLyricOffsetDoubleClickApply) {
return;
}
const currentSongId = musicStore.playSong?.id;
if (!currentSongId) return;

const offsetValue = settingStore.globalLyricOffsetValue;
const currentOffset = statusStore.getSongOffset(currentSongId);
const sign = offsetValue > 0 ? "+" : "";

if (settingStore.globalLyricOffsetAlwaysApply) {
if (currentOffset === offsetValue) {
statusStore.setSongOffset(currentSongId, -offsetValue);
window.$message?.success("本歌曲将临时关闭偏移");
} else {
statusStore.resetSongOffset(currentSongId);
window.$message?.success(`已恢复全局偏移: ${sign}${offsetValue}ms`);
}
} else {
if (currentOffset === offsetValue) {
statusStore.resetSongOffset(currentSongId);
window.$message?.success(`已关闭单曲偏移: ${sign}${offsetValue}ms`);
} else {
statusStore.setSongOffset(currentSongId, offsetValue);
window.$message?.success(`已开启单曲偏移: ${sign}${offsetValue}ms`);
}
}
};

// 歌词页封面使用双点,避免和移动端下滑手势冲突
const onLyricCoverPointerUp = (event: PointerEvent) => {
event.stopPropagation();
if (event.cancelable) event.preventDefault();
if (event.pointerType === "mouse" && event.button !== 0) return;

const now = Date.now();
const dx = event.clientX - lastLyricCoverTapX;
const dy = event.clientY - lastLyricCoverTapY;
const isDoubleTap =
now - lastLyricCoverTapAt <= LYRIC_COVER_DOUBLE_TAP_DELAY &&
Math.hypot(dx, dy) <= LYRIC_COVER_DOUBLE_TAP_DISTANCE;

if (isDoubleTap) {
lastLyricCoverTapAt = 0;
applyGlobalLyricOffsetToCurrentSong();
return;
}

lastLyricCoverTapAt = now;
lastLyricCoverTapX = event.clientX;
lastLyricCoverTapY = event.clientY;
};

const lyricHeaderHorizontalPadding = computed(() => {
const padding = Math.max(0, settingStore.lyricHorizontalOffset);
Expand Down Expand Up @@ -337,7 +408,8 @@ const dragHandleStyle = computed(() => {
if (currentPageType.value === "lyric") {
return {
top: "calc(52px + var(--mobile-safe-top))",
left: "16px",
// 歌词页保留下滑手势,但避开左侧封面区域,防止封面双点被捕获层吃掉
left: "calc(20px + var(--lyric-h-offset, 0px) + 72px)",
right: "72px",
height: "74px",
};
Expand Down Expand Up @@ -926,11 +998,20 @@ const contentTransform = computed(() => {
}

.lyric-cover {
position: relative;
z-index: 11;
width: 50px;
height: 50px;
flex-shrink: 0;
border-radius: 6px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
overflow: hidden;
touch-action: manipulation;

.lyric-cover-image {
width: 100%;
height: 100%;
}

:deep(img) {
width: 100%;
Expand Down Expand Up @@ -1239,6 +1320,11 @@ const contentTransform = computed(() => {
height: 64px;
border-radius: 10px;

.lyric-cover-image {
width: 100%;
height: 100%;
}

:deep(img) {
border-radius: 10px;
}
Expand Down
9 changes: 7 additions & 2 deletions src/components/Player/PlayerLyric/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,12 @@ const offsetMilliseconds = computed({
return statusStore.getSongOffset(currentSongId.value);
},
set: (val: number | null) => {
statusStore.setSongOffset(currentSongId.value, val || 0);
const settingStore = useSettingStore();
const globalOffset = (settingStore.globalLyricOffsetEnabled && settingStore.globalLyricOffsetAlwaysApply)
? settingStore.globalLyricOffsetValue
: 0;
const localOffset = (val || 0) - globalOffset;
statusStore.setSongOffset(currentSongId.value, localOffset);
},
});

Expand All @@ -179,7 +184,7 @@ const changeOffset = (delta: number) => {
* 重置进度偏移
*/
const resetOffset = () => {
statusStore.resetSongOffset(currentSongId.value);
offsetMilliseconds.value = 0;
};

onMounted(() => {
Expand Down
61 changes: 61 additions & 0 deletions src/components/Player/PlayerMeta/PlayerCover.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
v-if="settingStore.playerType === 'fullscreen' && !isPhone"
class="full-screen"
:style="{ '--gradient-percent': settingStore.playerFullscreenGradient + '%' }"
@pointerdown.stop
@pointerup="onCoverPointerUp"
>
<s-image
:src="getCoverUrl('xl')"
Expand All @@ -19,6 +21,8 @@
<div
v-else
:class="['player-cover', settingStore.playerType, { playing: statusStore.playStatus }]"
@pointerdown.stop
@pointerup="onCoverPointerUp"
>
<!-- 指针 -->
<img
Expand Down Expand Up @@ -188,6 +192,63 @@ const getCoverUrl = (size: "s" | "m" | "l" | "xl" = "l") => {
return musicStore.getSongCover(size);
};

// 双击封面切换当前歌曲的偏移状态
const onDoubleClickCover = () => {
if (settingStore.globalLyricOffsetEnabled && settingStore.globalLyricOffsetDoubleClickApply) {
const currentSongId = musicStore.playSong?.id;
if (!currentSongId) return;

const offsetValue = settingStore.globalLyricOffsetValue;
const currentOffset = statusStore.getSongOffset(currentSongId);
const sign = offsetValue > 0 ? "+" : "";

if (settingStore.globalLyricOffsetAlwaysApply) {
if (currentOffset === offsetValue) {
statusStore.setSongOffset(currentSongId, -offsetValue);
window.$message?.success("本歌曲将临时关闭偏移");
} else {
statusStore.resetSongOffset(currentSongId);
window.$message?.success(`已恢复全局偏移: ${sign}${offsetValue}ms`);
}
} else {
if (currentOffset === offsetValue) {
statusStore.resetSongOffset(currentSongId);
window.$message?.success(`已关闭单曲偏移: ${sign}${offsetValue}ms`);
} else {
statusStore.setSongOffset(currentSongId, offsetValue);
window.$message?.success(`已开启单曲偏移: ${sign}${offsetValue}ms`);
}
}
}
};

let lastCoverTapAt = 0;
let lastCoverTapX = 0;
let lastCoverTapY = 0;
const COVER_DOUBLE_TAP_DELAY = 320;
const COVER_DOUBLE_TAP_DISTANCE = 24;

const onCoverPointerUp = (event: PointerEvent) => {
if (event.pointerType === "mouse" && event.button !== 0) return;

const now = Date.now();
const dx = event.clientX - lastCoverTapX;
const dy = event.clientY - lastCoverTapY;
const isDoubleTap =
now - lastCoverTapAt <= COVER_DOUBLE_TAP_DELAY &&
Math.hypot(dx, dy) <= COVER_DOUBLE_TAP_DISTANCE;

if (isDoubleTap) {
lastCoverTapAt = 0;
onDoubleClickCover();
return;
}

lastCoverTapAt = now;
lastCoverTapX = event.clientX;
lastCoverTapY = event.clientY;
};

watch(
() => [musicStore.playSong.id, settingStore.dynamicCover, settingStore.playerType],
() => getDynamicCover(),
Expand Down
111 changes: 111 additions & 0 deletions src/components/Player/PlayerQuickActionsMenu.vue
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,104 @@
</div>
<n-switch v-model:value="settingStore.lyricsBlur" :round="false" size="small" />
</div>

<!-- 全局偏移 -->
<div class="qa-item">
<div class="qa-item-label">
<SvgIcon name="Forward5" :size="18" />
<span class="qa-item-text">全局偏移</span>
</div>
<n-switch v-model:value="settingStore.globalLyricOffsetEnabled" :round="false" size="small" />
</div>

<div class="qa-item qa-volume" v-if="settingStore.globalLyricOffsetEnabled">
<div class="qa-item-label">
<SvgIcon name="Forward5" :size="18" />
<span class="qa-item-text">偏移数值</span>
<span class="qa-volume-value">{{ settingStore.globalLyricOffsetValue > 0 ? '+' : '' }}{{ settingStore.globalLyricOffsetValue }}ms</span>
</div>
<div class="qa-stepper">
<n-button
block
secondary
size="tiny"
:disabled="settingStore.globalLyricOffsetValue <= -10000"
@click="adjustGlobalLyricOffset(-50)"
>
-
</n-button>
<n-button
block
secondary
size="tiny"
:disabled="settingStore.globalLyricOffsetValue >= 10000"
@click="adjustGlobalLyricOffset(50)"
>
+
</n-button>
</div>
</div>

<div class="qa-item qa-volume" v-if="settingStore.globalLyricOffsetEnabled">
<div class="qa-presets" style="display: grid; grid-template-columns: repeat(2, 1fr); gap: 8px; width: 100%;">
<n-button block secondary size="tiny" @click="applyGlobalLyricOffsetPreset(0)">
0ms
</n-button>
<n-button block secondary size="tiny" @click="applyGlobalLyricOffsetPreset(250)">
{{ settingStore.globalLyricOffsetPresetSign === '-' ? '-250ms' : '+250ms' }}
</n-button>
<n-button block secondary size="tiny" @click="applyGlobalLyricOffsetPreset(500)">
{{ settingStore.globalLyricOffsetPresetSign === '-' ? '-500ms' : '+500ms' }}
</n-button>
<n-button block secondary size="tiny" @click="applyGlobalLyricOffsetPreset(850)">
{{ settingStore.globalLyricOffsetPresetSign === '-' ? '-850ms' : '+850ms' }}
</n-button>
</div>
</div>

<div class="qa-item qa-volume" v-if="settingStore.globalLyricOffsetEnabled">
<div class="qa-item-label">
<SvgIcon name="LibraryMusic" :size="18" />
<span class="qa-item-text">快捷预设单位</span>
<span class="qa-volume-value">{{ settingStore.globalLyricOffsetPresetSign }}</span>
</div>
<div class="qa-stepper">
<n-button
block
size="tiny"
:type="settingStore.globalLyricOffsetPresetSign === '+' ? 'primary' : 'default'"
:secondary="settingStore.globalLyricOffsetPresetSign === '+'"
@click="settingStore.globalLyricOffsetPresetSign = '+'"
>
+
</n-button>
<n-button
block
size="tiny"
:type="settingStore.globalLyricOffsetPresetSign === '-' ? 'primary' : 'default'"
:secondary="settingStore.globalLyricOffsetPresetSign === '-'"
@click="settingStore.globalLyricOffsetPresetSign = '-'"
>
-
</n-button>
</div>
</div>

<div class="qa-item" v-if="settingStore.globalLyricOffsetEnabled">
<div class="qa-item-label">
<SvgIcon name="Forward5" :size="18" />
<span class="qa-item-text">始终启用偏移</span>
</div>
<n-switch v-model:value="settingStore.globalLyricOffsetAlwaysApply" :round="false" size="small" />
</div>

<div class="qa-item" v-if="settingStore.globalLyricOffsetEnabled">
<div class="qa-item-label">
<SvgIcon name="Album" :size="18" />
<span class="qa-item-text">双击封面开关</span>
</div>
<n-switch v-model:value="settingStore.globalLyricOffsetDoubleClickApply" :round="false" size="small" />
</div>
</div>
</div>
</n-popover>
Expand Down Expand Up @@ -326,6 +424,19 @@ const adjustLandscapeCoverOffset = (delta: number) => {
);
};

const adjustGlobalLyricOffset = (delta: number) => {
settingStore.globalLyricOffsetValue = clampValue(
settingStore.globalLyricOffsetValue + delta,
-10000,
10000,
);
};

const applyGlobalLyricOffsetPreset = (value: number) => {
const signMultiplier = settingStore.globalLyricOffsetPresetSign === "-" ? -1 : 1;
settingStore.globalLyricOffsetValue = value * signMultiplier;
};

const adjustLandscapeLyricPadding = (delta: number) => {
settingStore.landscapeLyricPaddingX = clampValue(
settingStore.landscapeLyricPaddingX + delta,
Expand Down
Loading