|
1 | 1 | /** |
2 | | - * Binary Ninja Dark Mode - System Preference with Cookie Persistence |
| 2 | + * Binary Ninja Dark Mode - System Preference with localStorage Persistence |
3 | 3 | * Follows system color scheme automatically via CSS |
4 | | - * Provides console toggle for testing with cookie-based persistence |
| 4 | + * Provides console toggle for testing with localStorage-based persistence |
5 | 5 | */ |
6 | 6 |
|
7 | 7 | (function() { |
8 | 8 | 'use strict'; |
9 | 9 |
|
10 | | - const COOKIE_NAME = 'bn-docs-theme'; |
11 | | - const COOKIE_DAYS = 365; |
| 10 | + const STORAGE_KEY = 'bn-docs-theme'; |
| 11 | + const STORAGE_TIMESTAMP_KEY = 'bn-docs-theme-timestamp'; |
| 12 | + const EXPIRY_HOURS = 24; |
12 | 13 |
|
13 | 14 | /** |
14 | | - * Get cookie value by name |
| 15 | + * Get theme from localStorage if not expired |
15 | 16 | */ |
16 | | - function getCookie(name) { |
17 | | - const value = `; ${document.cookie}`; |
18 | | - const parts = value.split(`; ${name}=`); |
19 | | - if (parts.length === 2) return parts.pop().split(';').shift(); |
20 | | - return null; |
| 17 | + function getTheme() { |
| 18 | + try { |
| 19 | + const theme = localStorage.getItem(STORAGE_KEY); |
| 20 | + const timestamp = localStorage.getItem(STORAGE_TIMESTAMP_KEY); |
| 21 | + |
| 22 | + if (!theme || !timestamp) { |
| 23 | + return null; |
| 24 | + } |
| 25 | + |
| 26 | + // Check if expired (24 hours) |
| 27 | + const now = Date.now(); |
| 28 | + const age = now - parseInt(timestamp, 10); |
| 29 | + const maxAge = EXPIRY_HOURS * 60 * 60 * 1000; // 24 hours in ms |
| 30 | + |
| 31 | + if (age > maxAge) { |
| 32 | + // Expired, remove and return null |
| 33 | + removeTheme(); |
| 34 | + return null; |
| 35 | + } |
| 36 | + |
| 37 | + return theme; |
| 38 | + } catch (e) { |
| 39 | + return null; |
| 40 | + } |
21 | 41 | } |
22 | 42 |
|
23 | 43 | /** |
24 | | - * Set cookie value |
| 44 | + * Set theme in localStorage with timestamp |
25 | 45 | */ |
26 | | - function setCookie(name, value, days) { |
27 | | - const date = new Date(); |
28 | | - date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000)); |
29 | | - const expires = `expires=${date.toUTCString()}`; |
30 | | - document.cookie = `${name}=${value};${expires};path=/`; |
| 46 | + function setTheme(value) { |
| 47 | + try { |
| 48 | + localStorage.setItem(STORAGE_KEY, value); |
| 49 | + localStorage.setItem(STORAGE_TIMESTAMP_KEY, Date.now().toString()); |
| 50 | + } catch (e) { |
| 51 | + // Silently fail if localStorage is unavailable |
| 52 | + } |
31 | 53 | } |
32 | 54 |
|
33 | 55 | /** |
34 | | - * Delete cookie |
| 56 | + * Remove theme from localStorage |
35 | 57 | */ |
36 | | - function deleteCookie(name) { |
37 | | - document.cookie = `${name}=;expires=Thu, 01 Jan 1970 00:00:00 UTC;path=/`; |
| 58 | + function removeTheme() { |
| 59 | + try { |
| 60 | + localStorage.removeItem(STORAGE_KEY); |
| 61 | + localStorage.removeItem(STORAGE_TIMESTAMP_KEY); |
| 62 | + } catch (e) { |
| 63 | + // Silently fail if localStorage is unavailable |
| 64 | + } |
38 | 65 | } |
39 | 66 |
|
40 | 67 | /** |
|
57 | 84 | } |
58 | 85 |
|
59 | 86 | /** |
60 | | - * Console-accessible toggle function with cookie persistence |
| 87 | + * Console-accessible toggle function with localStorage persistence |
61 | 88 | * Usage: bnToggleDarkMode('dark'), bnToggleDarkMode('light'), or bnToggleDarkMode('auto') |
62 | 89 | */ |
63 | 90 | window.bnToggleDarkMode = function(mode) { |
64 | 91 | if (mode === 'dark') { |
65 | 92 | applyTheme('dark'); |
66 | | - setCookie(COOKIE_NAME, 'dark', COOKIE_DAYS); |
67 | | - console.log('Dark mode: FORCED ON (saved to cookie, will persist across reloads)'); |
| 93 | + setTheme('dark'); |
68 | 94 | } else if (mode === 'light') { |
69 | 95 | applyTheme('light'); |
70 | | - setCookie(COOKIE_NAME, 'light', COOKIE_DAYS); |
71 | | - console.log('Dark mode: FORCED OFF (saved to cookie, will persist across reloads)'); |
| 96 | + setTheme('light'); |
72 | 97 | } else if (mode === 'auto') { |
73 | 98 | applyTheme('auto'); |
74 | | - deleteCookie(COOKIE_NAME); |
75 | | - console.log('Dark mode: AUTO (following system preference, cookie cleared)'); |
| 99 | + removeTheme(); |
| 100 | + } |
| 101 | + }; |
| 102 | + |
| 103 | + /** |
| 104 | + * Update button icon based on current theme |
| 105 | + */ |
| 106 | + function updateButtonIcon() { |
| 107 | + const button = document.getElementById('bn-darkmode-toggle'); |
| 108 | + if (!button) return; |
| 109 | + |
| 110 | + const savedTheme = getTheme(); |
| 111 | + |
| 112 | + if (savedTheme === 'light') { |
| 113 | + button.textContent = '☀'; |
| 114 | + button.title = 'Light mode (click for Dark)'; |
| 115 | + } else if (savedTheme === 'dark') { |
| 116 | + button.textContent = '☾'; |
| 117 | + button.title = 'Dark mode (click for Light)'; |
| 118 | + } else { |
| 119 | + // No saved theme - show system preference |
| 120 | + const isDarkMode = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; |
| 121 | + if (isDarkMode) { |
| 122 | + button.textContent = '☾'; |
| 123 | + button.title = 'System: Dark (click for Light)'; |
| 124 | + } else { |
| 125 | + button.textContent = '☀'; |
| 126 | + button.title = 'System: Light (click for Dark)'; |
| 127 | + } |
| 128 | + } |
| 129 | + } |
| 130 | + |
| 131 | + /** |
| 132 | + * Cycle between light and dark mode |
| 133 | + */ |
| 134 | + window.cycleDarkMode = function() { |
| 135 | + const savedTheme = getTheme(); |
| 136 | + |
| 137 | + if (savedTheme === 'light') { |
| 138 | + window.bnToggleDarkMode('dark'); |
76 | 139 | } else { |
77 | | - console.log('Usage: bnToggleDarkMode("dark"), bnToggleDarkMode("light"), or bnToggleDarkMode("auto")'); |
78 | | - console.log('Current setting:', getCookie(COOKIE_NAME) || 'auto (system preference)'); |
| 140 | + // From dark or system -> go to light |
| 141 | + window.bnToggleDarkMode('light'); |
79 | 142 | } |
| 143 | + |
| 144 | + updateButtonIcon(); |
80 | 145 | }; |
81 | 146 |
|
82 | 147 | /** |
83 | 148 | * Initialize theme on page load |
84 | 149 | */ |
85 | 150 | function init() { |
86 | | - const savedTheme = getCookie(COOKIE_NAME); |
| 151 | + const savedTheme = getTheme(); |
87 | 152 |
|
88 | 153 | if (savedTheme) { |
89 | 154 | applyTheme(savedTheme); |
90 | | - console.log(`Binary Ninja Docs: Theme loaded from cookie: ${savedTheme}`); |
91 | | - } else { |
92 | | - console.log('Binary Ninja Docs: Following system preference (no cookie set)'); |
93 | 155 | } |
94 | 156 |
|
95 | | - console.log('Use bnToggleDarkMode("dark"), bnToggleDarkMode("light"), or bnToggleDarkMode("auto") to change theme'); |
| 157 | + // Update button icon after a short delay to ensure DOM is ready |
| 158 | + setTimeout(updateButtonIcon, 100); |
96 | 159 | } |
97 | 160 |
|
98 | 161 | // Initialize on page load |
|
0 commit comments