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>
</div>
<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">
<input type="text" id="user-name" placeholder="What's your name?">
<input type="text" id="user-name" placeholder="Enter your name">
</div>
<button id="next-step-btn" class="btn-primary">Continue</button>
</div>
<div class="step hidden" data-step="2">
<div class="step hidden" data-step="3">
<h2>Choose Your Search Engine</h2>
<div class="search-engine-options">
<!-- Search engine options -->
@ -148,7 +161,7 @@
<div class="setting-item">
<div class="setting-label">Custom Greeting</div>
<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 class="setting-item horizontal">
@ -176,6 +189,47 @@
</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 -->
<div class="settings-section">
<h3>Data Management</h3>
@ -197,12 +251,11 @@
<div class="settings-section">
<h3>About</h3>
<div class="about-content">
<p>JSTAR Tab v2.5.0</p>
<p>Author: JSTAR</p>
<p>JSTAR Tab v2.6.0</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>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>
@ -273,5 +326,6 @@
<script src="js/shortcuts.js"></script>
<script src="js/settings.js"></script>
<script src="js/main.js"></script>
<script src="js/keybinds.js"></script>
</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() {
const greeting = document.getElementById('greeting');
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 isAnonymous = Storage.get('anonymousMode') || false;
const userName = isAnonymous ?
@ -35,7 +35,7 @@ async function updateGreeting() {
}, 100);
}
// Set up event listeners for modal interactions
// Modal handling
function initModalHandlers() {
const modals = document.querySelectorAll('.modal');
@ -52,10 +52,18 @@ function initModalHandlers() {
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) {
if (!modal) return;
modal.classList.remove('hidden');
@ -64,7 +72,6 @@ function openModal(modal) {
});
}
// Close modal with animation
function closeModal(modal) {
if (!modal) return;
modal.classList.remove('active');
@ -73,9 +80,8 @@ function closeModal(modal) {
}, 300);
}
// Initialize application
// Application initialization
document.addEventListener('DOMContentLoaded', () => {
// Apply visibility settings
['greeting', 'search', 'shortcuts', 'addShortcut'].forEach(element => {
const isVisible = Storage.get(`show_${element}`);
if (isVisible === false) {
@ -84,28 +90,39 @@ document.addEventListener('DOMContentLoaded', () => {
}
});
// 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);
});
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
const onboarding = {
// Check if onboarding is complete
// Core functions
isComplete: () => {
return Storage.get('onboardingComplete') === true;
},
// Start the onboarding process
start: () => {
const modal = document.getElementById('onboarding-modal');
const mainContent = document.getElementById('main-content');
const importDataBtn = document.getElementById('import-data-btn');
const startFreshBtn = document.getElementById('start-fresh-btn');
const fileInput = document.getElementById('onboarding-import');
if (!onboarding.isComplete()) {
modal.classList.remove('hidden');
modal.classList.add('active');
mainContent.classList.add('hidden');
document.getElementById('next-step-btn').addEventListener('click', () => onboarding.nextStep(1));
document.getElementById('complete-setup-btn').addEventListener('click', onboarding.complete);
startFreshBtn.addEventListener('click', () => {
document.querySelector('[data-step="1"]').classList.add('hidden');
document.querySelector('[data-step="2"]').classList.remove('hidden');
document.getElementById('next-step-btn').addEventListener('click', () => onboarding.nextStep(2));
});
// 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');
engines.forEach(engine => {
engine.addEventListener('click', () => {
@ -26,31 +86,34 @@ const onboarding = {
engine.classList.add('selected');
});
});
document.getElementById('complete-setup-btn').addEventListener('click', onboarding.complete);
} else {
modal.classList.add('hidden');
mainContent.classList.remove('hidden');
}
},
// Move to the next step in onboarding
// Onboarding step navigation
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;
if (currentStep === 2) {
const name = document.getElementById('user-name').value.trim();
if (!name) {
notifications.show('Please enter your name!', 'error');
return;
}
Storage.set('userName', name);
}
Storage.set('userName', name);
currentStepEl.classList.add('hidden');
nextStepEl.classList.remove('hidden');
nextStepEl.classList.add('visible');
},
// Complete the onboarding process
// Finalize onboarding
complete: () => {
const selectedEngine = document.querySelector('.search-engine-option.selected');
if (!selectedEngine) {

View File

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

View File

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

View File

@ -1,7 +1,7 @@
{
"manifest_version": 3,
"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.",
"chrome_url_overrides": {
"newtab": "index.html"

View File

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