I may have created my own dead simple static site generation

This commit is contained in:
Christoph Stahl 2025-07-02 02:21:23 +02:00
parent 5b727a408f
commit ce14963576
41 changed files with 21001 additions and 289 deletions

74
build.py Normal file
View file

@ -0,0 +1,74 @@
import os
import shutil
from dataclasses import dataclass
import markdown
from jinja2 import Environment, FileSystemLoader
@dataclass
class Section:
name: str
type: str
content: str = ""
def render(self) -> str:
"""Render the section content based on its type."""
if self.type.lower() == "html":
return self.content
elif self.type.lower() == "markdown":
return markdown.markdown(
self.content,
extensions=[
"pymdownx.blocks.html",
"markdown.extensions.def_list",
"sane_lists",
"pymdownx.blocks.tab",
],
)
else:
raise ValueError(f"Unsupported content type: {self.type}")
@classmethod
def open(cls, filename: str) -> dict[str, "Section"]:
"""Open a file and return its contents."""
env = {}
with open(filename, "r") as file:
content = file.readlines()
key = None
for line in content:
if line.startswith("!!section:"):
keyline = line[10:].strip().split(",")
key = keyline[0].strip()
content_type = keyline[1].strip() if len(keyline) > 1 else "HTML"
env[key] = cls(name=key, type=content_type, content="")
elif key is not None and env[key] is not None:
env[key].content += line
return env
def main() -> None:
env = Environment(loader=FileSystemLoader("."))
template = env.get_template("site.template.html")
sites = os.listdir("sites")
for site in sites:
sitename, _ = os.path.splitext(site)
print(f"Found site: {sitename}")
site_config = {k: v.render() for k, v in Section.open(f"sites/{site}").items()}
rendered_html = template.render(site_config)
os.makedirs("dist", exist_ok=True)
with open(f"dist/{sitename}.html", "w") as f:
f.write(rendered_html)
static_folders = ["css", "js", "images"]
for folder in static_folders:
src_folder = os.path.join(folder)
dst_folder = os.path.join("dist", folder)
shutil.rmtree(dst_folder, ignore_errors=True)
shutil.copytree(src_folder, dst_folder)
if __name__ == "__main__":
main()

View file

@ -7,7 +7,7 @@
border-radius: 10px; border-radius: 10px;
} }
.textleft .work-feature-block-image { .showcase-image img {
display: block; display: block;
margin-right: auto; margin-right: auto;
margin-left: auto; margin-left: auto;
@ -50,7 +50,7 @@
justify-content: space-between; justify-content: space-between;
} }
body { body {
background: url("../images/background_small.jpg") center center no-repeat; background: url("../images/background_smaller.jpg") center center no-repeat;
background-size: cover; background-size: cover;
background-attachment: fixed; background-attachment: fixed;
} }

132
dist/css/app.css vendored Normal file
View file

@ -0,0 +1,132 @@
.hero-section-text {
display: block;
color: #0a0a0a;
text-shadow: 1px 1px 2px #fefefe;
background: rgba(255, 255, 255, 0.8);
padding: 20px;
border-radius: 10px;
}
.showcase-image img {
display: block;
margin-right: auto;
margin-left: auto;
max-height: 500px;
/* width: 100%; */
padding: 10px 0;
}
.textleft .text {
text-align: right;
}
.grid-container h1 {
border-bottom: 1px solid #0a0a0a;
}
.textright .work-feature-block-image {
display: block;
margin-left: auto;
margin-right: auto;
max-height: 500px;
/* width: 100%; */
padding: 10px 0;
}
.hero-full-screen {
padding-top: 100px;
margin-bottom: 100px;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-flex-direction: column;
-ms-flex-direction: column;
flex-direction: column;
-webkit-align-items: center;
-ms-flex-align: center;
align-items: center;
-webkit-justify-content: space-between;
-ms-flex-pack: justify;
justify-content: space-between;
}
body {
background: url("../images/background_smaller.jpg") center center no-repeat;
background-size: cover;
background-attachment: fixed;
}
.hero-full-screen .middle-content-section {
/* text-align: center; */
color: #fefefe;
}
@media (max-width: 768px) {
.hero-full-screen .middle-content-section {
width: 100%;
}
.overview-image {
display: none;
}
#main-content-section .grid-container {
max-width: 100%;
}
}
.top-content-section {
width: 100%;
}
.hero-full-screen .bottom-content-section {
padding: 1rem;
}
.hero-full-screen .bottom-content-section svg {
height: 3.75rem;
width: 3.75rem;
fill: #fefefe;
}
.top-bar {
background: rgba(0, 0, 0, 0.5);
width: 100%;
position: fixed;
}
.top-bar .menu {
background: transparent;
}
.top-bar .menu-text {
color: #fefefe;
}
.top-bar img {
height: 2.5rem;
width: 2.5rem;
}
.top-bar .menu li {
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-align-items: center;
-ms-flex-align: center;
align-items: center;
}
.top-bar .menu a {
color: #fefefe;
font-weight: bold;
}
#main-content-section .grid-container {
margin: auto;
background: rgba(255, 255, 255, 0.8);
border-radius: 10px;
padding: 20px;
/* max-width: 50%; */
}
#main-content-section .grid-container .grid-x {
display: flex;
align-items: center;
}

7753
dist/css/foundation.css vendored Normal file

File diff suppressed because it is too large Load diff

1
dist/css/foundation.min.css vendored Normal file

File diff suppressed because one or more lines are too long

114
dist/faq.html vendored Normal file
View file

@ -0,0 +1,114 @@
<!doctype html>
<html class="no-js" lang="en" dir="ltr">
<head>
<meta charset="utf-8">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Syng.Rocks</title>
<link rel="stylesheet" href="css/foundation.css">
<link rel="stylesheet" href="css/app.css">
</head>
<body>
<div class="top-content-section">
<div class="top-bar">
<div class="top-bar-left">
<ul class="menu">
<li class="menu-text"><img src="images/rocks.syng.Syng.png" alt="Syng.Rocks! Logo" height=30></li>
<li><a href="index.html">Home</a></li>
<li><a href="install.html">Installation</a></li>
<li><a href="faq.html">FAQ</a></li>
<li><a href="privacy.html">Privacy Policy</a></li>
</ul>
</div>
</div>
</div>
<div id="main-content-section" style="padding-top: 100px;">
<div class="grid-container">
<h1>FAQ</h1>
<dl>
<dt>Is using Syng.Rocks! free?</dt>
<dd>
<p>Yes, Syng.Rocks! is free software and licensed under the GNU Affero General Public License v3.0 (AGPL-3.0). You can use it for free without any limitations.</p>
</dd>
<dt>Can you add a specific song?</dt>
<dd>
<p>Syng.Rocks! does not host any songs itself. The client fetches songs from YouTube or any of your configured sources.</p>
</dd>
<dt>What are sources?</dt>
<dd>
<p>Since Syng.Rocks! does not host any songs itself, songs played by the playback client, need to come from external sources. Currently, the following sources can be added:</p>
<ul>
<li>YouTube</li>
<li>Any S3 compatible server</li>
<li>A local directory</li>
</ul>
<p>The sources are accessed from the playback client directly, the server is unaware of the contents of the sources and just uses a list of filenames to search for songs.</p>
</dd>
<dt>What file format can Syng.Rocks! play?</dt>
<dd>
<p>Syng.Rocks! uses <a href="https://mpv.io">MPV</a> to play the karaoke songs, so it supports most video formats including <code>cdg+mp3</code>.</p>
</dd>
<dt>What do you mean by <em>Playback Client</em>, <em>Web Client</em> and <em>Server</em>?</dt>
<dd>
<p>The <em>Playback Client</em> is the application that plays the songs and displays the lyrics. It is a desktop application.
The <em>Web Client</em> is the web application that allows you to manage the queue and search for songs. It is used by the guests to add songs to the queue.
The <em>Server</em> is the backend that manages the queue and handles connection between web clients and playback clients.</p>
<p>If you only want to host a karaoke event, you only need the playback client.
The server is optional, as you can use the public server at <a href="https://syng.rocks">syng.rocks</a>.</p>
</dd>
<dt>Can I use Syng.Rocks! without internet?</dt>
<dd>
<p>Yes and no. The playback client needs to connect to a server instance. While you can host the server yourself in your intranet, the experience can be limited, as some source (such as YouTube) need the playback client to reach YouTube. You can however use local and s3 sources, that are reachable from the playback client.</p>
</dd>
<dt>Can I use Syng.Rocks! without a server?</dt>
<dd>
<p>You do need some kind of server to manage the queue, but you can use the public server at <a href="https://syng.rocks">syng.rocks</a> to host your karaoke events. No need to roll up your own server.</p>
</dd>
<dt>Can I host my own server?</dt>
<dd>
<p>Yes, you can host your own server. The server is available as a Docker image on GitHub, or you can install it using the Python Package Index (PyPI) package <code>syng[server]</code>.
For more information, see the <a href="install.html">installation instructions</a>.</p>
</dd>
<dt><a id="waiting-room"></a>What is the waiting room?</dt>
<dd>
<p>The waiting room is a feature that allows you to limit the amount of songs a guest can have in the queue.
If the waiting room is enabled, guests can only add songs to the queue if they have no songs in the queue. All additional songs are placed in a waiting room and automatically leave the queue once no songs of the guest are left in the queue.</p>
</dd>
<dt>What data do you store?</dt>
<dd>
<p>Syng.Rocks! does not store any personal data. The server only stores the current state of the queue.
If you use the public server at <a href="https://syng.rocks">syng.rocks</a>, the server will also store your IP address for a limited time to prevent abuse.
For more information, see the <a href="privacy.html">privacy policy</a>.</p>
</dd>
<dt>I cannot connect to <a href="https://syng.rocks">syng.rocks</a>.</dt>
<dd>
<p>Sometimes the server is down for maintenance. Check the official <a href="https://floss.social/@syng">Mastodon account</a> for announcements. Also check, if your client is up-to-date. The server is only compatible with the latest release.</p>
</dd>
<dt>What is the restricted mode of the server?</dt>
<dd>
<p>When the server is in restricted mode, <em>server side search</em> is only available to authenticated rooms. For other rooms, a search request is forwarded to the playback client and results are then sent back to the web client.</p>
</dd>
<dt>Something does not work, or I have questions not covered here</dt>
<dd>
<p>Feel free to open an issue on the official issue tracker on <a href="https://github.com/christofsteel/syng/issues">GitHub</a> or reach out to us at our official Mastodon account: <a href="https://floss.social/@syng">@syng@floss.social</a> account or join our official matrix room: <a href="https://matrix.to/#/#syng:matrix.org">#syng:matrix.org</a>.</p>
</dd>
</dl>
</div>
</div>
<script src="js/vendor/jquery.js"></script>
<script src="js/vendor/what-input.js"></script>
<script src="js/vendor/foundation.js"></script>
<script src="js/app.js"></script>
</body>
</html>

BIN
dist/images/background.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

BIN
dist/images/background_small.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 348 KiB

BIN
dist/images/background_smaller.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 KiB

BIN
dist/images/overview.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 276 KiB

BIN
dist/images/overview.xcf vendored Normal file

Binary file not shown.

BIN
dist/images/rocks.syng.Syng.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
dist/images/sources.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 KiB

742
dist/images/sources.svg vendored Normal file
View file

@ -0,0 +1,742 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="210.547mm"
height="87.391151mm"
viewBox="0 0 210.547 87.391151"
version="1.1"
id="svg1"
xml:space="preserve"
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
sodipodi:docname="sources.svg"
inkscape:export-filename="sources.png"
inkscape:export-xdpi="266.60046"
inkscape:export-ydpi="266.60046"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:i="&amp;ns_ai;"
xmlns:ns="&amp;#38;ns_ai;"><sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
inkscape:zoom="0.54440085"
inkscape:cx="451.87291"
inkscape:cy="-17.450377"
inkscape:window-width="1440"
inkscape:window-height="888"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg1" /><defs
id="defs1"><clipPath
id="_clipPath_iJhCLJIxvnBD6H216wD2nheMtI7NQsNe"><rect
width="48"
height="48"
id="rect1"
x="0"
y="0" /></clipPath><clipPath
id="_clipPath_iJhCLJIxvnBD6H216wD2nheMtI7NQsNe-3"><rect
width="48"
height="48"
id="rect1-5"
x="0"
y="0" /></clipPath><filter
style="color-interpolation-filters:sRGB"
inkscape:label="Blur"
id="filter44"
x="-0.039903544"
y="-0.020766242"
width="1.0798071"
height="1.0415325"><feGaussianBlur
stdDeviation="2 2"
result="fbSourceGraphic"
id="feGaussianBlur44" /><feColorMatrix
result="fbSourceGraphicAlpha"
in="fbSourceGraphic"
values="0 0 0 -1 0 0 0 0 -1 0 0 0 0 -1 0 0 0 0 1 0"
id="feColorMatrix78" /><feGaussianBlur
id="feGaussianBlur78"
stdDeviation="3 3"
result="blur"
in="fbSourceGraphic" /></filter><filter
style="color-interpolation-filters:sRGB"
inkscape:label="Blur"
id="filter45"
x="-0.20689655"
y="-0.3"
width="1.4137931"
height="1.6"><feGaussianBlur
stdDeviation="2 2"
result="blur"
id="feGaussianBlur45" /></filter><filter
style="color-interpolation-filters:sRGB"
inkscape:label="Blur"
id="filter46"
x="-0.025940825"
y="-0.019008118"
width="1.0518816"
height="1.0380162"><feGaussianBlur
stdDeviation="2 2"
result="fbSourceGraphic"
id="feGaussianBlur46" /><feColorMatrix
result="fbSourceGraphicAlpha"
in="fbSourceGraphic"
values="0 0 0 -1 0 0 0 0 -1 0 0 0 0 -1 0 0 0 0 1 0"
id="feColorMatrix77" /><feGaussianBlur
id="feGaussianBlur77"
stdDeviation="3 3"
result="blur"
in="fbSourceGraphic" /></filter></defs><g
clip-path="url(#_clipPath_iJhCLJIxvnBD6H216wD2nheMtI7NQsNe-3)"
inkscape:label="Design.svg"
transform="matrix(2.9374705,0,0,3.4117573,-19.762837,-35.044531)"
id="g2-9"
style="fill:#000000;filter:url(#filter45)"><rect
x="19.393"
y="19.603001"
width="9.8009996"
height="9.0889997"
fill="#ffffff"
id="rect2-1"
style="fill:#000000" /><path
d="m 35.2,18.5 c 0,-0.1 0,-0.2 -0.1,-0.3 0,0 0,-0.1 0,-0.1 C 34.8,17.2 34,16.6 33,16.6 h 0.2 c 0,0 -3.9,-0.6 -9.2,-0.6 -5.2,0 -9.2,0.6 -9.2,0.6 H 15 c -1,0 -1.8,0.6 -2.1,1.5 0,0 0,0.1 0,0.1 0,0.1 0,0.2 -0.1,0.3 -0.1,1 -0.4,3.1 -0.4,5.5 0,2.4 0.3,4.5 0.4,5.5 0,0.1 0,0.2 0.1,0.3 0,0 0,0.1 0,0.1 0.3,0.9 1.1,1.5 2.1,1.5 h -0.2 c 0,0 3.9,0.6 9.2,0.6 5.2,0 9.2,-0.6 9.2,-0.6 H 33 c 1,0 1.8,-0.6 2.1,-1.5 0,0 0,-0.1 0,-0.1 0,-0.1 0,-0.2 0.1,-0.3 0.1,-1 0.4,-3.1 0.4,-5.5 0,-2.4 -0.2,-4.5 -0.4,-5.5 z m -7.8,6 -4.7,3.4 C 22.6,28 22.5,28 22.4,28 c -0.1,0 -0.2,0 -0.3,-0.1 -0.2,-0.1 -0.3,-0.3 -0.3,-0.5 v -6.8 c 0,-0.2 0.1,-0.4 0.3,-0.5 0.2,-0.1 0.4,-0.1 0.6,0 l 4.7,3.4 c 0.1,0.1 0.2,0.3 0.2,0.5 0.1,0.2 0,0.4 -0.2,0.5 z"
fill="#ca3737"
id="path2-2"
style="fill:#000000" /></g><switch
id="switch3-7"
transform="matrix(0.14520157,0,0,0.14520157,146.27311,-11.962151)"
style="fill:#000000;filter:url(#filter44)">
<foreignObject
requiredExtensions="http://ns.adobe.com/AdobeIllustrator/10.0/"
x="0"
y="0"
width="1"
height="1">
</foreignObject>
<g
ns:extraneous="self"
id="g3-0"
style="fill:#000000">
<g
id="g2-3-9"
style="fill:#000000">
<path
fill="#f15a29"
d="m 319.096,122.504 c 0,0 56.734,91.377 75.696,123.853 0.306,0.529 0.176,1.228 -0.28,1.684 -0.575,0.57 -1.441,0.637 -2.011,0.062 L 294.745,145.864 Z"
id="path1-3"
style="fill:#000000" />
<path
d="m 151.967,455.475 c 16.593,-35.104 38.975,-66.865 66.674,-94.564 10.535,-10.53 21.656,-20.288 33.321,-29.253 v 72.348 z m -57.734,66.818 157.729,-80.297 v 183.557 l 35.544,46.691 V 423.713 l 21.589,-11.105 c 9.945,-4.996 19.272,-11.644 27.569,-19.946 41.115,-41.11 41.768,-107.467 1.824,-149.293 l -81.484,-84.894 c -3.249,-3.472 -4.954,-8.017 -4.804,-12.795 0.15,-4.809 2.161,-9.276 5.659,-12.572 7.214,-6.783 18.708,-6.327 25.491,0.881 l 11.396,11.878 24.351,-23.361 C 290.326,85.189 255.123,89.874 234.732,108.54 c -10.053,9.463 -15.826,22.278 -16.262,36.083 -0.435,13.826 4.545,26.978 14.013,37.037 l 0.109,0.119 81.909,85.324 c 26.937,28.668 26.398,73.758 -1.622,101.772 -4.369,4.369 -9.157,8.069 -14.23,11.105 l -11.142,5.731 V 269.746 c -34.078,17.562 -65.155,40.125 -92.553,67.529 -34.124,34.119 -60.916,73.861 -79.634,118.117 -9.184,21.708 -16.221,44.064 -21.087,66.901"
id="path2-6-6"
style="fill:#000000" />
</g>
</g>
</switch><g
inkscape:label="Ebene 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-4.2133262,-15.880973)"><g
clip-path="url(#_clipPath_iJhCLJIxvnBD6H216wD2nheMtI7NQsNe)"
inkscape:label="Design.svg"
transform="matrix(2.9374705,0,0,3.4117573,-17.394632,-23.446126)"
id="g2"><rect
x="19.393"
y="19.603001"
width="9.8009996"
height="9.0889997"
fill="#ffffff"
id="rect2" /><path
d="m 35.2,18.5 c 0,-0.1 0,-0.2 -0.1,-0.3 0,0 0,-0.1 0,-0.1 C 34.8,17.2 34,16.6 33,16.6 h 0.2 c 0,0 -3.9,-0.6 -9.2,-0.6 -5.2,0 -9.2,0.6 -9.2,0.6 H 15 c -1,0 -1.8,0.6 -2.1,1.5 0,0 0,0.1 0,0.1 0,0.1 0,0.2 -0.1,0.3 -0.1,1 -0.4,3.1 -0.4,5.5 0,2.4 0.3,4.5 0.4,5.5 0,0.1 0,0.2 0.1,0.3 0,0 0,0.1 0,0.1 0.3,0.9 1.1,1.5 2.1,1.5 h -0.2 c 0,0 3.9,0.6 9.2,0.6 5.2,0 9.2,-0.6 9.2,-0.6 H 33 c 1,0 1.8,-0.6 2.1,-1.5 0,0 0,-0.1 0,-0.1 0,-0.1 0,-0.2 0.1,-0.3 0.1,-1 0.4,-3.1 0.4,-5.5 0,-2.4 -0.2,-4.5 -0.4,-5.5 z m -7.8,6 -4.7,3.4 C 22.6,28 22.5,28 22.4,28 c -0.1,0 -0.2,0 -0.3,-0.1 -0.2,-0.1 -0.3,-0.3 -0.3,-0.5 v -6.8 c 0,-0.2 0.1,-0.4 0.3,-0.5 0.2,-0.1 0.4,-0.1 0.6,0 l 4.7,3.4 c 0.1,0.1 0.2,0.3 0.2,0.5 0.1,0.2 0,0.4 -0.2,0.5 z"
fill="#ca3737"
id="path2" /></g><switch
id="switch3"
transform="matrix(0.14520157,0,0,0.14520157,149.58394,2.7783219)">
<foreignObject
requiredExtensions="http://ns.adobe.com/AdobeIllustrator/10.0/"
x="0"
y="0"
width="1"
height="1">
</foreignObject>
<g
i:extraneous="self"
id="g3">
<g
id="g2-3">
<path
fill="#f15a29"
d="m 319.096,122.504 c 0,0 56.734,91.377 75.696,123.853 0.306,0.529 0.176,1.228 -0.28,1.684 -0.575,0.57 -1.441,0.637 -2.011,0.062 L 294.745,145.864 Z"
id="path1" />
<path
d="m 151.967,455.475 c 16.593,-35.104 38.975,-66.865 66.674,-94.564 10.535,-10.53 21.656,-20.288 33.321,-29.253 v 72.348 z m -57.734,66.818 157.729,-80.297 v 183.557 l 35.544,46.691 V 423.713 l 21.589,-11.105 c 9.945,-4.996 19.272,-11.644 27.569,-19.946 41.115,-41.11 41.768,-107.467 1.824,-149.293 l -81.484,-84.894 c -3.249,-3.472 -4.954,-8.017 -4.804,-12.795 0.15,-4.809 2.161,-9.276 5.659,-12.572 7.214,-6.783 18.708,-6.327 25.491,0.881 l 11.396,11.878 24.351,-23.361 C 290.326,85.189 255.123,89.874 234.732,108.54 c -10.053,9.463 -15.826,22.278 -16.262,36.083 -0.435,13.826 4.545,26.978 14.013,37.037 l 0.109,0.119 81.909,85.324 c 26.937,28.668 26.398,73.758 -1.622,101.772 -4.369,4.369 -9.157,8.069 -14.23,11.105 l -11.142,5.731 V 269.746 c -34.078,17.562 -65.155,40.125 -92.553,67.529 -34.124,34.119 -60.916,73.861 -79.634,118.117 -9.184,21.708 -16.221,44.064 -21.087,66.901"
id="path2-6" />
</g>
</g>
</switch></g><g
inkscape:label="hdd.svg"
id="layer1-1-0"
transform="matrix(0.11642098,0,0,0.11642098,64.699557,-2.316239)"
style="fill:#000000;filter:url(#filter46)">
<rect
style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none"
id="rect11036-6"
width="462.5799"
height="631.18384"
x="260.13293"
y="79.689705"
ry="10"
rx="10" />
<path
style="fill:#000000;fill-opacity:1;stroke:none"
d="m 336.50948,472.01841 v 35.6662 l -39.2688,33.8649 v 65.9284 l 35.8463,35.8464 v 26.8397 h 285.5098 l 60.3443,-60.3444 v -66.4688 l -36.8371,-36.8372 v -34.4952 z"
id="path11038-2"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cccccccccccc" />
<path
style="fill:#000000;fill-opacity:1;stroke:none"
d="m 518.83658,541.83781 v 27.7658 h -28.5315 v 83.5567 h 112.3426 l 37.8298,-37.8299 v -48.0193 l 17.7686,-17.7686 -23.8824,-23.8824 -16.9405,16.1777 z"
id="path11042-6"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccccccc" />
<path
style="fill:#000000;fill-opacity:1;stroke:none"
d="m 311.29088,535.78531 v 67.3695 l 56.0212,56.0211 h 68.2702 v -40.1696 h -64.8476 l -19.8146,-19.8144 v -63.4066 z"
id="path11045-1"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccccc" />
<circle
style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:#333c45;stroke-width:4.70599;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path11047-8"
transform="matrix(3.187429,0,0,3.187429,19262.69,14995.327)"
cx="-5889.75"
cy="-4606.25"
r="67.25" />
<circle
style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:#333c45;stroke-width:12.2273;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path11049-7"
transform="matrix(0.81784386,0,0,0.81784386,5306.4261,4080.4251)"
cx="-5889.75"
cy="-4606.25"
r="67.25" />
<circle
style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none"
id="path11051-9"
transform="matrix(4.8829271,0,0,4.8829271,29486.67,22593.13)"
cx="-5960"
cy="-4506.6875"
r="7.5625" />
<path
style="fill:#000000;fill-opacity:1;stroke:none"
d="m 554.46978,419.78001 -144.6591,194.3395 -52.2033,-52.2035 z"
id="path11053-2"
inkscape:connector-curvature="0"
inkscape:transform-center-x="-67.882251"
inkscape:transform-center-y="-69.296465" />
<circle
style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:#333c45;stroke-width:1.15912;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path11055-0"
transform="matrix(3.4509084,0,0,3.4509084,20951.838,16139.469)"
cx="-5960"
cy="-4506.6875"
r="7.5625" />
<path
style="fill:#000000;fill-opacity:1;stroke:none"
d="m 552.20808,548.71451 v 44.3257 h -52.9869 v 51.7136 h 99.0959 l 33.3717,-33.3719 v -18.851 h -25.9841 v -43.8164 z"
id="path11057-2"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cccccccccc" />
<rect
style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none"
id="rect11059-3"
width="29.361576"
height="29.901972"
x="564.27551"
y="555.67114" />
<circle
transform="matrix(2.8821179,0,0,2.8821179,17559.96,13576.481)"
style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none"
id="path11062-7"
cx="-5917.644"
cy="-4508.8921"
r="2.1655145" />
<circle
id="path11065-5"
style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(2.8821179,0,0,2.8821179,17691.848,13542.296)"
cx="-5917.644"
cy="-4508.8921"
r="2.1655145" />
<g
transform="matrix(-1,0,0,1,-4012.4724,6304.2598)"
id="g11089-9"
style="fill:#000000">
<g
style="fill:#000000;fill-opacity:1"
id="g11091-2"
transform="matrix(2.8821179,0,0,2.8821179,12565.448,7645.2124)">
<path
inkscape:connector-curvature="0"
style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none"
d="m -4302.875,-6966.4375 c -8.7807,0 -15.9063,7.1255 -15.9062,15.9063 0,8.7807 7.1255,15.9062 15.9062,15.9062 h 30.2812 v -31.8125 z"
transform="matrix(-0.34696707,0,0,0.34696707,-7485.2033,-2258.3505)"
id="rect11093-2" />
</g>
<circle
transform="matrix(3.9132938,0,0,3.9132938,18734.2,12451.798)"
id="path11097-8"
style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none"
cx="-5989.9604"
cy="-4667.6646"
r="2.2894223" />
</g>
<path
style="fill:#000000;stroke:#fab82e;stroke-width:4.84193;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 534.91288,592.01131 c 0,0 -52.2205,-83.9328 -114.2875,11.3002"
id="path11102-9"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cc" />
<circle
transform="matrix(2.8821179,0,0,2.8821179,17544.878,13273.391)"
style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none"
id="path11105-7"
cx="-5917.644"
cy="-4508.8921"
r="2.1655145" />
<circle
id="path11107-3"
style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(2.8821179,0,0,2.8821179,17543.166,13343.54)"
cx="-5917.644"
cy="-4508.8921"
r="2.1655145" />
<circle
inkscape:transform-center-x="-34.999423"
inkscape:transform-center-y="-0.00011176152"
id="path11110-6"
style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0,2.8821179,-2.8821179,0,-12470.629,17368.58)"
cx="-5917.644"
cy="-4508.8921"
r="2.1655145" />
<circle
inkscape:transform-center-x="34.99928"
inkscape:transform-center-y="-0.00046723847"
id="path11114-1"
style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0,-2.8821179,2.8821179,0,13449.69,-16742.117)"
cx="-5917.644"
cy="-4508.8921"
r="2.1655145" />
<circle
id="path11118-2"
style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(2.8821179,0,0,2.8821179,17544.878,13273.391)"
cx="-5917.644"
cy="-4508.8921"
r="2.1655145" />
<g
id="g11120-9"
transform="matrix(0,1,1,0,6516.7671,4810.6757)"
style="fill:#000000">
<g
transform="matrix(2.0379651,-2.0379651,2.0379651,2.0379651,17022.519,-8513.1716)"
id="g11123-3"
style="fill:#000000;fill-opacity:1">
<path
style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none"
d="m -4298.6562,-7356.7812 c -8.7807,0 -15.9063,7.1255 -15.9063,15.9062 0,8.7704 7.1085,15.8895 15.875,15.9062 6.164,0 12.3339,0 18.5,0 l 8.8125,-8.8124 c 1.9587,-1.9587 2.9688,-4.5377 2.9688,-7.0938 0,-2.5562 -1.0101,-5.1038 -2.9688,-7.0625 l -8.8125,-8.8125 z"
transform="matrix(-0.34696707,0,0,0.34696707,-7483.7515,-2122.9129)"
id="path11126-1"
inkscape:connector-curvature="0"
sodipodi:nodetypes="csccssscc" />
</g>
<circle
style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none"
id="path11134-9"
transform="matrix(2.7671166,-2.7671166,2.7671166,2.7671166,24783.255,-9476.3687)"
cx="-5989.9604"
cy="-4667.6646"
r="2.2894223" />
</g>
<g
id="g11160-4"
transform="translate(4995.3295,6304.2598)"
style="fill:#000000">
<g
transform="matrix(2.8821179,0,0,2.8821179,12565.448,7645.2124)"
id="g11163-7"
style="fill:#000000;fill-opacity:1">
<path
id="path11165-8"
transform="matrix(-0.34696707,0,0,0.34696707,-7485.2033,-2258.3505)"
d="m -4302.875,-6966.4375 c -8.7807,0 -15.9063,7.1255 -15.9062,15.9063 0,8.7807 7.1255,15.9062 15.9062,15.9062 h 30.2812 v -31.8125 z"
style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none"
inkscape:connector-curvature="0" />
</g>
<circle
style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none"
id="path11167-4"
transform="matrix(3.9132938,0,0,3.9132938,18734.2,12451.798)"
cx="-5989.9604"
cy="-4667.6646"
r="2.2894223" />
</g>
<g
transform="rotate(90,-5172.2358,-361.68515)"
id="g11171-5"
style="fill:#000000">
<g
style="fill:#000000;fill-opacity:1"
id="g11173-0"
transform="matrix(2.0379651,-2.0379651,2.0379651,2.0379651,17022.519,-8513.1716)">
<path
sodipodi:nodetypes="csccssscc"
inkscape:connector-curvature="0"
id="path11175-3"
transform="matrix(-0.34696707,0,0,0.34696707,-7483.7515,-2122.9129)"
d="m -4298.6562,-7356.7812 c -8.7807,0 -15.9063,7.1255 -15.9063,15.9062 0,8.7704 7.1085,15.8895 15.875,15.9062 6.164,0 12.3339,0 18.5,0 l 8.8125,-8.8124 c 1.9587,-1.9587 2.9688,-4.5377 2.9688,-7.0938 0,-2.5562 -1.0101,-5.1038 -2.9688,-7.0625 l -8.8125,-8.8125 z"
style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none" />
</g>
<circle
transform="matrix(2.7671166,-2.7671166,2.7671166,2.7671166,24783.255,-9476.3687)"
id="path11177-6"
style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none"
cx="-5989.9604"
cy="-4667.6646"
r="2.2894223" />
</g>
<g
transform="rotate(-90,1248.3275,-5268.4396)"
id="g11179-1"
style="fill:#000000">
<g
style="fill:#000000;fill-opacity:1"
id="g11181-0"
transform="matrix(2.0379651,-2.0379651,2.0379651,2.0379651,17022.519,-8513.1716)">
<path
sodipodi:nodetypes="csccssscc"
inkscape:connector-curvature="0"
id="path11183-6"
transform="matrix(-0.34696707,0,0,0.34696707,-7483.7515,-2122.9129)"
d="m -4298.6562,-7356.7812 c -8.7807,0 -15.9063,7.1255 -15.9063,15.9062 0,8.7704 7.1085,15.8895 15.875,15.9062 6.164,0 12.3339,0 18.5,0 l 8.8125,-8.8124 c 1.9587,-1.9587 2.9688,-4.5377 2.9688,-7.0938 0,-2.5562 -1.0101,-5.1038 -2.9688,-7.0625 l -8.8125,-8.8125 z"
style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none" />
</g>
<circle
transform="matrix(2.7671166,-2.7671166,2.7671166,2.7671166,24783.255,-9476.3687)"
id="path11185-3"
style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none"
cx="-5989.9604"
cy="-4667.6646"
r="2.2894223" />
</g>
<g
id="g11187-2"
transform="matrix(0,-1,-1,0,-5533.921,-4020.1121)"
style="fill:#000000">
<g
transform="matrix(2.0379651,-2.0379651,2.0379651,2.0379651,17022.519,-8513.1716)"
id="g11189-0"
style="fill:#000000;fill-opacity:1">
<path
style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none"
d="m -4298.6562,-7356.7812 c -8.7807,0 -15.9063,7.1255 -15.9063,15.9062 0,8.7704 7.1085,15.8895 15.875,15.9062 6.164,0 12.3339,0 18.5,0 l 8.8125,-8.8124 c 1.9587,-1.9587 2.9688,-4.5377 2.9688,-7.0938 0,-2.5562 -1.0101,-5.1038 -2.9688,-7.0625 l -8.8125,-8.8125 z"
transform="matrix(-0.34696707,0,0,0.34696707,-7483.7515,-2122.9129)"
id="path11191-6"
inkscape:connector-curvature="0"
sodipodi:nodetypes="csccssscc" />
</g>
<circle
style="fill:#000000;fill-opacity:1;fill-rule:nonzero;stroke:none"
id="path11193-1"
transform="matrix(2.7671166,-2.7671166,2.7671166,2.7671166,24783.255,-9476.3687)"
cx="-5989.9604"
cy="-4667.6646"
r="2.2894223" />
</g>
</g><g
inkscape:label="hdd.svg"
id="layer1-1"
transform="matrix(0.11642098,0,0,0.11642098,63.797057,-3.456739)">
<rect
style="fill:#414d5b;fill-opacity:1;fill-rule:nonzero;stroke:none"
id="rect11036"
width="462.5799"
height="631.18384"
x="260.13293"
y="79.689705"
ry="10"
rx="10" />
<path
style="fill:#333c45;fill-opacity:1;stroke:none"
d="m 336.50948,472.01841 v 35.6662 l -39.2688,33.8649 v 65.9284 l 35.8463,35.8464 v 26.8397 h 285.5098 l 60.3443,-60.3444 v -66.4688 l -36.8371,-36.8372 v -34.4952 z"
id="path11038"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cccccccccccc" />
<path
style="fill:#eaeaea;fill-opacity:1;stroke:none"
d="m 518.83658,541.83781 v 27.7658 h -28.5315 v 83.5567 h 112.3426 l 37.8298,-37.8299 v -48.0193 l 17.7686,-17.7686 -23.8824,-23.8824 -16.9405,16.1777 z"
id="path11042"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccccccc" />
<path
style="fill:#eaeaea;fill-opacity:1;stroke:none"
d="m 311.29088,535.78531 v 67.3695 l 56.0212,56.0211 h 68.2702 v -40.1696 h -64.8476 l -19.8146,-19.8144 v -63.4066 z"
id="path11045"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccccccccc" />
<circle
style="fill:#eaeaea;fill-opacity:1;fill-rule:nonzero;stroke:#333c45;stroke-width:4.70599;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path11047"
transform="matrix(3.187429,0,0,3.187429,19262.69,14995.327)"
cx="-5889.75"
cy="-4606.25"
r="67.25" />
<circle
style="fill:#eaeaea;fill-opacity:1;fill-rule:nonzero;stroke:#333c45;stroke-width:12.2273;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path11049"
transform="matrix(0.81784386,0,0,0.81784386,5306.4261,4080.4251)"
cx="-5889.75"
cy="-4606.25"
r="67.25" />
<circle
style="fill:#f9794c;fill-opacity:1;fill-rule:nonzero;stroke:none"
id="path11051"
transform="matrix(4.8829271,0,0,4.8829271,29486.67,22593.13)"
cx="-5960"
cy="-4506.6875"
r="7.5625" />
<path
style="fill:#f9794c;fill-opacity:1;stroke:none"
d="m 554.46978,419.78001 -144.6591,194.3395 -52.2033,-52.2035 z"
id="path11053"
inkscape:connector-curvature="0"
inkscape:transform-center-x="-67.882251"
inkscape:transform-center-y="-69.296465" />
<circle
style="fill:#eaeaea;fill-opacity:1;fill-rule:nonzero;stroke:#333c45;stroke-width:1.15912;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path11055"
transform="matrix(3.4509084,0,0,3.4509084,20951.838,16139.469)"
cx="-5960"
cy="-4506.6875"
r="7.5625" />
<path
style="fill:#f9cd54;fill-opacity:1;stroke:none"
d="m 552.20808,548.71451 v 44.3257 h -52.9869 v 51.7136 h 99.0959 l 33.3717,-33.3719 v -18.851 h -25.9841 v -43.8164 z"
id="path11057"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cccccccccc" />
<rect
style="fill:#3e3e40;fill-opacity:1;fill-rule:nonzero;stroke:none"
id="rect11059"
width="29.361576"
height="29.901972"
x="564.27551"
y="555.67114" />
<circle
transform="matrix(2.8821179,0,0,2.8821179,17559.96,13576.481)"
style="fill:#76a8d4;fill-opacity:1;fill-rule:nonzero;stroke:none"
id="path11062"
cx="-5917.644"
cy="-4508.8921"
r="2.1655145" />
<circle
id="path11065"
style="fill:#76a8d4;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(2.8821179,0,0,2.8821179,17691.848,13542.296)"
cx="-5917.644"
cy="-4508.8921"
r="2.1655145" />
<g
transform="matrix(-1,0,0,1,-4012.4724,6304.2598)"
id="g11089">
<g
style="fill:#eaeaea;fill-opacity:1"
id="g11091"
transform="matrix(2.8821179,0,0,2.8821179,12565.448,7645.2124)">
<path
inkscape:connector-curvature="0"
style="fill:#eaeaea;fill-opacity:1;fill-rule:nonzero;stroke:none"
d="m -4302.875,-6966.4375 c -8.7807,0 -15.9063,7.1255 -15.9062,15.9063 0,8.7807 7.1255,15.9062 15.9062,15.9062 h 30.2812 v -31.8125 z"
transform="matrix(-0.34696707,0,0,0.34696707,-7485.2033,-2258.3505)"
id="rect11093" />
</g>
<circle
transform="matrix(3.9132938,0,0,3.9132938,18734.2,12451.798)"
id="path11097"
style="fill:#353c46;fill-opacity:1;fill-rule:nonzero;stroke:none"
cx="-5989.9604"
cy="-4667.6646"
r="2.2894223" />
</g>
<path
style="fill:none;stroke:#fab82e;stroke-width:4.84193;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 534.91288,592.01131 c 0,0 -52.2205,-83.9328 -114.2875,11.3002"
id="path11102"
inkscape:connector-curvature="0"
sodipodi:nodetypes="cc" />
<circle
transform="matrix(2.8821179,0,0,2.8821179,17544.878,13273.391)"
style="fill:#cccccc;fill-opacity:1;fill-rule:nonzero;stroke:none"
id="path11105"
cx="-5917.644"
cy="-4508.8921"
r="2.1655145" />
<circle
id="path11107"
style="fill:#76a8d4;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(2.8821179,0,0,2.8821179,17543.166,13343.54)"
cx="-5917.644"
cy="-4508.8921"
r="2.1655145" />
<circle
inkscape:transform-center-x="-34.999423"
inkscape:transform-center-y="-0.00011176152"
id="path11110"
style="fill:#76a8d4;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0,2.8821179,-2.8821179,0,-12470.629,17368.58)"
cx="-5917.644"
cy="-4508.8921"
r="2.1655145" />
<circle
inkscape:transform-center-x="34.99928"
inkscape:transform-center-y="-0.00046723847"
id="path11114"
style="fill:#76a8d4;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(0,-2.8821179,2.8821179,0,13449.69,-16742.117)"
cx="-5917.644"
cy="-4508.8921"
r="2.1655145" />
<circle
id="path11118"
style="fill:#76a8d4;fill-opacity:1;fill-rule:nonzero;stroke:none"
transform="matrix(2.8821179,0,0,2.8821179,17544.878,13273.391)"
cx="-5917.644"
cy="-4508.8921"
r="2.1655145" />
<g
id="g11120"
transform="matrix(0,1,1,0,6516.7671,4810.6757)">
<g
transform="matrix(2.0379651,-2.0379651,2.0379651,2.0379651,17022.519,-8513.1716)"
id="g11123"
style="fill:#eaeaea;fill-opacity:1">
<path
style="fill:#eaeaea;fill-opacity:1;fill-rule:nonzero;stroke:none"
d="m -4298.6562,-7356.7812 c -8.7807,0 -15.9063,7.1255 -15.9063,15.9062 0,8.7704 7.1085,15.8895 15.875,15.9062 6.164,0 12.3339,0 18.5,0 l 8.8125,-8.8124 c 1.9587,-1.9587 2.9688,-4.5377 2.9688,-7.0938 0,-2.5562 -1.0101,-5.1038 -2.9688,-7.0625 l -8.8125,-8.8125 z"
transform="matrix(-0.34696707,0,0,0.34696707,-7483.7515,-2122.9129)"
id="path11126"
inkscape:connector-curvature="0"
sodipodi:nodetypes="csccssscc" />
</g>
<circle
style="fill:#353c46;fill-opacity:1;fill-rule:nonzero;stroke:none"
id="path11134"
transform="matrix(2.7671166,-2.7671166,2.7671166,2.7671166,24783.255,-9476.3687)"
cx="-5989.9604"
cy="-4667.6646"
r="2.2894223" />
</g>
<g
id="g11160"
transform="translate(4995.3295,6304.2598)">
<g
transform="matrix(2.8821179,0,0,2.8821179,12565.448,7645.2124)"
id="g11163"
style="fill:#eaeaea;fill-opacity:1">
<path
id="path11165"
transform="matrix(-0.34696707,0,0,0.34696707,-7485.2033,-2258.3505)"
d="m -4302.875,-6966.4375 c -8.7807,0 -15.9063,7.1255 -15.9062,15.9063 0,8.7807 7.1255,15.9062 15.9062,15.9062 h 30.2812 v -31.8125 z"
style="fill:#eaeaea;fill-opacity:1;fill-rule:nonzero;stroke:none"
inkscape:connector-curvature="0" />
</g>
<circle
style="fill:#353c46;fill-opacity:1;fill-rule:nonzero;stroke:none"
id="path11167"
transform="matrix(3.9132938,0,0,3.9132938,18734.2,12451.798)"
cx="-5989.9604"
cy="-4667.6646"
r="2.2894223" />
</g>
<g
transform="rotate(90,-5172.2358,-361.68515)"
id="g11171">
<g
style="fill:#eaeaea;fill-opacity:1"
id="g11173"
transform="matrix(2.0379651,-2.0379651,2.0379651,2.0379651,17022.519,-8513.1716)">
<path
sodipodi:nodetypes="csccssscc"
inkscape:connector-curvature="0"
id="path11175"
transform="matrix(-0.34696707,0,0,0.34696707,-7483.7515,-2122.9129)"
d="m -4298.6562,-7356.7812 c -8.7807,0 -15.9063,7.1255 -15.9063,15.9062 0,8.7704 7.1085,15.8895 15.875,15.9062 6.164,0 12.3339,0 18.5,0 l 8.8125,-8.8124 c 1.9587,-1.9587 2.9688,-4.5377 2.9688,-7.0938 0,-2.5562 -1.0101,-5.1038 -2.9688,-7.0625 l -8.8125,-8.8125 z"
style="fill:#eaeaea;fill-opacity:1;fill-rule:nonzero;stroke:none" />
</g>
<circle
transform="matrix(2.7671166,-2.7671166,2.7671166,2.7671166,24783.255,-9476.3687)"
id="path11177"
style="fill:#353c46;fill-opacity:1;fill-rule:nonzero;stroke:none"
cx="-5989.9604"
cy="-4667.6646"
r="2.2894223" />
</g>
<g
transform="rotate(-90,1248.3275,-5268.4396)"
id="g11179">
<g
style="fill:#eaeaea;fill-opacity:1"
id="g11181"
transform="matrix(2.0379651,-2.0379651,2.0379651,2.0379651,17022.519,-8513.1716)">
<path
sodipodi:nodetypes="csccssscc"
inkscape:connector-curvature="0"
id="path11183"
transform="matrix(-0.34696707,0,0,0.34696707,-7483.7515,-2122.9129)"
d="m -4298.6562,-7356.7812 c -8.7807,0 -15.9063,7.1255 -15.9063,15.9062 0,8.7704 7.1085,15.8895 15.875,15.9062 6.164,0 12.3339,0 18.5,0 l 8.8125,-8.8124 c 1.9587,-1.9587 2.9688,-4.5377 2.9688,-7.0938 0,-2.5562 -1.0101,-5.1038 -2.9688,-7.0625 l -8.8125,-8.8125 z"
style="fill:#eaeaea;fill-opacity:1;fill-rule:nonzero;stroke:none" />
</g>
<circle
transform="matrix(2.7671166,-2.7671166,2.7671166,2.7671166,24783.255,-9476.3687)"
id="path11185"
style="fill:#353c46;fill-opacity:1;fill-rule:nonzero;stroke:none"
cx="-5989.9604"
cy="-4667.6646"
r="2.2894223" />
</g>
<g
id="g11187"
transform="matrix(0,-1,-1,0,-5533.921,-4020.1121)">
<g
transform="matrix(2.0379651,-2.0379651,2.0379651,2.0379651,17022.519,-8513.1716)"
id="g11189"
style="fill:#eaeaea;fill-opacity:1">
<path
style="fill:#eaeaea;fill-opacity:1;fill-rule:nonzero;stroke:none"
d="m -4298.6562,-7356.7812 c -8.7807,0 -15.9063,7.1255 -15.9063,15.9062 0,8.7704 7.1085,15.8895 15.875,15.9062 6.164,0 12.3339,0 18.5,0 l 8.8125,-8.8124 c 1.9587,-1.9587 2.9688,-4.5377 2.9688,-7.0938 0,-2.5562 -1.0101,-5.1038 -2.9688,-7.0625 l -8.8125,-8.8125 z"
transform="matrix(-0.34696707,0,0,0.34696707,-7483.7515,-2122.9129)"
id="path11191"
inkscape:connector-curvature="0"
sodipodi:nodetypes="csccssscc" />
</g>
<circle
style="fill:#353c46;fill-opacity:1;fill-rule:nonzero;stroke:none"
id="path11193"
transform="matrix(2.7671166,-2.7671166,2.7671166,2.7671166,24783.255,-9476.3687)"
cx="-5989.9604"
cy="-4667.6646"
r="2.2894223" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 30 KiB

BIN
dist/images/syng.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 29 KiB

BIN
dist/images/syng_advanced.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

BIN
dist/images/syng_mobile_search.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 242 KiB

BIN
dist/images/syng_player_empty.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

BIN
dist/images/syng_player_next_up.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

BIN
dist/images/syng_player_song.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

BIN
dist/images/syng_web.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

BIN
dist/images/syng_web2.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 550 KiB

BIN
dist/images/web-shadows.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 615 KiB

101
dist/index.html vendored Normal file
View file

@ -0,0 +1,101 @@
<!doctype html>
<html class="no-js" lang="en" dir="ltr">
<head>
<meta charset="utf-8">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Syng.Rocks</title>
<link rel="stylesheet" href="css/foundation.css">
<link rel="stylesheet" href="css/app.css">
</head>
<body>
<div class="top-content-section">
<div class="top-bar">
<div class="top-bar-left">
<ul class="menu">
<li class="menu-text"><img src="images/rocks.syng.Syng.png" alt="Syng.Rocks! Logo" height=30></li>
<li><a href="index.html">Home</a></li>
<li><a href="install.html">Installation</a></li>
<li><a href="faq.html">FAQ</a></li>
<li><a href="privacy.html">Privacy Policy</a></li>
</ul>
</div>
</div>
</div>
<div class="hero-full-screen">
<div class="middle-content-section grid-container">
<div class="grid-x align-middle">
<div class="cell large-6">
<p><img alt="Syng.Rocks! Overview" src="images/overview.png" /></p>
</div>
<div class="cell large-6">
<div class="hero-section-text">
<h1>Syng.Rocks!</h1>
<h5>Easily host karaoke events</h5>
<p>Syng.Rocks! is a karaoke app that allows you to host karaoke events without much hassle. Whether you have a big collection of karaoke songs, or just want to stream karaoke songs from YouTube, whether you want to sing with a couple of friends or with a crowd of over 100 people, Syng.Rocks! has you covered in a privacy-friendly manner. No need to register, no need to log in, Syng.Rocks! will never collect any personal data from you. You can even host your own server if you want to.</p>
<p><a href="https://flathub.org/apps/rocks.syng.Syng" target="_blank" class="button">Linux/Flatpak</a>
<a href="https://github.com/christofsteel/syng/releases" target="_blank" class="button">Windows</a>
<a href="https://syng.rocks" target="_blank" class="button hollow">Web Client</a>
<a href="https://github.com/christofsteel/syng" target="_blank" class="button hollow">Source Code</a></p>
</div>
</div>
</div>
</div>
</div>
<div id="main-content-section">
<div class="grid-container">
<div class="grid-x grid-padding textleft align-right">
<div class="cell medium-5 text-right">
<h2>Start Singing</h2>
<p>If you only want to have a small YouTube based karaoke event, you can just download the app, share the generated QR Code and start singing. No registration, no login and no configuration required. </p>
</div>
<div class="cell medium-5 showcase-image">
<p><img alt="Syng.Rocks! Client" src="images/syng.png" /></p>
</div>
</div>
<div class="grid-x grid-padding textright">
<div class="cell medium-8 showcase-image">
<p><img alt="Syng.Rocks! Web Client" src="images/web-shadows.png" /></p>
</div>
<div class="cell medium-4">
<h2>Web client</h2>
<p>Let your guests join your karaoke event using the web client. They can use their browser on their own devices to search for songs and queue them up.
The web client also contains an admin mode, that allows for manual moderation of the song queue.</p>
</div>
</div>
<div class="grid-x grid-padding textleft align-right">
<div class="cell medium-4 text-right">
<h2>Configure to your needs</h2>
<p>Syng.Rocks! allows for a lot of configuration options. You can set up your own song database, you can set up a <a href="faq.html#waiting-room">waiting room</a>, a set time the event ends and much more.</p>
</div>
<div class="cell medium-8 showcase-image">
<p><img alt="Syng.Rocks! Web Client" src="images/syng_advanced.png" /></p>
</div>
</div>
<div class="grid-x grid-padding textright">
<div class="cell medium-7 showcase-image">
<p><img alt="Syng.Rocks! Web Client" src="images/sources.svg" /></p>
</div>
<div class="cell medium-5">
<h2>Multiple Sources</h2>
<p>Syng.Rocks! supports multiple sources for karaoke songs. The default source is YouTube, but you can configure a remote S3 compatible server or simply a folder on your system. The client connects directly to the sources, no login data is ever shared with the server.</p>
</div>
</div>
</div>
</div>
<script src="js/vendor/jquery.js"></script>
<script src="js/vendor/what-input.js"></script>
<script src="js/vendor/foundation.js"></script>
<script src="js/app.js"></script>
</body>
</html>

72
dist/install.html vendored Normal file
View file

@ -0,0 +1,72 @@
<!doctype html>
<html class="no-js" lang="en" dir="ltr">
<head>
<meta charset="utf-8">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Syng.Rocks</title>
<link rel="stylesheet" href="css/foundation.css">
<link rel="stylesheet" href="css/app.css">
</head>
<body>
<div class="top-content-section">
<div class="top-bar">
<div class="top-bar-left">
<ul class="menu">
<li class="menu-text"><img src="images/rocks.syng.Syng.png" alt="Syng.Rocks! Logo" height=30></li>
<li><a href="index.html">Home</a></li>
<li><a href="install.html">Installation</a></li>
<li><a href="faq.html">FAQ</a></li>
<li><a href="privacy.html">Privacy Policy</a></li>
</ul>
</div>
</div>
</div>
<div id="main-content-section" style="padding-top: 100px;">
<div class="grid-container">
<h1>Installation</h1>
<h2>Client</h2>
<p>Depending on your platform, you can install Syng.Rocks! in different ways.</p>
<dl>
<dt>Linux</dt>
<dd>
<p>Syng.Rocks! is available as a Flatpak on <a href="https://flathub.org/apps/rocks.syng.Syng">Flathub</a>. You can install it using the command line with:</p>
<p><code>flatpak install rocks.syng.Syng</code></p>
<p>There also exists an Arch Linux package in the <a href="https://aur.archlinux.org/packages/syng-client">AUR</a>.</p>
</dd>
<dt>Windows</dt>
<dd>
<p>Windows binaries are available on <a href="https://github.com/christofsteel/syng/releases">Github</a>. This is a portable version and does not require installation.</p>
</dd>
<dt>Generic</dt>
<dd>
<p>Syng.Rocks! is available in the Python Package Index (PyPI) as <code>syng</code>. You can install the client using pip:</p>
<p><code>pip install syng[client]</code></p>
</dd>
</dl>
<h2>Server</h2>
<p>The server is available as a Docker image hosted on Github. You can run it with:</p>
<pre><code>docker run ghcr.io/christofsteel/syng
</code></pre>
<p>Alternatively, you can run the server using the Python Package Index (PyPI) package <code>syng</code>:</p>
<pre><code>pip install syng[server]
</code></pre>
</div>
</div>
<script src="js/vendor/jquery.js"></script>
<script src="js/vendor/what-input.js"></script>
<script src="js/vendor/foundation.js"></script>
<script src="js/app.js"></script>
</body>
</html>

1
dist/js/app.js vendored Normal file
View file

@ -0,0 +1 @@
$(document).foundation()

460
dist/js/vendor/foundation.js vendored Normal file

File diff suppressed because one or more lines are too long

1
dist/js/vendor/foundation.min.js vendored Normal file

File diff suppressed because one or more lines are too long

10716
dist/js/vendor/jquery.js vendored Normal file

File diff suppressed because it is too large Load diff

517
dist/js/vendor/what-input.js vendored Normal file
View file

@ -0,0 +1,517 @@
/**
* what-input - A global utility for tracking the current input method (mouse, keyboard or touch).
* @version v5.2.12
* @link https://github.com/ten1seven/what-input
* @license MIT
*/
(function webpackUniversalModuleDefinition(root, factory) {
if(typeof exports === 'object' && typeof module === 'object')
module.exports = factory();
else if(typeof define === 'function' && define.amd)
define("whatInput", [], factory);
else if(typeof exports === 'object')
exports["whatInput"] = factory();
else
root["whatInput"] = factory();
})(this, function() {
return /******/ (function(modules) { // webpackBootstrap
/******/ // The module cache
/******/ var installedModules = {};
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/ // Check if module is in cache
/******/ if(installedModules[moduleId])
/******/ return installedModules[moduleId].exports;
/******/ // Create a new module (and put it into the cache)
/******/ var module = installedModules[moduleId] = {
/******/ exports: {},
/******/ id: moduleId,
/******/ loaded: false
/******/ };
/******/ // Execute the module function
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/ // Flag the module as loaded
/******/ module.loaded = true;
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/ // expose the modules object (__webpack_modules__)
/******/ __webpack_require__.m = modules;
/******/ // expose the module cache
/******/ __webpack_require__.c = installedModules;
/******/ // __webpack_public_path__
/******/ __webpack_require__.p = "";
/******/ // Load entry module and return exports
/******/ return __webpack_require__(0);
/******/ })
/************************************************************************/
/******/ ([
/* 0 */
/***/ (function(module, exports) {
'use strict';
module.exports = function () {
/*
* bail out if there is no document or window
* (i.e. in a node/non-DOM environment)
*
* Return a stubbed API instead
*/
if (typeof document === 'undefined' || typeof window === 'undefined') {
return {
// always return "initial" because no interaction will ever be detected
ask: function ask() {
return 'initial';
},
// always return null
element: function element() {
return null;
},
// no-op
ignoreKeys: function ignoreKeys() {},
// no-op
specificKeys: function specificKeys() {},
// no-op
registerOnChange: function registerOnChange() {},
// no-op
unRegisterOnChange: function unRegisterOnChange() {}
};
}
/*
* variables
*/
// cache document.documentElement
var docElem = document.documentElement;
// currently focused dom element
var currentElement = null;
// last used input type
var currentInput = 'initial';
// last used input intent
var currentIntent = currentInput;
// UNIX timestamp of current event
var currentTimestamp = Date.now();
// check for a `data-whatpersist` attribute on either the `html` or `body` elements, defaults to `true`
var shouldPersist = false;
// form input types
var formInputs = ['button', 'input', 'select', 'textarea'];
// empty array for holding callback functions
var functionList = [];
// list of modifier keys commonly used with the mouse and
// can be safely ignored to prevent false keyboard detection
var ignoreMap = [16, // shift
17, // control
18, // alt
91, // Windows key / left Apple cmd
93 // Windows menu / right Apple cmd
];
var specificMap = [];
// mapping of events to input types
var inputMap = {
keydown: 'keyboard',
keyup: 'keyboard',
mousedown: 'mouse',
mousemove: 'mouse',
MSPointerDown: 'pointer',
MSPointerMove: 'pointer',
pointerdown: 'pointer',
pointermove: 'pointer',
touchstart: 'touch',
touchend: 'touch'
// boolean: true if the page is being scrolled
};var isScrolling = false;
// store current mouse position
var mousePos = {
x: null,
y: null
// map of IE 10 pointer events
};var pointerMap = {
2: 'touch',
3: 'touch', // treat pen like touch
4: 'mouse'
// check support for passive event listeners
};var supportsPassive = false;
try {
var opts = Object.defineProperty({}, 'passive', {
get: function get() {
supportsPassive = true;
}
});
window.addEventListener('test', null, opts);
} catch (e) {}
// fail silently
/*
* set up
*/
var setUp = function setUp() {
// add correct mouse wheel event mapping to `inputMap`
inputMap[detectWheel()] = 'mouse';
addListeners();
};
/*
* events
*/
var addListeners = function addListeners() {
// `pointermove`, `MSPointerMove`, `mousemove` and mouse wheel event binding
// can only demonstrate potential, but not actual, interaction
// and are treated separately
var options = supportsPassive ? { passive: true, capture: true } : true;
document.addEventListener('DOMContentLoaded', setPersist, true);
// pointer events (mouse, pen, touch)
if (window.PointerEvent) {
window.addEventListener('pointerdown', setInput, true);
window.addEventListener('pointermove', setIntent, true);
} else if (window.MSPointerEvent) {
window.addEventListener('MSPointerDown', setInput, true);
window.addEventListener('MSPointerMove', setIntent, true);
} else {
// mouse events
window.addEventListener('mousedown', setInput, true);
window.addEventListener('mousemove', setIntent, true);
// touch events
if ('ontouchstart' in window) {
window.addEventListener('touchstart', setInput, options);
window.addEventListener('touchend', setInput, true);
}
}
// mouse wheel
window.addEventListener(detectWheel(), setIntent, options);
// keyboard events
window.addEventListener('keydown', setInput, true);
window.addEventListener('keyup', setInput, true);
// focus events
window.addEventListener('focusin', setElement, true);
window.addEventListener('focusout', clearElement, true);
};
// checks if input persistence should happen and
// get saved state from session storage if true (defaults to `false`)
var setPersist = function setPersist() {
shouldPersist = !(docElem.getAttribute('data-whatpersist') === 'false' || document.body.getAttribute('data-whatpersist') === 'false');
if (shouldPersist) {
// check for session variables and use if available
try {
if (window.sessionStorage.getItem('what-input')) {
currentInput = window.sessionStorage.getItem('what-input');
}
if (window.sessionStorage.getItem('what-intent')) {
currentIntent = window.sessionStorage.getItem('what-intent');
}
} catch (e) {
// fail silently
}
}
// always run these so at least `initial` state is set
doUpdate('input');
doUpdate('intent');
};
// checks conditions before updating new input
var setInput = function setInput(event) {
var eventKey = event.which;
var value = inputMap[event.type];
if (value === 'pointer') {
value = pointerType(event);
}
var ignoreMatch = !specificMap.length && ignoreMap.indexOf(eventKey) === -1;
var specificMatch = specificMap.length && specificMap.indexOf(eventKey) !== -1;
var shouldUpdate = value === 'keyboard' && eventKey && (ignoreMatch || specificMatch) || value === 'mouse' || value === 'touch';
// prevent touch detection from being overridden by event execution order
if (validateTouch(value)) {
shouldUpdate = false;
}
if (shouldUpdate && currentInput !== value) {
currentInput = value;
persistInput('input', currentInput);
doUpdate('input');
}
if (shouldUpdate && currentIntent !== value) {
// preserve intent for keyboard interaction with form fields
var activeElem = document.activeElement;
var notFormInput = activeElem && activeElem.nodeName && (formInputs.indexOf(activeElem.nodeName.toLowerCase()) === -1 || activeElem.nodeName.toLowerCase() === 'button' && !checkClosest(activeElem, 'form'));
if (notFormInput) {
currentIntent = value;
persistInput('intent', currentIntent);
doUpdate('intent');
}
}
};
// updates the doc and `inputTypes` array with new input
var doUpdate = function doUpdate(which) {
docElem.setAttribute('data-what' + which, which === 'input' ? currentInput : currentIntent);
fireFunctions(which);
};
// updates input intent for `mousemove` and `pointermove`
var setIntent = function setIntent(event) {
var value = inputMap[event.type];
if (value === 'pointer') {
value = pointerType(event);
}
// test to see if `mousemove` happened relative to the screen to detect scrolling versus mousemove
detectScrolling(event);
// only execute if scrolling isn't happening
if ((!isScrolling && !validateTouch(value) || isScrolling && event.type === 'wheel' || event.type === 'mousewheel' || event.type === 'DOMMouseScroll') && currentIntent !== value) {
currentIntent = value;
persistInput('intent', currentIntent);
doUpdate('intent');
}
};
var setElement = function setElement(event) {
if (!event.target.nodeName) {
// If nodeName is undefined, clear the element
// This can happen if click inside an <svg> element.
clearElement();
return;
}
currentElement = event.target.nodeName.toLowerCase();
docElem.setAttribute('data-whatelement', currentElement);
if (event.target.classList && event.target.classList.length) {
docElem.setAttribute('data-whatclasses', event.target.classList.toString().replace(' ', ','));
}
};
var clearElement = function clearElement() {
currentElement = null;
docElem.removeAttribute('data-whatelement');
docElem.removeAttribute('data-whatclasses');
};
var persistInput = function persistInput(which, value) {
if (shouldPersist) {
try {
window.sessionStorage.setItem('what-' + which, value);
} catch (e) {
// fail silently
}
}
};
/*
* utilities
*/
var pointerType = function pointerType(event) {
if (typeof event.pointerType === 'number') {
return pointerMap[event.pointerType];
} else {
// treat pen like touch
return event.pointerType === 'pen' ? 'touch' : event.pointerType;
}
};
// prevent touch detection from being overridden by event execution order
var validateTouch = function validateTouch(value) {
var timestamp = Date.now();
var touchIsValid = value === 'mouse' && currentInput === 'touch' && timestamp - currentTimestamp < 200;
currentTimestamp = timestamp;
return touchIsValid;
};
// detect version of mouse wheel event to use
// via https://developer.mozilla.org/en-US/docs/Web/API/Element/wheel_event
var detectWheel = function detectWheel() {
var wheelType = null;
// Modern browsers support "wheel"
if ('onwheel' in document.createElement('div')) {
wheelType = 'wheel';
} else {
// Webkit and IE support at least "mousewheel"
// or assume that remaining browsers are older Firefox
wheelType = document.onmousewheel !== undefined ? 'mousewheel' : 'DOMMouseScroll';
}
return wheelType;
};
// runs callback functions
var fireFunctions = function fireFunctions(type) {
for (var i = 0, len = functionList.length; i < len; i++) {
if (functionList[i].type === type) {
functionList[i].fn.call(undefined, type === 'input' ? currentInput : currentIntent);
}
}
};
// finds matching element in an object
var objPos = function objPos(match) {
for (var i = 0, len = functionList.length; i < len; i++) {
if (functionList[i].fn === match) {
return i;
}
}
};
var detectScrolling = function detectScrolling(event) {
if (mousePos.x !== event.screenX || mousePos.y !== event.screenY) {
isScrolling = false;
mousePos.x = event.screenX;
mousePos.y = event.screenY;
} else {
isScrolling = true;
}
};
// manual version of `closest()`
var checkClosest = function checkClosest(elem, tag) {
var ElementPrototype = window.Element.prototype;
if (!ElementPrototype.matches) {
ElementPrototype.matches = ElementPrototype.msMatchesSelector || ElementPrototype.webkitMatchesSelector;
}
if (!ElementPrototype.closest) {
do {
if (elem.matches(tag)) {
return elem;
}
elem = elem.parentElement || elem.parentNode;
} while (elem !== null && elem.nodeType === 1);
return null;
} else {
return elem.closest(tag);
}
};
/*
* init
*/
// don't start script unless browser cuts the mustard
// (also passes if polyfills are used)
if ('addEventListener' in window && Array.prototype.indexOf) {
setUp();
}
/*
* api
*/
return {
// returns string: the current input type
// opt: 'intent'|'input'
// 'input' (default): returns the same value as the `data-whatinput` attribute
// 'intent': includes `data-whatintent` value if it's different than `data-whatinput`
ask: function ask(opt) {
return opt === 'intent' ? currentIntent : currentInput;
},
// returns string: the currently focused element or null
element: function element() {
return currentElement;
},
// overwrites ignored keys with provided array
ignoreKeys: function ignoreKeys(arr) {
ignoreMap = arr;
},
// overwrites specific char keys to update on
specificKeys: function specificKeys(arr) {
specificMap = arr;
},
// attach functions to input and intent "events"
// funct: function to fire on change
// eventType: 'input'|'intent'
registerOnChange: function registerOnChange(fn, eventType) {
functionList.push({
fn: fn,
type: eventType || 'input'
});
},
unRegisterOnChange: function unRegisterOnChange(fn) {
var position = objPos(fn);
if (position || position === 0) {
functionList.splice(position, 1);
}
},
clearStorage: function clearStorage() {
window.sessionStorage.clear();
}
};
}();
/***/ })
/******/ ])
});
;

66
dist/privacy.html vendored Normal file
View file

@ -0,0 +1,66 @@
<!doctype html>
<html class="no-js" lang="en" dir="ltr">
<head>
<meta charset="utf-8">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Syng.Rocks</title>
<link rel="stylesheet" href="css/foundation.css">
<link rel="stylesheet" href="css/app.css">
</head>
<body>
<div class="top-content-section">
<div class="top-bar">
<div class="top-bar-left">
<ul class="menu">
<li class="menu-text"><img src="images/rocks.syng.Syng.png" alt="Syng.Rocks! Logo" height=30></li>
<li><a href="index.html">Home</a></li>
<li><a href="install.html">Installation</a></li>
<li><a href="faq.html">FAQ</a></li>
<li><a href="privacy.html">Privacy Policy</a></li>
</ul>
</div>
</div>
</div>
<div id="main-content-section" style="padding-top: 100px;">
<div class="grid-container">
<h1>Privacy Policy</h1>
<p>Syng.Rocks! respects your privacy. We do not collect any personal data, apart from what is necessary for the application to function and for anti-abuse reasons. </p>
<p>The only data shared from using the web client is the following</p>
<ul>
<li>The chosen username</li>
<li>The name of the room you connect to</li>
<li>Search queries</li>
<li>Enqueued songs</li>
<li>The IP address of your device</li>
</ul>
<p>This data may be collected in log files and used for maintenance and anti-abuse reasons. We will never sell your data.</p>
<p>The data shared from using the playback client is the following</p>
<ul>
<li>The chosen room name</li>
<li>The admin password</li>
<li>The configuration of the room</li>
<li>The configuration of the sources, excluding any authentication data</li>
<li>A list of file names present in the sources to handle search request</li>
</ul>
<p>This data will be deleted from the server, once a karaoke session is removed from the server, which is two hours after the last client disconnects.</p>
<p>Syng.Rocks! does not use any cookies, but stores the chosen username and room name in the local storage of your browser. The local storage is cleared, if you press the <code>Log out</code> button.</p>
</div>
</div>
<script src="js/vendor/jquery.js"></script>
<script src="js/vendor/what-input.js"></script>
<script src="js/vendor/foundation.js"></script>
<script src="js/app.js"></script>
</body>
</html>

110
faq.html
View file

@ -1,110 +0,0 @@
<!doctype html>
<html class="no-js" lang="en" dir="ltr">
<head>
<meta charset="utf-8">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Syng.Rocks</title>
<link rel="stylesheet" href="css/foundation.css">
<link rel="stylesheet" href="css/app.css">
</head>
<body>
<div class="top-content-section">
<div class="top-bar">
<div class="top-bar-left">
<ul class="menu">
<li class="menu-text"><img src="images/rocks.syng.Syng.png" alt="Syng.Rocks! Logo" height=30></li>
<li><a href="index.html">Home</a></li>
<li><a href="install.html">Installation</a></li>
<li><a href="faq.html">FAQ</a></li>
<li><a href="#">Server</a></li>
<li><a href="#privacy">Privacy Policy</a></li>
</ul>
</div>
</div>
</div>
<div id="main-content-section" style="padding-top: 100px;">
<div class="grid-container">
<h1>FAQ</h1>
<dl>
<dt>Does Syng.Rocks! cost money?</dt>
<dd>
No, Syng.Rocks! is free software and licensed under the GNU Affero General Public License v3.0 (AGPL-3.0). You can use it for free without any limitations.
</dd>
<dt>Can you add song XYZ?</dt>
<dd>
Syng.Rocks! does not host any songs itself.
The client fetches songs from YouTube or any of your configured sources.
</dd>
<dt>What are sources?</dt>
<dd>
Since Syng.Rocks! does not host any songs itself, songs played by the playback client, need to come from external sources. Currently the following sources can be added:
<ul>
<li>YouTube</li>
<li>A S3 compatible server</li>
<li>A local directory</li>
</ul>
The sources are accessed from the playback client directly, the server is mostly unaware of the sources. It currently is sent a list of files to utilize the search feature.
</dd>
<dt>What file format can Syng.Rocks! play?</dt>
<dd>
Syng.Rocks! uses <a href="https://mpv.io" target="_blank">MPV</a> to play the karaoke songs, so it supports most video formats including <code>cdg+mp3</code>.
</dd>
<dt>What do you mean by <emph>Playback Client</emph>,<emph>Web Client</emph> and <emph>Server</emph>?</dt>
<dd>
<p></p>The <emph>Playback Client</emph> is the application that plays the songs and displays the lyrics. It is a desktop application.
The <emph>Web Client</emph> is the web application that allows you to manage the queue and search for songs. It is used by the guests to add songs to the queue.
The <emph>Server</emph> is the backend that manages the queue and handles connection between web clients and playback clients.</p>
<p>
If you only want to host a karaoke event, you only need the playback client.
The server is optional, as you can use the public server at <a href="https://syng.rocks" target="_blank">syng.rocks</a>.
</p>
</dd>
<dt>Can I use Syng.Rocks! without internet?</dt>
<dd>
Yes and no. The playback client needs to connect to a server instance. While you can host the server yourself in your intranet, the featureset will be limited, as some source (such as YouTube) need the playback client to reach YouTube. You can however use local and s3 sources, that are reachable from the playback client.
</dd>
<dt>Can I use Syng.Rocks! without a server?</dt>
<dd>
You can use the public server at <a href="https://syng.rocks" target="_blank">syng.rocks</a> to host your karaoke events. No need to roll up your own server.
</dd>
<dt>Can I host my own server?</dt>
<dd>
Yes, you can host your own server. The server is available as a Docker image on GitHub, or you can install it using the Python Package Index (PyPI) package <code>syng[server]</code>.
For more information, see the <a href="install.html">installation instructions</a>.
</dd>
<dt><a id="waiting-room"></a>What is the waiting room?</dt>
<dd>
The waiting room is a feature that allows you to limit the amount of songs a guest can have in the queue.
If the waiting room is enabled, guests can only add songs to the queue if they have no songs in the queue. All additional songs are placed in a waiting room and automatically leave the queue once no songs of the guest are left in the queue.
</dd>
<dt>What data do you store?</dt>
<dd>
Syng.Rocks! does not store any personal data. The server only stores the current state of the queue.
If you use the public server at <a href="https://syng.rocks" target="_blank">syng.rocks</a>, the server will also store your IP address for a limited time to prevent abuse.
For more information, see the <a href="#privacy">privacy policy</a>.
</dd>
<dt>I cannot connect to <a href="https://syng.rocks" target="_blank">https://syng.rocks</a>.</dt>
<dd>
Sometimes the server is down for maintenance. Check the official <a href="https://floss.social/@syng" target="_blank">Mastodon account</a> for announcements. Also check, if your client is up to date. The server is only compatible with the latest release.
</dd>
<dt>What is the restricted mode of the server?</dt>
<dd>
When the server is in restricted mode, <i>server side search</i> is only available to authenticated rooms. For other rooms, a search request is forwarded to the playback client and results are then send back to the web client.
</dd>
<dt>Something does not work, or I have questions not covered here</dt>
<dd>
Feel free to open an issue on the official issue tracker on <a href="https://github.com/christofsteel/syng/issues" target="_blank">GitHub</a> or reach out to us at our official Mastodon account: <a href="https://floss.social/@syng" target="_blank">@syng@floss.social</a> account or join our official matrix room: <a href="https://matrix.to/#/#syng:matrix.org" target="_blank">#syng:matrix.org</a>.
</dd>
</dl>
</div>
</div>
<script src="js/vendor/jquery.js"></script>
<script src="js/vendor/what-input.js"></script>
<script src="js/vendor/foundation.js"></script>
<script src="js/app.js"></script>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 165 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 29 KiB

View file

@ -1,110 +0,0 @@
<!doctype html>
<html class="no-js" lang="en" dir="ltr">
<head>
<meta charset="utf-8">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Syng.Rocks</title>
<link rel="stylesheet" href="css/foundation.css">
<link rel="stylesheet" href="css/app.css">
</head>
<body>
<div class="top-content-section">
<div class="top-bar">
<div class="top-bar-left">
<ul class="menu">
<li class="menu-text"><img src="images/rocks.syng.Syng.png" alt="Syng.Rocks! Logo" height=30></li>
<li><a href="index.html">Home</a></li>
<li><a href="install.html">Installation</a></li>
<li><a href="faq.html">FAQ</a></li>
<li><a href="#">Server</a></li>
<li><a href="#privacy">Privacy Policy</a></li>
</ul>
</div>
</div>
</div>
<div class="hero-full-screen">
<div class="middle-content-section grid-container">
<div class="grid-x align-middle">
<div class="cell large-6">
<img src="images/overview.png" alt="Syng.Rocks Overview" class="work-feature-block-image overview-image"/>
</div>
<div class="cell large-6">
<div class="hero-section-text">
<h1>Syng.Rocks!</h1>
<h5>Easily host karaoke events</h5>
<p>
Syng.Rocks! is a karaoke app that allows you to host karaoke events without much hassle.
Whether you have a big collection of karaoke songs, or just want to stream karaoke songs from YouTube, whether you want to sing with a couple of friends or with a crowd of over 100 people, Syng.Rocks! has you covered in a privacy friendly manner. No need to register, no need to log in, Syng.Rocks! will never collect any personal data from you. You can even host your own server if you want to.
</p>
<p>
<a href="https://flathub.org/apps/rocks.syng.Syng" target="_blank" class="button">Linux/Flatpak</a>
<a href="https://github.com/christofsteel/syng/releases" target="_blank" class="button">Windows</a>
<a href="https://syng.rocks" target="_blank" class="button hollow">Web Client</a>
<a href="https://github.com/christofsteel/syng" target="_blank" class="button hollow">Source Code</a>
</p>
</div>
</div>
</div>
</div>
</div>
<div id="main-content-section">
<div class="grid-container">
<div class="grid-x grid-padding-x textleft align-right">
<div class="cell medium-5 text-right">
<h2>Get started</h2>
<p>
If you only want to have a small YouTube based karaoke event, you can just download the app, share the generated QR Code and start singing. No registration, no login and no configuration required.
</p>
</div>
<div class="cell medium-5">
<img src="images/syng.png" alt="Syng.Rocks Client" class="work-feature-block-image"/>
</div>
</div>
<div class="grid-x grid-padding-x textright">
<div class="cell medium-8">
<img src="images/web-shadows.png" alt="Syng.Rocks Web Client" class="work-feature-block-image"/>
</div>
<div class="cell medium-4">
<h2>Web client</h2>
<p>
Let your guests join your karaoke event using the web client. They can use their browser on their own devices to search for songs and queue them up.
The web client also contains an admin mode, that allows for manual moderation of the song queue.
</p>
</div>
</div>
<div class="grid-x grid-padding-x textleft">
<div class="cell medium-4">
<h2>Configure to your needs</h2>
<p>
Syng allows for a lot of configuration options. You can set up your own song database, you can set up a <a href="faq.html#waiting-room">waiting room</a>, a set time the event ends and much more.
</p>
</div>
<div class="cell medium-8">
<img src="./images/syng_advanced.png" alt="Syng.Rocks Client" class="work-feature-block-image"/>
</div>
</div>
<div class="grid-x grid-padding-x textright">
<div class="cell medium-7">
<img src="images/sources.svg" style="max-height:250px" alt="Syng.Rocks Web Client" class="work-feature-block-image"/>
</div>
<div class="cell medium-5">
<h2>Multiple Sources</h2>
<p>
Syng.Rocks! supports multiple sources for karaoke songs. The default source is YouTube, but you can configure a remote S3 compatible server or simply a folder on your system. The client connects directly to the sources, no login data is ever shared with the server.
</p>
</div>
</div>
</div>
</div>
<script src="js/vendor/jquery.js"></script>
<script src="js/vendor/what-input.js"></script>
<script src="js/vendor/foundation.js"></script>
<script src="js/app.js"></script>
</body>
</html>

View file

@ -1,67 +0,0 @@
<!doctype html>
<html class="no-js" lang="en" dir="ltr">
<head>
<meta charset="utf-8">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Syng.Rocks</title>
<link rel="stylesheet" href="css/foundation.css">
<link rel="stylesheet" href="css/app.css">
</head>
<body>
<div class="top-content-section">
<div class="top-bar">
<div class="top-bar-left">
<ul class="menu">
<li class="menu-text"><img src="images/rocks.syng.Syng.png" alt="Syng.Rocks! Logo" height=30></li>
<li><a href="index.html">Home</a></li>
<li><a href="install.html">Installation</a></li>
<li><a href="#">FAQ</a></li>
<li><a href="#">Server</a></li>
<li><a href="#privacy">Privacy Policy</a></li>
</ul>
</div>
</div>
</div>
<div id="main-content-section" style="padding-top: 100px;">
<div class="grid-container">
<h1>Installation</h1>
<h2>Client</h2>
<p>
Depending on your platform, you can install Syng.Rocks! in different ways.
<dl>
<dt>Linux</dt>
<dd>
Syng.Rocks! is available as a Flatpak on <a href="https://flathub.org/apps/rocks.syng.Syng" target="_blank">Flathub</a>. You can install it using the command line with:
<pre>flatpak install rocks.syng.Syng</pre>
There also exists an Arch Linux package in the <a href="https://aur.archlinux.org/packages/syng-client" target="_blank">AUR</a>.
</dd>
<dt>Windows</dt>
<dd>
Windows binaries are available on <a href="https://github.com/christofsteel/syng/releases">GitHub</a>. This is a portable version and does not require installation.
</dd>
<dt>Generic</dt>
<dd>
The Syng.Rocks! client is available in the Python Package Index (PyPI) as <code>syng</code>. You can install it using pip:
<pre>pip install syng[client]</pre>
</dd>
</dl>
</p>
<h2>Server</h2>
<p>
The server is available as a Docker image hosted on GitHub. You can install it with
<pre>docker run ghcr.io/christofsteel/syng</pre>
Alternatively, you can also run the server using the Python Package Index (PyPI) package <code>syng</code>:
<pre>pip install syng[server]</pre>
</p>
</div>
</div>
<script src="js/vendor/jquery.js"></script>
<script src="js/vendor/what-input.js"></script>
<script src="js/vendor/foundation.js"></script>
<script src="js/app.js"></script>
</body>
</html>

41
site.template.html Normal file
View file

@ -0,0 +1,41 @@
{% set nav = {
"Home": "index.html",
"Installation": "install.html",
"FAQ": "faq.html",
"Privacy Policy": "privacy.html",
} %}
<!doctype html>
<html class="no-js" lang="en" dir="ltr">
<head>
<meta charset="utf-8">
<meta http-equiv="x-ua-compatible" content="ie=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Syng.Rocks</title>
<link rel="stylesheet" href="css/foundation.css">
<link rel="stylesheet" href="css/app.css">
</head>
<body>
<div class="top-content-section">
<div class="top-bar">
<div class="top-bar-left">
<ul class="menu">
<li class="menu-text"><img src="images/rocks.syng.Syng.png" alt="Syng.Rocks! Logo" height=30></li>
{% for name, url in nav.items() %}
<li><a href="{{ url }}">{{ name }}</a></li>
{% endfor %}
</ul>
</div>
</div>
</div>
{{ header }}
{{ content }}
<script src="js/vendor/jquery.js"></script>
<script src="js/vendor/what-input.js"></script>
<script src="js/vendor/foundation.js"></script>
<script src="js/app.js"></script>
</body>
</html>

73
sites/faq.md Normal file
View file

@ -0,0 +1,73 @@
!!section:content,markdown
/// html | div#main-content-section[style='padding-top: 100px;']
/// html | div.grid-container
# FAQ
Is using Syng.Rocks! free?
: Yes, Syng.Rocks! is free software and licensed under the GNU Affero General Public License v3.0 (AGPL-3.0). You can use it for free without any limitations.
Can you add a specific song?
: Syng.Rocks! does not host any songs itself. The client fetches songs from YouTube or any of your configured sources.
What are sources?
: Since Syng.Rocks! does not host any songs itself, songs played by the playback client, need to come from external sources. Currently, the following sources can be added:
* YouTube
* Any S3 compatible server
* A local directory
The sources are accessed from the playback client directly, the server is unaware of the contents of the sources and just uses a list of filenames to search for songs.
What file format can Syng.Rocks! play?
: Syng.Rocks! uses [MPV](https://mpv.io) to play the karaoke songs, so it supports most video formats including `cdg+mp3`.
What do you mean by *Playback Client*, *Web Client* and *Server*?
: The *Playback Client* is the application that plays the songs and displays the lyrics. It is a desktop application.
The *Web Client* is the web application that allows you to manage the queue and search for songs. It is used by the guests to add songs to the queue.
The *Server* is the backend that manages the queue and handles connection between web clients and playback clients.
If you only want to host a karaoke event, you only need the playback client.
The server is optional, as you can use the public server at [syng.rocks](https://syng.rocks).
Can I use Syng.Rocks! without internet?
: Yes and no. The playback client needs to connect to a server instance. While you can host the server yourself in your intranet, the experience can be limited, as some source (such as YouTube) need the playback client to reach YouTube. You can however use local and s3 sources, that are reachable from the playback client.
Can I use Syng.Rocks! without a server?
: You do need some kind of server to manage the queue, but you can use the public server at [syng.rocks](https://syng.rocks) to host your karaoke events. No need to roll up your own server.
Can I host my own server?
: Yes, you can host your own server. The server is available as a Docker image on GitHub, or you can install it using the Python Package Index (PyPI) package `syng[server]`.
For more information, see the [installation instructions](install.html).
<a id="waiting-room"></a>What is the waiting room?
: The waiting room is a feature that allows you to limit the amount of songs a guest can have in the queue.
If the waiting room is enabled, guests can only add songs to the queue if they have no songs in the queue. All additional songs are placed in a waiting room and automatically leave the queue once no songs of the guest are left in the queue.
What data do you store?
: Syng.Rocks! does not store any personal data. The server only stores the current state of the queue.
If you use the public server at [syng.rocks](https://syng.rocks), the server will also store your IP address for a limited time to prevent abuse.
For more information, see the [privacy policy](privacy.html).
I cannot connect to [syng.rocks](https://syng.rocks).
: Sometimes the server is down for maintenance. Check the official [Mastodon account](https://floss.social/@syng) for announcements. Also check, if your client is up-to-date. The server is only compatible with the latest release.
What is the restricted mode of the server?
: When the server is in restricted mode, *server side search* is only available to authenticated rooms. For other rooms, a search request is forwarded to the playback client and results are then sent back to the web client.
Something does not work, or I have questions not covered here
: Feel free to open an issue on the official issue tracker on [GitHub](https://github.com/christofsteel/syng/issues) or reach out to us at our official Mastodon account: [@syng@floss.social](https://floss.social/@syng) account or join our official matrix room: [#syng:matrix.org](https://matrix.to/#/#syng:matrix.org).
///
///

66
sites/index.md Normal file
View file

@ -0,0 +1,66 @@
!!section:header,markdown
/// html | div.hero-full-screen
/// html | div.middle-content-section.grid-container
/// html | div.grid-x.align-middle
/// html | div.cell.large-6
![Syng.Rocks! Overview](images/overview.png)
///
/// html | div.cell.large-6
/// html | div.hero-section-text
# Syng.Rocks!
##### Easily host karaoke events
Syng.Rocks! is a karaoke app that allows you to host karaoke events without much hassle. Whether you have a big collection of karaoke songs, or just want to stream karaoke songs from YouTube, whether you want to sing with a couple of friends or with a crowd of over 100 people, Syng.Rocks! has you covered in a privacy-friendly manner. No need to register, no need to log in, Syng.Rocks! will never collect any personal data from you. You can even host your own server if you want to.
<a href="https://flathub.org/apps/rocks.syng.Syng" target="_blank" class="button">Linux/Flatpak</a>
<a href="https://github.com/christofsteel/syng/releases" target="_blank" class="button">Windows</a>
<a href="https://syng.rocks" target="_blank" class="button hollow">Web Client</a>
<a href="https://github.com/christofsteel/syng" target="_blank" class="button hollow">Source Code</a>
!!section:content,markdown
/// html | div#main-content-section
/// html | div.grid-container
/// html | div.grid-x.grid-padding.textleft.align-right
/// html | div.cell.medium-5.text-right
## Start Singing
If you only want to have a small YouTube based karaoke event, you can just download the app, share the generated QR Code and start singing. No registration, no login and no configuration required.
///
/// html | div.cell.medium-5.showcase-image
![Syng.Rocks! Client](images/syng.png)
///
///
/// html | div.grid-x.grid-padding.textright
/// html | div.cell.medium-8.showcase-image
![Syng.Rocks! Web Client](images/web-shadows.png)
///
/// html | div.cell.medium-4
## Web client
Let your guests join your karaoke event using the web client. They can use their browser on their own devices to search for songs and queue them up.
The web client also contains an admin mode, that allows for manual moderation of the song queue.
///
///
/// html | div.grid-x.grid-padding.textleft.align-right
/// html | div.cell.medium-4.text-right
## Configure to your needs
Syng.Rocks! allows for a lot of configuration options. You can set up your own song database, you can set up a [waiting room](faq.html#waiting-room), a set time the event ends and much more.
///
/// html | div.cell.medium-8.showcase-image
![Syng.Rocks! Web Client](images/syng_advanced.png)
///
///
/// html | div.grid-x.grid-padding.textright
/// html | div.cell.medium-7.showcase-image
![Syng.Rocks! Web Client](images/sources.svg)
///
/// html | div.cell.medium-5
## Multiple Sources
Syng.Rocks! supports multiple sources for karaoke songs. The default source is YouTube, but you can configure a remote S3 compatible server or simply a folder on your system. The client connects directly to the sources, no login data is ever shared with the server.
///
///

41
sites/install.md Normal file
View file

@ -0,0 +1,41 @@
!!section:content,markdown
/// html | div#main-content-section[style='padding-top: 100px;']
/// html | div.grid-container
# Installation
## Client
Depending on your platform, you can install Syng.Rocks! in different ways.
Linux
: Syng.Rocks! is available as a Flatpak on [Flathub](https://flathub.org/apps/rocks.syng.Syng). You can install it using the command line with:
```
flatpak install rocks.syng.Syng
```
There also exists an Arch Linux package in the [AUR](https://aur.archlinux.org/packages/syng-client).
Windows
: Windows binaries are available on [Github](https://github.com/christofsteel/syng/releases). This is a portable version and does not require installation.
Generic
: Syng.Rocks! is available in the Python Package Index (PyPI) as `syng`. You can install the client using pip:
```
pip install syng[client]
```
## Server
The server is available as a Docker image hosted on Github. You can run it with:
docker run ghcr.io/christofsteel/syng
Alternatively, you can run the server using the Python Package Index (PyPI) package `syng`:
pip install syng[server]
///
///

28
sites/privacy.md Normal file
View file

@ -0,0 +1,28 @@
!!section:content,markdown
/// html | div#main-content-section[style='padding-top: 100px;']
/// html | div.grid-container
# Privacy Policy
Syng.Rocks! respects your privacy. We do not collect any personal data, apart from what is necessary for the application to function and for anti-abuse reasons.
The only data shared from using the web client is the following
- The chosen username
- The name of the room you connect to
- Search queries
- Enqueued songs
- The IP address of your device
This data may be collected in log files and used for maintenance and anti-abuse reasons. We will never sell your data.
The data shared from using the playback client is the following
- The chosen room name
- The admin password
- The configuration of the room
- The configuration of the sources, excluding any authentication data
- A list of file names present in the sources to handle search request
This data will be deleted from the server, once a karaoke session is removed from the server, which is two hours after the last client disconnects.
Syng.Rocks! does not use any cookies, but stores the chosen username and room name in the local storage of your browser. The local storage is cleared, if you press the `Log out` button.