mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-15 19:10:47 +02:00
Prettier 2: Electric Boogaloo (#6113)
# Description of Changes When I added Prettier formatting in #6052, my aim was to use just the default settings in Prettier. Turns out, Prettier looks _really hard_ for any config files if it's not explicitly given one, which means that if a developer has some sort of Prettier config file lying around on their system, Prettier might find it and use it. Also, Prettier changes its defaults based on stuff in `.editorconfig` without any good way of disabling that behaviour explicitly in its config file. To solve both of these issues, I've introduced a `.prettierrc` file which sets Prettier's defaults explicitly, and then reformatted all our code _again_ in Prettier's actual default settings. This should achieve the aim of #6052 and remove the possibility for it breaking on different dev computers.
This commit is contained in:
@@ -16,7 +16,11 @@ interface AddFileCardProps {
|
||||
multiple?: boolean;
|
||||
}
|
||||
|
||||
const AddFileCard = ({ onFileSelect, accept, multiple = true }: AddFileCardProps) => {
|
||||
const AddFileCard = ({
|
||||
onFileSelect,
|
||||
accept,
|
||||
multiple = true,
|
||||
}: AddFileCardProps) => {
|
||||
const { t } = useTranslation();
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const { openFilesModal } = useFilesModalContext();
|
||||
@@ -84,7 +88,9 @@ const AddFileCard = ({ onFileSelect, accept, multiple = true }: AddFileCardProps
|
||||
<div className={styles.logoMark}>
|
||||
<AddIcon sx={{ color: "inherit", fontSize: "1.5rem" }} />
|
||||
</div>
|
||||
<div className={styles.headerIndex}>{t("fileEditor.addFiles", "Add Files")}</div>
|
||||
<div className={styles.headerIndex}>
|
||||
{t("fileEditor.addFiles", "Add Files")}
|
||||
</div>
|
||||
<div className={styles.kebab} />
|
||||
</div>
|
||||
|
||||
@@ -131,8 +137,15 @@ const AddFileCard = ({ onFileSelect, accept, multiple = true }: AddFileCardProps
|
||||
onClick={handleOpenFilesModal}
|
||||
onMouseEnter={() => setIsUploadHover(false)}
|
||||
>
|
||||
<LocalIcon icon="add" width="1.5rem" height="1.5rem" className="text-[var(--accent-interactive)]" />
|
||||
{!isUploadHover && <span>{t("landing.addFiles", "Add Files")}</span>}
|
||||
<LocalIcon
|
||||
icon="add"
|
||||
width="1.5rem"
|
||||
height="1.5rem"
|
||||
className="text-[var(--accent-interactive)]"
|
||||
/>
|
||||
{!isUploadHover && (
|
||||
<span>{t("landing.addFiles", "Add Files")}</span>
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
aria-label="Upload"
|
||||
@@ -160,14 +173,22 @@ const AddFileCard = ({ onFileSelect, accept, multiple = true }: AddFileCardProps
|
||||
height="1.25rem"
|
||||
style={{ color: "var(--accent-interactive)" }}
|
||||
/>
|
||||
{isUploadHover && <span style={{ marginLeft: ".5rem" }}>{terminology.uploadFromComputer}</span>}
|
||||
{isUploadHover && (
|
||||
<span style={{ marginLeft: ".5rem" }}>
|
||||
{terminology.uploadFromComputer}
|
||||
</span>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Instruction Text */}
|
||||
<span
|
||||
className="text-[var(--accent-interactive)]"
|
||||
style={{ fontSize: ".8rem", textAlign: "center", marginTop: "0.5rem" }}
|
||||
style={{
|
||||
fontSize: ".8rem",
|
||||
textAlign: "center",
|
||||
marginTop: "0.5rem",
|
||||
}}
|
||||
>
|
||||
{terminology.dropFilesHere}
|
||||
</span>
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
import { useState, useCallback, useRef, useMemo, useEffect } from "react";
|
||||
import { Text, Center, Box, LoadingOverlay, Stack } from "@mantine/core";
|
||||
import { Dropzone } from "@mantine/dropzone";
|
||||
import { useFileSelection, useFileState, useFileManagement, useFileActions, useFileContext } from "@app/contexts/FileContext";
|
||||
import {
|
||||
useFileSelection,
|
||||
useFileState,
|
||||
useFileManagement,
|
||||
useFileActions,
|
||||
useFileContext,
|
||||
} from "@app/contexts/FileContext";
|
||||
import { useNavigationActions } from "@app/contexts/NavigationContext";
|
||||
import { useViewer } from "@app/contexts/ViewerContext";
|
||||
import { zipFileService } from "@app/services/zipFileService";
|
||||
@@ -22,7 +28,10 @@ interface FileEditorProps {
|
||||
supportedExtensions?: string[];
|
||||
}
|
||||
|
||||
const FileEditor = ({ toolMode = false, supportedExtensions = ["pdf"] }: FileEditorProps) => {
|
||||
const FileEditor = ({
|
||||
toolMode = false,
|
||||
supportedExtensions = ["pdf"],
|
||||
}: FileEditorProps) => {
|
||||
// Utility function to check if a file extension is supported
|
||||
const isFileSupported = useCallback(
|
||||
(fileName: string): boolean => {
|
||||
@@ -40,7 +49,10 @@ const FileEditor = ({ toolMode = false, supportedExtensions = ["pdf"] }: FileEdi
|
||||
const { clearAllFileErrors } = fileContextActions;
|
||||
|
||||
// Extract needed values from state (memoized to prevent infinite loops)
|
||||
const activeStirlingFileStubs = useMemo(() => selectors.getStirlingFileStubs(), [state.files.byId, state.files.ids]);
|
||||
const activeStirlingFileStubs = useMemo(
|
||||
() => selectors.getStirlingFileStubs(),
|
||||
[state.files.byId, state.files.ids],
|
||||
);
|
||||
const selectedFileIds = state.ui.selectedFileIds;
|
||||
const totalItems = state.files.ids.length;
|
||||
const selectedCount = selectedFileIds.length;
|
||||
@@ -58,11 +70,27 @@ const FileEditor = ({ toolMode = false, supportedExtensions = ["pdf"] }: FileEdi
|
||||
const [_error, _setError] = useState<string | null>(null);
|
||||
|
||||
// Toast helpers
|
||||
const showStatus = useCallback((message: string, type: "neutral" | "success" | "warning" | "error" = "neutral") => {
|
||||
alert({ alertType: type, title: message, expandable: false, durationMs: 4000 });
|
||||
}, []);
|
||||
const showStatus = useCallback(
|
||||
(
|
||||
message: string,
|
||||
type: "neutral" | "success" | "warning" | "error" = "neutral",
|
||||
) => {
|
||||
alert({
|
||||
alertType: type,
|
||||
title: message,
|
||||
expandable: false,
|
||||
durationMs: 4000,
|
||||
});
|
||||
},
|
||||
[],
|
||||
);
|
||||
const showError = useCallback((message: string) => {
|
||||
alert({ alertType: "error", title: "Error", body: message, expandable: true });
|
||||
alert({
|
||||
alertType: "error",
|
||||
title: "Error",
|
||||
body: message,
|
||||
expandable: true,
|
||||
});
|
||||
}, []);
|
||||
const [selectionMode, setSelectionMode] = useState(toolMode);
|
||||
|
||||
@@ -83,7 +111,9 @@ const FileEditor = ({ toolMode = false, supportedExtensions = ["pdf"] }: FileEdi
|
||||
}, [toolMode]);
|
||||
const [showFilePickerModal, setShowFilePickerModal] = useState(false);
|
||||
// Get selected file IDs from context (defensive programming)
|
||||
const contextSelectedIds = Array.isArray(selectedFileIds) ? selectedFileIds : [];
|
||||
const contextSelectedIds = Array.isArray(selectedFileIds)
|
||||
? selectedFileIds
|
||||
: [];
|
||||
|
||||
// Create refs for frequently changing values to stabilize callbacks
|
||||
const contextSelectedIdsRef = useRef<FileId[]>([]);
|
||||
@@ -95,7 +125,9 @@ const FileEditor = ({ toolMode = false, supportedExtensions = ["pdf"] }: FileEdi
|
||||
const handleSelectAllFiles = useCallback(() => {
|
||||
// Respect maxAllowed: if limited, select the last N files
|
||||
const allIds = state.files.ids;
|
||||
const idsToSelect = Number.isFinite(maxAllowed) ? allIds.slice(-maxAllowed) : allIds;
|
||||
const idsToSelect = Number.isFinite(maxAllowed)
|
||||
? allIds.slice(-maxAllowed)
|
||||
: allIds;
|
||||
setSelectedFiles(idsToSelect);
|
||||
try {
|
||||
clearAllFileErrors();
|
||||
@@ -147,7 +179,9 @@ const FileEditor = ({ toolMode = false, supportedExtensions = ["pdf"] }: FileEdi
|
||||
await addFiles(uploadedFiles, { selectFiles: true });
|
||||
// After auto-selection, enforce maxAllowed if needed
|
||||
if (Number.isFinite(maxAllowed)) {
|
||||
const nowSelectedIds = selectors.getSelectedStirlingFileStubs().map((r) => r.id);
|
||||
const nowSelectedIds = selectors
|
||||
.getSelectedStirlingFileStubs()
|
||||
.map((r) => r.id);
|
||||
if (nowSelectedIds.length > maxAllowed) {
|
||||
setSelectedFiles(nowSelectedIds.slice(-maxAllowed));
|
||||
}
|
||||
@@ -155,7 +189,8 @@ const FileEditor = ({ toolMode = false, supportedExtensions = ["pdf"] }: FileEdi
|
||||
showStatus(`Added ${uploadedFiles.length} file(s)`, "success");
|
||||
}
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : "Failed to process files";
|
||||
const errorMessage =
|
||||
err instanceof Error ? err.message : "Failed to process files";
|
||||
showError(errorMessage);
|
||||
console.error("File processing error:", err);
|
||||
}
|
||||
@@ -182,14 +217,18 @@ const FileEditor = ({ toolMode = false, supportedExtensions = ["pdf"] }: FileEdi
|
||||
// Add file to selection
|
||||
// Determine max files allowed from the active tool (negative or undefined means unlimited)
|
||||
const rawMax = selectedTool?.maxFiles;
|
||||
const maxAllowed = !toolMode || rawMax == null || rawMax < 0 ? Infinity : rawMax;
|
||||
const maxAllowed =
|
||||
!toolMode || rawMax == null || rawMax < 0 ? Infinity : rawMax;
|
||||
|
||||
if (maxAllowed === 1) {
|
||||
// Only one file allowed -> replace selection with the new file
|
||||
newSelection = [contextFileId];
|
||||
} else {
|
||||
// If at capacity, drop the oldest selected and append the new one
|
||||
if (Number.isFinite(maxAllowed) && currentSelectedIds.length >= maxAllowed) {
|
||||
if (
|
||||
Number.isFinite(maxAllowed) &&
|
||||
currentSelectedIds.length >= maxAllowed
|
||||
) {
|
||||
newSelection = [...currentSelectedIds.slice(1), contextFileId];
|
||||
} else {
|
||||
newSelection = [...currentSelectedIds, contextFileId];
|
||||
@@ -200,7 +239,13 @@ const FileEditor = ({ toolMode = false, supportedExtensions = ["pdf"] }: FileEdi
|
||||
// Update context (this automatically updates tool selection since they use the same action)
|
||||
setSelectedFiles(newSelection);
|
||||
},
|
||||
[setSelectedFiles, toolMode, _setStatus, activeStirlingFileStubs, selectedTool?.maxFiles],
|
||||
[
|
||||
setSelectedFiles,
|
||||
toolMode,
|
||||
_setStatus,
|
||||
activeStirlingFileStubs,
|
||||
selectedTool?.maxFiles,
|
||||
],
|
||||
);
|
||||
|
||||
// Enforce maxAllowed when tool changes or when an external action sets too many selected files
|
||||
@@ -226,13 +271,17 @@ const FileEditor = ({ toolMode = false, supportedExtensions = ["pdf"] }: FileEdi
|
||||
|
||||
// Handle multi-file selection reordering
|
||||
const filesToMove =
|
||||
selectedFileIds.length > 1 ? selectedFileIds.filter((id) => currentIds.includes(id)) : [sourceFileId];
|
||||
selectedFileIds.length > 1
|
||||
? selectedFileIds.filter((id) => currentIds.includes(id))
|
||||
: [sourceFileId];
|
||||
|
||||
// Create new order
|
||||
const newOrder = [...currentIds];
|
||||
|
||||
// Remove files to move from their current positions (in reverse order to maintain indices)
|
||||
const sourceIndices = filesToMove.map((id) => newOrder.findIndex((nId) => nId === id)).sort((a, b) => b - a); // Sort descending
|
||||
const sourceIndices = filesToMove
|
||||
.map((id) => newOrder.findIndex((nId) => nId === id))
|
||||
.sort((a, b) => b - a); // Sort descending
|
||||
|
||||
sourceIndices.forEach((index) => {
|
||||
newOrder.splice(index, 1);
|
||||
@@ -278,11 +327,19 @@ const FileEditor = ({ toolMode = false, supportedExtensions = ["pdf"] }: FileEdi
|
||||
removeFiles([contextFileId], false);
|
||||
|
||||
// Remove from context selections
|
||||
const currentSelected = selectedFileIds.filter((id) => id !== contextFileId);
|
||||
const currentSelected = selectedFileIds.filter(
|
||||
(id) => id !== contextFileId,
|
||||
);
|
||||
setSelectedFiles(currentSelected);
|
||||
}
|
||||
},
|
||||
[activeStirlingFileStubs, selectors, removeFiles, setSelectedFiles, selectedFileIds],
|
||||
[
|
||||
activeStirlingFileStubs,
|
||||
selectors,
|
||||
removeFiles,
|
||||
setSelectedFiles,
|
||||
selectedFileIds,
|
||||
],
|
||||
);
|
||||
|
||||
const handleDownloadFile = useCallback(
|
||||
@@ -315,7 +372,10 @@ const FileEditor = ({ toolMode = false, supportedExtensions = ["pdf"] }: FileEdi
|
||||
isDirty: false,
|
||||
});
|
||||
} else {
|
||||
console.log("[FileEditor] Skipping clean mark:", { savedPath: result.savedPath, isDirty: record.isDirty });
|
||||
console.log("[FileEditor] Skipping clean mark:", {
|
||||
savedPath: result.savedPath,
|
||||
isDirty: record.isDirty,
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -329,7 +389,10 @@ const FileEditor = ({ toolMode = false, supportedExtensions = ["pdf"] }: FileEdi
|
||||
if (record && file) {
|
||||
try {
|
||||
// Extract and store files using shared service method
|
||||
const result = await zipFileService.extractAndStoreFilesWithHistory(file, record);
|
||||
const result = await zipFileService.extractAndStoreFilesWithHistory(
|
||||
file,
|
||||
record,
|
||||
);
|
||||
|
||||
if (result.success && result.extractedStubs.length > 0) {
|
||||
// Add extracted file stubs to FileContext
|
||||
@@ -376,7 +439,12 @@ const FileEditor = ({ toolMode = false, supportedExtensions = ["pdf"] }: FileEdi
|
||||
navActions.setWorkbench("viewer");
|
||||
}
|
||||
},
|
||||
[activeStirlingFileStubs, setActiveFileId, setActiveFileIndex, navActions.setWorkbench],
|
||||
[
|
||||
activeStirlingFileStubs,
|
||||
setActiveFileId,
|
||||
setActiveFileIndex,
|
||||
navActions.setWorkbench,
|
||||
],
|
||||
);
|
||||
|
||||
const handleLoadFromStorage = useCallback(async (selectedFiles: File[]) => {
|
||||
@@ -417,7 +485,8 @@ const FileEditor = ({ toolMode = false, supportedExtensions = ["pdf"] }: FileEdi
|
||||
</Text>
|
||||
<Text c="dimmed">No files loaded</Text>
|
||||
<Text size="sm" c="dimmed">
|
||||
Upload PDF files, ZIP archives, or load from storage to get started
|
||||
Upload PDF files, ZIP archives, or load from storage to get
|
||||
started
|
||||
</Text>
|
||||
</Stack>
|
||||
</Center>
|
||||
@@ -432,7 +501,12 @@ const FileEditor = ({ toolMode = false, supportedExtensions = ["pdf"] }: FileEdi
|
||||
}}
|
||||
>
|
||||
{/* Add File Card - only show when files exist */}
|
||||
{activeStirlingFileStubs.length > 0 && <AddFileCard key="add-file-card" onFileSelect={handleFileUpload} />}
|
||||
{activeStirlingFileStubs.length > 0 && (
|
||||
<AddFileCard
|
||||
key="add-file-card"
|
||||
onFileSelect={handleFileUpload}
|
||||
/>
|
||||
)}
|
||||
|
||||
{activeStirlingFileStubs.map((record, index) => {
|
||||
return (
|
||||
|
||||
@@ -6,6 +6,8 @@ interface FileEditorFileNameProps {
|
||||
file: StirlingFileStub;
|
||||
}
|
||||
|
||||
const FileEditorFileName = ({ file }: FileEditorFileNameProps) => <PrivateContent>{file.name}</PrivateContent>;
|
||||
const FileEditorFileName = ({ file }: FileEditorFileNameProps) => (
|
||||
<PrivateContent>{file.name}</PrivateContent>
|
||||
);
|
||||
|
||||
export default FileEditorFileName;
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
import React, { useState, useCallback, useRef, useMemo } from "react";
|
||||
import { Text, ActionIcon, CheckboxIndicator, Tooltip, Modal, Button, Group, Stack, Loader } from "@mantine/core";
|
||||
import {
|
||||
Text,
|
||||
ActionIcon,
|
||||
CheckboxIndicator,
|
||||
Tooltip,
|
||||
Modal,
|
||||
Button,
|
||||
Group,
|
||||
Stack,
|
||||
Loader,
|
||||
} from "@mantine/core";
|
||||
import { useIsMobile } from "@app/hooks/useIsMobile";
|
||||
import { alert } from "@app/components/toast";
|
||||
import { useTranslation } from "react-i18next";
|
||||
@@ -14,7 +24,10 @@ import PushPinIcon from "@mui/icons-material/PushPin";
|
||||
import PushPinOutlinedIcon from "@mui/icons-material/PushPinOutlined";
|
||||
import LockOpenIcon from "@mui/icons-material/LockOpen";
|
||||
import DragIndicatorIcon from "@mui/icons-material/DragIndicator";
|
||||
import { draggable, dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
|
||||
import {
|
||||
draggable,
|
||||
dropTargetForElements,
|
||||
} from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
|
||||
import { StirlingFileStub } from "@app/types/fileContext";
|
||||
import { zipFileService } from "@app/services/zipFileService";
|
||||
|
||||
@@ -24,7 +37,9 @@ import { useFileState } from "@app/contexts/file/fileHooks";
|
||||
import { FileId } from "@app/types/file";
|
||||
import { formatFileSize } from "@app/utils/fileUtils";
|
||||
import ToolChain from "@app/components/shared/ToolChain";
|
||||
import HoverActionMenu, { HoverAction } from "@app/components/shared/HoverActionMenu";
|
||||
import HoverActionMenu, {
|
||||
HoverAction,
|
||||
} from "@app/components/shared/HoverActionMenu";
|
||||
import { downloadFile } from "@app/services/downloadService";
|
||||
import { PrivateContent } from "@app/components/shared/PrivateContent";
|
||||
import UploadToServerModal from "@app/components/shared/UploadToServerModal";
|
||||
@@ -42,7 +57,11 @@ interface FileEditorThumbnailProps {
|
||||
onCloseFile: (fileId: FileId) => void;
|
||||
onViewFile: (fileId: FileId) => void;
|
||||
_onSetStatus: (status: string) => void;
|
||||
onReorderFiles?: (sourceFileId: FileId, targetFileId: FileId, selectedFileIds: FileId[]) => void;
|
||||
onReorderFiles?: (
|
||||
sourceFileId: FileId,
|
||||
targetFileId: FileId,
|
||||
selectedFileIds: FileId[],
|
||||
) => void;
|
||||
onDownloadFile: (fileId: FileId) => void;
|
||||
onUnzipFile?: (fileId: FileId) => void;
|
||||
toolMode?: boolean;
|
||||
@@ -67,7 +86,14 @@ const FileEditorThumbnail = ({
|
||||
const terminology = useFileActionTerminology();
|
||||
const icons = useFileActionIcons();
|
||||
const DownloadOutlinedIcon = icons.download;
|
||||
const { pinFile, unpinFile, isFilePinned, activeFiles, actions: fileActions, openEncryptedUnlockPrompt } = useFileContext();
|
||||
const {
|
||||
pinFile,
|
||||
unpinFile,
|
||||
isFilePinned,
|
||||
activeFiles,
|
||||
actions: fileActions,
|
||||
openEncryptedUnlockPrompt,
|
||||
} = useFileContext();
|
||||
const { state, selectors } = useFileState();
|
||||
const hasError = state.ui.errorFileIds.includes(file.id);
|
||||
|
||||
@@ -117,18 +143,29 @@ const FileEditorThumbnail = ({
|
||||
const isCBZ = extLower === "cbz";
|
||||
const isCBR = extLower === "cbr";
|
||||
const uploadEnabled = config?.storageEnabled === true;
|
||||
const sharingEnabled = uploadEnabled && config?.storageSharingEnabled === true;
|
||||
const shareLinksEnabled = sharingEnabled && config?.storageShareLinksEnabled === true;
|
||||
const sharingEnabled =
|
||||
uploadEnabled && config?.storageSharingEnabled === true;
|
||||
const shareLinksEnabled =
|
||||
sharingEnabled && config?.storageShareLinksEnabled === true;
|
||||
const isOwnedOrLocal = file.remoteOwnedByCurrentUser !== false;
|
||||
const isSharedFile = file.remoteOwnedByCurrentUser === false || file.remoteSharedViaLink;
|
||||
const isSharedFile =
|
||||
file.remoteOwnedByCurrentUser === false || file.remoteSharedViaLink;
|
||||
const localUpdatedAt = file.createdAt ?? file.lastModified ?? 0;
|
||||
const remoteUpdatedAt = file.remoteStorageUpdatedAt ?? 0;
|
||||
const isUploaded = Boolean(file.remoteStorageId);
|
||||
const isUpToDate = isUploaded && remoteUpdatedAt >= localUpdatedAt;
|
||||
const canUpload = uploadEnabled && isOwnedOrLocal && file.isLeaf && (!isUploaded || !isUpToDate);
|
||||
const canUpload =
|
||||
uploadEnabled &&
|
||||
isOwnedOrLocal &&
|
||||
file.isLeaf &&
|
||||
(!isUploaded || !isUpToDate);
|
||||
const canShare = shareLinksEnabled && isOwnedOrLocal && file.isLeaf;
|
||||
|
||||
const pageLabel = useMemo(() => (pageCount > 0 ? `${pageCount} ${pageCount === 1 ? "Page" : "Pages"}` : ""), [pageCount]);
|
||||
const pageLabel = useMemo(
|
||||
() =>
|
||||
pageCount > 0 ? `${pageCount} ${pageCount === 1 ? "Page" : "Pages"}` : "",
|
||||
[pageCount],
|
||||
);
|
||||
|
||||
const dateLabel = useMemo(() => {
|
||||
const d = new Date(file.lastModified);
|
||||
@@ -198,7 +235,12 @@ const FileEditorThumbnail = ({
|
||||
|
||||
const handleConfirmClose = useCallback(() => {
|
||||
onCloseFile(file.id);
|
||||
alert({ alertType: "neutral", title: `Closed ${file.name}`, expandable: false, durationMs: 3500 });
|
||||
alert({
|
||||
alertType: "neutral",
|
||||
title: `Closed ${file.name}`,
|
||||
expandable: false,
|
||||
durationMs: 3500,
|
||||
});
|
||||
setShowCloseModal(false);
|
||||
}, [file.id, file.name, onCloseFile]);
|
||||
|
||||
@@ -222,16 +264,33 @@ const FileEditorThumbnail = ({
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to save ${file.name}:`, error);
|
||||
alert({ alertType: "error", title: "Save failed", body: `Could not save ${file.name}`, expandable: true });
|
||||
alert({
|
||||
alertType: "error",
|
||||
title: "Save failed",
|
||||
body: `Could not save ${file.name}`,
|
||||
expandable: true,
|
||||
});
|
||||
setShowCloseModal(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Then close
|
||||
onCloseFile(file.id);
|
||||
alert({ alertType: "success", title: `Saved and closed ${file.name}`, expandable: false, durationMs: 3500 });
|
||||
alert({
|
||||
alertType: "success",
|
||||
title: `Saved and closed ${file.name}`,
|
||||
expandable: false,
|
||||
durationMs: 3500,
|
||||
});
|
||||
setShowCloseModal(false);
|
||||
}, [file.id, file.name, file.localFilePath, onCloseFile, selectors, fileActions]);
|
||||
}, [
|
||||
file.id,
|
||||
file.name,
|
||||
file.localFilePath,
|
||||
onCloseFile,
|
||||
selectors,
|
||||
fileActions,
|
||||
]);
|
||||
|
||||
const handleCancelClose = useCallback(() => {
|
||||
setShowCloseModal(false);
|
||||
@@ -298,7 +357,12 @@ const FileEditorThumbnail = ({
|
||||
e.stopPropagation();
|
||||
if (onUnzipFile) {
|
||||
onUnzipFile(file.id);
|
||||
alert({ alertType: "success", title: `Unzipping ${file.name}`, expandable: false, durationMs: 2500 });
|
||||
alert({
|
||||
alertType: "success",
|
||||
title: `Unzipping ${file.name}`,
|
||||
expandable: false,
|
||||
durationMs: 2500,
|
||||
});
|
||||
}
|
||||
},
|
||||
hidden: !isZipFile || !onUnzipFile || isCBZ || isCBR,
|
||||
@@ -381,7 +445,10 @@ const FileEditorThumbnail = ({
|
||||
onDoubleClick={handleCardDoubleClick}
|
||||
>
|
||||
{/* Header bar */}
|
||||
<div className={`${styles.header} ${getHeaderClassName()}`} data-has-error={hasError}>
|
||||
<div
|
||||
className={`${styles.header} ${getHeaderClassName()}`}
|
||||
data-has-error={hasError}
|
||||
>
|
||||
{/* Logo/checkbox area */}
|
||||
<div className={styles.logoMark}>
|
||||
{hasError ? (
|
||||
@@ -402,16 +469,27 @@ const FileEditorThumbnail = ({
|
||||
</div>
|
||||
|
||||
{/* Centered index */}
|
||||
<div className={styles.headerIndex} aria-label={`Position ${index + 1}`}>
|
||||
<div
|
||||
className={styles.headerIndex}
|
||||
aria-label={`Position ${index + 1}`}
|
||||
>
|
||||
{index + 1}
|
||||
</div>
|
||||
|
||||
{/* Action buttons group */}
|
||||
<div className={styles.headerActions}>
|
||||
{isEncrypted && (
|
||||
<Tooltip label={t("encryptedPdfUnlock.unlockPrompt", "Unlock PDF to continue")}>
|
||||
<Tooltip
|
||||
label={t(
|
||||
"encryptedPdfUnlock.unlockPrompt",
|
||||
"Unlock PDF to continue",
|
||||
)}
|
||||
>
|
||||
<ActionIcon
|
||||
aria-label={t("encryptedPdfUnlock.unlockPrompt", "Unlock PDF to continue")}
|
||||
aria-label={t(
|
||||
"encryptedPdfUnlock.unlockPrompt",
|
||||
"Unlock PDF to continue",
|
||||
)}
|
||||
variant="subtle"
|
||||
className={styles.headerIconButton}
|
||||
onClick={(e) => {
|
||||
@@ -426,7 +504,9 @@ const FileEditorThumbnail = ({
|
||||
{/* Pin/Unpin icon */}
|
||||
<Tooltip
|
||||
label={
|
||||
isPinned ? t("unpin", "Unpin File (replace after tool run)") : t("pin", "Pin File (keep active after tool run)")
|
||||
isPinned
|
||||
? t("unpin", "Unpin File (replace after tool run)")
|
||||
: t("pin", "Pin File (keep active after tool run)")
|
||||
}
|
||||
>
|
||||
<ActionIcon
|
||||
@@ -443,15 +523,29 @@ const FileEditorThumbnail = ({
|
||||
if (actualFile) {
|
||||
if (isPinned) {
|
||||
unpinFile(actualFile);
|
||||
alert({ alertType: "neutral", title: `Unpinned ${file.name}`, expandable: false, durationMs: 3000 });
|
||||
alert({
|
||||
alertType: "neutral",
|
||||
title: `Unpinned ${file.name}`,
|
||||
expandable: false,
|
||||
durationMs: 3000,
|
||||
});
|
||||
} else {
|
||||
pinFile(actualFile);
|
||||
alert({ alertType: "success", title: `Pinned ${file.name}`, expandable: false, durationMs: 3000 });
|
||||
alert({
|
||||
alertType: "success",
|
||||
title: `Pinned ${file.name}`,
|
||||
expandable: false,
|
||||
durationMs: 3000,
|
||||
});
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
{isPinned ? <PushPinIcon fontSize="small" /> : <PushPinOutlinedIcon fontSize="small" />}
|
||||
{isPinned ? (
|
||||
<PushPinIcon fontSize="small" />
|
||||
) : (
|
||||
<PushPinOutlinedIcon fontSize="small" />
|
||||
)}
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</div>
|
||||
@@ -470,7 +564,13 @@ const FileEditorThumbnail = ({
|
||||
<Text size="lg" fw={700} className={styles.title} title={file.name}>
|
||||
<PrivateContent>{truncateCenter(file.name, 40)}</PrivateContent>
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed" className={styles.meta} lineClamp={3} title={`${extUpper || "FILE"} • ${prettySize}`}>
|
||||
<Text
|
||||
size="sm"
|
||||
c="dimmed"
|
||||
className={styles.meta}
|
||||
lineClamp={3}
|
||||
title={`${extUpper || "FILE"} • ${prettySize}`}
|
||||
>
|
||||
{/* e.g., v2 - Jan 29, 2025 - PDF file - 3 Pages */}
|
||||
{`v${file.versionNumber} - `}
|
||||
{dateLabel}
|
||||
@@ -482,7 +582,11 @@ const FileEditorThumbnail = ({
|
||||
{/* Preview area */}
|
||||
<div
|
||||
className={`${styles.previewBox} mx-6 mb-4 relative flex-1`}
|
||||
style={isSupported || hasError ? undefined : { filter: "grayscale(80%)", opacity: 0.6 }}
|
||||
style={
|
||||
isSupported || hasError
|
||||
? undefined
|
||||
: { filter: "grayscale(80%)", opacity: 0.6 }
|
||||
}
|
||||
>
|
||||
<div className={styles.previewPaper}>
|
||||
{file.thumbnailUrl ? (
|
||||
@@ -513,7 +617,12 @@ const FileEditorThumbnail = ({
|
||||
/>
|
||||
</PrivateContent>
|
||||
) : file.type?.startsWith("application/pdf") ? (
|
||||
<Stack align="center" justify="center" gap="xs" style={{ height: "100%" }}>
|
||||
<Stack
|
||||
align="center"
|
||||
justify="center"
|
||||
gap="xs"
|
||||
style={{ height: "100%" }}
|
||||
>
|
||||
<Loader size="sm" />
|
||||
<Text size="xs" c="dimmed">
|
||||
Loading thumbnail...
|
||||
@@ -554,7 +663,11 @@ const FileEditorThumbnail = ({
|
||||
</div>
|
||||
|
||||
{/* Hover Menu */}
|
||||
<HoverActionMenu show={showHoverMenu || isMobile} actions={hoverActions} position="outside" />
|
||||
<HoverActionMenu
|
||||
show={showHoverMenu || isMobile}
|
||||
actions={hoverActions}
|
||||
position="outside"
|
||||
/>
|
||||
|
||||
{/* Close Confirmation Modal */}
|
||||
<Modal
|
||||
@@ -567,7 +680,9 @@ const FileEditorThumbnail = ({
|
||||
<Stack gap="md">
|
||||
{file.isDirty && file.localFilePath ? (
|
||||
<>
|
||||
<Text size="md">{t("confirmCloseUnsaved", "This file has unsaved changes.")}</Text>
|
||||
<Text size="md">
|
||||
{t("confirmCloseUnsaved", "This file has unsaved changes.")}
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed" fw={500}>
|
||||
{file.name}
|
||||
</Text>
|
||||
@@ -575,7 +690,11 @@ const FileEditorThumbnail = ({
|
||||
<Button variant="light" onClick={handleCancelClose}>
|
||||
{t("confirmCloseCancel", "Cancel")}
|
||||
</Button>
|
||||
<Button variant="filled" color="red" onClick={handleConfirmClose}>
|
||||
<Button
|
||||
variant="filled"
|
||||
color="red"
|
||||
onClick={handleConfirmClose}
|
||||
>
|
||||
{t("confirmCloseDiscard", "Discard changes and close")}
|
||||
</Button>
|
||||
<Button variant="filled" onClick={handleSaveAndClose}>
|
||||
@@ -585,7 +704,12 @@ const FileEditorThumbnail = ({
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Text size="md">{t("confirmCloseMessage", "Are you sure you want to close this file?")}</Text>
|
||||
<Text size="md">
|
||||
{t(
|
||||
"confirmCloseMessage",
|
||||
"Are you sure you want to close this file?",
|
||||
)}
|
||||
</Text>
|
||||
<Text size="sm" c="dimmed" fw={500}>
|
||||
{file.name}
|
||||
</Text>
|
||||
@@ -593,7 +717,11 @@ const FileEditorThumbnail = ({
|
||||
<Button variant="light" onClick={handleCancelClose}>
|
||||
{t("confirmCloseCancel", "Cancel")}
|
||||
</Button>
|
||||
<Button variant="filled" color="red" onClick={handleConfirmClose}>
|
||||
<Button
|
||||
variant="filled"
|
||||
color="red"
|
||||
onClick={handleConfirmClose}
|
||||
>
|
||||
{t("confirmCloseConfirm", "Close File")}
|
||||
</Button>
|
||||
</Group>
|
||||
@@ -623,8 +751,20 @@ const FileEditorThumbnail = ({
|
||||
</Stack>
|
||||
</Modal>
|
||||
|
||||
{canUpload && <UploadToServerModal opened={showUploadModal} onClose={() => setShowUploadModal(false)} file={file} />}
|
||||
{canShare && <ShareFileModal opened={showShareModal} onClose={() => setShowShareModal(false)} file={file} />}
|
||||
{canUpload && (
|
||||
<UploadToServerModal
|
||||
opened={showUploadModal}
|
||||
onClose={() => setShowUploadModal(false)}
|
||||
file={file}
|
||||
/>
|
||||
)}
|
||||
{canShare && (
|
||||
<ShareFileModal
|
||||
opened={showShareModal}
|
||||
onClose={() => setShowShareModal(false)}
|
||||
file={file}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useRightRailButtons, RightRailButtonWithAction } from "@app/hooks/useRightRailButtons";
|
||||
import {
|
||||
useRightRailButtons,
|
||||
RightRailButtonWithAction,
|
||||
} from "@app/hooks/useRightRailButtons";
|
||||
import LocalIcon from "@app/components/shared/LocalIcon";
|
||||
|
||||
interface FileEditorRightRailButtonsParams {
|
||||
@@ -26,7 +29,10 @@ export function useFileEditorRightRailButtons({
|
||||
id: "file-select-all",
|
||||
icon: <LocalIcon icon="select-all" width="1.5rem" height="1.5rem" />,
|
||||
tooltip: t("rightRail.selectAll", "Select All"),
|
||||
ariaLabel: typeof t === "function" ? t("rightRail.selectAll", "Select All") : "Select All",
|
||||
ariaLabel:
|
||||
typeof t === "function"
|
||||
? t("rightRail.selectAll", "Select All")
|
||||
: "Select All",
|
||||
section: "top" as const,
|
||||
order: 10,
|
||||
disabled: totalItems === 0 || selectedCount === totalItems,
|
||||
@@ -35,9 +41,18 @@ export function useFileEditorRightRailButtons({
|
||||
},
|
||||
{
|
||||
id: "file-deselect-all",
|
||||
icon: <LocalIcon icon="crop-square-outline" width="1.5rem" height="1.5rem" />,
|
||||
icon: (
|
||||
<LocalIcon
|
||||
icon="crop-square-outline"
|
||||
width="1.5rem"
|
||||
height="1.5rem"
|
||||
/>
|
||||
),
|
||||
tooltip: t("rightRail.deselectAll", "Deselect All"),
|
||||
ariaLabel: typeof t === "function" ? t("rightRail.deselectAll", "Deselect All") : "Deselect All",
|
||||
ariaLabel:
|
||||
typeof t === "function"
|
||||
? t("rightRail.deselectAll", "Deselect All")
|
||||
: "Deselect All",
|
||||
section: "top" as const,
|
||||
order: 20,
|
||||
disabled: selectedCount === 0,
|
||||
@@ -48,7 +63,10 @@ export function useFileEditorRightRailButtons({
|
||||
id: "file-close-selected",
|
||||
icon: <LocalIcon icon="close-rounded" width="1.5rem" height="1.5rem" />,
|
||||
tooltip: t("rightRail.closeSelected", "Close Selected Files"),
|
||||
ariaLabel: typeof t === "function" ? t("rightRail.closeSelected", "Close Selected Files") : "Close Selected Files",
|
||||
ariaLabel:
|
||||
typeof t === "function"
|
||||
? t("rightRail.closeSelected", "Close Selected Files")
|
||||
: "Close Selected Files",
|
||||
section: "top" as const,
|
||||
order: 30,
|
||||
disabled: selectedCount === 0,
|
||||
@@ -56,7 +74,15 @@ export function useFileEditorRightRailButtons({
|
||||
onClick: onCloseSelected,
|
||||
},
|
||||
],
|
||||
[t, i18n.language, totalItems, selectedCount, onSelectAll, onDeselectAll, onCloseSelected],
|
||||
[
|
||||
t,
|
||||
i18n.language,
|
||||
totalItems,
|
||||
selectedCount,
|
||||
onSelectAll,
|
||||
onDeselectAll,
|
||||
onCloseSelected,
|
||||
],
|
||||
);
|
||||
|
||||
useRightRailButtons(buttons);
|
||||
|
||||
Reference in New Issue
Block a user