Compare commits

..

No commits in common. "7cc5e4010ed72d48fb4fba28b99421a4aae5e933" and "3e6b86b79d9409eec45384a9537f1bcc289d7065" have entirely different histories.

28 changed files with 2934 additions and 6566 deletions

View File

@ -2,7 +2,7 @@
Welcome to **JSTAR Tab**, the ultimate customizable new tab extension for your browser! Whether you're looking for a sleek, modern design or powerful personalization options, JSTAR Tab has you covered. 🚀 Welcome to **JSTAR Tab**, the ultimate customizable new tab extension for your browser! Whether you're looking for a sleek, modern design or powerful personalization options, JSTAR Tab has you covered. 🚀
Transform your browsing experience with custom greetings, themes, fonts, shortcuts, and more. 🎉 Transform your browsing experience with custom greetings, themes, shortcuts, and more. 🎉
## ✨ Features ## ✨ Features
@ -20,10 +20,8 @@ Transform your browsing experience with custom greetings, themes, fonts, shortcu
- "{greeting}, {name}! Today is {date}" - "{greeting}, {name}! Today is {date}"
- "Happy {day}, {name}!" - "Happy {day}, {name}!"
- **Font Selection**: Choose from multiple fonts (Inter, Poppins, Roboto, Montserrat, Quicksand, and Comic Sans) to personalize your experience. - **Customizable Themes**: Switch between light and dark modes to suit your mood. 🌗
- **Custom Background Images**: Upload and set your favorite images as your new tab background. - **Shortcut Management**: Add, edit, and remove shortcuts to your favorite websites.
- **Customizable Themes**: Switch between light and dark modes to suit your mood.
- **Shortcut Management**: Add, edit, and remove shortcuts to your favorite websites with an improved grid layout system.
- **Search Engine Selection**: Choose your preferred search engine for quick and efficient browsing. - **Search Engine Selection**: Choose your preferred search engine for quick and efficient browsing.
- **Keyboard Shortcuts**: Set up custom keybinds for various actions: - **Keyboard Shortcuts**: Set up custom keybinds for various actions:
- Open settings - Open settings
@ -31,7 +29,8 @@ Transform your browsing experience with custom greetings, themes, fonts, shortcu
- Toggle anonymous mode - Toggle anonymous mode
- Change themes - Change themes
- Redirect to a specific URL (with a notification and "Redirecting to..." message) - Redirect to a specific URL (with a notification and "Redirecting to..." message)
- **Data Backup and Restore**: Export and import your settings and shortcuts effortlessly.
- **Data Backup and Restore**: Export and import your settings and shortcuts effortlessly.
## 🌐 Getting Started ## 🌐 Getting Started
@ -55,18 +54,21 @@ Transform your browsing experience with custom greetings, themes, fonts, shortcu
### **Greeting Formats** ### **Greeting Formats**
Personalize your greeting with dynamic tags listed above. Example: "Good {greeting}, {name}! It's {time} on {day}!" Personalize your greeting with dynamic tags listed above. Example: "Good {greeting}, {name}! It's {time} on {day}!"
### **Themes & Fonts** ### **Themes**
- Toggle between light and dark themes from the settings panel. Toggle between light and dark themes from the settings panel.
- Select from various font options to match your style.
### **Shortcuts & Backgrounds** ### **Shortcuts**
Manage your favorite sites effortlessly:
- Add shortcuts with the "+" button. - Add shortcuts with the "+" button.
- Edit or delete shortcuts by right-clicking on them. - Edit or delete shortcuts by right-clicking on them.
- Customize your background by uploading your favorite images.
### **Keyboard Shortcuts** ### **Keyboard Shortcuts**
Set up custom keybinds for quick actions like opening settings or switching themes. Set up custom keybinds for quick actions like opening settings or switching themes.
## ❤️ Acknowledgments
Special thanks to **[Equa](https://github.com/EquaTechnologies)** for contributing amazing hover animations for shortcuts, settings, and the "Add Shortcut" button. Your work is truly appreciated! 🌟
## 📄 License ## 📄 License
This project is licensed under the [MIT License](https://github.com/DevJSTAR/JSTAR-Tab/blob/main/LICENSE). This project is licensed under the [MIT License](https://github.com/DevJSTAR/JSTAR-Tab/blob/main/LICENSE).
@ -77,4 +79,4 @@ This project is licensed under the [MIT License](https://github.com/DevJSTAR/JST
- **[Patreon](https://patreon.com/jstarsdev)** - **[Patreon](https://patreon.com/jstarsdev)**
- **[GitHub Releases](https://github.com/DevJSTAR/JSTAR-Tab/releases/latest)** - **[GitHub Releases](https://github.com/DevJSTAR/JSTAR-Tab/releases/latest)**
Thank you for choosing JSTAR Tab! We hope you enjoy the seamless and personalized browsing experience it brings. 🚀 Thank you for choosing JSTAR Tab! We hope you enjoy the seamless and personalized browsing experience it brings. 🚀

View File

@ -1,419 +0,0 @@
.container-ob {
position: fixed;
inset: 0;
z-index: 10000;
background-color: var(--background);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
overflow: hidden;
}
.container-ob::before {
content: '';
display: block;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 0;
}
.step-ob {
display: none;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
max-width: 800px;
width: 100%;
height: 100%;
padding: 2rem;
position: relative;
opacity: 0;
transform: translateY(20px);
transition: opacity 0.5s ease, transform 0.5s ease;
}
.step-ob.active-ob {
display: flex;
opacity: 1;
transform: translateY(0);
z-index: 1;
}
.title-ob {
font-size: 2.5rem;
font-weight: 700;
color: var(--text);
margin-bottom: 1rem;
transform-origin: center;
animation: titlePop-ob 0.6s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
}
.subtitle-ob {
font-size: 1.25rem;
color: var(--text-secondary);
margin-bottom: 2.5rem;
max-width: 600px;
}
.options-grid-ob {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1.5rem;
width: 100%;
margin-bottom: 2rem;
}
.step-ob[data-step="3"] .options-grid-ob {
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
max-width: 900px;
margin-left: auto;
margin-right: auto;
}
.step-ob[data-step="5"] .options-grid-ob {
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
max-width: 900px;
margin-left: auto;
margin-right: auto;
}
.option-card-ob {
background-color: var(--surface);
border: 2px solid var(--border);
border-radius: 16px;
padding: 1.5rem;
display: flex;
flex-direction: column;
align-items: center;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
position: relative;
overflow: hidden;
}
.option-card-ob:hover {
transform: translateY(-5px);
box-shadow: 0 10px 20px var(--shadow);
border-color: var(--text);
}
.option-card-ob.selected-ob {
border-color: var(--text);
background-color: var(--surface);
transform: scale(1.02);
}
.option-card-ob svg {
width: 48px;
height: 48px;
color: var(--text);
margin-bottom: 1rem;
}
.option-card-ob h3 {
font-size: 1.25rem;
font-weight: 600;
color: var(--text);
margin-bottom: 0.5rem;
}
.option-card-ob p {
font-size: 0.875rem;
color: var(--text-secondary);
}
.option-card-ob[data-engine] img {
width: 32px;
height: 32px;
border-radius: 50%;
object-fit: cover;
margin-bottom: 1rem;
}
.nav-ob {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
max-width: 800px;
padding: 0 1rem;
margin-top: auto;
margin-bottom: 40px;
position: relative;
z-index: 2;
}
.button-ob {
background-color: var(--surface);
border: 2px solid var(--border);
border-radius: 12px;
padding: 1rem 2rem;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
display: flex;
align-items: center;
gap: 0.5rem;
color: var(--text);
transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.button-ob:hover {
background-color: var(--primary);
border-color: var(--text);
transform: translateY(-2px);
}
.button-ob svg {
width: 20px;
height: 20px;
}
.button-ob.primary-ob {
background-color: var(--text);
color: var(--background);
border-color: var(--text);
}
.button-ob.primary-ob:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px var(--shadow);
}
.button-ob.disabled-ob,
.button-ob:disabled {
opacity: 0.5;
cursor: not-allowed;
pointer-events: none;
}
.button-ob.disabled-ob:hover,
.button-ob:disabled:hover {
transform: none;
box-shadow: none;
background-color: var(--surface);
border-color: var(--border);
}
.progress-dots-ob {
display: flex;
gap: 8px;
}
.dot-ob {
width: 10px;
height: 10px;
border-radius: 50%;
background-color: var(--border);
transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
}
.dot-ob.active-ob {
background-color: var(--text);
transform: scale(1.2);
}
.theme-preview-ob {
width: 100%;
border-radius: 16px;
padding: 1.5rem;
margin-bottom: 1rem;
transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1);
border: 2px solid var(--border);
}
.theme-preview-ob.light-ob {
background-color: white;
color: #1a1a1a;
}
.theme-preview-ob.dark-ob {
background-color: #1a1a1a;
color: white;
}
.theme-preview-ob.light-ob .preview-search-ob {
background-color: #f0f0f0;
border-color: #dddddd;
}
.theme-preview-content-ob {
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
}
.preview-search-ob {
width: 100%;
height: 50px;
border-radius: 25px;
background-color: var(--surface);
border: 2px solid var(--border);
}
.preview-shortcuts-ob {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 0.5rem;
width: 100%;
}
.preview-shortcut-ob {
width: 100%;
aspect-ratio: 1/1;
border-radius: 12px;
background-color: rgba(0, 0, 0, 0.1);
}
.dark-ob .preview-shortcut-ob {
background-color: rgba(255, 255, 255, 0.1);
}
.font-preview-ob {
font-size: 3rem;
margin-bottom: 1rem;
line-height: 1;
}
.name-input-container-ob {
width: 100%;
max-width: 500px;
margin: 2rem 0;
position: relative;
}
.name-input-ob {
width: 100%;
border: none !important;
background: transparent !important;
font-size: 4rem !important;
font-weight: 600 !important;
color: var(--text) !important;
text-align: left !important;
border-radius: 0 !important;
padding: 0.5rem 0 !important;
border-bottom: 2px solid #a3a3a3 !important;
transition: all 0.3s ease !important;
outline: none !important;
-webkit-appearance: none !important;
-moz-appearance: none !important;
appearance: none !important;
}
.name-input-ob:focus {
border-color: var(--text);
}
.name-input-ob::placeholder {
color: var(--text-secondary);
opacity: 0.5;
}
.name-input-ob:-webkit-autofill,
.name-input-ob:-webkit-autofill:hover,
.name-input-ob:-webkit-autofill:focus {
-webkit-text-fill-color: var(--text) !important;
-webkit-box-shadow: 0 0 0px 1000px transparent inset !important;
transition: background-color 5000s ease-in-out 0s !important;
background-clip: content-box !important;
}
@keyframes titlePop-ob {
0% { transform: scale(0.8); opacity: 0; }
50% { transform: scale(1.05); }
100% { transform: scale(1); opacity: 1; }
}
@keyframes slideInUp-ob {
0% { transform: translateY(40px); opacity: 0; }
100% { transform: translateY(0); opacity: 1; }
}
@keyframes fadeIn-ob {
0% { opacity: 0; }
100% { opacity: 1; }
}
.search-engine-preview-ob {
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
margin-top: 1rem;
}
.search-engine-preview-ob img {
width: 24px;
height: 24px;
border-radius: 50%;
}
.animate-in-ob {
animation: slideInUp-ob 0.5s cubic-bezier(0.34, 1.56, 0.64, 1) forwards;
}
@media (max-width: 768px) {
.options-grid-ob {
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1rem;
}
.title-ob {
font-size: 2rem;
}
.subtitle-ob {
font-size: 1rem;
}
.name-input-ob {
font-size: 1.5rem;
}
.nav-ob {
padding: 0.5rem;
margin-bottom: 30px;
}
}
@media (max-width: 576px) {
.step-ob {
padding: 1.5rem 1rem;
}
.options-grid-ob,
.step-ob[data-step="3"] .options-grid-ob,
.step-ob[data-step="5"] .options-grid-ob {
grid-template-columns: 1fr;
gap: 1rem;
}
.option-card-ob {
padding: 1rem;
}
.title-ob {
font-size: 1.75rem;
}
.button-ob {
padding: 0.75rem 1.5rem;
}
.font-preview-ob {
font-size: 2.5rem;
}
}
@media (min-width: 577px) and (max-width: 991px) {
.step-ob[data-step="3"] .options-grid-ob,
.step-ob[data-step="5"] .options-grid-ob {
grid-template-columns: repeat(2, 1fr);
}
}

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 339 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 648 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 235 KiB

BIN
images/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.9 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 584 B

After

Width:  |  Height:  |  Size: 479 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 978 B

1233
index.html

File diff suppressed because it is too large Load Diff

View File

@ -1,159 +0,0 @@
document.addEventListener('DOMContentLoaded', () => {
const MAX_IMAGE_SIZE_MB = 1.5;
const MAX_USER_BACKGROUNDS = 10;
const backgroundUpload = document.getElementById('background-upload');
const backgroundPreviewGrid = document.getElementById('background-preview-grid');
const resetBackground = document.getElementById('default-background');
resetBackground.style.order = '-1';
backgroundPreviewGrid.prepend(resetBackground);
loadBackgrounds();
setSavedBackground();
backgroundUpload.addEventListener('change', (event) => {
const file = event.target.files[0];
if (!file) return;
const maxSizeBytes = MAX_IMAGE_SIZE_MB * 1024 * 1024;
if (file.size > maxSizeBytes) {
notifications.show(`Image exceeds size limit (max ${MAX_IMAGE_SIZE_MB} MB)`, 'error');
event.target.value = '';
return;
}
const existingBackgrounds = JSON.parse(Storage.get('backgrounds') || '[]');
if (existingBackgrounds.length >= MAX_USER_BACKGROUNDS) {
notifications.show(`Maximum custom backgrounds limit reached!`, 'error');
event.target.value = '';
return;
}
const reader = new FileReader();
reader.onload = function (e) {
const imageUrl = e.target.result;
addBackgroundPreview(imageUrl, false);
saveBackground(imageUrl);
setCustomBackground(imageUrl);
notifications.show('Background uploaded successfully!', 'success');
};
reader.readAsDataURL(file);
});
resetBackground.addEventListener('click', () => {
const currentBackground = Storage.get('customBackground');
if (!currentBackground) return;
document.body.style.backgroundImage = '';
Storage.remove('customBackground');
removeAllSelection();
notifications.show('Background reset to default!', 'success');
});
function loadBackgrounds() {
try {
const backgrounds = JSON.parse(Storage.get('backgrounds') || '[]');
if (!Array.isArray(backgrounds)) {
throw new Error('Invalid backgrounds format');
}
backgrounds.forEach((bg) => {
if (typeof bg === 'string' &&
(bg.startsWith('data:image/') || bg.startsWith('images/backgrounds/'))) {
addBackgroundPreview(bg, false);
}
});
} catch (e) {
console.error('Failed to load backgrounds:', e);
Storage.set('backgrounds', '[]');
notifications.show('Corrupted backgrounds data - resetting', 'error');
}
}
function setSavedBackground() {
const customBackground = Storage.get('customBackground');
if (customBackground) {
setCustomBackground(customBackground, true);
}
}
function addBackgroundPreview(imageUrl, isPredefined) {
const preview = document.createElement('div');
preview.className = 'background-preview' + (isPredefined ? ' predefined' : ' custom');
preview.style.backgroundImage = `url(${imageUrl})`;
preview.dataset.url = imageUrl;
if (Storage.get('customBackground') === imageUrl) {
preview.classList.add('selected');
}
preview.addEventListener('click', () => {
if (Storage.get('customBackground') === imageUrl) return;
setCustomBackground(imageUrl);
removeAllSelection();
preview.classList.add('selected');
});
if (!isPredefined) {
const removeIcon = document.createElement('span');
removeIcon.className = 'remove-icon';
removeIcon.innerHTML = '<i class="fas fa-times"></i>';
removeIcon.addEventListener('click', (e) => {
e.stopPropagation();
const wasSelected = preview.classList.contains('selected');
if (wasSelected) {
document.body.style.backgroundImage = '';
Storage.remove('customBackground');
}
removeBackground(imageUrl);
preview.remove();
notifications.show('Background removed!', 'success');
});
preview.appendChild(removeIcon);
}
const insertPosition = isPredefined ? 1 : backgroundPreviewGrid.children.length;
backgroundPreviewGrid.insertBefore(preview, backgroundPreviewGrid.children[insertPosition]);
}
function removeAllSelection() {
document.querySelectorAll('.background-preview').forEach(preview => {
preview.classList.remove('selected');
});
}
function removeBackground(imageUrl) {
const backgrounds = JSON.parse(Storage.get('backgrounds') || '[]');
Storage.set('backgrounds', JSON.stringify(backgrounds.filter(bg => bg !== imageUrl)));
}
function saveBackground(imageUrl) {
const backgrounds = JSON.parse(Storage.get('backgrounds') || '[]');
if (!backgrounds.includes(imageUrl)) {
backgrounds.push(imageUrl);
Storage.set('backgrounds', JSON.stringify(backgrounds));
}
}
function setCustomBackground(imageUrl, initialLoad = false) {
document.body.style.backgroundImage = `url(${imageUrl})`;
Storage.set('customBackground', imageUrl);
if (!initialLoad) {
removeAllSelection();
const targetPreview = document.querySelector(`.background-preview[data-url="${imageUrl}"]`);
if (targetPreview) targetPreview.classList.add('selected');
}
}
const predefinedImages = [
'images/backgrounds/cherry.png',
'images/backgrounds/mommies.png',
'images/backgrounds/peachs-castle.png',
'images/backgrounds/windows-xp.jpg',
];
predefinedImages.forEach(image => addBackgroundPreview(image, true));
});

View File

@ -1,86 +0,0 @@
const CacheUpdater = {
update: () => {
const shortcuts = Storage.get('shortcuts') || [];
const faviconUrls = shortcuts.map(shortcut =>
`https://www.google.com/s2/favicons?domain=${encodeURIComponent(new URL(shortcut.url).hostname)}&sz=64`
);
if (navigator.serviceWorker?.controller && faviconUrls.length > 0) {
navigator.serviceWorker.controller.postMessage({
action: 'updateFavicons',
urls: faviconUrls
});
}
}
};
const cacheHandler = {
SHORTCUT_CACHE_DURATION: 7 * 24 * 60 * 60 * 1000,
init() {
this.cleanExpiredCache();
},
cleanExpiredCache() {
const cache = this.getAllCache();
const now = Date.now();
Object.entries(cache).forEach(([key, value]) => {
if (key.startsWith('shortcut_') && value.expiry && value.expiry < now) {
this.removeFromCache(key);
}
});
},
addToCache(key, value, isSearchEngine = false) {
const cacheItem = {
value,
timestamp: Date.now(),
expiry: isSearchEngine ? null : Date.now() + this.SHORTCUT_CACHE_DURATION
};
localStorage.setItem(key, JSON.stringify(cacheItem));
},
getFromCache(key) {
const item = localStorage.getItem(key);
if (!item) return null;
try {
const cacheItem = JSON.parse(item);
if (cacheItem.expiry && cacheItem.expiry < Date.now()) {
this.removeFromCache(key);
return null;
}
return cacheItem.value;
} catch (error) {
console.error('Cache read error:', error);
return null;
}
},
removeFromCache(key) {
localStorage.removeItem(key);
},
getAllCache() {
const cache = {};
for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i);
if (key.startsWith('shortcut_') || key.startsWith('search_engine_')) {
try {
cache[key] = JSON.parse(localStorage.getItem(key));
} catch (error) {
console.error('Cache read error:', error);
}
}
}
return cache;
}
};
document.addEventListener('DOMContentLoaded', () => {
cacheHandler.init();
});

View File

@ -1,337 +0,0 @@
const GridLayout = {
defaults: {
type: 'default',
columns: 6,
gap: 16,
size: 80,
resizable: false
},
layouts: {
default: {
columns: 6,
gap: 16,
size: 80
},
compact: {
columns: 5,
gap: 8,
size: 70
},
comfortable: {
columns: 3,
gap: 24,
size: 100
},
list: {
columns: 1,
gap: 12,
size: 60
},
custom: {}
},
init: function() {
const settings = this.getSettings();
this.applyLayout(settings);
this.setupEventListeners();
this.toggleCustomSettings(settings.type === 'custom');
this.updateUI(settings);
},
getSettings: function() {
const storedSettings = Storage.get('gridLayout');
return { ...this.defaults, ...storedSettings };
},
saveSettings: function(settings) {
Storage.set('gridLayout', settings);
},
updateUI: function(settings) {
const layoutTypeSelect = document.getElementById('grid-layout-type');
if (layoutTypeSelect) {
layoutTypeSelect.value = settings.type;
const event = new Event('change');
layoutTypeSelect.dispatchEvent(event);
const customSelectDiv = layoutTypeSelect.closest('.custom-select');
if (customSelectDiv) {
const selectSelected = customSelectDiv.querySelector('.select-selected');
if (selectSelected) {
const selectedOption = layoutTypeSelect.options[layoutTypeSelect.selectedIndex];
selectSelected.textContent = selectedOption.textContent;
}
}
}
const columnsInput = document.getElementById('grid-columns');
const gapInput = document.getElementById('grid-gap');
const sizeInput = document.getElementById('grid-size');
if (columnsInput) columnsInput.value = settings.columns;
if (gapInput) gapInput.value = settings.gap;
if (sizeInput) {
sizeInput.value = settings.size;
sizeInput.disabled = !settings.resizable;
if (!settings.resizable) {
sizeInput.setAttribute('title', 'Enable \'Resizable Items\' to customize item size');
sizeInput.style.cursor = 'not-allowed';
} else {
sizeInput.removeAttribute('title');
sizeInput.style.cursor = 'auto';
}
}
const resizableToggle = document.getElementById('toggle-resizable');
if (resizableToggle) resizableToggle.checked = settings.resizable;
this.toggleCustomSettings(settings.type === 'custom');
},
applyLayout: function(settings) {
const grid = document.getElementById('shortcuts-grid');
if (!grid) return;
grid.classList.remove('grid-default', 'grid-compact', 'grid-comfortable', 'grid-list', 'grid-custom');
grid.classList.add(`grid-${settings.type}`);
if (settings.type === 'custom') {
grid.style.gridTemplateColumns = `repeat(${settings.columns}, minmax(${settings.size}px, 1fr))`;
grid.style.gap = `${settings.gap}px`;
} else {
const layoutConfig = this.layouts[settings.type];
if (layoutConfig) {
grid.style.gridTemplateColumns = `repeat(${layoutConfig.columns}, minmax(${layoutConfig.size}px, 1fr))`;
grid.style.gap = `${layoutConfig.gap}px`;
if (settings.type === 'list') {
grid.style.gridTemplateColumns = '1fr';
}
} else {
grid.style.gridTemplateColumns = '';
grid.style.gap = '';
}
}
this.applyResizable(settings.resizable);
},
setupEventListeners: function() {
const layoutTypeSelect = document.getElementById('grid-layout-type');
if (layoutTypeSelect) {
layoutTypeSelect.addEventListener('change', () => {
const settings = this.getSettings();
settings.type = layoutTypeSelect.value;
this.saveSettings(settings);
this.applyLayout(settings);
this.toggleCustomSettings(settings.type === 'custom');
});
}
const columnsInput = document.getElementById('grid-columns');
const gapInput = document.getElementById('grid-gap');
const sizeInput = document.getElementById('grid-size');
const validateInputValue = (input) => {
const min = parseInt(input.getAttribute('min'), 10);
const max = parseInt(input.getAttribute('max'), 10);
let value = parseInt(input.value, 10);
if (isNaN(value)) {
value = parseInt(input.defaultValue, 10);
}
if (value < min) value = min;
if (value > max) value = max;
input.value = value;
return value;
};
[columnsInput, gapInput, sizeInput].forEach(input => {
if (input) {
input.addEventListener('input', () => {
validateInputValue(input);
});
input.addEventListener('change', () => {
const value = validateInputValue(input);
const settings = this.getSettings();
settings[input.id.split('-')[1]] = value;
this.saveSettings(settings);
this.applyLayout(settings);
});
}
});
const resizableToggle = document.getElementById('toggle-resizable');
if (resizableToggle) {
resizableToggle.addEventListener('change', () => {
const settings = this.getSettings();
settings.resizable = resizableToggle.checked;
this.saveSettings(settings);
this.applyLayout(settings);
this.toggleItemSizeInput(settings.resizable);
});
}
const resetButton = document.getElementById('reset-layout');
if (resetButton) {
resetButton.addEventListener('click', () => {
this.resetToDefaults();
});
}
},
toggleItemSizeInput: function(enabled) {
const sizeInput = document.getElementById('grid-size');
if (sizeInput) {
sizeInput.disabled = !enabled;
if (enabled) {
sizeInput.removeAttribute('title');
sizeInput.style.cursor = 'auto';
} else {
sizeInput.setAttribute('title', 'Enable \'Resizable Items\' to customize item size');
sizeInput.style.cursor = 'not-allowed';
}
}
},
toggleCustomSettings: function(show) {
const customSettings = document.getElementById('custom-grid-settings');
if (customSettings) {
if (show) {
customSettings.classList.remove('hidden');
} else {
customSettings.classList.add('hidden');
}
}
},
applyResizable: function(resizable) {
const shortcuts = document.querySelectorAll('.shortcut');
shortcuts.forEach(shortcut => {
const existingHandle = shortcut.querySelector('.resize-handle');
if (existingHandle) {
existingHandle.remove();
}
shortcut.classList.remove('resizable');
if (resizable) {
shortcut.classList.add('resizable');
const resizeHandle = document.createElement('div');
resizeHandle.className = 'resize-handle';
shortcut.appendChild(resizeHandle);
this.setupResizeEvents(shortcut, resizeHandle);
}
});
},
setupResizeEvents: function(shortcut, handle) {
let startX, startY, startWidth, startHeight;
const startResize = (e) => {
e.preventDefault();
shortcut.classList.add('resizing');
startX = e.clientX;
startY = e.clientY;
startWidth = shortcut.offsetWidth;
startHeight = shortcut.offsetHeight;
document.addEventListener('mousemove', resize);
document.addEventListener('mouseup', stopResize);
};
const resize = (e) => {
const newWidth = startWidth + (e.clientX - startX);
const newHeight = startHeight + (e.clientY - startY);
shortcut.style.width = `${Math.max(80, newWidth)}px`;
shortcut.style.height = `${Math.max(80, newHeight)}px`;
};
const stopResize = () => {
shortcut.classList.remove('resizing');
document.removeEventListener('mousemove', resize);
document.removeEventListener('mouseup', stopResize);
};
handle.addEventListener('mousedown', startResize);
},
resetToDefaults: function() {
this.saveSettings(this.defaults);
this.updateUI(this.defaults);
this.applyLayout(this.defaults);
this.toggleCustomSettings(false);
const visibilitySettings = {
showGreeting: true,
showSearch: true,
showShortcuts: true,
showAddButton: true,
showGrid: true
};
Storage.set('visibility', visibilitySettings);
Storage.set('show_greeting', true);
Storage.set('show_search', true);
Storage.set('show_shortcuts', true);
Storage.set('show_addShortcut', true);
const greetingToggle = document.getElementById('toggle-greeting');
const searchToggle = document.getElementById('toggle-search');
const shortcutsToggle = document.getElementById('toggle-shortcuts');
const addButtonToggle = document.getElementById('toggle-add-shortcut');
if (greetingToggle) greetingToggle.checked = true;
if (searchToggle) searchToggle.checked = true;
if (shortcutsToggle) shortcutsToggle.checked = true;
if (addButtonToggle) addButtonToggle.checked = true;
const greeting = document.getElementById('greeting');
const search = document.getElementById('search-container');
const shortcuts = document.getElementById('shortcuts-grid');
const addButton = document.getElementById('add-shortcut');
const showElement = (element) => {
if (element) {
element.style.visibility = 'visible';
element.style.opacity = '1';
element.style.position = 'relative';
element.style.pointerEvents = 'auto';
}
};
showElement(greeting);
showElement(search);
showElement(shortcuts);
showElement(addButton);
},
reset: function() {
this.resetToDefaults();
notifications.show('Layout settings reset to defaults.', 'success');
return this.defaults;
}
};
document.addEventListener('DOMContentLoaded', () => {
GridLayout.init();
});
window.addEventListener('load', () => {
setTimeout(() => {
if (!window.gridLayoutInitialized) {
GridLayout.init();
}
}, 500);
});

View File

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

View File

@ -1,145 +1,128 @@
if ('serviceWorker' in navigator) { // Greeting functionality
navigator.serviceWorker.register('/sw.js'); async function updateGreeting() {
} const greeting = document.getElementById('greeting');
if (!greeting) return;
async function updateGreeting() {
const greeting = document.getElementById('greeting'); const customFormat = Storage.get('customGreeting');
if (!greeting) return; if (customFormat) {
const formattedGreeting = await settings.formatGreeting(customFormat);
const customFormat = Storage.get('customGreeting'); if (formattedGreeting) {
if (customFormat) { greeting.textContent = formattedGreeting;
const formattedGreeting = await settings.formatGreeting(customFormat); greeting.style.opacity = '0';
if (formattedGreeting) { setTimeout(() => {
greeting.textContent = formattedGreeting; greeting.style.opacity = '1';
greeting.style.opacity = '0'; }, 100);
setTimeout(() => { return;
greeting.style.opacity = '1'; }
}, 100); }
return;
} const hour = new Date().getHours();
} const isAnonymous = Storage.get('anonymousMode') || false;
const userName = isAnonymous ?
const hour = new Date().getHours(); (Storage.get('anonymousName') || anonymousNames.generate()) :
const isAnonymous = Storage.get('anonymousMode') || false; (Storage.get('userName') || 'Friend');
const userName = isAnonymous ?
(Storage.get('anonymousName') || anonymousNames.generate()) : let timeGreeting = 'Hello';
(Storage.get('userName') || 'Friend'); if (hour >= 5 && hour < 12) timeGreeting = 'Good Morning';
else if (hour >= 12 && hour < 17) timeGreeting = 'Good Afternoon';
let timeGreeting = 'Hello'; else if (hour >= 17 && hour < 20) timeGreeting = 'Good Evening';
if (hour >= 5 && hour < 12) timeGreeting = 'Good Morning'; else timeGreeting = 'Good Night';
else if (hour >= 12 && hour < 17) timeGreeting = 'Good Afternoon';
else if (hour >= 17 && hour < 20) timeGreeting = 'Good Evening'; greeting.textContent = `${timeGreeting}, ${userName}!`;
else timeGreeting = 'Good Night'; greeting.style.opacity = '0';
setTimeout(() => {
greeting.textContent = `${timeGreeting}, ${userName}!`; greeting.style.opacity = '1';
greeting.style.opacity = '0'; }, 100);
setTimeout(() => { }
greeting.style.opacity = '1';
}, 100); // Modal handling
} function initModalHandlers() {
const modals = document.querySelectorAll('.modal');
function initModalHandlers() {
const modals = document.querySelectorAll('.modal'); modals.forEach(modal => {
modal.addEventListener('click', (e) => {
modals.forEach(modal => { if (e.target === modal && !modal.classList.contains('onboarding-modal')) {
modal.addEventListener('click', (e) => { closeModal(modal);
if (e.target === modal && !modal.classList.contains('onboarding-modal')) { }
closeModal(modal); });
}
}); const modalContent = modal.querySelector('.modal-content');
if (modalContent) {
const modalContent = modal.querySelector('.modal-content'); modalContent.addEventListener('click', (e) => {
if (modalContent) { e.stopPropagation();
modalContent.addEventListener('click', (e) => { });
e.stopPropagation(); }
});
} document.querySelectorAll('.modal .close-button').forEach(button => {
button.addEventListener('click', () => {
document.querySelectorAll('.modal .close-button').forEach(button => { const modal = button.closest('.modal');
button.addEventListener('click', () => { if (modal) {
const modal = button.closest('.modal'); closeModal(modal);
if (modal) { }
closeModal(modal); });
} });
}); });
}); }
});
} function openModal(modal) {
if (!modal) return;
function openModal(modal) { modal.classList.remove('hidden');
if (!modal) return; requestAnimationFrame(() => {
modal.classList.add('active');
const contextMenu = document.querySelector('.context-menu'); });
if (contextMenu) { }
contextMenu.classList.add('hidden');
} function closeModal(modal) {
if (!modal) return;
modal.classList.remove('hidden'); modal.classList.remove('active');
requestAnimationFrame(() => { setTimeout(() => {
modal.classList.add('active'); modal.classList.add('hidden');
}); }, 300);
} }
function closeModal(modal) { // Application initialization
if (!modal) return; document.addEventListener('DOMContentLoaded', () => {
modal.classList.remove('active'); ['greeting', 'search', 'shortcuts', 'addShortcut'].forEach(element => {
setTimeout(() => { const isVisible = Storage.get(`show_${element}`);
modal.classList.add('hidden'); if (isVisible === false) {
}, 300); const elementNode = document.getElementById(element === 'search' ? 'search-container' : element);
} if (elementNode) elementNode.style.display = 'none';
}
document.addEventListener('DOMContentLoaded', () => { });
if (typeof settings !== 'undefined' && typeof settings.updateVisibility === 'function') {
settings.updateVisibility(); if (!Storage.get('onboardingComplete')) {
} else { onboarding.start();
['greeting', 'search', 'shortcuts', 'addShortcut'].forEach(element => { } else {
const isVisible = Storage.get(`show_${element}`); document.getElementById('main-content').classList.remove('hidden');
if (isVisible === false) { }
const elementNode = document.getElementById(element === 'search' ? 'search-container' :
element === 'addShortcut' ? 'add-shortcut' : element); search.init();
shortcuts.init();
if (elementNode) { settings.init();
elementNode.style.visibility = 'hidden'; initModalHandlers();
elementNode.style.opacity = '0';
elementNode.style.position = 'absolute'; updateGreeting();
elementNode.style.pointerEvents = 'none'; setInterval(updateGreeting, 60000);
}
} const settingsButton = document.getElementById('settings-button');
}); const settingsModal = document.getElementById('settings-modal');
}
settingsButton.addEventListener('click', () => {
if (!Storage.get('onboardingComplete')) { openModal(settingsModal);
onboarding.start(); });
} else {
document.getElementById('main-content').classList.remove('hidden'); keybinds.init();
} });
search.init(); // Global keydown event handler
shortcuts.init(); document.addEventListener('keydown', (e) => {
settings.init(); if (e.key === 'Enter') {
initModalHandlers(); const activeModal = document.querySelector('.modal.active');
if (activeModal && !activeModal.matches('#settings-modal')) {
updateGreeting(); const primaryButton = activeModal.querySelector('.btn-primary');
setInterval(updateGreeting, 60000); if (primaryButton) {
primaryButton.click();
const settingsButton = document.getElementById('settings-button'); }
const settingsModal = document.getElementById('settings-modal'); }
}
settingsButton.addEventListener('click', () => { });
openModal(settingsModal);
});
keybinds.init();
});
document.addEventListener('keydown', (e) => {
if (e.key === 'Enter') {
const activeModal = document.querySelector('.modal.active');
if (activeModal && !activeModal.matches('#settings-modal')) {
const primaryButton = activeModal.querySelector('.btn-primary');
if (primaryButton) {
primaryButton.click();
}
}
}
});

View File

@ -1,121 +1,131 @@
class NotificationSystem { // Notification System Class
constructor() { class NotificationSystem {
this.container = document.getElementById('notification-container'); constructor() {
this.notifications = new Map(); this.container = document.getElementById('notification-container');
} this.notifications = new Map();
}
show(message, type = 'info', duration = 3000) {
const id = Date.now().toString(); // Display a new notification
const notification = document.createElement('div'); show(message, type = 'info', duration = 3000) {
notification.className = `notification notification-${type}`; const id = Date.now().toString();
const notification = document.createElement('div');
const icon = this.createIcon(type); notification.className = `notification notification-${type}`;
const content = this.createContent(message);
const closeBtn = this.createCloseButton(id); // Create notification elements
const progress = this.createProgressBar(type); const icon = this.createIcon(type);
const content = this.createContent(message);
notification.appendChild(icon); const closeBtn = this.createCloseButton(id);
notification.appendChild(content); const progress = this.createProgressBar(type);
notification.appendChild(closeBtn);
notification.appendChild(progress); // Assemble notification
notification.appendChild(icon);
this.container.appendChild(notification); notification.appendChild(content);
notification.appendChild(closeBtn);
setTimeout(() => this.remove(id), duration); notification.appendChild(progress);
this.notifications.set(id, { this.container.appendChild(notification);
element: notification,
duration // Set removal timer
}); setTimeout(() => this.remove(id), duration);
this.updateProgress(id); // Store notification reference
this.notifications.set(id, {
return id; element: notification,
} duration
});
remove(id) {
const notification = this.notifications.get(id); this.updateProgress(id);
if (notification) {
notification.element.style.animation = 'slideOutRight 0.3s cubic-bezier(0.16, 1, 0.3, 1)'; return id;
setTimeout(() => { }
notification.element.remove();
this.notifications.delete(id); // Remove a notification
}, 300); 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)';
updateProgress(id) { setTimeout(() => {
const notification = this.notifications.get(id); notification.element.remove();
if (notification) { this.notifications.delete(id);
const progress = notification.element.querySelector('.notification-progress'); }, 300);
const startTime = Date.now(); }
}
const update = () => {
const elapsed = Date.now() - startTime; // Update progress bar
const percent = 100 - (elapsed / notification.duration * 100); updateProgress(id) {
const notification = this.notifications.get(id);
if (percent > 0) { if (notification) {
progress.style.width = `${percent}%`; const progress = notification.element.querySelector('.notification-progress');
requestAnimationFrame(update); const startTime = Date.now();
}
}; const update = () => {
const elapsed = Date.now() - startTime;
requestAnimationFrame(update); const percent = 100 - (elapsed / notification.duration * 100);
}
} if (percent > 0) {
progress.style.width = `${percent}%`;
createIcon(type) { requestAnimationFrame(update);
const icon = document.createElement('i'); }
switch(type) { };
case 'success':
icon.className = 'fas fa-check-circle'; requestAnimationFrame(update);
icon.style.color = 'var(--success-color, #4caf50)'; }
break; }
case 'error':
icon.className = 'fas fa-times-circle'; // Helper methods for creating notification elements
icon.style.color = 'var(--error-color, #f44336)'; createIcon(type) {
break; const icon = document.createElement('i');
case 'info': switch(type) {
default: case 'success':
icon.className = 'fas fa-info-circle'; icon.className = 'fas fa-check-circle';
icon.style.color = 'var(--info-color, #2196f3)'; icon.style.color = 'var(--success-color, #4caf50)';
break; break;
} case 'error':
return icon; icon.className = 'fas fa-times-circle';
} icon.style.color = 'var(--error-color, #f44336)';
break;
createContent(message) { case 'info':
const content = document.createElement('div'); default:
content.className = 'notification-content'; icon.className = 'fas fa-info-circle';
content.innerHTML = message; icon.style.color = 'var(--info-color, #2196f3)';
return content; break;
} }
return icon;
createCloseButton(id) { }
const closeBtn = document.createElement('button');
closeBtn.className = 'notification-close'; createContent(message) {
closeBtn.innerHTML = '<i class="fas fa-times"></i>'; const content = document.createElement('div');
closeBtn.onclick = () => this.remove(id); content.className = 'notification-content';
return closeBtn; content.textContent = message;
} return content;
}
createProgressBar(type) {
const progress = document.createElement('div'); createCloseButton(id) {
progress.className = 'notification-progress'; const closeBtn = document.createElement('button');
switch(type) { closeBtn.className = 'notification-close';
case 'success': closeBtn.innerHTML = '<i class="fas fa-times"></i>';
progress.style.background = 'var(--success-color, #4caf50)'; closeBtn.onclick = () => this.remove(id);
break; return closeBtn;
case 'error': }
progress.style.background = 'var(--error-color, #f44336)';
break; createProgressBar(type) {
case 'info': const progress = document.createElement('div');
default: progress.className = 'notification-progress';
progress.style.background = 'var(--info-color, #2196f3)'; switch(type) {
break; case 'success':
} progress.style.background = 'var(--success-color, #4caf50)';
return progress; 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(); const notifications = new NotificationSystem();

View File

@ -1,327 +1,136 @@
const onboarding = { // Onboarding module
currentStep: 1, const onboarding = {
totalSteps: 5, // Core functions
settings: {}, isComplete: () => {
lastNotification: 0, return Storage.get('onboardingComplete') === true;
isCompleting: false, },
notificationShown: false,
start: () => {
showNotification(message, type = 'info') { const modal = document.getElementById('onboarding-modal');
const now = Date.now(); const mainContent = document.getElementById('main-content');
if (now - this.lastNotification >= 500) { const importDataBtn = document.getElementById('import-data-btn');
this.lastNotification = now; const startFreshBtn = document.getElementById('start-fresh-btn');
notifications.show(message, type); const fileInput = document.getElementById('onboarding-import');
}
}, if (!onboarding.isComplete()) {
modal.classList.remove('hidden');
isComplete: () => { modal.classList.add('active');
return Storage.get('onboardingComplete') === true; mainContent.classList.add('hidden');
},
startFreshBtn.addEventListener('click', () => {
start: () => { document.querySelector('[data-step="1"]').classList.add('hidden');
const onboardingContainer = document.getElementById('onboarding-container'); document.querySelector('[data-step="2"]').classList.remove('hidden');
const mainContent = document.getElementById('main-content'); document.getElementById('next-step-btn').addEventListener('click', () => onboarding.nextStep(2));
const fileInput = document.getElementById('onboarding-import'); });
document.getElementById('notification-container').style.zIndex = "20000"; importDataBtn.addEventListener('click', () => fileInput.click());
if (!onboarding.isComplete()) { // Data import handling
document.body.style.overflow = 'hidden'; fileInput.addEventListener('change', async (e) => {
onboardingContainer.classList.remove('hidden'); if (e.target.files.length > 0) {
try {
onboarding.initProgressDots(); const file = e.target.files[0];
onboarding.setupEventListeners(); const text = await file.text();
const data = JSON.parse(text);
const theme = Storage.get('theme') || 'light';
document.body.setAttribute('data-theme', theme); if (!data.settings || typeof data.settings !== 'object' ||
!Array.isArray(data.shortcuts)) {
document.querySelectorAll('.step-ob').forEach(step => { throw new Error('Invalid data structure');
if (step.dataset.step !== "1") { return;
step.classList.remove('active-ob'); }
}
}); Object.entries(data.settings).forEach(([key, value]) => {
Storage.set(key, value);
const firstStep = document.querySelector('.step-ob[data-step="1"]'); });
firstStep.classList.add('active-ob');
Storage.set('shortcuts', data.shortcuts);
document.getElementById('prev-step').style.visibility = 'hidden';
if (data.keybinds) {
const nextButton = document.getElementById('next-step'); Storage.set('keybinds', data.keybinds);
nextButton.innerHTML = 'Next <svg><use href="#icon-arrow-right"/></svg>'; }
if (onboarding.currentStep > 1) { // Initialize components
nextButton.disabled = true; search.init();
nextButton.classList.add('disabled-ob'); shortcuts.init();
} settings.init();
updateGreeting();
mainContent.classList.add('hidden'); document.body.setAttribute('data-theme', data.settings.theme || 'light');
} else {
mainContent.classList.remove('hidden'); // Update UI visibility
} ['greeting', 'search', 'shortcuts', 'addShortcut'].forEach(element => {
const isVisible = data.settings[`show_${element}`];
fileInput.addEventListener('change', async (e) => { const elementNode = document.getElementById(element === 'search' ? 'search-container' : element);
if (e.target.files.length > 0) { if (elementNode) {
try { elementNode.style.display = isVisible === false ? 'none' : 'block';
const file = e.target.files[0]; }
const text = await file.text(); });
const data = JSON.parse(text);
Storage.set('onboardingComplete', true);
if (!data.settings || !data.shortcuts || !Array.isArray(data.shortcuts)) { modal.classList.add('hidden');
throw new Error('Invalid data structure'); mainContent.classList.remove('hidden');
}
const userName = Storage.get('userName') || 'Guest';
Object.entries(data.settings).forEach(([key, value]) => { notifications.show(`Welcome back, ${userName}! 👋`, 'success');
Storage.set(key, value); } catch (error) {
}); notifications.show('Failed to import data: Invalid file format!', 'error');
fileInput.value = '';
Storage.set('shortcuts', data.shortcuts); }
}
if (data.keybinds) { });
Storage.set('keybinds', data.keybinds);
} // Search engine selection
const engines = document.querySelectorAll('.search-engine-option');
Storage.set('onboardingComplete', true); engines.forEach(engine => {
engine.addEventListener('click', () => {
localStorage.setItem('showWelcomeAfterImport', 'true'); engines.forEach(e => e.classList.remove('selected'));
engine.classList.add('selected');
window.location.reload(); });
} catch (error) { });
onboarding.showNotification('Failed to import data: Invalid file format!', 'error');
fileInput.value = ''; document.getElementById('complete-setup-btn').addEventListener('click', onboarding.complete);
} } else {
} modal.classList.add('hidden');
}); mainContent.classList.remove('hidden');
}, }
},
setupEventListeners: () => {
document.querySelectorAll('.option-card-ob').forEach(card => { // Onboarding step navigation
card.addEventListener('click', () => { nextStep: (currentStep) => {
const step = card.closest('.step-ob'); const currentStepEl = document.querySelector(`[data-step="${currentStep}"]`);
const stepNumber = parseInt(step.dataset.step); const nextStepEl = document.querySelector(`[data-step="${currentStep + 1}"]`);
const cards = step.querySelectorAll('.option-card-ob');
const nextButton = document.getElementById('next-step'); if (currentStep === 2) {
const name = document.getElementById('user-name').value.trim();
if (card.dataset.action === 'import-data') { if (!name) {
if (!card.classList.contains('selected-ob')) { notifications.show('Please enter your name!', 'error');
document.getElementById('onboarding-import').click(); return;
} }
cards.forEach(c => c.classList.remove('selected-ob')); Storage.set('userName', name);
card.classList.add('selected-ob'); }
return;
} currentStepEl.classList.add('hidden');
nextStepEl.classList.remove('hidden');
cards.forEach(c => c.classList.remove('selected-ob')); nextStepEl.classList.add('visible');
card.classList.add('selected-ob'); },
card.style.transform = 'scale(1.05)'; // Finalize onboarding
setTimeout(() => { complete: () => {
card.style.transform = 'scale(1.02)'; const selectedEngine = document.querySelector('.search-engine-option.selected');
}, 150); if (!selectedEngine) {
notifications.show('Please select a search engine!', 'error');
nextButton.disabled = false; return;
nextButton.classList.remove('disabled-ob'); }
if (card.dataset.theme) { const searchEngine = selectedEngine.dataset.engine;
onboarding.settings.theme = card.dataset.theme; Storage.set('searchEngine', searchEngine);
document.body.setAttribute('data-theme', card.dataset.theme); Storage.set('onboardingComplete', true);
} else if (card.dataset.font) {
onboarding.settings.fontFamily = card.dataset.font; const modal = document.getElementById('onboarding-modal');
document.documentElement.style.setProperty('--font-family', card.dataset.font); const mainContent = document.getElementById('main-content');
} else if (card.dataset.engine) {
onboarding.settings.searchEngine = card.dataset.engine; modal.classList.add('hidden');
} mainContent.classList.remove('hidden');
}); notifications.show('Welcome to your new tab! 👋', 'success');
}); updateGreeting();
}
const nameInput = document.getElementById('user-name'); };
const nextButton = document.getElementById('next-step');
nameInput.addEventListener('input', (e) => {
const name = e.target.value.trim();
if (name) {
onboarding.settings.userName = name;
if (onboarding.currentStep === 4) {
nextButton.disabled = false;
nextButton.classList.remove('disabled-ob');
}
} else {
if (onboarding.currentStep === 4) {
nextButton.disabled = true;
nextButton.classList.add('disabled-ob');
}
}
});
document.getElementById('prev-step').addEventListener('click', () => {
if (onboarding.currentStep > 1) {
onboarding.navigateToStep(onboarding.currentStep - 1);
}
});
document.getElementById('next-step').addEventListener('click', () => {
let canProceed = true;
if (onboarding.currentStep === 2 && !onboarding.settings.theme) {
onboarding.showNotification('Please select a theme!', 'error');
canProceed = false;
}
else if (onboarding.currentStep === 3 && !onboarding.settings.fontFamily) {
onboarding.showNotification('Please select a font!', 'error');
canProceed = false;
}
else if (onboarding.currentStep === 4) {
const name = document.getElementById('user-name').value.trim();
if (!name) {
onboarding.showNotification('Please enter your name!', 'error');
canProceed = false;
} else {
onboarding.settings.userName = name;
}
}
else if (onboarding.currentStep === 5 && !onboarding.settings.searchEngine) {
onboarding.showNotification('Please select a search engine!', 'error');
canProceed = false;
}
if (canProceed) {
if (onboarding.currentStep < onboarding.totalSteps) {
onboarding.navigateToStep(onboarding.currentStep + 1);
} else {
onboarding.isCompleting = true;
localStorage.setItem('showWelcomeAfterImport', 'true');
onboarding.complete();
}
}
});
document.querySelectorAll('.step-ob').forEach(step => {
if (step.dataset.step !== "1" && step.dataset.step !== "4") {
const firstOption = step.querySelector('.option-card-ob');
if (firstOption) {
setTimeout(() => {
firstOption.click();
}, 100);
}
}
});
},
navigateToStep: (step) => {
const prevButton = document.getElementById('prev-step');
const nextButton = document.getElementById('next-step');
const currentStepEl = document.querySelector(`.step-ob[data-step="${onboarding.currentStep}"]`);
if (currentStepEl) {
currentStepEl.classList.remove('active-ob');
}
setTimeout(() => {
const targetStepEl = document.querySelector(`.step-ob[data-step="${step}"]`);
if (targetStepEl) {
targetStepEl.classList.add('active-ob');
}
onboarding.currentStep = step;
prevButton.style.visibility = step === 1 ? 'hidden' : 'visible';
if (step === onboarding.totalSteps) {
nextButton.innerHTML = 'Get Started <svg><use href="#icon-sparkle"/></svg>';
} else {
nextButton.innerHTML = 'Next <svg><use href="#icon-arrow-right"/></svg>';
}
if ((step === 2 && !onboarding.settings.theme) ||
(step === 3 && !onboarding.settings.fontFamily) ||
(step === 5 && !onboarding.settings.searchEngine)) {
nextButton.disabled = true;
nextButton.classList.add('disabled-ob');
} else if (step === 4) {
const name = document.getElementById('user-name').value.trim();
if (!name) {
nextButton.disabled = true;
nextButton.classList.add('disabled-ob');
} else {
nextButton.disabled = false;
nextButton.classList.remove('disabled-ob');
}
} else if (step === 1) {
nextButton.disabled = false;
nextButton.classList.remove('disabled-ob');
}
onboarding.updateProgressDots();
}, 100);
},
initProgressDots: () => {
const container = document.querySelector('.progress-dots-ob');
container.innerHTML = '';
for (let i = 0; i < onboarding.totalSteps; i++) {
const dot = document.createElement('div');
dot.className = 'dot-ob' + (i === 0 ? ' active-ob' : '');
container.appendChild(dot);
}
},
updateProgressDots: () => {
const dots = document.querySelectorAll('.dot-ob');
dots.forEach((dot, index) => {
dot.classList.toggle('active-ob', index + 1 === onboarding.currentStep);
});
},
complete: () => {
const onboardingContainer = document.getElementById('onboarding-container');
const mainContent = document.getElementById('main-content');
if (!onboarding.settings.theme) onboarding.settings.theme = 'light';
if (!onboarding.settings.fontFamily) onboarding.settings.fontFamily = 'Inter';
if (!onboarding.settings.searchEngine) onboarding.settings.searchEngine = 'google';
if (!onboarding.settings.userName) onboarding.settings.userName = 'User';
document.body.setAttribute('data-theme', onboarding.settings.theme);
document.documentElement.style.setProperty('--font-family', onboarding.settings.fontFamily);
Object.entries(onboarding.settings).forEach(([key, value]) => {
Storage.set(key, value);
});
Storage.set('onboardingComplete', true);
setTimeout(() => {
onboardingContainer.classList.add('hidden');
mainContent.classList.remove('hidden');
document.body.style.overflow = '';
search.init();
shortcuts.init();
settings.init();
updateGreeting();
setTimeout(() => {
if (!onboarding.notificationShown) {
onboarding.notificationShown = true;
onboarding.showNotification('Welcome to your new JSTAR Tab! 🎉', 'success');
}
}, 100);
}, 500);
}
};
document.addEventListener('DOMContentLoaded', () => {
onboarding.start();
if (onboarding.isComplete() && localStorage.getItem('showWelcomeAfterImport') === 'true') {
localStorage.removeItem('showWelcomeAfterImport');
setTimeout(() => {
notifications.show('Welcome to your new JSTAR Tab! 🎉', 'success');
}, 500);
}
});

View File

@ -1,80 +1,47 @@
const search = { const search = {
engines: { // Supported search engines and their URLs
google: { engines: {
url: 'https://www.google.com/search?q=', google: 'https://www.google.com/search?q=',
icon: 'https://www.google.com/s2/favicons?domain=google.com&sz=32', bing: 'https://www.bing.com/search?q=',
name: 'Google' duckduckgo: 'https://duckduckgo.com/?q=',
}, brave: 'https://search.brave.com/search?q=',
bing: { qwant: 'https://www.qwant.com/?q=',
url: 'https://www.bing.com/search?q=', searxng: 'https://searx.org/search?q='
icon: 'https://www.google.com/s2/favicons?domain=bing.com&sz=32', },
name: 'Bing'
}, // Perform search using selected engine
duckduckgo: { perform: () => {
url: 'https://duckduckgo.com/?q=', const searchBar = document.getElementById('search-bar');
icon: 'https://www.google.com/s2/favicons?domain=duckduckgo.com&sz=32', const query = searchBar.value.trim();
name: 'DuckDuckGo' const engine = Storage.get('searchEngine') || 'google';
},
brave: { if (query) {
url: 'https://search.brave.com/search?q=', const searchUrl = search.engines[engine] + encodeURIComponent(query);
icon: 'https://www.google.com/s2/favicons?domain=brave.com&sz=32', window.location.href = searchUrl;
name: 'Brave' }
}, },
qwant: {
url: 'https://www.qwant.com/?q=', // Initialize search functionality
icon: 'https://www.google.com/s2/favicons?domain=qwant.com&sz=32', init: () => {
name: 'Qwant' const searchBar = document.getElementById('search-bar');
}, const searchButton = document.getElementById('search-button');
searxng: {
url: 'https://searx.be/search?q=', searchBar.addEventListener('keypress', (e) => {
icon: 'https://www.google.com/s2/favicons?domain=searx.be&sz=32', if (e.key === 'Enter') {
name: 'SearXNG' search.perform();
} }
}, });
init: () => { searchButton.addEventListener('click', search.perform);
const searchBar = document.getElementById('search-bar');
const searchButton = document.getElementById('search-button'); // Global keyboard shortcut to focus search bar
document.addEventListener('keydown', (e) => {
searchBar.addEventListener('keypress', (e) => { if (e.key === '/' &&
if (e.key === 'Enter') { !['INPUT', 'TEXTAREA'].includes(document.activeElement.tagName) &&
search.perform(); window.getSelection().toString() === '') {
} e.preventDefault();
}); searchBar.focus();
}
searchButton.addEventListener('click', search.perform); });
}
document.addEventListener('keydown', (e) => { };
if (e.key === '/' &&
!['INPUT', 'TEXTAREA'].includes(document.activeElement.tagName) &&
window.getSelection().toString() === '') {
e.preventDefault();
searchBar.focus();
}
});
const searchEngine = Storage.get('searchEngine') || 'google';
search.updateSearchEngineIcon(searchEngine);
},
updateSearchEngineIcon(engine) {
const searchIcon = document.querySelector('#search-container .search-icon img');
if (!searchIcon) return;
searchIcon.src = this.engines[engine].icon;
},
perform: () => {
const searchBar = document.getElementById('search-bar');
const query = searchBar.value.trim();
const engine = Storage.get('searchEngine') || 'google';
if (query) {
const searchUrl = search.engines[engine].url + encodeURIComponent(query);
window.location.href = searchUrl;
}
}
};
document.addEventListener('DOMContentLoaded', () => {
search.init();
});

File diff suppressed because it is too large Load Diff

View File

@ -1,275 +1,274 @@
const shortcuts = { const shortcuts = {
MAX_SHORTCUTS: 12, MAX_SHORTCUTS: 12,
validateAndFormatUrl: (url) => { // URL Validation
if (!/^https?:\/\//i.test(url)) { validateAndFormatUrl: (url) => {
url = 'https://' + url; if (!/^https?:\/\//i.test(url)) {
} url = 'https://' + url;
}
try {
new URL(url); try {
return url; new URL(url);
} catch (e) { return url;
return false; } catch (e) {
} return false;
}, }
},
add: (url, name) => {
const currentShortcuts = Storage.get('shortcuts') || []; // Shortcut Management
if (currentShortcuts.length >= shortcuts.MAX_SHORTCUTS) { add: (url, name) => {
notifications.show('Maximum shortcuts limit reached!', 'error'); const currentShortcuts = Storage.get('shortcuts') || [];
return; if (currentShortcuts.length >= shortcuts.MAX_SHORTCUTS) {
} notifications.show('Maximum shortcuts limit reached!', 'error');
return;
const formattedUrl = shortcuts.validateAndFormatUrl(url); }
if (!formattedUrl) {
notifications.show('Invalid URL format!', 'error'); const formattedUrl = shortcuts.validateAndFormatUrl(url);
return; if (!formattedUrl) {
} notifications.show('Invalid URL format!', 'error');
return;
currentShortcuts.push({ url: formattedUrl, name }); }
Storage.set('shortcuts', currentShortcuts);
shortcuts.render(); currentShortcuts.push({ url: formattedUrl, name });
CacheUpdater.update(); Storage.set('shortcuts', currentShortcuts);
}, shortcuts.render();
},
remove: (index) => {
const currentShortcuts = Storage.get('shortcuts') || []; remove: (index) => {
currentShortcuts.splice(index, 1); const currentShortcuts = Storage.get('shortcuts') || [];
Storage.set('shortcuts', currentShortcuts); currentShortcuts.splice(index, 1);
shortcuts.render(); Storage.set('shortcuts', currentShortcuts);
notifications.show('Shortcut removed!', 'success'); shortcuts.render();
CacheUpdater.update(); notifications.show('Shortcut removed!', 'success');
}, },
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 };
Storage.set('shortcuts', currentShortcuts); Storage.set('shortcuts', currentShortcuts);
shortcuts.render(); shortcuts.render();
notifications.show('Shortcut updated!', 'success'); notifications.show('Shortcut updated!', 'success');
CacheUpdater.update(); },
},
// UI Interactions
showContextMenu: (e, index) => { showContextMenu: (e, index) => {
e.preventDefault(); e.preventDefault();
const menu = document.getElementById('context-menu'); const menu = document.getElementById('context-menu');
const rect = e.target.getBoundingClientRect(); const rect = e.target.getBoundingClientRect();
menu.style.top = `${e.clientY}px`; menu.style.top = `${e.clientY}px`;
menu.style.left = `${e.clientX}px`; menu.style.left = `${e.clientX}px`;
menu.classList.remove('hidden'); menu.classList.remove('hidden');
menu.dataset.shortcutIndex = index; menu.dataset.shortcutIndex = index;
const handleClickOutside = (event) => { const handleClickOutside = (event) => {
if (!menu.contains(event.target)) { if (!menu.contains(event.target)) {
menu.classList.add('hidden'); menu.classList.add('hidden');
document.removeEventListener('click', handleClickOutside); document.removeEventListener('click', handleClickOutside);
} }
}; };
setTimeout(() => { setTimeout(() => {
document.addEventListener('click', handleClickOutside); document.addEventListener('click', handleClickOutside);
}, 0); }, 0);
}, },
render: () => { // Rendering
const grid = document.getElementById('shortcuts-grid'); render: () => {
const currentShortcuts = Storage.get('shortcuts') || []; const grid = document.getElementById('shortcuts-grid');
const isAnonymous = Storage.get('anonymousMode') || false; const currentShortcuts = Storage.get('shortcuts') || [];
const isAnonymous = Storage.get('anonymousMode') || false;
grid.innerHTML = '';
grid.innerHTML = '';
currentShortcuts.forEach((shortcut, index) => {
const element = document.createElement('div'); currentShortcuts.forEach((shortcut, index) => {
element.className = `shortcut ${isAnonymous ? 'blurred' : ''}`; const element = document.createElement('div');
element.className = `shortcut ${isAnonymous ? 'blurred' : ''}`;
element.dataset.index = index;
const icon = document.createElement('img');
const icon = document.createElement('img'); icon.src = `https://www.google.com/s2/favicons?domain=${shortcut.url}&sz=64`;
icon.src = `https://www.google.com/s2/favicons?domain=${shortcut.url}&sz=64`; icon.alt = shortcut.name;
icon.alt = shortcut.name;
icon.draggable = false; const name = document.createElement('span');
name.textContent = shortcut.name;
const name = document.createElement('span');
name.textContent = shortcut.name; element.appendChild(icon);
element.appendChild(name);
element.appendChild(icon);
element.appendChild(name); element.addEventListener('click', (e) => {
if (e.ctrlKey) {
element.addEventListener('click', (e) => { window.open(shortcut.url, '_blank');
if (!grid.classList.contains('grid-draggable') || !e.target.closest('.shortcut').classList.contains('drag-active')) { } else {
if (e.ctrlKey) { window.location.href = shortcut.url;
window.open(shortcut.url, '_blank'); }
} else { });
window.location.href = shortcut.url;
} element.addEventListener('contextmenu', (e) => {
} e.preventDefault();
}); const menu = document.getElementById('context-menu');
element.addEventListener('contextmenu', (e) => { menu.style.top = `${e.pageY}px`;
e.preventDefault(); menu.style.left = `${e.pageX}px`;
const menu = document.getElementById('context-menu'); menu.classList.remove('hidden');
menu.dataset.shortcutIndex = index;
menu.style.top = `${e.pageY}px`;
menu.style.left = `${e.pageX}px`; const closeMenu = (event) => {
menu.classList.remove('hidden'); if (!menu.contains(event.target)) {
menu.dataset.shortcutIndex = index; menu.classList.add('hidden');
document.removeEventListener('click', closeMenu);
const closeMenu = (event) => { }
if (!menu.contains(event.target)) { };
menu.classList.add('hidden');
document.removeEventListener('click', closeMenu); setTimeout(() => {
} document.addEventListener('click', closeMenu);
}; }, 0);
});
setTimeout(() => {
document.addEventListener('click', closeMenu); grid.appendChild(element);
}, 0); });
}); },
grid.appendChild(element); // Initialization
}); init: () => {
}, const addShortcutButton = document.getElementById('add-shortcut');
const modal = document.getElementById('add-shortcut-modal');
init: () => { const closeBtn = modal.querySelector('.close-modal');
const addShortcutButton = document.getElementById('add-shortcut');
const modal = document.getElementById('add-shortcut-modal'); if (closeBtn) {
const closeBtn = modal.querySelector('.close-modal'); closeBtn.addEventListener('click', () => {
modal.classList.remove('active');
if (closeBtn) { setTimeout(() => {
closeBtn.addEventListener('click', () => { modal.classList.add('hidden');
modal.classList.remove('active'); document.getElementById('shortcut-url').value = '';
setTimeout(() => { document.getElementById('shortcut-name').value = '';
modal.classList.add('hidden'); }, 300);
document.getElementById('shortcut-url').value = ''; });
document.getElementById('shortcut-name').value = ''; }
}, 300);
}); if (addShortcutButton) {
} addShortcutButton.addEventListener('click', (e) => {
e.stopPropagation();
if (addShortcutButton) { const currentShortcuts = Storage.get('shortcuts') || [];
addShortcutButton.addEventListener('click', (e) => {
e.stopPropagation(); if (currentShortcuts.length >= shortcuts.MAX_SHORTCUTS) {
const currentShortcuts = Storage.get('shortcuts') || []; notifications.show('Maximum shortcuts limit reached!', 'error');
return;
if (currentShortcuts.length >= shortcuts.MAX_SHORTCUTS) { }
notifications.show('Maximum shortcuts limit reached!', 'error');
return; if (modal) {
} modal.classList.remove('hidden');
modal.classList.add('active');
if (modal) {
modal.classList.remove('hidden'); const urlInput = document.getElementById('shortcut-url');
modal.classList.add('active'); const nameInput = document.getElementById('shortcut-name');
const urlInput = document.getElementById('shortcut-url'); const saveShortcutButton = document.getElementById('save-shortcut');
const nameInput = document.getElementById('shortcut-name'); if (saveShortcutButton) {
saveShortcutButton.onclick = () => {
const saveShortcutButton = document.getElementById('save-shortcut'); const url = urlInput.value.trim();
if (saveShortcutButton) { const name = nameInput.value.trim();
saveShortcutButton.onclick = () => {
const url = urlInput.value.trim(); if (url && name) {
const name = nameInput.value.trim(); try {
new URL(url);
if (url && name) { shortcuts.add(url, name);
try { modal.classList.remove('active');
new URL(url); setTimeout(() => {
shortcuts.add(url, name); modal.classList.add('hidden');
modal.classList.remove('active'); urlInput.value = '';
setTimeout(() => { nameInput.value = '';
modal.classList.add('hidden'); }, 300);
urlInput.value = ''; notifications.show('Shortcut added successfully!', 'success');
nameInput.value = ''; } catch (e) {
}, 300); notifications.show('Invalid URL format!', 'error');
notifications.show('Shortcut added successfully!', 'success'); }
} catch (e) { }
notifications.show('Invalid URL format!', 'error'); };
} }
}
}; const cancelShortcutButton = document.getElementById('cancel-shortcut');
} if (cancelShortcutButton) {
cancelShortcutButton.onclick = () => {
const cancelShortcutButton = document.getElementById('cancel-shortcut'); modal.classList.remove('active');
if (cancelShortcutButton) { setTimeout(() => {
cancelShortcutButton.onclick = () => { modal.classList.add('hidden');
modal.classList.remove('active'); urlInput.value = '';
setTimeout(() => { nameInput.value = '';
modal.classList.add('hidden'); }, 300);
urlInput.value = ''; };
nameInput.value = ''; }
}, 300); }
}; });
} }
}
}); // Context menu actions
} const contextMenu = document.getElementById('context-menu');
if (contextMenu) {
const contextMenu = document.getElementById('context-menu'); contextMenu.addEventListener('click', (e) => {
if (contextMenu) { const action = e.target.closest('.context-menu-item')?.dataset.action;
contextMenu.addEventListener('click', (e) => { const index = parseInt(contextMenu.dataset.shortcutIndex);
const action = e.target.closest('.context-menu-item')?.dataset.action;
const index = parseInt(contextMenu.dataset.shortcutIndex); if (action === 'edit') {
const currentShortcuts = Storage.get('shortcuts') || [];
if (action === 'edit') { const shortcut = currentShortcuts[index];
const currentShortcuts = Storage.get('shortcuts') || []; const modal = document.getElementById('edit-shortcut-modal');
const shortcut = currentShortcuts[index];
const modal = document.getElementById('edit-shortcut-modal'); if (modal) {
const urlInput = document.getElementById('edit-shortcut-url');
if (modal) { const nameInput = document.getElementById('edit-shortcut-name');
const urlInput = document.getElementById('edit-shortcut-url');
const nameInput = document.getElementById('edit-shortcut-name'); urlInput.value = shortcut.url;
nameInput.value = shortcut.name;
urlInput.value = shortcut.url;
nameInput.value = shortcut.name; modal.classList.remove('hidden');
modal.classList.add('active');
modal.classList.remove('hidden');
modal.classList.add('active'); const saveButton = document.getElementById('save-edit-shortcut');
const closeButton = document.getElementById('close-edit-shortcut');
const saveButton = document.getElementById('save-edit-shortcut'); const cancelButton = document.getElementById('cancel-edit-shortcut');
const closeButton = document.getElementById('close-edit-shortcut');
const cancelButton = document.getElementById('cancel-edit-shortcut'); const closeModal = () => {
modal.classList.remove('active');
const closeModal = () => { setTimeout(() => {
modal.classList.remove('active'); modal.classList.add('hidden');
setTimeout(() => { }, 300);
modal.classList.add('hidden'); };
}, 300);
}; const handleSave = () => {
const newUrl = urlInput.value.trim();
const handleSave = () => { const newName = nameInput.value.trim();
const newUrl = urlInput.value.trim();
const newName = nameInput.value.trim(); if (newUrl && newName) {
const formattedUrl = shortcuts.validateAndFormatUrl(newUrl);
if (newUrl && newName) { if (formattedUrl) {
const formattedUrl = shortcuts.validateAndFormatUrl(newUrl); shortcuts.edit(index, formattedUrl, newName);
if (formattedUrl) { closeModal();
shortcuts.edit(index, formattedUrl, newName); } else {
closeModal(); notifications.show('Invalid URL format!', 'error');
} else { }
notifications.show('Invalid URL format!', 'error'); }
} };
}
}; saveButton.onclick = handleSave;
closeButton.onclick = closeModal;
saveButton.onclick = handleSave; cancelButton.onclick = closeModal;
closeButton.onclick = closeModal; }
cancelButton.onclick = closeModal; } else if (action === 'delete') {
} shortcuts.remove(index);
} else if (action === 'delete') { } else if (action === 'open-new-tab') {
shortcuts.remove(index); const currentShortcuts = Storage.get('shortcuts') || [];
} else if (action === 'open-new-tab') { const shortcut = currentShortcuts[index];
const currentShortcuts = Storage.get('shortcuts') || [];
const shortcut = currentShortcuts[index]; // Open the URL of the shortcut in a new tab
if (shortcut && shortcut.url) {
if (shortcut && shortcut.url) { window.open(shortcut.url, '_blank');
window.open(shortcut.url, '_blank'); }
} }
}
contextMenu.classList.add('hidden');
contextMenu.classList.add('hidden'); });
}); }
}
shortcuts.render();
shortcuts.render(); }
} };
};

View File

@ -1,4 +1,9 @@
/**
* Storage utility object for managing localStorage operations
* All methods handle JSON parsing/stringifying and error cases
*/
const Storage = { const Storage = {
// Retrieve and parse stored value
get: (key) => { get: (key) => {
try { try {
return JSON.parse(localStorage.getItem(key)); return JSON.parse(localStorage.getItem(key));
@ -7,6 +12,7 @@ const Storage = {
} }
}, },
// Store value as JSON string
set: (key, value) => { set: (key, value) => {
try { try {
localStorage.setItem(key, JSON.stringify(value)); localStorage.setItem(key, JSON.stringify(value));
@ -16,6 +22,7 @@ const Storage = {
} }
}, },
// Delete specific key
remove: (key) => { remove: (key) => {
try { try {
localStorage.removeItem(key); localStorage.removeItem(key);
@ -25,6 +32,7 @@ const Storage = {
} }
}, },
// Remove all stored data
clear: () => { clear: () => {
try { try {
localStorage.clear(); localStorage.clear();

View File

@ -1,93 +0,0 @@
const versionUrl = 'https://www.junaid.xyz/projects/jstar-tab/version.txt';
const manifestVersion = chrome.runtime.getManifest().version;
function compareVersions(version1, version2) {
const v1 = version1.split('.').map(Number);
const v2 = version2.split('.').map(Number);
for (let i = 0; i < Math.max(v1.length, v2.length); i++) {
const diff = (v1[i] || 0) - (v2[i] || 0);
if (diff !== 0) return diff;
}
return 0;
}
async function checkForUpdate() {
try {
const response = await fetch(versionUrl, { cache: 'no-store' });
const latestVersion = await response.text();
handleVersionComparison(latestVersion);
} catch (error) {
const cachedResponse = await caches.match(versionUrl);
if (cachedResponse) {
const cachedVersion = await cachedResponse.text();
handleVersionComparison(cachedVersion, true);
} else {
updateVersionIcon(null);
}
}
}
function handleVersionComparison(latestVersion, isCached = false) {
latestVersion = latestVersion.trim();
const comparison = compareVersions(latestVersion, manifestVersion);
updateVersionIcon(comparison, latestVersion);
if (comparison > 0) {
const alertMessage = `New version ${latestVersion} available! ` +
`<a href="https://github.com/DevJSTAR/JSTAR-Tab/releases/${latestVersion}" ` +
`target="_blank" style="color: #2196F3;">Update now</a>`;
if (isCached) {
notifications.show(`${alertMessage} (Showing cached version)`, 'info', 8000);
} else {
notifications.show(alertMessage, 'info');
}
}
}
function updateVersionIcon(versionComparison, latestVersion) {
const versionIcon = document.getElementById('version-icon');
if (!versionIcon) return;
versionIcon.className = 'version-icon fas';
versionIcon.style.color = '';
versionIcon.removeAttribute('title');
latestVersion = latestVersion.trim();
if (versionComparison === 0) {
versionIcon.classList.add('fa-check-circle');
versionIcon.style.color = '#4caf50';
versionIcon.title = 'Youre up to date! Enjoy the latest features.';
} else if (versionComparison > 0) {
versionIcon.classList.add('fa-exclamation-circle');
versionIcon.style.color = '#ff9800';
versionIcon.title = `A newer version (${latestVersion}) is available! Dont miss out on the new goodies.`;
} else if (versionComparison < 0) {
versionIcon.classList.add('fa-question-circle');
versionIcon.style.color = '#2196f3';
versionIcon.title = 'Whoa! Youre ahead of the curve. Are you from the future?';
} else {
versionIcon.classList.add('fa-times-circle');
versionIcon.style.color = '#f44336';
versionIcon.title = 'Unable to check the version. Is the internet sleeping?';
}
}
function showUpdateNotification(latestVersion) {
const message = `Version ${latestVersion} is available! <a href="https://github.com/DevJSTAR/JSTAR-Tab/releases/${latestVersion}" target="_blank">Update now</a>!`;
notifications.show(message, 'info');
}
checkForUpdate();
document.addEventListener('DOMContentLoaded', () => {
const version = chrome.runtime.getManifest().version;
const versionElement = document.getElementById('extension-version');
if (versionElement) {
versionElement.innerHTML = `JSTAR Tab v<a href="https://github.com/DevJSTAR/JSTAR-Tab/releases/${version}" target="_blank" style="color: inherit;">${version}</a> <span id="version-icon" class="version-icon"></span>`;
}
});

View File

@ -1,7 +1,7 @@
{ {
"manifest_version": 3, "manifest_version": 3,
"name": "JSTAR Tab", "name": "JSTAR Tab",
"version": "3.0.0", "version": "2.6.2",
"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"
@ -32,4 +32,4 @@
], ],
"matches": ["<all_urls>"] "matches": ["<all_urls>"]
}] }]
} }

999
style.css Normal file
View File

@ -0,0 +1,999 @@
/* 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(0, 0, 0, 0.75);
--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: #dddddd;
border: 2px solid #dddddd;
}
.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;
transition: transform 0.2s ease;
}
#add-shortcut:hover {
background: var(--primary-hover);
transform: scale(1.1);
}
#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: scale(1.079);
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);
transition: transform 0.4s ease;
}
.settings-button:hover {
transform: rotate(180deg);
}
.settings-button:active {
transform: rotate(360deg);
}
.settings-panel {
max-height: 80vh;
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: var(--scrollbar-thumb) var(--scrollbar-track);
}
.settings-panel::-webkit-scrollbar {
width: 8px;
}
.settings-panel::-webkit-scrollbar-track {
background: var(--scrollbar-track);
border-radius: 0 24px 24px 0;
}
.settings-panel::-webkit-scrollbar-thumb {
background-color: var(--scrollbar-thumb);
border-radius: 4px;
}
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 0rem;
}
.modal-header h2 {
font-size: 1.5rem;
font-weight: 600;
}
.btn-icon {
background: none;
border: none;
color: var(--text);
cursor: pointer;
padding: 0.5rem;
font-size: 1.25rem;
opacity: 0.6;
}
.btn-icon:hover {
opacity: 1;
}
.settings-section {
padding: 1.5rem 0;
border-bottom: 1px solid var(--border);
}
.settings-section:last-child {
border-bottom: none;
padding-bottom: 0;
}
.settings-section h3 {
margin-bottom: 1.5rem;
font-weight: 600;
}
.setting-item {
display: flex;
flex-direction: column;
padding: 0.75rem 0;
}
.setting-item.horizontal {
flex-direction: row;
align-items: center;
justify-content: space-between;
}
.setting-item .setting-label {
font-weight: 500;
margin-bottom: 0.5rem;
}
.data-management-buttons {
display: flex;
flex-direction: column;
gap: 1rem;
}
.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%;
transition: transform 0.3s ease;
}
input:checked + .toggle-slider {
background-color: var(--text);
}
input:checked + .toggle-slider:before {
transform: translateX(24px);
}
#keybind-url-combo {
margin-bottom: 0.5rem;
}
#keybind-url {
margin-top: 0.5rem;
}
.keybind-container {
display: flex;
gap: 0.5rem;
align-items: center;
}
.clear-keybind {
padding: 0.25rem 0.5rem;
background: var(--accent);
border: none;
border-radius: 4px;
color: var(--text);
cursor: pointer;
font-size: 1.2rem;
line-height: 1;
}
.clear-keybind:hover {
opacity: 0.8;
}
.format-hint {
display: block;
font-size: 0.75rem;
color: var(--text-secondary);
opacity: 0.7;
margin-top: 0.25rem;
}
/* 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;
}
/* Mobile-first responsive styles */
@media screen and (max-width: 768px) {
.center-container {
padding: 1rem;
gap: 2rem;
}
#greeting {
font-size: 1.5rem;
height: 36px;
}
.search-container {
max-width: 100%;
}
#search-bar {
padding: 1rem;
font-size: 0.9rem;
}
.search-engine-options {
grid-template-columns: repeat(2, 1fr);
gap: 0.75rem;
}
.search-engine-option {
padding: 1rem;
}
.search-engine-option img {
width: 24px;
height: 24px;
}
#shortcuts-grid {
grid-template-columns: repeat(auto-fill, minmax(80px, 1fr));
gap: 0.75rem;
padding: 0.5rem;
}
.shortcut {
padding: 0.75rem;
}
.shortcut img {
width: 24px;
height: 24px;
}
.shortcut span {
font-size: 0.75rem;
}
.modal-content {
width: 95%;
padding: 1.5rem;
border-radius: 16px;
}
.step h2 {
font-size: 1.5rem;
}
.setting-item {
padding: 0.5rem 0;
}
.setting-label {
font-size: 0.9rem;
}
input[type="text"], select {
padding: 0.75rem;
font-size: 0.9rem;
}
.notification {
width: 90%;
max-width: none;
margin: 0.5rem;
padding: 0.75rem;
}
}
/* Styles for very small screens */
@media screen and (max-width: 480px) {
#greeting {
font-size: 1.25rem;
height: 32px;
}
.search-engine-options {
display: grid;
grid-template-columns: 1fr;
gap: 0.5rem;
padding: 0.5rem;
}
#shortcuts-grid {
grid-template-columns: repeat(auto-fill, minmax(70px, 1fr));
gap: 0.5rem;
}
.modal-content {
padding: 1rem;
}
.modal-actions {
flex-direction: column;
gap: 0.5rem;
}
.modal-actions button {
width: 100%;
}
.format-hint {
font-size: 0.7rem;
line-height: 1.2;
}
}
/* Landscape orientation adjustments */
@media screen and (max-height: 480px) and (orientation: landscape) {
.center-container {
padding: 0.5rem;
gap: 1rem;
}
#greeting {
font-size: 1.25rem;
height: 32px;
margin-bottom: 0;
}
.modal-content {
height: 90vh;
overflow-y: auto;
}
.search-engine-options {
grid-template-columns: repeat(3, 1fr);
gap: 0.5rem;
}
}
/* Mobile-specific styles for onboarding */
@media screen and (max-width: 480px) {
.search-engine-options {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 0.75rem;
padding: 0.5rem;
}
.search-engine-option {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
padding: 1rem;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 12px;
cursor: pointer;
transition: all 0.2s ease;
aspect-ratio: 1;
}
.search-engine-option img {
width: 24px;
height: 24px;
margin-bottom: 0.5rem;
}
.search-engine-option span {
font-size: 0.85rem;
font-weight: 500;
}
.search-engine-option.selected {
background: #eeeeee;
border: 2px solid var(--border);
}
.onboarding-modal .modal-content {
width: 90%;
max-width: none;
padding: 1.25rem;
}
.step h2 {
font-size: 1.35rem;
margin-bottom: 1rem;
}
#complete-setup-btn {
width: 100%;
margin-top: 1rem;
padding: 0.875rem;
font-size: 0.95rem;
}
}
.import-options {
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
width: 100%;
max-width: 300px;
margin: 0 auto;
}
.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;
}

60
sw.js
View File

@ -1,60 +0,0 @@
const CACHE_PREFIX = 'jstartab-cache';
const VERSION_URL = 'https://www.junaid.xyz/projects/jstar-tab/version.txt';
const STATIC_CACHE = 'jstartab-static';
self.addEventListener('install', (event) => {
self.skipWaiting();
event.waitUntil(
caches.open(STATIC_CACHE).then(cache => cache.add(VERSION_URL))
);
});
self.addEventListener('activate', (event) => {
event.waitUntil(clients.claim());
});
self.addEventListener('message', async (event) => {
if (event.data.action === 'updateFavicons') {
const faviconUrls = event.data.urls;
const currentDate = new Date();
const cacheName = `${CACHE_PREFIX}-${currentDate.toISOString().split('T')[0]}`;
const cache = await caches.open(cacheName);
await cache.addAll(faviconUrls);
const keys = await caches.keys();
keys.forEach((key) => {
if (key.startsWith(CACHE_PREFIX) && key !== cacheName) {
caches.delete(key);
}
});
}
});
self.addEventListener('fetch', (event) => {
const request = event.request;
if (event.request.url.startsWith('https://www.google.com/s2/favicons')) {
event.respondWith(
caches.match(event.request).then((response) => {
return response || fetch(event.request).then((fetchResponse) => {
const cacheCopy = fetchResponse.clone();
caches.open(`${CACHE_PREFIX}-${new Date().toISOString().split('T')[0]}`)
.then((cache) => cache.put(event.request, cacheCopy));
return fetchResponse;
});
})
);
}
if (request.url === VERSION_URL) {
event.respondWith(
caches.open(STATIC_CACHE).then(cache =>
fetch(request).then(networkResponse => {
cache.put(request, networkResponse.clone());
return networkResponse;
}).catch(() => cache.match(request))
)
);
}
});