Add files via upload

This commit is contained in:
JSTAR 2024-11-12 18:45:24 +05:00 committed by GitHub
parent 5a8ed20b50
commit 49f25d3405
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 664 additions and 60 deletions

View File

@ -19,13 +19,26 @@
<i class="fas fa-hand-wave"></i> <i class="fas fa-hand-wave"></i>
</div> </div>
<h2>Welcome to JSTAR Tab! 👋</h2> <h2>Welcome to JSTAR Tab! 👋</h2>
<p>Let's personalize your experience</p> <p>Let's get you started</p>
<div class="import-options">
<button id="start-fresh-btn" class="btn-primary">Start Fresh</button>
<div class="or-divider">
<span>OR</span>
</div>
<button id="import-data-btn" class="btn-primary">
<i class="fas fa-upload"></i> Import Data
</button>
</div>
<input type="file" id="onboarding-import" accept=".json" style="display: none;">
</div>
<div class="step hidden" data-step="2">
<h2>What's your name?</h2>
<div class="input-group"> <div class="input-group">
<input type="text" id="user-name" placeholder="What's your name?"> <input type="text" id="user-name" placeholder="Enter your name">
</div> </div>
<button id="next-step-btn" class="btn-primary">Continue</button> <button id="next-step-btn" class="btn-primary">Continue</button>
</div> </div>
<div class="step hidden" data-step="2"> <div class="step hidden" data-step="3">
<h2>Choose Your Search Engine</h2> <h2>Choose Your Search Engine</h2>
<div class="search-engine-options"> <div class="search-engine-options">
<!-- Search engine options --> <!-- Search engine options -->
@ -148,7 +161,7 @@
<div class="setting-item"> <div class="setting-item">
<div class="setting-label">Custom Greeting</div> <div class="setting-label">Custom Greeting</div>
<input type="text" id="custom-greeting" placeholder="Enter custom greeting"> <input type="text" id="custom-greeting" placeholder="Enter custom greeting">
<span class="format-hint">{name}, {time}, {date}, {day}, {month}, {year}</span> <span class="format-hint">{name}, {greeting}, {time}, {date}, {day}, {month}, {year}</span>
</div> </div>
<div class="setting-item horizontal"> <div class="setting-item horizontal">
@ -176,6 +189,47 @@
</div> </div>
</div> </div>
<!-- Keyboard Shortcuts Settings -->
<div class="settings-section">
<h3>Keyboard Shortcuts</h3>
<div class="setting-item">
<div class="setting-label">Settings Menu</div>
<div class="keybind-container">
<input type="text" id="keybind-settings" placeholder="Press keys..." readonly>
<button class="clear-keybind" data-for="settings">×</button>
</div>
</div>
<div class="setting-item">
<div class="setting-label">Add Shortcut</div>
<div class="keybind-container">
<input type="text" id="keybind-add-shortcut" placeholder="Press keys..." readonly>
<button class="clear-keybind" data-for="add-shortcut">×</button>
</div>
</div>
<div class="setting-item">
<div class="setting-label">Toggle Anonymous Mode</div>
<div class="keybind-container">
<input type="text" id="keybind-anonymous" placeholder="Press keys..." readonly>
<button class="clear-keybind" data-for="anonymous">×</button>
</div>
</div>
<div class="setting-item">
<div class="setting-label">Toggle Theme</div>
<div class="keybind-container">
<input type="text" id="keybind-theme" placeholder="Press keys..." readonly>
<button class="clear-keybind" data-for="theme">×</button>
</div>
</div>
<div class="setting-item">
<div class="setting-label">Quick URL</div>
<div class="keybind-container">
<input type="text" id="keybind-url-combo" placeholder="Press keys..." readonly>
<button class="clear-keybind" data-for="url">×</button>
</div>
<input type="text" id="keybind-url" placeholder="Enter URL">
</div>
</div>
<!-- Data Management --> <!-- Data Management -->
<div class="settings-section"> <div class="settings-section">
<h3>Data Management</h3> <h3>Data Management</h3>
@ -197,12 +251,11 @@
<div class="settings-section"> <div class="settings-section">
<h3>About</h3> <h3>About</h3>
<div class="about-content"> <div class="about-content">
<p>JSTAR Tab v2.5.0</p> <p>JSTAR Tab v2.6.0</p>
<p>Author: JSTAR</p>
<p>Homepage: <a href="https://github.com/DevJSTAR/JSTAR-Tab" target="_blank">GitHub Repository</a></p> <p>Homepage: <a href="https://github.com/DevJSTAR/JSTAR-Tab" target="_blank">GitHub Repository</a></p>
<p>Latest Update: <a href="https://github.com/DevJSTAR/JSTAR-Tab/releases/latest" target="_blank">Check for updates</a></p> <p>Latest Update: <a href="https://github.com/DevJSTAR/JSTAR-Tab/releases/latest" target="_blank">Check for updates</a></p>
<p>License: <a href="https://github.com/DevJSTAR/JSTAR-Tab/blob/main/LICENSE" target="_blank">MIT License</a></p> <p>License: <a href="https://github.com/DevJSTAR/JSTAR-Tab/blob/main/LICENSE" target="_blank">MIT License</a></p>
<p class="made-with">Made with <i class="fas fa-heart"></i> by JSTAR</p> <p class="made-with">Made with <i class="fas fa-heart"></i> by <a href="https://linktr.ee/jstarsdev" target="_blank">JSTAR</a></p>
</div> </div>
</div> </div>
</div> </div>
@ -273,5 +326,6 @@
<script src="js/shortcuts.js"></script> <script src="js/shortcuts.js"></script>
<script src="js/settings.js"></script> <script src="js/settings.js"></script>
<script src="js/main.js"></script> <script src="js/main.js"></script>
<script src="js/keybinds.js"></script>
</body> </body>
</html> </html>

265
js/keybinds.js Normal file
View File

@ -0,0 +1,265 @@
// List of keys that cannot be used as keybinds
const FORBIDDEN_KEYS = [
'Tab', 'CapsLock', 'Meta', 'ContextMenu',
'F1', 'F2', 'F3', 'F4', 'F5', 'F6', 'F7', 'F8', 'F9', 'F10', 'F11', 'F12',
'Home', 'End', 'PageUp', 'PageDown', 'Insert', 'Delete', 'ScrollLock', 'Pause', 'NumLock'
];
const keybinds = {
bindings: {},
init() {
this.bindings = Storage.get('keybinds') || {};
// URL keybind handling
const urlInput = document.getElementById('keybind-url');
const urlComboInput = document.getElementById('keybind-url-combo');
if (this.bindings.url) {
urlInput.value = this.bindings.url.url || '';
urlComboInput.value = this.bindings.url.keys || '';
}
let lastSavedUrl = urlInput.value;
function isValidUrl(string) {
try {
const urlString = string.match(/^https?:\/\//) ? string : `https://${string}`;
new URL(urlString);
return true;
} catch (_) {
return false;
}
}
urlInput.addEventListener('input', () => {
if (!this.bindings.url) {
this.bindings.url = { url: '', keys: '' };
}
this.bindings.url.url = urlInput.value;
Storage.set('keybinds', this.bindings);
});
urlInput.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
e.preventDefault();
urlInput.blur();
}
});
urlInput.addEventListener('blur', () => {
const currentUrl = urlInput.value.trim();
if (currentUrl === lastSavedUrl) {
return;
}
if (currentUrl) {
if (isValidUrl(currentUrl)) {
lastSavedUrl = currentUrl;
notifications.show('URL saved.', 'success');
} else {
notifications.show('Please enter a valid URL.', 'error');
urlInput.value = lastSavedUrl;
this.bindings.url.url = lastSavedUrl;
Storage.set('keybinds', this.bindings);
}
}
});
// Keybind input handling
const keybindInputs = document.querySelectorAll('[id^="keybind-"]');
keybindInputs.forEach(input => {
if (input.id === 'keybind-url') {
const urlBinding = this.bindings['url'];
if (urlBinding && urlBinding.url) {
input.value = urlBinding.url;
}
input.addEventListener('input', () => {
if (this.bindings['url']) {
this.bindings['url'].url = input.value;
Storage.set('keybinds', this.bindings);
}
});
return;
}
const action = input.id.replace('keybind-url-combo', 'url').replace('keybind-', '');
if (this.bindings[action]) {
input.value = this.bindings[action].keys;
}
let currentKeys = new Set();
let isProcessingKeybind = false;
input.addEventListener('keydown', (e) => {
e.preventDefault();
if (e.key === 'Escape') {
input.blur();
return;
}
if (e.ctrlKey) {
notifications.show('CTRL key combinations are not allowed.', 'error');
isProcessingKeybind = true;
return;
}
if (FORBIDDEN_KEYS.includes(e.key)) {
notifications.show('This key cannot be used as a keybind.', 'error');
isProcessingKeybind = true;
return;
}
isProcessingKeybind = false;
if (e.key !== 'Alt' && e.key !== 'Shift') {
currentKeys.add(e.key);
}
if (e.altKey) currentKeys.add('Alt');
if (e.shiftKey) currentKeys.add('Shift');
input.value = Array.from(currentKeys).join('+');
});
input.addEventListener('keyup', (e) => {
if (isProcessingKeybind) {
currentKeys.clear();
return;
}
if (e.key === 'Alt' || e.key === 'Shift') {
if (currentKeys.size === 1) {
notifications.show('Add another key with Alt or Shift.', 'error');
}
currentKeys.clear();
input.value = this.bindings[action]?.keys || '';
return;
}
const combo = Array.from(currentKeys).join('+');
if (!combo) return;
const duplicate = Object.entries(this.bindings).find(([key, value]) =>
value.keys === combo && key !== action
);
if (duplicate) {
notifications.show('This keybind is already in use.', 'error');
currentKeys.clear();
input.value = this.bindings[action]?.keys || '';
return;
}
this.bindings[action] = {
keys: combo,
url: action === 'url' ? document.getElementById('keybind-url').value : null
};
Storage.set('keybinds', this.bindings);
notifications.show('Keybind saved.', 'success');
});
input.addEventListener('blur', () => {
currentKeys.clear();
input.value = this.bindings[action]?.keys || '';
});
});
// Clear keybind button handling
document.querySelectorAll('.clear-keybind').forEach(button => {
button.addEventListener('click', () => {
const action = button.dataset.for;
const input = document.getElementById(`keybind-${action}-combo`) ||
document.getElementById(`keybind-${action}`);
input.value = '';
if (action === 'url') {
document.getElementById('keybind-url').value = '';
}
delete this.bindings[action];
Storage.set('keybinds', this.bindings);
notifications.show('Keybind removed.', 'success');
});
});
// Global keybind listener
document.addEventListener('keydown', (e) => {
if (e.target.tagName === 'INPUT') return;
const keys = [];
if (e.altKey) keys.push('Alt');
if (e.shiftKey) keys.push('Shift');
if (e.key !== 'Alt' && e.key !== 'Shift') keys.push(e.key);
const combo = keys.join('+');
Object.entries(this.bindings).forEach(([action, binding]) => {
if (binding.keys === combo) {
e.preventDefault();
this.executeAction(action, binding);
}
});
});
},
// Execute the action associated with a keybind
executeAction(action, binding) {
const activeModal = document.querySelector('.modal.active');
if (activeModal) {
closeModal(activeModal);
}
switch (action) {
case 'settings':
const settingsModal = document.getElementById('settings-modal');
if (settingsModal === activeModal) {
notifications.show('Settings closed.', 'info');
settings.updateSettingsUI();
} else {
notifications.show('Opening settings...', 'info');
settings.updateSettingsUI();
openModal(settingsModal);
}
break;
case 'add-shortcut':
const currentShortcuts = Storage.get('shortcuts') || [];
if (currentShortcuts.length >= shortcuts.MAX_SHORTCUTS) {
notifications.show('Maximum shortcuts limit reached!', 'error');
return;
}
const shortcutModal = document.getElementById('add-shortcut-modal');
if (shortcutModal === activeModal) {
notifications.show('Add shortcut closed.', 'info');
} else {
notifications.show('Opening add shortcut...', 'info');
openModal(shortcutModal);
}
break;
case 'anonymous':
settings.toggleAnonymousMode();
break;
case 'theme':
settings.toggleTheme();
break;
case 'url':
if (binding.url) {
const url = binding.url;
const fullUrl = url.startsWith('http://') || url.startsWith('https://') ?
url : `https://${url}`;
notifications.show(`Redirecting to ${url}...`, 'info');
setTimeout(() => {
window.location.href = fullUrl;
}, 1000);
} else {
notifications.show('No URL set for this keybind.', 'error');
}
break;
}
}
};

View File

@ -1,3 +1,4 @@
// Greeting functionality
async function updateGreeting() { async function updateGreeting() {
const greeting = document.getElementById('greeting'); const greeting = document.getElementById('greeting');
if (!greeting) return; if (!greeting) return;
@ -15,7 +16,6 @@ async function updateGreeting() {
} }
} }
// Fall back to default greeting if custom format is not set or fails
const hour = new Date().getHours(); const hour = new Date().getHours();
const isAnonymous = Storage.get('anonymousMode') || false; const isAnonymous = Storage.get('anonymousMode') || false;
const userName = isAnonymous ? const userName = isAnonymous ?
@ -35,7 +35,7 @@ async function updateGreeting() {
}, 100); }, 100);
} }
// Set up event listeners for modal interactions // Modal handling
function initModalHandlers() { function initModalHandlers() {
const modals = document.querySelectorAll('.modal'); const modals = document.querySelectorAll('.modal');
@ -52,10 +52,18 @@ function initModalHandlers() {
e.stopPropagation(); e.stopPropagation();
}); });
} }
document.querySelectorAll('.modal .close-button').forEach(button => {
button.addEventListener('click', () => {
const modal = button.closest('.modal');
if (modal) {
closeModal(modal);
}
});
});
}); });
} }
// Open modal with animation
function openModal(modal) { function openModal(modal) {
if (!modal) return; if (!modal) return;
modal.classList.remove('hidden'); modal.classList.remove('hidden');
@ -64,7 +72,6 @@ function openModal(modal) {
}); });
} }
// Close modal with animation
function closeModal(modal) { function closeModal(modal) {
if (!modal) return; if (!modal) return;
modal.classList.remove('active'); modal.classList.remove('active');
@ -73,9 +80,8 @@ function closeModal(modal) {
}, 300); }, 300);
} }
// Initialize application // Application initialization
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
// Apply visibility settings
['greeting', 'search', 'shortcuts', 'addShortcut'].forEach(element => { ['greeting', 'search', 'shortcuts', 'addShortcut'].forEach(element => {
const isVisible = Storage.get(`show_${element}`); const isVisible = Storage.get(`show_${element}`);
if (isVisible === false) { if (isVisible === false) {
@ -84,28 +90,39 @@ document.addEventListener('DOMContentLoaded', () => {
} }
}); });
// Start onboarding or show main content
if (!Storage.get('onboardingComplete')) { if (!Storage.get('onboardingComplete')) {
onboarding.start(); onboarding.start();
} else { } else {
document.getElementById('main-content').classList.remove('hidden'); document.getElementById('main-content').classList.remove('hidden');
} }
// Initialize features
search.init(); search.init();
shortcuts.init(); shortcuts.init();
settings.init(); settings.init();
initModalHandlers(); initModalHandlers();
// Set up greeting
updateGreeting(); updateGreeting();
setInterval(updateGreeting, 60000); setInterval(updateGreeting, 60000);
// Settings button handler
const settingsButton = document.getElementById('settings-button'); const settingsButton = document.getElementById('settings-button');
const settingsModal = document.getElementById('settings-modal'); const settingsModal = document.getElementById('settings-modal');
settingsButton.addEventListener('click', () => { settingsButton.addEventListener('click', () => {
openModal(settingsModal); openModal(settingsModal);
}); });
keybinds.init();
});
// Global keydown event handler
document.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
const activeModal = document.querySelector('.modal.active');
if (activeModal && !activeModal.matches('#settings-modal')) {
const primaryButton = activeModal.querySelector('.btn-primary');
if (primaryButton) {
primaryButton.click();
}
}
}
}); });

View File

@ -1,24 +1,84 @@
// Onboarding module // Onboarding module
const onboarding = { const onboarding = {
// Check if onboarding is complete // Core functions
isComplete: () => { isComplete: () => {
return Storage.get('onboardingComplete') === true; return Storage.get('onboardingComplete') === true;
}, },
// Start the onboarding process
start: () => { start: () => {
const modal = document.getElementById('onboarding-modal'); const modal = document.getElementById('onboarding-modal');
const mainContent = document.getElementById('main-content'); const mainContent = document.getElementById('main-content');
const importDataBtn = document.getElementById('import-data-btn');
const startFreshBtn = document.getElementById('start-fresh-btn');
const fileInput = document.getElementById('onboarding-import');
if (!onboarding.isComplete()) { if (!onboarding.isComplete()) {
modal.classList.remove('hidden'); modal.classList.remove('hidden');
modal.classList.add('active'); modal.classList.add('active');
mainContent.classList.add('hidden'); mainContent.classList.add('hidden');
document.getElementById('next-step-btn').addEventListener('click', () => onboarding.nextStep(1)); startFreshBtn.addEventListener('click', () => {
document.getElementById('complete-setup-btn').addEventListener('click', onboarding.complete); document.querySelector('[data-step="1"]').classList.add('hidden');
document.querySelector('[data-step="2"]').classList.remove('hidden');
document.getElementById('next-step-btn').addEventListener('click', () => onboarding.nextStep(2));
});
// Set up search engine selection importDataBtn.addEventListener('click', () => fileInput.click());
// Data import handling
fileInput.addEventListener('change', async (e) => {
if (e.target.files.length > 0) {
try {
const file = e.target.files[0];
const text = await file.text();
const data = JSON.parse(text);
if (!data.settings || typeof data.settings !== 'object' ||
!Array.isArray(data.shortcuts)) {
throw new Error('Invalid data structure');
return;
}
Object.entries(data.settings).forEach(([key, value]) => {
Storage.set(key, value);
});
Storage.set('shortcuts', data.shortcuts);
if (data.keybinds) {
Storage.set('keybinds', data.keybinds);
}
// Initialize components
search.init();
shortcuts.init();
settings.init();
updateGreeting();
document.body.setAttribute('data-theme', data.settings.theme || 'light');
// Update UI visibility
['greeting', 'search', 'shortcuts', 'addShortcut'].forEach(element => {
const isVisible = data.settings[`show_${element}`];
const elementNode = document.getElementById(element === 'search' ? 'search-container' : element);
if (elementNode) {
elementNode.style.display = isVisible === false ? 'none' : 'block';
}
});
Storage.set('onboardingComplete', true);
modal.classList.add('hidden');
mainContent.classList.remove('hidden');
const userName = Storage.get('userName') || 'Guest';
notifications.show(`Welcome back, ${userName}! 👋`, 'success');
} catch (error) {
notifications.show('Failed to import data: Invalid file format!', 'error');
fileInput.value = '';
}
}
});
// Search engine selection
const engines = document.querySelectorAll('.search-engine-option'); const engines = document.querySelectorAll('.search-engine-option');
engines.forEach(engine => { engines.forEach(engine => {
engine.addEventListener('click', () => { engine.addEventListener('click', () => {
@ -26,31 +86,34 @@ const onboarding = {
engine.classList.add('selected'); engine.classList.add('selected');
}); });
}); });
document.getElementById('complete-setup-btn').addEventListener('click', onboarding.complete);
} else { } else {
modal.classList.add('hidden'); modal.classList.add('hidden');
mainContent.classList.remove('hidden'); mainContent.classList.remove('hidden');
} }
}, },
// Move to the next step in onboarding // Onboarding step navigation
nextStep: (currentStep) => { nextStep: (currentStep) => {
const currentStepEl = document.querySelector(`[data-step="${currentStep}"]`); const currentStepEl = document.querySelector(`[data-step="${currentStep}"]`);
const nextStepEl = document.querySelector(`[data-step="${currentStep + 1}"]`); const nextStepEl = document.querySelector(`[data-step="${currentStep + 1}"]`);
const name = document.getElementById('user-name').value.trim();
if (currentStep === 2) {
const name = document.getElementById('user-name').value.trim();
if (!name) { if (!name) {
notifications.show('Please enter your name!', 'error'); notifications.show('Please enter your name!', 'error');
return; return;
} }
Storage.set('userName', name); Storage.set('userName', name);
}
currentStepEl.classList.add('hidden'); currentStepEl.classList.add('hidden');
nextStepEl.classList.remove('hidden'); nextStepEl.classList.remove('hidden');
nextStepEl.classList.add('visible'); nextStepEl.classList.add('visible');
}, },
// Complete the onboarding process // Finalize onboarding
complete: () => { complete: () => {
const selectedEngine = document.querySelector('.search-engine-option.selected'); const selectedEngine = document.querySelector('.search-engine-option.selected');
if (!selectedEngine) { if (!selectedEngine) {

View File

@ -10,7 +10,6 @@ const anonymousNames = {
} }
}; };
// Define getTimeBasedGreeting function
const getTimeBasedGreeting = () => { const getTimeBasedGreeting = () => {
const hour = new Date().getHours(); const hour = new Date().getHours();
if (hour < 12) return 'Good Morning'; if (hour < 12) return 'Good Morning';
@ -21,7 +20,8 @@ const getTimeBasedGreeting = () => {
// Main settings object // Main settings object
const settings = { const settings = {
// Toggle between light and dark themes GREETING_MAX_LENGTH: 60,
toggleTheme: () => { toggleTheme: () => {
const currentTheme = document.body.getAttribute('data-theme'); const currentTheme = document.body.getAttribute('data-theme');
const newTheme = currentTheme === 'dark' ? 'light' : 'dark'; const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
@ -32,7 +32,6 @@ const settings = {
themeIcon.className = `fas fa-${newTheme === 'dark' ? 'sun' : 'moon'}`; themeIcon.className = `fas fa-${newTheme === 'dark' ? 'sun' : 'moon'}`;
}, },
// Toggle anonymous mode
toggleAnonymousMode: () => { toggleAnonymousMode: () => {
const isAnonymous = Storage.get('anonymousMode') || false; const isAnonymous = Storage.get('anonymousMode') || false;
Storage.set('anonymousMode', !isAnonymous); Storage.set('anonymousMode', !isAnonymous);
@ -50,7 +49,6 @@ const settings = {
updateGreeting(); updateGreeting();
}, },
// Update the search engine
updateSearchEngine: (engine) => { updateSearchEngine: (engine) => {
Storage.set('searchEngine', engine); Storage.set('searchEngine', engine);
notifications.show('Search engine updated successfully!', 'success'); notifications.show('Search engine updated successfully!', 'success');
@ -198,8 +196,25 @@ const settings = {
settings.updateVisibility(); settings.updateVisibility();
const customGreetingInput = document.getElementById('custom-greeting'); const customGreetingInput = document.getElementById('custom-greeting');
customGreetingInput.maxLength = settings.GREETING_MAX_LENGTH;
customGreetingInput.addEventListener('input', (e) => {
const value = e.target.value;
if (value.length > settings.GREETING_MAX_LENGTH) {
e.target.value = value.substring(0, settings.GREETING_MAX_LENGTH);
notifications.show(`Custom greeting must be less than ${settings.GREETING_MAX_LENGTH} characters`, 'error');
}
});
customGreetingInput.addEventListener('change', (e) => { customGreetingInput.addEventListener('change', (e) => {
const value = e.target.value.trim(); const value = e.target.value.trim();
if (value.length > settings.GREETING_MAX_LENGTH) {
e.target.value = value.substring(0, settings.GREETING_MAX_LENGTH);
notifications.show(`Custom greeting must be less than ${settings.GREETING_MAX_LENGTH} characters`, 'error');
return;
}
if (value === '') { if (value === '') {
Storage.remove('customGreeting'); Storage.remove('customGreeting');
notifications.show('Using default greeting format', 'info'); notifications.show('Using default greeting format', 'info');
@ -239,15 +254,32 @@ const settings = {
try { try {
if (!format) return null; if (!format) return null;
const validTokens = ['{name}', '{greeting}', '{time}', '{date}', '{day}', '{month}', '{year}'];
const tokens = format.match(/{[^}]+}/g) || [];
if (!tokens.every(token => validTokens.includes(token))) {
notifications.show('Invalid greeting format: Unknown token used', 'error');
return null;
}
const now = new Date(); const now = new Date();
const hour = now.getHours();
let timeGreeting = 'good night';
if (hour >= 5 && hour < 12) timeGreeting = 'good morning';
else if (hour >= 12 && hour < 17) timeGreeting = 'good afternoon';
else if (hour >= 17 && hour < 20) timeGreeting = 'good evening';
const userName = Storage.get('anonymousMode') ? const userName = Storage.get('anonymousMode') ?
(Storage.get('anonymousName') || anonymousNames.generate()) : (Storage.get('anonymousName') || anonymousNames.generate()) :
(Storage.get('userName') || 'Friend'); (Storage.get('userName') || 'Friend');
const formats = { const formats = {
name: userName, name: userName,
greeting: format.startsWith('{greeting}') ?
timeGreeting.charAt(0).toUpperCase() + timeGreeting.slice(1) :
timeGreeting,
time: now.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' }), time: now.toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' }),
date: now.toLocaleDateString(), date: now.getDate().toString(),
day: now.toLocaleDateString([], { weekday: 'long' }), day: now.toLocaleDateString([], { weekday: 'long' }),
month: now.toLocaleDateString([], { month: 'long' }), month: now.toLocaleDateString([], { month: 'long' }),
year: now.getFullYear() year: now.getFullYear()
@ -267,38 +299,113 @@ const settings = {
return formattedGreeting; return formattedGreeting;
} catch (e) { } catch (e) {
Storage.remove('customGreeting'); notifications.show('Invalid greeting format. Using default.', 'error');
notifications.show('Error in greeting format. Using default.', 'error');
return null; return null;
} }
}, },
// Data management functions
exportData: () => { exportData: () => {
const data = { const data = {
settings: { settings: {
theme: document.body.getAttribute('data-theme'), theme: Storage.get('theme'),
userName: Storage.get('userName'), userName: Storage.get('userName'),
anonymousMode: Storage.get('anonymousMode'), anonymousMode: Storage.get('anonymousMode'),
anonymousName: Storage.get('anonymousName'), anonymousName: Storage.get('anonymousName'),
searchEngine: Storage.get('searchEngine'), searchEngine: Storage.get('searchEngine'),
customGreeting: Storage.get('customGreeting'),
show_greeting: Storage.get('show_greeting'), show_greeting: Storage.get('show_greeting'),
show_search: Storage.get('show_search'), show_search: Storage.get('show_search'),
show_shortcuts: Storage.get('show_shortcuts'), show_shortcuts: Storage.get('show_shortcuts'),
show_addShortcut: Storage.get('show_addShortcut'), show_addShortcut: Storage.get('show_addShortcut')
customGreeting: Storage.get('customGreeting')
}, },
shortcuts: Storage.get('shortcuts') || [] shortcuts: Storage.get('shortcuts') || [],
keybinds: Storage.get('keybinds') || {}
}; };
if (!data.settings || !data.shortcuts || !data.keybinds) {
notifications.show('Failed to export data: Invalid data structure.', 'error');
return;
}
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }); const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
const url = URL.createObjectURL(blob); const url = URL.createObjectURL(blob);
const a = document.createElement('a'); const a = document.createElement('a');
a.href = url; a.href = url;
a.download = 'jstar-tab-settings.json'; a.download = 'jstar-tab-backup.json';
document.body.appendChild(a); document.body.appendChild(a);
a.click(); a.click();
document.body.removeChild(a); document.body.removeChild(a);
URL.revokeObjectURL(url); URL.revokeObjectURL(url);
},
importData: async (file) => {
const text = await file.text();
try {
const data = JSON.parse(text);
if (!data.settings || typeof data.settings !== 'object' ||
!Array.isArray(data.shortcuts) ||
!data.keybinds || typeof data.keybinds !== 'object') {
throw new Error('Invalid data structure');
}
Object.entries(data.settings).forEach(([key, value]) => {
Storage.set(key, value);
});
Storage.set('shortcuts', data.shortcuts);
const validatedKeybinds = {};
Object.entries(data.keybinds).forEach(([action, binding]) => {
if (binding && typeof binding === 'object' &&
typeof binding.keys === 'string' &&
(!binding.url || typeof binding.url === 'string')) {
validatedKeybinds[action] = binding;
}
});
Storage.set('keybinds', validatedKeybinds);
settings.updateSettingsUI();
settings.updateVisibility();
shortcuts.render();
document.body.setAttribute('data-theme', data.settings.theme || 'light');
notifications.show('Data imported successfully!', 'success');
} catch (error) {
notifications.show('Failed to import data: Invalid file format!', 'error');
console.error('Import error:', error);
}
},
resetData: () => {
const defaultSettings = {
theme: 'light',
userName: '',
anonymousMode: false,
searchEngine: 'Google',
show_greeting: true,
show_search: true,
show_shortcuts: true,
show_addShortcut: true
};
Object.entries(defaultSettings).forEach(([key, value]) => {
Storage.set(key, value);
});
Storage.remove('shortcuts');
Storage.remove('keybinds');
Storage.remove('anonymousName');
Storage.remove('customGreeting');
settings.updateSettingsUI();
settings.updateVisibility();
shortcuts.render();
document.body.setAttribute('data-theme', 'light');
notifications.show('All data has been reset!', 'success');
} }
}; };
@ -309,8 +416,10 @@ settings.initDataManagement = () => {
const resetBtn = document.getElementById('reset-data'); const resetBtn = document.getElementById('reset-data');
const fileInput = document.getElementById('import-file'); const fileInput = document.getElementById('import-file');
// Export Data exportBtn.replaceWith(exportBtn.cloneNode(true));
exportBtn.addEventListener('click', () => { const newExportBtn = document.getElementById('export-data');
newExportBtn.addEventListener('click', () => {
try { try {
const data = { const data = {
settings: { settings: {
@ -325,7 +434,8 @@ settings.initDataManagement = () => {
show_addShortcut: Storage.get('show_addShortcut'), show_addShortcut: Storage.get('show_addShortcut'),
customGreeting: Storage.get('customGreeting') customGreeting: Storage.get('customGreeting')
}, },
shortcuts: Storage.get('shortcuts') || [] shortcuts: Storage.get('shortcuts') || [],
keybinds: Storage.get('keybinds') || {}
}; };
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }); const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
@ -345,7 +455,6 @@ settings.initDataManagement = () => {
} }
}); });
// Import Data
importBtn.addEventListener('click', () => fileInput.click()); importBtn.addEventListener('click', () => fileInput.click());
fileInput.addEventListener('change', (e) => { fileInput.addEventListener('change', (e) => {
@ -357,7 +466,7 @@ settings.initDataManagement = () => {
try { try {
const data = JSON.parse(event.target.result); const data = JSON.parse(event.target.result);
if (!data.shortcuts || !data.settings) { if (!data.shortcuts || !data.settings || !data.keybinds) {
throw new Error('Invalid backup file format'); throw new Error('Invalid backup file format');
} }
@ -365,6 +474,19 @@ settings.initDataManagement = () => {
Storage.set(key, value); Storage.set(key, value);
}); });
if (data.keybinds) {
const validatedKeybinds = {};
Object.entries(data.keybinds).forEach(([action, binding]) => {
if (binding && typeof binding === 'object' &&
typeof binding.keys === 'string' &&
(!binding.url || typeof binding.url === 'string')) {
validatedKeybinds[action] = binding;
}
});
Storage.set('keybinds', validatedKeybinds);
keybinds.init();
}
Storage.set('shortcuts', data.shortcuts); Storage.set('shortcuts', data.shortcuts);
fileInput.value = ''; fileInput.value = '';
@ -454,7 +576,6 @@ settings.initDataManagement = () => {
reader.readAsText(file); reader.readAsText(file);
}); });
// Reset Data
resetBtn.addEventListener('click', () => { resetBtn.addEventListener('click', () => {
const confirmReset = confirm('Are you sure you want to reset all data? This action cannot be undone.'); const confirmReset = confirm('Are you sure you want to reset all data? This action cannot be undone.');

View File

@ -1,7 +1,7 @@
const shortcuts = { const shortcuts = {
MAX_SHORTCUTS: 12, MAX_SHORTCUTS: 12,
// Validate and format URL // URL Validation
validateAndFormatUrl: (url) => { validateAndFormatUrl: (url) => {
if (!/^https?:\/\//i.test(url)) { if (!/^https?:\/\//i.test(url)) {
url = 'https://' + url; url = 'https://' + url;
@ -15,7 +15,7 @@ const shortcuts = {
} }
}, },
// Add new shortcut // Shortcut Management
add: (url, name) => { add: (url, name) => {
const currentShortcuts = Storage.get('shortcuts') || []; const currentShortcuts = Storage.get('shortcuts') || [];
if (currentShortcuts.length >= shortcuts.MAX_SHORTCUTS) { if (currentShortcuts.length >= shortcuts.MAX_SHORTCUTS) {
@ -34,7 +34,6 @@ const shortcuts = {
shortcuts.render(); shortcuts.render();
}, },
// Remove shortcut
remove: (index) => { remove: (index) => {
const currentShortcuts = Storage.get('shortcuts') || []; const currentShortcuts = Storage.get('shortcuts') || [];
currentShortcuts.splice(index, 1); currentShortcuts.splice(index, 1);
@ -43,7 +42,6 @@ const shortcuts = {
notifications.show('Shortcut removed!', 'success'); notifications.show('Shortcut removed!', 'success');
}, },
// Edit existing shortcut
edit: (index, newUrl, newName) => { edit: (index, newUrl, newName) => {
const currentShortcuts = Storage.get('shortcuts') || []; const currentShortcuts = Storage.get('shortcuts') || [];
currentShortcuts[index] = { url: newUrl, name: newName }; currentShortcuts[index] = { url: newUrl, name: newName };
@ -52,7 +50,7 @@ const shortcuts = {
notifications.show('Shortcut updated!', 'success'); notifications.show('Shortcut updated!', 'success');
}, },
// Show context menu for shortcut // UI Interactions
showContextMenu: (e, index) => { showContextMenu: (e, index) => {
e.preventDefault(); e.preventDefault();
const menu = document.getElementById('context-menu'); const menu = document.getElementById('context-menu');
@ -75,7 +73,7 @@ const shortcuts = {
}, 0); }, 0);
}, },
// Render shortcuts grid // Rendering
render: () => { render: () => {
const grid = document.getElementById('shortcuts-grid'); const grid = document.getElementById('shortcuts-grid');
const currentShortcuts = Storage.get('shortcuts') || []; const currentShortcuts = Storage.get('shortcuts') || [];
@ -130,9 +128,23 @@ const shortcuts = {
}); });
}, },
// Initialize shortcuts functionality // Initialization
init: () => { init: () => {
const addShortcutButton = document.getElementById('add-shortcut'); const addShortcutButton = document.getElementById('add-shortcut');
const modal = document.getElementById('add-shortcut-modal');
const closeBtn = modal.querySelector('.close-modal');
if (closeBtn) {
closeBtn.addEventListener('click', () => {
modal.classList.remove('active');
setTimeout(() => {
modal.classList.add('hidden');
document.getElementById('shortcut-url').value = '';
document.getElementById('shortcut-name').value = '';
}, 300);
});
}
if (addShortcutButton) { if (addShortcutButton) {
addShortcutButton.addEventListener('click', (e) => { addShortcutButton.addEventListener('click', (e) => {
e.stopPropagation(); e.stopPropagation();
@ -143,7 +155,6 @@ const shortcuts = {
return; return;
} }
const modal = document.getElementById('add-shortcut-modal');
if (modal) { if (modal) {
modal.classList.remove('hidden'); modal.classList.remove('hidden');
modal.classList.add('active'); modal.classList.add('active');

View File

@ -1,7 +1,7 @@
{ {
"manifest_version": 3, "manifest_version": 3,
"name": "JSTAR Tab", "name": "JSTAR Tab",
"version": "2.5.0", "version": "2.6.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.", "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": { "chrome_url_overrides": {
"newtab": "index.html" "newtab": "index.html"

View File

@ -416,7 +416,7 @@ input[type="text"]:focus {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
margin-bottom: 2rem; margin-bottom: 0rem;
} }
.modal-header h2 { .modal-header h2 {
@ -445,6 +445,7 @@ input[type="text"]:focus {
.settings-section:last-child { .settings-section:last-child {
border-bottom: none; border-bottom: none;
padding-bottom: 0;
} }
.settings-section h3 { .settings-section h3 {
@ -518,6 +519,7 @@ input[type="text"]:focus {
bottom: 3px; bottom: 3px;
background-color: var(--background); background-color: var(--background);
border-radius: 50%; border-radius: 50%;
transition: transform 0.3s ease;
} }
input:checked + .toggle-slider { input:checked + .toggle-slider {
@ -528,6 +530,35 @@ input:checked + .toggle-slider:before {
transform: translateX(24px); transform: translateX(24px);
} }
#keybind-url-combo {
margin-bottom: 0.5rem;
}
#keybind-url {
margin-top: 0.5rem;
}
.keybind-container {
display: flex;
gap: 0.5rem;
align-items: center;
}
.clear-keybind {
padding: 0.25rem 0.5rem;
background: var(--accent);
border: none;
border-radius: 4px;
color: var(--text);
cursor: pointer;
font-size: 1.2rem;
line-height: 1;
}
.clear-keybind:hover {
opacity: 0.8;
}
.format-hint { .format-hint {
display: block; display: block;
font-size: 0.75rem; font-size: 0.75rem;
@ -917,3 +948,45 @@ input:checked + .toggle-slider:before {
font-size: 0.95rem; font-size: 0.95rem;
} }
} }
.import-options {
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
width: 100%;
max-width: 300px;
margin: 0 auto;
}
.or-divider {
position: relative;
width: 100%;
text-align: center;
margin: 0.5rem 0;
}
.or-divider::before,
.or-divider::after {
content: '';
position: absolute;
top: 50%;
width: calc(50% - 24px);
height: 1px;
background-color: var(--border);
}
.or-divider::before {
left: 0;
}
.or-divider::after {
right: 0;
}
.or-divider span {
background-color: var(--background);
padding: 0 8px;
color: var(--text);
font-size: 0.9rem;
}