webplayercontrol 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.
- webplayercontrol-1.0.0/PKG-INFO +59 -0
- webplayercontrol-1.0.0/README.md +40 -0
- webplayercontrol-1.0.0/pyproject.toml +88 -0
- webplayercontrol-1.0.0/pyproject.toml.orig +88 -0
- webplayercontrol-1.0.0/src/webplayercontrol/__init__.py +3 -0
- webplayercontrol-1.0.0/src/webplayercontrol/assets/common.css +12 -0
- webplayercontrol-1.0.0/src/webplayercontrol/assets/index.html +14 -0
- webplayercontrol-1.0.0/src/webplayercontrol/assets/index.js +26 -0
- webplayercontrol-1.0.0/src/webplayercontrol/assets/player.css +43 -0
- webplayercontrol-1.0.0/src/webplayercontrol/assets/player.html +29 -0
- webplayercontrol-1.0.0/src/webplayercontrol/assets/player.js +114 -0
- webplayercontrol-1.0.0/src/webplayercontrol/assets.py +284 -0
- webplayercontrol-1.0.0/src/webplayercontrol/dbus.xml +132 -0
- webplayercontrol-1.0.0/src/webplayercontrol/main.py +473 -0
- webplayercontrol-1.0.0/src/webplayercontrol/mpris.py +698 -0
- webplayercontrol-1.0.0/src/webplayercontrol/mpris.xml +97 -0
- webplayercontrol-1.0.0/src/webplayercontrol/player.xml +737 -0
- webplayercontrol-1.0.0/src/webplayercontrol/utils.py +109 -0
- webplayercontrol-1.0.0/tests/__init__.py +1 -0
- webplayercontrol-1.0.0/tests/webplayercontrol/__init__.py +1 -0
- webplayercontrol-1.0.0/tests/webplayercontrol/test_utils.py +117 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: webplayercontrol
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: An ASGI application that exposes Media Player Remote Interfacing Specification (MPRIS)-compliant media players over the web.
|
|
5
|
+
Author: Christopher Head
|
|
6
|
+
License-Expression: GPL-3.0-only
|
|
7
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
8
|
+
Classifier: Environment :: Web Environment
|
|
9
|
+
Classifier: Intended Audience :: End Users/Desktop
|
|
10
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
|
|
13
|
+
Classifier: Topic :: Multimedia :: Sound/Audio :: Players
|
|
14
|
+
Requires-Dist: aioscgi>=2.5.1
|
|
15
|
+
Requires-Dist: dbus-fast>=5.0.22
|
|
16
|
+
Requires-Python: >=3.14
|
|
17
|
+
Project-URL: Repository, https://gitlab.com/Hawk777/webplayercontrol
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
What is MPRIS?
|
|
21
|
+
==============
|
|
22
|
+
|
|
23
|
+
The [Media Player Remote Interfacing
|
|
24
|
+
Specification](https://specifications.freedesktop.org/mpris/latest/), or MPRIS,
|
|
25
|
+
is a standard for remotely controlling media players via D-Bus in a Linux
|
|
26
|
+
system. Media players can expose MPRIS interfaces via D-Bus, and any
|
|
27
|
+
MPRIS-compliant client can control those players (pause, play, advance through
|
|
28
|
+
the playlist, adjust volume, seek, etc.).
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
What is webplayercontrol?
|
|
32
|
+
=========================
|
|
33
|
+
|
|
34
|
+
Webplayercontrol is a web application (implemented using
|
|
35
|
+
[ASGI](https://asgi.readthedocs.io/en/stable/) that provides a web interface
|
|
36
|
+
for controlling MPRIS-compliant media players. The web interface is optimized
|
|
37
|
+
for cell phone usage. The intention is to use a cell phone to control a media
|
|
38
|
+
player running on a desktop, for example while sitting too far away from the
|
|
39
|
+
desktop to use its own mouse and keyboard.
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
How do I set it up?
|
|
43
|
+
===================
|
|
44
|
+
|
|
45
|
+
You’ll need an ASGI server. There are lots available to choose from, some which
|
|
46
|
+
are usable on their own and some which expect to have a traditional Web server
|
|
47
|
+
(like Apache, Lighttpd, or Nginx) placed in front of them. Then, once
|
|
48
|
+
everything is installed, run your ASGI server and tell it the application
|
|
49
|
+
callable is `webplayercontrol:main`.
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
What options can I configure?
|
|
53
|
+
=============================
|
|
54
|
+
|
|
55
|
+
If your server supports [the pathsend
|
|
56
|
+
extension](https://asgi.readthedocs.io/en/stable/extensions.html#path-send),
|
|
57
|
+
you can set the `WEBPLAYERCONTROL_ASSETS` environment variable to the name of a
|
|
58
|
+
directory. The directory will be created, static assets will be extracted into
|
|
59
|
+
it, and the pathsend extension will be used to send those assets.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
What is MPRIS?
|
|
2
|
+
==============
|
|
3
|
+
|
|
4
|
+
The [Media Player Remote Interfacing
|
|
5
|
+
Specification](https://specifications.freedesktop.org/mpris/latest/), or MPRIS,
|
|
6
|
+
is a standard for remotely controlling media players via D-Bus in a Linux
|
|
7
|
+
system. Media players can expose MPRIS interfaces via D-Bus, and any
|
|
8
|
+
MPRIS-compliant client can control those players (pause, play, advance through
|
|
9
|
+
the playlist, adjust volume, seek, etc.).
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
What is webplayercontrol?
|
|
13
|
+
=========================
|
|
14
|
+
|
|
15
|
+
Webplayercontrol is a web application (implemented using
|
|
16
|
+
[ASGI](https://asgi.readthedocs.io/en/stable/) that provides a web interface
|
|
17
|
+
for controlling MPRIS-compliant media players. The web interface is optimized
|
|
18
|
+
for cell phone usage. The intention is to use a cell phone to control a media
|
|
19
|
+
player running on a desktop, for example while sitting too far away from the
|
|
20
|
+
desktop to use its own mouse and keyboard.
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
How do I set it up?
|
|
24
|
+
===================
|
|
25
|
+
|
|
26
|
+
You’ll need an ASGI server. There are lots available to choose from, some which
|
|
27
|
+
are usable on their own and some which expect to have a traditional Web server
|
|
28
|
+
(like Apache, Lighttpd, or Nginx) placed in front of them. Then, once
|
|
29
|
+
everything is installed, run your ASGI server and tell it the application
|
|
30
|
+
callable is `webplayercontrol:main`.
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
What options can I configure?
|
|
34
|
+
=============================
|
|
35
|
+
|
|
36
|
+
If your server supports [the pathsend
|
|
37
|
+
extension](https://asgi.readthedocs.io/en/stable/extensions.html#path-send),
|
|
38
|
+
you can set the `WEBPLAYERCONTROL_ASSETS` environment variable to the name of a
|
|
39
|
+
directory. The directory will be created, static assets will be extracted into
|
|
40
|
+
it, and the pathsend extension will be used to send those assets.
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["uv_build ~= 0.13.4"]
|
|
3
|
+
build-backend = "uv_build"
|
|
4
|
+
|
|
5
|
+
[dependency-groups]
|
|
6
|
+
ci = ["pylint-gitlab"]
|
|
7
|
+
dev = [
|
|
8
|
+
"asgi-types >= 0.2.0",
|
|
9
|
+
"pylint ~= 4.1.3",
|
|
10
|
+
"pyright ~= 1.1.414",
|
|
11
|
+
"pytest ~= 9.2.0",
|
|
12
|
+
"pytest-asyncio ~= 1.4.1",
|
|
13
|
+
"ruff ~= 0.17.3",
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
[project]
|
|
17
|
+
name = "webplayercontrol"
|
|
18
|
+
version = "1.0.0"
|
|
19
|
+
dependencies = [
|
|
20
|
+
"aioscgi >= 2.5.1",
|
|
21
|
+
"dbus-fast >= 5.0.22",
|
|
22
|
+
]
|
|
23
|
+
requires-python = ">= 3.14"
|
|
24
|
+
license = "GPL-3.0-only"
|
|
25
|
+
description = "An ASGI application that exposes Media Player Remote Interfacing Specification (MPRIS)-compliant media players over the web."
|
|
26
|
+
readme = "README.md"
|
|
27
|
+
classifiers = [
|
|
28
|
+
"Development Status :: 5 - Production/Stable",
|
|
29
|
+
"Environment :: Web Environment",
|
|
30
|
+
"Intended Audience :: End Users/Desktop",
|
|
31
|
+
"Operating System :: POSIX :: Linux",
|
|
32
|
+
"Programming Language :: Python :: 3",
|
|
33
|
+
"Topic :: Internet :: WWW/HTTP :: Dynamic Content",
|
|
34
|
+
"Topic :: Multimedia :: Sound/Audio :: Players",
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
[[project.authors]]
|
|
38
|
+
name = "Christopher Head"
|
|
39
|
+
|
|
40
|
+
[project.urls]
|
|
41
|
+
Repository = "https://gitlab.com/Hawk777/webplayercontrol"
|
|
42
|
+
|
|
43
|
+
[tool.pylint.imports]
|
|
44
|
+
allow-reexport-from-package = true
|
|
45
|
+
|
|
46
|
+
[tool.pylint.messages_control]
|
|
47
|
+
enable = [
|
|
48
|
+
"deprecated-pragma",
|
|
49
|
+
"useless-suppression",
|
|
50
|
+
"use-symbolic-message-instead",
|
|
51
|
+
]
|
|
52
|
+
disable = [
|
|
53
|
+
"broad-exception-caught",
|
|
54
|
+
"line-too-long",
|
|
55
|
+
"too-few-public-methods",
|
|
56
|
+
"too-many-instance-attributes",
|
|
57
|
+
"too-many-locals",
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
[tool.pyright]
|
|
61
|
+
exclude = [".ruff_cache"]
|
|
62
|
+
pythonVersion = "3.14"
|
|
63
|
+
typeCheckingMode = "strict"
|
|
64
|
+
|
|
65
|
+
[tool.pytest]
|
|
66
|
+
addopts = ["--import-mode=importlib"]
|
|
67
|
+
asyncio_mode = "strict"
|
|
68
|
+
asyncio_default_fixture_loop_scope = "function"
|
|
69
|
+
strict = true
|
|
70
|
+
testpaths = ["tests"]
|
|
71
|
+
|
|
72
|
+
[tool.ruff]
|
|
73
|
+
src = ["src"]
|
|
74
|
+
target-version = "py314"
|
|
75
|
+
|
|
76
|
+
[tool.ruff.lint]
|
|
77
|
+
allowed-confusables = ["’"]
|
|
78
|
+
ignore = [
|
|
79
|
+
"COM812",
|
|
80
|
+
"CPY001",
|
|
81
|
+
"D203",
|
|
82
|
+
"D212",
|
|
83
|
+
"S101",
|
|
84
|
+
]
|
|
85
|
+
select = ["ALL"]
|
|
86
|
+
|
|
87
|
+
[tool.uv.build-backend]
|
|
88
|
+
source-include = ["tests/**"]
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["uv_build ~= 0.13.4"]
|
|
3
|
+
build-backend = "uv_build"
|
|
4
|
+
|
|
5
|
+
[dependency-groups]
|
|
6
|
+
ci = [
|
|
7
|
+
"pylint-gitlab",
|
|
8
|
+
]
|
|
9
|
+
dev = [
|
|
10
|
+
"asgi-types >= 0.2.0",
|
|
11
|
+
"pylint ~= 4.1.3",
|
|
12
|
+
"pyright ~= 1.1.414",
|
|
13
|
+
"pytest ~= 9.2.0",
|
|
14
|
+
"pytest-asyncio ~= 1.4.1",
|
|
15
|
+
"ruff ~= 0.17.3",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
[project]
|
|
19
|
+
name = "webplayercontrol"
|
|
20
|
+
version = "1.0.0"
|
|
21
|
+
dependencies = [
|
|
22
|
+
"aioscgi >= 2.5.1",
|
|
23
|
+
"dbus-fast >= 5.0.22",
|
|
24
|
+
]
|
|
25
|
+
requires-python = ">= 3.14"
|
|
26
|
+
license = "GPL-3.0-only"
|
|
27
|
+
authors = [
|
|
28
|
+
{ name = "Christopher Head" },
|
|
29
|
+
]
|
|
30
|
+
description = "An ASGI application that exposes Media Player Remote Interfacing Specification (MPRIS)-compliant media players over the web."
|
|
31
|
+
readme = "README.md"
|
|
32
|
+
classifiers = [
|
|
33
|
+
"Development Status :: 5 - Production/Stable",
|
|
34
|
+
"Environment :: Web Environment",
|
|
35
|
+
"Intended Audience :: End Users/Desktop",
|
|
36
|
+
"Operating System :: POSIX :: Linux",
|
|
37
|
+
"Programming Language :: Python :: 3",
|
|
38
|
+
"Topic :: Internet :: WWW/HTTP :: Dynamic Content",
|
|
39
|
+
"Topic :: Multimedia :: Sound/Audio :: Players",
|
|
40
|
+
]
|
|
41
|
+
urls.Repository = "https://gitlab.com/Hawk777/webplayercontrol"
|
|
42
|
+
|
|
43
|
+
[tool.pylint.imports]
|
|
44
|
+
allow-reexport-from-package = true
|
|
45
|
+
|
|
46
|
+
[tool.pylint.messages_control]
|
|
47
|
+
enable = [
|
|
48
|
+
"deprecated-pragma",
|
|
49
|
+
"useless-suppression",
|
|
50
|
+
"use-symbolic-message-instead",
|
|
51
|
+
]
|
|
52
|
+
disable = [
|
|
53
|
+
"broad-exception-caught",
|
|
54
|
+
"line-too-long",
|
|
55
|
+
"too-few-public-methods",
|
|
56
|
+
"too-many-instance-attributes",
|
|
57
|
+
"too-many-locals",
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
[tool.pyright]
|
|
61
|
+
exclude = [".ruff_cache"]
|
|
62
|
+
pythonVersion = "3.14"
|
|
63
|
+
typeCheckingMode = "strict"
|
|
64
|
+
|
|
65
|
+
[tool.pytest]
|
|
66
|
+
addopts = ["--import-mode=importlib"]
|
|
67
|
+
asyncio_mode = "strict"
|
|
68
|
+
asyncio_default_fixture_loop_scope = "function"
|
|
69
|
+
strict = true
|
|
70
|
+
testpaths = ["tests"]
|
|
71
|
+
|
|
72
|
+
[tool.ruff]
|
|
73
|
+
src = ["src"]
|
|
74
|
+
target-version = "py314"
|
|
75
|
+
|
|
76
|
+
[tool.ruff.lint]
|
|
77
|
+
allowed-confusables = ["’"]
|
|
78
|
+
ignore = [
|
|
79
|
+
"COM812", # missing-trailing-comma
|
|
80
|
+
"CPY001", # missing-copyright-notice
|
|
81
|
+
"D203", # one-blank-line-before-class
|
|
82
|
+
"D212", # multi-line-summary-first-line
|
|
83
|
+
"S101", # assert
|
|
84
|
+
]
|
|
85
|
+
select = ["ALL"]
|
|
86
|
+
|
|
87
|
+
[tool.uv.build-backend]
|
|
88
|
+
source-include = ["tests/**"]
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<title>WebPlayerControl</title>
|
|
6
|
+
<link href="{{hash-urlsafe:common.css}}" integrity="sha256-{{hash:common.css}}" rel="stylesheet">
|
|
7
|
+
<script type="module" integrity="sha256-{{hash:index.js}}" src="{{hash-urlsafe:index.js}}"></script>
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<h1>Select a player to control</h1>
|
|
11
|
+
<ul id="player-list">
|
|
12
|
+
</ul>
|
|
13
|
+
</body>
|
|
14
|
+
</html>
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const onLoad = async function () {
|
|
2
|
+
const playerList = document.getElementById("player-list");
|
|
3
|
+
try {
|
|
4
|
+
// Fetch the list of players from the server.
|
|
5
|
+
const listingResp = await fetch("players/", { headers: { Accept: "application/json" } });
|
|
6
|
+
if (!listingResp.ok) {
|
|
7
|
+
throw new Error(`Fetching player list failed: HTTP error ${listingResp.status}`);
|
|
8
|
+
}
|
|
9
|
+
const listing = await listingResp.json();
|
|
10
|
+
for (const [name, display] of listing) {
|
|
11
|
+
const item = document.createElement("li");
|
|
12
|
+
const link = document.createElement("a");
|
|
13
|
+
link.href = `players/${name}/`;
|
|
14
|
+
link.textContent = display;
|
|
15
|
+
item.append(link);
|
|
16
|
+
playerList.append(item);
|
|
17
|
+
}
|
|
18
|
+
} catch (exp) {
|
|
19
|
+
const para = document.createElement("p");
|
|
20
|
+
para.textContent = exp.toString();
|
|
21
|
+
para.classList.add("error-message");
|
|
22
|
+
playerList.parentElement.replaceChild(para, playerList);
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
window.addEventListener("load", onLoad);
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
html {
|
|
2
|
+
text-align: center;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
button {
|
|
6
|
+
background: black;
|
|
7
|
+
border: 0;
|
|
8
|
+
font-size: min(10vw, 35vh);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
button:disabled {
|
|
12
|
+
filter: grayscale(100%);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
#status {
|
|
16
|
+
font-size: 2rem;
|
|
17
|
+
margin-bottom: 2rem;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
#spinner-wrapper {
|
|
21
|
+
display: inline-block;
|
|
22
|
+
min-height: 2em;
|
|
23
|
+
vertical-align: middle;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
#spinner {
|
|
27
|
+
animation: spin 2s linear infinite;
|
|
28
|
+
border: 0.2em solid #333;
|
|
29
|
+
border-top: 0.2em solid #888;
|
|
30
|
+
border-radius: 50%;
|
|
31
|
+
display: inline-block;
|
|
32
|
+
width: 1em;
|
|
33
|
+
height: 1em;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
#spinner.off {
|
|
37
|
+
display: none;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
@keyframes spin {
|
|
41
|
+
0% { transform: rotate(0deg); }
|
|
42
|
+
100% { transform: rotate(360deg); }
|
|
43
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<title>WebPlayerControl</title>
|
|
6
|
+
<link href="../../{{hash-urlsafe:common.css}}" integrity="sha256-{{hash:common.css}}" rel="stylesheet">
|
|
7
|
+
<link href="../../{{hash-urlsafe:player.css}}" integrity="sha256-{{hash:player.css}}" rel="stylesheet">
|
|
8
|
+
<script type="module" integrity="sha256-{{hash:player.js}}" src="../../{{hash-urlsafe:player.js}}"></script>
|
|
9
|
+
</head>
|
|
10
|
+
<body>
|
|
11
|
+
<h1>WebPlayerControl</h1>
|
|
12
|
+
<a href="../..">← Back to player list</a>
|
|
13
|
+
<div id="status">
|
|
14
|
+
<div id="spinner-wrapper">
|
|
15
|
+
<div id="spinner"></div>
|
|
16
|
+
</div>
|
|
17
|
+
<span id="status-text">Initializing</span>
|
|
18
|
+
</div>
|
|
19
|
+
<div id="buttons">
|
|
20
|
+
<button id="goPrevious" type="button" disabled>⏮️</button>
|
|
21
|
+
<button id="seekBack" type="button" disabled>⏪️</button>
|
|
22
|
+
<button id="stop" type="button" disabled>⏹️</button>
|
|
23
|
+
<button id="pause" type="button" disabled>⏸️</button>
|
|
24
|
+
<button id="play" type="button" disabled>▶️</button>
|
|
25
|
+
<button id="seekForward" type="button" disabled>⏩️</button>
|
|
26
|
+
<button id="goNext" type="button" disabled>⏭️</button>
|
|
27
|
+
</div>
|
|
28
|
+
</body>
|
|
29
|
+
</html>
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Capitalizes the first character of a string.
|
|
3
|
+
*/
|
|
4
|
+
const capFirst = function (string) {
|
|
5
|
+
return string.charAt(0).toUpperCase() + string.slice(1);
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
/*
|
|
9
|
+
* Lowercases the first character of a string.
|
|
10
|
+
*/
|
|
11
|
+
const lowerFirst = function (string) {
|
|
12
|
+
return string.charAt(0).toLowerCase() + string.slice(1);
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/*
|
|
16
|
+
* Display a status message for an error or reconnection.
|
|
17
|
+
*
|
|
18
|
+
* All controls are disabled.
|
|
19
|
+
*/
|
|
20
|
+
const showStatus = function (msg, error) {
|
|
21
|
+
for (const button of document.getElementsByTagName("button")) {
|
|
22
|
+
button.disabled = true;
|
|
23
|
+
}
|
|
24
|
+
const spinner = document.getElementById("spinner");
|
|
25
|
+
const statusText = document.getElementById("status-text");
|
|
26
|
+
statusText.textContent = msg;
|
|
27
|
+
if (error) {
|
|
28
|
+
spinner.classList.add("off");
|
|
29
|
+
statusText.classList.add("error-message");
|
|
30
|
+
} else {
|
|
31
|
+
spinner.classList.remove("off");
|
|
32
|
+
statusText.classList.remove("error-message");
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/*
|
|
37
|
+
* Handles a server-sent event starting with “can”.
|
|
38
|
+
*
|
|
39
|
+
* Such an event enables or disables the indicated button.
|
|
40
|
+
*/
|
|
41
|
+
const onCan = function (evt) {
|
|
42
|
+
const buttonName = lowerFirst(evt.type.slice(3));
|
|
43
|
+
const button = document.getElementById(buttonName);
|
|
44
|
+
button.disabled = evt.data === "0";
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/*
|
|
48
|
+
* Handles a button being clicked.
|
|
49
|
+
*/
|
|
50
|
+
const onClick = async function (evt, source) {
|
|
51
|
+
const buttonName = evt.target.id;
|
|
52
|
+
const result = await (async () => {
|
|
53
|
+
try {
|
|
54
|
+
const resp = await fetch(buttonName, { body: "", method: "POST", mode: "same-origin" });
|
|
55
|
+
return [resp, null];
|
|
56
|
+
} catch (err) {
|
|
57
|
+
if (err instanceof TypeError) {
|
|
58
|
+
return [null, err];
|
|
59
|
+
}
|
|
60
|
+
throw err;
|
|
61
|
+
}
|
|
62
|
+
})();
|
|
63
|
+
const [resp, err] = result;
|
|
64
|
+
if (resp === null) {
|
|
65
|
+
source.close();
|
|
66
|
+
showStatus(`POST to “${buttonName}” failed with ${err.message}`, true);
|
|
67
|
+
} else if (!resp.ok) {
|
|
68
|
+
source.close();
|
|
69
|
+
showStatus(`POST to “${resp.url}” failed with HTTP ${resp.status}`, true);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/*
|
|
74
|
+
* Handles an error in the server-sent event connection.
|
|
75
|
+
*
|
|
76
|
+
* The problem is displayed and all controls are disabled until the event
|
|
77
|
+
* stream reconnects (if it is going to do so).
|
|
78
|
+
*/
|
|
79
|
+
const onError = function (evt) {
|
|
80
|
+
if (evt.target.readyState === EventSource.CONNECTING) {
|
|
81
|
+
showStatus("SSE reconnecting", false);
|
|
82
|
+
} else {
|
|
83
|
+
showStatus("SSE connection failed", true);
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/*
|
|
88
|
+
* Handles a server-sent “title” event.
|
|
89
|
+
*
|
|
90
|
+
* The title is displayed for the user. The loading spinner is also hidden,
|
|
91
|
+
* since “title” is always the last event in the initial burst on a new
|
|
92
|
+
* connection.
|
|
93
|
+
*/
|
|
94
|
+
const onTitle = function (evt) {
|
|
95
|
+
const statusText = document.getElementById("status-text");
|
|
96
|
+
statusText.textContent = evt.data;
|
|
97
|
+
const spinner = document.getElementById("spinner");
|
|
98
|
+
spinner.classList.add("off");
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
/*
|
|
102
|
+
* Starts the application.
|
|
103
|
+
*/
|
|
104
|
+
const main = function () {
|
|
105
|
+
const source = new EventSource("status");
|
|
106
|
+
source.addEventListener("error", onError);
|
|
107
|
+
for (const button of document.getElementsByTagName("button")) {
|
|
108
|
+
source.addEventListener(`can${capFirst(button.id)}`, onCan);
|
|
109
|
+
button.addEventListener("click", (evt) => onClick(evt, source));
|
|
110
|
+
}
|
|
111
|
+
source.addEventListener("title", onTitle);
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
main();
|