Add website

This commit is contained in:
Martchus 2024-04-21 21:19:57 +02:00
commit 5be9432ab1
18 changed files with 1642 additions and 0 deletions

60
css/basics.css Normal file
View File

@ -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;
}

144
css/layout.css Normal file
View File

@ -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;
}

152
css/specifics.css Normal file
View File

@ -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%;
}

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 21.5C10.65 20.65 8.2 20 6.5 20C4.85 20 3.15 20.3 1.75 21.05C1.65 21.1 1.6 21.1 1.5 21.1C1.25 21.1 1 20.85 1 20.6V6C1.6 5.55 2.25 5.25 3 5C4.11 4.65 5.33 4.5 6.5 4.5C8.45 4.5 10.55 4.9 12 6C13.45 4.9 15.55 4.5 17.5 4.5C18.67 4.5 19.89 4.65 21 5C21.75 5.25 22.4 5.55 23 6V20.6C23 20.85 22.75 21.1 22.5 21.1C22.4 21.1 22.35 21.1 22.25 21.05C20.85 20.3 19.15 20 17.5 20C15.8 20 13.35 20.65 12 21.5M12 8V19.5C13.35 18.65 15.8 18 17.5 18C18.7 18 19.9 18.15 21 18.5V7C19.9 6.65 18.7 6.5 17.5 6.5C15.8 6.5 13.35 7.15 12 8M13 11.5C14.11 10.82 15.6 10.5 17.5 10.5C18.41 10.5 19.26 10.59 20 10.78V9.23C19.13 9.08 18.29 9 17.5 9C15.73 9 14.23 9.28 13 9.84V11.5M17.5 11.67C15.79 11.67 14.29 11.93 13 12.46V14.15C14.11 13.5 15.6 13.16 17.5 13.16C18.54 13.16 19.38 13.24 20 13.4V11.9C19.13 11.74 18.29 11.67 17.5 11.67M20 14.57C19.13 14.41 18.29 14.33 17.5 14.33C15.67 14.33 14.17 14.6 13 15.13V16.82C14.11 16.16 15.6 15.83 17.5 15.83C18.54 15.83 19.38 15.91 20 16.07V14.57Z" /></svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M15.41,16.58L10.83,12L15.41,7.41L14,6L8,12L14,18L15.41,16.58Z" /></svg>

After

Width:  |  Height:  |  Size: 356 B

1
img/icon/code-braces.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M8,3A2,2 0 0,0 6,5V9A2,2 0 0,1 4,11H3V13H4A2,2 0 0,1 6,15V19A2,2 0 0,0 8,21H10V19H8V14A2,2 0 0,0 6,12A2,2 0 0,0 8,10V5H10V3M16,3A2,2 0 0,1 18,5V9A2,2 0 0,0 20,11H21V13H20A2,2 0 0,0 18,15V19A2,2 0 0,1 16,21H14V19H16V14A2,2 0 0,1 18,12A2,2 0 0,1 16,10V5H14V3H16Z" /></svg>

After

Width:  |  Height:  |  Size: 339 B

1
img/icon/download.svg Normal file
View File

@ -0,0 +1 @@
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="24" height="24" viewBox="0 0 24 24"><path d="M5,20H19V18H5M19,9H15V3H9V9H5L12,16L19,9Z" /></svg>

After

Width:  |  Height:  |  Size: 337 B

1
img/icon/forum.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M17,12V3A1,1 0 0,0 16,2H3A1,1 0 0,0 2,3V17L6,13H16A1,1 0 0,0 17,12M21,6H19V15H6V17A1,1 0 0,0 7,18H18L22,22V7A1,1 0 0,0 21,6Z" /></svg>

After

Width:  |  Height:  |  Size: 203 B

1
img/icon/information.svg Normal file
View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M13,9H11V7H13M13,17H11V11H13M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z" /></svg>

After

Width:  |  Height:  |  Size: 184 B

32
img/logo.svg Normal file
View File

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 18.1.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 117.3 117.3" enable-background="new 0 0 117.3 117.3" xml:space="preserve">
<g>
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="58.666" y1="117.332" x2="58.666" y2="0">
<stop offset="0" style="stop-color:#0882C8"/>
<stop offset="1" style="stop-color:#26B6DB"/>
</linearGradient>
<circle fill="url(#SVGID_1_)" cx="58.7" cy="58.7" r="58.7"/>
<g>
<circle fill="none" stroke="#FFFFFF" stroke-width="6" stroke-miterlimit="10" cx="58.7" cy="58.5" r="43.7"/>
<g>
<path fill="#FFFFFF" d="M94.7,47.8c4.7,1.6,9.8-0.9,11.4-5.6c1.6-4.7-0.9-9.8-5.6-11.4c-4.7-1.6-9.8,0.9-11.4,5.6
C87.5,41.1,90,46.2,94.7,47.8z"/>
<line fill="none" stroke="#FFFFFF" stroke-width="6" stroke-miterlimit="10" x1="97.6" y1="39.4" x2="67.5" y2="64.4"/>
</g>
<g>
<path fill="#FFFFFF" d="M77.6,91c-0.4,4.9,3.2,9.3,8.2,9.8c5,0.4,9.3-3.2,9.8-8.2c0.4-4.9-3.2-9.3-8.2-9.8
C82.4,82.4,78,86,77.6,91z"/>
<line fill="none" stroke="#FFFFFF" stroke-width="6" stroke-miterlimit="10" x1="86.5" y1="91.8" x2="67.5" y2="64.4"/>
</g>
<path fill="#FFFFFF" d="M60,69.3c2.7,4.2,8.3,5.4,12.4,2.7c4.2-2.7,5.4-8.3,2.7-12.4c-2.7-4.2-8.3-5.4-12.4-2.7
C58.5,59.5,57.3,65.1,60,69.3z"/>
<g>
<path fill="#FFFFFF" d="M21.2,61.4c-4.3-2.5-9.8-1.1-12.3,3.1c-2.5,4.3-1.1,9.8,3.1,12.3c4.3,2.5,9.8,1.1,12.3-3.1
C26.8,69.5,25.4,64,21.2,61.4z"/>
<line fill="none" stroke="#FFFFFF" stroke-width="6" stroke-miterlimit="10" x1="16.6" y1="69.1" x2="67.5" y2="64.4"/>
</g>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
img/screenshots/plasma.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

BIN
img/screenshots/webview.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

221
index.html Normal file
View File

@ -0,0 +1,221 @@
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>Syncthing Tray</title>
<meta charset="UTF-8" />
<meta name="description" content="A GUI integration" />
<meta name="author" content="Martchus" />
<meta name="keywords" content="Syncthing Tray, Syncthing, GUI" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=yes" />
<script type="module" src="js/main.js"></script>
<link rel="stylesheet" type="text/css" href="css/basics.css" />
<link rel="stylesheet" type="text/css" href="css/layout.css" />
<link rel="stylesheet" type="text/css" href="css/specifics.css" />
<link rel="shortcut icon" href="img/logo.svg" />
</head>
<body>
<header>
<nav>
<div>
<img src="img/logo.svg" alt="Logo" />
Syncthing Tray<br />
<span>Tray application and Dolphin/Plasma integration for Syncthing</span>
</div>
<ul>
<li id="intro-nav-link">
<a href="#intro-section" title="Intro">Intro</a>
</li>
<li id="downloads-nav-link">
<a href="#downloads-section" title="Downloads">Downloads</a>
</li>
<li id="doc-nav-link">
<a href="#doc-section" title="Documentation">Docs</a>
</li>
<li id="contact-nav-link">
<a href="#contact-section" title="Contact">Contact</a>
</li>
<li id="code-nav-link">
<a href="https://github.com/Martchus/syncthingtray" target="blank" title="Code">Code</a>
</li>
</ul>
</nav>
</header>
<main>
<section id="intro-section">
<div class="banner">
<p>
<strong>Syncthing Tray provides a tray icon and further platform integration for Syncthing.</strong><br />
It focuses on GNU/Linux and Windows.
If you don't know Syncthing yet, it makes most sense to checkout the <a href="https://syncthing.net" target="blank">website of Syncthing</a> itself first.
</p>
<p>
This website is meant to provide an overview. Checkout the <a href="https://github.com/Martchus/syncthingtray/blob/master/README.md" target="blank">README
on GitHub</a> for many more (technical) details.
</p>
</div>
</section>
<section id="downloads-section" style="display: none">
<h2>Downloads</h2>
<p>
The latest release is <span id="downloads-latest-release"></span>.
Checkout the <a href="https://github.com/Martchus/syncthingtray/releases" target="blank">releases section
on GitHub</a> for older releases.
</p>
<p>
This page is supposed to provide an overview. Checkout the <a href="https://github.com/Martchus/syncthingtray/blob/master/README.md#download" target="blank">"Downloads"-section</a>
of the README for all options and further details.
</p>
<div class="downloads-grid">
<div id="official-downloads">
<h3>Official downloads</h3>
<p>
Official downloads are signed with the GPG key
<a href="https://keyserver.ubuntu.com/pks/lookup?search=B9E36A7275FC61B464B67907E06FE8F53CDC6A4C&fingerprint=on&op=index" target="blank">B9E36A7275FC61B464B67907E06FE8F53CDC6A4C</a>
(except those hosted on OBS).
</p>
<div class="downloads-by-platform">
<div class="downloads-platform">
<input type="checkbox" id="downloads-checkbox-arch"/><label for="downloads-checkbox-arch">Arch Linux</label>
<ul id="downloads-platform-obs">
<li>AUR</li>
<li><a href="https://martchus.no-ip.biz/repo/arch/ownstuff" target="blank">Binary repository</a></li>
</ul>
<div>
Available packages:
<ul style="border: none;">
<li><code>syncthingtray</code>/<code>syncthingtray-qt6</code>: the latest release</li>
<li><code>syncthingtray-git</code>: Git version</li>
</ul>
</div>
<p>Install preferably the <code>*-qt6</code> packages.</p>
<p>The binary repository can <strong>not</strong> be used on derivats such as Manjaro Linux.</p>
</div>
<div class="downloads-platform">
<input type="checkbox" id="downloads-checkbox-obs"/><label for="downloads-checkbox-obs">openSUSE Tumbleweed and Leap, Fedora</label>
<ul id="downloads-platform-obs">
<li>
<a href="https://software.opensuse.org/download.html?project=home:mkittler&package=syncthingtray" target="blank">Latest releases</a>
(<a href="https://download.opensuse.org/repositories/home:/mkittler">repository URL</a>)
</li>
<li>
<a href="https://software.opensuse.org/download.html?project=home:mkittler:vcs&package=syncthingtray" target="blank">Git versions</a>
(<a href="https://download.opensuse.org/repositories/home:/mkittler:/vcs">repository URL</a>)
</li>
</ul>
<div>
Available packages:
<ul style="border: none;">
<li><code>syncthingtray</code>/<code>syncthingtray-qt6</code>: Qt-widgets based GUI</li>
<li><code>syncthingplasmoid</code>/<code>syncthingplasmoid-qt6</code>: applet/plasmoid for Plasma desktop</li>
<li><code>syncthingfileitemaction</code>/<code>syncthingfileitemaction-qt6</code>: Dolphin/KIO integration</li>
<li><code>syncthingctl</code>/<code>syncthingctl-qt6</code>: command-line interface</li>
</ul>
</div>
<p>Install preferably the <code>*-qt6</code> packages if available for your OS.</p>
<p>Be sure to add the repository that matches the version of your OS and to keep it in sync when upgrading.</p>
<p>The linked download pages might be incomplete, use the repositories URL for a full list.</p>
</div>
<div class="downloads-platform">
<input type="checkbox" id="downloads-checkbox-pc-linux-gnu"/><label for="downloads-checkbox-pc-linux-gnu">GNU/Linux</label>
<ul id="downloads-platform-pc-linux-gnu"></ul>
<p>Requires <code>glibc>=2.26</code>, OpenGL and libX11. The distributions openSUSE Leap 15, Fedora 27, Debian 10 and Ubuntu 18.04 are recent enough (be sure the package <code>libopengl0</code> is installed on Debian/Ubuntu).</p>
<p>These downloads cannot include the Plasmoid/Applet (for the Plasma desktop) and the Dolphin integration for techical reasons. The KDE integrations therefore need to be installed via distribution-specific packages.</p>
</div>
<div class="downloads-platform">
<input type="checkbox" id="downloads-checkbox-windows10"/><label for="downloads-checkbox-windows10">Windows 10 and 11</label>
<ul id="downloads-platform-windows10"></ul>
<p>
These downloads require Windows 10 version 1809 or newer.
</p>
<p>
Windows SmartScreen will likely block the execution (you'll get a window saying "Windows protected your PC"); right click on the executable, select properties and tick the checkbox to allow the execution.
</p>
<p>
Antivirus software often <strong>wrongly</strong> considers the executable harmful. This is a known problem. Please don't create issues about it.
</p>
<p>
32-bit releases work and Pentium Pro or newer Intel/AMD-CPUs.
</p>
</div>
<div class="downloads-platform">
<input type="checkbox" id="downloads-checkbox-windows"/><label for="downloads-checkbox-windows">Windows 7 and newer</label>
<ul id="downloads-platform-windows"></ul>
<p>
On Windows 7 the bundled Go/Syncthing does not work. Use a version of Go/Syncthing that is older than 1.21/1.27.0 instead.
</p>
<p>
32-bit releases work on Pentium Pro or newer Intel/AMD-CPUs.
</p>
</div>
<div class="downloads-platform">
<input type="checkbox" id="downloads-checkbox-other"/><label for="downloads-checkbox-other">Other</label>
<ul id="downloads-platform-other"></ul>
</div>
</div>
</div>
<div id="community-downloads">
<h3>Community downloads</h3>
<p>
Those packages are provided by the community and might be more convenient to use depending on preferences. Community packages might not always
be up-to-date, though.
</p>
<div class="downloads-platform">
<input type="checkbox" id="downloads-community-checkbox-debian"/><label for="downloads-community-checkbox-debian">Debian</label>
<ul>
<li>Generic tray application: <code>sudo apt install syncthingtray</code></li>
<li>KDE integrations: <code>sudo apt install syncthingtray-kde-plasma</code></li>
</ul>
<p>
Those packages <em>may</em> be available on Debian derivats (Ubuntu, Pop!_OS, …) as well.
</p>
</div>
<div class="downloads-platform">
<input type="checkbox" id="downloads-community-checkbox-windows10"/><label for="downloads-community-checkbox-windows10">Windows 10 and 11</label>
<ul>
<li><a href="https://community.chocolatey.org/packages/syncthingtray" target="blank">Chocolatey package</a></li>
<li><a href="https://winstall.app/apps/Martchus.syncthingtray" target="blank"><code>winget</code> package</a></li>
</ul>
</div>
</div>
</div>
</section>
<section id="doc-section" style="display: none">
<h2>Documentation</h2>
<p>
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
<a href="https://github.com/Martchus/syncthingtray/blob/master/README.md#does-this-launch-or-bundle-syncthing-itself-what-about-my-existing-syncthing-installation" target="blank">
the relevant README section</a> 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 <a href="https://docs.syncthing.net/intro/getting-started.html#configuring" target="blank">"Getting started" documentation of Syncthing itself</a>.
</p>
<p>
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 <a href="https://github.com/Martchus/syncthingtray/blob/master/README.md#general-remarks-on-the-configuration" target="blank">"General remarks on the configuration"
section</a>.
</p>
<h3>License and attribution</h3>
<p>
Copyright © Marius Kittler
</p>
<p>
Syncthing Tray is licensed under GPL-2-or-later. It also contains 3rd party content. Checkout the sections about licensing and attributions in
the <a href="https://github.com/Martchus/syncthingtray/blob/master/README.md">README on GitHub</a> for details. The icons on this website are from
<a href="https://materialdesignicons.com" target="blank">Material Design Icons</a>.
</p>
</section>
<section id="contact-section" style="display: none">
<h2>Contact</h2>
<p>
Issues can be <a href="https://github.com/Martchus/syncthingtray/issues/new/choose" target="blank">reported on GitHub</a>.
Please read the instructions in the issue template before filing an issue.
</p>
<p>
Discussions are welcome <a href="https://github.com/Martchus/syncthingtray/discussions" target="blank">on GitHub</a> and in the
<a href="https://forum.syncthing.net/t/yet-another-syncthing-tray" target="blank">Syncthing Tray thread</a> on the Syncthing forum.
</p>
</ul>
</section>
</main>
</body>
</html>

29
js/ajaxhelper.js Normal file

File diff suppressed because one or more lines are too long

641
js/genericrendering.js Normal file
View File

@ -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;
}

159
js/main.js Normal file
View File

@ -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();

68
js/singlepage.js Normal file
View File

@ -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;
}

130
js/utils.js Normal file
View File

@ -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;
}