-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathschemep3-panel-playlist-view.scm
425 lines (369 loc) · 15.5 KB
/
schemep3-panel-playlist-view.scm
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
#lang scheme/gui
(provide
playlist-view%
playback-queue-view%
)
(require scheme/date)
(require framework)
(require srfi/2)
(require srfi/26)
(require srfi/54)
(require (planet untyped/unlib/list))
;;; local modules
(require "schemep3-playback.scm")
(require "schemep3-database.scm")
(require "schemep3-playlist.scm")
(require "schemep3-dialog-playlist-search.scm")
(require "schemep3-context-menu-manager.scm")
(require "schemep3-context-menu.scm")
(require "schemep3-helpers.scm")
(require "schemep3-gui-helpers.scm")
(require "schemep3-mixins-gui.scm")
(define (database-index->filename-only file-index)
(path->string
(file-name-from-path
(schemep3-database-index->filename file-index))))
(define (database-index->containing-folder file-index)
(let ((filename (schemep3-database-index->filename file-index)))
(let ((explosion (explode-path filename)))
(path->string
(list-ref explosion (- (length explosion) 2))))))
(define (days-ago now then)
(- (date->julian/scalinger now)
(date->julian/scalinger then)))
(define (date->my-hour dt)
(let ([h (modulo (date-hour dt) 12)])
(if (zero? h) 12 h)))
(define (date->time dt)
(format "~A:~A~A"
(cat (date->my-hour dt) 2 #\space)
(cat (date-minute dt) 2 #\0)
(if (< (date-hour dt) 12) "am" "pm")))
(define (format-last-played lp)
(let ([lp-date (seconds->date lp)]
[now-date (seconds->date (current-seconds))])
(let ([n-days (days-ago now-date lp-date)])
(cond [(zero? n-days)
(date->time lp-date)]
[(= 1 n-days) "Yesterday"]
[(< n-days 7)
(format "~A days ago" n-days)]
[else (format-date lp-date)]))))
(define playlist-view-base%
(class list-box%
(define refresh-thread #f)
(define _current-mode 'active)
;;; integer or one of ('active 'playing 'queue)
(define/public (current-mode (new-mode #f))
(when (and new-mode (not (eq? new-mode _current-mode)))
(set! _current-mode new-mode)
(case _current-mode
((queue)
(local-set-to-queue))
(else
(local-set-contents (playlist-contents (current-list))))))
_current-mode)
(define/public (current-list)
(let ((mode (current-mode)))
(case mode
((queue) 'queue)
((active) (playlists-active))
((playing) (playlists-playing))
(else mode))))
(define/private (affects-us? playlist)
(let ((mode (current-mode)))
(or (eq? playlist mode)
(and (eq? mode 'active)
(= playlist (playlists-active)))
(and (eq? mode 'playing)
(= playlist (playlists-playing)))
(eq? (current-list) playlist))))
(define (clear-pending)
(thread-send-and-wait refresh-thread 'quit))
(define (wait-pending)
(and-let* ((t refresh-thread))
(thread-wait t)))
(define (format-column-item fmt file-index)
(let* ((len (playlist-format-item-length fmt))
(field (playlist-format-item-field fmt))
(field-value (schemep3-database-retrieve-field file-index field))
[str (cond
((and field-value (playlist-format-item-format-function fmt))
=> (cut <> field-value))
(field-value
(ensure-string field-value))
((playlist-format-item-default-function fmt)
=> (cut <> file-index))
(else ""))])
(padded-string
(gui-utils:trim-string str (abs len))
len)))
(define-struct playlist-format-item
(field length format-function default-function)
#:property prop:procedure format-column-item)
(define (make-column field length #:formatter (formatter #f) #:otherwise (otherwise #f))
(make-playlist-format-item field length formatter otherwise))
(define playlist-format-list
(list (make-column 'duration 5 #:formatter format-time)
(make-column 'rating 1)
(make-column 'artist -20)
(make-column 'title -30 #:otherwise database-index->filename-only)
(make-column 'album -30 #:otherwise database-index->containing-folder)
(make-column 'play_count 2)
(make-column 'last_played 10 #:formatter format-last-played)))
(define/private (display-string playlist-index file-index)
(let ([base-string
(string-join (map (cut <> file-index) playlist-format-list) " ")])
(decorate-if-playing
(decorate-if-queue base-string file-index)
playlist-index file-index)))
(define/private (local-clear)
(clear-pending)
(send this set (list)))
(define/private (local-add-item db-index)
(let* ([n (send this get-number)]
[string (display-string n db-index)])
(send this append string db-index)))
(define/private (non-threaded-set-contents file-indexes)
(send this set (for/list (((file-index n) (in-indexed file-indexes)))
(display-string n file-index)))
(let* ((element-count (send this get-number)))
(for (((f n) (in-indexed file-indexes)))
(send this set-data n f))))
(define/private (threaded-set-contents file-indexes)
(send this set (map number->string file-indexes))
(let* ((element-count (send this get-number)))
(for (((f n) (in-indexed file-indexes)))
(send this set-data n f))
(thread-set! refresh-thread
(lambda ()
(for/or ((n (in-range element-count)))
(refresh-item n)
(thread-try-receive))))))
(define/private (local-set-contents file-indexes)
(clear-pending)
(if (< (length file-indexes) 500)
(non-threaded-set-contents file-indexes)
(threaded-set-contents file-indexes)))
(define highlited-items (list))
(define/private (decorate-if-queue string file-index)
(if (playback-queue-member-file-index? file-index)
(string-append string " Q")
string))
(define/private (decorate-if-playing string playlist-index file-index)
(cond
[(and (eq? file-index (now-playing-database-index))
(eq? (playlists-playing) (current-list))
(eq? playlist-index (now-playing-playlist-index)))
(push! highlited-items playlist-index)
(regexp-replace* " " string "=")]
[(and (eq? file-index (now-playing-database-index)))
(push! highlited-items playlist-index)
(regexp-replace* " " string "-")]
[else string]))
(define/public (refresh-item playlist-index)
(and-let* (((< playlist-index (send this get-number)))
(file-index (send this get-data playlist-index))
(string (display-string playlist-index file-index)))
(send this set-string playlist-index string)))
(define/private (local-set-to-queue)
(local-set-contents
(map third (playback-queue-contents))))
(define/public (ensure-visible n)
(let ((i (send this get-first-visible-item))
(j (send this number-of-visible-items)))
(cond
((>= i n)
(send this set-first-visible-item n))
((>= n (+ i j))
(send this set-first-visible-item (+ (- n j) 1))))))
(define/public (index-from-position x y)
(let ((n (send this number-of-visible-items))
(offset (send this get-first-visible-item))
(height (send this get-height)))
(let ((item-height (/ height n)))
(+ offset (floor (/ y item-height))))))
(define keymap
`((#\5 . ,(cut context-menu-rate-5 (get-selection-data)))
(#\4 . ,(cut context-menu-rate-4 (get-selection-data)))
(#\3 . ,(cut context-menu-rate-3 (get-selection-data)))
(#\2 . ,(cut context-menu-rate-2 (get-selection-data)))
(#\1 . ,(cut context-menu-rate-1 (get-selection-data)))
(#\n . ,next)
(#\N . ,next)
(#\f . ,show-playlist-search)
(#\space . ,toggle-pause)))
(define alt-keymap
`((up . ,(cut move-selected-playlist-items-up (playlist-selected-playlist-indexes)))
(down . ,(cut move-selected-playlist-items-down (playlist-selected-playlist-indexes)))))
(define (call-and-return-true fn)
(fn)
#t)
(define/override (on-subwindow-char receiver event)
(let ((keycode (send event get-key-code)))
(cond
[(assoc-value/default keycode keymap #f) => call-and-return-true]
[(and
(send event get-meta-down)
(assoc-value/default keycode alt-keymap #f)) => call-and-return-true]
[else (super on-subwindow-char receiver event)])))
(define/private (get-selection-data)
(map (cut send this get-data <>)
(send this get-selections)))
(define (item-context-menu x y)
(when (<= (length (send this get-selections)) 1)
(let ((p-index (send this index-from-position x y)))
(when (< p-index (send this get-number))
(send this set-selection p-index)
(playlist-select p-index))))
(let ((pm (new popup-menu%)))
(if (eq? (current-mode) 'queue)
(generate-context-menu pm (get-selection-data))
(generate-context-menu pm))
(send this popup-menu pm x y)))
(define (playlist-selector-context-menu x y)
(let ((m (new popup-menu%)))
(new checkable-menu-item%
(parent m)
(label "<Active>")
(callback (lambda (m e) (current-mode 'active)))
(checked (eq? (current-mode) 'active)))
(new checkable-menu-item%
(parent m)
(label "<Playing>")
(callback (lambda (m e) (current-mode 'playing)))
(checked (eq? (current-mode) 'playing)))
(new checkable-menu-item%
(parent m)
(label "<Queue>")
(callback (lambda (m e) (current-mode 'queue)))
(checked (eq? (current-mode) 'queue)))
(new separator-menu-item% (parent m))
(for ((n (in-range (playlists-count))))
(new checkable-menu-item%
[parent m]
[label (format "~A - ~A" n (playlists-name n))]
[callback (lambda (m e) (current-mode n))]
[checked (eq? (current-mode) n)]))
(send this popup-menu m x y)))
(define/override (on-subwindow-event receiver event)
(case (send event get-event-type)
((right-down)
(item-context-menu (send event get-x) (send event get-y))
#t)
((middle-down)
(playlist-selector-context-menu (send event get-x) (send event get-y))
#t)
(else (super on-subwindow-event receiver event))))
(define/private (find-item file-index)
(for/list ((n (in-range (send this get-number)))
#:when (and-let* ((z (send this get-data n)))
(= z file-index)))
n))
(define/private (clear-selection)
(for ((selected-index (send this get-selections)))
(send this select selected-index #f)))
(define/private (set-selection-list new-selections)
(let ((current-selections (send this get-selections)))
(unless (equal? current-selections new-selections)
(clear-selection)
(for ((new-selection new-selections))
(send this select new-selection #t)))))
(define/public (on-double-click)
(stop)
(playback-queue-add (send this get-selections) #t (current-list))
(play))
(super-new
(label #f)
(choices (list))
(style '(multiple))
(callback
(lambda (lb e)
(unless (eq? (current-mode) 'queue)
(playlist-select
(send this get-selections)))
(cond
((equal? (send e get-event-type) 'list-box-dclick)
(send lb on-double-click)))))
(font (get-medium-mono-font)))
(exit:insert-on-callback-with-status clear-pending "playlist-view::clear-pending")
(define (refresh-queue-items Q)
(let ([our-list (current-list)])
(for ((queue-item Q)
#:when (= our-list (second queue-item)))
(send this refresh-item (first queue-item)))))
(playlist-add-hook
(match-lambda*
((list 'add items playlist)
(when (affects-us? playlist)
(for ((item items))
(local-add-item item))))
((list 'remove index playlist)
(when (affects-us? playlist)
(wait-pending)
(send this delete index)))
((list 'clear playlist)
(when (affects-us? playlist)
(local-clear)))
((list 'set-item index value playlist)
(when (affects-us? playlist)
(send this set-data index value)
(send this refresh-item index)))
((list 'set playlist)
(when (affects-us? playlist)
(local-set-contents (playlist-contents playlist))))
((list 'playback-queue-clear previous-queue)
(if (eq? (current-mode) 'queue)
(local-clear)
(refresh-queue-items previous-queue)))
((list 'playback-queue-add)
(if (eq? (current-mode) 'queue)
(local-set-to-queue)
(refresh-queue-items (playback-queue-contents))))
((list 'select items)
(when (affects-us? (playlists-active))
(set-selection-list items)))
;; ((list 'playlists-delete index)
;; (cond
;; [(eq? (current-mode) 'queue) #t]
;; [(or
;; (eq? (current-mode) 'active)
;; (eq? (current-mode) 'playing))
;; (local-set-contents (playlist-contents (current-list)))]
;; [(>= index (current-mode))
;; (current-mode (sub1 index))]))
((list 'playlists-set-playing p-index)
(when (eq? (current-mode) 'playing)
(local-set-contents (playlist-contents p-index))))
((list 'playlists-set-active p-index)
(when (eq? (current-mode) 'active)
(local-set-contents (playlist-contents p-index))))
((list 'show n p)
(when (eq? (current-list) p)
(send this ensure-visible n)))
(_ (void))))
(schemep3-database-add-update-hook
(lambda (file-index)
(for ((n (find-item file-index)))
(refresh-item n))))
(add-pre-play-hook
(lambda (index item)
(when (affects-us? (playlists-playing))
(refresh-item index))))
(add-post-play-hook
(lambda x
(let ([refresh-items highlited-items])
(set! highlited-items (list))
(for ((item refresh-items))
(refresh-item item)))))
(add-post-play-hook
(lambda x
(when (eq? (current-mode) 'queue)
(local-set-to-queue))))))
(define playlist-view%
(class (checkable-panel-mixin playlist-view-base% "Playlist")
(super-new)))
(define playback-queue-view%
(class (checkable-panel-mixin playlist-view-base% "Queue Viewer")
(super-new)
(send this current-mode 'queue)))