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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3944c7a74517407dbb4ee683c628111668149c622be67a4ba3d69a2655d8b48e
4
- data.tar.gz: e6c9d4f7472e4b56d818884ba6ca33307706891da62c40121398036507568fcd
3
+ metadata.gz: e965687c88e23b15e8727f9b871b6a2529bc28c388fa2e8733fb5034915ec405
4
+ data.tar.gz: c1299d3ce7b27a13be79d0018535c8f1b64171f7154d9dd1f425eeacbcbc0129
5
5
  SHA512:
6
- metadata.gz: 1fd0a512a23f1439cc4030543f92272682917d1024ae8ef6eed7449f51d7677add6a0974ed4783b56ad088729ac53a7343e0c00d9026c83d762fb9f825406eb2
7
- data.tar.gz: 70c1ba02c0df2d7e427fc079f0bee439fdeea0c120f348d562f734e10f10b210c11db34fbc99c76894afa5622714445efb1fee17fa44029080ba22c216462b1d
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
- # trmnl_preview
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 locally as HTML, leveraging the [TRMNL Design System](https://usetrmnl.com/framework). This server does NOT generate a rendered BMP file. Hence, this is just a _preview_ of the final rendered dashboard.
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
  ![Screenshot](docs/preview.png)
8
10
 
@@ -34,17 +36,22 @@ docker run \
34
36
 
35
37
  ## Running the Server (Local Ruby)
36
38
 
37
- Ruby 3.x is required. In the plugin repository:
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
- bundle add trmnl_preview # Creates Gemfile and Gemfile.lock
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
@@ -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 :render_view do
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
@@ -5,6 +5,7 @@ Usage:
5
5
 
6
6
  Commands (-h for command-specific help):
7
7
 
8
- serve Start the TRMNL Preview server
9
8
  build Generate static HTML files
9
+ serve Start the TRMNL Preview server
10
+ version Print the version number
10
11
  USAGE
@@ -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 'liquid_filters'
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
- 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)
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', 'render_view.erb'))
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module TRMNLPreview
4
- VERSION = "0.3.0"
4
+ VERSION = "0.3.2"
5
5
  end
@@ -31,12 +31,25 @@ Gem::Specification.new do |spec|
31
31
  spec.executables = ["trmnlp"]
32
32
  spec.require_paths = ["lib"]
33
33
 
34
- # Uncomment to register a new dependency of your gem
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
+ }
@@ -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
- <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>
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 src="/render/<%= @view %>" width="800" height="480"></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.0
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-06 00:00:00.000000000 Z
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/liquid_filters.rb
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/render_view.erb
198
+ - web/views/render_html.erb
111
199
  homepage: https://github.com/schrockwell/trmnl_preview
112
200
  licenses:
113
201
  - MIT
@@ -1,8 +0,0 @@
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
7
- end
8
- end
File without changes