trmnl_preview 0.2.0 → 0.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1d511eb133cb9ee46d6d343f9ed1bef3c11c3e581ae193310d2d6f50d900c4a4
4
- data.tar.gz: ed60374a348285201cd0f49396cd2995c5b87301866aec421cd3c21131c6fe98
3
+ metadata.gz: 3944c7a74517407dbb4ee683c628111668149c622be67a4ba3d69a2655d8b48e
4
+ data.tar.gz: e6c9d4f7472e4b56d818884ba6ca33307706891da62c40121398036507568fcd
5
5
  SHA512:
6
- metadata.gz: 063e9d97310fdc7c20609e83573006e18df1a081455d8d60d1694fbbc21a43c82c99290395b54e0b815d498884ebf69f984f4d153c3d50131c34b090c6752749
7
- data.tar.gz: c44116ac437e7636c403481d6b5b773a5f0d0ccf425b6e2181bc987d272056bef407ce43919d967fa86da80b96b032534d423b43dd60ef71d56c96ec22e4b9ab
6
+ metadata.gz: 1fd0a512a23f1439cc4030543f92272682917d1024ae8ef6eed7449f51d7677add6a0974ed4783b56ad088729ac53a7343e0c00d9026c83d762fb9f825406eb2
7
+ data.tar.gz: 70c1ba02c0df2d7e427fc079f0bee439fdeea0c120f348d562f734e10f10b210c11db34fbc99c76894afa5622714445efb1fee17fa44029080ba22c216462b1d
data/CHANGELOG.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.0
4
+
5
+ - Add poll button
6
+ - Add case image overlays
7
+ - Add `trmnlp build` command
8
+ - Add support for `url` pointing to a local JSON data file
9
+
3
10
  ## 0.2.0
4
11
 
5
12
  - Add "commands" concept to `trmnlp` executable
data/docs/preview.png CHANGED
Binary file
data/exe/trmnlp CHANGED
@@ -5,6 +5,8 @@ require_relative '../lib/trmnl_preview'
5
5
  case ARGV[0].to_s.downcase
6
6
  when 'serve'
7
7
  require_relative '../lib/trmnl_preview/cmd/serve'
8
+ when 'build'
9
+ require_relative '../lib/trmnl_preview/cmd/build'
8
10
  else
9
11
  require_relative '../lib/trmnl_preview/cmd/usage'
10
12
  end
@@ -1,97 +1,53 @@
1
- require 'json'
2
- require 'liquid'
3
- require 'open-uri'
1
+
4
2
  require 'sinatra'
5
3
  require 'sinatra/base'
6
- require 'toml-rb'
7
-
8
- require_relative 'liquid_filters'
9
-
10
- class TRMNLPreview::App < Sinatra::Base
11
- # Constants
12
- VIEWS = %w{full half_horizontal half_vertical quadrant}
13
4
 
14
- # Sinatra settings
15
- set :views, File.join(File.dirname(__FILE__), '..', '..', 'views')
16
-
17
- def initialize(*args)
18
- super
19
-
20
- @config_path = File.join(settings.user_dir, 'config.toml')
21
- @user_views_dir = File.join(settings.user_dir, 'views')
22
- @temp_dir = File.join(settings.user_dir, 'tmp')
23
- @data_json_path = File.join(@temp_dir, 'data.json')
5
+ require_relative 'context'
6
+
7
+ module TRMNLPreview
8
+ class App < Sinatra::Base
9
+ # Sinatra settings
10
+ set :views, File.join(File.dirname(__FILE__), '..', '..', 'web', 'views')
11
+ set :public_folder, File.join(File.dirname(__FILE__), '..', '..', 'web', 'public')
12
+
13
+ def initialize(*args)
14
+ super
15
+
16
+ begin
17
+ @context = Context.new(settings.user_dir)
18
+ rescue StandardError => e
19
+ puts e.message
20
+ exit 1
21
+ end
24
22
 
25
- unless File.exist?(@config_path)
26
- puts "No config.toml found in #{settings.user_dir}"
27
- exit 1
28
- end
29
-
30
- unless Dir.exist?(@user_views_dir)
31
- puts "No views found at #{@user_views_dir}"
32
- exit 1
23
+ @context.poll_data if @context.strategy == 'polling'
33
24
  end
34
25
 
35
- FileUtils.mkdir_p(@temp_dir)
36
-
37
- @config = TomlRB.load_file(@config_path)
38
- strategy = @config['strategy']
39
-
40
- unless ['polling', 'webhook'].include?(strategy)
41
- puts "Invalid strategy: #{strategy} (must be 'polling' or 'webhook')"
42
- exit 1
26
+ post '/webhook' do
27
+ @context.set_data(request.body.read)
28
+ "OK"
43
29
  end
44
-
45
- url = @config['url']
46
- polling_headers = @config['polling_headers'] || {}
47
-
48
- if strategy == 'polling'
49
- if url.nil?
50
- puts "URL is required for polling strategy"
51
- exit 1
52
- end
53
-
54
- print "Fetching #{url}... "
55
- payload = URI.open(url, polling_headers).read
56
- File.write(@data_json_path, payload)
57
- puts "got #{payload.size} bytes"
58
- end
59
-
60
- @liquid_environment = Liquid::Environment.build do |env|
61
- env.register_filter(TRMNLPreview::LiquidFilters)
30
+
31
+ get '/' do
32
+ redirect '/full'
62
33
  end
63
- end
64
34
 
65
- post '/webhook' do
66
- body = request.body.read
67
- File.write(@data_json_path, body)
68
- "OK"
69
- end
70
-
71
- get '/' do
72
- redirect '/full'
73
- end
74
-
75
- VIEWS.each do |view|
76
- get "/#{view}" do
77
- @view = view
78
- erb :index
35
+ get '/poll' do
36
+ @context.poll_data
37
+ redirect back
79
38
  end
80
-
81
- get "/render/#{view}" do
82
- path = File.join(@user_views_dir, "#{view}.liquid")
83
- unless File.exist?(path)
84
- halt 404, "Plugin template not found: views/#{view}.liquid"
39
+
40
+ VIEWS.each do |view|
41
+ get "/#{view}" do
42
+ @view = view
43
+ erb :index
85
44
  end
86
45
 
87
- user_template = Liquid::Template.parse(File.read(path), environment: @liquid_environment)
88
-
89
- @view = view
90
- erb :render_view do
91
- data = JSON.parse(File.read(@data_json_path))
92
- data = { data: data } if data.is_a?(Array) # per TRMNL docs, bare array is wrapped in 'data' key
93
-
94
- user_template.render(data)
46
+ get "/render/#{view}" do
47
+ @view = view
48
+ erb :render_view do
49
+ @context.render_template(view)
50
+ end
95
51
  end
96
52
  end
97
53
  end
@@ -0,0 +1,25 @@
1
+ require 'optionparser'
2
+
3
+ require_relative '../context'
4
+
5
+ OptionParser.new do |opts|
6
+ opts.banner = "Usage: trmnlp build [directory]"
7
+ end.parse!
8
+
9
+ root = ARGV[1] || Dir.pwd
10
+ begin
11
+ context = TRMNLPreview::Context.new(root)
12
+ rescue StandardError => e
13
+ puts e.message
14
+ exit 1
15
+ end
16
+
17
+ context.poll_data
18
+
19
+ TRMNLPreview::VIEWS.each do |view|
20
+ output_path = File.join(context.temp_dir, "#{view}.html")
21
+ puts "Creating #{output_path}..."
22
+ File.write(output_path, context.render_full_page(view))
23
+ end
24
+
25
+ puts "Done!"
@@ -6,4 +6,5 @@ Usage:
6
6
  Commands (-h for command-specific help):
7
7
 
8
8
  serve Start the TRMNL Preview server
9
+ build Generate static HTML files
9
10
  USAGE
@@ -0,0 +1,102 @@
1
+ require 'erb'
2
+ require 'fileutils'
3
+ require 'json'
4
+ require 'liquid'
5
+ require 'open-uri'
6
+ require 'toml-rb'
7
+
8
+ require_relative 'liquid_filters'
9
+
10
+ module TRMNLPreview
11
+ class Context
12
+ attr_reader :strategy, :temp_dir
13
+
14
+ def initialize(root)
15
+ config_path = File.join(root, 'config.toml')
16
+ @user_views_dir = File.join(root, 'views')
17
+ @temp_dir = File.join(root, 'tmp')
18
+ @data_json_path = File.join(@temp_dir, 'data.json')
19
+
20
+ @liquid_environment = Liquid::Environment.build do |env|
21
+ env.register_filter(LiquidFilters)
22
+ end
23
+
24
+ unless File.exist?(config_path)
25
+ raise "No config.toml found in #{root}"
26
+ end
27
+
28
+ unless Dir.exist?(@user_views_dir)
29
+ raise "No views found at #{@user_views_dir}"
30
+ end
31
+
32
+ config = TomlRB.load_file(config_path)
33
+ @strategy = config['strategy']
34
+ @url = config['url']
35
+ @polling_headers = config['polling_headers'] || {}
36
+
37
+ unless ['polling', 'webhook'].include?(@strategy)
38
+ raise "Invalid strategy: #{strategy} (must be 'polling' or 'webhook')"
39
+ end
40
+
41
+ FileUtils.mkdir_p(@temp_dir)
42
+ end
43
+
44
+ def user_data
45
+ data = JSON.parse(File.read(@data_json_path))
46
+ data = { data: data } if data.is_a?(Array) # per TRMNL docs, bare array is wrapped in 'data' key
47
+ data
48
+ end
49
+
50
+ def poll_data
51
+ if @url.nil?
52
+ raise "URL is required for polling strategy"
53
+ end
54
+
55
+ print "Fetching #{@url}... "
56
+
57
+ if @url.match?(/^https?:\/\//)
58
+ payload = URI.open(@url, @polling_headers).read
59
+ else
60
+ payload = File.read(@url)
61
+ end
62
+
63
+ File.write(@data_json_path, payload)
64
+ puts "got #{payload.size} bytes"
65
+
66
+ user_data
67
+ end
68
+
69
+ def set_data(payload)
70
+ File.write(@data_json_path, payload)
71
+ end
72
+
73
+ def view_path(view)
74
+ File.join(@user_views_dir, "#{view}.liquid")
75
+ end
76
+
77
+ def render_template(view)
78
+ path = view_path(view)
79
+ unless File.exist?(path)
80
+ return "Missing plugin template: views/#{view}.liquid"
81
+ end
82
+
83
+ user_template = Liquid::Template.parse(File.read(path), environment: @liquid_environment)
84
+ user_template.render(user_data)
85
+ end
86
+
87
+ def render_full_page(view)
88
+ page_erb_template = File.read(File.join(__dir__, '..', '..', 'web', 'views', 'render_view.erb'))
89
+
90
+ ERB.new(page_erb_template).result(ERBBinding.new(view).get_binding do
91
+ render_template(view)
92
+ end)
93
+ end
94
+
95
+ private
96
+
97
+ class ERBBinding
98
+ def initialize(view) = @view = view
99
+ def get_binding = binding
100
+ end
101
+ end
102
+ end
@@ -1,6 +1,8 @@
1
- module TRMNLPreview::LiquidFilters
2
- def number_with_delimiter(number)
3
- # TODO: Replace with ActiveSupport's number_with_delimiter
4
- number.to_s.reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse
1
+ module TRMNLPreview
2
+ module LiquidFilters
3
+ def number_with_delimiter(number)
4
+ # TODO: Replace with ActiveSupport's number_with_delimiter
5
+ number.to_s.reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse
6
+ end
5
7
  end
6
8
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module TRMNLPreview
4
- VERSION = "0.2.0"
4
+ VERSION = "0.3.0"
5
5
  end
data/lib/trmnl_preview.rb CHANGED
@@ -2,10 +2,9 @@
2
2
 
3
3
  module TRMNLPreview; end
4
4
 
5
- # require_relative "trmnl_preview/app"
5
+ require_relative "trmnl_preview/context"
6
6
  require_relative "trmnl_preview/version"
7
7
 
8
8
  module TRMNLPreview
9
- class Error < StandardError; end
10
- # Your code goes here...
11
- end
9
+ VIEWS = %w{full half_horizontal half_vertical quadrant}
10
+ end
Binary file
Binary file
Binary file
@@ -0,0 +1,113 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>TRMNL Preview</title>
7
+ <style>
8
+ body {
9
+ font-family: sans-serif;
10
+ margin: 10px;
11
+ }
12
+ main {
13
+ display: flex;
14
+ flex-direction: column;
15
+ width: fit-content;
16
+ }
17
+ menu {
18
+ padding: 0;
19
+ margin: 0;
20
+ display: flex;
21
+ justify-content: space-between;
22
+ }
23
+ menu a {
24
+ padding: 0.5em 1em;
25
+ background: #ddd;
26
+ border-radius: 0.5em;
27
+ display: inline-block;
28
+ text-decoration: none;
29
+ color: black;
30
+ }
31
+ menu a:hover {
32
+ background: #ccc;
33
+ }
34
+ menu a.active {
35
+ background: #333;
36
+ color: white;
37
+ }
38
+ .case {
39
+ width: 1000px;
40
+ height: 680px;
41
+ position: relative;
42
+ }
43
+ iframe {
44
+ position: absolute;
45
+ border: none;
46
+ left: 98px;
47
+ top: 65px;
48
+ filter: grayscale(100%);
49
+ }
50
+ .case .case-overlay {
51
+ position: absolute;
52
+ width: 100%;
53
+ height: 100%;
54
+ top: 0;
55
+ left: 0;
56
+ background-size: cover;
57
+ mix-blend-mode: multiply;
58
+ pointer-events: none;
59
+ }
60
+ .case--none .case-overlay { background: none; }
61
+ .case--white .case-overlay { background-image: url('white-case.jpg') }
62
+ .case--black .case-overlay { background-image: url('black-case.jpg') }
63
+ .case--clear .case-overlay { background-image: url('clear-case.jpg') }
64
+ .case--none iframe {
65
+ border: 1px solid black;
66
+ }
67
+ </style>
68
+ <script>
69
+ function updateCase() {
70
+ const value = document.querySelector('.select-case').value;
71
+ document.querySelector('.case').className = `case case--${value}`;
72
+ localStorage.setItem('trmnlp-case', value);
73
+ }
74
+
75
+ document.addEventListener('DOMContentLoaded', () => {
76
+ const caseValue = localStorage.getItem('trmnlp-case') || 'black';
77
+
78
+ const select = document.querySelector('.select-case');
79
+ select.value = caseValue;
80
+ select.addEventListener('change', () => { updateCase() });
81
+
82
+ updateCase();
83
+ });
84
+ </script>
85
+ </head>
86
+ <body>
87
+ <main>
88
+ <menu>
89
+ <div>
90
+ <a class="<%= 'active' if @view == 'full' %>" href="/full">Full</a>
91
+ <a class="<%= 'active' if @view == 'half_horizontal' %>" href="/half_horizontal">Half Horizontal</a>
92
+ <a class="<%= 'active' if @view == 'half_vertical' %>" href="/half_vertical">Half Vertical</a>
93
+ <a class="<%= 'active' if @view == 'quadrant' %>" href="/quadrant">Quadrant</a>
94
+ </div>
95
+ <div>
96
+ <select class="select-case">
97
+ <option value="none">None</option>
98
+ <option value="white">White</option>
99
+ <option value="black" selected>Black</option>
100
+ <option value="clear">Clear</option>
101
+ </select>
102
+
103
+ <a href="/poll">Poll</a>
104
+ </div>
105
+ </menu>
106
+
107
+ <div class="case case--black">
108
+ <iframe src="/render/<%= @view %>" width="800" height="480"></iframe>
109
+ <div class="case-overlay"></div>
110
+ </div>
111
+ </main>
112
+ </body>
113
+ </html>
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: trmnl_preview
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Rockwell Schrock
@@ -96,13 +96,18 @@ files:
96
96
  - exe/trmnlp
97
97
  - lib/trmnl_preview.rb
98
98
  - lib/trmnl_preview/app.rb
99
+ - lib/trmnl_preview/cmd/build.rb
99
100
  - lib/trmnl_preview/cmd/serve.rb
100
101
  - lib/trmnl_preview/cmd/usage.rb
102
+ - lib/trmnl_preview/context.rb
101
103
  - lib/trmnl_preview/liquid_filters.rb
102
104
  - lib/trmnl_preview/version.rb
103
105
  - trmnl_preview.gemspec
104
- - views/index.erb
105
- - views/render_view.erb
106
+ - web/public/black-case.jpg
107
+ - web/public/clear-case.jpg
108
+ - web/public/white-case.jpg
109
+ - web/views/index.erb
110
+ - web/views/render_view.erb
106
111
  homepage: https://github.com/schrockwell/trmnl_preview
107
112
  licenses:
108
113
  - MIT
data/views/index.erb DELETED
@@ -1,42 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>TRMNL Preview</title>
7
- <style>
8
- body {
9
- font-family: sans-serif;
10
- background: #eee;
11
- }
12
- .menu {
13
- margin-bottom: 1em;
14
- }
15
- .menu a {
16
- padding: 0.5em 1em;
17
- background: #ddd;
18
- border-radius: 0.5em;
19
- display: inline-block;
20
- text-decoration: none;
21
- color: black;
22
- }
23
- .menu a:hover {
24
- background: #ccc;
25
- }
26
- .menu a.active {
27
- background: #333;
28
- color: white;
29
- }
30
- </style>
31
- </head>
32
- <body>
33
- <div class="menu">
34
- <a class="<%= 'active' if @view == 'full' %>" href="/full">Full</a>
35
- <a class="<%= 'active' if @view == 'half_horizontal' %>" href="/half_horizontal">Half Horizontal</a>
36
- <a class="<%= 'active' if @view == 'half_vertical' %>" href="/half_vertical">Half Vertical</a>
37
- <a class="<%= 'active' if @view == 'quadrant' %>" href="/quadrant">Quadrant</a>
38
- </div>
39
-
40
- <iframe src="/render/<%= @view %>" width="800" height="480"></iframe>
41
- </body>
42
- </html>
File without changes