🔀 Rebased from master
This commit is contained in:
27
src/App.vue
27
src/App.vue
@@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<div id="dashy">
|
||||
<div id="dashy" :style="topLevelStyleModifications">
|
||||
<EditModeTopBanner v-if="isEditMode" />
|
||||
<LoadingScreen :isLoading="isLoading" v-if="shouldShowSplash" />
|
||||
<Header :pageInfo="pageInfo" />
|
||||
<router-view />
|
||||
<Footer :text="footerText" v-if="visibleComponents.footer" />
|
||||
<router-view v-if="!isFetching" />
|
||||
<Footer :text="footerText" v-if="visibleComponents.footer && !isFetching" />
|
||||
</div>
|
||||
</template>
|
||||
<script>
|
||||
@@ -33,6 +33,7 @@ export default {
|
||||
data() {
|
||||
return {
|
||||
isLoading: true, // Set to false after mount complete
|
||||
isFetching: true, // Set to false after the conf has been fetched
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
@@ -40,6 +41,9 @@ export default {
|
||||
// When in edit mode, show confirmation dialog on page exit
|
||||
window.onbeforeunload = isEditMode ? this.confirmExit : null;
|
||||
},
|
||||
config() {
|
||||
this.isFetching = false;
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
/* If the user has specified custom text for footer - get it */
|
||||
@@ -68,9 +72,17 @@ export default {
|
||||
isEditMode() {
|
||||
return this.$store.state.editMode;
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.$store.dispatch(Keys.INITIALIZE_CONFIG);
|
||||
topLevelStyleModifications() {
|
||||
const vc = this.visibleComponents;
|
||||
if (!vc.footer && !vc.pageTitle) {
|
||||
return '--footer-height: 1rem;';
|
||||
} else if (!vc.footer) {
|
||||
return '--footer-height: 5rem;';
|
||||
} else if (!vc.pageTitle) {
|
||||
return '--footer-height: 4rem;';
|
||||
}
|
||||
return '';
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
/* Injects the users custom CSS as a style tag */
|
||||
@@ -135,7 +147,8 @@ export default {
|
||||
},
|
||||
},
|
||||
/* Basic initialization tasks on app load */
|
||||
mounted() {
|
||||
async mounted() {
|
||||
await this.$store.dispatch(Keys.INITIALIZE_CONFIG); // Initialize config before moving on
|
||||
this.applyLanguage(); // Apply users local language
|
||||
this.hideSplash(); // Hide the splash screen, if visible
|
||||
if (this.appConfig.customCss) { // Inject users custom CSS, if present
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
"change-language-button": "Change App Language",
|
||||
"reset-settings-button": "Reset Local Settings",
|
||||
"app-info-button": "App Info",
|
||||
"backup-note": "It is recommend to make a backup of your configuration before making changes.",
|
||||
"backup-note": "It is recommended to make a backup of your configuration before making changes.",
|
||||
"reset-config-msg-l1": "This will remove all user settings from local storage, but won't effect your 'conf.yml' file.",
|
||||
"reset-config-msg-l2": "You should first backup any changes you've made locally, if you want to use them in the future.",
|
||||
"reset-config-msg-l3": "Are you sure you want to proceed?",
|
||||
@@ -204,7 +204,7 @@
|
||||
"start-editing-tooltip": "Enter the Interactive Editor",
|
||||
"edit-site-data-subheading": "Edit Site Data",
|
||||
"edit-page-info-btn": "Edit Page Info",
|
||||
"edit-page-info-tooltip": "App title, description, nav links, footer text, etc",
|
||||
"edit-page-info-tooltip": "App title, description, nav links, footer text, etc.",
|
||||
"edit-app-config-btn": "Edit App Config",
|
||||
"edit-app-config-tooltip": "All other app configuration options",
|
||||
"config-save-methods-subheading": "Config Saving Options",
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
{
|
||||
"home": {
|
||||
"no-results": "nessun risultato trovato",
|
||||
"no-data": "Nessun dato configurato"
|
||||
"no-results": "Nessun risultato trovato",
|
||||
"no-data": "Nessun dato configurato",
|
||||
"no-items-section": "Nessun elemento"
|
||||
},
|
||||
"search": {
|
||||
"search-label": "Ricerca",
|
||||
@@ -12,24 +13,25 @@
|
||||
"login": {
|
||||
"title": "Dashy",
|
||||
"username-label": "Nome utente",
|
||||
"password-label": "Parola d'ordine",
|
||||
"password-label": "Password",
|
||||
"login-button": "Login",
|
||||
"remember-me-label": "Ricordami per",
|
||||
"remember-me-never": "Mai",
|
||||
"remember-me-hour": "4 ore",
|
||||
"remember-me-day": "1 giorno",
|
||||
"remember-me-week": "1 settimana",
|
||||
"remember-me-long-time": "Molto tempo",
|
||||
"error-missing-username": "Nome utente mancante",
|
||||
"error-missing-password": "Password mancante",
|
||||
"error-incorrect-username": "Utente non trovato",
|
||||
"error-incorrect-password": "Password errata",
|
||||
"success-message": "Entrando...",
|
||||
"success-message": "Login in corso...",
|
||||
"logout-message": "Disconnesso",
|
||||
"already-logged-in-title": "Ha già effettuato il login",
|
||||
"already-logged-in-text": "Hai effettuato l'accesso come",
|
||||
"proceed-to-dashboard": "Procedi alla dashboard",
|
||||
"log-out-button": "Disconnettersi",
|
||||
"proceed-guest-button": "Procedi come ospite"
|
||||
"already-logged-in-text": "Hai effettuato il login come",
|
||||
"proceed-to-dashboard": "Vai alla dashboard",
|
||||
"log-out-button": "Disconnessione",
|
||||
"proceed-guest-button": "Procedi come Ospite"
|
||||
},
|
||||
"config": {
|
||||
"main-tab": "Menu principale",
|
||||
@@ -42,12 +44,12 @@
|
||||
"edit-css-button": "Modifica CSS personalizzato",
|
||||
"cloud-sync-button": "Abilita sincronizzazione cloud",
|
||||
"edit-cloud-sync-button": "Modifica sincronizzazione cloud",
|
||||
"rebuild-app-button": "Ricostruisci applicazione",
|
||||
"rebuild-app-button": "Ricompila l'applicazione",
|
||||
"change-language-button": "Cambia la lingua dell'app",
|
||||
"reset-settings-button": "Ripristina impostazioni locali",
|
||||
"app-info-button": "Informazioni sull'app",
|
||||
"backup-note": "Si consiglia di eseguire un backup della configurazione prima di apportare modifiche.",
|
||||
"reset-config-msg-l1": "Questo rimuoverà tutte le impostazioni utente dalla memoria locale, ma non influenzerà il tuo file 'conf.yml'.",
|
||||
"reset-config-msg-l1": "Questo rimuoverà tutte le impostazioni utente dalla memoria locale, ma non modificherà il tuo file 'conf.yml'.",
|
||||
"reset-config-msg-l2": "Dovresti prima eseguire il backup di tutte le modifiche apportate localmente, se desideri utilizzarle in futuro.",
|
||||
"reset-config-msg-l3": "Sei sicuro di voler procedere?",
|
||||
"data-cleared-msg": "Dati cancellati con successo",
|
||||
@@ -58,14 +60,14 @@
|
||||
"css-save-btn": "Salvare le modifiche",
|
||||
"css-note-label": "Nota",
|
||||
"css-note-l1": "Sarà necessario aggiornare la pagina affinché le modifiche abbiano effetto.",
|
||||
"css-note-l2": "Le sostituzioni di stili sono memorizzate solo localmente, quindi si consiglia di fare una copia del proprio CSS.",
|
||||
"css-note-l2": "Le personalizzazioni degli stili sono memorizzate solo localmente, quindi si consiglia di fare una copia del proprio CSS.",
|
||||
"css-note-l3": "Per rimuovere tutti gli stili personalizzati, elimina i contenuti e premi Salva modifiche"
|
||||
},
|
||||
"alternate-views": {
|
||||
"alternate-view-heading": "Cambia vista",
|
||||
"default": "Predefinito",
|
||||
"default": "Predefinita",
|
||||
"workspace": "Area di lavoro",
|
||||
"minimal": "Minimo"
|
||||
"minimal": "Minima"
|
||||
},
|
||||
"settings": {
|
||||
"theme-label": "Tema",
|
||||
@@ -73,22 +75,22 @@
|
||||
"layout-auto": "Auto",
|
||||
"layout-horizontal": "Orizzontale",
|
||||
"layout-vertical": "Verticale",
|
||||
"item-size-label": "Dimensione articolo",
|
||||
"item-size-label": "Dimensione Elemento",
|
||||
"item-size-small": "Piccolo",
|
||||
"item-size-medium": "medio",
|
||||
"item-size-medium": "Medio",
|
||||
"item-size-large": "Grande",
|
||||
"config-launcher-label": "config",
|
||||
"config-launcher-tooltip": "Aggiorna configurazione",
|
||||
"config-launcher-label": "Configurazione",
|
||||
"config-launcher-tooltip": "Aggiorna Configurazione",
|
||||
"sign-out-tooltip": "Disconnessione",
|
||||
"sign-in-tooltip": "Accesso",
|
||||
"sign-in-welcome": "Ciao {nome utente}!"
|
||||
"sign-in-welcome": "Ciao {username}!"
|
||||
},
|
||||
"updates": {
|
||||
"app-version-note": "Versione Dash",
|
||||
"up-to-date": "Aggiornato",
|
||||
"app-version-note": "Versione Dashy",
|
||||
"up-to-date": "Aggiornata",
|
||||
"out-of-date": "Aggiornamento disponibile",
|
||||
"unsupported-version-l1": "Stai utilizzando una versione non supportata di Dashy",
|
||||
"unsupported-version-l2": "Per la migliore esperienza e le patch di sicurezza recenti, aggiorna a"
|
||||
"unsupported-version-l2": "Per un miglior utilizzo e una maggior sicurezza, si consiglia l'aggiornamento alla versione"
|
||||
},
|
||||
"language-switcher": {
|
||||
"title": "Cambia lingua applicazione",
|
||||
@@ -97,52 +99,54 @@
|
||||
"success-msg": "Lingua aggiornata a"
|
||||
},
|
||||
"theme-maker": {
|
||||
"title": "Configuratore di temi",
|
||||
"title": "Configurazione dei Temi",
|
||||
"export-button": "Esporta variabili personalizzate",
|
||||
"reset-button": "Ripristina stili per",
|
||||
"reset-button": "Reset degli Stili per",
|
||||
"show-all-button": "Mostra tutte le variabili",
|
||||
"change-fonts-button": "Change Fonts",
|
||||
"save-button": "Salva",
|
||||
"cancel-button": "Annulla",
|
||||
"saved-toast": "{theme} aggiornato con successo",
|
||||
"copied-toast": "Dati del tema per {theme} copiati negli appunti",
|
||||
"reset-toast": "Colori personalizzati per {theme} rimossi"
|
||||
"copied-toast": "Dati del tema {theme} copiati negli appunti",
|
||||
"reset-toast": "Rimossi Colori personalizzati per {theme}"
|
||||
},
|
||||
"config-editor": {
|
||||
"save-location-label": "Salva l'indirizzo",
|
||||
"location-local-label": "Applicare localmente",
|
||||
"location-disk-label": "Scrivi modifiche al file di configurazione",
|
||||
"save-location-label": "Posizione del salvataggio",
|
||||
"location-local-label": "Salvataggio Locale",
|
||||
"location-disk-label": "Salva le modifiche nel file di configurazione",
|
||||
"save-button": "Salvare le modifiche",
|
||||
"preview-button": "Preview Changes",
|
||||
"valid-label": "La configurazione è valida",
|
||||
"status-success-msg": "Compito completato",
|
||||
"status-success-msg": "Attività completata",
|
||||
"status-fail-msg": "Attività fallita",
|
||||
"success-msg-disk": "File di configurazione scritto su disco con successo",
|
||||
"success-msg-local": "Modifiche locali salvate con successo",
|
||||
"success-note-l1": "L'app dovrebbe ricostruirsi automaticamente.",
|
||||
"success-msg-disk": "File di configurazione scritto correttamente su disco",
|
||||
"success-msg-local": "Modifiche locali salvate correttamente",
|
||||
"success-note-l1": "L'app dovrebbe ricompilarsi automaticamente.",
|
||||
"success-note-l2": "Questa operazione potrebbe richiedere fino a un minuto.",
|
||||
"success-note-l3": "Sarà necessario aggiornare la pagina affinché le modifiche abbiano effetto.",
|
||||
"error-msg-save-mode": "Seleziona una modalità di salvataggio: Locale o File",
|
||||
"error-msg-save-mode": "Seleziona una modalità di salvataggio: Localmente o in un File",
|
||||
"error-msg-cannot-save": "Si è verificato un errore durante il salvataggio della configurazione",
|
||||
"error-msg-bad-json": "Errore in JSON, probabilmente non valido",
|
||||
"error-msg-bad-json": "Errore nella struttura JSON, probabilmente non formattata correttamente",
|
||||
"warning-msg-validation": "Avviso di convalida",
|
||||
"not-admin-note": "Non puoi scrivere le modifiche su disco, perché non sei loggato come amministratore"
|
||||
"not-admin-note": "Non puoi scrivere le modifiche su disco, perché non sei autenticato come amministratore"
|
||||
},
|
||||
"app-rebuild": {
|
||||
"title": "Ricostruisci applicazione",
|
||||
"rebuild-note-l1": "È necessaria una ricostruzione affinché le modifiche scritte nel file conf.yml abbiano effetto.",
|
||||
"rebuild-note-l2": "Questo dovrebbe accadere automaticamente, ma in caso contrario, puoi attivarlo manualmente qui.",
|
||||
"title": "Ricompila l'applicazione",
|
||||
"rebuild-note-l1": "È necessaria una ricompilazione affinché le modifiche scritte nel file conf.yml abbiano effetto.",
|
||||
"rebuild-note-l2": "La ricompilazione dovrebbe avvenire automaticamente, in caso contrario, può essere avviata manualmente.",
|
||||
"rebuild-note-l3": "Questo non è richiesto per le modifiche memorizzate localmente.",
|
||||
"rebuild-button": "Inizia a costruire",
|
||||
"rebuilding-status-1": "Costruzione...",
|
||||
"rebuild-button": "Inizia la ricompilazione",
|
||||
"rebuilding-status-1": "Ricompilazione...",
|
||||
"rebuilding-status-2": "Questo potrebbe richiedere alcuni minuti",
|
||||
"error-permission": "Non hai l'autorizzazione per attivare questa azione",
|
||||
"error-permission": "Non hai l'autorizzazione per avviare questa azione",
|
||||
"success-msg": "Build completata con successo",
|
||||
"fail-msg": "Operazione di compilazione non riuscita",
|
||||
"reload-note": "È ora necessario ricaricare la pagina affinché le modifiche abbiano effetto",
|
||||
"fail-msg": "Operazione di ricompilazione non riuscita",
|
||||
"reload-note": "È necessario ricaricare la pagina affinché le modifiche abbiano effetto",
|
||||
"reload-button": "Ricarica la pagina"
|
||||
},
|
||||
"cloud-sync": {
|
||||
"title": "Backup e ripristino su cloud",
|
||||
"intro-l1": "Il backup e il ripristino su cloud sono una funzionalità opzionale che ti consente di caricare la tua configurazione su Internet e quindi ripristinarla su qualsiasi altro dispositivo o istanza di Dashy.",
|
||||
"title": "Backup e Restore su Cloud",
|
||||
"intro-l1": "Il backup e il restore su cloud consentono di salvare la tua configurazione su un server internet e quindi ripristinarla su qualsiasi altro dispositivo o istanza Dashy.",
|
||||
"intro-l2": "Tutti i dati sono completamente crittografati end-to-end con AES, utilizzando la tua password come chiave.",
|
||||
"intro-l3": "Per maggiori informazioni, vedere il",
|
||||
"backup-title-setup": "Fai un backup",
|
||||
@@ -152,11 +156,11 @@
|
||||
"backup-button-setup": "Backup",
|
||||
"backup-button-update": "Aggiorna backup",
|
||||
"backup-id-label": "Il tuo ID di backup",
|
||||
"backup-id-note": "Questo viene utilizzato per ripristinare dai backup in un secondo momento. Quindi tienilo, insieme alla tua password, in un posto sicuro.",
|
||||
"backup-id-note": "ID utilizzato per il restore dal cloud. Conservarlo, insieme alla password, in un posto sicuro.",
|
||||
"restore-title": "Ripristina un backup",
|
||||
"restore-id-label": "Ripristina ID",
|
||||
"restore-password-label": "Parola d'ordine",
|
||||
"restore-button": "Ristabilire",
|
||||
"restore-password-label": "Password",
|
||||
"restore-button": "Restore",
|
||||
"backup-missing-password": "Password mancante",
|
||||
"backup-error-unknown": "Impossibile elaborare la richiesta",
|
||||
"backup-error-password": "Password errata. Inserisci la tua password attuale.",
|
||||
@@ -164,9 +168,128 @@
|
||||
"restore-success-msg": "Configurazione ripristinata con successo"
|
||||
},
|
||||
"menu": {
|
||||
"sametab": "Apri nella scheda corrente",
|
||||
"newtab": "Apri in una nuova scheda",
|
||||
"modal": "Apri in modalità pop-up",
|
||||
"workspace": "Apri nella vista dell'area di lavoro"
|
||||
"open-section-title": "Open In",
|
||||
"sametab": "Tab Attuale",
|
||||
"newtab": "Nuovo Tab",
|
||||
"modal": "Pop-Up",
|
||||
"workspace": "Workspace",
|
||||
"options-section-title": "Opzioni",
|
||||
"edit-item": "Edita",
|
||||
"move-item": "Copia/Sposta",
|
||||
"remove-item": "Rimuovi"
|
||||
},
|
||||
"context-menus": {
|
||||
"item": {
|
||||
"open-section-title": "Apri In",
|
||||
"sametab": "Tab Attuale",
|
||||
"newtab": "Nuovo Tab",
|
||||
"modal": "Pop-Up",
|
||||
"workspace": "Workspace",
|
||||
"clipboard": "Copia negli Appunti",
|
||||
"options-section-title": "Opzioni",
|
||||
"edit-item": "Edita",
|
||||
"move-item": "Copia/Sposta",
|
||||
"remove-item": "Rimuovi",
|
||||
"copied-toast": "URL copiata negli appunti"
|
||||
},
|
||||
"section": {
|
||||
"open-section": "Apri Sezione",
|
||||
"edit-section": "Edita",
|
||||
"move-section": "Sposta in",
|
||||
"remove-section": "Rimuovi"
|
||||
}
|
||||
},
|
||||
"interactive-editor": {
|
||||
"menu": {
|
||||
"start-editing-tooltip": "Apri l'Editor Interattivo",
|
||||
"edit-site-data-subheading": "Modifica i dati del sito",
|
||||
"edit-page-info-btn": "Modifica le informazioni della pagina",
|
||||
"edit-page-info-tooltip": "Titolo App, descrizione, link navigazione, piè di pagina, etc.",
|
||||
"edit-app-config-btn": "Modifica Configurazione App",
|
||||
"edit-app-config-tooltip": "Altre opzioni di configurazione",
|
||||
"config-save-methods-subheading": "Opzioni di salvataggio della configurazione",
|
||||
"save-locally-btn": "Salva Localmente",
|
||||
"save-locally-tooltip": "Save localmente, nell'area dati del browser. Il file di configurazione non verrà modificato, le modifiche sono salvate solo su questo dispositivo",
|
||||
"save-disk-btn": "Salva su Disco",
|
||||
"save-disk-tooltip": "Salva la configurazione su disco, nel file conf.yml. Verrà effettuato un backup, la configurazione attuale verrà sovrascritta",
|
||||
"export-config-btn": "Esporta la Configurazione",
|
||||
"export-config-tooltip": "Visualizza ed esporta la configurazione, in un file o negli appunti",
|
||||
"cloud-backup-btn": "Backup nel Cloud",
|
||||
"cloud-backup-tooltip": "Salva il backup criptato della configurazione nel cloud",
|
||||
"edit-raw-config-btn": "Modifica la configurazione nativa",
|
||||
"edit-raw-config-tooltip": "Visualizza e modifica la configurazione nativa con l'editor JSON",
|
||||
"cancel-changes-btn": "Annulla le Modifiche",
|
||||
"cancel-changes-tooltip": "Annulla le modifiche ed esci dalla modalità modifica. La configurazione attuale non verrà modificata",
|
||||
"edit-mode-name": "Modalità Modifica",
|
||||
"edit-mode-subtitle": "Sei in modalità modifica",
|
||||
"edit-mode-description": "Puoi modificare la configurazione e verificare il risultato, ma prima del salvataggio, nessun cambiamento verrà preservato",
|
||||
"save-stage-btn": "Salva",
|
||||
"cancel-stage-btn": "Annulla"
|
||||
},
|
||||
"edit-item": {
|
||||
"missing-title-err": "Il titolo è obbligatorio"
|
||||
},
|
||||
"edit-section": {
|
||||
"edit-section-title": "Modifica Sezione",
|
||||
"add-section-title": "Aggiungi Nuova Sezione",
|
||||
"edit-tooltip": "Seleziona per modificare, tasto destro per altre opzioni",
|
||||
"remove-confirm": "Sei sicuro di voler rimuovere questa sezione? Questa scelta può essere rivista successivamente."
|
||||
},
|
||||
"edit-app-config": {
|
||||
"warning-msg-title": "Procedi con cautela",
|
||||
"warning-msg-l1": "Queste opzioni sono necessarie per la configurazione avanzata dell'app",
|
||||
"warning-msg-l2": "Se non sei sicuro di qualche opzione, controlla la",
|
||||
"warning-msg-docs": "documentazione",
|
||||
"warning-msg-l3": "per evitare spiacevoli conseguenze."
|
||||
},
|
||||
"export": {
|
||||
"export-title": "Esporta Configurazione",
|
||||
"copy-clipboard-btn": "Copia negli Appunti",
|
||||
"copy-clipboard-tooltip": "Copia la configurazione negli appunti, in formato YAML",
|
||||
"download-file-btn": "Salva file",
|
||||
"download-file-tooltip": "Salva la configurazione dell'app in un file YAML",
|
||||
"view-title": "View Config"
|
||||
}
|
||||
},
|
||||
"widgets": {
|
||||
"general": {
|
||||
"loading": "Caricamento...",
|
||||
"show-more": "Dettagli",
|
||||
"show-less": "Minori dettagli",
|
||||
"open-link": "Continua Lettura"
|
||||
},
|
||||
"pi-hole": {
|
||||
"status-heading": "Stato"
|
||||
},
|
||||
"stat-ping": {
|
||||
"up": "Online",
|
||||
"down": "Offline"
|
||||
},
|
||||
"net-data": {
|
||||
"cpu-chart-title": "Utilizzo CPU",
|
||||
"mem-chart-title": "Utilizzo Memoria",
|
||||
"mem-breakdown-title": "Dettaglio Utilizzo Memoria",
|
||||
"load-chart-title": "Carico del Sistema"
|
||||
},
|
||||
"glances": {
|
||||
"disk-space-free": "Utilizzabile",
|
||||
"disk-space-used": "Utilizzato",
|
||||
"disk-mount-point": "Mount Point",
|
||||
"disk-file-system": "File System",
|
||||
"disk-io-read": "Lettura",
|
||||
"disk-io-write": "Scrittura",
|
||||
"system-load-desc": "Numero di processi in attesa nella coda di esecuzione, calcolati sulla media di tutti i core"
|
||||
},
|
||||
"system-info": {
|
||||
"uptime": "Uptime"
|
||||
},
|
||||
"flight-data": {
|
||||
"arrivals": "Arrivi",
|
||||
"departures": "Partenze"
|
||||
},
|
||||
"tfl-status": {
|
||||
"good-service-all": "Buon servizio su tutte le Linee",
|
||||
"good-service-rest": "Buon servizio su tutte le altre Linee"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -231,8 +231,8 @@ export default {
|
||||
const newItem = item;
|
||||
newItem.id = this.itemId;
|
||||
if (newItem.hotkey) newItem.hotkey = parseInt(newItem.hotkey, 10);
|
||||
const strToTags = (str) => {
|
||||
const tagArr = str.split(',');
|
||||
const strToTags = (tags) => {
|
||||
const tagArr = (typeof tags === 'string') ? tags.split(',') : tags;
|
||||
return tagArr.map((tag) => tag.trim().toLowerCase().replace(/[^a-z0-9]+/, ''));
|
||||
};
|
||||
const strToBool = (str) => {
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
<h3>{{ title }}</h3>
|
||||
<EditModeIcon v-if="isEditMode" @click="openEditModal"
|
||||
v-tooltip="editTooltip()" class="edit-mode-item" />
|
||||
<OpenIcon @click.prevent.stop="openContextMenu" @contextmenu.prevent
|
||||
class="edit-mode-item" />
|
||||
</label>
|
||||
<div class="collapsible-content">
|
||||
<div class="content-inner">
|
||||
@@ -31,6 +33,7 @@
|
||||
import { localStorageKeys } from '@/utils/defaults';
|
||||
import Icon from '@/components/LinkItems/ItemIcon.vue';
|
||||
import EditModeIcon from '@/assets/interface-icons/interactive-editor-edit-mode.svg';
|
||||
import OpenIcon from '@/assets/interface-icons/config-open-settings.svg';
|
||||
|
||||
export default {
|
||||
name: 'CollapsableContainer',
|
||||
@@ -48,6 +51,7 @@ export default {
|
||||
components: {
|
||||
Icon,
|
||||
EditModeIcon,
|
||||
OpenIcon,
|
||||
},
|
||||
computed: {
|
||||
isEditMode() {
|
||||
@@ -244,6 +248,8 @@ export default {
|
||||
float: right;
|
||||
right: 0.5rem;
|
||||
top: 0.5rem;
|
||||
margin-left: 0.2rem;
|
||||
margin-right: 0.2rem;
|
||||
}
|
||||
|
||||
/* Makes sections fill available space */
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
classes="dashy-modal">
|
||||
<div slot="top-right" @click="hide()">Close</div>
|
||||
<a @click="hide()" class="close-button" title="Close">x</a>
|
||||
<iframe v-if="url" :src="url" @keydown.esc="close" class="frame"/>
|
||||
<iframe v-if="url" :src="url" @keydown.esc="close" class="frame" allow="fullscreen" />
|
||||
<div v-else class="no-url">No URL Specified</div>
|
||||
</modal>
|
||||
</template>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<template ref="container">
|
||||
<div :class="`item-wrapper wrap-size-${itemSize}`">
|
||||
<a @click="beforeLaunchItem"
|
||||
<a @click="itemClicked"
|
||||
@mouseup.right="openContextMenu"
|
||||
@contextmenu.prevent
|
||||
:href="hyperLinkHref"
|
||||
:href="url"
|
||||
:target="anchorTarget"
|
||||
:class="`item ${makeClassList}`"
|
||||
v-tooltip="getTooltipOptions()"
|
||||
@@ -91,6 +91,7 @@ export default {
|
||||
statusCheckInterval: Number, // Num seconds beteween repeating checks
|
||||
statusCheckAllowInsecure: Boolean, // Status check ignore SSL certs
|
||||
statusCheckAcceptCodes: String, // Allow status checks to pass with a code other than 200
|
||||
statusCheckMaxRedirects: Number, // Specify max number of redirects
|
||||
parentSectionTitle: String, // Title of parent section (for add new)
|
||||
isAddNew: Boolean, // Only set if 'fake' item used as Add New button
|
||||
},
|
||||
@@ -136,30 +137,6 @@ export default {
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
/* Called when an item is clicked, manages the opening of modal & resets the search field */
|
||||
beforeLaunchItem(e) {
|
||||
if (this.isEditMode) { // If in edit mode, open settings, don't launch app
|
||||
this.openItemSettings();
|
||||
return;
|
||||
}
|
||||
if (e.altKey) {
|
||||
e.preventDefault();
|
||||
this.launchItem('modal');
|
||||
} else if (this.accumulatedTarget === 'modal') {
|
||||
this.launchItem('modal');
|
||||
} else if (this.accumulatedTarget === 'workspace') {
|
||||
this.launchItem('workspace');
|
||||
} else if (this.accumulatedTarget === 'clipboard') {
|
||||
this.launchItem('clipboard');
|
||||
}
|
||||
// Clear search bar
|
||||
this.$emit('itemClicked');
|
||||
// Update the most/ last used ledger, for smart-sorting
|
||||
if (!this.appConfig.disableSmartSort) {
|
||||
this.incrementMostUsedCount(this.id);
|
||||
this.incrementLastUsedCount(this.id);
|
||||
}
|
||||
},
|
||||
/* Returns configuration object for the tooltip */
|
||||
getTooltipOptions() {
|
||||
if (!this.description && !this.provider) return {}; // If no description, then skip
|
||||
@@ -179,7 +156,6 @@ export default {
|
||||
classes: `item-description-tooltip tooltip-is-${this.itemSize}`,
|
||||
};
|
||||
},
|
||||
/* Open the Edit Item modal form */
|
||||
openItemSettings() {
|
||||
this.editMenuOpen = true;
|
||||
this.contextMenuOpen = false;
|
||||
|
||||
@@ -45,11 +45,11 @@ export default {
|
||||
return this.$store.getters.appConfig;
|
||||
},
|
||||
/* Determines the type of icon */
|
||||
iconType: function iconType() {
|
||||
iconType() {
|
||||
return this.determineImageType(this.icon);
|
||||
},
|
||||
/* Gets the icon path, dependent on icon type */
|
||||
iconPath: function iconPath() {
|
||||
iconPath() {
|
||||
if (this.broken) return this.getFallbackIcon();
|
||||
return this.getIconPath(this.icon, this.url);
|
||||
},
|
||||
@@ -176,7 +176,7 @@ export default {
|
||||
},
|
||||
/* Fetches the path of local images, from Docker container */
|
||||
getLocalImagePath(img) {
|
||||
return `${iconCdns.localPath}/${img}`;
|
||||
return `/${iconCdns.localPath}/${img}`;
|
||||
},
|
||||
/* Formats the URL for fetching the generative icons */
|
||||
getGenerativeIcon(url, cdn) {
|
||||
|
||||
@@ -54,6 +54,7 @@
|
||||
:statusCheckInterval="statusCheckInterval"
|
||||
:statusCheckAllowInsecure="item.statusCheckAllowInsecure"
|
||||
:statusCheckAcceptCodes="item.statusCheckAcceptCodes"
|
||||
:statusCheckMaxRedirects="item.statusCheckMaxRedirects"
|
||||
@itemClicked="$emit('itemClicked')"
|
||||
@triggerModal="triggerModal"
|
||||
:isAddNew="false"
|
||||
@@ -166,7 +167,7 @@ export default {
|
||||
return this.$store.state.editMode;
|
||||
},
|
||||
itemSize() {
|
||||
return this.$store.getters.iconSize;
|
||||
return this.displayData.itemSize || this.$store.getters.iconSize;
|
||||
},
|
||||
sortOrder() {
|
||||
return this.displayData.sortBy || defaultSortOrder;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<template>
|
||||
<transition name="slide">
|
||||
<div class="context-menu" v-if="show && !isMenuDisabled"
|
||||
:style="posX && posY ? `top:${posY}px;left:${posX}px;` : ''">
|
||||
:style="posX && posY ? calcPosition() : ''">
|
||||
<!-- Open Options -->
|
||||
<ul class="menu-section">
|
||||
<li @click="openSection()">
|
||||
@@ -59,6 +59,13 @@ export default {
|
||||
removeSection() {
|
||||
this.$emit('removeSection');
|
||||
},
|
||||
calcPosition() {
|
||||
const bounds = this.$parent.$el.getBoundingClientRect();
|
||||
const left = this.posX < (bounds.right + bounds.left) / 2;
|
||||
const position = `top:${this.posY}px;${left ? 'left' : 'right'}:\
|
||||
${left ? this.posX : document.documentElement.clientWidth - this.posX}px;`;
|
||||
return position;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
:enableStatusCheck="shouldEnableStatusCheck(item.statusCheck)"
|
||||
:statusCheckAllowInsecure="item.statusCheckAllowInsecure"
|
||||
:statusCheckAcceptCodes="item.statusCheckAcceptCodes"
|
||||
:statusCheckMaxRedirects="item.statusCheckMaxRedirects"
|
||||
:statusCheckInterval="getStatusCheckInterval()"
|
||||
@itemClicked="$emit('itemClicked')"
|
||||
@triggerModal="triggerModal"
|
||||
|
||||
@@ -5,15 +5,23 @@
|
||||
@click="navVisible = !navVisible"
|
||||
/>
|
||||
<nav id="nav" v-if="navVisible">
|
||||
<router-link
|
||||
v-for="(link, index) in links"
|
||||
:key="index"
|
||||
:to="link.path"
|
||||
:href="link.path"
|
||||
:target="isUrl(link.path) ? '_blank' : ''"
|
||||
rel="noopener noreferrer"
|
||||
class="nav-item"
|
||||
>{{link.title}}</router-link>
|
||||
<!-- Render either router-link or anchor, depending if internal / external link -->
|
||||
<template v-for="(link, index) in links">
|
||||
<router-link v-if="!isUrl(link.path)"
|
||||
:key="index"
|
||||
:to="link.path"
|
||||
class="nav-item"
|
||||
>{{link.title}}
|
||||
</router-link>
|
||||
<a v-else
|
||||
:key="index"
|
||||
:href="link.path"
|
||||
:target="determineTarget(link)"
|
||||
class="nav-item"
|
||||
rel="noopener noreferrer"
|
||||
>{{link.title}}
|
||||
</a>
|
||||
</template>
|
||||
</nav>
|
||||
</div>
|
||||
</template>
|
||||
@@ -43,6 +51,16 @@ export default {
|
||||
return screenWidth && screenWidth < 600;
|
||||
},
|
||||
isUrl: (str) => new RegExp(/(http|https):\/\/(\S+)(:[0-9]+)?/).test(str),
|
||||
determineTarget(link) {
|
||||
if (!link.target) return '_blank';
|
||||
switch (link.target) {
|
||||
case 'sametab': return '_self';
|
||||
case 'newtab': return '_blank';
|
||||
case 'parent': return '_parent';
|
||||
case 'top': return '_top';
|
||||
default: return undefined;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
@@ -476,10 +476,13 @@ export default {
|
||||
/* Returns users specified widget options, or empty object */
|
||||
widgetOptions() {
|
||||
const options = this.widget.options || {};
|
||||
const timeout = this.widget.timeout || 500;
|
||||
const useProxy = this.appConfig.widgetsAlwaysUseProxy || !!this.widget.useProxy;
|
||||
const updateInterval = this.widget.updateInterval !== undefined
|
||||
? this.widget.updateInterval : null;
|
||||
return { useProxy, updateInterval, ...options };
|
||||
return {
|
||||
timeout, useProxy, updateInterval, ...options,
|
||||
};
|
||||
},
|
||||
/* A unique string to reference the widget by */
|
||||
widgetRef() {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div class="web-content" :id="id">
|
||||
<iframe :src="url" />
|
||||
<iframe :src="url" allow="fullscreen" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -14,8 +14,7 @@ export default {
|
||||
credentials() {
|
||||
if (this.options.username && this.options.password) {
|
||||
const stringifiedUser = `${this.options.username}:${this.options.password}`;
|
||||
const headers = { Authorization: `Basic ${window.btoa(stringifiedUser)}` };
|
||||
return { headers };
|
||||
return { Authorization: `Basic ${window.btoa(stringifiedUser)}` };
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
@@ -106,8 +106,9 @@ const WidgetMixin = {
|
||||
const CustomHeaders = options || null;
|
||||
const headers = this.useProxy
|
||||
? { 'Target-URL': endpoint, CustomHeaders: JSON.stringify(CustomHeaders) } : CustomHeaders;
|
||||
const timeout = this.options.timeout || 500;
|
||||
const requestConfig = {
|
||||
method, url, headers, data,
|
||||
method, url, headers, data, timeout,
|
||||
};
|
||||
// Make request
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
11
src/store.js
11
src/store.js
@@ -1,6 +1,8 @@
|
||||
/* eslint-disable no-param-reassign, prefer-destructuring */
|
||||
import Vue from 'vue';
|
||||
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';
|
||||
@@ -14,6 +16,7 @@ Vue.use(Vuex);
|
||||
const {
|
||||
INITIALIZE_CONFIG,
|
||||
SET_CONFIG,
|
||||
SET_REMOTE_CONFIG,
|
||||
SET_MODAL_OPEN,
|
||||
SET_LANGUAGE,
|
||||
SET_ITEM_LAYOUT,
|
||||
@@ -38,6 +41,7 @@ const {
|
||||
const store = new Vuex.Store({
|
||||
state: {
|
||||
config: {},
|
||||
remoteConfig: {}, // The configuration stored on the server
|
||||
editMode: false, // While true, the user can drag and edit items + sections
|
||||
modalOpen: false, // KB shortcut functionality will be disabled when modal is open
|
||||
navigateConfToTab: undefined, // Used to switch active tab in config modal
|
||||
@@ -126,6 +130,9 @@ const store = new Vuex.Store({
|
||||
[SET_CONFIG](state, config) {
|
||||
state.config = config;
|
||||
},
|
||||
[SET_REMOTE_CONFIG](state, config) {
|
||||
state.remoteConfig = config;
|
||||
},
|
||||
[SET_LANGUAGE](state, lang) {
|
||||
const newConfig = state.config;
|
||||
newConfig.appConfig.language = lang;
|
||||
@@ -271,7 +278,9 @@ const store = new Vuex.Store({
|
||||
},
|
||||
actions: {
|
||||
/* Called when app first loaded. Reads config and sets state */
|
||||
[INITIALIZE_CONFIG]({ commit }) {
|
||||
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);
|
||||
|
||||
@@ -14,30 +14,34 @@ import {
|
||||
} from '@/utils/defaults';
|
||||
import ErrorHandler from '@/utils/ErrorHandler';
|
||||
import { applyItemId } from '@/utils/SectionHelpers';
|
||||
import conf from '../../public/conf.yml';
|
||||
import $store from '@/store';
|
||||
|
||||
import buildConf from '../../public/conf.yml';
|
||||
|
||||
export default class ConfigAccumulator {
|
||||
constructor() {
|
||||
this.conf = conf;
|
||||
this.conf = $store.state.remoteConfig;
|
||||
}
|
||||
|
||||
/* App Config */
|
||||
appConfig() {
|
||||
let appConfigFile = {};
|
||||
// Set app config from file
|
||||
if (this.conf) appConfigFile = this.conf.appConfig || {};
|
||||
if (this.conf) appConfigFile = this.conf.appConfig || buildConf.appConfig || {};
|
||||
// Fill in defaults if anything missing
|
||||
let usersAppConfig = defaultAppConfig;
|
||||
if (localStorage[localStorageKeys.APP_CONFIG]) {
|
||||
usersAppConfig = JSON.parse(localStorage[localStorageKeys.APP_CONFIG]);
|
||||
} else if (appConfigFile !== {}) {
|
||||
} else if (Object.keys(appConfigFile).length > 0) {
|
||||
usersAppConfig = appConfigFile;
|
||||
}
|
||||
// Some settings have their own local storage keys, apply them here
|
||||
usersAppConfig.layout = localStorage[localStorageKeys.LAYOUT_ORIENTATION]
|
||||
|| appConfigFile.layout || defaultLayout;
|
||||
usersAppConfig.iconSize = localStorage[localStorageKeys.ICON_SIZE]
|
||||
|| appConfigFile.iconSize || defaultIconSize;
|
||||
usersAppConfig.layout = appConfigFile.layout
|
||||
|| localStorage[localStorageKeys.LAYOUT_ORIENTATION]
|
||||
|| defaultLayout;
|
||||
usersAppConfig.iconSize = appConfigFile.iconSize
|
||||
|| localStorage[localStorageKeys.ICON_SIZE]
|
||||
|| defaultIconSize;
|
||||
// Don't let users modify users locally
|
||||
if (appConfigFile.auth) usersAppConfig.auth = appConfigFile.auth;
|
||||
// All done, return final appConfig object
|
||||
|
||||
@@ -36,6 +36,18 @@
|
||||
},
|
||||
"path": {
|
||||
"type": "string"
|
||||
},
|
||||
"target": {
|
||||
"title": "Opening Method",
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"newtab",
|
||||
"sametab",
|
||||
"parent",
|
||||
"top"
|
||||
],
|
||||
"default": "newtab",
|
||||
"description": "Where / how the item is opened when it's clicked"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -789,11 +801,16 @@
|
||||
"description": "Allows for running status checks on insecure content/ non-HTTPS apps. Prevents checks failing for non-SSL sites"
|
||||
},
|
||||
"statusCheckAcceptCodes": {
|
||||
"title": "Accepted HTTP Status Codes",
|
||||
"title": "Status Check - Accepted HTTP Codes",
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"description": "If your service's response code is anything other than 2xx, then you can opt to specify an alternative success code"
|
||||
},
|
||||
"statusCheckMaxRedirects": {
|
||||
"title": "Status Check - Max Redirects",
|
||||
"type": "number",
|
||||
"default": "0",
|
||||
"description": "If your service redirects to another page, and you would like status checks to follow redirects, then specify the maximum number of redirects here"
|
||||
},
|
||||
"color": {
|
||||
"title": "Custom Color",
|
||||
"type": "string",
|
||||
@@ -820,6 +837,21 @@
|
||||
"type": "string",
|
||||
"description": "The type of widget to use, see docs for supported options"
|
||||
},
|
||||
"updateInterval": {
|
||||
"title": "Update Interval",
|
||||
"type": "number",
|
||||
"description": "Specified in seconds. If set, widget data will be re-retched at this interval to display up-to-date data"
|
||||
},
|
||||
"timeout": {
|
||||
"title": "Timeout",
|
||||
"type": "number",
|
||||
"description": "Specified in milliseconds. If set, request will timeout after the specified interval. Defaults to 500/ half a sec"
|
||||
},
|
||||
"useProxy": {
|
||||
"title": "Use Proxy?",
|
||||
"type": "boolean",
|
||||
"description": "If set to true, request will be proxied through the backend. Requires the Node server to be running"
|
||||
},
|
||||
"options": {
|
||||
"title": "Widget Options",
|
||||
"type": "object",
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import axios from 'axios';
|
||||
import yaml from 'js-yaml';
|
||||
import { register } from 'register-service-worker';
|
||||
import { sessionStorageKeys } from '@/utils/defaults';
|
||||
import { statusMsg, statusErrorMsg } from '@/utils/CoolConsole';
|
||||
import conf from '../../public/conf.yml';
|
||||
|
||||
/* Sets a local storage item with the state from the SW lifecycle */
|
||||
const setSwStatus = (swStateToSet) => {
|
||||
@@ -31,7 +32,8 @@ const setSwStatus = (swStateToSet) => {
|
||||
* Disable if not running in production
|
||||
* Or disable if user specified to disable
|
||||
*/
|
||||
const shouldEnableServiceWorker = () => {
|
||||
const shouldEnableServiceWorker = async () => {
|
||||
const conf = yaml.load((await axios.get('conf.yml')).data);
|
||||
if (conf && conf.appConfig && conf.appConfig.enableServiceWorker) {
|
||||
setSwStatus({ disabledByUser: false });
|
||||
return true;
|
||||
@@ -51,8 +53,8 @@ const printSwStatus = (msg) => {
|
||||
const swUrl = `${process.env.BASE_URL || '/'}service-worker.js`;
|
||||
|
||||
/* If service worker enabled, then register it, and print message when status changes */
|
||||
const registerServiceWorker = () => {
|
||||
if (shouldEnableServiceWorker()) {
|
||||
const registerServiceWorker = async () => {
|
||||
if (await shouldEnableServiceWorker()) {
|
||||
register(swUrl, {
|
||||
ready() {
|
||||
setSwStatus({ ready: true });
|
||||
|
||||
@@ -14,7 +14,7 @@ class KeycloakAuth {
|
||||
const { auth } = getAppConfig();
|
||||
const { serverUrl, realm, clientId } = auth.keycloak;
|
||||
const initOptions = {
|
||||
url: `${serverUrl}/auth`, realm, clientId, onLoad: 'login-required',
|
||||
url: `${serverUrl}`, realm, clientId, onLoad: 'login-required',
|
||||
};
|
||||
|
||||
this.keycloakClient = Keycloak(initOptions);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
const KEY_NAMES = [
|
||||
'INITIALIZE_CONFIG',
|
||||
'SET_CONFIG',
|
||||
'SET_REMOTE_CONFIG',
|
||||
'SET_MODAL_OPEN',
|
||||
'SET_LANGUAGE',
|
||||
'SET_EDIT_MODE',
|
||||
|
||||
@@ -32,6 +32,7 @@ export const LoadExternalTheme = function th() {
|
||||
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) => {
|
||||
|
||||
@@ -201,7 +201,7 @@ module.exports = {
|
||||
generativeFallback: 'https://evatar.io/{icon}',
|
||||
localPath: './item-icons',
|
||||
faviconName: 'favicon.ico',
|
||||
homeLabIcons: 'https://raw.githubusercontent.com/WalkxCode/dashboard-icons/master/png/{icon}.png',
|
||||
homeLabIcons: 'https://raw.githubusercontent.com/walkxhub/dashboard-icons/master/png/{icon}.png',
|
||||
homeLabIconsFallback: 'https://raw.githubusercontent.com/NX211/homer-icons/master/png/{icon}.png',
|
||||
},
|
||||
/* API endpoints for widgets that need to fetch external data */
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
</router-link>
|
||||
</div>
|
||||
<!-- Main content, section for each group of items -->
|
||||
<div v-if="checkTheresData(sections)"
|
||||
<div v-if="checkTheresData(sections) || isEditMode"
|
||||
:class="`item-group-container `
|
||||
+ `orientation-${layout} `
|
||||
+ `item-size-${itemSizeBound} `
|
||||
@@ -50,7 +50,7 @@
|
||||
<AddNewSection v-if="isEditMode" />
|
||||
</div>
|
||||
<!-- Show message when there's no data to show -->
|
||||
<div v-if="checkIfResults()" class="no-data">
|
||||
<div v-if="checkIfResults() && !isEditMode" class="no-data">
|
||||
{{searchValue ? $t('home.no-results') : $t('home.no-data')}}
|
||||
</div>
|
||||
<!-- Show banner at bottom of screen, for Saving config changes -->
|
||||
@@ -213,7 +213,7 @@ export default {
|
||||
overflow: auto;
|
||||
@extend .scroll-bar;
|
||||
@include monitor-up {
|
||||
max-width: 1400px;
|
||||
max-width: 85%;
|
||||
}
|
||||
|
||||
/* Options for alternate layouts, triggered by buttons */
|
||||
|
||||
Reference in New Issue
Block a user