diff --git a/css/onboarding.css b/css/onboarding.css new file mode 100644 index 0000000..28a8fe6 --- /dev/null +++ b/css/onboarding.css @@ -0,0 +1,419 @@ +.container-ob { + position: fixed; + inset: 0; + z-index: 10000; + background-color: var(--background); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + overflow: hidden; +} + +.container-ob::before { + content: ''; + display: block; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 0; +} + +.step-ob { + display: none; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; + max-width: 800px; + width: 100%; + height: 100%; + padding: 2rem; + position: relative; + opacity: 0; + transform: translateY(20px); + transition: opacity 0.5s ease, transform 0.5s ease; +} + +.step-ob.active-ob { + display: flex; + opacity: 1; + transform: translateY(0); + z-index: 1; +} + +.title-ob { + font-size: 2.5rem; + font-weight: 700; + color: var(--text); + margin-bottom: 1rem; + transform-origin: center; + animation: titlePop-ob 0.6s cubic-bezier(0.34, 1.56, 0.64, 1) forwards; +} + +.subtitle-ob { + font-size: 1.25rem; + color: var(--text-secondary); + margin-bottom: 2.5rem; + max-width: 600px; +} + +.options-grid-ob { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 1.5rem; + width: 100%; + margin-bottom: 2rem; +} + +.step-ob[data-step="3"] .options-grid-ob { + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + max-width: 900px; + margin-left: auto; + margin-right: auto; +} + +.step-ob[data-step="5"] .options-grid-ob { + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + max-width: 900px; + margin-left: auto; + margin-right: auto; +} + +.option-card-ob { + background-color: var(--surface); + border: 2px solid var(--border); + border-radius: 16px; + padding: 1.5rem; + display: flex; + flex-direction: column; + align-items: center; + cursor: pointer; + transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); + position: relative; + overflow: hidden; +} + +.option-card-ob:hover { + transform: translateY(-5px); + box-shadow: 0 10px 20px var(--shadow); + border-color: var(--text); +} + +.option-card-ob.selected-ob { + border-color: var(--text); + background-color: var(--surface); + transform: scale(1.02); +} + +.option-card-ob svg { + width: 48px; + height: 48px; + color: var(--text); + margin-bottom: 1rem; +} + +.option-card-ob h3 { + font-size: 1.25rem; + font-weight: 600; + color: var(--text); + margin-bottom: 0.5rem; +} + +.option-card-ob p { + font-size: 0.875rem; + color: var(--text-secondary); +} + +.option-card-ob[data-engine] img { + width: 32px; + height: 32px; + border-radius: 50%; + object-fit: cover; + margin-bottom: 1rem; +} + +.nav-ob { + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; + max-width: 800px; + padding: 0 1rem; + margin-top: auto; + margin-bottom: 40px; + position: relative; + z-index: 2; +} + +.button-ob { + background-color: var(--surface); + border: 2px solid var(--border); + border-radius: 12px; + padding: 1rem 2rem; + font-size: 1rem; + font-weight: 500; + cursor: pointer; + display: flex; + align-items: center; + gap: 0.5rem; + color: var(--text); + transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); +} + +.button-ob:hover { + background-color: var(--primary); + border-color: var(--text); + transform: translateY(-2px); +} + +.button-ob svg { + width: 20px; + height: 20px; +} + +.button-ob.primary-ob { + background-color: var(--text); + color: var(--background); + border-color: var(--text); +} + +.button-ob.primary-ob:hover { + transform: translateY(-2px); + box-shadow: 0 4px 12px var(--shadow); +} + +.button-ob.disabled-ob, +.button-ob:disabled { + opacity: 0.5; + cursor: not-allowed; + pointer-events: none; +} + +.button-ob.disabled-ob:hover, +.button-ob:disabled:hover { + transform: none; + box-shadow: none; + background-color: var(--surface); + border-color: var(--border); +} + +.progress-dots-ob { + display: flex; + gap: 8px; +} + +.dot-ob { + width: 10px; + height: 10px; + border-radius: 50%; + background-color: var(--border); + transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); +} + +.dot-ob.active-ob { + background-color: var(--text); + transform: scale(1.2); +} + +.theme-preview-ob { + width: 100%; + border-radius: 16px; + padding: 1.5rem; + margin-bottom: 1rem; + transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); + border: 2px solid var(--border); +} + +.theme-preview-ob.light-ob { + background-color: white; + color: #1a1a1a; +} + +.theme-preview-ob.dark-ob { + background-color: #1a1a1a; + color: white; +} + +.theme-preview-ob.light-ob .preview-search-ob { + background-color: #f0f0f0; + border-color: #dddddd; +} + +.theme-preview-content-ob { + display: flex; + flex-direction: column; + align-items: center; + gap: 1rem; +} + +.preview-search-ob { + width: 100%; + height: 50px; + border-radius: 25px; + background-color: var(--surface); + border: 2px solid var(--border); +} + +.preview-shortcuts-ob { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 0.5rem; + width: 100%; +} + +.preview-shortcut-ob { + width: 100%; + aspect-ratio: 1/1; + border-radius: 12px; + background-color: rgba(0, 0, 0, 0.1); +} + +.dark-ob .preview-shortcut-ob { + background-color: rgba(255, 255, 255, 0.1); +} + +.font-preview-ob { + font-size: 3rem; + margin-bottom: 1rem; + line-height: 1; +} + +.name-input-container-ob { + width: 100%; + max-width: 500px; + margin: 2rem 0; + position: relative; +} + +.name-input-ob { + width: 100%; + border: none !important; + background: transparent !important; + font-size: 4rem !important; + font-weight: 600 !important; + color: var(--text) !important; + text-align: left !important; + border-radius: 0 !important; + padding: 0.5rem 0 !important; + border-bottom: 2px solid #a3a3a3 !important; + transition: all 0.3s ease !important; + outline: none !important; + -webkit-appearance: none !important; + -moz-appearance: none !important; + appearance: none !important; +} + +.name-input-ob:focus { + border-color: var(--text); +} + +.name-input-ob::placeholder { + color: var(--text-secondary); + opacity: 0.5; +} + +.name-input-ob:-webkit-autofill, +.name-input-ob:-webkit-autofill:hover, +.name-input-ob:-webkit-autofill:focus { + -webkit-text-fill-color: var(--text) !important; + -webkit-box-shadow: 0 0 0px 1000px transparent inset !important; + transition: background-color 5000s ease-in-out 0s !important; + background-clip: content-box !important; +} + +@keyframes titlePop-ob { + 0% { transform: scale(0.8); opacity: 0; } + 50% { transform: scale(1.05); } + 100% { transform: scale(1); opacity: 1; } +} + +@keyframes slideInUp-ob { + 0% { transform: translateY(40px); opacity: 0; } + 100% { transform: translateY(0); opacity: 1; } +} + +@keyframes fadeIn-ob { + 0% { opacity: 0; } + 100% { opacity: 1; } +} + +.search-engine-preview-ob { + display: flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + margin-top: 1rem; +} + +.search-engine-preview-ob img { + width: 24px; + height: 24px; + border-radius: 50%; +} + +.animate-in-ob { + animation: slideInUp-ob 0.5s cubic-bezier(0.34, 1.56, 0.64, 1) forwards; +} + +@media (max-width: 768px) { + .options-grid-ob { + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 1rem; + } + + .title-ob { + font-size: 2rem; + } + + .subtitle-ob { + font-size: 1rem; + } + + .name-input-ob { + font-size: 1.5rem; + } + + .nav-ob { + padding: 0.5rem; + margin-bottom: 30px; + } +} + +@media (max-width: 576px) { + .step-ob { + padding: 1.5rem 1rem; + } + + .options-grid-ob, + .step-ob[data-step="3"] .options-grid-ob, + .step-ob[data-step="5"] .options-grid-ob { + grid-template-columns: 1fr; + gap: 1rem; + } + + .option-card-ob { + padding: 1rem; + } + + .title-ob { + font-size: 1.75rem; + } + + .button-ob { + padding: 0.75rem 1.5rem; + } + + .font-preview-ob { + font-size: 2.5rem; + } +} + +@media (min-width: 577px) and (max-width: 991px) { + .step-ob[data-step="3"] .options-grid-ob, + .step-ob[data-step="5"] .options-grid-ob { + grid-template-columns: repeat(2, 1fr); + } +} \ No newline at end of file diff --git a/css/style.css b/css/style.css new file mode 100644 index 0000000..f6d1742 --- /dev/null +++ b/css/style.css @@ -0,0 +1,2240 @@ +:root { + --primary: #f5f5f5; + --primary-hover: #e0e0e0; + --background: #ffffff; + --surface: #fafafa; + --border: #eaeaea; + --text: #1a1a1a; + --text-secondary: #666666; + --shadow: rgba(0, 0, 0, 0.08); + --modal-backdrop: rgba(0, 0, 0, 0.5); + --scrollbar-thumb: #e0e0e0; + --scrollbar-track: #f5f5f5; + --modal-background: #ffffff; + --toggle-bg: #e0e0e0; + --toggle-bg-active: var(--text); + --toggle-knob: var(--background); +} + +[data-theme="dark"] { + --primary: #1a1a1a; + --primary-hover: #2a2a2a; + --background: #000000; + --surface: #111111; + --border: #333333; + --text: #ffffff; + --text-secondary: #999999; + --shadow: rgba(0, 0, 0, 0.3); + --modal-backdrop: rgba(0, 0, 0, 0.75); + --scrollbar-thumb: #333333; + --scrollbar-track: #1a1a1a; + --modal-background: #1a1a1a; + --toggle-bg: #333333; + --toggle-bg-active: var(--text); + --toggle-knob: var(--background); +} + +[data-theme="dark"] .btn-primary { + background: var(--primary-hover); + color: var(--text); +} + +[data-theme="dark"] .btn-primary:hover { + background: #3a3a3a; +} + +* { + margin: 0; + padding: 0; + box-sizing: border-box; + font-family: var(--font-family); + font-size: var(--font-size-base); +} + +body { + background: var(--background); + color: var(--text); + min-height: 100vh; + background-size: cover; + background-position: center; +} + +svg { + width: 24px; + height: 24px; + stroke: currentColor; + stroke-width: 2; + stroke-linecap: round; + stroke-linejoin: round; + transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); +} + +[data-theme="dark"] svg { + color: currentColor; +} + +.hidden { + display: none !important; +} + +.modal { + position: fixed; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + opacity: 0; + visibility: hidden; + transition: all 0.3s ease; + background: var(--modal-backdrop); +} + +.modal-actions { + display: flex; + gap: 1rem; + margin-top: 1rem; +} + +.modal-actions button { + flex: 1; +} + +.modal.active { + opacity: 1; + visibility: visible; +} + +.modal-content { + background: var(--modal-background); + border-radius: 24px; + padding: 2rem; + width: 90%; + max-width: 480px; + transform: translateY(20px); + opacity: 0; + transition: all 0.3s ease; + box-shadow: 0 10px 25px var(--shadow); +} + +@keyframes slideIn { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.modal.active .modal-content { + transform: translateY(0); + opacity: 1; + animation: modalSlideIn 0.3s ease forwards; +} + +@keyframes modalSlideIn { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +select { + width: 100%; + padding: 0.75rem 1rem; + border: 2px solid var(--border); + border-radius: 12px; + background: var(--surface); + color: var(--text); + font-size: 1rem; + cursor: pointer; + appearance: none; + background-image: url("data:image/svg+xml;charset=US-ASCII,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%22292.4%22%20height%3D%22292.4%22%3E%3Cpath%20fill%3D%22%23666666%22%20d%3D%22M287%2069.4a17.6%2017.6%200%200%200-13-5.4H18.4c-5%200-9.3%201.8-12.9%205.4A17.6%2017.6%200%200%200%200%2082.2c0%205%201.8%209.3%205.4%2012.9l128%20127.9c3.6%203.6%207.8%205.4%2012.8%205.4s9.2-1.8%2012.8-5.4L287%2095c3.5-3.5%205.4-7.8%205.4-12.8%200-5-1.9-9.2-5.4-12.8z%22%2F%3E%3C%2Fsvg%3E"); + background-repeat: no-repeat; + background-position: right 1rem center; + background-size: 0.65em auto; +} + +select:focus { + outline: none; + border-color: var(--text); +} + +.modal-content { + overflow: hidden; +} + +.onboarding-progress { + width: 100%; + position: absolute; + bottom: 0; + left: 0; + height: 4px; + background: var(--border); + border-radius: 2px; + margin-top: 1.5rem; + overflow: hidden; +} + +.progress-bar { + height: 100%; + width: 0; + background: var(--text); + border-radius: 2px; + transition: width 0.3s ease; +} + +.step h2 { + font-size: 1.75rem; + margin-bottom: 1rem; + font-weight: 700; +} + +.step p { + color: var(--text-secondary); + margin-bottom: 2rem; +} + +.input-group { + margin-bottom: 1.5rem; +} + +input[type="text"] { + width: 100%; + padding: 1rem; + border: 2px solid var(--border); + border-radius: 12px; + background: var(--surface); + color: var(--text); + font-size: 1rem; +} + +input[type="text"]:focus { + outline: none; + border-color: var(--text); +} + +.center-container { + min-height: 100vh; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 3rem; + padding: 2rem; + position: relative; +} + +#greeting { + font-size: 2rem; + font-weight: 700; + margin: 0; + opacity: 0; + animation: greetingSlideIn 0.6s cubic-bezier(0.34, 1.56, 0.64, 1); + height: 48px; + visibility: hidden; +} + +#greeting:not([style*="visibility"]) { + visibility: visible; +} + +.search-container { + width: 100%; + max-width: 640px; + height: 56px; + visibility: visible; + animation: searchBarSlideIn 0.6s cubic-bezier(0.34, 1.56, 0.64, 1) 0.1s backwards; +} + +.search-engine-options { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 1rem; + margin: 2rem 0; +} + +.search-engine-option { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 0.5rem; + padding: 1.5rem; + border-radius: 16px; + background: var(--surface); + cursor: pointer; + transition: all 0.2s ease; + box-shadow: 0 2px 8px var(--shadow); + border: 2px solid transparent; + width: 100%; + height: 100%; + box-sizing: border-box; + animation: searchEngineOptionFadeIn 0.3s cubic-bezier(0.34, 1.56, 0.64, 1) backwards; +} + +.search-engine-option:hover { + transform: translateY(-4px); + box-shadow: 0 8px 24px var(--shadow); +} + +.search-engine-option.selected { + background: #eeeeee; + border: 2px solid var(--border); +} + +.search-engine-option img { + width: 20px; + height: 20px; + margin-right: 8px; + transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); +} + +.search-engine-option span { + font-size: 0.875rem; + color: var(--text); + text-align: center; +} +.search-wrapper { + position: relative; +} + +#search-bar { + width: 100%; + padding: 1.25rem 1.5rem; + border: none; + border-radius: 16px; + background: var(--surface); + color: var(--text); + font-size: 1rem; + box-shadow: 0 4px 24px var(--shadow); + transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); +} + +.search-icon { + position: absolute; + right: 1.5rem; + top: 50%; + transform: translateY(-50%); + background: none; + border: none; + color: var(--text); + cursor: pointer; + padding: 0.5rem; +} + +.shortcuts-container { + width: 100%; + max-width: 640px; +} + +#add-shortcut { + width: 40px; + height: 40px; + border-radius: 50%; + padding: 0; + display: flex; + align-items: center; + justify-content: center; + margin: 0 auto; + background: var(--primary); + color: var(--text); + border: none; + cursor: pointer; + transition: background 0.2s ease; + transition: transform 0.2s ease; + animation: addButtonFadeIn 0.5s cubic-bezier(0.34, 1.56, 0.64, 1) 0.3s backwards; +} + +#add-shortcut:hover { + background: var(--primary-hover); + transform: scale(1.1) rotate(180deg); +} + +#add-shortcut:active { + transform: scale(0.9) rotate(180deg); +} + +#shortcuts-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(80px, 1fr)); + gap: 1rem; + margin-bottom: 1.5rem; + animation: shortcutsSlideIn 0.6s cubic-bezier(0.34, 1.56, 0.64, 1) 0.2s backwards; +} + +.shortcut { + background: var(--surface); + border-radius: 12px; + padding: 1rem; + cursor: pointer; + transition: all 0.2s ease; + display: flex; + flex-direction: column; + align-items: center; + gap: 0.5rem; + box-shadow: 0 2px 10px var(--shadow); + animation: shortcutFadeIn 0.5s cubic-bezier(0.34, 1.56, 0.64, 1) backwards; + user-select: none; + -webkit-user-select: none; + -webkit-tap-highlight-color: transparent; + position: relative; +} + +.shortcut:hover { + transform: scale(1.08); + box-shadow: 0 8px 24px var(--shadow); +} + +.shortcut img { + width: 24px; + height: 24px; + border-radius: 6px; + pointer-events: none; + -webkit-user-drag: none; +} + +.shortcut span { + font-size: 0.75rem; + text-align: center; + color: var(--text-secondary); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 100%; +} + +.shortcut.blurred { + filter: blur(4px); + opacity: 0.7; +} + +.shortcut.blurred:hover { + filter: blur(0); + opacity: 1; +} + +.settings-button { + position: fixed; + bottom: 2rem; + right: 2rem; + width: 48px; + height: 48px; + border-radius: 50%; + background: var(--surface); + border: none; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + font-size: 1.25rem; + color: var(--text); + box-shadow: 0 4px 24px var(--shadow); + transition: transform 0.4s ease; + z-index: 999; + animation: settingsButtonFadeIn 0.5s cubic-bezier(0.34, 1.56, 0.64, 1) 0.4s backwards; +} + +.settings-button:hover { + transform: rotate(180deg) scale(1.1); +} + +.settings-button:active { + transform: rotate(180deg) scale(0.9); +} + +.settings-panel { + max-height: 80vh; + overflow-y: auto; + scrollbar-width: thin; + scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track); +} + +.settings-panel::-webkit-scrollbar { + width: 8px; +} + +.settings-panel::-webkit-scrollbar-track { + background: var(--scrollbar-track); + border-radius: 0 24px 24px 0; +} + +.settings-panel::-webkit-scrollbar-thumb { + background-color: var(--scrollbar-thumb); + border-radius: 4px; +} + +.modal-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 0rem; +} + +.modal-header h2 { + font-size: 1.5rem; + font-weight: 600; +} + +.btn-icon { + background: none; + border: none; + color: var(--text); + cursor: pointer; + padding: 0.5rem; + font-size: 1.25rem; + opacity: 0.6; +} + +.btn-icon:hover { + opacity: 1; +} + +.settings-section { + padding: 1.5rem 0; + border-bottom: 1px solid var(--border); +} + +.settings-section:last-child { + border-bottom: none; + padding-bottom: 0; +} + +.settings-section h3 { + margin-bottom: 1.5rem; + font-weight: 600; +} + +.setting-item { + display: flex; + flex-direction: column; + padding: 0.75rem 0; +} + +.setting-item.horizontal { + flex-direction: row; + align-items: center; + justify-content: space-between; +} + +.setting-item .setting-label { + font-weight: 500; + margin-bottom: 0.5rem; +} + +.data-management-buttons { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.data-management-buttons svg { + vertical-align: middle; + margin-right: 5px; + margin-top: -6px; +} + +.btn-danger { + background: #dc3545 !important; + color: white !important; +} + +.btn-danger:hover { + background: #c82333 !important; +} + +.toggle { + position: relative; + display: inline-block; + width: 50px; + height: 26px; +} + +.toggle input { + opacity: 0; + width: 0; + height: 0; +} + +.toggle-slider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background-color: var(--toggle-bg); + border-radius: 34px; +} + +.toggle-slider:before { + position: absolute; + content: ""; + height: 20px; + width: 20px; + left: 3px; + bottom: 3px; + background-color: var(--toggle-knob); + border-radius: 50%; + transition: transform 0.3s ease; +} + +input:checked + .toggle-slider { + background-color: var(--toggle-bg-active); +} + +input:checked + .toggle-slider:before { + transform: translateX(24px); +} + +#keybind-url-combo { + margin-bottom: 0.5rem; +} + +#keybind-url { + margin-top: 0.5rem; +} + +.keybind-container { + display: flex; + gap: 0.5rem; + align-items: center; +} + +.clear-keybind { + padding: 0.25rem 0.5rem; + background: var(--accent); + border: none; + border-radius: 4px; + color: var(--text); + cursor: pointer; + font-size: 1.2rem; + line-height: 1; +} + +.clear-keybind:hover { + opacity: 0.8; +} + +.format-hint { + display: block; + font-size: 0.75rem; + color: var(--text-secondary); + opacity: 0.7; + margin-top: 0.25rem; +} + +.background-preview-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); + gap: 1rem; + margin: 1rem 0; + justify-content: center; +} + +.background-preview { + position: relative; + border: 2px solid var(--border); + border-radius: 12px; + background-size: cover; + background-position: center; + overflow: hidden; + height: 150px; + width: 100%; + cursor: pointer; + transition: transform 0.2s ease, box-shadow 0.2s ease; +} + +.background-preview:hover { + transform: scale(1.05); + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.2); +} + +.background-preview.selected { + border: 3px solid var(--text); + box-shadow: 0 0 0 3px var(--surface), 0 0 0 6px var(--text); + transform: scale(0.95); +} + +.background-preview.empty { + display: flex; + align-items: center; + justify-content: center; + color: var(--text-secondary); + font-size: 2rem; + background-color: var(--background-secondary); + cursor: pointer; + border: 2px dashed var(--border); +} + +.remove-icon { + position: absolute; + top: 8px; + right: 8px; + background: var(--primary); + border-radius: 50%; + padding: 0.3rem 0.5rem; + font-size: 0.9rem; + color: var(--text); + cursor: pointer; + transition: background 0.2s ease; +} + +.remove-icon:hover { + background: var(--primary-hover); +} + +.file-input { + display: none; +} + +.file-input-label { + background: var(--primary); + color: var(--text); + display: flex; + justify-content: center; + align-items: center; + border: none; + padding: 1rem 1rem; + border-radius: 12px; + font-size: 1rem; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; + width: 100%; +} + +.file-input-label:hover { + background: var(--primary-hover); +} + +[data-theme="dark"] .file-input-label { + background: var(--primary-hover); + color: var(--text); +} + +[data-theme="dark"] .file-input-label:hover { + background: #3a3a3a; +} + +.btn-primary { + background: var(--primary); + color: var(--text); + border: none; + padding: 1rem 2rem; + border-radius: 12px; + font-size: 1rem; + font-weight: 500; + cursor: pointer; + transition: all 0.2s ease; + width: 100%; +} + +.btn-primary:hover { + background: var(--primary-hover); +} + +#notification-container { + position: fixed; + top: 1.5rem; + right: 1.5rem; + z-index: 1000; + display: flex; + flex-direction: column; + gap: 1rem; + pointer-events: none; +} + +.notification { + background: var(--surface); + color: var(--text); + padding: 1.25rem 1.5rem; + border-radius: 16px; + box-shadow: 0 8px 32px rgba(var(--shadow-rgb), 0.1); + display: flex; + align-items: center; + gap: 1.25rem; + pointer-events: auto; + position: relative; + overflow: hidden; + animation: slideInRight 0.4s cubic-bezier(0.16, 1, 0.3, 1), fadeIn 0.4s ease; + max-width: 420px; + border: 1px solid rgba(var(--text-rgb), 0.1); +} + +.notification-content { + flex: 1; + font-size: 0.95rem; + line-height: 1.5; + white-space: normal; + overflow: visible; + text-overflow: clip; +} + +.notification-content a { + color: var(--text); + text-decoration: none; + font-weight: bold; +} + +.notification-content a:hover { + text-decoration: underline; + color: var(--primary-dark); +} + +.notification-close { + background: none; + border: none; + color: var(--text); + cursor: pointer; + padding: 0.5rem; + opacity: 0.6; +} + +.notification-close:hover { + opacity: 1; + background: rgba(var(--text-rgb), 0.1); +} + +.notification-progress { + position: absolute; + bottom: 0; + left: 0; + height: 3px; + background: var(--primary); + opacity: 0.8; + transition: width 0.1s linear; +} + +@keyframes slideInRight { + from { + opacity: 0; + transform: translateX(100%); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +.about-content { + text-align: center; + color: var(--text-secondary); + padding: 1rem 0; +} + +.about-content a { + color: var(--text); + text-decoration: none; +} + +.about-content a:hover { + text-decoration: underline; +} + +.about-content .version { + font-size: 1.1rem; + font-weight: 600; + color: var(--text); + margin-bottom: 0.5rem; +} + +.about-content .description { + margin-bottom: 1rem; +} + +.about-content .features { + font-size: 0.9rem; + margin-bottom: 1rem; +} + +.about-content .copyright { + font-size: 0.8rem; + margin-top: 1rem; +} + +.made-with { + margin-top: 0.5rem; + font-size: 0.875rem; +} + +.made-with i { + color: #ff6b6b; + animation: heartbeat 1.5s infinite ease-in-out; +} + +@keyframes heartbeat { + 0%, 100% { + transform: scale(1); + } + 50% { + transform: scale(1.1); + } + 100% { + transform: scale(1); + } +} + +.context-menu { + position: fixed; + background: var(--surface); + border-radius: 8px; + padding: 0.5rem; + box-shadow: 0 2px 10px var(--shadow); + z-index: 1000; + animation: contextMenuFadeIn 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); +} + +.context-menu.hidden { + display: none; +} + +.context-menu-item { + padding: 0.75rem 1rem; + cursor: pointer; + display: flex; + align-items: center; + font-size: 12px !important; + gap: 0.5rem; + border-radius: 4px; + transition: transform 0.2s ease; +} + +.context-menu-item:hover { + background: var(--primary); + transform: translateX(2.5px); +} + +.context-menu-item i { + width: 16px; +} + +.search-container.hidden, +#greeting.hidden, +#shortcuts-grid.hidden, +#add-shortcut.hidden { + visibility: hidden !important; + opacity: 0 !important; + position: absolute !important; + pointer-events: none !important; +} + +@media screen and (max-width: 768px) { + .center-container { + padding: 1rem; + gap: 2rem; + } + + #greeting { + font-size: 1.5rem; + height: 36px; + } + + .search-container { + max-width: 100%; + } + + #search-bar { + padding: 1rem; + font-size: 0.9rem; + } + + .search-engine-options { + grid-template-columns: repeat(2, 1fr); + gap: 0.75rem; + } + + .search-engine-option { + padding: 1rem; + } + + .search-engine-option img { + width: 24px; + height: 24px; + } + + #shortcuts-grid { + grid-template-columns: repeat(auto-fill, minmax(80px, 1fr)); + gap: 0.75rem; + padding: 0.5rem; + } + + .shortcut { + padding: 0.75rem; + } + + .shortcut img { + width: 24px; + height: 24px; + } + + .shortcut span { + font-size: 0.75rem; + } + + .modal-content { + width: 95%; + padding: 1.5rem; + border-radius: 16px; + } + + .step h2 { + font-size: 1.5rem; + } + + .setting-item { + padding: 0.5rem 0; + } + + .setting-label { + font-size: 0.9rem; + } + + input[type="text"], select { + padding: 0.75rem; + font-size: 0.9rem; + } + + .notification { + width: 90%; + max-width: none; + margin: 0.5rem; + padding: 0.75rem; + } + + .font-size-control { + flex-direction: column; + gap: 0.75rem; + } + + .font-size-input { + width: 100%; + justify-content: space-between; + } + + .font-size-input input[type="number"] { + width: 80px; + } + + .btn-reset { + width: 100%; + } + + + .settings-container { + flex-direction: column; + } + + .settings-sidebar { + width: 100%; + border-right: none; + border-bottom: 1px solid var(--border); + } + + .settings-content { + padding: 1.5rem; + } + + .data-management-buttons { + grid-template-columns: 1fr; + } + + .custom-grid-controls { + flex-direction: column; + align-items: flex-start; + } + + .grid-control { + width: 100%; + justify-content: space-between; + } +} + +@media screen and (max-width: 480px) { + #greeting { + font-size: 1.25rem; + height: 32px; + } + + .search-engine-options { + display: grid; + grid-template-columns: 1fr; + gap: 0.5rem; + padding: 0.5rem; + } + + #shortcuts-grid { + grid-template-columns: repeat(auto-fill, minmax(70px, 1fr)); + gap: 0.5rem; + } + + .modal-content { + padding: 1rem; + } + + .modal-actions { + flex-direction: column; + gap: 0.5rem; + } + + .modal-actions button { + width: 100%; + } + + .format-hint { + font-size: 0.7rem; + line-height: 1.2; + } +} + +@media screen and (max-height: 480px) and (orientation: landscape) { + .center-container { + padding: 0.5rem; + gap: 1rem; + } + + #greeting { + font-size: 1.25rem; + height: 32px; + margin-bottom: 0; + } + + .modal-content { + height: 90vh; + overflow-y: auto; + } + + .search-engine-options { + grid-template-columns: repeat(3, 1fr); + gap: 0.5rem; + } +} + +@media screen and (max-width: 480px) { + .search-engine-options { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 0.75rem; + padding: 0.5rem; + } + + .search-engine-option { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + text-align: center; + padding: 1rem; + background: var(--surface); + border: 1px solid var(--border); + border-radius: 12px; + cursor: pointer; + transition: all 0.2s ease; + aspect-ratio: 1; + } + + .search-engine-option img { + width: 24px; + height: 24px; + margin-bottom: 0.5rem; + } + + .search-engine-option span { + font-size: 0.85rem; + font-weight: 500; + } + + .search-engine-option.selected { + background: #eeeeee; + border: 2px solid var(--border); + } + + .onboarding-modal .modal-content { + width: 90%; + max-width: none; + padding: 1.25rem; + } + + .step h2 { + font-size: 1.35rem; + margin-bottom: 1rem; + } + + #complete-setup-btn { + width: 100%; + margin-top: 1rem; + padding: 0.875rem; + font-size: 0.95rem; + } +} + +.import-options { + display: flex; + flex-direction: column; + align-items: center; + gap: 1rem; + width: 100%; + max-width: 300px; + margin: 0 auto; +} + +#import-data-btn svg { + vertical-align: middle; + margin-right: 5px; + margin-top: -6px; +} + +.or-divider { + position: relative; + width: 100%; + text-align: center; + margin: 0.5rem 0; +} + +.or-divider::before, +.or-divider::after { + content: ''; + position: absolute; + top: 50%; + width: calc(50% - 24px); + height: 1px; + background-color: var(--border); +} + +.or-divider::before { + left: 0; +} + +.or-divider::after { + right: 0; +} + +.or-divider span { + background-color: var(--background); + padding: 0 8px; + color: var(--text); + font-size: 0.9rem; +} + +.settings-page { + position: fixed; + inset: 0; + background: var(--background); + z-index: 1000; + display: flex; + align-items: stretch; + opacity: 0; + visibility: hidden; + transition: all 0.3s ease; +} + +.settings-page.active { + opacity: 1; + visibility: visible; +} + +.settings-container { + display: flex; + width: 100%; + height: 100%; + margin: 0 auto; + background: var(--background); +} + +.settings-sidebar { + width: 280px; + background: var(--surface); + border-right: 1px solid var(--border); + display: flex; + flex-direction: column; + flex-shrink: 0; + padding: 1rem; +} + +#back-to-home { + color: var(--text); + border: none; + border-radius: 8px; + padding: 1rem 0.2rem; + cursor: pointer; + transition: color 0.3s ease, transform 0.3s ease; +} + +#back-to-home:hover { + color: var(--primary-hover); + transform: scale(1.15); +} + +#back-to-home i { + font-size: 1.25rem; + transition: transform 0.3s ease; +} + +#back-to-home:hover i { + transform: scale(1.2); +} + +.settings-sidebar-header { + padding: 1rem 1.25rem; + display: flex; + align-items: center; + gap: 1rem; + margin-bottom: 1rem; +} + +.settings-sidebar-header h2 { + font-size: 1.5rem; + font-weight: 600; + margin: 0; +} + +.settings-nav { + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.settings-nav-item { + display: flex; + align-items: center; + gap: 0.875rem; + padding: 0.875rem 1.25rem; + color: var(--text-secondary); + text-decoration: none; + transition: all 0.2s ease; + border-radius: 8px; + font-weight: 500; +} + +.settings-nav-item:hover { + background: var(--primary); + color: var(--text); +} + +.settings-nav-item.active { + background: var(--primary); + color: var(--text); + font-weight: 600; +} + +.settings-nav-item.active svg { + transform: scale(1.15) translateX(2px); +} + +.settings-nav-item svg { + width: 18px; + height: 18px; + margin-right: 12px; + transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); +} + +.settings-nav-item:hover svg { + transform: scale(1.15) translateX(2px); +} + +.settings-content { + flex: 1; + padding: 2rem 3rem; + overflow-y: auto; + background: var(--background); +} + +.settings-section { + display: none; + animation: fadeIn 0.3s ease; + max-width: 800px; + margin: 0 auto; +} + +.settings-section.active { + display: block; +} + +.settings-section h3 { + font-size: 1.5rem; + font-weight: 600; + margin-bottom: 1.5rem; + color: var(--text); +} + +.settings-card { + background: var(--surface); + border-radius: 12px; + padding: 1.5rem; + margin-bottom: 1.5rem; + border: 1px solid var(--border); +} + +.settings-card-header { + display: flex; + align-items: center; + gap: 1rem; + padding: 1rem; + border-bottom: 1px solid var(--border); +} + +.settings-card-header .btn-reset { + margin-left: auto; + padding: 0.5rem 1rem; + font-size: 0.875rem; +} + +.settings-card-icon { + display: flex; + align-items: center; + justify-content: center; + width: 2.5rem; + height: 2.5rem; + background-color: var(--primary); + border-radius: 0.75rem; +} + +.settings-card-icon svg { + width: 20px; + height: 20px; + transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); +} + +.settings-card-icon:hover svg { + transform: scale(1.15) rotate(5deg); +} + +.settings-card-title { + flex: 1; +} + +.settings-card-title h4 { + font-size: 1.125rem !important; + font-weight: 600; + margin-bottom: 0.25rem; + color: var(--text); +} + +.settings-card-title p { + color: var(--text-secondary); + font-size: 0.875rem; + line-height: 1.5; +} + +.setting-item { + display: flex; + flex-direction: column; + padding: 1rem 0; + border-top: 1px solid var(--border); +} + +.setting-item:first-child { + border-top: none; + padding-top: 0; +} + +.setting-item:last-child { + padding-bottom: 0; +} + +.setting-item.horizontal { + flex-direction: row; + align-items: center; + justify-content: space-between; + gap: 2rem; +} + +.setting-item .setting-label { + font-weight: 500; + margin-bottom: 0.5rem; + color: var(--text); +} + +.setting-item.horizontal .setting-label { + margin-bottom: 0; +} + +.setting-item .setting-description { + color: var(--text-secondary); + font-size: 0.875rem; + margin-bottom: 1rem; + line-height: 1.5; +} + +.data-management-buttons { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 1rem; +} + +.data-management-buttons .btn-primary { + padding: 1rem; + border-radius: 8px; + font-size: 0.875rem; + font-weight: 500; + display: flex; + align-items: center; + justify-content: center; + gap: 0.5rem; + background: var(--primary); + transition: all 0.2s ease; +} + +.data-management-buttons .btn-primary:hover { + background: var(--primary-hover); + transform: translateY(-1px); +} + +.data-management-buttons .btn-danger { + grid-column: 1 / -1; + background: #dc3545; +} + +.data-management-buttons .btn-danger:hover { + background: #c82333; +} + +.data-management-buttons svg { + width: 18px; + height: 18px; +} + +input[type="text"], +input[type="password"], +select { + width: 100%; + padding: 0.75rem 1rem; + border: 1px solid var(--border); + border-radius: 8px; + background: var(--background); + color: var(--text); + font-size: 0.875rem; + transition: all 0.2s ease; +} + +input[type="text"]:focus, +input[type="password"]:focus, +select:focus { + outline: none; + border-color: var(--text); + box-shadow: 0 0 0 2px rgba(var(--text-rgb), 0.1); +} + +.format-hint { + display: block; + font-size: 0.75rem; + color: var(--text-secondary); + margin-top: 0.5rem; +} + +.toggle { + position: relative; + display: inline-block; + width: 44px; + height: 24px; +} + +.toggle input { + opacity: 0; + width: 0; + height: 0; +} + +.toggle-slider { + position: absolute; + cursor: pointer; + inset: 0; + background-color: var(--toggle-bg); + border-radius: 24px; + transition: background-color 0.3s cubic-bezier(0.4, 0, 0.2, 1); +} + +.toggle-slider:before { + position: absolute; + content: ""; + height: 18px; + width: 18px; + left: 3px; + bottom: 3px; + background-color: var(--toggle-knob); + border-radius: 50%; + transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); +} + +input:checked + .toggle-slider { + background-color: var(--toggle-bg-active); +} + +input:checked + .toggle-slider:before { + transform: translateX(20px); +} + +.keybind-container { + display: flex; + gap: 0.5rem; + align-items: center; +} + +.keybind-container input { + flex: 1; +} + +.clear-keybind { + padding: 0.5rem; + background: var(--primary); + border: none; + border-radius: 6px; + color: var(--text); + cursor: pointer; + font-size: 1rem; + line-height: 1; + transition: all 0.2s ease; +} + +.clear-keybind:hover { + background: var(--primary-hover); +} + +.custom-grid-controls { + display: flex; + flex-direction: column; + gap: 12px; + width: 100%; + margin-top: 10px; +} + +.grid-control { + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + width: 100%; +} + +.grid-control input[type="number"] { + width: 80px; + padding: 6px 10px; + border: 1px solid var(--border); + border-radius: 4px; + background: var(--surface); + color: var(--text); + font-size: 14px; +} + +.grid-control input[type="number"]:disabled { + opacity: 0.5; + cursor: not-allowed; + background: rgba(0, 0, 0, 0.05); + border-color: var(--border); +} + +[data-theme="dark"] .grid-control input[type="number"]:disabled { + background: rgba(255, 255, 255, 0.05); +} + +.grid-control label { + font-size: 14px; + color: var(--text); + font-weight: 500; + flex: 1; +} + +.grid-default { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(80px, 1fr)); + gap: 1rem; +} + +.grid-compact { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(70px, 1fr)); + gap: 0.5rem; +} + +.grid-comfortable { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(100px, 1fr)); + gap: 1.5rem; +} + +.grid-list { + display: grid; + grid-template-columns: 1fr; + gap: 0.75rem; +} + +.grid-list .shortcut { + display: flex; + flex-direction: row; + justify-content: flex-start; + padding: 0.75rem 1.25rem; + text-align: left; + gap: 1rem; +} + +.grid-list .shortcut img { + margin-right: 0.5rem; +} + +.grid-list .shortcut span { + white-space: normal; +} + +@keyframes fadeIn { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.custom-select { + position: relative; + width: 100%; + user-select: none; + -webkit-user-select: none; + -webkit-tap-highlight-color: transparent; +} + +.custom-select select { + display: none; +} + +.select-selected { + background: var(--surface); + padding: 1rem 1.25rem; + border: 2px solid var(--border); + border-radius: 12px; + cursor: pointer; + display: flex; + align-items: center; + gap: 0.875rem; + color: var(--text); + position: relative; + transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); + font-weight: 500; +} + +.select-selected::after { + content: ''; + position: absolute; + right: 1.25rem; + top: 50%; + width: 8px; + height: 8px; + border-right: 2px solid var(--text); + border-bottom: 2px solid var(--text); + transform: translateY(-70%) rotate(45deg); + transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); +} + +.select-selected.select-arrow-active::after { + transform: translateY(-30%) rotate(-135deg); +} + +.select-items { + position: absolute; + top: calc(100% + 0.5rem); + left: 0; + right: 0; + z-index: 99; + background: var(--surface); + border: 2px solid var(--border); + border-radius: 12px; + overflow: hidden; + box-shadow: 0 4px 20px var(--shadow); +} + +.select-items div { + padding: 1rem 1.25rem; + cursor: pointer; + display: flex; + align-items: center; + gap: 0.875rem; + color: var(--text); + font-weight: 500; +} + +.select-hide { + display: none; +} + +.search-engine-google::before { + content: ''; + display: inline-block; + width: 20px; + height: 20px; + background-image: url('https://www.google.com/images/branding/googleg/1x/googleg_standard_color_128dp.png'); + background-size: contain; + background-repeat: no-repeat; + background-position: center; + margin-right: 0.5rem; +} + +.search-engine-bing::before { + content: ''; + display: inline-block; + width: 20px; + height: 20px; + background-image: url('https://www.bing.com/favicon.ico'); + background-size: contain; + background-repeat: no-repeat; + background-position: center; + margin-right: 0.5rem; +} + +.search-engine-duckduckgo::before { + content: ''; + display: inline-block; + width: 20px; + height: 20px; + background-image: url('https://duckduckgo.com/favicon.ico'); + background-size: contain; + background-repeat: no-repeat; + background-position: center; + margin-right: 0.5rem; +} + +.search-engine-brave::before { + content: ''; + display: inline-block; + width: 20px; + height: 20px; + background-image: url('https://brave.com/static-assets/images/brave-favicon.png'); + background-size: contain; + background-repeat: no-repeat; + background-position: center; + margin-right: 0.5rem; +} + +.search-engine-qwant::before { + content: ''; + display: inline-block; + width: 20px; + height: 20px; + background-image: url('https://www.qwant.com/favicon.ico'); + background-size: contain; + background-repeat: no-repeat; + background-position: center; + margin-right: 0.5rem; +} + +.search-engine-searxng::before { + content: ''; + display: inline-block; + width: 20px; + height: 20px; + background-image: url('https://www.searx.org/favicon.ico'); + background-size: contain; + background-repeat: no-repeat; + background-position: center; + margin-right: 0.5rem; +} + +.select-items::-webkit-scrollbar { + display: none; +} + +.select-items { + scrollbar-width: none; + -ms-overflow-style: none; +} + +.select-selected:hover { + border-color: var(--text); + transform: translateY(-1px); + box-shadow: 0 4px 12px var(--shadow); +} + +.select-selected:active { + transform: translateY(0); + box-shadow: 0 2px 8px var(--shadow); +} + +.select-items div { + position: relative; + overflow: hidden; +} + +.select-items div:hover { + background-color: var(--primary); + color: var(--text); +} + +.same-as-selected { + background: var(--primary); + font-weight: 600; +} + +.same-as-selected::before { + opacity: 1; + transform: translateX(0); +} + +.select-items div::after { + content: ''; + position: absolute; + top: 50%; + left: 50%; + width: 100%; + height: 100%; + background: var(--text); + opacity: 0; + transform: translate(-50%, -50%) scale(0); + transition: transform 0.5s cubic-bezier(0.34, 1.56, 0.64, 1), opacity 0.3s; + border-radius: 50%; + pointer-events: none; +} + +.select-items div:active::after { + opacity: 0.1; + transform: translate(-50%, -50%) scale(2); + transition: 0s; +} + +.modal-content { + animation: modalSlideIn 0.4s cubic-bezier(0.34, 1.56, 0.64, 1); +} + +@keyframes modalSlideIn { + from { + opacity: 0; + transform: scale(0.9) translateY(-20px); + } + to { + opacity: 1; + transform: scale(1) translateY(0); + } +} + +.settings-page { + transition: opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1), + transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); +} + +.settings-page.active { + transform: translateX(0); +} + +.notification { + animation: slideInRight 0.4s cubic-bezier(0.34, 1.56, 0.64, 1); +} + +@keyframes slideInRight { + from { + transform: translateX(100%); + opacity: 0; + } + to { + transform: translateX(0); + opacity: 1; + } +} + +#greeting { + animation: greetingSlideIn 0.6s cubic-bezier(0.34, 1.56, 0.64, 1); +} + +@keyframes greetingSlideIn { + from { + opacity: 0; + transform: translateY(-20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.search-container { + animation: searchBarSlideIn 0.6s cubic-bezier(0.34, 1.56, 0.64, 1) 0.1s backwards; +} + +@keyframes searchBarSlideIn { + from { + opacity: 0; + transform: translateY(-15px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +#search-bar { + transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); +} + +#search-bar:hover { + box-shadow: 0 8px 32px var(--shadow); +} + +#search-bar:focus { + box-shadow: 0 8px 32px var(--shadow); +} + +#shortcuts-grid { + animation: shortcutsSlideIn 0.6s cubic-bezier(0.34, 1.56, 0.64, 1) 0.2s backwards; +} + +@keyframes shortcutsSlideIn { + from { + opacity: 0; + transform: translateY(-10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.shortcut { + animation: shortcutFadeIn 0.5s cubic-bezier(0.34, 1.56, 0.64, 1) backwards; + transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); +} + +@keyframes shortcutFadeIn { + from { + opacity: 0; + transform: scale(0.9); + } + to { + opacity: 1; + transform: scale(1); + } +} + +.shortcut:active { + transform: scale(0.95); + box-shadow: 0 4px 12px var(--shadow); +} + +#add-shortcut { + animation: addButtonFadeIn 0.5s cubic-bezier(0.34, 1.56, 0.64, 1) 0.3s backwards; + transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); +} + +@keyframes addButtonFadeIn { + from { + opacity: 0; + transform: scale(0.9); + } + to { + opacity: 1; + transform: scale(1); + } +} + +#add-shortcut:hover { + transform: scale(1.1) rotate(180deg); +} + +#add-shortcut:active { + transform: scale(0.9) rotate(180deg); +} +.settings-button { + animation: settingsButtonFadeIn 0.5s cubic-bezier(0.34, 1.56, 0.64, 1) 0.4s backwards; + transition: all 0.4s cubic-bezier(0.34, 1.56, 0.64, 1); +} + +@keyframes settingsButtonFadeIn { + from { + opacity: 0; + transform: translateY(20px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +.settings-button:hover { + transform: rotate(180deg) scale(1.1); +} + +.settings-button:active { + transform: rotate(180deg) scale(0.9); +} + +.context-menu { + animation: contextMenuFadeIn 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); +} + +@keyframes contextMenuFadeIn { + from { + opacity: 0; + transform: scale(0.95); + } + to { + opacity: 1; + transform: scale(1); + } +} + +.search-engine-option { + transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); +} + +.search-engine-option:hover { + transform: translateY(-4px); + box-shadow: 0 8px 24px var(--shadow); +} + +.search-engine-option:active { + transform: scale(0.95); + box-shadow: 0 4px 12px var(--shadow); +} + +.settings-nav-item svg { + width: 18px; + height: 18px; + margin-right: 12px; + transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); +} + +.settings-card-icon svg { + width: 20px; + height: 20px; + transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); +} + +.btn-icon svg { + width: 16px; + height: 16px; +} + +.data-management-buttons svg { + width: 18px; + height: 18px; + vertical-align: middle; + margin-right: 8px; +} + +.settings-nav-item:hover svg { + transform: scale(1.15) translateX(2px); +} + +.settings-card-icon:hover svg { + transform: scale(1.15) rotate(5deg); +} + +[data-icon-style="solid"] svg use { + fill: currentColor; + stroke: none; +} + +[data-icon-style="linear"] svg use { + fill: none; + stroke: currentColor; +} + +.font-size-control { + display: flex; + align-items: center; + gap: 1rem; +} + +.slider-container { + flex: 1; +} + +.font-size-input { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.font-size-input input[type="number"] { + width: 60px; + padding: 0.5rem; + border: 1px solid var(--border); + border-radius: 6px; + background: var(--background); + color: var(--text); + font-size: 0.875rem; +} + +.font-size-input span { + color: var(--text-secondary); +} + +input[type="range"] { + width: 100%; + height: 6px; + background: var(--primary); + border-radius: 3px; + appearance: none; + -webkit-appearance: none; +} + +input[type="range"]::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 18px; + height: 18px; + background: var(--text); + border-radius: 50%; + cursor: pointer; + transition: transform 0.2s ease; +} + +input[type="range"]::-webkit-slider-thumb:hover { + transform: scale(1.2); +} + +.btn-reset { + padding: 0.5rem 1rem; + background: var(--primary); + border: none; + border-radius: 6px; + color: var(--text); + font-size: 0.875rem; + cursor: pointer; + transition: all 0.2s ease; +} + +.btn-reset:hover { + background: var(--primary-hover); + transform: translateY(-1px); +} + +.color-setting { + display: flex; + align-items: center; + gap: 1rem; + padding: 0.75rem 0; + border-top: 1px solid var(--border); +} + +.color-setting:first-child { + border-top: none; +} + +.color-setting-label { + flex: 1; + font-size: 0.875rem; + color: var(--text); +} + +.color-setting input[type="color"] { + width: 40px; + height: 40px; + padding: 0; + border: none; + border-radius: 6px; + background: none; + cursor: pointer; +} + +.color-setting input[type="color"]::-webkit-color-swatch-wrapper { + padding: 0; +} + +.color-setting input[type="color"]::-webkit-color-swatch { + border: 2px solid var(--border); + border-radius: 6px; +} \ No newline at end of file diff --git a/images/backgrounds/cherry.png b/images/backgrounds/cherry.png new file mode 100644 index 0000000..b5ff7f1 Binary files /dev/null and b/images/backgrounds/cherry.png differ diff --git a/images/backgrounds/mommies.png b/images/backgrounds/mommies.png new file mode 100644 index 0000000..5ab3b85 Binary files /dev/null and b/images/backgrounds/mommies.png differ diff --git a/images/backgrounds/peachs-castle.png b/images/backgrounds/peachs-castle.png new file mode 100644 index 0000000..acb0a6d Binary files /dev/null and b/images/backgrounds/peachs-castle.png differ diff --git a/images/backgrounds/windows-xp.jpg b/images/backgrounds/windows-xp.jpg new file mode 100644 index 0000000..3e2ff55 Binary files /dev/null and b/images/backgrounds/windows-xp.jpg differ diff --git a/images/favicon.png b/images/favicon.png new file mode 100644 index 0000000..a2afa5f Binary files /dev/null and b/images/favicon.png differ diff --git a/images/icon128.png b/images/icon128.png new file mode 100644 index 0000000..ee97353 Binary files /dev/null and b/images/icon128.png differ diff --git a/images/icon16.png b/images/icon16.png new file mode 100644 index 0000000..c20d5c1 Binary files /dev/null and b/images/icon16.png differ diff --git a/images/icon48.png b/images/icon48.png new file mode 100644 index 0000000..39c1cd5 Binary files /dev/null and b/images/icon48.png differ diff --git a/index.html b/index.html index 8288f96..02b48f4 100644 --- a/index.html +++ b/index.html @@ -1,334 +1,899 @@ - - - - - - New JSTAR Tab - - - - - - - - - - - - - - - - - - - - - - -
- - - - - - - - - - - - - - - + + + + + + New JSTAR Tab + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/js/backgrounds.js b/js/backgrounds.js new file mode 100644 index 0000000..fe9a249 --- /dev/null +++ b/js/backgrounds.js @@ -0,0 +1,159 @@ +document.addEventListener('DOMContentLoaded', () => { + const MAX_IMAGE_SIZE_MB = 1.5; + const MAX_USER_BACKGROUNDS = 10; + + const backgroundUpload = document.getElementById('background-upload'); + const backgroundPreviewGrid = document.getElementById('background-preview-grid'); + const resetBackground = document.getElementById('default-background'); + + resetBackground.style.order = '-1'; + backgroundPreviewGrid.prepend(resetBackground); + + loadBackgrounds(); + setSavedBackground(); + + backgroundUpload.addEventListener('change', (event) => { + const file = event.target.files[0]; + if (!file) return; + + const maxSizeBytes = MAX_IMAGE_SIZE_MB * 1024 * 1024; + if (file.size > maxSizeBytes) { + notifications.show(`Image exceeds size limit (max ${MAX_IMAGE_SIZE_MB} MB)`, 'error'); + event.target.value = ''; + return; + } + + const existingBackgrounds = JSON.parse(Storage.get('backgrounds') || '[]'); + if (existingBackgrounds.length >= MAX_USER_BACKGROUNDS) { + notifications.show(`Maximum custom backgrounds limit reached!`, 'error'); + event.target.value = ''; + return; + } + + const reader = new FileReader(); + reader.onload = function (e) { + const imageUrl = e.target.result; + addBackgroundPreview(imageUrl, false); + saveBackground(imageUrl); + setCustomBackground(imageUrl); + notifications.show('Background uploaded successfully!', 'success'); + }; + reader.readAsDataURL(file); + }); + + resetBackground.addEventListener('click', () => { + const currentBackground = Storage.get('customBackground'); + if (!currentBackground) return; + + document.body.style.backgroundImage = ''; + Storage.remove('customBackground'); + removeAllSelection(); + notifications.show('Background reset to default!', 'success'); + }); + + function loadBackgrounds() { + try { + const backgrounds = JSON.parse(Storage.get('backgrounds') || '[]'); + if (!Array.isArray(backgrounds)) { + throw new Error('Invalid backgrounds format'); + } + backgrounds.forEach((bg) => { + if (typeof bg === 'string' && + (bg.startsWith('data:image/') || bg.startsWith('images/backgrounds/'))) { + addBackgroundPreview(bg, false); + } + }); + } catch (e) { + console.error('Failed to load backgrounds:', e); + Storage.set('backgrounds', '[]'); + notifications.show('Corrupted backgrounds data - resetting', 'error'); + } + } + + function setSavedBackground() { + const customBackground = Storage.get('customBackground'); + if (customBackground) { + setCustomBackground(customBackground, true); + } + } + + function addBackgroundPreview(imageUrl, isPredefined) { + const preview = document.createElement('div'); + preview.className = 'background-preview' + (isPredefined ? ' predefined' : ' custom'); + preview.style.backgroundImage = `url(${imageUrl})`; + preview.dataset.url = imageUrl; + + if (Storage.get('customBackground') === imageUrl) { + preview.classList.add('selected'); + } + + preview.addEventListener('click', () => { + if (Storage.get('customBackground') === imageUrl) return; + setCustomBackground(imageUrl); + removeAllSelection(); + preview.classList.add('selected'); + }); + + if (!isPredefined) { + const removeIcon = document.createElement('span'); + removeIcon.className = 'remove-icon'; + removeIcon.innerHTML = ''; + removeIcon.addEventListener('click', (e) => { + e.stopPropagation(); + const wasSelected = preview.classList.contains('selected'); + + if (wasSelected) { + document.body.style.backgroundImage = ''; + Storage.remove('customBackground'); + } + + removeBackground(imageUrl); + preview.remove(); + notifications.show('Background removed!', 'success'); + }); + preview.appendChild(removeIcon); + } + + const insertPosition = isPredefined ? 1 : backgroundPreviewGrid.children.length; + backgroundPreviewGrid.insertBefore(preview, backgroundPreviewGrid.children[insertPosition]); + } + + function removeAllSelection() { + document.querySelectorAll('.background-preview').forEach(preview => { + preview.classList.remove('selected'); + }); + } + + function removeBackground(imageUrl) { + const backgrounds = JSON.parse(Storage.get('backgrounds') || '[]'); + Storage.set('backgrounds', JSON.stringify(backgrounds.filter(bg => bg !== imageUrl))); + } + + function saveBackground(imageUrl) { + const backgrounds = JSON.parse(Storage.get('backgrounds') || '[]'); + if (!backgrounds.includes(imageUrl)) { + backgrounds.push(imageUrl); + Storage.set('backgrounds', JSON.stringify(backgrounds)); + } + } + + function setCustomBackground(imageUrl, initialLoad = false) { + document.body.style.backgroundImage = `url(${imageUrl})`; + Storage.set('customBackground', imageUrl); + + if (!initialLoad) { + removeAllSelection(); + const targetPreview = document.querySelector(`.background-preview[data-url="${imageUrl}"]`); + if (targetPreview) targetPreview.classList.add('selected'); + } + } + + const predefinedImages = [ + 'images/backgrounds/cherry.png', + 'images/backgrounds/mommies.png', + 'images/backgrounds/peachs-castle.png', + 'images/backgrounds/windows-xp.jpg', + ]; + + predefinedImages.forEach(image => addBackgroundPreview(image, true)); +}); \ No newline at end of file diff --git a/js/cache-handler.js b/js/cache-handler.js new file mode 100644 index 0000000..0466e41 --- /dev/null +++ b/js/cache-handler.js @@ -0,0 +1,86 @@ +const CacheUpdater = { + update: () => { + const shortcuts = Storage.get('shortcuts') || []; + const faviconUrls = shortcuts.map(shortcut => + `https://www.google.com/s2/favicons?domain=${encodeURIComponent(new URL(shortcut.url).hostname)}&sz=64` + ); + + if (navigator.serviceWorker?.controller && faviconUrls.length > 0) { + navigator.serviceWorker.controller.postMessage({ + action: 'updateFavicons', + urls: faviconUrls + }); + } + } + }; + + const cacheHandler = { + SHORTCUT_CACHE_DURATION: 7 * 24 * 60 * 60 * 1000, + + init() { + this.cleanExpiredCache(); + }, + + cleanExpiredCache() { + const cache = this.getAllCache(); + const now = Date.now(); + + Object.entries(cache).forEach(([key, value]) => { + if (key.startsWith('shortcut_') && value.expiry && value.expiry < now) { + this.removeFromCache(key); + } + }); + }, + + addToCache(key, value, isSearchEngine = false) { + const cacheItem = { + value, + timestamp: Date.now(), + expiry: isSearchEngine ? null : Date.now() + this.SHORTCUT_CACHE_DURATION + }; + + localStorage.setItem(key, JSON.stringify(cacheItem)); + }, + + getFromCache(key) { + const item = localStorage.getItem(key); + if (!item) return null; + + try { + const cacheItem = JSON.parse(item); + + if (cacheItem.expiry && cacheItem.expiry < Date.now()) { + this.removeFromCache(key); + return null; + } + + return cacheItem.value; + } catch (error) { + console.error('Cache read error:', error); + return null; + } + }, + + removeFromCache(key) { + localStorage.removeItem(key); + }, + + getAllCache() { + const cache = {}; + for (let i = 0; i < localStorage.length; i++) { + const key = localStorage.key(i); + if (key.startsWith('shortcut_') || key.startsWith('search_engine_')) { + try { + cache[key] = JSON.parse(localStorage.getItem(key)); + } catch (error) { + console.error('Cache read error:', error); + } + } + } + return cache; + } + }; + + document.addEventListener('DOMContentLoaded', () => { + cacheHandler.init(); + }); \ No newline at end of file diff --git a/js/grid-layout.js b/js/grid-layout.js new file mode 100644 index 0000000..44734ae --- /dev/null +++ b/js/grid-layout.js @@ -0,0 +1,337 @@ +const GridLayout = { + defaults: { + type: 'default', + columns: 6, + gap: 16, + size: 80, + resizable: false + }, + + layouts: { + default: { + columns: 6, + gap: 16, + size: 80 + }, + compact: { + columns: 5, + gap: 8, + size: 70 + }, + comfortable: { + columns: 3, + gap: 24, + size: 100 + }, + list: { + columns: 1, + gap: 12, + size: 60 + }, + custom: {} + }, + + init: function() { + const settings = this.getSettings(); + this.applyLayout(settings); + this.setupEventListeners(); + this.toggleCustomSettings(settings.type === 'custom'); + this.updateUI(settings); + }, + + getSettings: function() { + const storedSettings = Storage.get('gridLayout'); + return { ...this.defaults, ...storedSettings }; + }, + + saveSettings: function(settings) { + Storage.set('gridLayout', settings); + }, + + updateUI: function(settings) { + const layoutTypeSelect = document.getElementById('grid-layout-type'); + if (layoutTypeSelect) { + layoutTypeSelect.value = settings.type; + const event = new Event('change'); + layoutTypeSelect.dispatchEvent(event); + + const customSelectDiv = layoutTypeSelect.closest('.custom-select'); + if (customSelectDiv) { + const selectSelected = customSelectDiv.querySelector('.select-selected'); + if (selectSelected) { + const selectedOption = layoutTypeSelect.options[layoutTypeSelect.selectedIndex]; + selectSelected.textContent = selectedOption.textContent; + } + } + } + + const columnsInput = document.getElementById('grid-columns'); + const gapInput = document.getElementById('grid-gap'); + const sizeInput = document.getElementById('grid-size'); + + if (columnsInput) columnsInput.value = settings.columns; + if (gapInput) gapInput.value = settings.gap; + if (sizeInput) { + sizeInput.value = settings.size; + sizeInput.disabled = !settings.resizable; + if (!settings.resizable) { + sizeInput.setAttribute('title', 'Enable \'Resizable Items\' to customize item size'); + sizeInput.style.cursor = 'not-allowed'; + } else { + sizeInput.removeAttribute('title'); + sizeInput.style.cursor = 'auto'; + } + } + + const resizableToggle = document.getElementById('toggle-resizable'); + if (resizableToggle) resizableToggle.checked = settings.resizable; + + this.toggleCustomSettings(settings.type === 'custom'); + }, + + applyLayout: function(settings) { + const grid = document.getElementById('shortcuts-grid'); + if (!grid) return; + + grid.classList.remove('grid-default', 'grid-compact', 'grid-comfortable', 'grid-list', 'grid-custom'); + grid.classList.add(`grid-${settings.type}`); + + if (settings.type === 'custom') { + grid.style.gridTemplateColumns = `repeat(${settings.columns}, minmax(${settings.size}px, 1fr))`; + grid.style.gap = `${settings.gap}px`; + } else { + const layoutConfig = this.layouts[settings.type]; + if (layoutConfig) { + grid.style.gridTemplateColumns = `repeat(${layoutConfig.columns}, minmax(${layoutConfig.size}px, 1fr))`; + grid.style.gap = `${layoutConfig.gap}px`; + + if (settings.type === 'list') { + grid.style.gridTemplateColumns = '1fr'; + } + } else { + grid.style.gridTemplateColumns = ''; + grid.style.gap = ''; + } + } + + this.applyResizable(settings.resizable); + }, + + setupEventListeners: function() { + const layoutTypeSelect = document.getElementById('grid-layout-type'); + if (layoutTypeSelect) { + layoutTypeSelect.addEventListener('change', () => { + const settings = this.getSettings(); + settings.type = layoutTypeSelect.value; + this.saveSettings(settings); + this.applyLayout(settings); + this.toggleCustomSettings(settings.type === 'custom'); + }); + } + + const columnsInput = document.getElementById('grid-columns'); + const gapInput = document.getElementById('grid-gap'); + const sizeInput = document.getElementById('grid-size'); + + const validateInputValue = (input) => { + const min = parseInt(input.getAttribute('min'), 10); + const max = parseInt(input.getAttribute('max'), 10); + let value = parseInt(input.value, 10); + + if (isNaN(value)) { + value = parseInt(input.defaultValue, 10); + } + + if (value < min) value = min; + if (value > max) value = max; + + input.value = value; + return value; + }; + + [columnsInput, gapInput, sizeInput].forEach(input => { + if (input) { + input.addEventListener('input', () => { + validateInputValue(input); + }); + + input.addEventListener('change', () => { + const value = validateInputValue(input); + const settings = this.getSettings(); + settings[input.id.split('-')[1]] = value; + this.saveSettings(settings); + this.applyLayout(settings); + }); + } + }); + + const resizableToggle = document.getElementById('toggle-resizable'); + if (resizableToggle) { + resizableToggle.addEventListener('change', () => { + const settings = this.getSettings(); + settings.resizable = resizableToggle.checked; + this.saveSettings(settings); + this.applyLayout(settings); + this.toggleItemSizeInput(settings.resizable); + }); + } + + const resetButton = document.getElementById('reset-layout'); + if (resetButton) { + resetButton.addEventListener('click', () => { + this.resetToDefaults(); + }); + } + }, + + toggleItemSizeInput: function(enabled) { + const sizeInput = document.getElementById('grid-size'); + if (sizeInput) { + sizeInput.disabled = !enabled; + if (enabled) { + sizeInput.removeAttribute('title'); + sizeInput.style.cursor = 'auto'; + } else { + sizeInput.setAttribute('title', 'Enable \'Resizable Items\' to customize item size'); + sizeInput.style.cursor = 'not-allowed'; + } + } + }, + + toggleCustomSettings: function(show) { + const customSettings = document.getElementById('custom-grid-settings'); + if (customSettings) { + if (show) { + customSettings.classList.remove('hidden'); + } else { + customSettings.classList.add('hidden'); + } + } + }, + + applyResizable: function(resizable) { + const shortcuts = document.querySelectorAll('.shortcut'); + + shortcuts.forEach(shortcut => { + const existingHandle = shortcut.querySelector('.resize-handle'); + if (existingHandle) { + existingHandle.remove(); + } + + shortcut.classList.remove('resizable'); + + if (resizable) { + shortcut.classList.add('resizable'); + + const resizeHandle = document.createElement('div'); + resizeHandle.className = 'resize-handle'; + shortcut.appendChild(resizeHandle); + + this.setupResizeEvents(shortcut, resizeHandle); + } + }); + }, + + setupResizeEvents: function(shortcut, handle) { + let startX, startY, startWidth, startHeight; + + const startResize = (e) => { + e.preventDefault(); + shortcut.classList.add('resizing'); + + startX = e.clientX; + startY = e.clientY; + startWidth = shortcut.offsetWidth; + startHeight = shortcut.offsetHeight; + + document.addEventListener('mousemove', resize); + document.addEventListener('mouseup', stopResize); + }; + + const resize = (e) => { + const newWidth = startWidth + (e.clientX - startX); + const newHeight = startHeight + (e.clientY - startY); + + shortcut.style.width = `${Math.max(80, newWidth)}px`; + shortcut.style.height = `${Math.max(80, newHeight)}px`; + }; + + const stopResize = () => { + shortcut.classList.remove('resizing'); + document.removeEventListener('mousemove', resize); + document.removeEventListener('mouseup', stopResize); + }; + + handle.addEventListener('mousedown', startResize); + }, + + resetToDefaults: function() { + this.saveSettings(this.defaults); + this.updateUI(this.defaults); + this.applyLayout(this.defaults); + this.toggleCustomSettings(false); + + const visibilitySettings = { + showGreeting: true, + showSearch: true, + showShortcuts: true, + showAddButton: true, + showGrid: true + }; + + Storage.set('visibility', visibilitySettings); + Storage.set('show_greeting', true); + Storage.set('show_search', true); + Storage.set('show_shortcuts', true); + Storage.set('show_addShortcut', true); + + const greetingToggle = document.getElementById('toggle-greeting'); + const searchToggle = document.getElementById('toggle-search'); + const shortcutsToggle = document.getElementById('toggle-shortcuts'); + const addButtonToggle = document.getElementById('toggle-add-shortcut'); + + if (greetingToggle) greetingToggle.checked = true; + if (searchToggle) searchToggle.checked = true; + if (shortcutsToggle) shortcutsToggle.checked = true; + if (addButtonToggle) addButtonToggle.checked = true; + + const greeting = document.getElementById('greeting'); + const search = document.getElementById('search-container'); + const shortcuts = document.getElementById('shortcuts-grid'); + const addButton = document.getElementById('add-shortcut'); + + const showElement = (element) => { + if (element) { + element.style.visibility = 'visible'; + element.style.opacity = '1'; + element.style.position = 'relative'; + element.style.pointerEvents = 'auto'; + } + }; + + showElement(greeting); + showElement(search); + showElement(shortcuts); + showElement(addButton); + }, + + reset: function() { + this.resetToDefaults(); + + notifications.show('Layout settings reset to defaults.', 'success'); + + return this.defaults; + } +}; + +document.addEventListener('DOMContentLoaded', () => { + GridLayout.init(); +}); + +window.addEventListener('load', () => { + setTimeout(() => { + if (!window.gridLayoutInitialized) { + GridLayout.init(); + } + }, 500); +}); \ No newline at end of file diff --git a/js/keybinds.js b/js/keybinds.js index fcee857..26e6a8f 100644 --- a/js/keybinds.js +++ b/js/keybinds.js @@ -1,265 +1,267 @@ -// List of keys that cannot be used as keybinds -const FORBIDDEN_KEYS = [ - 'Tab', 'CapsLock', 'Meta', 'ContextMenu', - 'F1', 'F2', 'F3', 'F4', 'F5', 'F6', 'F7', 'F8', 'F9', 'F10', 'F11', 'F12', - 'Home', 'End', 'PageUp', 'PageDown', 'Insert', 'Delete', 'ScrollLock', 'Pause', 'NumLock' -]; - -const keybinds = { - bindings: {}, - - init() { - this.bindings = Storage.get('keybinds') || {}; - - // URL keybind handling - const urlInput = document.getElementById('keybind-url'); - const urlComboInput = document.getElementById('keybind-url-combo'); - - if (this.bindings.url) { - urlInput.value = this.bindings.url.url || ''; - urlComboInput.value = this.bindings.url.keys || ''; - } - - let lastSavedUrl = urlInput.value; - - function isValidUrl(string) { - try { - const urlString = string.match(/^https?:\/\//) ? string : `https://${string}`; - new URL(urlString); - return true; - } catch (_) { - return false; - } - } - - urlInput.addEventListener('input', () => { - if (!this.bindings.url) { - this.bindings.url = { url: '', keys: '' }; - } - this.bindings.url.url = urlInput.value; - Storage.set('keybinds', this.bindings); - }); - - urlInput.addEventListener('keydown', (e) => { - if (e.key === 'Enter') { - e.preventDefault(); - urlInput.blur(); - } - }); - - urlInput.addEventListener('blur', () => { - const currentUrl = urlInput.value.trim(); - - if (currentUrl === lastSavedUrl) { - return; - } - - if (currentUrl) { - if (isValidUrl(currentUrl)) { - lastSavedUrl = currentUrl; - notifications.show('URL saved.', 'success'); - } else { - notifications.show('Please enter a valid URL.', 'error'); - urlInput.value = lastSavedUrl; - this.bindings.url.url = lastSavedUrl; - Storage.set('keybinds', this.bindings); - } - } - }); - - // Keybind input handling - const keybindInputs = document.querySelectorAll('[id^="keybind-"]'); - keybindInputs.forEach(input => { - if (input.id === 'keybind-url') { - const urlBinding = this.bindings['url']; - if (urlBinding && urlBinding.url) { - input.value = urlBinding.url; - } - - input.addEventListener('input', () => { - if (this.bindings['url']) { - this.bindings['url'].url = input.value; - Storage.set('keybinds', this.bindings); - } - }); - return; - } - - const action = input.id.replace('keybind-url-combo', 'url').replace('keybind-', ''); - if (this.bindings[action]) { - input.value = this.bindings[action].keys; - } - - let currentKeys = new Set(); - let isProcessingKeybind = false; - - input.addEventListener('keydown', (e) => { - e.preventDefault(); - - if (e.key === 'Escape') { - input.blur(); - return; - } - - if (e.ctrlKey) { - notifications.show('CTRL key combinations are not allowed.', 'error'); - isProcessingKeybind = true; - return; - } - - if (FORBIDDEN_KEYS.includes(e.key)) { - notifications.show('This key cannot be used as a keybind.', 'error'); - isProcessingKeybind = true; - return; - } - - isProcessingKeybind = false; - - if (e.key !== 'Alt' && e.key !== 'Shift') { - currentKeys.add(e.key); - } - if (e.altKey) currentKeys.add('Alt'); - if (e.shiftKey) currentKeys.add('Shift'); - - input.value = Array.from(currentKeys).join('+'); - }); - - input.addEventListener('keyup', (e) => { - if (isProcessingKeybind) { - currentKeys.clear(); - return; - } - - if (e.key === 'Alt' || e.key === 'Shift') { - if (currentKeys.size === 1) { - notifications.show('Add another key with Alt or Shift.', 'error'); - } - currentKeys.clear(); - input.value = this.bindings[action]?.keys || ''; - return; - } - - const combo = Array.from(currentKeys).join('+'); - - if (!combo) return; - - const duplicate = Object.entries(this.bindings).find(([key, value]) => - value.keys === combo && key !== action - ); - - if (duplicate) { - notifications.show('This keybind is already in use.', 'error'); - currentKeys.clear(); - input.value = this.bindings[action]?.keys || ''; - return; - } - - this.bindings[action] = { - keys: combo, - url: action === 'url' ? document.getElementById('keybind-url').value : null - }; - Storage.set('keybinds', this.bindings); - notifications.show('Keybind saved.', 'success'); - }); - - input.addEventListener('blur', () => { - currentKeys.clear(); - input.value = this.bindings[action]?.keys || ''; - }); - }); - - // Clear keybind button handling - document.querySelectorAll('.clear-keybind').forEach(button => { - button.addEventListener('click', () => { - const action = button.dataset.for; - const input = document.getElementById(`keybind-${action}-combo`) || - document.getElementById(`keybind-${action}`); - - input.value = ''; - if (action === 'url') { - document.getElementById('keybind-url').value = ''; - } - - delete this.bindings[action]; - Storage.set('keybinds', this.bindings); - notifications.show('Keybind removed.', 'success'); - }); - }); - - // Global keybind listener - document.addEventListener('keydown', (e) => { - if (e.target.tagName === 'INPUT') return; - - const keys = []; - if (e.altKey) keys.push('Alt'); - if (e.shiftKey) keys.push('Shift'); - if (e.key !== 'Alt' && e.key !== 'Shift') keys.push(e.key); - - const combo = keys.join('+'); - - Object.entries(this.bindings).forEach(([action, binding]) => { - if (binding.keys === combo) { - e.preventDefault(); - this.executeAction(action, binding); - } - }); - }); - }, - - // Execute the action associated with a keybind - executeAction(action, binding) { - const activeModal = document.querySelector('.modal.active'); - if (activeModal) { - closeModal(activeModal); - } - - switch (action) { - case 'settings': - const settingsModal = document.getElementById('settings-modal'); - if (settingsModal === activeModal) { - notifications.show('Settings closed.', 'info'); - settings.updateSettingsUI(); - } else { - notifications.show('Opening settings...', 'info'); - settings.updateSettingsUI(); - openModal(settingsModal); - } - break; - case 'add-shortcut': - const currentShortcuts = Storage.get('shortcuts') || []; - if (currentShortcuts.length >= shortcuts.MAX_SHORTCUTS) { - notifications.show('Maximum shortcuts limit reached!', 'error'); - return; - } - - const shortcutModal = document.getElementById('add-shortcut-modal'); - if (shortcutModal === activeModal) { - notifications.show('Add shortcut closed.', 'info'); - } else { - notifications.show('Opening add shortcut...', 'info'); - openModal(shortcutModal); - } - break; - case 'anonymous': - settings.toggleAnonymousMode(); - break; - case 'theme': - settings.toggleTheme(); - break; - case 'url': - if (binding.url) { - const url = binding.url; - const fullUrl = url.startsWith('http://') || url.startsWith('https://') ? - url : `https://${url}`; - - notifications.show(`Redirecting to ${url}...`, 'info'); - setTimeout(() => { - window.location.href = fullUrl; - }, 1000); - } else { - notifications.show('No URL set for this keybind.', 'error'); - } - break; - } - } -}; +const FORBIDDEN_KEYS = [ + 'Tab', 'CapsLock', 'Meta', 'ContextMenu', + 'F1', 'F2', 'F3', 'F4', 'F5', 'F6', 'F7', 'F8', 'F9', 'F10', 'F11', 'F12', + 'Home', 'End', 'PageUp', 'PageDown', 'Insert', 'Delete', 'ScrollLock', 'Pause', 'NumLock', + '/' +]; + +const keybinds = { + bindings: {}, + + init() { + this.bindings = Storage.get('keybinds') || {}; + + const urlInput = document.getElementById('keybind-url'); + const urlComboInput = document.getElementById('keybind-url-combo'); + + if (this.bindings.url) { + urlInput.value = this.bindings.url.url || ''; + urlComboInput.value = this.bindings.url.keys || ''; + } + + let lastSavedUrl = urlInput.value; + + function isValidUrl(string) { + try { + const urlString = string.match(/^https?:\/\//) ? string : `https://${string}`; + new URL(urlString); + return true; + } catch (_) { + return false; + } + } + + urlInput.addEventListener('input', () => { + if (!this.bindings.url) { + this.bindings.url = { url: '', keys: '' }; + } + this.bindings.url.url = urlInput.value; + Storage.set('keybinds', this.bindings); + }); + + urlInput.addEventListener('keydown', (e) => { + if (e.key === 'Enter') { + e.preventDefault(); + urlInput.blur(); + } + }); + + urlInput.addEventListener('blur', () => { + const currentUrl = urlInput.value.trim(); + + if (currentUrl === lastSavedUrl) { + return; + } + + if (currentUrl) { + if (isValidUrl(currentUrl)) { + lastSavedUrl = currentUrl; + notifications.show('URL saved.', 'success'); + } else { + notifications.show('Please enter a valid URL.', 'error'); + urlInput.value = lastSavedUrl; + this.bindings.url.url = lastSavedUrl; + Storage.set('keybinds', this.bindings); + } + } + }); + + const keybindInputs = document.querySelectorAll('[id^="keybind-"]'); + keybindInputs.forEach(input => { + if (input.id === 'keybind-url') { + const urlBinding = this.bindings['url']; + if (urlBinding && urlBinding.url) { + input.value = urlBinding.url; + } + + input.addEventListener('input', () => { + if (this.bindings['url']) { + this.bindings['url'].url = input.value; + Storage.set('keybinds', this.bindings); + } + }); + return; + } + + const action = input.id.replace('keybind-url-combo', 'url').replace('keybind-', ''); + if (this.bindings[action]) { + input.value = this.bindings[action].keys; + } + + let currentKeys = new Set(); + let isProcessingKeybind = false; + + input.addEventListener('keydown', (e) => { + e.preventDefault(); + + if (e.key === 'Escape') { + input.blur(); + return; + } + + if (e.ctrlKey) { + notifications.show('CTRL key combinations are not allowed.', 'error'); + isProcessingKeybind = true; + return; + } + + if (FORBIDDEN_KEYS.includes(e.key)) { + notifications.show('This key cannot be used as a keybind.', 'error'); + isProcessingKeybind = true; + return; + } + + isProcessingKeybind = false; + + if (e.key !== 'Alt' && e.key !== 'Shift') { + currentKeys.add(e.key); + } + if (e.altKey) currentKeys.add('Alt'); + if (e.shiftKey) currentKeys.add('Shift'); + + input.value = Array.from(currentKeys).join('+'); + }); + + input.addEventListener('keyup', (e) => { + if (isProcessingKeybind) { + currentKeys.clear(); + return; + } + + if (e.key === 'Alt' || e.key === 'Shift') { + if (currentKeys.size === 1) { + notifications.show('Add another key with Alt or Shift.', 'error'); + } + currentKeys.clear(); + input.value = this.bindings[action]?.keys || ''; + return; + } + + const combo = Array.from(currentKeys).join('+'); + + if (!combo) return; + + const duplicate = Object.entries(this.bindings).find(([key, value]) => + value.keys === combo && key !== action + ); + + if (duplicate) { + notifications.show('This keybind is already in use.', 'error'); + currentKeys.clear(); + input.value = this.bindings[action]?.keys || ''; + return; + } + + this.bindings[action] = { + keys: combo, + url: action === 'url' ? document.getElementById('keybind-url').value : null + }; + Storage.set('keybinds', this.bindings); + notifications.show('Keybind saved.', 'success'); + }); + + input.addEventListener('blur', () => { + currentKeys.clear(); + input.value = this.bindings[action]?.keys || ''; + }); + }); + + document.querySelectorAll('.clear-keybind').forEach(button => { + button.addEventListener('click', () => { + const action = button.dataset.for; + const input = document.getElementById(`keybind-${action}-combo`) || + document.getElementById(`keybind-${action}`); + + input.value = ''; + if (action === 'url') { + document.getElementById('keybind-url').value = ''; + } + + delete this.bindings[action]; + Storage.set('keybinds', this.bindings); + notifications.show('Keybind removed.', 'success'); + }); + }); + + document.addEventListener('keydown', (e) => { + if (e.target.tagName === 'INPUT') return; + + const keys = []; + if (e.altKey) keys.push('Alt'); + if (e.shiftKey) keys.push('Shift'); + if (e.key !== 'Alt' && e.key !== 'Shift') keys.push(e.key); + + const combo = keys.join('+'); + + Object.entries(this.bindings).forEach(([action, binding]) => { + if (binding.keys === combo) { + e.preventDefault(); + this.executeAction(action, binding); + } + }); + }); + }, + + executeAction(action, binding) { + const settingsPage = document.getElementById('settings-page'); + if (settingsPage.classList.contains('active')) { + settingsPage.classList.remove('active'); + setTimeout(() => { + settingsPage.classList.add('hidden'); + }, 300); + } + + const activeModal = document.querySelector('.modal.active'); + + switch (action) { + case 'settings': + if (settingsPage.classList.contains('hidden')) { + notifications.show('Opening settings.', 'info'); + settings.updateSettingsUI(); + settingsPage.classList.remove('hidden'); + setTimeout(() => { + settingsPage.classList.add('active'); + }, 10); + } else { + notifications.show('Settings closed.', 'info'); + settings.updateSettingsUI(); + } + break; + case 'add-shortcut': + const currentShortcuts = Storage.get('shortcuts') || []; + if (currentShortcuts.length >= shortcuts.MAX_SHORTCUTS) { + notifications.show('Maximum shortcuts limit reached!', 'error'); + return; + } + + const shortcutModal = document.getElementById('add-shortcut-modal'); + if (shortcutModal === activeModal) { + notifications.show('Add shortcut menu closed.', 'info'); + } else { + notifications.show('Opening add shortcut menu.', 'info'); + openModal(shortcutModal); + } + break; + case 'anonymous': + settings.toggleAnonymousMode(); + break; + case 'theme': + settings.toggleTheme(); + break; + case 'url': + if (binding.url) { + const url = binding.url; + const fullUrl = url.startsWith('http://') || url.startsWith('https://') ? + url : `https://${url}`; + + notifications.show(`Redirecting to ${url}...`, 'info'); + setTimeout(() => { + window.location.href = fullUrl; + }, 1000); + } else { + notifications.show('No URL set for this keybind.', 'error'); + } + break; + } + } +}; \ No newline at end of file diff --git a/js/main.js b/js/main.js index 56d15fc..d668620 100644 --- a/js/main.js +++ b/js/main.js @@ -1,128 +1,145 @@ -// Greeting functionality -async function updateGreeting() { - const greeting = document.getElementById('greeting'); - if (!greeting) return; - - const customFormat = Storage.get('customGreeting'); - if (customFormat) { - const formattedGreeting = await settings.formatGreeting(customFormat); - if (formattedGreeting) { - greeting.textContent = formattedGreeting; - greeting.style.opacity = '0'; - setTimeout(() => { - greeting.style.opacity = '1'; - }, 100); - return; - } - } - - const hour = new Date().getHours(); - const isAnonymous = Storage.get('anonymousMode') || false; - const userName = isAnonymous ? - (Storage.get('anonymousName') || anonymousNames.generate()) : - (Storage.get('userName') || 'Friend'); - - let timeGreeting = 'Hello'; - if (hour >= 5 && hour < 12) timeGreeting = 'Good Morning'; - else if (hour >= 12 && hour < 17) timeGreeting = 'Good Afternoon'; - else if (hour >= 17 && hour < 20) timeGreeting = 'Good Evening'; - else timeGreeting = 'Good Night'; - - greeting.textContent = `${timeGreeting}, ${userName}!`; - greeting.style.opacity = '0'; - setTimeout(() => { - greeting.style.opacity = '1'; - }, 100); -} - -// Modal handling -function initModalHandlers() { - const modals = document.querySelectorAll('.modal'); - - modals.forEach(modal => { - modal.addEventListener('click', (e) => { - if (e.target === modal && !modal.classList.contains('onboarding-modal')) { - closeModal(modal); - } - }); - - const modalContent = modal.querySelector('.modal-content'); - if (modalContent) { - modalContent.addEventListener('click', (e) => { - e.stopPropagation(); - }); - } - - document.querySelectorAll('.modal .close-button').forEach(button => { - button.addEventListener('click', () => { - const modal = button.closest('.modal'); - if (modal) { - closeModal(modal); - } - }); - }); - }); -} - -function openModal(modal) { - if (!modal) return; - modal.classList.remove('hidden'); - requestAnimationFrame(() => { - modal.classList.add('active'); - }); -} - -function closeModal(modal) { - if (!modal) return; - modal.classList.remove('active'); - setTimeout(() => { - modal.classList.add('hidden'); - }, 300); -} - -// Application initialization -document.addEventListener('DOMContentLoaded', () => { - ['greeting', 'search', 'shortcuts', 'addShortcut'].forEach(element => { - const isVisible = Storage.get(`show_${element}`); - if (isVisible === false) { - const elementNode = document.getElementById(element === 'search' ? 'search-container' : element); - if (elementNode) elementNode.style.display = 'none'; - } - }); - - if (!Storage.get('onboardingComplete')) { - onboarding.start(); - } else { - document.getElementById('main-content').classList.remove('hidden'); - } - - search.init(); - shortcuts.init(); - settings.init(); - initModalHandlers(); - - updateGreeting(); - setInterval(updateGreeting, 60000); - - const settingsButton = document.getElementById('settings-button'); - const settingsModal = document.getElementById('settings-modal'); - - settingsButton.addEventListener('click', () => { - openModal(settingsModal); - }); - - keybinds.init(); -}); - -// Global keydown event handler -document.addEventListener('keydown', (e) => { - if (e.key === 'Enter') { - const activeModal = document.querySelector('.modal.active'); - if (activeModal && !activeModal.matches('#settings-modal')) { - const primaryButton = activeModal.querySelector('.btn-primary'); - if (primaryButton) { - primaryButton.click(); - } - } - } -}); \ No newline at end of file +if ('serviceWorker' in navigator) { + navigator.serviceWorker.register('/sw.js'); +} + +async function updateGreeting() { + const greeting = document.getElementById('greeting'); + if (!greeting) return; + + const customFormat = Storage.get('customGreeting'); + if (customFormat) { + const formattedGreeting = await settings.formatGreeting(customFormat); + if (formattedGreeting) { + greeting.textContent = formattedGreeting; + greeting.style.opacity = '0'; + setTimeout(() => { + greeting.style.opacity = '1'; + }, 100); + return; + } + } + + const hour = new Date().getHours(); + const isAnonymous = Storage.get('anonymousMode') || false; + const userName = isAnonymous ? + (Storage.get('anonymousName') || anonymousNames.generate()) : + (Storage.get('userName') || 'Friend'); + + let timeGreeting = 'Hello'; + if (hour >= 5 && hour < 12) timeGreeting = 'Good Morning'; + else if (hour >= 12 && hour < 17) timeGreeting = 'Good Afternoon'; + else if (hour >= 17 && hour < 20) timeGreeting = 'Good Evening'; + else timeGreeting = 'Good Night'; + + greeting.textContent = `${timeGreeting}, ${userName}!`; + greeting.style.opacity = '0'; + setTimeout(() => { + greeting.style.opacity = '1'; + }, 100); +} + +function initModalHandlers() { + const modals = document.querySelectorAll('.modal'); + + modals.forEach(modal => { + modal.addEventListener('click', (e) => { + if (e.target === modal && !modal.classList.contains('onboarding-modal')) { + closeModal(modal); + } + }); + + const modalContent = modal.querySelector('.modal-content'); + if (modalContent) { + modalContent.addEventListener('click', (e) => { + e.stopPropagation(); + }); + } + + document.querySelectorAll('.modal .close-button').forEach(button => { + button.addEventListener('click', () => { + const modal = button.closest('.modal'); + if (modal) { + closeModal(modal); + } + }); + }); + }); +} + +function openModal(modal) { + if (!modal) return; + + const contextMenu = document.querySelector('.context-menu'); + if (contextMenu) { + contextMenu.classList.add('hidden'); + } + + modal.classList.remove('hidden'); + requestAnimationFrame(() => { + modal.classList.add('active'); + }); +} + +function closeModal(modal) { + if (!modal) return; + modal.classList.remove('active'); + setTimeout(() => { + modal.classList.add('hidden'); + }, 300); +} + +document.addEventListener('DOMContentLoaded', () => { + if (typeof settings !== 'undefined' && typeof settings.updateVisibility === 'function') { + settings.updateVisibility(); + } else { + ['greeting', 'search', 'shortcuts', 'addShortcut'].forEach(element => { + const isVisible = Storage.get(`show_${element}`); + if (isVisible === false) { + const elementNode = document.getElementById(element === 'search' ? 'search-container' : + element === 'addShortcut' ? 'add-shortcut' : element); + + if (elementNode) { + elementNode.style.visibility = 'hidden'; + elementNode.style.opacity = '0'; + elementNode.style.position = 'absolute'; + elementNode.style.pointerEvents = 'none'; + } + } + }); + } + + if (!Storage.get('onboardingComplete')) { + onboarding.start(); + } else { + document.getElementById('main-content').classList.remove('hidden'); + } + + search.init(); + shortcuts.init(); + settings.init(); + initModalHandlers(); + + updateGreeting(); + setInterval(updateGreeting, 60000); + + const settingsButton = document.getElementById('settings-button'); + const settingsModal = document.getElementById('settings-modal'); + + settingsButton.addEventListener('click', () => { + openModal(settingsModal); + }); + + keybinds.init(); +}); + +document.addEventListener('keydown', (e) => { + if (e.key === 'Enter') { + const activeModal = document.querySelector('.modal.active'); + if (activeModal && !activeModal.matches('#settings-modal')) { + const primaryButton = activeModal.querySelector('.btn-primary'); + if (primaryButton) { + primaryButton.click(); + } + } + } +}); diff --git a/js/notifications.js b/js/notifications.js index 0a2b762..8ab00a7 100644 --- a/js/notifications.js +++ b/js/notifications.js @@ -1,131 +1,121 @@ -// Notification System Class -class NotificationSystem { - constructor() { - this.container = document.getElementById('notification-container'); - this.notifications = new Map(); - } - - // Display a new notification - show(message, type = 'info', duration = 3000) { - const id = Date.now().toString(); - const notification = document.createElement('div'); - notification.className = `notification notification-${type}`; - - // Create notification elements - const icon = this.createIcon(type); - const content = this.createContent(message); - const closeBtn = this.createCloseButton(id); - const progress = this.createProgressBar(type); - - // Assemble notification - notification.appendChild(icon); - notification.appendChild(content); - notification.appendChild(closeBtn); - notification.appendChild(progress); - - this.container.appendChild(notification); - - // Set removal timer - setTimeout(() => this.remove(id), duration); - - // Store notification reference - this.notifications.set(id, { - element: notification, - duration - }); - - this.updateProgress(id); - - return id; - } - - // Remove a notification - remove(id) { - const notification = this.notifications.get(id); - if (notification) { - notification.element.style.animation = 'slideOutRight 0.3s cubic-bezier(0.16, 1, 0.3, 1)'; - setTimeout(() => { - notification.element.remove(); - this.notifications.delete(id); - }, 300); - } - } - - // Update progress bar - updateProgress(id) { - const notification = this.notifications.get(id); - if (notification) { - const progress = notification.element.querySelector('.notification-progress'); - const startTime = Date.now(); - - const update = () => { - const elapsed = Date.now() - startTime; - const percent = 100 - (elapsed / notification.duration * 100); - - if (percent > 0) { - progress.style.width = `${percent}%`; - requestAnimationFrame(update); - } - }; - - requestAnimationFrame(update); - } - } - - // Helper methods for creating notification elements - createIcon(type) { - const icon = document.createElement('i'); - switch(type) { - case 'success': - icon.className = 'fas fa-check-circle'; - icon.style.color = 'var(--success-color, #4caf50)'; - break; - case 'error': - icon.className = 'fas fa-times-circle'; - icon.style.color = 'var(--error-color, #f44336)'; - break; - case 'info': - default: - icon.className = 'fas fa-info-circle'; - icon.style.color = 'var(--info-color, #2196f3)'; - break; - } - return icon; - } - - createContent(message) { - const content = document.createElement('div'); - content.className = 'notification-content'; - content.textContent = message; - return content; - } - - createCloseButton(id) { - const closeBtn = document.createElement('button'); - closeBtn.className = 'notification-close'; - closeBtn.innerHTML = ''; - closeBtn.onclick = () => this.remove(id); - return closeBtn; - } - - createProgressBar(type) { - const progress = document.createElement('div'); - progress.className = 'notification-progress'; - switch(type) { - case 'success': - progress.style.background = 'var(--success-color, #4caf50)'; - break; - case 'error': - progress.style.background = 'var(--error-color, #f44336)'; - break; - case 'info': - default: - progress.style.background = 'var(--info-color, #2196f3)'; - break; - } - return progress; - } -} - -// Initialize the notification system +class NotificationSystem { + constructor() { + this.container = document.getElementById('notification-container'); + this.notifications = new Map(); + } + + show(message, type = 'info', duration = 3000) { + const id = Date.now().toString(); + const notification = document.createElement('div'); + notification.className = `notification notification-${type}`; + + const icon = this.createIcon(type); + const content = this.createContent(message); + const closeBtn = this.createCloseButton(id); + const progress = this.createProgressBar(type); + + notification.appendChild(icon); + notification.appendChild(content); + notification.appendChild(closeBtn); + notification.appendChild(progress); + + this.container.appendChild(notification); + + setTimeout(() => this.remove(id), duration); + + this.notifications.set(id, { + element: notification, + duration + }); + + this.updateProgress(id); + + return id; + } + + remove(id) { + const notification = this.notifications.get(id); + if (notification) { + notification.element.style.animation = 'slideOutRight 0.3s cubic-bezier(0.16, 1, 0.3, 1)'; + setTimeout(() => { + notification.element.remove(); + this.notifications.delete(id); + }, 300); + } + } + + updateProgress(id) { + const notification = this.notifications.get(id); + if (notification) { + const progress = notification.element.querySelector('.notification-progress'); + const startTime = Date.now(); + + const update = () => { + const elapsed = Date.now() - startTime; + const percent = 100 - (elapsed / notification.duration * 100); + + if (percent > 0) { + progress.style.width = `${percent}%`; + requestAnimationFrame(update); + } + }; + + requestAnimationFrame(update); + } + } + + createIcon(type) { + const icon = document.createElement('i'); + switch(type) { + case 'success': + icon.className = 'fas fa-check-circle'; + icon.style.color = 'var(--success-color, #4caf50)'; + break; + case 'error': + icon.className = 'fas fa-times-circle'; + icon.style.color = 'var(--error-color, #f44336)'; + break; + case 'info': + default: + icon.className = 'fas fa-info-circle'; + icon.style.color = 'var(--info-color, #2196f3)'; + break; + } + return icon; + } + + createContent(message) { + const content = document.createElement('div'); + content.className = 'notification-content'; + content.innerHTML = message; + return content; + } + + createCloseButton(id) { + const closeBtn = document.createElement('button'); + closeBtn.className = 'notification-close'; + closeBtn.innerHTML = ''; + closeBtn.onclick = () => this.remove(id); + return closeBtn; + } + + createProgressBar(type) { + const progress = document.createElement('div'); + progress.className = 'notification-progress'; + switch(type) { + case 'success': + progress.style.background = 'var(--success-color, #4caf50)'; + break; + case 'error': + progress.style.background = 'var(--error-color, #f44336)'; + break; + case 'info': + default: + progress.style.background = 'var(--info-color, #2196f3)'; + break; + } + return progress; + } +} + const notifications = new NotificationSystem(); \ No newline at end of file diff --git a/js/onboarding.js b/js/onboarding.js index b644eb9..8854d8d 100644 --- a/js/onboarding.js +++ b/js/onboarding.js @@ -1,136 +1,327 @@ -// Onboarding module -const onboarding = { - // Core functions - isComplete: () => { - return Storage.get('onboardingComplete') === true; - }, - - start: () => { - const modal = document.getElementById('onboarding-modal'); - const mainContent = document.getElementById('main-content'); - const importDataBtn = document.getElementById('import-data-btn'); - const startFreshBtn = document.getElementById('start-fresh-btn'); - const fileInput = document.getElementById('onboarding-import'); - - if (!onboarding.isComplete()) { - modal.classList.remove('hidden'); - modal.classList.add('active'); - mainContent.classList.add('hidden'); - - startFreshBtn.addEventListener('click', () => { - document.querySelector('[data-step="1"]').classList.add('hidden'); - document.querySelector('[data-step="2"]').classList.remove('hidden'); - document.getElementById('next-step-btn').addEventListener('click', () => onboarding.nextStep(2)); - }); - - importDataBtn.addEventListener('click', () => fileInput.click()); - - // Data import handling - fileInput.addEventListener('change', async (e) => { - if (e.target.files.length > 0) { - try { - const file = e.target.files[0]; - const text = await file.text(); - const data = JSON.parse(text); - - if (!data.settings || typeof data.settings !== 'object' || - !Array.isArray(data.shortcuts)) { - throw new Error('Invalid data structure'); - return; - } - - Object.entries(data.settings).forEach(([key, value]) => { - Storage.set(key, value); - }); - - Storage.set('shortcuts', data.shortcuts); - - if (data.keybinds) { - Storage.set('keybinds', data.keybinds); - } - - // Initialize components - search.init(); - shortcuts.init(); - settings.init(); - updateGreeting(); - document.body.setAttribute('data-theme', data.settings.theme || 'light'); - - // Update UI visibility - ['greeting', 'search', 'shortcuts', 'addShortcut'].forEach(element => { - const isVisible = data.settings[`show_${element}`]; - const elementNode = document.getElementById(element === 'search' ? 'search-container' : element); - if (elementNode) { - elementNode.style.display = isVisible === false ? 'none' : 'block'; - } - }); - - Storage.set('onboardingComplete', true); - modal.classList.add('hidden'); - mainContent.classList.remove('hidden'); - - const userName = Storage.get('userName') || 'Guest'; - notifications.show(`Welcome back, ${userName}! 👋`, 'success'); - } catch (error) { - notifications.show('Failed to import data: Invalid file format!', 'error'); - fileInput.value = ''; - } - } - }); - - // Search engine selection - const engines = document.querySelectorAll('.search-engine-option'); - engines.forEach(engine => { - engine.addEventListener('click', () => { - engines.forEach(e => e.classList.remove('selected')); - engine.classList.add('selected'); - }); - }); - - document.getElementById('complete-setup-btn').addEventListener('click', onboarding.complete); - } else { - modal.classList.add('hidden'); - mainContent.classList.remove('hidden'); - } - }, - - // Onboarding step navigation - nextStep: (currentStep) => { - const currentStepEl = document.querySelector(`[data-step="${currentStep}"]`); - const nextStepEl = document.querySelector(`[data-step="${currentStep + 1}"]`); - - if (currentStep === 2) { - const name = document.getElementById('user-name').value.trim(); - if (!name) { - notifications.show('Please enter your name!', 'error'); - return; - } - Storage.set('userName', name); - } - - currentStepEl.classList.add('hidden'); - nextStepEl.classList.remove('hidden'); - nextStepEl.classList.add('visible'); - }, - - // Finalize onboarding - complete: () => { - const selectedEngine = document.querySelector('.search-engine-option.selected'); - if (!selectedEngine) { - notifications.show('Please select a search engine!', 'error'); - return; - } - - const searchEngine = selectedEngine.dataset.engine; - Storage.set('searchEngine', searchEngine); - Storage.set('onboardingComplete', true); - - const modal = document.getElementById('onboarding-modal'); - const mainContent = document.getElementById('main-content'); - - modal.classList.add('hidden'); - mainContent.classList.remove('hidden'); - notifications.show('Welcome to your new tab! 👋', 'success'); - updateGreeting(); - } -}; \ No newline at end of file +const onboarding = { + currentStep: 1, + totalSteps: 5, + settings: {}, + lastNotification: 0, + isCompleting: false, + notificationShown: false, + + showNotification(message, type = 'info') { + const now = Date.now(); + if (now - this.lastNotification >= 500) { + this.lastNotification = now; + notifications.show(message, type); + } + }, + + isComplete: () => { + return Storage.get('onboardingComplete') === true; + }, + + start: () => { + const onboardingContainer = document.getElementById('onboarding-container'); + const mainContent = document.getElementById('main-content'); + const fileInput = document.getElementById('onboarding-import'); + + document.getElementById('notification-container').style.zIndex = "20000"; + + if (!onboarding.isComplete()) { + document.body.style.overflow = 'hidden'; + onboardingContainer.classList.remove('hidden'); + + onboarding.initProgressDots(); + onboarding.setupEventListeners(); + + const theme = Storage.get('theme') || 'light'; + document.body.setAttribute('data-theme', theme); + + document.querySelectorAll('.step-ob').forEach(step => { + if (step.dataset.step !== "1") { + step.classList.remove('active-ob'); + } + }); + + const firstStep = document.querySelector('.step-ob[data-step="1"]'); + firstStep.classList.add('active-ob'); + + document.getElementById('prev-step').style.visibility = 'hidden'; + + const nextButton = document.getElementById('next-step'); + nextButton.innerHTML = 'Next '; + + if (onboarding.currentStep > 1) { + nextButton.disabled = true; + nextButton.classList.add('disabled-ob'); + } + + mainContent.classList.add('hidden'); + } else { + mainContent.classList.remove('hidden'); + } + + fileInput.addEventListener('change', async (e) => { + if (e.target.files.length > 0) { + try { + const file = e.target.files[0]; + const text = await file.text(); + const data = JSON.parse(text); + + if (!data.settings || !data.shortcuts || !Array.isArray(data.shortcuts)) { + throw new Error('Invalid data structure'); + } + + Object.entries(data.settings).forEach(([key, value]) => { + Storage.set(key, value); + }); + + Storage.set('shortcuts', data.shortcuts); + + if (data.keybinds) { + Storage.set('keybinds', data.keybinds); + } + + Storage.set('onboardingComplete', true); + + localStorage.setItem('showWelcomeAfterImport', 'true'); + + window.location.reload(); + } catch (error) { + onboarding.showNotification('Failed to import data: Invalid file format!', 'error'); + fileInput.value = ''; + } + } + }); + }, + + setupEventListeners: () => { + document.querySelectorAll('.option-card-ob').forEach(card => { + card.addEventListener('click', () => { + const step = card.closest('.step-ob'); + const stepNumber = parseInt(step.dataset.step); + const cards = step.querySelectorAll('.option-card-ob'); + const nextButton = document.getElementById('next-step'); + + if (card.dataset.action === 'import-data') { + if (!card.classList.contains('selected-ob')) { + document.getElementById('onboarding-import').click(); + } + cards.forEach(c => c.classList.remove('selected-ob')); + card.classList.add('selected-ob'); + return; + } + + cards.forEach(c => c.classList.remove('selected-ob')); + card.classList.add('selected-ob'); + + card.style.transform = 'scale(1.05)'; + setTimeout(() => { + card.style.transform = 'scale(1.02)'; + }, 150); + + nextButton.disabled = false; + nextButton.classList.remove('disabled-ob'); + + if (card.dataset.theme) { + onboarding.settings.theme = card.dataset.theme; + document.body.setAttribute('data-theme', card.dataset.theme); + } else if (card.dataset.font) { + onboarding.settings.fontFamily = card.dataset.font; + document.documentElement.style.setProperty('--font-family', card.dataset.font); + } else if (card.dataset.engine) { + onboarding.settings.searchEngine = card.dataset.engine; + } + }); + }); + + const nameInput = document.getElementById('user-name'); + const nextButton = document.getElementById('next-step'); + + nameInput.addEventListener('input', (e) => { + const name = e.target.value.trim(); + if (name) { + onboarding.settings.userName = name; + + if (onboarding.currentStep === 4) { + nextButton.disabled = false; + nextButton.classList.remove('disabled-ob'); + } + } else { + if (onboarding.currentStep === 4) { + nextButton.disabled = true; + nextButton.classList.add('disabled-ob'); + } + } + }); + + document.getElementById('prev-step').addEventListener('click', () => { + if (onboarding.currentStep > 1) { + onboarding.navigateToStep(onboarding.currentStep - 1); + } + }); + + document.getElementById('next-step').addEventListener('click', () => { + let canProceed = true; + + if (onboarding.currentStep === 2 && !onboarding.settings.theme) { + onboarding.showNotification('Please select a theme!', 'error'); + canProceed = false; + } + + else if (onboarding.currentStep === 3 && !onboarding.settings.fontFamily) { + onboarding.showNotification('Please select a font!', 'error'); + canProceed = false; + } + + else if (onboarding.currentStep === 4) { + const name = document.getElementById('user-name').value.trim(); + + if (!name) { + onboarding.showNotification('Please enter your name!', 'error'); + canProceed = false; + } else { + onboarding.settings.userName = name; + } + } + + else if (onboarding.currentStep === 5 && !onboarding.settings.searchEngine) { + onboarding.showNotification('Please select a search engine!', 'error'); + canProceed = false; + } + + if (canProceed) { + if (onboarding.currentStep < onboarding.totalSteps) { + onboarding.navigateToStep(onboarding.currentStep + 1); + } else { + onboarding.isCompleting = true; + localStorage.setItem('showWelcomeAfterImport', 'true'); + onboarding.complete(); + } + } + }); + + document.querySelectorAll('.step-ob').forEach(step => { + if (step.dataset.step !== "1" && step.dataset.step !== "4") { + const firstOption = step.querySelector('.option-card-ob'); + if (firstOption) { + setTimeout(() => { + firstOption.click(); + }, 100); + } + } + }); + }, + + navigateToStep: (step) => { + const prevButton = document.getElementById('prev-step'); + const nextButton = document.getElementById('next-step'); + + const currentStepEl = document.querySelector(`.step-ob[data-step="${onboarding.currentStep}"]`); + if (currentStepEl) { + currentStepEl.classList.remove('active-ob'); + } + + setTimeout(() => { + const targetStepEl = document.querySelector(`.step-ob[data-step="${step}"]`); + if (targetStepEl) { + targetStepEl.classList.add('active-ob'); + } + + onboarding.currentStep = step; + + prevButton.style.visibility = step === 1 ? 'hidden' : 'visible'; + + if (step === onboarding.totalSteps) { + nextButton.innerHTML = 'Get Started '; + } else { + nextButton.innerHTML = 'Next '; + } + + if ((step === 2 && !onboarding.settings.theme) || + (step === 3 && !onboarding.settings.fontFamily) || + (step === 5 && !onboarding.settings.searchEngine)) { + nextButton.disabled = true; + nextButton.classList.add('disabled-ob'); + } else if (step === 4) { + const name = document.getElementById('user-name').value.trim(); + if (!name) { + nextButton.disabled = true; + nextButton.classList.add('disabled-ob'); + } else { + nextButton.disabled = false; + nextButton.classList.remove('disabled-ob'); + } + } else if (step === 1) { + nextButton.disabled = false; + nextButton.classList.remove('disabled-ob'); + } + + onboarding.updateProgressDots(); + }, 100); + }, + + initProgressDots: () => { + const container = document.querySelector('.progress-dots-ob'); + container.innerHTML = ''; + + for (let i = 0; i < onboarding.totalSteps; i++) { + const dot = document.createElement('div'); + dot.className = 'dot-ob' + (i === 0 ? ' active-ob' : ''); + container.appendChild(dot); + } + }, + + updateProgressDots: () => { + const dots = document.querySelectorAll('.dot-ob'); + dots.forEach((dot, index) => { + dot.classList.toggle('active-ob', index + 1 === onboarding.currentStep); + }); + }, + + complete: () => { + const onboardingContainer = document.getElementById('onboarding-container'); + const mainContent = document.getElementById('main-content'); + + if (!onboarding.settings.theme) onboarding.settings.theme = 'light'; + if (!onboarding.settings.fontFamily) onboarding.settings.fontFamily = 'Inter'; + if (!onboarding.settings.searchEngine) onboarding.settings.searchEngine = 'google'; + if (!onboarding.settings.userName) onboarding.settings.userName = 'User'; + + document.body.setAttribute('data-theme', onboarding.settings.theme); + document.documentElement.style.setProperty('--font-family', onboarding.settings.fontFamily); + + Object.entries(onboarding.settings).forEach(([key, value]) => { + Storage.set(key, value); + }); + + Storage.set('onboardingComplete', true); + + setTimeout(() => { + onboardingContainer.classList.add('hidden'); + mainContent.classList.remove('hidden'); + document.body.style.overflow = ''; + + search.init(); + shortcuts.init(); + settings.init(); + updateGreeting(); + + setTimeout(() => { + if (!onboarding.notificationShown) { + onboarding.notificationShown = true; + onboarding.showNotification('Welcome to your new JSTAR Tab! 🎉', 'success'); + } + }, 100); + }, 500); + } +}; + +document.addEventListener('DOMContentLoaded', () => { + onboarding.start(); + + if (onboarding.isComplete() && localStorage.getItem('showWelcomeAfterImport') === 'true') { + localStorage.removeItem('showWelcomeAfterImport'); + setTimeout(() => { + notifications.show('Welcome to your new JSTAR Tab! 🎉', 'success'); + }, 500); + } +}); \ No newline at end of file diff --git a/js/search.js b/js/search.js index 7998479..c6230d5 100644 --- a/js/search.js +++ b/js/search.js @@ -1,47 +1,80 @@ -const search = { - // Supported search engines and their URLs - engines: { - google: 'https://www.google.com/search?q=', - bing: 'https://www.bing.com/search?q=', - duckduckgo: 'https://duckduckgo.com/?q=', - brave: 'https://search.brave.com/search?q=', - qwant: 'https://www.qwant.com/?q=', - searxng: 'https://searx.org/search?q=' - }, - - // Perform search using selected engine - perform: () => { - const searchBar = document.getElementById('search-bar'); - const query = searchBar.value.trim(); - const engine = Storage.get('searchEngine') || 'google'; - - if (query) { - const searchUrl = search.engines[engine] + encodeURIComponent(query); - window.location.href = searchUrl; - } - }, - - // Initialize search functionality - init: () => { - const searchBar = document.getElementById('search-bar'); - const searchButton = document.getElementById('search-button'); - - searchBar.addEventListener('keypress', (e) => { - if (e.key === 'Enter') { - search.perform(); - } - }); - - searchButton.addEventListener('click', search.perform); - - // Global keyboard shortcut to focus search bar - document.addEventListener('keydown', (e) => { - if (e.key === '/' && - !['INPUT', 'TEXTAREA'].includes(document.activeElement.tagName) && - window.getSelection().toString() === '') { - e.preventDefault(); - searchBar.focus(); - } - }); - } -}; \ No newline at end of file +const search = { + engines: { + google: { + url: 'https://www.google.com/search?q=', + icon: 'https://www.google.com/s2/favicons?domain=google.com&sz=32', + name: 'Google' + }, + bing: { + url: 'https://www.bing.com/search?q=', + icon: 'https://www.google.com/s2/favicons?domain=bing.com&sz=32', + name: 'Bing' + }, + duckduckgo: { + url: 'https://duckduckgo.com/?q=', + icon: 'https://www.google.com/s2/favicons?domain=duckduckgo.com&sz=32', + name: 'DuckDuckGo' + }, + brave: { + url: 'https://search.brave.com/search?q=', + icon: 'https://www.google.com/s2/favicons?domain=brave.com&sz=32', + name: 'Brave' + }, + qwant: { + url: 'https://www.qwant.com/?q=', + icon: 'https://www.google.com/s2/favicons?domain=qwant.com&sz=32', + name: 'Qwant' + }, + searxng: { + url: 'https://searx.be/search?q=', + icon: 'https://www.google.com/s2/favicons?domain=searx.be&sz=32', + name: 'SearXNG' + } + }, + + init: () => { + const searchBar = document.getElementById('search-bar'); + const searchButton = document.getElementById('search-button'); + + searchBar.addEventListener('keypress', (e) => { + if (e.key === 'Enter') { + search.perform(); + } + }); + + searchButton.addEventListener('click', search.perform); + + document.addEventListener('keydown', (e) => { + if (e.key === '/' && + !['INPUT', 'TEXTAREA'].includes(document.activeElement.tagName) && + window.getSelection().toString() === '') { + e.preventDefault(); + searchBar.focus(); + } + }); + + const searchEngine = Storage.get('searchEngine') || 'google'; + search.updateSearchEngineIcon(searchEngine); + }, + + updateSearchEngineIcon(engine) { + const searchIcon = document.querySelector('#search-container .search-icon img'); + if (!searchIcon) return; + searchIcon.src = this.engines[engine].icon; + }, + + perform: () => { + const searchBar = document.getElementById('search-bar'); + const query = searchBar.value.trim(); + const engine = Storage.get('searchEngine') || 'google'; + + if (query) { + const searchUrl = search.engines[engine].url + encodeURIComponent(query); + window.location.href = searchUrl; + } + } +}; + +document.addEventListener('DOMContentLoaded', () => { + search.init(); +}); \ No newline at end of file diff --git a/js/settings.js b/js/settings.js index be7738f..30f2498 100644 --- a/js/settings.js +++ b/js/settings.js @@ -1,597 +1,1045 @@ -// Anonymous name generator -const anonymousNames = { - adjectives: ['Hidden', 'Secret', 'Mystery', 'Shadow', 'Unknown', 'Silent', 'Stealth', 'Phantom', 'Ghost', 'Anon'], - nouns: [' User', ' Visitor', ' Guest', ' Agent', ' Entity', ' Person', ' Browser', ' Explorer', ' Wanderer', ' Navigator'], - - generate: function() { - const adjective = this.adjectives[Math.floor(Math.random() * this.adjectives.length)]; - const noun = this.nouns[Math.floor(Math.random() * this.nouns.length)]; - return `${adjective}${noun}`; - } -}; - -const getTimeBasedGreeting = () => { - const hour = new Date().getHours(); - if (hour < 12) return 'Good Morning'; - if (hour < 18) return 'Good Afternoon'; - if (hour < 22) return 'Good Evening'; - return 'Good Night'; -}; - -// Main settings object -const settings = { - GREETING_MAX_LENGTH: 60, - - toggleTheme: () => { - const currentTheme = document.body.getAttribute('data-theme'); - const newTheme = currentTheme === 'dark' ? 'light' : 'dark'; - document.body.setAttribute('data-theme', newTheme); - Storage.set('theme', newTheme); - - const themeIcon = document.querySelector('#toggle-theme i'); - themeIcon.className = `fas fa-${newTheme === 'dark' ? 'sun' : 'moon'}`; - }, - - toggleAnonymousMode: () => { - const isAnonymous = Storage.get('anonymousMode') || false; - Storage.set('anonymousMode', !isAnonymous); - - if (!isAnonymous) { - const randomName = anonymousNames.generate(); - Storage.set('anonymousName', randomName); - notifications.show('Anonymous mode enabled!', 'info'); - } else { - Storage.remove('anonymousName'); - notifications.show('Anonymous mode disabled!', 'info'); - } - - shortcuts.render(); - updateGreeting(); - }, - - updateSearchEngine: (engine) => { - Storage.set('searchEngine', engine); - notifications.show('Search engine updated successfully!', 'success'); - }, - - // Update visibility of UI elements - updateVisibility: () => { - const elements = { - greeting: { - id: 'greeting', - toggle: 'toggle-greeting', - functions: ['updateGreeting'], - name: 'Greeting' - }, - search: { - id: 'search-container', - toggle: 'toggle-search', - functions: ['search.init', 'search.perform'], - name: 'Search bar' - }, - shortcuts: { - id: 'shortcuts-grid', - toggle: 'toggle-shortcuts', - functions: ['shortcuts.init', 'shortcuts.render'], - name: 'Shortcuts' - }, - addShortcut: { - id: 'add-shortcut', - toggle: 'toggle-add-shortcut', - functions: [], - name: 'Add shortcut button' - } - }; - - Object.entries(elements).forEach(([key, element]) => { - const isVisible = Storage.get(`show_${key}`); - if (isVisible === null) Storage.set(`show_${key}`, true); - - const toggle = document.getElementById(element.toggle); - const elementNode = document.getElementById(element.id); - - if (toggle && elementNode) { - toggle.checked = isVisible !== false; - if (isVisible === false) { - elementNode.style.visibility = 'hidden'; - elementNode.style.opacity = '0'; - elementNode.style.position = 'absolute'; - elementNode.style.pointerEvents = 'none'; - } else { - elementNode.style.visibility = 'visible'; - elementNode.style.opacity = '1'; - elementNode.style.position = 'relative'; - elementNode.style.pointerEvents = 'auto'; - } - - toggle.addEventListener('change', (e) => { - const isChecked = e.target.checked; - Storage.set(`show_${key}`, isChecked); - - if (isChecked) { - elementNode.style.visibility = 'visible'; - elementNode.style.opacity = '1'; - elementNode.style.position = 'relative'; - elementNode.style.pointerEvents = 'auto'; - } else { - elementNode.style.visibility = 'hidden'; - elementNode.style.opacity = '0'; - elementNode.style.position = 'absolute'; - elementNode.style.pointerEvents = 'none'; - } - - if (key === 'shortcuts') { - const addShortcutBtn = document.getElementById('add-shortcut'); - const addShortcutVisible = Storage.get('show_addShortcut') !== false; - - if (addShortcutBtn && !isChecked && !addShortcutVisible) { - addShortcutBtn.style.visibility = 'hidden'; - addShortcutBtn.style.opacity = '0'; - addShortcutBtn.style.position = 'absolute'; - addShortcutBtn.style.pointerEvents = 'none'; - } else if (addShortcutBtn && addShortcutVisible) { - addShortcutBtn.style.visibility = 'visible'; - addShortcutBtn.style.opacity = '1'; - addShortcutBtn.style.position = 'relative'; - addShortcutBtn.style.pointerEvents = 'auto'; - } - } - - notifications.show( - `${element.name} ${isChecked ? 'shown' : 'hidden'}!`, - isChecked ? 'success' : 'info' - ); - }); - } - }); - }, - - // Initialize settings - init: () => { - const settingsButton = document.getElementById('settings-button'); - const settingsModal = document.getElementById('settings-modal'); - const closeSettings = document.getElementById('close-settings'); - - settingsButton.addEventListener('click', (e) => { - e.stopPropagation(); - settings.updateSettingsUI(); - settingsModal.classList.remove('hidden'); - settingsModal.classList.add('active'); - }); - - closeSettings.addEventListener('click', () => { - settingsModal.classList.remove('active'); - setTimeout(() => { - settingsModal.classList.add('hidden'); - }, 300); - }); - - const themeToggle = document.getElementById('toggle-theme'); - themeToggle.addEventListener('click', settings.toggleTheme); - - const anonymousToggle = document.getElementById('toggle-anonymous'); - anonymousToggle.addEventListener('change', settings.toggleAnonymousMode); - - const searchEngineSelect = document.getElementById('search-engine-select'); - searchEngineSelect.addEventListener('change', (e) => { - settings.updateSearchEngine(e.target.value); - }); - - const nameInput = document.getElementById('settings-name'); - nameInput.addEventListener('change', (e) => { - const newName = e.target.value.trim(); - if (newName) { - Storage.set('userName', newName); - updateGreeting(); - notifications.show('Name updated successfully!', 'success'); - } - }); - - const savedTheme = Storage.get('theme') || 'light'; - document.body.setAttribute('data-theme', savedTheme); - const themeIcon = document.querySelector('#toggle-theme i'); - themeIcon.className = `fas fa-${savedTheme === 'dark' ? 'sun' : 'moon'}`; - - settings.initDataManagement(); - settings.updateVisibility(); - - const customGreetingInput = document.getElementById('custom-greeting'); - customGreetingInput.maxLength = settings.GREETING_MAX_LENGTH; - - customGreetingInput.addEventListener('input', (e) => { - const value = e.target.value; - if (value.length > settings.GREETING_MAX_LENGTH) { - e.target.value = value.substring(0, settings.GREETING_MAX_LENGTH); - notifications.show(`Custom greeting must be less than ${settings.GREETING_MAX_LENGTH} characters`, 'error'); - } - }); - - customGreetingInput.addEventListener('change', (e) => { - const value = e.target.value.trim(); - - if (value.length > settings.GREETING_MAX_LENGTH) { - e.target.value = value.substring(0, settings.GREETING_MAX_LENGTH); - notifications.show(`Custom greeting must be less than ${settings.GREETING_MAX_LENGTH} characters`, 'error'); - return; - } - - if (value === '') { - Storage.remove('customGreeting'); - notifications.show('Using default greeting format', 'info'); - } else { - const testFormat = settings.formatGreeting(value); - if (testFormat) { - Storage.set('customGreeting', value); - notifications.show('Custom greeting updated!', 'success'); - } - } - updateGreeting(); - }); - }, - - updateSettingsUI: () => { - const userName = Storage.get('userName') || ''; - const isAnonymous = Storage.get('anonymousMode') || false; - const currentEngine = Storage.get('searchEngine') || 'google'; - - document.getElementById('settings-name').value = userName; - document.getElementById('toggle-anonymous').checked = isAnonymous; - document.getElementById('search-engine-select').value = currentEngine; - - ['greeting', 'search', 'shortcuts', 'addShortcut'].forEach(element => { - const isVisible = Storage.get(`show_${element}`); - const toggle = document.getElementById(`toggle-${element}`); - if (toggle) { - toggle.checked = isVisible !== false; - } - }); - - const customGreeting = Storage.get('customGreeting') || ''; - document.getElementById('custom-greeting').value = customGreeting; - }, - - formatGreeting: (format) => { - try { - if (!format) return null; - - const validTokens = ['{name}', '{greeting}', '{time}', '{date}', '{day}', '{month}', '{year}']; - const tokens = format.match(/{[^}]+}/g) || []; - - if (!tokens.every(token => validTokens.includes(token))) { - notifications.show('Invalid greeting format: Unknown token used', 'error'); - return null; - } - - const now = new Date(); - const hour = now.getHours(); - let timeGreeting = 'good night'; - if (hour >= 5 && hour < 12) timeGreeting = 'good morning'; - else if (hour >= 12 && hour < 17) timeGreeting = 'good afternoon'; - else if (hour >= 17 && hour < 20) timeGreeting = 'good evening'; - - const userName = Storage.get('anonymousMode') ? - (Storage.get('anonymousName') || anonymousNames.generate()) : - (Storage.get('userName') || 'Friend'); - - const formats = { - name: userName, - greeting: format.startsWith('{greeting}') ? - timeGreeting.charAt(0).toUpperCase() + timeGreeting.slice(1) : - timeGreeting, - time: now.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' }), - date: now.getDate().toString(), - day: now.toLocaleDateString([], { weekday: 'long' }), - month: now.toLocaleDateString([], { month: 'long' }), - year: now.getFullYear() - }; - - let formattedGreeting = format; - Object.entries(formats).forEach(([key, value]) => { - formattedGreeting = formattedGreeting.replace( - new RegExp(`{${key}}`, 'g'), - value - ); - }); - - if (/{[^}]+}/.test(formattedGreeting)) { - throw new Error('Invalid format used'); - } - - return formattedGreeting; - } catch (e) { - notifications.show('Invalid greeting format. Using default.', 'error'); - return null; - } - }, - - // Data management functions - exportData: () => { - const data = { - settings: { - theme: Storage.get('theme'), - userName: Storage.get('userName'), - anonymousMode: Storage.get('anonymousMode'), - anonymousName: Storage.get('anonymousName'), - searchEngine: Storage.get('searchEngine'), - customGreeting: Storage.get('customGreeting'), - show_greeting: Storage.get('show_greeting'), - show_search: Storage.get('show_search'), - show_shortcuts: Storage.get('show_shortcuts'), - show_addShortcut: Storage.get('show_addShortcut') - }, - shortcuts: Storage.get('shortcuts') || [], - keybinds: Storage.get('keybinds') || {} - }; - - if (!data.settings || !data.shortcuts || !data.keybinds) { - notifications.show('Failed to export data: Invalid data structure.', 'error'); - return; - } - - const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }); - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = 'jstar-tab-backup.json'; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - URL.revokeObjectURL(url); - }, - - importData: async (file) => { - const text = await file.text(); - - try { - const data = JSON.parse(text); - - if (!data.settings || typeof data.settings !== 'object' || - !Array.isArray(data.shortcuts) || - !data.keybinds || typeof data.keybinds !== 'object') { - throw new Error('Invalid data structure'); - } - - Object.entries(data.settings).forEach(([key, value]) => { - Storage.set(key, value); - }); - - Storage.set('shortcuts', data.shortcuts); - - const validatedKeybinds = {}; - Object.entries(data.keybinds).forEach(([action, binding]) => { - if (binding && typeof binding === 'object' && - typeof binding.keys === 'string' && - (!binding.url || typeof binding.url === 'string')) { - validatedKeybinds[action] = binding; - } - }); - Storage.set('keybinds', validatedKeybinds); - - settings.updateSettingsUI(); - settings.updateVisibility(); - shortcuts.render(); - document.body.setAttribute('data-theme', data.settings.theme || 'light'); - - notifications.show('Data imported successfully!', 'success'); - } catch (error) { - notifications.show('Failed to import data: Invalid file format!', 'error'); - console.error('Import error:', error); - } - }, - - resetData: () => { - const defaultSettings = { - theme: 'light', - userName: '', - anonymousMode: false, - searchEngine: 'Google', - show_greeting: true, - show_search: true, - show_shortcuts: true, - show_addShortcut: true - }; - - Object.entries(defaultSettings).forEach(([key, value]) => { - Storage.set(key, value); - }); - - Storage.remove('shortcuts'); - Storage.remove('keybinds'); - Storage.remove('anonymousName'); - Storage.remove('customGreeting'); - - settings.updateSettingsUI(); - settings.updateVisibility(); - shortcuts.render(); - document.body.setAttribute('data-theme', 'light'); - - notifications.show('All data has been reset!', 'success'); - } -}; - -// Initialize data management -settings.initDataManagement = () => { - const exportBtn = document.getElementById('export-data'); - const importBtn = document.getElementById('import-data'); - const resetBtn = document.getElementById('reset-data'); - const fileInput = document.getElementById('import-file'); - - exportBtn.replaceWith(exportBtn.cloneNode(true)); - const newExportBtn = document.getElementById('export-data'); - - newExportBtn.addEventListener('click', () => { - try { - const data = { - settings: { - theme: Storage.get('theme'), - userName: Storage.get('userName'), - anonymousMode: Storage.get('anonymousMode'), - anonymousName: Storage.get('anonymousName'), - searchEngine: Storage.get('searchEngine'), - show_greeting: Storage.get('show_greeting'), - show_search: Storage.get('show_search'), - show_shortcuts: Storage.get('show_shortcuts'), - show_addShortcut: Storage.get('show_addShortcut'), - customGreeting: Storage.get('customGreeting') - }, - shortcuts: Storage.get('shortcuts') || [], - keybinds: Storage.get('keybinds') || {} - }; - - const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }); - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = 'jstar-tab-backup.json'; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - URL.revokeObjectURL(url); - - notifications.show('Data exported successfully!', 'success'); - } catch (error) { - notifications.show('Failed to export data!', 'error'); - console.error('Export error:', error); - } - }); - - importBtn.addEventListener('click', () => fileInput.click()); - - fileInput.addEventListener('change', (e) => { - const file = e.target.files[0]; - if (!file) return; - - const reader = new FileReader(); - reader.onload = (event) => { - try { - const data = JSON.parse(event.target.result); - - if (!data.shortcuts || !data.settings || !data.keybinds) { - throw new Error('Invalid backup file format'); - } - - Object.entries(data.settings).forEach(([key, value]) => { - Storage.set(key, value); - }); - - if (data.keybinds) { - const validatedKeybinds = {}; - Object.entries(data.keybinds).forEach(([action, binding]) => { - if (binding && typeof binding === 'object' && - typeof binding.keys === 'string' && - (!binding.url || typeof binding.url === 'string')) { - validatedKeybinds[action] = binding; - } - }); - Storage.set('keybinds', validatedKeybinds); - keybinds.init(); - } - - Storage.set('shortcuts', data.shortcuts); - - fileInput.value = ''; - - ['greeting', 'search', 'shortcuts', 'addShortcut'].forEach(element => { - const isVisible = data.settings[`show_${element}`]; - const elementNode = document.getElementById(element === 'search' ? 'search-container' : element); - const toggle = document.getElementById(`toggle-${element}`); - - if (elementNode && toggle) { - toggle.checked = isVisible !== false; - if (isVisible === false) { - elementNode.style.visibility = 'hidden'; - elementNode.style.opacity = '0'; - elementNode.style.position = 'absolute'; - elementNode.style.pointerEvents = 'none'; - } else { - elementNode.style.visibility = 'visible'; - elementNode.style.opacity = '1'; - elementNode.style.position = 'relative'; - elementNode.style.pointerEvents = 'auto'; - } - } - }); - - const shortcutsVisible = data.settings.show_shortcuts !== false; - const addShortcutVisible = data.settings.show_addShortcut !== false; - const addShortcutBtn = document.getElementById('add-shortcut'); - - if (addShortcutBtn) { - if (!shortcutsVisible && !addShortcutVisible) { - addShortcutBtn.style.visibility = 'hidden'; - addShortcutBtn.style.opacity = '0'; - addShortcutBtn.style.position = 'absolute'; - addShortcutBtn.style.pointerEvents = 'none'; - } else if (addShortcutVisible) { - addShortcutBtn.style.visibility = 'visible'; - addShortcutBtn.style.opacity = '1'; - addShortcutBtn.style.position = 'relative'; - addShortcutBtn.style.pointerEvents = 'auto'; - } - } - - shortcuts.render(); - updateGreeting(); - search.init(); - document.body.setAttribute('data-theme', data.settings.theme || 'light'); - settings.updateSettingsUI(); - settings.updateVisibility(); - - const greetingElement = document.getElementById('greeting'); - if (greetingElement) { - const userName = data.settings.anonymousMode ? data.settings.anonymousName : data.settings.userName; - const timeBasedGreeting = getTimeBasedGreeting(); - greetingElement.textContent = `${timeBasedGreeting}, ${userName || 'Guest'}!`; - } - - const searchInput = document.getElementById('search-input'); - if (searchInput) { - searchInput.placeholder = `Search ${data.settings.searchEngine || 'Google'}...`; - } - - if (addShortcutBtn) { - addShortcutBtn.style.display = data.settings.show_addShortcut !== false ? 'block' : 'none'; - } - - if (data.settings.customGreeting) { - Storage.set('customGreeting', data.settings.customGreeting); - const testFormat = settings.formatGreeting(data.settings.customGreeting); - if (!testFormat) { - notifications.show('Invalid custom greeting format in imported data', 'error'); - Storage.remove('customGreeting'); - } - } - - notifications.show('Data imported successfully!', 'success'); - } catch (error) { - notifications.show('Failed to import data: Invalid file format!', 'error'); - console.error('Import error:', error); - } - }; - - reader.onerror = () => { - notifications.show('Failed to read file!', 'error'); - }; - - reader.readAsText(file); - }); - - resetBtn.addEventListener('click', () => { - const confirmReset = confirm('Are you sure you want to reset all data? This action cannot be undone.'); - - if (confirmReset) { - try { - Storage.remove('customGreeting'); - Storage.clear(); - closeModal(document.getElementById('settings-modal')); - notifications.show('All data has been reset!', 'success'); - setTimeout(() => { - window.location.reload(); - }, 1000); - } catch (error) { - notifications.show('Failed to reset data!', 'error'); - console.error('Reset error:', error); - } - } - }); -}; \ No newline at end of file +const defaultSettings = { + theme: 'light', + userName: '', + anonymousMode: false, + searchEngine: 'Google', + updateAlerts: true, + show_greeting: true, + show_search: true, + show_shortcuts: true, + show_addShortcut: true, + fontFamily: 'Inter', + fontSize: '16', + lightModeColors: { + '--primary': '#f5f5f5', + '--primary-hover': '#e0e0e0', + '--background': '#ffffff', + '--surface': '#fafafa', + '--border': '#eaeaea', + '--text': '#1a1a1a', + '--text-secondary': '#666666', + '--shadow': 'hsla(0, 0.00%, 0.00%, 0.08)', + '--modal-backdrop': 'rgba(0, 0, 0, 0.5)', + '--scrollbar-thumb': '#e0e0e0', + '--scrollbar-track': '#f5f5f5', + '--modal-background': '#ffffff', + '--toggle-bg': '#e0e0e0', + '--toggle-bg-active': 'var(--text)', + '--toggle-knob': 'var(--background)' + }, + darkModeColors: { + '--primary': '#1a1a1a', + '--primary-hover': '#2a2a2a', + '--background': '#000000', + '--surface': '#111111', + '--border': '#333333', + '--text': '#ffffff', + '--text-secondary': '#999999', + '--shadow': 'rgba(0, 0, 0, 0.3)', + '--modal-backdrop': 'rgba(0, 0, 0, 0.75)', + '--scrollbar-thumb': '#333333', + '--scrollbar-track': '#1a1a1a', + '--modal-background': '#1a1a1a', + '--toggle-bg': '#333333', + '--toggle-bg-active': 'var(--text)', + '--toggle-knob': 'var(--background)' + } +}; + +const anonymousNames = { + adjectives: ['Hidden', 'Secret', 'Mystery', 'Shadow', 'Unknown', 'Silent', 'Stealth', 'Phantom', 'Ghost', 'Anon'], + nouns: [' User', ' Visitor', ' Guest', ' Agent', ' Entity', ' Person', ' Browser', ' Explorer', ' Wanderer', ' Navigator'], + + generate: function() { + const adjective = this.adjectives[Math.floor(Math.random() * this.adjectives.length)]; + const noun = this.nouns[Math.floor(Math.random() * this.nouns.length)]; + return `${adjective}${noun}`; + } +}; + +const getTimeBasedGreeting = () => { + const hour = new Date().getHours(); + if (hour < 12) return 'Good Morning'; + if (hour < 18) return 'Good Afternoon'; + if (hour < 22) return 'Good Evening'; + return 'Good Night'; +}; + +const updateAlerts = Storage.get('updateAlerts'); + +document.getElementById('toggle-update-alerts').addEventListener('change', function() { + Storage.set('updateAlerts', this.checked); + + notifications.show( + this.checked ? 'Update alerts enabled!' : 'Update alerts disabled!', + this.checked ? 'success' : 'success' + ); +}); + +document.addEventListener('DOMContentLoaded', () => { + document.getElementById('toggle-update-alerts').checked = + updateAlerts !== false; +}); + +const settings = { + GREETING_MAX_LENGTH: 60, + + toggleTheme: () => { + const currentTheme = document.body.getAttribute('data-theme'); + const newTheme = currentTheme === 'dark' ? 'light' : 'dark'; + document.body.setAttribute('data-theme', newTheme); + Storage.set('theme', newTheme); + + const themeIcon = document.querySelector('#toggle-theme svg use'); + const lightModeIcon = newTheme === 'dark' ? 'dark-mode' : 'light-mode'; + const darkModeIcon = newTheme === 'dark' ? 'light-mode' : 'dark-mode'; + + themeIcon.setAttribute('href', `#icon-${newTheme === 'dark' ? darkModeIcon : lightModeIcon}`); + }, + + + toggleAnonymousMode: () => { + const isAnonymous = Storage.get('anonymousMode') || false; + Storage.set('anonymousMode', !isAnonymous); + + if (!isAnonymous) { + const randomName = anonymousNames.generate(); + Storage.set('anonymousName', randomName); + notifications.show('Anonymous mode enabled!', 'info'); + } else { + Storage.remove('anonymousName'); + notifications.show('Anonymous mode disabled!', 'info'); + } + + shortcuts.render(); + updateGreeting(); + }, + + updateSearchEngine: (engine) => { + Storage.set('searchEngine', engine); + notifications.show('Search engine updated successfully!', 'success'); + }, + + updateVisibility: () => { + const elements = { + greeting: { + id: 'greeting', + toggle: 'toggle-greeting', + functions: ['updateGreeting'], + name: 'Greeting' + }, + search: { + id: 'search-container', + toggle: 'toggle-search', + functions: ['search.init', 'search.perform'], + name: 'Search bar' + }, + shortcuts: { + id: 'shortcuts-grid', + toggle: 'toggle-shortcuts', + functions: ['shortcuts.init', 'shortcuts.render'], + name: 'Shortcuts' + }, + addShortcut: { + id: 'add-shortcut', + toggle: 'toggle-add-shortcut', + functions: [], + name: 'Add shortcut button' + } + }; + + Object.entries(elements).forEach(([key, element]) => { + const isVisible = Storage.get(`show_${key}`); + if (isVisible === null) Storage.set(`show_${key}`, true); + + const toggle = document.getElementById(element.toggle); + const elementNode = document.getElementById(element.id); + + if (toggle && elementNode) { + toggle.checked = isVisible !== false; + + if (isVisible === false) { + elementNode.style.visibility = 'hidden'; + elementNode.style.opacity = '0'; + elementNode.style.position = 'absolute'; + elementNode.style.pointerEvents = 'none'; + } else { + elementNode.style.visibility = 'visible'; + elementNode.style.opacity = '1'; + elementNode.style.position = 'relative'; + elementNode.style.pointerEvents = 'auto'; + } + + if (!toggle.hasAttribute('data-visibility-initialized')) { + toggle.setAttribute('data-visibility-initialized', 'true'); + + toggle.addEventListener('change', (e) => { + const isChecked = e.target.checked; + Storage.set(`show_${key}`, isChecked); + + if (isChecked) { + elementNode.style.visibility = 'visible'; + elementNode.style.opacity = '1'; + elementNode.style.position = 'relative'; + elementNode.style.pointerEvents = 'auto'; + } else { + elementNode.style.visibility = 'hidden'; + elementNode.style.opacity = '0'; + elementNode.style.position = 'absolute'; + elementNode.style.pointerEvents = 'none'; + } + + if (key === 'shortcuts') { + const addShortcutBtn = document.getElementById('add-shortcut'); + const addShortcutVisible = Storage.get('show_addShortcut') !== false; + + if (addShortcutBtn && !isChecked && !addShortcutVisible) { + addShortcutBtn.style.visibility = 'hidden'; + addShortcutBtn.style.opacity = '0'; + addShortcutBtn.style.position = 'absolute'; + addShortcutBtn.style.pointerEvents = 'none'; + } else if (addShortcutBtn && addShortcutVisible) { + addShortcutBtn.style.visibility = 'visible'; + addShortcutBtn.style.opacity = '1'; + addShortcutBtn.style.position = 'relative'; + addShortcutBtn.style.pointerEvents = 'auto'; + } + } + + notifications.show( + `${element.name} ${isChecked ? 'shown' : 'hidden'}!`, + isChecked ? 'success' : 'info' + ); + }); + } + } + }); + }, + + init: () => { + const settingsButton = document.getElementById('settings-button'); + const settingsPage = document.getElementById('settings-page'); + const backToHome = document.getElementById('back-to-home'); + + settingsButton.addEventListener('click', (e) => { + e.stopPropagation(); + settings.updateSettingsUI(); + settingsPage.classList.remove('hidden'); + setTimeout(() => { + settingsPage.classList.add('active'); + }, 10); + }); + + backToHome.addEventListener('click', () => { + settingsPage.classList.remove('active'); + setTimeout(() => { + settingsPage.classList.add('hidden'); + }, 300); + }); + + const navItems = document.querySelectorAll('.settings-nav-item'); + navItems.forEach(item => { + item.addEventListener('click', (e) => { + e.preventDefault(); + const section = item.getAttribute('data-section'); + + navItems.forEach(navItem => navItem.classList.remove('active')); + item.classList.add('active'); + + document.querySelectorAll('.settings-section').forEach(section => { + section.classList.remove('active'); + }); + document.getElementById(section).classList.add('active'); + }); + }); + + const themeToggle = document.getElementById('toggle-theme'); + themeToggle.addEventListener('click', settings.toggleTheme); + + const anonymousToggle = document.getElementById('toggle-anonymous'); + anonymousToggle.addEventListener('change', settings.toggleAnonymousMode); + + const searchEngineSelect = document.getElementById('search-engine-select'); + searchEngineSelect.addEventListener('change', (e) => { + settings.updateSearchEngine(e.target.value); + }); + + const nameInput = document.getElementById('settings-name'); + nameInput.addEventListener('change', (e) => { + const newName = e.target.value.trim(); + if (newName) { + Storage.set('userName', newName); + updateGreeting(); + notifications.show('Name updated successfully!', 'success'); + } + }); + + const fontFamilySelect = document.getElementById('font-family-select'); + const fontSizeSlider = document.getElementById('font-size-slider'); + const fontSizeNumber = document.getElementById('font-size-number'); + const resetFontSize = document.getElementById('reset-font-size'); + const resetLightColors = document.getElementById('reset-light-colors'); + const resetDarkColors = document.getElementById('reset-dark-colors'); + + fontFamilySelect.value = Storage.get('fontFamily') || defaultSettings.fontFamily; + const currentFontSize = Storage.get('fontSize') || defaultSettings.fontSize; + fontSizeSlider.value = currentFontSize; + fontSizeNumber.value = currentFontSize; + + fontFamilySelect.addEventListener('change', (e) => { + Storage.set('fontFamily', e.target.value); + settings.updateTypography(); + notifications.show('Font updated successfully!', 'success'); + }); + + const updateFontSize = (value) => { + if (value >= 8 && value <= 36) { + fontSizeSlider.value = value; + fontSizeNumber.value = value; + Storage.set('fontSize', value); + settings.updateTypography(); + } + }; + + fontSizeSlider.addEventListener('input', (e) => updateFontSize(e.target.value)); + fontSizeNumber.addEventListener('change', (e) => updateFontSize(e.target.value)); + + resetFontSize.addEventListener('click', settings.resetTypography); + resetLightColors.addEventListener('click', () => settings.resetColors('light')); + resetDarkColors.addEventListener('click', () => settings.resetColors('dark')); + + settings.initColorSettings(); + + settings.updateTypography(); + settings.updateColors(); + + const savedTheme = Storage.get('theme') || 'light'; + document.body.setAttribute('data-theme', savedTheme); + + const themeIcon = document.querySelector('#toggle-theme svg use'); + const lightModeIcon = 'light-mode'; + const darkModeIcon = 'dark-mode'; + + themeIcon.setAttribute('href', `#icon-${savedTheme === 'dark' ? darkModeIcon : lightModeIcon}`); + + settings.initDataManagement(); + settings.updateVisibility(); + + const customGreetingInput = document.getElementById('custom-greeting'); + customGreetingInput.maxLength = settings.GREETING_MAX_LENGTH; + + customGreetingInput.addEventListener('input', (e) => { + const value = e.target.value; + if (value.length > settings.GREETING_MAX_LENGTH) { + e.target.value = value.substring(0, settings.GREETING_MAX_LENGTH); + notifications.show(`Custom greeting must be less than ${settings.GREETING_MAX_LENGTH} characters`, 'error'); + } + }); + + customGreetingInput.addEventListener('change', (e) => { + const value = e.target.value.trim(); + + if (value.length > settings.GREETING_MAX_LENGTH) { + e.target.value = value.substring(0, settings.GREETING_MAX_LENGTH); + notifications.show(`Custom greeting must be less than ${settings.GREETING_MAX_LENGTH} characters`, 'error'); + return; + } + + if (value === '') { + Storage.remove('customGreeting'); + notifications.show('Using default greeting format', 'info'); + } else { + const testFormat = settings.formatGreeting(value); + if (testFormat) { + Storage.set('customGreeting', value); + notifications.show('Custom greeting updated!', 'success'); + } + } + updateGreeting(); + }); + + document.addEventListener('keydown', (e) => { + if (e.key === 'Escape' && settingsPage.classList.contains('active')) { + backToHome.click(); + } + }); + }, + + updateSettingsUI: () => { + const userName = Storage.get('userName') || ''; + const isAnonymous = Storage.get('anonymousMode') || false; + const currentEngine = Storage.get('searchEngine') || 'google'; + + document.getElementById('settings-name').value = userName; + document.getElementById('toggle-anonymous').checked = isAnonymous; + document.getElementById('search-engine-select').value = currentEngine; + document.getElementById('toggle-update-alerts').checked = updateAlerts; + + ['greeting', 'search', 'shortcuts', 'addShortcut'].forEach(element => { + const isVisible = Storage.get(`show_${element}`); + const toggle = document.getElementById(`toggle-${element}`); + if (toggle) { + toggle.checked = isVisible !== false; + } + }); + + const customGreeting = Storage.get('customGreeting') || ''; + document.getElementById('custom-greeting').value = customGreeting; + + const fontFamily = Storage.get('fontFamily') || defaultSettings.fontFamily; + const fontSize = Storage.get('fontSize') || defaultSettings.fontSize; + + document.getElementById('font-family-select').value = fontFamily; + document.getElementById('font-size-slider').value = fontSize; + document.getElementById('font-size-number').value = fontSize; + }, + + formatGreeting: (format) => { + try { + if (!format) return null; + + const validTokens = ['{name}', '{greeting}', '{time}', '{date}', '{day}', '{month}', '{year}']; + const tokens = format.match(/{[^}]+}/g) || []; + + if (!tokens.every(token => validTokens.includes(token))) { + notifications.show('Invalid greeting format: Unknown token used', 'error'); + return null; + } + + const now = new Date(); + const hour = now.getHours(); + let timeGreeting = 'good night'; + if (hour >= 5 && hour < 12) timeGreeting = 'good morning'; + else if (hour >= 12 && hour < 17) timeGreeting = 'good afternoon'; + else if (hour >= 17 && hour < 20) timeGreeting = 'good evening'; + + const userName = Storage.get('anonymousMode') ? + (Storage.get('anonymousName') || anonymousNames.generate()) : + (Storage.get('userName') || 'Friend'); + + const formats = { + name: userName, + greeting: format.startsWith('{greeting}') ? + timeGreeting.charAt(0).toUpperCase() + timeGreeting.slice(1) : + timeGreeting, + time: now.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' }), + date: now.getDate().toString(), + day: now.toLocaleDateString([], { weekday: 'long' }), + month: now.toLocaleDateString([], { month: 'long' }), + year: now.getFullYear() + }; + + let formattedGreeting = format; + Object.entries(formats).forEach(([key, value]) => { + formattedGreeting = formattedGreeting.replace( + new RegExp(`{${key}}`, 'g'), + value + ); + }); + + if (/{[^}]+}/.test(formattedGreeting)) { + throw new Error('Invalid format used'); + } + + return formattedGreeting; + } catch (e) { + notifications.show('Invalid greeting format. Using default.', 'error'); + return null; + } + }, + + exportData: () => { + try { + const data = { + settings: { + theme: Storage.get('theme') || defaultSettings.theme, + userName: Storage.get('userName') || '', + anonymousMode: Storage.get('anonymousMode') || false, + anonymousName: Storage.get('anonymousName') || '', + searchEngine: Storage.get('searchEngine') || 'Google', + customGreeting: Storage.get('customGreeting') || '', + updateAlerts: Storage.get('updateAlerts') !== false, + + fontFamily: Storage.get('fontFamily') || defaultSettings.fontFamily, + fontSize: Storage.get('fontSize') || defaultSettings.fontSize, + + show_greeting: Storage.get('show_greeting') !== false, + show_search: Storage.get('show_search') !== false, + show_shortcuts: Storage.get('show_shortcuts') !== false, + show_addShortcut: Storage.get('show_addShortcut') !== false, + + gridLayout: Storage.get('gridLayout') ? + (typeof Storage.get('gridLayout') === 'string' ? + JSON.parse(Storage.get('gridLayout')) : + Storage.get('gridLayout')) : + { + type: 'default', + columns: 6, + gap: 16, + size: 80, + resizable: false + }, + + backgrounds: typeof Storage.get('backgrounds') === 'string' ? + JSON.parse(Storage.get('backgrounds') || '[]') : + [], + customBackground: Storage.get('customBackground') || null, + + lightModeColors: typeof Storage.get('lightModeColors') === 'string' ? + JSON.parse(Storage.get('lightModeColors')) : + defaultSettings.lightModeColors, + darkModeColors: typeof Storage.get('darkModeColors') === 'string' ? + JSON.parse(Storage.get('darkModeColors')) : + defaultSettings.darkModeColors + }, + shortcuts: Storage.get('shortcuts') || [], + keybinds: typeof Storage.get('keybinds') === 'string' ? + JSON.parse(Storage.get('keybinds') || '{}') : + (Storage.get('keybinds') || {}) + }; + + if (data.settings.backgrounds && Array.isArray(data.settings.backgrounds)) { + data.settings.backgrounds = data.settings.backgrounds.filter(bg => { + return typeof bg === 'string' && + (bg.startsWith('data:image/') || bg.startsWith('images/backgrounds/')); + }); + } + + if (data.settings.customBackground && + data.settings.backgrounds && + !data.settings.backgrounds.includes(data.settings.customBackground)) { + data.settings.customBackground = null; + } + + if (!data.settings || typeof data.settings !== 'object' || + !Array.isArray(data.shortcuts) || + !data.keybinds || typeof data.keybinds !== 'object') { + throw new Error('Invalid data structure'); + } + + const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'jstar-tab-backup.json'; + a.click(); + URL.revokeObjectURL(url); + + notifications.show('Data exported successfully!', 'success'); + } catch (error) { + notifications.show(`Failed to export data: ${error.message}`, 'error'); + console.error('Export error:', error); + } + }, + + importData: async (file) => { + try { + const text = await file.text(); + const data = JSON.parse(text); + + if (!data.settings || typeof data.settings !== 'object' || + !Array.isArray(data.shortcuts) || + !data.keybinds || typeof data.keybinds !== 'object') { + throw new Error('Invalid data structure'); + } + + if (data.settings.backgrounds) { + if (!Array.isArray(data.settings.backgrounds)) { + throw new Error('Invalid backgrounds format'); + } + + const validBackgrounds = data.settings.backgrounds.filter(bg => { + return typeof bg === 'string' && + (bg.startsWith('data:image/') || bg.startsWith('images/backgrounds/')); + }); + + if (validBackgrounds.length !== data.settings.backgrounds.length) { + throw new Error('Invalid background images detected'); + } + + if (data.settings.customBackground && + !validBackgrounds.includes(data.settings.customBackground)) { + delete data.settings.customBackground; + } + } + + if (data.settings.gridLayout) { + if (typeof data.settings.gridLayout !== 'object') { + data.settings.gridLayout = { + type: 'default', + columns: 6, + gap: 16, + size: 80, + resizable: false + }; + } else { + const defaultLayout = { + type: 'default', + columns: 6, + gap: 16, + size: 80, + resizable: false + }; + + if (!data.settings.gridLayout.type || + !['default', 'compact', 'comfortable', 'list', 'custom'].includes(data.settings.gridLayout.type)) { + data.settings.gridLayout.type = defaultLayout.type; + } + + const columns = parseInt(data.settings.gridLayout.columns); + if (isNaN(columns) || columns < 1 || columns > 12) { + data.settings.gridLayout.columns = defaultLayout.columns; + } + + const gap = parseInt(data.settings.gridLayout.gap); + if (isNaN(gap) || gap < 0 || gap > 50) { + data.settings.gridLayout.gap = defaultLayout.gap; + } + + const size = parseInt(data.settings.gridLayout.size); + if (isNaN(size) || size < 40 || size > 200) { + data.settings.gridLayout.size = defaultLayout.size; + } + + if (typeof data.settings.gridLayout.resizable !== 'boolean') { + data.settings.gridLayout.resizable = defaultLayout.resizable; + } + } + } + + const validateThemeColors = (colorObj, defaultColors) => { + if (!colorObj || typeof colorObj !== 'object') { + return defaultColors; + } + + const validatedColors = {...defaultColors}; + + Object.keys(defaultColors).forEach(key => { + if (colorObj[key] && typeof colorObj[key] === 'string') { + validatedColors[key] = colorObj[key]; + } + }); + + return validatedColors; + }; + + if (data.settings.lightModeColors) { + data.settings.lightModeColors = validateThemeColors( + data.settings.lightModeColors, + defaultSettings.lightModeColors + ); + } + + if (data.settings.darkModeColors) { + data.settings.darkModeColors = validateThemeColors( + data.settings.darkModeColors, + defaultSettings.darkModeColors + ); + } + + Object.entries(data.settings).forEach(([key, value]) => { + if (key === 'backgrounds') { + Storage.set(key, JSON.stringify(value)); + } else if (key === 'gridLayout') { + Storage.set(key, JSON.stringify(value)); + } else if (key === 'lightModeColors' || key === 'darkModeColors') { + Storage.set(key, JSON.stringify(value)); + } else { + Storage.set(key, value); + } + }); + + Storage.set('shortcuts', data.shortcuts); + + const validatedKeybinds = {}; + Object.entries(data.keybinds).forEach(([action, binding]) => { + if (binding && typeof binding === 'object' && + typeof binding.keys === 'string' && + (!binding.url || typeof binding.url === 'string')) { + validatedKeybinds[action] = binding; + } + }); + Storage.set('keybinds', validatedKeybinds); + + settings.updateSettingsUI(); + settings.updateVisibility(); + shortcuts.render(); + + if (typeof GridLayout !== 'undefined' && GridLayout.init) { + setTimeout(() => { + GridLayout.init(); + }, 100); + } + + document.body.setAttribute('data-theme', data.settings.theme || 'light'); + settings.updateColors(); + + if (data.settings.lightModeColors || data.settings.darkModeColors) { + settings.initColorSettings(); + } + + if (data.settings.fontFamily) { + document.documentElement.style.setProperty('--font-family', data.settings.fontFamily); + } + + if (data.settings.fontSize) { + document.documentElement.style.setProperty('--font-size', data.settings.fontSize + 'px'); + } + + if (Array.isArray(data.settings.backgrounds)) { + data.settings.backgrounds.forEach(bg => { + if (bg.startsWith('data:image/')) { + addBackgroundPreview(bg, false); + } + }); + } + + if (data.settings.customBackground) { + setCustomBackground(data.settings.customBackground, true); + } else { + document.body.style.backgroundImage = ''; + Storage.remove('customBackground'); + } + + notifications.show('Data imported successfully!', 'success'); + } catch (error) { + notifications.show('Failed to import data: Invalid file format!', 'error'); + console.error('Import error:', error); + } + }, + + resetData: () => { + Object.entries(defaultSettings).forEach(([key, value]) => { + Storage.set(key, value); + }); + + Storage.remove('shortcuts'); + Storage.remove('keybinds'); + Storage.remove('anonymousName'); + Storage.remove('customGreeting'); + + if (typeof GridLayout !== 'undefined') { + if (GridLayout.reset) { + GridLayout.reset(); + } else if (GridLayout.defaults) { + Storage.set('gridLayout', GridLayout.defaults); + if (GridLayout.init) { + setTimeout(() => { + GridLayout.init(); + }, 100); + } + } else { + Storage.remove('gridLayout'); + } + } else { + Storage.remove('gridLayout'); + } + + if (typeof GridLayout === 'undefined' || !GridLayout.reset) { + const defaultVisibility = { + showGreeting: true, + showSearch: true, + showShortcuts: true, + showAddButton: true, + showGrid: true + }; + Storage.set('visibility', defaultVisibility); + } + + settings.updateSettingsUI(); + settings.updateVisibility(); + settings.updateTypography(); + shortcuts.render(); + document.body.setAttribute('data-theme', 'light'); + + notifications.show('All data has been reset!', 'success'); + }, + + updateTypography: () => { + const fontFamily = Storage.get('fontFamily') || defaultSettings.fontFamily; + const fontSize = Storage.get('fontSize') || defaultSettings.fontSize; + + document.documentElement.style.setProperty('--font-family', fontFamily); + document.documentElement.style.setProperty('--font-size-base', `${fontSize}px`); + }, + + updateColors: () => { + const theme = document.body.getAttribute('data-theme'); + let colors; + + if (theme === 'dark') { + const darkColors = Storage.get('darkModeColors'); + colors = typeof darkColors === 'string' ? + JSON.parse(darkColors) : + (darkColors || defaultSettings.darkModeColors); + } else { + const lightColors = Storage.get('lightModeColors'); + colors = typeof lightColors === 'string' ? + JSON.parse(lightColors) : + (lightColors || defaultSettings.lightModeColors); + } + + Object.entries(colors).forEach(([variable, value]) => { + document.documentElement.style.setProperty(variable, value); + }); + }, + + resetTypography: () => { + Storage.set('fontFamily', defaultSettings.fontFamily); + Storage.set('fontSize', defaultSettings.fontSize); + settings.updateTypography(); + settings.updateSettingsUI(); + notifications.show('Typography reset to default!', 'success'); + }, + + resetColors: (theme) => { + if (theme === 'light') { + Storage.set('lightModeColors', JSON.stringify(defaultSettings.lightModeColors)); + } else { + Storage.set('darkModeColors', JSON.stringify(defaultSettings.darkModeColors)); + } + settings.updateColors(); + settings.initColorSettings(); + notifications.show(`${theme === 'light' ? 'Light' : 'Dark'} mode colors reset to default!`, 'success'); + }, + + initColorSettings: () => { + const colorVariables = { + 'Primary Color': '--primary', + 'Primary Hover': '--primary-hover', + 'Background': '--background', + 'Surface': '--surface', + 'Border': '--border', + 'Text': '--text', + 'Secondary Text': '--text-secondary', + 'Shadow': '--shadow', + 'Modal Backdrop': '--modal-backdrop', + 'Scrollbar Thumb': '--scrollbar-thumb', + 'Scrollbar Track': '--scrollbar-track', + 'Modal Background': '--modal-background', + 'Toggle Background': '--toggle-bg', + 'Toggle Active': '--toggle-bg-active', + 'Toggle Knob': '--toggle-knob' + }; + + const createColorInputs = (containerId, theme) => { + const container = document.getElementById(containerId); + container.innerHTML = ''; + + let colors; + if (theme === 'dark') { + const darkColors = Storage.get('darkModeColors'); + colors = typeof darkColors === 'string' ? + JSON.parse(darkColors) : + (darkColors || defaultSettings.darkModeColors); + } else { + const lightColors = Storage.get('lightModeColors'); + colors = typeof lightColors === 'string' ? + JSON.parse(lightColors) : + (lightColors || defaultSettings.lightModeColors); + } + + Object.entries(colorVariables).forEach(([label, variable]) => { + const div = document.createElement('div'); + div.className = 'color-setting'; + div.innerHTML = ` +
${label}
+ + `; + + const input = div.querySelector('input'); + input.addEventListener('change', (e) => { + let updatedColors; + if (theme === 'dark') { + const darkColors = Storage.get('darkModeColors'); + updatedColors = typeof darkColors === 'string' ? + JSON.parse(darkColors) : + (darkColors || {...defaultSettings.darkModeColors}); + } else { + const lightColors = Storage.get('lightModeColors'); + updatedColors = typeof lightColors === 'string' ? + JSON.parse(lightColors) : + (lightColors || {...defaultSettings.lightModeColors}); + } + + updatedColors[variable] = e.target.value; + Storage.set(theme === 'dark' ? 'darkModeColors' : 'lightModeColors', JSON.stringify(updatedColors)); + settings.updateColors(); + }); + + container.appendChild(div); + }); + }; + + createColorInputs('light-mode-colors', 'light'); + createColorInputs('dark-mode-colors', 'dark'); + } +}; + +settings.initDataManagement = () => { + const importBtn = document.getElementById('import-data'); + const exportBtn = document.getElementById('export-data'); + const resetBtn = document.getElementById('reset-data'); + + const fileInput = document.createElement('input'); + fileInput.type = 'file'; + fileInput.id = 'import-file'; + fileInput.accept = '.json'; + fileInput.style.display = 'none'; + document.body.appendChild(fileInput); + + if (!importBtn || !exportBtn || !resetBtn) { + console.warn('Data management buttons not found'); + return; + } + + exportBtn.replaceWith(exportBtn.cloneNode(true)); + const newExportBtn = document.getElementById('export-data'); + + newExportBtn.addEventListener('click', () => { + settings.exportData(); + }); + + importBtn.addEventListener('click', () => fileInput.click()); + + fileInput.addEventListener('change', (e) => { + const file = e.target.files[0]; + if (!file) return; + + settings.importData(file); + fileInput.value = ''; + }); + + resetBtn.addEventListener('click', () => { + const confirmReset = confirm('Are you sure you want to reset all data? This action cannot be undone.'); + + if (confirmReset) { + try { + Storage.remove('customGreeting'); + Storage.clear(); + closeModal(document.getElementById('settings-modal')); + notifications.show('All data has been reset!', 'success'); + setTimeout(() => { + window.location.reload(); + }, 1000); + } catch (error) { + notifications.show('Failed to reset data!', 'error'); + console.error('Reset error:', error); + } + } + }); +}; + +function initCustomSelects() { + const customSelects = document.querySelectorAll(".custom-select"); + + customSelects.forEach(select => { + const nativeSelect = select.querySelector("select"); + if (!nativeSelect) return; + + const selectedDiv = document.createElement("div"); + selectedDiv.className = "select-selected"; + + if (nativeSelect.id === 'search-engine-select') { + nativeSelect.value = Storage.get('searchEngine') || 'google'; + } else if (nativeSelect.id === 'font-family-select') { + nativeSelect.value = Storage.get('fontFamily') || 'Inter'; + selectedDiv.style.fontFamily = nativeSelect.value; + } else if (nativeSelect.id === 'grid-layout-type') { + const savedGridType = Storage.get('gridLayoutType'); + if (savedGridType) { + nativeSelect.value = savedGridType; + } + } + + const initialOption = nativeSelect.options[nativeSelect.selectedIndex]; + selectedDiv.innerHTML = ` + + ${initialOption.innerHTML} + + `; + + const itemsDiv = document.createElement("div"); + itemsDiv.className = "select-items select-hide"; + + for (let i = 0; i < nativeSelect.options.length; i++) { + const optionDiv = document.createElement("div"); + const option = nativeSelect.options[i]; + + optionDiv.innerHTML = option.innerHTML; + optionDiv.className = option.className || ''; + optionDiv.setAttribute('data-value', option.value); + + if (nativeSelect.id === 'font-family-select') { + optionDiv.style.fontFamily = option.value; + } + + if (option.value === nativeSelect.value) { + optionDiv.classList.add('same-as-selected'); + } + + optionDiv.addEventListener("click", function() { + nativeSelect.selectedIndex = i; + nativeSelect.dispatchEvent(new Event('change')); + + selectedDiv.innerHTML = ` + + ${this.innerHTML} + + `; + + if (nativeSelect.id === 'font-family-select') { + selectedDiv.style.fontFamily = nativeSelect.options[i].value; + } + + if (nativeSelect.id === 'grid-layout-type') { + Storage.set('gridLayoutType', nativeSelect.value); + } + + const sameAsSelected = itemsDiv.querySelector('.same-as-selected'); + if (sameAsSelected) { + sameAsSelected.classList.remove('same-as-selected'); + } + this.classList.add('same-as-selected'); + + itemsDiv.classList.add('select-hide'); + selectedDiv.classList.remove('select-arrow-active'); + }); + + itemsDiv.appendChild(optionDiv); + } + + selectedDiv.addEventListener("click", function(e) { + e.stopPropagation(); + closeAllSelects(this); + this.nextSibling.classList.toggle("select-hide"); + this.classList.toggle("select-arrow-active"); + }); + + select.appendChild(selectedDiv); + select.appendChild(itemsDiv); + }); + + function closeAllSelects(element) { + const selectItems = document.getElementsByClassName("select-items"); + const selectSelected = document.getElementsByClassName("select-selected"); + + for (let i = 0; i < selectSelected.length; i++) { + if (element !== selectSelected[i]) { + selectSelected[i].classList.remove("select-arrow-active"); + } + } + + for (let i = 0; i < selectItems.length; i++) { + if (element !== selectItems[i].previousSibling) { + selectItems[i].classList.add("select-hide"); + } + } + } + + document.addEventListener("click", closeAllSelects); +} + +document.addEventListener("DOMContentLoaded", function() { + initCustomSelects(); +}); \ No newline at end of file diff --git a/js/shortcuts.js b/js/shortcuts.js index a197444..995540e 100644 --- a/js/shortcuts.js +++ b/js/shortcuts.js @@ -1,274 +1,275 @@ -const shortcuts = { - MAX_SHORTCUTS: 12, - - // URL Validation - validateAndFormatUrl: (url) => { - if (!/^https?:\/\//i.test(url)) { - url = 'https://' + url; - } - - try { - new URL(url); - return url; - } catch (e) { - return false; - } - }, - - // Shortcut Management - add: (url, name) => { - const currentShortcuts = Storage.get('shortcuts') || []; - if (currentShortcuts.length >= shortcuts.MAX_SHORTCUTS) { - notifications.show('Maximum shortcuts limit reached!', 'error'); - return; - } - - const formattedUrl = shortcuts.validateAndFormatUrl(url); - if (!formattedUrl) { - notifications.show('Invalid URL format!', 'error'); - return; - } - - currentShortcuts.push({ url: formattedUrl, name }); - Storage.set('shortcuts', currentShortcuts); - shortcuts.render(); - }, - - remove: (index) => { - const currentShortcuts = Storage.get('shortcuts') || []; - currentShortcuts.splice(index, 1); - Storage.set('shortcuts', currentShortcuts); - shortcuts.render(); - notifications.show('Shortcut removed!', 'success'); - }, - - edit: (index, newUrl, newName) => { - const currentShortcuts = Storage.get('shortcuts') || []; - currentShortcuts[index] = { url: newUrl, name: newName }; - Storage.set('shortcuts', currentShortcuts); - shortcuts.render(); - notifications.show('Shortcut updated!', 'success'); - }, - - // UI Interactions - showContextMenu: (e, index) => { - e.preventDefault(); - const menu = document.getElementById('context-menu'); - const rect = e.target.getBoundingClientRect(); - - menu.style.top = `${e.clientY}px`; - menu.style.left = `${e.clientX}px`; - menu.classList.remove('hidden'); - menu.dataset.shortcutIndex = index; - - const handleClickOutside = (event) => { - if (!menu.contains(event.target)) { - menu.classList.add('hidden'); - document.removeEventListener('click', handleClickOutside); - } - }; - - setTimeout(() => { - document.addEventListener('click', handleClickOutside); - }, 0); - }, - - // Rendering - render: () => { - const grid = document.getElementById('shortcuts-grid'); - const currentShortcuts = Storage.get('shortcuts') || []; - const isAnonymous = Storage.get('anonymousMode') || false; - - grid.innerHTML = ''; - - currentShortcuts.forEach((shortcut, index) => { - const element = document.createElement('div'); - element.className = `shortcut ${isAnonymous ? 'blurred' : ''}`; - - const icon = document.createElement('img'); - icon.src = `https://www.google.com/s2/favicons?domain=${shortcut.url}&sz=64`; - icon.alt = shortcut.name; - - const name = document.createElement('span'); - name.textContent = shortcut.name; - - element.appendChild(icon); - element.appendChild(name); - - element.addEventListener('click', (e) => { - if (e.ctrlKey) { - window.open(shortcut.url, '_blank'); - } else { - window.location.href = shortcut.url; - } - }); - - element.addEventListener('contextmenu', (e) => { - e.preventDefault(); - const menu = document.getElementById('context-menu'); - - menu.style.top = `${e.pageY}px`; - menu.style.left = `${e.pageX}px`; - menu.classList.remove('hidden'); - menu.dataset.shortcutIndex = index; - - const closeMenu = (event) => { - if (!menu.contains(event.target)) { - menu.classList.add('hidden'); - document.removeEventListener('click', closeMenu); - } - }; - - setTimeout(() => { - document.addEventListener('click', closeMenu); - }, 0); - }); - - grid.appendChild(element); - }); - }, - - // Initialization - init: () => { - const addShortcutButton = document.getElementById('add-shortcut'); - const modal = document.getElementById('add-shortcut-modal'); - const closeBtn = modal.querySelector('.close-modal'); - - if (closeBtn) { - closeBtn.addEventListener('click', () => { - modal.classList.remove('active'); - setTimeout(() => { - modal.classList.add('hidden'); - document.getElementById('shortcut-url').value = ''; - document.getElementById('shortcut-name').value = ''; - }, 300); - }); - } - - if (addShortcutButton) { - addShortcutButton.addEventListener('click', (e) => { - e.stopPropagation(); - const currentShortcuts = Storage.get('shortcuts') || []; - - if (currentShortcuts.length >= shortcuts.MAX_SHORTCUTS) { - notifications.show('Maximum shortcuts limit reached!', 'error'); - return; - } - - if (modal) { - modal.classList.remove('hidden'); - modal.classList.add('active'); - - const urlInput = document.getElementById('shortcut-url'); - const nameInput = document.getElementById('shortcut-name'); - - const saveShortcutButton = document.getElementById('save-shortcut'); - if (saveShortcutButton) { - saveShortcutButton.onclick = () => { - const url = urlInput.value.trim(); - const name = nameInput.value.trim(); - - if (url && name) { - try { - new URL(url); - shortcuts.add(url, name); - modal.classList.remove('active'); - setTimeout(() => { - modal.classList.add('hidden'); - urlInput.value = ''; - nameInput.value = ''; - }, 300); - notifications.show('Shortcut added successfully!', 'success'); - } catch (e) { - notifications.show('Invalid URL format!', 'error'); - } - } - }; - } - - const cancelShortcutButton = document.getElementById('cancel-shortcut'); - if (cancelShortcutButton) { - cancelShortcutButton.onclick = () => { - modal.classList.remove('active'); - setTimeout(() => { - modal.classList.add('hidden'); - urlInput.value = ''; - nameInput.value = ''; - }, 300); - }; - } - } - }); - } - - // Context menu actions - const contextMenu = document.getElementById('context-menu'); - if (contextMenu) { - contextMenu.addEventListener('click', (e) => { - const action = e.target.closest('.context-menu-item')?.dataset.action; - const index = parseInt(contextMenu.dataset.shortcutIndex); - - if (action === 'edit') { - const currentShortcuts = Storage.get('shortcuts') || []; - const shortcut = currentShortcuts[index]; - const modal = document.getElementById('edit-shortcut-modal'); - - if (modal) { - const urlInput = document.getElementById('edit-shortcut-url'); - const nameInput = document.getElementById('edit-shortcut-name'); - - urlInput.value = shortcut.url; - nameInput.value = shortcut.name; - - modal.classList.remove('hidden'); - modal.classList.add('active'); - - const saveButton = document.getElementById('save-edit-shortcut'); - const closeButton = document.getElementById('close-edit-shortcut'); - const cancelButton = document.getElementById('cancel-edit-shortcut'); - - const closeModal = () => { - modal.classList.remove('active'); - setTimeout(() => { - modal.classList.add('hidden'); - }, 300); - }; - - const handleSave = () => { - const newUrl = urlInput.value.trim(); - const newName = nameInput.value.trim(); - - if (newUrl && newName) { - const formattedUrl = shortcuts.validateAndFormatUrl(newUrl); - if (formattedUrl) { - shortcuts.edit(index, formattedUrl, newName); - closeModal(); - } else { - notifications.show('Invalid URL format!', 'error'); - } - } - }; - - saveButton.onclick = handleSave; - closeButton.onclick = closeModal; - cancelButton.onclick = closeModal; - } - } else if (action === 'delete') { - shortcuts.remove(index); - } else if (action === 'open-new-tab') { - const currentShortcuts = Storage.get('shortcuts') || []; - const shortcut = currentShortcuts[index]; - - // Open the URL of the shortcut in a new tab - if (shortcut && shortcut.url) { - window.open(shortcut.url, '_blank'); - } - } - - contextMenu.classList.add('hidden'); - }); - } - - shortcuts.render(); - } -}; +const shortcuts = { + MAX_SHORTCUTS: 12, + + validateAndFormatUrl: (url) => { + if (!/^https?:\/\//i.test(url)) { + url = 'https://' + url; + } + + try { + new URL(url); + return url; + } catch (e) { + return false; + } + }, + + add: (url, name) => { + const currentShortcuts = Storage.get('shortcuts') || []; + if (currentShortcuts.length >= shortcuts.MAX_SHORTCUTS) { + notifications.show('Maximum shortcuts limit reached!', 'error'); + return; + } + + const formattedUrl = shortcuts.validateAndFormatUrl(url); + if (!formattedUrl) { + notifications.show('Invalid URL format!', 'error'); + return; + } + + currentShortcuts.push({ url: formattedUrl, name }); + Storage.set('shortcuts', currentShortcuts); + shortcuts.render(); + CacheUpdater.update(); + }, + + remove: (index) => { + const currentShortcuts = Storage.get('shortcuts') || []; + currentShortcuts.splice(index, 1); + Storage.set('shortcuts', currentShortcuts); + shortcuts.render(); + notifications.show('Shortcut removed!', 'success'); + CacheUpdater.update(); + }, + + edit: (index, newUrl, newName) => { + const currentShortcuts = Storage.get('shortcuts') || []; + currentShortcuts[index] = { url: newUrl, name: newName }; + Storage.set('shortcuts', currentShortcuts); + shortcuts.render(); + notifications.show('Shortcut updated!', 'success'); + CacheUpdater.update(); + }, + + showContextMenu: (e, index) => { + e.preventDefault(); + const menu = document.getElementById('context-menu'); + const rect = e.target.getBoundingClientRect(); + + menu.style.top = `${e.clientY}px`; + menu.style.left = `${e.clientX}px`; + menu.classList.remove('hidden'); + menu.dataset.shortcutIndex = index; + + const handleClickOutside = (event) => { + if (!menu.contains(event.target)) { + menu.classList.add('hidden'); + document.removeEventListener('click', handleClickOutside); + } + }; + + setTimeout(() => { + document.addEventListener('click', handleClickOutside); + }, 0); + }, + + render: () => { + const grid = document.getElementById('shortcuts-grid'); + const currentShortcuts = Storage.get('shortcuts') || []; + const isAnonymous = Storage.get('anonymousMode') || false; + + grid.innerHTML = ''; + + currentShortcuts.forEach((shortcut, index) => { + const element = document.createElement('div'); + element.className = `shortcut ${isAnonymous ? 'blurred' : ''}`; + + element.dataset.index = index; + + const icon = document.createElement('img'); + icon.src = `https://www.google.com/s2/favicons?domain=${shortcut.url}&sz=64`; + icon.alt = shortcut.name; + icon.draggable = false; + + const name = document.createElement('span'); + name.textContent = shortcut.name; + + element.appendChild(icon); + element.appendChild(name); + + element.addEventListener('click', (e) => { + if (!grid.classList.contains('grid-draggable') || !e.target.closest('.shortcut').classList.contains('drag-active')) { + if (e.ctrlKey) { + window.open(shortcut.url, '_blank'); + } else { + window.location.href = shortcut.url; + } + } + }); + + element.addEventListener('contextmenu', (e) => { + e.preventDefault(); + const menu = document.getElementById('context-menu'); + + menu.style.top = `${e.pageY}px`; + menu.style.left = `${e.pageX}px`; + menu.classList.remove('hidden'); + menu.dataset.shortcutIndex = index; + + const closeMenu = (event) => { + if (!menu.contains(event.target)) { + menu.classList.add('hidden'); + document.removeEventListener('click', closeMenu); + } + }; + + setTimeout(() => { + document.addEventListener('click', closeMenu); + }, 0); + }); + + grid.appendChild(element); + }); + }, + + init: () => { + const addShortcutButton = document.getElementById('add-shortcut'); + const modal = document.getElementById('add-shortcut-modal'); + const closeBtn = modal.querySelector('.close-modal'); + + if (closeBtn) { + closeBtn.addEventListener('click', () => { + modal.classList.remove('active'); + setTimeout(() => { + modal.classList.add('hidden'); + document.getElementById('shortcut-url').value = ''; + document.getElementById('shortcut-name').value = ''; + }, 300); + }); + } + + if (addShortcutButton) { + addShortcutButton.addEventListener('click', (e) => { + e.stopPropagation(); + const currentShortcuts = Storage.get('shortcuts') || []; + + if (currentShortcuts.length >= shortcuts.MAX_SHORTCUTS) { + notifications.show('Maximum shortcuts limit reached!', 'error'); + return; + } + + if (modal) { + modal.classList.remove('hidden'); + modal.classList.add('active'); + + const urlInput = document.getElementById('shortcut-url'); + const nameInput = document.getElementById('shortcut-name'); + + const saveShortcutButton = document.getElementById('save-shortcut'); + if (saveShortcutButton) { + saveShortcutButton.onclick = () => { + const url = urlInput.value.trim(); + const name = nameInput.value.trim(); + + if (url && name) { + try { + new URL(url); + shortcuts.add(url, name); + modal.classList.remove('active'); + setTimeout(() => { + modal.classList.add('hidden'); + urlInput.value = ''; + nameInput.value = ''; + }, 300); + notifications.show('Shortcut added successfully!', 'success'); + } catch (e) { + notifications.show('Invalid URL format!', 'error'); + } + } + }; + } + + const cancelShortcutButton = document.getElementById('cancel-shortcut'); + if (cancelShortcutButton) { + cancelShortcutButton.onclick = () => { + modal.classList.remove('active'); + setTimeout(() => { + modal.classList.add('hidden'); + urlInput.value = ''; + nameInput.value = ''; + }, 300); + }; + } + } + }); + } + + const contextMenu = document.getElementById('context-menu'); + if (contextMenu) { + contextMenu.addEventListener('click', (e) => { + const action = e.target.closest('.context-menu-item')?.dataset.action; + const index = parseInt(contextMenu.dataset.shortcutIndex); + + if (action === 'edit') { + const currentShortcuts = Storage.get('shortcuts') || []; + const shortcut = currentShortcuts[index]; + const modal = document.getElementById('edit-shortcut-modal'); + + if (modal) { + const urlInput = document.getElementById('edit-shortcut-url'); + const nameInput = document.getElementById('edit-shortcut-name'); + + urlInput.value = shortcut.url; + nameInput.value = shortcut.name; + + modal.classList.remove('hidden'); + modal.classList.add('active'); + + const saveButton = document.getElementById('save-edit-shortcut'); + const closeButton = document.getElementById('close-edit-shortcut'); + const cancelButton = document.getElementById('cancel-edit-shortcut'); + + const closeModal = () => { + modal.classList.remove('active'); + setTimeout(() => { + modal.classList.add('hidden'); + }, 300); + }; + + const handleSave = () => { + const newUrl = urlInput.value.trim(); + const newName = nameInput.value.trim(); + + if (newUrl && newName) { + const formattedUrl = shortcuts.validateAndFormatUrl(newUrl); + if (formattedUrl) { + shortcuts.edit(index, formattedUrl, newName); + closeModal(); + } else { + notifications.show('Invalid URL format!', 'error'); + } + } + }; + + saveButton.onclick = handleSave; + closeButton.onclick = closeModal; + cancelButton.onclick = closeModal; + } + } else if (action === 'delete') { + shortcuts.remove(index); + } else if (action === 'open-new-tab') { + const currentShortcuts = Storage.get('shortcuts') || []; + const shortcut = currentShortcuts[index]; + + if (shortcut && shortcut.url) { + window.open(shortcut.url, '_blank'); + } + } + + contextMenu.classList.add('hidden'); + }); + } + + shortcuts.render(); + } +}; \ No newline at end of file diff --git a/js/storage.js b/js/storage.js index 2f451e8..ae5f18e 100644 --- a/js/storage.js +++ b/js/storage.js @@ -1,9 +1,4 @@ -/** - * Storage utility object for managing localStorage operations - * All methods handle JSON parsing/stringifying and error cases - */ const Storage = { - // Retrieve and parse stored value get: (key) => { try { return JSON.parse(localStorage.getItem(key)); @@ -12,7 +7,6 @@ const Storage = { } }, - // Store value as JSON string set: (key, value) => { try { localStorage.setItem(key, JSON.stringify(value)); @@ -22,7 +16,6 @@ const Storage = { } }, - // Delete specific key remove: (key) => { try { localStorage.removeItem(key); @@ -32,7 +25,6 @@ const Storage = { } }, - // Remove all stored data clear: () => { try { localStorage.clear(); diff --git a/js/version.js b/js/version.js new file mode 100644 index 0000000..edb09cc --- /dev/null +++ b/js/version.js @@ -0,0 +1,93 @@ +const versionUrl = 'https://www.junaid.xyz/projects/jstar-tab/version.txt'; +const manifestVersion = chrome.runtime.getManifest().version; + +function compareVersions(version1, version2) { + const v1 = version1.split('.').map(Number); + const v2 = version2.split('.').map(Number); + + for (let i = 0; i < Math.max(v1.length, v2.length); i++) { + const diff = (v1[i] || 0) - (v2[i] || 0); + if (diff !== 0) return diff; + } + + return 0; +} + +async function checkForUpdate() { + try { + const response = await fetch(versionUrl, { cache: 'no-store' }); + const latestVersion = await response.text(); + handleVersionComparison(latestVersion); + } catch (error) { + const cachedResponse = await caches.match(versionUrl); + if (cachedResponse) { + const cachedVersion = await cachedResponse.text(); + handleVersionComparison(cachedVersion, true); + } else { + updateVersionIcon(null); + } + } +} + +function handleVersionComparison(latestVersion, isCached = false) { + latestVersion = latestVersion.trim(); + const comparison = compareVersions(latestVersion, manifestVersion); + + updateVersionIcon(comparison, latestVersion); + + if (comparison > 0) { + const alertMessage = `New version ${latestVersion} available! ` + + `Update now`; + + if (isCached) { + notifications.show(`${alertMessage} (Showing cached version)`, 'info', 8000); + } else { + notifications.show(alertMessage, 'info'); + } + } +} + +function updateVersionIcon(versionComparison, latestVersion) { + const versionIcon = document.getElementById('version-icon'); + if (!versionIcon) return; + + versionIcon.className = 'version-icon fas'; + versionIcon.style.color = ''; + versionIcon.removeAttribute('title'); + + latestVersion = latestVersion.trim(); + + if (versionComparison === 0) { + versionIcon.classList.add('fa-check-circle'); + versionIcon.style.color = '#4caf50'; + versionIcon.title = 'You’re up to date! Enjoy the latest features.'; + } else if (versionComparison > 0) { + versionIcon.classList.add('fa-exclamation-circle'); + versionIcon.style.color = '#ff9800'; + versionIcon.title = `A newer version (${latestVersion}) is available! Don’t miss out on the new goodies.`; + } else if (versionComparison < 0) { + versionIcon.classList.add('fa-question-circle'); + versionIcon.style.color = '#2196f3'; + versionIcon.title = 'Whoa! You’re ahead of the curve. Are you from the future?'; + } else { + versionIcon.classList.add('fa-times-circle'); + versionIcon.style.color = '#f44336'; + versionIcon.title = 'Unable to check the version. Is the internet sleeping?'; + } +} + +function showUpdateNotification(latestVersion) { + const message = `Version ${latestVersion} is available! Update now!`; + notifications.show(message, 'info'); +} + +checkForUpdate(); + +document.addEventListener('DOMContentLoaded', () => { + const version = chrome.runtime.getManifest().version; + const versionElement = document.getElementById('extension-version'); + if (versionElement) { + versionElement.innerHTML = `JSTAR Tab v${version} `; + } +}); \ No newline at end of file diff --git a/manifest.json b/manifest.json index 4d7a9ba..b714972 100644 --- a/manifest.json +++ b/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "JSTAR Tab", - "version": "2.6.2", + "version": "3.0.0", "description": "JSTAR Tab is a sleek, customizable new tab extension with personalized greetings, shortcuts, anonymous mode, search engine settings, themes, data management, and more, for an enhanced browsing experience.", "chrome_url_overrides": { "newtab": "index.html" @@ -32,4 +32,4 @@ ], "matches": [""] }] -} +} \ No newline at end of file diff --git a/sw.js b/sw.js new file mode 100644 index 0000000..1880d5a --- /dev/null +++ b/sw.js @@ -0,0 +1,60 @@ +const CACHE_PREFIX = 'jstartab-cache'; +const VERSION_URL = 'https://www.junaid.xyz/projects/jstar-tab/version.txt'; +const STATIC_CACHE = 'jstartab-static'; + +self.addEventListener('install', (event) => { + self.skipWaiting(); + event.waitUntil( + caches.open(STATIC_CACHE).then(cache => cache.add(VERSION_URL)) + ); +}); + +self.addEventListener('activate', (event) => { + event.waitUntil(clients.claim()); +}); + +self.addEventListener('message', async (event) => { + if (event.data.action === 'updateFavicons') { + const faviconUrls = event.data.urls; + const currentDate = new Date(); + const cacheName = `${CACHE_PREFIX}-${currentDate.toISOString().split('T')[0]}`; + + const cache = await caches.open(cacheName); + await cache.addAll(faviconUrls); + + const keys = await caches.keys(); + keys.forEach((key) => { + if (key.startsWith(CACHE_PREFIX) && key !== cacheName) { + caches.delete(key); + } + }); + } +}); + +self.addEventListener('fetch', (event) => { + const request = event.request; + + if (event.request.url.startsWith('https://www.google.com/s2/favicons')) { + event.respondWith( + caches.match(event.request).then((response) => { + return response || fetch(event.request).then((fetchResponse) => { + const cacheCopy = fetchResponse.clone(); + caches.open(`${CACHE_PREFIX}-${new Date().toISOString().split('T')[0]}`) + .then((cache) => cache.put(event.request, cacheCopy)); + return fetchResponse; + }); + }) + ); + } + + if (request.url === VERSION_URL) { + event.respondWith( + caches.open(STATIC_CACHE).then(cache => + fetch(request).then(networkResponse => { + cache.put(request, networkResponse.clone()); + return networkResponse; + }).catch(() => cache.match(request)) + ) + ); + } +}); \ No newline at end of file