ektoplayer 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|