@qtoggle/qui 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.eslintignore +2 -0
- package/.eslintrc.json +492 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +33 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +23 -0
- package/.github/ISSUE_TEMPLATE/improvement_proposal.md +20 -0
- package/.github/workflows/main.yml +74 -0
- package/.pre-commit-config.yaml +8 -0
- package/LICENSE.txt +177 -0
- package/README.md +4 -0
- package/font/dejavusans-bold.woff +0 -0
- package/font/dejavusans-bolditalic.woff +0 -0
- package/font/dejavusans-italic.woff +0 -0
- package/font/dejavusans-regular.woff +0 -0
- package/img/qui-icons.svg +1937 -0
- package/js/base/base.js +47 -0
- package/js/base/condition-variable.js +92 -0
- package/js/base/errors.js +36 -0
- package/js/base/i18n.js +20 -0
- package/js/base/mixwith.js +135 -0
- package/js/base/require-js-compat.js +78 -0
- package/js/base/signal.js +91 -0
- package/js/base/singleton.js +66 -0
- package/js/base/timer.js +126 -0
- package/js/config.js +184 -0
- package/js/forms/common-fields/check-field.js +42 -0
- package/js/forms/common-fields/choice-buttons-field.js +30 -0
- package/js/forms/common-fields/color-combo-field.js +37 -0
- package/js/forms/common-fields/combo-field.js +108 -0
- package/js/forms/common-fields/common-fields.js +23 -0
- package/js/forms/common-fields/composite-field.js +132 -0
- package/js/forms/common-fields/custom-html-field.js +51 -0
- package/js/forms/common-fields/email-field.js +30 -0
- package/js/forms/common-fields/file-picker-field.js +46 -0
- package/js/forms/common-fields/jquery-ui-field.js +111 -0
- package/js/forms/common-fields/labels-field.js +69 -0
- package/js/forms/common-fields/numeric-field.js +39 -0
- package/js/forms/common-fields/password-field.js +28 -0
- package/js/forms/common-fields/phone-field.js +26 -0
- package/js/forms/common-fields/progress-disk-field.js +69 -0
- package/js/forms/common-fields/push-button-field.js +138 -0
- package/js/forms/common-fields/slider-field.js +51 -0
- package/js/forms/common-fields/text-area-field.js +34 -0
- package/js/forms/common-fields/text-field.js +89 -0
- package/js/forms/common-fields/up-down-field.js +85 -0
- package/js/forms/common-forms/common-forms.js +16 -0
- package/js/forms/common-forms/options-form.js +77 -0
- package/js/forms/common-forms/page-form.js +115 -0
- package/js/forms/form-button.js +202 -0
- package/js/forms/form-field.js +1183 -0
- package/js/forms/form.js +1181 -0
- package/js/forms/forms.js +68 -0
- package/js/global-glass.js +100 -0
- package/js/icons/default-stock.js +173 -0
- package/js/icons/icon.js +64 -0
- package/js/icons/icons.js +16 -0
- package/js/icons/multi-state-sprites-icon.js +362 -0
- package/js/icons/stock-icon.js +219 -0
- package/js/icons/stock.js +98 -0
- package/js/icons/stocks.js +57 -0
- package/js/index.js +232 -0
- package/js/lib/jquery.longpress.js +79 -0
- package/js/lib/jquery.module.js +4 -0
- package/js/lib/logger.module.js +4 -0
- package/js/lib/pep.module.js +4 -0
- package/js/lists/common-items/common-items.js +5 -0
- package/js/lists/common-items/icon-label-list-item.js +86 -0
- package/js/lists/common-lists/common-lists.js +5 -0
- package/js/lists/common-lists/page-list.js +53 -0
- package/js/lists/list-item.js +147 -0
- package/js/lists/list.js +636 -0
- package/js/lists/lists.js +26 -0
- package/js/main-ui/main-ui.js +64 -0
- package/js/main-ui/menu-bar.js +144 -0
- package/js/main-ui/options-bar.js +181 -0
- package/js/main-ui/status.js +185 -0
- package/js/main-ui/top-bar.js +59 -0
- package/js/messages/common-message-forms/common-message-forms.js +7 -0
- package/js/messages/common-message-forms/confirm-message-form.js +81 -0
- package/js/messages/common-message-forms/simple-message-form.js +67 -0
- package/js/messages/common-message-forms/sticky-simple-message-form.js +27 -0
- package/js/messages/message-form.js +107 -0
- package/js/messages/messages.js +21 -0
- package/js/messages/sticky-modal-page.js +98 -0
- package/js/messages/sticky-modal-progress-message.js +27 -0
- package/js/messages/toast.js +164 -0
- package/js/navigation.js +654 -0
- package/js/pages/breadcrumbs.js +124 -0
- package/js/pages/common-pages/common-pages.js +6 -0
- package/js/pages/common-pages/modal-progress-page.js +83 -0
- package/js/pages/common-pages/structured-page.js +46 -0
- package/js/pages/page.js +1018 -0
- package/js/pages/pages-context.js +154 -0
- package/js/pages/pages.js +252 -0
- package/js/pwa.js +337 -0
- package/js/sections/section.js +612 -0
- package/js/sections/sections.js +300 -0
- package/js/tables/common-cells/common-cells.js +7 -0
- package/js/tables/common-cells/icon-label-table-cell.js +68 -0
- package/js/tables/common-cells/push-button-table-cell.js +133 -0
- package/js/tables/common-cells/simple-table-cell.js +37 -0
- package/js/tables/common-tables/common-tables.js +5 -0
- package/js/tables/common-tables/page-table.js +55 -0
- package/js/tables/table-cell.js +198 -0
- package/js/tables/table-row.js +126 -0
- package/js/tables/table.js +492 -0
- package/js/tables/tables.js +36 -0
- package/js/theme.js +304 -0
- package/js/utils/ajax.js +126 -0
- package/js/utils/array.js +194 -0
- package/js/utils/colors.js +445 -0
- package/js/utils/cookies.js +65 -0
- package/js/utils/crypto.js +439 -0
- package/js/utils/css.js +234 -0
- package/js/utils/date.js +300 -0
- package/js/utils/files.js +27 -0
- package/js/utils/gestures.js +165 -0
- package/js/utils/html.js +76 -0
- package/js/utils/misc.js +81 -0
- package/js/utils/object.js +324 -0
- package/js/utils/promise.js +49 -0
- package/js/utils/string.js +192 -0
- package/js/utils/url.js +187 -0
- package/js/utils/utils.js +3 -0
- package/js/utils/visibility-manager.js +211 -0
- package/js/views/common-views/common-views.js +7 -0
- package/js/views/common-views/icon-label-view.js +210 -0
- package/js/views/common-views/progress-view.js +89 -0
- package/js/views/common-views/structured-view.js +368 -0
- package/js/views/view.js +467 -0
- package/js/views/views.js +3 -0
- package/js/widgets/base-widget.js +23 -0
- package/js/widgets/common-widgets/check-button.js +109 -0
- package/js/widgets/common-widgets/choice-buttons.js +322 -0
- package/js/widgets/common-widgets/color-combo.js +104 -0
- package/js/widgets/common-widgets/combo.js +645 -0
- package/js/widgets/common-widgets/common-widgets.js +17 -0
- package/js/widgets/common-widgets/email-input.js +7 -0
- package/js/widgets/common-widgets/file-picker.js +133 -0
- package/js/widgets/common-widgets/labels.js +132 -0
- package/js/widgets/common-widgets/numeric-input.js +49 -0
- package/js/widgets/common-widgets/password-input.js +91 -0
- package/js/widgets/common-widgets/phone-input.js +7 -0
- package/js/widgets/common-widgets/progress-disk.js +174 -0
- package/js/widgets/common-widgets/push-button.js +155 -0
- package/js/widgets/common-widgets/slider.js +455 -0
- package/js/widgets/common-widgets/text-area.js +52 -0
- package/js/widgets/common-widgets/text-input.js +174 -0
- package/js/widgets/common-widgets/up-down.js +351 -0
- package/js/widgets/widgets.js +57 -0
- package/js/window.js +557 -0
- package/jsdoc.conf.json +20 -0
- package/less/base.less +123 -0
- package/less/forms/common-fields.less +101 -0
- package/less/forms/common-forms.less +5 -0
- package/less/forms/form-button.less +21 -0
- package/less/forms/form-field.less +266 -0
- package/less/forms/form.less +131 -0
- package/less/global-glass.less +64 -0
- package/less/icon-label-view.less +82 -0
- package/less/icons.less +144 -0
- package/less/lists.less +105 -0
- package/less/main-ui.less +328 -0
- package/less/messages.less +189 -0
- package/less/no-effects.less +24 -0
- package/less/pages/breadcrumbs.less +98 -0
- package/less/pages/common-pages.less +36 -0
- package/less/pages/page.less +70 -0
- package/less/progress-view.less +51 -0
- package/less/stock-icons.less +43 -0
- package/less/structured-view.less +245 -0
- package/less/tables.less +84 -0
- package/less/theme-dark.less +133 -0
- package/less/theme-light.less +132 -0
- package/less/theme.less +419 -0
- package/less/visibility-manager.less +11 -0
- package/less/widgets/check-button.less +96 -0
- package/less/widgets/choice-buttons.less +160 -0
- package/less/widgets/color-combo.less +33 -0
- package/less/widgets/combo.less +230 -0
- package/less/widgets/common-buttons.less +120 -0
- package/less/widgets/common.less +24 -0
- package/less/widgets/input.less +258 -0
- package/less/widgets/labels.less +81 -0
- package/less/widgets/progress-disk.less +70 -0
- package/less/widgets/slider.less +199 -0
- package/less/widgets/updown.less +115 -0
- package/less/widgets/various.less +36 -0
- package/package.json +47 -0
- package/pyproject.toml +45 -0
- package/qui/__init__.py +110 -0
- package/qui/constants.py +1 -0
- package/qui/exceptions.py +2 -0
- package/qui/j2template.py +71 -0
- package/qui/settings.py +60 -0
- package/qui/templates/manifest.json +25 -0
- package/qui/templates/qui.html +126 -0
- package/qui/templates/service-worker.js +188 -0
- package/qui/web/__init__.py +0 -0
- package/qui/web/tornado.py +220 -0
- package/scripts/postinstall.sh +10 -0
- package/webpack/webpack-adjust-css-urls-loader.js +36 -0
- package/webpack/webpack-common.js +384 -0
package/qui/settings.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from tornado.httpserver import HTTPRequest
|
|
6
|
+
|
|
7
|
+
from qui import constants, exceptions
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
DEFAULT_THEME_COLOR = "#62abea"
|
|
11
|
+
DEFAULT_BACKGROUND_COLOR = "#444444"
|
|
12
|
+
|
|
13
|
+
DEFAULT_FRONTEND_DIR = "frontend"
|
|
14
|
+
DEFAULT_FRONTEND_URL_PREFIX = "frontend"
|
|
15
|
+
|
|
16
|
+
DEFAULT_STATIC_URL = "{frontend_url_prefix}/static"
|
|
17
|
+
|
|
18
|
+
logger = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
name: str = ""
|
|
21
|
+
display_name: str = ""
|
|
22
|
+
display_short_name: str = ""
|
|
23
|
+
description: str = ""
|
|
24
|
+
version: str = ""
|
|
25
|
+
debug: bool = False
|
|
26
|
+
theme_color: str = DEFAULT_THEME_COLOR
|
|
27
|
+
background_color: str = DEFAULT_BACKGROUND_COLOR
|
|
28
|
+
frontend_dir: str = DEFAULT_FRONTEND_DIR
|
|
29
|
+
frontend_url_prefix: str = DEFAULT_FRONTEND_URL_PREFIX
|
|
30
|
+
static_url: str = DEFAULT_STATIC_URL
|
|
31
|
+
package_name: str = ""
|
|
32
|
+
enable_pwa: bool = True
|
|
33
|
+
extra_context: dict[str, Any] = {}
|
|
34
|
+
build_hash = None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def make_context(request: HTTPRequest) -> dict[str, Any]:
|
|
38
|
+
if not name:
|
|
39
|
+
raise exceptions.QUIException("QUI not configured")
|
|
40
|
+
|
|
41
|
+
base_prefix = request.headers.get(constants.BASE_PREFIX_HEADER, "/")
|
|
42
|
+
if not base_prefix.endswith("/"):
|
|
43
|
+
base_prefix += "/"
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
"name": name,
|
|
47
|
+
"display_name": display_name,
|
|
48
|
+
"display_short_name": display_short_name,
|
|
49
|
+
"description": description,
|
|
50
|
+
"version": version,
|
|
51
|
+
"debug": debug,
|
|
52
|
+
"theme_color": theme_color,
|
|
53
|
+
"background_color": background_color,
|
|
54
|
+
"navigation_base_prefix": f"{base_prefix}{frontend_url_prefix}",
|
|
55
|
+
"static_url": static_url,
|
|
56
|
+
"enable_pwa": enable_pwa,
|
|
57
|
+
"themes": ["dark", "light"],
|
|
58
|
+
"build_hash": build_hash,
|
|
59
|
+
**extra_context,
|
|
60
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"lang": "en-US",
|
|
3
|
+
{% if display_short_name or display_name -%}"short_name": "{{ display_short_name or display_name }}",{%- endif %}
|
|
4
|
+
{% if display_name or display_short_name -%}"name": "{{ display_name or display_short_name }}",{%- endif %}
|
|
5
|
+
{% if description -%}"description": "{{ description }}",{%- endif %}
|
|
6
|
+
{% if version -%}"version": "{{ version }}",{%- endif %}
|
|
7
|
+
{% if theme_color -%}"theme_color": "{{ theme_color }}",{%- endif %}
|
|
8
|
+
{% if background_color -%}"background_color": "{{ background_color }}",{%- endif %}
|
|
9
|
+
"icons": [
|
|
10
|
+
{"src": "{{ static_url }}/img/launcher-icon-16.png", "sizes": "16x16", "type": "image/png"},
|
|
11
|
+
{"src": "{{ static_url }}/img/launcher-icon-32.png", "sizes": "32x32", "type": "image/png"},
|
|
12
|
+
{"src": "{{ static_url }}/img/launcher-icon-36.png", "sizes": "36x36", "type": "image/png"},
|
|
13
|
+
{"src": "{{ static_url }}/img/launcher-icon-48.png", "sizes": "48x48", "type": "image/png"},
|
|
14
|
+
{"src": "{{ static_url }}/img/launcher-icon-64.png", "sizes": "64x64", "type": "image/png"},
|
|
15
|
+
{"src": "{{ static_url }}/img/launcher-icon-72.png", "sizes": "72x72", "type": "image/png"},
|
|
16
|
+
{"src": "{{ static_url }}/img/launcher-icon-96.png", "sizes": "96x96", "type": "image/png"},
|
|
17
|
+
{"src": "{{ static_url }}/img/launcher-icon-144.png", "sizes": "144x144", "type": "image/png"},
|
|
18
|
+
{"src": "{{ static_url }}/img/launcher-icon-192.png", "sizes": "192x192", "type": "image/png"},
|
|
19
|
+
{"src": "{{ static_url }}/img/launcher-icon-256.png", "sizes": "256x256", "type": "image/png"},
|
|
20
|
+
{"src": "{{ static_url }}/img/launcher-icon-384.png", "sizes": "384x384", "type": "image/png"},
|
|
21
|
+
{"src": "{{ static_url }}/img/launcher-icon-512.png", "sizes": "512x512", "type": "image/png"}
|
|
22
|
+
],
|
|
23
|
+
"start_url": ".",
|
|
24
|
+
"display": "standalone"
|
|
25
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
|
|
2
|
+
{% macro css_devel_file(theme, path) -%}
|
|
3
|
+
<link rel="stylesheet"
|
|
4
|
+
type="text/css"
|
|
5
|
+
href="{{ static_url }}/css/{{ theme }}/{{ path }}?h={{ build_hash }}"
|
|
6
|
+
disabled
|
|
7
|
+
theme="{{ theme }}"/>
|
|
8
|
+
{%- endmacro %}
|
|
9
|
+
|
|
10
|
+
{% macro main_script_file(params={}) -%}
|
|
11
|
+
<script type="module"
|
|
12
|
+
src="{{ static_url }}{% if debug %}/js/index.js{% else %}/{{ name }}-bundle.js{% endif %}?h={{ build_hash }}"
|
|
13
|
+
data-app-name="{{ name }}"
|
|
14
|
+
data-app-latest-version="{{ version }}"
|
|
15
|
+
data-app-display-name="{{ display_name }}"
|
|
16
|
+
data-debug="{{ debug|lower }}"
|
|
17
|
+
data-navigation-base-prefix="{{ navigation_base_prefix }}"
|
|
18
|
+
data-themes="{{ themes|join(',') }}"
|
|
19
|
+
data-build-hash="{{ build_hash }}"
|
|
20
|
+
{% for n, v in params.items() -%}
|
|
21
|
+
{{ n }}="{{ v }}"
|
|
22
|
+
{% endfor %}>
|
|
23
|
+
</script>
|
|
24
|
+
{%- endmacro %}
|
|
25
|
+
|
|
26
|
+
<!DOCTYPE HTML>
|
|
27
|
+
|
|
28
|
+
<html lang="en">
|
|
29
|
+
<head>
|
|
30
|
+
<title>{% block title %}{{ display_name }}{% endblock %}</title>
|
|
31
|
+
|
|
32
|
+
{% if enable_pwa %}
|
|
33
|
+
<link rel="manifest" href="{{ navigation_base_prefix }}/manifest.json?display_name={{
|
|
34
|
+
display_name|urlquote }}&theme_color={{
|
|
35
|
+
theme_color|urlquote }}&background_color={{
|
|
36
|
+
background_color|urlquote }}&description={{
|
|
37
|
+
description|urlquote }}&h={{ build_hash }}">
|
|
38
|
+
{% endif %}
|
|
39
|
+
<link rel="icon" href="{{ static_url }}/img/launcher-icon-32.png">
|
|
40
|
+
<link rel="apple-touch-icon" href="{{ static_url }}/img/launcher-icon-32.png">
|
|
41
|
+
|
|
42
|
+
{% block _meta %}
|
|
43
|
+
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
|
44
|
+
<meta name="viewport" content="initial-scale=1,minimum-scale=1,maximum-scale=1,user-scalable=no" />
|
|
45
|
+
<meta name="mobile-web-app-capable" content="yes">
|
|
46
|
+
<meta name="apple-mobile-web-app-capable" content="yes">
|
|
47
|
+
<meta name="theme-color" content="{{ theme_color }}">
|
|
48
|
+
<meta name="apple-mobile-web-app-status-bar-style" content="{{ theme_color }}">
|
|
49
|
+
{% endblock %}
|
|
50
|
+
{% block meta %}{% endblock %}
|
|
51
|
+
|
|
52
|
+
{% block _style %}
|
|
53
|
+
<style>
|
|
54
|
+
html {
|
|
55
|
+
background: #444444;
|
|
56
|
+
}
|
|
57
|
+
body {
|
|
58
|
+
opacity: 0;
|
|
59
|
+
}
|
|
60
|
+
</style>
|
|
61
|
+
{% for theme in themes %}
|
|
62
|
+
{% if debug %}
|
|
63
|
+
{{ css_devel_file(theme, "base.css") }}
|
|
64
|
+
{{ css_devel_file(theme, "main-ui.css") }}
|
|
65
|
+
{{ css_devel_file(theme, "no-effects.css") }}
|
|
66
|
+
{{ css_devel_file(theme, "visibility-manager.css") }}
|
|
67
|
+
{{ css_devel_file(theme, "icons.css") }}
|
|
68
|
+
|
|
69
|
+
{{ css_devel_file(theme, "widgets/check-button.css") }}
|
|
70
|
+
{{ css_devel_file(theme, "widgets/choice-buttons.css") }}
|
|
71
|
+
{{ css_devel_file(theme, "widgets/color-combo.css") }}
|
|
72
|
+
{{ css_devel_file(theme, "widgets/combo.css") }}
|
|
73
|
+
{{ css_devel_file(theme, "widgets/common.css") }}
|
|
74
|
+
{{ css_devel_file(theme, "widgets/common-buttons.css") }}
|
|
75
|
+
{{ css_devel_file(theme, "widgets/labels.css") }}
|
|
76
|
+
{{ css_devel_file(theme, "widgets/progress-disk.css") }}
|
|
77
|
+
{{ css_devel_file(theme, "widgets/slider.css") }}
|
|
78
|
+
{{ css_devel_file(theme, "widgets/input.css") }}
|
|
79
|
+
{{ css_devel_file(theme, "widgets/updown.css") }}
|
|
80
|
+
{{ css_devel_file(theme, "widgets/various.css") }}
|
|
81
|
+
|
|
82
|
+
{{ css_devel_file(theme, "messages.css") }}
|
|
83
|
+
{{ css_devel_file(theme, "global-glass.css") }}
|
|
84
|
+
{{ css_devel_file(theme, "structured-view.css") }}
|
|
85
|
+
{{ css_devel_file(theme, "progress-view.css") }}
|
|
86
|
+
{{ css_devel_file(theme, "icon-label-view.css") }}
|
|
87
|
+
|
|
88
|
+
{{ css_devel_file(theme, "pages/breadcrumbs.css") }}
|
|
89
|
+
{{ css_devel_file(theme, "pages/common-pages.css") }}
|
|
90
|
+
{{ css_devel_file(theme, "pages/page.css") }}
|
|
91
|
+
|
|
92
|
+
{{ css_devel_file(theme, "forms/common-fields.css") }}
|
|
93
|
+
{{ css_devel_file(theme, "forms/common-forms.css") }}
|
|
94
|
+
{{ css_devel_file(theme, "forms/form.css") }}
|
|
95
|
+
{{ css_devel_file(theme, "forms/form-button.css") }}
|
|
96
|
+
{{ css_devel_file(theme, "forms/form-field.css") }}
|
|
97
|
+
|
|
98
|
+
{{ css_devel_file(theme, "lists.css") }}
|
|
99
|
+
{{ css_devel_file(theme, "tables.css") }}
|
|
100
|
+
|
|
101
|
+
{{ css_devel_file(theme, "theme.css") }}
|
|
102
|
+
{% else %}
|
|
103
|
+
<link rel="stylesheet"
|
|
104
|
+
type="text/css"
|
|
105
|
+
href="{{ static_url }}/{{ name }}-bundle-{{ theme }}.css?h={{ build_hash }}"
|
|
106
|
+
disabled
|
|
107
|
+
theme="{{ theme }}"/>
|
|
108
|
+
{% endif %}
|
|
109
|
+
{% endfor %}
|
|
110
|
+
{% endblock %}
|
|
111
|
+
{% block style %}{% endblock %}
|
|
112
|
+
|
|
113
|
+
<script type="text/javascript">
|
|
114
|
+
if (localStorage) {
|
|
115
|
+
document.documentElement.style.background = localStorage.getItem('theme.background-color') || ''
|
|
116
|
+
}
|
|
117
|
+
</script>
|
|
118
|
+
|
|
119
|
+
<noscript>This application requires JavaScript!</noscript>
|
|
120
|
+
|
|
121
|
+
{% block script %}
|
|
122
|
+
{{ main_script_file() }}
|
|
123
|
+
{% endblock %}
|
|
124
|
+
</head>
|
|
125
|
+
<body></body>
|
|
126
|
+
</html>
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* This JS file is run in a service worker context.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/* eslint-disable no-undef */
|
|
6
|
+
|
|
7
|
+
const MESSAGE_ACTIVATE = 'qui-activate'
|
|
8
|
+
const DEF_APP_NAME = 'qui-app'
|
|
9
|
+
const DEF_APP_VERSION = 'unknown-version'
|
|
10
|
+
const DEF_BUILD_HASH = 'unknown-hash'
|
|
11
|
+
const DEF_CACHE_URL_REGEX = '.*\\.(svg|png|gif|jpg|jpe?g|ico|woff|html|json|js|css)$'
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
let cacheName
|
|
15
|
+
let debug = false
|
|
16
|
+
let appName = '__app_name_placeholder__'
|
|
17
|
+
let appVersion = '__app_version_placeholder__'
|
|
18
|
+
let buildHash = '__build_hash_placeholder__'
|
|
19
|
+
let cacheURLRegex = '__cache_url_regex_placeholder__'
|
|
20
|
+
let queryArguments = null
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
function getQueryArgument(name, def = null) {
|
|
24
|
+
if (queryArguments == null) {
|
|
25
|
+
let queryString = self.location.search.substring(1)
|
|
26
|
+
queryArguments = new Map(queryString.split('&').map(function (keyValuePair) {
|
|
27
|
+
let splits = keyValuePair.split('=');
|
|
28
|
+
let key = decodeURIComponent(splits[0]);
|
|
29
|
+
let value = decodeURIComponent(splits[1]);
|
|
30
|
+
if (value.indexOf(',') >= 0) {
|
|
31
|
+
value = value.split(',');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return [key, value];
|
|
35
|
+
}))
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let value = queryArguments.get(name)
|
|
39
|
+
if (value == null) {
|
|
40
|
+
value = def
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return value
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function setup() {
|
|
47
|
+
/* If webpack did not replace the placeholder... */
|
|
48
|
+
if (appName.startsWith('__app_name_')) {
|
|
49
|
+
appName = DEF_APP_NAME
|
|
50
|
+
}
|
|
51
|
+
if (appVersion.startsWith('__app_version_')) {
|
|
52
|
+
appVersion = DEF_APP_VERSION
|
|
53
|
+
}
|
|
54
|
+
if (buildHash.startsWith('__build_hash_')) {
|
|
55
|
+
buildHash = getQueryArgument('h', DEF_BUILD_HASH)
|
|
56
|
+
}
|
|
57
|
+
if (cacheURLRegex.startsWith('__cache_url_regex_')) {
|
|
58
|
+
cacheURLRegex = DEF_CACHE_URL_REGEX
|
|
59
|
+
}
|
|
60
|
+
if (getQueryArgument('debug') === 'true') {
|
|
61
|
+
debug = true
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/* Transform into RegExp instance */
|
|
65
|
+
cacheURLRegex = new RegExp(cacheURLRegex, 'i')
|
|
66
|
+
|
|
67
|
+
cacheName = `${appName}-cache-${buildHash}`
|
|
68
|
+
logDebug(`using cache name ${cacheName}`)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
function formatLogMessage(message) {
|
|
73
|
+
return 'Service Worker: ' + message
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function logDebug(message) {
|
|
77
|
+
console.debug(formatLogMessage(message))
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function logInfo(message) {
|
|
81
|
+
console.info(formatLogMessage(message))
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function logWarn(message) {
|
|
85
|
+
console.warn(formatLogMessage(message))
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function logError(message) {
|
|
89
|
+
console.error(formatLogMessage(message))
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
function sendClientMessage(message, uncontrolled = false) {
|
|
94
|
+
self.clients.matchAll({includeUncontrolled: uncontrolled}).then(function (clients) {
|
|
95
|
+
clients.forEach(function (client) {
|
|
96
|
+
client.postMessage(message)
|
|
97
|
+
})
|
|
98
|
+
})
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function shouldCacheRequest(request) {
|
|
102
|
+
return request.url.match(cacheURLRegex) && (request.method === 'GET')
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function shouldCacheResponse(response) {
|
|
106
|
+
return (response &&
|
|
107
|
+
response.status === 200 &&
|
|
108
|
+
response.type === 'basic')
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
self.addEventListener('activate', function (event) {
|
|
113
|
+
|
|
114
|
+
function deleteCache(name) {
|
|
115
|
+
logDebug(`deleting cache ${name}`)
|
|
116
|
+
caches.delete(name)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
event.waitUntil(
|
|
120
|
+
/* Become available to all pages */
|
|
121
|
+
self.clients.claim().then(function () {
|
|
122
|
+
/* Clear all caches starting with our app name */
|
|
123
|
+
caches.keys().then(function (cacheNames) {
|
|
124
|
+
return Promise.all(cacheNames
|
|
125
|
+
.filter(name => name.startsWith(`${appName}-`))
|
|
126
|
+
.map(name => deleteCache(name)))
|
|
127
|
+
})
|
|
128
|
+
})
|
|
129
|
+
)
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
self.addEventListener('fetch', function (event) {
|
|
133
|
+
if (debug) { /* Don't use cache in debug mode */
|
|
134
|
+
return
|
|
135
|
+
}
|
|
136
|
+
if (!shouldCacheRequest(event.request)) {
|
|
137
|
+
return
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
let url = event.request.url
|
|
141
|
+
/* Ensure URL has build hash */
|
|
142
|
+
if (!url.includes('?h=') && !url.includes('&h=')) {
|
|
143
|
+
if (url.includes('?')) {
|
|
144
|
+
url += `&h=${buildHash}`
|
|
145
|
+
}
|
|
146
|
+
else {
|
|
147
|
+
url += `?h=${buildHash}`
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
let request = new Request(url, event.request)
|
|
152
|
+
|
|
153
|
+
event.respondWith(
|
|
154
|
+
caches.match(request, {ignoreSearch: false}).then(function (response) {
|
|
155
|
+
if (response) {
|
|
156
|
+
return response
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return fetch(request.clone()).then(function (response) {
|
|
160
|
+
if (!shouldCacheResponse(response)) {
|
|
161
|
+
return response
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
let responseToCache = response.clone()
|
|
165
|
+
caches.open(cacheName).then(function (cache) {
|
|
166
|
+
cache.put(request.clone(), responseToCache)
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
return response.clone()
|
|
170
|
+
})
|
|
171
|
+
})
|
|
172
|
+
)
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
self.addEventListener('message', function (message) {
|
|
176
|
+
switch (message.data.type) {
|
|
177
|
+
case MESSAGE_ACTIVATE:
|
|
178
|
+
logInfo('received activation message')
|
|
179
|
+
self.skipWaiting()
|
|
180
|
+
break
|
|
181
|
+
|
|
182
|
+
default:
|
|
183
|
+
logWarn(`unexpected service worker message ${message.data.type}`)
|
|
184
|
+
}
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
setup()
|
|
File without changes
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import os
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
from re import Match
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from tornado.web import RequestHandler, StaticFileHandler, URLSpec
|
|
9
|
+
|
|
10
|
+
from qui import __file__ as qui_package_path
|
|
11
|
+
from qui import constants, exceptions, j2template, settings
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
JS_MODULE_PATH_RE = re.compile(rb"\'(\$qui|\$app|\$node|)([/.][A-Za-z0-9_./$-]+\.jsm?)\'")
|
|
15
|
+
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class TemplateHandler(RequestHandler):
|
|
20
|
+
def prepare(self) -> None:
|
|
21
|
+
self.set_header("Cache-Control", "no-cache, no-store, must-revalidate, max-age=0")
|
|
22
|
+
|
|
23
|
+
def get_context(self, path: str = "", path_offs: int = 0) -> dict[str, Any]:
|
|
24
|
+
context = settings.make_context(self.request)
|
|
25
|
+
|
|
26
|
+
# If using static URL that is relative to frontend URL prefix, adjust it to a relative path matching currently
|
|
27
|
+
# requested frontend path
|
|
28
|
+
static_url = context["static_url"]
|
|
29
|
+
if static_url.startswith(f"{settings.frontend_url_prefix}/"):
|
|
30
|
+
slashes = path.count("/") + path_offs
|
|
31
|
+
if slashes == 0:
|
|
32
|
+
prefix = f"{settings.frontend_url_prefix}/"
|
|
33
|
+
elif slashes > 1:
|
|
34
|
+
prefix = "/".join([".."] * (slashes - 1)) + "/"
|
|
35
|
+
|
|
36
|
+
else:
|
|
37
|
+
prefix = ""
|
|
38
|
+
|
|
39
|
+
context["static_url"] = prefix + static_url[len(settings.frontend_url_prefix) + 1 :]
|
|
40
|
+
|
|
41
|
+
return context
|
|
42
|
+
|
|
43
|
+
async def render(self, template_name: str, context: dict[str, Any] | None = None) -> None:
|
|
44
|
+
if context is None:
|
|
45
|
+
context = self.get_context()
|
|
46
|
+
|
|
47
|
+
env = j2template.get_env()
|
|
48
|
+
template = env.get_template(template_name)
|
|
49
|
+
template_str = await template.render_async(**context)
|
|
50
|
+
|
|
51
|
+
await self.finish(template_str)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class JSModuleMapperStaticFileHandler(StaticFileHandler):
|
|
55
|
+
def __init__(self, *args, **kwargs) -> None:
|
|
56
|
+
self._mapping = {
|
|
57
|
+
b"$qui": f"/{settings.static_url}/qui/js".encode(),
|
|
58
|
+
b"$app": f"/{settings.static_url}/app/js".encode(),
|
|
59
|
+
b"$node": f"/{settings.static_url}/app/node_modules".encode(),
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
self._mapped_content: bytes | None = None
|
|
63
|
+
|
|
64
|
+
super().__init__(*args, **kwargs)
|
|
65
|
+
|
|
66
|
+
def get_content_size(self) -> int:
|
|
67
|
+
return len(self.get_mapped_content())
|
|
68
|
+
|
|
69
|
+
def get_content(self, abspath: str, start: int | None = None, end: int | None = None) -> bytes:
|
|
70
|
+
return self.get_mapped_content()
|
|
71
|
+
|
|
72
|
+
@classmethod
|
|
73
|
+
def get_content_version(cls, abspath: str) -> str:
|
|
74
|
+
return ""
|
|
75
|
+
|
|
76
|
+
def replace_path(self, match: Match[bytes]) -> bytes:
|
|
77
|
+
prefix = match.group(1)
|
|
78
|
+
path = match.group(2)
|
|
79
|
+
|
|
80
|
+
prefix = self._mapping.get(prefix, prefix)
|
|
81
|
+
path = path + f"?h={settings.build_hash}".encode()
|
|
82
|
+
|
|
83
|
+
if path.startswith(b"."):
|
|
84
|
+
base_prefix = b""
|
|
85
|
+
else:
|
|
86
|
+
base_prefix = self.request.headers.get(constants.BASE_PREFIX_HEADER, "/")
|
|
87
|
+
base_prefix = base_prefix.rstrip("/").encode()
|
|
88
|
+
|
|
89
|
+
return b"'" + base_prefix + prefix + path + b"'"
|
|
90
|
+
|
|
91
|
+
def get_mapped_content(self) -> bytes:
|
|
92
|
+
if self._mapped_content is None:
|
|
93
|
+
content = b"".join(super().get_content(self.absolute_path))
|
|
94
|
+
|
|
95
|
+
if self.absolute_path.endswith(".js"):
|
|
96
|
+
content = JS_MODULE_PATH_RE.sub(self.replace_path, content)
|
|
97
|
+
|
|
98
|
+
self._mapped_content = content
|
|
99
|
+
|
|
100
|
+
return self._mapped_content
|
|
101
|
+
|
|
102
|
+
def should_return_304(self) -> bool:
|
|
103
|
+
return False
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class RedirectFrontendHandler(RequestHandler):
|
|
107
|
+
def get(self) -> None:
|
|
108
|
+
base_prefix = self.request.headers.get(constants.BASE_PREFIX_HEADER, "/")
|
|
109
|
+
if not base_prefix.endswith("/"):
|
|
110
|
+
base_prefix += "/"
|
|
111
|
+
|
|
112
|
+
self.redirect(f"{base_prefix}{settings.frontend_url_prefix}/")
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class FrontendHandler(TemplateHandler):
|
|
116
|
+
async def get(self, path: str) -> None:
|
|
117
|
+
await self.render("index.html", self.get_context(path))
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
class ManifestHandler(TemplateHandler):
|
|
121
|
+
PARAMS = [
|
|
122
|
+
"display_name",
|
|
123
|
+
"display_short_name",
|
|
124
|
+
"description",
|
|
125
|
+
"version",
|
|
126
|
+
"theme_color",
|
|
127
|
+
"background_color",
|
|
128
|
+
]
|
|
129
|
+
|
|
130
|
+
async def get(self) -> None:
|
|
131
|
+
context = self.get_context(path_offs=1)
|
|
132
|
+
for param in self.PARAMS:
|
|
133
|
+
value = self.get_query_argument(param, None)
|
|
134
|
+
if value is not None:
|
|
135
|
+
context[param] = value
|
|
136
|
+
|
|
137
|
+
self.set_header("Content-Type", 'application/manifest+json; charset="utf-8"')
|
|
138
|
+
await self.render("manifest.json", context)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
class ServiceWorkerHandler(TemplateHandler):
|
|
142
|
+
async def get(self) -> None:
|
|
143
|
+
self.set_header("Content-Type", 'application/javascript; charset="utf-8"')
|
|
144
|
+
await self.render("service-worker.js")
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def make_routing_table() -> list[URLSpec]:
|
|
148
|
+
frontend_dir = settings.frontend_dir
|
|
149
|
+
if not settings.debug: # in production mode, frontend files are found under the dist frontend subfolder
|
|
150
|
+
frontend_dir += "/dist"
|
|
151
|
+
|
|
152
|
+
# Look for frontend dir in all available package dirs
|
|
153
|
+
package = __import__(settings.package_name)
|
|
154
|
+
for package_dir in list(package.__path__):
|
|
155
|
+
frontend_path = os.path.join(package_dir, frontend_dir)
|
|
156
|
+
if os.path.exists(frontend_path):
|
|
157
|
+
break
|
|
158
|
+
|
|
159
|
+
else:
|
|
160
|
+
raise exceptions.QUIException(f"Cannot find frontend dir in package {settings.package_name}")
|
|
161
|
+
|
|
162
|
+
spec_list = []
|
|
163
|
+
|
|
164
|
+
static_url = settings.static_url
|
|
165
|
+
if not static_url.startswith("/"):
|
|
166
|
+
static_url = f"/{static_url}"
|
|
167
|
+
|
|
168
|
+
if settings.debug:
|
|
169
|
+
# In debug mode, we serve QUI static files from QUI folder
|
|
170
|
+
qui_path = os.path.join(os.path.dirname(qui_package_path), "..")
|
|
171
|
+
qui_path = os.path.abspath(qui_path)
|
|
172
|
+
|
|
173
|
+
spec_list.append(
|
|
174
|
+
URLSpec(
|
|
175
|
+
rf"^{static_url}/qui/(.*)$",
|
|
176
|
+
JSModuleMapperStaticFileHandler,
|
|
177
|
+
{"path": qui_path},
|
|
178
|
+
name="static-qui",
|
|
179
|
+
)
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
spec_list.append(
|
|
183
|
+
URLSpec(
|
|
184
|
+
rf"^{static_url}/(?:app/)?(.*)$",
|
|
185
|
+
JSModuleMapperStaticFileHandler,
|
|
186
|
+
{"path": frontend_path},
|
|
187
|
+
name="static-app",
|
|
188
|
+
)
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
else:
|
|
192
|
+
spec_list.append(
|
|
193
|
+
URLSpec(
|
|
194
|
+
rf"^{static_url}/(.*)$",
|
|
195
|
+
StaticFileHandler,
|
|
196
|
+
{"path": frontend_path},
|
|
197
|
+
name="static",
|
|
198
|
+
)
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
spec_list += [
|
|
202
|
+
URLSpec(r"^/?$", RedirectFrontendHandler, name="redirect-frontend"),
|
|
203
|
+
URLSpec(
|
|
204
|
+
rf"^/{settings.frontend_url_prefix}/service-worker.js$",
|
|
205
|
+
ServiceWorkerHandler,
|
|
206
|
+
name="service-worker",
|
|
207
|
+
),
|
|
208
|
+
URLSpec(
|
|
209
|
+
rf"^/{settings.frontend_url_prefix}/manifest.json$",
|
|
210
|
+
ManifestHandler,
|
|
211
|
+
name="manifest",
|
|
212
|
+
),
|
|
213
|
+
URLSpec(
|
|
214
|
+
rf"^/{settings.frontend_url_prefix}(?P<path>.*)",
|
|
215
|
+
FrontendHandler,
|
|
216
|
+
name="frontend",
|
|
217
|
+
),
|
|
218
|
+
]
|
|
219
|
+
|
|
220
|
+
return spec_list
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
|
|
3
|
+
# Create symlinks to libs
|
|
4
|
+
echo "Creating symlinks to libs"
|
|
5
|
+
cd js/lib
|
|
6
|
+
ln -sf ../../../../jquery/dist/jquery.js .
|
|
7
|
+
ln -sf ../../../../jquery-mousewheel/jquery.mousewheel.js .
|
|
8
|
+
ln -sf ../../../../jquery-ui-dist/jquery-ui.js
|
|
9
|
+
ln -sf ../../../../js-logger/src/logger.js
|
|
10
|
+
ln -sf ../../../../pepjs/dist/pep.js
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
|
|
2
|
+
const loaderUtils = require('loader-utils')
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
module.exports = function (source) {
|
|
6
|
+
const options = loaderUtils.getOptions(this)
|
|
7
|
+
const appLessPath = options.appLessPath
|
|
8
|
+
const quiLessPath = options.quiLessPath
|
|
9
|
+
|
|
10
|
+
let filename
|
|
11
|
+
if (this._module.resource.startsWith(appLessPath)) {
|
|
12
|
+
filename = this._module.resource.slice(appLessPath.length)
|
|
13
|
+
}
|
|
14
|
+
else if (this._module.resource.startsWith(quiLessPath)) {
|
|
15
|
+
filename = this._module.resource.slice(quiLessPath.length)
|
|
16
|
+
}
|
|
17
|
+
else { /* Cannot identify source file root path */
|
|
18
|
+
return source
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let dirsInPath = (filename.match(/\//g) || []).length - 1
|
|
22
|
+
if (dirsInPath < 0) {
|
|
23
|
+
return source
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
dirsInPath += options.additionalDirs || 0
|
|
27
|
+
|
|
28
|
+
/* Generate a sequence of "../" as long as the number of dirs in path */
|
|
29
|
+
let relPath = [...Array(dirsInPath).keys()].map(() => '../').join('')
|
|
30
|
+
if (!relPath) {
|
|
31
|
+
return source
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/* Adjust any url('...') by prefixing with the relative path, unless we deal with absolute paths */
|
|
35
|
+
return source.replace(new RegExp('url\\(([\'"])?(?!http)(./)?([^/].*)', 'g'), 'url($1' + relPath + '$3')
|
|
36
|
+
}
|