Skip to content

Commit

Permalink
Added folder settings screen to remove synced folders
Browse files Browse the repository at this point in the history
  • Loading branch information
wladimiiir committed Oct 30, 2024
1 parent 089da88 commit d339ac7
Show file tree
Hide file tree
Showing 10 changed files with 764 additions and 10 deletions.
456 changes: 452 additions & 4 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,13 @@
"dependencies": {
"@electron-toolkit/preload": "^3.0.1",
"@electron-toolkit/utils": "^3.0.0",
"@radix-ui/react-tooltip": "^1.1.3",
"conf": "^13.0.1",
"electron-store": "^10.0.0",
"electron-updater": "^6.1.7",
"notistack": "^3.0.1",
"path-browserify": "^1.0.1",
"react-complex-tree": "^2.4.5",
"react-icons": "^5.3.0",
"react-modal": "^3.16.1",
"react-router-dom": "^6.27.0",
Expand All @@ -47,6 +50,7 @@
"@electron-toolkit/eslint-config-ts": "^2.0.0",
"@electron-toolkit/tsconfig": "^1.0.1",
"@types/node": "^20.14.8",
"@types/path-browserify": "^1.0.3",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@types/react-modal": "^3.16.3",
Expand Down
7 changes: 7 additions & 0 deletions src/main/ipcHandlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,4 +102,11 @@ export const setupIpcHandlers = (webContents: Electron.WebContents, store: Store
}
throw new Error('Model provider not initialized or not found');
});

ipcMain.handle('remove-images-in-folder', async (_event, folderPath: string) => {
const images = store.getImages();
const updatedImages = images.filter((image) => !image.id.startsWith(folderPath));
store.setImages(updatedImages);
return updatedImages;
});
};
3 changes: 2 additions & 1 deletion src/preload/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ const api = {
saveSettings: (settings: Settings): Promise<void> => ipcRenderer.invoke('save-settings', settings),
getSettings: (): Promise<Settings> => ipcRenderer.invoke('get-settings'),
selectFolder: (): Promise<{ success: boolean; path?: string }> => ipcRenderer.invoke('select-folder'),
addFolder: (folderPath: string, includeSubdirs: boolean): Promise<{ success: boolean; message?: string }> =>
addFolder: (folderPath: string, includeSubdirs: boolean): Promise<{ success: boolean; message?: string }> =>
ipcRenderer.invoke('add-folder', folderPath, includeSubdirs),
getImages: (): Promise<Image[]> => ipcRenderer.invoke('get-images'),
generateImageCaption: (image: Image): Promise<string> => ipcRenderer.invoke('generate-image-caption', image),
Expand All @@ -28,6 +28,7 @@ const api = {
}
},
getModels: (settings: Settings): Promise<string[]> => ipcRenderer.invoke('get-models', settings),
removeImagesInFolder: (folderPath: string): Promise<Image[]> => ipcRenderer.invoke('remove-images-in-folder', folderPath),
};

// Use `contextBridge` APIs to expose Electron APIs to
Expand Down
10 changes: 6 additions & 4 deletions src/renderer/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { Link, Navigate, Route, Routes, useNavigate } from 'react-router-dom';
import { Link, Route, Routes, useNavigate } from 'react-router-dom';
import { HiCog, HiHome } from 'react-icons/hi';
import { useEffect } from 'react';
import Home from './components/Home';
import SettingsScreen from './components/SettingsScreen';
import FolderSettings from './components/FolderSettings';
import Modal from 'react-modal';
import icon from './assets/icon.png';

Expand All @@ -11,7 +12,7 @@ const App = () => {

useEffect(() => {
Modal.setAppElement('#app');
navigate('/home');
navigate('/home', { replace: true });
}, []);

return (
Expand All @@ -22,7 +23,7 @@ const App = () => {
<h1 className="text-2xl font-bold text-gray-800 uppercase">Albumate</h1>
</div>
<nav className="mt-6">
<Link to="/" className="flex items-center px-6 py-3 text-gray-700 hover:bg-gray-200">
<Link to="/home" className="flex items-center px-6 py-3 text-gray-700 hover:bg-gray-200">
<HiHome className="w-5 h-5 mr-3" />
Home
</Link>
Expand All @@ -37,9 +38,10 @@ const App = () => {
<main className="flex-1 overflow-x-hidden overflow-y-auto bg-gray-100">
<div className="container mx-auto px-4 sm:px-6 lg:px-8 py-8">
<Routes>
<Route path="/" element={<Navigate to="/home" />} />
<Route path="/" element={<Home />} />
<Route path="/home" element={<Home />} />
<Route path="/settings" element={<SettingsScreen />} />
<Route path="/folder-settings" element={<FolderSettings />} />
</Routes>
</div>
</main>
Expand Down
195 changes: 195 additions & 0 deletions src/renderer/src/components/FolderSettings.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
import { useEffect, useMemo, useState } from 'react';
import { StaticTreeDataProvider, Tree, UncontrolledTreeEnvironment } from 'react-complex-tree';
import '@renderer/styles/TreeStyles.css';
import { HiArrowLeft, HiTrash } from 'react-icons/hi';
import RemoveFolderConfirmDialog from './RemoveFolderConfirmDialog';
import { useNavigate } from 'react-router-dom';
import path from 'path-browserify';
import { Image } from '@shared/types';

interface TreeItem {
index: string;
isFolder: boolean;
children: string[];
data: string;
}

const FolderSettings = () => {
const [images, setImages] = useState<Image[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [confirmDialog, setConfirmDialog] = useState<{ isOpen: boolean; folderPath: string }>({
isOpen: false,
folderPath: '',
});

useEffect(() => {
const loadImages = async () => {
try {
const fetchedImages = await window.api.getImages();
console.log(fetchedImages);
setImages(fetchedImages);
} finally {
setIsLoading(false);
}
};
void loadImages();
}, []);
const navigate = useNavigate();

const treeData = useMemo(() => {
const items: Record<string, TreeItem> = {
root: {
index: 'root',
isFolder: true,
children: [],
data: 'Root',
},
};

// Process each image path to build the tree structure
images.forEach((image) => {
const imagePath = image.id;
// Handle Windows drive letters and root paths
const parts = imagePath.split(path.sep);
const dirParts = parts.slice(0, -1); // Skip the last part (filename)

// Handle root path specially
let currentPath = parts[0];
if (currentPath === '') {
currentPath = path.sep; // Unix root
}

// Add root path if not exists
if (!items[currentPath]) {
items[currentPath] = {
index: currentPath,
isFolder: true,
children: [],
data: currentPath,
};
items.root.children.push(currentPath);
}

// Start from index 1 since we handled root specially
// Process remaining path parts
for (let i = 1; i < dirParts.length; i++) {
const part = dirParts[i];
if (!part) continue; // Skip empty parts

const fullPath = path.join(currentPath, part);
currentPath = fullPath;

if (!items[fullPath]) {
items[fullPath] = {
index: fullPath,
isFolder: true,
children: [],
data: part,
};

// Add to parent's children
const parentPath = path.dirname(fullPath);
const parent = items[parentPath];

if (parent && !parent.children.includes(fullPath)) {
parent.children.push(fullPath);
}
}
}
});

console.log(items);

return items;
}, [images]);

return (
<div className="p-6">
<div className="flex items-center mb-6">
<button onClick={() => navigate('/')} className="mr-4 p-2 rounded-full hover:bg-gray-100">
<HiArrowLeft className="h-6 w-6" />
</button>
<h1 className="text-2xl font-bold">Folder Settings</h1>
</div>

<div className="bg-white rounded-lg shadow p-4 overflow-auto" style={{ height: 'calc(100vh - 200px)' }}>
{isLoading ? (
<div className="h-full flex items-center justify-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900"></div>
</div>
) : (
<UncontrolledTreeEnvironment
key={images.length} // Add key to force re-render when images change
dataProvider={
new StaticTreeDataProvider(treeData, (item) => ({
...item,
canMove: false,
canRename: false,
}))
}
getItemTitle={(item) => item.data}
viewState={{
'folder-tree': {
expandedItems: Object.keys(treeData),
},
}}
canDragAndDrop={false}
canDropOnFolder={false}
canReorderItems={false}
>
<Tree
treeId="folder-tree"
rootItem="root"
renderItem={({ item, title, arrow, children }) => (
<>
<div className="flex items-center justify-between w-full pr-2">
<div className="flex items-center">
{arrow}
<span className="ml-1">{title}</span>
</div>
{item.isFolder && (
<button
onClick={(e) => {
e.stopPropagation();
setConfirmDialog({
isOpen: true,
folderPath: (item as TreeItem).index,
});
}}
className="p-1 rounded hover:bg-gray-200 text-gray-600 hover:text-red-600"
>
<HiTrash className="w-4 h-4" />
</button>
)}
</div>
{children}
</>
)}
renderItemArrow={({ item, context }) =>
item.children?.length ? (
<div className={`rct-tree-item-arrow ${context.isExpanded ? 'rct-tree-item-arrow-expanded' : ''}`}>
<span style={{ fontSize: '10px' }}></span>
</div>
) : (
<div className="rct-tree-item-arrow" />
)
}
/>
</UncontrolledTreeEnvironment>
)}
</div>
<RemoveFolderConfirmDialog
isOpen={confirmDialog.isOpen}
onRequestClose={() => setConfirmDialog({ isOpen: false, folderPath: '' })}
onConfirm={async () => {
const updatedImages = await window.api.removeImagesInFolder(confirmDialog.folderPath);
setImages(updatedImages);
}}
title="Remove Folder"
message={`Are you sure you want to remove all images in "${confirmDialog.folderPath}" from the application? This won't delete the actual files from your disk.`}
/>
</div>
);
};

export default FolderSettings;
23 changes: 22 additions & 1 deletion src/renderer/src/components/Home.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import React, { useEffect, useMemo, useState } from 'react';
import { HiFolderAdd, HiOutlineRefresh, HiSearch } from 'react-icons/hi';
import { HiFolderAdd, HiOutlineRefresh, HiSearch, HiCog } from 'react-icons/hi';
import * as Tooltip from '@radix-ui/react-tooltip';
import { useNavigate } from 'react-router-dom';
import Modal from 'react-modal';
import { Image } from '@shared/types';
import ImageDetails from './ImageDetails';
Expand All @@ -8,6 +10,7 @@ import Pager from './Pager';
import AddFolderModal from './AddFolderModal';

const Home = () => {
const navigate = useNavigate();
const { enqueueSnackbar } = useSnackbar();
const [searchQuery, setSearchQuery] = useState('');
const [images, setImages] = useState<Image[]>([]);
Expand Down Expand Up @@ -100,6 +103,24 @@ const Home = () => {
<HiFolderAdd className="mr-2 h-5 w-5" />
Add Folder
</button>
<Tooltip.Provider>
<Tooltip.Root>
<Tooltip.Trigger asChild>
<button
onClick={() => navigate('/folder-settings')}
className="p-2 border border-gray-300 rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
<HiCog className="h-5 w-5" />
</button>
</Tooltip.Trigger>
<Tooltip.Portal>
<Tooltip.Content className="bg-gray-900 text-white px-2 py-1 rounded text-sm" sideOffset={5}>
Folder Settings
<Tooltip.Arrow className="fill-gray-900" />
</Tooltip.Content>
</Tooltip.Portal>
</Tooltip.Root>
</Tooltip.Provider>
</div>

<Pager
Expand Down
40 changes: 40 additions & 0 deletions src/renderer/src/components/RemoveFolderConfirmDialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import React from 'react';

type Props = {
isOpen: boolean;
onRequestClose: () => void;
onConfirm: () => void;
title: string;
message: string;
};

const RemoveFolderConfirmDialog: React.FC<Props> = ({ isOpen, onRequestClose, onConfirm, title, message }) => {
if (!isOpen) {
return null;
}

return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg p-6 max-w-md w-full mx-4">
<h2 className="text-xl font-bold mb-4">{title}</h2>
<p className="text-gray-600 mb-6">{message}</p>
<div className="flex justify-end space-x-4">
<button onClick={onRequestClose} className="px-4 py-2 text-gray-600 hover:bg-gray-100 rounded">
Cancel
</button>
<button
onClick={() => {
onConfirm();
onRequestClose();
}}
className="px-4 py-2 bg-red-600 text-white rounded hover:bg-red-700"
>
Remove
</button>
</div>
</div>
</div>
);
};

export default RemoveFolderConfirmDialog;
Loading

0 comments on commit d339ac7

Please sign in to comment.