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,25 @@
|
|
1
|
+
require_relative '../ui/widgets'
|
2
|
+
require_relative '../config'
|
3
|
+
|
4
|
+
module Ektoplayer
|
5
|
+
module Views
|
6
|
+
class Browser < UI::ListWidget
|
7
|
+
def initialize(**opts)
|
8
|
+
super(**opts)
|
9
|
+
self.item_renderer=(TrackRenderer.new(
|
10
|
+
width: @size.width, format: Config[:'browser.format']))
|
11
|
+
end
|
12
|
+
|
13
|
+
def attach(browser)
|
14
|
+
@browser = browser
|
15
|
+
browser.events.on(:changed, &self.method(:reload))
|
16
|
+
reload
|
17
|
+
end
|
18
|
+
|
19
|
+
private def reload
|
20
|
+
fail unless @browser
|
21
|
+
self.list=(@browser.current.map.to_a)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require_relative '../ui/widgets'
|
2
|
+
require_relative '../bindings'
|
3
|
+
require_relative '../theme'
|
4
|
+
|
5
|
+
module Ektoplayer
|
6
|
+
module Views
|
7
|
+
class Help < UI::Pad
|
8
|
+
def draw
|
9
|
+
self.pad_size=(UI::Size.new(
|
10
|
+
height: (Bindings.bindings.values.map(&:size).sum +
|
11
|
+
Bindings.bindings.size * 2),
|
12
|
+
width: [@size.width, 90].max
|
13
|
+
))
|
14
|
+
|
15
|
+
@win.erase
|
16
|
+
|
17
|
+
Bindings.bindings.each do |widget, commands|
|
18
|
+
@win.with_attr(Theme[:'help.widget_name']) do
|
19
|
+
@win << "\n#{widget}\n"
|
20
|
+
end
|
21
|
+
|
22
|
+
commands.each do |name, keys|
|
23
|
+
next if keys.empty?
|
24
|
+
|
25
|
+
@win.on_column(3)
|
26
|
+
keys.map { |k| Bindings.keyname(k) }.
|
27
|
+
sort.each_with_index do |key,i|
|
28
|
+
@win << ', ' if i > 0
|
29
|
+
@win.with_attr(Theme[:'help.key_name']) { @win << key }
|
30
|
+
end
|
31
|
+
|
32
|
+
@win.with_attr(Theme[:'help.command_name']) do
|
33
|
+
@win.on_column(18).addstr("#{name}")
|
34
|
+
end
|
35
|
+
|
36
|
+
@win.with_attr(Theme[:'help.command_desc']) do
|
37
|
+
@win.on_column(43).addstr(Bindings.commands[name.to_sym])
|
38
|
+
end
|
39
|
+
|
40
|
+
@win.addch(?\n)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,208 @@
|
|
1
|
+
require_relative '../ui/widgets'
|
2
|
+
require_relative '../bindings'
|
3
|
+
require_relative '../theme'
|
4
|
+
require_relative '../common'
|
5
|
+
|
6
|
+
require 'nokogiri'
|
7
|
+
|
8
|
+
module Ektoplayer
|
9
|
+
MIN_WIDTH = 90
|
10
|
+
START_HEADING = 1
|
11
|
+
START_TAG = 3
|
12
|
+
START_TAG_VALUE = 20
|
13
|
+
START_INFO = 3
|
14
|
+
START_INFO_VALUE = 26
|
15
|
+
LINE_WRAP = 65
|
16
|
+
|
17
|
+
module Views
|
18
|
+
class Info < UI::Pad
|
19
|
+
def attach(playlist, trackloader, database)
|
20
|
+
@playlist, @trackloader, @database = playlist, trackloader, database
|
21
|
+
|
22
|
+
Thread.new do
|
23
|
+
loop { sleep 1; with_lock { want_redraw } }
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def draw_heading(heading)
|
28
|
+
@win.with_attr(Theme[:'info.head']) do
|
29
|
+
@win.next_line.from_left(START_HEADING).addstr(heading)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def draw_tag(tag, value=nil)
|
34
|
+
@win.with_attr(Theme[:'info.tag']) do
|
35
|
+
@win.next_line.from_left(START_TAG).addstr(tag)
|
36
|
+
end
|
37
|
+
|
38
|
+
@win.from_left(START_TAG_VALUE)
|
39
|
+
@win.with_attr(Theme[:'info.value']) do
|
40
|
+
@win.addstr("#{value}")
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def draw_info(string, value=nil)
|
45
|
+
@win.with_attr(Theme[:'info.tag']) do
|
46
|
+
@win.next_line.from_left(START_INFO).addstr(string)
|
47
|
+
end
|
48
|
+
|
49
|
+
@win.from_left(START_INFO_VALUE)
|
50
|
+
@win.with_attr(Theme[:'info.value']) do
|
51
|
+
@win.addstr("#{value}")
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def draw_url(url, title=nil)
|
56
|
+
title ||= url
|
57
|
+
mevent = with_mouse_section_event do
|
58
|
+
@win.with_attr(Theme[:url]) { @win << title }
|
59
|
+
end
|
60
|
+
mevent.on(Curses::BUTTON1_CLICKED) do
|
61
|
+
Common::open_url_extern(url)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def draw_download(file, percent, error)
|
66
|
+
@win.with_attr(Theme[:'info.download.file']) do
|
67
|
+
@win.next_line.from_left(START_TAG).addstr(file)
|
68
|
+
end
|
69
|
+
@win.with_attr(Theme[:'info.download.percent']) do
|
70
|
+
@win.addstr(" #{percent}")
|
71
|
+
end
|
72
|
+
|
73
|
+
if error
|
74
|
+
@win.with_attr(Theme[:'info.download.error']) do
|
75
|
+
@win.addstr(" #{error}")
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def with_mouse_section_event
|
81
|
+
start_cursor = @win.cursor; yield
|
82
|
+
|
83
|
+
start_pos = UI::Point.new(
|
84
|
+
y: [start_cursor.y, @win.cursor.y].min,
|
85
|
+
x: [start_cursor.x, @win.cursor.x].min,
|
86
|
+
)
|
87
|
+
stop_pos = UI::Point.new(
|
88
|
+
y: [start_cursor.y, @win.cursor.y].max,
|
89
|
+
x: [start_cursor.x, @win.cursor.x].max
|
90
|
+
)
|
91
|
+
|
92
|
+
ev = UI::MouseSectionEvent.new(start_pos, stop_pos)
|
93
|
+
mouse_section.add(ev)
|
94
|
+
ev
|
95
|
+
end
|
96
|
+
|
97
|
+
def mouse_click(mevent)
|
98
|
+
if ev = mouse_event_transform(mevent)
|
99
|
+
ev.x += @pad_mincol
|
100
|
+
ev.y += @pad_minrow
|
101
|
+
trigger(@mouse, ev)
|
102
|
+
trigger(@mouse_section, ev)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
def draw
|
107
|
+
self.pad_size=(UI::Size.new(
|
108
|
+
height: 200,
|
109
|
+
width: [@size.width, MIN_WIDTH].max
|
110
|
+
))
|
111
|
+
|
112
|
+
mouse_section.clear
|
113
|
+
@win.erase
|
114
|
+
@win.setpos(0,0)
|
115
|
+
|
116
|
+
if @track = (@playlist[@playlist.current_playing] rescue nil)
|
117
|
+
draw_heading('Current track')
|
118
|
+
draw_tag('Number', "%0.2d" % @track['number'])
|
119
|
+
draw_tag('Title', @track['title'])
|
120
|
+
draw_tag('Remix', @track['remix']) if @track['remix']
|
121
|
+
draw_tag('Artist', @track['artist'])
|
122
|
+
draw_tag('Album', @track['album'])
|
123
|
+
draw_tag('BPM', @track['bpm'])
|
124
|
+
draw_tag('Length', Common::to_time(@length))
|
125
|
+
@win.next_line
|
126
|
+
|
127
|
+
draw_heading('Current album')
|
128
|
+
draw_tag('Album'); draw_url(@track['album_url'], @track['album'])
|
129
|
+
draw_tag('Artist', @track['album__artist']) if @track['album_artist']
|
130
|
+
draw_tag('Date', @track['date'])
|
131
|
+
|
132
|
+
if url = @track['released_by_url']
|
133
|
+
draw_tag('Released by'); draw_url(url, @track['released_by'])
|
134
|
+
end
|
135
|
+
|
136
|
+
if url = @track['posted_by_url']
|
137
|
+
draw_tag('Posted by'); draw_url(url, @track['posted_by'])
|
138
|
+
end
|
139
|
+
|
140
|
+
draw_tag('Styles', @track['styles'].sub(?,, ', '))
|
141
|
+
draw_tag('Downloads', @track['download_count'])
|
142
|
+
draw_tag('Rating', "%0.2d%% (%d Votes)" % [@track['rating'], @track['votes']])
|
143
|
+
draw_tag('Cover'); draw_url(@track['cover_url'], 'Cover')
|
144
|
+
@win.next_line
|
145
|
+
|
146
|
+
# -- description
|
147
|
+
draw_heading('Description')
|
148
|
+
line_length = START_TAG
|
149
|
+
wrap_length = @size.width.clamp(1, LINE_WRAP)
|
150
|
+
@win.next_line.from_left(START_TAG)
|
151
|
+
|
152
|
+
Nokogiri::HTML("<p>#{@track['description']}</p>").css(?p).each do |p|
|
153
|
+
p.children.each do |element|
|
154
|
+
if element[:href]
|
155
|
+
if (line_length += element.text.size) > wrap_length
|
156
|
+
@win.next_line.from_left(START_TAG)
|
157
|
+
line_length = START_TAG
|
158
|
+
end
|
159
|
+
|
160
|
+
draw_url(element[:href], element.text.strip)
|
161
|
+
@win.addch(' ')
|
162
|
+
else
|
163
|
+
element.text.split(' ').each do |text|
|
164
|
+
if (line_length += text.size) > wrap_length
|
165
|
+
@win.next_line.from_left(START_TAG)
|
166
|
+
line_length = START_TAG
|
167
|
+
end
|
168
|
+
|
169
|
+
@win.with_attr(Theme[:'info.description']) do
|
170
|
+
@win.mv_left(1) if text =~ /^[\.,:;]$/
|
171
|
+
@win << text
|
172
|
+
@win << ' '
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
@win.next_line
|
180
|
+
# --/description
|
181
|
+
end
|
182
|
+
|
183
|
+
if not @trackloader.downloads.empty?
|
184
|
+
draw_heading('Downloads')
|
185
|
+
@trackloader.downloads.each do |dl|
|
186
|
+
name = File.basename(dl.filename)
|
187
|
+
percent = Float(dl.progress) / dl.total * 100
|
188
|
+
percent = "%0.2f" % percent
|
189
|
+
draw_download(name, ?( + percent + '%)', dl.error)
|
190
|
+
end
|
191
|
+
@win.next_line
|
192
|
+
end
|
193
|
+
|
194
|
+
draw_heading('Player')
|
195
|
+
draw_info('Version', Application::VERSION)
|
196
|
+
draw_info('Tracks in database', @database.track_count)
|
197
|
+
draw_info('Albums in database', @database.album_count)
|
198
|
+
draw_info('Cache dir size', "%dMB" % (Dir.size(Config[:cache_dir]) / (1024 ** 2)))
|
199
|
+
draw_info('Archive dir size', "%dMB" % (Dir.size(Config[:archive_dir]) / (1024 ** 2)))
|
200
|
+
draw_info('Ektoplazm URL'); draw_url(Application::EKTOPLAZM_URL)
|
201
|
+
draw_info('Github URL'); draw_url(Application::GITHUB_URL)
|
202
|
+
|
203
|
+
self.pad_size=(@size.update(height: [@win.cursor.y + 2, @size.height].max))
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
208
|
+
|
@@ -0,0 +1,64 @@
|
|
1
|
+
%w( ../ui/widgets/tabbedcontainer playinginfo progressbar
|
2
|
+
volumemeter splash playlist browser info help ).
|
3
|
+
each {|_|require_relative(_)}
|
4
|
+
|
5
|
+
module Ektoplayer
|
6
|
+
module Views
|
7
|
+
class MainWindow < UI::VerticalContainer
|
8
|
+
attr_reader :progressbar, :volumemeter, :playinginfo
|
9
|
+
attr_reader :tabs, :splash, :playlist, :browser, :info, :help
|
10
|
+
|
11
|
+
def initialize(**opts)
|
12
|
+
super(**opts)
|
13
|
+
|
14
|
+
s1 = UI::Size.new(height: 1, width: 1) # TODO.....!!
|
15
|
+
|
16
|
+
@playinginfo = sub(PlayingInfo)
|
17
|
+
@progressbar = sub(ProgressBar)
|
18
|
+
@volumemeter = sub(VolumeMeter)
|
19
|
+
@tabs = sub(UI::TabbedContainer)
|
20
|
+
@help = @tabs.sub(Help, size: s1, visible: false)
|
21
|
+
@info = @tabs.sub(Info, size: s1, visible: false)
|
22
|
+
@splash = @tabs.sub(Splash, size: s1, visible: false)
|
23
|
+
@browser = @tabs.sub(Browser, size: s1, visible: false)
|
24
|
+
@playlist = @tabs.sub(Playlist, size: s1, visible: false)
|
25
|
+
|
26
|
+
@tabs.attributes=(
|
27
|
+
%w(tab_selected tabs).map do |attr|
|
28
|
+
[attr.to_sym, Theme[attr.to_sym]]
|
29
|
+
end.to_h
|
30
|
+
)
|
31
|
+
|
32
|
+
Config[:'tabs.widgets'].each do |widget|
|
33
|
+
@tabs.add(send(widget), widget)
|
34
|
+
end
|
35
|
+
|
36
|
+
Config[:'main.widgets'].each { |w| add(send(w)) }
|
37
|
+
self.selected=(@tabs)
|
38
|
+
end
|
39
|
+
|
40
|
+
def layout
|
41
|
+
height = @size.height
|
42
|
+
|
43
|
+
if @playinginfo.visible?
|
44
|
+
@playinginfo.size=(@size.update(height: 2))
|
45
|
+
height -= 2
|
46
|
+
end
|
47
|
+
|
48
|
+
if @volumemeter.visible?
|
49
|
+
@volumemeter.size=(@size.update(height: 1))
|
50
|
+
height -= 1
|
51
|
+
end
|
52
|
+
|
53
|
+
if @progressbar.visible?
|
54
|
+
@progressbar.size=(@size.update(height: 1))
|
55
|
+
height -= 1
|
56
|
+
end
|
57
|
+
|
58
|
+
@tabs.size=(@size.update(height: height))
|
59
|
+
|
60
|
+
super
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,135 @@
|
|
1
|
+
require_relative '../ui/widgets'
|
2
|
+
require_relative '../theme'
|
3
|
+
require_relative '../config'
|
4
|
+
require_relative '../common'
|
5
|
+
|
6
|
+
module Ektoplayer
|
7
|
+
module Views
|
8
|
+
class PlayingInfo < UI::Window
|
9
|
+
STOPPED_HEADING = '- Ektoplayer -'.freeze
|
10
|
+
|
11
|
+
def paused!
|
12
|
+
return if @state == :paused
|
13
|
+
with_lock { @state = :paused; want_redraw }
|
14
|
+
end
|
15
|
+
|
16
|
+
def playing!
|
17
|
+
return if @state == :playing
|
18
|
+
with_lock { @state = :playing; want_redraw }
|
19
|
+
end
|
20
|
+
|
21
|
+
def stopped!
|
22
|
+
return if @state == :stopped
|
23
|
+
with_lock { @state = :stopped; want_redraw }
|
24
|
+
end
|
25
|
+
|
26
|
+
def track=(t)
|
27
|
+
return if @track == t
|
28
|
+
with_lock { @track = t; want_redraw }
|
29
|
+
end
|
30
|
+
|
31
|
+
def length=(l)
|
32
|
+
return if @length == l.to_i
|
33
|
+
|
34
|
+
with_lock do
|
35
|
+
@length = l.to_i
|
36
|
+
draw_position_and_length
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def position=(p)
|
41
|
+
return if @position == p.to_i
|
42
|
+
|
43
|
+
with_lock do
|
44
|
+
@position = p.to_i
|
45
|
+
draw_position_and_length
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def draw_position_and_length
|
50
|
+
@win.setpos(0,0)
|
51
|
+
@win.with_attr(Theme[:'playinginfo.position']) do
|
52
|
+
@win << "[#{Common::to_time(@position)}/#{Common::to_time(@length)}]"
|
53
|
+
end
|
54
|
+
want_refresh
|
55
|
+
end
|
56
|
+
|
57
|
+
def attach(playlist, player)
|
58
|
+
with_lock do
|
59
|
+
player.events.on(:pause) { self.paused! }
|
60
|
+
player.events.on(:stop) { self.stopped! }
|
61
|
+
player.events.on(:play) { self.playing! }
|
62
|
+
|
63
|
+
player.events.on(:position_change) do
|
64
|
+
with_lock do
|
65
|
+
self.position=(player.position)
|
66
|
+
self.length=(player.length)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
playlist.events.on(:current_changed) {
|
71
|
+
self.track=(playlist[playlist.current_playing])
|
72
|
+
}
|
73
|
+
|
74
|
+
# TODO: move mouse?
|
75
|
+
self.mouse.on(Curses::BUTTON1_CLICKED) do |mevent|
|
76
|
+
player.toggle
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
private def fill(format)
|
82
|
+
sum = 0
|
83
|
+
format.each do |fmt|
|
84
|
+
if fmt[:tag] == 'text'
|
85
|
+
fmt[:filled] = fmt[:text]
|
86
|
+
elsif value = @track[fmt[:tag]]
|
87
|
+
fmt[:filled] = value.to_s
|
88
|
+
else
|
89
|
+
fmt[:filled] = ''
|
90
|
+
end
|
91
|
+
|
92
|
+
sum += fmt[:filled].size
|
93
|
+
end
|
94
|
+
|
95
|
+
format.each { |fmt| fmt[:sum] = sum }
|
96
|
+
format
|
97
|
+
end
|
98
|
+
|
99
|
+
def draw
|
100
|
+
@win.erase
|
101
|
+
|
102
|
+
if @track
|
103
|
+
draw_position_and_length
|
104
|
+
|
105
|
+
fill(Config[:'playinginfo.format1']).each_with_index do |fmt,i|
|
106
|
+
@win.center(fmt[:sum]) if i == 0
|
107
|
+
@win.with_attr(UI::Colors.set(nil, *fmt[:curses_attrs])) do
|
108
|
+
@win << fmt[:filled]
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
@win.with_attr(Theme[:'playinginfo.state']) do
|
113
|
+
@win.from_right("[#{@state}]".size) << "[#{@state}]"
|
114
|
+
end
|
115
|
+
|
116
|
+
@win.next_line
|
117
|
+
|
118
|
+
fill(Config[:'playinginfo.format2']).each_with_index do |fmt,i|
|
119
|
+
@win.center(fmt[:sum]) if i == 0
|
120
|
+
@win.with_attr(UI::Colors.set(nil, *fmt[:curses_attrs])) do
|
121
|
+
@win << fmt[:filled]
|
122
|
+
end
|
123
|
+
end
|
124
|
+
else
|
125
|
+
@win.center_string(STOPPED_HEADING)
|
126
|
+
@win.with_attr(Theme[:'playinginfo.state']) do
|
127
|
+
@win.from_right('[stopped]'.size) << '[stopped]'
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
#@win.next_line.addstr('─' * @size.width)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|