477 lines
16 KiB
JavaScript
477 lines
16 KiB
JavaScript
|
// Clock Class
|
||
|
class Clock {
|
||
|
constructor(clockElement, greetingElement) {
|
||
|
this.clockElement = clockElement;
|
||
|
this.greetingElement = greetingElement;
|
||
|
this.interval = null;
|
||
|
}
|
||
|
|
||
|
start() {
|
||
|
this.update();
|
||
|
this.interval = setInterval(() => this.update(), 1000);
|
||
|
}
|
||
|
|
||
|
stop() {
|
||
|
if (this.interval) clearInterval(this.interval);
|
||
|
}
|
||
|
|
||
|
update() {
|
||
|
const now = new Date();
|
||
|
this.clockElement.textContent = now.toLocaleTimeString('en-US', {
|
||
|
hour12: false,
|
||
|
hour: '2-digit',
|
||
|
minute: '2-digit'
|
||
|
});
|
||
|
this.updateGreeting();
|
||
|
}
|
||
|
|
||
|
updateGreeting() {
|
||
|
this.greetingElement.textContent = this.getGreeting();
|
||
|
}
|
||
|
|
||
|
getGreeting() {
|
||
|
const hour = new Date().getHours();
|
||
|
let greeting = '';
|
||
|
|
||
|
if (hour >= 5 && hour < 12) greeting = 'Good morning';
|
||
|
else if (hour >= 12 && hour < 18) greeting = 'Good afternoon';
|
||
|
else if (hour >= 18 && hour < 22) greeting = 'Good evening';
|
||
|
else greeting = 'Good night';
|
||
|
|
||
|
const name = localStorage.getItem('username');
|
||
|
return name ? `${greeting}, ${name}` : greeting;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Make LinksManager available globally
|
||
|
window.linksManager = null;
|
||
|
|
||
|
class LinksManager {
|
||
|
|
||
|
}
|
||
|
|
||
|
class SearchEngineManager {
|
||
|
constructor() {
|
||
|
this.engines = JSON.parse(localStorage.getItem('searchEngines')) || [
|
||
|
{ name: 'SearXNG', url: 'https://search.ploszukiwacz.pl/search?q=%s', default: true }
|
||
|
];
|
||
|
this.searchForm = document.getElementById('search-form');
|
||
|
this.enginesContainer = document.getElementById('search-engines');
|
||
|
this.init();
|
||
|
}
|
||
|
|
||
|
init() {
|
||
|
this.renderEngines();
|
||
|
this.attachEventListeners();
|
||
|
this.setDefaultEngine();
|
||
|
}
|
||
|
|
||
|
renderEngines() {
|
||
|
this.enginesContainer.innerHTML = this.engines.map((engine, index) => `
|
||
|
<div class="search-engine">
|
||
|
<input type="text" value="${engine.name}" placeholder="Name"
|
||
|
onchange="searchManager.updateEngine(${index}, 'name', this.value)">
|
||
|
<input type="text" value="${engine.url}" placeholder="URL with %s"
|
||
|
onchange="searchManager.updateEngine(${index}, 'url', this.value)">
|
||
|
<label><input type="radio" name="default-engine" ${engine.default ? 'checked' : ''}
|
||
|
onchange="searchManager.setDefault(${index})"> Default</label>
|
||
|
<button class="settings-btn small" onclick="searchManager.removeEngine(${index})">
|
||
|
<i class="fas fa-times"></i>
|
||
|
</button>
|
||
|
</div>
|
||
|
`).join('');
|
||
|
}
|
||
|
|
||
|
attachEventListeners() {
|
||
|
document.getElementById('addSearchEngine').addEventListener('click', () => {
|
||
|
this.addEngine({
|
||
|
name: 'New Engine',
|
||
|
url: 'https://',
|
||
|
default: false
|
||
|
});
|
||
|
});
|
||
|
}
|
||
|
|
||
|
addEngine(engine) {
|
||
|
this.engines.push(engine);
|
||
|
this.save();
|
||
|
this.renderEngines();
|
||
|
}
|
||
|
|
||
|
updateEngine(index, field, value) {
|
||
|
if (this.engines[index]) {
|
||
|
this.engines[index][field] = value;
|
||
|
this.save();
|
||
|
this.renderEngines();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
removeEngine(index) {
|
||
|
if (this.engines[index].default && this.engines.length > 1) {
|
||
|
this.engines[0].default = true;
|
||
|
}
|
||
|
this.engines.splice(index, 1);
|
||
|
this.save();
|
||
|
this.renderEngines();
|
||
|
}
|
||
|
|
||
|
setDefault(index) {
|
||
|
this.engines.forEach((engine, i) => {
|
||
|
engine.default = i === index;
|
||
|
});
|
||
|
this.save();
|
||
|
this.setDefaultEngine();
|
||
|
}
|
||
|
|
||
|
setDefaultEngine() {
|
||
|
const defaultEngine = this.engines.find(e => e.default) || this.engines[0];
|
||
|
if (defaultEngine) {
|
||
|
this.searchForm.action = defaultEngine.url.replace('%s', '');
|
||
|
this.searchForm.querySelector('input').placeholder = `Search with ${defaultEngine.name}...`;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
save() {
|
||
|
localStorage.setItem('searchEngines', JSON.stringify(this.engines));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
class ShortcutManager {
|
||
|
constructor() {
|
||
|
this.shortcuts = JSON.parse(localStorage.getItem('shortcuts')) || {
|
||
|
focusSearch: 's',
|
||
|
openSettings: ',',
|
||
|
switchLayout: 'l'
|
||
|
};
|
||
|
this.init();
|
||
|
}
|
||
|
|
||
|
init() {
|
||
|
this.renderShortcuts();
|
||
|
this.attachGlobalListeners();
|
||
|
}
|
||
|
|
||
|
renderShortcuts() {
|
||
|
const container = document.getElementById('shortcuts-editor');
|
||
|
if (!container) return;
|
||
|
|
||
|
container.innerHTML = Object.entries(this.shortcuts)
|
||
|
.map(([action, key]) => `
|
||
|
<div class="shortcut-item">
|
||
|
<span>${this.formatActionName(action)}</span>
|
||
|
<input type="text" value="${key}" maxlength="1"
|
||
|
onkeydown="event.preventDefault()"
|
||
|
onkeyup="shortcutManager.updateShortcut('${action}', event.key)">
|
||
|
</div>
|
||
|
`).join('');
|
||
|
}
|
||
|
|
||
|
formatActionName(action) {
|
||
|
return action
|
||
|
.replace(/([A-Z])/g, ' $1')
|
||
|
.replace(/^./, str => str.toUpperCase());
|
||
|
}
|
||
|
|
||
|
attachGlobalListeners() {
|
||
|
document.addEventListener('keydown', (e) => {
|
||
|
// Ignore if target is an input or textarea
|
||
|
if (e.target.matches('input, textarea')) return;
|
||
|
|
||
|
if (e.key === this.shortcuts.focusSearch) {
|
||
|
e.preventDefault();
|
||
|
document.getElementById('search-input').focus();
|
||
|
} else if (e.key === this.shortcuts.openSettings) {
|
||
|
e.preventDefault();
|
||
|
document.querySelector('.settings-button').click();
|
||
|
} else if (e.key === this.shortcuts.switchLayout) {
|
||
|
e.preventDefault();
|
||
|
const layoutSelect = document.getElementById('layoutSelect');
|
||
|
layoutSelect.value = layoutSelect.value === 'grid' ? 'list' : 'grid';
|
||
|
layoutSelect.dispatchEvent(new Event('change'));
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
|
||
|
updateShortcut(action, key) {
|
||
|
if (key && key !== 'Escape' && key !== 'Tab') {
|
||
|
this.shortcuts[action] = key.toLowerCase();
|
||
|
this.save();
|
||
|
this.renderShortcuts();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
save() {
|
||
|
localStorage.setItem('shortcuts', JSON.stringify(this.shortcuts));
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Make ShortcutManager available globally
|
||
|
window.shortcutManager = null;
|
||
|
|
||
|
// Settings Manager Class
|
||
|
class SettingsManager {
|
||
|
// ...existing SettingsManager code...
|
||
|
}
|
||
|
|
||
|
// Main initialization
|
||
|
document.addEventListener('DOMContentLoaded', () => {
|
||
|
// Initialize clock
|
||
|
const clock = new Clock(
|
||
|
document.getElementById('clock'),
|
||
|
document.getElementById('greeting')
|
||
|
);
|
||
|
clock.start();
|
||
|
|
||
|
// Initialize settings
|
||
|
const settings = new SettingsManager();
|
||
|
|
||
|
// Focus search on load
|
||
|
document.getElementById('search-input').focus();
|
||
|
|
||
|
// Name handling
|
||
|
const nameInput = document.getElementById('nameInput');
|
||
|
const saveNameBtn = document.getElementById('saveName');
|
||
|
|
||
|
// Load saved name
|
||
|
const savedName = localStorage.getItem('username');
|
||
|
if (savedName) {
|
||
|
nameInput.value = savedName;
|
||
|
}
|
||
|
|
||
|
// Save name
|
||
|
saveNameBtn.addEventListener('click', () => {
|
||
|
const name = nameInput.value.trim();
|
||
|
if (name) {
|
||
|
localStorage.setItem('username', name);
|
||
|
} else {
|
||
|
localStorage.removeItem('username');
|
||
|
nameInput.value = ''; // Clear input if empty
|
||
|
}
|
||
|
clock.updateGreeting();
|
||
|
});
|
||
|
|
||
|
// Also handle Enter key in name input
|
||
|
nameInput.addEventListener('keypress', (e) => {
|
||
|
if (e.key === 'Enter') {
|
||
|
e.preventDefault();
|
||
|
saveNameBtn.click();
|
||
|
}
|
||
|
});
|
||
|
|
||
|
// Background handling
|
||
|
const fileInput = document.getElementById('bgImage');
|
||
|
const removeButton = document.getElementById('removeButton');
|
||
|
const presetOptions = document.querySelectorAll('.preset-bg-option');
|
||
|
|
||
|
function setBackground(url) {
|
||
|
document.body.style.backgroundImage = url ? `url(${url})` : 'none';
|
||
|
localStorage.setItem('background', url || '');
|
||
|
|
||
|
// Update active state of preset options
|
||
|
presetOptions.forEach(option => {
|
||
|
const optionBg = option.dataset.bg;
|
||
|
option.classList.toggle('active', optionBg === (url || 'none'));
|
||
|
});
|
||
|
}
|
||
|
|
||
|
// Load saved background
|
||
|
const savedBg = localStorage.getItem('background');
|
||
|
if (savedBg) {
|
||
|
setBackground(savedBg);
|
||
|
}
|
||
|
|
||
|
// Handle preset backgrounds
|
||
|
presetOptions.forEach(option => {
|
||
|
const bgUrl = option.dataset.bg;
|
||
|
|
||
|
// Add none class for the no-background option
|
||
|
if (bgUrl === 'none') {
|
||
|
option.classList.add('none');
|
||
|
} else {
|
||
|
// Set preview image
|
||
|
option.style.backgroundImage = `url(${bgUrl})`;
|
||
|
|
||
|
// Preload image
|
||
|
const img = new Image();
|
||
|
img.src = bgUrl;
|
||
|
}
|
||
|
|
||
|
option.addEventListener('click', () => {
|
||
|
setBackground(bgUrl === 'none' ? '' : bgUrl);
|
||
|
});
|
||
|
});
|
||
|
|
||
|
// Handle custom upload
|
||
|
fileInput.addEventListener('change', (e) => {
|
||
|
const file = e.target.files[0];
|
||
|
if (file) {
|
||
|
const reader = new FileReader();
|
||
|
reader.onload = (e) => {
|
||
|
setBackground(e.target.result);
|
||
|
};
|
||
|
reader.readAsDataURL(file);
|
||
|
}
|
||
|
});
|
||
|
|
||
|
removeButton.addEventListener('click', () => {
|
||
|
setBackground('');
|
||
|
fileInput.value = '';
|
||
|
});
|
||
|
|
||
|
// Settings popup handling
|
||
|
const settingsButton = document.querySelector('.settings-button');
|
||
|
const settingsPopup = document.getElementById('settingsPopup');
|
||
|
const closeSettings = document.querySelector('.close-settings');
|
||
|
|
||
|
settingsButton.addEventListener('click', () => {
|
||
|
settingsPopup.classList.add('show');
|
||
|
});
|
||
|
|
||
|
closeSettings.addEventListener('click', () => {
|
||
|
settingsPopup.classList.remove('show');
|
||
|
});
|
||
|
|
||
|
// Close popup when clicking outside
|
||
|
settingsPopup.addEventListener('click', (e) => {
|
||
|
if (e.target === settingsPopup) {
|
||
|
settingsPopup.classList.remove('show');
|
||
|
}
|
||
|
});
|
||
|
|
||
|
// Theme handling
|
||
|
const themeSelect = document.getElementById('themeSelect');
|
||
|
|
||
|
function setTheme(theme) {
|
||
|
const root = document.documentElement;
|
||
|
root.style.setProperty('--base', `var(--${theme}-base)`);
|
||
|
root.style.setProperty('--base-rgb', `var(--${theme}-base-rgb)`);
|
||
|
root.style.setProperty('--surface0', `var(--${theme}-surface0)`);
|
||
|
root.style.setProperty('--surface1', `var(--${theme}-surface1)`);
|
||
|
root.style.setProperty('--text', `var(--${theme}-text)`);
|
||
|
root.style.setProperty('--blue', `var(--${theme}-blue)`);
|
||
|
root.style.setProperty('--pink', `var(--${theme}-pink)`);
|
||
|
localStorage.setItem('theme', theme);
|
||
|
}
|
||
|
|
||
|
// Load saved theme
|
||
|
const savedTheme = localStorage.getItem('theme') || 'mocha';
|
||
|
themeSelect.value = savedTheme;
|
||
|
setTheme(savedTheme);
|
||
|
|
||
|
// Handle theme changes
|
||
|
themeSelect.addEventListener('change', (e) => {
|
||
|
setTheme(e.target.value);
|
||
|
});
|
||
|
|
||
|
// Initialize managers globally
|
||
|
window.linksManager = new LinksManager();
|
||
|
window.searchManager = new SearchEngineManager();
|
||
|
window.shortcutManager = new ShortcutManager();
|
||
|
const shortcutManager = new ShortcutManager();
|
||
|
|
||
|
// Layout handling
|
||
|
const layoutSelect = document.getElementById('layoutSelect');
|
||
|
layoutSelect.addEventListener('change', (e) => {
|
||
|
document.body.setAttribute('data-layout', e.target.value);
|
||
|
localStorage.setItem('layout', e.target.value);
|
||
|
});
|
||
|
|
||
|
// Load saved layout
|
||
|
const savedLayout = localStorage.getItem('layout') || 'grid';
|
||
|
layoutSelect.value = savedLayout;
|
||
|
document.body.setAttribute('data-layout', savedLayout);
|
||
|
|
||
|
// Custom CSS handling
|
||
|
const customCSSEditor = document.getElementById('customCSS');
|
||
|
customCSSEditor.value = localStorage.getItem('customCSS') || '';
|
||
|
customCSSEditor.addEventListener('change', () => {
|
||
|
localStorage.setItem('customCSS', customCSSEditor.value);
|
||
|
applyCustomCSS();
|
||
|
});
|
||
|
|
||
|
function applyCustomCSS() {
|
||
|
let customStyle = document.getElementById('custom-css') || document.createElement('style');
|
||
|
customStyle.id = 'custom-css';
|
||
|
customStyle.textContent = customCSSEditor.value;
|
||
|
document.head.appendChild(customStyle);
|
||
|
}
|
||
|
|
||
|
// Apply custom CSS on load
|
||
|
applyCustomCSS();
|
||
|
|
||
|
// Font handling
|
||
|
const fontSelect = document.getElementById('fontSelect');
|
||
|
fontSelect.addEventListener('change', (e) => {
|
||
|
document.documentElement.style.setProperty('--font-family', e.target.value);
|
||
|
if (e.target.value === 'MapleMono') {
|
||
|
document.documentElement.style.setProperty('--font-mono', 'MapleMono');
|
||
|
}
|
||
|
localStorage.setItem('font', e.target.value);
|
||
|
});
|
||
|
|
||
|
// Load saved font
|
||
|
const savedFont = localStorage.getItem('font');
|
||
|
if (savedFont) {
|
||
|
fontSelect.value = savedFont;
|
||
|
document.documentElement.style.setProperty('--font-family', savedFont);
|
||
|
if (savedFont === 'MapleMono') {
|
||
|
document.documentElement.style.setProperty('--font-mono', 'MapleMono');
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Event delegation for settings actions
|
||
|
document.addEventListener('click', (e) => {
|
||
|
const action = e.target.dataset.action;
|
||
|
if (action) {
|
||
|
switch (action) {
|
||
|
case 'openSettings':
|
||
|
settingsPopup.classList.add('show');
|
||
|
break;
|
||
|
case 'closeSettings':
|
||
|
settingsPopup.classList.remove('show');
|
||
|
break;
|
||
|
case 'removeBackground':
|
||
|
setBackground('');
|
||
|
fileInput.value = '';
|
||
|
break;
|
||
|
case 'saveName':
|
||
|
const name = nameInput.value.trim();
|
||
|
if (name) {
|
||
|
localStorage.setItem('username', name);
|
||
|
} else {
|
||
|
localStorage.removeItem('username');
|
||
|
nameInput.value = ''; // Clear input if empty
|
||
|
}
|
||
|
clock.updateGreeting();
|
||
|
break;
|
||
|
case 'addCategory':
|
||
|
const categoryName = prompt('Enter category name:');
|
||
|
if (categoryName) linksManager.addCategory(categoryName);
|
||
|
break;
|
||
|
case 'addLink':
|
||
|
const category = prompt('Enter category:');
|
||
|
if (category && linksManager.links[category]) {
|
||
|
linksManager.addLink(category, {
|
||
|
name: 'New Link',
|
||
|
url: 'https://',
|
||
|
icon: 'fas fa-link'
|
||
|
});
|
||
|
}
|
||
|
break;
|
||
|
case 'exportLinks':
|
||
|
linksManager.export();
|
||
|
break;
|
||
|
case 'importLinks':
|
||
|
document.getElementById('importLinks').click();
|
||
|
break;
|
||
|
default:
|
||
|
break;
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
|
||
|
document.getElementById('importLinks').addEventListener('change', (e) => {
|
||
|
if (e.target.files[0]) linksManager.import(e.target.files[0]);
|
||
|
});
|
||
|
});
|