diff --git a/src/app/index.tsx b/src/app/index.tsx index 6297ed8..b99165e 100644 --- a/src/app/index.tsx +++ b/src/app/index.tsx @@ -5,12 +5,11 @@ import { useSafeAreaInsets } from "react-native-safe-area-context"; import { CoverGrid } from "@/components/cover/CoverGrid"; import { CreatePanel } from "@/components/cover/CreatePanel"; import { PromptModal } from "@/components/ui/PromptModal"; -import { pickBackup } from "@/lib/backup"; import { notebookDisplayName } from "@/lib/model"; import { colors, fonts } from "@/lib/theme"; import type { NotebookType } from "@/lib/types"; +import { useImportBackup } from "@/state/backup"; import { useNotebooks } from "@/state/notebooks"; -import { useUi } from "@/state/ui"; export default function CoverScreen() { const router = useRouter(); @@ -22,8 +21,6 @@ export default function CoverScreen() { const deleteNotebook = useNotebooks((s) => s.deleteNotebook); const renameNotebook = useNotebooks((s) => s.renameNotebook); const recheckStorage = useNotebooks((s) => s.recheckStorage); - const importState = useNotebooks((s) => s.importState); - const showToast = useUi((s) => s.showToast); const [creating, setCreating] = useState(false); const [renameId, setRenameId] = useState(null); @@ -43,29 +40,7 @@ export default function CoverScreen() { ]); }; - const onImport = async () => { - const res = await pickBackup(); - if (res.status === "canceled") return; - if (res.status === "invalid") { - showToast("⚠️ 読み込みに失敗しました…ファイルを確認してね"); - return; - } - Alert.alert( - "バックアップを読み込む", - "現在のすべての手帳を、このバックアップの内容で上書きします。よろしいですか?", - [ - { text: "やめる", style: "cancel" }, - { - text: "上書き", - style: "destructive", - onPress: () => { - importState(res.state); - showToast("バックアップを読み込みました🌸"); - }, - }, - ], - ); - }; + const onImport = useImportBackup(); const renameTarget = notebooks.find((n) => n.id === renameId); diff --git a/src/app/notebook/[id].tsx b/src/app/notebook/[id].tsx index 54a879c..4dce7a2 100644 --- a/src/app/notebook/[id].tsx +++ b/src/app/notebook/[id].tsx @@ -12,11 +12,12 @@ import { StickerTray } from "@/components/board/StickerTray"; import { FlowchartSection } from "@/components/flowchart/FlowchartSection"; import { FreeformCanvas } from "@/components/freeform/FreeformCanvas"; import { NotebookSection } from "@/components/notebook/NotebookSection"; -import { exportBackup, pickBackup } from "@/lib/backup"; +import { exportBackup } from "@/lib/backup"; import { captureAndShare } from "@/lib/capture"; import { pickPhotoAsDataUrl } from "@/lib/files"; import { colors, fonts, TRAY_BASE_HEIGHT } from "@/lib/theme"; import type { StickerType } from "@/lib/types"; +import { useImportBackup } from "@/state/backup"; import { selectCurrentNotebook, selectCurrentPage, useNotebooks } from "@/state/notebooks"; import { useUi } from "@/state/ui"; @@ -46,7 +47,6 @@ export default function NotebookEditor() { const addShape = useNotebooks((s) => s.addShape); const addPhotoTo = useNotebooks((s) => s.addPhotoTo); const undo = useNotebooks((s) => s.undo); - const importState = useNotebooks((s) => s.importState); const connectMode = useUi((s) => s.connectMode); const select = useUi((s) => s.select); @@ -65,6 +65,8 @@ export default function NotebookEditor() { useUi.getState().resetBoardUi(); }, [page?.id]); + const onImport = useImportBackup(() => router.replace("/")); + const goBack = () => { if (router.canGoBack()) router.back(); else router.replace("/"); @@ -136,30 +138,6 @@ export default function NotebookEditor() { showToast("書き出しに失敗しました…"); } }; - const onImport = async () => { - const res = await pickBackup(); - if (res.status === "canceled") return; - if (res.status === "invalid") { - showToast("⚠️ 読み込みに失敗しました…ファイルを確認してね"); - return; - } - Alert.alert( - "バックアップを読み込む", - "現在のすべての手帳を、このバックアップの内容で上書きします。よろしいですか?", - [ - { text: "やめる", style: "cancel" }, - { - text: "上書き", - style: "destructive", - onPress: () => { - importState(res.state); - showToast("バックアップを読み込みました🌸"); - router.replace("/"); - }, - }, - ], - ); - }; const onReset = () => { Alert.alert("ページを消去", "このページの中身を全部消します。よろしいですか?", [ { text: "やめる", style: "cancel" }, diff --git a/src/lib/model.ts b/src/lib/model.ts index c1be45b..1f4fce9 100644 --- a/src/lib/model.ts +++ b/src/lib/model.ts @@ -10,6 +10,7 @@ import type { Page, PageType, Photo, + Placement, RuleStyle, Shape, Step, @@ -215,14 +216,21 @@ function normalizeStep(raw: unknown): Step { return { id: str(r.id) || newId(), type: "step", text: str(r.text), done: bool(r.done) }; } -function normalizeSticker(raw: unknown): Sticker { - const r = rec(raw); +// ステッカー・シェイプ・写真に共通する配置情報(id と座標・回転)を正規化する。 +function normalizePlacement(r: Record): Placement { return { id: str(r.id) || newId(), - type: oneOf(r.type, STICKER_TYPES, "star"), x: clampNum(r.x, 0, 0, MAX_POSITION), y: clampNum(r.y, 0, 0, MAX_POSITION), rot: num(r.rot), + }; +} + +function normalizeSticker(raw: unknown): Sticker { + const r = rec(raw); + return { + ...normalizePlacement(r), + type: oneOf(r.type, STICKER_TYPES, "star"), size: clampNum(r.size, STICKER_DEFAULT_SIZE, STICKER_MIN_SIZE, STICKER_MAX_SIZE), }; } @@ -230,12 +238,9 @@ function normalizeSticker(raw: unknown): Sticker { function normalizeShape(raw: unknown): Shape { const r = rec(raw); return { - id: str(r.id) || newId(), + ...normalizePlacement(r), text: str(r.text), - x: clampNum(r.x, 0, 0, MAX_POSITION), - y: clampNum(r.y, 0, 0, MAX_POSITION), w: clampNum(r.w, SHAPE_DEFAULT_WIDTH, SHAPE_MIN_WIDTH, SHAPE_MAX_WIDTH), - rot: num(r.rot), }; } @@ -244,11 +249,8 @@ function normalizePhoto(raw: unknown): Photo | null { const dataUrl = str(r.dataUrl) || str(r.image); if (!/^data:image\//i.test(dataUrl)) return null; return { - id: str(r.id) || newId(), - x: clampNum(r.x, 0, 0, MAX_POSITION), - y: clampNum(r.y, 0, 0, MAX_POSITION), + ...normalizePlacement(r), w: clampNum(r.w, PHOTO_DEFAULT_WIDTH, PHOTO_MIN_WIDTH, PHOTO_MAX_WIDTH), - rot: num(r.rot), dataUrl, }; } diff --git a/src/lib/types.ts b/src/lib/types.ts index 629d58e..cb30d70 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -31,30 +31,26 @@ export type Step = NormalStep | IfStep; export type BranchKey = "yes" | "no"; -export interface Sticker { +// ボード上に配置される装飾(ステッカー・テキスト・写真)に共通する配置情報。 +export interface Placement { id: string; - type: StickerType; x: number; y: number; rot: number; +} + +export interface Sticker extends Placement { + type: StickerType; size: number; } -export interface Shape { - id: string; +export interface Shape extends Placement { text: string; - x: number; - y: number; w: number; - rot: number; } -export interface Photo { - id: string; - x: number; - y: number; +export interface Photo extends Placement { w: number; - rot: number; dataUrl: string; } diff --git a/src/state/backup.ts b/src/state/backup.ts new file mode 100644 index 0000000..61232af --- /dev/null +++ b/src/state/backup.ts @@ -0,0 +1,37 @@ +import { Alert } from "react-native"; +import { pickBackup } from "@/lib/backup"; +import { useNotebooks } from "@/state/notebooks"; +import { useUi } from "@/state/ui"; + +// バックアップファイルを選ばせ、確認ダイアログを経て文書全体を上書き取り込みする共通フック。 +// 一覧画面・編集画面のどちらからも使う。取り込み完了後に onImported があれば呼ぶ +// (編集画面では一覧へ戻るために使う)。 +export function useImportBackup(onImported?: () => void): () => Promise { + const importState = useNotebooks((s) => s.importState); + const showToast = useUi((s) => s.showToast); + + return async () => { + const res = await pickBackup(); + if (res.status === "canceled") return; + if (res.status === "invalid") { + showToast("⚠️ 読み込みに失敗しました…ファイルを確認してね"); + return; + } + Alert.alert( + "バックアップを読み込む", + "現在のすべての手帳を、このバックアップの内容で上書きします。よろしいですか?", + [ + { text: "やめる", style: "cancel" }, + { + text: "上書き", + style: "destructive", + onPress: () => { + importState(res.state); + showToast("バックアップを読み込みました🌸"); + onImported?.(); + }, + }, + ], + ); + }; +}