StartPage/startpage/js/modules/LinksManager.js
2025-01-31 20:35:44 +01:00

387 lines
16 KiB
JavaScript
Executable file
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;
}
}
}