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})
+}
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))
<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>
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>