Add files via upload
419
css/onboarding.css
Normal file
|
@ -0,0 +1,419 @@
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
}
|
2240
css/style.css
Normal file
BIN
images/backgrounds/cherry.png
Normal file
After Width: | Height: | Size: 339 KiB |
BIN
images/backgrounds/mommies.png
Normal file
After Width: | Height: | Size: 648 KiB |
BIN
images/backgrounds/peachs-castle.png
Normal file
After Width: | Height: | Size: 1.7 MiB |
BIN
images/backgrounds/windows-xp.jpg
Normal file
After Width: | Height: | Size: 235 KiB |
BIN
images/favicon.png
Normal file
After Width: | Height: | Size: 29 KiB |
BIN
images/icon128.png
Normal file
After Width: | Height: | Size: 2.9 KiB |
BIN
images/icon16.png
Normal file
After Width: | Height: | Size: 584 B |
BIN
images/icon48.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
1233
index.html
159
js/backgrounds.js
Normal file
|
@ -0,0 +1,159 @@
|
||||||
|
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));
|
||||||
|
});
|
86
js/cache-handler.js
Normal file
|
@ -0,0 +1,86 @@
|
||||||
|
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();
|
||||||
|
});
|
337
js/grid-layout.js
Normal file
|
@ -0,0 +1,337 @@
|
||||||
|
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);
|
||||||
|
});
|
532
js/keybinds.js
|
@ -1,265 +1,267 @@
|
||||||
// List of keys that cannot be used as keybinds
|
const FORBIDDEN_KEYS = [
|
||||||
const FORBIDDEN_KEYS = [
|
'Tab', 'CapsLock', 'Meta', 'ContextMenu',
|
||||||
'Tab', 'CapsLock', 'Meta', 'ContextMenu',
|
'F1', 'F2', 'F3', 'F4', 'F5', 'F6', 'F7', 'F8', 'F9', 'F10', 'F11', 'F12',
|
||||||
'F1', 'F2', 'F3', 'F4', 'F5', 'F6', 'F7', 'F8', 'F9', 'F10', 'F11', 'F12',
|
'Home', 'End', 'PageUp', 'PageDown', 'Insert', 'Delete', 'ScrollLock', 'Pause', 'NumLock',
|
||||||
'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') || {};
|
||||||
|
|
||||||
// URL keybind handling
|
const urlInput = document.getElementById('keybind-url');
|
||||||
const urlInput = document.getElementById('keybind-url');
|
const urlComboInput = document.getElementById('keybind-url-combo');
|
||||||
const urlComboInput = document.getElementById('keybind-url-combo');
|
|
||||||
|
if (this.bindings.url) {
|
||||||
if (this.bindings.url) {
|
urlInput.value = this.bindings.url.url || '';
|
||||||
urlInput.value = this.bindings.url.url || '';
|
urlComboInput.value = this.bindings.url.keys || '';
|
||||||
urlComboInput.value = this.bindings.url.keys || '';
|
}
|
||||||
}
|
|
||||||
|
let lastSavedUrl = urlInput.value;
|
||||||
let lastSavedUrl = urlInput.value;
|
|
||||||
|
function isValidUrl(string) {
|
||||||
function isValidUrl(string) {
|
try {
|
||||||
try {
|
const urlString = string.match(/^https?:\/\//) ? string : `https://${string}`;
|
||||||
const urlString = string.match(/^https?:\/\//) ? string : `https://${string}`;
|
new URL(urlString);
|
||||||
new URL(urlString);
|
return true;
|
||||||
return true;
|
} catch (_) {
|
||||||
} catch (_) {
|
return false;
|
||||||
return false;
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
urlInput.addEventListener('input', () => {
|
||||||
urlInput.addEventListener('input', () => {
|
if (!this.bindings.url) {
|
||||||
if (!this.bindings.url) {
|
this.bindings.url = { url: '', keys: '' };
|
||||||
this.bindings.url = { url: '', keys: '' };
|
}
|
||||||
}
|
this.bindings.url.url = urlInput.value;
|
||||||
this.bindings.url.url = urlInput.value;
|
Storage.set('keybinds', this.bindings);
|
||||||
Storage.set('keybinds', this.bindings);
|
});
|
||||||
});
|
|
||||||
|
urlInput.addEventListener('keydown', (e) => {
|
||||||
urlInput.addEventListener('keydown', (e) => {
|
if (e.key === 'Enter') {
|
||||||
if (e.key === 'Enter') {
|
e.preventDefault();
|
||||||
e.preventDefault();
|
urlInput.blur();
|
||||||
urlInput.blur();
|
}
|
||||||
}
|
});
|
||||||
});
|
|
||||||
|
urlInput.addEventListener('blur', () => {
|
||||||
urlInput.addEventListener('blur', () => {
|
const currentUrl = urlInput.value.trim();
|
||||||
const currentUrl = urlInput.value.trim();
|
|
||||||
|
if (currentUrl === lastSavedUrl) {
|
||||||
if (currentUrl === lastSavedUrl) {
|
return;
|
||||||
return;
|
}
|
||||||
}
|
|
||||||
|
if (currentUrl) {
|
||||||
if (currentUrl) {
|
if (isValidUrl(currentUrl)) {
|
||||||
if (isValidUrl(currentUrl)) {
|
lastSavedUrl = currentUrl;
|
||||||
lastSavedUrl = currentUrl;
|
notifications.show('URL saved.', 'success');
|
||||||
notifications.show('URL saved.', 'success');
|
} else {
|
||||||
} else {
|
notifications.show('Please enter a valid URL.', 'error');
|
||||||
notifications.show('Please enter a valid URL.', 'error');
|
urlInput.value = lastSavedUrl;
|
||||||
urlInput.value = lastSavedUrl;
|
this.bindings.url.url = lastSavedUrl;
|
||||||
this.bindings.url.url = lastSavedUrl;
|
Storage.set('keybinds', this.bindings);
|
||||||
Storage.set('keybinds', this.bindings);
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
});
|
|
||||||
|
const keybindInputs = document.querySelectorAll('[id^="keybind-"]');
|
||||||
// Keybind input handling
|
keybindInputs.forEach(input => {
|
||||||
const keybindInputs = document.querySelectorAll('[id^="keybind-"]');
|
if (input.id === 'keybind-url') {
|
||||||
keybindInputs.forEach(input => {
|
const urlBinding = this.bindings['url'];
|
||||||
if (input.id === 'keybind-url') {
|
if (urlBinding && urlBinding.url) {
|
||||||
const urlBinding = this.bindings['url'];
|
input.value = urlBinding.url;
|
||||||
if (urlBinding && urlBinding.url) {
|
}
|
||||||
input.value = urlBinding.url;
|
|
||||||
}
|
input.addEventListener('input', () => {
|
||||||
|
if (this.bindings['url']) {
|
||||||
input.addEventListener('input', () => {
|
this.bindings['url'].url = input.value;
|
||||||
if (this.bindings['url']) {
|
Storage.set('keybinds', this.bindings);
|
||||||
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]) {
|
||||||
const action = input.id.replace('keybind-url-combo', 'url').replace('keybind-', '');
|
input.value = this.bindings[action].keys;
|
||||||
if (this.bindings[action]) {
|
}
|
||||||
input.value = this.bindings[action].keys;
|
|
||||||
}
|
let currentKeys = new Set();
|
||||||
|
let isProcessingKeybind = false;
|
||||||
let currentKeys = new Set();
|
|
||||||
let isProcessingKeybind = false;
|
input.addEventListener('keydown', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
input.addEventListener('keydown', (e) => {
|
|
||||||
e.preventDefault();
|
if (e.key === 'Escape') {
|
||||||
|
input.blur();
|
||||||
if (e.key === 'Escape') {
|
return;
|
||||||
input.blur();
|
}
|
||||||
return;
|
|
||||||
}
|
if (e.ctrlKey) {
|
||||||
|
notifications.show('CTRL key combinations are not allowed.', 'error');
|
||||||
if (e.ctrlKey) {
|
isProcessingKeybind = true;
|
||||||
notifications.show('CTRL key combinations are not allowed.', 'error');
|
return;
|
||||||
isProcessingKeybind = true;
|
}
|
||||||
return;
|
|
||||||
}
|
if (FORBIDDEN_KEYS.includes(e.key)) {
|
||||||
|
notifications.show('This key cannot be used as a keybind.', 'error');
|
||||||
if (FORBIDDEN_KEYS.includes(e.key)) {
|
isProcessingKeybind = true;
|
||||||
notifications.show('This key cannot be used as a keybind.', 'error');
|
return;
|
||||||
isProcessingKeybind = true;
|
}
|
||||||
return;
|
|
||||||
}
|
isProcessingKeybind = false;
|
||||||
|
|
||||||
isProcessingKeybind = false;
|
if (e.key !== 'Alt' && e.key !== 'Shift') {
|
||||||
|
currentKeys.add(e.key);
|
||||||
if (e.key !== 'Alt' && e.key !== 'Shift') {
|
}
|
||||||
currentKeys.add(e.key);
|
if (e.altKey) currentKeys.add('Alt');
|
||||||
}
|
if (e.shiftKey) currentKeys.add('Shift');
|
||||||
if (e.altKey) currentKeys.add('Alt');
|
|
||||||
if (e.shiftKey) currentKeys.add('Shift');
|
input.value = Array.from(currentKeys).join('+');
|
||||||
|
});
|
||||||
input.value = Array.from(currentKeys).join('+');
|
|
||||||
});
|
input.addEventListener('keyup', (e) => {
|
||||||
|
if (isProcessingKeybind) {
|
||||||
input.addEventListener('keyup', (e) => {
|
currentKeys.clear();
|
||||||
if (isProcessingKeybind) {
|
return;
|
||||||
currentKeys.clear();
|
}
|
||||||
return;
|
|
||||||
}
|
if (e.key === 'Alt' || e.key === 'Shift') {
|
||||||
|
if (currentKeys.size === 1) {
|
||||||
if (e.key === 'Alt' || e.key === 'Shift') {
|
notifications.show('Add another key with Alt or Shift.', 'error');
|
||||||
if (currentKeys.size === 1) {
|
}
|
||||||
notifications.show('Add another key with Alt or Shift.', 'error');
|
currentKeys.clear();
|
||||||
}
|
input.value = this.bindings[action]?.keys || '';
|
||||||
currentKeys.clear();
|
return;
|
||||||
input.value = this.bindings[action]?.keys || '';
|
}
|
||||||
return;
|
|
||||||
}
|
const combo = Array.from(currentKeys).join('+');
|
||||||
|
|
||||||
const combo = Array.from(currentKeys).join('+');
|
if (!combo) return;
|
||||||
|
|
||||||
if (!combo) return;
|
const duplicate = Object.entries(this.bindings).find(([key, value]) =>
|
||||||
|
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');
|
||||||
if (duplicate) {
|
currentKeys.clear();
|
||||||
notifications.show('This keybind is already in use.', 'error');
|
input.value = this.bindings[action]?.keys || '';
|
||||||
currentKeys.clear();
|
return;
|
||||||
input.value = this.bindings[action]?.keys || '';
|
}
|
||||||
return;
|
|
||||||
}
|
this.bindings[action] = {
|
||||||
|
keys: combo,
|
||||||
this.bindings[action] = {
|
url: action === 'url' ? document.getElementById('keybind-url').value : null
|
||||||
keys: combo,
|
};
|
||||||
url: action === 'url' ? document.getElementById('keybind-url').value : null
|
Storage.set('keybinds', this.bindings);
|
||||||
};
|
notifications.show('Keybind saved.', 'success');
|
||||||
Storage.set('keybinds', this.bindings);
|
});
|
||||||
notifications.show('Keybind saved.', 'success');
|
|
||||||
});
|
input.addEventListener('blur', () => {
|
||||||
|
currentKeys.clear();
|
||||||
input.addEventListener('blur', () => {
|
input.value = this.bindings[action]?.keys || '';
|
||||||
currentKeys.clear();
|
});
|
||||||
input.value = this.bindings[action]?.keys || '';
|
});
|
||||||
});
|
|
||||||
});
|
document.querySelectorAll('.clear-keybind').forEach(button => {
|
||||||
|
button.addEventListener('click', () => {
|
||||||
// Clear keybind button handling
|
const action = button.dataset.for;
|
||||||
document.querySelectorAll('.clear-keybind').forEach(button => {
|
const input = document.getElementById(`keybind-${action}-combo`) ||
|
||||||
button.addEventListener('click', () => {
|
document.getElementById(`keybind-${action}`);
|
||||||
const action = button.dataset.for;
|
|
||||||
const input = document.getElementById(`keybind-${action}-combo`) ||
|
input.value = '';
|
||||||
document.getElementById(`keybind-${action}`);
|
if (action === 'url') {
|
||||||
|
document.getElementById('keybind-url').value = '';
|
||||||
input.value = '';
|
}
|
||||||
if (action === 'url') {
|
|
||||||
document.getElementById('keybind-url').value = '';
|
delete this.bindings[action];
|
||||||
}
|
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;
|
||||||
|
|
||||||
// Global keybind listener
|
const keys = [];
|
||||||
document.addEventListener('keydown', (e) => {
|
if (e.altKey) keys.push('Alt');
|
||||||
if (e.target.tagName === 'INPUT') return;
|
if (e.shiftKey) keys.push('Shift');
|
||||||
|
if (e.key !== 'Alt' && e.key !== 'Shift') keys.push(e.key);
|
||||||
const keys = [];
|
|
||||||
if (e.altKey) keys.push('Alt');
|
const combo = keys.join('+');
|
||||||
if (e.shiftKey) keys.push('Shift');
|
|
||||||
if (e.key !== 'Alt' && e.key !== 'Shift') keys.push(e.key);
|
Object.entries(this.bindings).forEach(([action, binding]) => {
|
||||||
|
if (binding.keys === combo) {
|
||||||
const combo = keys.join('+');
|
e.preventDefault();
|
||||||
|
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');
|
||||||
// Execute the action associated with a keybind
|
setTimeout(() => {
|
||||||
executeAction(action, binding) {
|
settingsPage.classList.add('hidden');
|
||||||
const activeModal = document.querySelector('.modal.active');
|
}, 300);
|
||||||
if (activeModal) {
|
}
|
||||||
closeModal(activeModal);
|
|
||||||
}
|
const activeModal = document.querySelector('.modal.active');
|
||||||
|
|
||||||
switch (action) {
|
switch (action) {
|
||||||
case 'settings':
|
case 'settings':
|
||||||
const settingsModal = document.getElementById('settings-modal');
|
if (settingsPage.classList.contains('hidden')) {
|
||||||
if (settingsModal === activeModal) {
|
notifications.show('Opening settings.', 'info');
|
||||||
notifications.show('Settings closed.', 'info');
|
settings.updateSettingsUI();
|
||||||
settings.updateSettingsUI();
|
settingsPage.classList.remove('hidden');
|
||||||
} else {
|
setTimeout(() => {
|
||||||
notifications.show('Opening settings...', 'info');
|
settingsPage.classList.add('active');
|
||||||
settings.updateSettingsUI();
|
}, 10);
|
||||||
openModal(settingsModal);
|
} else {
|
||||||
}
|
notifications.show('Settings closed.', 'info');
|
||||||
break;
|
settings.updateSettingsUI();
|
||||||
case 'add-shortcut':
|
}
|
||||||
const currentShortcuts = Storage.get('shortcuts') || [];
|
break;
|
||||||
if (currentShortcuts.length >= shortcuts.MAX_SHORTCUTS) {
|
case 'add-shortcut':
|
||||||
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 shortcutModal = document.getElementById('add-shortcut-modal');
|
}
|
||||||
if (shortcutModal === activeModal) {
|
|
||||||
notifications.show('Add shortcut closed.', 'info');
|
const shortcutModal = document.getElementById('add-shortcut-modal');
|
||||||
} else {
|
if (shortcutModal === activeModal) {
|
||||||
notifications.show('Opening add shortcut...', 'info');
|
notifications.show('Add shortcut menu closed.', 'info');
|
||||||
openModal(shortcutModal);
|
} else {
|
||||||
}
|
notifications.show('Opening add shortcut menu.', 'info');
|
||||||
break;
|
openModal(shortcutModal);
|
||||||
case 'anonymous':
|
}
|
||||||
settings.toggleAnonymousMode();
|
break;
|
||||||
break;
|
case 'anonymous':
|
||||||
case 'theme':
|
settings.toggleAnonymousMode();
|
||||||
settings.toggleTheme();
|
break;
|
||||||
break;
|
case 'theme':
|
||||||
case 'url':
|
settings.toggleTheme();
|
||||||
if (binding.url) {
|
break;
|
||||||
const url = binding.url;
|
case 'url':
|
||||||
const fullUrl = url.startsWith('http://') || url.startsWith('https://') ?
|
if (binding.url) {
|
||||||
url : `https://${url}`;
|
const url = binding.url;
|
||||||
|
const fullUrl = url.startsWith('http://') || url.startsWith('https://') ?
|
||||||
notifications.show(`Redirecting to ${url}...`, 'info');
|
url : `https://${url}`;
|
||||||
setTimeout(() => {
|
|
||||||
window.location.href = fullUrl;
|
notifications.show(`Redirecting to ${url}...`, 'info');
|
||||||
}, 1000);
|
setTimeout(() => {
|
||||||
} else {
|
window.location.href = fullUrl;
|
||||||
notifications.show('No URL set for this keybind.', 'error');
|
}, 1000);
|
||||||
}
|
} else {
|
||||||
break;
|
notifications.show('No URL set for this keybind.', 'error');
|
||||||
}
|
}
|
||||||
}
|
break;
|
||||||
};
|
}
|
||||||
|
}
|
||||||
|
};
|
273
js/main.js
|
@ -1,128 +1,145 @@
|
||||||
// Greeting functionality
|
if ('serviceWorker' in navigator) {
|
||||||
async function updateGreeting() {
|
navigator.serviceWorker.register('/sw.js');
|
||||||
const greeting = document.getElementById('greeting');
|
}
|
||||||
if (!greeting) return;
|
|
||||||
|
async function updateGreeting() {
|
||||||
const customFormat = Storage.get('customGreeting');
|
const greeting = document.getElementById('greeting');
|
||||||
if (customFormat) {
|
if (!greeting) return;
|
||||||
const formattedGreeting = await settings.formatGreeting(customFormat);
|
|
||||||
if (formattedGreeting) {
|
const customFormat = Storage.get('customGreeting');
|
||||||
greeting.textContent = formattedGreeting;
|
if (customFormat) {
|
||||||
greeting.style.opacity = '0';
|
const formattedGreeting = await settings.formatGreeting(customFormat);
|
||||||
setTimeout(() => {
|
if (formattedGreeting) {
|
||||||
greeting.style.opacity = '1';
|
greeting.textContent = formattedGreeting;
|
||||||
}, 100);
|
greeting.style.opacity = '0';
|
||||||
return;
|
setTimeout(() => {
|
||||||
}
|
greeting.style.opacity = '1';
|
||||||
}
|
}, 100);
|
||||||
|
return;
|
||||||
const hour = new Date().getHours();
|
}
|
||||||
const isAnonymous = Storage.get('anonymousMode') || false;
|
}
|
||||||
const userName = isAnonymous ?
|
|
||||||
(Storage.get('anonymousName') || anonymousNames.generate()) :
|
const hour = new Date().getHours();
|
||||||
(Storage.get('userName') || 'Friend');
|
const isAnonymous = Storage.get('anonymousMode') || false;
|
||||||
|
const userName = isAnonymous ?
|
||||||
let timeGreeting = 'Hello';
|
(Storage.get('anonymousName') || anonymousNames.generate()) :
|
||||||
if (hour >= 5 && hour < 12) timeGreeting = 'Good Morning';
|
(Storage.get('userName') || 'Friend');
|
||||||
else if (hour >= 12 && hour < 17) timeGreeting = 'Good Afternoon';
|
|
||||||
else if (hour >= 17 && hour < 20) timeGreeting = 'Good Evening';
|
let timeGreeting = 'Hello';
|
||||||
else timeGreeting = 'Good Night';
|
if (hour >= 5 && hour < 12) timeGreeting = 'Good Morning';
|
||||||
|
else if (hour >= 12 && hour < 17) timeGreeting = 'Good Afternoon';
|
||||||
greeting.textContent = `${timeGreeting}, ${userName}!`;
|
else if (hour >= 17 && hour < 20) timeGreeting = 'Good Evening';
|
||||||
greeting.style.opacity = '0';
|
else timeGreeting = 'Good Night';
|
||||||
setTimeout(() => {
|
|
||||||
greeting.style.opacity = '1';
|
greeting.textContent = `${timeGreeting}, ${userName}!`;
|
||||||
}, 100);
|
greeting.style.opacity = '0';
|
||||||
}
|
setTimeout(() => {
|
||||||
|
greeting.style.opacity = '1';
|
||||||
// Modal handling
|
}, 100);
|
||||||
function initModalHandlers() {
|
}
|
||||||
const modals = document.querySelectorAll('.modal');
|
|
||||||
|
function initModalHandlers() {
|
||||||
modals.forEach(modal => {
|
const modals = document.querySelectorAll('.modal');
|
||||||
modal.addEventListener('click', (e) => {
|
|
||||||
if (e.target === modal && !modal.classList.contains('onboarding-modal')) {
|
modals.forEach(modal => {
|
||||||
closeModal(modal);
|
modal.addEventListener('click', (e) => {
|
||||||
}
|
if (e.target === modal && !modal.classList.contains('onboarding-modal')) {
|
||||||
});
|
closeModal(modal);
|
||||||
|
}
|
||||||
const modalContent = modal.querySelector('.modal-content');
|
});
|
||||||
if (modalContent) {
|
|
||||||
modalContent.addEventListener('click', (e) => {
|
const modalContent = modal.querySelector('.modal-content');
|
||||||
e.stopPropagation();
|
if (modalContent) {
|
||||||
});
|
modalContent.addEventListener('click', (e) => {
|
||||||
}
|
e.stopPropagation();
|
||||||
|
});
|
||||||
document.querySelectorAll('.modal .close-button').forEach(button => {
|
}
|
||||||
button.addEventListener('click', () => {
|
|
||||||
const modal = button.closest('.modal');
|
document.querySelectorAll('.modal .close-button').forEach(button => {
|
||||||
if (modal) {
|
button.addEventListener('click', () => {
|
||||||
closeModal(modal);
|
const modal = button.closest('.modal');
|
||||||
}
|
if (modal) {
|
||||||
});
|
closeModal(modal);
|
||||||
});
|
}
|
||||||
});
|
});
|
||||||
}
|
});
|
||||||
|
});
|
||||||
function openModal(modal) {
|
}
|
||||||
if (!modal) return;
|
|
||||||
modal.classList.remove('hidden');
|
function openModal(modal) {
|
||||||
requestAnimationFrame(() => {
|
if (!modal) return;
|
||||||
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('active');
|
modal.classList.remove('hidden');
|
||||||
setTimeout(() => {
|
requestAnimationFrame(() => {
|
||||||
modal.classList.add('hidden');
|
modal.classList.add('active');
|
||||||
}, 300);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Application initialization
|
function closeModal(modal) {
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
if (!modal) return;
|
||||||
['greeting', 'search', 'shortcuts', 'addShortcut'].forEach(element => {
|
modal.classList.remove('active');
|
||||||
const isVisible = Storage.get(`show_${element}`);
|
setTimeout(() => {
|
||||||
if (isVisible === false) {
|
modal.classList.add('hidden');
|
||||||
const elementNode = document.getElementById(element === 'search' ? 'search-container' : element);
|
}, 300);
|
||||||
if (elementNode) elementNode.style.display = 'none';
|
}
|
||||||
}
|
|
||||||
});
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
if (typeof settings !== 'undefined' && typeof settings.updateVisibility === 'function') {
|
||||||
if (!Storage.get('onboardingComplete')) {
|
settings.updateVisibility();
|
||||||
onboarding.start();
|
} else {
|
||||||
} else {
|
['greeting', 'search', 'shortcuts', 'addShortcut'].forEach(element => {
|
||||||
document.getElementById('main-content').classList.remove('hidden');
|
const isVisible = Storage.get(`show_${element}`);
|
||||||
}
|
if (isVisible === false) {
|
||||||
|
const elementNode = document.getElementById(element === 'search' ? 'search-container' :
|
||||||
search.init();
|
element === 'addShortcut' ? 'add-shortcut' : element);
|
||||||
shortcuts.init();
|
|
||||||
settings.init();
|
if (elementNode) {
|
||||||
initModalHandlers();
|
elementNode.style.visibility = 'hidden';
|
||||||
|
elementNode.style.opacity = '0';
|
||||||
updateGreeting();
|
elementNode.style.position = 'absolute';
|
||||||
setInterval(updateGreeting, 60000);
|
elementNode.style.pointerEvents = 'none';
|
||||||
|
}
|
||||||
const settingsButton = document.getElementById('settings-button');
|
}
|
||||||
const settingsModal = document.getElementById('settings-modal');
|
});
|
||||||
|
}
|
||||||
settingsButton.addEventListener('click', () => {
|
|
||||||
openModal(settingsModal);
|
if (!Storage.get('onboardingComplete')) {
|
||||||
});
|
onboarding.start();
|
||||||
|
} else {
|
||||||
keybinds.init();
|
document.getElementById('main-content').classList.remove('hidden');
|
||||||
});
|
}
|
||||||
|
|
||||||
// Global keydown event handler
|
search.init();
|
||||||
document.addEventListener('keydown', (e) => {
|
shortcuts.init();
|
||||||
if (e.key === 'Enter') {
|
settings.init();
|
||||||
const activeModal = document.querySelector('.modal.active');
|
initModalHandlers();
|
||||||
if (activeModal && !activeModal.matches('#settings-modal')) {
|
|
||||||
const primaryButton = activeModal.querySelector('.btn-primary');
|
updateGreeting();
|
||||||
if (primaryButton) {
|
setInterval(updateGreeting, 60000);
|
||||||
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
|
@ -1,131 +1,121 @@
|
||||||
// Notification System Class
|
class NotificationSystem {
|
||||||
class NotificationSystem {
|
constructor() {
|
||||||
constructor() {
|
this.container = document.getElementById('notification-container');
|
||||||
this.container = document.getElementById('notification-container');
|
this.notifications = new Map();
|
||||||
this.notifications = new Map();
|
}
|
||||||
}
|
|
||||||
|
show(message, type = 'info', duration = 3000) {
|
||||||
// Display a new notification
|
const id = Date.now().toString();
|
||||||
show(message, type = 'info', duration = 3000) {
|
const notification = document.createElement('div');
|
||||||
const id = Date.now().toString();
|
notification.className = `notification notification-${type}`;
|
||||||
const notification = document.createElement('div');
|
|
||||||
notification.className = `notification notification-${type}`;
|
const icon = this.createIcon(type);
|
||||||
|
const content = this.createContent(message);
|
||||||
// Create notification elements
|
const closeBtn = this.createCloseButton(id);
|
||||||
const icon = this.createIcon(type);
|
const progress = this.createProgressBar(type);
|
||||||
const content = this.createContent(message);
|
|
||||||
const closeBtn = this.createCloseButton(id);
|
notification.appendChild(icon);
|
||||||
const progress = this.createProgressBar(type);
|
notification.appendChild(content);
|
||||||
|
notification.appendChild(closeBtn);
|
||||||
// Assemble notification
|
notification.appendChild(progress);
|
||||||
notification.appendChild(icon);
|
|
||||||
notification.appendChild(content);
|
this.container.appendChild(notification);
|
||||||
notification.appendChild(closeBtn);
|
|
||||||
notification.appendChild(progress);
|
setTimeout(() => this.remove(id), duration);
|
||||||
|
|
||||||
this.container.appendChild(notification);
|
this.notifications.set(id, {
|
||||||
|
element: notification,
|
||||||
// Set removal timer
|
duration
|
||||||
setTimeout(() => this.remove(id), duration);
|
});
|
||||||
|
|
||||||
// Store notification reference
|
this.updateProgress(id);
|
||||||
this.notifications.set(id, {
|
|
||||||
element: notification,
|
return id;
|
||||||
duration
|
}
|
||||||
});
|
|
||||||
|
remove(id) {
|
||||||
this.updateProgress(id);
|
const notification = this.notifications.get(id);
|
||||||
|
if (notification) {
|
||||||
return id;
|
notification.element.style.animation = 'slideOutRight 0.3s cubic-bezier(0.16, 1, 0.3, 1)';
|
||||||
}
|
setTimeout(() => {
|
||||||
|
notification.element.remove();
|
||||||
// Remove a notification
|
this.notifications.delete(id);
|
||||||
remove(id) {
|
}, 300);
|
||||||
const notification = this.notifications.get(id);
|
}
|
||||||
if (notification) {
|
}
|
||||||
notification.element.style.animation = 'slideOutRight 0.3s cubic-bezier(0.16, 1, 0.3, 1)';
|
|
||||||
setTimeout(() => {
|
updateProgress(id) {
|
||||||
notification.element.remove();
|
const notification = this.notifications.get(id);
|
||||||
this.notifications.delete(id);
|
if (notification) {
|
||||||
}, 300);
|
const progress = notification.element.querySelector('.notification-progress');
|
||||||
}
|
const startTime = Date.now();
|
||||||
}
|
|
||||||
|
const update = () => {
|
||||||
// Update progress bar
|
const elapsed = Date.now() - startTime;
|
||||||
updateProgress(id) {
|
const percent = 100 - (elapsed / notification.duration * 100);
|
||||||
const notification = this.notifications.get(id);
|
|
||||||
if (notification) {
|
if (percent > 0) {
|
||||||
const progress = notification.element.querySelector('.notification-progress');
|
progress.style.width = `${percent}%`;
|
||||||
const startTime = Date.now();
|
requestAnimationFrame(update);
|
||||||
|
}
|
||||||
const update = () => {
|
};
|
||||||
const elapsed = Date.now() - startTime;
|
|
||||||
const percent = 100 - (elapsed / notification.duration * 100);
|
requestAnimationFrame(update);
|
||||||
|
}
|
||||||
if (percent > 0) {
|
}
|
||||||
progress.style.width = `${percent}%`;
|
|
||||||
requestAnimationFrame(update);
|
createIcon(type) {
|
||||||
}
|
const icon = document.createElement('i');
|
||||||
};
|
switch(type) {
|
||||||
|
case 'success':
|
||||||
requestAnimationFrame(update);
|
icon.className = 'fas fa-check-circle';
|
||||||
}
|
icon.style.color = 'var(--success-color, #4caf50)';
|
||||||
}
|
break;
|
||||||
|
case 'error':
|
||||||
// Helper methods for creating notification elements
|
icon.className = 'fas fa-times-circle';
|
||||||
createIcon(type) {
|
icon.style.color = 'var(--error-color, #f44336)';
|
||||||
const icon = document.createElement('i');
|
break;
|
||||||
switch(type) {
|
case 'info':
|
||||||
case 'success':
|
default:
|
||||||
icon.className = 'fas fa-check-circle';
|
icon.className = 'fas fa-info-circle';
|
||||||
icon.style.color = 'var(--success-color, #4caf50)';
|
icon.style.color = 'var(--info-color, #2196f3)';
|
||||||
break;
|
break;
|
||||||
case 'error':
|
}
|
||||||
icon.className = 'fas fa-times-circle';
|
return icon;
|
||||||
icon.style.color = 'var(--error-color, #f44336)';
|
}
|
||||||
break;
|
|
||||||
case 'info':
|
createContent(message) {
|
||||||
default:
|
const content = document.createElement('div');
|
||||||
icon.className = 'fas fa-info-circle';
|
content.className = 'notification-content';
|
||||||
icon.style.color = 'var(--info-color, #2196f3)';
|
content.innerHTML = message;
|
||||||
break;
|
return content;
|
||||||
}
|
}
|
||||||
return icon;
|
|
||||||
}
|
createCloseButton(id) {
|
||||||
|
const closeBtn = document.createElement('button');
|
||||||
createContent(message) {
|
closeBtn.className = 'notification-close';
|
||||||
const content = document.createElement('div');
|
closeBtn.innerHTML = '<i class="fas fa-times"></i>';
|
||||||
content.className = 'notification-content';
|
closeBtn.onclick = () => this.remove(id);
|
||||||
content.textContent = message;
|
return closeBtn;
|
||||||
return content;
|
}
|
||||||
}
|
|
||||||
|
createProgressBar(type) {
|
||||||
createCloseButton(id) {
|
const progress = document.createElement('div');
|
||||||
const closeBtn = document.createElement('button');
|
progress.className = 'notification-progress';
|
||||||
closeBtn.className = 'notification-close';
|
switch(type) {
|
||||||
closeBtn.innerHTML = '<i class="fas fa-times"></i>';
|
case 'success':
|
||||||
closeBtn.onclick = () => this.remove(id);
|
progress.style.background = 'var(--success-color, #4caf50)';
|
||||||
return closeBtn;
|
break;
|
||||||
}
|
case 'error':
|
||||||
|
progress.style.background = 'var(--error-color, #f44336)';
|
||||||
createProgressBar(type) {
|
break;
|
||||||
const progress = document.createElement('div');
|
case 'info':
|
||||||
progress.className = 'notification-progress';
|
default:
|
||||||
switch(type) {
|
progress.style.background = 'var(--info-color, #2196f3)';
|
||||||
case 'success':
|
break;
|
||||||
progress.style.background = 'var(--success-color, #4caf50)';
|
}
|
||||||
break;
|
return progress;
|
||||||
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();
|
463
js/onboarding.js
|
@ -1,136 +1,327 @@
|
||||||
// Onboarding module
|
const onboarding = {
|
||||||
const onboarding = {
|
currentStep: 1,
|
||||||
// Core functions
|
totalSteps: 5,
|
||||||
isComplete: () => {
|
settings: {},
|
||||||
return Storage.get('onboardingComplete') === true;
|
lastNotification: 0,
|
||||||
},
|
isCompleting: false,
|
||||||
|
notificationShown: false,
|
||||||
start: () => {
|
|
||||||
const modal = document.getElementById('onboarding-modal');
|
showNotification(message, type = 'info') {
|
||||||
const mainContent = document.getElementById('main-content');
|
const now = Date.now();
|
||||||
const importDataBtn = document.getElementById('import-data-btn');
|
if (now - this.lastNotification >= 500) {
|
||||||
const startFreshBtn = document.getElementById('start-fresh-btn');
|
this.lastNotification = now;
|
||||||
const fileInput = document.getElementById('onboarding-import');
|
notifications.show(message, type);
|
||||||
|
}
|
||||||
if (!onboarding.isComplete()) {
|
},
|
||||||
modal.classList.remove('hidden');
|
|
||||||
modal.classList.add('active');
|
isComplete: () => {
|
||||||
mainContent.classList.add('hidden');
|
return Storage.get('onboardingComplete') === true;
|
||||||
|
},
|
||||||
startFreshBtn.addEventListener('click', () => {
|
|
||||||
document.querySelector('[data-step="1"]').classList.add('hidden');
|
start: () => {
|
||||||
document.querySelector('[data-step="2"]').classList.remove('hidden');
|
const onboardingContainer = document.getElementById('onboarding-container');
|
||||||
document.getElementById('next-step-btn').addEventListener('click', () => onboarding.nextStep(2));
|
const mainContent = document.getElementById('main-content');
|
||||||
});
|
const fileInput = document.getElementById('onboarding-import');
|
||||||
|
|
||||||
importDataBtn.addEventListener('click', () => fileInput.click());
|
document.getElementById('notification-container').style.zIndex = "20000";
|
||||||
|
|
||||||
// Data import handling
|
if (!onboarding.isComplete()) {
|
||||||
fileInput.addEventListener('change', async (e) => {
|
document.body.style.overflow = 'hidden';
|
||||||
if (e.target.files.length > 0) {
|
onboardingContainer.classList.remove('hidden');
|
||||||
try {
|
|
||||||
const file = e.target.files[0];
|
onboarding.initProgressDots();
|
||||||
const text = await file.text();
|
onboarding.setupEventListeners();
|
||||||
const data = JSON.parse(text);
|
|
||||||
|
const theme = Storage.get('theme') || 'light';
|
||||||
if (!data.settings || typeof data.settings !== 'object' ||
|
document.body.setAttribute('data-theme', theme);
|
||||||
!Array.isArray(data.shortcuts)) {
|
|
||||||
throw new Error('Invalid data structure');
|
document.querySelectorAll('.step-ob').forEach(step => {
|
||||||
return;
|
if (step.dataset.step !== "1") {
|
||||||
}
|
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) {
|
|
||||||
Storage.set('keybinds', data.keybinds);
|
const nextButton = document.getElementById('next-step');
|
||||||
}
|
nextButton.innerHTML = 'Next <svg><use href="#icon-arrow-right"/></svg>';
|
||||||
|
|
||||||
// Initialize components
|
if (onboarding.currentStep > 1) {
|
||||||
search.init();
|
nextButton.disabled = true;
|
||||||
shortcuts.init();
|
nextButton.classList.add('disabled-ob');
|
||||||
settings.init();
|
}
|
||||||
updateGreeting();
|
|
||||||
document.body.setAttribute('data-theme', data.settings.theme || 'light');
|
mainContent.classList.add('hidden');
|
||||||
|
} else {
|
||||||
// Update UI visibility
|
mainContent.classList.remove('hidden');
|
||||||
['greeting', 'search', 'shortcuts', 'addShortcut'].forEach(element => {
|
}
|
||||||
const isVisible = data.settings[`show_${element}`];
|
|
||||||
const elementNode = document.getElementById(element === 'search' ? 'search-container' : element);
|
fileInput.addEventListener('change', async (e) => {
|
||||||
if (elementNode) {
|
if (e.target.files.length > 0) {
|
||||||
elementNode.style.display = isVisible === false ? 'none' : 'block';
|
try {
|
||||||
}
|
const file = e.target.files[0];
|
||||||
});
|
const text = await file.text();
|
||||||
|
const data = JSON.parse(text);
|
||||||
Storage.set('onboardingComplete', true);
|
|
||||||
modal.classList.add('hidden');
|
if (!data.settings || !data.shortcuts || !Array.isArray(data.shortcuts)) {
|
||||||
mainContent.classList.remove('hidden');
|
throw new Error('Invalid data structure');
|
||||||
|
}
|
||||||
const userName = Storage.get('userName') || 'Guest';
|
|
||||||
notifications.show(`Welcome back, ${userName}! 👋`, 'success');
|
Object.entries(data.settings).forEach(([key, value]) => {
|
||||||
} catch (error) {
|
Storage.set(key, value);
|
||||||
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');
|
|
||||||
engines.forEach(engine => {
|
Storage.set('onboardingComplete', true);
|
||||||
engine.addEventListener('click', () => {
|
|
||||||
engines.forEach(e => e.classList.remove('selected'));
|
localStorage.setItem('showWelcomeAfterImport', 'true');
|
||||||
engine.classList.add('selected');
|
|
||||||
});
|
window.location.reload();
|
||||||
});
|
} catch (error) {
|
||||||
|
onboarding.showNotification('Failed to import data: Invalid file format!', 'error');
|
||||||
document.getElementById('complete-setup-btn').addEventListener('click', onboarding.complete);
|
fileInput.value = '';
|
||||||
} else {
|
}
|
||||||
modal.classList.add('hidden');
|
}
|
||||||
mainContent.classList.remove('hidden');
|
});
|
||||||
}
|
},
|
||||||
},
|
|
||||||
|
setupEventListeners: () => {
|
||||||
// Onboarding step navigation
|
document.querySelectorAll('.option-card-ob').forEach(card => {
|
||||||
nextStep: (currentStep) => {
|
card.addEventListener('click', () => {
|
||||||
const currentStepEl = document.querySelector(`[data-step="${currentStep}"]`);
|
const step = card.closest('.step-ob');
|
||||||
const nextStepEl = document.querySelector(`[data-step="${currentStep + 1}"]`);
|
const stepNumber = parseInt(step.dataset.step);
|
||||||
|
const cards = step.querySelectorAll('.option-card-ob');
|
||||||
if (currentStep === 2) {
|
const nextButton = document.getElementById('next-step');
|
||||||
const name = document.getElementById('user-name').value.trim();
|
|
||||||
if (!name) {
|
if (card.dataset.action === 'import-data') {
|
||||||
notifications.show('Please enter your name!', 'error');
|
if (!card.classList.contains('selected-ob')) {
|
||||||
return;
|
document.getElementById('onboarding-import').click();
|
||||||
}
|
}
|
||||||
Storage.set('userName', name);
|
cards.forEach(c => c.classList.remove('selected-ob'));
|
||||||
}
|
card.classList.add('selected-ob');
|
||||||
|
return;
|
||||||
currentStepEl.classList.add('hidden');
|
}
|
||||||
nextStepEl.classList.remove('hidden');
|
|
||||||
nextStepEl.classList.add('visible');
|
cards.forEach(c => c.classList.remove('selected-ob'));
|
||||||
},
|
card.classList.add('selected-ob');
|
||||||
|
|
||||||
// Finalize onboarding
|
card.style.transform = 'scale(1.05)';
|
||||||
complete: () => {
|
setTimeout(() => {
|
||||||
const selectedEngine = document.querySelector('.search-engine-option.selected');
|
card.style.transform = 'scale(1.02)';
|
||||||
if (!selectedEngine) {
|
}, 150);
|
||||||
notifications.show('Please select a search engine!', 'error');
|
|
||||||
return;
|
nextButton.disabled = false;
|
||||||
}
|
nextButton.classList.remove('disabled-ob');
|
||||||
|
|
||||||
const searchEngine = selectedEngine.dataset.engine;
|
if (card.dataset.theme) {
|
||||||
Storage.set('searchEngine', searchEngine);
|
onboarding.settings.theme = card.dataset.theme;
|
||||||
Storage.set('onboardingComplete', true);
|
document.body.setAttribute('data-theme', card.dataset.theme);
|
||||||
|
} else if (card.dataset.font) {
|
||||||
const modal = document.getElementById('onboarding-modal');
|
onboarding.settings.fontFamily = card.dataset.font;
|
||||||
const mainContent = document.getElementById('main-content');
|
document.documentElement.style.setProperty('--font-family', card.dataset.font);
|
||||||
|
} else if (card.dataset.engine) {
|
||||||
modal.classList.add('hidden');
|
onboarding.settings.searchEngine = card.dataset.engine;
|
||||||
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);
|
||||||
|
}
|
||||||
|
});
|
127
js/search.js
|
@ -1,47 +1,80 @@
|
||||||
const search = {
|
const search = {
|
||||||
// Supported search engines and their URLs
|
engines: {
|
||||||
engines: {
|
google: {
|
||||||
google: 'https://www.google.com/search?q=',
|
url: 'https://www.google.com/search?q=',
|
||||||
bing: 'https://www.bing.com/search?q=',
|
icon: 'https://www.google.com/s2/favicons?domain=google.com&sz=32',
|
||||||
duckduckgo: 'https://duckduckgo.com/?q=',
|
name: 'Google'
|
||||||
brave: 'https://search.brave.com/search?q=',
|
},
|
||||||
qwant: 'https://www.qwant.com/?q=',
|
bing: {
|
||||||
searxng: 'https://searx.org/search?q='
|
url: 'https://www.bing.com/search?q=',
|
||||||
},
|
icon: 'https://www.google.com/s2/favicons?domain=bing.com&sz=32',
|
||||||
|
name: 'Bing'
|
||||||
// Perform search using selected engine
|
},
|
||||||
perform: () => {
|
duckduckgo: {
|
||||||
const searchBar = document.getElementById('search-bar');
|
url: 'https://duckduckgo.com/?q=',
|
||||||
const query = searchBar.value.trim();
|
icon: 'https://www.google.com/s2/favicons?domain=duckduckgo.com&sz=32',
|
||||||
const engine = Storage.get('searchEngine') || 'google';
|
name: 'DuckDuckGo'
|
||||||
|
},
|
||||||
if (query) {
|
brave: {
|
||||||
const searchUrl = search.engines[engine] + encodeURIComponent(query);
|
url: 'https://search.brave.com/search?q=',
|
||||||
window.location.href = searchUrl;
|
icon: 'https://www.google.com/s2/favicons?domain=brave.com&sz=32',
|
||||||
}
|
name: 'Brave'
|
||||||
},
|
},
|
||||||
|
qwant: {
|
||||||
// Initialize search functionality
|
url: 'https://www.qwant.com/?q=',
|
||||||
init: () => {
|
icon: 'https://www.google.com/s2/favicons?domain=qwant.com&sz=32',
|
||||||
const searchBar = document.getElementById('search-bar');
|
name: 'Qwant'
|
||||||
const searchButton = document.getElementById('search-button');
|
},
|
||||||
|
searxng: {
|
||||||
searchBar.addEventListener('keypress', (e) => {
|
url: 'https://searx.be/search?q=',
|
||||||
if (e.key === 'Enter') {
|
icon: 'https://www.google.com/s2/favicons?domain=searx.be&sz=32',
|
||||||
search.perform();
|
name: 'SearXNG'
|
||||||
}
|
}
|
||||||
});
|
},
|
||||||
|
|
||||||
searchButton.addEventListener('click', search.perform);
|
init: () => {
|
||||||
|
const searchBar = document.getElementById('search-bar');
|
||||||
// Global keyboard shortcut to focus search bar
|
const searchButton = document.getElementById('search-button');
|
||||||
document.addEventListener('keydown', (e) => {
|
|
||||||
if (e.key === '/' &&
|
searchBar.addEventListener('keypress', (e) => {
|
||||||
!['INPUT', 'TEXTAREA'].includes(document.activeElement.tagName) &&
|
if (e.key === 'Enter') {
|
||||||
window.getSelection().toString() === '') {
|
search.perform();
|
||||||
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();
|
||||||
|
});
|
1642
js/settings.js
549
js/shortcuts.js
|
@ -1,274 +1,275 @@
|
||||||
const shortcuts = {
|
const shortcuts = {
|
||||||
MAX_SHORTCUTS: 12,
|
MAX_SHORTCUTS: 12,
|
||||||
|
|
||||||
// URL Validation
|
validateAndFormatUrl: (url) => {
|
||||||
validateAndFormatUrl: (url) => {
|
if (!/^https?:\/\//i.test(url)) {
|
||||||
if (!/^https?:\/\//i.test(url)) {
|
url = 'https://' + url;
|
||||||
url = 'https://' + url;
|
}
|
||||||
}
|
|
||||||
|
try {
|
||||||
try {
|
new URL(url);
|
||||||
new URL(url);
|
return url;
|
||||||
return url;
|
} catch (e) {
|
||||||
} catch (e) {
|
return false;
|
||||||
return false;
|
}
|
||||||
}
|
},
|
||||||
},
|
|
||||||
|
add: (url, name) => {
|
||||||
// Shortcut Management
|
const currentShortcuts = Storage.get('shortcuts') || [];
|
||||||
add: (url, name) => {
|
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 formattedUrl = shortcuts.validateAndFormatUrl(url);
|
||||||
|
if (!formattedUrl) {
|
||||||
const formattedUrl = shortcuts.validateAndFormatUrl(url);
|
notifications.show('Invalid URL format!', 'error');
|
||||||
if (!formattedUrl) {
|
return;
|
||||||
notifications.show('Invalid URL format!', 'error');
|
}
|
||||||
return;
|
|
||||||
}
|
currentShortcuts.push({ url: formattedUrl, name });
|
||||||
|
Storage.set('shortcuts', currentShortcuts);
|
||||||
currentShortcuts.push({ url: formattedUrl, name });
|
shortcuts.render();
|
||||||
Storage.set('shortcuts', currentShortcuts);
|
CacheUpdater.update();
|
||||||
shortcuts.render();
|
},
|
||||||
},
|
|
||||||
|
remove: (index) => {
|
||||||
remove: (index) => {
|
const currentShortcuts = Storage.get('shortcuts') || [];
|
||||||
const currentShortcuts = Storage.get('shortcuts') || [];
|
currentShortcuts.splice(index, 1);
|
||||||
currentShortcuts.splice(index, 1);
|
Storage.set('shortcuts', currentShortcuts);
|
||||||
Storage.set('shortcuts', currentShortcuts);
|
shortcuts.render();
|
||||||
shortcuts.render();
|
notifications.show('Shortcut removed!', 'success');
|
||||||
notifications.show('Shortcut removed!', 'success');
|
CacheUpdater.update();
|
||||||
},
|
},
|
||||||
|
|
||||||
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);
|
||||||
},
|
},
|
||||||
|
|
||||||
// Rendering
|
render: () => {
|
||||||
render: () => {
|
const grid = document.getElementById('shortcuts-grid');
|
||||||
const grid = document.getElementById('shortcuts-grid');
|
const currentShortcuts = Storage.get('shortcuts') || [];
|
||||||
const currentShortcuts = Storage.get('shortcuts') || [];
|
const isAnonymous = Storage.get('anonymousMode') || false;
|
||||||
const isAnonymous = Storage.get('anonymousMode') || false;
|
|
||||||
|
grid.innerHTML = '';
|
||||||
grid.innerHTML = '';
|
|
||||||
|
currentShortcuts.forEach((shortcut, index) => {
|
||||||
currentShortcuts.forEach((shortcut, index) => {
|
const element = document.createElement('div');
|
||||||
const element = document.createElement('div');
|
element.className = `shortcut ${isAnonymous ? 'blurred' : ''}`;
|
||||||
element.className = `shortcut ${isAnonymous ? 'blurred' : ''}`;
|
|
||||||
|
element.dataset.index = index;
|
||||||
const icon = document.createElement('img');
|
|
||||||
icon.src = `https://www.google.com/s2/favicons?domain=${shortcut.url}&sz=64`;
|
const icon = document.createElement('img');
|
||||||
icon.alt = shortcut.name;
|
icon.src = `https://www.google.com/s2/favicons?domain=${shortcut.url}&sz=64`;
|
||||||
|
icon.alt = shortcut.name;
|
||||||
const name = document.createElement('span');
|
icon.draggable = false;
|
||||||
name.textContent = shortcut.name;
|
|
||||||
|
const name = document.createElement('span');
|
||||||
element.appendChild(icon);
|
name.textContent = shortcut.name;
|
||||||
element.appendChild(name);
|
|
||||||
|
element.appendChild(icon);
|
||||||
element.addEventListener('click', (e) => {
|
element.appendChild(name);
|
||||||
if (e.ctrlKey) {
|
|
||||||
window.open(shortcut.url, '_blank');
|
element.addEventListener('click', (e) => {
|
||||||
} else {
|
if (!grid.classList.contains('grid-draggable') || !e.target.closest('.shortcut').classList.contains('drag-active')) {
|
||||||
window.location.href = shortcut.url;
|
if (e.ctrlKey) {
|
||||||
}
|
window.open(shortcut.url, '_blank');
|
||||||
});
|
} else {
|
||||||
|
window.location.href = shortcut.url;
|
||||||
element.addEventListener('contextmenu', (e) => {
|
}
|
||||||
e.preventDefault();
|
}
|
||||||
const menu = document.getElementById('context-menu');
|
});
|
||||||
|
|
||||||
menu.style.top = `${e.pageY}px`;
|
element.addEventListener('contextmenu', (e) => {
|
||||||
menu.style.left = `${e.pageX}px`;
|
e.preventDefault();
|
||||||
menu.classList.remove('hidden');
|
const menu = document.getElementById('context-menu');
|
||||||
menu.dataset.shortcutIndex = index;
|
|
||||||
|
menu.style.top = `${e.pageY}px`;
|
||||||
const closeMenu = (event) => {
|
menu.style.left = `${e.pageX}px`;
|
||||||
if (!menu.contains(event.target)) {
|
menu.classList.remove('hidden');
|
||||||
menu.classList.add('hidden');
|
menu.dataset.shortcutIndex = index;
|
||||||
document.removeEventListener('click', closeMenu);
|
|
||||||
}
|
const closeMenu = (event) => {
|
||||||
};
|
if (!menu.contains(event.target)) {
|
||||||
|
menu.classList.add('hidden');
|
||||||
setTimeout(() => {
|
document.removeEventListener('click', closeMenu);
|
||||||
document.addEventListener('click', closeMenu);
|
}
|
||||||
}, 0);
|
};
|
||||||
});
|
|
||||||
|
setTimeout(() => {
|
||||||
grid.appendChild(element);
|
document.addEventListener('click', closeMenu);
|
||||||
});
|
}, 0);
|
||||||
},
|
});
|
||||||
|
|
||||||
// Initialization
|
grid.appendChild(element);
|
||||||
init: () => {
|
});
|
||||||
const addShortcutButton = document.getElementById('add-shortcut');
|
},
|
||||||
const modal = document.getElementById('add-shortcut-modal');
|
|
||||||
const closeBtn = modal.querySelector('.close-modal');
|
init: () => {
|
||||||
|
const addShortcutButton = document.getElementById('add-shortcut');
|
||||||
if (closeBtn) {
|
const modal = document.getElementById('add-shortcut-modal');
|
||||||
closeBtn.addEventListener('click', () => {
|
const closeBtn = modal.querySelector('.close-modal');
|
||||||
modal.classList.remove('active');
|
|
||||||
setTimeout(() => {
|
if (closeBtn) {
|
||||||
modal.classList.add('hidden');
|
closeBtn.addEventListener('click', () => {
|
||||||
document.getElementById('shortcut-url').value = '';
|
modal.classList.remove('active');
|
||||||
document.getElementById('shortcut-name').value = '';
|
setTimeout(() => {
|
||||||
}, 300);
|
modal.classList.add('hidden');
|
||||||
});
|
document.getElementById('shortcut-url').value = '';
|
||||||
}
|
document.getElementById('shortcut-name').value = '';
|
||||||
|
}, 300);
|
||||||
if (addShortcutButton) {
|
});
|
||||||
addShortcutButton.addEventListener('click', (e) => {
|
}
|
||||||
e.stopPropagation();
|
|
||||||
const currentShortcuts = Storage.get('shortcuts') || [];
|
if (addShortcutButton) {
|
||||||
|
addShortcutButton.addEventListener('click', (e) => {
|
||||||
if (currentShortcuts.length >= shortcuts.MAX_SHORTCUTS) {
|
e.stopPropagation();
|
||||||
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');
|
||||||
if (modal) {
|
return;
|
||||||
modal.classList.remove('hidden');
|
}
|
||||||
modal.classList.add('active');
|
|
||||||
|
if (modal) {
|
||||||
const urlInput = document.getElementById('shortcut-url');
|
modal.classList.remove('hidden');
|
||||||
const nameInput = document.getElementById('shortcut-name');
|
modal.classList.add('active');
|
||||||
|
|
||||||
const saveShortcutButton = document.getElementById('save-shortcut');
|
const urlInput = document.getElementById('shortcut-url');
|
||||||
if (saveShortcutButton) {
|
const nameInput = document.getElementById('shortcut-name');
|
||||||
saveShortcutButton.onclick = () => {
|
|
||||||
const url = urlInput.value.trim();
|
const saveShortcutButton = document.getElementById('save-shortcut');
|
||||||
const name = nameInput.value.trim();
|
if (saveShortcutButton) {
|
||||||
|
saveShortcutButton.onclick = () => {
|
||||||
if (url && name) {
|
const url = urlInput.value.trim();
|
||||||
try {
|
const name = nameInput.value.trim();
|
||||||
new URL(url);
|
|
||||||
shortcuts.add(url, name);
|
if (url && name) {
|
||||||
modal.classList.remove('active');
|
try {
|
||||||
setTimeout(() => {
|
new URL(url);
|
||||||
modal.classList.add('hidden');
|
shortcuts.add(url, name);
|
||||||
urlInput.value = '';
|
modal.classList.remove('active');
|
||||||
nameInput.value = '';
|
setTimeout(() => {
|
||||||
}, 300);
|
modal.classList.add('hidden');
|
||||||
notifications.show('Shortcut added successfully!', 'success');
|
urlInput.value = '';
|
||||||
} catch (e) {
|
nameInput.value = '';
|
||||||
notifications.show('Invalid URL format!', 'error');
|
}, 300);
|
||||||
}
|
notifications.show('Shortcut added successfully!', 'success');
|
||||||
}
|
} catch (e) {
|
||||||
};
|
notifications.show('Invalid URL format!', 'error');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
const cancelShortcutButton = document.getElementById('cancel-shortcut');
|
};
|
||||||
if (cancelShortcutButton) {
|
}
|
||||||
cancelShortcutButton.onclick = () => {
|
|
||||||
modal.classList.remove('active');
|
const cancelShortcutButton = document.getElementById('cancel-shortcut');
|
||||||
setTimeout(() => {
|
if (cancelShortcutButton) {
|
||||||
modal.classList.add('hidden');
|
cancelShortcutButton.onclick = () => {
|
||||||
urlInput.value = '';
|
modal.classList.remove('active');
|
||||||
nameInput.value = '';
|
setTimeout(() => {
|
||||||
}, 300);
|
modal.classList.add('hidden');
|
||||||
};
|
urlInput.value = '';
|
||||||
}
|
nameInput.value = '';
|
||||||
}
|
}, 300);
|
||||||
});
|
};
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// Context menu actions
|
});
|
||||||
const contextMenu = document.getElementById('context-menu');
|
}
|
||||||
if (contextMenu) {
|
|
||||||
contextMenu.addEventListener('click', (e) => {
|
const contextMenu = document.getElementById('context-menu');
|
||||||
const action = e.target.closest('.context-menu-item')?.dataset.action;
|
if (contextMenu) {
|
||||||
const index = parseInt(contextMenu.dataset.shortcutIndex);
|
contextMenu.addEventListener('click', (e) => {
|
||||||
|
const action = e.target.closest('.context-menu-item')?.dataset.action;
|
||||||
if (action === 'edit') {
|
const index = parseInt(contextMenu.dataset.shortcutIndex);
|
||||||
const currentShortcuts = Storage.get('shortcuts') || [];
|
|
||||||
const shortcut = currentShortcuts[index];
|
if (action === 'edit') {
|
||||||
const modal = document.getElementById('edit-shortcut-modal');
|
const currentShortcuts = Storage.get('shortcuts') || [];
|
||||||
|
const shortcut = currentShortcuts[index];
|
||||||
if (modal) {
|
const modal = document.getElementById('edit-shortcut-modal');
|
||||||
const urlInput = document.getElementById('edit-shortcut-url');
|
|
||||||
const nameInput = document.getElementById('edit-shortcut-name');
|
if (modal) {
|
||||||
|
const urlInput = document.getElementById('edit-shortcut-url');
|
||||||
urlInput.value = shortcut.url;
|
const nameInput = document.getElementById('edit-shortcut-name');
|
||||||
nameInput.value = shortcut.name;
|
|
||||||
|
urlInput.value = shortcut.url;
|
||||||
modal.classList.remove('hidden');
|
nameInput.value = shortcut.name;
|
||||||
modal.classList.add('active');
|
|
||||||
|
modal.classList.remove('hidden');
|
||||||
const saveButton = document.getElementById('save-edit-shortcut');
|
modal.classList.add('active');
|
||||||
const closeButton = document.getElementById('close-edit-shortcut');
|
|
||||||
const cancelButton = document.getElementById('cancel-edit-shortcut');
|
const saveButton = document.getElementById('save-edit-shortcut');
|
||||||
|
const closeButton = document.getElementById('close-edit-shortcut');
|
||||||
const closeModal = () => {
|
const cancelButton = document.getElementById('cancel-edit-shortcut');
|
||||||
modal.classList.remove('active');
|
|
||||||
setTimeout(() => {
|
const closeModal = () => {
|
||||||
modal.classList.add('hidden');
|
modal.classList.remove('active');
|
||||||
}, 300);
|
setTimeout(() => {
|
||||||
};
|
modal.classList.add('hidden');
|
||||||
|
}, 300);
|
||||||
const handleSave = () => {
|
};
|
||||||
const newUrl = urlInput.value.trim();
|
|
||||||
const newName = nameInput.value.trim();
|
const handleSave = () => {
|
||||||
|
const newUrl = urlInput.value.trim();
|
||||||
if (newUrl && newName) {
|
const newName = nameInput.value.trim();
|
||||||
const formattedUrl = shortcuts.validateAndFormatUrl(newUrl);
|
|
||||||
if (formattedUrl) {
|
if (newUrl && newName) {
|
||||||
shortcuts.edit(index, formattedUrl, newName);
|
const formattedUrl = shortcuts.validateAndFormatUrl(newUrl);
|
||||||
closeModal();
|
if (formattedUrl) {
|
||||||
} else {
|
shortcuts.edit(index, formattedUrl, newName);
|
||||||
notifications.show('Invalid URL format!', 'error');
|
closeModal();
|
||||||
}
|
} else {
|
||||||
}
|
notifications.show('Invalid URL format!', 'error');
|
||||||
};
|
}
|
||||||
|
}
|
||||||
saveButton.onclick = handleSave;
|
};
|
||||||
closeButton.onclick = closeModal;
|
|
||||||
cancelButton.onclick = closeModal;
|
saveButton.onclick = handleSave;
|
||||||
}
|
closeButton.onclick = closeModal;
|
||||||
} else if (action === 'delete') {
|
cancelButton.onclick = closeModal;
|
||||||
shortcuts.remove(index);
|
}
|
||||||
} else if (action === 'open-new-tab') {
|
} else if (action === 'delete') {
|
||||||
const currentShortcuts = Storage.get('shortcuts') || [];
|
shortcuts.remove(index);
|
||||||
const shortcut = currentShortcuts[index];
|
} else if (action === 'open-new-tab') {
|
||||||
|
const currentShortcuts = Storage.get('shortcuts') || [];
|
||||||
// Open the URL of the shortcut in a new tab
|
const shortcut = currentShortcuts[index];
|
||||||
if (shortcut && shortcut.url) {
|
|
||||||
window.open(shortcut.url, '_blank');
|
if (shortcut && shortcut.url) {
|
||||||
}
|
window.open(shortcut.url, '_blank');
|
||||||
}
|
}
|
||||||
|
}
|
||||||
contextMenu.classList.add('hidden');
|
|
||||||
});
|
contextMenu.classList.add('hidden');
|
||||||
}
|
});
|
||||||
|
}
|
||||||
shortcuts.render();
|
|
||||||
}
|
shortcuts.render();
|
||||||
};
|
}
|
||||||
|
};
|
|
@ -1,9 +1,4 @@
|
||||||
/**
|
|
||||||
* 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));
|
||||||
|
@ -12,7 +7,6 @@ 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));
|
||||||
|
@ -22,7 +16,6 @@ const Storage = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// Delete specific key
|
|
||||||
remove: (key) => {
|
remove: (key) => {
|
||||||
try {
|
try {
|
||||||
localStorage.removeItem(key);
|
localStorage.removeItem(key);
|
||||||
|
@ -32,7 +25,6 @@ const Storage = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// Remove all stored data
|
|
||||||
clear: () => {
|
clear: () => {
|
||||||
try {
|
try {
|
||||||
localStorage.clear();
|
localStorage.clear();
|
||||||
|
|
93
js/version.js
Normal file
|
@ -0,0 +1,93 @@
|
||||||
|
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 = 'You’re 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! Don’t miss out on the new goodies.`;
|
||||||
|
} else if (versionComparison < 0) {
|
||||||
|
versionIcon.classList.add('fa-question-circle');
|
||||||
|
versionIcon.style.color = '#2196f3';
|
||||||
|
versionIcon.title = 'Whoa! You’re 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>`;
|
||||||
|
}
|
||||||
|
});
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"manifest_version": 3,
|
"manifest_version": 3,
|
||||||
"name": "JSTAR Tab",
|
"name": "JSTAR Tab",
|
||||||
"version": "2.6.2",
|
"version": "3.0.0",
|
||||||
"description": "JSTAR Tab is a sleek, customizable new tab extension with personalized greetings, shortcuts, anonymous mode, search engine settings, themes, data management, and more, for an enhanced browsing experience.",
|
"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>"]
|
||||||
}]
|
}]
|
||||||
}
|
}
|
60
sw.js
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
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))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|