Preserve local paths for desktop saves (#5543)

# Summary

- Adds desktop file tracking: local paths are preserved and save buttons
now work as expcted (doing Save/Save As as appropriate)
- Adds logic to track whether files are 'dirty' (they've been modified
by some tool, and not saved to disk yet).
- Improves file state UX (dirty vs saved) and close warnings
- Web behaviour should be unaffected by these changes

## Indicators
Files now have indicators in desktop mode to tell you their state.

### File up-to-date with disk

<img width="318" height="393" alt="image"
src="https://github.com/user-attachments/assets/06325f9a-afd7-4c2f-8a5b-6d11e3093115"
/>

### File modified by a tool but not saved to disk yet

<img width="357" height="385" alt="image"
src="https://github.com/user-attachments/assets/1a7716d9-c6f7-4d13-be0d-c1de6493954b"
/>

### File not tracked on disk

<img width="312" height="379" alt="image"
src="https://github.com/user-attachments/assets/9cffe300-bd9a-4e19-97c7-9b98bebefacc"
/>

# Limitations
- It's a bit weird that we still have files stored in indexeddb in the
app, which are still loadable. We might want to change this behaviour in
the future
- Viewer's Save doesn't persist to disk. I've left that out here because
it'd need a lot of testing to make sure the logic's right with making
sure you can leave the Viewer with applying the changes to the PDF
_without_ saving to disk
- There's no current way to do Save As on a file that has already been
persisted to disk - it's only ever Save. Similarly, there's no way to
duplicate a file.

---------

Co-authored-by: James Brunton <[email protected]>
Co-authored-by: James Brunton <[email protected]>
This commit is contained in:
Anthony Stirling
2026-02-13 23:15:28 +00:00
committed by GitHub
co-authored by James Brunton James Brunton
parent 946196de43
commit b8ce4e47c1
56 changed files with 1367 additions and 182 deletions
@@ -12,8 +12,14 @@ const FileActions: React.FC = () => {
const terminology = useFileActionTerminology();
const icons = useFileActionIcons();
const DownloadIcon = icons.download;
const { recentFiles, selectedFileIds, filteredFiles, onSelectAll, onDeleteSelected, onDownloadSelected } =
useFileManagerContext();
const {
recentFiles,
selectedFileIds,
filteredFiles,
onSelectAll,
onDeleteSelected,
onDownloadSelected
} = useFileManagerContext();
const handleSelectAll = () => {
onSelectAll();
@@ -31,6 +37,7 @@ const FileActions: React.FC = () => {
}
};
// Only show actions if there are files
if (recentFiles.length === 0) {
return null;
@@ -46,7 +46,7 @@ const FileListItem: React.FC<FileListItemProps> = ({
const [isHovered, setIsHovered] = useState(false);
const [isMenuOpen, setIsMenuOpen] = useState(false);
const { t } = useTranslation();
const {expandedFileIds, onToggleExpansion, onUnzipFile } = useFileManagerContext();
const { expandedFileIds, onToggleExpansion, onUnzipFile } = useFileManagerContext();
const { removeFiles } = useFileManagement();
// Check if this is a ZIP file
@@ -269,6 +269,7 @@ const FileListItem: React.FC<FileListItemProps> = ({
>
{t('fileManager.delete', 'Delete')}
</Menu.Item>
</Menu.Dropdown>
</Menu>
</Group>