OctoPrint-Wrapped 1.0.0__py3-none-any.whl
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/__init__.py +257 -0
- octoprint_wrapped/static/clientjs/wrapped.js +27 -0
- octoprint_wrapped/static/js/ko.src.svgtopng.js +36 -0
- octoprint_wrapped/static/js/wrapped.js +178 -0
- octoprint_wrapped/static/pure-snow/LICENSE +21 -0
- octoprint_wrapped/static/pure-snow/pure-snow.css +10 -0
- octoprint_wrapped/static/pure-snow/pure-snow.js +146 -0
- octoprint_wrapped/templates/wrapped.svg.jinja2 +1258 -0
- octoprint_wrapped/templates/wrapped_about.jinja2 +24 -0
- octoprint_wrapped/templates/wrapped_navbar_snowfall.jinja2 +7 -0
- octoprint_wrapped/templates/wrapped_navbar_wrapped.jinja2 +7 -0
- octoprint_wrapped-1.0.0.dist-info/METADATA +45 -0
- octoprint_wrapped-1.0.0.dist-info/RECORD +16 -0
- octoprint_wrapped-1.0.0.dist-info/WHEEL +5 -0
- octoprint_wrapped-1.0.0.dist-info/entry_points.txt +2 -0
- octoprint_wrapped-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -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.
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
let snowflakesCount = 200; // Snowflake count, can be overwritten by attrs
|
|
2
|
+
let minDuration = 10; // can be overwritten by attrs
|
|
3
|
+
let maxDuration = 30; // can be overwritten by attrs
|
|
4
|
+
let baseCSS = ``;
|
|
5
|
+
|
|
6
|
+
// set global attributes
|
|
7
|
+
if (typeof SNOWFLAKES_COUNT !== "undefined") {
|
|
8
|
+
snowflakesCount = SNOWFLAKES_COUNT;
|
|
9
|
+
}
|
|
10
|
+
if (typeof SNOWFLAKES_MIN_DURATION !== "undefined") {
|
|
11
|
+
minDuration = SNOWFLAKES_MIN_DURATION;
|
|
12
|
+
}
|
|
13
|
+
if (typeof SNOWFLAKES_MAX_DURATION !== "undefined") {
|
|
14
|
+
maxDuration = SNOWFLAKES_MAX_DURATION;
|
|
15
|
+
}
|
|
16
|
+
if (typeof BASE_CSS !== "undefined") {
|
|
17
|
+
baseCSS = BASE_CSS;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
let bodyHeightPx = null;
|
|
21
|
+
let pageHeightVh = null;
|
|
22
|
+
|
|
23
|
+
function setHeightVariables() {
|
|
24
|
+
bodyHeightPx = document.body.offsetHeight;
|
|
25
|
+
pageHeightVh = Math.max((100 * bodyHeightPx) / window.innerHeight, 100.0);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// get params set in snow div
|
|
29
|
+
function getSnowAttributes() {
|
|
30
|
+
const snowWrapper = document.getElementById("snow");
|
|
31
|
+
snowflakesCount = Number(snowWrapper?.dataset?.count || snowflakesCount);
|
|
32
|
+
minDuration = Number(snowWrapper?.dataset?.durmin || minDuration);
|
|
33
|
+
maxDuration = Number(snowWrapper?.dataset?.durmax || maxDuration);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// This function allows you to turn on and off the snow
|
|
37
|
+
function showSnow(value) {
|
|
38
|
+
if (value) {
|
|
39
|
+
document.getElementById("snow").style.display = "block";
|
|
40
|
+
} else {
|
|
41
|
+
document.getElementById("snow").style.display = "none";
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Creating snowflakes
|
|
46
|
+
function generateSnow(snowDensity = 200) {
|
|
47
|
+
snowDensity -= 1;
|
|
48
|
+
const snowWrapper = document.getElementById("snow");
|
|
49
|
+
snowWrapper.innerHTML = "";
|
|
50
|
+
for (let i = 0; i < snowDensity; i++) {
|
|
51
|
+
let board = document.createElement("div");
|
|
52
|
+
board.className = "snowflake";
|
|
53
|
+
snowWrapper.appendChild(board);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function getOrCreateCSSElement() {
|
|
58
|
+
let cssElement = document.getElementById("psjs-css");
|
|
59
|
+
if (cssElement) return cssElement;
|
|
60
|
+
|
|
61
|
+
cssElement = document.createElement("style");
|
|
62
|
+
cssElement.id = "psjs-css";
|
|
63
|
+
document.head.appendChild(cssElement);
|
|
64
|
+
return cssElement;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Append style for each snowflake to the head
|
|
68
|
+
function addCSS(rule) {
|
|
69
|
+
const cssElement = getOrCreateCSSElement();
|
|
70
|
+
cssElement.innerHTML = rule; // safe to use innerHTML
|
|
71
|
+
document.head.appendChild(cssElement);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Math
|
|
75
|
+
function randomInt(value = 100) {
|
|
76
|
+
return Math.floor(Math.random() * value) + 1;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function randomIntRange(min, max) {
|
|
80
|
+
min = Math.ceil(min);
|
|
81
|
+
max = Math.floor(max);
|
|
82
|
+
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function getRandomArbitrary(min, max) {
|
|
86
|
+
return Math.random() * (max - min) + min;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Create style for snowflake
|
|
90
|
+
function generateSnowCSS(snowDensity = 200) {
|
|
91
|
+
let snowflakeName = "snowflake";
|
|
92
|
+
let rule = baseCSS;
|
|
93
|
+
|
|
94
|
+
for (let i = 1; i < snowDensity; i++) {
|
|
95
|
+
let randomX = Math.random() * 100; // vw
|
|
96
|
+
let randomOffset = Math.random() * 10; // vw;
|
|
97
|
+
let randomXEnd = randomX + randomOffset;
|
|
98
|
+
let randomXEndYoyo = randomX + randomOffset / 2;
|
|
99
|
+
let randomYoyoTime = getRandomArbitrary(0.3, 0.8);
|
|
100
|
+
let randomYoyoY = randomYoyoTime * pageHeightVh; // vh
|
|
101
|
+
let randomScale = Math.random();
|
|
102
|
+
let fallDuration = Math.min(
|
|
103
|
+
randomIntRange(minDuration, (pageHeightVh / 10) * 3),
|
|
104
|
+
maxDuration
|
|
105
|
+
); // s
|
|
106
|
+
let fallDelay = randomInt((pageHeightVh / 10) * 3) * -1; // s
|
|
107
|
+
let opacity = Math.random();
|
|
108
|
+
|
|
109
|
+
rule += `
|
|
110
|
+
.${snowflakeName}:nth-child(${i}) {
|
|
111
|
+
opacity: ${opacity};
|
|
112
|
+
transform: translate(${randomX}vw, -10px) scale(${randomScale});
|
|
113
|
+
animation: fall-${i} ${fallDuration}s ${fallDelay}s linear infinite;
|
|
114
|
+
}
|
|
115
|
+
@keyframes fall-${i} {
|
|
116
|
+
${randomYoyoTime * 100}% {
|
|
117
|
+
transform: translate(${randomXEnd}vw, ${randomYoyoY}vh) scale(${randomScale});
|
|
118
|
+
}
|
|
119
|
+
to {
|
|
120
|
+
transform: translate(${randomXEndYoyo}vw, ${pageHeightVh}vh) scale(${randomScale});
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
`;
|
|
124
|
+
}
|
|
125
|
+
addCSS(rule);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Load the rules and execute after the DOM loads
|
|
129
|
+
function createSnow() {
|
|
130
|
+
setHeightVariables();
|
|
131
|
+
getSnowAttributes();
|
|
132
|
+
generateSnowCSS(snowflakesCount);
|
|
133
|
+
generateSnow(snowflakesCount);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
window.addEventListener("resize", createSnow);
|
|
137
|
+
|
|
138
|
+
// export createSnow function if using node or CommonJS environment
|
|
139
|
+
if (typeof module !== "undefined") {
|
|
140
|
+
module.exports = {
|
|
141
|
+
createSnow,
|
|
142
|
+
showSnow
|
|
143
|
+
};
|
|
144
|
+
} else {
|
|
145
|
+
window.onload = createSnow;
|
|
146
|
+
}
|