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
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Welcome to JSTAR Tab! đź‘‹
-
Let's get you started
-
-
-
- OR
-
-
-
-
-
-
-
What's your name?
-
-
-
-
-
-
-
Choose Your Search Engine
-
-
-
-

-
Google
-
-
-

-
Bing
-
-
-

-
DuckDuckGo
-
-
-

-
Brave
-
-
-

-
Qwant
-
-
-

-
SearXNG
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
Personalization
-
-
-
Anonymous Mode
-
-
-
-
-
-
-
Search
-
-
Search Engine
-
-
-
-
-
-
-
Appearance
-
-
-
-
Show Greeting
-
-
-
-
-
Custom Greeting
-
-
{name}, {greeting}, {time}, {date}, {day}, {month}, {year}
-
-
-
-
Show Search Bar
-
-
-
-
-
Show Shortcuts
-
-
-
-
-
Show Add Shortcut Button
-
-
-
-
-
-
-
Keyboard Shortcuts
-
-
Settings Menu
-
-
-
-
-
-
-
Add Shortcut
-
-
-
-
-
-
-
Toggle Anonymous Mode
-
-
-
-
-
-
-
Toggle Theme
-
-
-
-
-
-
-
Quick URL
-
-
-
-
-
-
-
-
-
-
-
Data Management
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+ New JSTAR Tab
+
+
+
+
+
+
+
+
+
+
+
+
Welcome to JSTAR Tab
+
Let's personalize your new tab experience together. We'll help you set up everything just the way you like it.
+
+
+
+
+
Start Fresh
+
Begin with a clean slate and customize everything from scratch
+
+
+
+
Import Settings
+
Already have a JSTAR Tab setup? Import your existing settings
+
+
+
+
+
+
+
Choose Your Theme
+
Pick a theme that's easy on your eyes. You can always change this later.
+
+
+
+
+
Light Theme
+
Clean and bright interface
+
+
+
+
Dark Theme
+
Easy on the eyes at night
+
+
+
+
+
+
+
Pick Your Font
+
Choose a font that makes reading comfortable for you.
+
+
+
+
Aa
+
Inter
+
Modern and clean
+
+
+
Aa
+
Poppins
+
Geometric and friendly
+
+
+
Aa
+
Roboto
+
Clear and readable
+
+
+
Aa
+
Montserrat
+
Elegant and stylish
+
+
+
Aa
+
Quicksand
+
Rounded and soft
+
+
+
Aa
+
Comic Sans
+
Fun and playful
+
+
+
+
+
+
+
Make It Personal
+
Tell us your name and we'll greet you every time you open a new tab.
+
+
+
+
+
+
+
+
+
Choose Your Search Engine
+
Select your preferred way to search the web.
+
+
+
+

+
Google
+
The world's most popular search engine
+
+
+

+
Bing
+
Microsoft's intelligent search
+
+
+

+
DuckDuckGo
+
Privacy-focused search
+
+
+

+
Brave
+
Independent and secure
+
+
+

+
Qwant
+
European privacy-first search
+
+
+

+
SearXNG
+
Meta search engine
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Personalization
+
+
+
+
Display Name
+
This name will be used in your greeting message
+
+
+
+
+
Anonymous Mode
+
Hide your real name and use a randomly generated one
+
+
+
+
+
+
+
+
+
Custom Greeting
+
Create your own greeting format using the available variables
+
+
Available variables: {name}, {greeting}, {time}, {date}, {day}, {month}, {year}
+
+
+
+
+
+
Appearance
+
+
+
+
+
Theme
+
Switch between light and dark mode
+
+
+
+
+
+
+
+
+
+
Font Family
+
Choose your preferred font for the interface
+
+
+
+
+
+
Font Size
+
Adjust the size of text throughout the interface
+
+
+
+
+
+
+
+
+
Show Greeting
+
Display the welcome message
+
+
+
+
+
+
Show Search Bar
+
Display the search bar for quick web searches
+
+
+
+
+
+
Show Shortcuts
+
Display your favorite website shortcuts
+
+
+
+
+
+
Show Add Shortcut Button
+
Display the button to add new shortcuts
+
+
+
+
+
Grid Layout
+
Choose how your content is organized
+
+
+
+
+
+
Custom Grid Settings
+
Customize your grid layout with specific dimensions
+
+
+
+
+
Resizable Items
+
Allow items to be resized within the grid
+
+
+
+
+
+
+
+
+
+
Update Alerts
+
Get notified when new updates are available
+
+
+
+
+
+
+
+
+
+
+
+
Search
+
+
+
+
Default Search Engine
+
Select which search engine to use when searching from the new tab page
+
+
+
+
+
+
+
+
+
Keyboard Shortcuts
+
+
+
+
Settings Menu
+
+
+
+
+
+
+
Add Shortcut
+
+
+
+
+
+
+
Toggle Anonymous Mode
+
+
+
+
+
+
+
Toggle Theme
+
+
+
+
+
+
+
Quick URL
+
+
+
+
+
+
+
+
+
+
+
Backgrounds
+
+
+
+
+
+
+
+
+
+
Data Management
+
+
+
+
Export your settings, shortcuts, and customizations to a file, or restore them from a previous backup.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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