@@ -137,10 +137,13 @@ type DanmuUserSettings = {
137137 duration: number ;
138138 area: number ;
139139 mode: ' scroll' | ' top' | ' bottom' ;
140+ opacity: number ;
140141};
141142
142143const DANMU_PREFERENCES_STORAGE_KEY = ' dtv_danmu_preferences_v1' ;
143144const DANMU_AREA_OPTIONS = [0.25 , 0.5 , 0.75 ] as const ;
145+ const DANMU_OPACITY_MIN = 0.2 ;
146+ const DANMU_OPACITY_MAX = 1 ;
144147const PLAYER_VOLUME_STORAGE_KEY = ' dtv_player_volume_v1' ;
145148const DEFAULT_DANMU_FONT_FAMILY = ' "HarmonyOS Sans Bold", "HarmonyOS Sans", "PingFang SC", "Helvetica Neue", Arial, sans-serif' ;
146149const 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+
152162const 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
14641511const 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
24852544const 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+
26022673watch (danmuInstance , (instance ) => {
26032674 applyDanmuOverlayPreferences (instance );
26042675 syncDanmuEnabledState (instance );
0 commit comments