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.
Files changed (51) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +49 -0
  3. data/bin/ektoplayer +7 -0
  4. data/lib/ektoplayer.rb +10 -0
  5. data/lib/ektoplayer/application.rb +148 -0
  6. data/lib/ektoplayer/bindings.rb +230 -0
  7. data/lib/ektoplayer/browsepage.rb +138 -0
  8. data/lib/ektoplayer/client.rb +18 -0
  9. data/lib/ektoplayer/common.rb +91 -0
  10. data/lib/ektoplayer/config.rb +247 -0
  11. data/lib/ektoplayer/controllers/browser.rb +47 -0
  12. data/lib/ektoplayer/controllers/controller.rb +9 -0
  13. data/lib/ektoplayer/controllers/help.rb +21 -0
  14. data/lib/ektoplayer/controllers/info.rb +22 -0
  15. data/lib/ektoplayer/controllers/mainwindow.rb +40 -0
  16. data/lib/ektoplayer/controllers/playlist.rb +60 -0
  17. data/lib/ektoplayer/database.rb +199 -0
  18. data/lib/ektoplayer/events.rb +56 -0
  19. data/lib/ektoplayer/models/browser.rb +127 -0
  20. data/lib/ektoplayer/models/database.rb +49 -0
  21. data/lib/ektoplayer/models/model.rb +15 -0
  22. data/lib/ektoplayer/models/player.rb +28 -0
  23. data/lib/ektoplayer/models/playlist.rb +72 -0
  24. data/lib/ektoplayer/models/search.rb +42 -0
  25. data/lib/ektoplayer/models/trackloader.rb +17 -0
  26. data/lib/ektoplayer/mp3player.rb +151 -0
  27. data/lib/ektoplayer/operations/browser.rb +19 -0
  28. data/lib/ektoplayer/operations/operations.rb +26 -0
  29. data/lib/ektoplayer/operations/player.rb +11 -0
  30. data/lib/ektoplayer/operations/playlist.rb +67 -0
  31. data/lib/ektoplayer/theme.rb +102 -0
  32. data/lib/ektoplayer/trackloader.rb +146 -0
  33. data/lib/ektoplayer/ui.rb +404 -0
  34. data/lib/ektoplayer/ui/colors.rb +105 -0
  35. data/lib/ektoplayer/ui/widgets.rb +195 -0
  36. data/lib/ektoplayer/ui/widgets/container.rb +125 -0
  37. data/lib/ektoplayer/ui/widgets/labelwidget.rb +43 -0
  38. data/lib/ektoplayer/ui/widgets/listwidget.rb +332 -0
  39. data/lib/ektoplayer/ui/widgets/tabbedcontainer.rb +110 -0
  40. data/lib/ektoplayer/updater.rb +77 -0
  41. data/lib/ektoplayer/views/browser.rb +25 -0
  42. data/lib/ektoplayer/views/help.rb +46 -0
  43. data/lib/ektoplayer/views/info.rb +208 -0
  44. data/lib/ektoplayer/views/mainwindow.rb +64 -0
  45. data/lib/ektoplayer/views/playinginfo.rb +135 -0
  46. data/lib/ektoplayer/views/playlist.rb +39 -0
  47. data/lib/ektoplayer/views/progressbar.rb +51 -0
  48. data/lib/ektoplayer/views/splash.rb +99 -0
  49. data/lib/ektoplayer/views/trackrenderer.rb +137 -0
  50. data/lib/ektoplayer/views/volumemeter.rb +74 -0
  51. 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