ektoplayer 0.1.2 → 0.1.3
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 +4 -4
- data/README.md +19 -15
- data/lib/ektoplayer/application.rb +23 -2
- data/lib/ektoplayer/bindings.rb +3 -1
- data/lib/ektoplayer/config.rb +6 -3
- data/lib/ektoplayer/controllers/info.rb +2 -2
- data/lib/ektoplayer/controllers/mainwindow.rb +22 -7
- data/lib/ektoplayer/database.rb +8 -0
- data/lib/ektoplayer/events.rb +4 -0
- data/lib/ektoplayer/theme.rb +8 -7
- data/lib/ektoplayer/trackloader.rb +7 -1
- data/lib/ektoplayer/ui/widgets/container.rb +37 -3
- data/lib/ektoplayer/ui/widgets/labelwidget.rb +2 -0
- data/lib/ektoplayer/ui/widgets.rb +27 -1
- data/lib/ektoplayer/ui.rb +56 -45
- data/lib/ektoplayer/updater.rb +20 -3
- data/lib/ektoplayer/views/info.rb +8 -32
- data/lib/ektoplayer/views/mainwindow.rb +25 -34
- data/lib/ektoplayer/views/tabbar.rb +53 -0
- metadata +7 -6
- data/lib/ektoplayer/ui/widgets/tabbedcontainer.rb +0 -110
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 55540b5adfb0dec28702908ee968d434b802221a
|
4
|
+
data.tar.gz: 5f11aef936c061803e546ea22a431193fa106685
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: '098d4f6de14917ac00d6321729827ca49469628b26d9a1b16ba3054660886893f6f5e7ef1837cdc3ebacae2703b8b56e835d6ecec5243ec5ee45bccc5ab8dff8'
|
7
|
+
data.tar.gz: 1bf895998e866f025d9b6d13775c0d32a9352c21a0aaa7fa6d4743885775444f43a741f86fc1a8288d8251d3ab35e7883aac23eb92471ab87df3e945c0d9ba63
|
data/README.md
CHANGED
@@ -1,9 +1,25 @@
|
|
1
1
|
# Ektoplayer
|
2
2
|
|
3
|
-
Ektoplayer is a
|
3
|
+
Ektoplayer is a commandline client for [ektoplazm.com](http://www.ektoplazm.com), a website where you can listen and download freely licensed psytrance, techno and downtempo music.
|
4
4
|
|
5
|
-
|
6
|
-
|
5
|
+
It allows you to
|
6
|
+
* Search for tracks by tags (artist, album, style, ...)
|
7
|
+
* Play tracks located at ektoplazm.com
|
8
|
+
* Display information about albums
|
9
|
+
* Download a whole albums as mp3
|
10
|
+
|
11
|
+
## Features
|
12
|
+
|
13
|
+
* Mouse support
|
14
|
+
* Vi-like keybindings (`hjkl`, `^d`, `^u`, `/`, `?`, `n`, `N`, ...)
|
15
|
+
* Up to 256 colors are supported
|
16
|
+
* Local sound file cache
|
17
|
+
|
18
|
+
## Screenshots
|
19
|
+
|
20
|
+

|
21
|
+

|
22
|
+

|
7
23
|
|
8
24
|
## Requirements
|
9
25
|
|
@@ -28,17 +44,6 @@ library to compile the native extensions.
|
|
28
44
|
apt-get install ruby ruby-dev portaudio19-dev libmpg123-dev sqlite3 libsqlite3-dev libncurses-dev libz1g-dev build-essential
|
29
45
|
gem install ektoplayer
|
30
46
|
|
31
|
-
## Features
|
32
|
-
|
33
|
-
* Listen to ektoplazm tracks
|
34
|
-
* Download whole albums
|
35
|
-
* Browse database by tags
|
36
|
-
* Vi keybindings (`hjkl`, `^d`, `^u`, `/`, `?`, `n`, `N`, ...)
|
37
|
-
* Mouse is supported
|
38
|
-
* Supports 256/16/mono colors
|
39
|
-
* Local sound file cache and download archive
|
40
|
-
* Highly configurable
|
41
|
-
|
42
47
|
## Authors
|
43
48
|
|
44
49
|
* [Benjamin Abendroth](https://github.com/braph)
|
@@ -47,4 +52,3 @@ library to compile the native extensions.
|
|
47
52
|
|
48
53
|
* Ektoplayer was inspired by [Soundcloud2000](https://github.com/grobie/soundcloud2000) and [ncmpcpp](https://github.com/arybczak/ncmpcpp)
|
49
54
|
* It uses [Audite](https://github.com/georgi/audite) as playback engine and [Nokogiri](http://www.nokogiri.org/) for parsing HTML
|
50
|
-
|
@@ -10,7 +10,7 @@ require 'date'
|
|
10
10
|
|
11
11
|
module Ektoplayer
|
12
12
|
class Application
|
13
|
-
VERSION = '0.1.
|
13
|
+
VERSION = '0.1.4'.freeze
|
14
14
|
GITHUB_URL = 'https://github.com/braph/ektoplayer'.freeze
|
15
15
|
EKTOPLAZM_URL = 'http://www.ektoplazm.com'.freeze
|
16
16
|
|
@@ -35,6 +35,7 @@ module Ektoplayer
|
|
35
35
|
|
36
36
|
def run
|
37
37
|
#Thread.abort_on_exception=(true)
|
38
|
+
Thread.report_on_exception=(true) rescue nil
|
38
39
|
|
39
40
|
# make each configuration object globally accessible as a singleton
|
40
41
|
[Config, Bindings, Theme].each { |c| Common::mksingleton(c) }
|
@@ -99,13 +100,15 @@ module Ektoplayer
|
|
99
100
|
# next operations may take some time, espacially the ones
|
100
101
|
# using the database (browser), so we put this inside a thread
|
101
102
|
Thread.new do
|
103
|
+
begin
|
104
|
+
|
102
105
|
# ... controllers ...
|
103
106
|
view_ops = Operations::Operations.new
|
104
107
|
Controllers::MainWindow.new(main_w, view_ops)
|
105
108
|
Controllers::Browser.new(main_w.browser, browser, view_ops, operations)
|
106
109
|
Controllers::Playlist.new(main_w.playlist, playlist, view_ops, operations)
|
107
110
|
Controllers::Help.new(main_w.help, view_ops)
|
108
|
-
Controllers::Info.new(main_w.info, playlist, trackloader, database, view_ops)
|
111
|
+
Controllers::Info.new(main_w.info, player, playlist, trackloader, database, view_ops)
|
109
112
|
main_w.progressbar.attach(player)
|
110
113
|
main_w.volumemeter.attach(player)
|
111
114
|
main_w.playinginfo.attach(playlist, player)
|
@@ -115,6 +118,20 @@ module Ektoplayer
|
|
115
118
|
player.events.on(:stop) do |reason|
|
116
119
|
operations.send(:'playlist.play_next') if reason == :track_completed
|
117
120
|
end
|
121
|
+
|
122
|
+
if Config[:prefetch]
|
123
|
+
trackloader_mutex = Mutex.new
|
124
|
+
player.events.on(:position_change) do
|
125
|
+
Thread.new do
|
126
|
+
if player.length > 30 and player.position_percent > 0.8
|
127
|
+
trackloader_mutex.synchronize do
|
128
|
+
trackloader.get_track_file(playlist[playlist.get_next_pos]['url'])
|
129
|
+
sleep 5
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
118
135
|
|
119
136
|
# ... bindings ...
|
120
137
|
Bindings.bind_view(:global, main_w, view_ops, operations)
|
@@ -135,6 +152,10 @@ module Ektoplayer
|
|
135
152
|
r = client.database.select(order_by: 'date', limit: n)
|
136
153
|
playlist.add(*r)
|
137
154
|
end
|
155
|
+
|
156
|
+
rescue
|
157
|
+
Application.log(self, $!)
|
158
|
+
end
|
138
159
|
end
|
139
160
|
|
140
161
|
UI::Canvas.update_screen
|
data/lib/ektoplayer/bindings.rb
CHANGED
@@ -41,6 +41,7 @@ module Ektoplayer
|
|
41
41
|
reg 'playinginfo.toggle', 'Toggle playinginfo visibility'
|
42
42
|
reg 'progressbar.toggle', 'Toggle progressbar visibility'
|
43
43
|
reg 'volumemeter.toggle', 'Toggle volumemeter visibility'
|
44
|
+
reg 'tabbar.toggle', 'Toggle tabbar visibility'
|
44
45
|
|
45
46
|
reg 'playlist.goto_current', 'Go to current playing track'
|
46
47
|
reg 'playlist.clear', 'Delete all items in playlist'
|
@@ -78,7 +79,8 @@ module Ektoplayer
|
|
78
79
|
|
79
80
|
:'playinginfo.toggle' => [ Curses::KEY_F2 ],
|
80
81
|
:'progressbar.toggle' => [ Curses::KEY_F3 ],
|
81
|
-
:'
|
82
|
+
:'tabbar.toggle' => [ Curses::KEY_F4 ],
|
83
|
+
:'volumemeter.toggle' => [ Curses::KEY_F5 ],
|
82
84
|
|
83
85
|
:'player.forward' => [?f, Curses::KEY_RIGHT ],
|
84
86
|
:'player.backward' => [?b, Curses::KEY_LEFT ],
|
data/lib/ektoplayer/config.rb
CHANGED
@@ -122,6 +122,9 @@ module Ektoplayer
|
|
122
122
|
reg :use_cache,
|
123
123
|
'Enable/disable local mp3 cache', true
|
124
124
|
|
125
|
+
reg :prefetch,
|
126
|
+
'Enable prefetching next track do be played', true
|
127
|
+
|
125
128
|
reg :small_update_pages,
|
126
129
|
'How many pages should be fetched after start', 5
|
127
130
|
|
@@ -177,8 +180,8 @@ module Ektoplayer
|
|
177
180
|
'Format of second line in playinginfo', DEFAULT_PLAYINGINFO_FORMAT2,
|
178
181
|
ColumnFormat.method(:parse_simple_format)
|
179
182
|
|
180
|
-
# -
|
181
|
-
reg '
|
183
|
+
# - Tabbar
|
184
|
+
reg 'tabbar.display',
|
182
185
|
'Enable/disable tabbar', true
|
183
186
|
|
184
187
|
reg 'tabs.widgets', 'Specify widget order of tabbar (left to right)',
|
@@ -186,7 +189,7 @@ module Ektoplayer
|
|
186
189
|
lambda { |v| v.split(/\s*,\s*/).map(&:to_sym) }
|
187
190
|
|
188
191
|
reg 'main.widgets', 'Specify widgets to show (up to down)',
|
189
|
-
'playinginfo,progressbar,
|
192
|
+
'playinginfo,progressbar,tabbar,windows,volumemeter',
|
190
193
|
lambda { |v| v.split(/\s*,\s*/).map(&:to_sym) }
|
191
194
|
end
|
192
195
|
|
@@ -5,9 +5,9 @@ require_relative 'controller'
|
|
5
5
|
module Ektoplayer
|
6
6
|
module Controllers
|
7
7
|
class Info < Controller
|
8
|
-
def initialize(view, playlist, trackloader, database, view_operations)
|
8
|
+
def initialize(view, player, playlist, trackloader, database, view_operations)
|
9
9
|
super(view)
|
10
|
-
view.attach(playlist, trackloader, database)
|
10
|
+
view.attach(player, playlist, trackloader, database)
|
11
11
|
register = view_operations.with_register('info.')
|
12
12
|
%w(up down page_up page_down top bottom).
|
13
13
|
each { |op| register.(op, &view.method(op)) }
|
@@ -6,13 +6,20 @@ module Ektoplayer
|
|
6
6
|
def initialize(view, view_operations)
|
7
7
|
super(view)
|
8
8
|
ops = view_operations
|
9
|
-
ops.reg('splash.show') { view.
|
10
|
-
ops.reg('playlist.show') { view.
|
11
|
-
ops.reg('browser.show') { view.
|
12
|
-
ops.reg('info.show') { view.
|
13
|
-
ops.reg('help.show') { view.
|
14
|
-
ops.reg('tabs.next') { view.
|
15
|
-
ops.reg('tabs.prev') { view.
|
9
|
+
ops.reg('splash.show') { view.windows.selected=(view.splash) }
|
10
|
+
ops.reg('playlist.show') { view.windows.selected=(view.playlist) }
|
11
|
+
ops.reg('browser.show') { view.windows.selected=(view.browser) }
|
12
|
+
ops.reg('info.show') { view.windows.selected=(view.info) }
|
13
|
+
ops.reg('help.show') { view.windows.selected=(view.help) }
|
14
|
+
ops.reg('tabs.next') { view.windows.select_next }
|
15
|
+
ops.reg('tabs.prev') { view.windows.select_prev }
|
16
|
+
|
17
|
+
ops.reg('tabbar.toggle') do
|
18
|
+
view.with_lock do
|
19
|
+
view.tabbar.visible=(!view.tabbar.visible?)
|
20
|
+
view.want_layout
|
21
|
+
end
|
22
|
+
end
|
16
23
|
|
17
24
|
ops.reg('playinginfo.toggle') do
|
18
25
|
view.with_lock do
|
@@ -34,6 +41,14 @@ module Ektoplayer
|
|
34
41
|
view.want_layout
|
35
42
|
end
|
36
43
|
end
|
44
|
+
|
45
|
+
view.tabbar.events.on(:tab_clicked) do |index|
|
46
|
+
view.windows.selected_index=(index)
|
47
|
+
end
|
48
|
+
|
49
|
+
view.windows.events.on(:changed) do |index|
|
50
|
+
view.tabbar.selected=(index)
|
51
|
+
end
|
37
52
|
end
|
38
53
|
end
|
39
54
|
end
|
data/lib/ektoplayer/database.rb
CHANGED
@@ -117,6 +117,14 @@ module Ektoplayer
|
|
117
117
|
each { |t| @db.execute("DROP TABLE IF EXISTS #{t}") }
|
118
118
|
end
|
119
119
|
|
120
|
+
def transaction
|
121
|
+
@db.transaction rescue Application.log(self, $!)
|
122
|
+
end
|
123
|
+
|
124
|
+
def commit
|
125
|
+
@db.commit rescue Application.log(self, $!)
|
126
|
+
end
|
127
|
+
|
120
128
|
def insert_into(table, hash, mode: :insert)
|
121
129
|
cols = ?( + (hash.keys * ?,) + ?)
|
122
130
|
values = ?( + (([??] * hash.size) * ?,) + ?)
|
data/lib/ektoplayer/events.rb
CHANGED
@@ -16,11 +16,13 @@ class Events
|
|
16
16
|
# Disables auto creation of non existent events
|
17
17
|
def no_auto_create
|
18
18
|
@map.default_proc = proc { |h,k| fail KeyError, "Unknown event: #{k}" }
|
19
|
+
self
|
19
20
|
end
|
20
21
|
|
21
22
|
# Enables auto creation of non existent events
|
22
23
|
def auto_create
|
23
24
|
@map.default_proc = proc { |h,k| h[k] = [] }
|
25
|
+
self
|
24
26
|
end
|
25
27
|
|
26
28
|
# Registers a new event
|
@@ -52,5 +54,7 @@ class Events
|
|
52
54
|
if @map.key?(event)
|
53
55
|
@map[event].each { |callback| callback.call(*args) }
|
54
56
|
end
|
57
|
+
rescue
|
58
|
+
Ektoplayer::Application.log(self, 'event hook failed', $!)
|
55
59
|
end
|
56
60
|
end
|
data/lib/ektoplayer/theme.rb
CHANGED
@@ -8,7 +8,8 @@ module Ektoplayer
|
|
8
8
|
@current = 0
|
9
9
|
@theme = {
|
10
10
|
0 => { default: [-1, -1].freeze,
|
11
|
-
:'url' => [-1, -1, :underline ].freeze
|
11
|
+
:'url' => [-1, -1, :underline ].freeze,
|
12
|
+
:'tabbar.selected' => [-1, -1, :bold ].freeze},
|
12
13
|
8 => { default: [-1, -1].freeze,
|
13
14
|
:'url' => [:magenta, -1, :underline].freeze,
|
14
15
|
|
@@ -26,8 +27,8 @@ module Ektoplayer
|
|
26
27
|
:'volumemeter.level' => [:magenta ].freeze,
|
27
28
|
:'volumemeter.rest' => [:black ].freeze,
|
28
29
|
|
29
|
-
:'
|
30
|
-
:'
|
30
|
+
:'tabbar.selected' => [:blue ].freeze,
|
31
|
+
:'tabbar.unselected' => [:none ].freeze,
|
31
32
|
|
32
33
|
:'list.item_even' => [:blue ].freeze,
|
33
34
|
:'list.item_odd' => [:blue ].freeze,
|
@@ -56,8 +57,8 @@ module Ektoplayer
|
|
56
57
|
:'volumemeter.level' => [:magenta ].freeze,
|
57
58
|
:'volumemeter.rest' => [236 ].freeze,
|
58
59
|
|
59
|
-
:'
|
60
|
-
:'
|
60
|
+
:'tabbar.selected' => [75 ].freeze,
|
61
|
+
:'tabbar.unselected' => [250 ].freeze,
|
61
62
|
|
62
63
|
:'list.item_even' => [:blue ].freeze,
|
63
64
|
:'list.item_odd' => [25 ].freeze,
|
@@ -77,10 +78,10 @@ module Ektoplayer
|
|
77
78
|
def color_256(*args) color(*args, theme: 256) end
|
78
79
|
|
79
80
|
def get(theme_def) UI::Colors.get(theme_def) end
|
80
|
-
|
81
|
+
def [](theme_def) UI::Colors.get(theme_def) end
|
81
82
|
|
82
83
|
def use_colors(colors)
|
83
|
-
fail ArgumentError unless @theme[colors]
|
84
|
+
fail ArgumentError, 'unknown theme' unless @theme[colors]
|
84
85
|
@current = colors
|
85
86
|
|
86
87
|
UI::Colors.reset
|
@@ -55,6 +55,8 @@ module Ektoplayer
|
|
55
55
|
end
|
56
56
|
|
57
57
|
@downloads << dl.start!
|
58
|
+
rescue
|
59
|
+
Application.log(self, $!)
|
58
60
|
end
|
59
61
|
|
60
62
|
def get_track_file(url, reload: false)
|
@@ -97,6 +99,8 @@ module Ektoplayer
|
|
97
99
|
@downloads << dl.start!
|
98
100
|
|
99
101
|
return temp_file
|
102
|
+
rescue
|
103
|
+
Application.log(self, $!)
|
100
104
|
end
|
101
105
|
end
|
102
106
|
|
@@ -113,6 +117,8 @@ module Ektoplayer
|
|
113
117
|
end
|
114
118
|
|
115
119
|
def start!
|
120
|
+
Application.log(self, 'starting download: ', @url)
|
121
|
+
|
116
122
|
Thread.new do
|
117
123
|
begin
|
118
124
|
http = Net::HTTP.new(@url.host, @url.port)
|
@@ -139,7 +145,7 @@ module Ektoplayer
|
|
139
145
|
end
|
140
146
|
|
141
147
|
sleep 0.1 while @total.nil?
|
142
|
-
sleep 0.
|
148
|
+
sleep 0.2
|
143
149
|
self
|
144
150
|
end
|
145
151
|
end
|
@@ -6,6 +6,7 @@ module UI
|
|
6
6
|
|
7
7
|
def initialize(widgets: [], **opts)
|
8
8
|
super(**opts)
|
9
|
+
events.register(:changed)
|
9
10
|
@selected, @selected_index, @widgets = nil, nil, widgets
|
10
11
|
end
|
11
12
|
|
@@ -24,6 +25,7 @@ module UI
|
|
24
25
|
end
|
25
26
|
|
26
27
|
@selected_index = index
|
28
|
+
trigger(@events, :changed, @selected_index)
|
27
29
|
want_layout
|
28
30
|
end
|
29
31
|
end
|
@@ -39,6 +41,7 @@ module UI
|
|
39
41
|
end
|
40
42
|
|
41
43
|
@selected = widget
|
44
|
+
trigger(@events, :changed, @selected_index)
|
42
45
|
want_layout
|
43
46
|
end
|
44
47
|
end
|
@@ -70,19 +73,19 @@ module UI
|
|
70
73
|
|
71
74
|
def draw; visible_widgets.each(&:draw) end
|
72
75
|
def refresh; visible_widgets.each(&:refresh) end
|
73
|
-
def layout;
|
76
|
+
def layout; @widgets.each(&:layout) end
|
74
77
|
|
75
78
|
def on_key_press(key)
|
76
79
|
@selected.key_press(key) if @selected
|
77
80
|
super(key)
|
78
81
|
end
|
79
82
|
|
80
|
-
def
|
83
|
+
def select_next
|
81
84
|
return unless @selected
|
82
85
|
self.selected_index=((@selected_index + 1) % @widgets.size)
|
83
86
|
end
|
84
87
|
|
85
|
-
def
|
88
|
+
def select_prev
|
86
89
|
return unless @selected
|
87
90
|
return self.selected_index=(@widgets.size - 1) if @selected_index == 0
|
88
91
|
self.selected_index=(@selected_index - 1)
|
@@ -122,4 +125,35 @@ module UI
|
|
122
125
|
super
|
123
126
|
end
|
124
127
|
end
|
128
|
+
|
129
|
+
class SwitchContainer < GenericContainer
|
130
|
+
def layout
|
131
|
+
@widgets.each do |widget|
|
132
|
+
widget.with_lock do
|
133
|
+
widget.size=(@size)
|
134
|
+
widget.pos=(@pos)
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
super
|
139
|
+
end
|
140
|
+
|
141
|
+
def selected=(widget)
|
142
|
+
with_lock do
|
143
|
+
(@selected.invisible!) if @selected
|
144
|
+
super(widget)
|
145
|
+
(@selected.visible!) if @selected
|
146
|
+
want_layout
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
def selected_index=(index)
|
151
|
+
with_lock do
|
152
|
+
(@selected.invisible!) if @selected
|
153
|
+
super(index)
|
154
|
+
(@selected.visible!) if @selected
|
155
|
+
want_layout
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
125
159
|
end
|
@@ -3,8 +3,8 @@ module UI
|
|
3
3
|
WANT_REFRESH, WANT_REDRAW, WANT_LAYOUT = 1, 2, 4
|
4
4
|
|
5
5
|
attr_reader :pos, :size
|
6
|
+
def events; @events ||= Events.new.no_auto_create end
|
6
7
|
def keys; @keys ||= Events.new end
|
7
|
-
def events; @events ||= Events.new end
|
8
8
|
def mouse; @mouse ||= MouseEvents.new end
|
9
9
|
def mouse_section; @mouse_section ||= MouseSectionEvents.new end
|
10
10
|
|
@@ -184,6 +184,32 @@ module UI
|
|
184
184
|
self.pad_minrow=(new_minrow)
|
185
185
|
end
|
186
186
|
|
187
|
+
def with_mouse_section_event
|
188
|
+
start_cursor = @win.cursor; yield
|
189
|
+
|
190
|
+
start_pos = UI::Point.new(
|
191
|
+
y: [start_cursor.y, @win.cursor.y].min,
|
192
|
+
x: [start_cursor.x, @win.cursor.x].min,
|
193
|
+
)
|
194
|
+
stop_pos = UI::Point.new(
|
195
|
+
y: [start_cursor.y, @win.cursor.y].max,
|
196
|
+
x: [start_cursor.x, @win.cursor.x].max
|
197
|
+
)
|
198
|
+
|
199
|
+
ev = UI::MouseSectionEvent.new(start_pos, stop_pos)
|
200
|
+
mouse_section.add(ev)
|
201
|
+
ev
|
202
|
+
end
|
203
|
+
|
204
|
+
def mouse_click(mevent)
|
205
|
+
if ev = mouse_event_transform(mevent)
|
206
|
+
ev.x += @pad_mincol
|
207
|
+
ev.y += @pad_minrow
|
208
|
+
trigger(@mouse, ev)
|
209
|
+
trigger(@mouse_section, ev)
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
187
213
|
def refresh
|
188
214
|
@win.noutrefresh(
|
189
215
|
@pad_minrow, @pad_mincol,
|
data/lib/ektoplayer/ui.rb
CHANGED
@@ -30,7 +30,7 @@ module UI
|
|
30
30
|
each {|_|Curses.send(_)}
|
31
31
|
Curses.mousemask(Curses::ALL_MOUSE_EVENTS)
|
32
32
|
Curses.stdscr.keypad(true)
|
33
|
-
|
33
|
+
UI::Colors.start
|
34
34
|
|
35
35
|
self.enable_resize_detection
|
36
36
|
end
|
@@ -66,6 +66,8 @@ module UI
|
|
66
66
|
Curses.clear
|
67
67
|
Curses.addstr('terminal too small!')
|
68
68
|
Curses.refresh
|
69
|
+
rescue
|
70
|
+
nil
|
69
71
|
end
|
70
72
|
|
71
73
|
def self.sub(cls, **opts)
|
@@ -92,6 +94,8 @@ module UI
|
|
92
94
|
rescue UI::WidgetSizeError
|
93
95
|
Curses.clear
|
94
96
|
Curses.addstr('terminal too small!')
|
97
|
+
rescue
|
98
|
+
Application.log(self, $!)
|
95
99
|
end
|
96
100
|
|
97
101
|
Curses.doupdate
|
@@ -115,26 +119,19 @@ module UI
|
|
115
119
|
KEYMAP_WORKAROUND.default_proc = proc { |h,k| k }
|
116
120
|
KEYMAP_WORKAROUND.freeze
|
117
121
|
|
118
|
-
def self.start
|
119
|
-
@@mode = :curses
|
120
|
-
Readline.input, @@readline_in_write = IO.pipe
|
121
|
-
Readline.output = File.open(File::NULL, ?w)
|
122
|
-
end
|
123
|
-
|
124
122
|
#def self.getch(timeout=-1)
|
125
123
|
# KEYMAP_WORKAROUND[@@widget.getch(timeout)]
|
126
124
|
#end
|
127
125
|
|
128
126
|
def self.start_loop
|
129
|
-
@@
|
130
|
-
@@readline_cond ||= ConditionVariable.new
|
127
|
+
@@readline_obj ||= ReadlineWindow.new
|
131
128
|
|
132
129
|
loop do
|
133
|
-
|
130
|
+
unless @@readline_obj.active?
|
134
131
|
Curses.curs_set(0)
|
135
132
|
Curses.nonl
|
136
133
|
|
137
|
-
|
134
|
+
begin
|
138
135
|
UI::Canvas.widget.win.keypad=(true)
|
139
136
|
c = KEYMAP_WORKAROUND[UI::Canvas.widget.win.getch1]
|
140
137
|
|
@@ -145,65 +142,79 @@ module UI
|
|
145
142
|
elsif c # (not nil)
|
146
143
|
UI::Canvas.widget.key_press(c.is_a?(Integer) ? c : c.to_sym)
|
147
144
|
end
|
148
|
-
end
|
145
|
+
end while !@@readline_obj.active?
|
149
146
|
else
|
150
147
|
Curses.curs_set(1)
|
151
148
|
Curses.nl
|
152
149
|
|
153
|
-
|
150
|
+
begin
|
154
151
|
win = UI::Canvas.widget.win
|
155
152
|
win.keypad=(false)
|
156
|
-
c = win.getch1
|
153
|
+
next unless (c = win.getch1)
|
157
154
|
|
158
155
|
if c == 10 or c == 4
|
159
|
-
@@
|
160
|
-
@@mode = :curses
|
156
|
+
@@readline_obj.feed(?\n)
|
161
157
|
else
|
162
|
-
@@
|
158
|
+
@@readline_obj.feed(c.chr)
|
163
159
|
|
164
160
|
if c == 27 # pass 3-character escape sequence
|
165
161
|
if c = win.getch1(1)
|
166
|
-
@@
|
162
|
+
@@readline_obj.feed(c.chr)
|
167
163
|
if c = win.getch1(1)
|
168
|
-
@@
|
164
|
+
@@readline_obj.feed(c.chr)
|
169
165
|
end
|
170
166
|
end
|
171
167
|
end
|
172
168
|
end
|
173
|
-
|
174
|
-
@@readline_cond.signal
|
175
|
-
end
|
169
|
+
end while @@readline_obj.active?
|
176
170
|
end
|
177
171
|
end
|
178
172
|
end
|
179
173
|
|
180
|
-
def self.readline(
|
181
|
-
@@
|
174
|
+
def self.readline(*args, **opts, &block)
|
175
|
+
(@@readline_obj ||= ReadlineWindow.new).readline(*args, **opts, &block)
|
176
|
+
end
|
177
|
+
end
|
182
178
|
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
179
|
+
class ReadlineWindow
|
180
|
+
def initialize
|
181
|
+
@mutex, @cond = Mutex.new, ConditionVariable.new
|
182
|
+
Readline.input, @readline_in_write = IO.pipe
|
183
|
+
Readline.output = File.open(File::NULL, ?w)
|
184
|
+
@thread = nil
|
185
|
+
end
|
186
|
+
|
187
|
+
def active?; @thread; end
|
188
|
+
|
189
|
+
def readline(pos, size, prompt: '', add_hist: false, &block)
|
190
|
+
@thread ||= Thread.new do
|
191
|
+
window = Curses::Window.new(size.height, size.width, pos.y, pos.x)
|
192
|
+
|
193
|
+
Readline.set_screen_size(size.height, size.width)
|
194
|
+
Readline.delete_text
|
195
|
+
rlt = Thread.new { Readline.readline(prompt, add_hist) }
|
196
|
+
|
197
|
+
while rlt.alive?
|
198
|
+
window.erase
|
199
|
+
buffer = "#{prompt}#{Readline.line_buffer}"
|
200
|
+
window << buffer[(buffer.size - size.width).clamp(0, buffer.size)..-1]
|
201
|
+
window.cursor=(Point.new(x: Readline.point + prompt.size, y: 0))
|
202
|
+
window.refresh
|
203
|
+
@mutex.synchronize { @cond.wait(@mutex, 0.2) }
|
204
204
|
end
|
205
|
+
|
206
|
+
window.clear
|
207
|
+
block.(Readline.line_buffer)
|
208
|
+
UI::Canvas.update_screen(true)
|
209
|
+
@thread = nil
|
205
210
|
end
|
206
211
|
end
|
212
|
+
|
213
|
+
def feed(c)
|
214
|
+
@readline_in_write.write(c)
|
215
|
+
@cond.signal
|
216
|
+
@thread = nil if c == ?\n
|
217
|
+
end
|
207
218
|
end
|
208
219
|
|
209
220
|
class Output
|
data/lib/ektoplayer/updater.rb
CHANGED
@@ -22,6 +22,7 @@ module Ektoplayer
|
|
22
22
|
def update(start_url: FREE_MUSIC_URL, pages: 0, parallel: 10)
|
23
23
|
queue = parallel > 0 ? SizedQueue.new(parallel) : Queue.new
|
24
24
|
insert_browserpage(bp = BrowsePage.new(start_url))
|
25
|
+
results = Queue.new
|
25
26
|
|
26
27
|
if pages > 0
|
27
28
|
bp.page_urls[(bp.current_page_index + 1)..(bp.current_page_index + pages + 1)]
|
@@ -30,13 +31,26 @@ module Ektoplayer
|
|
30
31
|
end.
|
31
32
|
each do |url|
|
32
33
|
queue << Thread.new do
|
33
|
-
|
34
|
+
results << BrowsePage.new(url)
|
34
35
|
queue.pop # unregister our thread
|
36
|
+
end.priority=(-10)
|
37
|
+
|
38
|
+
if results.size > 40
|
39
|
+
@db.transaction
|
40
|
+
40.times { insert_browserpage(results.pop(true)) }
|
41
|
+
@db.commit
|
35
42
|
end
|
36
43
|
end
|
37
44
|
|
38
45
|
sleep 1 while not queue.empty?
|
39
|
-
|
46
|
+
|
47
|
+
@db.transaction
|
48
|
+
while (result = queue.pop(true) rescue nil)
|
49
|
+
insert_browserpage(result)
|
50
|
+
end
|
51
|
+
@db.commit
|
52
|
+
rescue
|
53
|
+
Application.log(self, $!)
|
40
54
|
end
|
41
55
|
|
42
56
|
private def insert_browserpage(browserpage)
|
@@ -45,7 +59,8 @@ module Ektoplayer
|
|
45
59
|
end
|
46
60
|
|
47
61
|
browserpage.albums.each { |album| insert_album album }
|
48
|
-
rescue
|
62
|
+
rescue
|
63
|
+
Application.log(self, $!)
|
49
64
|
end
|
50
65
|
|
51
66
|
private def insert_album(album)
|
@@ -72,6 +87,8 @@ module Ektoplayer
|
|
72
87
|
track_r[:album_url] = album[:url]
|
73
88
|
@db.replace_into(:tracks, track_r)
|
74
89
|
end
|
90
|
+
rescue
|
91
|
+
Application.log(self, $!)
|
75
92
|
end
|
76
93
|
end
|
77
94
|
end
|
@@ -1,5 +1,4 @@
|
|
1
1
|
require_relative '../ui/widgets'
|
2
|
-
require_relative '../bindings'
|
3
2
|
require_relative '../theme'
|
4
3
|
require_relative '../common'
|
5
4
|
|
@@ -16,12 +15,13 @@ module Ektoplayer
|
|
16
15
|
|
17
16
|
module Views
|
18
17
|
class Info < UI::Pad
|
19
|
-
def attach(playlist, trackloader, database)
|
20
|
-
@playlist, @trackloader, @database =
|
18
|
+
def attach(player, playlist, trackloader, database)
|
19
|
+
@player, @playlist, @trackloader, @database =
|
20
|
+
player, playlist, trackloader, database
|
21
21
|
|
22
22
|
Thread.new do
|
23
23
|
loop { sleep 1; with_lock { want_redraw } }
|
24
|
-
end
|
24
|
+
end.priority=(-10)
|
25
25
|
end
|
26
26
|
|
27
27
|
def draw_heading(heading)
|
@@ -77,33 +77,9 @@ module Ektoplayer
|
|
77
77
|
end
|
78
78
|
end
|
79
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
80
|
def draw
|
81
|
+
return unless @player
|
82
|
+
|
107
83
|
self.pad_size=(UI::Size.new(
|
108
84
|
height: 200,
|
109
85
|
width: [@size.width, MIN_WIDTH].max
|
@@ -121,7 +97,7 @@ module Ektoplayer
|
|
121
97
|
draw_tag('Artist', @track['artist'])
|
122
98
|
draw_tag('Album', @track['album'])
|
123
99
|
draw_tag('BPM', @track['bpm'])
|
124
|
-
draw_tag('Length', Common::to_time(@length))
|
100
|
+
draw_tag('Length', Common::to_time(@player.length))
|
125
101
|
@win.next_line
|
126
102
|
|
127
103
|
draw_heading('Current album')
|
@@ -137,7 +113,7 @@ module Ektoplayer
|
|
137
113
|
draw_tag('Posted by'); draw_url(url, @track['posted_by'])
|
138
114
|
end
|
139
115
|
|
140
|
-
draw_tag('Styles', @track['styles'].
|
116
|
+
draw_tag('Styles', @track['styles'].gsub(?,, ', '))
|
141
117
|
draw_tag('Downloads', @track['download_count'])
|
142
118
|
draw_tag('Rating', "%0.2d%% (%d Votes)" % [@track['rating'], @track['votes']])
|
143
119
|
draw_tag('Cover'); draw_url(@track['cover_url'], 'Cover')
|
@@ -1,61 +1,52 @@
|
|
1
|
-
%w( ../ui/widgets/
|
2
|
-
volumemeter splash playlist browser info help ).
|
1
|
+
%w( ../ui/widgets/container playinginfo progressbar
|
2
|
+
volumemeter splash playlist browser info help tabbar ).
|
3
3
|
each {|_|require_relative(_)}
|
4
4
|
|
5
5
|
module Ektoplayer
|
6
6
|
module Views
|
7
7
|
class MainWindow < UI::VerticalContainer
|
8
|
-
attr_reader :progressbar, :volumemeter, :playinginfo
|
9
|
-
attr_reader :
|
8
|
+
attr_reader :progressbar, :volumemeter, :playinginfo, :tabbar
|
9
|
+
attr_reader :windows, :splash, :playlist, :browser, :info, :help
|
10
10
|
|
11
11
|
def initialize(**opts)
|
12
12
|
super(**opts)
|
13
13
|
|
14
|
-
s1 = UI::Size.new(height: 1, width: 1) # TODO.....!!
|
15
|
-
|
16
14
|
@playinginfo = sub(PlayingInfo)
|
17
15
|
@progressbar = sub(ProgressBar)
|
18
16
|
@volumemeter = sub(VolumeMeter)
|
19
|
-
@
|
20
|
-
@
|
21
|
-
@
|
22
|
-
@
|
23
|
-
@
|
24
|
-
@
|
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
|
-
)
|
17
|
+
@tabbar = sub(TabBar)
|
18
|
+
@windows = sub(UI::SwitchContainer)
|
19
|
+
@help = @windows.sub(Help, visible: false)
|
20
|
+
@info = @windows.sub(Info, visible: false)
|
21
|
+
@splash = @windows.sub(Splash, visible: false)
|
22
|
+
@browser = @windows.sub(Browser, visible: false)
|
23
|
+
@playlist = @windows.sub(Playlist, visible: false)
|
31
24
|
|
32
25
|
Config[:'tabs.widgets'].each do |widget|
|
33
|
-
@
|
26
|
+
@windows.add(send(widget))
|
27
|
+
@tabbar.add(widget)
|
34
28
|
end
|
35
29
|
|
36
30
|
Config[:'main.widgets'].each { |w| add(send(w)) }
|
37
|
-
|
31
|
+
|
32
|
+
@windows.selected=(@splash)
|
33
|
+
self.selected=(@windows)
|
38
34
|
end
|
39
35
|
|
40
36
|
def layout
|
41
37
|
height = @size.height
|
42
38
|
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
39
|
+
@playinginfo.size=(@size.update(height: 2))
|
40
|
+
@volumemeter.size=(@size.update(height: 1))
|
41
|
+
@progressbar.size=(@size.update(height: 1))
|
42
|
+
@tabbar.size=(@size.update(height: 1))
|
47
43
|
|
48
|
-
if @
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
if @progressbar.visible?
|
54
|
-
@progressbar.size=(@size.update(height: 1))
|
55
|
-
height -= 1
|
56
|
-
end
|
44
|
+
height -= 2 if @playinginfo.visible?
|
45
|
+
height -= 1 if @volumemeter.visible?
|
46
|
+
height -= 1 if @progressbar.visible?
|
47
|
+
height -= 1 if @tabbar.visible?
|
57
48
|
|
58
|
-
@
|
49
|
+
@windows.size=(@size.update(height: height))
|
59
50
|
|
60
51
|
super
|
61
52
|
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require_relative '../ui/widgets'
|
2
|
+
require_relative '../theme'
|
3
|
+
|
4
|
+
module Ektoplayer
|
5
|
+
module Views
|
6
|
+
class TabBar < UI::Pad
|
7
|
+
def initialize(**opts)
|
8
|
+
super(**opts)
|
9
|
+
events.register(:tab_clicked)
|
10
|
+
@selected, @tabs = 0, []
|
11
|
+
end
|
12
|
+
|
13
|
+
def add(title)
|
14
|
+
with_lock do
|
15
|
+
@tabs << title
|
16
|
+
want_redraw
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def selected=(index)
|
21
|
+
index = index.clamp(0, @tabs.size - 1)
|
22
|
+
return if index == @selected
|
23
|
+
|
24
|
+
with_lock do
|
25
|
+
@selected = index
|
26
|
+
want_redraw
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def draw
|
31
|
+
self.pad_size=(@size.update(height: 1))
|
32
|
+
mouse_section.clear
|
33
|
+
@win.erase
|
34
|
+
@win.setpos(0,0)
|
35
|
+
|
36
|
+
@tabs.each_with_index do |title, i|
|
37
|
+
mevent = with_mouse_section_event do
|
38
|
+
if i == @selected
|
39
|
+
@win.with_attr(Theme[:'tabbar.selected']) { @win << title.to_s }
|
40
|
+
else
|
41
|
+
@win.with_attr(Theme[:'tabbar.unselected']) { @win << title.to_s }
|
42
|
+
end
|
43
|
+
|
44
|
+
@win.addch(' ')
|
45
|
+
end
|
46
|
+
mevent.on(Curses::BUTTON1_CLICKED) do
|
47
|
+
trigger(@events, :tab_clicked, i)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ektoplayer
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Benjamin Abendroth
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-03-
|
11
|
+
date: 2017-03-20 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: audite
|
@@ -80,7 +80,8 @@ dependencies:
|
|
80
80
|
- - "~>"
|
81
81
|
- !ruby/object:Gem::Version
|
82
82
|
version: '1.2'
|
83
|
-
description:
|
83
|
+
description: Ektoplayer is a commandline client for http://ektoplazm.com, a website
|
84
|
+
providing free electronic music such as techno, goa and psy-trance
|
84
85
|
email: braph93@gmx.de
|
85
86
|
executables:
|
86
87
|
- ektoplayer
|
@@ -126,7 +127,6 @@ files:
|
|
126
127
|
- lib/ektoplayer/ui/widgets/container.rb
|
127
128
|
- lib/ektoplayer/ui/widgets/labelwidget.rb
|
128
129
|
- lib/ektoplayer/ui/widgets/listwidget.rb
|
129
|
-
- lib/ektoplayer/ui/widgets/tabbedcontainer.rb
|
130
130
|
- lib/ektoplayer/updater.rb
|
131
131
|
- lib/ektoplayer/views/browser.rb
|
132
132
|
- lib/ektoplayer/views/help.rb
|
@@ -136,9 +136,10 @@ files:
|
|
136
136
|
- lib/ektoplayer/views/playlist.rb
|
137
137
|
- lib/ektoplayer/views/progressbar.rb
|
138
138
|
- lib/ektoplayer/views/splash.rb
|
139
|
+
- lib/ektoplayer/views/tabbar.rb
|
139
140
|
- lib/ektoplayer/views/trackrenderer.rb
|
140
141
|
- lib/ektoplayer/views/volumemeter.rb
|
141
|
-
homepage: http://
|
142
|
+
homepage: http://github.com/braph/ektoplayer
|
142
143
|
licenses:
|
143
144
|
- GPL-3.0
|
144
145
|
metadata: {}
|
@@ -161,5 +162,5 @@ rubyforge_project:
|
|
161
162
|
rubygems_version: 2.6.8
|
162
163
|
signing_key:
|
163
164
|
specification_version: 4
|
164
|
-
summary: play music from ektoplazm.com
|
165
|
+
summary: play or download music from ektoplazm.com
|
165
166
|
test_files: []
|
@@ -1,110 +0,0 @@
|
|
1
|
-
require_relative 'container'
|
2
|
-
require_relative 'labelwidget'
|
3
|
-
|
4
|
-
module UI
|
5
|
-
class TabbedContainer < GenericContainer
|
6
|
-
attr_reader :show_tabbar, :attributes
|
7
|
-
|
8
|
-
def initialize(**opts)
|
9
|
-
super(**opts)
|
10
|
-
@show_tabbar = true
|
11
|
-
@tabbar = sub(HorizontalContainer)
|
12
|
-
@attributes = Hash.new { 0 }
|
13
|
-
end
|
14
|
-
|
15
|
-
def show_tabbar=(new)
|
16
|
-
return if @show_tabbar == new
|
17
|
-
with_lock { @show_tabbar = new; want_refresh }
|
18
|
-
end
|
19
|
-
|
20
|
-
def layout
|
21
|
-
if @show_tabbar
|
22
|
-
@tabbar.with_lock do
|
23
|
-
@tabbar.visible!
|
24
|
-
@tabbar.pos=(@pos)
|
25
|
-
@tabbar.size=(@size.update(height: 1))
|
26
|
-
end
|
27
|
-
|
28
|
-
if @selected
|
29
|
-
@selected.with_lock do
|
30
|
-
@selected.size=(@size.calc(height: -1))
|
31
|
-
@selected.pos=(@pos.calc(y: 1))
|
32
|
-
end
|
33
|
-
end
|
34
|
-
else
|
35
|
-
if @selected
|
36
|
-
@selected.with_lock do
|
37
|
-
@selected.size=(@size)
|
38
|
-
@selected.pos=(@pos)
|
39
|
-
end
|
40
|
-
end
|
41
|
-
end
|
42
|
-
|
43
|
-
super
|
44
|
-
end
|
45
|
-
|
46
|
-
def attributes=(new)
|
47
|
-
return if @attributes == new
|
48
|
-
with_lock { @attributes.update(new); update_tabbar }
|
49
|
-
end
|
50
|
-
|
51
|
-
def visible_widgets
|
52
|
-
return [@tabbar, @selected] if @show_tabbar and @selected
|
53
|
-
return [@selected] if @selected
|
54
|
-
return [@tabbar] if @show_tabbar
|
55
|
-
return []
|
56
|
-
end
|
57
|
-
|
58
|
-
def add(widget, title)
|
59
|
-
with_lock do
|
60
|
-
super(widget)
|
61
|
-
tab = @tabbar.sub(LabelWidget, text: title, pad: {left: 1})
|
62
|
-
tab.fit
|
63
|
-
tab.mouse.on_all { self.selected=(widget) }
|
64
|
-
@tabbar.add(tab)
|
65
|
-
update_tabbar
|
66
|
-
end
|
67
|
-
end
|
68
|
-
|
69
|
-
def remove(widget)
|
70
|
-
with_lock do
|
71
|
-
index = @widgets.index(widget) or fail KeyError
|
72
|
-
@tabbar.remove(@tabbar.widgets[index])
|
73
|
-
super(widget)
|
74
|
-
update_tabbar
|
75
|
-
end
|
76
|
-
end
|
77
|
-
|
78
|
-
def selected=(widget)
|
79
|
-
with_lock do
|
80
|
-
(@selected.invisible!) if @selected
|
81
|
-
super(widget)
|
82
|
-
(@selected.visible!) if @selected
|
83
|
-
update_tabbar
|
84
|
-
want_layout
|
85
|
-
end
|
86
|
-
end
|
87
|
-
|
88
|
-
def selected_index=(index)
|
89
|
-
with_lock do
|
90
|
-
(@selected.invisible!) if @selected
|
91
|
-
super(index)
|
92
|
-
update_tabbar
|
93
|
-
(@selected.visible!) if @selected
|
94
|
-
want_layout
|
95
|
-
end
|
96
|
-
end
|
97
|
-
|
98
|
-
private def update_tabbar
|
99
|
-
with_lock do
|
100
|
-
@tabbar.widgets.each_with_index do |tab, i|
|
101
|
-
if @widgets[i].equal?(@selected)
|
102
|
-
tab.attributes=(@attributes[:'tab_selected'])
|
103
|
-
else
|
104
|
-
tab.attributes=(@attributes[:'tabs'])
|
105
|
-
end
|
106
|
-
end
|
107
|
-
end
|
108
|
-
end
|
109
|
-
end
|
110
|
-
end
|