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
24 changes: 24 additions & 0 deletions REPORT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# V2 UI migration report

`/v2/*` は `DESIGN.md` と Design Lab を基準にした移行用フロントエンドです。既存APIで成立する検索、ソース操作、メディア管理、AI操作、設定、インポート、データ転送は実データへ接続し、未対応機能は推測データを表示せず無効化しています。

## Backend work required

| 項目 | 現状の原因 | 必要な実装 | V2での扱い |
| --- | --- | --- | --- |
| Jobs一覧・履歴・詳細 | jobs contractはイベント購読のみで、一覧取得APIがない | Job DTO、list/detail query | 専用画面の骨格と未対応状態を表示 |
| JobのRetry / Cancel | 対応するuse caseとmutationがない | retry/cancel command、状態遷移、権限制御 | 操作をdisabled |
| Sourceの件数・同期中状態 | `SafeMediaSource`にmedia countとsync lifecycleがない | source summary DTO、集計、sync status event | sidebarに「件数未取得」 |
| Managerの利用件数 | Project / IP / Character一覧にmedia count等がない | 集計queryまたはsummary DTO | 取得可能な実データだけ表示 |
| Export / RestoreのJobs追跡 | 現行はHTTP download/uploadを直接実行する | job type、progress、artifact、失敗履歴 | 転送自体は実行し、Jobs連携は表示しない |
| AI接続状態・latency | health check contractがない | typed health endpointとtimeout/error定義 | Settingsで「未確認」、確認操作をdisabled |
| リロード後の前後メディア移動 | 詳細routeだけでは元のfilter/sort/cursorを復元できない | navigation context永続化、またはneighbor query | 前後ボタンをdisabled |

## Frontend follow-up

| 項目 | 理由 | V2での扱い |
| --- | --- | --- |
| Collectionのlist表示 | 4:3 gridを先行し、listの情報設計とvirtual row実装が未着手 | toggleをdisabled |
| Tauri route adapter | TauriはWebと別のhash routerを持つ | 今回はWebの`/v2/*`のみ。共有screenを使う薄いroute追加が必要 |

バックエンド追加時は、無効状態を消すだけでなく、loading / error / offline / retryとリアルタイム更新まで同じ画面内で接続します。
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ import {
import { orpc } from "~/infrastructure/api-clients/orpc-client";
import { fetchMediaSources } from "~/infrastructure/api-clients/sources-api";

export function PendingDownloadsIndicator() {
const props: PendingDownloadsIndicatorProps = {
export function PendingDownloadsIndicator(
displayProps: { compact?: boolean; variant?: "default" | "v2" } = {},
) {
const indicatorProps: PendingDownloadsIndicatorProps = {
countPending: async () => {
const result = await orpc.imports.countPending();
return result.count;
Expand Down Expand Up @@ -44,5 +46,7 @@ export function PendingDownloadsIndicator() {
},
};

return <SharedPendingDownloadsIndicator {...props} />;
return (
<SharedPendingDownloadsIndicator {...indicatorProps} {...displayProps} />
);
}
87 changes: 68 additions & 19 deletions apps/server/src/components/media/media-grid-item.tsx
Original file line number Diff line number Diff line change
@@ -1,44 +1,93 @@
import type { Media } from "@solid-imager/core/domain/media/schemas";
import { MediaGridItem as SharedMediaGridItem } from "@solid-imager/ui/media-grid-item";
import { Link } from "@tanstack/solid-router";
import {
type MediaGridLinkProps,
MediaGridItem as SharedMediaGridItem,
} from "@solid-imager/ui/media-grid-item";
import { Link, useLocation } from "@tanstack/solid-router";
import { Show } from "solid-js";
import { ThumbnailImage } from "./thumbnail-image";

type MediaGridItemProps = {
linkPrefix?: string;
routeVersion?: "default" | "v2";
media: Media;
onContextMenu?: (event: MouseEvent) => void;
priority?: boolean;
sourceRootPath?: string;
isBulkSelectMode?: boolean;
isSelected?: boolean;
onToggleSelect?: () => void;
onPreviewSelect?: () => void;
};

export function MediaGridItem(props: MediaGridItemProps) {
const location = useLocation();
const detailLink = (linkProps: MediaGridLinkProps) =>
props.routeVersion === "v2" ? (
<Link
class={linkProps.class}
data-media-id={linkProps["data-media-id"]}
onClick={(event: MouseEvent) => {
if (
props.onPreviewSelect &&
window.matchMedia("(min-width: 1536px)").matches
) {
event.preventDefault();
props.onPreviewSelect();
return;
}
if (
event.button === 0 &&
!event.metaKey &&
!event.ctrlKey &&
!event.shiftKey &&
!event.altKey
) {
sessionStorage.setItem("v2:media-return", location().href);
}
}}
onContextMenu={linkProps.onContextMenu}
params={{
mediaId: props.media.id,
mediaSourceId: props.media.mediaSourceId,
}}
to="/v2/sources/$mediaSourceId/$mediaId"
>
{linkProps.children}
</Link>
) : (
<Link
class={linkProps.class}
data-media-id={linkProps["data-media-id"]}
onClick={(event: MouseEvent) => {
if (
props.onPreviewSelect &&
window.matchMedia("(min-width: 1536px)").matches
) {
event.preventDefault();
props.onPreviewSelect();
}
}}
onContextMenu={linkProps.onContextMenu}
params={{
mediaId: props.media.id,
mediaSourceId: props.media.mediaSourceId,
}}
to="/sources/$mediaSourceId/$mediaId"
>
{linkProps.children}
</Link>
);

return (
<SharedMediaGridItem
variant={props.routeVersion === "v2" ? "v2" : "default"}
isBulkSelectMode={props.isBulkSelectMode}
isSelected={props.isSelected}
linkComponent={(linkProps) => (
<Show
fallback={
<Link
class={linkProps.class}
data-media-id={linkProps["data-media-id"]}
onContextMenu={linkProps.onContextMenu}
params={{
mediaId: props.media.id,
mediaSourceId: props.media.mediaSourceId,
}}
to="/sources/$mediaSourceId/$mediaId"
>
{linkProps.children}
</Link>
}
when={props.isBulkSelectMode}
>
<Show fallback={detailLink(linkProps)} when={props.isBulkSelectMode}>
<button
aria-pressed={props.isSelected}
type="button"
class={linkProps.class}
data-media-id={linkProps["data-media-id"]}
Expand Down
Loading