cryptopunks-gui 0.0.10 → 0.0.14

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,187 @@
1
+ class CryptopunksGui
2
+ module Model
3
+ class Image
4
+ PALETTES = ['Standard'] + (Palette8bit.constants).map(&:to_s).map {|palette| palette.split('_').map(&:capitalize).join(' ')}.reject { |palette| palette.include?(' ') }.sort
5
+ STYLES = ['Normal', 'Led', 'Sketch']
6
+ OUTPUT_LOCATION_DEFAULT = File.join(Dir.home, 'cryptopunks')
7
+ CONFIG_FILE = File.join(OUTPUT_LOCATION_DEFAULT, 'cryptopunks.yml')
8
+ COLLECTIONS_YAML_PATH = File.join(OUTPUT_LOCATION_DEFAULT, 'cryptopunks-collections.yml')
9
+ COLLECTIONS_YAML_URL = 'https://raw.githubusercontent.com/cryptopunksnotdead/cryptopunks-gui/master/cryptopunks-collections.yml'
10
+ COLLECTIONS_YAML_REPO_PATH = File.expand_path('../../cryptopunks-collections.yml', __dir__)
11
+
12
+ attr_accessor :collection, :image_index, :zoom, :palette, :style, :led_spacing, :led_round_corner, :sketch_line, :flip, :mirror,
13
+ :collection_size, :collections_map, :images, :image_location, :output_location, :config
14
+
15
+ def initialize
16
+ initialize_output_location
17
+ initialize_collections_map
18
+ initialize_collection
19
+ load_config
20
+ initialize_defaults
21
+ observe_image_attribute_changes
22
+ end
23
+
24
+ def collection_options
25
+ @collections_map.keys.select {|collection_name| @collections_map[collection_name][:enabled]}
26
+ end
27
+
28
+ def palette_options
29
+ PALETTES
30
+ end
31
+
32
+ def style_options
33
+ STYLES
34
+ end
35
+
36
+ def initialize_output_location
37
+ @output_location = OUTPUT_LOCATION_DEFAULT
38
+ FileUtils.mkdir_p(@output_location)
39
+ end
40
+
41
+ def initialize_collections_map(reset: false)
42
+ FileUtils.touch(COLLECTIONS_YAML_PATH)
43
+ @collections_map = reset ? {} : (YAML.load(File.read(COLLECTIONS_YAML_PATH)) || {})
44
+ new_collections_map = {}
45
+ begin
46
+ http_response = Net::HTTP.get_response(URI(COLLECTIONS_YAML_URL))
47
+ if http_response.is_a?(Net::HTTPSuccess)
48
+ new_collections_map = YAML.load(http_response.body)
49
+ else
50
+ raise "code: #{http_response.code} message: #{http_response.message}"
51
+ end
52
+ rescue StandardError, SocketError => e
53
+ puts "Failed to utilize collection YAML from: #{COLLECTIONS_YAML_URL}"
54
+ puts e.full_message
55
+ puts "Utilizing local collection YAML instead: #{COLLECTIONS_YAML_REPO_PATH}"
56
+ new_collections_map = YAML.load(File.read(COLLECTIONS_YAML_REPO_PATH)) rescue {}
57
+ end
58
+ @collections_map_observers ||= {}
59
+ new_collections_map.each do |collection_name, collection_options|
60
+ @collections_map[collection_name] ||= {}
61
+ original_collections_map_for_collection = @collections_map[collection_name]
62
+ @collections_map[collection_name] = @collections_map[collection_name]
63
+ @collections_map[collection_name].reverse_merge!(collection_options)
64
+ @collections_map[collection_name][:enabled] = true unless @collections_map[collection_name].has_key?(:enabled)
65
+ @collections_map_observers[collection_name] ||= Glimmer::DataBinding::Observer.proc { |value, key|
66
+ if key == :enabled
67
+ self.collection = @collections_map.find { |name, options| options[:enabled] }.first if key == :enabled && value == false && @collection == collection_name
68
+ notify_observers(:collection_options)
69
+ end
70
+ save_collections_map
71
+ }.tap {|o| o.observe(original_collections_map_for_collection)}
72
+ end
73
+ @collections_map_observer ||= Glimmer::DataBinding::Observer.proc { |collection_options, collection_name|
74
+ if collection_options.nil?
75
+ self.collection = @collections_map.find { |name, options| options[:enabled] }.first if @collection == collection_name
76
+ self.collection = collection_name if @collections_map.select { |name, options| options[:enabled] }.count == 0
77
+ end
78
+ notify_observers(:collection_options)
79
+ save_collections_map
80
+ }.tap {|o| o.observe(@collections_map)}
81
+ save_collections_map
82
+ end
83
+
84
+ def save_collections_map
85
+ normalized_collections_map = Hash[@collections_map.map {|k,v| [k, v.to_h]}]
86
+ File.write(COLLECTIONS_YAML_PATH, YAML.dump(normalized_collections_map))
87
+ end
88
+
89
+ def initialize_collection
90
+ return if @collection && @collection == @last_collection
91
+ @collection ||= @collections_map.keys.first
92
+ url = @collections_map[@collection][:url]
93
+ width = @collections_map[@collection][:width]
94
+ height = @collections_map[@collection][:height]
95
+ @image_file = File.join(OUTPUT_LOCATION_DEFAULT, File.basename(url))
96
+ File.binwrite(@image_file, Net::HTTP.get(URI(url))) unless File.exist?(@image_file)
97
+ @images ||= {}
98
+ @images[@collection] ||= Punks::Image::Composite.read(@image_file, width: width, height: height)
99
+ @last_collection = @collection
100
+ self.image_index = 0
101
+ end
102
+
103
+ def load_config
104
+ FileUtils.touch(CONFIG_FILE)
105
+ @config = YAML.load(File.read(CONFIG_FILE)) || {}
106
+ @output_location = @config[:output_location]
107
+ @config[:output_location] = @output_location = OUTPUT_LOCATION_DEFAULT if @output_location.nil?
108
+ end
109
+
110
+ def save_config
111
+ File.write(CONFIG_FILE, YAML.dump(@config))
112
+ end
113
+
114
+ def initialize_defaults
115
+ @collection = @collections_map.keys.first
116
+ @zoom = 12
117
+ @palette = PALETTES.first
118
+ @style = STYLES.first
119
+ @led_spacing = 2
120
+ @led_round_corner = false
121
+ @sketch_line = 1
122
+ @mirror = false
123
+ @flip = false
124
+ end
125
+
126
+ def observe_image_attribute_changes
127
+ observer = Glimmer::DataBinding::Observer.proc { generate_image }
128
+ observer.observe(self, :collection)
129
+ observer.observe(self, :image_index)
130
+ observer.observe(self, :zoom)
131
+ observer.observe(self, :palette)
132
+ observer.observe(self, :style)
133
+ observer.observe(self, :led_spacing)
134
+ observer.observe(self, :led_round_corner)
135
+ observer.observe(self, :sketch_line)
136
+ observer.observe(self, :mirror)
137
+ observer.observe(self, :flip)
138
+ end
139
+
140
+ def generate_image
141
+ initialize_collection
142
+ return if @image_index.to_i >= @images[@collection].size
143
+ new_image_location = File.join(@output_location, "#{@collection.gsub(' ', '').downcase}-#{@image_index}#{"x#{@zoom}" if @zoom.to_i > 1}#{"-#{@palette.underscore}" if @palette != PALETTES.first}#{"-#{@style.underscore}" if @style != STYLES.first}#{"-spacing#{@led_spacing.to_i}" if @style == 'Led'}#{'-round-corner' if @style == 'Led' && @led_round_corner}#{"-line#{@sketch_line.to_i}" if @style == 'Sketch'}#{'-mirror' if @mirror}#{'-flip' if @flip}.png")
144
+ puts "Writing punk image to #{new_image_location}"
145
+ selected_punk = @images[@collection][@image_index.to_i]
146
+ selected_punk = selected_punk.change_palette8bit(Palette8bit.const_get(@palette.gsub(' ', '_').upcase.to_sym)) if @palette != PALETTES.first
147
+ @original_zoom = @zoom
148
+ if @previous_collection && @collection != @previous_collection && @collections_map[@collection][:width] != @collections_map[@previous_collection][:width]
149
+ @zoom = @collections_map[@collection][:default_zoom]
150
+ end
151
+ if @style != STYLES.first
152
+ style_options = {}
153
+ if @style == 'Led'
154
+ style_options[:spacing] = @led_spacing.to_i
155
+ style_options[:round_corner] = @led_round_corner
156
+ end
157
+ if @style == 'Sketch'
158
+ style_options[:line] = @sketch_line.to_i
159
+ end
160
+ selected_punk = selected_punk.send(@style.underscore, @zoom.to_i, **style_options)
161
+ end
162
+ selected_punk = selected_punk.mirror if @mirror
163
+ selected_punk = selected_punk.flip if @flip
164
+ selected_punk = selected_punk.zoom(@zoom.to_i) if @style == STYLES.first
165
+ selected_punk.save(new_image_location)
166
+ self.image_location = new_image_location
167
+ notify_observers(:zoom) if @zoom != @original_zoom
168
+ @previous_style = @style
169
+ @previous_collection = @collection
170
+ end
171
+
172
+ def change_output_location(new_output_location)
173
+ @output_location = new_output_location
174
+ @config[:output_location] = @output_location
175
+ save_config
176
+ generate_image
177
+ end
178
+
179
+ def reset_output_location
180
+ @output_location = OUTPUT_LOCATION_DEFAULT
181
+ @config[:output_location] = @output_location
182
+ save_config
183
+ generate_image
184
+ end
185
+ end
186
+ end
187
+ end
@@ -0,0 +1,31 @@
1
+ class CryptopunksGui
2
+ module View
3
+ module HelpDialog
4
+ def help_dialog(root: )
5
+ toplevel(root) {
6
+ title 'CryptoPunks GUI - README.md'
7
+ width 800
8
+ height 600
9
+ escapable true
10
+
11
+ help_dialog = text {
12
+ grid row: 0, column: 0, row_weight: 1, column_weight: 1
13
+ value help
14
+ }
15
+
16
+ help_dialog_yscrollbar = scrollbar {
17
+ grid row: 0, column: 1
18
+ orient 'vertical'
19
+ }
20
+ help_dialog.yscrollbar help_dialog_yscrollbar.tk
21
+
22
+ help_dialog_xscrollbar = scrollbar {
23
+ grid row: 1, column: 0, column_span: 2
24
+ orient 'horizontal'
25
+ }
26
+ help_dialog.xscrollbar help_dialog_xscrollbar.tk
27
+ }
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,145 @@
1
+ class CryptopunksGui
2
+ module View
3
+ module ImageFrame
4
+ def image_frame(root: , image: )
5
+ image_label = output_location_entry = image_index_spinbox = style_options_container_frame = nil
6
+
7
+ frame {
8
+ label {
9
+ text 'Collection:'
10
+ }
11
+ combobox {
12
+ readonly true
13
+ values <= [image, :collection_options]
14
+ text <=> [image, :collection]
15
+ }
16
+
17
+ label {
18
+ text 'Image Index:'
19
+ }
20
+ image_index_spinbox = spinbox {
21
+ from 0
22
+ to image.images[image.collection].size - 1
23
+ text <=> [image, :image_index]
24
+ }
25
+
26
+ label {
27
+ text 'Zoom:'
28
+ }
29
+ spinbox {
30
+ from 1
31
+ to 72
32
+ text <=> [image, :zoom]
33
+ }
34
+
35
+ label {
36
+ text 'Palette:'
37
+ }
38
+ combobox {
39
+ readonly true
40
+ text <=> [image, :palette]
41
+ }
42
+
43
+ label {
44
+ text 'Style:'
45
+ }
46
+ combobox {
47
+ readonly true
48
+ text <=> [image, :style, after_write: ->(val) {add_style_options(style_options_container_frame: style_options_container_frame, image: image)}]
49
+ }
50
+
51
+ style_options_container_frame = frame {
52
+ padding 0
53
+ }
54
+
55
+ frame {
56
+ padding 0
57
+
58
+ checkbutton {
59
+ grid row: 0, column: 0, column_weight: 0
60
+ variable <=> [image, :mirror]
61
+ }
62
+ label {
63
+ grid row: 0, column: 1
64
+ text 'Mirror'
65
+
66
+ on('Button-1') do
67
+ image.mirror = !image.mirror
68
+ end
69
+ }
70
+
71
+ checkbutton {
72
+ grid row: 0, column: 2
73
+ variable <=> [image, :flip]
74
+ }
75
+ label {
76
+ grid row: 0, column: 3
77
+ text 'Flip'
78
+
79
+ on('Button-1') do
80
+ image.flip = !image.flip
81
+ end
82
+ }
83
+ }
84
+
85
+ label {
86
+ text 'Output Location:'
87
+ }
88
+ frame {
89
+ padding 0
90
+
91
+ output_location_entry = entry {
92
+ grid row: 0, column: 0
93
+ readonly true
94
+ width 47
95
+ }
96
+ button {
97
+ grid row: 0, column: 1
98
+ text '...'
99
+ width 1.1
100
+
101
+ on('command') do
102
+ change_output_location(root: root, image: image)
103
+ end
104
+ }
105
+ }
106
+
107
+ image_label = label {
108
+ grid row_weight: 1
109
+ }
110
+ }
111
+
112
+ register_image_frame_observers(image_label: image_label, output_location_entry: output_location_entry, image_index_spinbox: image_index_spinbox, image: image)
113
+ end
114
+
115
+ def register_image_frame_observers(image_label: , output_location_entry: , image_index_spinbox: , image: )
116
+ Glimmer::DataBinding::Observer.proc do
117
+ image_label.image = output_location_entry.text = image.image_location
118
+ end.observe(image, :image_location)
119
+
120
+ Glimmer::DataBinding::Observer.proc do
121
+ image_index_spinbox.to = image.images[image.collection].size - 1
122
+ end.observe(image, :collection)
123
+ end
124
+
125
+ def add_style_options(style_options_container_frame: , image: )
126
+ style_options_container_frame.children.each(&:destroy)
127
+ style_options_container_frame.content {
128
+ case image.style
129
+ when 'Led'
130
+ led_style_options_frame(image: image)
131
+ when 'Sketch'
132
+ sketch_style_options_frame(image: image)
133
+ else
134
+ no_style_options_frame
135
+ end
136
+ }
137
+ end
138
+
139
+ def change_output_location(root: , image: )
140
+ new_output_location = choose_directory(parent: root)
141
+ image.change_output_location(new_output_location) unless new_output_location.to_s.empty?
142
+ end
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,88 @@
1
+ # require_relative 'preferences_dialog'
2
+ require_relative 'help_dialog'
3
+
4
+ class CryptopunksGui
5
+ module View
6
+ module MenuBar
7
+ # include View::PreferencesDialog
8
+ include View::HelpDialog
9
+
10
+ LICENSE = File.expand_path('../../LICENSE.txt', __dir__)
11
+ HELP = File.expand_path('../../README.md', __dir__)
12
+
13
+ def cryptopunks_gui_menu_bar(root: , image: )
14
+ menu {
15
+ if OS.mac?
16
+ menu(:application) {
17
+ menu_item(:about, label: 'About') {
18
+ accelerator OS.mac? ? 'Command+Shift+A' : 'Control+Alt+A'
19
+
20
+ on('command') do
21
+ show_about_dialog(root: root)
22
+ end
23
+ }
24
+
25
+ # menu_item(:preferences) {
26
+ # on('command') do
27
+ # preferences_dialog(root: root, image: image)
28
+ # end
29
+ # }
30
+ }
31
+ end
32
+
33
+ menu(label: 'File', underline: 0) {
34
+ menu_item(label: 'Change Output Location...', underline: 7) {
35
+ accelerator OS.mac? ? 'Command+O' : 'Control+O'
36
+
37
+ on('command') do
38
+ change_output_location(root: root, image: image) # assumes View::ImageFrame is mixed in
39
+ end
40
+ }
41
+
42
+ menu_item(label: 'Reset Output Location', underline: 0) {
43
+ on('command') do
44
+ image.reset_output_location
45
+ end
46
+ }
47
+
48
+ menu_item(:separator)
49
+
50
+ # menu_item(label: 'Preferences...', underline: 0) {
51
+ # on('command') do
52
+ # preferences_dialog(root: root, image: image)
53
+ # end
54
+ # }
55
+
56
+ menu_item(:separator)
57
+
58
+ menu_item(label: 'Exit', underline: 1) {
59
+ on('command') do
60
+ exit(0)
61
+ end
62
+ }
63
+ }
64
+
65
+ menu(label: 'Help') {
66
+ menu_item(:help) {
67
+ on('command') do
68
+ help_dialog(root: root)
69
+ end
70
+ }
71
+ }
72
+ }
73
+ end
74
+
75
+ def show_about_dialog(root: )
76
+ message_box(parent: root, title: 'CryptoPunks GUI', message: "CryptoPunks GUI\n\n#{license}")
77
+ end
78
+
79
+ def license
80
+ @license ||= File.read(LICENSE)
81
+ end
82
+
83
+ def help
84
+ @help ||= File.read(HELP)
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,174 @@
1
+ class CryptopunksGui
2
+ module View
3
+ module PreferencesDialog
4
+ def preferences_dialog(root: , image: )
5
+ toplevel(root) {
6
+ title 'Preferences'
7
+ width 1280
8
+ height 700
9
+ escapable true
10
+
11
+ scrollbar_frame {
12
+ label {
13
+ grid row: 0, column: 0
14
+ text ''
15
+ }
16
+
17
+ label {
18
+ grid row: 0, column: 1
19
+ text 'Collection Name'
20
+ }
21
+
22
+ label {
23
+ grid row: 0, column: 2
24
+ text 'URL'
25
+ width 82
26
+ }
27
+
28
+ label {
29
+ grid row: 0, column: 3
30
+ text 'Width'
31
+ width 3
32
+ }
33
+
34
+ label {
35
+ grid row: 0, column: 4
36
+ text 'Height'
37
+ width 3
38
+ }
39
+
40
+ label {
41
+ grid row: 0, column: 5
42
+ text "Default\nZoom"
43
+ width 3
44
+ }
45
+
46
+ label {
47
+ grid row: 0, column: 6, column_span: 3
48
+ text 'Actions'
49
+ }
50
+
51
+ image.collections_map.each_with_index do |pair, index|
52
+ collection_name, collection_options = pair
53
+ row = index + 1
54
+
55
+ checkbutton { |cb|
56
+ grid row: row, column: 0
57
+ # TODO consider using hash_proxy object to bind more effectively
58
+ variable <= [image.collections_map[collection_name], :enabled]
59
+
60
+ on('command') do
61
+ collection_options[:enabled] = cb.variable
62
+ end
63
+ }
64
+
65
+ entry {
66
+ grid row: row, column: 1
67
+ text collection_name
68
+ }
69
+
70
+ entry {
71
+ grid row: row, column: 2
72
+ text collection_options[:url]
73
+ width 82
74
+ }
75
+
76
+ spinbox {
77
+ grid row: row, column: 3
78
+ from 1
79
+ to 512
80
+ text collection_options[:width]
81
+ width 3
82
+ }
83
+
84
+ spinbox {
85
+ grid row: row, column: 4
86
+ from 1
87
+ to 512
88
+ text collection_options[:height]
89
+ width 3
90
+ }
91
+
92
+ spinbox {
93
+ grid row: row, column: 5
94
+ from 1
95
+ to 72
96
+ text collection_options[:default_zoom]
97
+ width 3
98
+ }
99
+
100
+ button {
101
+ grid row: row, column: 6
102
+ text 'X'
103
+ width 1
104
+ }
105
+ button {
106
+ grid row: row, column: 7
107
+ text '^'
108
+ width 1
109
+ }
110
+ button {
111
+ grid row: row, column: 8
112
+ text 'v'
113
+ width 1
114
+ }
115
+ end
116
+
117
+ row = image.collections_map.keys.size + 1
118
+
119
+ checkbutton {
120
+ grid row: row, column: 0
121
+
122
+ }
123
+
124
+ entry {
125
+ grid row: row, column: 1
126
+ text ''
127
+ }
128
+
129
+ entry {
130
+ grid row: row, column: 2
131
+ text ''
132
+ width 82
133
+ }
134
+
135
+ spinbox {
136
+ grid row: row, column: 3
137
+ from 1
138
+ to 512
139
+ text '24'
140
+ width 3
141
+ }
142
+
143
+ spinbox {
144
+ grid row: row, column: 4
145
+ from 1
146
+ to 512
147
+ text '24'
148
+ width 3
149
+ }
150
+
151
+ spinbox {
152
+ grid row: row, column: 5
153
+ from 1
154
+ to 72
155
+ text '12'
156
+ width 3
157
+ }
158
+ }
159
+
160
+ frame {
161
+ button {
162
+ text 'Reset'
163
+
164
+ on('command') do
165
+ image.initialize_collections_map(reset: true)
166
+ # TODO cause an update to all collections
167
+ end
168
+ }
169
+ }
170
+ }
171
+ end
172
+ end
173
+ end
174
+ end
@@ -0,0 +1,60 @@
1
+ class CryptopunksGui
2
+ module View
3
+ module StyleOptionsFrame
4
+ def led_style_options_frame(image: )
5
+ frame {
6
+ padding 0
7
+
8
+ label {
9
+ grid row: 0, column: 0, column_weight: 0
10
+ text 'Spacing:'
11
+ }
12
+ spinbox {
13
+ grid row: 0, column: 1
14
+ from 1
15
+ to 72
16
+ text <=> [image, :led_spacing]
17
+ }
18
+
19
+ checkbutton {
20
+ grid row: 0, column: 2
21
+ variable <=> [image, :led_round_corner]
22
+ }
23
+ label {
24
+ grid row: 0, column: 3
25
+ text 'Round Corner'
26
+
27
+ on('Button-1') do
28
+ image.led_round_corner = !image.led_round_corner
29
+ end
30
+ }
31
+ }
32
+ end
33
+
34
+ def sketch_style_options_frame(image: )
35
+ frame {
36
+ padding 0
37
+
38
+ label {
39
+ grid row: 0, column: 0, column_weight: 0
40
+ text 'Line:'
41
+ }
42
+ spinbox {
43
+ grid row: 0, column: 1
44
+ from 1
45
+ to 72
46
+ text <=> [image, :sketch_line]
47
+ }
48
+ }
49
+ end
50
+
51
+ def no_style_options_frame
52
+ frame { # filler
53
+ padding 0
54
+ height 0
55
+ width 0
56
+ }
57
+ end
58
+ end
59
+ end
60
+ end
data/bin/cryptopunks-gui CHANGED
File without changes