var dashboardViewType = localStorage && localStorage.getItem('viewType') if(dashboardViewType != 'icon') $('#filesDisplay').addClass('list-view') function confirmFileRename(path) { $('#renamePathInput').val(path); $('#renameNewPathInput').val(path); $('#renameModal').modal(); } function confirmFileDelete(name) { resetDeleteConfirmModal(); $('#deleteConfirmModal').data('delete-paths', [name]); $('#deleteFileName').text(name); $('#deleteFileCount').text('1'); $('#deleteFileList').empty(); $('#deleteSingleMessage').show(); $('#deleteMultipleMessage').hide(); $('#deleteConfirmModal').modal(); } function setDeletePending(pending) { var modal = $('#deleteConfirmModal'); var deleteButton = $('#deleteConfirmButton'); var cancelButton = $('#deleteCancelButton'); var closeButton = $('#deleteConfirmCloseButton'); var paths = modal.data('delete-paths') || []; var deleteLabel = paths.length === 1 ? 'Deleting file...' : 'Deleting ' + paths.length + ' items...'; modal.data('delete-pending', pending); deleteButton.prop('disabled', pending); cancelButton.prop('disabled', pending); closeButton.prop('disabled', pending); if (pending) { deleteButton.html('Deleting...'); $('#deleteProgressText').text(deleteLabel); $('#deleteProgressMessage').show(); } else { deleteButton.html('Delete'); $('#deleteProgressMessage').hide(); } } function resetDeleteConfirmModal() { setDeletePending(false); } function fileDelete() { var modal = $('#deleteConfirmModal'); var paths = modal.data('delete-paths') || [$('#deleteFileName').text()]; if (modal.data('delete-pending')) { return; } setDeletePending(true); $.ajax({ url: '/api/delete', type: 'POST', data: { csrf_token: $('#deleteCSRFToken').val(), filenames: paths }, success: function() { setDeletePending(false); modal.modal('hide'); setBulkSelectMode(false); alertClear(); alertType('success'); if (paths.length === 1) { alertAdd($('
').text(paths[0]).html() + ' has been deleted.'); } else { alertAdd(paths.length + ' items have been deleted.'); } reloadDashboardFiles(); }, error: function(xhr) { var message = 'Failed to delete file(s).'; try { message = JSON.parse(xhr.responseText).message || message; } catch(e) { } alertClear(); alertType('error'); alertAdd($('
').text(message).html()); setDeletePending(false); modal.modal('hide'); }, complete: function() { setDeletePending(false); } }); } function selectedFilePaths() { return $('.bulk-select-checkbox:checked').map(function() { return $(this).val(); }).get(); } function updateBulkActions() { var checkboxes = $('.bulk-select-checkbox'); var checked = $('.bulk-select-checkbox:checked'); var count = checked.length; var selectAll = $('#bulkSelectAll').get(0); $('#selectedFileCount').text(count); $('#bulkDeleteButton').prop('disabled', count === 0); checkboxes.each(function() { $(this).closest('.file').toggleClass('bulk-selected', $(this).prop('checked')); }); if (selectAll) { selectAll.checked = count > 0 && count === checkboxes.length; selectAll.indeterminate = count > 0 && count < checkboxes.length; } } function setBulkSelectMode(enabled) { $('#filesDisplay').toggleClass('bulk-selecting', enabled); if (!enabled) { $('.bulk-select-checkbox').prop('checked', false); } updateBulkActions(); } function toggleBulkSelect(event) { if (event) { event.preventDefault(); } setBulkSelectMode(!$('#filesDisplay').hasClass('bulk-selecting')); } function clearFileSelection() { $('.bulk-select-checkbox').prop('checked', false); updateBulkActions(); } function toggleSelectAllFiles(checked) { $('.bulk-select-checkbox').prop('checked', checked); updateBulkActions(); } function confirmBulkDelete() { var paths = selectedFilePaths(); if (paths.length === 0) { return; } resetDeleteConfirmModal(); $('#deleteConfirmModal').data('delete-paths', paths); $('#deleteFileName').text(paths[0]); $('#deleteFileCount').text(paths.length); $('#deleteFileList').empty(); paths.forEach(function(path) { $('
  • ').text(path).appendTo('#deleteFileList'); }); $('#deleteSingleMessage').toggle(paths.length === 1); $('#deleteMultipleMessage').toggle(paths.length > 1); $('#deleteConfirmModal').modal(); } $('#deleteConfirmModal').on('hide', function() { return !$(this).data('delete-pending'); }); function initBulkFileSelection() { $('.bulk-select-checkbox').off('change.bulk').on('change.bulk', updateBulkActions); $('.file').off('click.bulk').on('click.bulk', function(event) { if (!$('#filesDisplay').hasClass('bulk-selecting')) { return; } if ($(event.target).closest('a, button, label').length > 0) { return; } var checkbox = $(this).find('.bulk-select-checkbox'); if (checkbox.length === 0) { return; } checkbox.prop('checked', !checkbox.prop('checked')).trigger('change'); event.preventDefault(); }); updateBulkActions(); } function clickUploadFiles() { $("input[id='uploadFiles']").click() } function showUploadProgress() { $('#uploadingOverlay').css('display', 'block') } function hideUploadProgress() { $('#progressBar').css('display', 'none') $('#uploadingOverlay').css('display', 'none') } $('#createDir').on('shown', function () { $('#newDirInput').focus(); }) $('#createFile').on('shown', function () { $('#newFileInput').focus(); $('#newFileInput').val(''); clearCreateFileError(); }) function showCreateFileError(message) { var errorDiv = $('#createFileError'); errorDiv.text(message); errorDiv.show(); } function clearCreateFileError() { var errorDiv = $('#createFileError'); errorDiv.hide(); errorDiv.text(''); } function fileExists(filePath) { // Check if a file with this path already exists in the current file listing // Each file has a hidden div containing the file path var exists = false; var lowerFilePath = filePath.toLowerCase(); $('#filesDisplay .file .overlay div[style*="display: none"]').each(function() { var existingPath = $(this).text().trim(); if (existingPath.toLowerCase() === lowerFilePath) { exists = true; return false; // Break out of each loop } }); return exists; } $('#newFileInput').on('keypress', function(e) { if (e.which === 13) { // Enter key handleCreateFile(); } }) function handleCreateFile() { if (createFileSubmitting) { return; } setCreateFileSubmitting(true); var filename = $('#newFileInput').val(); var dir = $('#createFileDir').val(); var csrfToken = $('#createFileCSRFToken').val(); // Don't hide modal yet - wait for success or error createFileViaAPI(filename, dir, csrfToken); } function listView() { if(localStorage) localStorage.setItem('viewType', 'list') $('#filesDisplay').addClass('list-view') } function iconView() { if(localStorage) localStorage.setItem('viewType', 'icon') $('#filesDisplay').removeClass('list-view') } function alertAdd(text) { var a = $('#alertDialogue'); a.css('display', 'block'); a.append(text+'
    '); } function alertClear(){ var a = $('#alertDialogue'); a.css('display', 'none'); a.text(''); } function alertType(type){ var a = $('#alertDialogue'); a.removeClass('alert-success'); a.removeClass('alert-error'); a.addClass('alert-'+type); } var processedFiles = 0; var uploadedFiles = 0; var uploadedFileErrors = 0; var createFileSubmitting = false; function setCreateFileSubmitting(submitting) { createFileSubmitting = submitting; $('#createFileSubmitButton').prop('disabled', submitting); } function joinPaths(...paths) { return paths .map(path => path.replace(/(^\/|\/$)/g, '')) .filter(path => path !== '') .join('/'); } function reInitDashboardFiles() { new Dropzone("#uploads", { url: "/api/upload", paramName: 'file', dictDefaultMessage: "", uploadMultiple: false, parallelUploads: 1, maxFilesize: 50000000, // 100MB clickable: document.getElementById('uploadButton'), init: function() { this.on("processing", function(file) { var dir = $('#uploads input[name="dir"]').val(); if(file.fullPath) { this.options.paramName = joinPaths(dir,file.fullPath); } else { this.options.paramName = joinPaths(dir, file.name); } processedFiles++; $('#uploadFileName').text(this.options.paramName).prepend(' '); }); this.on("success", function(file) { uploadedFiles++; }); this.on("error", function(file, message) { uploadedFiles++; uploadedFileErrors++; alertType('error'); if (message && message.message) { alertAdd(message.message); } else { alertAdd(this.options.paramName+' failed to upload'); } }); this.on("queuecomplete", function() { hideUploadProgress(); if(uploadedFileErrors > 0) { alertType('error'); alertAdd(uploadedFiles-uploadedFileErrors+'/'+uploadedFiles+' files uploaded successfully'); } else { alertType('success'); alertAdd(uploadedFiles+' files uploaded successfully'); } reloadDashboardFiles(); }); this.on("addedfiles", function(files) { uploadedFiles = 0; uploadedFileErrors = 0; alertClear(); showUploadProgress(); }); } }); document.getElementById('uploadButton').addEventListener('click', function(event) { event.preventDefault(); }); initBulkFileSelection(); } function reloadDashboardFiles() { var dir = $('#uploads input[name="dir"]').val(); var bulkSelecting = $('#filesDisplay').hasClass('bulk-selecting'); $.get('/dashboard/files?dir='+encodeURIComponent(dir), function(data) { $('#filesDisplay').html(data); $('#filesDisplay').toggleClass('bulk-selecting', bulkSelecting); reInitDashboardFiles(); }); } function createFileViaAPI(filename, dir, csrfToken) { clearCreateFileError(); showUploadProgress(); filename = filename.replace(/[^a-zA-Z0-9_\-.]/g, ''); if (!filename || filename.trim() === '') { hideUploadProgress(); setCreateFileSubmitting(false); showCreateFileError('You must provide a file name.'); return; } var extMatch = filename.match(/\.([^.]+)$/); // Check if extension is allowed for editing (if there is one) if (extMatch) { var extension = extMatch[1].toLowerCase(); var validExtensions = [ 'html', 'htm', 'txt', 'js', 'css', 'scss', 'md', 'manifest', 'less', 'webmanifest', 'xml', 'json', 'opml', 'rdf', 'svg', 'gpg', 'pgp', 'resolvehandle', 'pls', 'yaml', 'yml', 'toml', 'osdx', 'mjs', 'cjs', 'ts', 'py', 'rss', 'glsl' ]; if (validExtensions.indexOf(extension) === -1) { hideUploadProgress(); setCreateFileSubmitting(false); showCreateFileError('Must be an editable file type (' + validExtensions.join(', ') + ') or a file with no extension.'); return; } } // Files with no extension are allowed (they're treated as text files) var fullPath = dir ? joinPaths(dir, filename) : filename; // Check if file already exists if (fileExists(fullPath)) { hideUploadProgress(); setCreateFileSubmitting(false); showCreateFileError('A file with this name already exists. Please choose a different name.'); return; } var isHTML = /\.(html|htm)$/i.test(filename); // Create default HTML template content var htmlTemplate = '\n' + '\n' + ' \n' + ' \n' + ' \n' + ' My Page\n' + ' \n' + ' \n' + ' \n' + ' \n' + ''; var content = isHTML ? htmlTemplate : ''; // Create a blob with the content var blob = new Blob([content], { type: 'text/plain' }); // Create FormData for the API upload var formData = new FormData(); formData.append('csrf_token', csrfToken); formData.append(fullPath, blob, filename); $.ajax({ url: '/api/upload', type: 'POST', data: formData, processData: false, contentType: false, success: function(response) { hideUploadProgress(); setCreateFileSubmitting(false); alertClear(); alertType('success'); var escapedName = $('
    ').text(fullPath).html(); // HTML escape alertAdd(escapedName + ' was created! Click here to edit it.'); reloadDashboardFiles(); $('#createFile').modal('hide'); // Hide modal on success }, error: function(xhr) { hideUploadProgress(); setCreateFileSubmitting(false); try { var errorData = JSON.parse(xhr.responseText); showCreateFileError(errorData.message || 'Failed to create file'); } catch(e) { showCreateFileError('Failed to create file'); } } }); } // for first time load reInitDashboardFiles();