diff --git a/css/history.css b/css/history.css new file mode 100644 index 0000000..58430fa --- /dev/null +++ b/css/history.css @@ -0,0 +1,567 @@ +:root { + --history-background: var(--card-bg, #ffffff); + --history-text: var(--text-color, #333333); + --history-accent: var(--accent-color, #4a6cf7); + --history-hover: var(--hover-bg, #f5f7ff); + --history-border: var(--border-color, #eaeaea); + --history-danger: #e53935; + --history-danger-hover: #c62828; + --history-success: #43a047; + --history-muted: #757575; + --history-header-height: 120px; + --history-shadow: 0 2px 12px rgba(0, 0, 0, 0.08); + --history-radius: 8px; +} + +body { + background-color: var(--bg-color); + color: var(--text-color); + margin: 0; + padding: 0; + font-family: var(--font-family, 'Inter', sans-serif); + overflow-x: hidden; +} + +.history-container { + display: flex; + flex-direction: column; + height: 100vh; + background-color: var(--history-background); + color: var(--history-text); + max-width: 1200px; + margin: 0 auto; + padding: 0 20px; +} + +.history-header { + position: sticky; + top: 0; + z-index: 10; + background-color: var(--history-background); + display: flex; + flex-direction: column; + gap: 16px; + padding: 20px 0; + border-bottom: 1px solid var(--history-border); +} + +.header-left { + display: flex; + align-items: center; + gap: 16px; +} + +.header-left h1 { + font-size: 24px; + font-weight: 600; + margin: 0; +} + +.header-right { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; +} + +.back-button { + display: flex; + align-items: center; + justify-content: center; + width: 36px; + height: 36px; + border-radius: 50%; + border: none; + background: var(--history-hover); + cursor: pointer; + transition: background 0.2s; +} + +.back-button:hover { + background: var(--history-border); +} + +.back-button svg { + width: 20px; + height: 20px; + stroke: var(--history-text); +} + +.search-bar { + display: flex; + align-items: center; + background-color: var(--history-hover); + border-radius: 24px; + padding: 8px 16px; + flex: 1; + max-width: 500px; + position: relative; + height: 40px; +} + +.search-bar svg { + width: 18px; + height: 18px; + margin-right: 8px; + stroke: var(--history-muted); +} + +.search-bar input { + flex: 1; + border: none; + background: transparent; + font-size: 14px; + outline: none; + color: var(--history-text); +} + +.search-bar input::placeholder { + color: var(--history-muted); +} + +.search-clear { + background: transparent; + border: none; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + padding: 4px; + border-radius: 50%; +} + +.search-clear svg { + width: 16px; + height: 16px; + margin: 0; +} + +.search-clear:hover { + background: rgba(0, 0, 0, 0.05); +} + +.filter-container { + position: relative; +} + +.filter-container select { + appearance: none; + background-color: var(--history-hover); + border: none; + border-radius: 24px; + padding: 8px 16px; + font-size: 14px; + color: var(--history-text); + cursor: pointer; + outline: none; + min-width: 140px; + height: 40px; +} + +.filter-container::after { + content: ""; + position: absolute; + right: 12px; + top: 50%; + transform: translateY(-50%); + width: 0; + height: 0; + border-left: 5px solid transparent; + border-right: 5px solid transparent; + border-top: 5px solid var(--history-text); + pointer-events: none; +} + +.history-actions { + display: flex; + gap: 12px; + padding: 12px 0; + border-bottom: 1px solid var(--history-border); +} + +.history-actions button { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 16px; + border-radius: 24px; + border: 1px solid var(--history-border); + background: transparent; + font-size: 14px; + font-weight: 500; + cursor: pointer; + transition: all 0.2s; +} + +.history-actions button svg { + width: 16px; + height: 16px; + stroke: currentColor; +} + +.history-actions button:hover { + background: var(--history-hover); +} + +.history-actions button:active { + transform: translateY(1px); +} + +.history-actions button[disabled] { + opacity: 0.5; + cursor: not-allowed; +} + +#clear-all { + color: var(--history-danger); +} + +#clear-all:hover { + background: rgba(229, 57, 53, 0.1); +} + +.history-content { + flex: 1; + overflow-y: auto; + padding: 20px 0; + display: flex; + flex-direction: column; + gap: 20px; + position: relative; +} + +.history-loading { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 40px; + gap: 16px; +} + +.loading-spinner { + width: 40px; + height: 40px; + border: 3px solid var(--history-border); + border-top-color: var(--history-accent); + border-radius: 50%; + animation: spin 1s linear infinite; +} + +@keyframes spin { + to { + transform: rotate(360deg); + } +} + +.history-empty { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 40px; + text-align: center; +} + +.history-empty .empty-icon { + width: 80px; + height: 80px; + margin-bottom: 16px; + color: var(--history-muted); + display: flex; + justify-content: center; + align-items: center; +} + +.history-empty .empty-icon svg { + width: 100%; + height: 100%; +} + +.history-empty h2 { + font-size: 20px; + font-weight: 600; + margin: 0 0 8px; +} + +.history-empty p { + color: var(--history-muted); + max-width: 300px; +} + +.history-items { + display: flex; + flex-direction: column; + gap: 20px; +} + +.history-date-group { + display: flex; + flex-direction: column; +} + +.history-date-header { + font-size: 14px; + font-weight: 600; + color: var(--history-muted); + padding: 8px 0; + border-bottom: 1px solid var(--history-border); + margin-bottom: 8px; +} + +.history-item { + display: flex; + align-items: center; + padding: 12px; + border-radius: var(--history-radius); + transition: background 0.2s; + position: relative; +} + +.history-item:hover { + background: var(--history-hover); +} + +.history-item-checkbox { + margin-right: 12px; +} + +.history-item-favicon { + width: 20px; + height: 20px; + margin-right: 12px; + border-radius: 4px; + background: var(--history-border); + overflow: hidden; +} + +.history-item-favicon img { + width: 100%; + height: 100%; + object-fit: cover; +} + +.history-item-content { + flex: 1; + display: flex; + flex-direction: column; + min-width: 0; +} + +.history-item-title { + font-size: 14px; + font-weight: 500; + margin-bottom: 4px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.history-item-url { + font-size: 12px; + color: var(--history-muted); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.history-item-time { + font-size: 12px; + color: var(--history-muted); + margin-left: 16px; + white-space: nowrap; + min-width: 70px; + text-align: right; +} + +.history-item-actions { + display: flex; + align-items: center; + gap: 8px; + margin-left: 16px; + opacity: 0; + transition: opacity 0.2s; +} + +.history-item:hover .history-item-actions { + opacity: 1; +} + +.history-item-action { + display: flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + border-radius: 50%; + border: none; + background: transparent; + cursor: pointer; +} + +.history-item-action:hover { + background: rgba(0, 0, 0, 0.05); +} + +.history-item-action svg { + width: 16px; + height: 16px; + stroke: var(--history-muted); +} + +.history-item-action.delete:hover svg { + stroke: var(--history-danger); +} + +.history-load-more { + display: flex; + justify-content: center; + padding: 20px 0; +} + +.history-load-more button { + padding: 8px 24px; + border-radius: 24px; + border: none; + background: var(--history-accent); + color: white; + font-size: 14px; + font-weight: 500; + cursor: pointer; + transition: background 0.2s; +} + +.history-load-more button:hover { + background: var(--history-accent); + filter: brightness(1.1); +} + +.hidden { + display: none !important; +} + +.modal { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 1000; + display: flex; + align-items: center; + justify-content: center; + opacity: 0; + visibility: hidden; + transition: opacity 0.3s, visibility 0.3s; +} + +.modal.hidden { + display: none; +} + +.modal:not(.hidden) { + opacity: 1; + visibility: visible; +} + +.modal-backdrop { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.5); +} + +.modal-content { + background-color: var(--history-background); + border-radius: var(--history-radius); + box-shadow: var(--history-shadow); + position: relative; + z-index: 1; + transform: translateY(20px); + transition: transform 0.3s; + max-width: 90%; +} + +.modal:not(.hidden) .modal-content { + transform: translateY(0); +} + +.modal-content.confirmation-dialog { + width: 400px; + max-width: 90%; + padding: 24px; + box-sizing: border-box; + opacity: 1 !important; + visibility: visible !important; +} + +.history-empty .empty-icon { + width: 80px; + height: 80px; + margin-bottom: 16px; + color: var(--history-muted); + display: flex; + justify-content: center; + align-items: center; +} + +.history-empty .empty-icon svg { + width: 100%; + height: 100%; +} + +.globe-icon { + width: 16px !important; + height: 16px !important; + stroke: var(--history-muted); + display: block; + margin: 0 auto; +} + +.history-item-favicon svg { + width: 16px; + height: 16px; + background: var(--history-hover); + border-radius: 4px; + padding: 2px; +} + +.history-empty:not(.hidden) + .history-items { + display: none !important; +} + +@media (min-width: 768px) { + .history-header { + flex-direction: row; + justify-content: space-between; + align-items: center; + } + + .header-right { + justify-content: flex-end; + } +} + +@media (max-width: 767px) { + .history-item-time { + display: none; + } + + .history-item-actions { + opacity: 1; + } + + .history-actions { + justify-content: space-between; + } + + .history-actions button span { + display: none; + } + + .history-actions button { + padding: 8px; + } +} \ No newline at end of file diff --git a/css/onboarding.css b/css/onboarding.css index 28a8fe6..46e88a2 100644 --- a/css/onboarding.css +++ b/css/onboarding.css @@ -219,27 +219,82 @@ } .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); + width: 100% !important; + border-radius: 16px !important; + padding: 1.5rem !important; + margin-bottom: 1rem !important; + transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1) !important; + border: 2px solid var(--border) !important; } .theme-preview-ob.light-ob { - background-color: white; - color: #1a1a1a; + background-color: #ffffff !important; + color: #1a1a1a !important; + border: 2px solid #eaeaea !important; } .theme-preview-ob.dark-ob { - background-color: #1a1a1a; - color: white; + background-color: #1a1a1a !important; + color: #ffffff !important; + border: 2px solid #333333 !important; } .theme-preview-ob.light-ob .preview-search-ob { - background-color: #f0f0f0; - border-color: #dddddd; + background-color: #f0f0f0 !important; + border-color: #dddddd !important; +} + +.theme-preview-ob.dark-ob .preview-search-ob { + background-color: #333333 !important; + border-color: #444444 !important; +} + +.option-card-ob[data-theme="light"] { + background-color: #ffffff !important; + border: 2px solid #eaeaea !important; + color: #1a1a1a !important; +} + +.option-card-ob[data-theme="light"]:hover { + transform: translateY(-5px); + box-shadow: 0 10px 20px rgba(0, 0, 0, 0.08) !important; + border-color: #1a1a1a !important; +} + +.option-card-ob[data-theme="light"].selected-ob { + border-color: #1a1a1a !important; + background-color: #ffffff !important; + transform: scale(1.02); +} + +.option-card-ob[data-theme="light"] h3 { + color: #1a1a1a !important; +} + +.option-card-ob[data-theme="light"] p { + color: #666666 !important; +} + +.option-card-ob[data-theme="light"] svg { + color: #1a1a1a !important; +} + +.option-card-ob[data-theme="dark"] { + background-color: #1a1a1a !important; + border: 2px solid #333333 !important; + color: #ffffff !important; +} + +.option-card-ob[data-theme="dark"]:hover { + transform: translateY(-5px); + box-shadow: 0 10px 20px rgba(0, 0, 0, 0.2) !important; + border-color: #ffffff !important; +} + +.option-card-ob[data-theme="dark"].selected-ob { + border-color: #ffffff !important; + background-color: #1a1a1a !important; + transform: scale(1.02); } .theme-preview-content-ob { diff --git a/css/style.css b/css/style.css index f6d1742..51b80aa 100644 --- a/css/style.css +++ b/css/style.css @@ -1263,21 +1263,25 @@ input:checked + .toggle-slider:before { border-radius: 8px; padding: 1rem 0.2rem; cursor: pointer; - transition: color 0.3s ease, transform 0.3s ease; + transition: transform 0.3s ease; + display: flex; + align-items: center; + justify-content: center; } #back-to-home:hover { - color: var(--primary-hover); - transform: scale(1.15); + color: var(--text); + transform: scale(1.05); } -#back-to-home i { - font-size: 1.25rem; +#back-to-home svg { + width: 20px; + height: 20px; transition: transform 0.3s ease; } -#back-to-home:hover i { - transform: scale(1.2); +#back-to-home:hover svg { + transform: translateX(-2px); } .settings-sidebar-header { @@ -2237,4 +2241,478 @@ input[type="range"]::-webkit-slider-thumb:hover { .color-setting input[type="color"]::-webkit-color-swatch { border: 2px solid var(--border); border-radius: 6px; +} + +:root, [data-theme] { + --theme-transition-speed: 0.2s; + transition: background-color var(--theme-transition-speed) ease, + color var(--theme-transition-speed) ease, + border-color var(--theme-transition-speed) ease; +} + +.instant-theme-change { + --theme-transition-speed: 0s !important; +} +.subtle-motion * { + animation-duration: 0.4s !important; + transition-duration: 0.4s !important; +} + +.subtle-motion *:not(:root *) { + transition-property: transform, opacity, visibility, box-shadow !important; +} + +.reduced-motion * { + animation-duration: 0.25s !important; + transition-duration: 0.25s !important; + transition-timing-function: ease-out !important; +} + +.reduced-motion *:not(:root *) { + transition-property: transform, opacity, visibility, box-shadow !important; +} + +.reduced-motion [class*='SlideIn'] { + animation-name: fadeIn !important; +} + +.minimal-motion * { + animation-duration: 0.1s !important; + transition-duration: 0.1s !important; + transition-timing-function: ease-out !important; +} + +.minimal-motion *:not(:root *) { + transition-property: transform, opacity, visibility, box-shadow !important; +} + +.minimal-motion [class*='SlideIn'], +.minimal-motion [style*="animation"] { + animation-name: fadeIn !important; +} + +.no-motion * { + animation: none !important; + transition: none !important; +} + +@media (prefers-reduced-motion) { + body:not(.subtle-motion):not(.reduced-motion):not(.minimal-motion):not(.no-motion) * { + animation-duration: 0.25s !important; + transition-duration: 0.25s !important; + transition-timing-function: ease-out !important; + } + + body:not(.subtle-motion):not(.reduced-motion):not(.minimal-motion):not(.no-motion) *:not(:root *) { + transition-property: transform, opacity, visibility, box-shadow !important; + } + + body:not(.subtle-motion):not(.reduced-motion):not(.minimal-motion):not(.no-motion) [class*='SlideIn'] { + animation-name: fadeIn !important; + } +} + +.confirmation-dialog { + max-width: 400px; + padding: 2rem; + text-align: center; + position: relative; +} + +.confirmation-icon, .lock-icon { + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 1.5rem; +} + +.confirmation-icon svg, .lock-icon svg { + width: 48px; + height: 48px; +} + +.confirmation-icon svg { + color: #ff4d4f; +} + +.lock-icon svg { + color: #9333EA; +} + +.confirmation-content { + margin-bottom: 1.5rem; +} + +.confirmation-content h3 { + font-size: 1.25rem; + margin-bottom: 0.5rem; + font-weight: 600; +} + +.confirmation-content p { + font-size: 0.9375rem; + color: var(--text-secondary); + margin-bottom: 1.5rem; +} + +.dialog-close { + position: absolute; + top: 15px; + right: 15px; + background: none; + border: none; + padding: 0; + width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + color: var(--text-color); + opacity: 0.7; + transition: opacity 0.2s; +} + +.dialog-close:hover { + opacity: 1; +} + +.dialog-close svg { + width: 18px; + height: 18px; +} + +.password-input-container { + margin-bottom: 1rem; +} + +.secrecy-note { + display: flex; + align-items: flex-start; + gap: 12px; + background-color: rgba(0, 0, 0, 0.03); + border-radius: 10px; + padding: 14px; + margin: 16px 0; + font-size: 0.875rem; + line-height: 1.5; + color: var(--text-secondary); + text-align: left; + border: 1px solid rgba(0, 0, 0, 0.06); +} + +.secrecy-note img { + width: 28px; + height: 28px; + object-fit: contain; + margin-top: 2px; +} + +.secrecy-note a { + color: #9333EA; + text-decoration: none; + font-weight: 600; + text-decoration: underline; +} + +.secrecy-note a:hover { + color: #7e22ce; +} + +[data-theme="dark"] .secrecy-note { + background-color: rgba(255, 255, 255, 0.05); + border-color: rgba(255, 255, 255, 0.08); +} + +.password-input-container input { + width: 100% !important; + padding: 0.75rem 1rem !important; + border: 2px solid var(--border) !important; + border-radius: 12px !important; + background-color: var(--surface) !important; + color: var(--text) !important; + font-size: 0.9375rem !important; + transition: all 0.2s ease !important; +} + +.password-input-container input:focus { + outline: none !important; + border-color: var(--accent-color) !important; +} + +.password-error { + color: #ff4d4f !important; + margin-top: 0.75rem; + font-size: 0.875rem; +} + +.modal-actions { + display: flex; + justify-content: center; + gap: 0.75rem; +} + +.password-protected { + position: relative; +} + +.password-protected::before { + content: ''; + position: absolute; + top: 8px; + right: 8px; + width: 14px; + height: 14px; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='14' height='14'%3E%3Crect x='3' y='11' width='18' height='11' rx='2' ry='2' fill='black' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3C/rect%3E%3Cpath d='M7 11V7a5 5 0 0 1 10 0v4' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' fill='none'%3E%3C/path%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: center; + z-index: 1; +} + +body[data-theme="dark"] .password-protected::before { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' width='14' height='14'%3E%3Crect x='3' y='11' width='18' height='11' rx='2' ry='2' fill='white' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3C/rect%3E%3Cpath d='M7 11V7a5 5 0 0 1 10 0v4' stroke='white' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' fill='none'%3E%3C/path%3E%3C/svg%3E"); +} + +.checkbox-container { + margin: 15px 0; +} + +.checkbox-label { + display: flex; + align-items: center; + cursor: pointer; +} + +.checkbox-label input[type="checkbox"] { + margin-right: 8px; +} + +.dialog { + position: fixed; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.5); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; + opacity: 0; + transition: opacity 0.3s ease; +} + +.dialog.active { + opacity: 1; +} + +.dialog-container { + background-color: var(--bg-color); + border-radius: 8px; + box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2); + max-width: 90%; + width: 400px; + overflow: hidden; + transform: translateY(-20px); + transition: transform 0.3s ease; +} + +.dialog.active .dialog-container { + transform: translateY(0); +} + +.dialog-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 15px 20px; + border-bottom: 1px solid var(--border-color); +} + +.dialog-body { + padding: 20px; +} + +.dialog-title { + margin: 0; + font-size: 1.2rem; + font-weight: 500; +} + +.dialog-message { + margin-bottom: 20px; +} + +.dialog-actions { + display: flex; + justify-content: flex-end; + gap: 10px; +} + +.shortcut-protection-manager { + margin-top: 20px; + border-top: 1px solid var(--border-color); + padding-top: 15px; +} + +.shortcut-protection-manager h4 { + margin-bottom: 10px; + font-size: 1rem; +} + +.shortcut-selector-container { + display: flex; + flex-direction: column; + gap: 15px; + margin-top: 15px; +} + +.shortcut-dropdown { + position: relative; + width: 100%; + user-select: none; + -webkit-user-select: none; + -webkit-tap-highlight-color: transparent; +} + +.shortcut-dropdown-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; +} + +.shortcut-dropdown-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); +} + +.shortcut-dropdown-selected.active::after { + transform: translateY(-30%) rotate(-135deg); +} + +.shortcut-dropdown-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); +} + +.shortcut-dropdown-items.active { + display: block; +} + +.shortcut-dropdown-item { + padding: 0.75rem 1.25rem; + cursor: pointer; + display: flex; + align-items: center; + gap: 0.875rem; + color: var(--text); + font-weight: 500; +} + +.shortcut-dropdown-item:hover { + background-color: var(--primary); + color: var(--text); +} + +.shortcut-dropdown-selected:hover { + border-color: var(--text); + transform: translateY(-1px); + box-shadow: 0 4px 12px var(--shadow); +} + +.shortcut-dropdown-selected:active { + transform: translateY(0); + box-shadow: 0 2px 8px var(--shadow); +} + +.selected-shortcuts-container { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin: 15px 0; +} + +.shortcut-chip { + display: flex; + align-items: center; + background-color: var(--accent-light); + border-radius: 20px; + padding: 6px 12px; + font-size: 0.9rem; + gap: 8px; + transition: all 0.2s ease; + border: 1px solid var(--border); +} + +.shortcut-chip img { + width: 16px; + height: 16px; +} + +.remove-chip-btn { + background: none; + border: none; + color: var(--text); + cursor: pointer; + font-size: 16px; + display: flex; + align-items: center; + justify-content: center; + padding: 0; + width: 20px; + height: 20px; + border-radius: 50%; + margin-left: 4px; +} + +.remove-chip-btn:hover { + background-color: rgba(0, 0, 0, 0.1); +} + +.empty-protection-state { + color: var(--text-color-secondary); + font-size: 0.9rem; + font-style: italic; + margin: 10px 0; + text-align: center; +} + +.no-shortcuts-message { + color: var(--text-color-secondary); + font-size: 0.9rem; + margin-top: 15px; + text-align: center; +} + +[data-theme="dark"] .shortcut-chip { + background-color: var(--accent-dark); +} + +[data-theme="dark"] .remove-chip-btn:hover { + background-color: rgba(255, 255, 255, 0.1); } \ No newline at end of file diff --git a/history.html b/history.html new file mode 100644 index 0000000..76bd368 --- /dev/null +++ b/history.html @@ -0,0 +1,144 @@ + + + + + + Browsing History - JSTAR Tab + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ +

Browsing History

+
+ +
+ +
+
+ + +
+ + + +
+ + +
+ +
+
+

Loading your browsing history...

+
+ + + + + +
+ + + +
+
+ + + + + +
+ + + + + + \ No newline at end of file diff --git a/images/secrecy.png b/images/secrecy.png new file mode 100644 index 0000000..3fba18d Binary files /dev/null and b/images/secrecy.png differ diff --git a/index.html b/index.html index 02b48f4..97fd6ef 100644 --- a/index.html +++ b/index.html @@ -220,9 +220,13 @@ Personalization - + Appearance + + + Privacy + Search @@ -292,6 +296,28 @@ Available variables: {name}, {greeting}, {time}, {date}, {day}, {month}, {year} + +
+
+
+ +
+
+

Notifications

+

Configure update notifications and alerts

+
+
+
+
+
Update Alerts
+
Get notified when new updates are available
+
+ +
+
@@ -315,7 +341,29 @@
- +
+
Icon Style
+
Choose between linear or solid icon styles
+
+ +
+
+
+
Motion Preference
+
Control how animations and transitions appear
+
+ +
+
@@ -452,28 +500,6 @@
-
-
-
- -
-
-

Notifications

-

Configure update notifications and alerts

-
-
-
-
-
Update Alerts
-
Get notified when new updates are available
-
- -
-
-
@@ -555,13 +581,6 @@
-
-
Add Shortcut
-
- - -
-
Toggle Anonymous Mode
@@ -576,6 +595,13 @@
+
+
History Page
+
+ + +
+
Quick URL
@@ -587,6 +613,49 @@
+
+

Privacy

+
+
+
+ +
+
+

Privacy Settings

+

Control privacy and security features

+
+
+
+
+
Anonymous Mode
+
Hide your real name and use a randomly generated one
+
+ +
+
+
+
Password Protected Shortcuts
+
Add password protection to specific shortcuts
+
+ +
+ +
+
+

Backgrounds

@@ -707,6 +776,56 @@
+ + + + +
+
@@ -730,12 +849,6 @@ - - - - - - @@ -744,6 +857,15 @@ + + + + + + + + + @@ -789,24 +911,6 @@ - - - - - - - - - - - - - - - - - - @@ -876,10 +980,205 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/js/grid-layout.js b/js/grid-layout.js index 44734ae..8d4b9d7 100644 --- a/js/grid-layout.js +++ b/js/grid-layout.js @@ -179,7 +179,11 @@ const GridLayout = { const resetButton = document.getElementById('reset-layout'); if (resetButton) { resetButton.addEventListener('click', () => { - this.resetToDefaults(); + shortcuts.showConfirmDialog( + 'Reset Layout', + 'Are you sure you want to reset the layout settings to default?', + () => this.resetToDefaults() + ); }); } }, @@ -325,10 +329,12 @@ const GridLayout = { }; document.addEventListener('DOMContentLoaded', () => { + console.log('DOM content loaded - initializing grid layout'); GridLayout.init(); }); window.addEventListener('load', () => { + console.log('Window fully loaded - ensuring grid layout is initialized'); setTimeout(() => { if (!window.gridLayoutInitialized) { GridLayout.init(); diff --git a/js/history.js b/js/history.js new file mode 100644 index 0000000..7c22e18 --- /dev/null +++ b/js/history.js @@ -0,0 +1,904 @@ +document.addEventListener('DOMContentLoaded', () => { + const historyContainer = document.querySelector('.history-container'); + const searchInput = document.querySelector('.search-bar input'); + const searchClear = document.querySelector('.search-clear'); + const selectAllCheckbox = document.querySelector('#select-all'); + const deleteSelectedBtn = document.querySelector('#delete-selected'); + const clearAllBtn = document.querySelector('#clear-all'); + const historyContent = document.querySelector('.history-content'); + const historyItems = document.querySelector('.history-items'); + const historyLoading = document.querySelector('.history-loading'); + const historyEmpty = document.querySelector('.history-empty'); + const loadMoreBtn = document.querySelector('.history-load-more button'); + const confirmationDialog = document.querySelector('#confirmation-dialog'); + const confirmTitle = document.querySelector('#confirmation-title'); + const confirmMessage = document.querySelector('#confirmation-message'); + const confirmYesBtn = document.querySelector('#confirm-yes'); + const confirmNoBtn = document.querySelector('#confirm-no'); + const searchResultsCount = document.querySelector('.search-results-count'); + + const browserInfo = detectBrowser(); + console.log(`Detected browser: ${browserInfo.name} ${browserInfo.version}`); + + const faviconCache = new Map(); + + let state = { + history: [], + filteredHistory: [], + searchTerm: '', + selectedItems: new Set(), + isLoading: true, + page: 1, + hasMore: true, + itemsPerPage: 100, + currentAction: null + }; + + initEvents(); + loadHistory(); + + function initEvents() { + if (searchInput) searchInput.addEventListener('input', debounce(handleSearch, 300)); + if (searchClear) { + searchClear.addEventListener('click', clearSearch); + hideElement(searchClear); + } + + if (selectAllCheckbox) selectAllCheckbox.addEventListener('change', handleSelectAll); + + if (deleteSelectedBtn) deleteSelectedBtn.addEventListener('click', () => { + if (state.selectedItems.size === 0) { + showNotification('No items selected', 'error'); + return; + } + state.currentAction = 'delete-selected'; + confirmTitle.textContent = 'Delete selected items?'; + confirmMessage.textContent = 'This will remove the selected browsing history items.'; + showConfirmationDialog(); + }); + + if (clearAllBtn) clearAllBtn.addEventListener('click', () => { + if (state.filteredHistory.length === 0) { + showNotification('No history to clear', 'error'); + return; + } + state.currentAction = 'clear-all'; + confirmTitle.textContent = 'Clear all history?'; + confirmMessage.textContent = 'This will remove all your browsing history.'; + showConfirmationDialog(); + }); + + if (loadMoreBtn) loadMoreBtn.addEventListener('click', loadMoreHistory); + + if (confirmYesBtn) confirmYesBtn.addEventListener('click', handleConfirmedDelete); + if (confirmNoBtn) confirmNoBtn.addEventListener('click', hideConfirmationDialog); + + const modalBackdrop = document.querySelector('.modal-backdrop'); + if (modalBackdrop) { + modalBackdrop.addEventListener('click', hideConfirmationDialog); + } + + const backButton = document.querySelector('.back-button'); + if (backButton) { + backButton.addEventListener('click', () => { + window.history.back(); + }); + } + } + + function detectBrowser() { + const userAgent = navigator.userAgent; + let browserName = "Unknown"; + let version = ""; + + if (userAgent.indexOf("Firefox") > -1) { + browserName = "Firefox"; + version = userAgent.match(/Firefox\/([0-9.]+)/)[1]; + } else if (userAgent.indexOf("Edg") > -1) { + browserName = "Edge"; + version = userAgent.match(/Edg\/([0-9.]+)/)[1]; + } else if (userAgent.indexOf("Chrome") > -1) { + browserName = "Chrome"; + version = userAgent.match(/Chrome\/([0-9.]+)/)[1]; + } else if (userAgent.indexOf("Safari") > -1) { + browserName = "Safari"; + version = userAgent.match(/Safari\/([0-9.]+)/)[1]; + } else if (userAgent.indexOf("OPR") > -1 || userAgent.indexOf("Opera") > -1) { + browserName = "Opera"; + version = userAgent.match(/(?:OPR|Opera)\/([0-9.]+)/)[1]; + } + + return { name: browserName, version: version }; + } + + function extractDomain(url) { + try { + const urlObj = new URL(url); + return urlObj.hostname; + } catch (e) { + return url; + } + } + + async function loadHistory() { + showLoading(); + + try { + state.page = 1; + state.hasMore = true; + + if (typeof chrome !== 'undefined' && chrome.history) { + console.log('Using Chrome history API'); + const historyData = await fetchChromeHistory(); + state.history = historyData; + state.filteredHistory = historyData; + + if (historyData.length >= state.itemsPerPage) { + state.hasMore = true; + } else { + state.hasMore = false; + } + } else if (typeof browser !== 'undefined' && browser.history) { + console.log('Using Firefox history API'); + const historyData = await fetchFirefoxHistory(); + state.history = historyData; + state.filteredHistory = historyData; + + if (historyData.length >= state.itemsPerPage) { + state.hasMore = true; + } else { + state.hasMore = false; + } + } else { + console.warn('No browser history API available'); + state.history = []; + state.filteredHistory = []; + showNotification('Cannot access browser history. Make sure permissions are granted.', 'error'); + } + + state.isLoading = false; + + updateUI(); + + if (state.filteredHistory.length === 0) { + if (state.searchTerm) { + showEmptyState('No history found', 'No results match your search. Try different keywords.'); + } else { + showEmptyState('No history found', 'Your browsing history will appear here.'); + } + } + } catch (error) { + console.error('Error loading history:', error); + hideLoading(); + showEmptyState('Error loading history', 'There was a problem accessing your browsing history. Make sure the extension has the necessary permissions.'); + } + } + + async function fetchChromeHistory() { + return new Promise((resolve, reject) => { + const searchParams = getHistorySearchParams(); + + chrome.history.search(searchParams, (historyItems) => { + if (chrome.runtime.lastError) { + reject(chrome.runtime.lastError); + return; + } + + const formattedItems = historyItems.map(item => ({ + id: item.id, + url: item.url, + title: item.title || extractDomain(item.url), + lastVisitTime: item.lastVisitTime, + visitCount: item.visitCount, + domain: extractDomain(item.url) + })); + + resolve(formattedItems); + }); + }); + } + + async function fetchFirefoxHistory() { + return new Promise((resolve, reject) => { + const searchParams = getHistorySearchParams(); + + browser.history.search(searchParams).then(historyItems => { + const formattedItems = historyItems.map(item => ({ + id: item.id, + url: item.url, + title: item.title || extractDomain(item.url), + lastVisitTime: item.lastVisitTime, + visitCount: item.visitCount, + domain: extractDomain(item.url) + })); + + resolve(formattedItems); + }).catch(error => { + reject(error); + }); + }); + } + + function getHistorySearchParams() { + return { + text: state.searchTerm, + maxResults: state.itemsPerPage * 3, + startTime: 0 + }; + } + + async function loadMoreHistory() { + if (!state.hasMore) { + hideElement(document.querySelector('.history-load-more')); + return; + } + + state.page++; + + if (typeof chrome !== 'undefined' && chrome.history) { + try { + const lastItem = state.filteredHistory[state.filteredHistory.length - 1]; + const startTime = lastItem ? lastItem.lastVisitTime - 1 : 0; + + const searchParams = { + text: state.searchTerm, + maxResults: state.itemsPerPage * 2, + startTime: 0, + endTime: startTime + }; + + chrome.history.search(searchParams, (historyItems) => { + if (chrome.runtime.lastError) { + console.error('Error loading more history:', chrome.runtime.lastError); + return; + } + + const existingIds = new Set(state.filteredHistory.map(item => item.id)); + const newItems = historyItems + .filter(item => !existingIds.has(item.id)) + .map(item => ({ + id: item.id, + url: item.url, + title: item.title || extractDomain(item.url), + lastVisitTime: item.lastVisitTime, + visitCount: item.visitCount, + domain: extractDomain(item.url) + })); + + if (newItems.length === 0) { + state.hasMore = false; + hideElement(document.querySelector('.history-load-more')); + return; + } + + state.history = [...state.history, ...newItems]; + state.filteredHistory = [...state.filteredHistory, ...newItems]; + + renderMoreHistoryItems(newItems); + + if (newItems.length < state.itemsPerPage) { + state.hasMore = false; + hideElement(document.querySelector('.history-load-more')); + } + }); + } catch (error) { + console.error('Error loading more history:', error); + state.hasMore = false; + hideElement(document.querySelector('.history-load-more')); + } + } else if (typeof browser !== 'undefined' && browser.history) { + try { + const lastItem = state.filteredHistory[state.filteredHistory.length - 1]; + const startTime = lastItem ? lastItem.lastVisitTime - 1 : 0; + + const searchParams = { + text: state.searchTerm, + maxResults: state.itemsPerPage * 2, + startTime: 0, + endTime: startTime + }; + + const historyItems = await browser.history.search(searchParams); + + const existingIds = new Set(state.filteredHistory.map(item => item.id)); + const newItems = historyItems + .filter(item => !existingIds.has(item.id)) + .map(item => ({ + id: item.id, + url: item.url, + title: item.title || extractDomain(item.url), + lastVisitTime: item.lastVisitTime, + visitCount: item.visitCount, + domain: extractDomain(item.url) + })); + + if (newItems.length === 0) { + state.hasMore = false; + hideElement(document.querySelector('.history-load-more')); + return; + } + + state.history = [...state.history, ...newItems]; + state.filteredHistory = [...state.filteredHistory, ...newItems]; + + renderMoreHistoryItems(newItems); + + if (newItems.length < state.itemsPerPage) { + state.hasMore = false; + hideElement(document.querySelector('.history-load-more')); + } + } catch (error) { + console.error('Error loading more history:', error); + state.hasMore = false; + hideElement(document.querySelector('.history-load-more')); + } + } + } + + function renderMoreHistoryItems(items) { + if (items.length === 0) return; + + const groupedItems = groupByDate(items); + const historyItemsContainer = document.querySelector('.history-items'); + + for (const [date, dateItems] of Object.entries(groupedItems)) { + let dateGroup = document.querySelector(`.history-date-group[data-date="${date}"]`); + + if (!dateGroup) { + dateGroup = document.createElement('div'); + dateGroup.className = 'history-date-group'; + dateGroup.setAttribute('data-date', date); + + const dateHeader = document.createElement('div'); + dateHeader.className = 'history-date-header'; + dateHeader.textContent = formatDateHeading(date); + + dateGroup.appendChild(dateHeader); + historyItemsContainer.appendChild(dateGroup); + } + + for (const item of dateItems) { + const itemElement = createHistoryItemElement(item); + dateGroup.appendChild(itemElement); + } + } + } + + function handleSearch() { + const searchTerm = searchInput.value.trim().toLowerCase(); + state.searchTerm = searchTerm; + + if (searchTerm.length > 0) { + showElement(searchClear); + } else { + hideElement(searchClear); + } + + state.page = 1; + state.hasMore = true; + + loadHistory(); + } + + function clearSearch() { + if (searchInput) { + searchInput.value = ''; + state.searchTerm = ''; + hideElement(searchClear); + + state.page = 1; + state.hasMore = true; + + loadHistory(); + } + } + + function applySearch() { + if (state.searchTerm.length > 0) { + const filtered = state.history.filter(item => { + const title = (item.title || '').toLowerCase(); + const url = (item.url || '').toLowerCase(); + const domain = (item.domain || '').toLowerCase(); + + return title.includes(state.searchTerm) || + url.includes(state.searchTerm) || + domain.includes(state.searchTerm); + }); + + state.filteredHistory = filtered; + } else { + state.filteredHistory = state.history; + } + + updateUI(); + } + + function updateUI() { + hideLoading(); + + if (state.filteredHistory.length === 0) { + showEmptyState('No history found', 'No results match your search. Try different keywords.'); + } else { + hideEmptyState(); + } + + renderHistoryItems(state.filteredHistory); + + updateActionButtonsState(); + + if (state.hasMore) { + showElement(document.querySelector('.history-load-more')); + } else { + hideElement(document.querySelector('.history-load-more')); + } + + if (searchResultsCount) { + if (state.searchTerm) { + searchResultsCount.textContent = `${state.filteredHistory.length} results found for "${state.searchTerm}"`; + showElement(searchResultsCount); + } else { + hideElement(searchResultsCount); + } + } + } + + function renderHistoryItems(items) { + if (!historyItems) return; + + historyItems.innerHTML = ''; + + if (items.length === 0) { + showEmptyState(); + return; + } + + const groupedItems = groupByDate(items); + + for (const [date, dateItems] of Object.entries(groupedItems)) { + const dateGroup = document.createElement('div'); + dateGroup.className = 'history-date-group'; + dateGroup.setAttribute('data-date', date); + + const dateHeader = document.createElement('div'); + dateHeader.className = 'history-date-header'; + dateHeader.textContent = formatDateHeading(date); + + dateGroup.appendChild(dateHeader); + + for (const item of dateItems) { + const itemElement = createHistoryItemElement(item); + dateGroup.appendChild(itemElement); + } + + historyItems.appendChild(dateGroup); + } + } + + function createHistoryItemElement(item) { + const historyItem = document.createElement('div'); + historyItem.className = 'history-item'; + historyItem.setAttribute('data-id', item.id); + + const checkbox = document.createElement('div'); + checkbox.className = 'history-item-checkbox'; + const checkboxInput = document.createElement('input'); + checkboxInput.type = 'checkbox'; + checkboxInput.checked = state.selectedItems.has(item.id); + checkboxInput.addEventListener('change', (e) => { + handleItemSelection(item.id, e.target.checked); + }); + checkbox.appendChild(checkboxInput); + + const favicon = document.createElement('div'); + favicon.className = 'history-item-favicon'; + + let faviconUrl = faviconCache.get(item.domain); + if (!faviconUrl) { + faviconUrl = `https://www.google.com/s2/favicons?domain=${item.domain}&sz=32`; + faviconCache.set(item.domain, faviconUrl); + } + + const faviconImg = document.createElement('img'); + faviconImg.src = faviconUrl; + faviconImg.alt = ''; + faviconImg.onerror = () => { + favicon.innerHTML = ` + + + + + + `; + }; + + favicon.appendChild(faviconImg); + + const content = document.createElement('div'); + content.className = 'history-item-content'; + + const title = document.createElement('div'); + title.className = 'history-item-title'; + title.textContent = item.title || extractDomain(item.url); + + const url = document.createElement('div'); + url.className = 'history-item-url'; + url.textContent = item.url; + + content.appendChild(title); + content.appendChild(url); + + const time = document.createElement('div'); + time.className = 'history-item-time'; + time.textContent = formatTime(new Date(item.lastVisitTime)); + + const actions = document.createElement('div'); + actions.className = 'history-item-actions'; + + const openAction = document.createElement('button'); + openAction.className = 'history-item-action'; + openAction.title = 'Open in new tab'; + openAction.innerHTML = ''; + openAction.addEventListener('click', (e) => { + e.stopPropagation(); + openLink(item.url); + }); + + const deleteAction = document.createElement('button'); + deleteAction.className = 'history-item-action delete'; + deleteAction.title = 'Delete'; + deleteAction.innerHTML = ''; + deleteAction.addEventListener('click', (e) => { + e.stopPropagation(); + deleteHistoryItem(item.id); + }); + + actions.appendChild(openAction); + actions.appendChild(deleteAction); + + content.addEventListener('click', () => { + const isSelected = state.selectedItems.has(item.id); + checkboxInput.checked = !isSelected; + handleItemSelection(item.id, !isSelected); + }); + + historyItem.appendChild(checkbox); + historyItem.appendChild(favicon); + historyItem.appendChild(content); + historyItem.appendChild(time); + historyItem.appendChild(actions); + + return historyItem; + } + + function handleSelectAll(e) { + const isChecked = e.target.checked; + + if (isChecked) { + state.filteredHistory.forEach(item => { + state.selectedItems.add(item.id); + }); + } else { + state.selectedItems.clear(); + } + + const checkboxes = document.querySelectorAll('.history-item-checkbox input'); + checkboxes.forEach(checkbox => { + const itemId = checkbox.closest('.history-item').getAttribute('data-id'); + checkbox.checked = state.selectedItems.has(itemId); + }); + + updateActionButtonsState(); + } + + function handleItemSelection(id, isSelected) { + if (isSelected) { + state.selectedItems.add(id); + } else { + state.selectedItems.delete(id); + } + + updateActionButtonsState(); + } + + function updateActionButtonsState() { + if (deleteSelectedBtn) { + deleteSelectedBtn.disabled = state.selectedItems.size === 0; + } + + if (clearAllBtn) { + clearAllBtn.disabled = state.filteredHistory.length === 0; + } + + const selectAllCheckbox = document.querySelector('#select-all'); + if (selectAllCheckbox) { + if (state.filteredHistory.length === 0) { + selectAllCheckbox.checked = false; + selectAllCheckbox.disabled = true; + } else { + selectAllCheckbox.disabled = false; + selectAllCheckbox.checked = state.filteredHistory.length > 0 && + state.selectedItems.size === state.filteredHistory.length; + } + } + } + + function deleteHistoryItem(id) { + state.currentAction = 'delete-item'; + state.currentItemId = id; + confirmTitle.textContent = 'Delete item?'; + confirmMessage.textContent = 'This will remove this item from your browsing history.'; + showConfirmationDialog(); + } + + function removeHistoryItemFromState(id) { + state.selectedItems.delete(id); + + state.history = state.history.filter(item => item.id !== id); + state.filteredHistory = state.filteredHistory.filter(item => item.id !== id); + + const itemElement = document.querySelector(`.history-item[data-id="${id}"]`); + if (itemElement) { + const dateGroup = itemElement.closest('.history-date-group'); + itemElement.remove(); + + if (dateGroup && dateGroup.querySelectorAll('.history-item').length === 0) { + dateGroup.remove(); + } + } + + updateActionButtonsState(); + + if (state.filteredHistory.length === 0) { + if (state.searchTerm) { + showEmptyState('No history found', 'No results match your search. Try different keywords.'); + } else { + showEmptyState('No history found', 'Your browsing history will appear here.'); + } + } + } + + function handleConfirmedDelete() { + hideConfirmationDialog(); + + if (state.currentAction === 'delete-selected') { + deleteSelectedItems(); + } else if (state.currentAction === 'clear-all') { + clearAllHistory(); + } else if (state.currentAction === 'delete-item') { + const id = state.currentItemId; + if (typeof chrome !== 'undefined' && chrome.history) { + chrome.history.deleteUrl({ url: state.history.find(item => item.id === id).url }, () => { + removeHistoryItemFromState(id); + showNotification('Item deleted', 'success'); + }); + } else if (typeof browser !== 'undefined' && browser.history) { + browser.history.deleteUrl({ url: state.history.find(item => item.id === id).url }).then(() => { + removeHistoryItemFromState(id); + showNotification('Item deleted', 'success'); + }); + } else { + removeHistoryItemFromState(id); + showNotification('Item deleted', 'success'); + } + } + + state.currentAction = null; + state.currentItemId = null; + } + + function deleteSelectedItems() { + const selectedIds = Array.from(state.selectedItems); + + if (selectedIds.length === 0) { + showNotification('No items selected', 'error'); + return; + } + + const selectedUrls = state.history + .filter(item => state.selectedItems.has(item.id)) + .map(item => item.url); + + if (typeof chrome !== 'undefined' && chrome.history) { + let deletedCount = 0; + + selectedUrls.forEach(url => { + chrome.history.deleteUrl({ url }, () => { + deletedCount++; + if (deletedCount === selectedUrls.length) { + selectedIds.forEach(id => removeHistoryItemFromState(id)); + state.selectedItems.clear(); + updateActionButtonsState(); + + showNotification(`${selectedUrls.length} items deleted`, 'success'); + } + }); + }); + } else if (typeof browser !== 'undefined' && browser.history) { + Promise.all(selectedUrls.map(url => browser.history.deleteUrl({ url }))) + .then(() => { + selectedIds.forEach(id => removeHistoryItemFromState(id)); + state.selectedItems.clear(); + updateActionButtonsState(); + + showNotification(`${selectedUrls.length} items deleted`, 'success'); + }) + .catch(error => { + console.error('Error deleting history items:', error); + showNotification('Error deleting items', 'error'); + }); + } else { + selectedIds.forEach(id => removeHistoryItemFromState(id)); + state.selectedItems.clear(); + updateActionButtonsState(); + + showNotification(`${selectedUrls.length} items deleted`, 'success'); + } + } + + function clearAllHistory() { + if (state.filteredHistory.length === 0) { + showNotification('No history to clear', 'error'); + return; + } + + if (typeof chrome !== 'undefined' && chrome.history) { + chrome.history.deleteAll(() => { + state.history = []; + state.filteredHistory = []; + state.selectedItems.clear(); + + historyItems.innerHTML = ''; + updateActionButtonsState(); + showEmptyState('History cleared', 'Your browsing history has been cleared.'); + + showNotification('History cleared', 'success'); + }); + } else if (typeof browser !== 'undefined' && browser.history) { + browser.history.deleteAll().then(() => { + state.history = []; + state.filteredHistory = []; + state.selectedItems.clear(); + + historyItems.innerHTML = ''; + updateActionButtonsState(); + showEmptyState('History cleared', 'Your browsing history has been cleared.'); + + showNotification('History cleared', 'success'); + }); + } else { + state.history = []; + state.filteredHistory = []; + state.selectedItems.clear(); + + historyItems.innerHTML = ''; + updateActionButtonsState(); + showEmptyState('History cleared', 'Your browsing history has been cleared.'); + + showNotification('History cleared', 'success'); + } + } + + function showLoading() { + if (historyLoading) { + showElement(historyLoading); + } + + hideEmptyState(); + if (historyItems) { + historyItems.innerHTML = ''; + } + } + + function hideLoading() { + if (historyLoading) { + hideElement(historyLoading); + } + } + + function showEmptyState(title = 'No history found', message = 'There are no items in your browsing history that match your search.') { + if (!historyEmpty) return; + + const titleElement = historyEmpty.querySelector('h2'); + const messageElement = historyEmpty.querySelector('p'); + + if (titleElement) titleElement.textContent = title; + if (messageElement) messageElement.textContent = message; + + showElement(historyEmpty); + } + + function hideEmptyState() { + if (historyEmpty) { + hideElement(historyEmpty); + } + } + + function showElement(element) { + if (element) element.classList.remove('hidden'); + } + + function hideElement(element) { + if (element) element.classList.add('hidden'); + } + + function showConfirmationDialog() { + if (confirmationDialog) { + showElement(confirmationDialog); + } + } + + function hideConfirmationDialog() { + if (confirmationDialog) { + hideElement(confirmationDialog); + } + } + + function showNotification(message, type = 'success') { + if (window.notifications && typeof window.notifications.show === 'function') { + window.notifications.show(message, type); + } else { + console.log(`Notification: ${message} (${type})`); + } + } + + function openLink(url) { + window.open(url, '_blank'); + } + + function formatDateHeading(dateStr) { + const date = new Date(dateStr); + const today = new Date(); + const yesterday = new Date(today); + yesterday.setDate(yesterday.getDate() - 1); + + if (date.toDateString() === today.toDateString()) { + return 'Today'; + } else if (date.toDateString() === yesterday.toDateString()) { + return 'Yesterday'; + } else { + return date.toLocaleDateString(undefined, { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric' + }); + } + } + + function formatTime(date) { + return date.toLocaleTimeString(undefined, { hour: 'numeric', minute: '2-digit' }); + } + + function groupByDate(items) { + const grouped = {}; + + items.forEach(item => { + const date = new Date(item.lastVisitTime); + const dateKey = date.toDateString(); + + if (!grouped[dateKey]) { + grouped[dateKey] = []; + } + + grouped[dateKey].push(item); + }); + + for (const date in grouped) { + grouped[date].sort((a, b) => b.lastVisitTime - a.lastVisitTime); + } + + return grouped; + } + + function debounce(func, wait) { + let timeout; + return function executedFunction(...args) { + const later = () => { + clearTimeout(timeout); + func(...args); + }; + clearTimeout(timeout); + timeout = setTimeout(later, wait); + }; + } +}); \ No newline at end of file diff --git a/js/keybinds.js b/js/keybinds.js index 26e6a8f..e33d82e 100644 --- a/js/keybinds.js +++ b/js/keybinds.js @@ -11,13 +11,20 @@ const keybinds = { init() { this.bindings = Storage.get('keybinds') || {}; + this.setupDefaultKeybinds(); + const urlInput = document.getElementById('keybind-url'); const urlComboInput = document.getElementById('keybind-url-combo'); + const historyComboInput = document.getElementById('keybind-history-combo'); if (this.bindings.url) { urlInput.value = this.bindings.url.url || ''; urlComboInput.value = this.bindings.url.keys || ''; } + + if (this.bindings.history && historyComboInput) { + historyComboInput.value = this.bindings.history.keys || ''; + } let lastSavedUrl = urlInput.value; @@ -184,7 +191,7 @@ const keybinds = { }); document.addEventListener('keydown', (e) => { - if (e.target.tagName === 'INPUT') return; + if (e.target.tagName === 'INPUT' || !Storage.get('onboardingComplete')) return; const keys = []; if (e.altKey) keys.push('Alt'); @@ -201,10 +208,44 @@ const keybinds = { }); }); }, + + setupDefaultKeybinds() { + const defaultBindings = { + settings: { keys: 'Shift+S' }, + anonymous: { keys: 'Shift+X' }, + theme: { keys: 'Shift+T' }, + history: { keys: 'Shift+H' }, + url: { keys: 'Shift+Q', url: '' } + }; + + Object.entries(defaultBindings).forEach(([action, binding]) => { + if (!this.bindings[action]) { + this.bindings[action] = binding; + } + }); + + Storage.set('keybinds', this.bindings); + }, executeAction(action, binding) { + if (!Storage.get('onboardingComplete')) return; + const settingsPage = document.getElementById('settings-page'); - if (settingsPage.classList.contains('active')) { + const passwordDialog = document.getElementById('password-dialog'); + + if (passwordDialog && !passwordDialog.classList.contains('hidden')) { + const cancelBtn = document.getElementById('cancel-password'); + if (cancelBtn) { + cancelBtn.click(); + } else { + passwordDialog.classList.remove('active'); + setTimeout(() => { + passwordDialog.classList.add('hidden'); + }, 300); + } + } + + if (action === 'settings' && settingsPage.classList.contains('active')) { settingsPage.classList.remove('active'); setTimeout(() => { settingsPage.classList.add('hidden'); @@ -216,50 +257,55 @@ const keybinds = { 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'); + closeModal(shortcutModal); } else { - notifications.show('Opening add shortcut menu.', 'info'); openModal(shortcutModal); } break; case 'anonymous': - settings.toggleAnonymousMode(); + const isAnonymous = Storage.get('anonymousMode') || false; + Storage.set('anonymousMode', !isAnonymous); + + if (!isAnonymous) { + const randomName = anonymousNames.generate(); + Storage.set('anonymousName', randomName); + } else { + Storage.remove('anonymousName'); + } + + shortcuts.render(); + updateGreeting(); break; case 'theme': settings.toggleTheme(); break; + case 'history': + window.location.href = 'history.html'; + 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'); + + window.location.href = fullUrl; } break; } diff --git a/js/onboarding.js b/js/onboarding.js index 8854d8d..955da6b 100644 --- a/js/onboarding.js +++ b/js/onboarding.js @@ -293,6 +293,16 @@ const onboarding = { Storage.set(key, value); }); + if (!Storage.get('keybinds')) { + Storage.set('keybinds', { + settings: { keys: 'Shift+S' }, + anonymous: { keys: 'Shift+X' }, + theme: { keys: 'Shift+T' }, + history: { keys: 'Shift+H' }, + url: { keys: 'Shift+Q', url: '' } + }); + } + Storage.set('onboardingComplete', true); setTimeout(() => { diff --git a/js/settings.js b/js/settings.js index 30f2498..2858ff8 100644 --- a/js/settings.js +++ b/js/settings.js @@ -4,6 +4,7 @@ const defaultSettings = { anonymousMode: false, searchEngine: 'Google', updateAlerts: true, + motionPreference: 'default', show_greeting: true, show_search: true, show_shortcuts: true, @@ -43,6 +44,13 @@ const defaultSettings = { '--toggle-bg': '#333333', '--toggle-bg-active': 'var(--text)', '--toggle-knob': 'var(--background)' + }, + keybinds: { + settings: { keys: 'Shift+S' }, + anonymous: { keys: 'Shift+X' }, + theme: { keys: 'Shift+T' }, + history: { keys: 'Shift+H' }, + url: { keys: 'Shift+Q', url: '' } } }; @@ -81,20 +89,93 @@ document.addEventListener('DOMContentLoaded', () => { updateAlerts !== false; }); +const updateIconStyle = (style) => { + const icons = document.querySelectorAll('svg use'); + icons.forEach(icon => { + const iconId = icon.getAttribute('href').substring(1); + let newIconId; + + if (style === 'solid') { + if (iconId.endsWith('-solid')) { + newIconId = iconId; + } else { + newIconId = `${iconId}-solid`; + } + } else { + newIconId = iconId.endsWith('-solid') ? iconId.slice(0, -6) : iconId; + } + + icon.setAttribute('href', `#${newIconId}`); + }); +}; + +const savedIconStyle = Storage.get('iconStyle') || 'linear'; +document.getElementById('icon-style-select').value = savedIconStyle; +updateIconStyle(savedIconStyle); + +document.getElementById('icon-style-select').addEventListener('change', (event) => { + const selectedStyle = event.target.value; + Storage.set('iconStyle', selectedStyle); + updateIconStyle(selectedStyle); +}); + +const savedMotionPreference = Storage.get('motionPreference') || 'default'; +document.getElementById('motion-preference-select').value = savedMotionPreference; +updateMotionPreference(savedMotionPreference); + +document.getElementById('motion-preference-select').addEventListener('change', (event) => { + const selectedPreference = event.target.value; + Storage.set('motionPreference', selectedPreference); + updateMotionPreference(selectedPreference); + notifications.show(`Motion preference updated to ${selectedPreference}!`, 'success'); +}); + +function updateMotionPreference(preference) { + document.body.classList.remove('subtle-motion', 'reduced-motion', 'minimal-motion', 'no-motion'); + + switch(preference) { + case 'subtle': + document.body.classList.add('subtle-motion'); + break; + case 'reduced': + document.body.classList.add('reduced-motion'); + break; + case 'minimal': + document.body.classList.add('minimal-motion'); + break; + case 'disabled': + document.body.classList.add('no-motion'); + break; + } +} + const settings = { GREETING_MAX_LENGTH: 60, toggleTheme: () => { const currentTheme = document.body.getAttribute('data-theme'); const newTheme = currentTheme === 'dark' ? 'light' : 'dark'; + + const motionPreference = Storage.get('motionPreference') || 'default'; + if (motionPreference !== 'default') { + document.body.classList.add('instant-theme-change'); + } + document.body.setAttribute('data-theme', newTheme); Storage.set('theme', newTheme); + const iconStyle = Storage.get('iconStyle') || 'linear'; const themeIcon = document.querySelector('#toggle-theme svg use'); - const lightModeIcon = newTheme === 'dark' ? 'dark-mode' : 'light-mode'; - const darkModeIcon = newTheme === 'dark' ? 'light-mode' : 'dark-mode'; + const lightModeIcon = iconStyle === 'solid' ? 'icon-light-mode-solid' : 'icon-light-mode'; + const darkModeIcon = iconStyle === 'solid' ? 'icon-dark-mode-solid' : 'icon-dark-mode'; - themeIcon.setAttribute('href', `#icon-${newTheme === 'dark' ? darkModeIcon : lightModeIcon}`); + themeIcon.setAttribute('href', `#${newTheme === 'dark' ? lightModeIcon : darkModeIcon}`); + + if (motionPreference !== 'default') { + setTimeout(() => { + document.body.classList.remove('instant-theme-change'); + }, 50); + } }, @@ -304,9 +385,29 @@ const settings = { 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')); + resetFontSize.addEventListener('click', () => { + shortcuts.showConfirmDialog( + 'Reset Typography', + 'Are you sure you want to reset font settings to default?', + settings.resetTypography + ); + }); + + resetLightColors.addEventListener('click', () => { + shortcuts.showConfirmDialog( + 'Reset Light Colors', + 'Are you sure you want to reset light mode colors to default?', + () => settings.resetColors('light') + ); + }); + + resetDarkColors.addEventListener('click', () => { + shortcuts.showConfirmDialog( + 'Reset Dark Colors', + 'Are you sure you want to reset dark mode colors to default?', + () => settings.resetColors('dark') + ); + }); settings.initColorSettings(); @@ -316,9 +417,10 @@ const settings = { const savedTheme = Storage.get('theme') || 'light'; document.body.setAttribute('data-theme', savedTheme); + const iconStyle = Storage.get('iconStyle') || 'linear'; const themeIcon = document.querySelector('#toggle-theme svg use'); - const lightModeIcon = 'light-mode'; - const darkModeIcon = 'dark-mode'; + const lightModeIcon = iconStyle === 'solid' ? 'light-mode-solid' : 'light-mode'; + const darkModeIcon = iconStyle === 'solid' ? 'dark-mode-solid' : 'dark-mode'; themeIcon.setAttribute('href', `#icon-${savedTheme === 'dark' ? darkModeIcon : lightModeIcon}`); @@ -369,11 +471,13 @@ const settings = { const userName = Storage.get('userName') || ''; const isAnonymous = Storage.get('anonymousMode') || false; const currentEngine = Storage.get('searchEngine') || 'google'; + const masterPassword = Storage.get('masterPassword') || ''; 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; + document.getElementById('master-password').value = masterPassword; ['greeting', 'search', 'shortcuts', 'addShortcut'].forEach(element => { const isVisible = Storage.get(`show_${element}`); @@ -392,6 +496,7 @@ const settings = { document.getElementById('font-family-select').value = fontFamily; document.getElementById('font-size-slider').value = fontSize; document.getElementById('font-size-number').value = fontSize; + document.getElementById('motion-preference-select').value = Storage.get('motionPreference') || 'default'; }, formatGreeting: (format) => { @@ -450,6 +555,11 @@ const settings = { exportData: () => { try { + let masterPassword = Storage.get('masterPassword') || ''; + if (masterPassword) { + masterPassword = btoa(masterPassword); + } + const data = { settings: { theme: Storage.get('theme') || defaultSettings.theme, @@ -459,6 +569,11 @@ const settings = { searchEngine: Storage.get('searchEngine') || 'Google', customGreeting: Storage.get('customGreeting') || '', updateAlerts: Storage.get('updateAlerts') !== false, + iconStyle: Storage.get('iconStyle') || 'linear', + motionPreference: Storage.get('motionPreference') || 'default', + + passwordProtectionEnabled: Storage.get('passwordProtectionEnabled') || false, + masterPassword: masterPassword, fontFamily: Storage.get('fontFamily') || defaultSettings.fontFamily, fontSize: Storage.get('fontSize') || defaultSettings.fontSize, @@ -649,6 +764,14 @@ const settings = { } }); + if (data.settings.passwordProtectionEnabled) { + setTimeout(() => { + if (typeof shortcuts.createShortcutProtectionManager === 'function') { + shortcuts.createShortcutProtectionManager(); + } + }, 100); + } + Storage.set('shortcuts', data.shortcuts); const validatedKeybinds = {}; @@ -678,6 +801,8 @@ const settings = { settings.initColorSettings(); } + updateIconStyle(data.settings.iconStyle || 'linear'); + if (data.settings.fontFamily) { document.documentElement.style.setProperty('--font-family', data.settings.fontFamily); } @@ -701,6 +826,18 @@ const settings = { Storage.remove('customBackground'); } + let masterPassword = data.settings.masterPassword || ''; + if (masterPassword) { + try { + masterPassword = atob(masterPassword); + Storage.set('masterPassword', masterPassword); + settings.updateSettingsUI(); + } catch (e) { + console.error('Error decoding master password:', e); + notifications.show('Failed to decode master password!', 'error'); + } + } + notifications.show('Data imported successfully!', 'success'); } catch (error) { notifications.show('Failed to import data: Invalid file format!', 'error'); @@ -709,50 +846,27 @@ const settings = { }, 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) { + shortcuts.showConfirmDialog( + 'Reset All Data', + 'Are you sure you want to reset all data? This action cannot be undone.', + () => { + try { + Storage.remove('customGreeting'); + Storage.clear(); + + Storage.set('keybinds', defaultSettings.keybinds); + + closeModal(document.getElementById('settings-modal')); + notifications.show('All data has been reset!', 'success'); setTimeout(() => { - GridLayout.init(); - }, 100); + window.location.reload(); + }, 1000); + } catch (error) { + notifications.show('Failed to reset data!', 'error'); + console.error('Reset error:', error); } - } 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: () => { @@ -911,22 +1025,27 @@ settings.initDataManagement = () => { }); 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); + shortcuts.showConfirmDialog( + 'Reset All Data', + 'Are you sure you want to reset all data? This action cannot be undone.', + () => { + try { + Storage.remove('customGreeting'); + Storage.clear(); + + Storage.set('keybinds', defaultSettings.keybinds); + + 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); + } } - } + ); }); }; @@ -942,6 +1061,8 @@ function initCustomSelects() { if (nativeSelect.id === 'search-engine-select') { nativeSelect.value = Storage.get('searchEngine') || 'google'; + } else if (nativeSelect.id === 'icon-style-select') { + nativeSelect.value = Storage.get('iconStyle') || 'linear'; } else if (nativeSelect.id === 'font-family-select') { nativeSelect.value = Storage.get('fontFamily') || 'Inter'; selectedDiv.style.fontFamily = nativeSelect.value; diff --git a/js/shortcuts.js b/js/shortcuts.js index 995540e..e9d4031 100644 --- a/js/shortcuts.js +++ b/js/shortcuts.js @@ -14,7 +14,7 @@ const shortcuts = { } }, - add: (url, name) => { + add: (url, name, isPasswordProtected = false) => { const currentShortcuts = Storage.get('shortcuts') || []; if (currentShortcuts.length >= shortcuts.MAX_SHORTCUTS) { notifications.show('Maximum shortcuts limit reached!', 'error'); @@ -27,7 +27,11 @@ const shortcuts = { return; } - currentShortcuts.push({ url: formattedUrl, name }); + currentShortcuts.push({ + url: formattedUrl, + name, + isPasswordProtected: isPasswordProtected || false + }); Storage.set('shortcuts', currentShortcuts); shortcuts.render(); CacheUpdater.update(); @@ -42,9 +46,131 @@ const shortcuts = { CacheUpdater.update(); }, - edit: (index, newUrl, newName) => { + showConfirmDialog: (title, message, onConfirm) => { + const dialog = document.getElementById('confirmation-dialog'); + const titleEl = document.getElementById('confirmation-title'); + const messageEl = document.getElementById('confirmation-message'); + const confirmBtn = document.getElementById('confirm-action'); + const cancelBtn = document.getElementById('cancel-action'); + + titleEl.textContent = title; + messageEl.textContent = message; + + dialog.classList.remove('hidden'); + setTimeout(() => dialog.classList.add('active'), 10); + + const closeDialog = () => { + dialog.classList.remove('active'); + setTimeout(() => dialog.classList.add('hidden'), 300); + }; + + const handleConfirm = () => { + onConfirm(); + closeDialog(); + confirmBtn.removeEventListener('click', handleConfirm); + cancelBtn.removeEventListener('click', handleCancel); + }; + + const handleCancel = () => { + closeDialog(); + confirmBtn.removeEventListener('click', handleConfirm); + cancelBtn.removeEventListener('click', handleCancel); + }; + + confirmBtn.addEventListener('click', handleConfirm); + cancelBtn.addEventListener('click', handleCancel); + }, + + showPasswordDialog: (shortcut, callback) => { + const dialog = document.getElementById('password-dialog'); + const passwordInput = document.getElementById('shortcut-password'); + const submitBtn = document.getElementById('submit-password'); + const cancelBtn = document.getElementById('cancel-password'); + const closeBtn = document.getElementById('close-password-dialog'); + const errorMsg = document.getElementById('password-error'); + const contextMenu = document.getElementById('context-menu'); + + if (contextMenu) { + contextMenu.classList.add('hidden'); + } + + if (errorMsg) { + errorMsg.classList.add('hidden'); + } + + if (passwordInput) { + passwordInput.value = ''; + } + + if (dialog) { + dialog.classList.remove('hidden'); + setTimeout(() => { + dialog.classList.add('active'); + if (passwordInput) { + passwordInput.focus(); + } + }, 10); + } + + const closeDialog = () => { + dialog.classList.remove('active'); + setTimeout(() => dialog.classList.add('hidden'), 300); + }; + + const handleSubmit = () => { + const password = passwordInput.value; + const masterPassword = Storage.get('masterPassword'); + + if (!masterPassword) { + errorMsg.textContent = "No master password set. Please set one in settings."; + errorMsg.classList.remove('hidden'); + return; + } + + if (password === masterPassword) { + closeDialog(); + callback(); + submitBtn.removeEventListener('click', handleSubmit); + cancelBtn.removeEventListener('click', handleCancel); + closeBtn.removeEventListener('click', handleCancel); + passwordInput.removeEventListener('keydown', handleKeydown); + } else { + errorMsg.textContent = "Incorrect password. Please try again."; + errorMsg.classList.remove('hidden'); + passwordInput.value = ''; + passwordInput.focus(); + } + }; + + const handleCancel = () => { + closeDialog(); + submitBtn.removeEventListener('click', handleSubmit); + cancelBtn.removeEventListener('click', handleCancel); + closeBtn.removeEventListener('click', handleCancel); + passwordInput.removeEventListener('keydown', handleKeydown); + }; + + const handleKeydown = (e) => { + if (e.key === 'Enter') { + handleSubmit(); + } else if (e.key === 'Escape') { + handleCancel(); + } + }; + + submitBtn.addEventListener('click', handleSubmit); + cancelBtn.addEventListener('click', handleCancel); + closeBtn.addEventListener('click', handleCancel); + passwordInput.addEventListener('keydown', handleKeydown); + }, + + edit: (index, newUrl, newName, isPasswordProtected) => { const currentShortcuts = Storage.get('shortcuts') || []; - currentShortcuts[index] = { url: newUrl, name: newName }; + currentShortcuts[index] = { + url: newUrl, + name: newName, + isPasswordProtected: isPasswordProtected || false + }; Storage.set('shortcuts', currentShortcuts); shortcuts.render(); notifications.show('Shortcut updated!', 'success'); @@ -82,7 +208,7 @@ const shortcuts = { currentShortcuts.forEach((shortcut, index) => { const element = document.createElement('div'); - element.className = `shortcut ${isAnonymous ? 'blurred' : ''}`; + element.className = `shortcut ${isAnonymous ? 'blurred' : ''} ${shortcut.isPasswordProtected ? 'password-protected' : ''}`; element.dataset.index = index; @@ -99,10 +225,42 @@ const shortcuts = { element.addEventListener('click', (e) => { if (!grid.classList.contains('grid-draggable') || !e.target.closest('.shortcut').classList.contains('drag-active')) { - if (e.ctrlKey) { + if (shortcut.isPasswordProtected) { + e.preventDefault(); + e.stopPropagation(); + + const openShortcut = () => { + if (e.ctrlKey || e.which === 2 || e.button === 1) { + window.open(shortcut.url, '_blank'); + } else { + window.location.href = shortcut.url; + } + }; + + shortcuts.showPasswordDialog(shortcut, openShortcut); + return false; + } else { + if (e.ctrlKey || e.which === 2 || e.button === 1) { window.open(shortcut.url, '_blank'); } else { window.location.href = shortcut.url; + } + } + } + }); + + element.addEventListener('mousedown', (e) => { + if (e.button === 1) { + e.preventDefault(); + + if (shortcut.isPasswordProtected) { + const openShortcut = () => { + window.open(shortcut.url, '_blank'); + }; + + shortcuts.showPasswordDialog(shortcut, openShortcut); + } else { + window.open(shortcut.url, '_blank'); } } }); @@ -133,6 +291,14 @@ const shortcuts = { }, init: () => { + const masterPasswordInput = document.getElementById('master-password'); + if (masterPasswordInput) { + const savedPassword = Storage.get('masterPassword'); + if (savedPassword) { + masterPasswordInput.value = savedPassword; + } + } + const addShortcutButton = document.getElementById('add-shortcut'); const modal = document.getElementById('add-shortcut-modal'); const closeBtn = modal.querySelector('.close-modal'); @@ -174,12 +340,18 @@ const shortcuts = { if (url && name) { try { new URL(url); - shortcuts.add(url, name); + + const isPasswordProtectionEnabled = Storage.get('passwordProtectionEnabled') || false; + const passwordProtectCheckbox = document.getElementById('protect-shortcut'); + const isPasswordProtected = isPasswordProtectionEnabled && passwordProtectCheckbox && passwordProtectCheckbox.checked; + + shortcuts.add(url, name, isPasswordProtected); modal.classList.remove('active'); setTimeout(() => { modal.classList.add('hidden'); urlInput.value = ''; nameInput.value = ''; + if (passwordProtectCheckbox) passwordProtectCheckbox.checked = false; }, 300); notifications.show('Shortcut added successfully!', 'success'); } catch (e) { @@ -197,6 +369,8 @@ const shortcuts = { modal.classList.add('hidden'); urlInput.value = ''; nameInput.value = ''; + const passwordProtectCheckbox = document.getElementById('protect-shortcut'); + if (passwordProtectCheckbox) passwordProtectCheckbox.checked = false; }, 300); }; } @@ -204,6 +378,121 @@ const shortcuts = { }); } + const anonymousTogglePrivacy = document.getElementById('toggle-anonymous-privacy'); + if (anonymousTogglePrivacy) { + anonymousTogglePrivacy.checked = Storage.get('anonymousMode') || false; + + anonymousTogglePrivacy.addEventListener('change', () => { + const anonymousToggle = document.getElementById('toggle-anonymous'); + if (anonymousToggle) { + anonymousToggle.checked = anonymousTogglePrivacy.checked; + anonymousToggle.dispatchEvent(new Event('change')); + } else { + shortcuts.toggleAnonymousMode(); + } + }); + } + + const shortcutsPasswordToggle = document.getElementById('toggle-shortcuts-password'); + if (shortcutsPasswordToggle) { + const isEnabled = Storage.get('passwordProtectionEnabled') || false; + shortcutsPasswordToggle.checked = isEnabled; + const passwordSettings = document.getElementById('password-protection-settings'); + + if (passwordSettings) { + if (isEnabled) { + passwordSettings.classList.remove('hidden'); + } else { + passwordSettings.classList.add('hidden'); + } + } + + shortcutsPasswordToggle.addEventListener('change', () => { + const isEnabled = shortcutsPasswordToggle.checked; + Storage.set('passwordProtectionEnabled', isEnabled); + + if (passwordSettings) { + if (isEnabled) { + passwordSettings.classList.remove('hidden'); + } else { + passwordSettings.classList.add('hidden'); + } + } + + shortcuts.updateAddShortcutModal(); + + if (isEnabled) { + const masterPassword = Storage.get('masterPassword'); + + if (!masterPassword) { + const masterPasswordInput = document.getElementById('master-password'); + if (masterPasswordInput) { + masterPasswordInput.focus(); + notifications.show('Please set a master password!', 'warning'); + } + } else { + notifications.show('Password protection enabled!', 'success'); + + setTimeout(() => { + shortcuts.createShortcutProtectionManager(); + }, 10); + } + } else { + const currentShortcuts = Storage.get('shortcuts') || []; + currentShortcuts.forEach(shortcut => { + shortcut.isPasswordProtected = false; + }); + Storage.set('shortcuts', currentShortcuts); + shortcuts.render(); + + notifications.show('Password protection disabled!', 'info'); + } + }); + } + + if (Storage.get('passwordProtectionEnabled')) { + shortcuts.createShortcutProtectionManager(); + } + + const saveMasterPasswordBtn = document.getElementById('save-master-password'); + if (saveMasterPasswordBtn) { + saveMasterPasswordBtn.addEventListener('click', () => { + const masterPasswordInput = document.getElementById('master-password'); + if (masterPasswordInput) { + const password = masterPasswordInput.value.trim(); + + if (password) { + Storage.set('masterPassword', password); + + notifications.show('Master password updated!', 'success'); + + const shortcutsPasswordToggle = document.getElementById('toggle-shortcuts-password'); + if (shortcutsPasswordToggle && !shortcutsPasswordToggle.checked) { + shortcutsPasswordToggle.checked = true; + Storage.set('passwordProtectionEnabled', true); + const passwordSettings = document.getElementById('password-protection-settings'); + if (passwordSettings) { + passwordSettings.classList.remove('hidden'); + } + shortcuts.updateAddShortcutModal(); + + setTimeout(() => { + shortcuts.createShortcutProtectionManager(); + }, 10); + } else { + setTimeout(() => { + shortcuts.createShortcutProtectionManager(); + }, 10); + } + } else { + notifications.show('Please enter a valid password!', 'error'); + } + } + }); + } + + shortcuts.updateAddShortcutModal(); + const contextMenu = document.getElementById('context-menu'); if (contextMenu) { contextMenu.addEventListener('click', (e) => { @@ -222,6 +511,11 @@ const shortcuts = { urlInput.value = shortcut.url; nameInput.value = shortcut.name; + const protectCheckbox = document.getElementById('protect-shortcut-edit'); + if (protectCheckbox) { + protectCheckbox.checked = shortcut.isPasswordProtected || false; + } + modal.classList.remove('hidden'); modal.classList.add('active'); @@ -243,8 +537,14 @@ const shortcuts = { if (newUrl && newName) { const formattedUrl = shortcuts.validateAndFormatUrl(newUrl); if (formattedUrl) { - shortcuts.edit(index, formattedUrl, newName); + const isPasswordProtectionEnabled = Storage.get('passwordProtectionEnabled') || false; + const isPasswordProtected = isPasswordProtectionEnabled && + protectCheckbox && protectCheckbox.checked; + + shortcuts.edit(index, formattedUrl, newName, isPasswordProtected); closeModal(); + + shortcuts.createShortcutProtectionManager(); } else { notifications.show('Invalid URL format!', 'error'); } @@ -256,13 +556,29 @@ const shortcuts = { cancelButton.onclick = closeModal; } } else if (action === 'delete') { + const currentShortcuts = Storage.get('shortcuts') || []; + const shortcut = currentShortcuts[index]; + + shortcuts.showConfirmDialog( + 'Delete Shortcut', + `Are you sure you want to delete "${shortcut.name}"?`, + () => { shortcuts.remove(index); + shortcuts.createShortcutProtectionManager(); + } + ); } else if (action === 'open-new-tab') { const currentShortcuts = Storage.get('shortcuts') || []; const shortcut = currentShortcuts[index]; if (shortcut && shortcut.url) { + if (shortcut.isPasswordProtected) { + shortcuts.showPasswordDialog(shortcut, () => { + window.open(shortcut.url, '_blank'); + }); + } else { window.open(shortcut.url, '_blank'); + } } } @@ -271,5 +587,240 @@ const shortcuts = { } shortcuts.render(); + }, + + createShortcutProtectionManager: () => { + const passwordSettings = document.getElementById('password-protection-settings'); + if (!passwordSettings) return; + + let protectionManager = document.getElementById('shortcut-protection-manager'); + if (!protectionManager) { + protectionManager = document.createElement('div'); + protectionManager.id = 'shortcut-protection-manager'; + protectionManager.className = 'shortcut-protection-manager'; + + const managerTitle = document.createElement('h4'); + managerTitle.textContent = 'Protect Specific Shortcuts'; + + const managerDescription = document.createElement('p'); + managerDescription.className = 'setting-description'; + managerDescription.textContent = 'Select which shortcuts to password protect:'; + + protectionManager.appendChild(managerTitle); + protectionManager.appendChild(managerDescription); + + passwordSettings.appendChild(protectionManager); + } else { + const children = Array.from(protectionManager.children); + children.forEach((child, index) => { + if (index > 1) protectionManager.removeChild(child); + }); + } + + const currentShortcuts = Storage.get('shortcuts') || []; + + const selectedShortcutsContainer = document.createElement('div'); + selectedShortcutsContainer.className = 'selected-shortcuts-container'; + + const protectedShortcuts = currentShortcuts.filter(shortcut => shortcut.isPasswordProtected); + + if (protectedShortcuts.length > 0) { + protectedShortcuts.forEach((shortcut, index) => { + const shortcutChip = document.createElement('div'); + shortcutChip.className = 'shortcut-chip'; + shortcutChip.dataset.index = currentShortcuts.indexOf(shortcut); + + const shortcutIcon = document.createElement('img'); + shortcutIcon.src = `https://www.google.com/s2/favicons?domain=${shortcut.url}&sz=64`; + shortcutIcon.alt = shortcut.name; + + const shortcutName = document.createElement('span'); + shortcutName.textContent = shortcut.name; + + const removeButton = document.createElement('button'); + removeButton.className = 'remove-chip-btn'; + removeButton.innerHTML = '×'; + removeButton.title = 'Remove protection'; + + shortcutChip.appendChild(shortcutIcon); + shortcutChip.appendChild(shortcutName); + shortcutChip.appendChild(removeButton); + + removeButton.addEventListener('click', (e) => { + e.stopPropagation(); + currentShortcuts[shortcutChip.dataset.index].isPasswordProtected = false; + Storage.set('shortcuts', currentShortcuts); + + shortcuts.render(); + shortcuts.createShortcutProtectionManager(); + + notifications.show(`Removed protection from: ${shortcut.name}`, 'info'); + }); + + selectedShortcutsContainer.appendChild(shortcutChip); + }); + } else if (currentShortcuts.length > 0) { + const emptyState = document.createElement('p'); + emptyState.className = 'empty-protection-state'; + emptyState.textContent = 'No protected shortcuts yet.'; + selectedShortcutsContainer.appendChild(emptyState); + } + + protectionManager.appendChild(selectedShortcutsContainer); + + const selectorContainer = document.createElement('div'); + selectorContainer.className = 'shortcut-selector-container'; + + const unprotectedShortcuts = currentShortcuts.filter(shortcut => !shortcut.isPasswordProtected); + + if (unprotectedShortcuts.length > 0) { + const dropdown = document.createElement('div'); + dropdown.className = 'shortcut-dropdown'; + + const selected = document.createElement('div'); + selected.className = 'shortcut-dropdown-selected'; + selected.textContent = 'Select a shortcut to protect...'; + + const dropdownItems = document.createElement('div'); + dropdownItems.className = 'shortcut-dropdown-items'; + dropdownItems.classList.add('hidden'); + + unprotectedShortcuts.forEach(shortcut => { + const item = document.createElement('div'); + item.className = 'shortcut-dropdown-item'; + item.dataset.index = currentShortcuts.indexOf(shortcut); + + const icon = document.createElement('img'); + icon.src = `https://www.google.com/s2/favicons?domain=${shortcut.url}&sz=64`; + icon.alt = shortcut.name; + icon.style.width = '16px'; + icon.style.height = '16px'; + + const name = document.createElement('span'); + name.textContent = shortcut.name; + + item.appendChild(icon); + item.appendChild(name); + + item.addEventListener('click', () => { + currentShortcuts[item.dataset.index].isPasswordProtected = true; + Storage.set('shortcuts', currentShortcuts); + + shortcuts.render(); + shortcuts.createShortcutProtectionManager(); + + dropdownItems.classList.remove('active'); + selected.classList.remove('active'); + dropdownItems.classList.add('hidden'); + + notifications.show(`Protected shortcut: ${shortcut.name}`, 'success'); + }); + + dropdownItems.appendChild(item); + }); + + selected.addEventListener('click', (e) => { + e.stopPropagation(); + dropdownItems.classList.toggle('hidden'); + dropdownItems.classList.toggle('active'); + selected.classList.toggle('active'); + }); + + document.addEventListener('click', (e) => { + if (!dropdown.contains(e.target)) { + dropdownItems.classList.add('hidden'); + dropdownItems.classList.remove('active'); + selected.classList.remove('active'); + } + }); + + dropdown.appendChild(selected); + dropdown.appendChild(dropdownItems); + selectorContainer.appendChild(dropdown); + } else if (currentShortcuts.length === 0) { + const noShortcutsMessage = document.createElement('p'); + noShortcutsMessage.className = 'no-shortcuts-message'; + noShortcutsMessage.textContent = 'Add shortcuts to protect them with a password.'; + selectorContainer.appendChild(noShortcutsMessage); + } else { + const allProtectedMessage = document.createElement('p'); + allProtectedMessage.className = 'empty-protection-state'; + allProtectedMessage.textContent = 'All shortcuts are password protected.'; + selectorContainer.appendChild(allProtectedMessage); + } + + protectionManager.appendChild(selectorContainer); + }, + + updateAddShortcutModal: () => { + const modal = document.getElementById('add-shortcut-modal'); + if (!modal) return; + + const modalContent = modal.querySelector('.modal-content'); + if (!modalContent) return; + + const existingCheckbox = document.getElementById('protect-shortcut-container'); + if (existingCheckbox) return; + + const isPasswordProtectionEnabled = Storage.get('passwordProtectionEnabled') || false; + if (!isPasswordProtectionEnabled) return; + + const checkboxContainer = document.createElement('div'); + checkboxContainer.id = 'protect-shortcut-container'; + checkboxContainer.className = 'checkbox-container'; + checkboxContainer.innerHTML = ` + + `; + + const saveButton = modal.querySelector('#save-shortcut'); + if (saveButton) { + modalContent.insertBefore(checkboxContainer, saveButton); + } else { + modalContent.appendChild(checkboxContainer); + } + + const editModal = document.getElementById('edit-shortcut-modal'); + if (editModal) { + const existingEditCheckbox = document.getElementById('protect-shortcut-edit-container'); + if (!existingEditCheckbox) { + const editModalContent = editModal.querySelector('.modal-content'); + const editCheckboxContainer = document.createElement('div'); + editCheckboxContainer.id = 'protect-shortcut-edit-container'; + editCheckboxContainer.className = 'checkbox-container'; + editCheckboxContainer.innerHTML = ` + + `; + + const modalActions = editModal.querySelector('.modal-actions'); + if (modalActions) { + editModalContent.insertBefore(editCheckboxContainer, modalActions); + } else { + editModalContent.appendChild(editCheckboxContainer); + } + } + } + }, + + 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(); } }; \ No newline at end of file diff --git a/manifest.json b/manifest.json index b714972..a6fe4ed 100644 --- a/manifest.json +++ b/manifest.json @@ -1,35 +1,36 @@ -{ - "manifest_version": 3, - "name": "JSTAR Tab", - "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" - }, - "permissions": [ - "storage", - "favicon" - ], - "icons": { - "16": "images/icon16.png", - "48": "images/icon48.png", - "128": "images/icon128.png" - }, - "action": { - "default_title": "New JSTAR Tab", - "default_icon": { - "16": "images/icon16.png", - "48": "images/icon48.png", - "128": "images/icon128.png" - } - }, - "author": "JSTAR", - "homepage_url": "https://github.com/DevJSTAR/JSTAR-Tab", - "web_accessible_resources": [{ - "resources": [ - "fonts/*", - "images/*" - ], - "matches": [""] - }] +{ + "manifest_version": 3, + "name": "JSTAR Tab", + "version": "3.2.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" + }, + "permissions": [ + "storage", + "favicon", + "history" + ], + "icons": { + "16": "images/icon16.png", + "48": "images/icon48.png", + "128": "images/icon128.png" + }, + "action": { + "default_title": "New JSTAR Tab", + "default_icon": { + "16": "images/icon16.png", + "48": "images/icon48.png", + "128": "images/icon128.png" + } + }, + "author": "JSTAR", + "homepage_url": "https://github.com/DevJSTAR/JSTAR-Tab", + "web_accessible_resources": [{ + "resources": [ + "fonts/*", + "images/*" + ], + "matches": [""] + }] } \ No newline at end of file