added feature to reorder the files
authorPhiTux <redacted>
Wed, 10 Sep 2025 13:44:27 +0000 (15:44 +0200)
committerPhiTux <redacted>
Wed, 10 Sep 2025 13:44:27 +0000 (15:44 +0200)
backend/handlers/logs.go
backend/main.go
frontend/src/lib/FileList.svelte
frontend/src/routes/(authed)/write/+page.svelte

index b6e72db7279e8dbe16e69a16cacd96e7dbe032e2..faf59af890e0d0174499f777e1f63fada0207587 100644 (file)
@@ -1135,3 +1135,134 @@ func RenameFile(w http.ResponseWriter, r *http.Request) {
        utils.Logger.Printf("File renamed successfully for user %d: %s -> %s", userID, req.UUID, req.NewFilename)
        utils.JSONResponse(w, http.StatusOK, map[string]bool{"success": true})
 }
+
+// ReorderFilesRequest represents the reorder files request body
+type ReorderFilesRequest struct {
+       Day       int            `json:"day"`
+       Month     int            `json:"month"`
+       Year      int            `json:"year"`
+       FileOrder map[string]int `json:"file_order"` // UUID -> order index
+}
+
+// ReorderFiles handles reordering files within a day
+func ReorderFiles(w http.ResponseWriter, r *http.Request) {
+       // Get user ID from context
+       userID, ok := r.Context().Value(utils.UserIDKey).(int)
+       if !ok {
+               http.Error(w, "Unauthorized", http.StatusUnauthorized)
+               return
+       }
+
+       // Parse request body
+       var req ReorderFilesRequest
+       if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+               http.Error(w, "Invalid request body", http.StatusBadRequest)
+               return
+       }
+
+       if len(req.FileOrder) == 0 {
+               utils.JSONResponse(w, http.StatusBadRequest, map[string]any{
+                       "success": false,
+                       "message": "File order mapping is required",
+               })
+               return
+       }
+
+       // Get month data
+       content, err := utils.GetMonth(userID, req.Year, req.Month)
+       if err != nil {
+               http.Error(w, fmt.Sprintf("Error retrieving month data: %v", err), http.StatusInternalServerError)
+               return
+       }
+
+       // Find and reorder files for the specific day
+       days, ok := content["days"].([]any)
+       if !ok {
+               utils.JSONResponse(w, http.StatusNotFound, map[string]any{
+                       "success": false,
+                       "message": "No days found",
+               })
+               return
+       }
+
+       found := false
+       for _, d := range days {
+               day, ok := d.(map[string]any)
+               if !ok {
+                       continue
+               }
+
+               dayNum, ok := day["day"].(float64)
+               if !ok || int(dayNum) != req.Day {
+                       continue
+               }
+
+               files, ok := day["files"].([]any)
+               if !ok {
+                       continue
+               }
+
+               // Create a slice to hold files with their new order
+               type fileWithOrder struct {
+                       file  map[string]any
+                       order int
+               }
+
+               var orderedFiles []fileWithOrder
+
+               // Assign order to each file
+               for _, f := range files {
+                       file, ok := f.(map[string]any)
+                       if !ok {
+                               continue
+                       }
+
+                       uuid, ok := file["uuid_filename"].(string)
+                       if !ok {
+                               continue
+                       }
+
+                       if order, exists := req.FileOrder[uuid]; exists {
+                               orderedFiles = append(orderedFiles, fileWithOrder{file: file, order: order})
+                       } else {
+                               // Files not in the reorder map get appended at the end
+                               orderedFiles = append(orderedFiles, fileWithOrder{file: file, order: len(req.FileOrder)})
+                       }
+               }
+
+               // Sort files by their order
+               for i := 0; i < len(orderedFiles)-1; i++ {
+                       for j := i + 1; j < len(orderedFiles); j++ {
+                               if orderedFiles[i].order > orderedFiles[j].order {
+                                       orderedFiles[i], orderedFiles[j] = orderedFiles[j], orderedFiles[i]
+                               }
+                       }
+               }
+
+               // Update the files array with the new order
+               newFiles := make([]any, len(orderedFiles))
+               for i, fileWithOrder := range orderedFiles {
+                       newFiles[i] = fileWithOrder.file
+               }
+               day["files"] = newFiles
+
+               found = true
+               break
+       }
+
+       if !found {
+               utils.JSONResponse(w, http.StatusNotFound, map[string]any{
+                       "success": false,
+                       "message": "Day not found",
+               })
+               return
+       }
+
+       // Save the updated month data
+       if err := utils.WriteMonth(userID, req.Year, req.Month, content); err != nil {
+               http.Error(w, fmt.Sprintf("Error writing month data: %v", err), http.StatusInternalServerError)
+               return
+       }
+
+       utils.JSONResponse(w, http.StatusOK, map[string]bool{"success": true})
+}
index caabbef1bcb374eccb249cc5b05ba44e3a01b5a8..5ffeabab331feb2871fe3310e3ea7153da2909b4 100644 (file)
@@ -87,6 +87,7 @@ func main() {
        mux.HandleFunc("GET /logs/downloadFile", middleware.RequireAuth(handlers.DownloadFile))
        mux.HandleFunc("GET /logs/deleteFile", middleware.RequireAuth(handlers.DeleteFile))
        mux.HandleFunc("POST /logs/renameFile", middleware.RequireAuth(handlers.RenameFile))
+       mux.HandleFunc("POST /logs/reorderFiles", middleware.RequireAuth(handlers.ReorderFiles))
        mux.HandleFunc("GET /logs/getHistory", middleware.RequireAuth(handlers.GetHistory))
        mux.HandleFunc("GET /logs/bookmarkDay", middleware.RequireAuth(handlers.BookmarkDay))
        mux.HandleFunc("GET /logs/deleteDay", middleware.RequireAuth(handlers.DeleteDay))
index da709c7c0e0d8b59083c1e8f7a612687a5b4c868..116fafc20208f0a578870d9be5eb9f04c3d0ada0 100644 (file)
 <script>
        import { Fa } from 'svelte-fa';
-       import { faTrash, faWrench, faEdit, faSave, faTimes } from '@fortawesome/free-solid-svg-icons';
+       import {
+               faTrash,
+               faWrench,
+               faEdit,
+               faSave,
+               faTimes,
+               faGripVertical
+       } from '@fortawesome/free-solid-svg-icons';
        import { slide } from 'svelte/transition';
        import { formatBytes } from './helpers.js';
        import { getTranslate } from '@tolgee/svelte';
 
        const { t } = getTranslate();
 
-       let { files, downloadFile, askDeleteFile, deleteAllowed, renameFile } = $props();
+       let { files, downloadFile, askDeleteFile, deleteAllowed, renameFile, reorderFiles } = $props();
 
        let openOptionsMenu = $state(null); // UUID of file with open options menu
        let editingFilename = $state(null); // UUID of file being renamed
        let newFilename = $state('');
+
+       let draggedIndex = $state(null);
+       let dragOverIndex = $state(null);
+
+       function handleDragStart(event, index) {
+               draggedIndex = index;
+               event.dataTransfer.effectAllowed = 'move';
+               event.dataTransfer.setData('text/html', event.target.outerHTML);
+               event.target.style.opacity = '0.5';
+       }
+
+       function handleDragEnd(event) {
+               event.target.style.opacity = '';
+               draggedIndex = null;
+               dragOverIndex = null;
+       }
+
+       function handleDragOver(event, index) {
+               event.preventDefault();
+               event.dataTransfer.dropEffect = 'move';
+
+               // Only set dragOverIndex if we're actually dragging something
+               if (draggedIndex !== null) {
+                       dragOverIndex = index;
+               }
+       }
+
+       function handleDragLeave(event) {
+               // Only clear dragOverIndex if we're leaving the entire drop zone
+               if (!event.currentTarget.contains(event.relatedTarget)) {
+                       dragOverIndex = null;
+               }
+       }
+
+       function handleDrop(event, dropIndex) {
+               event.preventDefault();
+
+               if (draggedIndex !== null && draggedIndex !== dropIndex) {
+                       // Create new array with reordered items using a different approach
+                       const newFiles = [...files];
+
+                       // Use array movement: move element from draggedIndex to dropIndex
+                       const draggedFile = newFiles[draggedIndex];
+
+                       // Remove the dragged element
+                       newFiles.splice(draggedIndex, 1);
+
+                       // Insert at the correct position
+                       newFiles.splice(dropIndex, 0, draggedFile);
+
+                       // Call reorder function if provided
+                       if (reorderFiles) {
+                               reorderFiles(newFiles);
+                       }
+               }
+
+               draggedIndex = null;
+               dragOverIndex = null;
+       }
 </script>
 
-{#each files as file (file.uuid_filename)}
-       <div class="btn-group file mt-2" transition:slide>
+{#each files as file, index (file.uuid_filename)}
+       <!-- svelte-ignore a11y_no_static_element_interactions -->
+       <div
+               class="btn-group file mt-2 {dragOverIndex === index ? 'drag-over' : ''}"
+               transition:slide
+               draggable="false"
+               ondragover={(e) => handleDragOver(e, index)}
+               ondragleave={handleDragLeave}
+               ondrop={(e) => handleDrop(e, index)}
+       >
+               <!-- svelte-ignore a11y_no_static_element_interactions -->
+               <div
+                       class="drag-handle d-flex align-items-center px-2"
+                       draggable="true"
+                       ondragstart={(e) => {
+                               e.stopPropagation();
+                               handleDragStart(e, index);
+                       }}
+                       ondragend={(e) => {
+                               e.stopPropagation();
+                               handleDragEnd(e);
+                       }}
+               >
+                       <Fa icon={faGripVertical} class="text-muted" />
+               </div>
                <button
                        onclick={() => downloadFile(file.uuid_filename)}
                        class="p-2 fileBtn d-flex flex-column flex-fill"
                border-radius: 5px;
                backdrop-filter: blur(5px);
        }
+
+       .drag-handle {
+               cursor: grab;
+               background-color: rgba(0, 0, 0, 0.05);
+               border-right: 1px solid rgba(92, 92, 92, 0.445);
+               transition: all ease 0.3s;
+       }
+
+       .drag-handle:hover {
+               background-color: rgba(0, 0, 0, 0.1);
+       }
+
+       .drag-handle:active {
+               cursor: grabbing;
+       }
+
+       .file.drag-over {
+               border: 2px dashed #007bff;
+               background-color: rgba(0, 123, 255, 0.1);
+       }
 </style>
index a0670e3db7ea939471538920cee89d265c9467d6..c352e1b4c41a54d71aeab6212d53151a1fcff0ba 100644 (file)
                                toast.show();
                        });
        }
+
+       function reorderFiles(newFileOrder) {
+               // Create mapping of UUID to order
+               const fileOrderMap = {};
+               newFileOrder.forEach((file, index) => {
+                       fileOrderMap[file.uuid_filename] = index;
+               });
+
+               // Send to backend
+               axios
+                       .post(API_URL + '/logs/reorderFiles', {
+                               day: $selectedDate.day,
+                               month: $selectedDate.month,
+                               year: $selectedDate.year,
+                               file_order: fileOrderMap
+                       })
+                       .then((response) => {
+                               if (response.data.success) {
+                                       // Update local state
+                                       filesOfDay = newFileOrder;
+
+                                       // Update images array - preserve existing properties like src, loading, etc.
+                                       const newImagesOrder = [];
+                                       newFileOrder.forEach((file) => {
+                                               const existingImage = images.find((img) => img.uuid_filename === file.uuid_filename);
+                                               if (existingImage) {
+                                                       // Preserve existing image properties (src, loading, etc.) and update with new file data
+                                                       newImagesOrder.push({
+                                                               ...existingImage, // Keep existing image properties
+                                                               ...file // Update with new file data (filename, etc.)
+                                                       });
+                                               }
+                                       });
+                                       images = newImagesOrder;
+                               } else {
+                                       const toast = new bootstrap.Toast(document.getElementById('toastErrorReorderingFiles'));
+                                       toast.show();
+                               }
+                       })
+                       .catch((error) => {
+                               console.error(error);
+                               const toast = new bootstrap.Toast(document.getElementById('toastErrorReorderingFiles'));
+                               toast.show();
+                       });
+       }
 </script>
 
 <DatepickerLogic />
                                >
                                <input type="file" id="fileInput" multiple style="display: none;" onchange={onFileChange} />
 
-                               <FileList files={filesOfDay} {downloadFile} {askDeleteFile} {renameFile} deleteAllowed />
+                               <FileList
+                                       files={filesOfDay}
+                                       {downloadFile}
+                                       {askDeleteFile}
+                                       {renameFile}
+                                       {reorderFiles}
+                                       deleteAllowed
+                               />
                                {#each uploadingFiles as file}
                                        <div>
                                                {file.name}
                                <div class="toast-body">
                                        {$t('tags.toast.error_removing')}
                                </div>
+                               <button
+                                       type="button"
+                                       class="btn-close me-2 m-auto"
+                                       data-bs-dismiss="toast"
+                                       aria-label="Close"
+                               ></button>
                        </div>
                </div>
 
                                <div class="toast-body">
                                        {$t('tags.toast.error_adding')}
                                </div>
+                               <button
+                                       type="button"
+                                       class="btn-close me-2 m-auto"
+                                       data-bs-dismiss="toast"
+                                       aria-label="Close"
+                               ></button>
                        </div>
                </div>
 
                                <div class="toast-body">
                                        {$t('tags.toast.error_saving')}
                                </div>
+                               <button
+                                       type="button"
+                                       class="btn-close me-2 m-auto"
+                                       data-bs-dismiss="toast"
+                                       aria-label="Close"
+                               ></button>
                        </div>
                </div>
 
                                <div class="toast-body">
                                        {$t('log.toast.error_saving')}
                                </div>
+                               <button
+                                       type="button"
+                                       class="btn-close me-2 m-auto"
+                                       data-bs-dismiss="toast"
+                                       aria-label="Close"
+                               ></button>
                        </div>
                </div>
 
                                <div class="toast-body">
                                        {$t('log.toast.error_loading')}
                                </div>
+                               <button
+                                       type="button"
+                                       class="btn-close me-2 m-auto"
+                                       data-bs-dismiss="toast"
+                                       aria-label="Close"
+                               ></button>
                        </div>
                </div>
 
                                <div class="toast-body">
                                        {$t('files.toast.error_saving')}
                                </div>
+                               <button
+                                       type="button"
+                                       class="btn-close me-2 m-auto"
+                                       data-bs-dismiss="toast"
+                                       aria-label="Close"
+                               ></button>
                        </div>
                </div>
 
                                <div class="toast-body">
                                        {$t('files.toast.error_deleting')}
                                </div>
+                               <button
+                                       type="button"
+                                       class="btn-close me-2 m-auto"
+                                       data-bs-dismiss="toast"
+                                       aria-label="Close"
+                               ></button>
                        </div>
                </div>
 
                                <div class="toast-body">
                                        {$t('files.toast.error_loading')}
                                </div>
+                               <button
+                                       type="button"
+                                       class="btn-close me-2 m-auto"
+                                       data-bs-dismiss="toast"
+                                       aria-label="Close"
+                               ></button>
                        </div>
                </div>
 
                                <div class="toast-body">
                                        {$t('log.toast.error_deleting_day')}
                                </div>
+                               <button
+                                       type="button"
+                                       class="btn-close me-2 m-auto"
+                                       data-bs-dismiss="toast"
+                                       aria-label="Close"
+                               ></button>
                        </div>
                </div>
 
                >
                        <div class="d-flex">
                                <div class="toast-body">{$t('log.toast.error_renaming_file')}</div>
+                               <button
+                                       type="button"
+                                       class="btn-close me-2 m-auto"
+                                       data-bs-dismiss="toast"
+                                       aria-label="Close"
+                               ></button>
+                       </div>
+               </div>
+
+               <div
+                       id="toastErrorReorderingFiles"
+                       class="toast align-items-center text-bg-danger"
+                       role="alert"
+                       aria-live="assertive"
+                       aria-atomic="true"
+               >
+                       <div class="d-flex">
+                               <div class="toast-body">{$t('log.toast.error_reordering_files')}</div>
+                               <button
+                                       type="button"
+                                       class="btn-close me-2 m-auto"
+                                       data-bs-dismiss="toast"
+                                       aria-label="Close"
+                               ></button>
                        </div>
                </div>
        </div>
git clone https://git.99rst.org/PROJECT