Skip to content

Commit bb0058a

Browse files
committed
Implemented settings backup/restore
1 parent 0be304b commit bb0058a

File tree

6 files changed

+231
-14
lines changed

6 files changed

+231
-14
lines changed

src/lib/components/furniture/Header.svelte

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<script lang="ts">
22
import { site } from '$lib/constants/site';
3-
import { SITE_ICON } from '$lib/config/customizable-settings';
3+
import { SITE_ICON, SITE_DESCRIPTION } from '$lib/config/customizable-settings';
44
import Icon from '$lib/components/global/Icon.svelte';
55
import GlobalSearch from '$lib/components/global/GlobalSearch.svelte';
66
import BurgerMenu from '$lib/components/furniture/BurgerMenu.svelte';
@@ -31,7 +31,7 @@
3131
</div>
3232
<div>
3333
<h1><a href="/">{site.title}</a></h1>
34-
<p class="subtitle">The sysadmin's Swiss Army knife</p>
34+
<p class="subtitle">{SITE_DESCRIPTION || "The sysadmin's Swiss Army knife"}</p>
3535
</div>
3636
</div>
3737

src/lib/components/furniture/SettingsPanel.svelte

Lines changed: 174 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
import { primaryColor } from '$lib/stores/primaryColor';
1212
import { fontScale, fontScaleOptions, type FontScaleLevel } from '$lib/stores/fontScale';
1313
import { storage } from '$lib/utils/localStorage';
14+
import * as config from '$lib/config/customizable-settings';
15+
import SegmentedControl from '$lib/components/global/SegmentedControl.svelte';
1416
1517
interface Props {
1618
standalone?: boolean;
@@ -46,9 +48,12 @@
4648
let siteCustomizationErrors = $state<string[]>([]);
4749
let lastStoreValue = $state('');
4850
let lastColorStoreValue = $state('');
51+
let envVarsCopied = $state(false);
52+
let envFormat = $state<'env' | 'docker'>('env');
53+
let showExportSettings = $state(false);
4954
5055
// Constants
51-
const PRIMARY_A11Y_OPTIONS = ['reduce-motion'];
56+
const PRIMARY_A11Y_OPTIONS: string[] = [];
5257
const COLOR_PALETTE = [
5358
'#1a75ff',
5459
'#7711ff',
@@ -78,6 +83,9 @@
7883
const primaryOptions = $derived(
7984
$accessibilitySettings.options.filter((opt) => PRIMARY_A11Y_OPTIONS.includes(opt.id)),
8085
);
86+
// const primaryOptions = $derived(
87+
// $accessibilitySettings.options.filter((opt) => PRIMARY_A11Y_OPTIONS.includes(opt.id)),
88+
// );
8189
const additionalOptions = $derived(
8290
$accessibilitySettings.options.filter((opt) => !PRIMARY_A11Y_OPTIONS.includes(opt.id)),
8391
);
@@ -174,7 +182,44 @@
174182
window.location.reload();
175183
}
176184
},
185+
186+
copyEnvVars: async () => {
187+
try {
188+
await navigator.clipboard.writeText(envVarsString);
189+
envVarsCopied = true;
190+
setTimeout(() => (envVarsCopied = false), 2000);
191+
} catch {
192+
const textArea = document.createElement('textarea');
193+
textArea.value = envVarsString;
194+
document.body.appendChild(textArea);
195+
textArea.select();
196+
document.execCommand('copy');
197+
document.body.removeChild(textArea);
198+
envVarsCopied = true;
199+
setTimeout(() => (envVarsCopied = false), 2000);
200+
}
201+
},
177202
};
203+
204+
// Live-updating env vars string - reacts to store changes
205+
const envVarsString = $derived.by(() => {
206+
// Trigger reactivity on store changes
207+
void $currentTheme;
208+
void $currentFontScale;
209+
void $currentHomepageLayout;
210+
void $currentNavbarDisplay;
211+
void $primaryColor;
212+
void $siteCustomization;
213+
214+
const envVars = config.getUserSettingsList();
215+
216+
if (envFormat === 'docker') {
217+
const filtered = envVars.filter(({ value }) => value !== '');
218+
return 'environment:\n' + filtered.map(({ name, value }) => ` - ${name}=${value}`).join('\n');
219+
}
220+
221+
return envVars.map(({ name, value }) => `${name}='${value}'`).join('\n');
222+
});
178223
</script>
179224

180225
{#snippet validationMessages(errors: string[], warnings: string[])}
@@ -515,6 +560,55 @@
515560
</div>
516561
{/if}
517562

563+
<!-- Docs for saving settings -->
564+
{#if standalone}
565+
<div class="settings-section saving-section">
566+
<h3>Syncing Settings and Backup/Restore</h3>
567+
<p class="line-1">
568+
Your settings are saved in your browser's local storage, and so they will be retained even after you quit the
569+
app.
570+
</p>
571+
<p>
572+
Since we don't require login/signup to use the app, there is currently no way to automatically sync your
573+
settings across devices. But if you're self-hosting Networking Toolbox, you can apply settings in your config,
574+
by including the following environment variables. This way, you're settings will be applied to all users across
575+
all devices.
576+
</p>
577+
<button
578+
class="show-more-btn"
579+
onclick={() => (showExportSettings = !showExportSettings)}
580+
aria-expanded={showExportSettings}
581+
>
582+
<Icon name={showExportSettings ? 'chevron-up' : 'chevron-down'} size="sm" />
583+
<span>Export Settings</span>
584+
</button>
585+
{#if showExportSettings}
586+
<div class="env-vars-section" transition:slide={{ duration: 300 }}>
587+
<div class="env-header">
588+
<h4>Environment Variables</h4>
589+
<SegmentedControl
590+
options={[
591+
{ value: 'env', label: '.env' },
592+
{ value: 'docker', label: 'docker-compose' },
593+
]}
594+
bind:value={envFormat}
595+
/>
596+
</div>
597+
<p class="section-description">
598+
Current environment variable values. Copy and paste into your config file for self-hosted instances.
599+
</p>
600+
601+
<pre class="env-block"><code>{envVarsString}</code></pre>
602+
603+
<button class="action-btn apply" onclick={handlers.copyEnvVars}>
604+
<Icon name={envVarsCopied ? 'check' : 'copy'} size="sm" />
605+
{envVarsCopied ? 'Copied!' : 'Copy to Clipboard'}
606+
</button>
607+
</div>
608+
{/if}
609+
</div>
610+
{/if}
611+
518612
<!-- Navigation Links (only in dropdown mode) -->
519613
{#if !standalone}
520614
<div class="settings-section settings-links">
@@ -595,6 +689,14 @@
595689
&.info-more-section {
596690
grid-column: span 2;
597691
}
692+
&.saving-section {
693+
grid-column: 1/-1;
694+
.line-1 {
695+
color: var(--text-tertiary);
696+
font-size: var(--font-size-md);
697+
font-style: italic;
698+
}
699+
}
598700
599701
.theme-option {
600702
flex-direction: column;
@@ -611,6 +713,17 @@
611713
grid-column: span 1;
612714
}
613715
}
716+
@media (max-width: 990px) {
717+
&.site-branding-section {
718+
grid-column: span 1;
719+
}
720+
&.delete-section {
721+
grid-column: 1 / -1;
722+
}
723+
&.info-more-section {
724+
grid-column: 1 / -1;
725+
}
726+
}
614727
}
615728
616729
.theme-options {
@@ -990,8 +1103,8 @@
9901103
}
9911104
9921105
.show-more-btn {
993-
width: 100%;
994-
margin-top: var(--spacing-sm);
1106+
max-width: 16rem;
1107+
margin: var(--spacing-sm) auto;
9951108
justify-content: center;
9961109
9971110
&[aria-expanded='true'] {
@@ -1187,6 +1300,8 @@
11871300
background: var(--color-primary);
11881301
color: var(--bg-primary);
11891302
border-color: var(--color-primary);
1303+
float: right;
1304+
width: fit-content !important;
11901305
11911306
&:hover {
11921307
background: color-mix(in srgb, var(--color-primary), black 10%);
@@ -1358,6 +1473,62 @@
13581473
}
13591474
}
13601475
1476+
.saving-section {
1477+
.show-more-btn {
1478+
margin-top: var(--spacing-md);
1479+
}
1480+
1481+
.env-vars-section {
1482+
margin-top: var(--spacing-md);
1483+
padding-top: var(--spacing-md);
1484+
border-top: 1px solid var(--border-primary);
1485+
1486+
.env-header {
1487+
display: flex;
1488+
justify-content: space-between;
1489+
align-items: center;
1490+
margin-bottom: var(--spacing-sm);
1491+
1492+
h4 {
1493+
margin: 0;
1494+
font-size: var(--font-size-md);
1495+
font-weight: 600;
1496+
color: var(--text-primary);
1497+
}
1498+
}
1499+
1500+
.section-description {
1501+
font-size: var(--font-size-sm);
1502+
color: var(--text-secondary);
1503+
margin: 0 0 var(--spacing-md) 0;
1504+
}
1505+
1506+
.env-block {
1507+
width: 100%;
1508+
padding: var(--spacing-md);
1509+
background: var(--bg-tertiary);
1510+
border: 1px solid var(--border-primary);
1511+
border-radius: var(--radius-sm);
1512+
margin-bottom: var(--spacing-md);
1513+
overflow-x: auto;
1514+
1515+
code {
1516+
font-family: 'Courier New', monospace;
1517+
font-size: var(--font-size-sm);
1518+
color: var(--text-primary);
1519+
line-height: 1.6;
1520+
white-space: pre;
1521+
padding: 0;
1522+
}
1523+
}
1524+
1525+
.action-btn {
1526+
width: 100%;
1527+
justify-content: center;
1528+
}
1529+
}
1530+
}
1531+
13611532
@keyframes slideIn {
13621533
from {
13631534
opacity: 0;

src/lib/config/customizable-settings.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,3 +102,48 @@ export const BLOCK_PRIVATE_DNS_IPS = env.NTB_BLOCK_PRIVATE_DNS_IPS !== 'false';
102102
export const ALLOWED_DNS_SERVERS = env.NTB_ALLOWED_DNS_SERVERS
103103
? env.NTB_ALLOWED_DNS_SERVERS.split(',').map((ip) => ip.trim())
104104
: DEFAULT_TRUSTED_DNS_SERVERS;
105+
106+
/**
107+
* Get user settings list with values prioritized as:
108+
* 1. User-set value from localStorage
109+
* 2. Environment variable value
110+
* 3. Empty string
111+
*/
112+
export function getUserSettingsList(): Array<{ name: string; value: string }> {
113+
if (!browser) return [];
114+
115+
const getLocalStorageValue = (key: string): string | null => {
116+
try {
117+
return localStorage.getItem(key);
118+
} catch {
119+
return null;
120+
}
121+
};
122+
123+
const getUserCustomValue = (key: keyof typeof userCustom): string => {
124+
return userCustom?.[key] || '';
125+
};
126+
127+
// Read actual user preferences from localStorage
128+
const userTheme = getLocalStorageValue('theme');
129+
const userFontScale = getLocalStorageValue('font-scale');
130+
const userHomepageLayout = getLocalStorageValue('homepage-layout');
131+
const userNavbarDisplay = getLocalStorageValue('navbar-display');
132+
const userPrimaryColor = getLocalStorageValue('user-primary-color');
133+
134+
return [
135+
{ name: 'NTB_SITE_TITLE', value: getUserCustomValue('title') || env.NTB_SITE_TITLE || '' },
136+
{ name: 'NTB_SITE_DESCRIPTION', value: getUserCustomValue('description') || env.NTB_SITE_DESCRIPTION || '' },
137+
{ name: 'NTB_SITE_ICON', value: getUserCustomValue('iconUrl') || env.NTB_SITE_ICON || '' },
138+
{ name: 'NTB_HOMEPAGE_LAYOUT', value: userHomepageLayout || env.NTB_HOMEPAGE_LAYOUT || '' },
139+
{ name: 'NTB_NAVBAR_DISPLAY', value: userNavbarDisplay || env.NTB_NAVBAR_DISPLAY || '' },
140+
{ name: 'NTB_DEFAULT_THEME', value: userTheme || env.NTB_DEFAULT_THEME || '' },
141+
{ name: 'NTB_DEFAULT_LANGUAGE', value: env.NTB_DEFAULT_LANGUAGE || '' },
142+
{ name: 'NTB_PRIMARY_COLOR', value: userPrimaryColor || env.NTB_PRIMARY_COLOR || '' },
143+
{ name: 'NTB_FONT_SCALE', value: userFontScale || env.NTB_FONT_SCALE || '' },
144+
{ name: 'NTB_SHOW_TIPS_ON_HOMEPAGE', value: env.NTB_SHOW_TIPS_ON_HOMEPAGE || '' },
145+
{ name: 'NTB_ALLOW_CUSTOM_DNS', value: env.NTB_ALLOW_CUSTOM_DNS || '' },
146+
{ name: 'NTB_BLOCK_PRIVATE_DNS_IPS', value: env.NTB_BLOCK_PRIVATE_DNS_IPS || '' },
147+
{ name: 'NTB_ALLOWED_DNS_SERVERS', value: env.NTB_ALLOWED_DNS_SERVERS || '' },
148+
];
149+
}

src/lib/constants/site.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { SITE_TITLE, SITE_DESCRIPTION } from '$lib/config/customizable-settings'
33
export const site = {
44
name: SITE_TITLE || 'Networking Toolbox',
55
title: SITE_TITLE || 'Networking Toolbox',
6-
description: 'A free set of online tools to help with IP addressing and subnetting.',
6+
description: SITE_DESCRIPTION || 'A free set of online tools to help with IP addressing and subnetting.',
77
longDescription:
88
'Comprehensive IP address calculator with subnet calculations, CIDR conversion, IP format conversion, and network reference tools.',
99
heroDescription: SITE_DESCRIPTION || 'Your companion for all-things networking',

src/routes/about/+page.svelte

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,14 +24,15 @@
2424
<!-- Links #1: Live Demo, DockerHub, CodeBerg Mirror, Sponsor, More Apps -->
2525
<section class="contents">
2626
<div>
27-
<h3>On this page</h3>
27+
<h3>About</h3>
2828
<ul>
29-
<li><a href="#api">API</a></li>
30-
<li><a href="#self-hosting">Self-Hosting</a></li>
31-
<li><a href="#building">Developing</a></li>
32-
<li><a href="#author">Author</a></li>
33-
<li><a href="#more-apps">Attributions</a></li>
34-
<li><a href="#license">License</a></li>
29+
<li><a href="/about/api">API</a></li>
30+
<li><a href="/about/deploying">Self-Hosting</a></li>
31+
<li><a href="/about/building">Developing</a></li>
32+
<li><a href="/about/support">Get Support</a></li>
33+
<li><a href="/about/attributions">Attributions</a></li>
34+
<li><a href="/about/author">About Author</a></li>
35+
<li><a href="/about/legal/license">License</a></li>
3536
</ul>
3637
</div>
3738
<div>

src/styles/pages.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@
106106
display: flex;
107107
align-items: center;
108108
gap: var(--spacing-sm);
109-
margin-bottom: var(--spacing-lg);
109+
margin: var(--spacing-lg) 0 var(--spacing-md) 0;
110110
svg {
111111
color: var(--color-primary);
112112
}

0 commit comments

Comments
 (0)