diff --git a/images/favicon.ico b/images/favicon.ico new file mode 100644 index 0000000..a7a5a02 Binary files /dev/null and b/images/favicon.ico differ diff --git a/images/icon128.png b/images/icon128.png new file mode 100644 index 0000000..dd2c7c8 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..84c54c3 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..f2b6545 Binary files /dev/null and b/images/icon48.png differ diff --git a/index.html b/index.html new file mode 100644 index 0000000..a388811 --- /dev/null +++ b/index.html @@ -0,0 +1,271 @@ + + + + + + New JSTAR Tab + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + \ No newline at end of file diff --git a/js/main.js b/js/main.js new file mode 100644 index 0000000..65f7023 --- /dev/null +++ b/js/main.js @@ -0,0 +1,98 @@ +// Update greeting based on time of day and user settings +function updateGreeting() { + const greeting = document.getElementById('greeting'); + if (!greeting) 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); +} + +// Set up event listeners for modal interactions +function initModalHandlers() { + const modals = document.querySelectorAll('.modal'); + + modals.forEach(modal => { + modal.addEventListener('click', (e) => { + if (e.target === modal) { + closeModal(modal); + } + }); + + const modalContent = modal.querySelector('.modal-content'); + if (modalContent) { + modalContent.addEventListener('click', (e) => { + e.stopPropagation(); + }); + } + }); +} + +// Open modal with animation +function openModal(modal) { + if (!modal) return; + modal.classList.remove('hidden'); + requestAnimationFrame(() => { + modal.classList.add('active'); + }); +} + +// Close modal with animation +function closeModal(modal) { + if (!modal) return; + modal.classList.remove('active'); + setTimeout(() => { + modal.classList.add('hidden'); + }, 300); +} + +// Initialize application +document.addEventListener('DOMContentLoaded', () => { + // Apply visibility settings + ['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'; + } + }); + + // Start onboarding or show main content + if (!Storage.get('onboardingComplete')) { + onboarding.start(); + } else { + document.getElementById('main-content').classList.remove('hidden'); + } + + // Initialize features + search.init(); + shortcuts.init(); + settings.init(); + initModalHandlers(); + + // Set up greeting + updateGreeting(); + setInterval(updateGreeting, 60000); + + // Settings button handler + const settingsButton = document.getElementById('settings-button'); + const settingsModal = document.getElementById('settings-modal'); + + settingsButton.addEventListener('click', () => { + openModal(settingsModal); + }); +}); \ No newline at end of file diff --git a/js/notifications.js b/js/notifications.js new file mode 100644 index 0000000..0a2b762 --- /dev/null +++ b/js/notifications.js @@ -0,0 +1,131 @@ +// 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 +const notifications = new NotificationSystem(); \ No newline at end of file diff --git a/js/onboarding.js b/js/onboarding.js new file mode 100644 index 0000000..c092e1c --- /dev/null +++ b/js/onboarding.js @@ -0,0 +1,73 @@ +// Onboarding module +const onboarding = { + // Check if onboarding is complete + isComplete: () => { + return Storage.get('onboardingComplete') === true; + }, + + // Start the onboarding process + start: () => { + const modal = document.getElementById('onboarding-modal'); + const mainContent = document.getElementById('main-content'); + + if (!onboarding.isComplete()) { + modal.classList.remove('hidden'); + modal.classList.add('active'); + mainContent.classList.add('hidden'); + + document.getElementById('next-step-btn').addEventListener('click', () => onboarding.nextStep(1)); + document.getElementById('complete-setup-btn').addEventListener('click', onboarding.complete); + + // Set up 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'); + }); + }); + } else { + modal.classList.add('hidden'); + mainContent.classList.remove('hidden'); + } + }, + + // Move to the next step in onboarding + nextStep: (currentStep) => { + const currentStepEl = document.querySelector(`[data-step="${currentStep}"]`); + const nextStepEl = document.querySelector(`[data-step="${currentStep + 1}"]`); + 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'); + }, + + // Complete the onboarding process + 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 diff --git a/js/search.js b/js/search.js new file mode 100644 index 0000000..7998479 --- /dev/null +++ b/js/search.js @@ -0,0 +1,47 @@ +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 diff --git a/js/settings.js b/js/settings.js new file mode 100644 index 0000000..2f79f4b --- /dev/null +++ b/js/settings.js @@ -0,0 +1,336 @@ +// 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}`; + } +}; + +// Main settings object +const settings = { + // Toggle between light and dark themes + 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'}`; + }, + + // Toggle anonymous mode + 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(); + }, + + // Update the search engine + 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'; + } + + 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(); + 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; + + 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(); + } +}; + +// 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'); + + // Export Data + exportBtn.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') + }, + shortcuts: Storage.get('shortcuts') || [] + }; + + 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); + } + }); + + // Import Data + 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) { + throw new Error('Invalid backup file format'); + } + + Object.entries(data.settings).forEach(([key, value]) => { + Storage.set(key, value); + }); + + 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(); + 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); + } + }; + + reader.onerror = () => { + notifications.show('Failed to read file', 'error'); + }; + + reader.readAsText(file); + }); + + // Reset Data + resetBtn.addEventListener('click', () => { + const confirmReset = confirm('Are you sure you want to reset all data? This action cannot be undone.'); + + if (confirmReset) { + try { + 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 diff --git a/js/shortcuts.js b/js/shortcuts.js new file mode 100644 index 0000000..91005b4 --- /dev/null +++ b/js/shortcuts.js @@ -0,0 +1,255 @@ +const shortcuts = { + MAX_SHORTCUTS: 12, + + // Validate and format URL + validateAndFormatUrl: (url) => { + if (!/^https?:\/\//i.test(url)) { + url = 'https://' + url; + } + + try { + new URL(url); + return url; + } catch (e) { + return false; + } + }, + + // Add new shortcut + add: (url, name) => { + const currentShortcuts = Storage.get('shortcuts') || []; + if (currentShortcuts.length >= shortcuts.MAX_SHORTCUTS) { + notifications.show('Maximum shortcuts limit (12) 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 shortcut + remove: (index) => { + const currentShortcuts = Storage.get('shortcuts') || []; + currentShortcuts.splice(index, 1); + Storage.set('shortcuts', currentShortcuts); + shortcuts.render(); + notifications.show('Shortcut removed!', 'success'); + }, + + // Edit existing shortcut + 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'); + }, + + // Show context menu for shortcut + 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 shortcuts grid + 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); + }); + }, + + // Initialize shortcuts functionality + init: () => { + const addShortcutButton = document.getElementById('add-shortcut'); + if (addShortcutButton) { + addShortcutButton.addEventListener('click', (e) => { + e.stopPropagation(); + const currentShortcuts = Storage.get('shortcuts') || []; + + if (currentShortcuts.length >= shortcuts.MAX_SHORTCUTS) { + notifications.show('Maximum shortcuts limit (12) reached!', 'error'); + return; + } + + const modal = document.getElementById('add-shortcut-modal'); + 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); + } + + contextMenu.classList.add('hidden'); + }); + } + + shortcuts.render(); + } +}; \ No newline at end of file diff --git a/js/storage.js b/js/storage.js new file mode 100644 index 0000000..2f451e8 --- /dev/null +++ b/js/storage.js @@ -0,0 +1,44 @@ +/** + * 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)); + } catch (e) { + return null; + } + }, + + // Store value as JSON string + set: (key, value) => { + try { + localStorage.setItem(key, JSON.stringify(value)); + return true; + } catch (e) { + return false; + } + }, + + // Delete specific key + remove: (key) => { + try { + localStorage.removeItem(key); + return true; + } catch (e) { + return false; + } + }, + + // Remove all stored data + clear: () => { + try { + localStorage.clear(); + return true; + } catch (e) { + return false; + } + } +}; \ No newline at end of file diff --git a/manifest.json b/manifest.json new file mode 100644 index 0000000..23c719f --- /dev/null +++ b/manifest.json @@ -0,0 +1,36 @@ +{ + "manifest_version": 3, + "name": "JSTAR Tab", + "version": "2.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", + "update_url": "https://github.com/DevJSTAR/JSTAR-Tab/releases/latest", + "web_accessible_resources": [{ + "resources": [ + "fonts/*", + "images/*" + ], + "matches": [""] + }] +} \ No newline at end of file diff --git a/style.css b/style.css new file mode 100644 index 0000000..b0c1935 --- /dev/null +++ b/style.css @@ -0,0 +1,699 @@ +/* Root variables for light theme */ +: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; +} + +/* Dark theme variables */ +[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(255, 255, 255, 0.1); + --scrollbar-thumb: #333333; + --scrollbar-track: #1a1a1a; + --modal-background: #1a1a1a; +} + +/* Dark theme button styles */ +[data-theme="dark"] .btn-primary { + background: var(--primary-hover); + color: var(--text); +} + +[data-theme="dark"] .btn-primary:hover { + background: #3a3a3a; +} + +/* Global styles */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; + font-family: 'Inter', -apple-system, sans-serif; +} + +body { + background: var(--background); + color: var(--text); + min-height: 100vh; +} + +.hidden { + display: none !important; +} + +/* Modal styles */ +.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); +} + +/* Modal animations */ +@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); + } +} + +/* Form element styles */ +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); +} + +/* Step styles */ +.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); +} + +/* Main content styles */ +.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: fadeIn 0.5s ease forwards; + height: 48px; + visibility: hidden; +} + +#greeting:not([style*="visibility"]) { + visibility: visible; +} + +.search-container { + width: 100%; + max-width: 640px; + height: 56px; + visibility: visible; +} + +/* Search engine options */ +.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; +} + +.search-engine-option:hover { + transform: translateY(-2px); + box-shadow: 0 4px 12px var(--shadow); +} + +.search-engine-option.selected { + background: var(--surface); + border: 2px solid var(--border); +} + +.search-engine-option img { + width: 32px; + height: 32px; + border-radius: 4px; +} + +.search-engine-option span { + font-size: 0.875rem; + color: var(--text); + text-align: center; +} + +/* Search bar styles */ +.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); +} + +.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 styles */ +.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; +} + +#add-shortcut:hover { + background: var(--primary-hover); +} + +#shortcuts-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(80px, 1fr)); + gap: 1rem; + margin-bottom: 1.5rem; +} + +.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); +} + +.shortcut:hover { + transform: translateY(-2px); + box-shadow: 0 4px 15px var(--shadow); +} + +.shortcut img { + width: 24px; + height: 24px; + border-radius: 6px; +} + +.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 styles */ +.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); +} + +.settings-button:hover { + transform: translateY(-2px); + box-shadow: 0 8px 32px var(--shadow); +} + +.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: 2rem; +} + +.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; +} + +.settings-section h3 { + margin-bottom: 1.5rem; + font-weight: 600; +} + +.setting-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.75rem 0; +} + +.setting-label { + font-weight: 500; +} + +.data-management-buttons { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.btn-danger { + background: #dc3545 !important; + color: white !important; +} + +.btn-danger:hover { + background: #c82333 !important; +} + +/* Toggle switch styles */ +.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(--border); + border-radius: 34px; +} + +.toggle-slider:before { + position: absolute; + content: ""; + height: 20px; + width: 20px; + left: 3px; + bottom: 3px; + background-color: var(--background); + border-radius: 50%; +} + +input:checked + .toggle-slider { + background-color: var(--text); +} + +input:checked + .toggle-slider:before { + transform: translateX(24px); +} + +/* Button styles */ +.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 styles */ +#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; +} + +.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 section styles */ +.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; +} + +/* Context menu styles */ +.context-menu { + position: fixed; + background: var(--surface); + border-radius: 8px; + padding: 0.5rem; + box-shadow: 0 2px 10px var(--shadow); + z-index: 1000; +} + +.context-menu.hidden { + display: none; +} + +.context-menu-item { + padding: 0.75rem 1rem; + cursor: pointer; + display: flex; + align-items: center; + gap: 0.5rem; + border-radius: 4px; +} + +.context-menu-item:hover { + background: var(--primary); +} + +.context-menu-item i { + width: 16px; +} + +/* Hidden element styles */ +.search-container.hidden, +#greeting.hidden, +#shortcuts-grid.hidden, +#add-shortcut.hidden { + visibility: hidden !important; + opacity: 0 !important; + position: absolute !important; + pointer-events: none !important; +} \ No newline at end of file