import app from '../app.js';
import './login-view.js';
import './file-list.js';
import './file-upload-view.js';
import './share-modal.js';
import './photo-gallery.js';
import './file-preview.js';
import './deleted-files.js';
import './admin-dashboard.js';
import './toast-notification.js';
import './starred-items.js';
import './recent-files.js';
import './shared-items.js';
import './billing-dashboard.js';
import './admin-billing.js';
import './code-editor-view.js';
import './cookie-consent.js';
import './user-settings.js'; // Import the new user settings component
import { shortcuts } from '../shortcuts.js';
const api = app.getAPI();
const logger = app.getLogger();
const appState = app.getState();
export class MyWebdavApp extends HTMLElement {
constructor() {
super();
this.currentView = 'files';
this.user = null;
this.navigationStack = [];
this.boundHandlePopState = this.handlePopState.bind(this);
this.popstateAttached = false;
this.currentSearchId = 0;
}
async connectedCallback() {
try {
await this.init();
this.addEventListener('show-toast', this.handleShowToast);
if (!this.popstateAttached) {
window.addEventListener('popstate', this.boundHandlePopState);
this.popstateAttached = true;
logger.debug('Popstate listener attached');
}
} catch (error) {
logger.error('Failed to initialize MyWebdavApp', error);
this.innerHTML = `
<div style="padding: 2rem; text-align: center; font-family: sans-serif;">
<h1 style="color: #d32f2f;">Failed to Load Application</h1>
<p>${error.message}</p>
<button onclick="location.reload()" style="padding: 0.75rem 1.5rem; background: #2196F3; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 1rem;">
Reload Page
</button>
</div>
`;
}
}
disconnectedCallback() {
this.removeEventListener('show-toast', this.handleShowToast);
if (this.popstateAttached) {
window.removeEventListener('popstate', this.boundHandlePopState);
this.popstateAttached = false;
logger.debug('Popstate listener removed');
}
}
handleShowToast = (event) => {
const { message, type, duration } = event.detail;
this.showToast(message, type, duration);
}
showToast(message, type = 'info', duration = 3000) {
const toast = document.createElement('toast-notification');
document.body.appendChild(toast);
toast.show(message, type, duration);
}
async init() {
try {
if (!api.getToken()) {
logger.info('No token found, showing login');
this.showLogin();
} else {
logger.info('Initializing application with stored token');
this.user = await api.getCurrentUser();
appState.setState({ user: this.user });
logger.info('User loaded successfully', { username: this.user.username });
this.render();
}
} catch (error) {
logger.error('Failed to initialize application', error);
api.setToken(null);
this.showLogin();
}
}
showLogin() {
this.innerHTML = `
<div class="login-container">
<login-view></login-view>
</div>
<footer class="app-footer">
<nav class="footer-nav">
<ul class="footer-links">
<li><a href="/static/legal/privacy_policy.md" target="_blank" rel="noopener noreferrer">Privacy Policy</a></li>
<li><a href="/static/legal/data_processing_agreement.md" target="_blank" rel="noopener noreferrer">Data Processing Agreement</a></li>
<li><a href="/static/legal/terms_of_service.md" target="_blank" rel="noopener noreferrer">Terms of Service</a></li>
<li><a href="/static/legal/cookie_policy.md" target="_blank" rel="noopener noreferrer">Cookie Policy</a></li>
<li><a href="/static/legal/security_policy.md" target="_blank" rel="noopener noreferrer">Security Policy</a></li>
<li><a href="/static/legal/compliance_statement.md" target="_blank" rel="noopener noreferrer">Compliance Statement</a></li>
<li><a href="/static/legal/data_portability_deletion_policy.md" target="_blank" rel="noopener noreferrer">Data Portability & Deletion</a></li>
<li><a href="/static/legal/contact_complaint_mechanism.md" target="_blank" rel="noopener noreferrer">Contact & Complaints</a></li>
</ul>
</nav>
<p class="footer-text">&copy; ${new Date().getFullYear()} MyWebdav Cloud Storage. All rights reserved.</p>
</footer>
<cookie-consent></cookie-consent>
`;
const loginView = this.querySelector('login-view');
loginView.addEventListener('auth-success', () => this.init());
}
render() {
this.innerHTML = `
<div class="app-container">
<header class="app-header">
<div class="header-left">
<h1 class="app-title">MyWebdav</h1>
</div>
<div class="header-center">
<input type="search" placeholder="Search..." class="search-input" id="search-input">
</div>
<div class="header-right">
<span class="user-info">${this.user.username}</span>
<button class="button" id="logout-btn">Logout</button>
</div>
</header>
<div class="app-body">
<aside class="app-sidebar">
<nav class="sidebar-nav">
<h3 class="nav-title">Navigation</h3>
<ul class="nav-list">
<li><a href="#" class="nav-link active" data-view="files">My Files</a></li>
<li><a href="#" class="nav-link" data-view="photos">Photo Gallery</a></li>
<li><a href="#" class="nav-link" data-view="shared">Shared Items</a></li>
<li><a href="#" class="nav-link" data-view="deleted">Deleted Files</a></li>
<li><a href="#" class="nav-link" data-view="billing">Billing</a></li>
<li><a href="#" class="nav-link" data-view="user-settings">User Settings</a></li>
${this.user && this.user.is_superuser ? `<li><a href="#" class="nav-link" data-view="admin">Admin Dashboard</a></li>` : ''}
${this.user && this.user.is_superuser ? `<li><a href="#" class="nav-link" data-view="admin-billing">Admin Billing</a></li>` : ''}
</ul>
<h3 class="nav-title">Quick Access</h3>
<ul class="nav-list">
<li><a href="#" class="nav-link" data-view="starred">Starred</a></li>
<li><a href="#" class="nav-link" data-view="recent">Recent</a></li>
</ul>
</nav>
</aside>
<main class="app-main">
<div id="main-content">
<file-list></file-list>
</div>
</main>
</div>
<share-modal></share-modal>
<footer class="app-footer">
<nav class="footer-nav">
<ul class="footer-links">
<li><a href="/static/legal/privacy_policy.md" target="_blank" rel="noopener noreferrer">Privacy Policy</a></li>
<li><a href="/static/legal/data_processing_agreement.md" target="_blank" rel="noopener noreferrer">Data Processing Agreement</a></li>
<li><a href="/static/legal/terms_of_service.md" target="_blank" rel="noopener noreferrer">Terms of Service</a></li>
<li><a href="/static/legal/cookie_policy.md" target="_blank" rel="noopener noreferrer">Cookie Policy</a></li>
<li><a href="/static/legal/security_policy.md" target="_blank" rel="noopener noreferrer">Security Policy</a></li>
<li><a href="/static/legal/compliance_statement.md" target="_blank" rel="noopener noreferrer">Compliance Statement</a></li>
<li><a href="/static/legal/data_portability_deletion_policy.md" target="_blank" rel="noopener noreferrer">Data Portability & Deletion</a></li>
<li><a href="/static/legal/contact_complaint_mechanism.md" target="_blank" rel="noopener noreferrer">Contact & Complaints</a></li>
</ul>
</nav>
<p class="footer-text">&copy; ${new Date().getFullYear()} MyWebdav Cloud Storage. All rights reserved.</p>
</footer>
</div>
<cookie-consent></cookie-consent>
`;
this.initializeNavigation();
this.attachListeners();
this.registerShortcuts();
}
initializeNavigation() {
if (!window.history.state) {
const hash = window.location.hash.slice(1);
if (hash && hash !== '') {
const view = hash.split('/')[0];
const validViews = ['files', 'photos', 'shared', 'deleted', 'starred', 'recent', 'admin', 'billing', 'admin-billing'];
if (validViews.includes(view)) {
window.history.replaceState({ view: view }, '', `#${hash}`);
this.currentView = view;
} else {
window.history.replaceState({ view: 'files' }, '', '#files');
}
} else {
window.history.replaceState({ view: 'files' }, '', '#files');
}
}
}
registerShortcuts() {
shortcuts.register('ctrl+u', () => {
const fileList = this.querySelector('file-list');
const folderId = fileList ? fileList.currentFolderId : null;
this.showUpload(folderId);
});
shortcuts.register('ctrl+f', () => {
const searchInput = this.querySelector('#search-input');
if (searchInput) {
searchInput.focus();
}
});
shortcuts.register('ctrl+/', () => {
this.showShortcutsHelp();
});
shortcuts.register('ctrl+shift+n', () => {
if (this.currentView === 'files') {
const fileList = this.querySelector('file-list');
if (fileList) {
fileList.triggerCreateFolder();
}
}
});
shortcuts.register('1', () => {
this.switchView('files');
});
shortcuts.register('2', () => {
this.switchView('photos');
});
shortcuts.register('3', () => {
this.switchView('shared');
});
shortcuts.register('4', () => {
this.switchView('deleted');
});
shortcuts.register('5', () => {
if (this.user && this.user.is_superuser) {
this.switchView('admin');
}
});
shortcuts.register('f2', () => {
const fileListComponent = document.querySelector('file-list');
if (fileListComponent && fileListComponent.selectedFiles && fileListComponent.selectedFiles.size === 1) {
const fileId = Array.from(fileListComponent.selectedFiles)[0];
const file = fileListComponent.files.find(f => f.id === fileId);
if (file) {
const newName = prompt('Enter new name:', file.name);
if (newName && newName !== file.name) {
api.renameFile(fileId, newName).then(() => {
fileListComponent.loadContents(fileListComponent.currentFolderId);
document.dispatchEvent(new CustomEvent('show-toast', {
detail: { message: 'File renamed successfully', type: 'success' }
}));
}).catch(error => {
document.dispatchEvent(new CustomEvent('show-toast', {
detail: { message: 'Failed to rename file', type: 'error' }
}));
});
}
}
} else if (fileListComponent && fileListComponent.selectedFolders && fileListComponent.selectedFolders.size === 1) {
const folderId = Array.from(fileListComponent.selectedFolders)[0];
const folder = fileListComponent.folders.find(f => f.id === folderId);
if (folder) {
const newName = prompt('Enter new name:', folder.name);
if (newName && newName !== folder.name) {
api.updateFolder(folderId, { name: newName }).then(() => {
fileListComponent.loadContents(fileListComponent.currentFolderId);
document.dispatchEvent(new CustomEvent('show-toast', {
detail: { message: 'Folder renamed successfully', type: 'success' }
}));
}).catch(error => {
document.dispatchEvent(new CustomEvent('show-toast', {
detail: { message: 'Failed to rename folder', type: 'error' }
}));
});
}
}
}
});
}
showShortcutsHelp() {
const helpContent = `
<div class="shortcuts-help-modal">
<div class="shortcuts-help-content">
<h2>Keyboard Shortcuts</h2>
<div class="shortcuts-list">
<h3>File Operations</h3>
<div class="shortcut-item">
<kbd>Ctrl + U</kbd>
<span>Upload files</span>
</div>
<div class="shortcut-item">
<kbd>Ctrl + Shift + N</kbd>
<span>Create new folder</span>
</div>
<div class="shortcut-item">
<kbd>Ctrl + F</kbd>
<span>Focus search</span>
</div>
<h3>Navigation</h3>
<div class="shortcut-item">
<kbd>1</kbd>
<span>My Files</span>
</div>
<div class="shortcut-item">
<kbd>2</kbd>
<span>Photo Gallery</span>
</div>
<div class="shortcut-item">
<kbd>3</kbd>
<span>Shared Items</span>
</div>
<div class="shortcut-item">
<kbd>4</kbd>
<span>Deleted Files</span>
</div>
${this.user && this.user.is_superuser ? `
<div class="shortcut-item">
<kbd>5</kbd>
<span>Admin Dashboard</span>
</div>` : ''}
<h3>General</h3>
<div class="shortcut-item">
<kbd>ESC</kbd>
<span>Close modals</span>
</div>
<div class="shortcut-item">
<kbd>Ctrl + /</kbd>
<span>Show this help</span>
</div>
</div>
<button class="button" id="close-shortcuts-help">Close</button>
</div>
</div>
`;
const helpDiv = document.createElement('div');
helpDiv.innerHTML = helpContent;
helpDiv.querySelector('.shortcuts-help-modal').style.cssText = `
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0,0,0,0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 10000;
`;
document.body.appendChild(helpDiv);
const closeHelp = () => {
document.body.removeChild(helpDiv);
document.removeEventListener('keydown', handleEscape);
};
const handleEscape = (e) => {
if (e.key === 'Escape') {
closeHelp();
}
};
const closeBtn = helpDiv.querySelector('#close-shortcuts-help');
closeBtn.addEventListener('click', closeHelp);
helpDiv.querySelector('.shortcuts-help-modal').addEventListener('click', (e) => {
if (e.target.classList.contains('shortcuts-help-modal')) closeHelp();
});
document.addEventListener('keydown', handleEscape);
}
attachListeners() {
this.querySelector('#logout-btn')?.addEventListener('click', () => {
logger.info('User logout initiated', { action: 'USER_LOGOUT' });
api.logout();
});
this.querySelectorAll('.nav-link').forEach(link => {
link.addEventListener('click', (e) => {
e.preventDefault();
const view = link.dataset.view;
this.switchView(view);
});
});
const fileList = this.querySelector('file-list');
if (fileList) {
fileList.addEventListener('upload-request', (e) => {
this.showUpload(e.detail.folderId);
});
fileList.addEventListener('folder-open', (e) => {
fileList.loadContents(e.detail.folderId);
});
fileList.addEventListener('share-request', (e) => {
const modal = this.querySelector('share-modal');
modal.show(e.detail.fileId);
});
}
this.addEventListener('upload-complete', () => {
const fileList = this.querySelector('file-list');
if (fileList) {
fileList.loadContents(fileList.currentFolderId);
}
});
const searchInput = this.querySelector('#search-input');
if (searchInput) {
let searchTimeout;
searchInput.addEventListener('input', (e) => {
clearTimeout(searchTimeout);
const query = e.target.value.trim();
if (query.length > 0) {
searchTimeout = setTimeout(() => this.performSearch(query), 300);
}
});
}
this.addEventListener('photo-click', (e) => {
this.showFilePreview(e.detail.photo);
});
this.addEventListener('share-file', (e) => {
const modal = this.querySelector('share-modal');
modal.show(e.detail.file.id);
});
this.addEventListener('edit-file', (e) => {
this.showCodeEditor(e.detail.file);
});
}
handlePopState(e) {
logger.debug('Popstate event', { state: e.state, url: window.location.href });
this.closeAllOverlays();
if (e.state && e.state.view) {
const view = e.state.view;
if (view === 'code-editor' && e.state.file) {
logger.debug('Restoring code editor view');
this.showCodeEditor(e.state.file, false);
} else if (view === 'file-preview' && e.state.file) {
logger.debug('Restoring file preview view');
this.showFilePreview(e.state.file, false);
} else if (view === 'upload') {
logger.debug('Restoring upload view');
const folderId = e.state.folderId !== undefined ? e.state.folderId : null;
this.showUpload(folderId, false);
} else {
logger.debug('Switching to view', { view });
this.switchView(view, false);
}
} else {
logger.debug('No state, defaulting to files view');
this.switchView('files', false);
}
}
closeAllOverlays() {
logger.debug('Closing all overlays');
const existingEditor = this.querySelector('code-editor-view');
if (existingEditor) {
logger.debug('Hiding code editor');
existingEditor.hide();
}
const existingPreview = this.querySelector('file-preview');
if (existingPreview) {
logger.debug('Hiding file preview');
existingPreview.hide();
}
const existingUpload = this.querySelector('file-upload-view');
if (existingUpload) {
logger.debug('Hiding file upload');
existingUpload.hide();
}
const shareModal = this.querySelector('share-modal');
if (shareModal && shareModal.style.display !== 'none') {
logger.debug('Hiding share modal');
shareModal.style.display = 'none';
}
}
showCodeEditor(file, pushState = true) {
logger.debug('Showing code editor', { file: file.name, pushState });
this.closeAllOverlays();
const mainElement = this.querySelector('.app-main');
const editorView = document.createElement('code-editor-view');
mainElement.appendChild(editorView);
editorView.setFile(file, this.currentView);
if (pushState) {
const currentState = window.history.state || {};
const currentView = currentState.view || this.currentView;
if (currentView !== 'code-editor') {
window.history.pushState(
{ view: 'code-editor', file: file, previousView: currentView },
'',
`#editor/${file.id}`
);
logger.debug('Pushed code editor state', { previousView: currentView });
} else {
logger.debug('Already in code editor view, replacing state');
window.history.replaceState(
{ view: 'code-editor', file: file, previousView: currentView },
'',
`#editor/${file.id}`
);
}
}
}
showFilePreview(file, pushState = true) {
logger.debug('Showing file preview', { file: file.name, pushState });
this.closeAllOverlays();
const mainElement = this.querySelector('.app-main');
const preview = document.createElement('file-preview');
mainElement.appendChild(preview);
preview.show(file, false);
if (pushState) {
const currentState = window.history.state || {};
const currentView = currentState.view || this.currentView;
if (currentView !== 'file-preview') {
window.history.pushState(
{ view: 'file-preview', file: file, previousView: currentView },
'',
`#preview/${file.id}`
);
logger.debug('Pushed file preview state', { previousView: currentView });
} else {
logger.debug('Already in file preview view, replacing state');
window.history.replaceState(
{ view: 'file-preview', file: file, previousView: currentView },
'',
`#preview/${file.id}`
);
}
}
}
showUpload(folderId = null, pushState = true) {
logger.debug('Showing upload view', { folderId, pushState });
this.closeAllOverlays();
const mainElement = this.querySelector('.app-main');
const uploadView = document.createElement('file-upload-view');
mainElement.appendChild(uploadView);
uploadView.setFolder(folderId);
if (pushState) {
const currentState = window.history.state || {};
const currentView = currentState.view || this.currentView;
if (currentView !== 'upload') {
window.history.pushState(
{ view: 'upload', folderId: folderId, previousView: currentView },
'',
'#upload'
);
logger.debug('Pushed upload state', { previousView: currentView });
} else {
logger.debug('Already in upload view, replacing state');
window.history.replaceState(
{ view: 'upload', folderId: folderId, previousView: currentView },
'',
'#upload'
);
}
}
}
async performSearch(query) {
const searchId = ++this.currentSearchId;
try {
const files = await api.searchFiles(query);
if (searchId !== this.currentSearchId) return;
const mainContent = this.querySelector('#main-content');
mainContent.innerHTML = `
<div class="search-results">
<h2>Search Results for "${query}"</h2>
<file-list data-search-mode="true"></file-list>
</div>
`;
const fileList = mainContent.querySelector('file-list');
fileList.setFiles(files);
this.attachListeners();
} catch (error) {
if (searchId === this.currentSearchId) {
console.error('Search failed:', error);
}
}
}
switchView(view, pushState = true) {
this.closeAllOverlays();
if(this.currentView === view) return;
this.currentView = view;
this.querySelectorAll('.nav-link').forEach(link => {
link.classList.remove('active');
});
this.querySelector(`[data-view="${view}"]`)?.classList.add('active');
const mainContent = this.querySelector('#main-content');
if (pushState) {
window.history.pushState({ view: view }, '', `#${view}`);
}
switch (view) {
case 'files':
mainContent.innerHTML = '<file-list></file-list>';
this.attachListeners();
break;
case 'photos':
mainContent.innerHTML = '<photo-gallery></photo-gallery>';
this.attachListeners();
break;
case 'shared':
mainContent.innerHTML = '<shared-items></shared-items>';
this.attachListeners();
break;
case 'deleted':
mainContent.innerHTML = '<deleted-files></deleted-files>';
this.attachListeners();
break;
case 'starred':
mainContent.innerHTML = '<starred-items></starred-items>';
this.attachListeners();
break;
case 'recent':
mainContent.innerHTML = '<recent-files></recent-files>';
this.attachListeners();
break;
case 'admin':
mainContent.innerHTML = '<admin-dashboard></admin-dashboard>';
this.attachListeners();
break;
case 'billing':
mainContent.innerHTML = '<billing-dashboard></billing-dashboard>';
this.attachListeners();
break;
case 'user-settings':
mainContent.innerHTML = '<user-settings></user-settings>';
this.attachListeners();
break;
case 'admin-billing':
mainContent.innerHTML = '<admin-billing></admin-billing>';
this.attachListeners();
break;
}
}
}