commit 5be9432ab1ecf610924fe5ad27fe434a499865ed Author: Martchus Date: Sun Apr 21 21:19:57 2024 +0200 Add website diff --git a/css/basics.css b/css/basics.css new file mode 100644 index 0000000..dc6b1d0 --- /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 0000000..fa32a2f --- /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 0000000..2056081 --- /dev/null +++ b/css/specifics.css @@ -0,0 +1,152 @@ +/* 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 */ +.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; + } +} +.banner p { + position: relative; + top: 65px; + right: 50px; + margin-left: auto; + width: 300px; + background-color: #fff; + border: 1px solid #aaa; + border-radius: 5px; + padding: 10px; + font-size: 120%; + line-height: 170%; + box-sizing: border-box; +} +@media (min-width: 1730px) { + .banner p { + margin-left: 0px; + left: 100px; + width: 30%; + } +} +@media (max-width: 1010px) { + .banner p { + top: 660px; + left: 0px; + border: none; + width: 100%; + margin-bottom: 0px; + padding-bottom: 0px; + } +} +@media (max-width: 950px) { + .banner { + margin-top: -29px; + } +} + +/* 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 0000000..8ab15a5 --- /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 0000000..13317b9 --- /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 0000000..dd8f3b2 --- /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 0000000..209c193 --- /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 0000000..5286af4 --- /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 0000000..d882198 --- /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 0000000..ff643fd --- /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 0000000..420fa22 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 0000000..d394e2d Binary files /dev/null and b/img/screenshots/webview.png differ diff --git a/index.html b/index.html new file mode 100644 index 0000000..cb808b4 --- /dev/null +++ b/index.html @@ -0,0 +1,221 @@ + + + + Syncthing Tray + + + + + + + + + + + + +
+ +
+
+
+ +
+ + + +
+ + diff --git a/js/ajaxhelper.js b/js/ajaxhelper.js new file mode 100644 index 0000000..1515067 --- /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 0000000..64ff1d5 --- /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 0000000..d71e428 --- /dev/null +++ b/js/main.js @@ -0,0 +1,159 @@ +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'; + } + } +} + +main(); diff --git a/js/singlepage.js b/js/singlepage.js new file mode 100644 index 0000000..364ca2a --- /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 0000000..a0a4b25 --- /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; +}