OctoPrint-Wrapped 1.0.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- octoprint_wrapped-1.0.0/MANIFEST.in +4 -0
- octoprint_wrapped-1.0.0/OctoPrint_Wrapped.egg-info/PKG-INFO +45 -0
- octoprint_wrapped-1.0.0/OctoPrint_Wrapped.egg-info/SOURCES.txt +21 -0
- octoprint_wrapped-1.0.0/OctoPrint_Wrapped.egg-info/dependency_links.txt +1 -0
- octoprint_wrapped-1.0.0/OctoPrint_Wrapped.egg-info/entry_points.txt +2 -0
- octoprint_wrapped-1.0.0/OctoPrint_Wrapped.egg-info/requires.txt +3 -0
- octoprint_wrapped-1.0.0/OctoPrint_Wrapped.egg-info/top_level.txt +1 -0
- octoprint_wrapped-1.0.0/PKG-INFO +45 -0
- octoprint_wrapped-1.0.0/README.md +32 -0
- octoprint_wrapped-1.0.0/octoprint_wrapped/__init__.py +257 -0
- octoprint_wrapped-1.0.0/octoprint_wrapped/static/clientjs/wrapped.js +27 -0
- octoprint_wrapped-1.0.0/octoprint_wrapped/static/js/ko.src.svgtopng.js +36 -0
- octoprint_wrapped-1.0.0/octoprint_wrapped/static/js/wrapped.js +178 -0
- octoprint_wrapped-1.0.0/octoprint_wrapped/static/pure-snow/LICENSE +21 -0
- octoprint_wrapped-1.0.0/octoprint_wrapped/static/pure-snow/pure-snow.css +10 -0
- octoprint_wrapped-1.0.0/octoprint_wrapped/static/pure-snow/pure-snow.js +146 -0
- octoprint_wrapped-1.0.0/octoprint_wrapped/templates/wrapped.svg.jinja2 +1258 -0
- octoprint_wrapped-1.0.0/octoprint_wrapped/templates/wrapped_about.jinja2 +24 -0
- octoprint_wrapped-1.0.0/octoprint_wrapped/templates/wrapped_navbar_snowfall.jinja2 +7 -0
- octoprint_wrapped-1.0.0/octoprint_wrapped/templates/wrapped_navbar_wrapped.jinja2 +7 -0
- octoprint_wrapped-1.0.0/pyproject.toml +138 -0
- octoprint_wrapped-1.0.0/setup.cfg +4 -0
- octoprint_wrapped-1.0.0/setup.py +4 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: OctoPrint-Wrapped
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Get your yearly OctoPrint stats and let it snow! Depends on the Achievements plugin.
|
|
5
|
+
Author-email: Gina Häußge <gina@octoprint.org>
|
|
6
|
+
License: AGPL-3.0-or-later
|
|
7
|
+
Project-URL: Homepage, https://github.com/OctoPrint/OctoPrint-Wrapped
|
|
8
|
+
Requires-Python: <4,>=3.9
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
Provides-Extra: develop
|
|
11
|
+
Requires-Dist: go-task-bin; extra == "develop"
|
|
12
|
+
Dynamic: license
|
|
13
|
+
|
|
14
|
+
# OctoPrint Wrapped! 🎁
|
|
15
|
+
|
|
16
|
+
Get your yearly OctoPrint stats a shareable "wrapped" picture - and let it snow! ❄️
|
|
17
|
+
|
|
18
|
+
<img src="https://raw.githubusercontent.com/OctoPrint/OctoPrint-Wrapped/main/extras/wrapped-demo.png" width="400" height="400" alt="Demo OctoPrint Wrapped share picture" />
|
|
19
|
+
|
|
20
|
+

|
|
21
|
+
|
|
22
|
+
The stats picture depends on the Achievements plugin being enabled (as it takes care of
|
|
23
|
+
the stats collection during the year) and can be opened via the little gift package icon
|
|
24
|
+
in the navbar.
|
|
25
|
+
|
|
26
|
+
The snow effect can always be toggled during the season using the little snowflake icon
|
|
27
|
+
in the navbar, and its setting persists through the browser's local storage.
|
|
28
|
+
|
|
29
|
+
Both wrapped and snowfall are only available from December 1st until January 10th.
|
|
30
|
+
|
|
31
|
+
## Setup
|
|
32
|
+
|
|
33
|
+
Install via the bundled [Plugin Manager](https://docs.octoprint.org/en/main/bundledplugins/pluginmanager.html)
|
|
34
|
+
or manually using this URL:
|
|
35
|
+
|
|
36
|
+
https://github.com/OctoPrint/OctoPrint-Wrapped/archive/main.zip
|
|
37
|
+
|
|
38
|
+
## Configuration
|
|
39
|
+
|
|
40
|
+
The plugin does not have any configuration options.
|
|
41
|
+
|
|
42
|
+
## Acknowledgements
|
|
43
|
+
|
|
44
|
+
The snowfall is implemented with a slightly customized version of [pure-snow.js](https://github.com/hyperstown/pure-snow.js),
|
|
45
|
+
released under the MIT license and bundled here under `octoprint_wrapped/static/pure-snow`.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MANIFEST.in
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
setup.py
|
|
5
|
+
OctoPrint_Wrapped.egg-info/PKG-INFO
|
|
6
|
+
OctoPrint_Wrapped.egg-info/SOURCES.txt
|
|
7
|
+
OctoPrint_Wrapped.egg-info/dependency_links.txt
|
|
8
|
+
OctoPrint_Wrapped.egg-info/entry_points.txt
|
|
9
|
+
OctoPrint_Wrapped.egg-info/requires.txt
|
|
10
|
+
OctoPrint_Wrapped.egg-info/top_level.txt
|
|
11
|
+
octoprint_wrapped/__init__.py
|
|
12
|
+
octoprint_wrapped/static/clientjs/wrapped.js
|
|
13
|
+
octoprint_wrapped/static/js/ko.src.svgtopng.js
|
|
14
|
+
octoprint_wrapped/static/js/wrapped.js
|
|
15
|
+
octoprint_wrapped/static/pure-snow/LICENSE
|
|
16
|
+
octoprint_wrapped/static/pure-snow/pure-snow.css
|
|
17
|
+
octoprint_wrapped/static/pure-snow/pure-snow.js
|
|
18
|
+
octoprint_wrapped/templates/wrapped.svg.jinja2
|
|
19
|
+
octoprint_wrapped/templates/wrapped_about.jinja2
|
|
20
|
+
octoprint_wrapped/templates/wrapped_navbar_snowfall.jinja2
|
|
21
|
+
octoprint_wrapped/templates/wrapped_navbar_wrapped.jinja2
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
octoprint_wrapped
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: OctoPrint-Wrapped
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Get your yearly OctoPrint stats and let it snow! Depends on the Achievements plugin.
|
|
5
|
+
Author-email: Gina Häußge <gina@octoprint.org>
|
|
6
|
+
License: AGPL-3.0-or-later
|
|
7
|
+
Project-URL: Homepage, https://github.com/OctoPrint/OctoPrint-Wrapped
|
|
8
|
+
Requires-Python: <4,>=3.9
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
Provides-Extra: develop
|
|
11
|
+
Requires-Dist: go-task-bin; extra == "develop"
|
|
12
|
+
Dynamic: license
|
|
13
|
+
|
|
14
|
+
# OctoPrint Wrapped! 🎁
|
|
15
|
+
|
|
16
|
+
Get your yearly OctoPrint stats a shareable "wrapped" picture - and let it snow! ❄️
|
|
17
|
+
|
|
18
|
+
<img src="https://raw.githubusercontent.com/OctoPrint/OctoPrint-Wrapped/main/extras/wrapped-demo.png" width="400" height="400" alt="Demo OctoPrint Wrapped share picture" />
|
|
19
|
+
|
|
20
|
+

|
|
21
|
+
|
|
22
|
+
The stats picture depends on the Achievements plugin being enabled (as it takes care of
|
|
23
|
+
the stats collection during the year) and can be opened via the little gift package icon
|
|
24
|
+
in the navbar.
|
|
25
|
+
|
|
26
|
+
The snow effect can always be toggled during the season using the little snowflake icon
|
|
27
|
+
in the navbar, and its setting persists through the browser's local storage.
|
|
28
|
+
|
|
29
|
+
Both wrapped and snowfall are only available from December 1st until January 10th.
|
|
30
|
+
|
|
31
|
+
## Setup
|
|
32
|
+
|
|
33
|
+
Install via the bundled [Plugin Manager](https://docs.octoprint.org/en/main/bundledplugins/pluginmanager.html)
|
|
34
|
+
or manually using this URL:
|
|
35
|
+
|
|
36
|
+
https://github.com/OctoPrint/OctoPrint-Wrapped/archive/main.zip
|
|
37
|
+
|
|
38
|
+
## Configuration
|
|
39
|
+
|
|
40
|
+
The plugin does not have any configuration options.
|
|
41
|
+
|
|
42
|
+
## Acknowledgements
|
|
43
|
+
|
|
44
|
+
The snowfall is implemented with a slightly customized version of [pure-snow.js](https://github.com/hyperstown/pure-snow.js),
|
|
45
|
+
released under the MIT license and bundled here under `octoprint_wrapped/static/pure-snow`.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# OctoPrint Wrapped! 🎁
|
|
2
|
+
|
|
3
|
+
Get your yearly OctoPrint stats a shareable "wrapped" picture - and let it snow! ❄️
|
|
4
|
+
|
|
5
|
+
<img src="https://raw.githubusercontent.com/OctoPrint/OctoPrint-Wrapped/main/extras/wrapped-demo.png" width="400" height="400" alt="Demo OctoPrint Wrapped share picture" />
|
|
6
|
+
|
|
7
|
+

|
|
8
|
+
|
|
9
|
+
The stats picture depends on the Achievements plugin being enabled (as it takes care of
|
|
10
|
+
the stats collection during the year) and can be opened via the little gift package icon
|
|
11
|
+
in the navbar.
|
|
12
|
+
|
|
13
|
+
The snow effect can always be toggled during the season using the little snowflake icon
|
|
14
|
+
in the navbar, and its setting persists through the browser's local storage.
|
|
15
|
+
|
|
16
|
+
Both wrapped and snowfall are only available from December 1st until January 10th.
|
|
17
|
+
|
|
18
|
+
## Setup
|
|
19
|
+
|
|
20
|
+
Install via the bundled [Plugin Manager](https://docs.octoprint.org/en/main/bundledplugins/pluginmanager.html)
|
|
21
|
+
or manually using this URL:
|
|
22
|
+
|
|
23
|
+
https://github.com/OctoPrint/OctoPrint-Wrapped/archive/main.zip
|
|
24
|
+
|
|
25
|
+
## Configuration
|
|
26
|
+
|
|
27
|
+
The plugin does not have any configuration options.
|
|
28
|
+
|
|
29
|
+
## Acknowledgements
|
|
30
|
+
|
|
31
|
+
The snowfall is implemented with a slightly customized version of [pure-snow.js](https://github.com/hyperstown/pure-snow.js),
|
|
32
|
+
released under the MIT license and bundled here under `octoprint_wrapped/static/pure-snow`.
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
import flask
|
|
6
|
+
import octoprint.plugin
|
|
7
|
+
from flask_babel import gettext
|
|
8
|
+
from octoprint.access.permissions import Permissions
|
|
9
|
+
from octoprint.schema import BaseModel
|
|
10
|
+
|
|
11
|
+
WEEKDAYS = [
|
|
12
|
+
"Monday",
|
|
13
|
+
"Tuesday",
|
|
14
|
+
"Wednesday",
|
|
15
|
+
"Thursday",
|
|
16
|
+
"Friday",
|
|
17
|
+
"Saturday",
|
|
18
|
+
"Sunday",
|
|
19
|
+
]
|
|
20
|
+
SECONDS_MINUTE = 60
|
|
21
|
+
SECONDS_HOUR = SECONDS_MINUTE * 60
|
|
22
|
+
SECONDS_DAY = SECONDS_HOUR * 24
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class YearStats(BaseModel):
|
|
26
|
+
year: int
|
|
27
|
+
prints_completed: int
|
|
28
|
+
total_print_duration: str
|
|
29
|
+
longest_print: str
|
|
30
|
+
busiest_weekday: str
|
|
31
|
+
files_uploaded: int
|
|
32
|
+
octoprint_versions: int
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ApiResponse(BaseModel):
|
|
36
|
+
years: list[int]
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class WrappedPlugin(
|
|
40
|
+
octoprint.plugin.AssetPlugin,
|
|
41
|
+
octoprint.plugin.BlueprintPlugin,
|
|
42
|
+
octoprint.plugin.SimpleApiPlugin,
|
|
43
|
+
octoprint.plugin.TemplatePlugin,
|
|
44
|
+
):
|
|
45
|
+
##~~ AssetPlugin mixin
|
|
46
|
+
|
|
47
|
+
def get_assets(self):
|
|
48
|
+
return {
|
|
49
|
+
"clientjs": ["clientjs/wrapped.js"],
|
|
50
|
+
"js": ["js/wrapped.js", "js/ko.src.svgtopng.js"],
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
##~~ BlueprintPlugin mixin
|
|
54
|
+
|
|
55
|
+
def is_blueprint_csrf_protected(self):
|
|
56
|
+
return True
|
|
57
|
+
|
|
58
|
+
def is_protected(self):
|
|
59
|
+
return True
|
|
60
|
+
|
|
61
|
+
@octoprint.plugin.BlueprintPlugin.route("/<int:year>.svg", methods=["GET"])
|
|
62
|
+
def get_svg(self, year):
|
|
63
|
+
if (
|
|
64
|
+
not hasattr(Permissions, "PLUGIN_ACHIEVEMENTS_VIEW")
|
|
65
|
+
or not Permissions.PLUGIN_ACHIEVEMENTS_VIEW.can()
|
|
66
|
+
):
|
|
67
|
+
flask.abort(403)
|
|
68
|
+
|
|
69
|
+
stats = self._get_year_stats(year)
|
|
70
|
+
if stats is None:
|
|
71
|
+
flask.abort(404)
|
|
72
|
+
|
|
73
|
+
response = flask.make_response(
|
|
74
|
+
flask.render_template("wrapped.svg.jinja2", **stats.model_dump(by_alias=True))
|
|
75
|
+
)
|
|
76
|
+
response.headers["Content-Type"] = "image/svg+xml"
|
|
77
|
+
return response
|
|
78
|
+
|
|
79
|
+
##~~ SimpleApiPlugin mixin
|
|
80
|
+
|
|
81
|
+
def is_api_protected(self):
|
|
82
|
+
return True
|
|
83
|
+
|
|
84
|
+
def on_api_get(self, request):
|
|
85
|
+
if (
|
|
86
|
+
not hasattr(Permissions, "PLUGIN_ACHIEVEMENTS_VIEW")
|
|
87
|
+
or not Permissions.PLUGIN_ACHIEVEMENTS_VIEW.can()
|
|
88
|
+
):
|
|
89
|
+
flask.abort(403)
|
|
90
|
+
|
|
91
|
+
response = ApiResponse(years=self._get_available_years())
|
|
92
|
+
return flask.jsonify(response.model_dump(by_alias=True))
|
|
93
|
+
|
|
94
|
+
##~~ Softwareupdate hook
|
|
95
|
+
|
|
96
|
+
def get_update_information(self):
|
|
97
|
+
return {
|
|
98
|
+
"wrapped": {
|
|
99
|
+
"displayName": "OctoPrint Wrapped!",
|
|
100
|
+
"displayVersion": self._plugin_version,
|
|
101
|
+
# version check: github repository
|
|
102
|
+
"type": "github_release",
|
|
103
|
+
"user": "OctoPrint",
|
|
104
|
+
"repo": "OctoPrint-Wrapped",
|
|
105
|
+
"current": self._plugin_version,
|
|
106
|
+
# update method: pip
|
|
107
|
+
"pip": "https://github.com/OctoPrint/OctoPrint-Wrapped/archive/{target_version}.zip",
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
##~~ TemplatePlugin mixin
|
|
112
|
+
|
|
113
|
+
def is_template_autoescaped(self):
|
|
114
|
+
return True
|
|
115
|
+
|
|
116
|
+
def get_template_configs(self):
|
|
117
|
+
return [
|
|
118
|
+
{
|
|
119
|
+
"type": "about",
|
|
120
|
+
"name": gettext("OctoPrint Wrapped!"),
|
|
121
|
+
"template": "wrapped_about.jinja2",
|
|
122
|
+
"custom_bindings": True,
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
"type": "navbar",
|
|
126
|
+
"template": "wrapped_navbar_wrapped.jinja2",
|
|
127
|
+
"custom_bindings": True,
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
"type": "navbar",
|
|
131
|
+
"template": "wrapped_navbar_snowfall.jinja2",
|
|
132
|
+
"custom_bindings": True,
|
|
133
|
+
},
|
|
134
|
+
]
|
|
135
|
+
|
|
136
|
+
##~~ helpers
|
|
137
|
+
|
|
138
|
+
def _get_year_stats_folder(self) -> Optional[str]:
|
|
139
|
+
folder = os.path.join(self.get_plugin_data_folder(), "..", "achievements")
|
|
140
|
+
if not os.path.isdir(folder):
|
|
141
|
+
return None
|
|
142
|
+
return folder
|
|
143
|
+
|
|
144
|
+
def _get_year_stats_file(self, year: int) -> Optional[str]:
|
|
145
|
+
folder = self._get_year_stats_folder()
|
|
146
|
+
if not folder:
|
|
147
|
+
return None
|
|
148
|
+
|
|
149
|
+
year_path = os.path.join(folder, f"{year}.json")
|
|
150
|
+
if not os.path.isfile(year_path):
|
|
151
|
+
return None
|
|
152
|
+
|
|
153
|
+
return year_path
|
|
154
|
+
|
|
155
|
+
def _get_available_years(self) -> list[int]:
|
|
156
|
+
import re
|
|
157
|
+
|
|
158
|
+
stats_folder = self._get_year_stats_folder()
|
|
159
|
+
if not stats_folder:
|
|
160
|
+
return []
|
|
161
|
+
|
|
162
|
+
pattern = re.compile(r"\d{4}.json")
|
|
163
|
+
|
|
164
|
+
years = []
|
|
165
|
+
for entry in os.scandir(stats_folder):
|
|
166
|
+
if not entry.is_file():
|
|
167
|
+
continue
|
|
168
|
+
|
|
169
|
+
if pattern.fullmatch(entry.name):
|
|
170
|
+
year, _ = os.path.splitext(entry.name)
|
|
171
|
+
years.append(int(year))
|
|
172
|
+
|
|
173
|
+
return years
|
|
174
|
+
|
|
175
|
+
def _get_year_stats(self, year: int) -> Optional[YearStats]:
|
|
176
|
+
stats_file = self._get_year_stats_file(year)
|
|
177
|
+
if not stats_file:
|
|
178
|
+
return None
|
|
179
|
+
|
|
180
|
+
try:
|
|
181
|
+
with open(stats_file) as f:
|
|
182
|
+
stats = json.load(f)
|
|
183
|
+
except Exception:
|
|
184
|
+
self._logger.exception(
|
|
185
|
+
f"Error while reading yearly stats for {year} from {stats_file}"
|
|
186
|
+
)
|
|
187
|
+
return None
|
|
188
|
+
|
|
189
|
+
try:
|
|
190
|
+
weekday_stats = stats.get("prints_started_per_weekday", {})
|
|
191
|
+
busiest = None
|
|
192
|
+
for key, value in weekday_stats.items():
|
|
193
|
+
if busiest is None or value > busiest[1]:
|
|
194
|
+
busiest = (key, value)
|
|
195
|
+
|
|
196
|
+
if busiest:
|
|
197
|
+
weekday = WEEKDAYS[int(busiest[0])]
|
|
198
|
+
else:
|
|
199
|
+
weekday = "-"
|
|
200
|
+
|
|
201
|
+
return YearStats(
|
|
202
|
+
year=year,
|
|
203
|
+
prints_completed=stats.get("prints_finished", 0),
|
|
204
|
+
total_print_duration=self._to_duration_days(
|
|
205
|
+
int(stats.get("print_duration_total", 0))
|
|
206
|
+
),
|
|
207
|
+
longest_print=self._to_duration_hours(
|
|
208
|
+
int(stats.get("longest_print_duration", 0))
|
|
209
|
+
),
|
|
210
|
+
busiest_weekday=weekday,
|
|
211
|
+
files_uploaded=int(stats.get("files_uploaded", 0)),
|
|
212
|
+
octoprint_versions=int(stats.get("seen_versions", 1)),
|
|
213
|
+
)
|
|
214
|
+
except Exception:
|
|
215
|
+
self._logger.exception(
|
|
216
|
+
f"Error while parsing yearly stats for {year} from {stats_file}"
|
|
217
|
+
)
|
|
218
|
+
return None
|
|
219
|
+
|
|
220
|
+
def _to_duration_days(self, seconds: int) -> str:
|
|
221
|
+
days = int(seconds / SECONDS_DAY)
|
|
222
|
+
seconds -= days * SECONDS_DAY
|
|
223
|
+
|
|
224
|
+
hours = int(seconds / SECONDS_HOUR)
|
|
225
|
+
seconds -= hours * SECONDS_HOUR
|
|
226
|
+
|
|
227
|
+
minutes = int(seconds / SECONDS_MINUTE)
|
|
228
|
+
seconds -= minutes * SECONDS_MINUTE
|
|
229
|
+
|
|
230
|
+
if days >= 100:
|
|
231
|
+
# strip the minutes to keep things fitting...
|
|
232
|
+
return f"{days}d {hours}h"
|
|
233
|
+
else:
|
|
234
|
+
return f"{days}d {hours}h {minutes}m"
|
|
235
|
+
|
|
236
|
+
def _to_duration_hours(self, seconds: int) -> str:
|
|
237
|
+
hours = int(seconds / SECONDS_HOUR)
|
|
238
|
+
seconds -= hours * SECONDS_HOUR
|
|
239
|
+
|
|
240
|
+
minutes = int(seconds / SECONDS_MINUTE)
|
|
241
|
+
seconds -= minutes * SECONDS_MINUTE
|
|
242
|
+
|
|
243
|
+
return f"{hours}h {minutes}m"
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
__plugin_name__ = "OctoPrint Wrapped!"
|
|
247
|
+
__plugin_pythoncompat__ = ">=3.9,<4" # Only Python 3
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def __plugin_load__():
|
|
251
|
+
global __plugin_implementation__
|
|
252
|
+
__plugin_implementation__ = WrappedPlugin()
|
|
253
|
+
|
|
254
|
+
global __plugin_hooks__
|
|
255
|
+
__plugin_hooks__ = {
|
|
256
|
+
"octoprint.plugin.softwareupdate.check_config": __plugin_implementation__.get_update_information,
|
|
257
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
(function (global, factory) {
|
|
2
|
+
if (typeof define === "function" && define.amd) {
|
|
3
|
+
define(["OctoPrintClient"], factory);
|
|
4
|
+
} else {
|
|
5
|
+
factory(global.OctoPrintClient);
|
|
6
|
+
}
|
|
7
|
+
})(this, function (OctoPrintClient) {
|
|
8
|
+
var OctoPrintWrappedClient = function (base) {
|
|
9
|
+
this.base = base;
|
|
10
|
+
|
|
11
|
+
this.baseUrl = this.base.getBlueprintUrl("wrapped");
|
|
12
|
+
this.apiUrl = this.base.getSimpleApiUrl("wrapped");
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
OctoPrintWrappedClient.prototype.get = function (opts) {
|
|
16
|
+
return this.base.get(this.apiUrl, opts);
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
OctoPrintWrappedClient.prototype.getYearSvgUrl = function (year, opts) {
|
|
20
|
+
return this.baseUrl + year + ".svg";
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// register plugin component
|
|
24
|
+
OctoPrintClient.registerPluginComponent("wrapped", OctoPrintWrappedClient);
|
|
25
|
+
|
|
26
|
+
return OctoPrintWrappedClient;
|
|
27
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
ko.bindingHandlers["src.svgtopng"] = {
|
|
2
|
+
init: (element, valueAccessor, allBindingsAccessor, viewModel, bindingContext) => {
|
|
3
|
+
const value = valueAccessor();
|
|
4
|
+
const valueUnwrapped = ko.unwrap(value);
|
|
5
|
+
if (!valueUnwrapped) return;
|
|
6
|
+
|
|
7
|
+
const $element = $(element);
|
|
8
|
+
|
|
9
|
+
$.get(valueUnwrapped).done((data, textStatus, xhr) => {
|
|
10
|
+
const svgString = xhr.responseText;
|
|
11
|
+
|
|
12
|
+
const svg = new Blob([svgString], {
|
|
13
|
+
type: "image/svg+xml;charset=utf-8"
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const url = URL.createObjectURL(svg);
|
|
17
|
+
|
|
18
|
+
const img = new Image();
|
|
19
|
+
img.onload = () => {
|
|
20
|
+
URL.revokeObjectURL(url);
|
|
21
|
+
|
|
22
|
+
const canvas = document.createElement("canvas");
|
|
23
|
+
canvas.width = img.width;
|
|
24
|
+
canvas.height = img.height;
|
|
25
|
+
|
|
26
|
+
const ctx = canvas.getContext("2d");
|
|
27
|
+
ctx.drawImage(img, 0, 0);
|
|
28
|
+
|
|
29
|
+
const png = canvas.toDataURL("image/png");
|
|
30
|
+
$element.attr("src", png);
|
|
31
|
+
};
|
|
32
|
+
img.src = url;
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
ko.bindingHandlers["src.svgtopng"].update = ko.bindingHandlers["src.svgtopng"].init;
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* View model for OctoPrint-Wrapped
|
|
3
|
+
*
|
|
4
|
+
* Author: Gina Häußge
|
|
5
|
+
* License: AGPL-3.0-or-later
|
|
6
|
+
*/
|
|
7
|
+
$(function () {
|
|
8
|
+
const FLAKES = 50;
|
|
9
|
+
const MIN_DURATION = 5;
|
|
10
|
+
const MAX_DURATION = 15;
|
|
11
|
+
|
|
12
|
+
const MONTH_DECEMBER = 11;
|
|
13
|
+
const MONTH_JANUARY = 0;
|
|
14
|
+
|
|
15
|
+
const SNOWFALL_LOCAL_STORAGE_KEY = "plugin.wrapped.snowfall";
|
|
16
|
+
const snowfallToLocalStorage = (value) => {
|
|
17
|
+
saveToLocalStorage(SNOWFALL_LOCAL_STORAGE_KEY, {
|
|
18
|
+
enabled: value
|
|
19
|
+
});
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const snowfallFromLocalStorage = () => {
|
|
23
|
+
const data = loadFromLocalStorage(SNOWFALL_LOCAL_STORAGE_KEY);
|
|
24
|
+
if (data["enabled"] !== undefined) return !!data["enabled"];
|
|
25
|
+
return false;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
function WrappedViewModel(parameters) {
|
|
29
|
+
const self = this;
|
|
30
|
+
|
|
31
|
+
self.loginState = parameters[0];
|
|
32
|
+
self.access = parameters[1];
|
|
33
|
+
self.aboutVM = parameters[2];
|
|
34
|
+
|
|
35
|
+
self.availableYears = ko.observableArray([]);
|
|
36
|
+
|
|
37
|
+
self.snowfallEnabled = false;
|
|
38
|
+
self.snowfallContainer = undefined;
|
|
39
|
+
|
|
40
|
+
self.currentWrapped = ko.pureComputed(() => {
|
|
41
|
+
if (!self.withinWrappedSeason()) return false;
|
|
42
|
+
|
|
43
|
+
const now = new Date();
|
|
44
|
+
const year =
|
|
45
|
+
now.getMonth() == MONTH_DECEMBER
|
|
46
|
+
? now.getFullYear() // still in December
|
|
47
|
+
: now.getFullYear() - 1; // already January
|
|
48
|
+
const years = self.availableYears();
|
|
49
|
+
|
|
50
|
+
if (years.indexOf(year) !== -1 && self.withinWrappedSeason()) {
|
|
51
|
+
return year;
|
|
52
|
+
} else {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
self.currentWrappedAvailable = ko.pureComputed(() => {
|
|
58
|
+
return self.currentWrapped() !== false;
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
self.currentSvgUrl = ko.pureComputed(() => {
|
|
62
|
+
const year = self.currentWrapped();
|
|
63
|
+
if (!year) return false;
|
|
64
|
+
|
|
65
|
+
return OctoPrint.plugins.wrapped.getYearSvgUrl(year);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
self.withinWrappedSeason = ko.pureComputed(() => {
|
|
69
|
+
// wrapped Season = Dec 1st until January 10th
|
|
70
|
+
const now = new Date();
|
|
71
|
+
return (
|
|
72
|
+
now.getMonth() == MONTH_DECEMBER ||
|
|
73
|
+
(now.getMonth() == MONTH_JANUARY && now.getDate() < 10)
|
|
74
|
+
);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
self.requestData = () => {
|
|
78
|
+
if (
|
|
79
|
+
!self.loginState.hasPermission(
|
|
80
|
+
self.access.permissions.PLUGIN_ACHIEVEMENTS_VIEW
|
|
81
|
+
)
|
|
82
|
+
) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
OctoPrint.plugins.wrapped.get().done(self.fromResponse);
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
self.fromResponse = (response) => {
|
|
89
|
+
self.availableYears(response.years);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
self.showWrapped = () => {
|
|
93
|
+
self.aboutVM.show("about_plugin_wrapped");
|
|
94
|
+
return false;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
self.toggleSnowfall = () => {
|
|
98
|
+
self.snowfallEnabled = !self.snowfallEnabled;
|
|
99
|
+
snowfallToLocalStorage(self.snowfallEnabled);
|
|
100
|
+
self.updateSnowfall();
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
self.updateSnowfall = () => {
|
|
104
|
+
let container = document.getElementById("snow");
|
|
105
|
+
|
|
106
|
+
const body = document.getElementsByTagName("body")[0];
|
|
107
|
+
const head = document.getElementsByTagName("head")[0];
|
|
108
|
+
|
|
109
|
+
if (!self.snowfallEnabled) {
|
|
110
|
+
if (typeof showSnow !== "undefined") showSnow(false);
|
|
111
|
+
} else if (self.snowfallEnabled) {
|
|
112
|
+
if (!self.withinWrappedSeason()) return;
|
|
113
|
+
|
|
114
|
+
if (!container) {
|
|
115
|
+
container = document.createElement("div");
|
|
116
|
+
container.id = "snow";
|
|
117
|
+
container.dataset.count = FLAKES;
|
|
118
|
+
container.dataset.durmin = MIN_DURATION;
|
|
119
|
+
container.dataset.durmax = MAX_DURATION;
|
|
120
|
+
body.insertBefore(container, body.firstChild);
|
|
121
|
+
|
|
122
|
+
const styleSnow = document.createElement("link");
|
|
123
|
+
styleSnow.href =
|
|
124
|
+
BASEURL + "plugin/wrapped/static/pure-snow/pure-snow.css";
|
|
125
|
+
styleSnow.rel = "stylesheet";
|
|
126
|
+
head.appendChild(styleSnow);
|
|
127
|
+
|
|
128
|
+
const scriptSnow = document.createElement("script");
|
|
129
|
+
scriptSnow.src =
|
|
130
|
+
BASEURL + "plugin/wrapped/static/pure-snow/pure-snow.js";
|
|
131
|
+
scriptSnow.defer = true;
|
|
132
|
+
scriptSnow.onload = () => {
|
|
133
|
+
setTimeout(() => {
|
|
134
|
+
if (
|
|
135
|
+
typeof createSnow === "undefined" ||
|
|
136
|
+
typeof showSnow === "undefined"
|
|
137
|
+
)
|
|
138
|
+
return;
|
|
139
|
+
createSnow();
|
|
140
|
+
showSnow(true);
|
|
141
|
+
}, 500);
|
|
142
|
+
};
|
|
143
|
+
head.appendChild(scriptSnow);
|
|
144
|
+
} else {
|
|
145
|
+
if (typeof showSnow !== "undefined") showSnow(true);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
self.onUserPermissionsChanged =
|
|
151
|
+
self.onUserLoggedIn =
|
|
152
|
+
self.onUserLoggedOut =
|
|
153
|
+
(user) => {
|
|
154
|
+
if (
|
|
155
|
+
self.loginState.hasPermission(
|
|
156
|
+
self.access.permissions.PLUGIN_ACHIEVEMENTS_VIEW
|
|
157
|
+
)
|
|
158
|
+
) {
|
|
159
|
+
self.requestData();
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
self.onStartup = () => {
|
|
164
|
+
self.snowfallEnabled = snowfallFromLocalStorage();
|
|
165
|
+
self.updateSnowfall();
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
OCTOPRINT_VIEWMODELS.push({
|
|
170
|
+
construct: WrappedViewModel,
|
|
171
|
+
dependencies: ["loginStateViewModel", "accessViewModel", "aboutViewModel"],
|
|
172
|
+
elements: [
|
|
173
|
+
"#about_plugin_wrapped",
|
|
174
|
+
"#navbar_plugin_wrapped",
|
|
175
|
+
"#navbar_plugin_wrapped_2"
|
|
176
|
+
]
|
|
177
|
+
});
|
|
178
|
+
});
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2020-present, hyperstown (github.com/hyperstown/)
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|