11using System . Windows ;
22using System . Windows . Controls ;
33using System . Windows . Interop ;
4- using System . Windows . Media ;
54using System . Windows . Media . Imaging ;
5+ using Musebase . Engine ;
66using Musebase . Windows . Services ;
77
88namespace Musebase . Windows ;
99
1010/// <summary>
11- /// 작업표시줄에 상주하는 작은 창. 오버레이가 숨겨져도(사용자 숨김·일시정지·가림방지)
12- /// 여기서 항상 되살릴 수 있는 손잡이 역할을 한다.
13- /// - 닫기(X)는 종료가 아니라 최소화(작업표시줄 상주). 실제 종료는 "종료" 버튼/트레이.
14- /// - 작업표시줄에서 창을 복원(클릭)하면 오버레이를 다시 보이게 한다(Revive).
15- /// - "오버레이 표시/숨김"·"설정"·"종료" 버튼 제공. 표시 상태는 트레이와 동기화.
11+ /// 미니창(작업표시줄 상주)이 트레이·오버레이 없이 기본 기능을 쓰도록 주입받는 동작 묶음.
12+ /// Program.cs의 트레이 핸들러와 <b>같은 로컬 함수</b>를 가리켜 중복 구현을 피한다.
13+ /// </summary>
14+ public sealed record MiniWindowActions (
15+ Func < bool > IsOverlayVisible ,
16+ Action < bool > SetOverlayVisible ,
17+ Action ReviveOverlay ,
18+ Action OpenSettings ,
19+ Action Exit ,
20+ // 재생 컨트롤
21+ Func < PlaybackControls > GetControls ,
22+ Func < bool > IsPlaying ,
23+ Action OnPrevious ,
24+ Action OnPlayPause ,
25+ Action OnNext ,
26+ // 가사 오프셋 (delta, null=리셋) / 현재값
27+ Action < double ? > AdjustOffset ,
28+ Func < double > GetOffset ,
29+ // 가사 기능
30+ Action OpenSearch ,
31+ Action OpenLyricsEditor ,
32+ Action MarkWrong ,
33+ Func < bool > HasLyrics ,
34+ // 닫기 → 트레이 옵션(설정에서 실시간 조회)
35+ Func < bool > CloseToTray ) ;
36+
37+ /// <summary>
38+ /// 작업표시줄에 상주하는 컨트롤 허브. 오버레이가 숨겨져도(사용자 숨김·일시정지·가림방지)
39+ /// 여기서 항상 되살릴 수 있는 손잡이 역할을 하며, 트레이 없이도 기본 기능을 쓸 수 있다.
40+ /// - 곡 제목/아티스트 + 가사 소스 상태 표시.
41+ /// - 재생 컨트롤(이전/재생·정지/다음), 오프셋 조정, 가사 검색/열기/틀린가사.
42+ /// - 오버레이 표시·설정·종료. 표시 상태·오프셋·컨트롤 활성은 트레이와 동기화.
43+ /// - 닫기(X): 옵션 꺼짐(기본)=최소화(작업표시줄 상주), 켜짐=Hide()로 트레이로 숨김(실제 종료 아님).
1644/// </summary>
1745public sealed class MiniWindow : Window
1846{
19- private readonly TextBlock _status ;
47+ private readonly MiniWindowActions _a ;
48+
49+ private readonly TextBlock _title ;
50+ private readonly TextBlock _artist ;
51+ private readonly TextBlock _source ;
52+ private readonly Button _prev ;
53+ private readonly Button _playPause ;
54+ private readonly Button _next ;
55+ private readonly Button _offsetMinus ;
56+ private readonly Button _offsetPlus ;
57+ private readonly Button _offsetReset ;
58+ private readonly TextBlock _offsetLabel ;
59+ private readonly Button _search ;
60+ private readonly Button _openLyrics ;
61+ private readonly Button _wrong ;
2062 private readonly Button _overlayToggle ;
21- private readonly Func < bool > _isOverlayVisible ;
22- private readonly Action < bool > _setOverlayVisible ;
23- private readonly Action _reviveOverlay ;
63+ private readonly Button _settings ;
64+ private readonly Button _exit ;
65+
2466 private bool _closingToExit ; // "종료" 경로에서만 실제 닫힘 허용
2567
26- public MiniWindow (
27- System . Drawing . Icon ? appIcon ,
28- Func < bool > isOverlayVisible ,
29- Action < bool > setOverlayVisible ,
30- Action reviveOverlay ,
31- Action openSettings ,
32- Action exit )
68+ public MiniWindow ( System . Drawing . Icon ? appIcon , MiniWindowActions actions )
3369 {
34- _isOverlayVisible = isOverlayVisible ;
35- _setOverlayVisible = setOverlayVisible ;
36- _reviveOverlay = reviveOverlay ;
70+ _a = actions ;
3771
3872 Title = Loc . T ( "mini.title" ) ;
39- Width = 320 ;
73+ Width = 360 ;
4074 SizeToContent = SizeToContent . Height ;
4175 ResizeMode = ResizeMode . CanMinimize ;
4276 ShowInTaskbar = true ;
@@ -52,72 +86,228 @@ public MiniWindow(
5286 catch { /* 아이콘 변환 실패는 무시(기본 아이콘) */ }
5387 }
5488
55- _status = new TextBlock
89+ // ---- 1) 곡 제목 + 아티스트 ----
90+ _title = new TextBlock
91+ {
92+ Text = Loc . T ( "mini.noTrack" ) ,
93+ FontSize = 18 ,
94+ FontWeight = FontWeights . SemiBold ,
95+ TextTrimming = TextTrimming . CharacterEllipsis ,
96+ } ;
97+ _artist = new TextBlock
98+ {
99+ FontSize = 12 ,
100+ Opacity = 0.6 ,
101+ TextTrimming = TextTrimming . CharacterEllipsis ,
102+ Margin = new Thickness ( 0 , 1 , 0 , 0 ) ,
103+ } ;
104+
105+ // ---- 2) 가사 소스/상태 ----
106+ _source = new TextBlock
56107 {
57108 Text = Loc . T ( "mini.status.idle" ) ,
58109 TextWrapping = TextWrapping . Wrap ,
59- Margin = new Thickness ( 0 , 0 , 0 , 12 ) ,
60- MinHeight = 34 ,
110+ Opacity = 0.85 ,
111+ Margin = new Thickness ( 0 , 8 , 0 , 12 ) ,
112+ MinHeight = 18 ,
61113 } ;
62114
63- _overlayToggle = new Button
115+ // ---- 3) 재생 컨트롤 ----
116+ _prev = MediaButton ( ( ) => _a . OnPrevious ( ) ) ;
117+ _playPause = MediaButton ( ( ) => _a . OnPlayPause ( ) ) ;
118+ _next = MediaButton ( ( ) => _a . OnNext ( ) ) ;
119+ var playbackRow = new StackPanel
64120 {
65- Padding = new Thickness ( 10 , 4 , 10 , 4 ) ,
66- Margin = new Thickness ( 0 , 0 , 8 , 0 ) ,
121+ Orientation = Orientation . Horizontal ,
122+ HorizontalAlignment = HorizontalAlignment . Center ,
123+ Margin = new Thickness ( 0 , 0 , 0 , 12 ) ,
67124 } ;
68- _overlayToggle . Click += ( _ , _ ) => _setOverlayVisible ( ! _isOverlayVisible ( ) ) ;
125+ playbackRow . Children . Add ( _prev ) ;
126+ playbackRow . Children . Add ( _playPause ) ;
127+ playbackRow . Children . Add ( _next ) ;
69128
70- var settingsButton = new Button { Content = Loc . T ( "mini.settings" ) , Padding = new Thickness ( 10 , 4 , 10 , 4 ) , Margin = new Thickness ( 0 , 0 , 8 , 0 ) } ;
71- settingsButton . Click += ( _ , _ ) => openSettings ( ) ;
129+ // ---- 4) 가사 오프셋 조정 ----
130+ _offsetMinus = SmallButton ( ( ) => _a . AdjustOffset ( - 0.5 ) ) ;
131+ _offsetPlus = SmallButton ( ( ) => _a . AdjustOffset ( 0.5 ) ) ;
132+ _offsetReset = SmallButton ( ( ) => _a . AdjustOffset ( null ) ) ;
133+ _offsetLabel = new TextBlock
134+ {
135+ VerticalAlignment = VerticalAlignment . Center ,
136+ Margin = new Thickness ( 8 , 0 , 0 , 0 ) ,
137+ Opacity = 0.85 ,
138+ } ;
139+ var offsetRow = new StackPanel
140+ {
141+ Orientation = Orientation . Horizontal ,
142+ Margin = new Thickness ( 0 , 0 , 0 , 10 ) ,
143+ } ;
144+ offsetRow . Children . Add ( _offsetMinus ) ;
145+ offsetRow . Children . Add ( _offsetPlus ) ;
146+ offsetRow . Children . Add ( _offsetReset ) ;
147+ offsetRow . Children . Add ( _offsetLabel ) ;
72148
73- var exitButton = new Button { Content = Loc . T ( "mini.exit" ) , Padding = new Thickness ( 10 , 4 , 10 , 4 ) } ;
74- exitButton . Click += ( _ , _ ) => { _closingToExit = true ; exit ( ) ; } ;
149+ // ---- 5) 가사 기능 ----
150+ _search = FeatureButton ( ( ) => _a . OpenSearch ( ) ) ;
151+ _openLyrics = FeatureButton ( ( ) => _a . OpenLyricsEditor ( ) ) ;
152+ _wrong = FeatureButton ( ( ) => _a . MarkWrong ( ) ) ;
153+ var lyricsRow = new StackPanel
154+ {
155+ Orientation = Orientation . Horizontal ,
156+ Margin = new Thickness ( 0 , 0 , 0 , 12 ) ,
157+ } ;
158+ lyricsRow . Children . Add ( _search ) ;
159+ lyricsRow . Children . Add ( _openLyrics ) ;
160+ lyricsRow . Children . Add ( _wrong ) ;
75161
76- var buttonRow = new StackPanel { Orientation = Orientation . Horizontal } ;
77- buttonRow . Children . Add ( _overlayToggle ) ;
78- buttonRow . Children . Add ( settingsButton ) ;
79- buttonRow . Children . Add ( exitButton ) ;
162+ // ---- 6) 오버레이/설정/종료 ----
163+ _overlayToggle = FeatureButton ( ( ) => _a . SetOverlayVisible ( ! _a . IsOverlayVisible ( ) ) ) ;
164+ _settings = FeatureButton ( ( ) => _a . OpenSettings ( ) ) ;
165+ _exit = FeatureButton ( ( ) => { _closingToExit = true ; _a . Exit ( ) ; } ) ;
166+ var controlRow = new StackPanel { Orientation = Orientation . Horizontal } ;
167+ controlRow . Children . Add ( _overlayToggle ) ;
168+ controlRow . Children . Add ( _settings ) ;
169+ controlRow . Children . Add ( _exit ) ;
80170
81171 var root = new StackPanel { Margin = new Thickness ( 16 ) } ;
82- root . Children . Add ( _status ) ;
83- root . Children . Add ( buttonRow ) ;
172+ root . Children . Add ( _title ) ;
173+ root . Children . Add ( _artist ) ;
174+ root . Children . Add ( _source ) ;
175+ root . Children . Add ( playbackRow ) ;
176+ root . Children . Add ( offsetRow ) ;
177+ root . Children . Add ( lyricsRow ) ;
178+ root . Children . Add ( controlRow ) ;
84179 Content = root ;
85180
86- SyncOverlayVisible ( _isOverlayVisible ( ) ) ;
181+ ApplyText ( ) ;
182+ SyncOverlayVisible ( _a . IsOverlayVisible ( ) ) ;
183+ RefreshPlayback ( ) ;
184+ RefreshOffset ( ) ;
185+ RefreshLyricsFeatures ( ) ;
87186
88- // 닫기(X) → 최소화(작업표시줄 상주). "종료" 버튼일 때만 실제 닫힘.
187+ // 닫기(X): 옵션 켜짐=트레이로 숨김(Hide), 꺼짐(기본)= 최소화(작업표시줄 상주). "종료"만 실제 닫힘.
89188 Closing += ( _ , e ) =>
90189 {
91190 if ( _closingToExit ) return ;
92191 e . Cancel = true ;
93- WindowState = WindowState . Minimized ;
192+ if ( _a . CloseToTray ( ) )
193+ Hide ( ) ; // 작업표시줄에서 사라지고 트레이로(트레이 더블클릭/제어판 열기로 복귀)
194+ else
195+ WindowState = WindowState . Minimized ;
94196 } ;
95197
96198 // 작업표시줄에서 복원(클릭) → 오버레이 되살리기.
97199 StateChanged += ( _ , _ ) =>
98200 {
99- if ( WindowState == WindowState . Normal ) _reviveOverlay ( ) ;
201+ if ( WindowState == WindowState . Normal ) _a . ReviveOverlay ( ) ;
100202 } ;
101203 Activated += ( _ , _ ) =>
102204 {
103- if ( WindowState != WindowState . Minimized ) _reviveOverlay ( ) ;
205+ if ( WindowState != WindowState . Minimized ) _a . ReviveOverlay ( ) ;
104206 } ;
105207
106208 Loc . CultureChanged += ApplyText ;
107209 Closed += ( _ , _ ) => Loc . CultureChanged -= ApplyText ;
108210 }
109211
110- /// <summary>현재 상태 한 줄을 갱신한다(코디네이터 상태/힌트).</summary>
111- public void SetStatus ( string text ) => _status . Text = text ;
212+ private static Button MediaButton ( Action onClick )
213+ {
214+ var b = new Button
215+ {
216+ FontSize = 16 ,
217+ Width = 44 ,
218+ Padding = new Thickness ( 0 , 4 , 0 , 4 ) ,
219+ Margin = new Thickness ( 0 , 0 , 6 , 0 ) ,
220+ } ;
221+ b . Click += ( _ , _ ) => onClick ( ) ;
222+ return b ;
223+ }
224+
225+ private static Button SmallButton ( Action onClick )
226+ {
227+ var b = new Button { Padding = new Thickness ( 10 , 4 , 10 , 4 ) , Margin = new Thickness ( 0 , 0 , 6 , 0 ) } ;
228+ b . Click += ( _ , _ ) => onClick ( ) ;
229+ return b ;
230+ }
231+
232+ private static Button FeatureButton ( Action onClick )
233+ {
234+ var b = new Button { Padding = new Thickness ( 10 , 4 , 10 , 4 ) , Margin = new Thickness ( 0 , 0 , 8 , 0 ) } ;
235+ b . Click += ( _ , _ ) => onClick ( ) ;
236+ return b ;
237+ }
238+
239+ /// <summary>가사 소스/상태 한 줄을 갱신한다(코디네이터 CurrentStatus 현지화 문자열).</summary>
240+ public void SetStatus ( string text ) => _source . Text = text ;
241+
242+ /// <summary>곡 제목/아티스트를 갱신한다. 곡 없으면 "재생 없음".</summary>
243+ public void SetTrack ( string ? title , string ? artist )
244+ {
245+ if ( string . IsNullOrWhiteSpace ( title ) )
246+ {
247+ _title . Text = Loc . T ( "mini.noTrack" ) ;
248+ _artist . Text = "" ;
249+ _artist . Visibility = Visibility . Collapsed ;
250+ }
251+ else
252+ {
253+ _title . Text = title ;
254+ _artist . Text = artist ?? "" ;
255+ _artist . Visibility = string . IsNullOrWhiteSpace ( artist ) ? Visibility . Collapsed : Visibility . Visible ;
256+ }
257+ }
112258
113259 /// <summary>오버레이 표시 상태에 맞춰 토글 버튼 라벨을 갱신한다(트레이와 동기화).</summary>
114260 public void SyncOverlayVisible ( bool visible ) =>
115261 _overlayToggle . Content = Loc . T ( visible ? "mini.hideOverlay" : "mini.showOverlay" ) ;
116262
263+ /// <summary>재생 상태·컨트롤 가용성에 맞춰 재생 컨트롤 행을 갱신한다.</summary>
264+ public void RefreshPlayback ( )
265+ {
266+ var c = _a . GetControls ( ) ;
267+ var playing = _a . IsPlaying ( ) ;
268+ _prev . Content = Loc . T ( "mini.control.prev" ) ;
269+ _next . Content = Loc . T ( "mini.control.next" ) ;
270+ _playPause . Content = Loc . T ( playing ? "mini.control.pause" : "mini.control.play" ) ;
271+ _prev . IsEnabled = c . CanPrevious ;
272+ _playPause . IsEnabled = c . CanPlayPause ;
273+ _next . IsEnabled = c . CanNext ;
274+ }
275+
276+ /// <summary>현재 오프셋 라벨을 갱신한다(트레이 라벨과 동일 포맷).</summary>
277+ public void RefreshOffset ( ) =>
278+ _offsetLabel . Text = Loc . T ( "mini.offset.label" , ( "value" , _a . GetOffset ( ) . ToString ( "+0.0;-0.0;0" ) ) ) ;
279+
280+ /// <summary>가사 유무에 따라 열기·틀린가사 버튼을 활성/비활성(트레이 editItem/wrongItem과 동기화).</summary>
281+ public void RefreshLyricsFeatures ( )
282+ {
283+ var hasLyrics = _a . HasLyrics ( ) ;
284+ _openLyrics . IsEnabled = hasLyrics ;
285+ _wrong . IsEnabled = hasLyrics ;
286+ }
287+
117288 private void ApplyText ( )
118289 {
119290 Title = Loc . T ( "mini.title" ) ;
120- SyncOverlayVisible ( _isOverlayVisible ( ) ) ;
291+ SyncOverlayVisible ( _a . IsOverlayVisible ( ) ) ;
292+ _settings . Content = Loc . T ( "mini.settings" ) ;
293+ _exit . Content = Loc . T ( "mini.exit" ) ;
294+ _search . Content = Loc . T ( "mini.search" ) ;
295+ _openLyrics . Content = Loc . T ( "mini.openLyrics" ) ;
296+ _wrong . Content = Loc . T ( "mini.wrong" ) ;
297+ _offsetMinus . Content = Loc . T ( "mini.offset.minus" ) ;
298+ _offsetPlus . Content = Loc . T ( "mini.offset.plus" ) ;
299+ _offsetReset . Content = Loc . T ( "mini.offset.reset" ) ;
300+ RefreshOffset ( ) ;
301+ RefreshPlayback ( ) ;
302+ }
303+
304+ /// <summary>트레이(더블클릭/제어판 열기)에서 미니창을 다시 앞으로 가져온다.</summary>
305+ public void ShowFromTray ( )
306+ {
307+ Show ( ) ;
308+ WindowState = WindowState . Normal ;
309+ Activate ( ) ;
310+ _a . ReviveOverlay ( ) ;
121311 }
122312
123313 /// <summary>종료 경로(트레이 종료 등)에서 실제 닫힘을 허용하고 창을 닫는다.</summary>
0 commit comments