ektoplayer 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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