Merge branch 'master' into update-simple-icons

This commit is contained in:
Alicia Sykes
2024-04-29 00:54:02 +01:00
committed by GitHub
168 changed files with 7153 additions and 4891 deletions

View File

@@ -4,6 +4,7 @@
<LoadingScreen :isLoading="isLoading" v-if="shouldShowSplash" />
<Header :pageInfo="pageInfo" />
<router-view v-if="!isFetching" />
<CriticalError v-if="hasCriticalError" />
<Footer :text="footerText" v-if="visibleComponents.footer && !isFetching" />
</div>
</template>
@@ -12,6 +13,7 @@
import Header from '@/components/PageStrcture/Header.vue';
import Footer from '@/components/PageStrcture/Footer.vue';
import EditModeTopBanner from '@/components/InteractiveEditor/EditModeTopBanner.vue';
import CriticalError from '@/components/PageStrcture/CriticalError.vue';
import LoadingScreen from '@/components/PageStrcture/LoadingScreen.vue';
import { welcomeMsg } from '@/utils/CoolConsole';
import ErrorHandler from '@/utils/ErrorHandler';
@@ -29,6 +31,7 @@ export default {
Footer,
LoadingScreen,
EditModeTopBanner,
CriticalError,
},
data() {
return {
@@ -64,7 +67,7 @@ export default {
return this.$store.getters.pageInfo;
},
sections() {
return this.$store.getters.pageInfo;
return this.$store.getters.sections;
},
visibleComponents() {
return this.$store.getters.visibleComponents;
@@ -72,6 +75,9 @@ export default {
isEditMode() {
return this.$store.state.editMode;
},
hasCriticalError() {
return this.$store.state.criticalError;
},
subPageClassName() {
const currentSubPage = this.$store.state.currentConfigInfo;
return (currentSubPage && currentSubPage.pageId) ? currentSubPage.pageId : '';

View File

@@ -1,7 +1,7 @@
{
"home": {
"no-results": "لا نتائج للبحث",
"no-data": م يتم تكوين بيانات"
"no-data": ا يوجد بيانات"
},
"search": {
"search-label": "بحث",
@@ -33,11 +33,11 @@
},
"config": {
"main-tab": "القائمة الرئيسية",
"view-config-tab": "مشاهدة ملف Config",
"edit-config-tab": حرير التكوين",
"view-config-tab": "مشاهده ملف الإعدادات",
"edit-config-tab": غير ملف الاع",
"custom-css-tab": "الأنماط المخصصة",
"heading": "خيارات الإعداد",
"download-config-button": "تنزيل Config",
"download-config-button": "تنزيل ملف الإعدادات",
"edit-config-button": "تحرير التكوين",
"edit-css-button": "تحرير CSS مخصص",
"cloud-sync-button": "قم بتمكين Cloud Sync",
@@ -84,7 +84,7 @@
"sign-in-welcome": "مرحبًا {username}!"
},
"updates": {
"app-version-note": "نسخة متهورة",
"app-version-note": "ملاحظة نسخة التطبيق",
"up-to-date": "حتى الآن",
"out-of-date": "التحديث متاح",
"unsupported-version-l1": "أنت تستخدم إصدارًا غير مدعوم من Dashy",

View File

@@ -1,421 +1,448 @@
{
"home":
{
"no-results": "keine Suchergebnisse",
"no-data": "keine Daten konfiguriert",
"no-items-section": "Noch keine Elemente zum Anzeigen"
},
"search":
{
"search-label": "Suche",
"search-placeholder": "Tippen um zu filtern",
"clear-search-tooltip": "Suchfeld leeren",
"enter-to-search-web": "Drücke Enter um das Internet zu durchsuchen"
},
"login":
{
"title": "Dashy",
"username-label": "Benutzername",
"password-label": "Passwort",
"login-button": "Anmelden",
"remember-me-label": "Angemeldet bleiben für",
"remember-me-never": "Niemals",
"remember-me-hour": "4 Stunden",
"remember-me-day": "1 Tag",
"remember-me-week": "1 Woche",
"remember-me-long-time": "Eine lange Zeit",
"error-missing-username": "Benutzername fehlt",
"error-missing-password": "Passwort fehlt",
"error-incorrect-username": "Benutzer nicht gefunden",
"error-incorrect-password": "Falsches Passwort",
"success-message": "Anmeldung läuft...",
"logout-message": "Abgemeldet",
"already-logged-in-title": "Bereits angemeldet",
"already-logged-in-text": "Angemeldet als",
"proceed-to-dashboard": "Zum Dashboard fortfahren",
"log-out-button": "Abmelden",
"proceed-guest-button": "Als Gast fortfahren"
},
"config":
{
"main-tab": "Hauptmenü",
"view-config-tab": "Konfiguration",
"edit-config-tab": "Konfiguration bearbeiten",
"custom-css-tab": "eigene CSS",
"heading": "Konfigurationseinstellungen",
"download-config-button": "Konfigurationsdownload",
"edit-config-button": "Konfiguration bearbeiten",
"edit-css-button": "CSS bearbeiten",
"cloud-sync-button": "Cloud-Synchronisation aktivieren",
"edit-cloud-sync-button": "Cloud-Synchronisation bearbeiten",
"rebuild-app-button": "Anwendung neu kompilieren",
"change-language-button": "App-Sprache ändern",
"reset-settings-button": "lokale Einstellungen zurücksetzen",
"app-info-button": "App Informationen",
"backup-note": "Es wird empfohlen ein Backup der Konfiguration zu erstellen bevor Änderungen durchgeführt werden.",
"reset-config-msg-l1": "Dadurch werden alle Benutzereinstellungen aus dem lokalen Speicher entfernt, dies hat jedoch keine Auswirkungen auf Ihre Datei 'conf.yml'.",
"reset-config-msg-l2": "Sie sollten zuerst alle Änderungen, die Sie lokal vorgenommen haben, sichern, wenn Sie sie in Zukunft wiederverwenden möchten.",
"reset-config-msg-l3": "Sind Sie sicher, dass Sie fortfahren möchten?",
"data-cleared-msg": "Daten erfolgreich gelöscht",
"actions-label": "Aktionen",
"copy-config-label": "Konfiguration kopieren",
"data-copied-msg": "Konfiguration wurde in die Zwischenablage kopiert",
"reset-config-label": "Konfiguration zurücksetzen",
"css-save-btn": "Änderungen speichern",
"css-note-label": "Bemerkung",
"css-note-l1": "Sie müssen die Seite aktualisieren, damit Ihre Änderungen wirksam werden.",
"css-note-l2": "Stilüberschreibungen werden nur lokal gespeichert, daher wird empfohlen vorher eine Kopie Ihres CSS zu erstellen.",
"css-note-l3": "Um alle benutzerdefinierten Stile zu entfernen löschen Sie den Inhalt und klicken Sie auf Änderungen speichern."
},
"alternate-views": {
"alternate-view-heading": "Ansicht wechseln",
"default": "Standard",
"workspace": "Arbeitsplatz",
"minimal": "Minimal"
},
"settings":
{
"theme-label": "Design",
"layout-label": "Layout",
"layout-auto": "Auto",
"layout-horizontal": "Horizontal",
"layout-vertical": "Vertikal",
"item-size-label": "Itemgröße",
"item-size-small": "klein",
"item-size-medium": "mittel",
"item-size-large": "groß",
"config-launcher-label": "Konfiguration",
"config-launcher-tooltip": "Konfiguration aktualisieren",
"sign-out-tooltip": "Abmelden",
"sign-in-tooltip": "Anmelden",
"sign-in-welcome": "Hallo {username}!"
},
"updates":
{
"app-version-note": "Dashy Version",
"up-to-date": "Aktuell",
"out-of-date": "Update verfügbar",
"unsupported-version-l1": "Sie verwenden eine nicht unterstützte Version von Dashy",
"unsupported-version-l2": "Für die beste Erfahrung und aktuelle Sicherheitspatches aktualisieren Sie bitte auf"
},
"language-switcher":
{
"title": "Applikationssprache ändern",
"dropdown-label": "Sprache auswählen",
"save-button": "Speichern",
"success-msg": "Sprache geändert auf"
},
"theme-maker":
{
"title": "Design Konfigurator",
"export-button": "Benutzerdefinierte Variablen exportieren",
"reset-button": "CSS zurücksetzen für",
"show-all-button": "Alle Variablen anzeigen",
"change-fonts-button": "Schriftart ändern",
"save-button": "Speichern",
"cancel-button": "Abbrechen",
"saved-toast": "{theme} wurde erfolgreich aktualisiert",
"copied-toast": "Designdaten für {theme} wurden in die Zwischenablage kopiert.",
"reset-toast": "Benutzerdefinierte Farben für {theme} wurden entfernt"
},
"config-editor":
{
"save-location-label": "Speicherort",
"location-local-label": "Lokal anwenden",
"location-disk-label": "Änderungen in die Konfigurationsdatei schreiben",
"save-button": "Änderungen speichern",
"preview-button": "Vorschau der Änderungen",
"valid-label": "Syntax ist gültig",
"status-success-msg": "Aufgabe abgeschlossen",
"status-fail-msg": "Aufgabe fehlgeschlagen",
"success-msg-disk": "Konfigurationsdatei wurde erfolgreich auf die Festplatte geschrieben",
"success-msg-local": "Lokale Änderungen wurden erfolgreich gespeichert",
"success-note-l1": "Die Applikation sollte automatisch re-kompiliert werden.",
"success-note-l2": "Dies kann bis zu einer Minute dauern.",
"success-note-l3": "Sie müssen die Seite aktualisieren damit die Änderungen wirksam werden.",
"error-msg-save-mode": "Bitte wählen Sie einen Speichermodus: Lokal oder Datei",
"error-msg-cannot-save": "Beim Speichern der Konfiguration ist ein Fehler aufgetreten",
"error-msg-bad-json": "Fehler in JSON-Daten, möglicherweise fehlerhafter Syntax",
"warning-msg-validation": "Validierungswarnung",
"not-admin-note": "Änderungen können nicht auf die Festplatte gespeichert werden, da Sie nicht als Administrator angemeldet sind"
},
"app-rebuild":
{
"title": "Applikation re-kompilieren",
"rebuild-note-l1": "Damit die in die Datei conf.yml geschriebenen Änderungen wirksam werden ist ein re-kompilieren erforderlich.",
"rebuild-note-l2": "Dies sollte automatisch passieren, aber falls nicht können Sie es hier manuell starten.",
"rebuild-note-l3": "Dies ist bei lokal gespeicherten Änderungen nicht erforderlich.",
"rebuild-button": "Start Kompilierung",
"rebuilding-status-1": "Baue...",
"rebuilding-status-2": "Das kann ein paar Minuten dauern",
"error-permission": "Sie sind nicht berechtigt diese Aktion auszulösen",
"success-msg": "Kompilierung erfolgreich abgeschlossen",
"fail-msg": "Kompilierung fehlgeschlagen",
"reload-note": "Ein neu Laden der Seite ist erforderlich, damit die Änderungen wirksam werden.",
"reload-button": "Seite neu laden"
},
"cloud-sync":
{
"title": "Cloud Backup & Wiederherstellung",
"intro-l1": "Cloud-Backup und Wiederherstellung ist eine optionale Funktion mit der Sie Ihre Konfiguration in das Internet hochladen und dann auf einem anderen Gerät oder einer anderen Dashy-Instanz wiederherstellen können.",
"intro-l2": "Alle Daten sind vollständig Ende-zu-Ende mit AES verschlüsselt. Ihr Passwort wird als Schlüssel verwendet.",
"intro-l3": "Weitere Informationen finden Sie im",
"backup-title-setup": "Backup erstellen",
"backup-title-update": "Backup aktualisieren",
"password-label-setup": "Passwort auswählen",
"password-label-update": "Passwort eingeben",
"backup-button-setup": "Backup",
"backup-button-update": "Backup aktualisieren",
"backup-id-label": "Ihre Backup ID",
"backup-id-note": "Diese wird zusammen mit dem Passwort benötigt um Ihr Backup wiederherzustellen. Bewahren Sie sie zusammen mit Ihrem Passwort an einem sicheren Ort auf.",
"restore-title": "Backup wiederherstellen",
"restore-id-label": "ID wiederherstellen",
"restore-password-label": "Passwort",
"restore-button": "Wiederherstellen",
"backup-error-unknown": "Anfrage kann nicht verarbeitet werden",
"backup-error-password": "Falsches Passwort. Bitte geben Sie Ihr aktuelles Passwort ein.",
"backup-success-msg": "Erfolgreich abgeschlossen",
"restore-success-msg": "Konfiguration erfolgreich wiederhergestellt"
},
"menu":
{
"open-section-title": "Öffnen in",
"sametab": "Aktueller Tab",
"newtab": "Neuer Tab",
"modal": "Popup Modal",
"workspace": "Arbeitsflächenansicht",
"options-section-title": "Optionen",
"edit-item": "Bearbeiten",
"move-item": "Kopieren oder Verschieben",
"remove-item": "Entfernen"
},
"context-menus":
{
"item":
{
"open-section-title": "Öffnen in",
"sametab": "Aktueller Tab",
"newtab": "Neuer Tab",
"modal": "Popup Modal",
"workspace": "Arbeitsflächenansicht",
"clipboard": "In Zwischenablage kopieren",
"options-section-title": "Optionen",
"edit-item": "Bearbeiten",
"move-item": "Kopieren oder Verschieben",
"remove-item": "Entfernen",
"copied-toast": "URL wurde in die Zwischenablage kopiert"
},
"section":
{
"open-section": "Sektion öffnen",
"edit-section": "Bearbeiten",
"expand-collapse": "Aus- / Einklappen",
"move-section": "Verschieben nach",
"remove-section": "Entfernen"
}
},
"interactive-editor":
{
"menu":
{
"start-editing-tooltip": "Interaktiven Editor starten",
"edit-site-data-subheading": "Seiteninformationen bearbeiten",
"edit-page-info-btn": "Seiteninformationen bearbeiten",
"edit-page-info-tooltip": "Applikationstitel, Beschreibung, Nav. links, Fußzeile, etc.",
"edit-app-config-btn": "Applikationskonfiguration bearbeiten",
"edit-app-config-tooltip": "Alle anderen Konfigurationsoptionen",
"edit-pages-btn": "Seiten bearbeiten",
"edit-pages-tooltip": "Hinzufügen oder entfernen von zusätzlichen Ansichten",
"config-save-methods-subheading": "Speicheroptionen der Konfiguration",
"save-locally-btn": "Lokal speichern",
"save-locally-tooltip": "Konfiguration lokal im Browser speichern. Dies hat keinen Einfluss auf die Konfigurationsdatei, aber Änderungen werden nur in diesem Browser gespeichert",
"save-disk-btn": "Auf Festplatte speichern",
"save-disk-tooltip": "Konfigurationsdatei conf.yml speichern. Dies erzeugt ein Backup und überschreibt dann die existierende Konfigurationsdatei",
"export-config-btn": "Konfiguration exportieren",
"export-config-tooltip": "Konfiguration anzeigen und exportieren, entweder in eine Datei oder in die Zwischenablage",
"cloud-backup-btn": "Cloud-Backup starten",
"cloud-backup-tooltip": "Speichert ein verrschlüsseltes Backup in die Cloud",
"edit-raw-config-btn": "Konfiguration als Rohdaten bearbeiten",
"edit-raw-config-tooltip": "Anzeigen und bearbeiten der Konfiguration als Rohdaten im JSON-Editor",
"cancel-changes-btn": "Verwerfen",
"cancel-changes-tooltip": "Modifikationen zurücksetzen und Bearbeitungsmodus schließen. Dies hat keinen Einfluss auf die Konfigurationsdatei",
"edit-mode-name": "Bearbeitung",
"edit-mode-subtitle": "Sie sind im Bearbeitungsmodus",
"edit-mode-description": "Das bedeutet, dass Änderungen an der Konfigurationsdatei vorgenommen werden können. Änderungen können vor dem Speichern betrachtet werden.",
"save-stage-btn": "Speichern",
"cancel-stage-btn": "Abbrechen",
"save-locally-warning": "Wenn Sie fortfahren werden die Änderungen nur in Ihrem Browser gespeichert. Um die Konfiguration auf anderen Geräten zu nutzen sollten Sie sie exportieren. Möchten Sie fortfahren?"
},
"edit-item":
{
"missing-title-err": "Ein Titel is zwingend notwendig"
},
"edit-section":
{
"edit-section-title": "Sektion bearbeiten",
"add-section-title": "Neue Sektion hinzufügen",
"edit-tooltip": "Klicken zum Bearbeiten oder Rechtsklick für weitere Optionen",
"remove-confirm": "Sind Sie sicher, dass sie diese Sektion entfernen möchten? Diese Aktion kann nicht rückgänging gemacht werden."
},
"edit-app-config":
{
"warning-msg-title": "Ab hier ist Vorsicht geboten",
"warning-msg-l1": "Die folgenden Optionen sind für fortgeschrittene Konfigurationen.",
"warning-msg-l2": "Sollten Felder unklar sein, konsultieren Sie die",
"warning-msg-docs": "Dokumentation",
"warning-msg-l3": "um unbeabsichtigte Folgen zu vermeiden."
},
"export":
{
"export-title": "Konfiguration exportieren",
"copy-clipboard-btn": "In Zwischenablage kopieren",
"copy-clipboard-tooltip": "Applikationskonfiguration als YAML in Zwischenablage kopieren",
"download-file-btn": "Datei herunterladen",
"download-file-tooltip": "Applikationskonfiguration auf Ihr Gerät herunterladen",
"view-title": "Konfiguration anzeigen"
}
},
"widgets":
{
"general":
{
"loading": "Lade...",
"show-more": "Details",
"show-less": "Weniger anzeigen",
"open-link": "Weiterlesen"
},
"pi-hole":
{
"status-heading": "Status"
},
"stat-ping":
{
"up": "Online",
"down": "Offline"
},
"net-data":
{
"cpu-chart-title": "CPU Historie",
"mem-chart-title": "Speichernutzung",
"mem-breakdown-title": "Speichernutzung",
"load-chart-title": "Systemlast"
},
"glances":
{
"disk-space-free": "Frei",
"disk-space-used": "Genutzt",
"disk-mount-point": "Mount Punkt",
"disk-file-system": "Dateisystem,",
"disk-io-read": "Lesen",
"disk-io-write": "Schreiben",
"system-load-desc": "Prozesse in Warteschlange. (Durchschnitt aller Kerne)"
},
"system-info":
{
"uptime": "Uptime"
},
"flight-data":
{
"arrivals": "Ankünfte",
"departures": "Abflüge"
},
"tfl-status":
{
"good-service-all": "Guter Service auf allen Leitungen",
"good-service-rest": "Guter Service auf allen anderen Leitungen"
},
"synology-download":
{
"download": "Download",
"upload": "Upload",
"downloaded": "Heruntergeladen",
"uploaded": "Hochgeladen",
"remaining": "Verbleibend",
"up": "Hoch",
"down": "Runter"
},
"gluetun-status":
{
"vpn-ip": "VPN IP",
"country": "Land",
"region": "Bundesland",
"city": "Stadt",
"post-code": "Postleitzahl",
"location": "Standort",
"timezone": "Zeitzone",
"organization": "Organisation"
},
"nextcloud":
{
"active": "Aktiv",
"and": "und",
"applications": "Anwendungen",
"available": "Verfügbar",
"away": "Abwesend",
"cache-full": "CACHE VOLL",
"chat-room": "Chatraum",
"delete-all": "Alle löschen",
"delete-notification": "Benachrichtigung löschen",
"disabled": "Deaktivert",
"disk-quota": "Disk Quota",
"disk-space": "Disk Speicherplatz",
"dnd": "Nicht stören",
"email": "EMail",
"enabled": "Aktiviert",
"federated-shares-ucfirst": "Verbundsfreigaben",
"federated-shares": "Verbundsfreigaben",
"files": "Dateien{plural}",
"free": "Frei",
"groups": "Gruppen",
"hit-rate": "Trefferrate",
"hits": "Treffer",
"home": "Zuhause",
"in": "in",
"keys": "Schlüssel",
"last-24-hours": "Letzte 24 Stunden",
"last-5-minutes": "in den letzten 5 Minuten",
"last-hour": "in der letzten Stunde",
"last-login": "Letzte Anmeldung",
"last-restart": "Letzter Neustart",
"load-averages": "Systemlast aller CPU-Kerne",
"local-shares": "Lokale Freigaben",
"local": "Lokal",
"max-keys": "Maximale Schlüssel",
"memory-used": "Speuchernutzung",
"memory-utilisation": "Speuchernutzung",
"memory": "Speicher",
"misses": "Fehlschläge",
"no-notifications": "Keine Benachrichtigungen",
"no-pending-updates": "Keine ausstehenden Aktualisierungen",
"nothing-to-show": "Momentan gibt es hier nichts zu zeigen",
"of-which": "welche",
"of": "von",
"offline": "Offline",
"online": "Online",
"other": "Andere",
"overall": "Insgesamt",
"private-link": "privater Link",
"public-link": "öffentlicher Link",
"quota-enabled": "Disk Quota ist {nicht}aktiviert für diesen Benutzer",
"received": "Empfangen",
"scripts": "Skripte",
"sent": "Gesendet",
"started": "Gestartet",
"storages-by-type": "Speicher nach Typ",
"storages": "Speicher{plural}",
"strings-use": "Strings benutzen",
"tasks": "Aufgaben",
"total-files": "Dateien gesamt",
"total-users": "Benutzer gesamt",
"total": "Insgesamt",
"until": "Bis",
"updates-available-for": "Aktualisierungen sind verfügbar für",
"updates-available": "Aktualisierungen{plural} verfügbar",
"used": "benutzt",
"user": "Benutzer",
"using": "nutzt",
"version": "Version",
"wasted": "verschwendet"
}
"home": {
"no-results": "keine Suchergebnisse",
"no-data": "keine Daten konfiguriert",
"no-items-section": "Noch keine Elemente zum Anzeigen"
},
"search": {
"search-label": "Suche",
"search-placeholder": "Tippe um zu filtern",
"clear-search-tooltip": "Suchfeld leeren",
"enter-to-search-web": "Drücke Enter um das Internet zu durchsuchen"
},
"splash-screen": {
"loading": "Lädt"
},
"login": {
"title": "Dashy",
"guest-label": "Gastzugriff",
"username-label": "Benutzername",
"password-label": "Passwort",
"login-button": "Anmelden",
"remember-me-label": "Angemeldet bleiben für",
"remember-me-never": "Niemals",
"remember-me-hour": "4 Stunden",
"remember-me-day": "1 Tag",
"remember-me-week": "1 Woche",
"remember-me-long-time": "Eine lange Zeit",
"error-missing-username": "Benutzername fehlt",
"error-missing-password": "Passwort fehlt",
"error-incorrect-username": "Benutzer nicht gefunden",
"error-incorrect-password": "Falsches Passwort",
"success-message": "Anmeldung läuft...",
"logout-message": "Abgemeldet",
"already-logged-in-title": "Bereits angemeldet",
"already-logged-in-text": "Angemeldet als",
"proceed-to-dashboard": "Zum Dashboard fortfahren",
"log-out-button": "Abmelden",
"proceed-guest-button": "Als Gast fortfahren",
"guest-intro-1": "Diese Instanz hat Gastzugriffe aktiviert",
"guest-intro-2": "Gäste haben lesenden Zugriff auf Dashboards, können also keine Veränderungen auf die Festplatte schreiben.",
"error": "Error",
"error-no-user-configured": "Authentifizierung ist nicht aktiviert, oder es sind keine Benutzer konfiguriert",
"error-go-home-button": "Geh nach Hause",
"logged-in-guest": "Als Gast eingeloggt, Umleitung...",
"error-guest-access": "Gastzugriff nicht erlaubt"
},
"app-info": {
"title": "App Info",
"error-log": "Fehlerprotokoll",
"no-errors": "Keine kürzlichen Fehler erkannt",
"help-support": "Hilfe & Support",
"help-support-description": "Um beim Betrieb oder der Konfiguration von Dashy Hilfe zu bekommen, siehe die",
"help-support-discussions": "Diskussionen",
"support-dashy": "Dashy unterstützen",
"support-dashy-description": "Für Wege, sich zu beteiligen, besuchen Sie die",
"support-dashy-link": "Contributions Seite",
"report-bug": "Melde einen Fehler",
"report-bug-description": "Wenn Sie glauben, einen Fehler gefunden zu haben, dann bitte",
"report-bug-link": "öffne ein Issue",
"more-info": "Mehr Informationen",
"source": "Quellcode",
"documentation": "Dokumentation",
"privacy-and-security": "Datenschutz & Sicherheit",
"privacy-and-security-l1": "Für eine Übersicht, wie Dashy mit Ihren Daten umgeht, siehe die",
"privacy-and-security-privacy-policy": "Datenschutzerklärung",
"privacy-and-security-advice": "Für Anleitung, um Ihr Dashboard zu sichern, konsultieren Sie die",
"privacy-and-security-advice-link": "Management Dokumentation",
"privacy-and-security-security-issue": "Wenn Sie glauben, eine mögliche Sicherheitslücke gefunden zu haben, melde sie wie beschrieben in unserer",
"privacy-and-security-security-policy": "Security Policy",
"license": "Lizenz",
"license-under": "Lizensiert unter",
"licence-third-party": "Für Lizenzen von Drittanbietermodulen, siehe",
"licence-third-party-link": "Legal",
"list-contributors": "Für eine vollstandige Liste aller Beteiligten und Dank, siehe",
"list-contributors-link": "Credits",
"version": "Version"
},
"config": {
"main-tab": "Hauptmenü",
"view-config-tab": "Konfiguration",
"edit-config-tab": "Konfiguration bearbeiten",
"custom-css-tab": "eigene CSS",
"heading": "Konfigurationseinstellungen",
"download-config-button": "Konfigurationsdownload",
"edit-config-button": "Konfiguration bearbeiten",
"edit-css-button": "CSS bearbeiten",
"cloud-sync-button": "Cloud-Synchronisation aktivieren",
"edit-cloud-sync-button": "Cloud-Synchronisation bearbeiten",
"rebuild-app-button": "Anwendung neu kompilieren",
"change-language-button": "App-Sprache ändern",
"reset-settings-button": "lokale Einstellungen zurücksetzen",
"disabled-note": "Einige Konfigurationsoptionen wurden vom Administrator deaktivert",
"small-screen-note": "Sie benutzen einen sehr kleinen Bildschirm. Einige Seiten in diesem Menü sind dafür möglicherweise nicht ideal.",
"app-info-button": "App Informationen",
"backup-note": "Es wird empfohlen ein Backup der Konfiguration zu erstellen bevor Änderungen durchgeführt werden.",
"reset-config-msg-l1": "Dadurch werden alle Benutzereinstellungen aus dem lokalen Speicher entfernt, dies hat jedoch keine Auswirkungen auf Ihre Datei 'conf.yml'.",
"reset-config-msg-l2": "Sie sollten zuerst alle Änderungen, die Sie lokal vorgenommen haben, sichern, wenn Sie sie in Zukunft wiederverwenden möchten.",
"reset-config-msg-l3": "Sind Sie sicher, dass Sie fortfahren möchten?",
"data-cleared-msg": "Daten erfolgreich gelöscht",
"actions-label": "Aktionen",
"copy-config-label": "Konfiguration kopieren",
"data-copied-msg": "Konfiguration wurde in die Zwischenablage kopiert",
"reset-config-label": "Konfiguration zurücksetzen",
"css-save-btn": "Änderungen speichern",
"css-note-label": "Bemerkung",
"css-note-l1": "Sie müssen die Seite aktualisieren, damit Ihre Änderungen wirksam werden.",
"css-note-l2": "Stilüberschreibungen werden nur lokal gespeichert, daher wird empfohlen vorher eine Kopie Ihres CSS zu erstellen.",
"css-note-l3": "Um alle benutzerdefinierten Stile zu entfernen löschen Sie den Inhalt und klicken Sie auf Änderungen speichern.",
"custom-css": {
"title": "Eigenes CSS",
"base-theme": "Basisdesign"
}
},
"alternate-views": {
"alternate-view-heading": "Ansicht wechseln",
"default": "Standard",
"workspace": "Arbeitsplatz",
"minimal": "Minimal"
},
"settings": {
"theme-label": "Design",
"layout-label": "Layout",
"layout-auto": "Auto",
"layout-horizontal": "Horizontal",
"layout-vertical": "Vertikal",
"item-size-label": "Itemgröße",
"item-size-small": "klein",
"item-size-medium": "mittel",
"item-size-large": "groß",
"config-launcher-label": "Konfiguration",
"config-launcher-tooltip": "Konfiguration aktualisieren",
"sign-out-tooltip": "Abmelden",
"sign-in-tooltip": "Anmelden",
"sign-in-welcome": "Hallo {username}!",
"hide": "Verstecke",
"open": "Öffne"
},
"updates": {
"app-version-note": "Dashy Version",
"up-to-date": "Aktuell",
"out-of-date": "Update verfügbar",
"unsupported-version-l1": "Sie verwenden eine nicht unterstützte Version von Dashy",
"unsupported-version-l2": "Für die beste Erfahrung und aktuelle Sicherheitspatches aktualisieren Sie bitte auf"
},
"language-switcher": {
"title": "Applikationssprache ändern",
"dropdown-label": "Sprache auswählen",
"save-button": "Speichern",
"success-msg": "Sprache geändert auf"
},
"theme-maker": {
"title": "Design Konfigurator",
"export-button": "Benutzerdefinierte Variablen exportieren",
"reset-button": "CSS zurücksetzen für",
"show-all-button": "Alle Variablen anzeigen",
"change-fonts-button": "Schriftart ändern",
"save-button": "Speichern",
"cancel-button": "Abbrechen",
"saved-toast": "{theme} wurde erfolgreich aktualisiert",
"copied-toast": "Designdaten für {theme} wurden in die Zwischenablage kopiert.",
"reset-toast": "Benutzerdefinierte Farben für {theme} wurden entfernt"
},
"config-editor": {
"save-location-label": "Speicherort",
"location-local-label": "Lokal anwenden",
"location-disk-label": "Änderungen in die Konfigurationsdatei schreiben",
"save-button": "Änderungen speichern",
"preview-button": "Vorschau der Änderungen",
"valid-label": "Syntax ist gültig",
"status-success-msg": "Aufgabe abgeschlossen",
"status-fail-msg": "Aufgabe fehlgeschlagen",
"success-msg-disk": "Konfigurationsdatei wurde erfolgreich auf die Festplatte geschrieben",
"success-msg-local": "Lokale Änderungen wurden erfolgreich gespeichert",
"success-note-l1": "Die Applikation sollte automatisch re-kompiliert werden.",
"success-note-l2": "Dies kann bis zu einer Minute dauern.",
"success-note-l3": "Sie müssen die Seite aktualisieren damit die Änderungen wirksam werden.",
"error-msg-save-mode": "Bitte wählen Sie einen Speichermodus: Lokal oder Datei",
"error-msg-cannot-save": "Beim Speichern der Konfiguration ist ein Fehler aufgetreten",
"error-msg-bad-json": "Fehler in JSON-Daten, möglicherweise fehlerhafter Syntax",
"warning-msg-validation": "Validierungswarnung",
"not-admin-note": "Änderungen können nicht auf die Festplatte gespeichert werden, da Sie nicht als Administrator angemeldet sind"
},
"app-rebuild": {
"title": "Applikation neu kompilieren",
"rebuild-note-l1": "Damit die in die Datei conf.yml geschriebenen Änderungen wirksam werden ist ein Neukompilieren erforderlich.",
"rebuild-note-l2": "Dies sollte automatisch passieren, aber falls nicht können Sie es hier manuell starten.",
"rebuild-note-l3": "Dies ist bei lokal gespeicherten Änderungen nicht erforderlich.",
"rebuild-button": "Starte Kompilierung",
"rebuilding-status-1": "Baue...",
"rebuilding-status-2": "Das kann ein paar Minuten dauern",
"error-permission": "Sie sind nicht berechtigt diese Aktion auszulösen",
"success-msg": "Kompilierung erfolgreich abgeschlossen",
"fail-msg": "Kompilierung fehlgeschlagen",
"reload-note": "Ein neu Laden der Seite ist erforderlich, damit die Änderungen wirksam werden.",
"reload-button": "Seite neu laden"
},
"cloud-sync": {
"title": "Cloud Backup & Wiederherstellung",
"intro-l1": "Cloud-Backup und Wiederherstellung ist eine optionale Funktion mit der Sie Ihre Konfiguration in das Internet hochladen und dann auf einem anderen Gerät oder einer anderen Dashy-Instanz wiederherstellen können.",
"intro-l2": "Alle Daten sind vollständig Ende-zu-Ende mit AES verschlüsselt. Ihr Passwort wird als Schlüssel verwendet.",
"intro-l3": "Weitere Informationen finden Sie in der",
"intro-docs": "Dokumentation",
"backup-title-setup": "Backup erstellen",
"backup-title-update": "Backup aktualisieren",
"password-label-setup": "Passwort auswählen",
"password-label-update": "Passwort eingeben",
"backup-button-setup": "Backup",
"backup-button-update": "Backup aktualisieren",
"backup-id-label": "Ihre Backup ID",
"backup-id-note": "Diese wird zusammen mit dem Passwort benötigt um Ihr Backup wiederherzustellen. Bewahren Sie sie zusammen mit Ihrem Passwort an einem sicheren Ort auf.",
"restore-title": "Backup wiederherstellen",
"restore-id-label": "ID wiederherstellen",
"restore-password-label": "Passwort",
"restore-button": "Wiederherstellen",
"backup-missing-password": "Passwort fehlt",
"backup-error-unknown": "Anfrage kann nicht verarbeitet werden",
"backup-error-password": "Falsches Passwort. Bitte geben Sie Ihr aktuelles Passwort ein.",
"backup-success-msg": "Erfolgreich abgeschlossen",
"restore-success-msg": "Konfiguration erfolgreich wiederhergestellt"
},
"menu": {
"open-section-title": "Öffne in",
"sametab": "Aktueller Tab",
"newtab": "Neuer Tab",
"modal": "Popup Modal",
"workspace": "Arbeitsflächenansicht",
"options-section-title": "Optionen",
"edit-item": "Bearbeiten",
"move-item": "Kopieren oder Verschieben",
"remove-item": "Entfernen"
},
"context-menus": {
"item": {
"open-section-title": "Öffnen in",
"sametab": "Aktueller Tab",
"newtab": "Neuer Tab",
"modal": "Popup Modal",
"workspace": "Arbeitsflächenansicht",
"clipboard": "In Zwischenablage kopieren",
"options-section-title": "Optionen",
"edit-item": "Bearbeiten",
"move-item": "Kopieren oder Verschieben",
"remove-item": "Entfernen",
"copied-toast": "URL wurde in die Zwischenablage kopiert"
},
"section": {
"open-section": "Sektion öffnen",
"edit-section": "Bearbeiten",
"expand-collapse": "Aus- / Einklappen",
"move-section": "Verschieben nach",
"remove-section": "Entfernen"
}
},
"footer": {
"dev-by": "Entwickelt von",
"licensed-under": "Lizensiert unter",
"get-the": "Hole dir den",
"source-code": "Quellcode"
},
"interactive-editor": {
"menu": {
"start-editing-tooltip": "Interaktiven Editor starten",
"edit-site-data-subheading": "Seiteninformationen bearbeiten",
"edit-page-info-btn": "Seiteninformationen bearbeiten",
"edit-page-info-tooltip": "Applikationstitel, Beschreibung, Nav. links, Fußzeile, etc.",
"edit-app-config-btn": "Applikationskonfiguration bearbeiten",
"edit-app-config-tooltip": "Alle anderen Konfigurationsoptionen",
"edit-pages-btn": "Seiten bearbeiten",
"edit-pages-tooltip": "Hinzufügen oder entfernen von zusätzlichen Ansichten",
"config-save-methods-subheading": "Speicheroptionen der Konfiguration",
"save-locally-btn": "Lokal speichern",
"save-locally-tooltip": "Konfiguration lokal im Browser speichern. Dies hat keinen Einfluss auf die Konfigurationsdatei, aber Änderungen werden nur in diesem Browser gespeichert",
"save-disk-btn": "Auf Festplatte speichern",
"save-disk-tooltip": "Konfigurationsdatei conf.yml speichern. Dies erzeugt ein Backup und überschreibt dann die existierende Konfigurationsdatei",
"export-config-btn": "Konfiguration exportieren",
"export-config-tooltip": "Konfiguration anzeigen und exportieren, entweder in eine Datei oder in die Zwischenablage",
"cloud-backup-btn": "Cloud-Backup starten",
"cloud-backup-tooltip": "Speichert ein verrschlüsseltes Backup in die Cloud",
"edit-raw-config-btn": "Konfiguration als Rohdaten bearbeiten",
"edit-raw-config-tooltip": "Anzeigen und bearbeiten der Konfiguration als Rohdaten im JSON-Editor",
"cancel-changes-btn": "Verwerfen",
"cancel-changes-tooltip": "Modifikationen zurücksetzen und Bearbeitungsmodus schließen. Dies hat keinen Einfluss auf die Konfigurationsdatei",
"edit-mode-name": "Bearbeitung",
"edit-mode-subtitle": "Sie sind im Bearbeitungsmodus",
"edit-mode-description": "Das bedeutet, dass Änderungen an der Konfigurationsdatei vorgenommen werden können. Änderungen können vor dem Speichern betrachtet werden.",
"save-stage-btn": "Speichern",
"cancel-stage-btn": "Abbrechen",
"save-locally-warning": "Wenn Sie fortfahren werden die Änderungen nur in Ihrem Browser gespeichert. Um die Konfiguration auf anderen Geräten zu nutzen sollten Sie sie exportieren. Möchten Sie fortfahren?"
},
"edit-item": {
"missing-title-err": "Ein Titel is zwingend notwendig"
},
"edit-section": {
"edit-section-title": "Sektion bearbeiten",
"add-section-title": "Neue Sektion hinzufügen",
"edit-tooltip": "Klicken zum Bearbeiten oder Rechtsklick für weitere Optionen",
"remove-confirm": "Sind Sie sicher, dass sie diese Sektion entfernen möchten? Diese Aktion kann nicht rückgänging gemacht werden."
},
"edit-app-config": {
"warning-msg-title": "Ab hier ist Vorsicht geboten",
"warning-msg-l1": "Die folgenden Optionen sind für fortgeschrittene Konfigurationen.",
"warning-msg-l2": "Sollten Felder unklar sein, konsultieren Sie die",
"warning-msg-docs": "Dokumentation",
"warning-msg-l3": "um unbeabsichtigte Folgen zu vermeiden."
},
"export": {
"export-title": "Konfiguration exportieren",
"copy-clipboard-btn": "In Zwischenablage kopieren",
"copy-clipboard-tooltip": "Applikationskonfiguration als YAML in Zwischenablage kopieren",
"download-file-btn": "Datei herunterladen",
"download-file-tooltip": "Applikationskonfiguration auf Ihr Gerät herunterladen",
"view-title": "Konfiguration anzeigen"
}
},
"widgets": {
"general": {
"loading": "Lade...",
"show-more": "Details",
"cpu-details": "Details CPU",
"mem-details": "Details Arbeitsspeicher",
"show-less": "Weniger anzeigen",
"open-link": "Weiterlesen"
},
"pi-hole": {
"status-heading": "Status"
},
"stat-ping": {
"up": "Online",
"down": "Offline"
},
"net-data": {
"cpu-chart-title": "CPU Historie",
"mem-chart-title": "Speichernutzung",
"mem-breakdown-title": "Speichernutzung",
"load-chart-title": "Systemlast"
},
"glances": {
"disk-space-free": "Frei",
"disk-space-used": "Genutzt",
"disk-mount-point": "Mountpunkt",
"disk-file-system": "Dateisystem,",
"disk-io-read": "Lesen",
"disk-io-write": "Schreiben",
"system-load-desc": "Prozesse in der Warteschlange, Durchschnitt aller Kerne"
},
"system-info": {
"uptime": "Uptime"
},
"flight-data": {
"arrivals": "Ankünfte",
"departures": "Abflüge"
},
"tfl-status": {
"good-service-all": "Guter Service auf allen Leitungen",
"good-service-rest": "Guter Service auf allen anderen Leitungen"
},
"synology-download": {
"download": "Download",
"upload": "Upload",
"downloaded": "Heruntergeladen",
"uploaded": "Hochgeladen",
"remaining": "Verbleibend",
"up": "Hoch",
"down": "Runter"
},
"gluetun-status": {
"vpn-ip": "VPN IP",
"country": "Land",
"region": "Bundesland",
"city": "Stadt",
"post-code": "Postleitzahl",
"location": "Standort",
"timezone": "Zeitzone",
"organization": "Organisation"
},
"nextcloud": {
"active": "Aktiv",
"and": "und",
"applications": "Anwendungen",
"available": "Verfügbar",
"away": "Abwesend",
"cache-full": "CACHE VOLL",
"chat-room": "Chatraum",
"delete-all": "Alle löschen",
"delete-notification": "Benachrichtigung löschen",
"disabled": "deaktivert",
"disk-quota": "Disk Quota",
"disk-space": "Disk Speicherplatz",
"dnd": "Nicht stören",
"email": "E-Mail",
"enabled": "aktiviert",
"federated-shares-ucfirst": "Föderierte Freigaben",
"federated-shares": "föderierte Freigaben",
"files": "Dateien",
"free": "frei",
"groups": "Gruppen",
"hit-rate": "Trefferrate",
"hits": "Treffer",
"home": "Zuhause",
"in": "in",
"keys": "Schlüssel",
"last-24-hours": "letzte 24 Stunden",
"last-5-minutes": "in den letzten 5 Minuten",
"last-hour": "in der letzten Stunde",
"last-login": "Letzte Anmeldung",
"last-restart": "Letzter Neustart",
"load-averages": "Systemlast aller CPU-Kerne",
"local-shares": "Lokale Freigaben",
"local": "lokal",
"max-keys": "Maximale Schlüssel",
"memory-used": "Speichernutzung",
"memory-utilisation": "Speichernutzung",
"memory": "Speicher",
"misses": "Fehlschläge",
"no-notifications": "Keine Benachrichtigungen",
"no-pending-updates": "keine ausstehenden Aktualisierungen",
"nothing-to-show": "Momentan gibt es hier nichts zu zeigen",
"of-which": "welche",
"of": "von",
"offline": "Offline",
"online": "Online",
"other": "andere",
"overall": "Insgesamt",
"private-link": "privater Link",
"public-link": "öffentlicher Link",
"quota-enabled": "Disk Quota ist {nicht}aktiviert für diesen Benutzer",
"received": "empfangen",
"scripts": "Skripte",
"sent": "gesendet",
"started": "gestartet",
"storages-by-type": "Speicher nach Typ",
"storages": "Speicher",
"strings-use": "Strings benutzen",
"tasks": "Aufgaben",
"total-files": "Dateien gesamt",
"total-users": "Benutzer gesamt",
"total": "insgesamt",
"until": "Bis",
"updates-available-for": "Aktualisierungen sind verfügbar für",
"updates-available": "Aktualisierungen verfügbar",
"used": "benutzt",
"user": "Benutzer",
"using": "nutzt",
"version": "Version",
"wasted": "verschwendet"
}
}
}

View File

@@ -63,8 +63,8 @@
"privacy-and-security": "Privacy & Security",
"privacy-and-security-l1": "For a break-down of how your data is managed by Dashy, see the",
"privacy-and-security-privacy-policy": "Privacy Policy",
"app-info.privacy-and-security-advice": "For advise in securing your dashboard, you can reference the",
"app-info.privacy-and-security-advice-link": "Management Docs",
"privacy-and-security-advice": "For advise in securing your dashboard, you can reference the",
"privacy-and-security-advice-link": "Management Docs",
"privacy-and-security-security-issue": "If you've found a potential security issue, report it following our",
"privacy-and-security-security-policy": "Security Policy",
"license": "License",
@@ -171,9 +171,9 @@
"status-fail-msg": "Task Failed",
"success-msg-disk": "Config file written to disk successfully",
"success-msg-local": "Local changes saved successfully",
"success-note-l1": "The app should rebuild automatically.",
"success-note-l2": "This may take up to a minute.",
"success-note-l3": "You will need to refresh the page for changes to take effect.",
"success-note-l1": "You will need to refresh the page for changes to take effect.",
"success-note-l2": "",
"success-note-l3": "",
"error-msg-save-mode": "Please select a Save Mode: Local or File",
"error-msg-cannot-save": "An error occurred saving config",
"error-msg-bad-json": "Error in JSON, possibly malformed",
@@ -182,9 +182,9 @@
},
"app-rebuild": {
"title": "Rebuild Application",
"rebuild-note-l1": "A rebuild is required for changes written to the conf.yml file to take effect.",
"rebuild-note-l2": "This should happen automatically, but if it hasn't, you can manually trigger it here.",
"rebuild-note-l3": "This is not required for modifications stored locally.",
"rebuild-note-l1": "A rebuild is no longer required for changes to take effect.",
"rebuild-note-l2": "Some changes (entry-point, and auth settings) are read at build-time. So to apply these, you should trigger a rebuild here.",
"rebuild-note-l3": "Note that this is only available on Node and Docker installations, not via statically deployed instances.",
"rebuild-button": "Start Build",
"rebuilding-status-1": "Building...",
"rebuilding-status-2": "This may take a few minutes",
@@ -312,10 +312,20 @@
"view-title": "View Config"
}
},
"critical-error": {
"title": "Configuration Load Error",
"subtitle": "Dashy has failed to load correctly due to a configuration error.",
"sub-ensure-that": "Ensure that",
"sub-error-details": "Error Details",
"sub-next-steps": "Next Steps",
"ignore-button": "Ignore Critical Errors"
},
"widgets": {
"general": {
"loading": "Loading...",
"show-more": "Expand Details",
"cpu-details": "CPU Details",
"mem-details": "Memory Details",
"show-less": "Show Less",
"open-link": "Continue Reading"
},

View File

@@ -63,8 +63,8 @@
"privacy-and-security": "Privacidade e seguridade",
"privacy-and-security-l1": "Para obter detalles sobre como os teus datos son xestionados por Dashy, consulta a",
"privacy-and-security-privacy-policy": "Política de privacidade",
"app-info.privacy-and-security-advice": "Para consellos sobre como asegurar o teu panel de control, consulta os",
"app-info.privacy-and-security-advice-link": "Documentos de xestión",
"privacy-and-security-advice": "Para consellos sobre como asegurar o teu panel de control, consulta os",
"privacy-and-security-advice-link": "Documentos de xestión",
"privacy-and-security-security-issue": "Se atopaches un problema de seguridade potencial, informa seguindo a nosa",
"privacy-and-security-security-policy": "Política de seguridade",
"license": "Licenza",

View File

@@ -63,8 +63,8 @@
"privacy-and-security": "プライバシーとセキュリティー",
"privacy-and-security-l1": "Daisyがどのようにあなたのデータを管理するかについての概要は、こちらをご覧ください: ",
"privacy-and-security-privacy-policy": "プライバシーポリシー",
"app-info.privacy-and-security-advice": "ダッシュボードを安全に保つためのアドバイスは、こちらを参照してください: ",
"app-info.privacy-and-security-advice-link": "管理ドキュメント",
"privacy-and-security-advice": "ダッシュボードを安全に保つためのアドバイスは、こちらを参照してください: ",
"privacy-and-security-advice-link": "管理ドキュメント",
"privacy-and-security-security-issue": "セキュリティー問題の可能性のある事象を見つけた場合、セキュリティーポリシーに従ってください: ",
"privacy-and-security-security-policy": "セキュリティーポリシー",
"license": "ライセンス",

View File

@@ -10,8 +10,12 @@
"clear-search-tooltip": "Limpar busca",
"enter-to-search-web": "Tecle enter para buscar na rede"
},
"splash-screen": {
"loading": "Carregando"
},
"login": {
"title": "Dashy",
"guest-label": "Entrar como convidado",
"username-label": "Nome do usuário",
"password-label": "Senha",
"login-button": "Conectar",
@@ -31,8 +35,20 @@
"already-logged-in-text": "Você está logado como",
"proceed-to-dashboard": "Ir para o Painel",
"log-out-button": "Sair",
"proceed-guest-button": "Seguir como Convidado"
"proceed-guest-button": "Seguir como Convidado",
"guest-intro-1": "Essa instância possui o acesso como convidado ativado",
"guest-intro-2": "Convidados podem apenas visualizar os painéis, logo não podem escrever quaisquer mudanças no disco.",
"error": "Erro",
"error-no-user-configured": "Autenticação não está habilitada, ou nenhum usuário foi configurado.",
"error-go-home-button": "Ir para a página inicial",
"logged-in-guest": "Logado como convidado. Redirecionando...",
"error-guest-access": "O acesso como convidado não foi permitido"
},
"app-info": {
"title": "Informação do App"
},
"config": {
"main-tab": "Menu Principal",
"view-config-tab": "Ver configuração",
@@ -299,6 +315,16 @@
"remaining": "Restante",
"up": "Up",
"down": "Down"
},
"gluetun-status": {
"vpn-ip": "IP da VPN",
"country": "País",
"region": "Região",
"city": "Cidade",
"post-code": "Código postal",
"location": "Localização",
"timezone": "Fuso horário",
"organization": "Organização"
}
}
}
}

448
src/assets/locales/ro.json Normal file
View File

@@ -0,0 +1,448 @@
{
"home": {
"no-results": "Niciun rezultat găsit",
"no-data": "Nicio dată configurată",
"no-items-section": "Niciun element de afișat încă"
},
"search": {
"search-label": "Caută",
"search-placeholder": "Începeți să tastați pentru a filtra",
"clear-search-tooltip": "Șterge căutarea",
"enter-to-search-web": "Apasă enter pentru a căuta pe web"
},
"splash-screen": {
"loading": "Încărcare"
},
"login": {
"title": "Dashy",
"guest-label": "Acces Vizitator",
"username-label": "Nume utilizator",
"password-label": "Parolă",
"login-button": "Autentificare",
"remember-me-label": "Ține-mă minte pentru",
"remember-me-never": "Niciodată",
"remember-me-hour": "4 Ore",
"remember-me-day": "1 Zi",
"remember-me-week": "1 Săptămână",
"remember-me-long-time": "O perioadă lungă",
"error-missing-username": "Lipsește numele de utilizator",
"error-missing-password": "Lipsește parola",
"error-incorrect-username": "Utilizator negăsit",
"error-incorrect-password": "Parolă incorectă",
"success-message": "Autentificare...",
"logout-message": "Deconectat",
"already-logged-in-title": "Deja autentificat",
"already-logged-in-text": "Ești autentificat ca",
"proceed-to-dashboard": "Continuă către Tabloul de bord",
"log-out-button": "Deconectare",
"proceed-guest-button": "Continuă ca Vizitator",
"guest-intro-1": "Această instanță are acces pentru vizitatori activat.",
"guest-intro-2": "Vizitatorii au acces doar pentru vizualizare la tablourile de bord, deci nu pot scrie modificări pe disc.",
"error": "Eroare",
"error-no-user-configured": "Autentificarea nu este activată, sau nu au fost configurați utilizatori",
"error-go-home-button": "Mergi la Pagina Principală",
"logged-in-guest": "Autentificat ca Vizitator, Redirecționare...",
"error-guest-access": "Acces Vizitator Interzis"
},
"app-info": {
"title": "Informații Aplicație",
"error-log": "Jurnal Erori",
"no-errors": "Nicio eroare recentă detectată",
"help-support": "Ajutor & Suport",
"help-support-description" : "Pentru suport în utilizarea sau configurarea Dashy, consultați",
"help-support-discussions": "Discuțiile",
"support-dashy": "Suport pentru Dashy",
"support-dashy-description": "Pentru moduri în care poți contribui, verificați",
"support-dashy-link": "Pagina de Contribuții",
"report-bug": "Raportează o Eroare",
"report-bug-description": "Dacă crezi că ai găsit o eroare, atunci te rog",
"report-bug-link": "deschide o Problemă",
"more-info": "Mai Multe Informații",
"source": "Sursă",
"documentation": "Documentație",
"privacy-and-security": "Confidențialitate & Securitate",
"privacy-and-security-l1": "Pentru o descompunere a modului în care datele tale sunt gestionate de Dashy, consultați",
"privacy-and-security-privacy-policy": "Politica de Confidențialitate",
"privacy-and-security-advice": "Pentru sfaturi în securizarea tabloului tău de bord, poți consulta",
"privacy-and-security-advice-link": "Documentele de Management",
"privacy-and-security-security-issue": "Dacă ați descoperit o potențială problemă de securitate, raportați-o urmând",
"privacy-and-security-security-policy": "Politica de Securitate",
"license": "Licență",
"license-under": "Licențiat sub",
"licence-third-party": "Pentru licențele modulelor terțe părți, vă rugăm să consultați",
"licence-third-party-link": "Legal",
"list-contributors": "Pentru lista completă a contribuitorilor și mulțumiri, vedeți",
"list-contributors-link": "Credite",
"version": "Versiune"
},
"config": {
"main-tab": "Meniu Principal",
"view-config-tab": "Vizualizare Configurație",
"edit-config-tab": "Editare Configurație",
"custom-css-tab": "Stiluri Personalizate",
"heading": "Opțiuni de Configurare",
"download-config-button": "Vizualizați / Exportați Configurația",
"edit-config-button": "Editare Configurație",
"edit-css-button": "Editare CSS Personalizat",
"cloud-sync-button": "Activează Sincronizarea în Cloud",
"edit-cloud-sync-button": "Editare Sincronizare în Cloud",
"rebuild-app-button": "Reconstruire Aplicație",
"change-language-button": "Schimbă Limba Aplicației",
"reset-settings-button": "Resetează Setările Locale",
"disabled-note": "Unele caracteristici de configurare au fost dezactivate de administratorul tău",
"small-screen-note": "Utilizați un ecran foarte mic, și unele ecrane din acest meniu s-ar putea să nu fie optimale",
"app-info-button": "Informații Aplicație",
"backup-note": "Este recomandat să faceți o copie de siguranță a configurației înainte de a face modificări.",
"reset-config-msg-l1": "Aceasta va elimina toate setările utilizatorilor din stocarea locală, dar nu va afecta fișierul 'conf.yml'.",
"reset-config-msg-l2": "Ar trebui să faceți mai întâi o copie de siguranță a oricăror modificări pe care le-ați făcut local, dacă doriți să le utilizați în viitor.",
"reset-config-msg-l3": "Sunteți sigur că doriți să continuați?",
"data-cleared-msg": "Datele au fost șterse cu succes",
"actions-label": "Acțiuni",
"copy-config-label": "Copiază Configurația",
"data-copied-msg": "Configurația a fost copiată în clipboard",
"reset-config-label": "Resetează Configurația",
"css-save-btn": "Salvează Modificările",
"css-note-label": "Notă",
"css-note-l1": "Va trebui să reîmprospătați pagina pentru ca modificările să aibă efect.",
"css-note-l2": "Suprascrierile de stiluri sunt stocate doar local, deci este recomandat să faceți o copie a CSS-ului dvs.",
"css-note-l3": "Pentru a elimina toate stilurile personalizate, ștergeți conținutul și apăsați 'Salvează Modificările'",
"custom-css": {
"title": "CSS Personalizat",
"base-theme": "Tema de Bază"
}
},
"alternate-views": {
"alternate-view-heading": "Schimbă Vederea",
"default": "Implicit",
"workspace": "Spațiu de Lucru",
"minimal": "Minimal"
},
"settings": {
"theme-label": "Temă",
"layout-label": "Aspect",
"layout-auto": "Automat",
"layout-horizontal": "Orizontal",
"layout-vertical": "Vertical",
"item-size-label": "Dimensiune Element",
"item-size-small": "Mic",
"item-size-medium": "Mediu",
"item-size-large": "Mare",
"config-launcher-label": "Config",
"config-launcher-tooltip": "Actualizează Configurația",
"sign-out-tooltip": "Deconectare",
"sign-in-tooltip": "Conectare",
"sign-in-welcome": "Bună {username}!",
"hide": "Ascunde",
"open": "Deschide"
},
"updates": {
"app-version-note": "Versiune Dashy",
"up-to-date": "Actualizat",
"out-of-date": "Actualizare Disponibilă",
"unsupported-version-l1": "Utilizați o versiune nesuportată de Dashy",
"unsupported-version-l2": "Pentru cea mai bună experiență și patch-uri de securitate recente, vă rugăm să actualizați la"
},
"language-switcher": {
"title": "Schimbă Limba Aplicației",
"dropdown-label": "Selectați o Limbă",
"save-button": "Salvează",
"success-msg": "Limba Actualizată la"
},
"theme-maker": {
"title": "Configurator de Temă",
"export-button": "Exportă Variabile Personalizate",
"reset-button": "Resetează Stilurile pentru",
"show-all-button": "Arată Toate Variabilele",
"change-fonts-button": "Schimbă Fonturile",
"save-button": "Salvează",
"cancel-button": "Anulează",
"saved-toast": "{theme} Actualizat cu Succes",
"copied-toast": "Datele temei pentru {theme} au fost copiate în clipboard",
"reset-toast": "Culorile Personalizate pentru {theme} au fost Eliminate"
},
"config-editor": {
"save-location-label": "Locația de Salvare",
"location-local-label": "Aplică Local",
"location-disk-label": "Scrie Modificările în Fișierul de Configurație",
"save-button": "Salvează Modificările",
"preview-button": "Previzualizează Modificările",
"valid-label": "Configurația este Valabilă",
"status-success-msg": "Sarcină Completată",
"status-fail-msg": "Sarcină Eșuată",
"success-msg-disk": "Fișierul de configurație a fost scris pe disc cu succes",
"success-msg-local": "Modificările locale au fost salvate cu succes",
"success-note-l1": "Aplicația ar trebui să se reconstruiască automat.",
"success-note-l2": "Aceasta poate dura până la un minut.",
"success-note-l3": "Va trebui să reîmprospătați pagina pentru ca modificările să aibă efect.",
"error-msg-save-mode": "Vă rugăm să selectați un Mod de Salvare: Local sau Fișier",
"error-msg-cannot-save": "A apărut o eroare la salvarea configurației",
"error-msg-bad-json": "Eroare în JSON, posibil malformat",
"warning-msg-validation": "Avertisment de Validare",
"not-admin-note": "Nu puteți scrie modificările pe disc deoarece nu sunteți conectat ca administrator"
},
"app-rebuild": {
"title": "Reconstruire Aplicație",
"rebuild-note-l1": "O reconstruire este necesară pentru ca modificările scrise în fișierul conf.yml să aibă efect.",
"rebuild-note-l2": "Aceasta ar trebui să se întâmple automat, dar dacă nu s-a întâmplat, o puteți declanșa manual aici.",
"rebuild-note-l3": "Aceasta nu este necesară pentru modificările stocate local.",
"rebuild-button": "Începeți Reconstruirea",
"rebuilding-status-1": "Se reconstruiește...",
"rebuilding-status-2": "Aceasta poate dura câteva minute",
"error-permission": "Nu aveți permisiunea de a declanșa această acțiune",
"success-msg": "Reconstruirea a fost completată cu succes",
"fail-msg": "Operațiunea de reconstruire a eșuat",
"reload-note": "Este necesară reîncărcarea paginii pentru ca modificările să aibă efect",
"reload-button": "Reîncarcă Pagina"
},
"cloud-sync": {
"title": "Backup și Restaurare în Cloud",
"intro-l1": "Backup-ul și restaurarea în cloud este o caracteristică opțională, care vă permite să încărcați configurația pe internet și apoi să o restaurați pe orice alt dispozitiv sau instanță de Dashy.",
"intro-l2": "Toate datele sunt criptate end-to-end cu AES, folosind parola dumneavoastră ca cheie.",
"intro-l3": "Pentru mai multe informații, vă rugăm să consultați",
"intro-docs": "documentația",
"backup-title-setup": "Creați un Backup",
"backup-title-update": "Actualizați Backup-ul",
"password-label-setup": "Alegeți o Parolă",
"password-label-update": "Introduceți Parola",
"backup-button-setup": "Backup",
"backup-button-update": "Actualizați Backup-ul",
"backup-id-label": "ID-ul Dvs. de Backup",
"backup-id-note": "Acesta este utilizat pentru a restaura din backup-uri mai târziu. Deci păstrați-l, împreună cu parola dvs., într-un loc sigur.",
"restore-title": "Restaurare Backup",
"restore-id-label": "ID de Restaurare",
"restore-password-label": "Parola",
"restore-button": "Restaurare",
"backup-missing-password": "Lipsește Parola",
"backup-error-unknown": "Imposibil de procesat solicitarea",
"backup-error-password": "Parolă incorectă. Vă rugăm să introduceți parola actuală.",
"backup-success-msg": "Finalizat cu Succes",
"restore-success-msg": "Configurația a fost Restaurată cu Succes"
},
"menu": {
"open-section-title": "Deschide În",
"sametab": "Tab Curent",
"newtab": "Tab Nou",
"modal": "Modal Pop-Up",
"workspace": "Vizualizare Spațiu de Lucru",
"options-section-title": "Opțiuni",
"edit-item": "Editare",
"move-item": "Copiază sau Mută",
"remove-item": "Șterge"
},
"context-menus": {
"item": {
"open-section-title": "Deschide În",
"sametab": "Tab Curent",
"newtab": "Tab Nou",
"modal": "Modal Pop-Up",
"workspace": "Vizualizare Spațiu de Lucru",
"clipboard": "Copiază în Clipboard",
"options-section-title": "Opțiuni",
"edit-item": "Editare",
"move-item": "Copiază sau Mută",
"remove-item": "Șterge",
"copied-toast": "URL-ul a fost copiat în clipboard"
},
"section": {
"open-section": "Deschide Secțiunea",
"edit-section": "Editare",
"expand-collapse": "Extinde / Colapsează",
"move-section": "Mută La",
"remove-section": "Șterge"
}
},
"footer": {
"dev-by": "Dezvoltat de",
"licensed-under": "Licențiat sub",
"get-the": "Obțineți",
"source-code": "Codul Sursă"
},
"interactive-editor": {
"menu": {
"start-editing-tooltip": "Intră în Editorul Interactiv",
"edit-site-data-subheading": "Editează Datele Site-ului",
"edit-page-info-btn": "Editează Informațiile Paginii",
"edit-page-info-tooltip": "Titlul aplicației, descriere, link-uri de navigare, textul din footer, etc.",
"edit-app-config-btn": "Editează Configurația Aplicației",
"edit-app-config-tooltip": "Toate celelalte opțiuni de configurare a aplicației",
"edit-pages-btn": "Editează Paginile",
"edit-pages-tooltip": "Adaugă sau elimină vizualizări suplimentare",
"config-save-methods-subheading": "Opțiuni de Salvare a Configurației",
"save-locally-btn": "Salvează Local",
"save-locally-tooltip": "Salvează configurația local, în stocarea browserului. Aceasta nu va afecta fișierul tău de configurare, dar schimbările vor fi salvate doar pe acest dispozitiv",
"save-disk-btn": "Salvează pe Disc",
"save-disk-tooltip": "Salvează configurația în fișierul conf.yml de pe disc. Aceasta va face un backup și apoi va suprascrie configurația existentă",
"export-config-btn": "Exportă Configurația",
"export-config-tooltip": "Vizualizează și exportă noua configurație, fie într-un fișier, fie în clipboard",
"cloud-backup-btn": "Backup în Cloud",
"cloud-backup-tooltip": "Salvează un backup criptat al configurației în cloud",
"edit-raw-config-btn": "Editează Configurația Brută",
"edit-raw-config-tooltip": "Vizualizează și modifică configurația brută prin editorul JSON",
"cancel-changes-btn": "Anulează Modificările",
"cancel-changes-tooltip": "Resetează modificările curente și ieși din Modul de Editare. Aceasta nu va afecta configurația salvată",
"edit-mode-name": "Mod de Editare",
"edit-mode-subtitle": "Ești în Modul de Editare",
"edit-mode-description": "Aceasta înseamnă că poți face modificări la configurația ta și să previzualizezi rezultatele, dar până nu salvezi, niciuna dintre modificările tale nu va fi păstrată.",
"save-stage-btn": "Salvează",
"cancel-stage-btn": "Anulează",
"save-locally-warning": "Dacă vei continua, modificările vor fi salvate doar în browserul tău. Ar trebui să exporți o copie a configurației tale pentru utilizare pe alte mașini. Dorești să continui?"
},
"edit-item": {
"missing-title-err": "Este necesar un titlu pentru element"
},
"edit-section": {
"edit-section-title": "Editează Secțiunea",
"add-section-title": "Adaugă o Secțiune Nouă",
"edit-tooltip": "Clic pentru a edita, sau clic dreapta pentru mai multe opțiuni",
"remove-confirm": "Ești sigur că vrei să elimini această secțiune? Această acțiune poate fi anulată mai târziu."
},
"edit-app-config": {
"warning-msg-title": "Procedează cu Atenție",
"warning-msg-l1": "Următoarele opțiuni sunt pentru configurarea avansată a aplicației.",
"warning-msg-l2": "Dacă nu ești sigur în legătură cu oricare dintre câmpuri, te rog să consulți",
"warning-msg-docs": "documentația",
"warning-msg-l3": "pentru a evita consecințele nedorite."
},
"export": {
"export-title": "Exportă Configurația",
"copy-clipboard-btn": "Copiază în Clipboard",
"copy-clipboard-tooltip": "Copiază toată configurația aplicației în clipboard-ul sistemului, în format YAML",
"download-file-btn": "Descarcă ca Fișier",
"download-file-tooltip": "Descarcă toată configurația aplicației pe dispozitivul tău, într-un fișier YAML",
"view-title": "Vizualizează Configurația"
}
},
"widgets": {
"general": {
"loading": "Se încarcă...",
"show-more": "Extinde Detaliile",
"cpu-details": "Detalii CPU",
"mem-details": "Detalii Memorie",
"show-less": "Arată Mai Puțin",
"open-link": "Continuă Citirea"
},
"pi-hole": {
"status-heading": "Stare"
},
"stat-ping": {
"up": "Online",
"down": "Offline"
},
"net-data": {
"cpu-chart-title": "Istoric CPU",
"mem-chart-title": "Utilizare Memorie",
"mem-breakdown-title": "Detalii Memorie",
"load-chart-title": "Încărcare Sistem"
},
"glances": {
"disk-space-free": "Liber",
"disk-space-used": "Utilizat",
"disk-mount-point": "Punct de Montare",
"disk-file-system": "Sistem de Fișiere",
"disk-io-read": "Citire",
"disk-io-write": "Scriere",
"system-load-desc": "Numărul de procese așteptând în coada de execuție, mediat pe toate nucleele"
},
"system-info": {
"uptime": "Timp de Funcționare"
},
"flight-data": {
"arrivals": "Sosiri",
"departures": "Plecări"
},
"tfl-status": {
"good-service-all": "Servicii Bune pe Toate Liniile",
"good-service-rest": "Servicii Bune pe Celelalte Linii"
},
"synology-download": {
"download": "Descărcare",
"upload": "Încărcare",
"downloaded": "Descărcat",
"uploaded": "Încărcat",
"remaining": "Rămas",
"up": "Sus",
"down": "Jos"
},
"gluetun-status": {
"vpn-ip": "IP VPN",
"country": "Țară",
"region": "Regiune",
"city": "Oraș",
"post-code": "Cod Poștal",
"location": "Locație",
"timezone": "Fus Orar",
"organization": "Organizație"
},
"nextcloud": {
"active": "activ",
"and": "și",
"applications": "aplicații",
"available": "disponibil",
"away": "Plecat",
"cache-full": "CACHE PLIN",
"chat-room": "camera de chat",
"delete-all": "Șterge tot",
"delete-notification": "Șterge notificarea",
"disabled": "dezactivat",
"disk-quota": "Cotă de Disc",
"disk-space": "Spațiu pe Disc",
"dnd": "Nu Deranja",
"email": "email",
"enabled": "activat",
"federated-shares-ucfirst": "Partajări Federate",
"federated-shares": "partajări federate",
"files": "fișier{plural}",
"free": "liber",
"groups": "grupuri",
"hit-rate": "rata de accesare",
"hits": "accesări",
"home": "acasă",
"in": "în",
"keys": "chei",
"last-24-hours": "ultimele 24 de ore",
"last-5-minutes": "în ultimele 5 minute",
"last-hour": "în ultima oră",
"last-login": "Ultima autentificare",
"last-restart": "Ultimul restart",
"load-averages": "Mediile de Încărcare pe toate nucleele CPU",
"local-shares": "Partajări Locale",
"local": "local",
"max-keys": "chei maxime",
"memory-used": "memorie utilizată",
"memory-utilisation": "utilizarea memoriei",
"memory": "memorie",
"misses": "rateuri",
"no-notifications": "Fără notificări",
"no-pending-updates": "fără actualizări în așteptare",
"nothing-to-show": "Nimic de afișat aici în acest moment",
"of-which": "din care",
"of": "din",
"offline": "Deconectat",
"online": "Conectat",
"other": "alt",
"overall": "În total",
"private-link": "link privat",
"public-link": "link public",
"quota-enabled": "Cota de Disc este {not}activată pentru acest utilizator",
"received": "primit",
"scripts": "scripturi",
"sent": "trimis",
"started": "Început",
"storages-by-type": "Stocări pe tip",
"storages": "stocare{plural}",
"strings-use": "utilizare șiruri",
"tasks": "Sarcini",
"total-files": "total fișiere",
"total-users": "total utilizatori",
"total": "total",
"until": "Până la",
"updates-available-for": "Actualizări disponibile pentru",
"updates-available": "actualizare{plural} disponibilă",
"used": "utilizat",
"user": "utilizator",
"using": "utilizând",
"version": "versiune",
"wasted": "pierdut"
}
}
}

View File

@@ -63,8 +63,8 @@
"privacy-and-security": "Конфіденційність та безпека",
"privacy-and-security-l1": "Для детальної інформації про те, як Dashy керує вашими даними, див.",
"privacy-and-security-privacy-policy": "Політика конфіденційності",
"app-info.privacy-and-security-advice": "Щоб отримати поради щодо захисту вашої інформаційної панелі, ви можете звернутися до розділу",
"app-info.privacy-and-security-advice-link": "Документи керування",
"privacy-and-security-advice": "Щоб отримати поради щодо захисту вашої інформаційної панелі, ви можете звернутися до розділу",
"privacy-and-security-advice-link": "Документи керування",
"privacy-and-security-security-issue": "Якщо ви виявили потенційну проблему з безпекою, повідомте про це до розділу",
"privacy-and-security-security-policy": "Політика безпеки",
"license": "Ліцензія",

View File

@@ -29,7 +29,7 @@
"error-missing-password": "密码空缺",
"error-incorrect-username": "用户不存在",
"error-incorrect-password": "密码不正确",
"success-message": "登陆成功。。。",
"success-message": "登陆成功。",
"logout-message": "注销",
"already-logged-in-title": "已经成功登陆",
"already-logged-in-text": "你的登陆身份",
@@ -37,11 +37,11 @@
"log-out-button": "注销",
"proceed-guest-button": "以游客身份前往",
"guest-intro-1": "该实例已启用访客访问.",
"guest-intro-2": "访客只有访问权限,无法保存变更",
"guest-intro-2": "访客只有访问权限无法保存变更",
"error": "错误",
"error-no-user-configured": "没有启用验证,或者未配置用户",
"error-no-user-configured": "没有启用验证或者未配置用户",
"error-go-home-button": "Go Home",
"logged-in-guest": "以访客身份登陆,正在跳转...",
"logged-in-guest": "以访客身份登陆正在跳转...",
"error-guest-access": "不允许访客访问"
},
"app-info": {
@@ -63,8 +63,8 @@
"privacy-and-security": "隐私与安全",
"privacy-and-security-l1": "关于Dashy如何管理您的数据的详细信息请参阅",
"privacy-and-security-privacy-policy": "隐私政策",
"app-info.privacy-and-security-advice": "如果您需要保护您的仪表盘,请参考",
"app-info.privacy-and-security-advice-link": "管理文档",
"privacy-and-security-advice": "如果您需要保护您的仪表盘,请参考",
"privacy-and-security-advice-link": "管理文档",
"privacy-and-security-security-issue": "如果您发现潜在的安全问题,请遵循我们的",
"privacy-and-security-security-policy": "安全政策",
"license": "许可证",
@@ -78,22 +78,22 @@
"config": {
"main-tab": "主菜单",
"view-config-tab": "视图设置",
"edit-config-tab": "编辑设置",
"edit-config-tab": "修改设置",
"custom-css-tab": "自定义样式",
"heading": "设置选项",
"download-config-button": "下载配置",
"edit-config-button": "编辑设置",
"edit-css-button": "编辑自定义 CSS",
"cloud-sync-button": "启用云端同步",
"edit-cloud-sync-button": "编辑云端同步",
"edit-config-button": "修改设置",
"edit-css-button": "自定义CSS",
"cloud-sync-button": "云端同步",
"edit-cloud-sync-button": "修改云端同步",
"rebuild-app-button": "重建应用",
"change-language-button": "更改语言",
"reset-settings-button": "恢复本地设置",
"reset-settings-button": "恢复默认设置",
"disabled-note": "您的管理员已禁用某些配置功能",
"small-screen-note": "您正在使用非常小的屏幕,某些菜单屏幕可能不够优化",
"app-info-button": "应用详情",
"backup-note": "建议在进行更改之前备份你的配置。",
"reset-config-msg-l1": "这将从本地存储中删除所有用户设置,但不会影响conf.yml文件。",
"reset-config-msg-l1": "这将从本地存储中删除所有用户设置,但不会影响 conf.yml 文件。",
"reset-config-msg-l2": "如果想在以后使用它们,应该首先备份你所做的任何更改。",
"reset-config-msg-l3": "确定执行吗?",
"data-cleared-msg": "成功清空数据",
@@ -103,9 +103,9 @@
"reset-config-label": "重置设置",
"css-save-btn": "保存更改",
"css-note-label": "注意",
"css-note-l1": "你需要刷新页面才能使更改生效。",
"css-note-l2": "样式覆盖仅存储在本地,因此建议复制你的 CSS。",
"css-note-l3": "要删除所有自定义样式,请删除内容并点击保存更改",
"css-note-l1": "刷新页面使其生效。",
"css-note-l2": "自定义样式仅在本地有效,设置储存在当前浏览器,建议做好备份。",
"css-note-l3": "如需删除自定义样式,清空上面内容并点击保存",
"custom-css": {
"title": "自定义 CSS",
"base-theme": "基础主题"
@@ -132,8 +132,8 @@
"sign-out-tooltip": "注销",
"sign-in-tooltip": "登陆",
"sign-in-welcome": "你好 {username}",
"hide": "Hide",
"open": "Open"
"hide": "隐藏",
"open": "打开"
},
"updates": {
"app-version-note": "Dashy 版本",
@@ -195,9 +195,10 @@
"reload-button": "刷新页面"
},
"cloud-sync": {
"title": "云备份 & 云恢复",
"title": "云备份&恢复",
"intro-l1": "云备份和云恢复是一项试验性功能,你将配置上传到网络,然后在其他设备或 Dashy 实例上恢复。",
"intro-l2": "所有数据都使用AES端对端加密使用你的密码作为密钥。",
"intro-docs": "文档",
"intro-l3": "有关更多信息,请参阅",
"backup-title-setup": "创建备份",
"backup-title-update": "更新备份",
@@ -268,11 +269,11 @@
"edit-pages-tooltip": "添加或删除其他的视图",
"config-save-methods-subheading": "配置保存选项",
"save-locally-btn": "暂存本地",
"save-locally-tooltip": "将设置保存在本地浏览器上. 这不会影响配置文件,但更改只会保留在当前设备上.",
"save-locally-tooltip": "将设置保存在浏览器上这不会影响配置文件,仅作用于当前的浏览器。",
"save-disk-btn": "保存",
"save-disk-tooltip": "将设置保存到服务端的conf.yml文件.它会备份之前的配置文件.",
"save-disk-tooltip": "将设置保存到服务端的conf.yml文件它会备份之前的配置文件.",
"export-config-btn": "导出配置",
"export-config-tooltip": "查看并导出新的配置 到 文件 或 剪贴板",
"export-config-tooltip": "查看并导出新的配置到 文件 或 剪贴板",
"cloud-backup-btn": "备份到云端",
"cloud-backup-tooltip": "以加密的方式保存到云端",
"edit-raw-config-btn": "编辑原始配置",
@@ -284,10 +285,10 @@
"edit-mode-description": "你可以对配置进行修改并预览,在保存之前,你的任何更改都不会被保留。",
"save-stage-btn": "保存",
"cancel-stage-btn": "取消",
"save-locally-warning": "如果你继续,更改将仅保存在你的浏览器中。 你应该导出配置的副本以在其他机器上使用。 你想继续"
"save-locally-warning": "配置将保存到你当前的浏览器上。你也可以导出配置到其他设备上使用。是否继续?"
},
"edit-item": {
"missing-title-err": "项目标题是必需的"
"missing-title-err": "标题是必需的"
},
"edit-section": {
"edit-section-title": "编辑 Section",
@@ -314,7 +315,9 @@
"widgets": {
"general": {
"loading": "加载中...",
"show-more": "展开详情",
"show-more": "显示更多",
"cpu-details": "CPU 详情",
"mem-details": "内存 详情",
"show-less": "显示更少信息",
"open-link": "继续读取"
},

View File

@@ -63,8 +63,8 @@
"privacy-and-security": "隱私權和安全性",
"privacy-and-security-l1": "若要了解 Dashy 是如何管理您的資料,請參閱",
"privacy-and-security-privacy-policy": "隱私權政策",
"app-info.privacy-and-security-advice": "關於提升儀錶板安全性的建議,請參閱",
"app-info.privacy-and-security-advice-link": "管理文件",
"privacy-and-security-advice": "關於提升儀錶板安全性的建議,請參閱",
"privacy-and-security-advice-link": "管理文件",
"privacy-and-security-security-issue": "若您找到潛在的安全問題,請回報,並遵照我們的",
"privacy-and-security-security-policy": "安全政策",
"license": "授權條款",

View File

@@ -30,7 +30,7 @@
<!-- License -->
<h3>{{ $t('app-info.license') }}</h3>
{{ $t('app-info.license-under') }} <a href="https://github.com/Lissy93/dashy/blob/master/LICENSE">MIT X11</a>.
Copyright <a href="https://aliciasykes.com">Alicia Sykes</a> © 2021.<br>
Copyright <a href="https://aliciasykes.com">Alicia Sykes</a> © {{new Date().getFullYear()}}.<br>
{{ $t('app-info.licence-third-party') }} <a href="https://github.com/Lissy93/dashy/blob/master/.github/LEGAL.md">{{ $t('app-info.licence-third-party-link') }}</a>.<br>
{{ $t('app-info.list-contributors') }} <a href="https://github.com/Lissy93/dashy/blob/master/docs/credits.md">{{ $t('app-info.list-contributors-link') }}</a>.
<!-- App Version -->

View File

@@ -155,12 +155,23 @@ export default {
},
/* When restored data is revieved, then save to local storage, and apply it in state */
applyRestoredData(config, backupId) {
// Store restored data in local storage
localStorage.setItem(localStorageKeys.CONF_SECTIONS, JSON.stringify(config.sections));
localStorage.setItem(localStorageKeys.APP_CONFIG, JSON.stringify(config.appConfig));
localStorage.setItem(localStorageKeys.PAGE_INFO, JSON.stringify(config.pageInfo));
if (config.appConfig.theme) {
localStorage.setItem(localStorageKeys.THEME, config.appConfig.theme);
const isSubPage = !!this.$store.state.currentConfigInfo.confId;
if (isSubPage) { // Apply to sub-page only
const subConfigId = this.$store.state.currentConfigInfo.confId;
const sectionStorageKey = `${localStorageKeys.CONF_SECTIONS}-${subConfigId}`;
const pageInfoStorageKey = `${localStorageKeys.PAGE_INFO}-${subConfigId}`;
const themeStoreKey = `${localStorageKeys.THEME}-${subConfigId}`;
localStorage.setItem(sectionStorageKey, JSON.stringify(config.sections));
localStorage.setItem(pageInfoStorageKey, JSON.stringify(config.pageInfo));
localStorage.setItem(themeStoreKey, config.appConfig.theme);
} else { // Apply to main config
localStorage.setItem(localStorageKeys.CONF_SECTIONS, JSON.stringify(config.sections));
localStorage.setItem(localStorageKeys.APP_CONFIG, JSON.stringify(config.appConfig));
localStorage.setItem(localStorageKeys.PAGE_INFO, JSON.stringify(config.pageInfo));
localStorage.setItem(localStorageKeys.CONF_PAGES, JSON.stringify(config.pages || []));
if (config.appConfig.theme) {
localStorage.setItem(localStorageKeys.THEME, config.appConfig.theme);
}
}
// Save hashed token in local storage
this.setBackupIdLocally(backupId, this.restorePassword);

View File

@@ -47,16 +47,17 @@
</Button>
<!-- Display app version and language -->
<p class="language">{{ getLanguage() }}</p>
<p v-if="$store.state.currentConfigInfo" class="config-location">
Using Config From<br>
{{ $store.state.currentConfigInfo.confPath }}
<!-- Display location of config file -->
<p class="config-location">
Using config from
<a :href="configPath">{{ configPath }}</a>
</p>
<AppVersion />
</div>
<!-- Display note if Config disabled, or if on mobile -->
<p v-if="!enableConfig" class="config-disabled-note">{{ $t('config.disabled-note') }}</p>
<p class="small-screen-note" style="display: none;">{{ $t('config.small-screen-note') }}</p>
<div class="config-note">
<div class="config-note" @click="openExportConfigModal">
<span>{{ $t('config.backup-note') }}</span>
</div>
</div>
@@ -116,6 +117,11 @@ export default {
enableConfig() {
return this.$store.getters.permissions.allowViewConfig;
},
configPath() {
return this.$store.state.currentConfigInfo?.confPath
|| process.env.VUE_APP_CONFIG_PATH
|| '/conf.yml';
},
},
components: {
Button,
@@ -248,8 +254,12 @@ a.hyperlink-wrapper {
p.app-version, p.language, p.config-location {
margin: 0.5rem auto;
font-size: 1rem;
color: var(--transparent-white-50);
color: var(--config-settings-color);
cursor: default;
opacity: var(--dimming-factor);
a {
color: var(--config-settings-color);
}
}
div.code-container {

View File

@@ -115,7 +115,10 @@ export default {
},
},
mounted() {
this.jsonData = this.config;
const jsonData = { ...this.config };
jsonData.sections = (jsonData.sections || []).map(({ filteredItems, ...section }) => section);
if (!jsonData.pageInfo) jsonData.pageInfo = { title: 'Dashy' };
this.jsonData = jsonData;
if (!this.allowWriteToDisk) this.saveMode = 'local';
},
methods: {
@@ -141,7 +144,11 @@ export default {
this.$modal.hide(modalNames.CONF_EDITOR);
},
writeToDisk() {
this.writeConfigToDisk(this.config);
const newData = this.jsonData;
this.writeConfigToDisk(newData);
// this.$store.commit(StoreKeys.SET_APP_CONFIG, newData.appConfig);
this.$store.commit(StoreKeys.SET_PAGE_INFO, newData.pageInfo);
this.$store.commit(StoreKeys.SET_SECTIONS, newData.sections);
},
saveLocally() {
const msg = this.$t('interactive-editor.menu.save-locally-warning');

View File

@@ -94,6 +94,7 @@ export default {
const raw = rawAppConfig;
const isEmptyObject = (obj) => (typeof obj === 'object' && Object.keys(obj).length === 0);
const isEmpty = (value) => (value === undefined || isEmptyObject(value));
// Delete empty values
Object.keys(raw).forEach(key => {
if (isEmpty(raw[key])) delete raw[key];

View File

@@ -22,6 +22,14 @@
<DownloadConfigIcon />
</Button>
</div>
<!-- Show path to which config file is being used -->
<div class="config-path-info">
<h3>Config Location</h3>
<p>
The base config file you are currently using is
<a :href="configPath">{{ configPath }}</a>
</p>
</div>
<!-- View Config in Tree Mode Section -->
<h3>{{ $t('interactive-editor.export.view-title') }}</h3>
<tree-view :data="config" class="config-tree-view" />
@@ -61,6 +69,11 @@ export default {
allowViewConfig() {
return this.$store.getters.permissions.allowViewConfig;
},
configPath() {
return this.$store.state.currentConfigInfo?.confPath
|| process.env.VUE_APP_CONFIG_PATH
|| '/conf.yml';
},
},
methods: {
convertJsonToYaml() {
@@ -121,6 +134,13 @@ export default {
border-bottom: 1px dashed var(--interactive-editor-color);
button { margin: 0 1rem; }
}
.config-path-info {
p, a {
color: var(--interactive-editor-color);
font-size: 1.2rem;
}
border-bottom: 1px dashed var(--interactive-editor-color);
}
.config-tree-view {
padding: 0.5rem;
font-family: var(--font-monospace);

View File

@@ -64,7 +64,6 @@ export default {
return this.$store.state.editMode;
},
sectionKey() {
if (this.isEditMode) return undefined;
return `collapsible-${this.uniqueKey}`;
},
collapseClass() {
@@ -104,12 +103,23 @@ export default {
watch: {
checkboxState(newState) {
this.isExpanded = newState;
this.updateLocalStorage(); // Save every change immediately
},
uniqueKey() {
this.checkboxState = this.isExpanded;
uniqueKey(newVal, oldVal) {
if (newVal !== oldVal) {
this.refreshCollapseState(); // Refresh state when key changes
}
},
},
methods: {
refreshCollapseState() {
this.checkboxState = this.isExpanded;
},
updateLocalStorage() {
const collapseState = this.locallyStoredCollapseStates();
collapseState[this.uniqueKey] = this.checkboxState;
localStorage.setItem(localStorageKeys.COLLAPSE_STATE, JSON.stringify(collapseState));
},
/* Either expand or collapse section, based on it's current state */
toggle() {
this.checkboxState = !this.checkboxState;

View File

@@ -9,7 +9,8 @@
:target="anchorTarget"
:class="`item ${makeClassList}`"
v-tooltip="getTooltipOptions()"
rel="noopener noreferrer" tabindex="0"
:rel="`${item.rel || 'noopener noreferrer'}`"
tabindex="0"
:id="`link-${item.id}`"
:style="customStyle"
>
@@ -255,8 +256,12 @@ export default {
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
word-break: keep-all;
overflow: hidden;
span.text {
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
display: block;
}
}
@@ -385,6 +390,7 @@ p.description {
font-size: .9em;
line-height: 1rem;
height: 2rem;
overflow: hidden;
}
}
}

View File

@@ -74,8 +74,8 @@
</div>
<!-- Modal for opening in modal view -->
<IframeModal
:ref="`iframeModal`"
:name="`iframeModal`"
:ref="`iframeModal-${groupId}`"
:name="`iframeModal-${groupId}`"
@closed="$emit('itemClicked')"
/>
<!-- Edit item menu -->
@@ -213,7 +213,7 @@ export default {
methods: {
/* Opens the iframe modal */
triggerModal(url) {
this.$refs.iframeModal.show(url);
this.$refs[`iframeModal-${this.groupId}`].show(url);
},
/* Sorts items alphabetically using the title attribute */
sortAlphabetically(items) {

View File

@@ -0,0 +1,153 @@
<template>
<div class="critical-error-wrap" v-if="shouldShow">
<button class="close" title="Close Warning" @click="close">🗙</button>
<h3>{{ $t('critical-error.title') }}</h3>
<p>{{ $t('critical-error.subtitle') }}</p>
<h4>{{ $t('critical-error.sub-ensure-that') }}</h4>
<ul>
<li>The configuration file can be found at the specified location</li>
<li>There are no CORS rules preventing client-side access</li>
<li>The YAML is valid, parsable and matches the schema</li>
</ul>
<h4>{{ $t('critical-error.sub-error-details') }}</h4>
<pre>{{ this.$store.state.criticalError }}</pre>
<h4>{{ $t('critical-error.sub-next-steps') }}</h4>
<ul>
<li>Check the browser console for more details
(<a href="https://github.com/Lissy93/dashy/blob/master/docs/troubleshooting.md#how-to-open-browser-console">see how</a>)
</li>
<li>View the
<a href="https://github.com/Lissy93/dashy/blob/master/docs/troubleshooting.md">Troubleshooting Guide</a>
and <a href="https://dashy.to/docs/">Docs</a>
</li>
<li>
If you've verified the config is present, accessible and valid, and cannot find the solution
in the troubleshooting, docs or GitHub issues,
then <a href="https://github.com/Lissy93/dashy/issues/new/choose">open a ticket on GitHub</a>
</li>
<li>Click 'Ignore Critical Errors' below to not show this warning again</li>
</ul>
<button class="user-doesnt-care" @click="ignoreWarning">
{{ $t('critical-error.ignore-button') }}
</button>
</div>
</template>
<script>
import { localStorageKeys } from '@/utils/defaults';
import Keys from '@/utils/StoreMutations';
export default {
name: 'CriticalError',
computed: {
/* Determines if we should show this component.
* If error present AND user hasn't disabled */
shouldShow() {
return this.$store.state.criticalError
&& !localStorage[localStorageKeys.DISABLE_CRITICAL_WARNING];
},
},
methods: {
/* Ignore all future errors, by putting a key in local storage */
ignoreWarning() {
localStorage.setItem(localStorageKeys.DISABLE_CRITICAL_WARNING, true);
this.close();
},
/* Close this dialog, by removing this error from the local store */
close() {
this.$store.commit(Keys.CRITICAL_ERROR_MSG, null);
},
},
};
</script>
<style scoped lang="scss">
@import '@/styles/media-queries.scss';
.critical-error-wrap {
position: absolute;
top: 40%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 3;
max-width: 50rem;
background: var(--background-darker);
padding: 1rem;
border-radius: var(--curve-factor);
color: var(--danger);
border: 2px solid var(--danger);
display: flex;
flex-direction: column;
justify-content: center;
gap: 0.5rem;
transition: all 0.2s ease-in-out;
@include tablet-down {
top: 50%;
width: 85vw;
}
p, ul, h4, a {
margin: 0;
color: var(--white);
}
pre {
color: var(--warning);
font-size: 0.8rem;
overflow: auto;
background: var(--transparent-white-10);
padding: 0.5rem;
border-radius: var(--curve-factor);
}
h4 {
margin: 0.5rem 0 0 0;
font-size: 1.2rem;
}
h3 {
font-size: 2.2rem;
text-align: center;
background: var(--danger);
color: var(--white);
margin: -1rem -1rem 1rem -1rem;
padding: 0.5rem;
}
ul {
padding-left: 1rem;
}
.user-doesnt-care {
background: var(--background-darker);
color: var(--white);
border-radius: var(--curve-factor);
border: none;
text-decoration: underline;
padding: 0.25rem 0.5rem;
cursor: pointer;
width: fit-content;
margin: 0 auto;
transition: all 0.2s ease-in-out;
&:hover {
background: var(--danger);
color: var(--background-darker);
text-decoration: none;
}
}
.close {
position: absolute;
top: 1rem;
right: 1rem;
width: 1.5rem;
height: 1.5rem;
padding: 0;
display: flex;
justify-content: center;
align-items: center;
font-size: 1rem;
background: var(--background);
color: var(--primary);
border: none;
border-radius: var(--curve-factor);
transition: all 0.2s ease-in-out;
&:hover {
background: var(--primary);
color: var(--background);
}
}
}
</style>

View File

@@ -1,16 +1,13 @@
<template>
<!-- User Footer -->
<footer v-if="text && text !== '' && visible" v-html="text"></footer>
<!-- Default Footer -->
<footer v-else-if="visible">
<span v-if="$store.state.currentConfigInfo" class="path-to-config">
Using: {{ $store.state.currentConfigInfo.confPath }}
</span>
<span>
{{ $t('footer.dev-by') }} <a :href="authorUrl">{{authorName}}</a>.
{{ $t('footer.licensed-under') }} <a :href="licenseUrl">{{license}}</a>
{{ showCopyright? '©': '' }} {{date}}.
{{ $t('footer.get-the') }} <a :href="repoUrl">{{ $t('footer.source-code') }}</a>.
<footer v-if="visible">
<!-- User-defined footer -->
<span v-if="text" v-html="text"></span>
<!-- Default footer -->
<span v-else>
<a :href="defaultInfo.projectUrl">Dashy</a> is free & open source
- licensed under <a :href="defaultInfo.licenseUrl">{{defaultInfo.license}}</a>,
© <a :href="defaultInfo.authorUrl">{{defaultInfo.authorName}}</a> {{defaultInfo.date}}.
Get support on GitHub, at <a :href="defaultInfo.repoUrl">{{defaultInfo.repoName}}</a>.
</span>
</footer>
</template>
@@ -23,13 +20,20 @@ export default {
name: 'Footer',
props: {
text: String,
authorName: { type: String, default: 'Alicia Sykes' },
authorUrl: { type: String, default: 'https://aliciasykes.com' },
license: { type: String, default: 'MIT' },
licenseUrl: { type: String, default: 'https://gist.github.com/Lissy93/143d2ee01ccc5c052a17' },
date: { type: String, default: `${new Date().getFullYear()}` },
showCopyright: { type: Boolean, default: true },
repoUrl: { type: String, default: 'https://github.com/lissy93/dashy' },
},
data() {
return {
defaultInfo: {
authorName: 'Alicia Sykes',
authorUrl: 'https://as93.net',
license: 'MIT',
licenseUrl: 'https://gist.github.com/Lissy93/143d2ee01ccc5c052a17',
date: `${new Date().getFullYear()}`,
repoUrl: 'https://github.com/lissy93/dashy',
repoName: 'Lissy93/Dashy',
projectUrl: 'https://dashy.to',
},
};
},
computed: {
visible() {
@@ -56,7 +60,7 @@ footer {
display: none;
}
span.path-to-config {
float: right;
float: left;
font-size: 0.75rem;
margin: 0.1rem 0.5rem 0 0;
opacity: var(--dimming-factor);

View File

@@ -66,7 +66,7 @@ export default {
span.subtitle {
color: var(--heading-text-color);
font-style: italic;
text-shadow: 1px 1px 2px #130f23;
text-shadow: 1px 1px 2px #130f2347;
opacity: var(--dimming-factor);
}
img.site-logo {

View File

@@ -319,6 +319,10 @@ div.action-buttons {
min-width: 6rem;
padding: 0.25rem 0.5rem;
margin: 1rem 0.5rem 0.5rem;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
}
}

View File

@@ -37,8 +37,10 @@ export default {
input: '',
};
},
props: {
iconSize: String,
computed: {
iconSize() {
return this.$store.getters.iconSize;
},
},
components: {
IconSmall,

View File

@@ -5,19 +5,19 @@
<IconDeafault
@click="updateDisplayLayout('auto')"
v-tooltip="tooltip($t('settings.layout-auto'))"
:class="`layout-icon ${displayLayout === 'auto' ? 'selected' : ''}`"
:class="`layout-icon ${layout === 'auto' ? 'selected' : ''}`"
tabindex="-2"
/>
<IconHorizontal
@click="updateDisplayLayout('horizontal')"
v-tooltip="tooltip($t('settings.layout-horizontal'))"
:class="`layout-icon ${displayLayout === 'horizontal' ? 'selected' : ''}`"
:class="`layout-icon ${layout === 'horizontal' ? 'selected' : ''}`"
tabindex="-2"
/>
<IconVertical
@click="updateDisplayLayout('vertical')"
v-tooltip="tooltip($t('settings.layout-vertical'))"
:class="`layout-icon ${displayLayout === 'vertical' ? 'selected' : ''}`"
:class="`layout-icon ${layout === 'vertical' ? 'selected' : ''}`"
tabindex="-2"
/>
</div>
@@ -40,6 +40,11 @@ export default {
IconHorizontal,
IconVertical,
},
computed: {
layout() {
return this.$store.getters.layout;
},
},
methods: {
updateDisplayLayout(layout) {
this.$store.commit(StoreKeys.SET_ITEM_LAYOUT, layout);

View File

@@ -1,36 +1,70 @@
<template>
<transition name="slide-fade">
<div class="kb-sc-info" v-if="!shouldHide">
<h5>There are keyboard shortcuts! 🙌</h5>
<h5>{{ popupContent.title }}</h5>
<div class="close" title="Hide forever [Esc]" @click="hideWelcomeHelper()">x</div>
<p title="Press [Esc] to hide this tip forever. See there's even a shortcut for that! 🚀">
Just start typing to filter. Then use the tab key to cycle through results,
and press enter to launch the selected item, or alt + enter to open in a modal.
You can hit Esc at anytime to clear the search. Easy 🥳
</p>
<p :title="popupContent.hoverText">{{ popupContent.message }}</p>
<p :title="popupContent.hoverText">{{ popupContent.messageContinued }}</p>
<div class="action-buttons">
<button @click="exportConfig">Export Local Config</button>
<button @click="saveConfig">Save Changes to Disk</button>
<button @click="resetLocalConfig">Reset Local Changes</button>
<button @click="hideWelcomeHelper">Dismiss this Notification</button>
</div>
</div>
</transition>
</template>
<script>
import { localStorageKeys } from '@/utils/defaults';
import { localStorageKeys, modalNames } from '@/utils/defaults';
import StoreKeys from '@/utils/StoreMutations';
import configSavingMixin from '@/mixins/ConfigSaving';
export default {
name: 'KeyboardShortcutInfo',
mixins: [configSavingMixin],
data() {
return {
shouldHide: true, // False = show/ true = hide. Intuitive, eh?
timeDelay: 3000, // Short delay in ms before popup appears
timeDelay: 2000, // Short delay in ms before popup appears
popupContent: {
title: '⚠️ You\'re using a local config',
message: `This means that your settings are saved in this browser only,
and won't persist across devices.`,
messageContinued: `To ensure you don't loose your changes,
it's recommended to download a copy of your config, so you can restore it later.`,
hoverText: 'Press [Esc] to hide this warning',
},
};
},
methods: {
exportConfig() {
this.$modal.show(modalNames.EXPORT_CONFIG_MENU);
this.shouldHide = true;
},
saveConfig() {
const localConfig = this.$store.state.config;
this.writeConfigToDisk(localConfig);
this.shouldHide = true;
},
resetLocalConfig() {
const msg = `${this.$t('config.reset-config-msg-l1')} `
+ `${this.$t('config.reset-config-msg-l2')}\n\n${this.$t('config.reset-config-msg-l3')}`;
const isTheUserSure = confirm(msg); // eslint-disable-line no-alert, no-restricted-globals
if (isTheUserSure) {
localStorage.clear();
this.$toasted.show(this.$t('config.data-cleared-msg'));
this.$store.dispatch(StoreKeys.INITIALIZE_CONFIG);
this.shouldHide = true;
}
},
/**
* Returns true if the key exists in session storage, otherwise false
* And the !! just converts 'false' to false, as strings resolve to true
*/
shouldHideWelcomeMessage() {
return !!localStorage[localStorageKeys.HIDE_WELCOME_BANNER];
return !!localStorage[localStorageKeys.HIDE_INFO_NOTIFICATION];
},
/**
* Update session storage, so that it won't be shown again
@@ -38,7 +72,7 @@ export default {
*/
hideWelcomeHelper() {
this.shouldHide = true;
localStorage.setItem(localStorageKeys.HIDE_WELCOME_BANNER, true);
localStorage.setItem(localStorageKeys.HIDE_INFO_NOTIFICATION, true);
window.removeEventListener('keyup', this.keyPressEvent);
},
/* Passed to window function, to add/ remove event listener */
@@ -114,6 +148,23 @@ export default {
}
}
}
.action-buttons {
display: flex;
justify-content: space-around;
margin-top: 1em;
button {
padding: 0.2rem;
background: var(--welcome-popup-background);
color: var(--welcome-popup-text-color);
border: 1px solid var(--welcome-popup-text-color);
border-radius: var(--curve-factor);
transition: all 0.2s ease-in-out;
&:hover {
background: var(--welcome-popup-text-color);
color: var(--welcome-popup-background);
}
}
}
/* Animations, animations everywhere */
.slide-fade-enter-active {
transition: all 1s ease;

View File

@@ -95,7 +95,8 @@ export default {
},
/* If configured, launch specific app when hotkey pressed */
handleHotKey(key) {
const usersHotKeys = this.getCustomKeyShortcuts();
const sections = this.$store.getters.sections || [];
const usersHotKeys = this.getCustomKeyShortcuts(sections);
usersHotKeys.forEach((hotkey) => {
if (hotkey.hotkey === parseInt(key, 10)) {
if (hotkey.url) window.open(hotkey.url, '_blank');

View File

@@ -7,7 +7,7 @@
<div class="options-outer">
<div :class="`options-container ${!settingsVisible ? 'hide' : ''}`">
<ThemeSelector />
<LayoutSelector :displayLayout="displayLayout" />
<LayoutSelector :displayLayout="$store.getters.layout" />
<ItemSizeSelector :iconSize="iconSize" />
<ConfigLauncher />
<AuthButtons v-if="userState !== 0" :userType="userState" />

View File

@@ -8,7 +8,7 @@
:value="$store.getters.theme"
class="theme-dropdown"
:tabindex="-2"
@input="themeChanged"
@input="themeChangedInUI"
/>
</div>
<IconPalette
@@ -28,18 +28,13 @@
<script>
import CustomThemeMaker from '@/components/Settings/CustomThemeMaker';
import {
LoadExternalTheme,
ApplyLocalTheme,
ApplyCustomVariables,
} from '@/utils/ThemeHelper';
import Defaults, { localStorageKeys } from '@/utils/defaults';
import Keys from '@/utils/StoreMutations';
import ErrorHandler from '@/utils/ErrorHandler';
import IconPalette from '@/assets/interface-icons/config-color-palette.svg';
import ThemingMixin from '@/mixins/ThemingMixin';
export default {
name: 'ThemeSelector',
mixins: [ThemingMixin],
props: {
hidePallete: Boolean,
},
@@ -47,101 +42,16 @@ export default {
CustomThemeMaker,
IconPalette,
},
watch: {
/* When theme in VueX store changes, then update theme */
themeFromStore(newTheme) {
this.selectedTheme = newTheme;
this.updateTheme(newTheme);
},
},
data() {
return {
selectedTheme: '',
themeConfiguratorOpen: false, // Control the opening of theme config popup
themeHelper: new LoadExternalTheme(),
ApplyLocalTheme,
ApplyCustomVariables,
};
},
computed: {
/* Get appConfig from store */
appConfig() {
return this.$store.getters.appConfig;
},
/* Get users theme from store */
themeFromStore() {
return this.$store.getters.theme;
},
/* Combines all theme names (builtin and user defined) together */
themeNames: function themeNames() {
const externalThemeNames = Object.keys(this.externalThemes);
const specialThemes = ['custom'];
return [...this.extraThemeNames, ...externalThemeNames,
...Defaults.builtInThemes, ...specialThemes];
},
extraThemeNames() {
const userThemes = this.appConfig.cssThemes || [];
if (typeof userThemes === 'string') return [userThemes];
return userThemes;
},
/* Returns an array of links to external CSS from the Config */
externalThemes() {
const availibleThemes = {};
if (this.appConfig && this.appConfig.externalStyleSheet) {
const externals = this.appConfig.externalStyleSheet;
if (Array.isArray(externals)) {
externals.forEach((ext, i) => {
availibleThemes[`External Stylesheet ${i + 1}`] = ext;
});
} else if (typeof externals === 'string') {
availibleThemes['External Stylesheet'] = this.appConfig.externalStyleSheet;
} else {
ErrorHandler('External stylesheets must be of type string or string[]');
}
}
// availibleThemes.Default = '#';
return availibleThemes;
},
},
computed: {},
mounted() {
const initialTheme = this.getInitialTheme();
this.selectedTheme = initialTheme;
// Quicker loading, if the theme is local we can apply it immidiatley
if (this.isThemeLocal(initialTheme)) {
this.updateTheme(initialTheme);
}
// If it's an external stylesheet, then wait for promise to resolve
if (this.externalThemes && Object.entries(this.externalThemes).length > 0) {
const added = Object.keys(this.externalThemes).map(
name => this.themeHelper.add(name, this.externalThemes[name]),
);
// Once, added, then apply users initial theme
Promise.all(added).then(() => {
this.updateTheme(initialTheme);
});
}
this.initializeTheme();
},
methods: {
/* Called when dropdown changed
* Updates store, which will in turn update theme through watcher
*/
themeChanged() {
const pageId = this.$store.state.currentConfigInfo?.pageId || null;
this.$store.commit(Keys.SET_THEME, { theme: this.selectedTheme, pageId });
this.updateTheme(this.selectedTheme);
},
/* Returns the initial theme */
getInitialTheme() {
const localTheme = localStorage[localStorageKeys.THEME];
if (localTheme && localTheme !== 'undefined') return localTheme;
return this.appConfig.theme || Defaults.theme;
},
/* Determines if a given theme is local / not a custom user stylesheet */
isThemeLocal(themeToCheck) {
const localThemes = [...Defaults.builtInThemes, ...this.extraThemeNames];
return localThemes.includes(themeToCheck);
},
/* Opens the theme color configurator popup */
openThemeConfigurator() {
this.$store.commit(Keys.SET_MODAL_OPEN, true);
@@ -154,24 +64,6 @@ export default {
this.themeConfiguratorOpen = false;
}
},
/* Updates theme. Checks if the new theme is local or external,
and calls appropirate updating function. Updates local storage */
updateTheme(newTheme) {
if (newTheme === 'Default') {
this.resetToDefault();
this.themeHelper.theme = 'Default';
} else if (this.isThemeLocal(newTheme)) {
this.ApplyLocalTheme(newTheme);
} else {
this.themeHelper.theme = newTheme;
}
this.ApplyCustomVariables(newTheme);
// localStorage.setItem(localStorageKeys.THEME, newTheme);
},
/* Removes any applied themes */
resetToDefault() {
document.getElementsByTagName('html')[0].removeAttribute('data-theme');
},
},
};
</script>

View File

@@ -26,7 +26,7 @@ export default {
/* URL/ IP or hostname to the AdGuardHome instance, without trailing slash */
hostname() {
if (!this.options.hostname) this.error('You must specify the path to your AdGuard server');
return this.options.hostname;
return this.parseAsEnvVar(this.options.hostname);
},
showFullInfo() {
return this.options.showFullInfo;
@@ -39,7 +39,9 @@ export default {
},
authHeaders() {
if (this.options.username && this.options.password) {
const encoded = window.btoa(`${this.options.username}:${this.options.password}`);
const password = this.parseAsEnvVar(this.options.password);
const username = this.parseAsEnvVar(this.options.username);
const encoded = window.btoa(`${username}:${password}`);
return { Authorization: `Basic ${encoded}` };
}
return {};

View File

@@ -38,7 +38,7 @@ export default {
/* URL/ IP or hostname to the AdGuardHome instance, without trailing slash */
hostname() {
if (!this.options.hostname) this.error('You must specify the path to your AdGuard server');
return this.options.hostname;
return this.parseAsEnvVar(this.options.hostname);
},
showOnOffStatusOnly() {
return this.options.showOnOffStatusOnly;
@@ -48,7 +48,9 @@ export default {
},
authHeaders() {
if (this.options.username && this.options.password) {
const encoded = window.btoa(`${this.options.username}:${this.options.password}`);
const username = this.parseAsEnvVar(this.options.username);
const password = this.parseAsEnvVar(this.options.password);
const encoded = window.btoa(`${username}:${password}`);
return { Authorization: `Basic ${encoded}` };
}
return {};

View File

@@ -20,14 +20,16 @@ export default {
/* URL/ IP or hostname to the AdGuardHome instance, without trailing slash */
hostname() {
if (!this.options.hostname) this.error('You must specify the path to your AdGuard server');
return this.options.hostname;
return this.parseAsEnvVar(this.options.hostname);
},
endpoint() {
return `${this.hostname}/control/stats`;
},
authHeaders() {
if (this.options.username && this.options.password) {
const encoded = window.btoa(`${this.options.username}:${this.options.password}`);
const username = this.parseAsEnvVar(this.options.username);
const password = this.parseAsEnvVar(this.options.password);
const encoded = window.btoa(`${username}:${password}`);
return { Authorization: `Basic ${encoded}` };
}
return {};

View File

@@ -36,11 +36,13 @@ export default {
/* URL/ IP or hostname to the AdGuardHome instance, without trailing slash */
hostname() {
if (!this.options.hostname) this.error('You must specify the path to your AdGuard server');
return this.options.hostname;
return this.parseAsEnvVar(this.options.hostname);
},
authHeaders() {
if (this.options.username && this.options.password) {
const encoded = window.btoa(`${this.options.username}:${this.options.password}`);
const username = this.parseAsEnvVar(this.options.username);
const password = this.parseAsEnvVar(this.options.password);
const encoded = window.btoa(`${username}:${password}`);
return { Authorization: `Basic ${encoded}` };
}
return {};

View File

@@ -113,7 +113,7 @@ export default {
},
computed: {
hostname() {
return this.options.hostname || widgetApiEndpoints.anonAddy;
return this.parseAsEnvVar(this.options.hostname) || widgetApiEndpoints.anonAddy;
},
apiVersion() {
return this.options.apiVersion || 'v1';
@@ -132,7 +132,7 @@ export default {
},
apiKey() {
if (!this.options.apiKey) this.error('An apiKey is required');
return this.options.apiKey;
return this.parseAsEnvVar(this.options.apiKey);
},
hideMeta() {
return this.options.hideMeta;

View File

@@ -35,7 +35,7 @@ export default {
},
apiKey() {
if (!this.options.apiKey) this.error('Missing API Key');
return this.options.apiKey;
return this.parseAsEnvVar(this.options.apiKey);
},
endpoint() {
return `${widgetApiEndpoints.blacklistCheck}/${this.ipAddress}`;

View File

@@ -38,12 +38,12 @@ export default {
/* The username to fetch data from - REQUIRED */
username() {
if (!this.options.username) this.error('You must specify a username');
return this.options.username;
return this.parseAsEnvVar(this.options.username);
},
/* Optionally override hostname, if using a self-hosted instance */
hostname() {
if (this.options.hostname) return this.options.hostname;
return widgetApiEndpoints.codeStats;
return this.parseAsEnvVar(widgetApiEndpoints.codeStats);
},
hideMeta() {
return this.options.hideMeta || false;

View File

@@ -63,11 +63,11 @@ export default {
computed: {
apiKey() {
if (!this.options.apiKey) this.error('Missing API Key');
return this.options.apiKey;
return this.parseAsEnvVar(this.options.apiKey);
},
domain() {
if (!this.options.domain) this.error('Missing Domain Name Key');
return this.options.domain;
return this.parseAsEnvVar(this.options.domain);
},
endpoint() {
return `${widgetApiEndpoints.domainMonitor}/?domain=${this.domain}&r=whois&apikey=${this.apiKey}`;

View File

@@ -106,7 +106,7 @@ export default {
if (!this.options.apiKey) {
this.error('An API key is required, please see the docs for more info');
}
return this.options.apiKey;
return this.parseAsEnvVar(this.options.apiKey);
},
},
methods: {

View File

@@ -45,7 +45,7 @@ export default {
computed: {
/* The users API key for exchangerate-api.com */
apiKey() {
return this.options.apiKey;
return this.parseAsEnvVar(this.options.apiKey);
},
/* The currency to convert results into */
inputCurrency() {

View File

@@ -71,7 +71,7 @@ export default {
this.error('An API key must be supplied');
return '';
}
return usersChoice;
return this.parseAsEnvVar(usersChoice);
},
/* The direction of flights: Arrival, Departure or Both */
direction() {

View File

@@ -1,9 +1,15 @@
<template>
<div class="readme-stats">
<img class="stats-card" v-if="!hideProfileCard" :src="profileCard" alt="Profile Card" />
<img class="stats-card" v-if="!hideLanguagesCard" :src="topLanguagesCard" alt="Languages" />
<a v-if="!hideProfileCard" :href="profileCardLink" target="_blank">
<img class="stats-card" :src="profileCard" alt="Profile Card" />
</a>
<a v-if="!hideLanguagesCard" :href="profileCardLink" target="_blank">
<img class="stats-card" :src="topLanguagesCard" alt="Languages" />
</a>
<template v-if="repos">
<img class="stats-card" v-for="(repo, i) in repoCards" :key="i" :src="repo" :alt="repo" />
<a v-for="(repo, i) in repoCards" :key="i" :href="repo.cardHref" target="_blank">
<img class="stats-card" :src="repo.cardSrc" :alt="repo" />
</a>
</template>
</div>
</template>
@@ -61,6 +67,9 @@ export default {
profileCard() {
return `${widgetApiEndpoints.readMeStats}?username=${this.username}${this.cardConfig}`;
},
profileCardLink() {
return `https://github.com/${this.username}`;
},
topLanguagesCard() {
return `${widgetApiEndpoints.readMeStats}/top-langs/?username=${this.username}`
+ `${this.cardConfig}&langs_count=12`;
@@ -70,8 +79,11 @@ export default {
this.repos.forEach((repo) => {
const username = repo.split('/')[0];
const repoName = repo.split('/')[1];
cards.push(`${widgetApiEndpoints.readMeStats}/pin/?username=${username}&repo=${repoName}`
+ `${this.cardConfig}&show_owner=true`);
cards.push({
cardSrc: `${widgetApiEndpoints.readMeStats}/pin/?username=${username}`
+ `&repo=${repoName}${this.cardConfig}&show_owner=true`,
cardHref: `https://github.com/${username}/${repoName}`,
});
});
return cards;
},

View File

@@ -112,6 +112,7 @@ export default {
cursor: pointer;
font-size: 0.9rem;
text-align: center;
width: 100%;
width: fit-content;
margin: 0.25rem auto;
padding: 0.1rem 0.25rem;

View File

@@ -0,0 +1,150 @@
<template>
<div class="glances-cpu-gauge-wrapper">
<GaugeChart class="gl-speedometer" :value="gaugeValue"
:baseColor="baseColor" :shadowColor="shadowColor" :gaugeColor="gaugeColor"
:startAngle="startAngle" :endAngle="endAngle" :innerRadius="innerRadius"
:separatorThickness="separatorThickness">
<p class="percentage">{{ gaugeValue }}%</p>
</GaugeChart>
<p class="show-more-btn" @click="toggleMoreInfo">
{{ showMoreInfo ? $t('widgets.general.show-less') : $t('widgets.general.cpu-details') }}
</p>
<div class="more-info" v-if="moreInfo && showMoreInfo">
<div class="more-info-row" v-for="(info, key) in moreInfo" :key="key">
<p class="label">{{ info.label }}</p>
<p class="value">{{ info.value }}</p>
</div>
</div>
</div>
</template>
<script>
import WidgetMixin from '@/mixins/WidgetMixin';
import GlancesMixin from '@/mixins/GlancesMixin';
import GaugeChart from '@/components/Charts/Gauge';
import { capitalize } from '@/utils/MiscHelpers';
export default {
mixins: [WidgetMixin, GlancesMixin],
components: {
GaugeChart,
},
data() {
return {
gaugeValue: 0,
baseColor: '#101010ED',
shadowColor: '#00000000',
gaugeColor: [
{ offset: 0, color: '#20e253' },
{ offset: 35, color: '#f6f000' },
{ offset: 65, color: '#fca016' },
{ offset: 90, color: '#f80363' },
],
showMoreInfo: false,
moreInfo: null,
startAngle: -135,
endAngle: 135,
innerRadius: 80,
separatorThickness: 0,
};
},
computed: {
endpoint() {
return this.makeGlancesUrl('cpu');
},
},
methods: {
processData(cpuData) {
this.gaugeValue = cpuData.total;
const moreInfo = [];
const ignore = ['total', 'cpucore', 'time_since_update',
'interrupts', 'soft_interrupts', 'ctx_switches', 'syscalls'];
Object.keys(cpuData).forEach((key) => {
if (!ignore.includes(key) && cpuData[key]) {
moreInfo.push({ label: capitalize(key), value: `${cpuData[key].toFixed(1)}%` });
}
});
this.moreInfo = moreInfo;
},
toggleMoreInfo() {
this.showMoreInfo = !this.showMoreInfo;
},
},
created() {
this.overrideUpdateInterval = 2;
},
};
</script>
<style scoped lang="scss">
.glances-cpu-gauge-wrapper {
max-width: 15rem;
margin: 0rem auto;
p.percentage {
color: var(--widget-text-color);
text-align: center;
position: absolute;
font-size: 1.3rem;
margin: 3.5rem 0;
width: 100%;
bottom: 0;
}
.more-info {
background: var(--widget-accent-color);
border-radius: var(--curve-factor);
padding: 0.25rem 0.5rem;
margin: 0.5rem auto;
.more-info-row {
display: flex;
justify-content: space-between;
align-items: center;
p.label, p.value {
color: var(--widget-text-color);
margin: 0.25rem 0;
}
p.value {
font-family: var(--font-monospace);
}
&:not(:last-child) {
border-bottom: 1px dashed var(--widget-text-color);
}
}
}
p.show-more-btn {
cursor: pointer;
font-size: 0.9rem;
text-align: center;
width: fit-content;
margin: -1.1rem auto 0 auto;
padding: 0.1rem 0.25rem;
border: 1px solid transparent;
color: var(--widget-text-color);
opacity: var(--dimming-factor);
border-radius: var(--curve-factor);
&:hover {
border: 1px solid var(--widget-text-color);
}
&:focus,
&:active {
background: var(--widget-text-color);
color: var(--widget-background-color);
}
}
}
</style>
<style>
/* global override for the Guage tick lines */
.gl-speedometer svg line {
stroke: var(--widget-text-color);
opacity: .3;
}
</style>

View File

@@ -0,0 +1,150 @@
<template>
<div class="glances-cpu-gauge-wrapper">
<GaugeChart class="gl-speedometer" :value="gaugeValue"
:baseColor="baseColor" :shadowColor="shadowColor" :gaugeColor="gaugeColor"
:startAngle="startAngle" :endAngle="endAngle" :innerRadius="innerRadius"
:separatorThickness="separatorThickness">
<p class="percentage">{{ gaugeValue }}%</p>
</GaugeChart>
<p class="show-more-btn" @click="toggleMoreInfo">
{{ showMoreInfo ? $t('widgets.general.show-less') : $t('widgets.general.mem-details') }}
</p>
<div class="more-info" v-if="moreInfo && showMoreInfo">
<div class="more-info-row" v-for="(info, key) in moreInfo" :key="key">
<p class="label">{{ info.label }}</p>
<p class="value">{{ info.value }}</p>
</div>
</div>
</div>
</template>
<script>
import WidgetMixin from '@/mixins/WidgetMixin';
import GlancesMixin from '@/mixins/GlancesMixin';
import GaugeChart from '@/components/Charts/Gauge';
import { capitalize, convertBytes } from '@/utils/MiscHelpers';
export default {
mixins: [WidgetMixin, GlancesMixin],
components: {
GaugeChart,
},
data() {
return {
gaugeValue: 0,
baseColor: '#101010ED',
shadowColor: '#00000000',
gaugeColor: [
{ offset: 0, color: '#20e253' },
{ offset: 35, color: '#f6f000' },
{ offset: 65, color: '#fca016' },
{ offset: 90, color: '#f80363' },
],
showMoreInfo: false,
moreInfo: null,
startAngle: -135,
endAngle: 135,
innerRadius: 80,
separatorThickness: 0,
};
},
computed: {
endpoint() {
return this.makeGlancesUrl('mem');
},
},
methods: {
processData(memData) {
this.gaugeValue = memData.percent;
const moreInfo = [];
const ignore = ['percent'];
Object.keys(memData).forEach((key) => {
if (!ignore.includes(key) && memData[key]) {
moreInfo.push({ label: capitalize(key), value: convertBytes(memData[key]) });
}
});
this.moreInfo = moreInfo;
},
toggleMoreInfo() {
this.showMoreInfo = !this.showMoreInfo;
},
},
created() {
this.overrideUpdateInterval = 2;
},
};
</script>
<style scoped lang="scss">
.glances-cpu-gauge-wrapper {
max-width: 15rem;
margin: 0rem auto;
p.percentage {
color: var(--widget-text-color);
text-align: center;
position: absolute;
font-size: 1.3rem;
margin: 3.5rem 0;
width: 100%;
bottom: 0;
}
.more-info {
background: var(--widget-accent-color);
border-radius: var(--curve-factor);
padding: 0.25rem 0.5rem;
margin: 0.5rem auto;
.more-info-row {
display: flex;
justify-content: space-between;
align-items: center;
p.label,
p.value {
color: var(--widget-text-color);
margin: 0.25rem 0;
}
p.value {
font-family: var(--font-monospace);
}
&:not(:last-child) {
border-bottom: 1px dashed var(--widget-text-color);
}
}
}
p.show-more-btn {
cursor: pointer;
font-size: 0.9rem;
text-align: center;
width: fit-content;
margin: -1.1rem auto 0 auto;
padding: 0.1rem 0.25rem;
border: 1px solid transparent;
color: var(--widget-text-color);
opacity: var(--dimming-factor);
border-radius: var(--curve-factor);
&:hover {
border: 1px solid var(--widget-text-color);
}
&:focus,
&:active {
background: var(--widget-text-color);
color: var(--widget-background-color);
}
}
}
</style>
<style>
/* global override for the Guage tick lines */
.gl-speedometer svg line {
stroke: var(--widget-text-color);
opacity: .3;
}
</style>

View File

@@ -58,7 +58,7 @@ export default {
},
hostname() {
if (!this.options.hostname) this.error('`hostname` is required');
return this.options.hostname;
return this.parseAsEnvVar(this.options.hostname);
},
},
methods: {

View File

@@ -56,7 +56,7 @@ export default {
this.error('An API key is required, please see the docs for more info');
}
if (typeof this.options.apiKey === 'string') {
return [this.options.apiKey];
return [this.parseAsEnvVar(this.options.apiKey)];
}
return this.options.apiKey;
},

View File

@@ -74,6 +74,7 @@ export default {
this.jokeLine2 = data.delivery;
} else if (this.jokeType === 'single') {
this.jokeLine1 = data.joke;
this.jokeLine2 = null;
}
},
},

View File

@@ -30,11 +30,11 @@ export default {
computed: {
endpoint() {
if (!this.options.host) this.error('linkgding Host is required');
return `${this.options.host}/api/bookmarks`;
return `${this.parseAsEnvVar(this.options.host)}/api/bookmarks`;
},
apiKey() {
if (!this.options.apiKey) this.error('linkgding apiKey is required');
return this.options.apiKey;
return this.parseAsEnvVar(this.options.apiKey);
},
filtertags() {
return this.options.tags;

View File

@@ -29,7 +29,7 @@ export default {
computed: {
apiKey() {
if (!this.options.apiKey) this.error('An API key is required, see docs for more info');
return this.options.apiKey;
return this.parseAsEnvVar(this.options.apiKey);
},
country() {
return this.options.country ? `&country=${this.options.country}` : '';

View File

@@ -22,7 +22,7 @@
</span>
<span v-if="canDeleteNotification('delete')">
<a @click="deleteNotification(notification.notification_id)"
class="action secondary">{{ tt('delete-notification') }}</a>
class="action secondary">{{ tt('delete-notification') }}</a>
</span>
</p>
</div>

View File

@@ -44,7 +44,7 @@
<em v-html="formatNumber(shares.num_shares)"></em>
<strong>{{ tt('local') }}</strong> <small> {{ tt('and') }}</small>
<em v-html="formatNumber(shares.num_fed_shares_sent
+ shares.num_fed_shares_received)"></em>
+ shares.num_fed_shares_received)"></em>
<strong>
{{ tt('federated-shares') }}
</strong>

View File

@@ -36,13 +36,14 @@ export default {
computed: {
/* Let user select which comic to display: random, latest or a specific number */
hostname() {
const usersChoice = this.options.hostname;
const usersChoice = this.parseAsEnvVar(this.options.hostname);
if (!usersChoice) this.error('You must specify the hostname for your Pi-Hole server');
return usersChoice || 'http://pi.hole';
},
apiKey() {
if (!this.options.apiKey) this.error('API Key is required, please see the docs');
return this.options.apiKey;
const usersChoice = this.parseAsEnvVar(this.options.apiKey);
if (!usersChoice) this.error('API Key is required, please see the docs');
return usersChoice;
},
endpoint() {
return `${this.hostname}/admin/api.php?summary&auth=${this.apiKey}`;

View File

@@ -34,22 +34,22 @@ export default {
computed: {
clusterUrl() {
if (!this.options.cluster_url) this.error('The cluster URL is required.');
return this.options.cluster_url || '';
return this.parseAsEnvVar(this.options.cluster_url) || '';
},
userName() {
if (!this.options.user_name) this.error('The user name is required.');
return this.options.user_name || '';
return this.parseAsEnvVar(this.options.user_name) || '';
},
tokenName() {
if (!this.options.token_name) this.error('The token name is required.');
return this.options.token_name || '';
return this.parseAsEnvVar(this.options.token_name) || '';
},
tokenUuid() {
if (!this.options.token_uuid) this.error('The token uuid is required.');
return this.options.token_uuid || '';
return this.parseAsEnvVar(this.options.token_uuid) || '';
},
node() {
return this.options.node || '';
return this.parseAsEnvVar(this.options.node) || '';
},
nodeData() {
return this.options.node_data || false;
@@ -94,7 +94,7 @@ export default {
}
},
processData(data) {
this.data = data.data.sort((a, b) => a.vmid > b.vmid);
this.data = data.data.sort((a, b) => Number(a.vmid) > Number(b.vmid));
if (this.hideTemplates) {
this.data = this.data.filter(item => item.template !== 1);
}

View File

@@ -90,7 +90,8 @@ export default {
const formatType = (ht) => capitalize(ht.replaceAll('_', ' '));
holidays.forEach((holiday) => {
results.push({
name: holiday.name.filter(p => p.lang == this.options.lang)[0].text || holiday.name[0].text,
name: holiday.name
.filter(p => p.lang === this.options.lang)[0].text || holiday.name[0].text,
date: makeDate(holiday.date),
type: formatType(holiday.holidayType),
observed: holiday.observedOn ? makeDate(holiday.observedOn) : '',

View File

@@ -35,7 +35,7 @@ export default {
},
provider() {
// Can be either `ip-api`, `ipapi.co` or `ipgeolocation`
return this.options.provider || 'ipapi.co';
return this.parseAsEnvVar(this.options.provider) || 'ipapi.co';
},
},
data() {

View File

@@ -51,7 +51,7 @@ export default {
return this.options.rssUrl || '';
},
apiKey() {
return this.options.apiKey;
return this.parseAsEnvVar(this.options.apiKey);
},
parseLocally() {
return this.options.parseLocally;

View File

@@ -93,7 +93,7 @@ export default {
return this.options.leagueId;
},
apiKey() {
return this.options.apiKey || '50130162';
return this.parseAsEnvVar(this.options.apiKey) || '50130162';
},
limit() {
return this.options.limit || 20;

View File

@@ -29,7 +29,7 @@ export default {
},
/* The users API key for AlphaVantage */
apiKey() {
return this.options.apiKey;
return this.parseAsEnvVar(this.options.apiKey);
},
/* The formatted GET request API endpoint to fetch stock data from */
endpoint() {

View File

@@ -45,15 +45,15 @@ export default {
computed: {
hostname() {
if (!this.options.hostname) this.error('A hostname is required');
return this.options.hostname;
return this.parseAsEnvVar(this.options.hostname);
},
username() {
if (!this.options.username) this.error('A username is required');
return this.options.username;
return this.parseAsEnvVar(this.options.username);
},
password() {
if (!this.options.password) this.error('A password is required');
return this.options.password;
return this.parseAsEnvVar(this.options.password);
},
endpointLogin() {
return `${this.hostname}/webapi/auth.cgi?api=SYNO.API.Auth&version=3&method=login&account=${this.username}&passwd=${this.password}&session=DownloadStation&format=sid`;

View File

@@ -0,0 +1,238 @@
<template>
<div>
<template v-if="monitors">
<div v-for="(monitor, index) in monitors" :key="index" class="item-wrapper">
<div class="item monitor-row">
<div class="title-title"><span class="text">{{ monitor.name }}</span></div>
<div class="monitors-container">
<div class="status-container">
<span class="status-pill" :class="[monitor.statusClass]">{{ monitor.status }}</span>
</div>
<div class="status-container">
<span class="response-time">{{ monitor.responseTime }}ms</span>
</div>
</div>
</div>
</div>
</template>
<template v-if="errorMessage">
<div class="error-message">
<span class="text">{{ errorMessage }}</span>
</div>
</template>
</div>
</template>
<script>
/**
* A simple example which you can use as a template for creating your own widget.
* Takes two optional parameters (`text` and `count`), and fetches a list of images
* from dummyapis.com, then renders the results to the UI.
*/
import WidgetMixin from '@/mixins/WidgetMixin';
export default {
mixins: [WidgetMixin],
components: {},
data() {
return {
monitors: null,
errorMessage: null,
errorMessageConstants: {
missingApiKey: 'No API key set',
missingUrl: 'No URL set',
},
};
},
mounted() {
this.fetchData();
},
computed: {
/* Get API key for access to instance */
apiKey() {
return this.parseAsEnvVar(this.options.apiKey);
},
/* Get instance URL */
url() {
return this.parseAsEnvVar(this.options.url);
},
/* Create authorisation header for the instance from the apiKey */
authHeaders() {
if (!this.options.apiKey) {
return {};
}
const encoded = window.btoa(`:${this.options.apiKey}`);
return { Authorization: `Basic ${encoded}` };
},
},
methods: {
/* The update() method extends mixin, used to update the data.
* It's called by parent component, when the user presses update
*/
update() {
this.startLoading();
this.fetchData();
},
/* Make the data request to the computed API endpoint */
fetchData() {
const { authHeaders, url } = this;
if (!this.optionsValid({ authHeaders, url })) {
return;
}
this.makeRequest(url, authHeaders)
.then(this.processData);
},
/* Convert API response data into a format to be consumed by the UI */
processData(response) {
const monitorRows = this.getMonitorRows(response);
const monitors = new Map();
for (let index = 0; index < monitorRows.length; index += 1) {
const row = monitorRows[index];
this.processRow(row, monitors);
}
this.monitors = Array.from(monitors.values());
},
getMonitorRows(response) {
return response.split('\n').filter(row => row.startsWith('monitor_'));
},
processRow(row, monitors) {
const dataType = this.getRowDataType(row);
const monitorName = this.getRowMonitorName(row);
if (!monitors.has(monitorName)) {
monitors.set(monitorName, { name: monitorName });
}
const monitor = monitors.get(monitorName);
const value = this.getRowValue(row);
const updated = this.setMonitorValue(dataType, monitor, value);
monitors.set(monitorName, updated);
},
setMonitorValue(key, monitor, value) {
const copy = { ...monitor };
switch (key) {
case 'monitor_cert_days_remaining': {
copy.certDaysRemaining = value;
break;
}
case 'monitor_cert_is_valid': {
copy.certValid = value;
break;
}
case 'monitor_response_time': {
copy.responseTime = value;
break;
}
case 'monitor_status': {
copy.status = value === '1' ? 'Up' : 'Down';
copy.statusClass = copy.status.toLowerCase();
break;
}
default:
break;
}
return copy;
},
getRowValue(row) {
return this.getValueWithRegex(row, /\b\d+\b$/);
},
getRowMonitorName(row) {
return this.getValueWithRegex(row, /monitor_name="([^"]+)"/);
},
getRowDataType(row) {
return this.getValueWithRegex(row, /^(.*?)\{/);
},
getValueWithRegex(string, regex) {
const result = string.match(regex);
const isArray = Array.isArray(result);
if (!isArray) {
return result;
}
return result.length > 1 ? result[1] : result[0];
},
optionsValid({ url, authHeaders }) {
const errors = [];
if (url === undefined) {
errors.push(this.errorMessageConstants.missingUrl);
}
if (authHeaders === undefined) {
errors.push(this.errorMessageConstants.missingApiKey);
}
if (errors.length === 0) { return true; }
this.errorMessage = errors.join('\n');
return false;
},
},
};
</script>
<style scoped lang="scss">
.status-pill {
border-radius: 50em;
box-sizing: border-box;
font-size: 0.75em;
display: inline-block;
font-weight: 700;
text-align: center;
white-space: nowrap;
vertical-align: baseline;
padding: .35em .65em;
margin: 1em 0.5em;
min-width: 64px;
&.up {
background-color: rgb(92, 221, 139);
color: black;
}
&.down {
background-color: rgb(220, 53, 69);
color: white;
}
}
div.item.monitor-row:hover {
background-color: var(--item-background);
color: var(--current-color);
opacity: 1;
div.title-title>span.text {
color: var(--current-color);
}
}
.monitors-container {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-around;
width: 50%;
}
.monitor-row {
display: flex;
justify-content: space-between;
padding: 0.35em 0.5em;
align-items: center;
}
.title-title {
font-weight: bold;
}
</style>

View File

@@ -53,7 +53,7 @@ export default {
},
address() {
if (!this.options.address) this.error('You must specify a public address');
return this.options.address;
return this.parseAsEnvVar(this.options.address);
},
network() {
return this.options.network || 'main';

View File

@@ -46,8 +46,12 @@ export default {
return this.options.units || 'metric';
},
endpoint() {
const { apiKey, city } = this.options;
return `${widgetApiEndpoints.weather}?q=${city}&appid=${apiKey}&units=${this.units}`;
const apiKey = this.parseAsEnvVar(this.options.apiKey);
const { city, lat, lon } = this.options;
const params = (lat && lon)
? `lat=${lat}&lon=${lon}&appid=${apiKey}&units=${this.units}`
: `q=${city}&appid=${apiKey}&units=${this.units}`;
return `${widgetApiEndpoints.weather}?${params}`;
},
tempDisplayUnits() {
switch (this.units) {
@@ -106,7 +110,11 @@ export default {
checkProps() {
const ops = this.options;
if (!ops.apiKey) this.error('Missing API key for OpenWeatherMap');
if (!ops.city) this.error('A city name is required to fetch weather');
if ((!ops.lat || !ops.lon) && !ops.city) {
this.error('A city name or lat + lon is required to fetch weather');
}
if (ops.units && ops.units !== 'metric' && ops.units !== 'imperial') {
this.error('Invalid units specified, must be either \'metric\' or \'imperial\'');
}

View File

@@ -67,12 +67,14 @@ const COMPAT = {
'gl-alerts': 'GlAlerts',
'gl-current-cores': 'GlCpuCores',
'gl-current-cpu': 'GlCpuGauge',
'gl-cpu-speedometer': 'GlCpuSpeedometer',
'gl-cpu-history': 'GlCpuHistory',
'gl-disk-io': 'GlDiskIo',
'gl-disk-space': 'GlDiskSpace',
'gl-ip-address': 'GlIpAddress',
'gl-load-history': 'GlLoadHistory',
'gl-current-mem': 'GlMemGauge',
'gl-mem-speedometer': 'GlMemSpeedometer',
'gl-mem-history': 'GlMemHistory',
'gl-network-interfaces': 'GlNetworkInterfaces',
'gl-network-traffic': 'GlNetworkTraffic',
@@ -113,6 +115,7 @@ const COMPAT = {
'synology-download': 'SynologyDownload',
'system-info': 'SystemInfo',
'tfl-status': 'TflStatus',
'uptime-kuma': 'UptimeKuma',
'wallet-balance': 'WalletBalance',
weather: 'Weather',
'weather-forecast': 'WeatherForecast',
@@ -203,14 +206,16 @@ export default {
</script>
<style scoped lang="scss">
@import '@/styles/media-queries.scss';
@import "@/styles/media-queries.scss";
.widget-base {
position: relative;
padding: 0.75rem 0.5rem 0.5rem 0.5rem;
background: var(--widget-base-background);
box-shadow: var(--widget-base-shadow, none);
// Refresh and full-page action buttons
button.action-btn {
button.action-btn {
height: 1rem;
min-width: auto;
width: 1.75rem;
@@ -221,21 +226,26 @@ export default {
border: none;
opacity: var(--dimming-factor);
color: var(--widget-text-color);
&:hover {
opacity: 1;
color: var(--widget-background-color);
}
&.update-btn {
right: -0.25rem;
}
&.open-btn {
right: 1.75rem;
}
}
// Optional widget label
.widget-label {
color: var(--widget-text-color);
}
// Actual widget container
.widget-wrap {
&.has-error {
@@ -243,9 +253,11 @@ export default {
opacity: 0.5;
border-radius: var(--curve-factor);
background: #ffff0040;
&:hover { background: none; }
}
}
// Error message output
.widget-error {
p.error-msg {
@@ -254,12 +266,14 @@ export default {
font-size: 1rem;
margin: 0 auto 0.5rem auto;
}
p.error-output {
font-family: var(--font-monospace);
color: var(--widget-text-color);
font-size: 0.85rem;
margin: 0.5rem auto;
}
p.retry-link {
cursor: pointer;
text-decoration: underline;
@@ -268,14 +282,17 @@ export default {
margin: 0;
}
}
// Loading spinner
.loading {
margin: 0.2rem auto;
text-align: center;
svg.loader {
width: 100px;
}
}
// Hide widget contents while loading
&.is-loading {
.widget-wrap {
@@ -283,5 +300,4 @@ export default {
}
}
}
</style>

View File

@@ -8,7 +8,6 @@
</template>
<script>
import axios from 'axios';
import WidgetMixin from '@/mixins/WidgetMixin';
import { widgetApiEndpoints } from '@/utils/defaults';
@@ -41,11 +40,17 @@ export default {
methods: {
/* Make GET request to CoinGecko API endpoint */
fetchData() {
axios.get(this.endpoint)
.then((response) => {
this.processData(response.data);
fetch(this.endpoint)
.then(response => {
if (!response.ok) {
this.error('Network response was not ok');
}
return response.json();
})
.catch((dataFetchError) => {
.then(data => {
this.processData(data);
})
.catch(dataFetchError => {
this.error('Unable to fetch data', dataFetchError);
})
.finally(() => {
@@ -71,7 +76,7 @@ export default {
<style scoped lang="scss">
.xkcd-wrapper {
.xkcd-title {
.xkcd-title {
font-size: 1.2rem;
margin: 0.25rem auto;
color: var(--widget-text-color);

View File

@@ -2,7 +2,7 @@
* A Vue directive to trigger an event when the user
* clicks anywhere other than the specified elements
* Used to close context menus popup modals and tips
* Dashy: Licensed under MIT - (C) Alicia Sykes 2022
* Dashy: Licensed under MIT - (C) Alicia Sykes 2024
*/
const instances = []; // List of click event instances

View File

@@ -2,7 +2,7 @@
* A Vue directive to call event when element is long-pressed
* Used to open context menus on touch-enabled devices
* Inspired by: FeliciousX/vue-directive-long-press
* Dashy: Licensed under MIT - (C) Alicia Sykes 2022
* Dashy: Licensed under MIT - (C) Alicia Sykes 2024
*/
const LONG_PRESS_DEFAULT_DELAY = 750;

View File

@@ -13,14 +13,17 @@ import TreeView from 'vue-json-tree-view';
// Import base Dashy components and utils
import Dashy from '@/App.vue'; // Main Dashy Vue app
import router from '@/router'; // Router, for navigation
import store from '@/store'; // Store, for local state management
import router from '@/router'; // Router, for navigation
import serviceWorker from '@/utils/InitServiceWorker'; // Service worker initialization
import { messages } from '@/utils/languages'; // Language texts
import ErrorReporting from '@/utils/ErrorReporting'; // Error reporting initializer (off)
import clickOutside from '@/directives/ClickOutside'; // Directive for closing popups, modals, etc
import { toastedOptions, tooltipOptions, language as defaultLanguage } from '@/utils/defaults';
import { initKeycloakAuth, isKeycloakEnabled } from '@/utils/KeycloakAuth';
import { initHeaderAuth, isHeaderAuthEnabled } from '@/utils/HeaderAuth';
import Keys from '@/utils/StoreMutations';
import ErrorHandler from '@/utils/ErrorHandler';
// Initialize global Vue components
Vue.use(VueI18n);
@@ -58,11 +61,20 @@ const mount = () => new Vue({
store, router, render, i18n,
}).$mount('#app');
// If Keycloak not enabled, then proceed straight to the app
if (!isKeycloakEnabled()) {
mount();
} else { // Keycloak is enabled, redirect to KC login page
initKeycloakAuth()
.then(() => mount())
.catch(() => window.location.reload());
}
store.dispatch(Keys.INITIALIZE_CONFIG).then(() => {
if (isKeycloakEnabled()) { // If Keycloak is enabled, initialize auth
initKeycloakAuth()
.then(() => mount())
.catch((e) => {
ErrorHandler('Failed to authenticate with Keycloak', e);
});
} else if (isHeaderAuthEnabled()) { // If header auth is enabled, initialize auth
initHeaderAuth()
.then(() => mount())
.catch((e) => {
ErrorHandler('Failed to authenticate with server', e);
});
} else { // If no third-party auth, just mount the app as normal
mount();
}
});

View File

@@ -21,12 +21,22 @@ export default {
return;
}
// 1. Get the config, and strip appConfig if is sub-page
const isSubPag = !!this.$store.state.currentConfigInfo;
const isSubPag = !!this.$store.state.currentConfigInfo.confId;
const jsonConfig = config;
if (isSubPag) delete jsonConfig.appConfig;
jsonConfig.sections = jsonConfig.sections.map(({ filteredItems, ...section }) => section);
// If a sub-config, then remove appConfig, and check path isn't an external URL
if (isSubPag) {
delete jsonConfig.appConfig;
if (this.$store.state.currentConfigInfo.confPath.includes('http')) {
ErrorHandler('Cannot save to an external URL');
return;
}
}
// 2. Convert JSON into YAML
const yamlOptions = {};
const yaml = jsYaml.dump(jsonConfig, yamlOptions);
const strjsonConfig = JSON.stringify(jsonConfig);
const jsonObj = JSON.parse(strjsonConfig);
const yaml = jsYaml.dump(jsonObj, yamlOptions);
// 3. Prepare the request
const baseUrl = process.env.VUE_APP_DOMAIN || window.location.origin;
const endpoint = `${baseUrl}${serviceEndpoints.save}`;
@@ -63,20 +73,39 @@ export default {
ErrorHandler('Unable to save changes locally, this feature has been disabled');
return;
}
localStorage.setItem(localStorageKeys.CONF_SECTIONS, JSON.stringify(config.sections));
localStorage.setItem(localStorageKeys.PAGE_INFO, JSON.stringify(config.pageInfo));
localStorage.setItem(localStorageKeys.APP_CONFIG, JSON.stringify(config.appConfig));
const isSubPag = !!this.$store.state.currentConfigInfo.confId;
if (isSubPag) { // Save for sub-page only
const configId = this.$store.state.currentConfigInfo.confId;
const localStorageKeySections = `${localStorageKeys.CONF_SECTIONS}-${configId}`;
const localStorageKeyPageInfo = `${localStorageKeys.PAGE_INFO}-${configId}`;
localStorage.setItem(localStorageKeySections, JSON.stringify(config.sections));
localStorage.setItem(localStorageKeyPageInfo, JSON.stringify(config.pageInfo));
} else { // Or save to main config
localStorage.setItem(localStorageKeys.CONF_SECTIONS, JSON.stringify(config.sections));
localStorage.setItem(localStorageKeys.PAGE_INFO, JSON.stringify(config.pageInfo));
localStorage.setItem(localStorageKeys.APP_CONFIG, JSON.stringify(config.appConfig));
}
if (config.appConfig.theme) {
localStorage.setItem(localStorageKeys.THEME, config.appConfig.theme);
}
InfoHandler('Config has succesfully been saved in browser storage', 'Config Update');
InfoHandler('Config has successfully been saved in browser storage', 'Config Update');
this.showToast(this.$t('config-editor.success-msg-local'), true);
this.$store.commit(StoreKeys.SET_EDIT_MODE, false);
},
carefullyClearLocalStorage() {
// Delete the main keys
localStorage.removeItem(localStorageKeys.PAGE_INFO);
localStorage.removeItem(localStorageKeys.APP_CONFIG);
localStorage.removeItem(localStorageKeys.CONF_SECTIONS);
// Then, if we've got any sub-pages, delete those too
(this.$store.getters.pages || []).forEach((page) => {
const localStorageKeySections = `${localStorageKeys.CONF_SECTIONS}-${page.id}`;
const localStorageKeyPageInfo = `${localStorageKeys.PAGE_INFO}-${page.id}`;
localStorage.removeItem(localStorageKeySections);
localStorage.removeItem(localStorageKeyPageInfo);
});
},
},
};

View File

@@ -6,7 +6,6 @@ import Defaults, { localStorageKeys, iconCdns } from '@/utils/defaults';
import Keys from '@/utils/StoreMutations';
import { searchTiles } from '@/utils/Search';
import { checkItemVisibility } from '@/utils/CheckItemVisibility';
import { GetTheme, ApplyLocalTheme, ApplyCustomVariables } from '@/utils/ThemeHelper';
const HomeMixin = {
props: {
@@ -29,29 +28,40 @@ const HomeMixin = {
return this.$store.state.modalOpen;
},
pageId() {
return (this.subPageInfo && this.subPageInfo.pageId) ? this.subPageInfo.pageId : 'home';
return this.$store.state.currentConfigInfo?.confId || 'home';
},
},
data: () => ({
searchValue: '',
}),
async mounted() {
await this.getConfigForRoute();
},
watch: {
async $route() {
await this.getConfigForRoute();
this.setTheme();
this.loadUpConfig();
},
pageInfo: {
handler(newPageInfo) {
if (newPageInfo && newPageInfo.title) {
document.title = newPageInfo.title;
}
},
immediate: true,
},
},
async created() {
this.loadUpConfig();
},
methods: {
async getConfigForRoute() {
this.$store.commit(Keys.SET_CURRENT_SUB_PAGE, this.subPageInfo);
if (this.subPageInfo && this.subPageInfo.confPath) { // Get config for sub-page
await this.$store.dispatch(Keys.INITIALIZE_MULTI_PAGE_CONFIG, this.subPageInfo.confPath);
} else { // Otherwise, use main config
this.$store.commit(Keys.USE_MAIN_CONFIG);
}
/* When page loaded / sub-page changed, initiate config fetch */
async loadUpConfig() {
const subPage = this.determineConfigFile();
await this.$store.dispatch(Keys.INITIALIZE_CONFIG, subPage);
},
/* Based on the current route, get which config to display, null will use default */
determineConfigFile() {
const pagePath = this.$router.currentRoute.path;
const isSubPage = new RegExp((/(home|workspace|minimal)\/[a-zA-Z0-9-]+/g)).test(pagePath);
const subPageName = isSubPage ? pagePath.split('/').pop() : null;
return subPageName;
},
/* TEMPORARY: If on sub-page, check if custom theme is set and return it */
getSubPageTheme() {
@@ -63,9 +73,9 @@ const HomeMixin = {
}
},
setTheme() {
const theme = this.getSubPageTheme() || GetTheme();
ApplyLocalTheme(theme);
ApplyCustomVariables(theme);
// const theme = this.getSubPageTheme() || GetTheme();
// ApplyLocalTheme(theme);
// ApplyCustomVariables(theme);
},
updateModalVisibility(modalState) {
this.$store.commit('SET_MODAL_OPEN', modalState);
@@ -74,6 +84,14 @@ const HomeMixin = {
searching(searchValue) {
this.searchValue = searchValue || '';
},
/* Returns a unique ID based on the page and section name */
makeSectionId(section) {
const normalize = (str) => (
str ? str.trim().toLowerCase().replace(/[^a-zA-Z0-9]/g, '-')
: `unnamed-${(`000${Math.floor(Math.random() * 1000)}`).slice(-3)}`
);
return `${this.pageId || 'unknown-page'}-${normalize(section.name)}`;
},
/* Returns true if there is one or more sections in the config */
checkTheresData(sections) {
const localSections = localStorage[localStorageKeys.CONF_SECTIONS];

143
src/mixins/ThemingMixin.js Normal file
View File

@@ -0,0 +1,143 @@
/**
* This mixin can be extended by any component or view which needs to manage themes
* It handles fetching and applying themes from the store, updating themes,
* applying custom CSS variables and loading external stylesheets.
* */
import Keys from '@/utils/StoreMutations';
import ErrorHandler from '@/utils/ErrorHandler';
import { builtInThemes, localStorageKeys, mainCssVars } from '@/utils/defaults';
const ThemingMixin = {
data: () => ({
selectedTheme: '', // Used only to bind current them to theme dropdown
}),
computed: {
/* This is the theme from the central store. When it changes, the UI will update */
themeFromStore() {
return this.$store.getters.theme;
},
appConfig() {
return this.$store.getters.appConfig;
},
/* Any extra user-defined themes, to add to dropdown */
extraThemeNames() {
const userThemes = this.appConfig?.cssThemes || [];
if (typeof userThemes === 'string') return [userThemes];
return userThemes;
},
/* If user specified external stylesheet(s), format and return */
externalThemes() {
const availableThemes = {};
if (this.appConfig?.externalStyleSheet) {
const externals = this.appConfig.externalStyleSheet;
if (Array.isArray(externals)) {
externals.forEach((ext, i) => {
availableThemes[`External Stylesheet ${i + 1}`] = ext;
});
} else if (typeof externals === 'string') {
availableThemes['External Stylesheet'] = this.appConfig.externalStyleSheet;
} else {
ErrorHandler('External stylesheets must be of type string or string[]');
}
}
return availableThemes;
},
/* Combines all theme names for dropdown (built-in, user-defined and stylesheets) */
themeNames() {
const externalThemeNames = Object.keys(this.externalThemes);
return [...this.extraThemeNames, ...externalThemeNames, ...builtInThemes];
},
},
watch: {
/* When theme in VueX store changes, then update theme */
themeFromStore(newTheme) {
if (newTheme) {
this.resetToDefault();
this.selectedTheme = newTheme;
this.updateTheme(newTheme);
}
},
},
methods: {
/* Called when user changes theme through the UI
* Updates store, which will in turn update theme through watcher
*/
themeChangedInUI() {
this.$store.commit(Keys.SET_THEME, this.selectedTheme); // Update store
this.updateTheme(this.selectedTheme); // Apply theme to UI
},
/**
* Gets any custom styles the user has applied, wither from local storage, or from the config
* @returns {object} An array of objects, one for each theme, containing kvps for variables
*/
getCustomColors() {
const localColors = JSON.parse(localStorage[localStorageKeys.CUSTOM_COLORS] || '{}');
const configColors = this.appConfig.customColors || {};
return Object.assign(configColors, localColors);
},
/* Gets user custom color preferences for current theme, and applies to DOM */
applyCustomVariables(theme) {
mainCssVars.forEach((vName) => { document.documentElement.style.removeProperty(`--${vName}`); });
const themeColors = this.getCustomColors()[theme];
if (themeColors) {
Object.keys(themeColors).forEach((customVar) => {
document.documentElement.style.setProperty(`--${customVar}`, themeColors[customVar]);
});
}
},
/* Sets the theme, by updating data-theme attribute on the html tag */
applyLocalTheme(newTheme) {
const htmlTag = document.getElementsByTagName('html')[0];
if (htmlTag.hasAttribute('data-theme')) htmlTag.removeAttribute('data-theme');
htmlTag.setAttribute('data-theme', newTheme);
},
/* If using an external stylesheet, load it in */
applyRemoteTheme(href) {
this.resetToDefault();
const element = document.createElement('link');
element.setAttribute('rel', 'stylesheet');
element.setAttribute('type', 'text/css');
element.setAttribute('id', 'user-defined-stylesheet');
element.setAttribute('href', href);
document.getElementsByTagName('head')[0].appendChild(element);
},
/* Determines if a given theme is local / not a custom user stylesheet */
isThemeLocal(themeToCheck) {
const localThemes = [...builtInThemes, ...this.extraThemeNames];
return localThemes.includes(themeToCheck);
},
/* Updates theme. Checks if the new theme is local or external,
and calls appropriate updating function. Updates local storage */
updateTheme(newTheme) {
if (newTheme.toLowerCase() === 'default') {
this.resetToDefault();
} else if (this.isThemeLocal(newTheme)) {
this.applyLocalTheme(newTheme);
} else if (this.externalThemes[newTheme]) {
this.applyRemoteTheme(this.externalThemes[newTheme]);
}
this.applyCustomVariables(newTheme);
},
/* Removes any applied themes, and deletes any externally loaded stylesheets */
resetToDefault() {
const externalStyles = document.getElementById('user-defined-stylesheet');
if (externalStyles) document.getElementsByTagName('head')[0].removeChild(externalStyles);
document.getElementsByTagName('html')[0].removeAttribute('data-theme');
},
/* Call within mounted hook within a page to apply the correct theme */
initializeTheme() {
const initialTheme = this.themeFromStore;
this.selectedTheme = initialTheme;
const hasExternal = this.externalThemes && Object.entries(this.externalThemes).length > 0;
if (this.isThemeLocal(initialTheme)) {
this.updateTheme(initialTheme);
} else if (hasExternal) {
this.applyRemoteTheme(this.externalThemes[initialTheme]);
}
},
},
};
export default ThemingMixin;

View File

@@ -2,7 +2,6 @@
* Mixin that all pre-built and custom widgets extend from.
* Manages loading state, error handling, data updates and user options
*/
import axios from 'axios';
import { Progress } from 'rsup-progress';
import ErrorHandler from '@/utils/ErrorHandler';
import { serviceEndpoints } from '@/utils/defaults';
@@ -106,31 +105,76 @@ const WidgetMixin = {
const method = protocol || 'GET';
const url = this.useProxy ? this.proxyReqEndpoint : endpoint;
const data = JSON.stringify(body || {});
const CustomHeaders = options || null;
const headers = this.useProxy
? { 'Target-URL': endpoint, CustomHeaders: JSON.stringify(CustomHeaders) } : CustomHeaders;
const CustomHeaders = options || {};
const headers = new Headers();
// If using a proxy, set the 'Target-URL' header
if (this.useProxy) {
headers.append('Target-URL', endpoint);
}
// Apply widget-specific custom headers
Object.entries(CustomHeaders).forEach(([key, value]) => {
headers.append(key, value);
});
// If the request is a GET, delete the body
const bodyContent = method.toUpperCase() === 'GET' ? undefined : data;
const timeout = this.options.timeout || this.defaultTimeout;
// Setup Fetch request configuration
const requestConfig = {
method, url, headers, data, timeout,
method,
headers,
body: bodyContent,
signal: undefined, // This will be set below
};
// Make request
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), timeout);
requestConfig.signal = controller.signal;
// Make request using Fetch API
return new Promise((resolve, reject) => {
axios.request(requestConfig)
.then((response) => {
if (response.data.success === false) {
this.error('Proxy returned error from target server', response.data.message);
fetch(url, requestConfig)
.then(async response => {
const responseData = await response.json();
if (responseData.error) {
this.error('Proxy returned error from target server', responseData.error?.message);
}
resolve(response.data);
if (responseData.success === false) {
this.error('Proxy didn\'t return success from target server', responseData.message);
}
resolve(responseData);
})
.catch((dataFetchError) => {
this.error('Unable to fetch data', dataFetchError);
reject(dataFetchError);
.catch(error => {
if (error.name === 'AbortError') {
this.error('Request timed out', error);
} else {
this.error('Unable to fetch data', error);
}
reject(error);
})
.finally(() => {
clearTimeout(timeoutId);
this.finishLoading();
});
});
},
/* Check if a value is an environment variable, return its value if so. */
parseAsEnvVar(str) {
if (typeof str !== 'string') return str;
if (str.includes('VUE_APP_')) {
const envVar = process.env[str];
if (!envVar) {
this.error(`Environment variable ${str} not found`);
} else {
return envVar;
}
}
return str;
},
},
};

View File

@@ -14,23 +14,9 @@ import Home from '@/views/Home.vue';
// Import helper functions, config data and defaults
import { isAuthEnabled, isLoggedIn, isGuestAccessEnabled } from '@/utils/Auth';
import { makePageSlug, makePageName } from '@/utils/ConfigHelpers';
import { metaTagData, startingView, routePaths } from '@/utils/defaults';
import { metaTagData, startingView as defaultStartingView, routePaths } from '@/utils/defaults';
import ErrorHandler from '@/utils/ErrorHandler';
// Import data from users conf file. Note that rebuild is required for this to update.
import conf from '../public/conf.yml';
if (!conf) {
ErrorHandler('You\'ve not got any data in your config file yet.');
}
// Assign top-level config fields, check not null
const config = conf || {};
const pages = config.pages || [];
const pageInfo = config.pageInfo || {};
const appConfig = config.appConfig || {};
Vue.use(Router);
const progress = new Progress({ color: 'var(--progress-bar)' });
@@ -42,16 +28,15 @@ const isAuthenticated = () => {
return (!authEnabled || userLoggedIn || guestEnabled);
};
/* Get the users chosen starting view from app config, or return default */
const getStartingView = () => appConfig.startingView || startingView;
// Get the default starting view from environmental variable
const startingView = process.env.VUE_APP_STARTING_VIEW || defaultStartingView;
/**
* Returns the component that should be rendered at the base path,
* Defaults to Home, but the user can change this to Workspace of Minimal
*/
const getStartingComponent = () => {
const usersPreference = getStartingView();
switch (usersPreference) {
switch (startingView) {
case 'minimal': return () => import('./views/Minimal.vue');
case 'workspace': return () => import('./views/Workspace.vue');
default: return Home;
@@ -59,71 +44,23 @@ const getStartingComponent = () => {
};
/* Returns the meta tags for each route */
const makeMetaTags = (defaultTitle) => ({
title: pageInfo.title || defaultTitle,
metaTags: metaTagData,
});
const makeSubConfigPath = (rawPath) => {
if (!rawPath) return '';
if (rawPath.startsWith('/') || rawPath.startsWith('http')) return rawPath;
else return `/${rawPath}`;
};
/* For each additional config file, create routes for home, minimal and workspace views */
const makeMultiPageRoutes = (userPages) => {
// If no multi pages specified, or is not array, then return nothing
if (!userPages || !Array.isArray(userPages)) return [];
const multiPageRoutes = [];
// For each user page, create an additional route
userPages.forEach((page) => {
if (!page.name || !page.path) { // Sumin not right, show warning
ErrorHandler('Additional pages must have both a `name` and `path`');
}
// Props to be passed to home mixin
const subPageInfo = {
subPageInfo: {
confPath: makeSubConfigPath(page.path),
pageId: makePageName(page.name),
pageTitle: page.name,
},
};
// Create route for default homepage
multiPageRoutes.push({
path: makePageSlug(page.name, 'home'),
name: `${subPageInfo.subPageInfo.pageId}-home`,
component: Home,
props: subPageInfo,
});
// Create route for the workspace view
multiPageRoutes.push({
path: makePageSlug(page.name, 'workspace'),
name: `${subPageInfo.subPageInfo.pageId}-workspace`,
component: () => import('./views/Workspace.vue'),
props: subPageInfo,
});
// Create route for the minimal view
multiPageRoutes.push({
path: makePageSlug(page.name, 'minimal'),
name: `${subPageInfo.subPageInfo.pageId}-minimal`,
component: () => import('./views/Minimal.vue'),
props: subPageInfo,
});
});
return multiPageRoutes;
const makeMetaTags = (defaultTitle) => {
const userTitle = process.env.VUE_APP_TITLE || '';
const title = userTitle ? `${userTitle} | ${defaultTitle}` : defaultTitle;
return { title, metaTags: metaTagData };
};
/* Routing mode, can be either 'hash', 'history' or 'abstract' */
const mode = appConfig.routingMode || 'history';
const mode = process.env.VUE_APP_ROUTING_MODE || 'history';
/* List of all routes, props, components and metadata */
const router = new Router({
mode,
routes: [
...makeMultiPageRoutes(pages),
// ...makeMultiPageRoutes(pages),
{ // The default view can be customized by the user
path: '/',
name: `landing-page-${getStartingView()}`,
name: `landing-page-${startingView}`,
component: getStartingComponent(),
meta: makeMetaTags('Home Page'),
},
@@ -197,7 +134,7 @@ const router = new Router({
* if so, then ensure that they are correctly logged in as a valid user
* If not logged in, prevent all access and redirect them to login page
* */
router.beforeEach((to, from, next) => {
router.beforeEach(async (to, from, next) => {
progress.start();
if (to.name !== 'login' && !isAuthenticated()) next({ name: 'login' });
else next();

View File

@@ -4,22 +4,22 @@ import Vuex from 'vuex';
import axios from 'axios';
import yaml from 'js-yaml';
import Keys from '@/utils/StoreMutations';
import ConfigAccumulator from '@/utils/ConfigAccumalator';
import { componentVisibility } from '@/utils/ConfigHelpers';
import { makePageName, formatConfigPath, componentVisibility } from '@/utils/ConfigHelpers';
import { applyItemId } from '@/utils/SectionHelpers';
import filterUserSections from '@/utils/CheckSectionVisibility';
import ErrorHandler, { InfoHandler, InfoKeys } from '@/utils/ErrorHandler';
import { isUserAdmin } from '@/utils/Auth';
import { localStorageKeys } from './utils/defaults';
import { isUserAdmin, makeBasicAuthHeaders, isLoggedInAsGuest } from '@/utils/Auth';
import { localStorageKeys, theme as defaultTheme } from './utils/defaults';
Vue.use(Vuex);
const {
INITIALIZE_CONFIG,
INITIALIZE_MULTI_PAGE_CONFIG,
INITIALIZE_ROOT_CONFIG,
SET_CONFIG,
SET_REMOTE_CONFIG,
SET_CURRENT_SUB_PAGE,
SET_ROOT_CONFIG,
SET_CURRENT_CONFIG_INFO,
SET_IS_USING_LOCAL_CONFIG,
SET_MODAL_OPEN,
SET_LANGUAGE,
SET_ITEM_LAYOUT,
@@ -41,15 +41,24 @@ const {
INSERT_ITEM,
UPDATE_CUSTOM_CSS,
CONF_MENU_INDEX,
CRITICAL_ERROR_MSG,
} = Keys;
const emptyConfig = {
appConfig: {},
pageInfo: { title: 'Dashy' },
sections: [],
};
const store = new Vuex.Store({
state: {
config: {}, // The current config, rendered to the UI
remoteConfig: {}, // The configuration stored on the server
config: {}, // The current config being used, and rendered to the UI
rootConfig: null, // Always the content of main config file, never used directly
editMode: false, // While true, the user can drag and edit items + sections
modalOpen: false, // KB shortcut functionality will be disabled when modal is open
currentConfigInfo: undefined, // For multi-page support, will store info about config file
currentConfigInfo: {}, // For multi-page support, will store info about config file
isUsingLocalConfig: false, // If true, will use local config instead of fetched
criticalError: null, // Will store a message, if a critical error occurs
navigateConfToTab: undefined, // Used to switch active tab in config modal
},
getters: {
@@ -68,17 +77,14 @@ const store = new Vuex.Store({
return filterUserSections(state.config.sections || []);
},
pages(state) {
return state.remoteConfig.pages || [];
return state.config.pages || [];
},
theme(state) {
let localTheme = null;
if (state.currentConfigInfo?.pageId) {
const themeStoreKey = `${localStorageKeys.THEME}-${state.currentConfigInfo?.pageId}`;
localTheme = localStorage[themeStoreKey];
} else {
localTheme = localStorage[localStorageKeys.THEME];
}
return localTheme || state.config.appConfig.theme;
const localStorageKey = state.currentConfigInfo.confId
? `${localStorageKeys.THEME}-${state.currentConfigInfo.confId}` : localStorageKeys.THEME;
const localTheme = localStorage[localStorageKey];
// Return either theme from local storage, or from appConfig
return localTheme || state.config.appConfig.theme || defaultTheme;
},
webSearch(state, getters) {
return getters.appConfig.webSearch || {};
@@ -108,7 +114,8 @@ const store = new Vuex.Store({
}
// Disable everything
if (appConfig.disableConfiguration
|| (appConfig.disableConfigurationForNonAdmin && !isUserAdmin())) {
|| (appConfig.disableConfigurationForNonAdmin && !isUserAdmin())
|| isLoggedInAsGuest()) {
perms.allowWriteToDisk = false;
perms.allowSaveLocally = false;
perms.allowViewConfig = false;
@@ -139,21 +146,36 @@ const store = new Vuex.Store({
return foundSection;
},
layout(state) {
return state.config.appConfig.layout || 'auto';
const pageId = state.currentConfigInfo.confId;
const layoutStoreKey = pageId
? `${localStorageKeys.LAYOUT_ORIENTATION}-${pageId}` : localStorageKeys.LAYOUT_ORIENTATION;
const appConfigLayout = state.config.appConfig.layout;
return localStorage.getItem(layoutStoreKey) || appConfigLayout || 'auto';
},
iconSize(state) {
return state.config.appConfig.iconSize || 'medium';
const pageId = state.currentConfigInfo.confId;
const sizeStoreKey = pageId
? `${localStorageKeys.ICON_SIZE}-${pageId}` : localStorageKeys.ICON_SIZE;
const appConfigSize = state.config.appConfig.iconSize;
return localStorage.getItem(sizeStoreKey) || appConfigSize || 'medium';
},
},
mutations: {
/* Set the master config */
[SET_ROOT_CONFIG](state, config) {
if (!config.appConfig) config.appConfig = {};
state.config = config;
},
/* The config to display and edit. Will differ from ROOT_CONFIG when using multi-page */
[SET_CONFIG](state, config) {
if (!config.appConfig) config.appConfig = {};
state.config = config;
},
[SET_REMOTE_CONFIG](state, config) {
const notNullConfig = config || {};
if (!notNullConfig.appConfig) notNullConfig.appConfig = {};
state.remoteConfig = notNullConfig;
[SET_CURRENT_CONFIG_INFO](state, subConfigInfo) {
state.currentConfigInfo = subConfigInfo;
},
[SET_IS_USING_LOCAL_CONFIG](state, isUsingLocalConfig) {
state.isUsingLocalConfig = isUsingLocalConfig;
},
[SET_LANGUAGE](state, lang) {
const newConfig = state.config;
@@ -169,6 +191,10 @@ const store = new Vuex.Store({
state.editMode = editMode;
}
},
[CRITICAL_ERROR_MSG](state, message) {
if (message) ErrorHandler(message);
state.criticalError = message;
},
[UPDATE_ITEM](state, payload) {
const { itemId, newItem } = payload;
const newConfig = { ...state.config };
@@ -276,12 +302,13 @@ const store = new Vuex.Store({
config.sections = applyItemId(config.sections);
state.config = config;
},
[SET_THEME](state, themOps) {
const { theme, pageId } = themOps;
[SET_THEME](state, theme) {
const newConfig = { ...state.config };
newConfig.appConfig.theme = theme;
state.config = newConfig;
const themeStoreKey = pageId ? `${localStorageKeys.THEME}-${pageId}` : localStorageKeys.THEME;
const pageId = state.currentConfigInfo.confId;
const themeStoreKey = pageId
? `${localStorageKeys.THEME}-${pageId}` : localStorageKeys.THEME;
localStorage.setItem(themeStoreKey, theme);
InfoHandler('Theme updated', InfoKeys.VISUAL);
},
@@ -292,11 +319,23 @@ const store = new Vuex.Store({
InfoHandler('Color palette updated', InfoKeys.VISUAL);
},
[SET_ITEM_LAYOUT](state, layout) {
state.config.appConfig.layout = layout;
const newConfig = { ...state.config };
newConfig.appConfig.layout = layout;
state.config = newConfig;
const pageId = state.currentConfigInfo.confId;
const layoutStoreKey = pageId
? `${localStorageKeys.LAYOUT_ORIENTATION}-${pageId}` : localStorageKeys.LAYOUT_ORIENTATION;
localStorage.setItem(layoutStoreKey, layout);
InfoHandler('Layout updated', InfoKeys.VISUAL);
},
[SET_ITEM_SIZE](state, iconSize) {
state.config.appConfig.iconSize = iconSize;
const newConfig = { ...state.config };
newConfig.appConfig.iconSize = iconSize;
state.config = newConfig;
const pageId = state.currentConfigInfo.confId;
const sizeStoreKey = pageId
? `${localStorageKeys.ICON_SIZE}-${pageId}` : localStorageKeys.ICON_SIZE;
localStorage.setItem(sizeStoreKey, iconSize);
InfoHandler('Item size updated', InfoKeys.VISUAL);
},
[UPDATE_CUSTOM_CSS](state, customCss) {
@@ -306,42 +345,117 @@ const store = new Vuex.Store({
[CONF_MENU_INDEX](state, index) {
state.navigateConfToTab = index;
},
[SET_CURRENT_SUB_PAGE](state, subPageObject) {
if (!subPageObject) {
// Set theme back to primary when navigating to index page
const defaulTheme = localStorage.getItem(localStorageKeys.PRIMARY_THEME);
if (defaulTheme) state.config.appConfig.theme = defaulTheme;
}
state.currentConfigInfo = subPageObject;
},
[USE_MAIN_CONFIG](state) {
if (state.remoteConfig) {
state.config = state.remoteConfig;
} else {
this.dispatch(Keys.INITIALIZE_CONFIG);
}
/* Set config to rootConfig, by calling initialize with no params */
async [USE_MAIN_CONFIG]() {
this.dispatch(Keys.INITIALIZE_CONFIG);
},
},
actions: {
/* Called when app first loaded. Reads config and sets state */
async [INITIALIZE_CONFIG]({ commit }) {
// Get the config file from the server and store it for use by the accumulator
commit(SET_REMOTE_CONFIG, yaml.load((await axios.get('/conf.yml')).data));
const deepCopy = (json) => JSON.parse(JSON.stringify(json));
const config = deepCopy(new ConfigAccumulator().config());
commit(SET_CONFIG, config);
/* Fetches the root config file, only ever called by INITIALIZE_CONFIG */
async [INITIALIZE_ROOT_CONFIG]({ commit }) {
const configFilePath = process.env.VUE_APP_CONFIG_PATH || '/conf.yml';
try {
// Attempt to fetch the YAML file
const response = await axios.get(configFilePath, makeBasicAuthHeaders());
let data;
try {
data = yaml.load(response.data);
} catch (parseError) {
commit(CRITICAL_ERROR_MSG, `Failed to parse YAML: ${parseError.message}`);
return { ...emptyConfig };
}
// Replace missing root properties with empty objects
if (!data.appConfig) data.appConfig = {};
if (!data.pageInfo) data.pageInfo = {};
if (!data.sections) data.sections = [];
// Set the state, and return data
commit(SET_ROOT_CONFIG, data);
commit(CRITICAL_ERROR_MSG, null);
return data;
} catch (fetchError) {
if (fetchError.response) {
commit(
CRITICAL_ERROR_MSG,
'Failed to fetch configuration: Server responded with status '
+ `${fetchError.response?.status || 'mystery status'}`,
);
} else if (fetchError.request) {
commit(CRITICAL_ERROR_MSG, 'Failed to fetch configuration: No response from server');
} else {
commit(CRITICAL_ERROR_MSG, `Failed to fetch configuration: ${fetchError.message}`);
}
return { ...emptyConfig };
}
},
/* Fetch config for a sub-page (sections and pageInfo only) */
async [INITIALIZE_MULTI_PAGE_CONFIG]({ commit, state }, configPath) {
axios.get(configPath).then((response) => {
const subConfig = yaml.load(response.data);
const pageTheme = subConfig.appConfig?.theme;
subConfig.appConfig = state.config.appConfig; // Always use parent appConfig
if (pageTheme) subConfig.appConfig.theme = pageTheme; // Apply page theme override
commit(SET_CONFIG, subConfig);
}).catch((err) => {
ErrorHandler(`Unable to load config from '${configPath}'`, err);
});
/**
* Fetches config and updates state
* If not on sub-page, will trigger the fetch of main config, then use that
* If using sub-page config, then fetch that sub-config, then
* override certain fields (appConfig, pages) and update config
*/
async [INITIALIZE_CONFIG]({ commit, state }, subConfigId) {
const rootConfig = state.rootConfig || await this.dispatch(Keys.INITIALIZE_ROOT_CONFIG);
commit(SET_IS_USING_LOCAL_CONFIG, false);
if (!subConfigId) { // Use root config as config
commit(SET_CONFIG, rootConfig);
commit(SET_CURRENT_CONFIG_INFO, {});
let localSections = [];
const localSectionsRaw = localStorage[localStorageKeys.CONF_SECTIONS];
if (localSectionsRaw) {
try {
const json = JSON.parse(localSectionsRaw);
if (json.length >= 1) localSections = json;
} catch (e) {
commit(CRITICAL_ERROR_MSG, 'Malformed section data in local storage');
}
}
if (localSections.length > 0) {
rootConfig.sections = localSections;
commit(SET_IS_USING_LOCAL_CONFIG, true);
}
return rootConfig;
} else {
// Find and format path to fetch sub-config from
const subConfigPath = formatConfigPath(rootConfig?.pages?.find(
(page) => makePageName(page.name) === subConfigId,
)?.path);
if (!subConfigPath) {
commit(CRITICAL_ERROR_MSG, `Unable to find config for '${subConfigId}'`);
return { ...emptyConfig };
}
axios.get(subConfigPath, makeBasicAuthHeaders()).then((response) => {
// Parse the YAML
const configContent = yaml.load(response.data) || {};
// Certain values must be inherited from root config
const theme = configContent?.appConfig?.theme || rootConfig.appConfig?.theme || 'default';
configContent.appConfig = rootConfig.appConfig;
configContent.pages = rootConfig.pages;
configContent.appConfig.theme = theme;
// Load local sections if they exist
const localSectionsRaw = localStorage[`${localStorageKeys.CONF_SECTIONS}-${subConfigId}`];
if (localSectionsRaw) {
try {
const json = JSON.parse(localSectionsRaw);
if (json.length >= 1) {
configContent.sections = json;
commit(SET_IS_USING_LOCAL_CONFIG, true);
}
} catch (e) {
commit(CRITICAL_ERROR_MSG, 'Malformed section data in local storage for sub-config');
}
}
// Set the config
commit(SET_CONFIG, configContent);
commit(SET_CURRENT_CONFIG_INFO, { confPath: subConfigPath, confId: subConfigId });
}).catch((err) => {
commit(CRITICAL_ERROR_MSG, `Unable to load config from '${subConfigPath}'`, err);
});
}
return { ...emptyConfig };
},
},
modules: {},

View File

@@ -31,6 +31,7 @@
--transparent-white-70: #ffffffb3;
--transparent-white-50: #ffffff80;
--transparent-white-30: #ffffff4d;
--transparent-white-10: #ffffff0f;
/* Color variables for specific components
* all variables are optional, since they inherit initial values from above*

View File

@@ -1619,6 +1619,236 @@ html[data-theme='lissy'] {
}
}
html[data-theme='glass'],
html[data-theme='glass-2'],
html[data-theme='neomorphic'] {
--primary: #fff;
--item-group-outer-background: rgba(0, 0, 0, 0.25);
--item-group-background: transparent;
--item-group-heading-text-color: #fff;
--item-group-heading-text-color-hover: #ffffffd6;
--item-group-shadow: 5px 2px 20px rgba(0, 0, 0, 0.5);
--background: #190842;
--background-darker: #190842;
--settings-background: transparent;
--search-container-background: transparent;
--font-headings: 'Segoe UI', 'Ariel', 'sans-serif';
--font-body: 'Roboto', 'Segoe UI', 'Ariel', 'sans-serif';
--minimal-view-background-color: transparent;
--minimal-view-group-background: rgba(255, 255, 255, 0.15);
--minimal-view-section-heading-background: rgba(255, 255, 255, 0.15);
--minimal-view-section-heading-color: rgba(255, 255, 255, 0.15);
--config-settings-background: #16073de3;
--cloud-backup-background: #16073de3;
@mixin item-transition-styles($bg: transparent, $hover-bg: rgba(255, 255, 255, 0.15), $hover-shadow: rgba(0, 0, 0, 0.75)) {
background: $bg;
border: 1px solid transparent;
box-shadow: none;
transition: 0.2s all ease-in-out;
&:hover {
border-radius: 0.35rem;
box-shadow: 0 4px 30px $hover-shadow;
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.19);
background: $hover-bg;
}
}
@mixin transform-scale($normal-scale: 1, $hover-scale: 1.25) {
transition: 0.1s all ease-in-out;
transform: scale($normal-scale);
&:hover {
transform: scale($hover-scale);
}
}
body {
background-size: cover;
background-color: #090317;
.home {
background: transparent;
}
}
.settings-outer, header, .dashy-modal, .dashy-modal .tabs {
background: transparent;
// backdrop-filter: blur(4px);
}
// Minimal view components
.minimal-section-inner, div.minimal-section-heading {
backdrop-filter: blur(10px);
border: 1px solid rgba(145, 145, 145, 0.45);
border-bottom: none;
&.selected {
border: 1px solid rgba(145, 145, 145, 0.45);
background: var(--minimal-view-group-background);
}
}
.minimal-section-heading {
color: var(--minimal-view-section-heading-background);
&.selected {
.section-icon, .section-title {
color: var(--primary) !important;
}
}
}
--glass-button-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
--glass-button-hover-shadow: 2px 2px 5px rgba(0, 0, 0, 0.7);
// Forms and inputs
button.save-button,
.action-buttons button,
.cloud-backup-restore-wrapper button,
.tab__nav__item,
div.input-container input.input-field,
form.normal input,
.nav-outer nav .nav-item,
div.edit-mode-bottom-banner .edit-banner-section button,
.v-select.theme-dropdown.vs__dropdown-toggle,
.theme-dropdown div.vs__dropdown-toggle,
.config-buttons > svg,
.display-options svg,
form.minimal input,
.critical-error-wrap button.user-doesnt-care,
a.config-button, button.config-button {
border-radius: 0.35rem;
box-shadow: var(--glass-button-shadow);
backdrop-filter: blur(10px);
border: 1px solid rgba(255, 255, 255, 0.19);
background: rgba(255, 255, 255, 0.15);
transition: all 0.2s ease-in-out;
text-decoration: none;
&:hover, &.selected {
box-shadow: var(--glass-button-hover-shadow);
border: 1px solid rgba(255, 255, 255, 0.25) !important;
background: #ffffff42 !important;
color: var(--primary) !important;
path { fill: var(--primary); }
}
}
.tab__nav__items {
gap: 1rem;
margin: 0.5rem 0 0;
.tab__nav__item {
padding: 0.5rem 0.5rem;
&:hover, .active, .active:hover {
background: #ffffff42 !important;
span { color: var(--primary) !important; }
}
}
}
.main-options-container .config-buttons, div.cloud-backup-restore-wrapper {
background: none;
}
// Item and collapsable specific styles
.item {
@include item-transition-styles(transparent, rgba(255, 255, 255, 0.15), rgba(0, 0, 0, 0.75));
.item-icon {
@include transform-scale(1.1, 1.25);
}
}
.collapsable {
border-radius: 0.5rem;
border: 1px solid rgba(0, 0, 0, 0.45);
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1);
}
// Modal specific styles
.dashy-modal {
box-shadow: 0 20px 40px -2px #000000b8, 1px 1px 6px #000000a6 !important;
}
.tab-item {
background: var(--config-settings-background);
}
.theme-configurator-wrapper, .view-switcher {
backdrop-filter: blur(10px);
background: var(--config-settings-background);
border: 1px solid rgba(255, 255, 255, 0.19);
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1);
}
.edit-mode-top-banner {
backdrop-filter: blur(10px);
background: #ffffff6b;
border-bottom: 1px solid black;
span { color: #eaff9d; }
}
div.edit-mode-bottom-banner, .add-new-section {
background: rgba(255, 255, 255, 0.15);
backdrop-filter: blur(50px);
}
.critical-error-wrap {
backdrop-filter: blur(15px);
background: #0f0528c4;
}
}
html[data-theme='glass'] {
body {
background: url('https://zeabur.com/images/bg.png') center center no-repeat;
background-size: cover;
background-color: #090317;
.home {
background: transparent;
}
}
}
html[data-theme='glass-2'] {
body {
background: url('https://i.ibb.co/FnLH6bj/dashy-glass.jpg') center center no-repeat;
background-size: cover;
background-color: #090317;
}
}
html[data-theme='neomorphic'] {
--primary: #fff;
--item-group-outer-background: rgba(255, 255, 255, 0.15);
--item-group-background: transparent;
--item-group-heading-text-color: #fff;
--item-group-shadow: 5px 2px 20px rgba(0, 0, 0, 0.5);
--background: #5b56f7;
// --background: #4bdbfd;
--background-darker: #12103c;
--settings-background: transparent;
--search-container-background: transparent;
--font-headings: 'Segoe UI', 'Ariel', 'sans-serif';
--font-body: 'Roboto', 'Segoe UI', 'Ariel', 'sans-serif';
--minimal-view-background-color: transparent;
--minimal-view-group-background: rgba(255, 255, 255, 0.15);
--minimal-view-section-heading-background: rgba(255, 255, 255, 0.15);
--minimal-view-section-heading-color: rgba(255, 255, 255, 0.15);
--config-settings-background: #1fb8f4e3;
--cloud-backup-background: #16073de3;
--glass-button-shadow: 0px 1px 5px rgba(0, 0, 0, 0.5);
--glass-button-hover-shadow: 2px 2px 5px rgba(0, 0, 0, 0.7);
body {
background: var(--background);
}
.item:hover { box-shadow: 0 3px 10px rgba(0, 0, 0, 0.5); }
.collapsable { border: 1px solid rgba(255, 255, 255, 0.25) !important; }
}
html[data-theme='cherry-blossom'] {
--primary: #e1e8ee;
--background: #11171d;
@@ -1777,7 +2007,7 @@ html[data-theme="tama"] {
// large tile spacing adjustment
.there-are-items:has(.item-wrapper.wrap-size-large) { padding-top: .25rem; }
.item-wrapper.wrap-size-large { margin: 0rem .5rem .5rem 0rem; }
.item-wrapper.wrap-size-large { margin: 0rem .5rem .5rem 0rem; overflow: hidden; }
.item.size-large .tile-title { padding: 0rem 0rem 0rem .7rem; }
// Hide open method icon

View File

@@ -22,6 +22,11 @@ html {
}
}
#dashy {
position: relative;
min-height: 100vh;
}
/* Hide text, and show 'Loading...' while Vue is initializing tags */
[v-cloak] > * { display:none }
[v-cloak]::before { content: "loading…" }
@@ -103,6 +108,7 @@ html {
border-radius: var(--curve-factor-small);
color: var(--description-tooltip-color);
padding: var(--tooltip-padding);
overflow: hidden;
}
.tooltip-arrow {
width: 0;

View File

@@ -39,37 +39,52 @@ const getUsers = () => {
* @returns {String} The hashed token
*/
const generateUserToken = (user) => {
if (!user.user || !user.hash) {
ErrorHandler('Invalid user object. Must have `user` and `hash` parameters');
if (!user.user || (!user.hash && !user.password)) {
ErrorHandler('Invalid user object. Must have `user` and either a `hash` or `password` param');
return undefined;
}
const passHash = user.hash || sha256(process.env[user.password]).toString().toUpperCase();
const strAndUpper = (input) => input.toString().toUpperCase();
const sha = sha256(strAndUpper(user.user) + strAndUpper(user.hash));
const sha = sha256(strAndUpper(user.user) + strAndUpper(passHash));
return strAndUpper(sha);
};
export const getCookieToken = () => {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${cookieKeys.AUTH_TOKEN}=`);
if (parts.length === 2) return parts.pop().split(';').shift();
return null;
};
export const makeBasicAuthHeaders = () => {
const token = getCookieToken();
const bearerAuth = (token && token.length > 5) ? `Bearer ${token}` : null;
const username = process.env.VUE_APP_BASIC_AUTH_USERNAME
|| localStorage[localStorageKeys.USERNAME]
|| 'user';
const password = process.env.VUE_APP_BASIC_AUTH_PASSWORD || bearerAuth;
const basicAuth = `Basic ${btoa(`${username}:${password}`)}`;
const headers = password
? { headers: { Authorization: basicAuth, 'WWW-Authenticate': 'true' } }
: {};
return headers;
};
/**
* Checks if the user is currently authenticated
* @returns {Boolean} Will return true if the user is logged in, else false
*/
export const isLoggedIn = () => {
const users = getUsers();
let userAuthenticated = document.cookie.split(';').some((cookie) => {
if (cookie && cookie.split('=').length > 1) {
const cookieKey = cookie.split('=')[0].trim();
const cookieValue = cookie.split('=')[1].trim();
if (cookieKey === cookieKeys.AUTH_TOKEN) {
userAuthenticated = users.some((user) => {
if (generateUserToken(user) === cookieValue) {
localStorage.setItem(localStorageKeys.USERNAME, user.user);
return true;
} else return false;
});
return userAuthenticated;
} else return false;
const cookieToken = getCookieToken();
return users.some((user) => {
if (generateUserToken(user) === cookieToken) {
localStorage.setItem(localStorageKeys.USERNAME, user.user);
return true;
} else return false;
});
return userAuthenticated;
};
/* Returns true if authentication is enabled */
@@ -106,7 +121,18 @@ export const checkCredentials = (username, pass, users, messages) => {
} else {
users.forEach((user) => {
if (user.user.toLowerCase() === username.toLowerCase()) { // User found
if (user.hash.toLowerCase() === sha256(pass).toString().toLowerCase()) {
if (user.password) {
if (!user.password.startsWith('VUE_APP_')) {
ErrorHandler('Invalid password format. Please use VUE_APP_ prefix');
response = { correct: false, msg: messages.incorrectPassword };
} else if (!process.env[user.password]) {
ErrorHandler(`Missing environmental variable for ${user.password}`);
} else if (process.env[user.password] === pass) {
response = { correct: true, msg: messages.successMsg };
} else {
response = { correct: false, msg: messages.incorrectPassword };
}
} else if (user.hash && user.hash.toLowerCase() === sha256(pass).toString().toLowerCase()) {
response = { correct: true, msg: messages.successMsg }; // Password is correct
} else { // User found, but password is not a match
response = { correct: false, msg: messages.incorrectPassword };
@@ -161,9 +187,9 @@ export const getCurrentUser = () => {
* Checks if the user is viewing the dashboard as a guest
* Returns true if guest mode enabled, and user not logged in
* */
export const isLoggedInAsGuest = (currentUser) => {
export const isLoggedInAsGuest = () => {
const guestEnabled = isGuestAccessEnabled();
const loggedIn = isLoggedIn() && currentUser;
const loggedIn = isLoggedIn();
return guestEnabled && !loggedIn;
};

View File

@@ -16,11 +16,9 @@ import ErrorHandler from '@/utils/ErrorHandler';
import { applyItemId } from '@/utils/SectionHelpers';
import $store from '@/store';
import buildConf from '../../public/conf.yml';
export default class ConfigAccumulator {
constructor() {
this.conf = $store.state.remoteConfig;
this.conf = $store.state.config;
}
pages() {
@@ -33,8 +31,6 @@ export default class ConfigAccumulator {
// Set app config from file
if (this.conf && this.conf.appConfig) {
appConfigFile = this.conf.appConfig;
} else if (buildConf && buildConf.appConfig) {
appConfigFile = buildConf.appConfig;
}
// Fill in defaults if anything missing
let usersAppConfig = defaultAppConfig;

View File

@@ -1,10 +1,10 @@
import ConfigAccumulator from '@/utils/ConfigAccumalator';
// import $store from '@/store';
import filterUserSections from '@/utils/CheckSectionVisibility';
import { languages } from '@/utils/languages';
import {
visibleComponents,
localStorageKeys,
theme as defaultTheme,
language as defaultLanguage,
} from '@/utils/defaults';
import ErrorHandler from '@/utils/ErrorHandler';
@@ -26,6 +26,13 @@ export const makePageSlug = (pageName, pageType) => {
return `/${pageType}/${formattedName}`;
};
/* Put fetch path for additional configs in correct format */
export const formatConfigPath = (configPath) => {
if (configPath.includes('http')) return configPath;
if (configPath.substring(0, 1) !== '/') return `/${configPath}`;
return configPath;
};
/**
* Initiates the Accumulator class and generates a complete config object
* Self-executing function, returns the full user config as a JSON object
@@ -67,34 +74,12 @@ export const componentVisibility = (appConfig) => {
};
};
/**
* Gets the users saved theme, first looks for local storage theme,
* then looks at user's appConfig, and finally checks the defaults
* @returns {string} Name of theme to apply
*/
export const getTheme = () => {
const localTheme = localStorage[localStorageKeys.THEME];
const appConfigTheme = config.appConfig.theme;
return localTheme || appConfigTheme || defaultTheme;
};
/**
* Gets any custom styles the user has applied, wither from local storage, or from the config
* @returns {object} An array of objects, one for each theme, containing kvps for variables
*/
export const getCustomColors = () => {
const localColors = JSON.parse(localStorage[localStorageKeys.CUSTOM_COLORS] || '{}');
const configColors = config.appConfig.customColors || {};
return Object.assign(configColors, localColors);
};
/**
* Returns a list of items which the user has assigned a hotkey to
* So that when the hotkey is pressed, the app/ service can be launched
*/
export const getCustomKeyShortcuts = () => {
export const getCustomKeyShortcuts = (sections) => {
const results = [];
const sections = config.sections || [];
sections.forEach((section) => {
const itemsWithHotKeys = section.items.filter(item => item.hotkey);
results.push(itemsWithHotKeys.map(item => ({ hotkey: item.hotkey, url: item.url })));

View File

@@ -500,14 +500,9 @@
"users": {
"title": "Users",
"type": "array",
"description": "Usernames and hashed credentials for frontend authentication",
"description": "Usernames and hashed credentials for frontend authentication. Needs to be set at build-time.",
"items": {
"type": "object",
"additionalProperties": false,
"required": [
"user",
"hash"
],
"properties": {
"user": {
"title": "Username",
@@ -521,6 +516,12 @@
"minLength": 64,
"maxLength": 64
},
"password": {
"title": "Password",
"type": "string",
"description": "The environmental variable pointing to a plaintext password for that user. Must start with VUE_APP_",
"pattern": "^VUE_APP_.*"
},
"type": {
"title": "Privileges",
"type": "string",
@@ -531,6 +532,43 @@
"description": "User type, denoting privilege level, either admin or normal",
"default": "normal"
}
},
"additionalProperties": false,
"required": ["user"],
"oneOf": [
{ "required": ["hash"] },
{ "required": ["password"] }
]
}
},
"enableHeaderAuth": {
"title": "Enable HeaderAuth?",
"type": "boolean",
"default": false,
"description": "If set to true, enable Header Authentication. See appConfig.auth.headerAuth"
},
"headerAuth": {
"type": "object",
"description": "Configuration for headerAuth",
"additionalProperties": false,
"required": [
"proxyWhitelist"
],
"properties": {
"userHeader": {
"title": "User Header",
"type": "string",
"description": "Header name which contains username",
"default": "REMOTE_USER"
},
"proxyWhitelist": {
"title": "Upstream Proxy Auth Trust",
"type": "array",
"description": "Upstream proxy servers to expect authenticated requests from",
"items": {
"type": "string",
"description": "IPs of upstream proxies that will be trusted"
}
}
}
},
@@ -982,6 +1020,11 @@
"type": "number",
"description": "A numeric shortcut key, between 0 and 9. Useful for quickly launching frequently used applications"
},
"rel": {
"title": "rel",
"type": "string",
"description": "The rel attribute for the link. For specifying the relationship between the current document and the linked document"
},
"tags": {
"title": "Tags",
"type": "array",
@@ -1086,6 +1129,11 @@
}
}
}
},
"filteredItems": {
"title": "Filtered Items - temp",
"type": "array",
"description": "This attribute will be deprecated in the next release - do not use!"
}
}
}

View File

@@ -9,7 +9,7 @@ export const welcomeMsg = () => {
/* Prints warning message, usually when there is a configuration error */
export const warningMsg = (message, stack) => {
console.info(
`\n%c⚠ Warning ⚠️%c \n${message} \n\n%cThis is likely not an issue with Dashy, but rather your configuration. If you think it is a bug, please open a ticket on GitHub: https://git.io/JukXk`,
`\n%c⚠ Warning ⚠️%c \n${message} \n\n%cThis is likely not an issue with Dashy, but rather your configuration.\nIf you think it is a bug, please open a ticket on GitHub: https://git.io/JukXk`,
"color:#ceb73f; background: #ceb73f33; font-size:1.5rem; padding:0.15rem; margin: 1rem auto; font-family: Rockwell, Tahoma, 'Trebuchet MS', Helvetica; border: 2px solid #ceb73f; border-radius: 4px; font-weight: bold; text-shadow: 1px 1px 1px #000000bf;",
'font-weight: bold; font-size: 1rem;color: #ceb73f;',
"color: #ceb73f; font-size: 0.75rem; font-family: Tahoma, 'Trebuchet MS', Helvetica;",

View File

@@ -9,12 +9,12 @@
/* eslint-disable global-require */
import ConfigAccumulator from '@/utils/ConfigAccumalator';
import $store from '@/store';
import { sentryDsn } from '@/utils/defaults';
const ErrorReporting = (Vue, router) => {
// Fetch users config
const appConfig = new ConfigAccumulator().appConfig() || {};
const appConfig = $store.getters.appConfig || {};
// Check if error reporting is enabled. Only proceed if user has turned it on.
if (appConfig.enableErrorReporting) {
// Get current app version

79
src/utils/HeaderAuth.js Normal file
View File

@@ -0,0 +1,79 @@
import axios from 'axios';
import sha256 from 'crypto-js/sha256';
import ConfigAccumulator from '@/utils/ConfigAccumalator';
import { cookieKeys, localStorageKeys, serviceEndpoints } from '@/utils/defaults';
import { InfoHandler, ErrorHandler, InfoKeys } from '@/utils/ErrorHandler';
import { logout as authLogout } from '@/utils/Auth';
const getAppConfig = () => {
const Accumulator = new ConfigAccumulator();
const config = Accumulator.config();
return config.appConfig || {};
};
class HeaderAuth {
constructor() {
const { auth } = getAppConfig();
const {
userHeader, proxyWhitelist,
} = auth.headerAuth;
this.userHeader = userHeader;
this.proxyWhitelist = proxyWhitelist;
this.users = auth.users;
}
login() {
return new Promise((resolve, reject) => {
const baseUrl = process.env.VUE_APP_DOMAIN || window.location.origin;
axios.get(`${baseUrl}${serviceEndpoints.getUser}`).then((response) => {
if (!response.data) {
reject(Error('Error, expected data nout returned'));
} else if (response.data.errorMsg) {
reject(response.data.errorMsg);
} else {
try {
this.users.forEach((user) => {
if (user.user.toLowerCase() === response.data.user.toLowerCase()) { // User found
const strAndUpper = (input) => input.toString().toUpperCase();
const sha = strAndUpper(sha256(strAndUpper(user.user) + strAndUpper(user.hash)));
document.cookie = `${cookieKeys.AUTH_TOKEN}=${sha};`;
localStorage.setItem(localStorageKeys.USERNAME, user.user);
InfoHandler(`Successfully signed in as ${response.data.user}`, InfoKeys.AUTH);
resolve(response.data.user);
}
});
} catch (e) {
ErrorHandler('Error while trying to login using header authentication', e);
reject(e);
}
}
});
});
}
// eslint-disable-next-line class-methods-use-this
logout() {
authLogout();
}
}
export const isHeaderAuthEnabled = () => {
const { auth } = getAppConfig();
if (!auth) return false;
return auth.enableHeaderAuth || false;
};
let headerAuth;
export const initHeaderAuth = () => {
headerAuth = new HeaderAuth();
return headerAuth.login();
};
// TODO: Find where this is implemented
export const getHeaderAuth = () => {
if (!headerAuth) {
ErrorHandler("HeaderAuth not initialized, can't get instance of class");
}
return headerAuth;
};

View File

@@ -27,7 +27,7 @@ const determineIntersection = (source = [], target = []) => {
/* Returns false if the displayData of a section/item
should not be rendered for the current user/ guest */
export const isVisibleToUser = (displayData, currentUser) => {
const isGuest = isLoggedInAsGuest(currentUser); // Check if current user is a guest
const isGuest = isLoggedInAsGuest(); // Check if current user is a guest
// Checks if user explicitly has access to a certain section
const checkVisibility = () => {

View File

@@ -1,4 +1,4 @@
/* Dashy: Licensed under MIT, (C) Alicia Sykes 2021 <https://aliciasykes.com> */
/* Dashy: Licensed under MIT, (C) Alicia Sykes 2024 <https://aliciasykes.com> */
/* Tile filtering utility */
import ErrorHandler from '@/utils/ErrorHandler';

View File

@@ -1,9 +1,12 @@
// A list of mutation names
const KEY_NAMES = [
'INITIALIZE_CONFIG',
'INITIALIZE_ROOT_CONFIG',
'INITIALIZE_MULTI_PAGE_CONFIG',
'SET_CONFIG',
'SET_REMOTE_CONFIG',
'SET_ROOT_CONFIG',
'SET_CURRENT_CONFIG_INFO',
'SET_IS_USING_LOCAL_CONFIG',
'SET_CURRENT_SUB_PAGE',
'SET_MODAL_OPEN',
'SET_LANGUAGE',
@@ -26,6 +29,7 @@ const KEY_NAMES = [
'INSERT_ITEM',
'UPDATE_CUSTOM_CSS',
'CONF_MENU_INDEX',
'CRITICAL_ERROR_MSG',
];
// Convert array of key names into an object, and export

View File

@@ -1,72 +0,0 @@
import ErrorHandler from '@/utils/ErrorHandler';
import { getTheme, getCustomColors } from '@/utils/ConfigHelpers';
import { mainCssVars } from '@/utils/defaults';
/* Returns users current theme */
export const GetTheme = () => getTheme();
/* Gets user custom color preferences for current theme, and applies to DOM */
export const ApplyCustomVariables = (theme) => {
mainCssVars.forEach((vName) => { document.documentElement.style.removeProperty(`--${vName}`); });
const themeColors = getCustomColors()[theme];
if (themeColors) {
Object.keys(themeColors).forEach((customVar) => {
document.documentElement.style.setProperty(`--${customVar}`, themeColors[customVar]);
});
}
};
/* Sets the theme, by updating data-theme attribute on the html tag */
export const ApplyLocalTheme = (newTheme) => {
const htmlTag = document.getElementsByTagName('html')[0];
if (htmlTag.hasAttribute('data-theme')) htmlTag.removeAttribute('data-theme');
htmlTag.setAttribute('data-theme', newTheme);
};
/**
* A function for pre-loading, and easy switching of external stylesheets
* External CSS is preloaded to avoid FOUC
*/
export const LoadExternalTheme = function th() {
/* Preload selected external theme */
const preloadTheme = (href) => {
const link = document.createElement('link');
link.rel = 'stylesheet';
link.type = 'text/css';
link.href = href;
document.head.appendChild(link);
return new Promise((resolve, reject) => {
link.onload = e => {
const { sheet } = e.target;
sheet.disabled = true;
resolve(sheet);
};
link.onerror = reject;
});
};
/* Check theme is selected, and it exists */
const checkTheme = (themes, name) => {
if ((!name) || (name !== 'custom' && !themes[name])) {
ErrorHandler(`Theme: '${name || '[not selected]'}' does not exist.`);
return false;
}
return true;
};
/* Disable all but selected theme */
const selectTheme = (themes, name) => {
if (checkTheme(themes, name)) {
const t = themes; // To avoid ESLint complaining about mutating a param
Object.keys(themes).forEach(n => { t[n].disabled = (n !== name); });
}
};
const themes = {};
return {
add(name, href) { return preloadTheme(href).then(s => { themes[name] = s; }); },
set theme(name) { selectTheme(themes, name); },
get theme() { return Object.keys(themes).find(n => !themes[n].disabled); },
};
};

View File

@@ -28,9 +28,9 @@ module.exports = {
openingMethod: 'newtab',
/* The page paths for each route within the app for the router */
routePaths: {
home: '/home',
minimal: '/minimal',
workspace: '/workspace',
home: '/home/:config?/',
minimal: '/minimal/:config?/',
workspace: '/workspace/:config?/',
about: '/about',
login: '/login',
download: '/download',
@@ -44,10 +44,12 @@ module.exports = {
rebuild: '/config-manager/rebuild',
systemInfo: '/system-info',
corsProxy: '/cors-proxy',
getUser: '/get-user',
},
/* List of built-in themes, to be displayed within the theme-switcher dropdown */
builtInThemes: [
'default',
'glass',
'callisto',
'material',
'material-dark',
@@ -85,6 +87,8 @@ module.exports = {
'adventure-basic',
'basic',
'tama',
'neomorphic',
'glass-2',
],
/* Default color options for the theme configurator swatches */
swatches: [
@@ -112,7 +116,7 @@ module.exports = {
/* Key names for local storage identifiers */
localStorageKeys: {
LANGUAGE: 'language',
HIDE_WELCOME_BANNER: 'hideWelcomeHelpers',
HIDE_INFO_NOTIFICATION: 'hideWelcomeHelpers',
LAYOUT_ORIENTATION: 'layoutOrientation',
COLLAPSE_STATE: 'collapseState',
ICON_SIZE: 'iconSize',
@@ -120,6 +124,7 @@ module.exports = {
PRIMARY_THEME: 'primaryTheme',
CUSTOM_COLORS: 'customColors',
CONF_SECTIONS: 'confSections',
CONF_PAGES: 'confPages',
CONF_WIDGETS: 'confSections',
PAGE_INFO: 'pageInfo',
APP_CONFIG: 'appConfig',
@@ -130,6 +135,7 @@ module.exports = {
MOST_USED: 'mostUsed',
LAST_USED: 'lastUsed',
KEYCLOAK_INFO: 'keycloakInfo',
DISABLE_CRITICAL_WARNING: 'disableCriticalWarning',
},
/* Key names for cookie identifiers */
cookieKeys: {
@@ -184,7 +190,7 @@ module.exports = {
// delay: { show: 380, hide: 0 },
},
/* Server location of the Backup & Sync cloud function */
backupEndpoint: 'https://dashy-sync-service.as93.net',
backupEndpoint: 'https://sync-service.dashy.to',
/* Available services for fetching favicon icon for user apps */
faviconApiEndpoints: {
allesedv: 'https://f1.allesedv.com/128/$URL',
@@ -215,7 +221,7 @@ module.exports = {
/* API endpoints for widgets that need to fetch external data */
widgetApiEndpoints: {
anonAddy: 'https://app.anonaddy.com',
astronomyPictureOfTheDay: 'https://go-apod.herokuapp.com/apod',
astronomyPictureOfTheDay: 'https://apod.as93.net/apod',
blacklistCheck: 'https://api.blacklistchecker.com/check',
codeStats: 'https://codestats.net/',
covidStats: 'https://disease.sh/v3/covid-19',

View File

@@ -19,6 +19,7 @@ import pl from '@/assets/locales/pl.json';
import pt from '@/assets/locales/pt.json';
import gl from '@/assets/locales/gl.json';
import ru from '@/assets/locales/ru.json';
import ro from '@/assets/locales/ro.json';
import sk from '@/assets/locales/sk.json';
import sl from '@/assets/locales/sl.json';
import sv from '@/assets/locales/sv.json';
@@ -151,6 +152,12 @@ export const languages = [
locale: ru,
flag: '🇷🇺',
},
{ // Romanian
name: 'Romana',
code: 'ro',
locale: ro,
flag: '🇷🇴',
},
{ // Slovak
name: 'Slovenčina',
code: 'sk',

View File

@@ -54,7 +54,7 @@
<section class="license">
<h2>License</h2>
<code>
Copyright © 2021 Alicia Sykes (https://aliciasykes.com)
Copyright © {{new Date().getFullYear()}} Alicia Sykes (https://aliciasykes.com)
</code>
<br><br>
<code>

View File

@@ -19,14 +19,7 @@
</router-link>
</div>
<!-- Main content, section for each group of items -->
<div v-if="checkTheresData(sections) || isEditMode"
:class="`item-group-container `
+ `orientation-${layout} `
+ `item-size-${itemSizeBound} `
+ (isEditMode ? 'edit-mode ' : '')
+ (singleSectionView ? 'single-section-view ' : '')
+ (this.colCount ? `col-count-${this.colCount} ` : '')"
>
<div v-if="checkTheresData(sections) || isEditMode" :class="computedClass">
<template v-for="(section, index) in filteredSections">
<Section
:key="index"
@@ -34,7 +27,7 @@
:title="section.name"
:icon="section.icon || undefined"
:displayData="getDisplayData(section)"
:groupId="`${pageId}-section-${index}`"
:groupId="makeSectionId(section)"
:items="section.filteredItems"
:widgets="section.widgets"
:searchTerm="searchValue"
@@ -56,6 +49,8 @@
<EditModeSaveMenu v-if="isEditMode" />
<!-- Modal for viewing and exporting configuration file -->
<ExportConfigMenu />
<!-- Shows pertinent info -->
<NotificationThing v-if="$store.state.isUsingLocalConfig"/>
</div>
</template>
@@ -66,8 +61,9 @@ import Section from '@/components/LinkItems/Section.vue';
import EditModeSaveMenu from '@/components/InteractiveEditor/EditModeSaveMenu.vue';
import ExportConfigMenu from '@/components/InteractiveEditor/ExportConfigMenu.vue';
import AddNewSection from '@/components/InteractiveEditor/AddNewSectionLauncher.vue';
import NotificationThing from '@/components/Settings/LocalConfigWarning.vue';
import StoreKeys from '@/utils/StoreMutations';
import { localStorageKeys, modalNames } from '@/utils/defaults';
import { modalNames } from '@/utils/defaults';
import ErrorHandler from '@/utils/ErrorHandler';
import BackIcon from '@/assets/interface-icons/back-arrow.svg';
@@ -79,6 +75,7 @@ export default {
EditModeSaveMenu,
ExportConfigMenu,
AddNewSection,
NotificationThing,
Section,
BackIcon,
},
@@ -116,15 +113,13 @@ export default {
iconSize() {
return this.$store.getters.iconSize;
},
},
watch: {
layoutOrientation(layout) {
localStorage.setItem(localStorageKeys.LAYOUT_ORIENTATION, layout);
this.layout = layout;
},
iconSize(size) {
localStorage.setItem(localStorageKeys.ICON_SIZE, size);
this.itemSizeBound = size;
computedClass() {
let classes = 'item-group-container '
+ ` orientation-${this.$store.getters.layout} item-size-${this.itemSizeBound}`;
if (this.isEditMode) classes += ' edit-mode';
if (this.singleSectionView) classes += ' single-section-view';
if (this.colCount) classes += ` col-count-${this.colCount}`;
return classes;
},
},
methods: {

View File

@@ -17,7 +17,7 @@
:class="`item-group-container ${!tabbedView ? 'showing-all' : ''}`">
<!-- Section heading buttons -->
<MinimalHeading
v-for="(section, index) in getSections(sections)"
v-for="(section, index) in sections"
:key="`heading-${index}`"
:index="index"
:title="section.name"
@@ -29,12 +29,12 @@
/>
<!-- Section item groups -->
<MinimalSection
v-for="(section, index) in getSections(sections)"
v-for="(section, index) in sections"
:key="`body-${index}`"
:index="index"
:title="section.name"
:icon="section.icon || undefined"
:groupId="`section-${index}`"
:groupId="makeSectionId(section)"
:items="filterTiles(section.items)"
:widgets="section.widgets"
:selected="selectedSection === index"
@@ -57,7 +57,6 @@ import HomeMixin from '@/mixins/HomeMixin';
import MinimalSection from '@/components/MinimalView/MinimalSection.vue';
import MinimalHeading from '@/components/MinimalView/MinimalHeading.vue';
import MinimalSearch from '@/components/MinimalView/MinimalSearch.vue';
import { localStorageKeys } from '@/utils/defaults';
import ConfigLauncher from '@/components/Settings/ConfigLauncher';
export default {
@@ -83,17 +82,6 @@ export default {
sectionSelected(index) {
this.selectedSection = index;
},
/* Returns sections from local storage if available, otherwise uses the conf.yml */
getSections(sections) {
// If the user has stored sections in local storage, return those
const localSections = localStorage[localStorageKeys.CONF_SECTIONS];
if (localSections) {
const json = JSON.parse(localSections);
if (json.length >= 1) return json;
}
// Otherwise, return the usuall data from conf.yml
return sections;
},
/* Clears input field, once a searched item is opened */
finishedSearching() {
if (this.$refs.filterComp) this.$refs.filterComp.clearMinFilterInput();

View File

@@ -19,7 +19,6 @@ import WebContent from '@/components/Workspace/WebContent';
import WidgetView from '@/components/Workspace/WidgetView';
import MultiTaskingWebComtent from '@/components/Workspace/MultiTaskingWebComtent';
import Defaults from '@/utils/defaults';
import { GetTheme, ApplyLocalTheme, ApplyCustomVariables } from '@/utils/ThemeHelper';
export default {
name: 'Workspace',
@@ -27,9 +26,6 @@ export default {
data: () => ({
url: '',
widgets: null,
GetTheme,
ApplyLocalTheme,
ApplyCustomVariables,
}),
computed: {
sections() {
@@ -89,6 +85,9 @@ export default {
<style scoped lang="scss">
.work-space {
min-height: calc(100vh - var(--footer-height));
min-height: fit-content;
}
:global(footer) {
display: none;
}
</style>