├── lib
├── lunch_money
│ ├── .DS_Store
│ ├── previews
│ │ ├── accounts.bmp
│ │ └── budgets.bmp
│ ├── views
│ │ ├── common
│ │ │ ├── _title_bar.html.erb
│ │ │ ├── _headings.html.erb
│ │ │ └── _chart.html.erb
│ │ ├── full.html.erb
│ │ ├── half_horizontal.html.erb
│ │ ├── quadrant.html.erb
│ │ └── half_vertical.html.erb
│ └── lunch_money.rb
├── calendar
│ ├── previews
│ │ ├── week.bmp
│ │ ├── default.bmp
│ │ └── month.bmp
│ ├── views
│ │ ├── _title_bar.html.erb
│ │ ├── _full.html.erb
│ │ ├── _all_day_event.html.erb
│ │ ├── _quadrant.html.erb
│ │ ├── _half_horizontal.html.erb
│ │ ├── _full_month.html.erb
│ │ ├── _full_auto.html.erb
│ │ ├── _full_week.html.erb
│ │ └── _common.html.erb
│ └── helpers
│ │ └── base.rb
├── github_commit_graph
│ ├── views
│ │ ├── _common.html.erb
│ │ ├── errors
│ │ │ └── _full.html.erb
│ │ ├── v1
│ │ │ ├── _common.html.erb
│ │ │ ├── _quadrant.html.erb
│ │ │ ├── _half_vertical.html.erb
│ │ │ ├── _half_horizontal.html.erb
│ │ │ └── _full.html.erb
│ │ ├── quadrant.html.erb
│ │ ├── half_vertical.html.erb
│ │ ├── half_horizontal.html.erb
│ │ └── full.html.erb
│ ├── helpers.rb
│ └── github_commit_graph.rb
├── google_analytics
│ ├── previews
│ │ ├── full.png
│ │ ├── quadrant.png
│ │ ├── half_vertical.png
│ │ └── half_horizontal.png
│ └── views
│ │ ├── full.html.erb
│ │ ├── quadrant.html.erb
│ │ ├── half_vertical.html.erb
│ │ └── half_horizontal.html.erb
├── hacker_news
│ ├── previews
│ │ └── hacker_news.bmp
│ ├── hacker_news.rb
│ └── views
│ │ └── full.html.erb
├── mondrian
│ ├── mondrian.rb
│ └── views
│ │ ├── full.html.erb
│ │ ├── quadrant.html.erb
│ │ ├── half_vertical.html.erb
│ │ ├── half_horizontal.html.erb
│ │ ├── _common.html.erb
│ │ └── mondrian
│ │ └── _common.html.erb
├── route_planner
│ ├── previews
│ │ ├── fullview.bmp
│ │ ├── quadrantview.bmp
│ │ ├── half_verticalview.bmp
│ │ └── half_horizontalview.bmp
│ ├── route_planner.rb
│ └── views
│ │ ├── quadrant.html.erb
│ │ ├── half_vertical.html.erb
│ │ ├── half_horizontal.html.erb
│ │ └── full.html.erb
├── tempest_weather_station
│ ├── previews
│ │ └── tempest_weather_station.bmp
│ └── views
│ │ └── _common.html.erb
├── eight_sleep
│ ├── views
│ │ ├── quadrant.html.erb
│ │ ├── half_horizontal.html.erb
│ │ ├── half_vertical.html.erb
│ │ ├── shared
│ │ │ └── _title_bar.html.erb
│ │ └── _chart_js.html.erb
│ └── eight_sleep.rb
├── notion
│ └── views
│ │ ├── shared
│ │ ├── _title_bar.html.erb
│ │ ├── _error_section.html.erb
│ │ ├── _database_item.html.erb
│ │ └── _page_block.html.erb
│ │ ├── quadrant.html.erb
│ │ ├── half_vertical.html.erb
│ │ ├── full.html.erb
│ │ └── half_horizontal.html.erb
├── withings
│ └── views
│ │ ├── shared
│ │ └── _title_bar.html.erb
│ │ └── _chart_js.html.erb
├── parcel
│ ├── views
│ │ ├── shared
│ │ │ ├── _title_bar.html.erb
│ │ │ ├── _deliveries.html.erb
│ │ │ ├── _header.html.erb
│ │ │ └── _delivery.html.erb
│ │ ├── parcel
│ │ │ └── shared
│ │ │ │ ├── _title_bar.html.erb
│ │ │ │ ├── _deliveries.html.erb
│ │ │ │ ├── _header.html.erb
│ │ │ │ └── _delivery.html.erb
│ │ ├── quadrant.html.erb
│ │ ├── half_horizontal.html.erb
│ │ ├── full.html.erb
│ │ └── half_vertical.html.erb
│ └── parcel.rb
├── todoist
│ ├── views
│ │ ├── shared
│ │ │ └── _title_bar.html.erb
│ │ ├── quadrant.html.erb
│ │ ├── half_horizontal.html.erb
│ │ ├── half_vertical.html.erb
│ │ └── full.html.erb
│ └── form_fields.yaml
├── chatgpt
│ └── views
│ │ ├── shared
│ │ └── _title_bar.html.erb
│ │ ├── quadrant.html.erb
│ │ ├── full.html.erb
│ │ ├── half_vertical.html.erb
│ │ └── half_horizontal.html.erb
├── weather
│ └── views
│ │ ├── _common.html.erb
│ │ ├── quadrant.html.erb
│ │ ├── half_horizontal.html.erb
│ │ └── half_vertical.html.erb
├── stock_price
│ ├── views
│ │ ├── shared
│ │ │ ├── _ticker_xsmall.erb
│ │ │ ├── _ticker_xxsmall.erb
│ │ │ ├── _ticker_medium.erb
│ │ │ ├── _ticker_small.erb
│ │ │ ├── _ticker_xlarge.erb
│ │ │ ├── _ticker_xxlarge.erb
│ │ │ └── _ticker_large.erb
│ │ ├── quadrant.html.erb
│ │ ├── half_vertical.html.erb
│ │ └── half_horizontal.html.erb
│ └── stock_price.rb
├── days_left_until
│ ├── views
│ │ ├── _common.html.erb
│ │ ├── _progress_bar.html.erb
│ │ ├── quadrant.html.erb
│ │ ├── full.html.erb
│ │ ├── half_horizontal.html.erb
│ │ └── half_vertical.html.erb
│ └── days_left_until.rb
├── screenshot
│ └── screenshot.rb
├── email_meter
│ ├── email_meter.rb
│ └── views
│ │ ├── half_vertical.html.erb
│ │ ├── full.html.erb
│ │ ├── quadrant.html.erb
│ │ └── half_horizontal.html.erb
├── lunar_calendar
│ └── views
│ │ ├── quadrant.html.erb
│ │ ├── full.html.erb
│ │ └── half_vertical.html.erb
├── youtube_analytics
│ ├── views
│ │ ├── quadrant.html.erb
│ │ ├── full.html.erb
│ │ ├── half_vertical.html.erb
│ │ └── half_horizontal.html.erb
│ └── youtube_analytics.rb
└── usa_college_football_rankings.md
└── .github
└── ISSUE_TEMPLATE
└── bounty---insert.md
/lib/lunch_money/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/usetrmnl/plugins/HEAD/lib/lunch_money/.DS_Store
--------------------------------------------------------------------------------
/lib/calendar/previews/week.bmp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/usetrmnl/plugins/HEAD/lib/calendar/previews/week.bmp
--------------------------------------------------------------------------------
/lib/calendar/previews/default.bmp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/usetrmnl/plugins/HEAD/lib/calendar/previews/default.bmp
--------------------------------------------------------------------------------
/lib/calendar/previews/month.bmp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/usetrmnl/plugins/HEAD/lib/calendar/previews/month.bmp
--------------------------------------------------------------------------------
/lib/github_commit_graph/views/_common.html.erb:
--------------------------------------------------------------------------------
1 | <%= stylesheet_link_tag 'plugins/github_commit_graph', media: 'all' %>
--------------------------------------------------------------------------------
/lib/google_analytics/previews/full.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/usetrmnl/plugins/HEAD/lib/google_analytics/previews/full.png
--------------------------------------------------------------------------------
/lib/lunch_money/previews/accounts.bmp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/usetrmnl/plugins/HEAD/lib/lunch_money/previews/accounts.bmp
--------------------------------------------------------------------------------
/lib/lunch_money/previews/budgets.bmp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/usetrmnl/plugins/HEAD/lib/lunch_money/previews/budgets.bmp
--------------------------------------------------------------------------------
/lib/hacker_news/previews/hacker_news.bmp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/usetrmnl/plugins/HEAD/lib/hacker_news/previews/hacker_news.bmp
--------------------------------------------------------------------------------
/lib/mondrian/mondrian.rb:
--------------------------------------------------------------------------------
1 | module Plugins
2 | class Mondrian < Base
3 | def locals
4 | {}
5 | end
6 | end
7 | end
8 |
--------------------------------------------------------------------------------
/lib/route_planner/previews/fullview.bmp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/usetrmnl/plugins/HEAD/lib/route_planner/previews/fullview.bmp
--------------------------------------------------------------------------------
/lib/google_analytics/previews/quadrant.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/usetrmnl/plugins/HEAD/lib/google_analytics/previews/quadrant.png
--------------------------------------------------------------------------------
/lib/route_planner/previews/quadrantview.bmp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/usetrmnl/plugins/HEAD/lib/route_planner/previews/quadrantview.bmp
--------------------------------------------------------------------------------
/lib/google_analytics/previews/half_vertical.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/usetrmnl/plugins/HEAD/lib/google_analytics/previews/half_vertical.png
--------------------------------------------------------------------------------
/lib/route_planner/previews/half_verticalview.bmp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/usetrmnl/plugins/HEAD/lib/route_planner/previews/half_verticalview.bmp
--------------------------------------------------------------------------------
/lib/google_analytics/previews/half_horizontal.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/usetrmnl/plugins/HEAD/lib/google_analytics/previews/half_horizontal.png
--------------------------------------------------------------------------------
/lib/route_planner/previews/half_horizontalview.bmp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/usetrmnl/plugins/HEAD/lib/route_planner/previews/half_horizontalview.bmp
--------------------------------------------------------------------------------
/lib/mondrian/views/full.html.erb:
--------------------------------------------------------------------------------
1 |
4 |
5 | <%= render 'plugins/mondrian/common' %>
--------------------------------------------------------------------------------
/lib/tempest_weather_station/previews/tempest_weather_station.bmp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/usetrmnl/plugins/HEAD/lib/tempest_weather_station/previews/tempest_weather_station.bmp
--------------------------------------------------------------------------------
/lib/mondrian/views/quadrant.html.erb:
--------------------------------------------------------------------------------
1 |
4 |
5 | <%= render 'plugins/mondrian/common' %>
--------------------------------------------------------------------------------
/lib/mondrian/views/half_vertical.html.erb:
--------------------------------------------------------------------------------
1 |
4 |
5 | <%= render 'plugins/mondrian/common' %>
--------------------------------------------------------------------------------
/lib/mondrian/views/half_horizontal.html.erb:
--------------------------------------------------------------------------------
1 |
4 |
5 | <%= render 'plugins/mondrian/common' %>
--------------------------------------------------------------------------------
/lib/eight_sleep/views/quadrant.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | <%= render "plugins/#{@plugin.keyname}/shared/title_bar", instance_name: instance_name %>
6 |
7 |
--------------------------------------------------------------------------------
/lib/eight_sleep/views/half_horizontal.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | <%= render "plugins/#{@plugin.keyname}/shared/title_bar", instance_name: instance_name %>
6 |
7 |
--------------------------------------------------------------------------------
/lib/eight_sleep/views/half_vertical.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | <%= render "plugins/#{@plugin.keyname}/shared/title_bar", instance_name: instance_name %>
6 |
7 |
--------------------------------------------------------------------------------
/lib/notion/views/shared/_title_bar.html.erb:
--------------------------------------------------------------------------------
1 |
2 |

3 |
<%= instance_name %>
4 |
--------------------------------------------------------------------------------
/lib/withings/views/shared/_title_bar.html.erb:
--------------------------------------------------------------------------------
1 |
2 |

3 |
Withings
4 |
<%= instance_name %>
5 |
6 |
--------------------------------------------------------------------------------
/lib/parcel/views/shared/_title_bar.html.erb:
--------------------------------------------------------------------------------
1 |
2 |

parcel--render.png"/>
3 |
Parcel
4 |
<%= instance_name %>
5 |
--------------------------------------------------------------------------------
/lib/lunch_money/views/common/_title_bar.html.erb:
--------------------------------------------------------------------------------
1 |
2 |

3 |
Lunch Money
4 |
<%= instance_name %>
5 |
6 |
--------------------------------------------------------------------------------
/lib/parcel/views/parcel/shared/_title_bar.html.erb:
--------------------------------------------------------------------------------
1 |
2 |

parcel--render.png"/>
3 |
Parcel
4 |
<%= instance_name %>
5 |
--------------------------------------------------------------------------------
/lib/eight_sleep/views/shared/_title_bar.html.erb:
--------------------------------------------------------------------------------
1 |
2 | <%= image_tag plugin_image_path("eight_sleep--render.svg"), class: "image", alt: "Eight Sleep plugin icon" %>
3 |
Eight Sleep
4 | <%= instance_name %>
5 |
6 |
--------------------------------------------------------------------------------
/lib/calendar/views/_title_bar.html.erb:
--------------------------------------------------------------------------------
1 |
2 |

3 |
Upcoming Events
4 |
<%= instance_name %>
5 |
--------------------------------------------------------------------------------
/lib/todoist/views/shared/_title_bar.html.erb:
--------------------------------------------------------------------------------
1 |
2 |

3 |
Todo List
4 |
<%= instance_name %>
5 |
6 |
7 |
--------------------------------------------------------------------------------
/lib/chatgpt/views/shared/_title_bar.html.erb:
--------------------------------------------------------------------------------
1 |
2 | <%= image_tag plugin_image_path("chatgpt--render.svg"), class: "image image-stroke image-stroke--black image-stroke--small", alt: "ChatGPT plugin icon" %>
3 | ChatGPT
4 | <%= instance_name %>
5 |
6 |
--------------------------------------------------------------------------------
/lib/parcel/views/quadrant.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 | <%= render 'plugins/parcel/shared/deliveries', max_height: 190, max_columns: 2, deliveries:, error:, style: %>
4 |
5 |
6 | <%= render "plugins/parcel/shared/title_bar", instance_name: instance_name %>
7 |
--------------------------------------------------------------------------------
/lib/parcel/views/half_horizontal.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 | <%= render 'plugins/parcel/shared/deliveries', max_height: 190, max_columns: 3, deliveries:, error:, style: %>
4 |
5 |
6 | <%= render "plugins/parcel/shared/title_bar", instance_name: instance_name %>
7 |
--------------------------------------------------------------------------------
/lib/chatgpt/views/quadrant.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
<%= simple_format(answer) %>
5 |
6 |
7 |
8 | <%= render 'plugins/chatgpt/shared/title_bar', instance_name: instance_name %>
9 |
10 |
--------------------------------------------------------------------------------
/lib/chatgpt/views/full.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
<%= simple_format(answer) %>
5 |
6 |
7 |
8 | <%= render 'plugins/chatgpt/shared/title_bar', instance_name: instance_name %>
9 |
10 |
--------------------------------------------------------------------------------
/lib/calendar/views/_full.html.erb:
--------------------------------------------------------------------------------
1 | <% if event_layout == 'week' %>
2 | <%= render partial: 'plugins/calendars/full_week', locals: local_assigns %>
3 | <% elsif %w[month rolling_month].include?(event_layout) %>
4 | <%= render partial: 'plugins/calendars/full_month', locals: local_assigns %>
5 | <% else %>
6 | <%= render partial: 'plugins/calendars/full_auto', locals: local_assigns %>
7 | <% end %>
8 |
--------------------------------------------------------------------------------
/lib/chatgpt/views/half_vertical.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
<%= simple_format(answer) %>
5 |
6 |
7 |
8 | <%= render 'plugins/chatgpt/shared/title_bar', instance_name: instance_name %>
9 |
10 |
--------------------------------------------------------------------------------
/lib/chatgpt/views/half_horizontal.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
<%= simple_format(answer) %>
5 |
6 |
7 |
8 | <%= render 'plugins/chatgpt/shared/title_bar', instance_name: instance_name %>
9 |
10 |
--------------------------------------------------------------------------------
/lib/calendar/views/_all_day_event.html.erb:
--------------------------------------------------------------------------------
1 | <% heading_size ||= 'small' %>
2 |
3 |
4 |
5 | #
6 |
7 |
8 | <%= event[:summary] %>
9 | <% if include_description %>
10 | <%= event[:description] %>
11 | <% end %>
12 |
13 |
14 |
--------------------------------------------------------------------------------
/lib/notion/views/shared/_error_section.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | ⚠
5 |
6 |
7 | <%= title_text %>
8 | <%= description_text %>
9 |
10 |
11 |
--------------------------------------------------------------------------------
/lib/weather/views/_common.html.erb:
--------------------------------------------------------------------------------
1 |
23 |
--------------------------------------------------------------------------------
/lib/lunch_money/views/common/_headings.html.erb:
--------------------------------------------------------------------------------
1 | <% top_items_count ||= 3 %>
2 | <% top_items = items.sort_by { |_, amount| -amount }.first(top_items_count) %>
3 | <% top_items.each do |item, amount| %>
4 |
5 |
6 |
7 | <%= number_to_currency(amount, precision: 0) %>
8 | <%= item %>
9 |
10 |
11 | <% end %>
12 |
--------------------------------------------------------------------------------
/lib/tempest_weather_station/views/_common.html.erb:
--------------------------------------------------------------------------------
1 |
23 |
--------------------------------------------------------------------------------
/lib/parcel/views/full.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 | <%= render 'plugins/parcel/shared/header', deliveries:, filter_mode:, value_size: 'large' %>
4 |
5 |
6 |
7 | <%= render 'plugins/parcel/shared/deliveries', max_height: 270, max_columns: 3, deliveries:, error:, style: %>
8 |
9 |
10 | <%= render "plugins/parcel/shared/title_bar", instance_name: instance_name %>
11 |
--------------------------------------------------------------------------------
/lib/parcel/views/half_vertical.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 | <%= render 'plugins/parcel/shared/header', deliveries:, filter_mode: %>
4 |
5 |
6 |
7 | <%= render 'plugins/parcel/shared/deliveries', max_height: 360, max_columns: 1, deliveries:, error:, style: %>
8 |
9 |
10 | <%= render "plugins/parcel/shared/title_bar", instance_name: instance_name %>
11 |
--------------------------------------------------------------------------------
/lib/lunch_money/views/full.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | <%= render partial: 'plugins/lunch_money/common/headings', locals: local_assigns %>
5 |
6 |
7 | <%= render partial: 'plugins/lunch_money/common/chart', locals: local_assigns.merge(graph_items_count: 16) %>
8 |
9 |
10 | <%= render 'plugins/lunch_money/common/title_bar', instance_name: instance_name %>
11 |
12 |
--------------------------------------------------------------------------------
/lib/lunch_money/views/half_horizontal.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | <%= render partial: 'plugins/lunch_money/common/headings', locals: local_assigns %>
5 |
6 |
7 | <% chart_opts = { graph_items_count: 9, chart_size: { height: 180, width: 583 }, y_axis_labels: { enabled: false } } %>
8 | <%= render partial: 'plugins/lunch_money/common/chart', locals: local_assigns.merge(chart_opts) %>
9 |
10 |
11 | <%= render 'plugins/lunch_money/common/title_bar', instance_name: instance_name %>
12 |
13 |
--------------------------------------------------------------------------------
/lib/stock_price/views/shared/_ticker_xsmall.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | <%= ticker[:symbol].gsub(' ', ' ').html_safe %>
6 | <%= ticker[:change] %>
7 |
8 |
9 |
10 |
11 |
12 | <%= number_to_currency(ticker[:price], unit: currency_symbol, separator: currency_separator) %>
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/lib/stock_price/views/shared/_ticker_xxsmall.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | <%= ticker[:symbol].gsub(' ', ' ').html_safe %>
6 | <%= ticker[:change] %>
7 |
8 |
9 |
10 |
11 |
12 | <%= number_to_currency(ticker[:price], unit: currency_symbol, separator: currency_separator) %>
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/lib/lunch_money/views/quadrant.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | <%= render partial: 'plugins/lunch_money/common/headings', locals: local_assigns.merge(top_items_count: 2) %>
5 |
6 |
7 | <% chart_opts = { graph_items_count: 5, chart_size: { height: 120 }, series_point_width: 5, y_axis_labels: { enabled: false } } %>
8 | <%= render partial: 'plugins/lunch_money/common/chart', locals: local_assigns.merge(chart_opts) %>
9 |
10 |
11 | <%= render 'plugins/lunch_money/common/title_bar', instance_name: instance_name %>
12 |
13 |
--------------------------------------------------------------------------------
/lib/lunch_money/views/half_vertical.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | <%= render partial: 'plugins/lunch_money/common/headings', locals: local_assigns.merge(top_items_count: 2) %>
5 |
6 |
7 | <% chart_opts = { graph_items_count: 16, chart_size: { height: 340 }, series_point_width: 5, y_axis_labels: { style: { fontSize: '16px' } }, tick_amount: 3 } %>
8 | <%= render partial: 'plugins/lunch_money/common/chart', locals: local_assigns.merge(chart_opts) %>
9 |
10 |
11 | <%= render 'plugins/lunch_money/common/title_bar', instance_name: instance_name %>
12 |
13 |
--------------------------------------------------------------------------------
/lib/eight_sleep/views/_chart_js.html.erb:
--------------------------------------------------------------------------------
1 | <% libs = local_assigns[:libs] || [:highcharts, :chartkick] %>
2 |
3 | <% if libs.include?(:highcharts) %>
4 |
5 | <% end %>
6 |
7 | <% if libs.include?(:highcharts_more) %>
8 |
9 | <% end %>
10 |
11 | <% if libs.include?(:highcharts_pattern_fill) %>
12 |
13 | <% end %>
14 |
15 | <% if libs.include?(:chartkick) %>
16 |
17 | <% end %>
18 |
--------------------------------------------------------------------------------
/lib/withings/views/_chart_js.html.erb:
--------------------------------------------------------------------------------
1 | <% libs = local_assigns[:libs] || [:highcharts, :chartkick] %>
2 |
3 | <% if libs.include?(:highcharts) %>
4 |
5 | <% end %>
6 |
7 | <% if libs.include?(:highcharts_more) %>
8 |
9 | <% end %>
10 |
11 | <% if libs.include?(:highcharts_pattern_fill) %>
12 |
13 | <% end %>
14 |
15 | <% if libs.include?(:chartkick) %>
16 |
17 | <% end %>
18 |
--------------------------------------------------------------------------------
/lib/todoist/views/quadrant.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | <% todos = todos.first %>
5 | <% todos[:items].each_with_index do |item, idx| %>
6 |
7 |
8 | <%= idx + 1 %>
9 |
10 |
11 | <%= item %>
12 |
13 |
14 | <% end %>
15 |
16 |
17 |
18 | <%= render 'plugins/todo_list/shared/title_bar', instance_name: instance_name %>
19 |
20 |
--------------------------------------------------------------------------------
/lib/github_commit_graph/views/errors/_full.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | <% if defined?(error_title) %>
5 |
<%= error_title %>
6 | <% end %>
7 |
8 |
9 |
<%= error_message %>
10 |
11 |
12 |
13 |
14 |
15 |
16 |

17 |
<%= title %>
18 |
<%= instance %>
19 |
20 |
--------------------------------------------------------------------------------
/lib/parcel/views/shared/_deliveries.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
9 | <% if error.present? %>
10 |
11 | <%= error %>
12 |
13 | <% elsif deliveries.empty? %>
14 |
15 | <%= t('renders.parcel.empty', locale:) %>
16 |
17 | <% else %>
18 | <%= render partial: "plugins/parcel/shared/delivery", collection: deliveries, locals: { style: } %>
19 | <% end %>
20 |
21 |
--------------------------------------------------------------------------------
/lib/parcel/views/parcel/shared/_deliveries.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
9 | <% if error.present? %>
10 |
11 | <%= error %>
12 |
13 | <% elsif deliveries.empty? %>
14 |
15 | <%= t('renders.parcel.empty', locale:) %>
16 |
17 | <% else %>
18 | <%= render partial: "plugins/parcel/shared/delivery", collection: deliveries, locals: { style: } %>
19 | <% end %>
20 |
21 |
--------------------------------------------------------------------------------
/lib/lunch_money/views/common/_chart.html.erb:
--------------------------------------------------------------------------------
1 | <% chart_size ||= { height: 320 } %>
2 | <% series_point_width ||= 6 %>
3 | <% y_axis_labels ||= { style: { fontSize: '16px' } } %>
4 | <% tick_amount ||= 5 %>
5 |
6 | <% graph_items = items.sort_by { |_, amount| -amount }.first(graph_items_count) %>
7 | <%= bar_chart graph_items.to_h, adapter: :highcharts,
8 | prefix: '$',
9 | thousands: ',',
10 | colors: ['black'],
11 | library: {
12 | chart: chart_size,
13 | plotOptions: { series: { animation: false, pointWidth: series_point_width } },
14 | yAxis: { labels: y_axis_labels, gridLineDashStyle: 'shortdot', gridLineWidth: 1, gridLineColor: '#000000', tickAmount: tick_amount },
15 | xAxis: { type: 'daytime', labels: { style: { fontSize: '16px' } }, lineWidth: 0, gridLineDashStyle: 'dot', tickWidth: 1, tickLength: 0, gridLineWidth: 1, gridLineColor: '#000000', tickPixelInterval: 120 }
16 | }
17 | %>
18 |
--------------------------------------------------------------------------------
/lib/stock_price/views/shared/_ticker_medium.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | <%= ticker[:symbol].gsub(' ', ' ').html_safe %>
7 | <%= ticker[:name] %>
8 |
9 |
10 |
11 |
12 |
13 | <%= ticker[:change] %>
14 | Daily Change
15 |
16 |
17 |
18 |
19 |
20 |
21 | <%= number_to_currency(ticker[:price], unit: currency_symbol, separator: currency_separator) %>
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/lib/days_left_until/views/_common.html.erb:
--------------------------------------------------------------------------------
1 |
47 |
--------------------------------------------------------------------------------
/lib/stock_price/views/shared/_ticker_small.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | <%= ticker[:symbol].gsub(' ', ' ').html_safe %>
7 | <%= ticker[:name] %>
8 |
9 |
10 |
11 |
12 |
13 | <%= ticker[:change] %>
14 | Daily Change
15 |
16 |
17 |
18 |
19 |
20 |
21 | <%= number_to_currency(ticker[:price], unit: currency_symbol, separator: currency_separator) %>
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/lib/github_commit_graph/helpers.rb:
--------------------------------------------------------------------------------
1 | def git_commit_grayscale(contribution_count, opts = {})
2 | if opts[:framework_v2] == false # @lucian opt-in to v1 colors
3 | case contribution_count
4 | when 1..3
5 | 'bg--gray-5'
6 | when 4..7
7 | 'bg--gray-4'
8 | when 8..10
9 | 'bg--gray-3'
10 | when 11..20
11 | 'bg--gray-2'
12 | when 20..1000
13 | 'bg-black'
14 | end
15 | else
16 | case contribution_count
17 | when 1
18 | 'bg--gray-60'
19 | when 2
20 | 'bg--gray-55'
21 | when 3
22 | 'bg--gray-50'
23 | when 4
24 | 'bg--gray-45'
25 | when 5
26 | 'bg--gray-40'
27 | when 6
28 | 'bg--gray-35'
29 | when 7..8
30 | 'bg--gray-30'
31 | when 9..10
32 | 'bg--gray-25'
33 | when 11..12
34 | 'bg--gray-20'
35 | when 13..15
36 | 'bg--gray-15'
37 | when 16..20
38 | 'bg--gray-10'
39 | when 21..1000
40 | 'bg-black'
41 | end
42 | end
43 | end
44 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bounty---insert.md:
--------------------------------------------------------------------------------
1 | ---
2 | name: bounty - INSERT
3 | about: insert summary from usetrmnl.com/admin/bounties
4 | title: ''
5 | labels: help wanted
6 | assignees: ''
7 |
8 | ---
9 |
10 | ### desired outcome
11 | insert hints or existing plugins with a similar aesthetic to mimic.
12 |
13 | ### prerequisites
14 |
15 | - TRMNL account
16 | - Developer edition (for access to Private Plugin > Markup Editor)
17 | - Must use native [Framework](https://usetrmnl.com/framework)
18 |
19 | ### scope
20 | - [ ] Frontend markup (4x layouts)
21 | - [ ] Example API requests with valid endpoints to reproduce data in frontend
22 | - INSERT more if applicable
23 |
24 | ### compensation
25 |
26 | $X
27 |
28 | ### next steps
29 |
30 | 1. reply to claim, then a **X hour clock** begins before it's released to other devs
31 | 2. [export](https://help.usetrmnl.com/en/articles/10542599-importing-and-exporting-private-plugins) your Private Plugin and email team@usetrmnl.com, subject "Bounty - INSERT"
32 |
--------------------------------------------------------------------------------
/lib/stock_price/views/shared/_ticker_xlarge.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | <%= ticker[:symbol].gsub(' ', ' ').html_safe %>
8 | <%= ticker[:name] %>
9 |
10 |
11 |
12 |
13 |
14 | <%= ticker[:change] %>
15 | Daily Change
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | <%= number_to_currency(ticker[:price], unit: currency_symbol, separator: currency_separator) %>
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/lib/stock_price/views/shared/_ticker_xxlarge.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | <%= ticker[:symbol].gsub(' ', ' ').html_safe %>
8 | <%= ticker[:name] %>
9 |
10 |
11 |
12 |
13 |
14 | <%= ticker[:change] %>
15 | Daily Change
16 |
17 |
18 |
19 |
20 |
21 |
22 | <%= number_to_currency(ticker[:price], unit: currency_symbol, separator: currency_separator) %>
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/lib/route_planner/route_planner.rb:
--------------------------------------------------------------------------------
1 | module Plugins
2 | class Routes < Base
3 | def locals
4 | { routes:, origin:, destination:, last_updated: }
5 | end
6 |
7 | private
8 |
9 | def routes
10 | [
11 | {
12 | name: "Via N234",
13 | distance: "13.2 km",
14 | duration: "17 min",
15 | is_fastest: true,
16 | travel_mode: '🚘'
17 | },
18 | {
19 | name: "via R. São Romão/N234 and N234",
20 | distance: "15.8 km",
21 | duration: "18 min",
22 | is_fastest: false,
23 | travel_mode: '🚘'
24 | },
25 | {
26 | name: "via CM1037",
27 | distance: "12.9 km",
28 | duration: "18 min",
29 | is_fastest: false,
30 | travel_mode: '🚘'
31 | }
32 | ]
33 | end
34 |
35 | def origin = "Febres, Portugal"
36 | end
37 |
38 | def destination = "Ourentã, Portugal"
39 | end
40 |
41 | def last_updated = "7:30 AM"
42 | end
43 | end
--------------------------------------------------------------------------------
/lib/stock_price/views/shared/_ticker_large.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | <%= ticker[:symbol].gsub(' ', ' ').html_safe %>
8 | <%= ticker[:name] %>
9 |
10 |
11 |
12 |
13 |
14 | <%= ticker[:change] %>
15 | Daily Change
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | <%= number_to_currency(ticker[:price], unit: currency_symbol, separator: currency_separator) %>
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/lib/todoist/views/half_horizontal.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 | <% todos.reverse.each_with_index do |doing_and_done, list_index| %>
4 |
5 |
<%= doing_and_done[:label].titleize %>
6 |
7 | <% doing_and_done[:items].each_with_index do |item, idx| %>
8 |
9 |
10 | <%= idx + 1 %>
11 |
12 |
13 | <%= item %>
14 |
15 |
16 | <% end %>
17 |
18 |
19 | <% end %>
20 |
21 |
22 | <%= render 'plugins/todo_list/shared/title_bar', instance_name: instance_name %>
23 |
24 |
--------------------------------------------------------------------------------
/lib/todoist/views/half_vertical.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 | <% todos.each_with_index do |doing_and_done, list_index| %>
4 |
5 |
<%= doing_and_done[:label].titleize %>
6 | <% doing_and_done[:items].each_with_index do |item, idx| %>
7 |
8 |
9 | <%= idx + 1 %>
10 |
11 |
12 | <%= item %>
13 |
14 |
15 | <% end %>
16 | <% hidden_items_count = doing_and_done[:items].size - 3 %>
17 |
18 | <% if list_index == 0 %>
19 |
20 | <% end %>
21 | <% end %>
22 |
23 |
24 | <%= render 'plugins/todo_list/shared/title_bar', instance_name: instance_name %>
25 |
26 |
--------------------------------------------------------------------------------
/lib/days_left_until/days_left_until.rb:
--------------------------------------------------------------------------------
1 | module Plugins
2 | class DaysLeftUntil < Base
3 |
4 | def locals
5 | { days_passed:, days_left:, percent_passed:, show_days_passed:, show_days_left:, message: }
6 | end
7 |
8 | private
9 |
10 | def start_date
11 | return plugin_settings.created_at.to_date.to_s unless settings['start_date'].present?
12 |
13 | settings['start_date']
14 | end
15 |
16 | def end_date = settings['end_date']
17 |
18 | def days_passed
19 | (today - start_date.to_date).to_i
20 | end
21 |
22 | def days_left
23 | (end_date.to_date - today).to_i
24 | end
25 |
26 | def percent_passed
27 | ((days_passed.to_f / (days_passed + days_left)) * 100).round
28 | rescue FloatDomainError
29 | 100
30 | end
31 |
32 | def show_days_passed
33 | return true unless settings['show_days_passed'].present?
34 |
35 | settings['show_days_passed'] == 'yes'
36 | end
37 |
38 | def show_days_left
39 | return true unless settings['show_days_left'].present?
40 |
41 | settings['show_days_left'] == 'yes'
42 | end
43 |
44 | def today = user.datetime_now.to_date
45 |
46 | # TODO: NOT in use
47 | def message
48 | settings['message']
49 | end
50 | end
51 | end
52 |
--------------------------------------------------------------------------------
/lib/todoist/views/full.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | <% todos.reverse.each_with_index do |doing_and_done, list_index| %>
5 |
6 |
<%= doing_and_done[:label].titleize %>
7 | <% visible_items = doing_and_done[:items] %>
8 | <% hidden_items_count = doing_and_done[:items].size - visible_items.size %>
9 |
10 | <% visible_items.each_with_index do |item, idx| %>
11 |
12 |
13 | <%= idx + 1 %>
14 |
15 |
16 | <%= item %>
17 |
18 |
19 | <% end %>
20 |
21 |
22 | <% end %>
23 |
24 |
25 |
26 | <%= render 'plugins/todo_list/shared/title_bar', instance_name: instance_name %>
27 |
28 |
--------------------------------------------------------------------------------
/lib/hacker_news/hacker_news.rb:
--------------------------------------------------------------------------------
1 | module Plugins
2 | class HackerNews < Base
3 |
4 | STORY_COUNT = 14.freeze
5 |
6 | STORY_TYPES = {
7 | 'top_stories' => 'topstories',
8 | 'show_hn' => 'showstories'
9 | }.freeze
10 |
11 | STORY_LABEL = {
12 | 'top_stories' => 'Top Stories',
13 | 'show_hn' => 'Show HN'
14 | }.freeze
15 |
16 | def locals
17 | { stories:, category: }
18 | end
19 |
20 | private
21 |
22 | def stories
23 | fetch_stories[..STORY_COUNT].map do |story_id|
24 | story = fetch_item(story_id)
25 | {
26 | title: story['title'].gsub(/show hn: /i, ''),
27 | score: story['score'],
28 | by: story['by']
29 | }
30 | end
31 | end
32 |
33 | def fetch_stories = HTTParty.get("https://hacker-news.firebaseio.com/v0/#{story_type}.json", headers:)
34 |
35 | def fetch_item(story_id) = HTTParty.get("https://hacker-news.firebaseio.com/v0/item/#{story_id}.json", headers:)
36 |
37 | # https://github.com/HackerNews/API?tab=readme-ov-file#ask-show-and-job-stories
38 | def story_type
39 | STORY_TYPES[settings['story_type']]
40 | end
41 |
42 | def category
43 | STORY_LABEL[settings['story_type']]
44 | end
45 |
46 | def headers
47 | { 'content-type' => 'application/json' }
48 | end
49 | end
50 | end
51 |
--------------------------------------------------------------------------------
/lib/route_planner/views/quadrant.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | <%= origin %> → <%= destination %>
7 |
8 |
9 |
10 |
11 | <% routes.each_with_index do |route, index| %>
12 |
13 |
14 | <% if route[:is_fastest] %>
15 | ★
16 | <% end %>
17 |
18 |
19 |
<%= route[:name] %> (<%= route[:travel_mode] %>)
20 |
21 | <%= route[:duration] %>
22 | (<%= route[:distance] %>)
23 |
24 |
25 |
26 | <% end %>
27 |
28 |
29 |
30 |
31 |

32 |
Daily Commute
33 |
Route Planner
34 |
35 |
--------------------------------------------------------------------------------
/lib/calendar/views/_quadrant.html.erb:
--------------------------------------------------------------------------------
1 | <% days_to_show = event_layout == 'today_only' ? 1 : 2 %>
2 |
3 |
4 |
5 |
6 |
7 | <% formulate_and_group_events_by_day(events, today_in_tz, days_to_show).to_h.each_pair do |day, events| %>
8 | <% idx = 0 %>
9 |
<%= day %>
10 | <% events.each do |event| %>
11 | <% if event[:all_day] %>
12 | <%= render partial: 'plugins/calendars/all_day_event', locals: { event: event, include_description: } %>
13 | <% else %>
14 |
15 |
16 | <%= idx += 1 %>
17 |
18 |
19 |
<%= event[:summary] %>
20 |
21 | <%= "#{event[:start]}" %> - <%= event[:end] %>
22 |
23 |
24 |
25 | <% end %>
26 | <% end %>
27 | <% end %>
28 |
29 |
30 |
31 |
32 | <%= render partial: "plugins/calendars/title_bar", locals: local_assigns %>
33 |
34 |
--------------------------------------------------------------------------------
/lib/days_left_until/views/_progress_bar.html.erb:
--------------------------------------------------------------------------------
1 |
47 |
48 |
49 | <% max_days ||= 74 %>
50 |
51 |
52 | <% max_days.times do |idx| %>
53 | <% if ((idx.to_f / max_days) * 100) <= percent_passed %>
54 |
55 | <% elsif ((idx.to_f / max_days) * 100) >= percent_passed && (((idx-1).to_f / max_days) * 100) <= percent_passed %>
56 |
57 | <% else %>
58 |
59 | <% end %>
60 | <% end %>
61 |
62 |
--------------------------------------------------------------------------------
/lib/github_commit_graph/views/v1/_common.html.erb:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/lib/github_commit_graph/views/v1/_quadrant.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | <%= contributions[:total] %>
8 | Contributions
9 |
10 |
11 |
12 |
13 |
14 | <%= stats[:current_streak] %>
15 | Current streak
16 |
17 |
18 |
19 |
20 |
21 |
22 | <% contributions[:commits].each do |week_of_commits| %>
23 | <% week_of_commits[:contributionDays].each do |day| %>
24 |
25 | <% end %>
26 | <% end %>
27 |
28 |
29 |
30 |
31 |

32 |
GitHub
33 |
<%= instance_name %>
34 |
35 |
36 |
37 | <%= render 'plugins/github_commit_graph/v1/common' %>
--------------------------------------------------------------------------------
/lib/parcel/views/shared/_header.html.erb:
--------------------------------------------------------------------------------
1 | <% compact ||= false %>
2 | <% value_size ||= '' %>
3 |
4 |
5 |
6 |
7 |
8 | <% if compact %>
9 |
10 | <%= deliveries.count %>
11 | <%= t("renders.parcel.filter_modes.#{filter_mode.downcase}", count: deliveries.count, locale:) %>
12 |
13 | <% else %>
14 |
15 | <%= deliveries.count %>
16 |
17 |
18 | <%= t("renders.parcel.filter_modes.#{filter_mode.downcase}", count: deliveries.count, locale:) %>
19 |
20 | <% end %>
21 |
22 |
23 |
24 |
25 |
26 |
27 | <% if compact %>
28 |
29 | <%= deliveries.count { it[:status_key] == 'out_for_delivery' } %>
30 | <%= t('renders.parcel.status.out_for_delivery', locale:) %>
31 |
32 | <% else %>
33 |
34 | <%= deliveries.count { it[:status_key] == 'out_for_delivery' } %>
35 |
36 |
37 | <%= t('renders.parcel.status.out_for_delivery', locale:) %>
38 |
39 | <% end %>
40 |
41 |
42 |
--------------------------------------------------------------------------------
/lib/parcel/views/parcel/shared/_header.html.erb:
--------------------------------------------------------------------------------
1 | <% compact ||= false %>
2 | <% value_size ||= '' %>
3 |
4 |
5 |
6 |
7 |
8 | <% if compact %>
9 |
10 | <%= deliveries.count %>
11 | <%= t("renders.parcel.filter_modes.#{filter_mode.downcase}", count: deliveries.count, locale:) %>
12 |
13 | <% else %>
14 |
15 | <%= deliveries.count %>
16 |
17 |
18 | <%= t("renders.parcel.filter_modes.#{filter_mode.downcase}", count: deliveries.count, locale:) %>
19 |
20 | <% end %>
21 |
22 |
23 |
24 |
25 |
26 |
27 | <% if compact %>
28 |
29 | <%= deliveries.count { it[:status_key] == 'out_for_delivery' } %>
30 | <%= t('renders.parcel.status.out_for_delivery', locale:) %>
31 |
32 | <% else %>
33 |
34 | <%= deliveries.count { it[:status_key] == 'out_for_delivery' } %>
35 |
36 |
37 | <%= t('renders.parcel.status.out_for_delivery', locale:) %>
38 |
39 | <% end %>
40 |
41 |
42 |
--------------------------------------------------------------------------------
/lib/weather/views/quadrant.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |

6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | <%= temperature&.round %>°
14 | <%= conditions %>
15 |
16 |
17 |
18 |
19 |
20 | <%= forecast[:today][:mintemp] %>° - <%= forecast[:today][:maxtemp] %>°
21 | UV <%= forecast[:today][:uv_index] %>
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |

31 |
<%= t('renders.weather.title') %>
32 |
<%= instance_name %>
33 |
34 |
35 |
36 | <%= render 'plugins/weather/common' %>
--------------------------------------------------------------------------------
/lib/weather/views/half_horizontal.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |

7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | <%= temperature&.round %>°
15 | <%= t('renders.weather.temperature') %>
16 |
17 |
18 |
19 |
20 |
21 | <%= conditions %>
22 | Conditions
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |

32 |
<%= t('renders.weather.title') %>
33 |
<%= instance_name %>
34 |
35 |
36 |
37 | <%= render 'plugins/weather/common' %>
--------------------------------------------------------------------------------
/lib/days_left_until/views/quadrant.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | <% if show_days_left || show_days_passed %>
5 |
6 | <% if show_days_left %>
7 |
8 |
9 |
10 | <%= days_left %>
11 | <%= t('renders.days_left_year.days_left') %>
12 |
13 |
14 | <% end %>
15 |
16 | <% if show_days_passed %>
17 |
18 |
19 |
20 | <%= days_passed %>
21 | <%= t('renders.days_left_year.days_passed') %>
22 |
23 |
24 | <% end %>
25 |
26 | <% end %>
27 |
28 | <%= render 'plugins/days_left_until/progress_bar', percent_passed: percent_passed %>
29 |
<%= message %>
30 |
31 |
32 |
33 |

34 |
Days Left Until...
35 |
<%= instance_name %>
36 |
37 |
38 |
--------------------------------------------------------------------------------
/lib/github_commit_graph/views/quadrant.html.erb:
--------------------------------------------------------------------------------
1 | <% unless framework_v2 %>
2 | <%= render 'plugins/github_commit_graph/v1/quadrant', local_assigns %>
3 | <% else %>
4 |
5 |
6 |
7 |
8 |
9 |
10 | <%= contributions[:total] %>
11 | Contributions
12 |
13 |
14 |
15 |
16 |
17 | <%= stats[:current_streak] %>
18 | Current streak
19 |
20 |
21 |
22 |
23 |
24 |
25 | <% contributions[:commits].each do |week_of_commits| %>
26 | <% week_of_commits[:contributionDays].each do |day| %>
27 |
28 | <% end %>
29 | <% end %>
30 |
31 |
32 |
33 |
34 |

35 |
GitHub
36 |
<%= instance_name %>
37 |
38 |
39 |
40 | <%= render 'plugins/github_commit_graph/common' %>
41 | <% end %>
--------------------------------------------------------------------------------
/lib/days_left_until/views/full.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | <% if show_days_left || show_days_passed %>
7 |
8 | <% if show_days_passed %>
9 |
10 | <%= days_passed %>
11 | <%= t('renders.days_left_year.days_passed') %>
12 |
13 | <% end %>
14 |
15 | <% if show_days_left %>
16 |
17 | <%= days_left %>
18 | <%= t('renders.days_left_year.days_left') %>
19 |
20 | <% end %>
21 |
22 | <% end %>
23 |
24 | <%= render 'plugins/days_left_until/progress_bar', percent_passed: percent_passed %>
25 |
<%= message %>
26 |
27 |
28 |
29 |
30 |
31 |

32 |
Days Left Until...
33 |
<%= instance_name %>
34 |
35 |
36 |
--------------------------------------------------------------------------------
/lib/days_left_until/views/half_horizontal.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | <% if show_days_left || show_days_passed %>
5 |
6 | <% if show_days_left %>
7 |
8 |
9 |
10 | <%= days_left %>
11 | <%= t('renders.days_left_year.days_left') %>
12 |
13 |
14 | <% end %>
15 |
16 | <% if show_days_passed %>
17 |
18 |
19 |
20 | <%= days_passed %>
21 | <%= t('renders.days_left_year.days_passed') %>
22 |
23 |
24 | <% end %>
25 |
26 | <% end %>
27 |
28 | <%= render 'plugins/days_left_until/progress_bar', max_days: 54, percent_passed: percent_passed %>
29 |
<%= message %>
30 |
31 |
32 |
33 |

34 |
Days Left Until...
35 |
<%= instance_name %>
36 |
37 |
38 |
--------------------------------------------------------------------------------
/lib/stock_price/views/quadrant.html.erb:
--------------------------------------------------------------------------------
1 |
2 | <% if tickers.size == 1 %>
3 |
4 | <% tickers.each_with_index do |ticker, index| %>
5 | <%= render 'lib/stock_price/views/shared/ticker_medium', ticker:, currency_symbol:, currency_separator: %>
6 | <% end %>
7 |
8 | <% elsif tickers.size == 2 %>
9 |
10 | <% tickers_to_display = tickers.first(2) %>
11 | <% tickers_to_display.each_with_index do |ticker, index| %>
12 | <%= render 'lib/stock_price/views/shared/ticker_xsmall', ticker:, currency_symbol:, currency_separator: %>
13 | <% if index < tickers_to_display.size - 1 %>
14 |
15 | <% end %>
16 | <% end %>
17 |
18 | <% elsif tickers.size >= 3 %>
19 |
20 | <% tickers_to_display = tickers.first(3) %>
21 | <% tickers_to_display.each_with_index do |ticker, index| %>
22 | <%= render 'lib/stock_price/views/shared/ticker_xxsmall', ticker:, currency_symbol:, currency_separator: %>
23 | <% if index < tickers_to_display.size - 1 %>
24 |
25 | <% end %>
26 | <% end %>
27 |
28 | <% end %>
29 |
30 |
31 |

32 |
Stock Prices
33 |
34 |
35 |
--------------------------------------------------------------------------------
/lib/days_left_until/views/half_vertical.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | <% if show_days_left || show_days_passed %>
5 |
6 | <% if show_days_left %>
7 |
8 |
9 |
10 | <%= days_left %>
11 | <%= t('renders.days_left_year.days_left') %>
12 |
13 |
14 | <% end %>
15 |
16 | <% if show_days_passed %>
17 |
18 |
19 |
20 | <%= days_passed %>
21 | <%= t('renders.days_left_year.days_passed') %>
22 |
23 |
24 | <% end %>
25 |
26 | <% end %>
27 |
28 | <%= render 'plugins/days_left_until/progress_bar', percent_passed: percent_passed %>
29 |
<%= message %>
30 |
31 |
32 |
33 |

34 |
Days Left Until...
35 |
<%= instance_name %>
36 |
37 |
38 |
--------------------------------------------------------------------------------
/lib/weather/views/half_vertical.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |

4 |
5 |
6 |
7 |
8 |
9 | <%= temperature&.round %>°
10 | <%= conditions %>
11 | <%= forecast[:today][:mintemp] %>° - <%= forecast[:today][:maxtemp] %>°
12 | UV <%= forecast[:today][:uv_index] %>
13 |
14 |
15 |
16 |
17 |
18 | <%= forecast[:tomorrow][:day_override] || t('renders.weather.tomorrow') %>
19 | <%= forecast[:tomorrow][:conditions] %>
20 | [L/H] <%= forecast[:tomorrow][:mintemp] %>° - <%= forecast[:tomorrow][:maxtemp] %>°
21 | [UV] <%= forecast[:tomorrow][:uv_index] %>
22 |
23 |
24 |
25 |
26 |
27 |
28 |

29 |
<%= t('renders.weather.title') %>
30 |
<%= instance_name %>
31 |
32 |
33 |
34 | <%= render 'plugins/weather/common' %>
--------------------------------------------------------------------------------
/lib/route_planner/views/half_vertical.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | <%= origin %> → <%= destination %>
7 |
8 |
9 |
10 |
11 |
12 |
13 | <% routes.each do |route| %>
14 |
15 |
16 | <% if route[:is_fastest] %>
17 | ★
18 | <% end %>
19 |
20 |
21 |
<%= route[:name] %> (<%= route[:travel_mode] %>)
22 |
23 | <%= route[:duration] %>
24 | (<%= route[:distance] %>)
25 |
26 | <% if route[:is_fastest] %>
27 |
Fastest Route
28 | <% else %>
29 |
Alternative Route
30 | <% end %>
31 |
32 |
33 |
34 | <% end %>
35 |
36 |
37 |
38 |
39 |

40 |
Daily Commute
41 |
Route Planner
42 |
43 |
--------------------------------------------------------------------------------
/lib/calendar/views/_half_horizontal.html.erb:
--------------------------------------------------------------------------------
1 | <% days_to_show = event_layout == 'today_only' ? 1 : 3 %>
2 |
3 |
4 |
5 |
6 | <% formulate_and_group_events_by_day(events, today_in_tz, days_to_show).to_h.each_pair do |day, events| %>
7 | <% idx = 0 %>
8 |
<%= day %>
9 | <% events.each do |event| %>
10 | <% if event[:all_day] %>
11 | <%= render partial: 'plugins/calendars/all_day_event', locals: { event: event, include_description: } %>
12 | <% else %>
13 |
14 |
15 | <%= idx += 1 %>
16 |
17 |
18 |
<%= event[:summary] %>
19 | <% if include_description %>
20 |
<%= event[:description] %>
21 | <% end %>
22 |
23 | <%= day %>
24 | <%= "#{event[:start]}" %> - <%= event[:end] %>
25 |
26 |
27 |
28 | <% end %>
29 | <% end %>
30 | <% end %>
31 |
32 |
33 |
34 |
35 | <%= render partial: "plugins/calendars/title_bar", locals: local_assigns %>
36 |
37 |
--------------------------------------------------------------------------------
/lib/screenshot/screenshot.rb:
--------------------------------------------------------------------------------
1 | require 'open-uri'
2 |
3 | module Plugins
4 | class Screenshot < Base
5 | def locals
6 | { html_hex: Digest::SHA2.hexdigest(html_document) } # Adding to compare the hex of the HTML document
7 | end
8 |
9 | # rubocop:disable Security/Open, Lint/ShadowedException
10 | def html_document(*)
11 | @doc ||= begin
12 | raise InvalidURL if settings['url'].include?('localhost')
13 |
14 | document = Nokogiri::HTML(URI.open(url, request_specific_fields: headers, open_timeout: 5))
15 |
16 | %w[script img].each do |element|
17 | absolutize_element(document, element)
18 | end
19 | document.to_s.encode("UTF-8")
20 | end
21 | rescue Addressable::URI::InvalidURIError, OpenURI::HTTPError, Errno::ENOENT, Net::OpenTimeout, Socket::ResolutionError, SocketError, InvalidURL => e
22 | handle_erroring_state(e.message)
23 | erroring_html(e.message)
24 | end
25 | # rubocop:enable Security/Open, Lint/ShadowedException
26 |
27 | private
28 |
29 | def url
30 | @url ||= begin
31 | uri = URI.parse(settings['url'])
32 | response = Net::HTTP.get_response(uri, headers)
33 | response.is_a?(Net::HTTPRedirection) ? response['location'] : settings['url']
34 | end
35 | end
36 |
37 | def headers
38 | return {} unless settings['headers']
39 |
40 | string_to_hash(settings['headers'])
41 | end
42 |
43 | def erroring_html(message) = "Error #{message}
"
44 |
45 | def absolutize_element(doc, element)
46 | doc.at(element)&.attribute_nodes&.each do |tag|
47 | tag.value = "#{url.chomp('/')}/#{tag.value}" unless tag.value.include?('http')
48 | end
49 | end
50 | end
51 | end
52 |
--------------------------------------------------------------------------------
/lib/route_planner/views/half_horizontal.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | <%= origin %> → <%= destination %>
7 |
8 |
9 |
10 |
11 |
12 |
13 | <% routes.each do |route| %>
14 |
15 |
16 |
17 | <% if route[:is_fastest] %>
18 | ★
19 | <% end %>
20 |
21 |
22 |
<%= route[:name] %> (<%= route[:travel_mode] %>)
23 |
24 | <%= route[:duration] %>
25 | <%= route[:distance] %>
26 |
27 | <% if route[:is_fastest] %>
28 |
Fastest Route
29 | <% else %>
30 |
Alternative Route
31 | <% end %>
32 |
33 |
34 |
35 |
36 |
37 | <% end %>
38 |
39 |
40 |
41 |
42 |

43 |
Daily Commute
44 |
Route Planner
45 |
46 |
--------------------------------------------------------------------------------
/lib/google_analytics/views/full.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | <% metrics.each do |metric, value| %>
8 |
9 |
10 |
11 | <%= format_number(value) %>
12 | <%= metric.to_s.titleize %>
13 |
14 |
15 | <% end %>
16 |
17 |
18 | <% formatted_data = histogram.map { |period| [period[:date], period[:pageviews]] } %>
19 | <%= line_chart formatted_data, adapter: :highcharts, prefix: "", thousands: ",", points: false, colors: ["black"], curve: true,
20 | library: {
21 | chart: { height: 260 },
22 | plotOptions: { series: { animation: false, lineWidth: 4 } },
23 | yAxis: { labels: { style: { fontSize: '16px', color: '#000000' } }, gridLineDashStyle: 'shortdot', gridLineWidth: 1, gridLineColor: '#000000', tickAmount: 5 },
24 | xAxis: { type: 'daytime', labels: { style: { fontSize: '16px', color: '#000000' } }, lineWidth: 0, gridLineDashStyle: 'dot', tickWidth: 1, tickLength: 0, gridLineWidth: 1, gridLineColor: '#000000', tickPixelInterval: 120 }}
25 | %>
26 |
27 |
28 |
29 |

30 |
Google Analytics
31 |
<%= instance_name %>
32 |
33 |
--------------------------------------------------------------------------------
/lib/google_analytics/views/quadrant.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | <% metrics.first(2).each do |metric, value| %>
8 |
9 |
10 |
11 | <%= format_number(value) %>
12 | <%= metric.to_s.titleize %>
13 |
14 |
15 | <% end %>
16 |
17 |
18 | <% formatted_data = histogram.map { |period| [period[:date], period[:pageviews]] } %>
19 | <%= line_chart formatted_data, adapter: :highcharts, prefix: "", thousands: ",", points: false, colors: ["black"], curve: true,
20 | library: {
21 | chart: { height: 112 },
22 | plotOptions: { series: { animation: false, lineWidth: 4 } },
23 | yAxis: { labels: { style: { fontSize: '16px', color: '#000000' } }, gridLineDashStyle: 'shortdot', gridLineWidth: 1, gridLineColor: '#000000', tickAmount: 5 },
24 | xAxis: { type: 'daytime', labels: { enabled: false }, lineWidth: 0, gridLineDashStyle: 'dot', tickWidth: 1, tickLength: 0, gridLineWidth: 1, gridLineColor: '#000000', tickPixelInterval: 120 }}
25 | %>
26 |
27 |
28 |
29 |

30 |
Google Analytics
31 |
<%= instance_name %>
32 |
33 |
--------------------------------------------------------------------------------
/lib/parcel/views/shared/_delivery.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | <% if style == 'compact' %>
8 |
9 |
10 | <%= delivery[:title] %>
11 |
12 |
13 | <%= t("renders.parcel.status.#{delivery[:status_key]}", locale:) %>
14 |
15 |
16 |
17 | <% if delivery[:delivery_by] %>
18 |
19 | <%= t('renders.parcel.delivery_by', locale:) %>:
20 | <%= delivery[:delivery_by] %>
21 |
22 | <% end %>
23 |
24 | <% else %>
25 |
26 | <%= delivery[:title] %>
27 |
28 |
29 | <%= t("renders.parcel.status.#{delivery[:status_key]}", locale:) %>
30 |
31 |
32 |
33 | <%= delivery[:latest] %>
34 |
35 | <% if delivery[:delivery_by] %>
36 |
37 | <%= t('renders.parcel.delivery_by', locale:) %>:
38 | <%= delivery[:delivery_by] %>
39 |
40 | <% end %>
41 |
42 | <% end %>
43 |
44 |
--------------------------------------------------------------------------------
/lib/parcel/views/parcel/shared/_delivery.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | <% if style == 'compact' %>
8 |
9 |
10 | <%= delivery[:title] %>
11 |
12 |
13 | <%= t("renders.parcel.status.#{delivery[:status_key]}", locale:) %>
14 |
15 |
16 |
17 | <% if delivery[:delivery_by] %>
18 |
19 | <%= t('renders.parcel.delivery_by', locale:) %>:
20 | <%= delivery[:delivery_by] %>
21 |
22 | <% end %>
23 |
24 | <% else %>
25 |
26 | <%= delivery[:title] %>
27 |
28 |
29 | <%= t("renders.parcel.status.#{delivery[:status_key]}", locale:) %>
30 |
31 |
32 |
33 | <%= delivery[:latest] %>
34 |
35 | <% if delivery[:delivery_by] %>
36 |
37 | <%= t('renders.parcel.delivery_by', locale:) %>:
38 | <%= delivery[:delivery_by] %>
39 |
40 | <% end %>
41 |
42 | <% end %>
43 |
44 |
--------------------------------------------------------------------------------
/lib/google_analytics/views/half_vertical.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | <% metrics.each do |metric, value| %>
8 |
9 |
10 |
11 | <%= format_number(value) %>
12 | <%= metric.to_s.titleize %>
13 |
14 |
15 | <% end %>
16 |
17 |
18 | <% formatted_data = histogram.map { |period| [period[:date], period[:pageviews]] } %>
19 | <%= line_chart formatted_data, adapter: :highcharts, prefix: "", thousands: ",", points: false, colors: ["black"], curve: true,
20 | library: {
21 | chart: { height: 280 },
22 | plotOptions: { series: { animation: false, lineWidth: 4 } },
23 | yAxis: { labels: { style: { fontSize: '16px', color: '#000000' } }, gridLineDashStyle: 'shortdot', gridLineWidth: 1, gridLineColor: '#000000', tickAmount: 5 },
24 | xAxis: { type: 'daytime', labels: { style: { fontSize: '16px', color: '#000000' } }, lineWidth: 0, gridLineDashStyle: 'dot', tickWidth: 1, tickLength: 0, gridLineWidth: 1, gridLineColor: '#000000', tickPixelInterval: 120 }}
25 | %>
26 |
27 |
28 |
29 |

30 |
Google Analytics
31 |
<%= instance_name %>
32 |
33 |
--------------------------------------------------------------------------------
/lib/github_commit_graph/views/v1/_half_vertical.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | <%= contributions[:total] %>
8 | Contributions in last year
9 |
10 |
11 |
12 |
13 |
14 |
15 | <%= stats[:longest_streak] %>
16 | Longest streak
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | <%= stats[:current_streak] %>
26 | Current streak
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | <% contributions[:commits].each do |week_of_commits| %>
36 | <% week_of_commits[:contributionDays].each do |day| %>
37 |
38 | <% end %>
39 | <% end %>
40 |
41 |
42 |
43 |
44 |

45 |
GitHub
46 |
<%= instance_name %>
47 |
48 |
49 |
50 | <%= render 'plugins/github_commit_graph/v1/common' %>
--------------------------------------------------------------------------------
/lib/github_commit_graph/views/v1/_half_horizontal.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | <%= contributions[:total] %>
9 | Contributions
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | <%= stats[:longest_streak] %>
19 | Longest streak
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | <%= stats[:current_streak] %>
29 | Current streak
30 |
31 |
32 |
33 |
34 |
35 | <% contributions[:commits].each do |week_of_commits| %>
36 | <% week_of_commits[:contributionDays].each do |day| %>
37 |
38 | <% end %>
39 | <% end %>
40 |
41 |
42 |
43 |
44 |
45 |
46 |

47 |
GitHub
48 |
<%= instance_name %>
49 |
50 |
51 |
52 | <%= render 'plugins/github_commit_graph/v1/common' %>
--------------------------------------------------------------------------------
/lib/hacker_news/views/full.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | <% stories[0..5].each_with_index do |post, idx| %>
7 |
8 |
9 | <%= idx + 1 %>
10 |
11 |
12 |
<%= post[:title] %>
13 |
14 | <%= post[:score] %> votes
15 | by <%= post[:by] %>
16 |
17 |
18 |
19 | <% end %>
20 |
21 |
22 |
23 |
24 |
25 | <% stories[6..11].each_with_index do |post, idx| %>
26 |
27 |
28 | <%= idx + 7 %>
29 |
30 |
31 |
<%= post[:title] %>
32 |
33 | <%= post[:score] %> votes
34 | by <%= post[:by] %>
35 |
36 |
37 |
38 | <% end %>
39 |
40 |
41 |
42 |
43 |
44 |
45 |

46 |
Hacker News
47 |
<%= category %>
48 |
49 |
50 |
--------------------------------------------------------------------------------
/lib/github_commit_graph/views/half_vertical.html.erb:
--------------------------------------------------------------------------------
1 | <% unless framework_v2 %>
2 | <%= render 'plugins/github_commit_graph/v1/half_vertical', local_assigns %>
3 | <% else %>
4 |
5 |
6 |
7 |
8 |
9 |
10 | <%= contributions[:total] %>
11 | Contributions in last year
12 |
13 |
14 |
15 |
16 |
17 |
18 | <%= stats[:longest_streak] %>
19 | Longest streak
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | <%= stats[:current_streak] %>
29 | Current streak
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 | <% contributions[:commits].each do |week_of_commits| %>
39 | <% week_of_commits[:contributionDays].each do |day| %>
40 |
41 | <% end %>
42 | <% end %>
43 |
44 |
45 |
46 |
47 |

48 |
GitHub
49 |
<%= instance_name %>
50 |
51 |
52 |
53 | <%= render 'plugins/github_commit_graph/common' %>
54 | <% end %>
--------------------------------------------------------------------------------
/lib/notion/views/shared/_database_item.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | <%= index + 1 %>
5 |
6 |
7 |
8 | <%= item[:title] %>
9 | <% if status_field.present? %>
10 | <% status_properties = filter_properties_by_names(item[:properties], [status_field]) %>
11 | <% if status_properties.any? %>
12 | <%= status_properties.first[:value] %>
13 | <% end %>
14 | <% end %>
15 |
16 |
17 | <% labeled_property_names = labeled_properties || [] %>
18 | <% if labeled_property_names.any? %>
19 | <% matched_labeled_properties = filter_properties_by_names(item[:properties], labeled_property_names) %>
20 |
21 | <% patterns = ["label--inverted", "label--outline", "label--underline"] %>
22 |
23 |
24 | <% matched_labeled_properties.each_with_index do |label, index| %>
25 | <% pattern = patterns[index % patterns.length] %>
26 | <% label_classes = "label label--small #{pattern}" %>
27 | <%= label[:value] %>
28 | <% end %>
29 |
30 | <% end %>
31 |
32 | <% listed_property_names = listed_properties || [] %>
33 | <% if listed_property_names.any? %>
34 | <% matched_listed_properties = filter_properties_by_names(item[:properties], listed_property_names) %>
35 |
36 | <% if matched_listed_properties.any? %>
37 |
38 | <% matched_listed_properties.each_with_index do |prop, index| %>
39 | <%= "✶" if index > 0 %> <%= prop[:value] %>
40 | <% end %>
41 |
42 | <% end %>
43 | <% end %>
44 |
45 |
--------------------------------------------------------------------------------
/lib/github_commit_graph/views/half_horizontal.html.erb:
--------------------------------------------------------------------------------
1 | <% unless framework_v2 %>
2 | <%= render 'plugins/github_commit_graph/v1/half_horizontal', local_assigns %>
3 | <% else %>
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | <%= contributions[:total] %>
12 | Contributions
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | <%= stats[:longest_streak] %>
22 | Longest streak
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | <%= stats[:current_streak] %>
32 | Current streak
33 |
34 |
35 |
36 |
37 |
38 | <% contributions[:commits].each do |week_of_commits| %>
39 | <% week_of_commits[:contributionDays].each do |day| %>
40 |
41 | <% end %>
42 | <% end %>
43 |
44 |
45 |
46 |
47 |
48 |
49 |

50 |
GitHub
51 |
<%= instance_name %>
52 |
53 |
54 |
55 | <%= render 'plugins/github_commit_graph/common' %>
56 | <% end %>
--------------------------------------------------------------------------------
/lib/email_meter/email_meter.rb:
--------------------------------------------------------------------------------
1 | module Plugins
2 | class EmailMeter < Base
3 |
4 | def locals
5 | { metrics: }
6 | end
7 |
8 | private
9 |
10 | def base_url = 'https://api02.emailmeter.com/bqi'
11 |
12 | def headers
13 | { authorization: "Token #{settings['api_token']}" }
14 | end
15 |
16 | def metrics
17 | {
18 | messages_sent:,
19 | recipients:,
20 | avg_response_time:,
21 | messages_received:,
22 | senders:
23 | }
24 | end
25 |
26 | def timestamp_from
27 | lookback_period.days.ago.to_date.to_s
28 | end
29 |
30 | def timestamp_to
31 | Time.now.in_time_zone(user.tz).to_date.to_s
32 | end
33 |
34 | def messages_sent
35 | endpoint = "/total_sent_emails?timestamp_from=#{timestamp_from}×tamp_to=#{timestamp_to}"
36 | response = fetch(base_url + endpoint, headers: headers)
37 | response.dig(0, 'count') || 0
38 | end
39 |
40 | def recipients
41 | endpoint = "/sent_messages_list_by_recipient?timestamp_from=#{timestamp_from}×tamp_to=#{timestamp_to}"
42 | response = fetch(base_url + endpoint, headers: headers)
43 | response.count
44 | end
45 |
46 | def avg_response_time
47 | endpoint = "/reply_times?timestamp_from=#{timestamp_from}×tamp_to=#{timestamp_to}"
48 | response = fetch(base_url + endpoint, headers: headers)
49 | avg_reply_time = response.dig(0, 'avg_reply_time') || 0
50 | ActiveSupport::Duration.build(avg_reply_time).parts # => {:hours=>22, :minutes=>21, :seconds=>22.639999999999418}
51 | end
52 |
53 | def messages_received
54 | endpoint = "/total_received_emails?timestamp_from=#{timestamp_from}×tamp_to=#{timestamp_to}"
55 | response = fetch(base_url + endpoint, headers: headers)
56 | response.dig(0, 'count') || 0
57 | end
58 |
59 | def senders
60 | endpoint = "/received_messages_list_by_sender?timestamp_from=#{timestamp_from}×tamp_to=#{timestamp_to}"
61 | response = fetch(base_url + endpoint, headers: headers)
62 | response.count
63 | end
64 | end
65 | end
66 |
--------------------------------------------------------------------------------
/lib/stock_price/views/half_vertical.html.erb:
--------------------------------------------------------------------------------
1 |
2 | <% if tickers.size <= 2 %>
3 |
4 | <% tickers.each_with_index do |ticker, index| %>
5 | <%= render 'lib/stock_price/views/shared/ticker_medium', ticker:, currency_symbol:, currency_separator: %>
6 | <% if tickers.size == 2 && index == 0 %>
7 |
8 | <% end %>
9 | <% end %>
10 |
11 | <% elsif tickers.size == 3 %>
12 |
13 | <% tickers.each_with_index do |ticker, index| %>
14 | <%= render 'lib/stock_price/views/shared/ticker_small', ticker:, currency_symbol:, currency_separator: %>
15 | <% if index == 0 || index == 1 %>
16 |
17 | <% end %>
18 | <% end %>
19 |
20 | <% elsif tickers.size == 4 %>
21 |
22 | <% tickers_to_display = tickers.first(6) %>
23 | <% tickers_to_display.each_with_index do |ticker, index| %>
24 | <%= render 'lib/stock_price/views/shared/ticker_xsmall', ticker:, currency_symbol:, currency_separator: %>
25 | <% if index < tickers_to_display.size - 1 %>
26 |
27 | <% end %>
28 | <% end %>
29 |
30 | <% else %>
31 |
32 | <% tickers_to_display = tickers.first(6) %>
33 | <% tickers_to_display.each_with_index do |ticker, index| %>
34 | <%= render 'lib/stock_price/views/shared/ticker_xxsmall', ticker:, currency_symbol:, currency_separator: %>
35 | <% if index < tickers_to_display.size - 1 %>
36 |
37 | <% end %>
38 | <% end %>
39 |
40 | <% end %>
41 |
42 |
43 |

44 |
Stock Prices
45 |
46 |
47 |
--------------------------------------------------------------------------------
/lib/notion/views/quadrant.html.erb:
--------------------------------------------------------------------------------
1 |
2 | <% if defined?(error) && error %>
3 | <%= render 'plugins/notion/shared/error_section',
4 | title_class: "title title--small",
5 | title_text: "Error",
6 | description_text: error %>
7 | <% elsif display_type == "database" %>
8 |
9 | <% if items && items.any? %>
10 |
11 |
12 | <% items.each_with_index do |item, idx| %>
13 | <%= render partial: 'plugins/notion/shared/database_item', locals: { item: item, index: idx, current_layout: "quadrant", labeled_properties: labeled_properties, listed_properties: listed_properties, status_field: status_field } %>
14 | <% end %>
15 |
16 |
17 | <% else %>
18 | <%= render 'plugins/notion/shared/error_section',
19 | title_class: "title title--small",
20 | title_text: "No Content",
21 | description_text: "Check config" %>
22 | <% end %>
23 |
24 | <% elsif display_type == "page" %>
25 |
26 | <% if items && items[:blocks] && items[:blocks].any? %>
27 |
28 |
29 | <% items[:blocks].each_with_index do |block, idx| %>
30 | <%= render 'plugins/notion/shared/page_block', block: block, index: idx, current_layout: "quadrant", image_height: image_height %>
31 | <% end %>
32 |
33 |
34 | <% else %>
35 | <%= render 'plugins/notion/shared/error_section',
36 | title_class: "title title--small",
37 | title_text: "No Content",
38 | description_text: "Check config" %>
39 | <% end %>
40 |
41 | <% end %>
42 |
43 | <%= render partial: 'plugins/notion/shared/title_bar', locals: { instance_name: instance_name } %>
44 |
--------------------------------------------------------------------------------
/lib/notion/views/half_vertical.html.erb:
--------------------------------------------------------------------------------
1 |
2 | <% if defined?(error) && error %>
3 | <%= render 'plugins/notion/shared/error_section',
4 | title_class: "title title--small",
5 | title_text: "Notion Error",
6 | description_text: error %>
7 | <% elsif display_type == "database" %>
8 |
9 | <% if items && items.any? %>
10 |
11 |
12 | <% items.each_with_index do |item, idx| %>
13 | <%= render partial: 'plugins/notion/shared/database_item', locals: { item: item, index: idx, current_layout: "half_vertical", labeled_properties: labeled_properties, listed_properties: listed_properties, status_field: status_field } %>
14 | <% end %>
15 |
16 |
17 | <% else %>
18 | <%= render 'plugins/notion/shared/error_section',
19 | title_class: "title title--small",
20 | title_text: "No Content",
21 | description_text: "Check your Notion configuration" %>
22 | <% end %>
23 |
24 | <% elsif display_type == "page" %>
25 |
26 | <% if items && items[:blocks] && items[:blocks].any? %>
27 |
28 |
29 | <% items[:blocks].each_with_index do |block, idx| %>
30 | <%= render 'plugins/notion/shared/page_block', block: block, index: idx, current_layout: "half_vertical", image_height: image_height %>
31 | <% end %>
32 |
33 |
34 | <% else %>
35 | <%= render 'plugins/notion/shared/error_section',
36 | title_class: "title title--small",
37 | title_text: "No Content",
38 | description_text: "Check your Notion configuration" %>
39 | <% end %>
40 |
41 | <% end %>
42 |
43 | <%= render partial: 'plugins/notion/shared/title_bar', locals: { instance_name: instance_name } %>
44 |
--------------------------------------------------------------------------------
/lib/notion/views/full.html.erb:
--------------------------------------------------------------------------------
1 |
2 | <% if defined?(error) && error %>
3 | <%= render 'plugins/notion/shared/error_section',
4 | title_class: "title",
5 | title_text: "Notion Error",
6 | description_text: error %>
7 | <% elsif display_type == "database" %>
8 |
9 | <% if items && items.any? %>
10 |
11 |
12 | <% items.each_with_index do |item, idx| %>
13 | <%= render partial: 'plugins/notion/shared/database_item', locals: { item: item, index: idx, current_layout: "full", labeled_properties: labeled_properties, listed_properties: listed_properties, status_field: status_field } %>
14 | <% end %>
15 |
16 |
17 | <% else %>
18 | <%= render 'plugins/notion/shared/error_section',
19 | title_class: "title",
20 | title_text: "No Content Available",
21 | description_text: "Check your Notion configuration and try again" %>
22 | <% end %>
23 |
24 | <% elsif display_type == "page" %>
25 |
26 | <% if items && items[:blocks] && items[:blocks].any? %>
27 |
28 |
29 | <% items[:blocks].each_with_index do |block, idx| %>
30 | <%= render 'plugins/notion/shared/page_block', block: block, index: idx, current_layout: "full", image_height: image_height %>
31 | <% end %>
32 |
33 |
34 | <% else %>
35 | <%= render 'plugins/notion/shared/error_section',
36 | title_class: "title",
37 | title_text: "No Content Available",
38 | description_text: "Check your Notion configuration and try again" %>
39 | <% end %>
40 |
41 | <% end %>
42 |
43 | <%= render partial: 'plugins/notion/shared/title_bar', locals: { instance_name: instance_name } %>
44 |
--------------------------------------------------------------------------------
/lib/google_analytics/views/half_horizontal.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | <% metrics.first(3).each_with_index do |(metric, value), index| %>
10 |
11 |
12 |
13 | <%= format_number(value) %>
14 | <%= metric.to_s.titleize %>
15 |
16 |
17 | <% if index < metrics.first(3).size - 1 %>
18 |
19 | <% end %>
20 | <% end %>
21 |
22 |
23 |
24 |
25 | <% formatted_data = histogram.map { |period| [period[:date], period[:pageviews]] } %>
26 | <%= line_chart formatted_data, adapter: :highcharts, prefix: "", thousands: ",", points: false, colors: ["black"], curve: true,
27 | library: {
28 | chart: { height: 176, width: 612 },
29 | plotOptions: { series: { animation: false, lineWidth: 4 } },
30 | yAxis: { labels: { style: { fontSize: '16px', color: '#000000' } }, gridLineDashStyle: 'shortdot', gridLineWidth: 1, gridLineColor: '#000000', tickAmount: 5 },
31 | xAxis: { type: 'daytime', labels: { enabled: false }, lineWidth: 0, gridLineDashStyle: 'dot', tickWidth: 1, tickLength: 0, gridLineWidth: 1, gridLineColor: '#000000', tickPixelInterval: 120 }
32 | }
33 | %>
34 |
35 |
36 |
37 |
38 |
39 |

40 |
Google Analytics
41 |
<%= instance_name %>
42 |
43 |
--------------------------------------------------------------------------------
/lib/notion/views/half_horizontal.html.erb:
--------------------------------------------------------------------------------
1 |
2 | <% if defined?(error) && error %>
3 | <%= render 'plugins/notion/shared/error_section',
4 | title_class: "title title--small",
5 | title_text: "Notion Error",
6 | description_text: error %>
7 | <% elsif display_type == "database" %>
8 |
9 | <% if items && items.any? %>
10 |
11 |
12 | <% items.each_with_index do |item, idx| %>
13 | <%= render partial: 'plugins/notion/shared/database_item', locals: { item: item, index: idx, current_layout: "half_horizontal", labeled_properties: labeled_properties, listed_properties: listed_properties, status_field: status_field } %>
14 | <% end %>
15 |
16 |
17 | <% else %>
18 | <%= render 'plugins/notion/shared/error_section',
19 | title_class: "title title--small",
20 | title_text: "No Content",
21 | description_text: "Check your Notion configuration" %>
22 | <% end %>
23 |
24 | <% elsif display_type == "page" %>
25 |
26 | <% if items && items[:blocks] && items[:blocks].any? %>
27 |
28 |
29 | <% items[:blocks].each_with_index do |block, idx| %>
30 | <%= render 'plugins/notion/shared/page_block', block: block, index: idx, current_layout: "half_horizontal", image_height: image_height %>
31 | <% end %>
32 |
33 |
34 | <% else %>
35 | <%= render 'plugins/notion/shared/error_section',
36 | title_class: "title title--small",
37 | title_text: "No Content",
38 | description_text: "Check your Notion configuration" %>
39 | <% end %>
40 |
41 | <% end %>
42 |
43 | <%= render partial: 'plugins/notion/shared/title_bar', locals: { instance_name: instance_name } %>
44 |
--------------------------------------------------------------------------------
/lib/route_planner/views/full.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | <%= origin %> → <%= destination %>
8 |
9 |
10 |
11 |
12 |
13 | <%= last_updated %>
14 | Last Updated
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 | <% routes.each do |route| %>
23 |
24 |
25 |
26 |
27 | <% if route[:is_fastest] %>
28 | ★
29 | <% end %>
30 |
31 |
32 |
<%= route[:name] %> (<%= route[:travel_mode] %>)
33 |
34 | <%= route[:duration] %>
35 | <%= route[:distance] %>
36 |
37 |
38 | <% if route[:is_fastest] %>
39 | Fastest Route
40 | <% else %>
41 | Alternative Route
42 | <% end %>
43 |
44 |
45 |
46 |
47 |
48 | <% end %>
49 |
50 |
51 |
52 |
53 |

54 |
Daily Commute
55 |
Route Planner
56 |
57 |
--------------------------------------------------------------------------------
/lib/email_meter/views/half_vertical.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | <%= number_with_delimiter(metrics[:messages_sent]) %>
7 | Messages Sent
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | <%= number_with_delimiter(metrics[:recipients]) %>
16 | Recipients
17 |
18 |
19 |
20 |
21 |
22 |
23 | <% if metrics.dig(:avg_response_time, :hours).to_i > 0 %>
24 | <%= metrics.dig(:avg_response_time, :hours) %>h
25 | <% end %>
26 | <%= metrics.dig(:avg_response_time, :minutes) %>m
27 |
28 | Avg Response Time
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | <%= number_with_delimiter(metrics[:messages_received]) %>
38 | Messages Received
39 |
40 |
41 |
42 |
43 |
44 | <%= number_with_delimiter(metrics[:senders]) %>
45 | Senders
46 |
47 |
48 |
49 |
50 |
51 |
52 |

53 |
Email Meter
54 |
<%= instance_name %>
55 |
56 |
--------------------------------------------------------------------------------
/lib/email_meter/views/full.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | <%= number_with_delimiter(metrics[:messages_sent]) %>
8 | Messages Sent
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | <%= number_with_delimiter(metrics[:recipients]) %>
18 | Recipients
19 |
20 |
21 |
22 |
23 |
24 |
25 | <% if metrics.dig(:avg_response_time, :hours).to_i > 0 %>
26 | <%= metrics.dig(:avg_response_time, :hours) %>h
27 | <% end %>
28 | <%= metrics.dig(:avg_response_time, :minutes) %>m
29 |
30 | Avg Response Time
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 | <%= number_with_delimiter(metrics[:messages_received]) %>
40 | Messages Received
41 |
42 |
43 |
44 |
45 |
46 | <%= number_with_delimiter(metrics[:senders]) %>
47 | Senders
48 |
49 |
50 |
51 |
52 |
53 |
54 |

55 |
Email Meter
56 |
<%= instance_name %>
57 |
58 |
--------------------------------------------------------------------------------
/lib/email_meter/views/quadrant.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | <%= number_with_delimiter(metrics[:messages_sent]) %>
8 | Messages Sent
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | <%= number_with_delimiter(metrics[:recipients]) %>
18 | Recipients
19 |
20 |
21 |
22 |
23 |
24 |
25 | <% if metrics.dig(:avg_response_time, :hours).to_i > 0 %>
26 | <%= metrics.dig(:avg_response_time, :hours) %>h
27 | <% end %>
28 | <%= metrics.dig(:avg_response_time, :minutes) %>m
29 |
30 | Avg Response Time
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 | <%= number_with_delimiter(metrics[:messages_received]) %>
40 | Messages Received
41 |
42 |
43 |
44 |
45 |
46 | <%= number_with_delimiter(metrics[:senders]) %>
47 | Senders
48 |
49 |
50 |
51 |
52 |
53 |
54 |

55 |
Email Meter
56 |
<%= instance_name %>
57 |
58 |
--------------------------------------------------------------------------------
/lib/todoist/form_fields.yaml:
--------------------------------------------------------------------------------
1 | - keyname: todoist_project_id
2 | field_type: xhrSelect
3 | name: Todoist Project
4 | description: Select Todoist Project
5 | encrypted: true
6 | - keyname: sort_grouping
7 | name: Sort/Grouping
8 | description: How to group tasks in the task list.
9 | field_type: select
10 | options:
11 | - None
12 | - Date
13 | - Date Added
14 | - Deadline
15 | - Priority
16 | - Label
17 | default: none
18 | - keyname: sort_sorting
19 | name: Sort/Sorting
20 | description: How to sort tasks in the task list.
21 | field_type: select
22 | options:
23 | - Manual
24 | - Name
25 | - Date
26 | - Date Added
27 | - Deadline
28 | - Priority
29 | default: date
30 | - keyname: sort_direction
31 | name: Sort/Direction
32 | description: The direction to sort tasks in the task list.
33 | field_type: select
34 | options:
35 | - Ascending
36 | - Descending
37 | default: ascending
38 | - keyname: filter_completed_tasks
39 | name: Completed Tasks
40 | description: To show completed tasks in the task list.
41 | field_type: select
42 | options:
43 | - Yes
44 | - No
45 | default: no
46 | - keyname: filter_date
47 | name: Filter/Date
48 | description: Filter tasks by date in the task list.
49 | field_type: select
50 | options:
51 | - All # nil
52 | - Today # `overdue | today`
53 | - This Week # `due before: next week`
54 | - Next 7 Days # `overdue | next 7 days`
55 | - This month # `due before: first day`
56 | - Next 30 Days # `overdue | next 30 days`
57 | - No date # `no date`
58 | default: all
59 | - keyname: filter_deadline
60 | name: Filter/Deadline
61 | description: Filter tasks by deadline in the task list.
62 | field_type: select
63 | options:
64 | - All
65 | - Today
66 | - This Week
67 | - Next 7 Days
68 | - This month
69 | - Next 30 Days
70 | - No deadline
71 | default: all
72 | - keyname: filter_priority
73 | name: Filter/Priority
74 | description: Filter tasks by priority in the task list.
75 | field_type: select
76 | options:
77 | - Priority 1: p1
78 | - Priority 2: p2
79 | - Priority 3: p3
80 | - Priority 4: p4
81 | multiple: true
82 | optional: true
83 | - keyname: todoist_filter_label_ids
84 | name: Filter/Label
85 | description: Filter tasks by labels.
86 | field_type: xhrSelect
87 | multiple: true
88 | optional: true
89 |
--------------------------------------------------------------------------------
/lib/calendar/views/_full_month.html.erb:
--------------------------------------------------------------------------------
1 | <%= render "plugins/calendars/common" %>
2 |
3 |
34 |
35 |
36 |
37 |
38 |
39 |
64 |
--------------------------------------------------------------------------------
/lib/email_meter/views/half_horizontal.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | <%= number_with_delimiter(metrics[:messages_sent]) %>
9 | Messages Sent
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | <%= number_with_delimiter(metrics[:recipients]) %>
18 | Recipients
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | <% if metrics.dig(:avg_response_time, :hours).to_i > 0 %>
27 | <%= metrics.dig(:avg_response_time, :hours) %>h
28 | <% end %>
29 | <%= metrics.dig(:avg_response_time, :minutes) %>m
30 |
31 | Avg Response Time
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 | <%= number_with_delimiter(metrics[:messages_received]) %>
40 | Messages Received
41 |
42 |
43 |
44 |
45 |
46 |
47 | <%= number_with_delimiter(metrics[:senders]) %>
48 | Senders
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |

57 |
Email Meter
58 |
<%= instance_name %>
59 |
60 |
--------------------------------------------------------------------------------
/lib/calendar/views/_full_auto.html.erb:
--------------------------------------------------------------------------------
1 | <% zoom_mode ||= false %>
2 |
3 | <% days_to_show = event_layout == 'today_only' ? 1 : 3 %>
4 | <% max_event_reduction = zoom_mode ? 1 : 0 %>
5 | <% date_heading_size = zoom_mode ? 'large' : 'medium' %>
6 | <% heading_size = zoom_mode ? 'medium' : 'small' %>
7 | <% sub_heading_size = zoom_mode ? 'small' : 'xsmall' %>
8 | <% event_time_style = zoom_mode ? 'inverted' : 'underline' %>
9 |
10 |
11 |
12 |
13 | <% formulate_and_group_events_by_day(events, today_in_tz, days_to_show).to_h.each_pair do |day, events| %>
14 | <% idx = 0 %>
15 |
16 |
17 |
<%= day %>
18 | <% max_event_count = include_description ? 6 : 8 %>
19 | <% hidden_events_count = events[max_event_count..].to_a.count %>
20 | <% events[0..max_event_count].each do |event| %>
21 | <% if event[:all_day] %>
22 | <%= render partial: 'plugins/calendars/all_day_event', locals: { event: event, include_description:, heading_size: } %>
23 | <% else %>
24 |
25 |
26 | <%= idx += 1 %>
27 |
28 |
29 |
<%= event[:summary] %>
30 | <% if include_description %>
<%= event[:description].html_safe %><% end %>
31 |
32 | <%= "#{event[:start]}" %> - <%= event[:end] %>
33 |
34 |
35 |
36 | <% end %>
37 | <% end %>
38 | <% if hidden_events_count.positive? %>
39 |
40 |
41 |
42 | And <%= hidden_events_count %> more
43 |
44 |
45 | <% end %>
46 |
47 |
48 | <% end %>
49 |
50 |
51 |
52 | <%= render partial: "plugins/calendars/title_bar", locals: local_assigns %>
53 |
54 |
--------------------------------------------------------------------------------
/lib/lunch_money/lunch_money.rb:
--------------------------------------------------------------------------------
1 | module Plugins
2 | class LunchMoney < Base
3 |
4 | BASE_URL = 'https://dev.lunchmoney.app/v1'.freeze
5 |
6 | def locals
7 | { items: }
8 | end
9 |
10 | private
11 |
12 | def items
13 | case item_type
14 | when 'budgets' then budgets
15 | when 'accounts' then accounts
16 | end
17 | end
18 |
19 | def budgets
20 | resp = fetch(budgets_url, headers:)
21 | budget_data = JSON.parse(resp.body)
22 | raise AccessTokenExpired if budget_data.is_a?(Hash) && budget_data['message'] == 'Access token does not exist.'
23 |
24 | budget_data.map do |budget|
25 | next if budget['is_income'] # show only expenses by default
26 |
27 | [CGI.unescapeHTML(budget['category_name']), budget.dig('data', beginning_of_month, 'spending_to_base') || 0]
28 | end.compact
29 | rescue AccessTokenExpired => e
30 | handle_erroring_state(e.message)
31 | []
32 | end
33 |
34 | # user probably expects all items from "Accounts Overview" widget, which actually lives in 2 places (accounts, assets)
35 | def accounts
36 | resp = fetch(accounts_url, headers:)
37 | account_data = JSON.parse(resp.body)
38 | raise AccessTokenExpired if account_data.is_a?(Hash) && account_data['message'] == 'Access token does not exist.'
39 |
40 | resp = fetch(assets_url, headers:)
41 | asset_data = JSON.parse(resp.body)
42 | raise AccessTokenExpired if asset_data.is_a?(Hash) && asset_data['message'] == 'Access token does not exist.'
43 |
44 | accounts_map = account_data['plaid_accounts'].map do |account|
45 | [CGI.unescapeHTML(account['display_name'] || account['name']), account['balance']]
46 | end
47 |
48 | assets_map = asset_data['assets'].map do |asset|
49 | [CGI.unescapeHTML(asset['display_name'] || asset['name']), asset['balance']]
50 | end
51 |
52 | accounts_map + assets_map
53 | rescue AccessTokenExpired => e
54 | handle_erroring_state(e.message)
55 | []
56 | end
57 |
58 | def headers
59 | { "Authorization" => "Bearer #{settings['access_token']}" }
60 | end
61 |
62 | def item_type
63 | return 'budgets' unless settings['item_type'].present?
64 |
65 | settings['item_type']
66 | end
67 |
68 | def beginning_of_month = Date.today.beginning_of_month.to_s
69 |
70 | def end_of_month = Date.today.end_of_month.to_s
71 |
72 | def budgets_url = "#{BASE_URL}/budgets?start_date=#{beginning_of_month}&end_date=#{end_of_month}"
73 |
74 | def accounts_url = "#{BASE_URL}/plaid_accounts"
75 |
76 | def assets_url = "#{BASE_URL}/assets"
77 | end
78 | end
79 |
--------------------------------------------------------------------------------
/lib/lunar_calendar/views/quadrant.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | <%= current_phase[:name] %>
8 | <%= t('renders.lunar_calendar.current_phase') %>
9 |
10 |
11 |
12 |
13 |
14 | <%= next_phase[:name] %>
15 | <%= t('renders.lunar_calendar.next_phase_short') %> (<%= next_phase[:date] %>)
16 |
17 |
18 |
19 |
20 |
21 |
22 | <% ordered_phase_sequence = phase_sequence.sort_by { |seq| seq[:date_full] } %>
23 | <% current = ordered_phase_sequence.find { |phase| phase[:current] } %>
24 |
25 |
26 | <% ordered_phase_sequence.each do |phase| %>
27 |
28 |

29 |
30 | <% end %>
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 | <%= next_full_moon %>
41 | <%= t('renders.lunar_calendar.next_full_moon') %>
42 |
43 |
44 |
45 |
46 |
47 | <%= next_new_moon %>
48 | <%= t('renders.lunar_calendar.next_new_moon') %>
49 |
50 |
51 |
52 |
53 |
54 |
55 |

56 |
<%= t('renders.lunar_calendar.title') %>
57 |
<%= instance_name %>
58 |
59 |
60 |
61 |
72 |
--------------------------------------------------------------------------------
/lib/notion/views/shared/_page_block.html.erb:
--------------------------------------------------------------------------------
1 | <% if block[:type] == "divider" %>
2 |
3 | <% elsif ["table_of_contents", "table", "unsupported", "synced_block", "breadcrumb", "column_list"].include?(block[:type]) %>
4 | <% return %>
5 | <% else %>
6 |
7 |
8 | <% # Show meta for placeholders, database views, and non-heading/non-image blocks %>
9 | <% show_meta = (block[:type] == "image" && !block[:image_url]) ||
10 | block[:type] == "child_database" ||
11 | !%w[heading_1 heading_2 heading_3 image].include?(block[:type]) %>
12 | <% if show_meta %>
13 | <% meta_content = case block[:type]
14 | when "bulleted_list_item", "numbered_list_item" then "•"
15 | when "to_do" then block[:checked] ? "✓" : "☐"
16 | when "quote" then '❝'
17 | when "callout" then "[!]"
18 | when "child_page", "bookmark" then "↗"
19 | when "code" then "{}"
20 | end %>
21 | <% if meta_content %>
22 | <% meta_classes = "label label--gray" if block[:type] == "to_do" && block[:checked] %>
23 | <%= meta_content %>
24 | <% end %>
25 | <% end %>
26 |
27 |
28 | <% if block[:type] == "image" && block[:image_url] %>
29 |
![<%= block[:caption] || 'Image' %>](<%= block[:image_url] %>)
30 | <% if block[:caption] %>
31 |
<%= block[:caption] %>
32 | <% end %>
33 | <% else %>
34 | <% text = case block[:type]
35 | when "image" then (block[:image_url] ? nil : "Image placeholder")
36 | when "child_database" then "Database view"
37 | else block[:text]
38 | end %>
39 | <% if text %>
40 | <% content_classes = ["clamp--none"] %>
41 | <% case block[:type]
42 | when "heading_1"
43 | content_classes << (current_layout == "quadrant" ? " title title--small" : " title title--large")
44 | when "heading_2"
45 | content_classes << (current_layout == "quadrant" ? " text text--small" : " title")
46 | when "heading_3"
47 | content_classes << (current_layout == "quadrant" ? " text text--small" : " title title--small")
48 | else
49 | content_classes << "description"
50 | end %>
51 |
" data-pixel-perfect="true"><%= text %>
52 | <% end %>
53 | <% end %>
54 |
55 |
56 | <% end %>
--------------------------------------------------------------------------------
/lib/github_commit_graph/views/v1/_full.html.erb:
--------------------------------------------------------------------------------
1 | <% unless (contributions[:errors].present?) %>
2 |
3 |
4 |
5 |
6 |
7 |
8 | <%= contributions[:total] %>
9 | Contributions in last year
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | <%= stats[:longest_streak] %>
18 | Longest streak
19 |
20 |
21 |
22 |
23 |
24 | <%= stats[:current_streak] %>
25 | Current streak
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 | <%= stats[:max_contributions] %>
37 | Most in a day
38 |
39 |
40 |
41 |
42 |
43 | <%= stats[:average_contributions] %>
44 | Average per day
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 | <% contributions[:commits].each do |week_of_commits| %>
55 | <% week_of_commits[:contributionDays].each do |day| %>
56 |
57 | <% end %>
58 | <% end %>
59 |
60 |
61 |
62 |
63 |

64 |
GitHub
65 |
<%= instance_name %>
66 |
67 |
68 | <%= render 'plugins/github_commit_graph/v1/common' %>
69 | <% else %>
70 | <%= render partial: 'plugins/errors/full', locals: {
71 | icon: 'github--render.svg',
72 | title: 'GitHub',
73 | instance: instance_name,
74 | error_message: contributions[:errors]
75 | }
76 | %>
77 | <% end %>
78 |
--------------------------------------------------------------------------------
/lib/stock_price/views/half_horizontal.html.erb:
--------------------------------------------------------------------------------
1 |
2 | <% if tickers.size == 1 %>
3 |
4 | <% tickers.each_with_index do |ticker, index| %>
5 | <%= render 'lib/stock_price/views/shared/ticker_xlarge', ticker:, currency_symbol:, currency_separator: %>
6 | <% end %>
7 |
8 | <% elsif tickers.size == 2 %>
9 |
10 | <% tickers.each_with_index do |ticker, index| %>
11 | <%= render 'lib/stock_price/views/shared/ticker_medium', ticker:, currency_symbol:, currency_separator: %>
12 | <% end %>
13 |
14 | <% elsif tickers.size >= 3 && tickers.size <= 4 %>
15 |
16 |
17 |
18 | <% [0, 1,].each do |i| %>
19 | <% if tickers[i].present? %>
20 | <%= render 'lib/stock_price/views/shared/ticker_xsmall', ticker: tickers[i], currency_symbol:, currency_separator: %>
21 | <% if i < 1 %>
22 |
23 | <% end %>
24 | <% end %>
25 | <% end %>
26 |
27 |
28 | <% [2, 3].each do |i| %>
29 | <% if tickers[i].present? %>
30 | <%= render 'lib/stock_price/views/shared/ticker_xsmall', ticker: tickers[i], currency_symbol:, currency_separator: %>
31 | <% if i < 3 && tickers[i+1].present? %>
32 |
33 | <% end %>
34 | <% end %>
35 | <% end %>
36 |
37 |
38 |
39 | <% elsif tickers.size >= 5 %>
40 |
41 |
42 |
43 | <% [0, 1, 2].each do |i| %>
44 | <% if tickers[i].present? %>
45 | <%= render 'lib/stock_price/views/shared/ticker_xxsmall', ticker: tickers[i], currency_symbol:, currency_separator: %>
46 | <% if i < 2 %>
47 |
48 | <% end %>
49 | <% end %>
50 | <% end %>
51 |
52 |
53 | <% [3, 4, 5].each do |i| %>
54 | <% if tickers[i].present? %>
55 | <%= render 'lib/stock_price/views/shared/ticker_xxsmall', ticker: tickers[i], currency_symbol:, currency_separator: %>
56 | <% if i < 5 && tickers[i+1].present? %>
57 |
58 | <% end %>
59 | <% end %>
60 | <% end %>
61 |
62 |
63 |
64 | <% end %>
65 |
66 |
67 |

68 |
Stock Prices
69 |
70 |
71 |
--------------------------------------------------------------------------------
/lib/github_commit_graph/views/full.html.erb:
--------------------------------------------------------------------------------
1 | <% unless framework_v2 %>
2 | <%= render 'plugins/github_commit_graph/v1/full', local_assigns %>
3 | <% else %>
4 | <% unless (contributions[:errors].present?) %>
5 |
6 |
7 |
8 |
9 |
10 |
11 | <%= contributions[:total] %>
12 | Contributions in last year
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | <%= stats[:longest_streak] %>
21 | Longest streak
22 |
23 |
24 |
25 |
26 |
27 | <%= stats[:current_streak] %>
28 | Current streak
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 | <%= stats[:max_contributions] %>
40 | Most in a day
41 |
42 |
43 |
44 |
45 |
46 | <%= stats[:average_contributions] %>
47 | Average per day
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 | <% contributions[:commits].each do |week_of_commits| %>
58 | <% week_of_commits[:contributionDays].each do |day| %>
59 |
60 | <% end %>
61 | <% end %>
62 |
63 |
64 |
65 |
66 |

67 |
GitHub
68 |
<%= instance_name %>
69 |
70 |
71 | <%= render 'plugins/github_commit_graph/common' %>
72 | <% else %>
73 | <%= render partial: 'plugins/errors/full', locals: {
74 | icon: 'github--render.svg',
75 | title: 'GitHub',
76 | instance: instance_name,
77 | error_message: contributions[:errors]
78 | }
79 | %>
80 | <% end %>
81 | <% end %>
--------------------------------------------------------------------------------
/lib/github_commit_graph/github_commit_graph.rb:
--------------------------------------------------------------------------------
1 | module Plugins
2 | class GithubCommitGraph < Base
3 | def locals
4 | { username:, contributions:, stats: }
5 | end
6 |
7 | private
8 |
9 | def username = settings['username']
10 |
11 | def contributions
12 | @contributions ||= begin
13 | query = "query($userName:String!) {
14 | user(login: $userName){
15 | contributionsCollection {
16 | contributionCalendar {
17 | totalContributions
18 | weeks {
19 | contributionDays {
20 | contributionCount
21 | date
22 | }
23 | }
24 | }
25 | }
26 | }
27 | }"
28 | body = { query: query, variables: { userName: username } }
29 | resp = post('https://api.github.com/graphql', body: body.to_json, headers: headers)
30 | data = resp.dig('data', 'user', 'contributionsCollection', 'contributionCalendar')
31 |
32 | if data.nil?
33 | { errors: "Cannot fetch data for #{username}" }
34 | else
35 | {
36 | total: data['totalContributions'],
37 | commits: data['weeks'].map(&:deep_symbolize_keys)
38 | }
39 | end
40 | end
41 | end
42 |
43 | def headers
44 | { 'authorization' => "Bearer #{Rails.application.credentials.plugins.github_commit_graph_token}" }
45 | end
46 |
47 | # re "Streak" calculations, these may be wrong for some users
48 | # either A), the 'time-zone' header does not work (https://github.com/orgs/community/discussions/20898)
49 | # or B), the time zone is only inferred by the token's owner, us! (https://docs.github.com/en/rest/metrics/statistics?apiVersion=2022-11-28#get-the-hourly-commit-count-for-each-day)
50 | def stats
51 | return {} if contributions[:commits].nil?
52 |
53 | days = contributions[:commits].flat_map { |week| week[:contributionDays] }
54 | sorted_days = days.sort_by { |day| Date.parse(day[:date]) }
55 |
56 | {
57 | longest_streak: longest_streak(sorted_days),
58 | current_streak: current_streak(sorted_days),
59 | max_contributions: days.map { |day| day[:contributionCount] }.max,
60 | average_contributions: average_contributions(days)
61 | }
62 | end
63 |
64 | def average_contributions(days)
65 | total_contributions = days.sum { |day| day[:contributionCount] }
66 | (total_contributions.to_f / days.size).round(2)
67 | end
68 |
69 | def longest_streak(days)
70 | longest = current = 0
71 | days.each do |day|
72 | if day[:contributionCount].positive?
73 | current += 1
74 | longest = [longest, current].max
75 | else
76 | current = 0
77 | end
78 | end
79 | longest
80 | end
81 |
82 | def current_streak(days)
83 | streak = 0
84 | streak += 1 if days.last[:contributionCount].positive?
85 |
86 | days[0..-2].reverse_each do |day|
87 | break if day[:contributionCount].zero?
88 |
89 | streak += 1
90 | end
91 | streak
92 | end
93 | end
94 | end
95 |
--------------------------------------------------------------------------------
/lib/eight_sleep/eight_sleep.rb:
--------------------------------------------------------------------------------
1 | module Plugins
2 | class EightSleep < Base
3 |
4 | AUTH_URL = 'https://auth-api.8slp.net/v1/tokens'.freeze
5 | BASE_URL = 'https://app-api.8slp.net/v1'.freeze
6 |
7 | def locals
8 | { metrics: }
9 | end
10 |
11 | private
12 |
13 | def metrics
14 | create_access_token
15 |
16 | resp = fetch(metrics_url, headers:)
17 | JSON.parse(resp.body)['periods'][0]
18 | end
19 |
20 | def metrics_url
21 | fields = 'sfs,sqs,srs,sleep,light,rem,rem_percent,deep,deep_percent,hr,hrv,br,bedtime,wakeup,ttfa,ttgu,snore,snore_heavy,snore_percent,snore_heavy_percent'
22 | "#{BASE_URL}/users/#{user_id}/metrics/aggregate?to=#{today}&tz=#{tz}&metrics=#{fields}&periods=week"
23 | end
24 |
25 | # spoofs mobile app login
26 | def create_access_token
27 | resp = post(AUTH_URL, body: auth_body, headers: auth_headers)
28 | update_credentials(resp)
29 | end
30 |
31 | def update_credentials(resp, refreshing: false)
32 | data = JSON.parse(resp.body) # => { 'access_token', 'refresh_token', 'expires_in', 'userId', etc }
33 |
34 | # refreshing tokens does not respond with userId
35 | data = data.merge({ 'userId' => user_id }) if refreshing
36 |
37 | credentials = plugin_setting.encrypted_settings
38 | credentials['eight_sleep'] = data
39 | plugin_setting.update(encrypted_settings: credentials)
40 | end
41 |
42 | # anything cool we can do with this endpoint data?
43 | def me
44 | fetch('https://client-api.8slp.net/v1/users/me', headers:)
45 | end
46 |
47 | # https://github.com/lukas-clarke/eight_sleep/blob/101bbf88cdce8aeb7b449a29a1761cfd6004b65a/custom_components/eight_sleep/pyEight/constants.py#L30
48 | def auth_body
49 | {
50 | client_id: '0894c7f33bb94800a03f1f4df13a4f38',
51 | client_secret: 'f0954a3ed5763ba3d06834c73731a32f15f168f47d4f164751275def86db0c76',
52 | grant_type: 'password',
53 | username:,
54 | password:
55 | }.to_json
56 | end
57 |
58 | def auth_headers
59 | {
60 | 'content-type': "application/json",
61 | 'user-agent': "Android App",
62 | 'accept-encoding': "gzip",
63 | accept: "application/json"
64 | }
65 | end
66 |
67 | def headers
68 | {
69 | 'content-type': 'application/json',
70 | connection: 'keep-alive',
71 | 'user-agent': 'Home Assistant/2024.10.3-14058 (Android 14; SM-S911U1)', # based on: https://github.com/home-assistant/frontend/issues/19374#issuecomment-2506470285
72 | 'accept-encoding': 'gzip',
73 | accept: 'application/json',
74 | host: 'app-api.8slp.net',
75 | authorization: "Bearer #{access_token}"
76 | }
77 | end
78 |
79 | def access_token = plugin_setting.encrypted_settings.dig('eight_sleep', 'access_token')
80 |
81 | def user_id = plugin_setting.encrypted_settings.dig('eight_sleep', 'userId')
82 |
83 | def password = settings['password']
84 |
85 | def username = settings['email']
86 |
87 | def tz = ActiveSupport::TimeZone::MAPPING[user.tz] || 'America/New_York'
88 |
89 | def today = user.datetime_now.to_date
90 | end
91 | end
92 |
--------------------------------------------------------------------------------
/lib/mondrian/views/_common.html.erb:
--------------------------------------------------------------------------------
1 |
48 |
49 |
107 |
--------------------------------------------------------------------------------
/lib/mondrian/views/mondrian/_common.html.erb:
--------------------------------------------------------------------------------
1 |
48 |
49 |
107 |
--------------------------------------------------------------------------------
/lib/youtube_analytics/views/quadrant.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | <%= number_with_delimiter(metrics[:views]) %>
8 | Views
9 |
10 |
11 |
12 |
13 |
14 |
15 | <% total_seconds = metrics[:minutes_watched].to_i * 60 %>
16 | <% days = total_seconds / 86400 %>
17 | <% hours = (total_seconds % 86400) / 3600 %>
18 | <% minutes = (total_seconds % 3600) / 60 %>
19 | <% seconds = total_seconds % 60 %>
20 |
21 | <% if days > 0 %>
22 | <%= days %>+
23 | <% time_unit = 'Days' %>
24 | <% elsif hours > 0 %>
25 | <%= hours %>+
26 | <% time_unit = 'Hours' %>
27 | <% elsif minutes > 0 %>
28 | <%= minutes %>+
29 | <% time_unit = 'Minutes' %>
30 | <% else %>
31 | <%= pluralize(seconds, 'second') %>
32 | <% time_unit = 'Seconds' %>
33 | <% end %>
34 |
35 | <%= time_unit %> Watched
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 | <%= (metrics[:average_view_percentage])&.round %>%
45 | View Through Rate
46 |
47 |
48 |
49 |
50 |
51 |
52 | <% avg_view_duration = metrics[:average_view_duration].to_i %>
53 | <% minutes = avg_view_duration / 60 %>
54 | <% seconds = avg_view_duration % 60 %>
55 | <%= "#{minutes}m #{seconds}s" %>
56 |
57 | Average View Duration
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 | <%= number_with_delimiter(metrics[:subscribers_gained]) %>
67 | Subscribers Gained
68 |
69 |
70 |
71 |
72 |
73 | <%= number_with_delimiter(metrics[:likes]) %>
74 | Likes
75 |
76 |
77 |
78 |
79 |
80 |
81 |

82 |
YouTube
83 |
<%= instance_name %>
84 |
85 |
--------------------------------------------------------------------------------
/lib/calendar/views/_full_week.html.erb:
--------------------------------------------------------------------------------
1 | <%= render "plugins/calendars/common" %>
2 |
3 | <%# week-only styling, will break month view so not inside common %>
4 |
45 |
46 |
47 |
48 |
49 |
50 |
85 |
--------------------------------------------------------------------------------
/lib/usa_college_football_rankings.md:
--------------------------------------------------------------------------------
1 | # College Football Top 10 Rankings Plugin on TRMNL
2 |
3 | 
4 |
5 | ## Step 1: Create a New Private Plugin
6 | Log in to your TRMNL dashboard.
7 | On the left-hand menu, click on the 'Go to Plugins' button.
8 | Find the 'Private Plugin' Plugin to create a Private Plugin.
9 | Click 'Add new' to create a new Private Plugin.
10 |
11 | ## Step 2: Set up the Polling Strategy
12 | Name your plugin (e.g., "College Football Top 10") then scroll down to the Strategy section.
13 | Choose the Polling strategy from the Strategy dropdown menu.
14 | In the Polling URL field, enter this URL:
15 |
16 | ```
17 | https://site.api.espn.com/apis/site/v2/sports/football/college-football/rankings
18 | ```
19 | Click Save. Once it is saved, the 'Edit Markup' button is not available.
20 |
21 | ## Step 3: Add the HTML Markup
22 | Click the 'Edit Markup' button.
23 |
24 | Copy and paste the following code into the Markup box. This code will display the Top 10 college football teams along with their rank, last week’s rank, and current record.
25 |
26 | ```
27 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 | | Rank |
40 | Team |
41 | Last |
42 | Record |
43 |
44 |
45 |
46 | {% assign counter = 1 %}
47 | {% for rank in rankings[0].ranks %}
48 | {% if counter <= 10 %}
49 |
50 | | {{ counter }} |
51 |
52 |
53 | 
54 | {{ rank.team.location }} {{ rank.team.name }}
55 |
56 | |
57 | {{ rank.previous }} |
58 | {{ rank.recordSummary }} |
59 |
60 | {% assign counter = counter | plus: 1 %}
61 | {% endif %}
62 | {% endfor %}
63 |
64 |
65 |
66 |
67 |
68 |
69 |

70 |
College Football
71 |
Top 10 Rankings
72 |
73 |
74 | ```
75 |
76 | ## Step 4: Save and Activate the Plugin
77 | Once you have entered the markup, click Save to store the plugin.
78 | Navigate to the Playlists tab in your TRMNL dashboard.
79 | Drag and drop your new College Football Top 10 plugin to the top of your playlist if not automatically added.
80 |
81 | ## Step 5: View the College Football Rankings on Your Device
82 | Once refreshed, your TRMNL device will display the Top 10 College Football Rankings, showing the team’s current rank, last week’s rank, and win-loss record.
83 |
84 | ### Customizations (Optional)
85 | Font Size: You can adjust the font size by changing the font-size values in the markup.
86 | More Teams: If you want to display more than 10 teams, you can increase the limit in the code by replacing 10 with the number of teams you want in this line:
87 |
88 | ```
89 | {% if counter <= 10 %}
90 | ```
91 |
--------------------------------------------------------------------------------
/lib/lunar_calendar/views/full.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | <%= current_phase[:name] %>
8 | <%= t('renders.lunar_calendar.current_phase') %>
9 |
10 |
11 |
12 |
13 |
14 |
15 | <%= illumination %>%
16 | <%= t('renders.lunar_calendar.illumination') %>
17 |
18 |
19 |
20 |
21 |
22 |
23 | <%= age %>
24 | <%= t('renders.lunar_calendar.age') %>
25 |
26 |
27 |
28 |
29 |
30 |
31 | <% ordered_phase_sequence = phase_sequence.sort_by { |seq| seq[:date_full] } %>
32 |
33 |
34 |
35 | <% ordered_phase_sequence.each do |phase| %>
36 |
37 | <%= phase[:date] %>
38 |
39 | <% end %>
40 |
41 |
42 | <% ordered_phase_sequence.each do |phase| %>
43 |
44 |

45 |
46 | <% end %>
47 |
48 |
49 | <% ordered_phase_sequence.each do |phase| %>
50 |
51 |
52 | <%= phase[:name] %>
53 |
54 |
55 | <% end %>
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 | <%= next_phase[:name] %>
66 | <%= t('renders.lunar_calendar.next_phase') %> (<%= next_phase[:date] %>)
67 |
68 |
69 |
70 |
71 |
72 |
73 | <%= next_full_moon %>
74 | <%= t('renders.lunar_calendar.next_full_moon') %>
75 |
76 |
77 |
78 |
79 |
80 |
81 | <%= next_new_moon %>
82 | <%= t('renders.lunar_calendar.next_new_moon') %>
83 |
84 |
85 |
86 |
87 |
88 |
89 |

90 |
<%= t('renders.lunar_calendar.title') %>
91 |
<%= instance_name %>
92 |
93 |
94 |
95 |
106 |
--------------------------------------------------------------------------------
/lib/parcel/parcel.rb:
--------------------------------------------------------------------------------
1 | module Plugins
2 | class Parcel < Base
3 |
4 | # maps Parcel status codes to I18n keys "renders.parcel.status.{status_key}"
5 | # (fall back to 'renders.parcel.status.unknown')
6 | STATUS_KEYS = {
7 | 0 => 'delivered',
8 | 1 => 'frozen',
9 | 2 => 'in_transit',
10 | 3 => 'expecting_pickup',
11 | 4 => 'out_for_delivery',
12 | 5 => 'not_found',
13 | 6 => 'failed_delivery_attempt',
14 | 7 => 'delivery_exception',
15 | 8 => 'information_received'
16 | }.freeze
17 |
18 | def locals
19 | result = fetch_deliveries
20 | skip_display = skip_if_empty? && result[:error].nil? && result[:deliveries].empty?
21 |
22 | {
23 | deliveries: result[:deliveries].map { delivery_item(it) },
24 | error: result[:error],
25 | filter_mode: filter_mode.humanize,
26 | skip_display:,
27 | style:
28 | }
29 | end
30 |
31 | private
32 |
33 | def delivery_item(delivery)
34 | {
35 | title: delivery['description'],
36 | status_key: status_key(delivery['status_code']),
37 | latest: latest_event(delivery['events']),
38 | delivery_by: delivery_by(delivery),
39 | days: delivery_days(delivery)
40 | }
41 | end
42 |
43 | def style = settings['style'] || 'detailed'
44 |
45 | def api_key = settings['api_key']
46 |
47 | def filter_mode = settings['filter_mode']
48 |
49 | def skip_if_empty? = settings['empty_state'] == 'skip'
50 |
51 | def headers = { 'api-key' => api_key, 'user-agent' => 'TRMNL Server' }
52 |
53 | def query = { 'filter_mode' => filter_mode }
54 |
55 | def url = 'https://api.parcel.app/external/deliveries/'
56 |
57 | def cache_key = "parcel_#{api_key}"
58 |
59 | def fetch_deliveries
60 | # API docs: https://parcelapp.net/help/api.html
61 | # "The rate limit is 20 requests per hour."
62 | Rails.cache.fetch(cache_key, expires_in: 5.minutes) do
63 | response = HTTParty.get(url, headers:, query:)
64 |
65 | if response.success?
66 | data = JSON.parse(response.body) # content-type is wrong (text/html), so we need to explicitly parse the body
67 | if data['success']
68 | { deliveries: data['deliveries'], error: nil }
69 | elsif data['error_message'].present?
70 | { deliveries: [], error: data['error_message'] }
71 | else
72 | { deliveries: [], error: t('renders.parcel.errors.api') }
73 | end
74 | else
75 | { deliveries: [], error: t('renders.parcel.errors.http', code: response.code) }
76 | end
77 | end
78 | rescue StandardError
79 | { deliveries: [], error: t('renders.parcel.errors.internal') }
80 | end
81 |
82 | def status_key(status_code)
83 | STATUS_KEYS.fetch(status_code, 'unknown')
84 | end
85 |
86 | def user_today
87 | user.datetime_now.to_date
88 | end
89 |
90 | def delivery_days(delivery)
91 | return unless delivery['date_expected'].present?
92 |
93 | date_expected = DateTime.parse(delivery['date_expected']).to_date
94 |
95 | days = (date_expected - user_today).to_i
96 | days >= 0 ? days : nil
97 | rescue Date::Error
98 | nil
99 | end
100 |
101 | def latest_event(events)
102 | return nil if events.blank?
103 |
104 | events.first['event']
105 | end
106 |
107 | def delivery_by(delivery)
108 | delivery_days = delivery_days(delivery)
109 | return unless delivery_days
110 |
111 | date_expected = DateTime.parse(delivery['date_expected']).to_date
112 | if delivery_days >= 7
113 | l(date_expected, format: :short) # far in the future
114 | elsif delivery_days == 1
115 | t('renders.parcel.tomorrow') # tomorrow
116 | elsif delivery_days.zero?
117 | t('renders.parcel.today') # today
118 | else
119 | l(date_expected, format: '%A') # day of week
120 | end
121 | rescue Date::Error
122 | nil
123 | end
124 | end
125 | end
126 |
--------------------------------------------------------------------------------
/lib/youtube_analytics/views/full.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | <%= number_with_delimiter(metrics[:views]) %>
9 | Views
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | <% total_seconds = metrics[:minutes_watched].to_i * 60 %>
19 | <% days = total_seconds / 86400 %>
20 | <% hours = (total_seconds % 86400) / 3600 %>
21 | <% minutes = (total_seconds % 3600) / 60 %>
22 | <% seconds = total_seconds % 60 %>
23 |
24 | <% if days > 0 %>
25 | <%= days %>+
26 | <% time_unit = 'Days' %>
27 | <% elsif hours > 0 %>
28 | <%= hours %>+
29 | <% time_unit = 'Hours' %>
30 | <% elsif minutes > 0 %>
31 | <%= minutes %>+
32 | <% time_unit = 'Minutes' %>
33 | <% else %>
34 | <%= seconds %>
35 | <% time_unit = 'Seconds' %>
36 | <% end %>
37 |
38 | <%= time_unit %> Watched
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 | <% avg_view_duration = metrics[:average_view_duration].to_i %>
50 | <% minutes = avg_view_duration / 60 %>
51 | <% seconds = avg_view_duration % 60 %>
52 | <%= "#{minutes}m #{seconds}s" %>
53 |
54 | Average View Duration
55 |
56 |
57 |
58 |
59 |
60 | <%= number_with_delimiter(metrics[:subscribers_gained]) %>
61 | Subscribers Gained
62 |
63 |
64 |
65 |
66 |
67 | <%= (metrics[:average_view_percentage])&.round %>%
68 | View Through Rate
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 | <%= number_with_delimiter(metrics[:likes]) %>
78 | Likes
79 |
80 |
81 |
82 |
83 |
84 | <%= number_with_delimiter(metrics[:comments]) %>
85 | Comments
86 |
87 |
88 |
89 |
90 |
91 | <%= number_with_delimiter(metrics[:shares]) %>
92 | Shares
93 |
94 |
95 |
96 |
97 |
98 |
99 |

100 |
YouTube
101 |
<%= instance_name %>
102 |
103 |
--------------------------------------------------------------------------------
/lib/calendar/views/_common.html.erb:
--------------------------------------------------------------------------------
1 |
157 |
--------------------------------------------------------------------------------
/lib/youtube_analytics/views/half_vertical.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | <%= number_with_delimiter(metrics[:views]) %>
7 | Views
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | <% total_seconds = metrics[:minutes_watched].to_i * 60 %>
16 | <% days = total_seconds / 86400 %>
17 | <% hours = (total_seconds % 86400) / 3600 %>
18 | <% minutes = (total_seconds % 3600) / 60 %>
19 | <% seconds = total_seconds % 60 %>
20 |
21 | <% if days > 0 %>
22 | <%= days %>+
23 | <% time_unit = 'Days' %>
24 | <% elsif hours > 0 %>
25 | <%= hours %>+
26 | <% time_unit = 'Hours' %>
27 | <% elsif minutes > 0 %>
28 | <%= minutes %>+
29 | <% time_unit = 'Minutes' %>
30 | <% else %>
31 | <%= pluralize(seconds, 'second') %>
32 | <% time_unit = 'Seconds' %>
33 | <% end %>
34 |
35 | <%= time_unit %> Watched
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 | <%= metrics[:average_view_percentage]&.round %>%
44 | View Through Rate
45 |
46 |
47 |
48 |
49 |
50 |
51 | <% avg_view_duration = metrics[:average_view_duration].to_i %>
52 | <% minutes = avg_view_duration / 60 %>
53 | <% seconds = avg_view_duration % 60 %>
54 | <%= "#{minutes}m #{seconds}s" %>
55 |
56 | Average View Duration
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 | <%= number_with_delimiter(metrics[:subscribers_gained]) %>
66 | Subscribers Gained
67 |
68 |
69 |
70 |
71 |
72 | <%= number_with_delimiter(metrics[:likes]) %>
73 | Likes
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 | <%= number_with_delimiter(metrics[:comments]) %>
83 | Comments
84 |
85 |
86 |
87 |
88 |
89 | <%= number_with_delimiter(metrics[:shares]) %>
90 | Shares
91 |
92 |
93 |
94 |
95 |
96 |
97 |

98 |
YouTube
99 |
<%= instance_name %>
100 |
101 |
--------------------------------------------------------------------------------
/lib/youtube_analytics/youtube_analytics.rb:
--------------------------------------------------------------------------------
1 | module Plugins
2 | class YoutubeAnalytics < Base
3 | def locals
4 | { metrics: }
5 | end
6 |
7 | class << self
8 | def redirect_url
9 | client = Signet::OAuth2::Client.new(client_options)
10 | client.authorization_uri.to_s
11 | end
12 |
13 | def fetch_access_token(code)
14 | client = Signet::OAuth2::Client.new(client_options)
15 | client.code = code
16 | client.fetch_access_token!
17 | end
18 |
19 | def client_options
20 | {
21 | client_id: Rails.application.credentials.plugins[:google][:client_id],
22 | client_secret: Rails.application.credentials.plugins[:google][:client_secret],
23 | authorization_uri: 'https://accounts.google.com/o/oauth2/auth', # may require change to: '/oauth2/v2/auth'
24 | token_credential_uri: 'https://accounts.google.com/o/oauth2/token', # may require change to: '/oauth2/v2/token'
25 | access_type: 'offline',
26 | scope: [Google::Apis::YoutubeAnalyticsV2::AUTH_YT_ANALYTICS_READONLY],
27 | redirect_uri: "#{Rails.application.credentials.base_url}/plugin_settings/youtube_analytics/redirect",
28 | additional_parameters: {
29 | prompt: 'consent select_account'
30 | }
31 | }
32 | end
33 | end
34 |
35 | private
36 |
37 | def metrics = fetch_analytics
38 |
39 | def fetch_analytics
40 | retry_count = 0
41 | begin
42 | service = Google::Apis::YoutubeAnalyticsV2::YouTubeAnalyticsService.new
43 | service.authorization = client
44 |
45 | report = service.query_report(
46 | ids: "channel==MINE",
47 | start_date: start_date,
48 | end_date: end_date,
49 | metrics: "views,estimatedMinutesWatched,averageViewDuration,averageViewPercentage,subscribersGained,likes,dislikes,comments,shares",
50 | dimensions: "day",
51 | sort: "day"
52 | )
53 |
54 | process_report(report)
55 | rescue Google::Apis::AuthorizationError
56 | refresh!
57 | retry_count += 1
58 | if retry_count <= 1 # Retry just once, otherwise retry next day.
59 | sleep 2
60 | retry
61 | else
62 | plugin_settings.refresh_in_24hr
63 | {}
64 | end
65 | rescue Google::Apis::ClientError => e
66 | handle_erroring_state(e.message)
67 | end
68 | end
69 |
70 | def process_report(report)
71 | kpis = {
72 | views: 0,
73 | minutes_watched: 0,
74 | average_view_duration: 0,
75 | average_view_percentage: 0,
76 | subscribers_gained: 0,
77 | likes: 0,
78 | dislikes: 0,
79 | comments: 0,
80 | shares: 0
81 | }
82 |
83 | report.rows.each do |metrics|
84 | kpis[:views] += metrics[1].round(2)
85 | kpis[:minutes_watched] += metrics[2].round(2)
86 | kpis[:average_view_duration] += metrics[3].round(2)
87 | kpis[:average_view_percentage] += metrics[4].round(2)
88 | kpis[:subscribers_gained] += metrics[5].round(2)
89 | kpis[:likes] += metrics[6].round(2)
90 | kpis[:dislikes] += metrics[7].round(2)
91 | kpis[:comments] += metrics[8].round(2)
92 | kpis[:shares] += metrics[9].round(2)
93 | end
94 |
95 | kpis[:average_view_duration] = (kpis[:average_view_duration] / settings['duration'].to_i).round(2)
96 | kpis[:average_view_percentage] = (kpis[:average_view_percentage] / settings['duration'].to_i).to_i
97 | kpis
98 | end
99 |
100 | def start_date = (Date.today - settings['duration'].to_i.days).to_s
101 |
102 | def end_date = Date.today.to_s
103 |
104 | def client
105 | credentials = settings['youtube_analytics']
106 | oauth_client = Signet::OAuth2::Client.new(Plugins::YoutubeAnalytics.client_options)
107 | oauth_client.update!(credentials)
108 | oauth_client
109 | end
110 |
111 | def refresh!
112 | response = client.refresh!
113 | credentials = plugin_settings.encrypted_settings
114 | credentials['youtube_analytics']['access_token'] = response['access_token']
115 | plugin_settings.update(encrypted_settings: credentials)
116 | self.settings = plugin_settings.settings.merge(plugin_settings.encrypted_settings)
117 | rescue Signet::AuthorizationError
118 | handle_erroring_state('Signet::AuthorizationError')
119 | end
120 | end
121 | end
122 |
--------------------------------------------------------------------------------
/lib/stock_price/stock_price.rb:
--------------------------------------------------------------------------------
1 | module Plugins
2 | class StockPrice < Base
3 | TICKER_NAME_DATA = JSON.parse(File.read("db/data/ticker-name.json"))
4 | STOCK_TICKER_LIMIT = 12.freeze
5 |
6 | SUPPORTED_INDEX = [
7 | 'VIX', 'MXACW', 'XWLD', 'MXUSA', 'MXEF', 'MXEA', 'DJI', 'RUI', 'RUT', 'OEX', 'OEX', 'SPX', 'XSP', 'XSP', 'SPESG', 'XND', 'IXIC'
8 | ].freeze
9 |
10 | SUPPORTED_METAL = ['XAG', 'XAU', 'XPD', 'HG'].freeze
11 |
12 | def locals = { tickers:, currency_symbol:, currency_separator: }
13 |
14 | private
15 |
16 | def tickers
17 | ticker_data = string_to_array(settings["symbol"], limit: STOCK_TICKER_LIMIT).map do |symbol|
18 | ticker_price(symbol.upcase)
19 | end.compact
20 |
21 | ticker_data.each do |stock|
22 | stock[:price] = localized_price(stock[:price])
23 | end
24 | end
25 |
26 | def invalid_symbol(symbol)
27 | {
28 | symbol: symbol,
29 | name: 'SYMBOL_NOT_SUPPORTED',
30 | price: 0,
31 | change: ''
32 | }
33 | end
34 |
35 | def ticker_name(symbol) = TICKER_NAME_DATA[symbol]
36 |
37 | def ticker_price(symbol)
38 | Rails.cache.fetch "STOCK_PRICE_#{symbol}_extended_#{extended_hours?}_v2", expire_in: 15.minutes, skip_nil: true do
39 | if SUPPORTED_INDEX.include?(symbol)
40 | response = fetch(index_url(symbol), headers:)
41 | format_data(response, symbol)
42 | elsif SUPPORTED_METAL.include?(symbol.upcase.gsub!('USD', ''))
43 | response = fetch(metal_url(symbol), headers:)
44 | format_metal_data(response, symbol)
45 | else
46 | response = fetch(stock_url(symbol), headers:)
47 | format_data(response, symbol)
48 | end
49 | end
50 | end
51 |
52 | def format_metal_data(response, symbol)
53 | {
54 | symbol: symbol,
55 | name: response['name'],
56 | price: response['price'],
57 | change: "N/A"
58 | }
59 | end
60 |
61 | def format_data(response, symbol)
62 | if %w[error no_data].include?(response['s'])
63 | invalid_symbol(symbol)
64 | else
65 | {
66 | symbol: symbol.upcase,
67 | name: ticker_name(symbol.upcase),
68 | price: response.dig("last", 0),
69 | change: "#{(response.dig('changepct', 0).to_f * 100)&.round(2)}%"
70 | }
71 | end
72 | end
73 |
74 | def localized_price(base_price)
75 | (base_price * currency_conversions[currency])&.round(2)
76 | end
77 |
78 | def currency = settings['currency'].upcase
79 |
80 | def currency_symbol
81 | {
82 | 'USD' => '$',
83 | 'EUR' => '€',
84 | 'CAD' => '$',
85 | 'CHF' => '₣',
86 | 'GBP' => '£',
87 | 'KRW' => '₩',
88 | 'CNY' => '¥',
89 | 'JPY' => '¥',
90 | 'INR' => '₹',
91 | 'ZAR' => 'R',
92 | 'CLP' => '$'
93 | }[currency]
94 | end
95 |
96 | def currency_separator
97 | case currency
98 | when 'EUR'
99 | ','
100 | else
101 | '.'
102 | end
103 | end
104 |
105 | def currency_conversions
106 | Rails.cache.fetch "STOCK_PRICE_CURRENCIES_#{supported_currencies.count}_VALUES", expire_in: 1.day, skip_nil: true do
107 | resp = HTTParty.get(currency_conversion_url)
108 | JSON.parse(resp.body)['data'].map { |k, v| { k => v['value'] } }.reduce({}, :merge)
109 | rescue StandardError
110 | nil
111 | end
112 | end
113 |
114 | def supported_currencies
115 | plugin.account_fields.find { |af| af['keyname'] == 'currency' }&.dig('options')
116 | end
117 |
118 | def extended_hours?
119 | settings['extended_hours'] == 'yes'
120 | end
121 |
122 | def stock_url(symbol) = "https://api.marketdata.app/v1/stocks/quotes/#{CGI.escape(symbol)}/?extended=#{extended_hours?}"
123 |
124 | def index_url(symbol) = "https://api.marketdata.app/v1/indices/quotes/#{CGI.escape(symbol)}/?extended=#{extended_hours?}"
125 |
126 | def metal_url(symbol) = "https://api.gold-api.com/price/#{symbol.gsub!('USD', '')}"
127 |
128 | def currency_conversion_url = "https://api.currencyapi.com/v3/latest?apikey=#{Rails.application.credentials.plugins.currency_api}¤cies=#{supported_currencies.join('%2C')}"
129 |
130 | def headers
131 | {
132 | Accept: "application/json",
133 | Authorization: "Token #{Rails.application.credentials.plugins[:marketdata_app]}"
134 | }
135 | end
136 | end
137 | end
138 |
--------------------------------------------------------------------------------
/lib/youtube_analytics/views/half_horizontal.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | <%= number_with_delimiter(metrics[:views]) %>
9 | Views
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | <% total_seconds = metrics[:minutes_watched].to_i * 60 %>
18 | <% days = total_seconds / 86400 %>
19 | <% hours = (total_seconds % 86400) / 3600 %>
20 | <% minutes = (total_seconds % 3600) / 60 %>
21 | <% seconds = total_seconds % 60 %>
22 |
23 | <% if days > 0 %>
24 | <%= days %>+
25 | <% time_unit = 'Days Watched' %>
26 | <% elsif hours > 0 %>
27 | <%= hours %>+
28 | <% time_unit = 'Hours Watched' %>
29 | <% elsif minutes > 0 %>
30 | <%= minutes %>+
31 | <% time_unit = 'Minutes Watched' %>
32 | <% else %>
33 | <%= pluralize(seconds, 'second') %>
34 | <% time_unit = 'Seconds Watched' %>
35 | <% end %>
36 |
37 | <%= time_unit %>
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 | <%= (metrics[:average_view_percentage])&.round %>%
46 | View Through Rate
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 | <% avg_view_duration = metrics[:average_view_duration].to_i %>
55 | <% minutes = avg_view_duration / 60 %>
56 | <% seconds = avg_view_duration % 60 %>
57 | <%= "#{minutes}m #{seconds}s" %>
58 |
59 | Average View Duration
60 |
61 |
62 |
63 |
64 |
65 |
66 | <%= number_with_delimiter(metrics[:subscribers_gained]) %>
67 | Subscribers Gained
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 | <%= number_with_delimiter(metrics[:likes]) %>
76 | Likes
77 |
78 |
79 |
80 |
81 |
82 |
83 | <%= number_with_delimiter(metrics[:comments]) %>
84 | Comments
85 |
86 |
87 |
88 |
89 |
90 |
91 | <%= number_with_delimiter(metrics[:shares]) %>
92 | Shares
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |

101 |
YouTube
102 |
<%= instance_name %>
103 |
104 |
--------------------------------------------------------------------------------
/lib/lunar_calendar/views/half_vertical.html.erb:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | <%= current_phase[:name] %>
8 | <%= t('renders.lunar_calendar.current_phase') %>
9 |
10 |
11 |
12 |
13 |
14 | <%= next_phase[:name] %>
15 | <%= t('renders.lunar_calendar.next_phase_short') %> (<%= next_phase[:date] %>)
16 |
17 |
18 |
19 |
20 |
21 |
22 | <% ordered_phase_sequence = phase_sequence.sort_by { |seq| seq[:date_full] } %>
23 | <% first_phase = ordered_phase_sequence.first %>
24 | <% current = ordered_phase_sequence.find { |phase| phase[:current] } %>
25 | <% last_phase = ordered_phase_sequence.last %>
26 |
27 |
28 |
29 |
30 | <%= first_phase[:date] %>
31 |
32 |
33 | <%= current[:date] %>
34 |
35 |
36 | <%= last_phase[:date] %>
37 |
38 |
39 |
40 |
41 | <% ordered_phase_sequence.each do |phase| %>
42 |
43 |

44 |
45 | <% end %>
46 |
47 |
48 |
49 |
50 | <%= first_phase[:name] %>
51 |
52 |
53 | <%= current[:name] %>
54 |
55 |
56 | <%= last_phase[:name] %>
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 | <%= age %>
68 | <%= t('renders.lunar_calendar.age') %>
69 |
70 |
71 |
72 |
73 |
74 | <%= illumination %>%
75 | <%= t('renders.lunar_calendar.illumination') %>
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 | <%= next_full_moon %>
87 | <%= t('renders.lunar_calendar.next_full_moon') %>
88 |
89 |
90 |
91 |
92 |
93 | <%= next_new_moon %>
94 | <%= t('renders.lunar_calendar.next_new_moon') %>
95 |
96 |
97 |
98 |
99 |
100 |
101 |

102 |
<%= t('renders.lunar_calendar.title') %>
103 |
<%= instance_name %>
104 |
105 |
106 |
107 |
118 |
--------------------------------------------------------------------------------
/lib/calendar/helpers/base.rb:
--------------------------------------------------------------------------------
1 | module Calendar
2 | module Helper
3 | # rubocop:disable Lint/DuplicateBranch
4 | def cutoff_time
5 | case [event_layout, include_past_event?]
6 | when ['default', true], ['today_only', true], ['week', false], ['month', false], ['rolling_month', false]
7 | beginning_of_day
8 | when ['default', false], ['today_only', false] # this is useful for busy calendar to remove event that have elapsed.
9 | now_in_tz
10 | when ['week', true], ['month', true], ['rolling_month', true]
11 | time_min
12 | else beginning_of_day
13 | end
14 | end
15 | # rubocop:enable Lint/DuplicateBranch
16 |
17 | def event_layout = settings['event_layout']
18 |
19 | def include_past_event? = settings['include_past_events'] == 'yes'
20 |
21 | def now_in_tz = user.datetime_now
22 |
23 | # required to ensure locals data has a 'diff' at least 1x per day
24 | # given week/month view 'highlight' current day
25 | # without this local, previous day will be highlighted if events dont change
26 | def today_in_tz = now_in_tz.to_date.to_s
27 |
28 | def beginning_of_day = now_in_tz.beginning_of_day
29 |
30 | def time_min
31 | days_behind = case event_layout
32 | when 'month', 'rolling_month'
33 | 30 # TODO: improve this to not store all 30 days, as right now we endup storing 30+3=60 days events.
34 | else
35 | 7
36 | end
37 |
38 | (beginning_of_day - days_behind.days)
39 | end
40 |
41 | def first_day = Date.strptime(settings['first_day'], '%a').wday
42 |
43 | def fixed_week = settings['fixed_week'] == 'yes'
44 |
45 | def ignore_based_on_status?(event)
46 | if event.instance_of?(Icalendar::Event)
47 | return true if event.status == 'CANCELLED' && event.dtstart.nil?
48 | return true if event.exdate.flatten.filter { |exception_date| event.dtstart&.to_datetime == exception_date.to_datetime }.present?
49 | end
50 | return false if event.status&.downcase == 'confirmed' # always include confirmed events
51 |
52 | # include non-confirmed events if user prefers to see them (received requests for both options)
53 | # if this branch is reached, event.status == [nil, 'rejected'] etc
54 | settings['event_status_filter'] == 'confirmed_only'
55 | end
56 |
57 | def include_description
58 | return true unless settings['include_description'] # backward compatible default value
59 |
60 | settings['include_description'] == 'yes'
61 | end
62 |
63 | def include_event_time
64 | return false unless settings['include_event_time'] # backward compatible default value
65 |
66 | settings['include_event_time'] == 'yes'
67 | end
68 |
69 | def ignored_phrases
70 | return [] unless settings['ignore_phrases']
71 |
72 | settings['ignore_phrases'].gsub("\n", "").gsub("\r", "").split(',').map(&:squish)
73 | end
74 |
75 | def time_format = settings['time_format'] || 'am/pm'
76 |
77 | def formatted_time
78 | return "%-I:%M %p" if time_format == 'am/pm'
79 |
80 | "%R"
81 | end
82 |
83 | def date_format
84 | return '%A, %B %-d' unless settings['date_format'].present? # => Monday, June 16
85 |
86 | # special i18n key that must be symbolized to be accepted as an arg to l(.. format:)
87 | return :short if settings['date_format'] == 'short'
88 |
89 | settings['date_format']
90 | end
91 |
92 | # ability to hard-code each day's first time slot in event_layout=week mode
93 | # by default we lookup the earliest event within the period, but some users
94 | # prefer to sacrifice morning visibility to see more throughout the day
95 | def scroll_time
96 | return settings['scroll_time'] if settings['scroll_time'].present?
97 |
98 | events.reject { |e| e[:all_day] }.map { |e| e[:start_full].to_time.strftime("%H:00:00") }.min || '08:00:00'
99 | end
100 |
101 | def scroll_time_end
102 | return settings['scroll_time_end'] if settings['scroll_time_end'].present?
103 |
104 | events.reject { |e| e[:all_day] }.map { |e| e[:end_full].to_time.strftime("%H:00:00") }.max || '24:00:00' # same default: https://fullcalendar.io/docs/slotMaxTime
105 | end
106 |
107 | def zoom_mode
108 | settings['zoom_mode'] == 'yes'
109 | end
110 |
111 | # Override the no_screen_padding? method from Formatter to handle calendar-specific logic
112 | def no_screen_padding?
113 | # Check if event_layout requires no padding
114 | if %w[week month rolling_month].include?(event_layout)
115 | return true
116 | end
117 |
118 | # Fall back to default behavior from Formatter
119 | super
120 | end
121 | end
122 | end
123 |
--------------------------------------------------------------------------------