+
Shared
diff --git a/static/js/app.js b/static/js/app.js
index 05429c6c..eb77490a 100644
--- a/static/js/app.js
+++ b/static/js/app.js
@@ -15,6 +15,10 @@ const app = {
isTrashView: false, // Whether we're in trash view
currentSection: 'files', // Current section: 'files' or 'trash'
isSearchMode: false, // Whether we're in search mode
+ // File sharing related properties
+ shareDialogItem: null, // Item being shared in share dialog
+ shareDialogItemType: null, // Type of item being shared ('file' or 'folder')
+ notificationShareUrl: null // URL for notification dialog
};
// DOM elements
@@ -29,8 +33,17 @@ function initApp() {
// Cache DOM elements
cacheElements();
- // Create menus and dialogs
- ui.initializeContextMenus();
+ // Initialize file sharing module first
+ if (window.fileSharing && window.fileSharing.init) {
+ window.fileSharing.init();
+ } else {
+ console.warn('fileSharing module not fully initialized');
+ }
+
+ // Then create menus and dialogs after modules have initialized
+ setTimeout(() => {
+ ui.initializeContextMenus();
+ }, 100);
// Setup event listeners
setupEventListeners();
@@ -152,6 +165,13 @@ function setupEventListeners() {
// Add active class to clicked item
item.classList.add('active');
+ // Check if this is the shared item
+ if (item.querySelector('span').getAttribute('data-i18n') === 'nav.shared') {
+ // Navigate to the shared page
+ window.location.href = '/shared.html';
+ return;
+ }
+
// Check if this is the trash item
if (item === elements.trashBtn) {
// Show trash view
diff --git a/static/js/auth.js b/static/js/auth.js
index 6254ee06..acc9b8ff 100644
--- a/static/js/auth.js
+++ b/static/js/auth.js
@@ -17,46 +17,78 @@ const TOKEN_EXPIRY_KEY = 'oxicloud_token_expiry';
const USER_DATA_KEY = 'oxicloud_user';
// DOM elements
-const loginPanel = document.getElementById('login-panel');
-const registerPanel = document.getElementById('register-panel');
-const adminSetupPanel = document.getElementById('admin-setup-panel');
+let loginPanel, registerPanel, adminSetupPanel;
+let loginForm, registerForm, adminSetupForm;
+let loginError, registerError, registerSuccess, adminSetupError;
-const loginForm = document.getElementById('login-form');
-const registerForm = document.getElementById('register-form');
-const adminSetupForm = document.getElementById('admin-setup-form');
+// Initialize DOM elements only if we're on the login page
+function initLoginElements() {
+ // Check if we're on the login page
+ if (!document.getElementById('login-form')) {
+ console.log('Not on login page, skipping element initialization');
+ return false;
+ }
+
+ loginPanel = document.getElementById('login-panel');
+ registerPanel = document.getElementById('register-panel');
+ adminSetupPanel = document.getElementById('admin-setup-panel');
-const loginError = document.getElementById('login-error');
-const registerError = document.getElementById('register-error');
-const registerSuccess = document.getElementById('register-success');
-const adminSetupError = document.getElementById('admin-setup-error');
+ loginForm = document.getElementById('login-form');
+ registerForm = document.getElementById('register-form');
+ adminSetupForm = document.getElementById('admin-setup-form');
-// Panel toggles
-document.getElementById('show-register').addEventListener('click', () => {
- loginPanel.style.display = 'none';
- registerPanel.style.display = 'block';
- adminSetupPanel.style.display = 'none';
-});
+ loginError = document.getElementById('login-error');
+ registerError = document.getElementById('register-error');
+ registerSuccess = document.getElementById('register-success');
+ adminSetupError = document.getElementById('admin-setup-error');
-document.getElementById('show-login').addEventListener('click', () => {
- loginPanel.style.display = 'block';
- registerPanel.style.display = 'none';
- adminSetupPanel.style.display = 'none';
-});
+ // Panel toggles
+ document.getElementById('show-register').addEventListener('click', () => {
+ loginPanel.style.display = 'none';
+ registerPanel.style.display = 'block';
+ adminSetupPanel.style.display = 'none';
+ });
-document.getElementById('show-admin-setup').addEventListener('click', () => {
- loginPanel.style.display = 'none';
- registerPanel.style.display = 'none';
- adminSetupPanel.style.display = 'block';
-});
+ document.getElementById('show-login').addEventListener('click', () => {
+ loginPanel.style.display = 'block';
+ registerPanel.style.display = 'none';
+ adminSetupPanel.style.display = 'none';
+ });
-document.getElementById('back-to-login').addEventListener('click', () => {
- loginPanel.style.display = 'block';
- registerPanel.style.display = 'none';
- adminSetupPanel.style.display = 'none';
-});
+ document.getElementById('show-admin-setup').addEventListener('click', () => {
+ loginPanel.style.display = 'none';
+ registerPanel.style.display = 'none';
+ adminSetupPanel.style.display = 'block';
+ });
+
+ document.getElementById('back-to-login').addEventListener('click', () => {
+ loginPanel.style.display = 'block';
+ registerPanel.style.display = 'none';
+ adminSetupPanel.style.display = 'none';
+ });
+
+ return true;
+}
+
+// Initialize login elements if on login page
+const isLoginPage = initLoginElements();
// Check if we already have a valid token
-document.addEventListener('DOMContentLoaded', async () => {
+let authInitialized = false;
+document.addEventListener('DOMContentLoaded', () => {
+ // Check if we're on the login page
+ if (!document.getElementById('login-form')) {
+ console.log('Not on login page, skipping auth check');
+ return;
+ }
+
+ if (authInitialized) {
+ console.log('Auth already initialized, skipping');
+ return;
+ }
+ authInitialized = true;
+
+ (async () => {
try {
const tokenExpiry = localStorage.getItem(TOKEN_EXPIRY_KEY);
if (tokenExpiry && new Date(tokenExpiry) > new Date()) {
@@ -87,17 +119,19 @@ document.addEventListener('DOMContentLoaded', async () => {
} catch (error) {
console.error('Authentication check failed:', error);
}
+ })();
});
// Login form submission
-loginForm.addEventListener('submit', async (e) => {
- e.preventDefault();
-
- // Clear previous errors
- loginError.style.display = 'none';
-
- const username = document.getElementById('login-username').value;
- const password = document.getElementById('login-password').value;
+if (isLoginPage && loginForm) {
+ loginForm.addEventListener('submit', async (e) => {
+ e.preventDefault();
+
+ // Clear previous errors
+ loginError.style.display = 'none';
+
+ const username = document.getElementById('login-username').value;
+ const password = document.getElementById('login-password').value;
try {
const data = await login(username, password);
@@ -161,9 +195,11 @@ loginForm.addEventListener('submit', async (e) => {
loginError.style.display = 'block';
}
});
+}
// Register form submission
-registerForm.addEventListener('submit', async (e) => {
+if (isLoginPage && registerForm) {
+ registerForm.addEventListener('submit', async (e) => {
e.preventDefault();
// Clear previous messages
@@ -202,9 +238,11 @@ registerForm.addEventListener('submit', async (e) => {
registerError.style.display = 'block';
}
});
+}
// Admin setup form submission
-adminSetupForm.addEventListener('submit', async (e) => {
+if (isLoginPage && adminSetupForm) {
+ adminSetupForm.addEventListener('submit', async (e) => {
e.preventDefault();
// Clear previous errors
@@ -235,6 +273,7 @@ adminSetupForm.addEventListener('submit', async (e) => {
adminSetupError.style.display = 'block';
}
});
+}
// API Functions
diff --git a/static/js/contextMenus.js b/static/js/contextMenus.js
index 2bf3c740..9e796fd2 100644
--- a/static/js/contextMenus.js
+++ b/static/js/contextMenus.js
@@ -23,6 +23,13 @@ const contextMenus = {
}
window.ui.closeContextMenu();
});
+
+ document.getElementById('share-folder-option').addEventListener('click', () => {
+ if (window.app.contextMenuTargetFolder) {
+ this.showShareDialog(window.app.contextMenuTargetFolder, 'folder');
+ }
+ window.ui.closeContextMenu();
+ });
document.getElementById('delete-folder-option').addEventListener('click', async () => {
if (window.app.contextMenuTargetFolder) {
@@ -42,6 +49,13 @@ const contextMenus = {
window.ui.closeFileContextMenu();
});
+ document.getElementById('share-file-option').addEventListener('click', () => {
+ if (window.app.contextMenuTargetFile) {
+ this.showShareDialog(window.app.contextMenuTargetFile, 'file');
+ }
+ window.ui.closeFileContextMenu();
+ });
+
document.getElementById('delete-file-option').addEventListener('click', async () => {
if (window.app.contextMenuTargetFile) {
await window.fileOps.deleteFile(
@@ -249,6 +263,226 @@ const contextMenus = {
} catch (error) {
console.error('Error loading folders:', error);
}
+ },
+ /**
+ * Show share dialog for files or folders
+ * @param {Object} item - File or folder object
+ * @param {string} itemType - 'file' or 'folder'
+ */
+ showShareDialog(item, itemType) {
+ // Update dialog title based on item type
+ const dialogHeader = document.getElementById('share-dialog').querySelector('.share-dialog-header');
+ const itemName = document.getElementById('shared-item-name');
+
+ // Update dialog content
+ dialogHeader.textContent = itemType === 'file' ?
+ (window.i18n ? window.i18n.t('dialogs.share_file') : 'Compartir archivo') :
+ (window.i18n ? window.i18n.t('dialogs.share_folder') : 'Compartir carpeta');
+
+ itemName.textContent = item.name;
+
+ // Reset form
+ document.getElementById('share-password').value = '';
+ document.getElementById('share-expiration').value = '';
+ document.getElementById('share-permission-read').checked = true;
+ document.getElementById('share-permission-write').checked = false;
+ document.getElementById('share-permission-reshare').checked = false;
+
+ // Store the current item and type for use when creating the share
+ window.app.shareDialogItem = item;
+ window.app.shareDialogItemType = itemType;
+
+ // Check if item already has shares
+ const existingShares = window.fileSharing.getSharedLinksForItem(item.id, itemType);
+ const existingSharesContainer = document.getElementById('existing-shares-container');
+
+ // Clear existing shares container
+ existingSharesContainer.innerHTML = '';
+
+ if (existingShares.length > 0) {
+ document.getElementById('existing-shares-section').style.display = 'block';
+
+ // Create elements for each existing share
+ existingShares.forEach(share => {
+ const shareEl = document.createElement('div');
+ shareEl.className = 'existing-share-item';
+
+ const expiresText = share.expires_at ?
+ `Vence: ${window.fileSharing.formatExpirationDate(share.expires_at)}` :
+ 'Sin vencimiento';
+
+ shareEl.innerHTML = `
+
${share.url}
+
+ ${share.password_protected ? ' Con contraseña' : ''}
+ ${expiresText}
+
+
+
+
+
+ `;
+
+ existingSharesContainer.appendChild(shareEl);
+ });
+
+ // Add event listeners for copy and delete buttons
+ document.querySelectorAll('.copy-link-btn').forEach(btn => {
+ btn.addEventListener('click', (e) => {
+ e.preventDefault();
+ const url = btn.getAttribute('data-share-url');
+ window.fileSharing.copyLinkToClipboard(url);
+ });
+ });
+
+ document.querySelectorAll('.delete-link-btn').forEach(btn => {
+ btn.addEventListener('click', (e) => {
+ e.preventDefault();
+ const shareId = btn.getAttribute('data-share-id');
+
+ if (confirm('¿Estás seguro de que quieres eliminar este enlace compartido?')) {
+ window.fileSharing.removeSharedLink(shareId);
+ btn.closest('.existing-share-item').remove();
+
+ // Check if we still have shares
+ if (existingSharesContainer.children.length === 0) {
+ document.getElementById('existing-shares-section').style.display = 'none';
+ }
+ }
+ });
+ });
+ } else {
+ document.getElementById('existing-shares-section').style.display = 'none';
+ }
+
+ // Show dialog
+ document.getElementById('share-dialog').style.display = 'flex';
+ },
+
+ /**
+ * Create a shared link with the configured options
+ */
+ createSharedLink() {
+ if (!window.app.shareDialogItem || !window.app.shareDialogItemType) {
+ window.ui.showNotification('Error', 'No se pudo compartir el elemento');
+ return;
+ }
+
+ // Get values from form
+ const password = document.getElementById('share-password').value;
+ const expirationDate = document.getElementById('share-expiration').value;
+ const permissionRead = document.getElementById('share-permission-read').checked;
+ const permissionWrite = document.getElementById('share-permission-write').checked;
+ const permissionReshare = document.getElementById('share-permission-reshare').checked;
+
+ // Prepare options
+ const options = {
+ password: password || null,
+ expirationDate: expirationDate || null,
+ permissions: {
+ read: permissionRead,
+ write: permissionWrite,
+ reshare: permissionReshare
+ }
+ };
+
+ try {
+ const item = window.app.shareDialogItem;
+ const itemType = window.app.shareDialogItemType;
+
+ // Create share
+ const shareInfo = window.fileSharing.generateSharedLink(
+ item.id,
+ itemType,
+ options
+ );
+
+ // Update UI with new share
+ const shareUrl = document.getElementById('generated-share-url');
+ shareUrl.value = shareInfo.url;
+ document.getElementById('new-share-section').style.display = 'block';
+
+ // Focus and select for easy copying
+ shareUrl.focus();
+ shareUrl.select();
+
+ // Show success message
+ window.ui.showNotification('Enlace creado', 'Enlace compartido creado correctamente');
+
+ // Reload existing shares
+ this.showShareDialog(item, itemType);
+
+ } catch (error) {
+ console.error('Error creating shared link:', error);
+ window.ui.showNotification('Error', 'No se pudo crear el enlace compartido');
+ }
+ },
+
+ /**
+ * Show email notification dialog
+ * @param {string} shareUrl - URL to share
+ */
+ showEmailNotificationDialog(shareUrl) {
+ // Update dialog content
+ document.getElementById('notification-share-url').textContent = shareUrl;
+ document.getElementById('notification-email').value = '';
+ document.getElementById('notification-message').value = '';
+
+ // Store the URL for later use
+ window.app.notificationShareUrl = shareUrl;
+
+ // Show dialog
+ document.getElementById('notification-dialog').style.display = 'flex';
+ },
+
+ /**
+ * Send share notification email
+ */
+ sendShareNotification() {
+ const email = document.getElementById('notification-email').value.trim();
+ const message = document.getElementById('notification-message').value.trim();
+ const shareUrl = window.app.notificationShareUrl;
+
+ if (!email || !shareUrl) {
+ window.ui.showNotification('Error', 'Por favor, ingresa un correo electrónico válido');
+ return;
+ }
+
+ // Validate email format
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
+ if (!emailRegex.test(email)) {
+ window.ui.showNotification('Error', 'Por favor, ingresa un correo electrónico válido');
+ return;
+ }
+
+ try {
+ window.fileSharing.sendShareNotification(shareUrl, email, message);
+ document.getElementById('notification-dialog').style.display = 'none';
+ } catch (error) {
+ console.error('Error sending notification:', error);
+ window.ui.showNotification('Error', 'No se pudo enviar la notificación');
+ }
+ },
+
+ /**
+ * Close share dialog
+ */
+ closeShareDialog() {
+ document.getElementById('share-dialog').style.display = 'none';
+ window.app.shareDialogItem = null;
+ window.app.shareDialogItemType = null;
+ },
+
+ /**
+ * Close notification dialog
+ */
+ closeNotificationDialog() {
+ document.getElementById('notification-dialog').style.display = 'none';
+ window.app.notificationShareUrl = null;
}
};
diff --git a/static/js/fileSharing.js b/static/js/fileSharing.js
new file mode 100644
index 00000000..3fd45e5b
--- /dev/null
+++ b/static/js/fileSharing.js
@@ -0,0 +1,379 @@
+/**
+ * OxiCloud - File Sharing Module
+ * This file handles file sharing functionality (shared links, permissions, etc.)
+ */
+
+// File Sharing Module
+const fileSharing = {
+ /**
+ * Generate a shared link for a file or folder
+ * @param {string} itemId - ID of the file or folder
+ * @param {string} itemType - Type ('file' or 'folder')
+ * @param {Object} options - Sharing options (password, expiration, etc.)
+ * @returns {Object} - Shared link information
+ */
+ generateSharedLink(itemId, itemType, options = {}) {
+ try {
+ // In a real implementation, this would be a call to the backend
+ // But for now, we'll simulate it with a mock response
+
+ // Default options
+ const defaultOptions = {
+ password: null,
+ expirationDate: null,
+ permissions: {
+ read: true,
+ write: false,
+ reshare: false
+ }
+ };
+
+ // Merge options
+ const finalOptions = { ...defaultOptions, ...options };
+
+ // Generate a mock link (would normally come from server)
+ const linkId = Math.random().toString(36).substring(2, 15);
+ const shareToken = Math.random().toString(36).substring(2, 20);
+ const baseUrl = window.location.origin;
+ const sharedUrl = `${baseUrl}/s/${shareToken}`;
+
+ // Create expiration date if set
+ let expiresAt = null;
+ if (finalOptions.expirationDate) {
+ expiresAt = new Date(finalOptions.expirationDate);
+ }
+
+ // Create a mock response that matches what we'd expect from the server
+ const response = {
+ id: linkId,
+ type: itemType,
+ itemId: itemId,
+ url: sharedUrl,
+ token: shareToken,
+ password_protected: !!finalOptions.password,
+ expires_at: expiresAt ? expiresAt.toISOString() : null,
+ permissions: finalOptions.permissions,
+ created_at: new Date().toISOString(),
+ created_by: {
+ id: "current-user-id", // Would be the actual user ID
+ username: "current-user" // Would be the actual username
+ },
+ access_count: 0,
+ // Add some UI friendly properties for shared.js compatibility
+ name: options.name || "Shared Item",
+ dateShared: new Date().toISOString(),
+ expiration: expiresAt ? expiresAt.toISOString() : null,
+ password: finalOptions.password
+ };
+
+ // In a real implementation, we would store this link in localStorage for now
+ // until backend implementation is ready
+ this.saveSharedLink(response);
+
+ // Return the "response" as if it came from the server
+ return response;
+ } catch (error) {
+ console.error('Error generating shared link:', error);
+ throw error;
+ }
+ },
+
+ /**
+ * Save a shared link to localStorage (temporary storage until backend is ready)
+ * @param {Object} linkData - Shared link data
+ */
+ saveSharedLink(linkData) {
+ try {
+ // Get existing shared links
+ const existingLinks = JSON.parse(localStorage.getItem('oxicloud_shared_links') || '[]');
+
+ // Add new link
+ existingLinks.push(linkData);
+
+ // Save back to localStorage
+ localStorage.setItem('oxicloud_shared_links', JSON.stringify(existingLinks));
+ } catch (error) {
+ console.error('Error saving shared link to local storage:', error);
+ }
+ },
+
+ /**
+ * Remove a shared link
+ * @param {string} linkId - ID of the shared link to remove
+ * @returns {Promise
} - Success status
+ */
+ removeSharedLink(linkId) {
+ try {
+ // Get existing shared links
+ const existingLinks = JSON.parse(localStorage.getItem('oxicloud_shared_links') || '[]');
+
+ // Filter out the link to remove
+ const updatedLinks = existingLinks.filter(link => link.id !== linkId);
+
+ // Save back to localStorage
+ localStorage.setItem('oxicloud_shared_links', JSON.stringify(updatedLinks));
+
+ // Removed network delay simulation
+
+ return true;
+ } catch (error) {
+ console.error('Error removing shared link:', error);
+ return false;
+ }
+ },
+
+ /**
+ * Update a shared link's properties
+ * @param {string} linkId - ID of the shared link to update
+ * @param {Object} updateData - Properties to update
+ * @returns {Promise