trmnl_preview 0.15.0 → 0.16.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 +4 -4
- data/CHANGELOG.md +16 -0
- data/README.md +11 -0
- data/db/data/and_x_more.yml +86 -0
- data/db/data/locales/cs.yml +339 -0
- data/db/data/locales/da.yml +339 -0
- data/db/data/locales/de-AT.yml +341 -0
- data/db/data/locales/de.yml +341 -0
- data/db/data/locales/en-AU.yml +79 -0
- data/db/data/locales/en-GB.yml +79 -0
- data/db/data/locales/en.yml +341 -0
- data/db/data/locales/es-ES.yml +341 -0
- data/db/data/locales/fr.yml +339 -0
- data/db/data/locales/he.yml +339 -0
- data/db/data/locales/hu.yml +339 -0
- data/db/data/locales/id.yml +339 -0
- data/db/data/locales/is.yml +339 -0
- data/db/data/locales/it.yml +341 -0
- data/db/data/locales/ja.yml +342 -0
- data/db/data/locales/ko.yml +337 -0
- data/db/data/locales/lt.yml +340 -0
- data/db/data/locales/nl.yml +339 -0
- data/db/data/locales/no.yml +4 -0
- data/db/data/locales/pl.yml +342 -0
- data/db/data/locales/pt-BR.yml +340 -0
- data/db/data/locales/raw.yml +256 -0
- data/db/data/locales/ro.yml +339 -0
- data/db/data/locales/ru.yml +342 -0
- data/db/data/locales/sk.yml +339 -0
- data/db/data/locales/sv.yml +340 -0
- data/db/data/locales/uk.yml +339 -0
- data/db/data/locales/zh-CN.yml +338 -0
- data/db/data/locales/zh-HK.yml +338 -0
- data/lib/trmnlp/app.rb +5 -4
- data/lib/trmnlp/async_callback.rb +75 -0
- data/lib/trmnlp/commands/serve.rb +3 -0
- data/lib/trmnlp/config/plugin.rb +52 -12
- data/lib/trmnlp/config/project.rb +14 -4
- data/lib/trmnlp/context.rb +17 -2
- data/lib/trmnlp/firefox_driver.rb +0 -3
- data/lib/trmnlp/framework_version.rb +9 -0
- data/lib/trmnlp/image_quantizer.rb +28 -15
- data/lib/trmnlp/lint/checks/no_custom_filters.rb +27 -0
- data/lib/trmnlp/lint/diagnostic.rb +5 -0
- data/lib/trmnlp/lint/source.rb +6 -0
- data/lib/trmnlp/lint.rb +3 -1
- data/lib/trmnlp/merged_plugin_locals.rb +57 -0
- data/lib/trmnlp/outbound_request.rb +99 -0
- data/lib/trmnlp/paths.rb +11 -6
- data/lib/trmnlp/poller.rb +83 -44
- data/lib/trmnlp/rails_helpers.rb +41 -0
- data/lib/trmnlp/renderer.rb +37 -11
- data/lib/trmnlp/screen_generator.rb +14 -2
- data/lib/trmnlp/screenshot.rb +29 -4
- data/lib/trmnlp/select_field_choices.rb +25 -0
- data/lib/trmnlp/transform_pipeline.rb +47 -6
- data/lib/trmnlp/transform_state.rb +48 -0
- data/lib/trmnlp/user_data_assembler.rb +63 -25
- data/lib/trmnlp/version.rb +1 -1
- data/lib/trmnlp/watcher.rb +2 -1
- data/lib/trmnlp/webhook_payload.rb +74 -0
- data/lib/trmnlp/webhook_receiver.rb +78 -0
- data/lib/trmnlp.rb +1 -1
- data/trmnl_preview.gemspec +1 -0
- data/web/public/index.js +18 -6
- data/web/views/index.erb +7 -0
- data/web/views/render_html.erb +36 -0
- metadata +54 -1
|
@@ -4,15 +4,20 @@ require 'active_support'
|
|
|
4
4
|
require 'active_support/time'
|
|
5
5
|
require 'json'
|
|
6
6
|
|
|
7
|
+
require_relative 'merged_plugin_locals'
|
|
8
|
+
require_relative 'transform_state'
|
|
9
|
+
|
|
7
10
|
module TRMNLP
|
|
8
11
|
class UserDataAssembler
|
|
9
12
|
DEFAULT_DEVICE_WIDTH = 800
|
|
10
13
|
DEFAULT_DEVICE_HEIGHT = 480
|
|
11
14
|
|
|
12
|
-
def initialize(config:, paths:, transform_pipeline:)
|
|
15
|
+
def initialize(config:, paths:, transform_pipeline:, oauth_session: nil)
|
|
13
16
|
@config = config
|
|
14
17
|
@paths = paths
|
|
15
18
|
@transform_pipeline = transform_pipeline
|
|
19
|
+
@oauth_session = oauth_session
|
|
20
|
+
@transform_state = TransformState.new(paths:)
|
|
16
21
|
end
|
|
17
22
|
|
|
18
23
|
# Assembles the merged data hash. The trmnl namespace is built first,
|
|
@@ -21,41 +26,61 @@ module TRMNLP
|
|
|
21
26
|
# (overrides included) is re-applied after the transform so it
|
|
22
27
|
# survives even when the transform doesn't pass it through.
|
|
23
28
|
def call(device: {})
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
result = transform_pipeline.call(transform_input(merged))
|
|
27
|
-
|
|
29
|
+
merged = assemble(base_trmnl_data(device:), source_data)
|
|
30
|
+
runs_transform = config.plugin.transforms_on_render? && transform_pipeline.configured?
|
|
31
|
+
result = runs_transform ? transform_pipeline.call(transform_input(merged)) : merged.except('trmnl')
|
|
32
|
+
# The markup renders the state this run's transform kept, as on TRMNL.
|
|
33
|
+
result['trmnl'] = merged['trmnl'].merge('state' => transform_state.read)
|
|
28
34
|
result
|
|
29
35
|
end
|
|
30
36
|
|
|
37
|
+
def transform_webhook_post(merge_variables)
|
|
38
|
+
transform_pipeline.call(transform_input(assemble(base_trmnl_data(device: {}), merge_variables)))
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# What a polling url, its headers and its body render with on TRMNL.
|
|
42
|
+
def polling_variables = assemble(base_trmnl_data(device: {}), {}).slice('trmnl')
|
|
43
|
+
|
|
31
44
|
def device_from_params(params)
|
|
32
|
-
{ 'width' => params[:width]&.to_i, 'height' => params[:height]&.to_i,
|
|
33
|
-
'
|
|
45
|
+
{ 'width' => params[:width]&.to_i, 'height' => params[:height]&.to_i, 'model' => params[:model],
|
|
46
|
+
'bit_depth' => params[:bit_depth]&.to_i, 'orientation' => params[:orientation] }.compact
|
|
34
47
|
end
|
|
35
48
|
|
|
36
49
|
private
|
|
37
50
|
|
|
38
|
-
attr_reader :config, :paths, :transform_pipeline
|
|
51
|
+
attr_reader :config, :paths, :transform_pipeline, :transform_state, :oauth_session
|
|
39
52
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
data.deep_merge!(config.project.user_data_overrides)
|
|
44
|
-
data
|
|
53
|
+
# The trmnl namespace wins over a trmnl key in the fetched data, as on TRMNL; only .trmnlp.yml overrides it.
|
|
54
|
+
def assemble(namespace, source)
|
|
55
|
+
source.merge(namespace).deep_merge(config.project.user_data_overrides)
|
|
45
56
|
end
|
|
46
57
|
|
|
47
|
-
# The hosted service exposes only user/device/plugin_settings to the
|
|
58
|
+
# The hosted service exposes only user/device/plugin_settings/state to the
|
|
48
59
|
# transform; the system namespace is added afterward. Mirror that slice
|
|
49
60
|
# so transforms behave the same locally as in production.
|
|
50
61
|
def transform_input(merged)
|
|
51
|
-
|
|
62
|
+
trmnl = merged['trmnl'].slice('user', 'device', 'plugin_settings', 'state')
|
|
63
|
+
trmnl['previous_merge_variables'] = previous_merge_variables
|
|
64
|
+
oauth = oauth_for_transform
|
|
65
|
+
trmnl['oauth'] = oauth unless oauth.empty?
|
|
66
|
+
merged.merge('trmnl' => trmnl)
|
|
52
67
|
end
|
|
53
68
|
|
|
54
|
-
|
|
69
|
+
# The connected account's token, so a transform can make its own authenticated calls.
|
|
70
|
+
def oauth_for_transform = (oauth_session&.liquid_variables || {}).transform_keys { it.delete_prefix('oauth_') }
|
|
71
|
+
|
|
72
|
+
# What the last run stored for the markup: a webhook's stored data, otherwise the last transform output.
|
|
73
|
+
def previous_merge_variables = config.plugin.webhook? ? source_data : transform_pipeline.previous_output
|
|
74
|
+
|
|
75
|
+
def source_data
|
|
55
76
|
if config.plugin.static?
|
|
56
|
-
|
|
77
|
+
config.plugin.static_data
|
|
78
|
+
elsif config.plugin.plugin_merge?
|
|
79
|
+
MergedPluginLocals.new(config:, paths:).call
|
|
57
80
|
elsif paths.user_data.exist?
|
|
58
|
-
|
|
81
|
+
JSON.parse(paths.user_data.read)
|
|
82
|
+
else
|
|
83
|
+
{}
|
|
59
84
|
end
|
|
60
85
|
end
|
|
61
86
|
|
|
@@ -68,7 +93,8 @@ module TRMNLP
|
|
|
68
93
|
'user' => user_namespace,
|
|
69
94
|
'device' => device_namespace(device),
|
|
70
95
|
'system' => { 'timestamp_utc' => Time.now.utc.to_i },
|
|
71
|
-
'plugin_settings' => plugin_settings_namespace
|
|
96
|
+
'plugin_settings' => plugin_settings_namespace,
|
|
97
|
+
'state' => transform_state.read
|
|
72
98
|
}
|
|
73
99
|
end
|
|
74
100
|
|
|
@@ -89,21 +115,33 @@ module TRMNLP
|
|
|
89
115
|
'height' => device['height'] || DEFAULT_DEVICE_HEIGHT,
|
|
90
116
|
'width' => device['width'] || DEFAULT_DEVICE_WIDTH,
|
|
91
117
|
'model' => device['model'] || 'og_plus', 'bit_depth' => device['bit_depth'] || 2,
|
|
92
|
-
'
|
|
118
|
+
'orientation' => device['orientation'] || 'landscape',
|
|
119
|
+
'firmware_version' => '1.8.17', 'refresh_interval_seconds' => 900,
|
|
93
120
|
'sleep_mode_enabled' => false, 'sleep_start_time' => 1320, 'sleep_end_time' => 480
|
|
94
121
|
}
|
|
95
122
|
end
|
|
96
123
|
|
|
97
124
|
def plugin_settings_namespace
|
|
98
125
|
{
|
|
99
|
-
'instance_name' => '
|
|
126
|
+
'instance_name' => config.plugin.settings['name'],
|
|
100
127
|
'refresh_interval_minutes' => config.plugin.refresh_interval,
|
|
101
128
|
'strategy' => config.plugin.strategy,
|
|
102
129
|
'dark_mode' => config.plugin.dark_mode,
|
|
103
|
-
'
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
130
|
+
'no_screen_padding' => config.plugin.no_screen_padding
|
|
131
|
+
}.merge(custom_fields_setting, polling_url_setting, data_fetched_setting)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def custom_fields_setting
|
|
135
|
+
values = config.plugin.custom_fields_values
|
|
136
|
+
values.empty? ? {} : { 'custom_fields_values' => values }
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# The polling url can carry the author's API key, so TRMNL exposes it only to a polling plugin.
|
|
140
|
+
def polling_url_setting = config.plugin.polling? ? { 'polling_url' => config.plugin.polling_url_text } : {}
|
|
141
|
+
|
|
142
|
+
def data_fetched_setting
|
|
143
|
+
fetched = !config.plugin.static? && paths.user_data.exist?
|
|
144
|
+
fetched ? { 'data_fetched_utc' => paths.user_data.mtime.to_i } : {}
|
|
107
145
|
end
|
|
108
146
|
end
|
|
109
147
|
end
|
data/lib/trmnlp/version.rb
CHANGED
data/lib/trmnlp/watcher.rb
CHANGED
|
@@ -67,7 +67,8 @@ module TRMNLP
|
|
|
67
67
|
# NOTE: transform.* changes don't trigger a re-poll — the transform
|
|
68
68
|
# runs inside user_data on every render against the cached polled
|
|
69
69
|
# response (or static_data), so editing the transform updates the
|
|
70
|
-
# preview without re-fetching the API.
|
|
70
|
+
# preview without re-fetching the API. A webhook plugin's transform
|
|
71
|
+
# runs only when a post arrives, as on TRMNL.
|
|
71
72
|
def notify(changes)
|
|
72
73
|
data = user_data_assembler.call
|
|
73
74
|
return unless @view_change_callback
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'active_support/core_ext/hash/deep_merge'
|
|
4
|
+
require 'json'
|
|
5
|
+
|
|
6
|
+
module TRMNLP
|
|
7
|
+
# A webhook post as TRMNL takes it: merge_variables inside an object, merged into the
|
|
8
|
+
# stored data under merge_strategy, which the body or the query string may carry.
|
|
9
|
+
class WebhookPayload
|
|
10
|
+
MERGE_STRATEGIES = [nil, 'replace', 'deep_merge', 'stream'].freeze
|
|
11
|
+
STORED_SIZE_LIMIT_BYTES = 5 * 1024
|
|
12
|
+
|
|
13
|
+
def initialize(body, params = {})
|
|
14
|
+
@params = params.to_h.merge(body.is_a?(Hash) ? body : {})
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def rejection
|
|
18
|
+
strategy = params['merge_strategy']
|
|
19
|
+
return "Invalid merge_strategy: '#{strategy}'" unless MERGE_STRATEGIES.include?(strategy)
|
|
20
|
+
|
|
21
|
+
case merge_variables
|
|
22
|
+
when Hash then nil
|
|
23
|
+
when nil, Array then 'Must be nested inside a merge_variables payload object'
|
|
24
|
+
when String then "Received string: '#{merge_variables}', expecting object"
|
|
25
|
+
else 'Expected an object payload'
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def merge_variables
|
|
30
|
+
return @merge_variables if defined?(@merge_variables)
|
|
31
|
+
|
|
32
|
+
value = params['merge_variables']
|
|
33
|
+
@merge_variables = value.is_a?(String) ? (parsed_object(value) || value) : value
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# A serverless webhook merges its transform output in place of the posted merge_variables.
|
|
37
|
+
def merged_into(stored, incoming = merge_variables)
|
|
38
|
+
case merge_strategy
|
|
39
|
+
when 'deep_merge' then stored.deep_merge(incoming)
|
|
40
|
+
when 'stream' then streamed_into(stored, incoming)
|
|
41
|
+
else incoming
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# TRMNL refuses a post past its plan's limit; trmnlp cannot know the plan, so it only warns.
|
|
46
|
+
def size_warning(merged)
|
|
47
|
+
size = JSON.generate(merged).bytesize
|
|
48
|
+
"#{size} bytes stored; TRMNL rejects more than 5 kB (10 kB with TRMNL+)" if size > STORED_SIZE_LIMIT_BYTES
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
attr_reader :params
|
|
54
|
+
|
|
55
|
+
def merge_strategy = params['deep_merge'].to_s == 'true' ? 'deep_merge' : params['merge_strategy']
|
|
56
|
+
|
|
57
|
+
def parsed_object(text)
|
|
58
|
+
parsed = JSON.parse(text)
|
|
59
|
+
parsed if parsed.is_a?(Hash)
|
|
60
|
+
rescue JSON::ParserError
|
|
61
|
+
nil
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def streamed_into(stored, incoming)
|
|
65
|
+
limit = params['stream_limit'].to_i
|
|
66
|
+
incoming.to_h do |key, value|
|
|
67
|
+
next [key, value] unless stored[key].is_a?(Array) && value.is_a?(Array)
|
|
68
|
+
|
|
69
|
+
combined = stored[key] + value
|
|
70
|
+
[key, limit.positive? ? combined.last(limit) : combined]
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'json'
|
|
4
|
+
|
|
5
|
+
require_relative 'reporter'
|
|
6
|
+
require_relative 'webhook_payload'
|
|
7
|
+
|
|
8
|
+
module TRMNLP
|
|
9
|
+
# Takes a webhook post the way TRMNL does: a serverless plugin transforms the post once, as it
|
|
10
|
+
# arrives, and the result is merged into the stored data. Answers TRMNL's status and JSON body.
|
|
11
|
+
class WebhookReceiver
|
|
12
|
+
MAX_SERVERLESS_INCOMING_BYTES = 1024 * 1024
|
|
13
|
+
|
|
14
|
+
def initialize(paths:, transform_pipeline:, user_data_assembler:, reporter: Reporter.new)
|
|
15
|
+
@paths = paths
|
|
16
|
+
@transform_pipeline = transform_pipeline
|
|
17
|
+
@user_data_assembler = user_data_assembler
|
|
18
|
+
@reporter = reporter
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def call(body, params = {})
|
|
22
|
+
payload = WebhookPayload.new(JSON.parse(body), params)
|
|
23
|
+
return reject(payload, payload.rejection) if payload.rejection
|
|
24
|
+
return store(payload, payload.merge_variables) unless transform_pipeline.configured?
|
|
25
|
+
|
|
26
|
+
transform_and_store(payload)
|
|
27
|
+
rescue JSON::ParserError => e
|
|
28
|
+
report_warning(e.message)
|
|
29
|
+
[400, { message: e.message }]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
attr_reader :paths, :transform_pipeline, :user_data_assembler, :reporter
|
|
35
|
+
|
|
36
|
+
def transform_and_store(payload)
|
|
37
|
+
oversized = serverless_size_rejection(payload)
|
|
38
|
+
return reject(payload, oversized) if oversized
|
|
39
|
+
|
|
40
|
+
transformed = user_data_assembler.transform_webhook_post(payload.merge_variables)
|
|
41
|
+
# TRMNL answers before its transform runs, so a failure keeps the stored data and never reaches the poster.
|
|
42
|
+
if transform_pipeline.error
|
|
43
|
+
report_warning("Transform failed: #{transform_pipeline.error}")
|
|
44
|
+
else
|
|
45
|
+
store(payload, transformed)
|
|
46
|
+
end
|
|
47
|
+
[200, { message: nil, merge_variables: payload.merge_variables, processing: 'serverless' }]
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def serverless_size_rejection(payload)
|
|
51
|
+
size = JSON.generate(payload.merge_variables).bytesize
|
|
52
|
+
"Large payload received (#{size} bytes), should be less than 1mb." if size > MAX_SERVERLESS_INCOMING_BYTES
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def store(payload, incoming)
|
|
56
|
+
merged = payload.merged_into(stored_data, incoming)
|
|
57
|
+
payload.size_warning(merged)&.then { report_warning(it) }
|
|
58
|
+
paths.user_data.dirname.mkpath
|
|
59
|
+
paths.user_data.write(JSON.generate(merged))
|
|
60
|
+
[200, { message: nil, merge_variables: merged }]
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def reject(payload, message)
|
|
64
|
+
report_warning(message)
|
|
65
|
+
posted = payload.merge_variables.nil? ? [] : payload.merge_variables
|
|
66
|
+
[422, { message:, merge_variables: stored_data.empty? ? posted : stored_data }]
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def stored_data
|
|
70
|
+
stored = paths.user_data.exist? ? JSON.parse(paths.user_data.read) : {}
|
|
71
|
+
stored.is_a?(Hash) ? stored : {}
|
|
72
|
+
rescue JSON::ParserError
|
|
73
|
+
{}
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def report_warning(message) = reporter.info(reporter.yellow("webhook warning: #{message}"))
|
|
77
|
+
end
|
|
78
|
+
end
|
data/lib/trmnlp.rb
CHANGED
|
@@ -4,7 +4,7 @@ module TRMNLP; end
|
|
|
4
4
|
require 'oj'
|
|
5
5
|
require 'trmnl/liquid'
|
|
6
6
|
Oj.mimic_JSON
|
|
7
|
-
|
|
7
|
+
require_relative 'trmnlp/rails_helpers'
|
|
8
8
|
require_relative 'trmnlp/errors'
|
|
9
9
|
require_relative 'trmnlp/oauth'
|
|
10
10
|
require_relative 'trmnlp/config'
|
data/trmnl_preview.gemspec
CHANGED
|
@@ -48,6 +48,7 @@ Gem::Specification.new do |spec|
|
|
|
48
48
|
# HTML rendering
|
|
49
49
|
spec.add_dependency 'activesupport', '~> 8.0'
|
|
50
50
|
spec.add_dependency 'crass', '~> 1.0'
|
|
51
|
+
spec.add_dependency 'csv', '~> 3.3'
|
|
51
52
|
spec.add_dependency 'nokogiri', '~> 1.18'
|
|
52
53
|
spec.add_dependency 'trmnl-liquid', '~> 0.8.2'
|
|
53
54
|
|
data/web/public/index.js
CHANGED
|
@@ -23,7 +23,8 @@ trmnlp.connectLiveRender = function () {
|
|
|
23
23
|
// trmnl.device values the picked model and palette decide.
|
|
24
24
|
trmnlp.deviceParams = function (state) {
|
|
25
25
|
const bitDepth = Math.ceil(Math.log2(state.palette?.grays || 2));
|
|
26
|
-
|
|
26
|
+
const orientation = state.isPortrait ? 'portrait' : 'landscape';
|
|
27
|
+
return new URLSearchParams({ width: state.width, height: state.height, model: state.model.name, bit_depth: bitDepth, orientation });
|
|
27
28
|
};
|
|
28
29
|
|
|
29
30
|
trmnlp.fetchPreview = function (pickerState) {
|
|
@@ -31,6 +32,7 @@ trmnlp.fetchPreview = function (pickerState) {
|
|
|
31
32
|
const screenClasses = (state?.screenClasses || []).join(" ");
|
|
32
33
|
const encodedScreenClasses = encodeURIComponent(screenClasses);
|
|
33
34
|
let src = `/render/${trmnlp.view}.${trmnlp.formatSelect.value}?screen_classes=${encodedScreenClasses}`;
|
|
35
|
+
if (trmnlp.themeSelect.value) src += `&theme=${encodeURIComponent(trmnlp.themeSelect.value)}`;
|
|
34
36
|
|
|
35
37
|
// Pass the device for both HTML and PNG renders so trmnl.device.{width,height,model,bit_depth}
|
|
36
38
|
// in the Liquid context tracks the picker model selection.
|
|
@@ -38,9 +40,8 @@ trmnlp.fetchPreview = function (pickerState) {
|
|
|
38
40
|
src += `&${trmnlp.deviceParams(state)}`;
|
|
39
41
|
}
|
|
40
42
|
|
|
41
|
-
// PNG-only:
|
|
43
|
+
// PNG-only: color depth from palette
|
|
42
44
|
if (trmnlp.formatSelect.value === 'png' && state) {
|
|
43
|
-
const isDarkMode = state.isDarkMode ? 1 : 0;
|
|
44
45
|
const grays = state.palette.grays || 2;
|
|
45
46
|
const colorDepth = Math.ceil(Math.log2(grays));
|
|
46
47
|
src += `&color_depth=${colorDepth}`;
|
|
@@ -214,9 +215,13 @@ trmnlp.addSelectFieldRow = function (container, key, selectedValue, options) {
|
|
|
214
215
|
const row = document.createElement('div');
|
|
215
216
|
row.className = 'custom-field-row';
|
|
216
217
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
218
|
+
// Saved as TRMNL saves them: "New York" as new_york, { Label: value } as its value.
|
|
219
|
+
const choices = options.map(opt => (opt && typeof opt === 'object')
|
|
220
|
+
? [String(Object.keys(opt)[0]), String(Object.values(opt)[0])]
|
|
221
|
+
: [String(opt), String(opt).toLowerCase().replaceAll(' ', '_')]);
|
|
222
|
+
const optionsHtml = choices.map(([label, value]) => {
|
|
223
|
+
const selected = [label, value].includes(String(selectedValue)) ? 'selected' : '';
|
|
224
|
+
return `<option value="${trmnlp.escapeHtml(value)}" ${selected}>${trmnlp.escapeHtml(label)}</option>`;
|
|
220
225
|
}).join('');
|
|
221
226
|
|
|
222
227
|
row.innerHTML = `
|
|
@@ -261,6 +266,7 @@ document.addEventListener("DOMContentLoaded", async function () {
|
|
|
261
266
|
trmnlp.view = document.querySelector("meta[name='trmnl-view']").content;
|
|
262
267
|
trmnlp.iframe = document.querySelector("iframe");
|
|
263
268
|
trmnlp.formatSelect = document.querySelector(".select-format");
|
|
269
|
+
trmnlp.themeSelect = document.querySelector("[data-theme-select]");
|
|
264
270
|
trmnlp.userData = document.getElementById("user-data");
|
|
265
271
|
trmnlp.spinner = document.querySelector(".spinner");
|
|
266
272
|
trmnlp.isLiveReloadEnabled =
|
|
@@ -278,6 +284,12 @@ document.addEventListener("DOMContentLoaded", async function () {
|
|
|
278
284
|
trmnlp.fetchPreview();
|
|
279
285
|
});
|
|
280
286
|
|
|
287
|
+
trmnlp.themeSelect.value = localStorage.getItem("trmnlp-theme") || "";
|
|
288
|
+
trmnlp.themeSelect.addEventListener("change", () => {
|
|
289
|
+
localStorage.setItem("trmnlp-theme", trmnlp.themeSelect.value);
|
|
290
|
+
trmnlp.fetchPreview();
|
|
291
|
+
});
|
|
292
|
+
|
|
281
293
|
trmnlp.iframe.addEventListener("load", () => {
|
|
282
294
|
trmnlp.spinner.style.display = "none";
|
|
283
295
|
});
|
data/web/views/index.erb
CHANGED
|
@@ -46,6 +46,13 @@
|
|
|
46
46
|
<span data-dark-mode-text>Light Mode</span>
|
|
47
47
|
</button>
|
|
48
48
|
|
|
49
|
+
<select data-theme-select>
|
|
50
|
+
<option value="">Default theme</option>
|
|
51
|
+
<% TRMNLP::FrameworkVersion::THEMES.each do |id, name| %>
|
|
52
|
+
<option value="<%= id %>"><%= name %></option>
|
|
53
|
+
<% end %>
|
|
54
|
+
</select>
|
|
55
|
+
|
|
49
56
|
<!-- Optional: Reset button -->
|
|
50
57
|
<button class="btn" type="button" data-reset-button>Reset</button>
|
|
51
58
|
</form>
|
data/web/views/render_html.erb
CHANGED
|
@@ -1,7 +1,43 @@
|
|
|
1
1
|
<!DOCTYPE html>
|
|
2
2
|
<html>
|
|
3
3
|
<head>
|
|
4
|
+
<script>
|
|
5
|
+
const [andXMorePrefix, andXMoreSuffix] = <%= @and_x_more.to_json %>;
|
|
6
|
+
window.I18n = { andXMore: (count) => `${andXMorePrefix} ${count} ${andXMoreSuffix}` };
|
|
7
|
+
|
|
8
|
+
// Copied from TRMNL's plugin_rendering.js: the PNG capture waits for TRMNL_HIGHCHARTS_DONE.
|
|
9
|
+
document.addEventListener("DOMContentLoaded", () => {
|
|
10
|
+
if (!window.Highcharts) {
|
|
11
|
+
window.TRMNL_HIGHCHARTS_DONE = true;
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
let counter = 0;
|
|
16
|
+
const settle = () => {
|
|
17
|
+
const allReady = Highcharts.charts.every(c => c && c.renderer && c.renderer.boxWrapper);
|
|
18
|
+
if (!allReady && counter <= 50) return false; // 5 second timeout
|
|
19
|
+
|
|
20
|
+
// A chart drawn before the title bar clamped its slot falls back to a 400px height.
|
|
21
|
+
Highcharts.charts.forEach(c => {
|
|
22
|
+
if (!c || !c.renderTo) return;
|
|
23
|
+
const w = c.renderTo.offsetWidth;
|
|
24
|
+
const h = c.renderTo.offsetHeight;
|
|
25
|
+
if (h && Math.round(c.chartHeight) !== Math.round(h)) c.setSize(w, h, false);
|
|
26
|
+
});
|
|
27
|
+
window.TRMNL_HIGHCHARTS_DONE = true;
|
|
28
|
+
return true;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
if (settle()) return;
|
|
32
|
+
|
|
33
|
+
const interval = setInterval(() => {
|
|
34
|
+
counter++;
|
|
35
|
+
if (settle()) clearInterval(interval);
|
|
36
|
+
}, 100);
|
|
37
|
+
});
|
|
38
|
+
</script>
|
|
4
39
|
<link rel="stylesheet" href="<%= @framework.css_url %>" />
|
|
40
|
+
<% if @theme_css_url %><link rel="stylesheet" href="<%= @theme_css_url %>" /><% end %>
|
|
5
41
|
<script src="<%= @framework.js_url %>"></script>
|
|
6
42
|
<meta name="trmnl-framework-version" content="<%= @framework.number %>" />
|
|
7
43
|
<meta name="trmnl-framework-pinned" content="<%= @framework.pinned? %>" />
|
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.
|
|
4
|
+
version: 0.16.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Rockwell Schrock
|
|
@@ -79,6 +79,20 @@ dependencies:
|
|
|
79
79
|
- - "~>"
|
|
80
80
|
- !ruby/object:Gem::Version
|
|
81
81
|
version: '1.0'
|
|
82
|
+
- !ruby/object:Gem::Dependency
|
|
83
|
+
name: csv
|
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - "~>"
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: '3.3'
|
|
89
|
+
type: :runtime
|
|
90
|
+
prerelease: false
|
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
92
|
+
requirements:
|
|
93
|
+
- - "~>"
|
|
94
|
+
- !ruby/object:Gem::Version
|
|
95
|
+
version: '3.3'
|
|
82
96
|
- !ruby/object:Gem::Dependency
|
|
83
97
|
name: nokogiri
|
|
84
98
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -302,11 +316,42 @@ files:
|
|
|
302
316
|
- README.md
|
|
303
317
|
- bin/rake
|
|
304
318
|
- bin/trmnlp
|
|
319
|
+
- db/data/and_x_more.yml
|
|
305
320
|
- db/data/form_fields.yml
|
|
306
321
|
- db/data/framework_versions.yml
|
|
322
|
+
- db/data/locales/cs.yml
|
|
323
|
+
- db/data/locales/da.yml
|
|
324
|
+
- db/data/locales/de-AT.yml
|
|
325
|
+
- db/data/locales/de.yml
|
|
326
|
+
- db/data/locales/en-AU.yml
|
|
327
|
+
- db/data/locales/en-GB.yml
|
|
328
|
+
- db/data/locales/en.yml
|
|
329
|
+
- db/data/locales/es-ES.yml
|
|
330
|
+
- db/data/locales/fr.yml
|
|
331
|
+
- db/data/locales/he.yml
|
|
332
|
+
- db/data/locales/hu.yml
|
|
333
|
+
- db/data/locales/id.yml
|
|
334
|
+
- db/data/locales/is.yml
|
|
335
|
+
- db/data/locales/it.yml
|
|
336
|
+
- db/data/locales/ja.yml
|
|
337
|
+
- db/data/locales/ko.yml
|
|
338
|
+
- db/data/locales/lt.yml
|
|
339
|
+
- db/data/locales/nl.yml
|
|
340
|
+
- db/data/locales/no.yml
|
|
341
|
+
- db/data/locales/pl.yml
|
|
342
|
+
- db/data/locales/pt-BR.yml
|
|
343
|
+
- db/data/locales/raw.yml
|
|
344
|
+
- db/data/locales/ro.yml
|
|
345
|
+
- db/data/locales/ru.yml
|
|
346
|
+
- db/data/locales/sk.yml
|
|
347
|
+
- db/data/locales/sv.yml
|
|
348
|
+
- db/data/locales/uk.yml
|
|
349
|
+
- db/data/locales/zh-CN.yml
|
|
350
|
+
- db/data/locales/zh-HK.yml
|
|
307
351
|
- lib/trmnlp.rb
|
|
308
352
|
- lib/trmnlp/api_client.rb
|
|
309
353
|
- lib/trmnlp/app.rb
|
|
354
|
+
- lib/trmnlp/async_callback.rb
|
|
310
355
|
- lib/trmnlp/browser_pool.rb
|
|
311
356
|
- lib/trmnlp/cli.rb
|
|
312
357
|
- lib/trmnlp/commands.rb
|
|
@@ -342,6 +387,7 @@ files:
|
|
|
342
387
|
- lib/trmnlp/lint/checks/layouts_have_content.rb
|
|
343
388
|
- lib/trmnlp/lint/checks/limited_inline_styles.rb
|
|
344
389
|
- lib/trmnlp/lint/checks/no_async_functions.rb
|
|
390
|
+
- lib/trmnlp/lint/checks/no_custom_filters.rb
|
|
345
391
|
- lib/trmnlp/lint/checks/no_opacity.rb
|
|
346
392
|
- lib/trmnlp/lint/checks/no_size_classes.rb
|
|
347
393
|
- lib/trmnlp/lint/checks/recipe_overview_length.rb
|
|
@@ -350,6 +396,7 @@ files:
|
|
|
350
396
|
- lib/trmnlp/lint/checks/waits_for_dom_load.rb
|
|
351
397
|
- lib/trmnlp/lint/diagnostic.rb
|
|
352
398
|
- lib/trmnlp/lint/source.rb
|
|
399
|
+
- lib/trmnlp/merged_plugin_locals.rb
|
|
353
400
|
- lib/trmnlp/oauth.rb
|
|
354
401
|
- lib/trmnlp/oauth/client.rb
|
|
355
402
|
- lib/trmnlp/oauth/pkce.rb
|
|
@@ -357,22 +404,28 @@ files:
|
|
|
357
404
|
- lib/trmnlp/oauth/session.rb
|
|
358
405
|
- lib/trmnlp/oauth/token_bundle.rb
|
|
359
406
|
- lib/trmnlp/oauth/token_store.rb
|
|
407
|
+
- lib/trmnlp/outbound_request.rb
|
|
360
408
|
- lib/trmnlp/paths.rb
|
|
361
409
|
- lib/trmnlp/poller.rb
|
|
410
|
+
- lib/trmnlp/rails_helpers.rb
|
|
362
411
|
- lib/trmnlp/renderer.rb
|
|
363
412
|
- lib/trmnlp/reporter.rb
|
|
364
413
|
- lib/trmnlp/screen.rb
|
|
365
414
|
- lib/trmnlp/screen_generator.rb
|
|
366
415
|
- lib/trmnlp/screenshot.rb
|
|
416
|
+
- lib/trmnlp/select_field_choices.rb
|
|
367
417
|
- lib/trmnlp/transform_backend/http.rb
|
|
368
418
|
- lib/trmnlp/transform_backend/subprocess.rb
|
|
369
419
|
- lib/trmnlp/transform_backend/which.rb
|
|
370
420
|
- lib/trmnlp/transform_backend/wrapper.rb
|
|
371
421
|
- lib/trmnlp/transform_client.rb
|
|
372
422
|
- lib/trmnlp/transform_pipeline.rb
|
|
423
|
+
- lib/trmnlp/transform_state.rb
|
|
373
424
|
- lib/trmnlp/user_data_assembler.rb
|
|
374
425
|
- lib/trmnlp/version.rb
|
|
375
426
|
- lib/trmnlp/watcher.rb
|
|
427
|
+
- lib/trmnlp/webhook_payload.rb
|
|
428
|
+
- lib/trmnlp/webhook_receiver.rb
|
|
376
429
|
- templates/init/.github/workflows/trmnl.yml
|
|
377
430
|
- templates/init/.gitignore
|
|
378
431
|
- templates/init/.trmnlp.yml
|