ampv 1.0.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 ADDED
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ ZTMwNzg3MDNhNjM2ZTk1Njk4MmYyNzllNTYzOTQ4MzQ1OTJiNmJkOQ==
5
+ data.tar.gz: !binary |-
6
+ MmFmYTY5YmQ4Zjc0OTQ4NzE3Y2IxNTBmNmZhNGQ5ZTc5M2E5NDBiMQ==
7
+ !binary "U0hBNTEy":
8
+ metadata.gz: !binary |-
9
+ YzFkNDdmOTYyOTg1NDdkNjEyZTgwN2NlY2Y0NzU3MjkzODE2OTQ1YWFjZDNm
10
+ ZTJlMWQyNTUwNTc5ZTVjZTFlZjEzZGZmMjMzY2E1MTRmZDZmOWQ2YzE3MjUx
11
+ NmU0ZTQyNzA2YWMzMjU2MGI5YzRiMmJjMjk5YjJhYTJmNDcxMDM=
12
+ data.tar.gz: !binary |-
13
+ MzE0ZjZhZGYyYzhkNWZmNjhiZDA1ZjAyNDBlMmVmMDQyYWVhNGZkOTk4YmQw
14
+ MWJiMDU0ZGNmYzBjMDIyNzg1OWI3NDE0MWM1Zjk3ZTgwMzRkY2VlOTE1ZWJj
15
+ OWNiMjZkOTYzY2YxOWEwMGZiMmZlN2RkYzdiNWQxODYxZTZkZjU=
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2013 ahoka
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
6
+ this software and associated documentation files (the "Software"), to deal in
7
+ the Software without restriction, including without limitation the rights to
8
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9
+ the Software, and to permit persons to whom the Software is furnished to do so,
10
+ subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/bin/ampv ADDED
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "ampv"
4
+
5
+ Ampv::MainWindow.new
6
+
data/input.conf ADDED
@@ -0,0 +1,28 @@
1
+ MOUSE_BTN0_DBL cycle fullscreen
2
+ MOUSE_BTN1 cycle pause
3
+ MOUSE_BTN3 add volume 1
4
+ MOUSE_BTN4 add volume -1
5
+ MOUSE_BTN7 playlist_prev
6
+ MOUSE_BTN8 playlist_next
7
+ MOUSE_BTN9 cycle pause
8
+
9
+ RIGHT seek 10
10
+ LEFT seek -10
11
+ UP add volume 1
12
+ DOWN add volume -1
13
+
14
+ PGUP seek -90 - exact
15
+ PGDWN seek 90 - exact
16
+ HOME add chapter -1
17
+ END add chapter 1
18
+ INS playlist_prev
19
+ DEL playlist_next
20
+
21
+ f cycle fullscreen
22
+ SPACE cycle pause
23
+ ESC quit_watch_later
24
+
25
+ # ampv commands
26
+ l cycle playlist
27
+ o open_file_chooser
28
+ p cycle progress_bar
data/lib/ampv.rb ADDED
@@ -0,0 +1,380 @@
1
+
2
+ require "gtk2"
3
+ require "ampv/mpvwidget"
4
+ require "ampv/playlist"
5
+ require "ampv/progressbarwidget"
6
+ require "ampv/version"
7
+ require "uri"
8
+
9
+ module Ampv
10
+ class MainWindow < Gtk::Window
11
+
12
+ MAIN_CONF = "#{ENV["HOME"]}/.config/ampv.conf"
13
+ INPUT_CONF = "#{ENV["HOME"]}/.mpv/input.conf"
14
+ VIDEO_EXTS = [ ".avi", ".mkv", ".mp4", ".mpeg", ".mpg", ".ogm", ".ogv" ]
15
+ KEY_NAMES = {
16
+ "esc" => "Escape",
17
+ "space" => "space",
18
+ "right" => "Right",
19
+ "left" => "Left",
20
+ "up" => "Up",
21
+ "down" => "Down",
22
+ "pgup" => "Page_Up",
23
+ "pgdwn" => "Page_Down",
24
+ "home" => "Home",
25
+ "end" => "End",
26
+ "ins" => "Insert",
27
+ "del" => "Delete",
28
+ }
29
+ WHEEL_BUTTONS = {
30
+ Gdk::EventScroll::UP => 4,
31
+ Gdk::EventScroll::DOWN => 5,
32
+ Gdk::EventScroll::LEFT => 6,
33
+ Gdk::EventScroll::RIGHT => 7
34
+ }
35
+
36
+ LEFT_PTR = Gdk::Cursor.new(Gdk::Cursor::LEFT_PTR)
37
+ BLANK_CURSOR = Gdk::Cursor.new(Gdk::Cursor::BLANK_CURSOR)
38
+
39
+ def initialize
40
+ load_config
41
+ super
42
+ set_title(PACKAGE)
43
+ set_default_size(@config["width"], @config["height"])
44
+ set_window_position(Gtk::Window::POS_CENTER)
45
+ move(@config["x"], @config["y"]) unless @config["x"] == -1 and @config["y"] == -1
46
+
47
+ Gtk::Drag.dest_set(self, Gtk::Drag::DEST_DEFAULT_ALL,
48
+ [ [ "text/uri-list", 0, 0 ] ],
49
+ Gdk::DragContext::ACTION_LINK)
50
+
51
+ signal_connect("delete_event") { quit }
52
+ signal_connect("scroll_event") { |w, e| handle_mouse_event(e) }
53
+ signal_connect("button_press_event") { |w, e| handle_mouse_event(e) }
54
+ signal_connect("key_press_event") { |w, e| handle_keyboard_event(e) }
55
+ signal_connect("drag_data_received") { |w, dc, x, y, sd, type, time|
56
+ handle_drop_event(sd.data, dc, time, false, true)
57
+ }
58
+
59
+ vbox = Gtk::VBox.new
60
+ add(vbox)
61
+
62
+ args = process_args
63
+ load_bindings
64
+
65
+ @mpv = MPvWidget.new(args, @config["scrobbler"])
66
+ @mpv.signal_connect("file_changed") { |w, file|
67
+ @playing = file
68
+ @mpv.send("show_text ${media-title} 1500") if window.state.fullscreen?
69
+ @playlist.set_selected(@playing)
70
+ set_title(File.basename(@playing))
71
+ }
72
+ @mpv.signal_connect("length_changed") { |w, len|
73
+ @length = len
74
+ @playlist.update_length(@length)
75
+ }
76
+ @mpv.signal_connect("time_pos_changed") { |w, pos| @progress_bar.value = pos / @length.to_f }
77
+ @mpv.signal_connect("stopped") {
78
+ @progress_bar.value = 0
79
+ next_file = @playlist.get_next
80
+ @really_stop = next_file.nil? or @really_stop
81
+ set_title(PACKAGE)
82
+ if @really_stop and window.state.fullscreen?
83
+ toggle_fullscreen
84
+ elsif not @really_stop
85
+ load_file(next_file, false)
86
+ end
87
+ @really_stop = false
88
+ }
89
+
90
+ vbox.pack_start(@mpv)
91
+
92
+ @playlist = Playlist.new(@config["playlist_x"],
93
+ @config["playlist_y"],
94
+ @config["playlist_width"],
95
+ @config["playlist_height"],
96
+ @config["playlist_visible"])
97
+ @playlist.signal_connect("open_file_chooser") { open_file_chooser }
98
+ @playlist.signal_connect("drag_data_received") { |w, dc, x, y, sd, type, time|
99
+ handle_drop_event(sd.data, dc, time, true, false)
100
+ }
101
+ @playlist.signal_connect("play_entry") { |w, file| load_file(file, false, false, false, true) }
102
+ @playlist.signal_connect("playing_removed") { @mpv.send("stop"); @really_stop = true }
103
+
104
+ @progress_bar = ProgressBarWidget.new(@config["bar_color"],
105
+ @config["head_color"],
106
+ @config["progress_bar_height"])
107
+ @progress_bar.add_events(Gdk::Event::BUTTON_PRESS_MASK)
108
+ @progress_bar.signal_connect("button_press_event") { |w, e| handle_seek_event(e) }
109
+ vbox.pack_start(@progress_bar, false, false)
110
+
111
+ show_all
112
+ @mpv.start
113
+
114
+ argv = ARGV.join(" ")
115
+ if not argv.empty?
116
+ load_file(argv)
117
+ elsif not @config["playlist"].empty?
118
+ @config["playlist"].each { |x| load_file(x, true, true, false) }
119
+ @playlist.set_selected(@config["playlist_selected"])
120
+ end
121
+
122
+ Gtk.main
123
+ end
124
+
125
+ def load_config
126
+ @config = {
127
+ "width" => Gdk::Screen.default.width > 1280 ? 1280 : 853,
128
+ "height" => Gdk::Screen.default.width > 1280 ? 726 : 486,
129
+ "x" => -1,
130
+ "y" => -1,
131
+ "fullscreen_progressbar" => false,
132
+ "progress_bar_visible" => true,
133
+ "progress_bar_height" => 6,
134
+ "bar_color" => "#8f5b5b",
135
+ "head_color" => "#c48181",
136
+ "playlist_width" => 360,
137
+ "playlist_height" => 550,
138
+ "playlist_x" => 0,
139
+ "playlist_y" => 0,
140
+ "playlist_visible" => true,
141
+ "always_save_position" => false,
142
+ "scrobbler" => "",
143
+ "playlist_selected" => "",
144
+ "playlist" => [ ],
145
+ }
146
+
147
+ if File.exists?(MAIN_CONF)
148
+ File.readlines(MAIN_CONF).each { |line|
149
+ key, _, val = line.partition("=")
150
+ key = key.strip
151
+ val = val.strip
152
+ next unless @config.has_key?(key) and not key.start_with?("#")
153
+
154
+ if @config[key].is_a?(Integer)
155
+ val = val.to_i
156
+ elsif @config[key].is_a?(TrueClass) or @config[key].is_a?(FalseClass)
157
+ val = val == "true"
158
+ elsif @config[key].is_a?(Array)
159
+ val = val.split("|")
160
+ elsif val.start_with?("#")
161
+ begin
162
+ c = Gdk::Color.parse(val)
163
+ rescue
164
+ puts("Invalid hexidecimal color for setting `#{key}': `#{val}")
165
+ c = Gdk::Color.parse(@config[key])
166
+ end
167
+ val = c
168
+ end
169
+
170
+ @config[key] = val
171
+ }
172
+ end
173
+ end
174
+
175
+ def process_args
176
+ args = [ ]
177
+ ARGV.dup.each { |arg|
178
+ if arg.start_with?("-")
179
+ args.push(arg)
180
+ ARGV.delete(arg)
181
+ end
182
+ }
183
+ return args
184
+ end
185
+
186
+ def load_bindings
187
+ @mouse_bindings = [ ]
188
+ @key_bindings = [ ]
189
+ if File.exists?(INPUT_CONF)
190
+ File.readlines(INPUT_CONF).each { |line|
191
+ line = line.strip
192
+ if line.start_with?("MOUSE_BTN")
193
+ # 4 = up, 5 = down, 6 = left, 7 = right
194
+ button, cmd = line.match(/MOUSE_BTN(\d+)(?:_DBL)?\s+(.+)$/).captures
195
+ button = button.to_i + 1
196
+ type = (4..7).include?(button) ? Gdk::Event::SCROLL :
197
+ line.include?("DBL") ? Gdk::Event::BUTTON2_PRESS : Gdk::Event::BUTTON_PRESS
198
+ @mouse_bindings[type] = [ ] if @mouse_bindings[type].nil?
199
+ @mouse_bindings[type][button] = cmd
200
+ elsif not line.empty?
201
+ key, cmd = line.match(/^([^\s]+)\s+(.+)$/).captures
202
+ if name = KEY_NAMES[key.downcase]
203
+ keyval = Gdk::Keyval.from_name(name)
204
+ else
205
+ keyval = Gdk::Keyval.from_name(key)
206
+ end
207
+
208
+ @key_bindings[keyval] = cmd if keyval > 0
209
+ end
210
+ }
211
+ end
212
+ end
213
+
214
+ def load_file(file, add_to_playlist=true, do_not_play=false, auto_add=true, force_play=false)
215
+ return if file.nil? or file.empty?
216
+ file = File.expand_path(file) if file[0] == "~"
217
+
218
+ if add_to_playlist
219
+ if @playlist.count == 0 and auto_add and file !~ /^https?:\/\//
220
+ dir = File.directory?(file) ? file : File.dirname(file)
221
+ entries = Dir.entries(dir).sort
222
+ entries.delete_if { |x| x.start_with?(".") or not valid_video_file(x) }
223
+ entries.map { |x|
224
+ x = dir + "/" + x
225
+ @playlist.add_file(x)
226
+ file = x if file == dir
227
+ }
228
+ else
229
+ @playlist.add_file(file)
230
+ end
231
+ end
232
+
233
+ @mpv.load_file(file, force_play) unless do_not_play
234
+ end
235
+
236
+ def handle_mouse_event(e)
237
+ button = e.event_type == Gdk::Event::SCROLL ? WHEEL_BUTTONS[e.direction] : e.button
238
+ return if @mouse_bindings[e.event_type].nil?
239
+
240
+ process_cmd(@mouse_bindings[e.event_type][button])
241
+
242
+ return true
243
+ end
244
+
245
+ def handle_keyboard_event(e)
246
+ process_cmd(@key_bindings[e.keyval])
247
+
248
+ return true
249
+ end
250
+
251
+ def handle_seek_event(e)
252
+ if e.event_type == Gdk::Event::BUTTON_PRESS and e.button == 1
253
+ pos = e.x / allocation.width * @length.to_f
254
+ seek("seek #{pos} absolute")
255
+ end
256
+
257
+ return true
258
+ end
259
+
260
+ def handle_drop_event(data, context, time, do_not_play, replace)
261
+ files = URI.decode(data).gsub("file://", "").split("\r\n")
262
+ @playlist.clear if replace
263
+
264
+ files.each { |x|
265
+ load_file(x, true, do_not_play) if valid_video_file(x)
266
+ }
267
+
268
+ Gtk::Drag.finish(context, true, true, time)
269
+ end
270
+
271
+ def process_cmd(cmd)
272
+ case cmd
273
+ when "cycle fullscreen"
274
+ toggle_fullscreen
275
+ when "cycle pause"
276
+ @mpv.play_pause
277
+ when "cycle playlist"
278
+ @playlist.visible? ? @playlist.hide : @playlist.show
279
+ when /seek /
280
+ seek(cmd)
281
+ when /add chapter/
282
+ seek(cmd)
283
+ when "playlist_next"
284
+ load_file(@playlist.get_next, false)
285
+ when "playlist_prev"
286
+ load_file(@playlist.get_prev, false)
287
+ when "open_file_chooser"
288
+ open_file_chooser
289
+ when "cycle progress_bar"
290
+ toggle_progress_bar
291
+ when "quit_watch_later"
292
+ quit(true)
293
+ when "quit"
294
+ quit
295
+ else
296
+ @mpv.send(cmd) if cmd
297
+ end
298
+ end
299
+
300
+ def seek(cmd)
301
+ cmd = "no-osd " + cmd unless cmd.start_with?("no-osd") or
302
+ not @progress_bar.visible?
303
+ @mpv.send(cmd)
304
+ @mpv.send("get_property time-pos")
305
+ end
306
+
307
+ def toggle_fullscreen
308
+ if window.state.fullscreen?
309
+ @progress_bar.show unless @progress_bar_user_hidden
310
+ unfullscreen
311
+ window.set_cursor(LEFT_PTR)
312
+ else
313
+ @progress_bar.hide unless @config["fullscreen_progressbar"]
314
+ fullscreen
315
+ window.set_cursor(BLANK_CURSOR)
316
+ end
317
+ end
318
+
319
+ def toggle_progress_bar
320
+ if @progress_bar.visible?
321
+ @progress_bar.hide
322
+ @progress_bar_user_hidden = true
323
+ else
324
+ @progress_bar.show
325
+ @progress_bar_user_hidden = false
326
+ end
327
+ end
328
+
329
+ def valid_video_file(x)
330
+ return (VIDEO_EXTS.include?(File.extname(x)) or
331
+ `file -b --mime-type "#{x}"`.start_with?("video"))
332
+ end
333
+
334
+
335
+ def open_file_chooser
336
+ dialog = Gtk::FileChooserDialog.new("Open File - #{PACKAGE}",
337
+ self, Gtk::FileChooser::ACTION_OPEN, nil,
338
+ [ Gtk::Stock::CANCEL, Gtk::Dialog::RESPONSE_CANCEL ],
339
+ [ Gtk::Stock::OPEN, Gtk::Dialog::RESPONSE_ACCEPT ])
340
+ dialog.select_multiple = true
341
+ do_not_play = @playlist.count > 0
342
+
343
+ filter = Gtk::FileFilter.new
344
+ filter.name = "Video Files"
345
+ VIDEO_EXTS.each { |x| filter.add_pattern("*#{x}") }
346
+ dialog.add_filter(filter)
347
+
348
+ filterAll = Gtk::FileFilter.new
349
+ filterAll.name = "All Files"
350
+ filterAll.add_pattern("*.*")
351
+ dialog.add_filter(filterAll)
352
+
353
+ dialog.filenames.each { |x|
354
+ load_file(x, true, do_not_play)
355
+ do_not_play = true
356
+ } if dialog.run == Gtk::Dialog::RESPONSE_ACCEPT
357
+ dialog.destroy
358
+ end
359
+
360
+ def quit(watch_later=false)
361
+ @config["x"],
362
+ @config["y"],
363
+ @config["width"],
364
+ @config["height"] = window.geometry unless window.state.fullscreen?
365
+ @config["playlist_x"],
366
+ @config["playlist_y"],
367
+ @config["playlist_width"],
368
+ @config["playlist_height"] = @playlist.window.geometry
369
+ @config["playlist_visible"] = @playlist.visible?
370
+ @config["playlist_selected"] = @playing
371
+ @config["playlist"] = @playlist.get_entries.join("|")
372
+ @config["progress_bar_visible"] = @progress_bar.visible?
373
+ File.open(MAIN_CONF, "w") { |file| @config.each { |k, v| file.puts("#{k}=#{v}") } }
374
+
375
+ @mpv.quit(@config["always_save_position"] ? true : watch_later)
376
+ Gtk.main_quit
377
+ end
378
+ end
379
+ end
380
+
@@ -0,0 +1,125 @@
1
+ require "fifo"
2
+
3
+ module Ampv
4
+ class MPvWidget < Gtk::EventBox
5
+
6
+ type_register
7
+ signal_new("file_changed", GLib::Signal::RUN_FIRST, nil, nil, String)
8
+ signal_new("length_changed", GLib::Signal::RUN_FIRST, nil, nil, Integer)
9
+ signal_new("time_pos_changed", GLib::Signal::RUN_FIRST, nil, nil, Float)
10
+ signal_new("stopped", GLib::Signal::RUN_FIRST, nil, nil)
11
+
12
+ def initialize(args, scrobbler)
13
+ if args.include?("--debug")
14
+ args.delete("--debug")
15
+ @debug = true
16
+ end
17
+
18
+ @scrobbler = scrobbler
19
+ @mpv_path = "/usr/bin/mpv"
20
+ @mpv_options = args.join(" ")
21
+ @mpv_fifo = "/tmp/mpv.fifo." + Process.pid.to_s
22
+
23
+ super()
24
+
25
+ @socket = Gtk::Socket.new
26
+ @socket.modify_bg(Gtk::STATE_NORMAL, Gdk::Color.parse("#000"))
27
+
28
+ @socket.signal_connect("plug_removed") { signal_emit("stopped"); true }
29
+ add(@socket)
30
+ end
31
+
32
+ def start
33
+ if @thread.nil?
34
+ @fifo = Fifo.new(@mpv_fifo, :w, :nowait)
35
+
36
+ cmd = "#{@mpv_path} \
37
+ --identify \
38
+ --idle \
39
+ --input-file=#{@mpv_fifo} \
40
+ --no-mouse-movements \
41
+ --cursor-autohide=no \
42
+ --msglevel=all=2 \
43
+ --msglevel=global=4 \
44
+ --wid=#{@socket.id} #{@mpv_options}"
45
+ @thread = Thread.new { slave_reader(cmd) }
46
+ end
47
+ end
48
+
49
+ def send(cmd)
50
+ @fifo.puts(cmd) unless @fifo.nil?
51
+ end
52
+
53
+ def load_file(file, force_play=false)
54
+ send("loadfile \"#{file}\"")
55
+ @force_play = force_play
56
+ end
57
+
58
+ def play_pause
59
+ send("cycle pause")
60
+ @is_paused = @is_paused ? false : true
61
+ end
62
+
63
+ def quit(watch_later)
64
+ send("quit" + (watch_later ? "_watch_later" : ""))
65
+ @thread.join unless @thread.nil? or not @thread.alive?
66
+ @fifo.close
67
+ File.delete(@mpv_fifo)
68
+ end
69
+
70
+ private
71
+ def slave_reader(cmd)
72
+ @pipe = IO.popen(cmd, "a+")
73
+
74
+ until @pipe.nil? or @pipe.closed? or @pipe.eof?
75
+ line = @pipe.readline.chomp
76
+ if line.include?("ID_FILENAME=")
77
+ signal_emit("file_changed", (@playing = line.partition("ID_FILENAME=").last))
78
+ send("get_property pause") # saved position also saves play state
79
+
80
+ @prog_thread.kill unless @prog_thread.nil? or not @prog_thread.alive?
81
+ @prog_thread = Thread.new { progress_update }
82
+ elsif line.start_with?("ID_LENGTH=")
83
+ signal_emit("length_changed", (@length = line.rpartition("=").last.to_i))
84
+ elsif line.start_with?("ANS_pause=")
85
+ @is_paused = line.rpartition("=").last == "yes"
86
+ play_pause if @force_play and @is_paused
87
+ elsif line.start_with?("ANS_time-pos=")
88
+ signal_emit("time_pos_changed", line.rpartition("=").last.to_f)
89
+ end
90
+
91
+ if @debug or line.start_with?("Error")
92
+ puts(line) unless line.start_with?("ANS_time-pos=") or
93
+ line.start_with?("ANS_ERROR") or
94
+ line.start_with?("Failed to get") or
95
+ line.start_with?("Command ")
96
+ end
97
+ end
98
+ end
99
+
100
+ def progress_update
101
+ scrobbled = false
102
+ watched = 0
103
+
104
+ while true
105
+ send("get_property time-pos") unless @is_paused
106
+
107
+ if not @scrobbler.nil? and not scrobbled and watched >= @length * 0.5
108
+ system("#{@scrobbler} \"#{@playing}\"")
109
+ scrobbled = true
110
+ end
111
+
112
+ sleep(1)
113
+ watched += 1 unless @is_paused
114
+ end
115
+ end
116
+
117
+ def signal_do_file_changed(file) end
118
+ def signal_do_length_changed(len) end
119
+ def signal_do_time_pos_changed(pos) end
120
+ def signal_do_stopped()
121
+ @prog_thread.kill
122
+ end
123
+ end
124
+ end
125
+
@@ -0,0 +1,207 @@
1
+ #require "gtk2"
2
+
3
+ module Ampv
4
+ class Playlist < Gtk::Window
5
+
6
+ type_register
7
+ signal_new("play_entry", GLib::Signal::RUN_FIRST, nil, nil, String)
8
+ signal_new("playing_removed", GLib::Signal::RUN_FIRST, nil, nil)
9
+ signal_new("open_file_chooser", GLib::Signal::RUN_FIRST, nil, nil)
10
+
11
+ def initialize(x, y, w, h, is_visible)
12
+ buttons = {
13
+ [ Gtk::Stock::OPEN, "Add to Playlist" ] => lambda { signal_emit("open_file_chooser") },
14
+ [ Gtk::Stock::GO_UP, "Move Up" ] => lambda { move_selected_up },
15
+ [ Gtk::Stock::GO_DOWN, "Move Down" ] => lambda { move_selected_down },
16
+ [ Gtk::Stock::REMOVE, "Remove Selected" ] => lambda { remove_selected },
17
+ [ Gtk::Stock::CLEAR, "Clear Playlist" ] => lambda { clear }
18
+ }
19
+
20
+ super()
21
+ set_title("Playlist - #{PACKAGE}")
22
+ set_default_size(w, h)
23
+ set_skip_taskbar_hint(true)
24
+ move(x, y)
25
+
26
+ Gtk::Drag.dest_set(self, Gtk::Drag::DEST_DEFAULT_ALL,
27
+ [ [ "text/uri-list", 0, 0 ] ],
28
+ Gdk::DragContext::ACTION_LINK)
29
+
30
+ signal_connect("show") { move(@pos[0], @pos[1]) unless @pos.nil? }
31
+ signal_connect("hide") { @pos = window.root_origin }
32
+ signal_connect("delete_event") { hide_on_delete }
33
+
34
+ signal_connect("key_press_event") { |_w, e|
35
+ hide_on_delete if e.keyval == Gdk::Keyval::GDK_Escape
36
+ }
37
+
38
+ vbox = Gtk::VBox.new(false, 10)
39
+ vbox.border_width = 10
40
+ add(vbox)
41
+
42
+ sw = Gtk::ScrolledWindow.new
43
+ sw.set_policy(Gtk::POLICY_AUTOMATIC, Gtk::POLICY_AUTOMATIC)
44
+ vbox.pack_start(sw)
45
+
46
+ @model = Gtk::ListStore.new(String, String)
47
+ @treeview = Gtk::TreeView.new(@model)
48
+ @treeview.enable_search = false
49
+ @treeview.rubber_banding = true
50
+ @treeview.reorderable = true
51
+ @treeview.selection.mode = Gtk::SELECTION_MULTIPLE
52
+
53
+ @treeview.signal_connect("row_activated") { |_w, p, c|
54
+ signal_emit("play_entry", @model.get_iter(p)[0])
55
+ }
56
+ @treeview.signal_connect("key_press_event") { |_w, e|
57
+ remove_selected if e.keyval == Gdk::Keyval::GDK_Delete
58
+ }
59
+ @treeview.signal_connect("button_press_event") { |_w, e|
60
+ @menu.popup(nil, nil, e.button, e.time) if
61
+ e.event_type == Gdk::Event::BUTTON_PRESS and e.button == 3
62
+ }
63
+
64
+ ["Name", "Length"].each_with_index { |_x, i|
65
+ renderer = Gtk::CellRendererText.new
66
+ column = Gtk::TreeViewColumn.new(_x,
67
+ renderer,
68
+ :text => i)
69
+ if _x == "Name"
70
+ renderer.ellipsize = Pango::ELLIPSIZE_MIDDLE
71
+ column.expand = true
72
+ column.set_cell_data_func(renderer) { |t, c, m, j|
73
+ c.text = File.basename(m.get_value(j, 0)) unless m.get_value(j, 0).nil?
74
+ }
75
+ end
76
+
77
+ @treeview.append_column(column)
78
+ }
79
+
80
+ sw.add(@treeview)
81
+
82
+ hbox = Gtk::HBox.new(true, 5)
83
+
84
+ buttons.each { |k, v|
85
+ button = Gtk::Button.new
86
+ button.image = Gtk::Image.new(k[0], Gtk::IconSize::BUTTON)
87
+ button.height_request = 36
88
+ button.set_tooltip_text(k[1])
89
+ button.signal_connect("clicked") { v.call }
90
+ hbox.pack_start(button)
91
+ }
92
+
93
+ vbox.pack_start(hbox, false)
94
+
95
+ @menu = Gtk::Menu.new
96
+ buttons.each { |k, v|
97
+ item = Gtk::ImageMenuItem.new(k[1])
98
+ item.image = Gtk::Image.new(k[0], Gtk::IconSize::MENU)
99
+ item.signal_connect("activate") { v.call }
100
+ @menu.append(item)
101
+ }
102
+ @menu.show_all
103
+
104
+ show_all if is_visible
105
+ end
106
+
107
+ def count
108
+ i = 0
109
+ @model.each { i += 1 }
110
+ return i
111
+ end
112
+
113
+ def add_file(file)
114
+ contains = false
115
+ @model.each { |m, p, iter|
116
+ if iter[0] == file
117
+ contains = true
118
+ break
119
+ end
120
+ }
121
+ unless contains
122
+ iter = @model.append
123
+ iter[0] = file
124
+ end
125
+ end
126
+
127
+ def move_selected_up
128
+ @treeview.selection.selected_rows.each { |path|
129
+ tmp = path.dup
130
+ break if not tmp.prev! or @treeview.selection.selected_rows.include?(tmp)
131
+ @model.move_before(@model.get_iter(path), @model.get_iter(tmp))
132
+ }
133
+ end
134
+
135
+ def move_selected_down
136
+ @treeview.selection.selected_rows.reverse.each { |path|
137
+ break if @treeview.selection.selected_rows.include?((tmp = path.dup.next!))
138
+ tmp = @model.get_iter(tmp)
139
+ @model.move_after(@model.get_iter(path), tmp) unless tmp.nil?
140
+ }
141
+ end
142
+
143
+ def remove_selected
144
+ to_remove = [ ]
145
+ @treeview.selection.selected_rows.each { |path|
146
+ to_remove.push(@model.get_iter(path))
147
+ }
148
+ to_remove.each { |iter|
149
+ signal_emit("playing_removed") if iter[0] == @playing
150
+ @model.remove(iter)
151
+ }
152
+ end
153
+
154
+ def get_next
155
+ @model.each { |m, p, iter|
156
+ return iter.next! ? iter[0] : nil if iter[0] == @playing
157
+ }
158
+ return nil
159
+ end
160
+
161
+ def get_prev
162
+ prev = nil
163
+ @model.each { |m, p, iter|
164
+ return prev if iter[0] == @playing
165
+ prev = iter[0]
166
+ }
167
+ end
168
+
169
+ def get_entries
170
+ entries = [ ]
171
+ @model.each { |m, p, iter| entries.push(iter[0]) }
172
+ return entries
173
+ end
174
+
175
+ def clear
176
+ @model.clear
177
+ signal_emit("playing_removed")
178
+ end
179
+
180
+ def set_selected(file)
181
+ @playing = file
182
+ i = 0
183
+ @model.each { |m, p, iter|
184
+ if iter[0] == @playing
185
+ @treeview.set_cursor(Gtk::TreePath.new(i), nil, false)
186
+ break
187
+ end
188
+ i += 1
189
+ }
190
+ end
191
+
192
+ def update_length(length)
193
+ @model.each { |m, p, iter|
194
+ if iter[0] == @playing
195
+ iter[1] = Time.at(length).utc.strftime("%H:%M:%S") unless length == 0
196
+ break
197
+ end
198
+ }
199
+ end
200
+
201
+ private
202
+ def signal_do_play_entry(file) end
203
+ def signal_do_playing_removed() end
204
+ def signal_do_open_file_chooser() end
205
+ end
206
+ end
207
+
@@ -0,0 +1,36 @@
1
+
2
+ module Ampv
3
+ class ProgressBarWidget < Gtk::DrawingArea
4
+ def initialize(bar_color, head_color, height)
5
+ super()
6
+ modify_bg(Gtk::STATE_NORMAL, Gdk::Color.parse("#000"))
7
+ set_height_request(height)
8
+
9
+ signal_connect("expose_event") {
10
+ @cx = window.create_cairo_context
11
+ draw_widget
12
+ }
13
+ @value = 0
14
+ @bar_color = bar_color
15
+ @head_color = head_color
16
+ end
17
+
18
+ def draw_widget
19
+ @cx.set_source_color(@bar_color)
20
+ @cx.rectangle(0, 0, allocation.width * @value.to_f, allocation.height)
21
+ @cx.fill
22
+
23
+ if @value > 0
24
+ @cx.set_source_color(@head_color)
25
+ @cx.rectangle(allocation.width * @value.to_f, 0, 2, allocation.height)
26
+ @cx.fill
27
+ end
28
+ end
29
+
30
+ def value=(v)
31
+ @value = v
32
+ queue_draw
33
+ end
34
+ end
35
+ end
36
+
@@ -0,0 +1,6 @@
1
+
2
+ module Ampv
3
+ PACKAGE = "ampv"
4
+ VERSION = "1.0.0"
5
+ end
6
+
metadata ADDED
@@ -0,0 +1,81 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ampv
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - ahoka
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2013-11-28 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: gtk2
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ! '>='
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ! '>='
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: ruby-fifo
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ! '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ! '>='
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ description: A minimal GTK2 mpv frontend.
42
+ email:
43
+ executables:
44
+ - ampv
45
+ extensions: []
46
+ extra_rdoc_files: []
47
+ files:
48
+ - LICENSE
49
+ - input.conf
50
+ - lib/ampv/version.rb
51
+ - lib/ampv/playlist.rb
52
+ - lib/ampv/progressbarwidget.rb
53
+ - lib/ampv/mpvwidget.rb
54
+ - lib/ampv.rb
55
+ - bin/ampv
56
+ homepage: https://github.com/ahodesuka/ampv
57
+ licenses:
58
+ - MIT
59
+ metadata: {}
60
+ post_install_message:
61
+ rdoc_options: []
62
+ require_paths:
63
+ - lib
64
+ required_ruby_version: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ! '>='
67
+ - !ruby/object:Gem::Version
68
+ version: 1.9.3
69
+ required_rubygems_version: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - ! '>='
72
+ - !ruby/object:Gem::Version
73
+ version: '0'
74
+ requirements:
75
+ - gtk2
76
+ rubyforge_project:
77
+ rubygems_version: 2.0.3
78
+ signing_key:
79
+ specification_version: 4
80
+ summary: ampv
81
+ test_files: []