diff --git a/css/basics.css b/css/basics.css
new file mode 100644
index 00000000..dc6b1d08
--- /dev/null
+++ b/css/basics.css
@@ -0,0 +1,60 @@
+/* style for basic HTML elements */
+
+html, body {
+ background: white;
+ color: #222;
+ font: normal 16px sans-serif;
+ margin: 0px;
+ padding: 0px;
+}
+
+h1, h2, h3, h4, h5, h6 {
+ padding: 0px;
+}
+h1 {
+ font-size: 150%;
+ margin: 15px 0px;
+}
+h2 {
+ font-size: 130%;
+ margin: 10px 0px;
+}
+h3 {
+ font-size: 110%;
+ margin: 10px 0px;
+}
+
+a:link {
+ color: #07b;
+ text-decoration: none;
+}
+a:hover {
+ text-decoration: underline;
+}
+a:visited {
+ color: #666;
+}
+
+pre {
+ border: 1px solid #aaa;
+ background-color: white;
+ margin: 5px;
+ padding: 5px;
+ white-space: pre-wrap;
+ overflow-x: auto;
+}
+
+fieldset {
+ border: 1px dotted #999;
+ background: #f6f9fc;
+ font-size: .846em;
+}
+fieldset legend {
+ font-weight: bold;
+ margin-bottom: 5px;
+}
+
+button, input[type="button"] {
+ border: 1px solid #aaa;
+ padding: 3px;
+}
\ No newline at end of file
diff --git a/css/layout.css b/css/layout.css
new file mode 100644
index 00000000..fa32a2f2
--- /dev/null
+++ b/css/layout.css
@@ -0,0 +1,144 @@
+/* style for defining the overall layout of the page (header, navigation, main container) */
+
+ul {
+ margin: 0px;
+}
+
+header nav {
+ margin: 0px;
+ display: block;
+ background-color: #ffffff;
+ border-bottom: 5px #08c solid!important;
+ color: #0891d1;
+ font-weight: bold;
+ cursor: default;
+ width: 100%;
+ z-index: 1000;
+}
+header nav > div {
+ box-sizing: border-box;
+ list-style-type: none;
+ width: 100%;
+ padding-top: 8px;
+ margin-bottom: 10px;
+ padding-left: 0em;
+}
+header nav > div > span {
+ font-size: 90%;
+ font-weight: normal;
+}
+header nav > div img {
+ float: left;
+ height: 40px;
+ margin-left: 15px;
+ margin-right: 10px;
+}
+header nav ul {
+ padding: 0px;
+ margin: 0px;
+ border-top: 1px solid #222;
+}
+header nav li {
+ display: inline-block;
+ margin: 0px;
+ padding: 0px;
+ background-repeat: no-repeat;
+ background-size: auto 45%;
+ background-position: center 20%;
+ opacity: .6;
+}
+header nav li.active {
+ filter: invert(100%);
+}
+header nav ul li a, header nav ul li a:link, header nav ul li a:visited {
+ display: inline-block;
+ position: relative;
+ box-sizing: border-box;
+ text-decoration: none;
+ padding-left: 20px;
+ padding-top: 35px;
+ padding-bottom: 5px;
+ padding-right: 20px;
+ /*color: #08c;*/
+ color: #000;
+ font-weight: bold;
+ font-size: 90%;
+}
+header nav ul li a:hover {
+ text-decoration: none;
+}
+header nav ul li.active a {
+ color: #000;
+ text-decoration: none;
+}
+header nav ul li.progress a:after {
+ content: '';
+ box-sizing: border-box;
+ position: absolute;
+ left: 0px;
+ bottom: 0px;
+ height: 5px;
+ width: 100%;
+ background: #ff7733;
+ animation: loading 2s infinite;
+}
+@keyframes loading {
+ 0% {
+ margin-left: 0%;
+ width: 30%;
+ }
+ 15% {
+ margin-left: 35%;
+ width: 50%;
+ }
+ 50% {
+ margin-left: 70%;
+ width: 30%;
+ }
+ 65% {
+ margin-left: 15%;
+ width: 50%;
+ }
+ 100% {
+ margin-left: 0%;
+ width: 30%;
+ }
+}
+header nav li.active {
+ background-color: #ff7733; /* inverted from #0088cc */
+}
+header nav ul li:hover {
+ opacity: .9;
+}
+@media (min-width: 950px) {
+ header nav {
+ position: fixed;
+ }
+ header nav ul {
+ float: right;
+ border-top: none;
+ }
+ header nav > div {
+ position: absolute;
+ top: 50%;
+ transform: translate(0%, -50%);
+ margin: 0px;
+ padding: 0px;
+ z-index: -1000;
+ }
+}
+
+main {
+ padding-left: 0.5em;
+ padding-right: 0.5em;
+}
+@media (min-width: 950px) {
+ main {
+ padding-top: 55px;
+ }
+}
+
+section {
+ background-color: white;
+ padding: 10px;
+}
diff --git a/css/specifics.css b/css/specifics.css
new file mode 100644
index 00000000..a760739e
--- /dev/null
+++ b/css/specifics.css
@@ -0,0 +1,154 @@
+/* style for page-specific elements */
+
+/* icons of buttons within navigation */
+#back-nav-link {
+ background-image: url(../img/icon/chevron-left.svg);
+ background-size: contain;
+ margin-right: -5px;
+ filter: invert(100%);
+}
+#back-nav-link a {
+ margin-right: 0px;
+ filter: none;
+}
+#intro-nav-link {
+ background-image: url(../img/icon/information.svg);
+}
+#downloads-nav-link {
+ background-image: url(../img/icon/download.svg);
+}
+#doc-nav-link {
+ background-image: url(../img/icon/book-open-variant.svg);
+}
+#contact-nav-link {
+ background-image: url(../img/icon/forum.svg);
+}
+#code-nav-link {
+ background-image: url(../img/icon/code-braces.svg);
+}
+
+/* elements of intro section */
+#intro-section {
+ font-size: 120%;
+ line-height: 140%;
+}
+.banner {
+ margin: -23px;
+ margin-bottom: 10px;
+ background-image: url(../img/screenshots/plasma.png);
+ background-position-y: -758px;
+ background-position-x: right;
+ background-repeat: no-repeat;
+ background-color: #416da0;
+ height: 655px;
+ width: calc(100% + 41px);
+ box-sizing: border-box;
+ border-bottom: 5px #08c solid!important;
+}
+@media (max-width: 1050px) {
+ .banner {
+ background-position-x: -1500px;
+ }
+}
+@media (min-width: 1010px) {
+ .banner-info {
+ height: 0px;
+ }
+ .banner-info p {
+ position: relative;
+ top: -610px;
+ right: 50px;
+ margin-left: auto;
+ width: 300px;
+ background-color: #ffffffad;
+ border: 1px solid #aaa;
+ border-radius: 5px;
+ padding: 10px;
+ line-height: 170%;
+ box-sizing: border-box;
+ }
+}
+@media (min-width: 1730px) {
+ .banner-info p {
+ margin-left: 0px;
+ left: 100px;
+ width: 30%;
+ }
+}
+@media (max-width: 1010px) {
+ .banner-info p {
+ }
+}
+@media (max-width: 950px) {
+ .banner {
+ margin-top: -10px;
+ }
+}
+
+/* elements of downloads section */
+.downloads-grid {
+ display: grid;
+ grid-template-columns: repeat(2, 1fr);
+ gap: 40px;
+}
+.downloads-grid h3 {
+ margin-top: 20px;
+}
+.downloads-grid h3 + p {
+ margin: 10px 0px;
+}
+@media (max-width: 900px) {
+ .downloads-grid {
+ display: block;
+ }
+}
+.downloads-platform {
+ padding: 5px;
+}
+.downloads-platform input[type="checkbox"] {
+ display: none;
+}
+.downloads-platform label {
+ display: block;
+ margin: 0px -5px -10px -5px;
+ padding: 0px;
+ font-weight: bold;
+ /*background-color: #e0e0ff;*/
+ cursor: pointer;
+ user-select: none;
+}
+.downloads-platform label:before {
+ display: inline-block;
+ width: 20px;
+ content: "⮞ ";
+ opacity: 0.5;
+}
+.downloads-platform > ul {
+ list-style: none;
+ padding-bottom: 5px;
+ border-bottom: 1px solid #aaa;
+}
+.downloads-platform input + label ~ * {
+ margin-top: 10px;
+ display: none;
+ padding-left: 0px;
+ margin-left: 20px;
+}
+.downloads-platform input:checked + label ~ * {
+ display: block;
+}
+.downloads-platform input:checked + label:before {
+ content: "⮟ ";
+}
+.downloads-platform p {
+ font-size: 90%;
+ padding: 5px !important;
+ border-radius: 5px;
+ border: 1px solid #ddd;
+ background-color: #e5f3ff;
+}
+
+/* elements of the documentation section */
+#doc-section p {
+ line-height: 140%;
+}
\ No newline at end of file
diff --git a/img/icon/book-open-variant.svg b/img/icon/book-open-variant.svg
new file mode 100644
index 00000000..8ab15a54
--- /dev/null
+++ b/img/icon/book-open-variant.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/img/icon/chevron-left.svg b/img/icon/chevron-left.svg
new file mode 100644
index 00000000..13317b9b
--- /dev/null
+++ b/img/icon/chevron-left.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/img/icon/code-braces.svg b/img/icon/code-braces.svg
new file mode 100644
index 00000000..dd8f3b2c
--- /dev/null
+++ b/img/icon/code-braces.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/img/icon/download.svg b/img/icon/download.svg
new file mode 100644
index 00000000..209c193d
--- /dev/null
+++ b/img/icon/download.svg
@@ -0,0 +1 @@
+
diff --git a/img/icon/forum.svg b/img/icon/forum.svg
new file mode 100644
index 00000000..5286af49
--- /dev/null
+++ b/img/icon/forum.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/img/icon/information.svg b/img/icon/information.svg
new file mode 100644
index 00000000..d8821989
--- /dev/null
+++ b/img/icon/information.svg
@@ -0,0 +1 @@
+
diff --git a/img/logo.svg b/img/logo.svg
new file mode 100644
index 00000000..ff643fd8
--- /dev/null
+++ b/img/logo.svg
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/img/screenshots/plasma.png b/img/screenshots/plasma.png
new file mode 100644
index 00000000..b5f7d064
Binary files /dev/null and b/img/screenshots/plasma.png differ
diff --git a/img/screenshots/webview.png b/img/screenshots/webview.png
new file mode 100644
index 00000000..3d93c9ca
Binary files /dev/null and b/img/screenshots/webview.png differ
diff --git a/index.html b/index.html
new file mode 100644
index 00000000..0b21c624
--- /dev/null
+++ b/index.html
@@ -0,0 +1,242 @@
+
+
+
+ Syncthing Tray
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Syncthing Tray
+
Tray application and Dolphin/Plasma integration for Syncthing
+
+
+
+
+
+
+
+
+
+
+ Syncthing Tray provides a tray icon and further platform integration for Syncthing.
+ It focuses on GNU/Linux and Windows.
+ If you don't know Syncthing yet, it makes most sense to checkout the website of Syncthing itself first.
+
+
+ This website is meant to provide an overview. Checkout the README
+ on GitHub for many more (technical) details.
+
+
+ Syncthing Tray complements the normal web-based UI of Syncthing itself providing the following features:
+
+ A tray icon showing the overall status of Syncthing
+ A tray menu showing more detailed status information that also allows to quickly perform common actions such as triggering a rescan
+ Quick access to the normal web-based UI of Syncthing
+ A launcher for Syncthing (e.g. useful under Windows to avoid having a console window around)
+
+ A context menu for the Dolphin file manager to show the status of a folder and triggering
+ common actions
+
+
+
+ The screenshot shows Syncthing Tray on KDE Plasma .
+ Checkout the screenshots section in the README for
+ screenshots on other platforms.
+
+
+
+ Downloads
+
+ Loading the latest release info from GitHub…
+
+
+ The latest release is .
+ Checkout the releases section
+ on GitHub for older releases.
+
+
+ This page is supposed to provide an overview. Checkout the "Downloads"-section
+ of the README for all options and further details.
+
+
+
+
+ Documentation
+
+ Syncthing Tray contains a wizard that will guide you though the setup of Syncthing Tray and Syncthing. It will allow you to use your currently running
+ Syncthing instance but can also start Syncthing for you (checkout
+
+ the relevant README section for details). Then you are ready to use Syncthing. That means pairing devices and adding folders to sync. For this, you should proceed
+ reading the "Getting started" documentation of Syncthing itself .
+
+
+ Syncthing Tray itself also has many configuration options. So it may be worthwhile to browse though the pages of the configuration dialog to tweak Syncthing
+ Tray to your needs, e.g. to turn off notification you may find annoying. Further documentation on how to configure Syncthing Tray can also be found in the README
+ as of the "General remarks on the configuration"
+ section .
+
+ License and attribution
+
+ Copyright © Marius Kittler
+
+
+ Syncthing Tray is licensed under GPL-2-or-later. It also contains 3rd party content. Checkout the sections about licensing and attributions in
+ the README on GitHub for details. The icons on this website are from
+ Material Design Icons .
+
+
+
+
+
+
diff --git a/js/ajaxhelper.js b/js/ajaxhelper.js
new file mode 100644
index 00000000..eff4a963
--- /dev/null
+++ b/js/ajaxhelper.js
@@ -0,0 +1,29 @@
+/// \brief Makes an AJAX query with basic error handling.
+export function queryRoute(method, path, callback) {
+ if ((window.location.protocol === 'file:' || window.location.hostname === 'localhost') && path.includes('releases')) {
+ return callback({
+ responseText: '[{ "url": "https://api.github.com/repos/Martchus/syncthingtray/releases/150394390", "assets_url": "https://api.github.com/repos/Martchus/syncthingtray/releases/150394390/assets", "upload_url": "https://uploads.github.com/repos/Martchus/syncthingtray/releases/150394390/assets{?name,label}", "html_url": "https://github.com/Martchus/syncthingtray/releases/tag/v1.5.2", "id": 150394390, "author": { "login": "Martchus", "id": 10248953, "node_id": "MDQ6VXNlcjEwMjQ4OTUz", "avatar_url": "https://avatars.githubusercontent.com/u/10248953?v=4", "gravatar_id": "", "url": "https://api.github.com/users/Martchus", "html_url": "https://github.com/Martchus", "followers_url": "https://api.github.com/users/Martchus/followers", "following_url": "https://api.github.com/users/Martchus/following{/other_user}", "gists_url": "https://api.github.com/users/Martchus/gists{/gist_id}", "starred_url": "https://api.github.com/users/Martchus/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/Martchus/subscriptions", "organizations_url": "https://api.github.com/users/Martchus/orgs", "repos_url": "https://api.github.com/users/Martchus/repos", "events_url": "https://api.github.com/users/Martchus/events{/privacy}", "received_events_url": "https://api.github.com/users/Martchus/received_events", "type": "User", "site_admin": false }, "node_id": "RE_kwDOA_bJvs4I9tYW", "tag_name": "v1.5.2", "target_commitish": "master", "name": "v1.5.2", "draft": false, "prerelease": false, "created_at": "2024-04-09T10:05:52Z", "published_at": "2024-04-09T13:05:48Z", "assets": [ { "url": "https://api.github.com/repos/Martchus/syncthingtray/releases/assets/161136826", "id": 161136826, "node_id": "RA_kwDOA_bJvs4JmsC6", "name": "syncthingctl-1.5.2-i686-w64-mingw32.exe.zip", "label": "", "uploader": { "login": "Martchus", "id": 10248953, "node_id": "MDQ6VXNlcjEwMjQ4OTUz", "avatar_url": "https://avatars.githubusercontent.com/u/10248953?v=4", "gravatar_id": "", "url": "https://api.github.com/users/Martchus", "html_url": "https://github.com/Martchus", "followers_url": "https://api.github.com/users/Martchus/followers", "following_url": "https://api.github.com/users/Martchus/following{/other_user}", "gists_url": "https://api.github.com/users/Martchus/gists{/gist_id}", "starred_url": "https://api.github.com/users/Martchus/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/Martchus/subscriptions", "organizations_url": "https://api.github.com/users/Martchus/orgs", "repos_url": "https://api.github.com/users/Martchus/repos", "events_url": "https://api.github.com/users/Martchus/events{/privacy}", "received_events_url": "https://api.github.com/users/Martchus/received_events", "type": "User", "site_admin": false }, "content_type": "application/octet-stream", "state": "uploaded", "size": 13850402, "download_count": 108, "created_at": "2024-04-09T13:25:05Z", "updated_at": "2024-04-09T13:25:17Z", "browser_download_url": "https://github.com/Martchus/syncthingtray/releases/download/v1.5.2/syncthingctl-1.5.2-i686-w64-mingw32.exe.zip" }, { "url": "https://api.github.com/repos/Martchus/syncthingtray/releases/assets/161136839", "id": 161136839, "node_id": "RA_kwDOA_bJvs4JmsDH", "name": "syncthingctl-1.5.2-i686-w64-mingw32.exe.zip.sig", "label": "", "uploader": { "login": "Martchus", "id": 10248953, "node_id": "MDQ6VXNlcjEwMjQ4OTUz", "avatar_url": "https://avatars.githubusercontent.com/u/10248953?v=4", "gravatar_id": "", "url": "https://api.github.com/users/Martchus", "html_url": "https://github.com/Martchus", "followers_url": "https://api.github.com/users/Martchus/followers", "following_url": "https://api.github.com/users/Martchus/following{/other_user}", "gists_url": "https://api.github.com/users/Martchus/gists{/gist_id}", "starred_url": "https://api.github.com/users/Martchus/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/Martchus/subscriptions", "organizations_url": "https://api.github.com/users/Martchus/orgs", "repos_url": "https://api.github.com/users/Martchus/repos", "events_url": "https://api.github.com/users/Martchus/events{/privacy}", "received_events_url": "https://api.github.com/users/Martchus/received_events", "type": "User", "site_admin": false }, "content_type": "application/octet-stream", "state": "uploaded", "size": 310, "download_count": 5, "created_at": "2024-04-09T13:25:19Z", "updated_at": "2024-04-09T13:25:19Z", "browser_download_url": "https://github.com/Martchus/syncthingtray/releases/download/v1.5.2/syncthingctl-1.5.2-i686-w64-mingw32.exe.zip.sig" }, { "url": "https://api.github.com/repos/Martchus/syncthingtray/releases/assets/161136877", "id": 161136877, "node_id": "RA_kwDOA_bJvs4JmsDt", "name": "syncthingctl-1.5.2-x86_64-pc-linux-gnu.tar.xz", "label": "", "uploader": { "login": "Martchus", "id": 10248953, "node_id": "MDQ6VXNlcjEwMjQ4OTUz", "avatar_url": "https://avatars.githubusercontent.com/u/10248953?v=4", "gravatar_id": "", "url": "https://api.github.com/users/Martchus", "html_url": "https://github.com/Martchus", "followers_url": "https://api.github.com/users/Martchus/followers", "following_url": "https://api.github.com/users/Martchus/following{/other_user}", "gists_url": "https://api.github.com/users/Martchus/gists{/gist_id}", "starred_url": "https://api.github.com/users/Martchus/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/Martchus/subscriptions", "organizations_url": "https://api.github.com/users/Martchus/orgs", "repos_url": "https://api.github.com/users/Martchus/repos", "events_url": "https://api.github.com/users/Martchus/events{/privacy}", "received_events_url": "https://api.github.com/users/Martchus/received_events", "type": "User", "site_admin": false }, "content_type": "application/octet-stream", "state": "uploaded", "size": 10403332, "download_count": 19, "created_at": "2024-04-09T13:25:56Z", "updated_at": "2024-04-09T13:25:58Z", "browser_download_url": "https://github.com/Martchus/syncthingtray/releases/download/v1.5.2/syncthingctl-1.5.2-x86_64-pc-linux-gnu.tar.xz" }, { "url": "https://api.github.com/repos/Martchus/syncthingtray/releases/assets/161136881", "id": 161136881, "node_id": "RA_kwDOA_bJvs4JmsDx", "name": "syncthingctl-1.5.2-x86_64-pc-linux-gnu.tar.xz.sig", "label": "", "uploader": { "login": "Martchus", "id": 10248953, "node_id": "MDQ6VXNlcjEwMjQ4OTUz", "avatar_url": "https://avatars.githubusercontent.com/u/10248953?v=4", "gravatar_id": "", "url": "https://api.github.com/users/Martchus", "html_url": "https://github.com/Martchus", "followers_url": "https://api.github.com/users/Martchus/followers", "following_url": "https://api.github.com/users/Martchus/following{/other_user}", "gists_url": "https://api.github.com/users/Martchus/gists{/gist_id}", "starred_url": "https://api.github.com/users/Martchus/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/Martchus/subscriptions", "organizations_url": "https://api.github.com/users/Martchus/orgs", "repos_url": "https://api.github.com/users/Martchus/repos", "events_url": "https://api.github.com/users/Martchus/events{/privacy}", "received_events_url": "https://api.github.com/users/Martchus/received_events", "type": "User", "site_admin": false }, "content_type": "application/octet-stream", "state": "uploaded", "size": 310, "download_count": 4, "created_at": "2024-04-09T13:26:00Z", "updated_at": "2024-04-09T13:26:00Z", "browser_download_url": "https://github.com/Martchus/syncthingtray/releases/download/v1.5.2/syncthingctl-1.5.2-x86_64-pc-linux-gnu.tar.xz.sig" }, { "url": "https://api.github.com/repos/Martchus/syncthingtray/releases/assets/161136862", "id": 161136862, "node_id": "RA_kwDOA_bJvs4JmsDe", "name": "syncthingctl-1.5.2-x86_64-w64-mingw32.exe.zip", "label": "", "uploader": { "login": "Martchus", "id": 10248953, "node_id": "MDQ6VXNlcjEwMjQ4OTUz", "avatar_url": "https://avatars.githubusercontent.com/u/10248953?v=4", "gravatar_id": "", "url": "https://api.github.com/users/Martchus", "html_url": "https://github.com/Martchus", "followers_url": "https://api.github.com/users/Martchus/followers", "following_url": "https://api.github.com/users/Martchus/following{/other_user}", "gists_url": "https://api.github.com/users/Martchus/gists{/gist_id}", "starred_url": "https://api.github.com/users/Martchus/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/Martchus/subscriptions", "organizations_url": "https://api.github.com/users/Martchus/orgs", "repos_url": "https://api.github.com/users/Martchus/repos", "events_url": "https://api.github.com/users/Martchus/events{/privacy}", "received_events_url": "https://api.github.com/users/Martchus/received_events", "type": "User", "site_admin": false }, "content_type": "application/octet-stream", "state": "uploaded", "size": 13764933, "download_count": 217, "created_at": "2024-04-09T13:25:40Z", "updated_at": "2024-04-09T13:25:43Z", "browser_download_url": "https://github.com/Martchus/syncthingtray/releases/download/v1.5.2/syncthingctl-1.5.2-x86_64-w64-mingw32.exe.zip" }, { "url": "https://api.github.com/repos/Martchus/syncthingtray/releases/assets/161136865", "id": 161136865, "node_id": "RA_kwDOA_bJvs4JmsDh", "name": "syncthingctl-1.5.2-x86_64-w64-mingw32.exe.zip.sig", "label": "", "uploader": { "login": "Martchus", "id": 10248953, "node_id": "MDQ6VXNlcjEwMjQ4OTUz", "avatar_url": "https://avatars.githubusercontent.com/u/10248953?v=4", "gravatar_id": "", "url": "https://api.github.com/users/Martchus", "html_url": "https://github.com/Martchus", "followers_url": "https://api.github.com/users/Martchus/followers", "following_url": "https://api.github.com/users/Martchus/following{/other_user}", "gists_url": "https://api.github.com/users/Martchus/gists{/gist_id}", "starred_url": "https://api.github.com/users/Martchus/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/Martchus/subscriptions", "organizations_url": "https://api.github.com/users/Martchus/orgs", "repos_url": "https://api.github.com/users/Martchus/repos", "events_url": "https://api.github.com/users/Martchus/events{/privacy}", "received_events_url": "https://api.github.com/users/Martchus/received_events", "type": "User", "site_admin": false }, "content_type": "application/octet-stream", "state": "uploaded", "size": 310, "download_count": 15, "created_at": "2024-04-09T13:25:44Z", "updated_at": "2024-04-09T13:25:45Z", "browser_download_url": "https://github.com/Martchus/syncthingtray/releases/download/v1.5.2/syncthingctl-1.5.2-x86_64-w64-mingw32.exe.zip.sig" }, { "url": "https://api.github.com/repos/Martchus/syncthingtray/releases/assets/161136753", "id": 161136753, "node_id": "RA_kwDOA_bJvs4JmsBx", "name": "syncthingctl-qt5-1.5.2-i686-w64-mingw32.exe.zip", "label": "", "uploader": { "login": "Martchus", "id": 10248953, "node_id": "MDQ6VXNlcjEwMjQ4OTUz", "avatar_url": "https://avatars.githubusercontent.com/u/10248953?v=4", "gravatar_id": "", "url": "https://api.github.com/users/Martchus", "html_url": "https://github.com/Martchus", "followers_url": "https://api.github.com/users/Martchus/followers", "following_url": "https://api.github.com/users/Martchus/following{/other_user}", "gists_url": "https://api.github.com/users/Martchus/gists{/gist_id}", "starred_url": "https://api.github.com/users/Martchus/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/Martchus/subscriptions", "organizations_url": "https://api.github.com/users/Martchus/orgs", "repos_url": "https://api.github.com/users/Martchus/repos", "events_url": "https://api.github.com/users/Martchus/events{/privacy}", "received_events_url": "https://api.github.com/users/Martchus/received_events", "type": "User", "site_admin": false }, "content_type": "application/octet-stream", "state": "uploaded", "size": 9749916, "download_count": 11, "created_at": "2024-04-09T13:24:29Z", "updated_at": "2024-04-09T13:24:32Z", "browser_download_url": "https://github.com/Martchus/syncthingtray/releases/download/v1.5.2/syncthingctl-qt5-1.5.2-i686-w64-mingw32.exe.zip" }, { "url": "https://api.github.com/repos/Martchus/syncthingtray/releases/assets/161136759", "id": 161136759, "node_id": "RA_kwDOA_bJvs4JmsB3", "name": "syncthingctl-qt5-1.5.2-i686-w64-mingw32.exe.zip.sig", "label": "", "uploader": { "login": "Martchus", "id": 10248953, "node_id": "MDQ6VXNlcjEwMjQ4OTUz", "avatar_url": "https://avatars.githubusercontent.com/u/10248953?v=4", "gravatar_id": "", "url": "https://api.github.com/users/Martchus", "html_url": "https://github.com/Martchus", "followers_url": "https://api.github.com/users/Martchus/followers", "following_url": "https://api.github.com/users/Martchus/following{/other_user}", "gists_url": "https://api.github.com/users/Martchus/gists{/gist_id}", "starred_url": "https://api.github.com/users/Martchus/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/Martchus/subscriptions", "organizations_url": "https://api.github.com/users/Martchus/orgs", "repos_url": "https://api.github.com/users/Martchus/repos", "events_url": "https://api.github.com/users/Martchus/events{/privacy}", "received_events_url": "https://api.github.com/users/Martchus/received_events", "type": "User", "site_admin": false }, "content_type": "application/octet-stream", "state": "uploaded", "size": 310, "download_count": 1, "created_at": "2024-04-09T13:24:34Z", "updated_at": "2024-04-09T13:24:34Z", "browser_download_url": "https://github.com/Martchus/syncthingtray/releases/download/v1.5.2/syncthingctl-qt5-1.5.2-i686-w64-mingw32.exe.zip.sig" }, { "url": "https://api.github.com/repos/Martchus/syncthingtray/releases/assets/161136789", "id": 161136789, "node_id": "RA_kwDOA_bJvs4JmsCV", "name": "syncthingctl-qt5-1.5.2-x86_64-w64-mingw32.exe.zip", "label": "", "uploader": { "login": "Martchus", "id": 10248953, "node_id": "MDQ6VXNlcjEwMjQ4OTUz", "avatar_url": "https://avatars.githubusercontent.com/u/10248953?v=4", "gravatar_id": "", "url": "https://api.github.com/users/Martchus", "html_url": "https://github.com/Martchus", "followers_url": "https://api.github.com/users/Martchus/followers", "following_url": "https://api.github.com/users/Martchus/following{/other_user}", "gists_url": "https://api.github.com/users/Martchus/gists{/gist_id}", "starred_url": "https://api.github.com/users/Martchus/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/Martchus/subscriptions", "organizations_url": "https://api.github.com/users/Martchus/orgs", "repos_url": "https://api.github.com/users/Martchus/repos", "events_url": "https://api.github.com/users/Martchus/events{/privacy}", "received_events_url": "https://api.github.com/users/Martchus/received_events", "type": "User", "site_admin": false }, "content_type": "application/octet-stream", "state": "uploaded", "size": 9568527, "download_count": 71, "created_at": "2024-04-09T13:24:45Z", "updated_at": "2024-04-09T13:24:47Z", "browser_download_url": "https://github.com/Martchus/syncthingtray/releases/download/v1.5.2/syncthingctl-qt5-1.5.2-x86_64-w64-mingw32.exe.zip" }, { "url": "https://api.github.com/repos/Martchus/syncthingtray/releases/assets/161136792", "id": 161136792, "node_id": "RA_kwDOA_bJvs4JmsCY", "name": "syncthingctl-qt5-1.5.2-x86_64-w64-mingw32.exe.zip.sig", "label": "", "uploader": { "login": "Martchus", "id": 10248953, "node_id": "MDQ6VXNlcjEwMjQ4OTUz", "avatar_url": "https://avatars.githubusercontent.com/u/10248953?v=4", "gravatar_id": "", "url": "https://api.github.com/users/Martchus", "html_url": "https://github.com/Martchus", "followers_url": "https://api.github.com/users/Martchus/followers", "following_url": "https://api.github.com/users/Martchus/following{/other_user}", "gists_url": "https://api.github.com/users/Martchus/gists{/gist_id}", "starred_url": "https://api.github.com/users/Martchus/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/Martchus/subscriptions", "organizations_url": "https://api.github.com/users/Martchus/orgs", "repos_url": "https://api.github.com/users/Martchus/repos", "events_url": "https://api.github.com/users/Martchus/events{/privacy}", "received_events_url": "https://api.github.com/users/Martchus/received_events", "type": "User", "site_admin": false }, "content_type": "application/octet-stream", "state": "uploaded", "size": 310, "download_count": 4, "created_at": "2024-04-09T13:24:49Z", "updated_at": "2024-04-09T13:24:49Z", "browser_download_url": "https://github.com/Martchus/syncthingtray/releases/download/v1.5.2/syncthingctl-qt5-1.5.2-x86_64-w64-mingw32.exe.zip.sig" }, { "url": "https://api.github.com/repos/Martchus/syncthingtray/releases/assets/161136842", "id": 161136842, "node_id": "RA_kwDOA_bJvs4JmsDK", "name": "syncthingtray-1.5.2-i686-w64-mingw32.exe.zip", "label": "", "uploader": { "login": "Martchus", "id": 10248953, "node_id": "MDQ6VXNlcjEwMjQ4OTUz", "avatar_url": "https://avatars.githubusercontent.com/u/10248953?v=4", "gravatar_id": "", "url": "https://api.github.com/users/Martchus", "html_url": "https://github.com/Martchus", "followers_url": "https://api.github.com/users/Martchus/followers", "following_url": "https://api.github.com/users/Martchus/following{/other_user}", "gists_url": "https://api.github.com/users/Martchus/gists{/gist_id}", "starred_url": "https://api.github.com/users/Martchus/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/Martchus/subscriptions", "organizations_url": "https://api.github.com/users/Martchus/orgs", "repos_url": "https://api.github.com/users/Martchus/repos", "events_url": "https://api.github.com/users/Martchus/events{/privacy}", "received_events_url": "https://api.github.com/users/Martchus/received_events", "type": "User", "site_admin": false }, "content_type": "application/octet-stream", "state": "uploaded", "size": 27204886, "download_count": 105, "created_at": "2024-04-09T13:25:21Z", "updated_at": "2024-04-09T13:25:35Z", "browser_download_url": "https://github.com/Martchus/syncthingtray/releases/download/v1.5.2/syncthingtray-1.5.2-i686-w64-mingw32.exe.zip" }, { "url": "https://api.github.com/repos/Martchus/syncthingtray/releases/assets/161136861", "id": 161136861, "node_id": "RA_kwDOA_bJvs4JmsDd", "name": "syncthingtray-1.5.2-i686-w64-mingw32.exe.zip.sig", "label": "", "uploader": { "login": "Martchus", "id": 10248953, "node_id": "MDQ6VXNlcjEwMjQ4OTUz", "avatar_url": "https://avatars.githubusercontent.com/u/10248953?v=4", "gravatar_id": "", "url": "https://api.github.com/users/Martchus", "html_url": "https://github.com/Martchus", "followers_url": "https://api.github.com/users/Martchus/followers", "following_url": "https://api.github.com/users/Martchus/following{/other_user}", "gists_url": "https://api.github.com/users/Martchus/gists{/gist_id}", "starred_url": "https://api.github.com/users/Martchus/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/Martchus/subscriptions", "organizations_url": "https://api.github.com/users/Martchus/orgs", "repos_url": "https://api.github.com/users/Martchus/repos", "events_url": "https://api.github.com/users/Martchus/events{/privacy}", "received_events_url": "https://api.github.com/users/Martchus/received_events", "type": "User", "site_admin": false }, "content_type": "application/octet-stream", "state": "uploaded", "size": 310, "download_count": 1, "created_at": "2024-04-09T13:25:37Z", "updated_at": "2024-04-09T13:25:37Z", "browser_download_url": "https://github.com/Martchus/syncthingtray/releases/download/v1.5.2/syncthingtray-1.5.2-i686-w64-mingw32.exe.zip.sig" }, { "url": "https://api.github.com/repos/Martchus/syncthingtray/releases/assets/161136885", "id": 161136885, "node_id": "RA_kwDOA_bJvs4JmsD1", "name": "syncthingtray-1.5.2-x86_64-pc-linux-gnu.tar.xz", "label": "", "uploader": { "login": "Martchus", "id": 10248953, "node_id": "MDQ6VXNlcjEwMjQ4OTUz", "avatar_url": "https://avatars.githubusercontent.com/u/10248953?v=4", "gravatar_id": "", "url": "https://api.github.com/users/Martchus", "html_url": "https://github.com/Martchus", "followers_url": "https://api.github.com/users/Martchus/followers", "following_url": "https://api.github.com/users/Martchus/following{/other_user}", "gists_url": "https://api.github.com/users/Martchus/gists{/gist_id}", "starred_url": "https://api.github.com/users/Martchus/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/Martchus/subscriptions", "organizations_url": "https://api.github.com/users/Martchus/orgs", "repos_url": "https://api.github.com/users/Martchus/repos", "events_url": "https://api.github.com/users/Martchus/events{/privacy}", "received_events_url": "https://api.github.com/users/Martchus/received_events", "type": "User", "site_admin": false }, "content_type": "application/octet-stream", "state": "uploaded", "size": 22288692, "download_count": 35, "created_at": "2024-04-09T13:26:02Z", "updated_at": "2024-04-09T13:26:07Z", "browser_download_url": "https://github.com/Martchus/syncthingtray/releases/download/v1.5.2/syncthingtray-1.5.2-x86_64-pc-linux-gnu.tar.xz" }, { "url": "https://api.github.com/repos/Martchus/syncthingtray/releases/assets/161136895", "id": 161136895, "node_id": "RA_kwDOA_bJvs4JmsD_", "name": "syncthingtray-1.5.2-x86_64-pc-linux-gnu.tar.xz.sig", "label": "", "uploader": { "login": "Martchus", "id": 10248953, "node_id": "MDQ6VXNlcjEwMjQ4OTUz", "avatar_url": "https://avatars.githubusercontent.com/u/10248953?v=4", "gravatar_id": "", "url": "https://api.github.com/users/Martchus", "html_url": "https://github.com/Martchus", "followers_url": "https://api.github.com/users/Martchus/followers", "following_url": "https://api.github.com/users/Martchus/following{/other_user}", "gists_url": "https://api.github.com/users/Martchus/gists{/gist_id}", "starred_url": "https://api.github.com/users/Martchus/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/Martchus/subscriptions", "organizations_url": "https://api.github.com/users/Martchus/orgs", "repos_url": "https://api.github.com/users/Martchus/repos", "events_url": "https://api.github.com/users/Martchus/events{/privacy}", "received_events_url": "https://api.github.com/users/Martchus/received_events", "type": "User", "site_admin": false }, "content_type": "application/octet-stream", "state": "uploaded", "size": 310, "download_count": 3, "created_at": "2024-04-09T13:26:09Z", "updated_at": "2024-04-09T13:26:09Z", "browser_download_url": "https://github.com/Martchus/syncthingtray/releases/download/v1.5.2/syncthingtray-1.5.2-x86_64-pc-linux-gnu.tar.xz.sig" }, { "url": "https://api.github.com/repos/Martchus/syncthingtray/releases/assets/161136867", "id": 161136867, "node_id": "RA_kwDOA_bJvs4JmsDj", "name": "syncthingtray-1.5.2-x86_64-w64-mingw32.exe.zip", "label": "", "uploader": { "login": "Martchus", "id": 10248953, "node_id": "MDQ6VXNlcjEwMjQ4OTUz", "avatar_url": "https://avatars.githubusercontent.com/u/10248953?v=4", "gravatar_id": "", "url": "https://api.github.com/users/Martchus", "html_url": "https://github.com/Martchus", "followers_url": "https://api.github.com/users/Martchus/followers", "following_url": "https://api.github.com/users/Martchus/following{/other_user}", "gists_url": "https://api.github.com/users/Martchus/gists{/gist_id}", "starred_url": "https://api.github.com/users/Martchus/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/Martchus/subscriptions", "organizations_url": "https://api.github.com/users/Martchus/orgs", "repos_url": "https://api.github.com/users/Martchus/repos", "events_url": "https://api.github.com/users/Martchus/events{/privacy}", "received_events_url": "https://api.github.com/users/Martchus/received_events", "type": "User", "site_admin": false }, "content_type": "application/octet-stream", "state": "uploaded", "size": 27343753, "download_count": 1469, "created_at": "2024-04-09T13:25:46Z", "updated_at": "2024-04-09T13:25:52Z", "browser_download_url": "https://github.com/Martchus/syncthingtray/releases/download/v1.5.2/syncthingtray-1.5.2-x86_64-w64-mingw32.exe.zip" }, { "url": "https://api.github.com/repos/Martchus/syncthingtray/releases/assets/161136875", "id": 161136875, "node_id": "RA_kwDOA_bJvs4JmsDr", "name": "syncthingtray-1.5.2-x86_64-w64-mingw32.exe.zip.sig", "label": "", "uploader": { "login": "Martchus", "id": 10248953, "node_id": "MDQ6VXNlcjEwMjQ4OTUz", "avatar_url": "https://avatars.githubusercontent.com/u/10248953?v=4", "gravatar_id": "", "url": "https://api.github.com/users/Martchus", "html_url": "https://github.com/Martchus", "followers_url": "https://api.github.com/users/Martchus/followers", "following_url": "https://api.github.com/users/Martchus/following{/other_user}", "gists_url": "https://api.github.com/users/Martchus/gists{/gist_id}", "starred_url": "https://api.github.com/users/Martchus/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/Martchus/subscriptions", "organizations_url": "https://api.github.com/users/Martchus/orgs", "repos_url": "https://api.github.com/users/Martchus/repos", "events_url": "https://api.github.com/users/Martchus/events{/privacy}", "received_events_url": "https://api.github.com/users/Martchus/received_events", "type": "User", "site_admin": false }, "content_type": "application/octet-stream", "state": "uploaded", "size": 310, "download_count": 11, "created_at": "2024-04-09T13:25:54Z", "updated_at": "2024-04-09T13:25:54Z", "browser_download_url": "https://github.com/Martchus/syncthingtray/releases/download/v1.5.2/syncthingtray-1.5.2-x86_64-w64-mingw32.exe.zip.sig" }, { "url": "https://api.github.com/repos/Martchus/syncthingtray/releases/assets/161136760", "id": 161136760, "node_id": "RA_kwDOA_bJvs4JmsB4", "name": "syncthingtray-qt5-1.5.2-i686-w64-mingw32.exe.zip", "label": "", "uploader": { "login": "Martchus", "id": 10248953, "node_id": "MDQ6VXNlcjEwMjQ4OTUz", "avatar_url": "https://avatars.githubusercontent.com/u/10248953?v=4", "gravatar_id": "", "url": "https://api.github.com/users/Martchus", "html_url": "https://github.com/Martchus", "followers_url": "https://api.github.com/users/Martchus/followers", "following_url": "https://api.github.com/users/Martchus/following{/other_user}", "gists_url": "https://api.github.com/users/Martchus/gists{/gist_id}", "starred_url": "https://api.github.com/users/Martchus/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/Martchus/subscriptions", "organizations_url": "https://api.github.com/users/Martchus/orgs", "repos_url": "https://api.github.com/users/Martchus/repos", "events_url": "https://api.github.com/users/Martchus/events{/privacy}", "received_events_url": "https://api.github.com/users/Martchus/received_events", "type": "User", "site_admin": false }, "content_type": "application/octet-stream", "state": "uploaded", "size": 27125311, "download_count": 15, "created_at": "2024-04-09T13:24:36Z", "updated_at": "2024-04-09T13:24:41Z", "browser_download_url": "https://github.com/Martchus/syncthingtray/releases/download/v1.5.2/syncthingtray-qt5-1.5.2-i686-w64-mingw32.exe.zip" }, { "url": "https://api.github.com/repos/Martchus/syncthingtray/releases/assets/161136781", "id": 161136781, "node_id": "RA_kwDOA_bJvs4JmsCN", "name": "syncthingtray-qt5-1.5.2-i686-w64-mingw32.exe.zip.sig", "label": "", "uploader": { "login": "Martchus", "id": 10248953, "node_id": "MDQ6VXNlcjEwMjQ4OTUz", "avatar_url": "https://avatars.githubusercontent.com/u/10248953?v=4", "gravatar_id": "", "url": "https://api.github.com/users/Martchus", "html_url": "https://github.com/Martchus", "followers_url": "https://api.github.com/users/Martchus/followers", "following_url": "https://api.github.com/users/Martchus/following{/other_user}", "gists_url": "https://api.github.com/users/Martchus/gists{/gist_id}", "starred_url": "https://api.github.com/users/Martchus/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/Martchus/subscriptions", "organizations_url": "https://api.github.com/users/Martchus/orgs", "repos_url": "https://api.github.com/users/Martchus/repos", "events_url": "https://api.github.com/users/Martchus/events{/privacy}", "received_events_url": "https://api.github.com/users/Martchus/received_events", "type": "User", "site_admin": false }, "content_type": "application/octet-stream", "state": "uploaded", "size": 310, "download_count": 1, "created_at": "2024-04-09T13:24:43Z", "updated_at": "2024-04-09T13:24:43Z", "browser_download_url": "https://github.com/Martchus/syncthingtray/releases/download/v1.5.2/syncthingtray-qt5-1.5.2-i686-w64-mingw32.exe.zip.sig" }, { "url": "https://api.github.com/repos/Martchus/syncthingtray/releases/assets/161136796", "id": 161136796, "node_id": "RA_kwDOA_bJvs4JmsCc", "name": "syncthingtray-qt5-1.5.2-x86_64-w64-mingw32.exe.zip", "label": "", "uploader": { "login": "Martchus", "id": 10248953, "node_id": "MDQ6VXNlcjEwMjQ4OTUz", "avatar_url": "https://avatars.githubusercontent.com/u/10248953?v=4", "gravatar_id": "", "url": "https://api.github.com/users/Martchus", "html_url": "https://github.com/Martchus", "followers_url": "https://api.github.com/users/Martchus/followers", "following_url": "https://api.github.com/users/Martchus/following{/other_user}", "gists_url": "https://api.github.com/users/Martchus/gists{/gist_id}", "starred_url": "https://api.github.com/users/Martchus/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/Martchus/subscriptions", "organizations_url": "https://api.github.com/users/Martchus/orgs", "repos_url": "https://api.github.com/users/Martchus/repos", "events_url": "https://api.github.com/users/Martchus/events{/privacy}", "received_events_url": "https://api.github.com/users/Martchus/received_events", "type": "User", "site_admin": false }, "content_type": "application/octet-stream", "state": "uploaded", "size": 26943294, "download_count": 134, "created_at": "2024-04-09T13:24:51Z", "updated_at": "2024-04-09T13:24:57Z", "browser_download_url": "https://github.com/Martchus/syncthingtray/releases/download/v1.5.2/syncthingtray-qt5-1.5.2-x86_64-w64-mingw32.exe.zip" }, { "url": "https://api.github.com/repos/Martchus/syncthingtray/releases/assets/161136811", "id": 161136811, "node_id": "RA_kwDOA_bJvs4JmsCr", "name": "syncthingtray-qt5-1.5.2-x86_64-w64-mingw32.exe.zip.sig", "label": "", "uploader": { "login": "Martchus", "id": 10248953, "node_id": "MDQ6VXNlcjEwMjQ4OTUz", "avatar_url": "https://avatars.githubusercontent.com/u/10248953?v=4", "gravatar_id": "", "url": "https://api.github.com/users/Martchus", "html_url": "https://github.com/Martchus", "followers_url": "https://api.github.com/users/Martchus/followers", "following_url": "https://api.github.com/users/Martchus/following{/other_user}", "gists_url": "https://api.github.com/users/Martchus/gists{/gist_id}", "starred_url": "https://api.github.com/users/Martchus/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/Martchus/subscriptions", "organizations_url": "https://api.github.com/users/Martchus/orgs", "repos_url": "https://api.github.com/users/Martchus/repos", "events_url": "https://api.github.com/users/Martchus/events{/privacy}", "received_events_url": "https://api.github.com/users/Martchus/received_events", "type": "User", "site_admin": false }, "content_type": "application/octet-stream", "state": "uploaded", "size": 310, "download_count": 4, "created_at": "2024-04-09T13:25:03Z", "updated_at": "2024-04-09T13:25:03Z", "browser_download_url": "https://github.com/Martchus/syncthingtray/releases/download/v1.5.2/syncthingtray-qt5-1.5.2-x86_64-w64-mingw32.exe.zip.sig" } ], "tarball_url": "https://api.github.com/repos/Martchus/syncthingtray/tarball/v1.5.2", "zipball_url": "https://api.github.com/repos/Martchus/syncthingtray/zipball/v1.5.2", "body": "v1.5.2", "reactions": { "url": "https://api.github.com/repos/Martchus/syncthingtray/releases/150394390/reactions", "total_count": 4, "+1": 0, "-1": 0, "laugh": 0, "hooray": 4, "confused": 0, "heart": 0, "rocket": 0, "eyes": 0 } }]'
+ }, true);
+ }
+
+ const ajaxRequest = new XMLHttpRequest();
+ ajaxRequest.onreadystatechange = function() {
+ if (this.readyState !== 4) {
+ return;
+ }
+ try {
+ // avoid showing HTML code from gateway
+ ajaxRequest.responseTextDisplay = ajaxRequest.status >= 500 && ajaxRequest.status < 600 ? 'internal server error' : ajaxRequest.responseText;
+ return callback(this, ajaxRequest.status === 200);
+ } catch (e) {
+ window.alert('Unable to process server response: ' + e);
+ throw e;
+ }
+ }
+ ;
+
+ const args = [method, path, true];
+ ajaxRequest.open(...args);
+ ajaxRequest.send();
+ return ajaxRequest;
+}
diff --git a/js/genericrendering.js b/js/genericrendering.js
new file mode 100644
index 00000000..64ff1d5c
--- /dev/null
+++ b/js/genericrendering.js
@@ -0,0 +1,641 @@
+/// \brief Renders the specified \a value as text or a grey 'none' if the value is 'none' or empty.
+export function renderNoneInGrey(value, row, elementName, noneText)
+{
+ const noValue = value === undefined || value === null || value === 'none' || value === 'None' ||
+ value === '' || value === 18446744073709552000;
+ const element = document.createElement((noValue || elementName === undefined) ? 'span' : elementName);
+ if (noValue) {
+ element.appendChild(document.createTextNode(noneText || 'none'));
+ element.style.color = 'grey';
+ element.dataset.isNone = true;
+ } else if (typeof value === 'boolean') {
+ element.appendChild(document.createTextNode(value ? 'yes' : 'no'));
+ } else {
+ element.appendChild(document.createTextNode(value));
+ }
+ return element;
+}
+
+/// \brief Renders a standard table cell.
+/// \remarks This is the default renderer used by renderTableFromJsonArray() and renderTableFromJsonObject().
+export function renderStandardTableCell(data, allData, level)
+{
+ const dataType = typeof data;
+ if (dataType !== 'object') {
+ return renderNoneInGrey(data);
+ }
+ if (!Array.isArray(data)) {
+ if (level !== undefined && level > 1) {
+ return renderNoneInGrey('', data, undefined, 'rendering stopped at this level')
+ }
+ return renderTableFromJsonObject({data: data, level: level !== undefined ? level + 1 : 1});
+ }
+ if (data.length === 0) {
+ return renderNoneInGrey('', data, undefined, 'empty array');
+ }
+ const ul = document.createElement('ul');
+ data.forEach(function(element) {
+ const li = document.createElement('li');
+ li.appendChild(renderStandardTableCell(element));
+ ul.appendChild(li);
+ });
+ return ul;
+}
+
+/// \brief Renders a custom list.
+export function renderCustomList(array, customRenderer, compareFunction)
+{
+ if (!Array.isArray(array) || array.length < 1) {
+ return renderNoneInGrey();
+ }
+ if (compareFunction !== undefined) {
+ array.sort(compareFunction);
+ }
+ const ul = document.createElement('ul');
+ array.forEach(function(arrayElement) {
+ const li = document.createElement('li');
+ const renderedDomElements = customRenderer(arrayElement, li);
+ if (Array.isArray(renderedDomElements)) {
+ renderedDomElements.forEach(function(renderedDomElement) {
+ li.appendChild(renderedDomElement);
+ });
+ } else {
+ li.appendChild(renderedDomElements);
+ }
+ ul.appendChild(li);
+ });
+ return ul;
+}
+
+/// \brief Renders a list of links.
+export function renderLinkList(array, obj, handler)
+{
+ return renderCustomList(array, function(arrayElement) {
+ return renderLink(array, obj, function() {
+ handler(arrayElement, array, obj);
+ });
+ });
+}
+
+/// \brief Returns a 'time ago' string used by the time stamp rendering functions.
+export function formatTimeAgoString(date)
+{
+ const seconds = Math.floor((new Date() - date) / 1000);
+ let interval = Math.floor(seconds / 31536000);
+ if (interval > 1) {
+ return interval + ' y ago';
+ }
+ interval = Math.floor(seconds / 2592000);
+ if (interval > 1) {
+ return interval + ' m ago';
+ }
+ interval = Math.floor(seconds / 86400);
+ if (interval > 1) {
+ return interval + ' d ago';
+ }
+ interval = Math.floor(seconds / 3600);
+ if (interval > 1) {
+ return interval + ' h ago';
+ }
+ interval = Math.floor(seconds / 60);
+ if (interval > 1) {
+ return interval + ' min ago';
+ }
+ return Math.floor(seconds) + ' s ago';
+}
+
+/// \brief Returns a Date object from the specified time stamp.
+export function dateFromTimeStamp(timeStamp)
+{
+ return new Date(timeStamp + 'Z');
+}
+
+/// \brief Renders a short time stamp, e.g. "12 hours ago" with the exact date as tooltip.
+export function renderShortTimeStamp(timeStamp)
+{
+ const date = dateFromTimeStamp(timeStamp);
+ if (date.getFullYear() === 1) {
+ return document.createTextNode('not yet');
+ }
+ const span = document.createElement('span');
+ span.appendChild(document.createTextNode(formatTimeAgoString(date)));
+ span.title = timeStamp;
+ return span;
+}
+
+/// \brief Renders a time stamp, e.g. "12 hours ago" with the exact date in brackets.
+export function renderTimeStamp(timeStamp)
+{
+ const date = dateFromTimeStamp(timeStamp);
+ if (date.getFullYear() === 1) {
+ return document.createTextNode('not yet');
+ }
+ return document.createTextNode(formatTimeAgoString(date) + ' (' + timeStamp + ')');
+}
+
+/// \brief Renders a time delta from 2 time stamps.
+export function renderTimeSpan(startTimeStamp, endTimeStamp)
+{
+ const startDate = dateFromTimeStamp(startTimeStamp);
+ if (startDate.getFullYear() === 1) {
+ return document.createTextNode('not yet');
+ }
+ let endDate = dateFromTimeStamp(endTimeStamp);
+ if (endDate.getFullYear() === 1) {
+ endDate = Date.now();
+ }
+ const elapsedMilliseconds = endDate - startDate;
+ let text;
+ if (elapsedMilliseconds >= (1000 * 60 * 60)) {
+ text = Math.floor(elapsedMilliseconds / 1000 / 60 / 60) + ' h';
+ } else if (elapsedMilliseconds >= (1000 * 60)) {
+ text = Math.floor(elapsedMilliseconds / 1000 / 60) + ' min';
+ } else if (elapsedMilliseconds >= (1000)) {
+ text = Math.floor(elapsedMilliseconds / 1000) + ' s';
+ } else {
+ text = '< 1 s';
+ }
+ return document.createTextNode(text);
+}
+
+/// \brief Renders a link which will invoke the specified \a handler when clicked.
+export function renderLink(value, row, handler, tooltip, href, middleClickHref)
+{
+ const linkElement = document.createElement('a');
+ const linkText = typeof value === 'object' ? value : renderNoneInGrey(value);
+ linkElement.appendChild(linkText);
+ linkElement.href = middleClickHref || href || '#';
+ if (tooltip !== undefined) {
+ linkElement.title = tooltip;
+ }
+ if (handler === undefined) {
+ return linkElement;
+ }
+ linkElement.onclick = function () {
+ handler(value, row);
+ return false;
+ };
+ linkElement.onmouseup = function (e) {
+ // treat middle-click as regular click
+ e.preventDefault();
+ e.stopPropagation();
+ if (e.which !== 2) {
+ return true;
+ }
+ if (!middleClickHref) {
+ handler(value, row);
+ }
+ return false;
+ };
+ return linkElement;
+}
+
+/// \brief Renders the specified array as comma-separated string or 'none' if the array is empty.
+export function renderArrayAsCommaSeparatedString(value)
+{
+ return renderNoneInGrey(!Array.isArray(value) || value.length <= 0 ? 'none' : value.join(', '));
+}
+
+/// \brief Renders the specified array as a possibly elided comma-separated string or 'none' if the array is empty.
+export function renderArrayElidedAsCommaSeparatedString(value)
+{
+ if (!Array.isArray(value) || value.length <= 0) {
+ return renderNoneInGrey('none');
+ }
+ return renderTextPossiblyElidingTheEnd(value.join(', '));
+}
+
+/// \brief Renders the specified value, possibly eliding the end.
+export function renderTextPossiblyElidingTheEnd(value)
+{
+ const limit = 50;
+ if (value.length < limit) {
+ return document.createTextNode(value);
+ }
+ const element = document.createElement('span');
+ const remainingText = document.createTextNode(value.substr(limit));
+ const elipses = document.createTextNode('…');
+ let expaned = false;
+ element.appendChild(document.createTextNode(value.substr(0, limit)));
+ element.appendChild(elipses);
+ element.onclick = function () {
+ element.removeChild(element.lastChild);
+ ((expaned = !expaned)) ? element.appendChild(remainingText) : element.appendChild(elipses);
+ };
+ return element;
+}
+
+/// \brief Rounds the specified \a num to two decimal places.
+function roundTwoDecimalPlaces(number)
+{
+ return Math.round((number + Number.EPSILON) * 100) / 100;
+}
+
+/// \brief Renders the specified \a sizeInByte using an appropriate unit.
+export function renderDataSize(sizeInByte, row, includeBytes)
+{
+ if (typeof(sizeInByte) !== 'number') {
+ return renderNoneInGrey('none');
+ }
+ let res;
+ if (sizeInByte < 1024) {
+ res = sizeInByte << " bytes";
+ } else if (sizeInByte < 1048576) {
+ res = roundTwoDecimalPlaces(sizeInByte / 1024.0) + " KiB";
+ } else if (sizeInByte < 1073741824) {
+ res = roundTwoDecimalPlaces(sizeInByte / 1048576.0) + " MiB";
+ } else if (sizeInByte < 1099511627776) {
+ res = roundTwoDecimalPlaces(sizeInByte / 1073741824.0) + " GiB";
+ } else {
+ res = roundTwoDecimalPlaces(sizeInByte / 1099511627776.0) + " TiB";
+ }
+ if (includeBytes && sizeInByte > 1024) {
+ res += ' (' + sizeInByte + " byte)";
+ }
+ return document.createTextNode(res);
+}
+
+// \brief Accesses the property of the specified \a object denoted by \a accessor which is a string like 'foo.bar'.
+// \returns Returns the propertie's value or undefined if it doesn't exist.
+function accessProperty(object, accessor)
+{
+ if (accessor === undefined) {
+ return;
+ }
+ const propertyNames = accessor.split(".");
+ for (let i = 0, count = propertyNames.length; i !== count; ++i) {
+ object = object[propertyNames[i]];
+ if (object === undefined || object === null) {
+ return;
+ }
+ }
+ return object;
+}
+
+/// \brief Renders a checkbox for selecting a table row.
+export function renderCheckBoxForTableRow(value, row, computeCheckBoxValue)
+{
+ const checkbox = document.createElement('input');
+ checkbox.type = 'checkbox';
+ checkbox.checked = row.selected;
+ checkbox.value = computeCheckBoxValue(row);
+ checkbox.onchange = function () { row.selected = this.checked };
+ return checkbox;
+}
+
+/// \brief Returns a table for the specified JSON array.
+export function renderTableFromJsonArray(args)
+{
+ // handle arguments
+ const rows = args.rows;
+ const columnHeaders = args.columnHeaders;
+ const columnAccessors = args.columnAccessors;
+ const columnSortAccessors = args.columnSortAccessors || [];
+ const defaultRenderer = args.defaultRenderer || renderStandardTableCell;
+ const customRenderer = args.customRenderer || {};
+ const rowHandler = args.rowHandler;
+ const maxPageButtons = args.maxPageButtons || 5;
+
+ const container = document.createElement("div");
+
+ // render note
+ let noteRenderer = customRenderer.note;
+ if (noteRenderer === undefined || typeof noteRenderer === 'string') {
+ const note = document.createElement("p");
+ note.appendChild(document.createTextNode(noteRenderer || "Showing " + rows.length + " results"));
+ container.appendChild(note);
+ } else {
+ const note = noteRenderer(rows);
+ if (note !== undefined) {
+ container.appendChild(note);
+ }
+ }
+
+ // add table
+ const rowsPerPage = args.rowsPerPage;
+ const table = document.createElement("table");
+ table.data = rows;
+ table.sortedData = rows;
+ table.className = "table-from-json table-from-json-array";
+
+ // define pagination stuff
+ if (rowsPerPage !== undefined && rowsPerPage > 0) {
+ table.hasPagination = true;
+ table.rowsPerPage = args.rowsPerPage;
+ table.currentPage = args.currentPage = 1;
+ table.pageCount = Math.ceil(rows.length / rowsPerPage);
+ }
+ table.pageInfo = function() {
+ if (!this.hasPagination) {
+ return {begin: 0, end: this.data.length}; // no pagination
+ }
+ if (this.currentPage === undefined || this.currentPage <= 0) {
+ return {begin: 0, end: 0}; // invalid page
+ }
+ return {
+ begin: Math.min((this.currentPage - 1) * this.rowsPerPage, this.data.length),
+ end: Math.min(this.currentPage * this.rowsPerPage, this.data.length),
+ };
+ };
+ table.forEachRowOnPage = function(callback) {
+ const pageInfo = this.pageInfo();
+ if (isNaN(pageInfo.begin) || isNaN(pageInfo.end)) {
+ return;
+ }
+ for (let i = pageInfo.begin, end = pageInfo.end; i != end; ++i) {
+ const row = this.data[i];
+ if (row === null || row === undefined) {
+ continue;
+ }
+ callback(row, i, pageInfo, this);
+ }
+ };
+
+ // render column header
+ const thead = document.createElement("thead");
+ const tr = document.createElement("tr");
+ columnHeaders.forEach(function (columnHeader) {
+ const th = document.createElement("th");
+ const columnIndex = tr.children.length;
+ th.columnAccessor = columnSortAccessors[columnIndex] || columnAccessors[columnIndex];
+ th.descending = true;
+ th.onclick = function () {
+ table.sort(this.columnAccessor, this.descending = !this.descending);
+ };
+ th.style.cursor = "pointer";
+ th.appendChild(document.createTextNode(columnHeader));
+ tr.appendChild(th);
+ });
+ thead.appendChild(tr);
+ table.appendChild(thead);
+
+ // add pagination
+ if (table.hasPagination) {
+ const td = document.createElement("td");
+ td.className = "pagination";
+ td.colSpan = columnAccessors.length;
+ const previousA = document.createElement("a");
+ previousA.appendChild(document.createTextNode("<"));
+ previousA.className = "prev";
+ previousA.onclick = function() {
+ const currentA = table.currentA;
+ if (currentA) {
+ const a = currentA.previousSibling;
+ if (a !== undefined && a !== previousA) {
+ a.onclick();
+ }
+ }
+ const pageNumInput = table.pageNumInput;
+ if (pageNumInput && table.currentPage > 1) {
+ pageNumInput.value = table.currentPage - 1;
+ pageNumInput.onchange();
+ }
+ };
+ const nextA = document.createElement("a");
+ nextA.appendChild(document.createTextNode(">"));
+ nextA.className = "next";
+ nextA.onclick = function() {
+ const currentA = table.currentA;
+ if (currentA) {
+ const a = currentA.nextSibling;
+ if (a !== undefined && a !== nextA) {
+ a.onclick();
+ }
+ }
+ const pageNumInput = table.pageNumInput;
+ if (pageNumInput && table.currentPage < table.pageCount) {
+ pageNumInput.value = table.currentPage + 1;
+ pageNumInput.onchange();
+ }
+ };
+
+ td.appendChild(previousA);
+
+ if (table.pageCount <= maxPageButtons) {
+ for (let pageNumber = 1; pageNumber <= table.pageCount; ++pageNumber) {
+ const a = document.createElement("a");
+ a.appendChild(document.createTextNode(pageNumber));
+ a.onclick = function () {
+ table.currentA.className = '';
+ table.currentA = this;
+ table.currentA.className = 'current';
+ table.currentPage = pageNumber;
+ table.rerender();
+ };
+ if (pageNumber === table.currentPage) {
+ table.currentA = a;
+ a.className = 'current';
+ }
+ td.appendChild(a);
+ }
+ } else {
+ const pageNumInput = document.createElement("input");
+ pageNumInput.type = "number";
+ pageNumInput.value = table.currentPage;
+ pageNumInput.min = 1;
+ pageNumInput.max = table.pageCount;
+ pageNumInput.onchange = function() {
+ const selectedPage = parseInt(this.value);
+ if (!isNaN(selectedPage) && selectedPage) {
+ table.currentPage = selectedPage;
+ table.rerender();
+ }
+ };
+ table.pageNumInput = pageNumInput;
+ td.appendChild(pageNumInput);
+ const totalSpan = document.createElement("span");
+ totalSpan.appendChild(document.createTextNode(" of " + table.pageCount));
+ td.appendChild(totalSpan);
+ }
+
+ td.appendChild(nextA);
+
+ const tr = document.createElement("tr");
+ const tfoot = document.createElement("tfoot");
+ tr.appendChild(td);
+ tfoot.appendChild(tr);
+ table.appendChild(tfoot);
+ }
+
+ // render table contents
+ const tbody = document.createElement("tbody");
+ const renderNewRow = function (row) {
+ const tr = document.createElement("tr");
+ columnAccessors.forEach(function (columnAccessor) {
+ const td = document.createElement("td");
+ const renderer = customRenderer[columnAccessor];
+ let data = accessProperty(row, columnAccessor);
+ if (data === undefined) {
+ data = "?";
+ }
+ const content = renderer ? renderer(data, row) : defaultRenderer(data, row);
+ td.appendChild(content);
+ tr.appendChild(td);
+ });
+ if (rowHandler) {
+ rowHandler(row, tr);
+ }
+ tbody.appendChild(tr);
+ };
+ table.forEachRowOnPage(renderNewRow);
+ table.appendChild(tbody);
+
+ // define function to re-render the table's contents
+ table.rerender = function() {
+ const sortedData = this.sortedData;
+ const trs = tbody.getElementsByTagName("tr");
+ const pageInfo = table.pageInfo();
+ let dataIndex = pageInfo.begin, dataEnd = pageInfo.end;
+ for (let tr = tbody.firstChild; tr; ++dataIndex) {
+ if (dataIndex >= dataEnd) {
+ const nextTr = tr.nextSibling;
+ tbody.removeChild(tr);
+ tr = nextTr;
+ continue;
+ }
+ const tds = tr.getElementsByTagName("td");
+ const row = sortedData[dataIndex];
+ for (let td = tr.firstChild, i = 0; td; td = td.nextSibling, ++i) {
+ const columnAccessor = columnAccessors[i];
+ while (td.firstChild) {
+ td.removeChild(td.firstChild);
+ }
+ const renderer = customRenderer[columnAccessor];
+ const data = accessProperty(row, columnAccessor);
+ const content = renderer !== undefined ? renderer(data, row) : defaultRenderer(data, row);
+ td.appendChild(content);
+ }
+ if (rowHandler) {
+ rowHandler(row, tr);
+ }
+ tr = tr.nextSibling;
+ }
+ for (; dataIndex < dataEnd; ++dataIndex) {
+ renderNewRow(sortedData[dataIndex]);
+ }
+ };
+
+ // define function to re-sort according to a specific column
+ table.sort = function(columnAccessor, descending) {
+ // sort the rows according to the column
+ table.sortedData = rows.sort(function (a, b) {
+ let aValue = accessProperty(a, columnAccessor);
+ let bValue = accessProperty(b, columnAccessor);
+ let aType = typeof aValue;
+ let bType = typeof bValue;
+
+ // handle undefined/null
+ if (aValue === undefined || aValue === null) {
+ return -1;
+ }
+ if (bValue === undefined || bValue === null) {
+ return 1;
+ }
+
+ // handle numbers
+ if (aType === "number" && bType === "number") {
+ if (aValue < bValue) {
+ return descending ? 1 : -1;
+ } else if (aValue > bValue) {
+ return descending ? -1 : 1;
+ } else {
+ return 0;
+ }
+ }
+
+ // handle arrays (sort them by length)
+ if (aType === "array" && bType === "array") {
+ if (aValue.length < bValue.length) {
+ return descending ? 1 : -1;
+ } else if (aValue > bValue) {
+ return descending ? -1 : 1;
+ } else {
+ return 0;
+ }
+ }
+
+ // handle non-strings
+ if (aType !== "string" || bType !== "string") {
+ aValue = aValue.toString();
+ bValue = bValue.toString();
+ aType = bType = "string";
+ }
+
+ // compare strings
+ return descending ? bValue.localeCompare(aValue) : aValue.localeCompare(bValue);
+ });
+
+ // re-render the table's contents
+ table.rerender();
+ };
+
+ // FIXME: implement filter
+
+ container.appendChild(table);
+ container.table = table;
+ return container;
+}
+
+/// \brief Returns a table for the specified JSON object.
+export function renderTableFromJsonObject(args)
+{
+ // handle arguments
+ const data = args.data;
+ const relatedRow = args.relatedRow;
+ const displayLabels = args.displayLabels || [];
+ const fieldAccessors = args.fieldAccessors || Object.getOwnPropertyNames(data);
+ const level = args.level;
+ const defaultRenderer = args.defaultRenderer || renderStandardTableCell;
+ const customRenderer = args.customRenderer || {};
+
+ const container = document.createElement("div");
+
+ // render table
+ const table = document.createElement("table");
+ table.className = "table-from-json table-from-json-object";
+
+ // render table contents
+ const tbody = document.createElement("tbody");
+ fieldAccessors.forEach(function (fieldAccessor) {
+ const tr = document.createElement("tr");
+ const th = document.createElement("th");
+ const displayLabel = displayLabels[tbody.children.length] || fieldAccessor;
+ if (displayLabel !== undefined) {
+ th.appendChild(document.createTextNode(displayLabel));
+ }
+ tr.appendChild(th);
+ const td = document.createElement("td");
+ const renderer = customRenderer[fieldAccessor];
+ let fieldData = accessProperty(data, fieldAccessor);
+ if (fieldData === undefined) {
+ fieldData = "?";
+ }
+ const content = renderer ? renderer(fieldData, data, level, relatedRow) : defaultRenderer(fieldData, data, level, relatedRow);
+ if (Array.isArray(content)) {
+ content.forEach(function(contentElement) {
+ td.appendChild(contentElement);
+ });
+ } else {
+ td.appendChild(content);
+ }
+ tr.appendChild(td);
+ tbody.appendChild(tr);
+ });
+ table.appendChild(tbody);
+
+ container.appendChild(table);
+ return container;
+}
+
+/// \brief Returns a heading for each key and values via renderStandardTableCell().
+export function renderObjectWithHeadings(object, row, level)
+{
+ const elements = [];
+ for (const [key, value] of Object.entries(object)) {
+ const heading = document.createElement('h4');
+ heading.className = 'compact-heading';
+ heading.appendChild(document.createTextNode(key));
+ elements.push(heading, renderStandardTableCell(value, object, level));
+ }
+ return elements;
+}
diff --git a/js/main.js b/js/main.js
new file mode 100644
index 00000000..e9de9326
--- /dev/null
+++ b/js/main.js
@@ -0,0 +1,162 @@
+import * as AjaxHelper from './ajaxhelper.js'
+import * as SinglePage from './singlepage.js';
+//import * as RenderUtil from './genericrendering.js'
+
+function main()
+{
+ SinglePage.initPage({
+ 'intro': {
+ },
+ 'downloads': {
+ initializer: initializeDownloadsSection,
+ state: {params: undefined},
+ },
+ 'doc': {
+ },
+ 'contact': {
+ },
+ });
+}
+
+function initializeDownloadsSection()
+{
+ if (window.downloadsInitialized) {
+ return true;
+ }
+ const query = new URLSearchParams(window.location.search);
+ queryReleases();
+ renderUserAgent(query.get("useragent") ?? window.navigator.userAgent);
+ return window.downloadsInitialized = true;
+}
+
+function renderUserAgent(userAgent)
+{
+ const platform = determinePlatformFromUserAgent(userAgent);
+ const platformCheckbox = document.getElementById("downloads-checkbox-" + platform);
+ if (platformCheckbox) {
+ platformCheckbox.checked = true;
+ }
+}
+
+function queryReleases()
+{
+ AjaxHelper.queryRoute("GET", "https://api.github.com/repos/Martchus/syncthingtray/releases", (xhr, ok) => {
+ if (!ok) {
+ return;
+ }
+ const releases = JSON.parse(xhr.responseText);
+ for (const release of releases) {
+ if (!release.draft) {
+ return renderRelease(release);
+ }
+ }
+ });
+}
+
+function determinePlatformFromAssetName(name)
+{
+ if (name.includes("mingw32")) {
+ return name.includes("-qt5") && !name.includes("-qt6") ? "windows" : "windows10";
+ } else if (name.includes("pc-linux-gnu")) {
+ return "pc-linux-gnu";
+ }
+}
+
+function determinePlatformFromUserAgent(userAgent)
+{
+ if (userAgent.includes("Linux")) {
+ return "pc-linux-gnu";
+ } else if (userAgent.includes("Windows")) {
+ return "windows10";
+ }
+}
+
+function determineDisplayNameForAsset(name)
+{
+ let arch;
+ if (name.includes("i686-")) {
+ arch = "32-bit (Intel/AMD)";
+ } else if (name.includes("x86_64-")) {
+ arch = "64-bit (Intel/AMD)";
+ }
+ let component;
+ if (name.startsWith("syncthingctl-")) {
+ component = "Additional command-line client for Syncthing";
+ } else if (name.startsWith("syncthingtray-")) {
+ component = "Tray application";
+ }
+ if (arch && component) {
+ return `${arch}: ${component}`;
+ }
+ return name;
+}
+
+function renderAsset(asset)
+{
+ const name = asset.name;
+ if (name.endsWith(".sig")) {
+ return;
+ }
+ const platform = determinePlatformFromAssetName(name);
+ const platformList = document.getElementById("downloads-platform-" + platform) ?? document.getElementById("downloads-platform-other");
+ const liElement = document.createElement("li");
+ const aElement = document.createElement("a");
+ const important = name.startsWith("syncthingtray-");
+ liElement.id = "downloads-asset-" + name;
+ aElement.target = "blank";
+ aElement.href = asset.browser_download_url;
+ aElement.appendChild(document.createTextNode(determineDisplayNameForAsset(name)));
+ liElement.appendChild(aElement);
+ if (important) {
+ aElement.style.fontWeight = "bold";
+ platformList.prepend(liElement);
+ } else {
+ platformList.appendChild(liElement);
+ }
+}
+
+function renderAssetSignature(asset)
+{
+ const name = asset.name;
+ if (!name.endsWith(".sig")) {
+ return;
+ }
+ const nameWithoutSig = name.substr(0, name.length - 4);
+ const liElement = document.getElementById("downloads-asset-" + nameWithoutSig);
+ if (!liElement) {
+ return;
+ }
+ const aElement = document.createElement("a");
+ aElement.target = "blank";
+ aElement.href = asset.browser_download_url;
+ aElement.appendChild(document.createTextNode("signature"));
+ liElement.appendChild(document.createTextNode(" ("));
+ liElement.appendChild(aElement);
+ liElement.appendChild(document.createTextNode(")"));
+}
+
+function renderRelease(releaseInfo)
+{
+ const releaseName = releaseInfo.name ?? "unknown";
+ const releaseDate = releaseInfo.published_at ?? "unknown";
+ document.getElementById("downloads-latest-release").innerText = `${releaseName} from ${releaseDate}`;
+
+ const assets = Array.isArray(releaseInfo.assets) ? releaseInfo.assets : [];
+ for (const asset of assets) {
+ renderAsset(asset);
+ }
+ for (const asset of assets) {
+ renderAssetSignature(asset);
+ }
+ const lists = document.querySelectorAll(".downloads-platform ul");
+ for (const list of lists) {
+ if (!list.firstChild) {
+ list.parentElement.style.display = 'none';
+ }
+ }
+
+ document.getElementById('downloads-loading').style.display = 'none';
+ document.getElementById('downloads-release-info').style.display = 'block';
+}
+
+main();
diff --git a/js/singlepage.js b/js/singlepage.js
new file mode 100644
index 00000000..364ca2a1
--- /dev/null
+++ b/js/singlepage.js
@@ -0,0 +1,68 @@
+import * as Utils from './utils.js';
+
+export let sections = {};
+export let sectionNames = [];
+
+/// \brief 'main()' function which initializes the single page app.
+export function initPage(pageSections)
+{
+ sections = pageSections;
+ sectionNames = Object.keys(sections);
+ handleHashChange();
+ document.body.onhashchange = handleHashChange;
+}
+
+let preventHandlingHashChange = false;
+let preventSectionInitializer = false;
+
+/// \brief Shows the current section and hides other sections.
+function handleHashChange()
+{
+ if (preventHandlingHashChange) {
+ return;
+ }
+
+ const hashParts = Utils.splitHashParts();
+ const currentSectionName = hashParts.shift() || 'intro-section';
+ if (!currentSectionName.endsWith('-section')) {
+ return;
+ }
+
+ sectionNames.forEach(function (sectionName) {
+ const sectionData = sections[sectionName];
+ const sectionElement = document.getElementById(sectionName + '-section');
+ if (sectionElement.id === currentSectionName) {
+ const sectionInitializer = sectionData.initializer;
+ if (sectionInitializer === undefined || preventSectionInitializer || sectionInitializer(sectionElement, sectionData, hashParts)) {
+ sectionElement.style.display = 'block';
+ }
+ } else {
+ const sectionDestructor = sectionData.destructor;
+ if (sectionDestructor === undefined || sectionDestructor(sectionElement, sectionData, hashParts)) {
+ sectionElement.style.display = 'none';
+ }
+ }
+ const navLinkElement = document.getElementById(sectionName + '-nav-link');
+ if (sectionElement.id === currentSectionName) {
+ navLinkElement.classList.add('active');
+ } else {
+ navLinkElement.classList.remove('active');
+ }
+ });
+}
+
+/// \brief Updates the #hash without triggering the handler.
+export function updateHashPreventingChangeHandler(newHash)
+{
+ preventHandlingHashChange = true;
+ window.location.hash = newHash;
+ preventHandlingHashChange = false;
+}
+
+/// \brief Updates the #hash without triggering the section initializer.
+export function updateHashPreventingSectionInitializer(newHash)
+{
+ preventSectionInitializer = true;
+ window.location.hash = newHash;
+ preventSectionInitializer = false;
+}
diff --git a/js/utils.js b/js/utils.js
new file mode 100644
index 00000000..a0a4b252
--- /dev/null
+++ b/js/utils.js
@@ -0,0 +1,130 @@
+
+export function splitHashParts()
+{
+ const currentHash = location.hash.substr(1);
+ const hashParts = currentHash.split('?');
+ for (let i = 0, len = hashParts.length; i != len; ++i) {
+ hashParts[i] = decodeURIComponent(hashParts[i]);
+ }
+ return hashParts;
+}
+
+export function hashAsObject(hash, multipleValuesAsArray)
+{
+ const hashObject = {};
+ (hash || location.hash.substr(1)).split('&').forEach(function(hashPart) {
+ const parts = hashPart.split('=', 2);
+ if (parts.length < 1) {
+ return;
+ }
+ const key = decodeURIComponent(parts[0]);
+ const thisValue = parts.length > 1 ? decodeURIComponent(parts[1]) : undefined;
+ const existingValue = hashObject[key];
+ if (multipleValuesAsArray && existingValue !== undefined) {
+ if (Array.isArray(existingValue)) {
+ existingValue.push(thisValue);
+ } else {
+ hashObject[key] = [existingValue, thisValue];
+ }
+ } else {
+ hashObject[key] = thisValue;
+ }
+ });
+ return hashObject;
+}
+
+export function getAndEmptyElement(elementId, specialActionsById)
+{
+ return emptyDomElement(document.getElementById(elementId), specialActionsById);
+}
+
+export function emptyDomElement(domElement, specialActionsById)
+{
+ let child = domElement.firstChild;
+ while (child) {
+ let specialAction = specialActionsById ? specialActionsById[child.id] : undefined;
+ let nextSibling = child.nextSibling;
+ if (specialAction !== 'keep') {
+ domElement.removeChild(child);
+ }
+ child = nextSibling;
+ }
+ return domElement;
+}
+
+export function alterFormSelection(form, command)
+{
+ // modify form elements
+ const elements = form.elements;
+ for (let i = 0, len = elements.length; i != len; ++i) {
+ const element = elements[i];
+ if (element.type !== 'checkbox') {
+ continue;
+ }
+ switch (command) {
+ case 'uncheck-all':
+ element.checked = false;
+ break;
+ case 'check-all':
+ element.checked = true;
+ break;
+ }
+ }
+ // modify the actual data
+ const tables = form.getElementsByTagName('table');
+ for (let i = 0, len = tables.length; i != len; ++i) {
+ const data = tables[i].data;
+ if (!Array.isArray(data)) {
+ return;
+ }
+ data.forEach(function (row) {
+ switch (command) {
+ case 'uncheck-all':
+ row.selected = false;
+ break;
+ case 'check-all':
+ row.selected = true;
+ break;
+ }
+ });
+ }
+}
+
+export function getProperty(object, property, fallback)
+{
+ if (typeof object !== 'object') {
+ return fallback;
+ }
+ const value = object[property];
+ return value !== undefined ? value : fallback;
+}
+
+export function makeRepoName(dbName, dbArch)
+{
+ return dbArch && dbArch !== 'x86_64' ? dbName + '@' + dbArch : dbName;
+}
+
+/// \brief Returns the table row data for the table within the element with the specified ID.
+export function getFormTableData(formId)
+{
+ const formElement = document.getElementById(formId);
+ const tableElement = formElement.getElementsByTagName('table')[0];
+ if (tableElement === undefined) {
+ return;
+ }
+ const data = tableElement.data;
+ return Array.isArray(data) ? data : undefined;
+}
+
+/// \brief Returns the cell values of selected rows.
+/// \remarks The row data needs to be passed. The cell is determined by the specified \a propertyName.
+export function getSelectedRowProperties(data, propertyName)
+{
+ const propertyValues = [];
+ data.forEach(function (row) {
+ if (row.selected) {
+ propertyValues.push(row[propertyName]);
+ }
+ });
+ return propertyValues;
+}