Skip to content

Commit 0fec4e9

Browse files
committed
修复关注列表自定义文件夹主播重复出现的问题,增加弹幕透明度调节
1 parent 88ef671 commit 0fec4e9

File tree

7 files changed

+128
-13
lines changed

7 files changed

+128
-13
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "dtv",
33
"private": true,
4-
"version": "2.3.7",
4+
"version": "2.3.8",
55
"type": "module",
66
"scripts": {
77
"dev": "vite",

src-tauri/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[package]
22
name = "dtv"
3-
version = "2.3.7"
3+
version = "2.3.8"
44
description = "A Tauri App"
55
authors = ["c-zeong"]
66
edition = "2021"

src-tauri/tauri.conf.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"$schema": "https://schema.tauri.app/config/2",
33
"productName": "DTV",
4-
"version": "2.3.7",
4+
"version": "2.3.8",
55
"identifier": "com.dtv.app",
66
"build": {
77
"beforeDevCommand": "npm run dev",

src/components/FollowsList/FolderItem.vue

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,22 @@ const emit = defineEmits<{
113113
(e: 'streamerDragStart', streamer: FollowedStreamer, event: MouseEvent): void;
114114
}>();
115115
116+
const sortStreamersByStatus = (items: FollowedStreamer[]): FollowedStreamer[] => {
117+
const live: FollowedStreamer[] = [];
118+
const looping: FollowedStreamer[] = [];
119+
const rest: FollowedStreamer[] = [];
120+
items.forEach(streamer => {
121+
if (streamer.liveStatus === 'LIVE' || (!streamer.liveStatus && streamer.isLive)) {
122+
live.push(streamer);
123+
} else if (streamer.liveStatus === 'REPLAY') {
124+
looping.push(streamer);
125+
} else {
126+
rest.push(streamer);
127+
}
128+
});
129+
return [...live, ...looping, ...rest];
130+
};
131+
116132
const folderItems = computed(() => {
117133
const seen = new Set<string>();
118134
const result: FollowedStreamer[] = [];
@@ -127,7 +143,7 @@ const folderItems = computed(() => {
127143
result.push(found);
128144
}
129145
}
130-
return result;
146+
return sortStreamersByStatus(result);
131147
});
132148
133149
const toggleExpand = () => {

src/components/FollowsList/index.vue

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,15 @@
280280
281281
const streamerKey = (platform: Platform | string, id: string) => `${String(platform).toUpperCase()}:${id}`;
282282
const toStreamerKey = (streamer: Pick<FollowedStreamer, 'platform' | 'id'>) => streamerKey(streamer.platform, streamer.id);
283+
const normalizeRawKey = (rawKey?: string | null) => {
284+
if (!rawKey) return '';
285+
const segments = String(rawKey).split(':');
286+
if (segments.length < 2) return '';
287+
const platformPart = segments.shift();
288+
const idPart = segments.join(':');
289+
if (!platformPart || !idPart) return '';
290+
return streamerKey(platformPart, idPart);
291+
};
283292
const isLiveStreamer = (streamer?: FollowedStreamer | null) => {
284293
if (!streamer) return false;
285294
if (streamer.liveStatus && streamer.liveStatus !== 'UNKNOWN') {
@@ -309,6 +318,15 @@
309318
...followStore.folders.map(folder => ({ type: 'folder' as const, data: folder })),
310319
...props.followedAnchors.map(streamer => ({ type: 'streamer' as const, data: streamer })),
311320
];
321+
const folderStreamerKeys = new Set<string>();
322+
followStore.folders.forEach(folder => {
323+
folder.streamerIds.forEach(id => {
324+
const normalized = normalizeRawKey(id);
325+
if (normalized) {
326+
folderStreamerKeys.add(normalized);
327+
}
328+
});
329+
});
312330
313331
if (!baseOrderSource.length && !followStore.folders.length) {
314332
return props.followedAnchors.length ? {
@@ -322,7 +340,8 @@
322340
streamerDataMap.set(toStreamerKey(streamer), streamer);
323341
});
324342
updateEntries.forEach(entry => {
325-
streamerDataMap.set(entry.originalKey, entry.updated);
343+
const normalized = normalizeRawKey(entry.originalKey) || toStreamerKey(entry.updated);
344+
streamerDataMap.set(normalized, entry.updated);
326345
});
327346
328347
const folderItems: FolderListItem[] = [];
@@ -341,8 +360,9 @@
341360
};
342361
343362
const pushStreamerByKey = (key: string) => {
344-
if (seenStreamerKeys.has(key)) return;
345-
const streamer = streamerDataMap.get(key);
363+
const normalizedKey = normalizeRawKey(key) || key;
364+
if (!normalizedKey || seenStreamerKeys.has(normalizedKey)) return;
365+
const streamer = streamerDataMap.get(normalizedKey);
346366
if (!streamer) return;
347367
const item: StreamerListItem = { type: 'streamer', data: streamer };
348368
const bucket = getStatusBucket(streamer);
@@ -353,7 +373,7 @@
353373
} else {
354374
offlineItems.push(item);
355375
}
356-
seenStreamerKeys.add(key);
376+
seenStreamerKeys.add(normalizedKey);
357377
};
358378
359379
baseOrderSource.forEach(item => {
@@ -368,7 +388,13 @@
368388
pushStreamerByKey(key);
369389
});
370390
371-
const nextListOrder = [...folderItems, ...liveItems, ...loopingItems, ...offlineItems];
391+
const filterOutFoldered = (items: StreamerListItem[]) => items.filter(item => !folderStreamerKeys.has(toStreamerKey(item.data)));
392+
const nextListOrder = [
393+
...folderItems,
394+
...filterOutFoldered(liveItems),
395+
...filterOutFoldered(loopingItems),
396+
...filterOutFoldered(offlineItems),
397+
];
372398
const streamerSequence: FollowedStreamer[] = [...liveItems, ...loopingItems, ...offlineItems].map(item => item.data);
373399
return { nextListOrder, streamerSequence };
374400
}

src/components/player/index.vue

Lines changed: 75 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -137,10 +137,13 @@ type DanmuUserSettings = {
137137
duration: number;
138138
area: number;
139139
mode: 'scroll' | 'top' | 'bottom';
140+
opacity: number;
140141
};
141142
142143
const DANMU_PREFERENCES_STORAGE_KEY = 'dtv_danmu_preferences_v1';
143144
const DANMU_AREA_OPTIONS = [0.25, 0.5, 0.75] as const;
145+
const DANMU_OPACITY_MIN = 0.2;
146+
const DANMU_OPACITY_MAX = 1;
144147
const PLAYER_VOLUME_STORAGE_KEY = 'dtv_player_volume_v1';
145148
const DEFAULT_DANMU_FONT_FAMILY = '"HarmonyOS Sans Bold", "HarmonyOS Sans", "PingFang SC", "Helvetica Neue", Arial, sans-serif';
146149
const WINDOWS_DANMU_FONT_FAMILY = '"HarmonyOS Sans Regular", "HarmonyOS Sans", "Microsoft YaHei", "Segoe UI", sans-serif';
@@ -149,6 +152,13 @@ const sanitizeDanmuArea = (value: number): number => {
149152
return DANMU_AREA_OPTIONS.reduce((prev, curr) => Math.abs(curr - value) < Math.abs(prev - value) ? curr : prev, DANMU_AREA_OPTIONS[0]);
150153
};
151154
155+
const sanitizeDanmuOpacity = (value: number): number => {
156+
if (!Number.isFinite(value)) {
157+
return 1;
158+
}
159+
return Math.min(DANMU_OPACITY_MAX, Math.max(DANMU_OPACITY_MIN, value));
160+
};
161+
152162
const loadStoredVolume = (): number | null => {
153163
if (typeof window === 'undefined' || !window.localStorage) {
154164
return null;
@@ -220,6 +230,7 @@ const loadDanmuPreferences = (): { enabled: boolean; settings: DanmuUserSettings
220230
duration: Number.isFinite(settings.duration) ? settings.duration : 10000,
221231
area: Number.isFinite(settings.area) ? sanitizeDanmuArea(settings.area) : 0.5,
222232
mode: settings.mode === 'top' || settings.mode === 'bottom' ? settings.mode : 'scroll',
233+
opacity: Number.isFinite(settings.opacity) ? sanitizeDanmuOpacity(settings.opacity) : 1,
223234
},
224235
};
225236
} catch (error) {
@@ -323,6 +334,7 @@ class DanmuSettingsControl extends Plugin {
323334
duration: 10000,
324335
area: 0.5,
325336
mode: 'scroll',
337+
opacity: 1,
326338
})) as () => DanmuUserSettings,
327339
onChange: (async (_partial: Partial<DanmuUserSettings>) => {}) as (partial: Partial<DanmuUserSettings>) => Promise<void> | void,
328340
};
@@ -341,12 +353,14 @@ class DanmuSettingsControl extends Plugin {
341353
duration: 10000,
342354
area: 0.5,
343355
mode: 'scroll',
356+
opacity: 1,
344357
};
345358
private textColorInput: HTMLInputElement | null = null;
346359
private strokeColorInput: HTMLInputElement | null = null;
347360
private fontSizeSlider: HTMLInputElement | null = null;
348361
private durationSlider: HTMLInputElement | null = null;
349362
private areaSlider: HTMLInputElement | null = null;
363+
private opacitySlider: HTMLInputElement | null = null;
350364
351365
override afterCreate() {
352366
if (this.config.disable) {
@@ -356,6 +370,7 @@ class DanmuSettingsControl extends Plugin {
356370
? this.config.getSettings()
357371
: this.currentSettings;
358372
this.currentSettings.area = sanitizeDanmuArea(this.currentSettings.area);
373+
this.currentSettings.opacity = sanitizeDanmuOpacity(this.currentSettings.opacity);
359374
if (typeof this.currentSettings.strokeColor !== 'string') {
360375
this.currentSettings.strokeColor = '#444444';
361376
}
@@ -429,6 +444,7 @@ class DanmuSettingsControl extends Plugin {
429444
this.fontSizeSlider = null;
430445
this.durationSlider = null;
431446
this.areaSlider = null;
447+
this.opacitySlider = null;
432448
}
433449
434450
override render() {
@@ -466,6 +482,10 @@ class DanmuSettingsControl extends Plugin {
466482
<label>显示区域 <span class="settings-value area-value">${this.formatAreaLabel(this.currentSettings.area)}</span></label>
467483
<input class="danmu-setting-area-range" type="range" min="0.25" max="0.75" step="0.25" value="${this.currentSettings.area}">
468484
</div>
485+
<div class="settings-row">
486+
<label>透明度 <span class="settings-value opacity-value">${this.formatOpacityLabel(this.currentSettings.opacity)}</span></label>
487+
<input class="danmu-setting-opacity-range" type="range" min="${DANMU_OPACITY_MIN}" max="${DANMU_OPACITY_MAX}" step="0.05" value="${this.currentSettings.opacity}">
488+
</div>
469489
</div>
470490
</div>
471491
`;
@@ -486,6 +506,7 @@ class DanmuSettingsControl extends Plugin {
486506
this.fontSizeSlider = this.panel.querySelector<HTMLInputElement>('.danmu-setting-font-range');
487507
this.durationSlider = this.panel.querySelector<HTMLInputElement>('.danmu-setting-duration-range');
488508
this.areaSlider = this.panel.querySelector<HTMLInputElement>('.danmu-setting-area-range');
509+
this.opacitySlider = this.panel.querySelector<HTMLInputElement>('.danmu-setting-opacity-range');
489510
490511
this.textColorInput?.addEventListener('input', (event) => {
491512
const value = (event.target as HTMLInputElement).value;
@@ -550,6 +571,14 @@ class DanmuSettingsControl extends Plugin {
550571
'.area-value',
551572
(value) => this.formatAreaLabel(value),
552573
);
574+
575+
handleRange(
576+
this.opacitySlider,
577+
'opacity',
578+
(value) => sanitizeDanmuOpacity(Number(value)),
579+
'.opacity-value',
580+
(value) => this.formatOpacityLabel(value),
581+
);
553582
}
554583
555584
private updateSliderVisual(el: HTMLInputElement | null) {
@@ -636,6 +665,15 @@ class DanmuSettingsControl extends Plugin {
636665
}
637666
this.updateSliderVisual(this.areaSlider);
638667
}
668+
if (this.opacitySlider) {
669+
const opacityValue = sanitizeDanmuOpacity(this.currentSettings.opacity);
670+
this.opacitySlider.value = String(opacityValue);
671+
const opacityLabel = this.panel.querySelector<HTMLSpanElement>('.opacity-value');
672+
if (opacityLabel) {
673+
opacityLabel.textContent = this.formatOpacityLabel(opacityValue);
674+
}
675+
this.updateSliderVisual(this.opacitySlider);
676+
}
639677
}
640678
641679
private formatDurationLabel(value: number): string {
@@ -666,6 +704,11 @@ class DanmuSettingsControl extends Plugin {
666704
return '上 3/4';
667705
}
668706
707+
private formatOpacityLabel(value: number): string {
708+
const normalized = sanitizeDanmuOpacity(value);
709+
return `${Math.round(normalized * 100)}%`;
710+
}
711+
669712
private emitChange(partial: Partial<DanmuUserSettings>) {
670713
const callback = this.config.onChange;
671714
if (typeof callback === 'function') {
@@ -678,6 +721,9 @@ class DanmuSettingsControl extends Plugin {
678721
if (typeof normalized.area === 'number') {
679722
normalized.area = sanitizeDanmuArea(normalized.area);
680723
}
724+
if (typeof normalized.opacity === 'number') {
725+
normalized.opacity = sanitizeDanmuOpacity(normalized.opacity);
726+
}
681727
if (typeof normalized.strokeColor !== 'undefined' && typeof normalized.strokeColor !== 'string') {
682728
delete (normalized as any).strokeColor;
683729
}
@@ -1459,6 +1505,7 @@ const danmuSettings = reactive<DanmuUserSettings>({
14591505
duration: 10000,
14601506
area: 0.5,
14611507
mode: 'scroll',
1508+
opacity: 1,
14621509
});
14631510
14641511
const storedDanmuPreferences = loadDanmuPreferences();
@@ -1674,6 +1721,7 @@ function createDanmuOverlay(player: Player | null) {
16741721
16751722
overlayHost.innerHTML = '';
16761723
overlayHost.style.setProperty('--danmu-stroke-color', danmuSettings.strokeColor);
1724+
overlayHost.style.setProperty('--danmu-opacity', String(isDanmuEnabled.value ? sanitizeDanmuOpacity(danmuSettings.opacity) : 0));
16771725
16781726
try {
16791727
const overlay = new DanmuJs({
@@ -1704,6 +1752,7 @@ function applyDanmuOverlayPreferences(overlay: DanmuOverlayInstance | null) {
17041752
if (!overlay) {
17051753
return;
17061754
}
1755+
const host = playerInstance.value?.root?.querySelector('.player-danmu-overlay') as HTMLElement | null;
17071756
const fontSizeValue = parseInt(danmuSettings.fontSize, 10);
17081757
if (!Number.isNaN(fontSizeValue)) {
17091758
try {
@@ -1726,12 +1775,14 @@ function applyDanmuOverlayPreferences(overlay: DanmuOverlayInstance | null) {
17261775
// Non-critical for players that do not support bulk duration updates
17271776
}
17281777
try {
1729-
overlay.setOpacity?.(isDanmuEnabled.value ? 1 : 0);
1778+
const normalizedOpacity = sanitizeDanmuOpacity(danmuSettings.opacity);
1779+
const nextOpacity = isDanmuEnabled.value ? normalizedOpacity : 0;
1780+
overlay.setOpacity?.(nextOpacity);
1781+
host?.style.setProperty('--danmu-opacity', String(nextOpacity));
17301782
} catch (error) {
17311783
// Non-critical
17321784
}
17331785
try {
1734-
const host = playerInstance.value?.root?.querySelector('.player-danmu-overlay') as HTMLElement | null;
17351786
host?.style.setProperty('--danmu-stroke-color', danmuSettings.strokeColor);
17361787
} catch (error) {
17371788
console.warn('[Player] Failed to apply danmu stroke color:', error);
@@ -1742,17 +1793,20 @@ function syncDanmuEnabledState(overlay: DanmuOverlayInstance | null) {
17421793
if (!overlay) {
17431794
return;
17441795
}
1796+
const normalizedOpacity = sanitizeDanmuOpacity(danmuSettings.opacity);
1797+
const targetOpacity = isDanmuEnabled.value ? normalizedOpacity : 0;
17451798
try {
17461799
if (isDanmuEnabled.value) {
17471800
overlay.play?.();
1748-
overlay.setOpacity?.(1);
17491801
overlay.show?.('scroll');
17501802
overlay.show?.('top');
17511803
overlay.show?.('bottom');
17521804
} else {
17531805
overlay.pause?.();
1754-
overlay.setOpacity?.(0);
17551806
}
1807+
overlay.setOpacity?.(targetOpacity);
1808+
const host = playerInstance.value?.root?.querySelector('.player-danmu-overlay') as HTMLElement | null;
1809+
host?.style.setProperty('--danmu-opacity', String(targetOpacity));
17561810
} catch (error) {
17571811
console.warn('[Player] Failed updating danmu enabled state:', error);
17581812
}
@@ -1967,6 +2021,7 @@ async function mountXgPlayer(
19672021
duration: danmuSettings.duration,
19682022
area: danmuSettings.area,
19692023
mode: danmuSettings.mode,
2024+
opacity: danmuSettings.opacity,
19702025
}),
19712026
onChange: (partial: Partial<DanmuUserSettings>) => {
19722027
if (partial.color) {
@@ -1987,6 +2042,9 @@ async function mountXgPlayer(
19872042
if (partial.mode) {
19882043
danmuSettings.mode = partial.mode;
19892044
}
2045+
if (typeof partial.opacity === 'number') {
2046+
danmuSettings.opacity = sanitizeDanmuOpacity(partial.opacity);
2047+
}
19902048
},
19912049
}) as DanmuSettingsControl;
19922050
@@ -2480,6 +2538,7 @@ const getDanmuSettingsSnapshot = (): DanmuUserSettings => ({
24802538
duration: danmuSettings.duration,
24812539
area: sanitizeDanmuArea(danmuSettings.area),
24822540
mode: danmuSettings.mode,
2541+
opacity: sanitizeDanmuOpacity(danmuSettings.opacity),
24832542
});
24842543
24852544
const persistCurrentDanmuPreferences = () => {
@@ -2562,6 +2621,7 @@ watch(danmuSettingsPlugin, (plugin) => {
25622621
duration: danmuSettings.duration,
25632622
area: sanitizeDanmuArea(danmuSettings.area),
25642623
mode: danmuSettings.mode,
2624+
opacity: sanitizeDanmuOpacity(danmuSettings.opacity),
25652625
});
25662626
});
25672627
@@ -2599,6 +2659,17 @@ watch(() => danmuSettings.area, (area) => {
25992659
persistCurrentDanmuPreferences();
26002660
});
26012661
2662+
watch(() => danmuSettings.opacity, (opacity) => {
2663+
const normalizedOpacity = sanitizeDanmuOpacity(opacity);
2664+
if (normalizedOpacity !== opacity) {
2665+
danmuSettings.opacity = normalizedOpacity;
2666+
return;
2667+
}
2668+
danmuSettingsPlugin.value?.setSettings({ opacity: normalizedOpacity });
2669+
applyDanmuOverlayPreferences(danmuInstance.value);
2670+
persistCurrentDanmuPreferences();
2671+
});
2672+
26022673
watch(danmuInstance, (instance) => {
26032674
applyDanmuOverlayPreferences(instance);
26042675
syncDanmuEnabledState(instance);

0 commit comments

Comments
 (0)