trmnl_preview 0.3.0 → 0.3.2
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/CHANGELOG.md +10 -0
- data/README.md +14 -6
- data/config.example.toml +3 -0
- data/exe/trmnlp +2 -0
- data/lib/trmnl_preview/app.rb +34 -2
- data/lib/trmnl_preview/cmd/usage.rb +2 -1
- data/lib/trmnl_preview/cmd/version.rb +1 -0
- data/lib/trmnl_preview/context.rb +49 -16
- data/lib/trmnl_preview/custom_filters.rb +13 -0
- data/lib/trmnl_preview/screen_generator.rb +137 -0
- data/lib/trmnl_preview/version.rb +1 -1
- data/trmnl_preview.gemspec +14 -1
- data/web/public/index.css +98 -0
- data/web/public/index.js +71 -0
- data/web/views/index.erb +13 -80
- metadata +92 -4
- data/lib/trmnl_preview/liquid_filters.rb +0 -8
- /data/web/views/{render_view.erb → render_html.erb} +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e965687c88e23b15e8727f9b871b6a2529bc28c388fa2e8733fb5034915ec405
|
4
|
+
data.tar.gz: c1299d3ce7b27a13be79d0018535c8f1b64171f7154d9dd1f425eeacbcbc0129
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d507258ba8a315daae87b83dc5609f2aa99c9d1c674f7067cd2b67b84382a4746c8f299c7c3d0e3e9cc4f378846ca3f5696f4aa8f1dcb65c6eec8b6e44f46d74
|
7
|
+
data.tar.gz: 02231d9584b0b24a4b7b2749c85cbf14af2c3b6d5321c63dc03bf6e8bb7655b03ee37d86ba69b7ac16ebb3acb367cad2bf54ec437e405a1a842eb1f9263090f3
|
data/CHANGELOG.md
CHANGED
@@ -1,5 +1,15 @@
|
|
1
1
|
# Changelog
|
2
2
|
|
3
|
+
## 0.3.2
|
4
|
+
|
5
|
+
- Add bitmap rendering
|
6
|
+
- Add TRMNL's [custom plugin filters](https://help.usetrmnl.com/en/articles/10347358-custom-plugin-filters)
|
7
|
+
- Add support for user-supplied custom filters
|
8
|
+
|
9
|
+
## 0.3.1
|
10
|
+
|
11
|
+
- Add live render
|
12
|
+
|
3
13
|
## 0.3.0
|
4
14
|
|
5
15
|
- Add poll button
|
data/README.md
CHANGED
@@ -1,8 +1,10 @@
|
|
1
|
-
#
|
1
|
+
# trmnlp
|
2
2
|
|
3
3
|
A basic self-hosted web server to ease the development and sharing of [TRMNL](https://usetrmnl.com/) plugins.
|
4
4
|
|
5
|
-
[Liquid](https://shopify.github.io/liquid/) templates are rendered
|
5
|
+
[Liquid](https://shopify.github.io/liquid/) templates are rendered leveraging the [TRMNL Design System](https://usetrmnl.com/framework). They may be generated as HTML (faster, and a good approximation of the final result) or as BMP images (slower, but more accurate).
|
6
|
+
|
7
|
+
The server watches the filesystem for changes to the Liquid templates, seamlessly updating the preview without the need to refresh.
|
6
8
|
|
7
9
|

|
8
10
|
|
@@ -34,17 +36,22 @@ docker run \
|
|
34
36
|
|
35
37
|
## Running the Server (Local Ruby)
|
36
38
|
|
37
|
-
|
39
|
+
Prerequisites:
|
40
|
+
|
41
|
+
- Ruby 3.x
|
42
|
+
- For BMP rendering (optional):
|
43
|
+
- Firefox
|
44
|
+
- ImageMagick
|
45
|
+
|
46
|
+
In the plugin repository:
|
38
47
|
|
39
48
|
```sh
|
40
|
-
|
49
|
+
gem install trmnl_preview
|
41
50
|
trmnlp serve # Starts the server
|
42
51
|
```
|
43
52
|
|
44
53
|
## Usage Notes
|
45
54
|
|
46
|
-
Simply refresh the page to re-render.
|
47
|
-
|
48
55
|
When the strategy is "polling", the specified URL will be fetched once, when the server starts.
|
49
56
|
|
50
57
|
When the strategy is "webhook", payloads can be POSTed to the `/webhook` endpoint. They are saved to `tmp/data.json` for future renders.
|
@@ -53,6 +60,7 @@ When the strategy is "webhook", payloads can be POSTed to the `/webhook` endpoin
|
|
53
60
|
|
54
61
|
- `strategy` - Either "polling" or "webhook"
|
55
62
|
- `url` - The URL from which to fetch JSON data (polling strategy only)
|
63
|
+
- `live_render` - Set to `false` to disable automatic rendering when Liquid templates change (default `true`)
|
56
64
|
- `[polling_headers]` - A section of headers to append to the HTTP poll request (polling strategy only)
|
57
65
|
|
58
66
|
## Contributing
|
data/config.example.toml
CHANGED
@@ -5,6 +5,9 @@ strategy = "polling"
|
|
5
5
|
# Poll URL (required for polling strategy)
|
6
6
|
url = "https://example.com/data.json"
|
7
7
|
|
8
|
+
# Automatically re-render the view when Liquid templates change (default: true)
|
9
|
+
live_render = true
|
10
|
+
|
8
11
|
# Polling headers (optional, for polling strategy)
|
9
12
|
[polling_headers]
|
10
13
|
authorization = "bearer 123"
|
data/exe/trmnlp
CHANGED
@@ -7,6 +7,8 @@ when 'serve'
|
|
7
7
|
require_relative '../lib/trmnl_preview/cmd/serve'
|
8
8
|
when 'build'
|
9
9
|
require_relative '../lib/trmnl_preview/cmd/build'
|
10
|
+
when 'version'
|
11
|
+
require_relative '../lib/trmnl_preview/cmd/version'
|
10
12
|
else
|
11
13
|
require_relative '../lib/trmnl_preview/cmd/usage'
|
12
14
|
end
|
data/lib/trmnl_preview/app.rb
CHANGED
@@ -1,8 +1,10 @@
|
|
1
1
|
|
2
|
+
require 'faye/websocket'
|
2
3
|
require 'sinatra'
|
3
4
|
require 'sinatra/base'
|
4
5
|
|
5
6
|
require_relative 'context'
|
7
|
+
require_relative 'screen_generator'
|
6
8
|
|
7
9
|
module TRMNLPreview
|
8
10
|
class App < Sinatra::Base
|
@@ -21,6 +23,13 @@ module TRMNLPreview
|
|
21
23
|
end
|
22
24
|
|
23
25
|
@context.poll_data if @context.strategy == 'polling'
|
26
|
+
|
27
|
+
@live_reload_clients = []
|
28
|
+
@context.on_view_change do |view|
|
29
|
+
@live_reload_clients.each do |ws|
|
30
|
+
ws.send('reload')
|
31
|
+
end
|
32
|
+
end
|
24
33
|
end
|
25
34
|
|
26
35
|
post '/webhook' do
|
@@ -32,6 +41,20 @@ module TRMNLPreview
|
|
32
41
|
redirect '/full'
|
33
42
|
end
|
34
43
|
|
44
|
+
get '/live_reload' do
|
45
|
+
ws = Faye::WebSocket.new(request.env)
|
46
|
+
|
47
|
+
ws.on(:open) do |event|
|
48
|
+
@live_reload_clients << ws
|
49
|
+
end
|
50
|
+
|
51
|
+
ws.on(:close) do |event|
|
52
|
+
@live_reload_clients.delete(ws)
|
53
|
+
end
|
54
|
+
|
55
|
+
ws.rack_response
|
56
|
+
end
|
57
|
+
|
35
58
|
get '/poll' do
|
36
59
|
@context.poll_data
|
37
60
|
redirect back
|
@@ -40,15 +63,24 @@ module TRMNLPreview
|
|
40
63
|
VIEWS.each do |view|
|
41
64
|
get "/#{view}" do
|
42
65
|
@view = view
|
66
|
+
@live_reload = @context.live_render
|
43
67
|
erb :index
|
44
68
|
end
|
45
69
|
|
46
|
-
get "/render/#{view}" do
|
70
|
+
get "/render/#{view}.html" do
|
47
71
|
@view = view
|
48
|
-
erb :
|
72
|
+
erb :render_html do
|
49
73
|
@context.render_template(view)
|
50
74
|
end
|
51
75
|
end
|
76
|
+
|
77
|
+
get "/render/#{view}.bmp" do
|
78
|
+
@view = view
|
79
|
+
html = @context.render_full_page(view)
|
80
|
+
generator = ScreenGenerator.new(html, image: true)
|
81
|
+
img_path = generator.process
|
82
|
+
send_file img_path, type: 'image/png', disposition: 'inline'
|
83
|
+
end
|
52
84
|
end
|
53
85
|
end
|
54
86
|
end
|
@@ -0,0 +1 @@
|
|
1
|
+
puts TRMNLPreview::VERSION
|
@@ -1,25 +1,22 @@
|
|
1
1
|
require 'erb'
|
2
2
|
require 'fileutils'
|
3
|
+
require 'filewatcher'
|
3
4
|
require 'json'
|
4
5
|
require 'liquid'
|
5
6
|
require 'open-uri'
|
6
7
|
require 'toml-rb'
|
7
8
|
|
8
|
-
require_relative '
|
9
|
+
require_relative 'custom_filters'
|
9
10
|
|
10
11
|
module TRMNLPreview
|
11
12
|
class Context
|
12
|
-
attr_reader :strategy, :temp_dir
|
13
|
+
attr_reader :strategy, :temp_dir, :live_render
|
13
14
|
|
14
|
-
def initialize(root)
|
15
|
+
def initialize(root, opts = {})
|
15
16
|
config_path = File.join(root, 'config.toml')
|
16
17
|
@user_views_dir = File.join(root, 'views')
|
17
18
|
@temp_dir = File.join(root, 'tmp')
|
18
19
|
@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
20
|
|
24
21
|
unless File.exist?(config_path)
|
25
22
|
raise "No config.toml found in #{root}"
|
@@ -33,12 +30,46 @@ module TRMNLPreview
|
|
33
30
|
@strategy = config['strategy']
|
34
31
|
@url = config['url']
|
35
32
|
@polling_headers = config['polling_headers'] || {}
|
33
|
+
@live_render = config['live_render'] != false
|
34
|
+
@user_filters = config['custom_filters'] || []
|
36
35
|
|
37
36
|
unless ['polling', 'webhook'].include?(@strategy)
|
38
37
|
raise "Invalid strategy: #{strategy} (must be 'polling' or 'webhook')"
|
39
38
|
end
|
40
|
-
|
39
|
+
|
41
40
|
FileUtils.mkdir_p(@temp_dir)
|
41
|
+
|
42
|
+
@liquid_environment = Liquid::Environment.build do |env|
|
43
|
+
env.register_filter(CustomFilters)
|
44
|
+
|
45
|
+
@user_filters.each do |module_name, relative_path|
|
46
|
+
require File.join(root, relative_path)
|
47
|
+
env.register_filter(Object.const_get(module_name))
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
start_filewatcher_thread if @live_render
|
52
|
+
end
|
53
|
+
|
54
|
+
def start_filewatcher_thread
|
55
|
+
Thread.new do
|
56
|
+
loop do
|
57
|
+
begin
|
58
|
+
Filewatcher.new(@user_views_dir).watch do |changes|
|
59
|
+
views = changes.map { |path, _change| File.basename(path, '.liquid') }
|
60
|
+
views.each do |view|
|
61
|
+
@view_change_callback.call(view) if @view_change_callback
|
62
|
+
end
|
63
|
+
end
|
64
|
+
rescue => e
|
65
|
+
puts "Error during live render: #{e}"
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def on_view_change(&block)
|
72
|
+
@view_change_callback = block
|
42
73
|
end
|
43
74
|
|
44
75
|
def user_data
|
@@ -75,17 +106,19 @@ module TRMNLPreview
|
|
75
106
|
end
|
76
107
|
|
77
108
|
def render_template(view)
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
109
|
+
path = view_path(view)
|
110
|
+
unless File.exist?(path)
|
111
|
+
return "Missing plugin template: views/#{view}.liquid"
|
112
|
+
end
|
113
|
+
|
114
|
+
user_template = Liquid::Template.parse(File.read(path), environment: @liquid_environment)
|
115
|
+
user_template.render(user_data)
|
116
|
+
rescue StandardError => e
|
117
|
+
e.message
|
85
118
|
end
|
86
119
|
|
87
120
|
def render_full_page(view)
|
88
|
-
page_erb_template = File.read(File.join(__dir__, '..', '..', 'web', 'views', '
|
121
|
+
page_erb_template = File.read(File.join(__dir__, '..', '..', 'web', 'views', 'render_html.erb'))
|
89
122
|
|
90
123
|
ERB.new(page_erb_template).result(ERBBinding.new(view).get_binding do
|
91
124
|
render_template(view)
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'active_support'
|
2
|
+
|
3
|
+
module TRMNLPreview
|
4
|
+
module CustomFilters
|
5
|
+
def number_with_delimiter(*args)
|
6
|
+
ActiveSupport::NumberHelper.number_to_delimited(*args)
|
7
|
+
end
|
8
|
+
|
9
|
+
def number_to_currency(*args)
|
10
|
+
ActiveSupport::NumberHelper.number_to_currency(*args)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,137 @@
|
|
1
|
+
require 'ferrum'
|
2
|
+
require 'mini_magick'
|
3
|
+
require 'puppeteer-ruby'
|
4
|
+
require 'base64'
|
5
|
+
|
6
|
+
module TRMNLPreview
|
7
|
+
class ScreenGenerator
|
8
|
+
|
9
|
+
def initialize(html, opts = {})
|
10
|
+
self.input = html
|
11
|
+
self.image = !!opts[:image]
|
12
|
+
end
|
13
|
+
|
14
|
+
attr_accessor :input, :output, :image, :processor, :img_path
|
15
|
+
|
16
|
+
def process
|
17
|
+
convert_to_image
|
18
|
+
image ? mono_image(output) : mono(output)
|
19
|
+
output.path
|
20
|
+
# IO.copy_stream(output, img_path)
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
# def img_path
|
26
|
+
# "#{Dir.pwd}/public/images/generated/#{SecureRandom.hex(3)}.bmp"
|
27
|
+
# end
|
28
|
+
|
29
|
+
# Constructs the command and passes the input to the vendor/puppeteer.js
|
30
|
+
# script for processing. Returns a base64 encoded string
|
31
|
+
def convert_to_image
|
32
|
+
retry_count = 0
|
33
|
+
begin
|
34
|
+
# context = browser_instance.create_incognito_browser_context
|
35
|
+
page = firefox_browser.new_page
|
36
|
+
page.viewport = Puppeteer::Viewport.new(width: width, height: height)
|
37
|
+
# NOTE: Use below for chromium
|
38
|
+
# page.set_content(input, wait_until: ['networkidle0', 'domcontentloaded'])
|
39
|
+
# Note: Use below for firefox
|
40
|
+
page.set_content(input, timeout: 10000)
|
41
|
+
page.evaluate(<<~JAVASCRIPT)
|
42
|
+
() => {
|
43
|
+
document.getElementsByTagName('html')[0].style.overflow = "hidden";
|
44
|
+
document.getElementsByTagName('body')[0].style.overflow = "hidden";
|
45
|
+
}
|
46
|
+
JAVASCRIPT
|
47
|
+
self.output = Tempfile.new
|
48
|
+
page.screenshot(path: output.path, type: 'png')
|
49
|
+
firefox_browser.close
|
50
|
+
end
|
51
|
+
rescue Puppeteer::TimeoutError, Puppeteer::FrameManager::NavigationError => e
|
52
|
+
retry_count += 1
|
53
|
+
firefox_browser.close
|
54
|
+
if retry_count <= 1
|
55
|
+
@browser = nil
|
56
|
+
retry
|
57
|
+
else
|
58
|
+
puts "ERROR -> Converter::Html#convert_to_image_by_firefox -> #{e.message}"
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# Refer this PR where the author reused the browser instance https://github.com/YusukeIwaki/puppeteer-ruby/pull/100/files
|
63
|
+
# This will increase the throughput of our image rendering process by 60-70%, saving about ~1.5 second per image generation.
|
64
|
+
# On local it takes < 1 second now to generate the subsequent image.
|
65
|
+
def firefox_browser
|
66
|
+
@browser ||= Puppeteer.launch(
|
67
|
+
product: 'firefox',
|
68
|
+
headless: true,
|
69
|
+
args: [
|
70
|
+
"--window-size=#{width},#{height}",
|
71
|
+
"--disable-web-security"
|
72
|
+
# "--hide-scrollbars" #works only on chrome, using page.evaluate for firefox
|
73
|
+
]
|
74
|
+
)
|
75
|
+
end
|
76
|
+
|
77
|
+
def Ferrum.cached_browser
|
78
|
+
return nil unless $cached_browser
|
79
|
+
|
80
|
+
$cached_browser
|
81
|
+
end
|
82
|
+
|
83
|
+
def Ferrum.cached_browser=(value)
|
84
|
+
$cached_browser = value
|
85
|
+
end
|
86
|
+
|
87
|
+
# Overall at max wait for 2.5 seconds
|
88
|
+
def wait_for_stop_loading(page)
|
89
|
+
count = 0
|
90
|
+
while page.frames.first.state != :stopped_loading && count < 20
|
91
|
+
count += 1
|
92
|
+
sleep 0.1
|
93
|
+
end
|
94
|
+
sleep 0.5 # wait_until: DomContentLoaded event is not available in ferrum
|
95
|
+
end
|
96
|
+
|
97
|
+
def mono(img)
|
98
|
+
MiniMagick::Tool::Convert.new do |m|
|
99
|
+
m << img.path
|
100
|
+
m.monochrome # Use built-in smart monochrome dithering (but it's not working as expected)
|
101
|
+
m.depth(color_depth) # Should be set to 1 for 1-bit output
|
102
|
+
m.strip # Remove any additional metadata
|
103
|
+
m << ('bmp3:' << img.path)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def mono_image(img)
|
108
|
+
# Changelog:
|
109
|
+
# ImageMagick 6.XX used to convert the png to bitmap with dithering while maintaining the channel to 1
|
110
|
+
# The same seems to be broken with imagemagick 7.XX
|
111
|
+
# So in order to reduce the channel from 8 to 1, I just rerun the command, and it's working
|
112
|
+
# TODO for future, find a better way to generate image screens.
|
113
|
+
MiniMagick::Tool::Convert.new do |m|
|
114
|
+
m << img.path
|
115
|
+
m.dither << 'FloydSteinberg'
|
116
|
+
m.remap << 'pattern:gray50'
|
117
|
+
m.depth(color_depth) # Should be set to 1 for 1-bit output
|
118
|
+
m.strip # Remove any additional metadata
|
119
|
+
m << ('bmp3:' << img.path) # Converts to Bitmap.
|
120
|
+
end
|
121
|
+
MiniMagick::Tool::Convert.new do |m|
|
122
|
+
m << img.path
|
123
|
+
m.dither << 'FloydSteinberg'
|
124
|
+
m.remap << 'pattern:gray50'
|
125
|
+
m.depth(color_depth) # Should be set to 1 for 1-bit output
|
126
|
+
m.strip # Remove any additional metadata
|
127
|
+
m << ('bmp3:' << img.path) # Converts to Bitmap.
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
def width = 800
|
132
|
+
|
133
|
+
def height = 480
|
134
|
+
|
135
|
+
def color_depth = 1
|
136
|
+
end
|
137
|
+
end
|
data/trmnl_preview.gemspec
CHANGED
@@ -31,12 +31,25 @@ Gem::Specification.new do |spec|
|
|
31
31
|
spec.executables = ["trmnlp"]
|
32
32
|
spec.require_paths = ["lib"]
|
33
33
|
|
34
|
-
|
34
|
+
|
35
|
+
# Web server
|
35
36
|
spec.add_dependency "sinatra", "~> 4.1"
|
36
37
|
spec.add_dependency "rackup", "~> 2.2"
|
37
38
|
spec.add_dependency "puma", "~> 6.5"
|
39
|
+
spec.add_dependency "faye-websocket", "~> 0.11.3"
|
40
|
+
|
41
|
+
# HTML rendering
|
38
42
|
spec.add_dependency "liquid", "~> 5.6"
|
43
|
+
spec.add_dependency "activesupport", "~> 8.0"
|
44
|
+
|
45
|
+
# BMP rendering
|
46
|
+
spec.add_dependency "ferrum", "~> 0.16"
|
47
|
+
spec.add_dependency 'puppeteer-ruby', '~> 0.45.6'
|
48
|
+
spec.add_dependency 'mini_magick', '~> 4.12.0'
|
49
|
+
|
50
|
+
# Utilities
|
39
51
|
spec.add_dependency "toml-rb", "~> 3.0"
|
52
|
+
spec.add_dependency "filewatcher", "~> 2.1"
|
40
53
|
|
41
54
|
# For more information and examples about making a new gem, check out our
|
42
55
|
# guide at: https://bundler.io/guides/creating_gem.html
|
@@ -0,0 +1,98 @@
|
|
1
|
+
body {
|
2
|
+
font-family: sans-serif;
|
3
|
+
margin: 10px;
|
4
|
+
}
|
5
|
+
|
6
|
+
main {
|
7
|
+
display: flex;
|
8
|
+
flex-direction: column;
|
9
|
+
width: fit-content;
|
10
|
+
}
|
11
|
+
|
12
|
+
menu {
|
13
|
+
padding: 0;
|
14
|
+
margin: 0;
|
15
|
+
display: flex;
|
16
|
+
justify-content: space-between;
|
17
|
+
}
|
18
|
+
|
19
|
+
menu a {
|
20
|
+
padding: 0.5em 1em;
|
21
|
+
background: #ddd;
|
22
|
+
border-radius: 0.5em;
|
23
|
+
display: inline-block;
|
24
|
+
text-decoration: none;
|
25
|
+
color: black;
|
26
|
+
}
|
27
|
+
|
28
|
+
menu a:hover {
|
29
|
+
background: #ccc;
|
30
|
+
}
|
31
|
+
|
32
|
+
menu a.active {
|
33
|
+
background: #333;
|
34
|
+
color: white;
|
35
|
+
}
|
36
|
+
|
37
|
+
.case {
|
38
|
+
width: 1000px;
|
39
|
+
height: 680px;
|
40
|
+
position: relative;
|
41
|
+
}
|
42
|
+
|
43
|
+
iframe {
|
44
|
+
position: absolute;
|
45
|
+
border: none;
|
46
|
+
left: 98px;
|
47
|
+
top: 65px;
|
48
|
+
filter: grayscale(100%);
|
49
|
+
}
|
50
|
+
|
51
|
+
.case .case-overlay {
|
52
|
+
position: absolute;
|
53
|
+
width: 100%;
|
54
|
+
height: 100%;
|
55
|
+
top: 0;
|
56
|
+
left: 0;
|
57
|
+
background-size: cover;
|
58
|
+
mix-blend-mode: multiply;
|
59
|
+
pointer-events: none;
|
60
|
+
}
|
61
|
+
|
62
|
+
.case--none .case-overlay {
|
63
|
+
background: none;
|
64
|
+
}
|
65
|
+
|
66
|
+
.case--white .case-overlay {
|
67
|
+
background-image: url("white-case.jpg");
|
68
|
+
}
|
69
|
+
|
70
|
+
.case--black .case-overlay {
|
71
|
+
background-image: url("black-case.jpg");
|
72
|
+
}
|
73
|
+
|
74
|
+
.case--clear .case-overlay {
|
75
|
+
background-image: url("clear-case.jpg");
|
76
|
+
}
|
77
|
+
|
78
|
+
.case--none iframe {
|
79
|
+
border: 1px solid black;
|
80
|
+
}
|
81
|
+
|
82
|
+
.spinner {
|
83
|
+
width: 22px;
|
84
|
+
height: 22px;
|
85
|
+
border: 5px solid #f3f3f3; /* Light gray */
|
86
|
+
border-top: 5px solid #3498db; /* Blue */
|
87
|
+
border-radius: 50%;
|
88
|
+
animation: spin 1s linear infinite;
|
89
|
+
}
|
90
|
+
|
91
|
+
@keyframes spin {
|
92
|
+
0% {
|
93
|
+
transform: rotate(0deg);
|
94
|
+
}
|
95
|
+
100% {
|
96
|
+
transform: rotate(360deg);
|
97
|
+
}
|
98
|
+
}
|
data/web/public/index.js
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
const trmnlp = {};
|
2
|
+
|
3
|
+
trmnlp.connectLiveRender = function () {
|
4
|
+
const ws = new WebSocket("/live_reload");
|
5
|
+
|
6
|
+
ws.onopen = function () {
|
7
|
+
console.log("Connected to live reload socket");
|
8
|
+
};
|
9
|
+
|
10
|
+
ws.onmessage = function (msg) {
|
11
|
+
if (msg.data === "reload") {
|
12
|
+
trmnlp.setIframeSrc(trmnlp.iframe.src);
|
13
|
+
}
|
14
|
+
};
|
15
|
+
|
16
|
+
ws.onclose = function () {
|
17
|
+
console.log("Reconnecting to live reload socket...");
|
18
|
+
setTimeout(trmnlp.connectLiveRender, 1000);
|
19
|
+
};
|
20
|
+
};
|
21
|
+
|
22
|
+
trmnlp.setCaseImage = function () {
|
23
|
+
const value = trmnlp.caseSelect.value;
|
24
|
+
document.querySelector(".case").className = `case case--${value}`;
|
25
|
+
localStorage.setItem("trmnlp-case", value);
|
26
|
+
};
|
27
|
+
|
28
|
+
trmnlp.setPreviewFormat = function () {
|
29
|
+
const value = trmnlp.formatSelect.value;
|
30
|
+
localStorage.setItem("trmnlp-format", value);
|
31
|
+
|
32
|
+
trmnlp.setIframeSrc(`/render/${trmnlp.view}.${value}`);
|
33
|
+
};
|
34
|
+
|
35
|
+
trmnlp.setIframeSrc = function (src) {
|
36
|
+
document.querySelector(".spinner").style.display = "inline-block";
|
37
|
+
trmnlp.iframe.src = src;
|
38
|
+
};
|
39
|
+
|
40
|
+
document.addEventListener("DOMContentLoaded", function () {
|
41
|
+
trmnlp.view = document.querySelector("meta[name='trmnl-view']").content;
|
42
|
+
trmnlp.iframe = document.querySelector("iframe");
|
43
|
+
trmnlp.caseSelect = document.querySelector(".select-case");
|
44
|
+
trmnlp.formatSelect = document.querySelector(".select-format");
|
45
|
+
trmnlp.isLiveReloadEnabled =
|
46
|
+
document.querySelector("meta[name='live-reload']").content === "true";
|
47
|
+
|
48
|
+
if (trmnlp.isLiveReloadEnabled) {
|
49
|
+
trmnlp.connectLiveRender();
|
50
|
+
}
|
51
|
+
|
52
|
+
const caseValue = localStorage.getItem("trmnlp-case") || "black";
|
53
|
+
const formatValue = localStorage.getItem("trmnlp-format") || "html";
|
54
|
+
|
55
|
+
trmnlp.caseSelect.value = caseValue;
|
56
|
+
trmnlp.caseSelect.addEventListener("change", () => {
|
57
|
+
trmnlp.setCaseImage();
|
58
|
+
});
|
59
|
+
|
60
|
+
trmnlp.formatSelect.value = formatValue;
|
61
|
+
trmnlp.formatSelect.addEventListener("change", () => {
|
62
|
+
trmnlp.setPreviewFormat();
|
63
|
+
});
|
64
|
+
|
65
|
+
trmnlp.iframe.addEventListener("load", () => {
|
66
|
+
document.querySelector(".spinner").style.display = "none";
|
67
|
+
});
|
68
|
+
|
69
|
+
trmnlp.setCaseImage();
|
70
|
+
trmnlp.setPreviewFormat();
|
71
|
+
});
|
data/web/views/index.erb
CHANGED
@@ -3,85 +3,13 @@
|
|
3
3
|
<head>
|
4
4
|
<meta charset="UTF-8">
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
6
|
+
<meta name="trmnl-view" content="<%= @view %>">
|
7
|
+
<meta name="live-reload" content="<%= @live_reload %>">
|
8
|
+
|
6
9
|
<title>TRMNL Preview</title>
|
7
|
-
|
8
|
-
|
9
|
-
|
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>
|
10
|
+
|
11
|
+
<link rel="stylesheet" href="/index.css">
|
12
|
+
<script src="/index.js"></script>
|
85
13
|
</head>
|
86
14
|
<body>
|
87
15
|
<main>
|
@@ -92,7 +20,12 @@
|
|
92
20
|
<a class="<%= 'active' if @view == 'half_vertical' %>" href="/half_vertical">Half Vertical</a>
|
93
21
|
<a class="<%= 'active' if @view == 'quadrant' %>" href="/quadrant">Quadrant</a>
|
94
22
|
</div>
|
95
|
-
<div>
|
23
|
+
<div style="display: flex; gap: 0.3em;">
|
24
|
+
<div class="spinner" style="display: none;"></div>
|
25
|
+
<select class="select-format">
|
26
|
+
<option value="bmp">BMP</option>
|
27
|
+
<option value="html" selected>HTML</option>
|
28
|
+
</select>
|
96
29
|
<select class="select-case">
|
97
30
|
<option value="none">None</option>
|
98
31
|
<option value="white">White</option>
|
@@ -105,7 +38,7 @@
|
|
105
38
|
</menu>
|
106
39
|
|
107
40
|
<div class="case case--black">
|
108
|
-
<iframe
|
41
|
+
<iframe width="800" height="480"></iframe>
|
109
42
|
<div class="case-overlay"></div>
|
110
43
|
</div>
|
111
44
|
</main>
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: trmnl_preview
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.3.
|
4
|
+
version: 0.3.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Rockwell Schrock
|
8
8
|
bindir: exe
|
9
9
|
cert_chain: []
|
10
|
-
date: 2025-01-
|
10
|
+
date: 2025-01-08 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
12
|
- !ruby/object:Gem::Dependency
|
13
13
|
name: sinatra
|
@@ -51,6 +51,20 @@ dependencies:
|
|
51
51
|
- - "~>"
|
52
52
|
- !ruby/object:Gem::Version
|
53
53
|
version: '6.5'
|
54
|
+
- !ruby/object:Gem::Dependency
|
55
|
+
name: faye-websocket
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
57
|
+
requirements:
|
58
|
+
- - "~>"
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: 0.11.3
|
61
|
+
type: :runtime
|
62
|
+
prerelease: false
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
64
|
+
requirements:
|
65
|
+
- - "~>"
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: 0.11.3
|
54
68
|
- !ruby/object:Gem::Dependency
|
55
69
|
name: liquid
|
56
70
|
requirement: !ruby/object:Gem::Requirement
|
@@ -65,6 +79,62 @@ dependencies:
|
|
65
79
|
- - "~>"
|
66
80
|
- !ruby/object:Gem::Version
|
67
81
|
version: '5.6'
|
82
|
+
- !ruby/object:Gem::Dependency
|
83
|
+
name: activesupport
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - "~>"
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: '8.0'
|
89
|
+
type: :runtime
|
90
|
+
prerelease: false
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
92
|
+
requirements:
|
93
|
+
- - "~>"
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: '8.0'
|
96
|
+
- !ruby/object:Gem::Dependency
|
97
|
+
name: ferrum
|
98
|
+
requirement: !ruby/object:Gem::Requirement
|
99
|
+
requirements:
|
100
|
+
- - "~>"
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: '0.16'
|
103
|
+
type: :runtime
|
104
|
+
prerelease: false
|
105
|
+
version_requirements: !ruby/object:Gem::Requirement
|
106
|
+
requirements:
|
107
|
+
- - "~>"
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '0.16'
|
110
|
+
- !ruby/object:Gem::Dependency
|
111
|
+
name: puppeteer-ruby
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
113
|
+
requirements:
|
114
|
+
- - "~>"
|
115
|
+
- !ruby/object:Gem::Version
|
116
|
+
version: 0.45.6
|
117
|
+
type: :runtime
|
118
|
+
prerelease: false
|
119
|
+
version_requirements: !ruby/object:Gem::Requirement
|
120
|
+
requirements:
|
121
|
+
- - "~>"
|
122
|
+
- !ruby/object:Gem::Version
|
123
|
+
version: 0.45.6
|
124
|
+
- !ruby/object:Gem::Dependency
|
125
|
+
name: mini_magick
|
126
|
+
requirement: !ruby/object:Gem::Requirement
|
127
|
+
requirements:
|
128
|
+
- - "~>"
|
129
|
+
- !ruby/object:Gem::Version
|
130
|
+
version: 4.12.0
|
131
|
+
type: :runtime
|
132
|
+
prerelease: false
|
133
|
+
version_requirements: !ruby/object:Gem::Requirement
|
134
|
+
requirements:
|
135
|
+
- - "~>"
|
136
|
+
- !ruby/object:Gem::Version
|
137
|
+
version: 4.12.0
|
68
138
|
- !ruby/object:Gem::Dependency
|
69
139
|
name: toml-rb
|
70
140
|
requirement: !ruby/object:Gem::Requirement
|
@@ -79,6 +149,20 @@ dependencies:
|
|
79
149
|
- - "~>"
|
80
150
|
- !ruby/object:Gem::Version
|
81
151
|
version: '3.0'
|
152
|
+
- !ruby/object:Gem::Dependency
|
153
|
+
name: filewatcher
|
154
|
+
requirement: !ruby/object:Gem::Requirement
|
155
|
+
requirements:
|
156
|
+
- - "~>"
|
157
|
+
- !ruby/object:Gem::Version
|
158
|
+
version: '2.1'
|
159
|
+
type: :runtime
|
160
|
+
prerelease: false
|
161
|
+
version_requirements: !ruby/object:Gem::Requirement
|
162
|
+
requirements:
|
163
|
+
- - "~>"
|
164
|
+
- !ruby/object:Gem::Version
|
165
|
+
version: '2.1'
|
82
166
|
description: Automatically rebuild and preview TRNML plugins in multiple views
|
83
167
|
email:
|
84
168
|
- rockwell@schrock.me
|
@@ -99,15 +183,19 @@ files:
|
|
99
183
|
- lib/trmnl_preview/cmd/build.rb
|
100
184
|
- lib/trmnl_preview/cmd/serve.rb
|
101
185
|
- lib/trmnl_preview/cmd/usage.rb
|
186
|
+
- lib/trmnl_preview/cmd/version.rb
|
102
187
|
- lib/trmnl_preview/context.rb
|
103
|
-
- lib/trmnl_preview/
|
188
|
+
- lib/trmnl_preview/custom_filters.rb
|
189
|
+
- lib/trmnl_preview/screen_generator.rb
|
104
190
|
- lib/trmnl_preview/version.rb
|
105
191
|
- trmnl_preview.gemspec
|
106
192
|
- web/public/black-case.jpg
|
107
193
|
- web/public/clear-case.jpg
|
194
|
+
- web/public/index.css
|
195
|
+
- web/public/index.js
|
108
196
|
- web/public/white-case.jpg
|
109
197
|
- web/views/index.erb
|
110
|
-
- web/views/
|
198
|
+
- web/views/render_html.erb
|
111
199
|
homepage: https://github.com/schrockwell/trmnl_preview
|
112
200
|
licenses:
|
113
201
|
- MIT
|
File without changes
|