ektoplayer 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/README.md +49 -0
- data/bin/ektoplayer +7 -0
- data/lib/ektoplayer.rb +10 -0
- data/lib/ektoplayer/application.rb +148 -0
- data/lib/ektoplayer/bindings.rb +230 -0
- data/lib/ektoplayer/browsepage.rb +138 -0
- data/lib/ektoplayer/client.rb +18 -0
- data/lib/ektoplayer/common.rb +91 -0
- data/lib/ektoplayer/config.rb +247 -0
- data/lib/ektoplayer/controllers/browser.rb +47 -0
- data/lib/ektoplayer/controllers/controller.rb +9 -0
- data/lib/ektoplayer/controllers/help.rb +21 -0
- data/lib/ektoplayer/controllers/info.rb +22 -0
- data/lib/ektoplayer/controllers/mainwindow.rb +40 -0
- data/lib/ektoplayer/controllers/playlist.rb +60 -0
- data/lib/ektoplayer/database.rb +199 -0
- data/lib/ektoplayer/events.rb +56 -0
- data/lib/ektoplayer/models/browser.rb +127 -0
- data/lib/ektoplayer/models/database.rb +49 -0
- data/lib/ektoplayer/models/model.rb +15 -0
- data/lib/ektoplayer/models/player.rb +28 -0
- data/lib/ektoplayer/models/playlist.rb +72 -0
- data/lib/ektoplayer/models/search.rb +42 -0
- data/lib/ektoplayer/models/trackloader.rb +17 -0
- data/lib/ektoplayer/mp3player.rb +151 -0
- data/lib/ektoplayer/operations/browser.rb +19 -0
- data/lib/ektoplayer/operations/operations.rb +26 -0
- data/lib/ektoplayer/operations/player.rb +11 -0
- data/lib/ektoplayer/operations/playlist.rb +67 -0
- data/lib/ektoplayer/theme.rb +102 -0
- data/lib/ektoplayer/trackloader.rb +146 -0
- data/lib/ektoplayer/ui.rb +404 -0
- data/lib/ektoplayer/ui/colors.rb +105 -0
- data/lib/ektoplayer/ui/widgets.rb +195 -0
- data/lib/ektoplayer/ui/widgets/container.rb +125 -0
- data/lib/ektoplayer/ui/widgets/labelwidget.rb +43 -0
- data/lib/ektoplayer/ui/widgets/listwidget.rb +332 -0
- data/lib/ektoplayer/ui/widgets/tabbedcontainer.rb +110 -0
- data/lib/ektoplayer/updater.rb +77 -0
- data/lib/ektoplayer/views/browser.rb +25 -0
- data/lib/ektoplayer/views/help.rb +46 -0
- data/lib/ektoplayer/views/info.rb +208 -0
- data/lib/ektoplayer/views/mainwindow.rb +64 -0
- data/lib/ektoplayer/views/playinginfo.rb +135 -0
- data/lib/ektoplayer/views/playlist.rb +39 -0
- data/lib/ektoplayer/views/progressbar.rb +51 -0
- data/lib/ektoplayer/views/splash.rb +99 -0
- data/lib/ektoplayer/views/trackrenderer.rb +137 -0
- data/lib/ektoplayer/views/volumemeter.rb +74 -0
- metadata +164 -0
@@ -0,0 +1,105 @@
|
|
1
|
+
require 'curses'
|
2
|
+
|
3
|
+
module UI
|
4
|
+
class ColorFader
|
5
|
+
def initialize(colors)
|
6
|
+
@colors = colors.map { |attrs| UI::Colors.set(nil, *attrs) }
|
7
|
+
end
|
8
|
+
|
9
|
+
def fade(size) ColorFader._fade(@colors, size) end
|
10
|
+
def fade2(size) ColorFader._fade2(@colors, size) end
|
11
|
+
|
12
|
+
def ColorFader._fade(colors, size)
|
13
|
+
return [] if size < 1
|
14
|
+
|
15
|
+
part_len = (size / colors.size)
|
16
|
+
diff = size - part_len * colors.size
|
17
|
+
|
18
|
+
(colors.size - 1).times.map do |color_i|
|
19
|
+
[colors[color_i]] * part_len
|
20
|
+
end.flatten.concat( [colors[-1]] * (part_len + diff) )
|
21
|
+
end
|
22
|
+
|
23
|
+
def ColorFader._fade2(colors, size)
|
24
|
+
half = size / 2
|
25
|
+
ColorFader._fade(colors, half) + ColorFader._fade(colors, size - half).reverse
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
class Colors
|
30
|
+
COLORS = {
|
31
|
+
none: -1, default: -1, nil => -1,
|
32
|
+
white: Curses::COLOR_WHITE,
|
33
|
+
black: Curses::COLOR_BLACK,
|
34
|
+
red: Curses::COLOR_RED,
|
35
|
+
blue: Curses::COLOR_BLUE,
|
36
|
+
cyan: Curses::COLOR_CYAN,
|
37
|
+
green: Curses::COLOR_GREEN,
|
38
|
+
yellow: Curses::COLOR_YELLOW,
|
39
|
+
magenta: Curses::COLOR_MAGENTA
|
40
|
+
}
|
41
|
+
COLORS.default_proc = proc do |h, key|
|
42
|
+
fail "Unknown color #{key}" unless key.is_a?Integer
|
43
|
+
key
|
44
|
+
end
|
45
|
+
COLORS.freeze
|
46
|
+
|
47
|
+
ATTRIBUTES = {
|
48
|
+
bold: Curses::A_BOLD, standout: Curses::A_STANDOUT,
|
49
|
+
blink: Curses::A_BLINK, underline: Curses::A_UNDERLINE
|
50
|
+
}
|
51
|
+
ATTRIBUTES.default_proc = proc { |h,k| k }
|
52
|
+
ATTRIBUTES.freeze
|
53
|
+
|
54
|
+
def self.start
|
55
|
+
@@id ||= 1
|
56
|
+
@@aliases ||= {}
|
57
|
+
@@volatile ||= {}
|
58
|
+
@@volatile_ids ||= {}
|
59
|
+
@@cached ||= Hash.new { |h,k| h[k] = {} }
|
60
|
+
end
|
61
|
+
def self.reset; self.start end
|
62
|
+
|
63
|
+
def self.init_pair_cached(fg, bg)
|
64
|
+
fg, bg = COLORS[fg], COLORS[bg]
|
65
|
+
|
66
|
+
unless id = @@cached[fg][bg]
|
67
|
+
id = @@cached[fg][bg] = @@id
|
68
|
+
@@id += 1
|
69
|
+
end
|
70
|
+
|
71
|
+
Curses.init_pair(id, fg, bg) or fail
|
72
|
+
Curses.color_pair(id)
|
73
|
+
end
|
74
|
+
|
75
|
+
def self.add_attributes(*attrs)
|
76
|
+
flags = 0
|
77
|
+
attrs.each { |attr| flags |= ATTRIBUTES[attr] }
|
78
|
+
flags
|
79
|
+
end
|
80
|
+
|
81
|
+
def self.[](name) @@aliases[name] || 0 end
|
82
|
+
def self.get(name) @@aliases[name] || 0 end
|
83
|
+
|
84
|
+
def self.set(name, fg, bg = -1, *attrs)
|
85
|
+
@@aliases[name] = self.init_pair_cached(fg, bg)
|
86
|
+
attrs.each { |attr| @@aliases[name] |= ATTRIBUTES[attr] }
|
87
|
+
@@aliases[name]
|
88
|
+
end
|
89
|
+
|
90
|
+
def self.get_volatile(name)
|
91
|
+
@@volatile[name] || 0
|
92
|
+
end
|
93
|
+
|
94
|
+
def self.set_volatile(name, fg, bg)
|
95
|
+
fg, bg = COLORS[fg], COLORS[bg]
|
96
|
+
|
97
|
+
unless id = @@volatile_ids[name]
|
98
|
+
id = @@volatile_ids[name] = @@id
|
99
|
+
@@id += 1
|
100
|
+
end
|
101
|
+
|
102
|
+
@@volatile[name] = Curses.init_pair(id, fg, bg)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,195 @@
|
|
1
|
+
module UI
|
2
|
+
class Widget
|
3
|
+
WANT_REFRESH, WANT_REDRAW, WANT_LAYOUT = 1, 2, 4
|
4
|
+
|
5
|
+
attr_reader :pos, :size
|
6
|
+
def keys; @keys ||= Events.new end
|
7
|
+
def events; @events ||= Events.new end
|
8
|
+
def mouse; @mouse ||= MouseEvents.new end
|
9
|
+
def mouse_section; @mouse_section ||= MouseSectionEvents.new end
|
10
|
+
|
11
|
+
def initialize(parent: nil, size: nil, pos: nil, visible: true)
|
12
|
+
if !parent and (!size or !pos)
|
13
|
+
fail ArgumentError, "must provide 'size:' and 'pos:' if 'parent:' is nil"
|
14
|
+
end
|
15
|
+
|
16
|
+
@parent, @visible, = parent, visible
|
17
|
+
@size = (size or @parent.size.dup)
|
18
|
+
@pos = (pos or @parent.pos.dup)
|
19
|
+
@want, @lock = WANT_LAYOUT, Monitor.new
|
20
|
+
end
|
21
|
+
|
22
|
+
# Proxy method for creating a new widget object with the current
|
23
|
+
# object as parent.
|
24
|
+
def sub(class_type, **opts)
|
25
|
+
class_type.new(parent: self, **opts)
|
26
|
+
end
|
27
|
+
|
28
|
+
# This method should be used each time a widget may modify
|
29
|
+
# its window contents.
|
30
|
+
#
|
31
|
+
# It ensures that operations that modify the window (such
|
32
|
+
# as draw, layout and refresh) are executed once and only once at the
|
33
|
+
# end of this function.
|
34
|
+
def with_lock
|
35
|
+
held_locks = lock; yield
|
36
|
+
ensure
|
37
|
+
unlock
|
38
|
+
end
|
39
|
+
|
40
|
+
def lock
|
41
|
+
@lock.enter
|
42
|
+
end
|
43
|
+
|
44
|
+
def unlock
|
45
|
+
return unless (@lock.exit rescue nil) or not visible?
|
46
|
+
|
47
|
+
layout if @want >= WANT_LAYOUT
|
48
|
+
draw if @want >= WANT_REDRAW
|
49
|
+
if @want >= WANT_REFRESH
|
50
|
+
Canvas.update_screen
|
51
|
+
end
|
52
|
+
|
53
|
+
@want = 0
|
54
|
+
end
|
55
|
+
|
56
|
+
def display(force_refresh=false, force_redraw=false)
|
57
|
+
return if not visible?
|
58
|
+
layout if @want >= WANT_LAYOUT
|
59
|
+
draw if @want >= WANT_REDRAW or force_redraw
|
60
|
+
refresh if @want >= WANT_REFRESH or force_refresh
|
61
|
+
end
|
62
|
+
|
63
|
+
def want_redraw; @want |= WANT_REDRAW end
|
64
|
+
def want_layout; @want |= WANT_LAYOUT end
|
65
|
+
def want_refresh; @want |= WANT_REFRESH end
|
66
|
+
|
67
|
+
def invisible?; !visible? end
|
68
|
+
def visible!; self.visible=(true) end
|
69
|
+
def invisible!; self.visible=(false) end
|
70
|
+
def visible?; @visible and (!@parent or @parent.visible?) end
|
71
|
+
|
72
|
+
def visible=(new)
|
73
|
+
return if @visible == new
|
74
|
+
with_lock { @visible = new; want_refresh }
|
75
|
+
end
|
76
|
+
|
77
|
+
def size=(size)
|
78
|
+
return if @size == size
|
79
|
+
with_lock { @size = size; want_layout }
|
80
|
+
end
|
81
|
+
|
82
|
+
def pos=(pos)
|
83
|
+
return if @pos == pos
|
84
|
+
with_lock { @pos = pos; want_layout }
|
85
|
+
end
|
86
|
+
|
87
|
+
def mouse_event_transform(mevent)
|
88
|
+
if mevent.y >= @pos.y and mevent.x >= @pos.x and
|
89
|
+
mevent.y < (@pos.y + @size.height) and
|
90
|
+
mevent.x < (@pos.x + @size.width)
|
91
|
+
new_mouse = mevent.to_fake
|
92
|
+
new_mouse.update!(y: mevent.y - @pos.y, x: mevent.x - @pos.x)
|
93
|
+
new_mouse
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
def key_press(key) on_key_press(key) end
|
98
|
+
def raise_widget(widget) on_widget_raise(widget) end
|
99
|
+
def on_key_press(key) trigger(@keys, key) end
|
100
|
+
|
101
|
+
def mouse_click(mevent)
|
102
|
+
if new_event = mouse_event_transform(mevent)
|
103
|
+
trigger(@mouse, new_event)
|
104
|
+
trigger(@mouse_event, new_event)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
def draw; fail NotImplementedError end
|
109
|
+
def refresh; fail NotImplementedError end
|
110
|
+
def layout; fail NotImplementedError end
|
111
|
+
|
112
|
+
protected def trigger(event_obj, event_name, *event_args)
|
113
|
+
event_obj.trigger(event_name, *event_args) if event_obj
|
114
|
+
end
|
115
|
+
|
116
|
+
def on_widget_raise(widget)
|
117
|
+
fail 'unhandled widget raise' unless @parent
|
118
|
+
@parent.raise_widget(widget)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
class Window < Widget
|
123
|
+
attr_reader :win
|
124
|
+
|
125
|
+
def initialize(**opts)
|
126
|
+
super(**opts)
|
127
|
+
@win = Curses::Window.new(@size.height, @size.width, @pos.y, @pos.x)
|
128
|
+
@win.keypad=(true)
|
129
|
+
end
|
130
|
+
|
131
|
+
def layout
|
132
|
+
fail WidgetSizeError if @size.height < 1 or @size.width < 1
|
133
|
+
@win.size=(@size)
|
134
|
+
@win.pos=(@pos)
|
135
|
+
end
|
136
|
+
|
137
|
+
def refresh
|
138
|
+
@win.noutrefresh
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
class Pad < Widget
|
143
|
+
attr_reader :win
|
144
|
+
|
145
|
+
def initialize(**opts)
|
146
|
+
super(**opts)
|
147
|
+
@win = Curses::Pad.new(@size.height, @size.width)
|
148
|
+
@pad_minrow = @pad_mincol = 0
|
149
|
+
end
|
150
|
+
|
151
|
+
def pad_minrow=(n)
|
152
|
+
return if @pad_minrow == n
|
153
|
+
with_lock { @pad_minrow = n; want_refresh }
|
154
|
+
end
|
155
|
+
|
156
|
+
def pad_mincol=(n)
|
157
|
+
return if @pad_mincol == n
|
158
|
+
with_lock { @pad_mincol = n; want_refresh }
|
159
|
+
end
|
160
|
+
|
161
|
+
def pad_size=(s)
|
162
|
+
@win.size=(s)
|
163
|
+
end
|
164
|
+
|
165
|
+
def layout
|
166
|
+
@win.pos=(@pos)
|
167
|
+
end
|
168
|
+
|
169
|
+
def top; self.pad_minrow=(0) end
|
170
|
+
def page_up; self.up(@size.height / 2) end
|
171
|
+
def page_down; self.down(@size.height / 2) end
|
172
|
+
|
173
|
+
def bottom
|
174
|
+
self.pad_minrow=(@win.height - @size.height)
|
175
|
+
end
|
176
|
+
|
177
|
+
def up(n=1)
|
178
|
+
new_minrow = (@pad_minrow - n).clamp(0, @win.height)
|
179
|
+
self.pad_minrow=(new_minrow)
|
180
|
+
end
|
181
|
+
|
182
|
+
def down(n=1)
|
183
|
+
new_minrow = (@pad_minrow + n).clamp(0, (@win.height - @size.height)) rescue 0
|
184
|
+
self.pad_minrow=(new_minrow)
|
185
|
+
end
|
186
|
+
|
187
|
+
def refresh
|
188
|
+
@win.noutrefresh(
|
189
|
+
@pad_minrow, @pad_mincol,
|
190
|
+
@pos.y, @pos.x,
|
191
|
+
@pos.y + @size.height - 1, @pos.x + @size.width - 1
|
192
|
+
)
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
@@ -0,0 +1,125 @@
|
|
1
|
+
require_relative '../widgets'
|
2
|
+
|
3
|
+
module UI
|
4
|
+
class GenericContainer < Widget
|
5
|
+
attr_reader :selected, :selected_index, :widgets
|
6
|
+
|
7
|
+
def initialize(widgets: [], **opts)
|
8
|
+
super(**opts)
|
9
|
+
@selected, @selected_index, @widgets = nil, nil, widgets
|
10
|
+
end
|
11
|
+
|
12
|
+
def visible_widgets
|
13
|
+
@widgets.select(&:visible?)
|
14
|
+
end
|
15
|
+
|
16
|
+
def selected_index=(index)
|
17
|
+
return if @selected_index == index
|
18
|
+
|
19
|
+
with_lock do
|
20
|
+
if index
|
21
|
+
unless @selected = @widgets[index]
|
22
|
+
fail KeyError, "#{self.class}: #{@widgets.size} #{index}"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
@selected_index = index
|
27
|
+
want_layout
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def selected=(widget)
|
32
|
+
return if @selected.equal?(widget)
|
33
|
+
|
34
|
+
with_lock do
|
35
|
+
if widget
|
36
|
+
unless @selected_index = @widgets.index(widget)
|
37
|
+
fail KeyError
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
@selected = widget
|
42
|
+
want_layout
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def win
|
47
|
+
(@selected or UI::Canvas).win
|
48
|
+
end
|
49
|
+
|
50
|
+
def add(widget)
|
51
|
+
with_lock do
|
52
|
+
@widgets << widget
|
53
|
+
self.selected=(widget) unless @selected
|
54
|
+
want_layout # important: layout, not redraw
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def remove(widget)
|
59
|
+
with_lock do
|
60
|
+
self.selected=(nil) if @selected.equal?(widget)
|
61
|
+
@widgets.delete widget
|
62
|
+
want_layout # important: layout, not redraw
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def mouse_click(mevent)
|
67
|
+
visible_widgets.each { |w| w.mouse_click(mevent) }
|
68
|
+
super(mevent)
|
69
|
+
end
|
70
|
+
|
71
|
+
def draw; visible_widgets.each(&:draw) end
|
72
|
+
def refresh; visible_widgets.each(&:refresh) end
|
73
|
+
def layout; visible_widgets.each(&:layout) end
|
74
|
+
|
75
|
+
def on_key_press(key)
|
76
|
+
@selected.key_press(key) if @selected
|
77
|
+
super(key)
|
78
|
+
end
|
79
|
+
|
80
|
+
def next
|
81
|
+
return unless @selected
|
82
|
+
self.selected_index=((@selected_index + 1) % @widgets.size)
|
83
|
+
end
|
84
|
+
|
85
|
+
def prev
|
86
|
+
return unless @selected
|
87
|
+
return self.selected_index=(@widgets.size - 1) if @selected_index == 0
|
88
|
+
self.selected_index=(@selected_index - 1)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
class HorizontalContainer < GenericContainer
|
93
|
+
def layout
|
94
|
+
xoff = 0
|
95
|
+
visible_widgets.each do |widget|
|
96
|
+
widget.with_lock do
|
97
|
+
fail WidgetSizeError if xoff + widget.size.width > @size.width
|
98
|
+
widget.size=(widget.size.update(height: @size.height))
|
99
|
+
widget.pos=(@pos.calc(x: xoff))
|
100
|
+
fail WidgetSizeError if widget.size.height > @size.height
|
101
|
+
xoff += (widget.size.width + (@pad or 0))
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
super
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
class VerticalContainer < GenericContainer
|
110
|
+
def layout
|
111
|
+
yoff = 0
|
112
|
+
visible_widgets.each do |widget|
|
113
|
+
widget.with_lock do
|
114
|
+
widget.size=(widget.size.update(width: @size.width))
|
115
|
+
widget.pos=(@pos.calc(y: yoff))
|
116
|
+
fail WidgetSizeError if widget.size.width > @size.width
|
117
|
+
fail WidgetSizeError if yoff + widget.size.height > @size.height
|
118
|
+
yoff += widget.size.height
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
super
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
require_relative '../widgets'
|
2
|
+
|
3
|
+
module UI
|
4
|
+
class LabelWidget < Window
|
5
|
+
attr_reader :text, :pad, :attributes
|
6
|
+
|
7
|
+
def initialize(text: '', attributes: 0, pad: {}, **opts)
|
8
|
+
super(**opts)
|
9
|
+
@text, @attributes, @pad = text.to_s, attributes, Hash.new(0)
|
10
|
+
@pad.update pad
|
11
|
+
end
|
12
|
+
|
13
|
+
def attributes=(new)
|
14
|
+
return if @attributes == new
|
15
|
+
with_lock { @attributes = new; want_redraw }
|
16
|
+
end
|
17
|
+
|
18
|
+
def text=(new)
|
19
|
+
return if @text == new
|
20
|
+
with_lock { @text = new.to_s; want_redraw }
|
21
|
+
end
|
22
|
+
|
23
|
+
def pad=(new)
|
24
|
+
return if @pad == new
|
25
|
+
with_lock { @pad.update!(new); want_redraw }
|
26
|
+
end
|
27
|
+
|
28
|
+
def draw
|
29
|
+
@win.erase
|
30
|
+
@text.split(?\n).each_with_index do |l, i|
|
31
|
+
@win.setpos(@pad[:top] + i, @pad[:left])
|
32
|
+
@win.attron(@attributes) { @win << l }
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def fit
|
37
|
+
self.size=(Size.new(
|
38
|
+
height: @pad[:top] + @pad[:bottom] + 1 + @text.count(?\n),
|
39
|
+
width: @pad[:left] + @pad[:right] + @text.split(?\n).max.size
|
40
|
+
))
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|