Launcher: Auto reload app dock every 5 seconds.
Animate install/uninstall flows.
Change-Id: Id06ddc0d5f8b3cef3205de99b3a16ba1ac285213
diff --git a/core/installer/welcome/launcher-tmpl/launcher.html b/core/installer/welcome/launcher-tmpl/launcher.html
index 0177c5d..6032869 100644
--- a/core/installer/welcome/launcher-tmpl/launcher.html
+++ b/core/installer/welcome/launcher-tmpl/launcher.html
@@ -5,7 +5,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>dodo: Launcher</title>
<link rel="stylesheet" type="text/css" href="/static/pico.2.0.6.min.css">
- <link rel="stylesheet" type="text/css" href="/static/launcher.css?v=0.0.18">
+ <link rel="stylesheet" type="text/css" href="/static/launcher.css?v=0.0.19">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/hack-font/3.3.0/web/hack.min.css">
</head>
<body class="container-fluid">
@@ -22,18 +22,19 @@
<hr class="separator">
<div class="app-list scrollbar-custom">
{{range .AllAppsInfo}}
- <div class="app-icon" data-app-id="{{ .Id }}" data-app-url="{{ .URL }}" {{ if not .URL }}data-modal-id="modal-{{ CleanAppName .Id }}"{{ end }}>
- {{.Icon}}
- </div>
- <div class="tooltip">
- <p>{{ .Name }}</p>
- {{ if .DisplayURL }}
+ <div class="app-container" id="{{ .Id }}">
+ <div class="app-icon" data-app-id="{{ .Id }}" data-app-url="{{ .URL }}" {{ if not .URL }}data-modal-id="modal-{{ CleanAppName .Id }}"{{ end }}>
+ {{.Icon}}
+ </div>
+ <div class="tooltip">
+ <p>{{ .Name }}</p>
+ {{ if .DisplayURL }}
<p>{{ .DisplayURL }}</p>
- {{ end }}
- {{ if .Help }}
+ {{ end }}
+ {{ if .Help }}
<button class="help-button" id="help-button-{{ CleanAppName .Id }}">Help</button>
- {{ end }}
- </div>
+ {{ end }}
+ </div>
<dialog class="app-help-modal" id="modal-{{ CleanAppName .Id }}" close>
<article class="modal-article">
<header>
@@ -56,6 +57,7 @@
</div>
</article>
</dialog>
+ </div>
{{end}}
</div>
</div>
@@ -80,6 +82,6 @@
{{ template "help-content-template" (dict "Help" $h.Children "First" false) }}
{{ end }}
{{ end }}
- <script src="/static/launcher.js?v=0.0.14"></script>
+ <script src="/static/launcher.js?v=0.0.19"></script>
</body>
</html>
diff --git a/core/installer/welcome/static/launcher.css b/core/installer/welcome/static/launcher.css
index 307c4e5..af8b293 100644
--- a/core/installer/welcome/static/launcher.css
+++ b/core/installer/welcome/static/launcher.css
@@ -90,6 +90,34 @@
cursor: pointer !important;
}
+@keyframes pulsate {
+ from { opacity: 1; }
+ 10% { opacity: 0; }
+ 20% { opacity: 1; }
+ 30% { opacity: 0; }
+ 40% { opacity: 1; }
+ 50% { opacity: 0; }
+ 60% { opacity: 1; }
+ 70% { opacity: 0; }
+ 80% { opacity: 1; }
+ 90% { opacity: 0; }
+ to { opacity: 1; }
+}
+
+@keyframes fadeout {
+ /* TODO(gio): figure out why animating from 1 does not work */
+ from { opacity: 0.999; }
+ to { opacity: 0; }
+}
+
+.pulsate {
+ animation: pulsate 5s linear;
+}
+
+.fadeout {
+ animation: fadeout 2s ease-in;
+}
+
.tooltip {
position: absolute;
width: 200px;
diff --git a/core/installer/welcome/static/launcher.js b/core/installer/welcome/static/launcher.js
index ed54a70..be5f216 100644
--- a/core/installer/welcome/static/launcher.js
+++ b/core/installer/welcome/static/launcher.js
@@ -10,6 +10,58 @@
document.addEventListener("DOMContentLoaded", function () {
document.getElementById('appFrame-default').contentDocument.write("Welcome to the dodo: application launcher, think of it as your desktop environment. You can launch applications from left-hand side dock. You should setup VPN clients on your devices, so you can install applications from Application Manager and access your private network. Instructions on how to do that can be viewed by clicking <b>Help</b> button after hovering over <b>Headscale</b> icon in the dock.");
document.getElementById('appFrame-default').style.backgroundColor = '#d6d6d6';
+ initDock();
+ setTimeout(reloadDock, 5000);
+});
+
+function copyToClipboard(elem, text) {
+ navigator.clipboard.writeText(text);
+ elem.setAttribute("data-tooltip", "Copied");
+ elem.setAttribute("data-placement", "bottom");
+ setTimeout(() => {
+ elem.removeAttribute("data-tooltip");
+ elem.removeAttribute("data-placement");
+ }, 500);
+};
+
+function reloadDock() {
+ fetch("/").then(resp => resp.text()).then(resp => {
+ const tmp = document.createElement("div");
+ tmp.innerHTML = resp;
+ const apps = document.querySelector(".app-list");
+ let existing = [...document.querySelectorAll(".app-container")];
+ let current = [...tmp.querySelectorAll(".app-container")];
+ const getId = (e) => e.getAttribute("id");
+ const existingIds = existing.map(getId);
+ const currentIds = current.map(getId);
+ existing.forEach((e) => {
+ const id = getId(e);
+ if (!currentIds.includes(id)) {
+ e.classList.add("fadeout");
+ setTimeout(() => apps.removeChild(e), 1900);
+ }
+ });
+ let prevId = undefined;
+ current.forEach((c) => {
+ const id = getId(c);
+ if (existingIds.includes(id)) {
+ prevId = id;
+ return;
+ }
+ c.classList.add("pulsate");
+ if (prevId) {
+ apps.insertBefore(c, document.getElementById(prevId).nextSibling);
+ } else {
+ apps.insertBefore(c, apps.firstChild);
+ }
+ prevId = id;
+ });
+ initDock();
+ setTimeout(reloadDock, 5000);
+ });
+}
+
+function initDock() {
const icons = document.querySelectorAll(".app-icon");
const circle = document.querySelector(".user-circle");
const tooltipUser = document.querySelector("#tooltip-user");
@@ -33,6 +85,9 @@
let activeTooltip;
icons.forEach(function (icon) {
+ if (activeAppId && icon.getAttribute("data-app-id") === activeAppId) {
+ icon.style.color = "var(--button)";
+ }
icon.addEventListener("click", function (event) {
event.stopPropagation();
const appUrl = this.getAttribute("data-app-url");
@@ -42,7 +97,10 @@
if (!appUrl && modalId) {
openModal(document.getElementById(modalId));
} else {
- if (!iframes[appId]) createIframe(appId, appUrl);
+ if (!iframes[appId]) {
+ createIframe(appId, appUrl);
+ }
+ activeAppId = appId;
showIframe(appId);
document.querySelectorAll(".app-icon").forEach((icon) => {
icon.style.color = "var(--bodyBg)";
@@ -167,33 +225,24 @@
closeModal(visibleModal);
}
});
+}
- const iframes = {};
- const rightPanel = document.getElementById('right-panel');
+let activeAppId = undefined;
+const iframes = {};
+const rightPanel = document.getElementById('right-panel');
- function showIframe(appId) {
- document.querySelectorAll('.appFrame').forEach(iframe => {
- iframe.style.display = iframe.id === `appFrame-${appId}` ? 'block' : 'none';
- });
- };
+function showIframe(appId) {
+ document.querySelectorAll('.appFrame').forEach(iframe => {
+ iframe.style.display = iframe.id === `appFrame-${appId}` ? 'block' : 'none';
+ });
+};
- function createIframe(appId, appUrl) {
- const iframe = document.createElement('iframe');
- iframe.id = `appFrame-${appId}`;
- iframe.className = 'appFrame';
- iframe.src = appUrl;
- iframe.style.display = 'none';
- rightPanel.appendChild(iframe);
- iframes[appId] = iframe;
- };
-});
-
-function copyToClipboard(elem, text) {
- navigator.clipboard.writeText(text);
- elem.setAttribute("data-tooltip", "Copied");
- elem.setAttribute("data-placement", "bottom");
- setTimeout(() => {
- elem.removeAttribute("data-tooltip");
- elem.removeAttribute("data-placement");
- }, 500);
+function createIframe(appId, appUrl) {
+ const iframe = document.createElement('iframe');
+ iframe.id = `appFrame-${appId}`;
+ iframe.className = 'appFrame';
+ iframe.src = appUrl;
+ iframe.style.display = 'none';
+ rightPanel.appendChild(iframe);
+ iframes[appId] = iframe;
};