|
11 | 11 | import { primaryColor } from '$lib/stores/primaryColor'; |
12 | 12 | import { fontScale, fontScaleOptions, type FontScaleLevel } from '$lib/stores/fontScale'; |
13 | 13 | import { storage } from '$lib/utils/localStorage'; |
| 14 | + import * as config from '$lib/config/customizable-settings'; |
| 15 | + import SegmentedControl from '$lib/components/global/SegmentedControl.svelte'; |
14 | 16 |
|
15 | 17 | interface Props { |
16 | 18 | standalone?: boolean; |
|
46 | 48 | let siteCustomizationErrors = $state<string[]>([]); |
47 | 49 | let lastStoreValue = $state(''); |
48 | 50 | let lastColorStoreValue = $state(''); |
| 51 | + let envVarsCopied = $state(false); |
| 52 | + let envFormat = $state<'env' | 'docker'>('env'); |
| 53 | + let showExportSettings = $state(false); |
49 | 54 |
|
50 | 55 | // Constants |
51 | | - const PRIMARY_A11Y_OPTIONS = ['reduce-motion']; |
| 56 | + const PRIMARY_A11Y_OPTIONS: string[] = []; |
52 | 57 | const COLOR_PALETTE = [ |
53 | 58 | '#1a75ff', |
54 | 59 | '#7711ff', |
|
78 | 83 | const primaryOptions = $derived( |
79 | 84 | $accessibilitySettings.options.filter((opt) => PRIMARY_A11Y_OPTIONS.includes(opt.id)), |
80 | 85 | ); |
| 86 | + // const primaryOptions = $derived( |
| 87 | + // $accessibilitySettings.options.filter((opt) => PRIMARY_A11Y_OPTIONS.includes(opt.id)), |
| 88 | + // ); |
81 | 89 | const additionalOptions = $derived( |
82 | 90 | $accessibilitySettings.options.filter((opt) => !PRIMARY_A11Y_OPTIONS.includes(opt.id)), |
83 | 91 | ); |
|
174 | 182 | window.location.reload(); |
175 | 183 | } |
176 | 184 | }, |
| 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 | + }, |
177 | 202 | }; |
| 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 | + }); |
178 | 223 | </script> |
179 | 224 |
|
180 | 225 | {#snippet validationMessages(errors: string[], warnings: string[])} |
|
515 | 560 | </div> |
516 | 561 | {/if} |
517 | 562 |
|
| 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 | + |
518 | 612 | <!-- Navigation Links (only in dropdown mode) --> |
519 | 613 | {#if !standalone} |
520 | 614 | <div class="settings-section settings-links"> |
|
595 | 689 | &.info-more-section { |
596 | 690 | grid-column: span 2; |
597 | 691 | } |
| 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 | + } |
598 | 700 |
|
599 | 701 | .theme-option { |
600 | 702 | flex-direction: column; |
|
611 | 713 | grid-column: span 1; |
612 | 714 | } |
613 | 715 | } |
| 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 | + } |
614 | 727 | } |
615 | 728 |
|
616 | 729 | .theme-options { |
|
990 | 1103 | } |
991 | 1104 |
|
992 | 1105 | .show-more-btn { |
993 | | - width: 100%; |
994 | | - margin-top: var(--spacing-sm); |
| 1106 | + max-width: 16rem; |
| 1107 | + margin: var(--spacing-sm) auto; |
995 | 1108 | justify-content: center; |
996 | 1109 |
|
997 | 1110 | &[aria-expanded='true'] { |
|
1187 | 1300 | background: var(--color-primary); |
1188 | 1301 | color: var(--bg-primary); |
1189 | 1302 | border-color: var(--color-primary); |
| 1303 | + float: right; |
| 1304 | + width: fit-content !important; |
1190 | 1305 |
|
1191 | 1306 | &:hover { |
1192 | 1307 | background: color-mix(in srgb, var(--color-primary), black 10%); |
|
1358 | 1473 | } |
1359 | 1474 | } |
1360 | 1475 |
|
| 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 | +
|
1361 | 1532 | @keyframes slideIn { |
1362 | 1533 | from { |
1363 | 1534 | opacity: 0; |
|
0 commit comments