utils.JSONResponse(w, http.StatusOK, map[string]bool{"success": true})
}
+
+// RenameFileRequest represents the rename file request body
+type RenameFileRequest struct {
+ UUID string `json:"uuid"`
+ NewFilename string `json:"new_filename"`
+ Day int `json:"day"`
+ Month int `json:"month"`
+ Year int `json:"year"`
+}
+
+// RenameFile handles renaming a file
+func RenameFile(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
+ }
+ derivedKey, ok := r.Context().Value(utils.DerivedKeyKey).(string)
+ if !ok {
+ http.Error(w, "Unauthorized", http.StatusUnauthorized)
+ return
+ }
+
+ // Parse request body
+ var req RenameFileRequest
+ if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+ http.Error(w, "Invalid request body", http.StatusBadRequest)
+ return
+ }
+
+ // Validate input
+ req.NewFilename = strings.TrimSpace(req.NewFilename)
+ if req.NewFilename == "" {
+ utils.JSONResponse(w, http.StatusBadRequest, map[string]any{
+ "success": false,
+ "message": "New filename cannot be empty",
+ })
+ return
+ }
+
+ if req.UUID == "" {
+ utils.JSONResponse(w, http.StatusBadRequest, map[string]any{
+ "success": false,
+ "message": "File UUID 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
+ }
+
+ encKey, err := utils.GetEncryptionKey(userID, derivedKey)
+ if err != nil {
+ http.Error(w, fmt.Sprintf("Error getting encryption key: %v", err), http.StatusInternalServerError)
+ return
+ }
+
+ enc_filename, err := utils.EncryptText(req.NewFilename, encKey)
+ if err != nil {
+ http.Error(w, fmt.Sprintf("Error encrypting text: %v", err), http.StatusInternalServerError)
+ return
+ }
+
+ // Find and update the file
+ 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
+ }
+
+ // Find and rename the specific file
+ for _, f := range files {
+ file, ok := f.(map[string]any)
+ if !ok {
+ continue
+ }
+
+ if uuid, ok := file["uuid_filename"].(string); ok && uuid == req.UUID {
+ file["enc_filename"] = enc_filename
+ found = true
+ break
+ }
+ }
+
+ if found {
+ break
+ }
+ }
+
+ if !found {
+ utils.JSONResponse(w, http.StatusNotFound, map[string]any{
+ "success": false,
+ "message": "File 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.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})
+}
mux.HandleFunc("POST /logs/uploadFile", middleware.RequireAuth(handlers.UploadFile))
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("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 } from '@fortawesome/free-solid-svg-icons';
+ import { faTrash, faWrench, faEdit, faSave, faTimes } 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 } = $props();
+ let { files, downloadFile, askDeleteFile, deleteAllowed, renameFile } = $props();
+
+ let openOptionsMenu = $state(null); // UUID of file with open options menu
+ let editingFilename = $state(null); // UUID of file being renamed
+ let newFilename = $state('');
</script>
{#each files as file (file.uuid_filename)}
</button>
{#if deleteAllowed}
<button
- class="p-2 fileBtn deleteFileBtn"
- onclick={() => askDeleteFile(file.uuid_filename, file.filename)}
- ><Fa icon={faTrash} fw /></button
+ class="p-2 fileBtn optionsBtn"
+ onclick={() => {
+ if (openOptionsMenu === file.uuid_filename) {
+ openOptionsMenu = null;
+ editingFilename = null;
+ } else {
+ openOptionsMenu = file.uuid_filename;
+ editingFilename = null;
+ }
+ }}
>
+ <Fa icon={faWrench} fw />
+ </button>
{/if}
</div>
+
+ {#if deleteAllowed && openOptionsMenu === file.uuid_filename}
+ <div transition:slide>
+ <div class="options-menu p-3 mt-1">
+ <div class="mb-3">
+ <!-- svelte-ignore a11y_label_has_associated_control -->
+ <label class="form-label small fw-bold">{$t('fileList.change_filename')}:</label>
+ <div class="d-flex gap-2">
+ {#if editingFilename === file.uuid_filename}
+ <input
+ type="text"
+ class="form-control form-control-sm"
+ id="newFilename-{file.uuid_filename}"
+ bind:value={newFilename}
+ onkeydown={(e) => {
+ if (e.key === 'Enter') {
+ if (renameFile) {
+ renameFile(file.uuid_filename, newFilename);
+ }
+ editingFilename = null;
+ openOptionsMenu = null;
+ } else if (e.key === 'Escape') {
+ editingFilename = null;
+ }
+ }}
+ />
+ <button
+ class="btn btn-sm btn-success"
+ onclick={() => {
+ if (renameFile) {
+ renameFile(file.uuid_filename, newFilename);
+ }
+ editingFilename = null;
+ openOptionsMenu = null;
+ }}
+ >
+ <Fa icon={faSave} fw />
+ </button>
+ <button
+ class="btn btn-sm btn-secondary"
+ onclick={() => {
+ editingFilename = null;
+ }}
+ >
+ <Fa icon={faTimes} fw />
+ </button>
+ {:else}
+ <input
+ type="text"
+ class="form-control form-control-sm"
+ value={file.filename}
+ disabled
+ />
+ <button
+ class="btn btn-sm btn-primary"
+ onclick={() => {
+ editingFilename = file.uuid_filename;
+ newFilename = file.filename;
+ }}
+ >
+ <Fa icon={faEdit} fw />
+ </button>
+ {/if}
+ </div>
+ </div>
+
+ <hr style="color: black;" />
+
+ <div>
+ <button
+ class="btn btn-sm btn-danger w-100"
+ onclick={() => {
+ askDeleteFile(file.uuid_filename, file.filename);
+ openOptionsMenu = null;
+ }}
+ >
+ <Fa icon={faTrash} fw class="me-2" />
+ {$t('fileList.delete_file')}
+ </button>
+ </div>
+ </div>
+ </div>
+ {/if}
{/each}
<style>
background-color: rgba(0, 0, 0, 0.1);
}
- .deleteFileBtn {
+ .optionsBtn {
border-left: 1px solid rgba(92, 92, 92, 0.445);
}
- .deleteFileBtn:hover {
- color: rgb(165, 0, 0);
+ .optionsBtn:hover {
+ color: rgb(0, 123, 255);
}
.file {
border: 0px solid #ececec77;
border-radius: 5px;
}
+
+ .options-menu {
+ background-color: rgba(248, 249, 250, 0.95);
+ border: 1px solid rgba(0, 0, 0, 0.125);
+ border-radius: 5px;
+ backdrop-filter: blur(5px);
+ }
</style>
{/if}
<div class="d-flex flex-column tagColumn mt-1">
{#each $tags as tag}
- <Tag
- {tag}
- isEditable
- editTag={openTagModal}
- isDeletable
- deleteTag={askDeleteTag}
- />
- {#if deleteTagId === tag.id}
- <div
- class="alert alert-danger align-items-center"
- role="alert"
- transition:slide
- >
- <div>
- <Fa icon={faTriangleExclamation} fw />
- {@html $t('settings.tags.delete_confirmation')}
- </div>
- <!-- svelte-ignore a11y_consider_explicit_label -->
- <div class="d-flex flex-row mt-2">
- <button class="btn btn-secondary" onclick={() => (deleteTagId = null)}
- >{$t('settings.abort')}
- </button>
- <button
- disabled={isDeletingTag}
- class="btn btn-danger ms-3"
- onclick={() => deleteTag(tag.id)}
- >{$t('settings.delete')}
- {#if isDeletingTag}
- <span
- class="spinner-border spinner-border-sm ms-2"
- role="status"
- aria-hidden="true"
- ></span>
- {/if}
- </button>
+ <div class="mt-2">
+ <Tag
+ {tag}
+ isEditable
+ editTag={openTagModal}
+ isDeletable
+ deleteTag={askDeleteTag}
+ />
+ {#if deleteTagId === tag.id}
+ <div transition:slide style="padding-top: 0.5rem">
+ <div
+ class="alert alert-danger align-items-center tagAlert"
+ role="alert"
+ >
+ <div>
+ <Fa icon={faTriangleExclamation} fw />
+ {@html $t('settings.tags.delete_confirmation')}
+ </div>
+ <!-- svelte-ignore a11y_consider_explicit_label -->
+ <div class="d-flex flex-row mt-2">
+ <button
+ class="btn btn-secondary"
+ onclick={() => (deleteTagId = null)}
+ >{$t('settings.abort')}
+ </button>
+ <button
+ disabled={isDeletingTag}
+ class="btn btn-danger ms-3"
+ onclick={() => deleteTag(tag.id)}
+ >{$t('settings.delete')}
+ {#if isDeletingTag}
+ <span
+ class="spinner-border spinner-border-sm ms-2"
+ role="status"
+ aria-hidden="true"
+ ></span>
+ {/if}
+ </button>
+ </div>
+ </div>
</div>
- </div>
- {/if}
+ {/if}
+ </div>
{/each}
</div>
</div>
</div>
<style>
+ .tagAlert {
+ margin-bottom: 0 !important;
+ }
+
.modal-header > div > div > button {
border: none;
border-radius: 10px !important;
width: min-content;
}
- .tagColumn {
- gap: 0.5rem;
- }
-
#selectMode:checked {
border-color: #da880e;
background-color: #da880e;
toast.show();
});
}
+
+ function renameFile(uuid_filename, new_filename) {
+ // Validate filename
+ if (!new_filename || new_filename.trim() === '') {
+ const toast = new bootstrap.Toast(document.getElementById('toastErrorRenamingFile'));
+ toast.show();
+ return;
+ }
+
+ new_filename = new_filename.trim();
+
+ axios
+ .post(API_URL + '/logs/renameFile', {
+ uuid: uuid_filename,
+ new_filename: new_filename,
+ day: $selectedDate.day,
+ month: $selectedDate.month,
+ year: $selectedDate.year
+ })
+ .then((response) => {
+ if (response.data.success) {
+ // Update local file list
+ filesOfDay = filesOfDay.map((file) => {
+ if (file.uuid_filename === uuid_filename) {
+ file.filename = new_filename;
+ }
+ return file;
+ });
+
+ // Update images list as well
+ images = images.map((image) => {
+ if (image.uuid_filename === uuid_filename) {
+ image.filename = new_filename;
+ }
+ return image;
+ });
+ } else {
+ const toast = new bootstrap.Toast(document.getElementById('toastErrorRenamingFile'));
+ toast.show();
+ }
+ })
+ .catch((error) => {
+ console.error(error);
+ const toast = new bootstrap.Toast(document.getElementById('toastErrorRenamingFile'));
+ toast.show();
+ });
+ }
</script>
<DatepickerLogic />
>
<input type="file" id="fileInput" multiple style="display: none;" onchange={onFileChange} />
- <FileList files={filesOfDay} {downloadFile} {askDeleteFile} deleteAllowed />
+ <FileList files={filesOfDay} {downloadFile} {askDeleteFile} {renameFile} deleteAllowed />
{#each uploadingFiles as file}
<div>
{file.name}
</div>
</div>
</div>
+
+ <div
+ id="toastErrorRenamingFile"
+ 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_renaming_file')}</div>
+ </div>
+ </div>
</div>
<div class="modal fade" id="modalConfirmDeleteFile" tabindex="-1">