buggy init :3
This commit is contained in:
commit
0f96641be1
2179 changed files with 388452 additions and 0 deletions
303
startpage/js/main.js
Executable file
303
startpage/js/main.js
Executable file
|
@ -0,0 +1,303 @@
|
|||
import { Clock } from './modules/Clock.js';
|
||||
import { LinksManager } from './modules/LinksManager.js';
|
||||
import { SearchEngineManager } from './modules/SearchManager.js';
|
||||
import { ShortcutManager } from './modules/ShortcutManager.js';
|
||||
import { SettingsManager } from './modules/SettingsManager.js';
|
||||
|
||||
// Make managers available globally
|
||||
window.linksManager = null;
|
||||
window.searchManager = null;
|
||||
window.shortcutManager = null;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Debug Font Awesome loading
|
||||
const testIcon = document.createElement('i');
|
||||
testIcon.className = 'fas fa-check';
|
||||
console.log('Font Awesome test:', window.getComputedStyle(testIcon).fontFamily);
|
||||
|
||||
// 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]);
|
||||
});
|
||||
|
||||
// Link size handling with reset functionality
|
||||
const linkSizeControl = document.getElementById('linkSizeControl');
|
||||
const linkSizeValue = document.getElementById('linkSizeValue');
|
||||
const resetLinkSize = document.getElementById('resetLinkSize');
|
||||
const DEFAULT_LINK_SIZE = 16;
|
||||
|
||||
const setLinkSize = (size) => {
|
||||
document.documentElement.style.setProperty('--link-size', `${size}px`);
|
||||
linkSizeValue.textContent = `${size}px`;
|
||||
linkSizeControl.value = size;
|
||||
localStorage.setItem('linkSize', size);
|
||||
};
|
||||
|
||||
// Load saved link size or set default
|
||||
const savedLinkSize = localStorage.getItem('linkSize') || DEFAULT_LINK_SIZE;
|
||||
setLinkSize(parseInt(savedLinkSize));
|
||||
|
||||
// Update on change
|
||||
linkSizeControl.addEventListener('input', (e) => {
|
||||
setLinkSize(parseInt(e.target.value));
|
||||
});
|
||||
|
||||
// Reset button handler
|
||||
resetLinkSize.addEventListener('click', () => {
|
||||
setLinkSize(DEFAULT_LINK_SIZE);
|
||||
});
|
||||
});
|
43
startpage/js/modules/Clock.js
Executable file
43
startpage/js/modules/Clock.js
Executable file
|
@ -0,0 +1,43 @@
|
|||
export 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;
|
||||
}
|
||||
}
|
387
startpage/js/modules/LinksManager.js
Executable file
387
startpage/js/modules/LinksManager.js
Executable file
|
@ -0,0 +1,387 @@
|
|||
export class LinksManager {
|
||||
constructor() {
|
||||
this.iconPacks = {
|
||||
'Font Awesome Icons': {
|
||||
prefix: {
|
||||
brands: 'fab fa-',
|
||||
solid: 'fas fa-'
|
||||
},
|
||||
icons: {
|
||||
brands: [
|
||||
'github', 'gitlab', 'reddit', 'twitter', 'youtube', 'spotify', 'discord', 'twitch',
|
||||
'steam', 'linkedin', 'instagram', 'facebook', 'amazon', 'apple', 'google',
|
||||
'pinterest', 'microsoft', 'windows', 'linux', 'chrome', 'firefox', 'opera',
|
||||
'html5', 'css3', 'js', 'npm', 'node', 'react', 'angular',
|
||||
'wordpress', 'sass', 'gulp', 'python', 'java', 'php', 'stack-overflow',
|
||||
'docker', 'android'
|
||||
],
|
||||
solid: [
|
||||
'home', 'link', 'folder', 'file', 'book', 'code', 'film',
|
||||
'envelope', 'star', 'heart', 'user', 'cog', 'search',
|
||||
'lock', 'check', 'times', 'plus', 'minus', 'edit', 'trash',
|
||||
'download', 'upload', 'print', 'camera', 'phone', 'desktop',
|
||||
'wifi', 'moon', 'sun', 'cloud', 'bell', 'bookmark', 'map',
|
||||
'globe', 'eye', 'pen', 'terminal', 'folder-open', 'image'
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
this.customIcons = JSON.parse(localStorage.getItem('customIcons')) || {};
|
||||
this.links = JSON.parse(localStorage.getItem('customLinks')) || this.getDefaultLinks();
|
||||
this.categoriesEditor = document.getElementById('categories-editor');
|
||||
this.init();
|
||||
}
|
||||
|
||||
getDefaultLinks() {
|
||||
return {
|
||||
'Development': [
|
||||
{ name: 'GitHub', url: 'https://github.com', icon: 'fab fa-github' },
|
||||
{ name: 'Stack Overflow', url: 'https://stackoverflow.com', icon: 'fab fa-stack-overflow' },
|
||||
{ name: 'MDN', url: 'https://developer.mozilla.org', icon: 'fas fa-book' }
|
||||
],
|
||||
'Social': [
|
||||
{ name: 'Reddit', url: 'https://www.reddit.com', icon: 'fab fa-reddit' },
|
||||
{ name: 'Twitter', url: 'https://www.twitter.com', icon: 'fab fa-twitter' },
|
||||
{ name: 'LinkedIn', url: 'https://www.linkedin.com', icon: 'fab fa-linkedin' }
|
||||
],
|
||||
'Entertainment': [
|
||||
{ name: 'YouTube', url: 'https://www.youtube.com', icon: 'fab fa-youtube' },
|
||||
{ name: 'Netflix', url: 'https://www.netflix.com', icon: 'fas fa-film' },
|
||||
{ name: 'Spotify', url: 'https://www.spotify.com', icon: 'fab fa-spotify' }
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
init() {
|
||||
this.renderLinks();
|
||||
this.attachEventListeners();
|
||||
}
|
||||
|
||||
renderLinks() {
|
||||
// Render links in settings
|
||||
this.renderLinksEditor();
|
||||
// Render links in main container
|
||||
this.renderLinksContainer();
|
||||
}
|
||||
|
||||
renderLinksEditor() {
|
||||
this.categoriesEditor.innerHTML = Object.entries(this.links)
|
||||
.map(([category, links]) => `
|
||||
<div class="category-item">
|
||||
<h5>${category}</h5>
|
||||
<button class="settings-btn small remove-category" data-category="${category}">
|
||||
<i class="fas fa-trash"></i>
|
||||
</button>
|
||||
<div class="category-links">
|
||||
${links.map((link, index) => {
|
||||
const iconHtml = link.icon.startsWith('custom-')
|
||||
? `<img src="${this.customIcons[link.icon.replace('custom-', '')]}" style="width: 16px; height: 16px; vertical-align: middle;">`
|
||||
: `<i class="${link.icon}"></i>`;
|
||||
return `
|
||||
<div class="link-item">
|
||||
<div class="icon-select" data-category="${category}" data-index="${index}">
|
||||
${iconHtml}
|
||||
</div>
|
||||
<input type="text" value="${link.name}" placeholder="Name"
|
||||
data-category="${category}" data-index="${index}" data-field="name">
|
||||
<input type="text" value="${link.url}" placeholder="URL"
|
||||
data-category="${category}" data-index="${index}" data-field="url">
|
||||
<button class="settings-btn small remove-link" data-category="${category}" data-index="${index}">
|
||||
<i class="fas fa-times"></i>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
}).join('')}
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
// Add event listeners
|
||||
this.categoriesEditor.addEventListener('click', (e) => {
|
||||
const target = e.target.closest('[data-category]');
|
||||
if (!target) return;
|
||||
|
||||
if (target.classList.contains('remove-category')) {
|
||||
this.removeCategory(target.dataset.category);
|
||||
} else if (target.classList.contains('remove-link')) {
|
||||
this.removeLink(target.dataset.category, parseInt(target.dataset.index));
|
||||
} else if (target.classList.contains('icon-select')) {
|
||||
this.showIconPicker(target.dataset.category, parseInt(target.dataset.index));
|
||||
}
|
||||
});
|
||||
|
||||
this.categoriesEditor.addEventListener('change', (e) => {
|
||||
const target = e.target;
|
||||
if (target.matches('input[data-field]')) {
|
||||
this.updateLink(
|
||||
target.dataset.category,
|
||||
parseInt(target.dataset.index),
|
||||
target.dataset.field,
|
||||
target.value
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
showIconPicker(category, linkIndex) {
|
||||
const popup = document.createElement('div');
|
||||
popup.className = 'icon-picker-popup';
|
||||
|
||||
const content = document.createElement('div');
|
||||
content.className = 'icon-picker-content';
|
||||
|
||||
const pack = this.iconPacks['Font Awesome Icons'];
|
||||
|
||||
content.innerHTML = `
|
||||
<div class="icon-picker-header">
|
||||
<h3>Icon Picker</h3>
|
||||
<div class="icon-picker-tabs">
|
||||
<button class="active" data-tab="fontawesome">Font Awesome Icons</button>
|
||||
<button data-tab="custom">Custom Icons</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="icon-sections">
|
||||
<div class="icon-section fontawesome-section">
|
||||
${[...pack.icons.brands.map(icon => ({
|
||||
class: pack.prefix.brands + icon,
|
||||
name: 'brands-' + icon
|
||||
})), ...pack.icons.solid.map(icon => ({
|
||||
class: pack.prefix.solid + icon,
|
||||
name: 'solid-' + icon
|
||||
}))].map(icon => `
|
||||
<div class="icon-item" data-icon="${icon.class}">
|
||||
<i class="${icon.class}"></i>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
<div class="icon-section custom-section" style="display: none;">
|
||||
<div class="custom-icons-grid">
|
||||
${Object.entries(this.customIcons).map(([name, url]) => `
|
||||
<div class="custom-icon-item" data-name="${name}">
|
||||
<img src="${url}" class="custom-icon" title="${name}">
|
||||
<button class="delete-icon" data-name="${name}">×</button>
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
<input type="file" id="customIconUpload" accept="image/*" style="display: none;">
|
||||
<button class="settings-btn upload-icon-btn">Upload Custom Icon</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// Event listeners for icons
|
||||
content.querySelectorAll('.icon-item').forEach(item => {
|
||||
item.addEventListener('click', () => {
|
||||
if (item.dataset.icon) {
|
||||
this.setIcon(category, linkIndex, item.dataset.icon);
|
||||
} else if (item.dataset.name && item.dataset.url) {
|
||||
this.setCustomIcon(category, linkIndex, item.dataset.name, item.dataset.url);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Add delete icon handler
|
||||
content.querySelectorAll('.delete-icon').forEach(btn => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
const name = btn.dataset.name;
|
||||
if (confirm(`Delete custom icon "${name}"?`)) {
|
||||
delete this.customIcons[name];
|
||||
localStorage.setItem('customIcons', JSON.stringify(this.customIcons));
|
||||
this.showIconPicker(category, linkIndex);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Update custom icon click handler
|
||||
content.querySelectorAll('.custom-icon-item').forEach(item => {
|
||||
item.addEventListener('click', () => {
|
||||
const name = item.dataset.name;
|
||||
if (name && this.customIcons[name]) {
|
||||
this.setCustomIcon(category, linkIndex, name, this.customIcons[name]);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Upload button handler
|
||||
const uploadBtn = content.querySelector('.upload-icon-btn');
|
||||
const fileInput = content.querySelector('#customIconUpload');
|
||||
uploadBtn?.addEventListener('click', () => fileInput.click());
|
||||
|
||||
// File input handler
|
||||
fileInput?.addEventListener('change', (e) => {
|
||||
const file = e.target.files[0];
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = (e) => {
|
||||
const name = file.name.replace(/\.[^/.]+$/, "");
|
||||
this.customIcons[name] = e.target.result;
|
||||
localStorage.setItem('customIcons', JSON.stringify(this.customIcons));
|
||||
this.showIconPicker(category, linkIndex);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
});
|
||||
|
||||
// Tab switching
|
||||
content.querySelectorAll('.icon-picker-tabs button').forEach(button => {
|
||||
button.addEventListener('click', () => {
|
||||
content.querySelectorAll('.icon-picker-tabs button').forEach(b => b.classList.remove('active'));
|
||||
button.classList.add('active');
|
||||
|
||||
const tab = button.dataset.tab;
|
||||
content.querySelectorAll('.icon-section').forEach(section => {
|
||||
section.style.display = section.classList.contains(`${tab}-section`) ? 'grid' : 'none';
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
popup.appendChild(content);
|
||||
document.body.appendChild(popup);
|
||||
|
||||
popup.addEventListener('click', (e) => {
|
||||
if (e.target === popup) popup.remove();
|
||||
});
|
||||
}
|
||||
|
||||
renderIconPackContent(packName) {
|
||||
const pack = this.iconPacks[packName];
|
||||
return Object.entries(pack.icons).map(([type, icons]) => `
|
||||
<div class="${type}-icons icon-grid">
|
||||
${icons.map(icon =>
|
||||
`<i class="${pack.prefix[type]}${icon}" title="${icon}"
|
||||
data-icon="${pack.prefix[type]}${icon}"></i>`
|
||||
).join('')}
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
setCustomIcon(category, index, name, url) {
|
||||
if (this.customIcons[name]) {
|
||||
this.updateLink(category, index, 'icon', `custom-${name}`);
|
||||
// Remove iconUrl since we're storing it in customIcons
|
||||
const link = this.links[category][index];
|
||||
delete link.iconUrl;
|
||||
this.save();
|
||||
document.querySelector('.icon-picker-popup').remove();
|
||||
}
|
||||
}
|
||||
|
||||
setIcon(category, index, icon) {
|
||||
this.updateLink(category, index, 'icon', icon);
|
||||
document.querySelector('.icon-picker-popup').remove();
|
||||
}
|
||||
|
||||
renderLinksContainer() {
|
||||
const container = document.querySelector('.links-container');
|
||||
container.innerHTML = Object.entries(this.links)
|
||||
.map(([category, links]) => `
|
||||
<div class="category">
|
||||
<h2>${category}</h2>
|
||||
<ul>
|
||||
${links.map(link => {
|
||||
if (link.icon.startsWith('custom-')) {
|
||||
const iconData = this.customIcons[link.icon.replace('custom-', '')];
|
||||
if (!iconData) return `<li><a href="${link.url}"><i class="fas fa-link"></i> ${link.name}</a></li>`;
|
||||
return `<li><a href="${link.url}"><img src="${iconData}" style="width: 1em; height: 1em; vertical-align: middle;"> ${link.name}</a></li>`;
|
||||
}
|
||||
return `<li><a href="${link.url}"><i class="${link.icon}"></i> ${link.name}</a></li>`;
|
||||
}).join('')}
|
||||
</ul>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
attachEventListeners() {
|
||||
document.getElementById('addCategory').addEventListener('click', () => {
|
||||
const name = prompt('Enter category name:');
|
||||
if (name) this.addCategory(name);
|
||||
});
|
||||
|
||||
document.getElementById('addLink').addEventListener('click', () => {
|
||||
const category = prompt('Enter category:');
|
||||
if (category && this.links[category]) {
|
||||
this.addLink(category, {
|
||||
name: 'New Link',
|
||||
url: 'https://',
|
||||
icon: 'fas fa-link'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('exportLinks').addEventListener('click', () => this.export());
|
||||
|
||||
document.getElementById('importLinksBtn').addEventListener('click', () => {
|
||||
document.getElementById('importLinks').click();
|
||||
});
|
||||
|
||||
document.getElementById('importLinks').addEventListener('change', (e) => {
|
||||
if (e.target.files[0]) this.import(e.target.files[0]);
|
||||
});
|
||||
}
|
||||
|
||||
addCategory(name) {
|
||||
if (!this.links[name]) {
|
||||
this.links[name] = [];
|
||||
this.save();
|
||||
this.renderLinks();
|
||||
}
|
||||
}
|
||||
|
||||
addLink(category, link) {
|
||||
this.links[category].push(link);
|
||||
this.save();
|
||||
this.renderLinks();
|
||||
}
|
||||
|
||||
updateLink(category, index, field, value) {
|
||||
if (this.links[category] && this.links[category][index]) {
|
||||
this.links[category][index][field] = value;
|
||||
this.save();
|
||||
this.renderLinks();
|
||||
}
|
||||
}
|
||||
|
||||
removeLink(category, index) {
|
||||
if (this.links[category]) {
|
||||
this.links[category].splice(index, 1);
|
||||
this.save();
|
||||
this.renderLinks();
|
||||
}
|
||||
}
|
||||
|
||||
removeCategory(category) {
|
||||
if (confirm(`Are you sure you want to delete the category "${category}" and all its links?`)) {
|
||||
delete this.links[category];
|
||||
this.save();
|
||||
this.renderLinks();
|
||||
}
|
||||
}
|
||||
|
||||
save() {
|
||||
localStorage.setItem('customLinks', JSON.stringify(this.links));
|
||||
}
|
||||
|
||||
export() {
|
||||
const blob = new Blob([JSON.stringify(this.links, null, 2)], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'links-config.json';
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
async import(file) {
|
||||
try {
|
||||
const text = await file.text();
|
||||
this.links = JSON.parse(text);
|
||||
this.save();
|
||||
this.renderLinks();
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Failed to import links:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
85
startpage/js/modules/SearchManager.js
Executable file
85
startpage/js/modules/SearchManager.js
Executable file
|
@ -0,0 +1,85 @@
|
|||
export 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));
|
||||
}
|
||||
}
|
140
startpage/js/modules/SettingsManager.js
Executable file
140
startpage/js/modules/SettingsManager.js
Executable file
|
@ -0,0 +1,140 @@
|
|||
export class SettingsManager {
|
||||
constructor() {
|
||||
this.themeManager = new ThemeManager();
|
||||
this.layoutManager = new LayoutManager();
|
||||
this.fontManager = new FontManager();
|
||||
this.settingsButton = document.querySelector('.settings-button');
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.attachEventListeners();
|
||||
this.loadSavedSettings();
|
||||
}
|
||||
|
||||
attachEventListeners() {
|
||||
const settingsPopup = document.getElementById('settingsPopup');
|
||||
const closeSettings = document.querySelector('.close-settings');
|
||||
|
||||
this.settingsButton.addEventListener('click', () => {
|
||||
settingsPopup.classList.add('show');
|
||||
this.settingsButton.classList.add('disabled');
|
||||
});
|
||||
|
||||
const closeSettingsHandler = () => {
|
||||
settingsPopup.classList.remove('show');
|
||||
this.settingsButton.classList.remove('disabled');
|
||||
};
|
||||
|
||||
closeSettings.addEventListener('click', closeSettingsHandler);
|
||||
|
||||
settingsPopup.addEventListener('click', (e) => {
|
||||
if (e.target === settingsPopup) {
|
||||
closeSettingsHandler();
|
||||
}
|
||||
});
|
||||
|
||||
// Add escape key handler
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape' && settingsPopup.classList.contains('show')) {
|
||||
closeSettingsHandler();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
loadSavedSettings() {
|
||||
// Load CSS
|
||||
const customCSSEditor = document.getElementById('customCSS');
|
||||
customCSSEditor.value = localStorage.getItem('customCSS') || '';
|
||||
this.applyCustomCSS();
|
||||
|
||||
// Load theme
|
||||
const savedTheme = localStorage.getItem('theme') || 'mocha';
|
||||
this.themeManager.setTheme(savedTheme);
|
||||
|
||||
// Load layout
|
||||
const savedLayout = localStorage.getItem('layout') || 'grid';
|
||||
this.layoutManager.setLayout(savedLayout);
|
||||
|
||||
// Load font
|
||||
const savedFont = localStorage.getItem('font');
|
||||
if (savedFont) {
|
||||
this.fontManager.setFont(savedFont);
|
||||
}
|
||||
}
|
||||
|
||||
applyCustomCSS() {
|
||||
const customCSSEditor = document.getElementById('customCSS');
|
||||
let customStyle = document.getElementById('custom-css') || document.createElement('style');
|
||||
customStyle.id = 'custom-css';
|
||||
customStyle.textContent = customCSSEditor.value;
|
||||
document.head.appendChild(customStyle);
|
||||
}
|
||||
}
|
||||
|
||||
class ThemeManager {
|
||||
constructor() {
|
||||
this.themeSelect = document.getElementById('themeSelect');
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.themeSelect.addEventListener('change', (e) => {
|
||||
this.setTheme(e.target.value);
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
this.themeSelect.value = theme;
|
||||
}
|
||||
}
|
||||
|
||||
class LayoutManager {
|
||||
constructor() {
|
||||
this.layoutSelect = document.getElementById('layoutSelect');
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.layoutSelect.addEventListener('change', (e) => {
|
||||
this.setLayout(e.target.value);
|
||||
});
|
||||
}
|
||||
|
||||
setLayout(layout) {
|
||||
document.body.setAttribute('data-layout', layout);
|
||||
localStorage.setItem('layout', layout);
|
||||
this.layoutSelect.value = layout;
|
||||
}
|
||||
}
|
||||
|
||||
class FontManager {
|
||||
constructor() {
|
||||
this.fontSelect = document.getElementById('fontSelect');
|
||||
this.init();
|
||||
}
|
||||
|
||||
init() {
|
||||
this.fontSelect.addEventListener('change', (e) => {
|
||||
this.setFont(e.target.value);
|
||||
});
|
||||
}
|
||||
|
||||
setFont(font) {
|
||||
document.documentElement.style.setProperty('--font-family', font);
|
||||
if (font === 'MapleMono') {
|
||||
document.documentElement.style.setProperty('--font-mono', 'MapleMono');
|
||||
}
|
||||
localStorage.setItem('font', font);
|
||||
this.fontSelect.value = font;
|
||||
}
|
||||
}
|
68
startpage/js/modules/ShortcutManager.js
Executable file
68
startpage/js/modules/ShortcutManager.js
Executable file
|
@ -0,0 +1,68 @@
|
|||
export 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));
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue