feat(import-progress): track and expose Immich import progress
This change introduces a cross-language import progress API for Immich imports and exposes a new query command to report live progress. A new ImportProgress model captures active state, totals, and bytes downloaded, allowing consuming code to display import status to users. - Adds ImportProgress data type and progress tracking during import - Exposes get_immich_import_progress command in Android and Rust - Updates permissions and schemas to include allow-get-immich-import-progress
This commit is contained in:
+12
-3
@@ -1,6 +1,6 @@
|
||||
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
|
||||
import { ArrowLeft, Check, ChevronRight, Cloud, CloudDownload, Crop, Home, Images, Languages, Link2Off, LockKeyhole, Plus, RotateCcw, Save, Server, Settings, Shuffle, Smartphone, Sparkles, Trash2 } from "lucide-react";
|
||||
import { connectImmich, deleteImages, disconnectImmich, getGallery, getImageIds, getImmichAlbums, getImmichAssets, getImmichConnection, getState, importImmichAssets, nextWallpaper, selectImages, setImageCrop, setIntervalMinutes, setSetting, type GalleryImage, type ImmichAlbum, type ImmichAsset, type ImmichConnection, type WallpaperState } from "./native";
|
||||
import { connectImmich, deleteImages, disconnectImmich, getGallery, getImageIds, getImmichAlbums, getImmichAssets, getImmichConnection, getImmichImportProgress, getState, importImmichAssets, nextWallpaper, selectImages, setImageCrop, setIntervalMinutes, setSetting, type GalleryImage, type ImmichAlbum, type ImmichAsset, type ImmichConnection, type ImmichImportProgress, type WallpaperState } from "./native";
|
||||
import { initialLanguage, languageNames, languages, translations, type Language } from "./i18n-local";
|
||||
import { immichTranslations } from "./immich-i18n";
|
||||
|
||||
@@ -42,6 +42,7 @@ export default function App() {
|
||||
const [immichPage, setImmichPage] = useState(1);
|
||||
const [immichHasMore, setImmichHasMore] = useState(false);
|
||||
const [immichLoading, setImmichLoading] = useState(false);
|
||||
const [immichImportProgress, setImmichImportProgress] = useState<ImmichImportProgress | null>(null);
|
||||
const phonePreviewRef = useRef<HTMLDivElement>(null);
|
||||
const phoneImageRef = useRef<HTMLImageElement>(null);
|
||||
const previewPointers = useRef(new Map<number, { x: number; y: number }>());
|
||||
@@ -228,14 +229,22 @@ export default function App() {
|
||||
async function importFromImmich() {
|
||||
if (!immichSelected.size) return;
|
||||
setBusy(true);
|
||||
setImmichImportProgress({ active: true, completed: 0, total: immichSelected.size, bytesDownloaded: 0, bytesTotal: 0 });
|
||||
setNotice(it.importing);
|
||||
const poll = window.setInterval(() => {
|
||||
void getImmichImportProgress().then(setImmichImportProgress).catch(() => undefined);
|
||||
}, 250);
|
||||
try {
|
||||
setState(await importImmichAssets([...immichSelected]));
|
||||
setImmichSelected(new Set());
|
||||
setNotice(it.importSuccess);
|
||||
setTab("home");
|
||||
} catch (error) { setNotice(String(error).replace(/^Error:\s*/, "") || it.immichLoadFailed); }
|
||||
finally { setBusy(false); }
|
||||
finally {
|
||||
window.clearInterval(poll);
|
||||
setBusy(false);
|
||||
setImmichImportProgress(null);
|
||||
}
|
||||
}
|
||||
|
||||
async function saveCrop() {
|
||||
@@ -389,7 +398,7 @@ export default function App() {
|
||||
{immichAssets.length ? <div className="gallery-grid immich-grid">{immichAssets.map((asset, index) => <article className={immichSelected.has(asset.id) ? "chosen" : ""} key={asset.id}><button className="gallery-image-button" aria-label={`${immichSelected.has(asset.id) ? t.deselectImage : t.selectImage} ${index + 1}`} aria-pressed={immichSelected.has(asset.id)} onClick={() => toggleImmichAsset(asset.id)}>{asset.thumbnailUrl ? <img src={asset.thumbnailUrl} alt={asset.fileName} /> : <span className="immich-placeholder"><Images /></span>}</button><span className="selection-check"><Check /></span></article>)}</div> : !immichLoading && <div className="gallery-empty"><Cloud /><h2>{it.noImmichPhotos}</h2></div>}
|
||||
{immichLoading && <p className="gallery-status">{t.galleryLoading}</p>}
|
||||
{!immichLoading && immichHasMore && <button className="load-more" onClick={() => loadImmichPhotos(immichAlbumId, immichPage + 1, true)}>{t.loadMore}</button>}
|
||||
{!!immichSelected.size && <button className="primary immich-import" onClick={importFromImmich} disabled={busy}><CloudDownload /> {busy ? it.importing : `${it.importSelected} (${immichSelected.size})`}</button>}
|
||||
{!!immichSelected.size && (busy && immichImportProgress ? <div className="immich-import-progress" role="status" aria-live="polite"><div><span>{it.importing}</span><strong>{immichImportProgress.completed} / {immichImportProgress.total}</strong></div><progress max={immichImportProgress.bytesTotal || immichImportProgress.total || 1} value={immichImportProgress.bytesTotal ? immichImportProgress.bytesDownloaded : immichImportProgress.completed} />{immichImportProgress.bytesTotal > 0 && <div><span>{Math.round(immichImportProgress.bytesDownloaded / immichImportProgress.bytesTotal * 100)}%</span><span>{(immichImportProgress.bytesDownloaded / 1024 / 1024).toFixed(1)} / {(immichImportProgress.bytesTotal / 1024 / 1024).toFixed(1)} MB</span></div>}</div> : <button className="primary immich-import" onClick={importFromImmich} disabled={busy}><CloudDownload /> {`${it.importSelected} (${immichSelected.size})`}</button>)}
|
||||
</section> : tab === "gallery" ?
|
||||
<section className="gallery-page" aria-label={t.myImages}>
|
||||
{!!galleryTotal && <div className="selection-toolbar"><span>{selectedIds.size ? `${selectedIds.size} ${t.selected}` : t.selectImagesToDelete}</span><button onClick={selectAll} disabled={busy}>{selectedIds.size === galleryTotal ? t.clearSelection : t.selectAll}</button></div>}
|
||||
|
||||
Reference in New Issue
Block a user