Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,8 @@ SECURE_COOKIE=false # Set to true to enable secure cookies

# Optional
GOOGLE_MAPS_API_KEY="" # Google Maps API Key for heatmap
GEMINI_API_KEY="" # Gemini API Key for parsing search query in "Find"
GEMINI_API_KEY="" # Gemini API Key for parsing search query in "Find"

# Immich Share Link
IMMICH_SHARE_LINK_KEY="" # Share link key for Immich
POWER_TOOLS_ENDPOINT_URL="" # URL of the Power Tools endpoint (Used for share links)
23 changes: 21 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,21 @@ A unofficial immich client to provide better tools to organize and manage your i

[![Immich Power Tools](./screenshots/screenshot-1.png)](https://www.loom.com/embed/13aa90d8ab2e4acab0993bdc8703a750?sid=71498690-b745-473f-b239-a7bdbe6efc21)

### 🎒Features
- **Manage people data in bulk 👫**: Options to update people data in bulk, and with advance filters
- **People Merge Suggestion 🎭**: Option to bulk merge people with suggested faces based on similarity.
- **Update Missing Locations 📍**: Find assets in your library those are without location and update them with the location of the asset.
- **Potential Albums 🖼️**: Find albums that are potential to be created based on the assets and people in your library.
- **Analytics 📈**: Get analytics on your library like assets over time, exif data, etc.
- **Smart Search 🔎**: Search your library with natural language, supports queries like "show me all my photos from 2024 of <person name>"
- **Bulk Date Offset 📅**: Offset the date of selected assets by a given amount of time. Majorly used to fix the date of assets that are out of sync with the actual date.

### Support me 🙏

If you find this tool useful, please consider supporting me by buying me a coffee.

[![Buy me a coffee](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/varunraj)

## 💭 Back story

Recently I've migrated my entire Google photos library to Immich, I was able to successfully migrate all my assets along with its albums to immich. But there were few things like people match that was lacking. I loved Immich UI on the whole but for organizing content I felt its quite restricted and I had to do a lot of things in bulk instead of opening each asset and doing it. Hence I built this tool (continuing to itereate) to make my life and any other Immich user's life easier.
Expand Down Expand Up @@ -51,7 +66,11 @@ Refer here for obtaining Immich API Key: https://immich.app/docs/features/comman
If you're using portainer, run the docker using `docker run` and add the power tools to the same network as immich.

```bash
# Run the power tools from docker
docker run -d --name immich_power_tools -p 8001:3000 --env-file .env ghcr.io/varun-raj/immich-power-tools:latest

# Add Power tools to the same network as immich
docker network connect immich_default immich_power_tools
```


Expand Down Expand Up @@ -90,7 +109,8 @@ bun run dev
- [x] Manage People
- [x] Smart Merge
- [x] Manage Albums
- [ ] Bulk Delete
- [x] Bulk Delete
- [x] Bulk Share
- [ ] Bulk Edit
- [ ] Filters
- [x] Potential Albums
Expand Down Expand Up @@ -127,7 +147,6 @@ Google Gemini 1.5 Flash model is used for parsing your search query in "Find" pa

> Code where Gemini is used: [src/helpers/gemini.helper.ts](./src/helpers/gemini.helper.ts)


## Contributing

Feel free to contribute to this project, I'm open to any suggestions and improvements. Please read the [CONTRIBUTING.md](./CONTRIBUTING.md) for more details.
Binary file modified bun.lockb
Binary file not shown.
3 changes: 1 addition & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -48,16 +48,15 @@
"cookie": "^0.6.0",
"date-fns": "^3.6.0",
"drizzle-orm": "^0.33.0",
"jsonwebtoken": "^9.0.2",
"leaflet": "^1.9.4",
"leaflet-defaulticon-compatibility": "0.1.2",
"lucide-react": "^0.428.0",
"next": "14.2.5",
"next-themes": "^0.3.0",
"pg": "^8.12.0",
"qs": "^6.13.0",
"rc-mentions": "^2.18.0",
"react": "^18",
"react-calendar-heatmap": "^1.9.0",
"react-day-picker": "9.0.8",
"react-dom": "^18",
"react-grid-gallery": "^1.0.1",
Expand Down
86 changes: 0 additions & 86 deletions src/components/albums/AlbumCreateDialog.tsx

This file was deleted.

79 changes: 72 additions & 7 deletions src/components/albums/AlbumSelectorDialog.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useEffect, useMemo, useState } from "react";
import React, { useCallback, useEffect, useMemo, useState } from "react";
import {
Dialog,
DialogContent,
Expand All @@ -9,18 +9,28 @@ import {
} from "../ui/dialog";
import { Button } from "../ui/button";
import { listAlbums } from "@/handlers/api/album.handler";
import { IAlbum } from "@/types/album";
import { IAlbum, IAlbumCreate } from "@/types/album";
import { Input } from "../ui/input";
import { usePotentialAlbumContext } from "@/contexts/PotentialAlbumContext";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs";
import toast from "react-hot-toast";
import { Label } from "../ui/label";

interface IProps {
onSelected: (album: IAlbum) => Promise<void>;
onCreated?: (album: IAlbum) => Promise<void>;
onSubmit?: (data: IAlbumCreate) => Promise<any>;
}
export default function AlbumSelectorDialog({ onSelected }: IProps) {
export default function AlbumSelectorDialog({ onSelected, onCreated, onSubmit }: IProps) {
const [open, setOpen] = useState(false);
const [albums, setAlbums] = useState<IAlbum[]>([]);
const [loading, setLoading] = useState(false);
const [errorMessage, setErrorMessage] = useState<string | null>(null);
const [search, setSearch] = useState("");
const { selectedIds, assets } = usePotentialAlbumContext();
const [formData, setFormData] = useState({
albumName: "",
});

const fetchData = () => {
return listAlbums()
Expand All @@ -42,11 +52,31 @@ export default function AlbumSelectorDialog({ onSelected }: IProps) {
});
}


const handleSubmit = useCallback((e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
if (!onSubmit) return;
setLoading(true);
onSubmit(formData)
.then(() => {
toast.success("Album created successfully");
setFormData({ albumName: "" });
setOpen(false);
})
.catch((e) => {
toast.error("Failed to create album");
})
.finally(() => {
setLoading(false);
});
}, [onSubmit, formData]);


useEffect(() => {
if (open && !albums.length) fetchData();
}, [open]);

const renderContent = () => {
const renderContent = useCallback(() => {
if (loading) return <p>Loading...</p>;
if (errorMessage) return <p>{errorMessage}</p>;
return (
Expand All @@ -73,12 +103,36 @@ export default function AlbumSelectorDialog({ onSelected }: IProps) {
</div>
</div>
);
};
}, [albums, filteredAlbums, handleSelect, loading, errorMessage]);

const renderCreateContent = useCallback(() => {
return (
<form onSubmit={handleSubmit} className="flex flex-col gap-4 max-h-[500px] min-h-[500px]">
<p className="text-sm text-zinc-500 dark:text-zinc-400">
Create a new album and add the selected assets to it
</p>
<div className="flex flex-col gap-2">
<Label>Album Name</Label>
<Input
placeholder="Album Name"
onChange={(e) => {
setFormData({ ...formData, albumName: e.target.value });
}}
/>
</div>
<div className="self-end">
<Button disabled={loading} type="submit">
{loading ? "Creating Album" : "Create Album"}
</Button>
</div>
</form>
)
}, [loading, formData, handleSubmit]);

return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button size={"sm"}>Select Album</Button>
<Button size={"sm"} disabled={!selectedIds.length}>Add to Album</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
Expand All @@ -87,7 +141,18 @@ export default function AlbumSelectorDialog({ onSelected }: IProps) {
Select the albums you want to add the selected assets to
</DialogDescription>
</DialogHeader>
{renderContent()}
<Tabs defaultValue="albums" className="w-full">
<TabsList className="w-full">
<TabsTrigger className="w-full" value="albums">Albums</TabsTrigger>
<TabsTrigger className="w-full" value="create">Create</TabsTrigger>
</TabsList>
<TabsContent value="albums">
{renderContent()}
</TabsContent>
<TabsContent value="create">
{renderCreateContent()}
</TabsContent>
</Tabs>
</DialogContent>
</Dialog>
);
Expand Down
Loading
Loading