From edbc953b2e2dd69750ee3ca46d81ad4897c76ec2 Mon Sep 17 00:00:00 2001 From: kumagallium Date: Thu, 3 Sep 2026 22:27:12 +0900 Subject: [PATCH 1/3] [fix] Show derived folders in the material peek opened from a note The material side peek that opens from inside the editor showed only hand-set folders, while the gallery showed those plus the folders of the notes a material is used in. Same asset, two answers. The lookup is now threaded through to it, so both read the same. Static wiring only: the real vault has no note citing a document material, so this path was not exercised end to end. The derivation itself is unit-tested and was verified in the gallery. Co-Authored-By: Claude Opus 5 --- src/note-app.tsx | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/note-app.tsx b/src/note-app.tsx index 607605242..4b11e3b32 100644 --- a/src/note-app.tsx +++ b/src/note-app.tsx @@ -199,7 +199,7 @@ import { DocumentProvenancePanel } from "./features/document-provenance"; import { cn } from "./lib/utils"; import { NoteListView, TrashView, buildKnowledgeMap, findIncomingReferences, readIndexFile, type GraphiumIndex, type NoteIndexEntry } from "./features/navigation"; import { UNFILED_PATH, buildFolderTree, collectFolderSource, expandFolderToContextValues } from "./features/note-context/folder-tree-model"; -import { buildNoteFolderLookup } from "./features/asset-browser/asset-folders"; +import { buildNoteFolderLookup, type NoteFolderLookup } from "./features/asset-browser/asset-folders"; import { addFolderDefinition, ensureFolderDefinitions, removeFolderDefinition, renameFolderDefinition } from "./features/note-context/folder-store"; import { FolderMenu } from "./features/note-context/FolderMenu"; import { computeFolderDrop } from "./features/note-context/folder-drop"; @@ -732,6 +732,12 @@ function NoteHeaderMenu({ type NoteEditorProps = { fileId: string | null; initialDoc: GraphiumDocument | null; + /** + * ノート id → そのノートのフォルダ。エディタ内から開く素材サイドピークで、 + * 素材が「使われているノートのフォルダ」に属して見えるようにするために渡す + * (素材ギャラリー側と同じ導出を使い、見え方を揃える)。 + */ + noteFolderLookup?: NoteFolderLookup; onSave: (doc: GraphiumDocument) => void; onDeriveNote: (title: string, sourceBlockId: string) => void; /** `@` メニューの「新規ノートを作成」用。空ノートを作って ID を返す(ナビゲーションしない) */ @@ -1053,6 +1059,7 @@ async function applyAtomReinforcement(opts: { function NoteEditorInner({ fileId, initialDoc, + noteFolderLookup, onSave, onDeriveNote, onCreateLinkedNote, @@ -5389,6 +5396,7 @@ function NoteEditorInner({ setMaterialSidePeekEntry(null)} mediaIndex={mediaIndex ?? null} onRegisterAsset={materialPeekUrlUnregistered ? handleRegisterUrlFromPeek : undefined} @@ -5411,6 +5419,7 @@ function NoteEditorInner({ {materialSidePeekEntry && !isDesktop && ( setMaterialSidePeekEntry(null)} mediaIndex={mediaIndex ?? null} onRegisterAsset={materialPeekUrlUnregistered ? handleRegisterUrlFromPeek : undefined} @@ -9679,6 +9688,7 @@ export function NoteApp() { provLabelsEnabled={provLabelsEnabled} fileId={fm.activeFileId?.replace("wiki:", "").replace("skill:", "") ?? fm.activeFileId} initialDoc={fm.activeDoc} + noteFolderLookup={noteFolderLookup} onDeleteContextEverywhere={handleDeleteContextEverywhere} contextDrawerSlot={ fm.activeDoc?.source === "ai" && fm.activeDoc?.wikiMeta From 4a3fc8ce6f967da2b0c2a11ce1ebcd30b5a67104 Mon Sep 17 00:00:00 2001 From: kumagallium Date: Thu, 3 Sep 2026 23:01:55 +0900 Subject: [PATCH 2/3] [feat] Send from your phone straight into a folder MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Set a folder in the phone settings and what you send arrives already filed, instead of landing unsorted and waiting to be sorted on the desktop. The setting is remembered until you change it — a phone is for getting something down quickly, and picking a folder every time would spend exactly the speed it exists for. Photos, video and audio travel as raw files with no channel for metadata, so the folder rides in the filename and the desktop lifts it back out on import; URLs already travel as JSON, so it goes in the payload. A sidecar .meta.json was the alternative and was not taken: it puts a second file in the inbox, which then has to be filtered out of the listing, paired on import, and cleaned up — and leaves half a pair behind when anything goes wrong. Memos are left out for now: they land in the capture store, which has no folder of its own yet, unlike materials. Verified in the running app at phone width: the field saves, and the name round-trip is unit-tested including subfolders, broken encodings, and ordinary filenames a person dropped in by hand. Co-Authored-By: Claude Opus 5 --- manual/ja/mobile.md | 2 +- manual/mobile.md | 2 +- .../mobile-capture/MobileSettingsSheet.tsx | 28 +++++++ .../mobile-capture/inbox/capture-file.ts | 8 +- .../mobile-capture/inbox/push/config.ts | 26 ++++++ .../inbox/push/folder-name.test.ts | 82 +++++++++++++++++++ .../mobile-capture/inbox/push/naming.ts | 48 ++++++++++- .../mobile-capture/inbox/push/queue.ts | 5 ++ .../mobile-capture/inbox/transport.ts | 6 ++ src/features/mobile-capture/inbox/types.ts | 6 ++ src/hooks/use-file-manager.ts | 5 ++ src/i18n/en.ts | 3 + src/i18n/ja.ts | 3 + src/note-app.tsx | 4 + 14 files changed, 224 insertions(+), 4 deletions(-) create mode 100644 src/features/mobile-capture/inbox/push/folder-name.test.ts diff --git a/manual/ja/mobile.md b/manual/ja/mobile.md index ace538131..af0928a1e 100644 --- a/manual/ja/mobile.md +++ b/manual/ja/mobile.md @@ -88,7 +88,7 @@ Graphium はインストール可能な Web アプリ(PWA)です。スマホ 2. **記録して送る。** キューの **送信** をタップすると、自分の Drive の `Graphium/Inbox` フォルダに流れ込みます。転送は TLS を使い、ファイルは*自分が管理する*ストレージに普通のファイルとして置かれます。 3. **フォルダをデスクトップに同期する。** クラウドのクライアント(たとえば Google ドライブ デスクトップ版)を動かし、そのフォルダをデスクトップのディスク上に存在させます。 4. **Graphium にその場所を教える。** デスクトップの[設定 → ストレージ](/ja/settings)の **モバイル送信** セクションに **受信フォルダ** のピッカーがあります。クラウドストレージが同期しているフォルダを選ぶと、Graphium はその中の `Inbox` サブフォルダを読みます。同じセクションの **接続はスマホ側で行います** には、そもそもスマホで Graphium を開くための QR コード(**URL をコピー** 付き)があります。 -5. **取り込む。** ファイルが届くと、デスクトップのサイドバーの **モバイル** に未処理件数が表示されます。開いて 1 件ずつプレビューし、**全部取り込み** か **選択したものを取り込み** を実行します。写真・動画・音声メモは[素材](/ja/materials-and-citations)になり、書いた記録は通常のメモや URL のブックマークになります。重複は自動でスキップされ、トーストが **取り込み・スキップ・失敗** の件数をまとめます。受信箱が空なら **新しいものはありません** と表示されるだけです。 +5. **取り込む。** ファイルが届くと、デスクトップのサイドバーの **モバイル** に未処理件数が表示されます。開いて 1 件ずつプレビューし、**全部取り込み** か **選択したものを取り込み** を実行します。写真・動画・音声メモは[素材](/ja/materials-and-citations)になり、書いた記録は通常のメモや URL のブックマークになります。スマホの設定で **送り先のフォルダ** を決めておくと、届いた素材はそのフォルダに入った状態になります(切り替えるまで覚えているので、送るたびに選ぶ必要はありません)。重複は自動でスキップされ、トーストが **取り込み・スキップ・失敗** の件数をまとめます。受信箱が空なら **新しいものはありません** と表示されるだけです。 既定では、取り込んだファイルは受信箱から削除されます。中身はすでにライブラリに入っているためです。控えを残したい場合は、**処理済みファイルを _imported/ に残す**(受信フォルダの設定、または設定 → ストレージ)をオンにしてください。 diff --git a/manual/mobile.md b/manual/mobile.md index d73121e93..c61a9da14 100644 --- a/manual/mobile.md +++ b/manual/mobile.md @@ -88,7 +88,7 @@ The pipeline, end to end: 2. **Capture and send.** Tap **Send** on the queue and it drains into the `Graphium/Inbox` folder of your own Drive. Transfers use TLS, and the files sit as ordinary files in storage *you* control. 3. **Sync the folder to your desktop.** Run your cloud client (for example Google Drive for desktop) so that folder exists on the desktop's disk. 4. **Point Graphium at it.** In the desktop's [Settings → Storage](/settings), the **Mobile upload** section has the **Inbox folder** picker — choose the folder your cloud storage syncs to, and Graphium reads the `Inbox` subfolder inside it. The same section shows the QR code under **Connect on your phone** (with **Copy URL**) that opens Graphium on the phone in the first place. -5. **Import.** When files arrive, a **Mobile** entry in the desktop sidebar shows a pending count. Open it to preview each item, then **Import all** or **Import selected**. Photos, videos, and voice notes become [materials](/materials-and-citations); written captures become regular memos and URL bookmarks. Duplicates are skipped automatically, a toast summarizes **Imported · skipped · failed**, and an empty inbox simply says **Nothing new**. +5. **Import.** When files arrive, a **Mobile** entry in the desktop sidebar shows a pending count. Open it to preview each item, then **Import all** or **Import selected**. Photos, videos, and voice notes become [materials](/materials-and-citations); written captures become regular memos and URL bookmarks. Set **Send into folder** in the phone settings and what arrives is already in that folder — it is remembered until you change it, so there is nothing to pick each time you send. Duplicates are skipped automatically, a toast summarizes **Imported · skipped · failed**, and an empty inbox simply says **Nothing new**. By default, imported files are deleted from the inbox — their content is already in your library. Check **Keep processed files in _imported/** (in the inbox's folder settings or in Settings → Storage) if you prefer an archive. diff --git a/src/features/mobile-capture/MobileSettingsSheet.tsx b/src/features/mobile-capture/MobileSettingsSheet.tsx index 81b49e096..1c67dc5c5 100644 --- a/src/features/mobile-capture/MobileSettingsSheet.tsx +++ b/src/features/mobile-capture/MobileSettingsSheet.tsx @@ -25,6 +25,7 @@ import { } from "lucide-react"; import { useLocale, type Locale } from "../../i18n"; import { getAppVersion } from "../../lib/updater"; +import { getInboxFolder, setInboxFolder } from "./inbox/push/config"; export type MobileSettingsSheetProps = { /** push モジュールのロードが済んだか。false の間はストレージ操作を無効化する。 */ @@ -61,6 +62,11 @@ export function MobileSettingsSheet({ const [version, setVersion] = useState(""); const [clientIdInput, setClientIdInput] = useState(clientIdOverride); const [clientIdSaved, setClientIdSaved] = useState(false); + // 送ったものを入れるフォルダ。localStorage に持つので、開いた時点の値を初期値にする + const [folderInput, setFolderInput] = useState(""); + useEffect(() => { + setFolderInput(getInboxFolder() ?? ""); + }, []); // バージョンは About タブと同じ経路(Tauri は実バージョン / PWA は package.json) useEffect(() => { @@ -173,6 +179,28 @@ export function MobileSettingsSheet({ )} + {/* 送り先のフォルダ。空なら未分類のまま届き、デスクトップで振り分ける。 + 毎回選ばせないのは、モバイルが「素早く放り込む」道具だから + (切り替えたいときだけここで変える)。 */} +
+ + { + setFolderInput(e.target.value); + setInboxFolder(e.target.value); + }} + placeholder={t("mobile.settings.folderPlaceholder")} + className="w-full text-sm px-2.5 py-1.5 rounded-md border border-border bg-background text-foreground placeholder:text-muted-foreground outline-none focus:border-primary transition-colors" + /> +

+ {t("mobile.settings.folderHint")} +

+
+ {/* 同梱 ID の無いビルドでは自前 client ID が必須(設定モーダルと同じ注記) */} {!hasBundledId && (

diff --git a/src/features/mobile-capture/inbox/capture-file.ts b/src/features/mobile-capture/inbox/capture-file.ts index e2b579f03..30add59b0 100644 --- a/src/features/mobile-capture/inbox/capture-file.ts +++ b/src/features/mobile-capture/inbox/capture-file.ts @@ -39,6 +39,11 @@ export type GraphiumUrlCapturePayload = { title?: string; description?: string; ogImage?: string; + /** + * 送信時に指定されたフォルダ。取り込み時に素材の noteContexts へ入る。 + * メディアは名前に埋め込むが(push/naming.ts)、URL は元から JSON なのでここに置く。 + */ + folder?: string; }; export type GraphiumCapturePayload = GraphiumMemoCapturePayload | GraphiumUrlCapturePayload; @@ -83,7 +88,7 @@ export function buildMemoCaptureFile(text: string, now: Date = new Date()): File /** URL 捕獲ファイルを作る(モバイルの [URL] → キュー)。メタはあるものだけ運ぶ。 */ export function buildUrlCaptureFile( - input: { url: string; title?: string; description?: string; ogImage?: string }, + input: { url: string; title?: string; description?: string; ogImage?: string; folder?: string }, now: Date = new Date(), ): File { const title = input.title?.trim(); @@ -96,6 +101,7 @@ export function buildUrlCaptureFile( ...(title ? { title } : {}), ...(description ? { description } : {}), ...(input.ogImage ? { ogImage: input.ogImage } : {}), + ...(input.folder?.trim() ? { folder: input.folder.trim() } : {}), }; return toCaptureFile("url", payload); } diff --git a/src/features/mobile-capture/inbox/push/config.ts b/src/features/mobile-capture/inbox/push/config.ts index f176b1853..c306427cf 100644 --- a/src/features/mobile-capture/inbox/push/config.ts +++ b/src/features/mobile-capture/inbox/push/config.ts @@ -31,6 +31,32 @@ const PROVIDER_KEY = "graphium-push-provider"; export const DEFAULT_GOOGLE_PUSH_CLIENT_ID: string = "743366655410-p5k3us8jof0ni4tintbkliq6dqhan13d.apps.googleusercontent.com"; +const INBOX_FOLDER_KEY = "graphium-mobile-inbox-folder"; + +/** + * 送ったものを入れるフォルダ(未設定なら null)。 + * + * 送信のたびに選ばせず、一度決めたら覚えておく — モバイルは「素早く放り込む」道具で、 + * 毎回の選択はその速さを損なう。切り替えたいときだけ設定から変える。 + */ +export function getInboxFolder(): string | null { + try { + const v = localStorage.getItem(INBOX_FOLDER_KEY); + return v && v.trim() ? v : null; + } catch { + return null; + } +} + +export function setInboxFolder(folder: string | null): void { + try { + if (!folder || !folder.trim()) localStorage.removeItem(INBOX_FOLDER_KEY); + else localStorage.setItem(INBOX_FOLDER_KEY, folder.trim()); + } catch { + // localStorage 不可の環境では黙って無視(他の設定と同じ非致命的挙動) + } +} + /** localStorage に保存された自前 client_id(未設定なら null)。 */ export function getGoogleClientIdOverride(): string | null { try { diff --git a/src/features/mobile-capture/inbox/push/folder-name.test.ts b/src/features/mobile-capture/inbox/push/folder-name.test.ts new file mode 100644 index 000000000..a24c2af21 --- /dev/null +++ b/src/features/mobile-capture/inbox/push/folder-name.test.ts @@ -0,0 +1,82 @@ +import { describe, it, expect } from "vitest"; +import { normalizeCaptureName, parseInboxFolder } from "./naming"; + +const when = new Date("2026-09-03T10:15:30"); + +describe("送信名へのフォルダ埋め込み", () => { + it("フォルダを指定すると名前に載る", () => { + const name = normalizeCaptureName({ + mime: "image/jpeg", + originalName: "IMG_0001.JPG", + when, + seq: 1, + folder: "材料X", + }); + expect(name.startsWith("graphium-20260903-101530-01~~")).toBe(true); + expect(name.endsWith(".jpg")).toBe(true); + }); + + it("指定が無ければ今までどおりの名前", () => { + const name = normalizeCaptureName({ mime: "image/jpeg", originalName: "a.jpg", when, seq: 1 }); + expect(name).toBe("graphium-20260903-101530-01.jpg"); + }); + + it("空白だけの指定は付けない", () => { + const name = normalizeCaptureName({ + mime: "image/jpeg", originalName: "a.jpg", when, seq: 1, folder: " ", + }); + expect(name).toBe("graphium-20260903-101530-01.jpg"); + }); + + it("メモ / URL の捕獲ファイルには付けない(中身の JSON が運ぶ)", () => { + const name = normalizeCaptureName({ + mime: "application/json", + originalName: "graphium-x-url.graphium.json", + when, + seq: 1, + folder: "材料X", + }); + expect(name).toBe("graphium-20260903-101530-01-url.graphium.json"); + }); +}); + +describe("parseInboxFolder", () => { + it("埋め込んだフォルダを取り出し、名前からは外す", () => { + const sent = normalizeCaptureName({ + mime: "image/png", originalName: "a.png", when, seq: 2, folder: "材料X", + }); + expect(parseInboxFolder(sent)).toEqual({ + folder: "材料X", + name: "graphium-20260903-101530-02.png", + }); + }); + + it("サブフォルダ(スラッシュ入り)も往復できる", () => { + const sent = normalizeCaptureName({ + mime: "image/png", originalName: "a.png", when, seq: 1, folder: "プロジェクトA/実験1", + }); + expect(parseInboxFolder(sent).folder).toBe("プロジェクトA/実験1"); + }); + + it("埋め込みが無い名前はそのまま返す", () => { + expect(parseInboxFolder("graphium-20260903-101530-01.jpg")).toEqual({ + name: "graphium-20260903-101530-01.jpg", + }); + }); + + it("人が置いた普通のファイル名も壊さない", () => { + expect(parseInboxFolder("IMG_0001.JPG")).toEqual({ name: "IMG_0001.JPG" }); + }); + + it("壊れたエンコードは「指定なし」として扱い、取り込み自体は通す", () => { + const r = parseInboxFolder("graphium-20260903-101530-01~~%E4%B8.jpg"); + expect(r.folder).toBeUndefined(); + expect(r.name).toBe("graphium-20260903-101530-01.jpg"); + }); + + it("区切りだけで中身が無い場合も名前を戻す", () => { + expect(parseInboxFolder("graphium-20260903-101530-01~~.jpg")).toEqual({ + name: "graphium-20260903-101530-01.jpg", + }); + }); +}); diff --git a/src/features/mobile-capture/inbox/push/naming.ts b/src/features/mobile-capture/inbox/push/naming.ts index d4a7089d6..165fa70ad 100644 --- a/src/features/mobile-capture/inbox/push/naming.ts +++ b/src/features/mobile-capture/inbox/push/naming.ts @@ -15,6 +15,45 @@ import { isGraphiumCaptureName, } from "../capture-file"; +/** + * 送信名にフォルダを埋め込むときの区切り。正規化後の名前は + * `graphium-<日時>-<連番>` なので、この並びには現れない。 + */ +const FOLDER_MARK = "~~"; + +/** + * フォルダ名を送信名に埋め込める形にする。`/`(サブフォルダ)や日本語をそのまま + * ファイル名に置けないので URL エンコードする。 + */ +function encodeFolderSegment(folder: string): string { + return encodeURIComponent(folder.trim()); +} + +/** + * 送信名からフォルダを取り出し、フォルダ抜きの名前と一緒に返す。 + * 埋め込みが無ければ folder は undefined、name はそのまま。 + * + * 受信側はこれで「素材の名前」と「入れるフォルダ」を分ける — 名前にエンコード済みの + * 文字列が残ったままだと、素材一覧に読めない名前が並んでしまう。 + */ +export function parseInboxFolder(name: string): { folder?: string; name: string } { + const dot = name.lastIndexOf("."); + const base = dot > 0 ? name.slice(0, dot) : name; + const ext = dot > 0 ? name.slice(dot) : ""; + const at = base.lastIndexOf(FOLDER_MARK); + if (at < 0) return { name }; + const encoded = base.slice(at + FOLDER_MARK.length); + if (!encoded) return { name: base.slice(0, at) + ext }; + let folder: string | undefined; + try { + folder = decodeURIComponent(encoded).trim() || undefined; + } catch { + // 壊れたエンコードは「フォルダ指定なし」として扱う(取り込み自体は通す) + folder = undefined; + } + return { folder, name: base.slice(0, at) + ext }; +} + // MIME → 正規拡張子。inbox/mime.ts の EXT_TO_MIME と対で保守する //(iPhone カメラ/ボイスメモ由来の heic / mov / m4a / caf を含む)。 const MIME_TO_EXT: Record = { @@ -84,6 +123,11 @@ export function normalizeCaptureName(opts: { originalName: string; when: Date; seq: number; + /** + * 入れるフォルダ(任意)。生のメディアにはメタを運ぶ経路が無いので名前に埋め込む。 + * メモ / URL は JSON の中に持たせるため、ここでは付けない。 + */ + folder?: string; }): string { const stamp = formatCaptureTimestamp(opts.when); const seq = String(opts.seq).padStart(2, "0"); @@ -92,5 +136,7 @@ export function normalizeCaptureName(opts: { return `graphium-${stamp}-${seq}-${kind}${GRAPHIUM_CAPTURE_EXTENSION}`; } const ext = extensionForCapture(opts.mime, opts.originalName); - return `graphium-${stamp}-${seq}.${ext}`; + const folder = opts.folder?.trim(); + const mark = folder ? `${FOLDER_MARK}${encodeFolderSegment(folder)}` : ""; + return `graphium-${stamp}-${seq}${mark}.${ext}`; } diff --git a/src/features/mobile-capture/inbox/push/queue.ts b/src/features/mobile-capture/inbox/push/queue.ts index 8361008fe..3bf703523 100644 --- a/src/features/mobile-capture/inbox/push/queue.ts +++ b/src/features/mobile-capture/inbox/push/queue.ts @@ -32,6 +32,7 @@ import { parseGraphiumCaptureFile, } from "../capture-file"; import { normalizeCaptureName } from "./naming"; +import { getInboxFolder } from "./config"; import { createCaptureThumbnail } from "./thumbnail"; import { PushAuthError, type InboxPusher, type PushProgress } from "./types"; @@ -336,6 +337,9 @@ async function attachThumbnails(records: PushQueueRecord[], files: File[]): Prom export async function enqueuePushFiles(files: File[]): Promise { if (files.length === 0) return []; const when = new Date(); + // 送信のたびに選ばせず、設定した既定のフォルダを使う(モバイルは素早く放り込む道具で、 + // 毎回の選択はその速さを損なう)。メディアは名前に埋め込んで運ぶ。 + const folder = getInboxFolder() ?? undefined; const records: PushQueueRecord[] = await Promise.all( files.map(async (file, index) => { const mime = file.type || "application/octet-stream"; @@ -344,6 +348,7 @@ export async function enqueuePushFiles(files: File[]): Promise { const { bytes, mime } = await this.readBytes(ref); const checksum = await computeBlobHash(bytes); + // 送信時に指定されたフォルダは名前に埋め込まれている(push/naming.ts)。 + // ここで取り出して meta に移す — 名前に残したままだと素材名がエンコード済みの + // 文字列になってしまう。 + const { folder } = parseInboxFolder(ref.name); return { blob: new Blob([bytes as BlobPart], { type: mime }), meta: { @@ -67,6 +72,7 @@ export class FolderInbox implements InboxTransport { mime, bytes: bytes.length, kind: kindFromMime(mime), + ...(folder ? { folder } : {}), }, }; } diff --git a/src/features/mobile-capture/inbox/types.ts b/src/features/mobile-capture/inbox/types.ts index 31a681db0..6f8a3a0ab 100644 --- a/src/features/mobile-capture/inbox/types.ts +++ b/src/features/mobile-capture/inbox/types.ts @@ -44,6 +44,12 @@ export type CaptureMeta = { quickNote?: string; /** 撮影時タグ(Phase 1b)。 */ tags?: string[]; + /** + * 送信時に指定されたフォルダ。取り込み時に素材の noteContexts へ入れる。 + * 生の写真・動画・音声にはメタを運ぶ経路が無いので、送信名に埋め込んだものを + * transport が取り出してここへ載せる(push/naming.ts の parseInboxFolder)。 + */ + folder?: string; }; /** diff --git a/src/hooks/use-file-manager.ts b/src/hooks/use-file-manager.ts index 7905debf7..b1431e39b 100644 --- a/src/hooks/use-file-manager.ts +++ b/src/hooks/use-file-manager.ts @@ -2168,6 +2168,11 @@ export function useFileManager(authenticated: boolean) { ? { derivedFromAssets: options.derivedFromAssets } : {}), ...(options?.capture ? { capture: options.capture } : {}), + // 送信時に指定されたフォルダをそのまま素材に付ける。モバイルで「いまは材料X」と + // 決めておけば、取り込んだ時点で片付いている状態になる。 + ...(options?.capture?.folder + ? { noteContexts: normalizeNoteContexts([options.capture.folder]) } + : {}), }; const current = mediaIndexRef.current ?? createEmptyIndex(); const updated = addMediaEntry(current, entry); diff --git a/src/i18n/en.ts b/src/i18n/en.ts index ad603be96..a33a31741 100644 --- a/src/i18n/en.ts +++ b/src/i18n/en.ts @@ -1018,6 +1018,9 @@ export const en: Record = { "nav.folders": "Folders", "nav.foldersTooltip": "Organize notes into folders. A note can live in more than one folder.", "nav.unfiled": "Unfiled", + "mobile.settings.folder": "Send into folder", + "mobile.settings.folderPlaceholder": "e.g. Material X (leave empty for none)", + "mobile.settings.folderHint": "What you send arrives already in this folder. Remembered until you change it.", "nav.newFolder": "New folder", "nav.newNoteInFolder": "New in folder", "nav.renameFolder": "Rename", diff --git a/src/i18n/ja.ts b/src/i18n/ja.ts index 3b0f3b47f..ca8579798 100644 --- a/src/i18n/ja.ts +++ b/src/i18n/ja.ts @@ -1019,6 +1019,9 @@ export const ja: Record = { "nav.folders": "フォルダ", "nav.foldersTooltip": "ノートをフォルダで整理します。1 つのノートを複数のフォルダに入れることもできます", "nav.unfiled": "未分類", + "mobile.settings.folder": "送り先のフォルダ", + "mobile.settings.folderPlaceholder": "例: 材料X(空ならフォルダなし)", + "mobile.settings.folderHint": "ここで決めたフォルダに入った状態で届きます。切り替えるまで覚えています。", "nav.newFolder": "新しいフォルダ", "nav.newNoteInFolder": "このフォルダに新規", "nav.renameFolder": "名前を変更", diff --git a/src/note-app.tsx b/src/note-app.tsx index 4b11e3b32..c664d939c 100644 --- a/src/note-app.tsx +++ b/src/note-app.tsx @@ -7754,6 +7754,10 @@ export function NoteApp() { ...(payload.ogImage ? { ogImage: payload.ogImage } : {}), }, capture: meta, + // 送信時に指定されたフォルダをそのまま素材に付ける(メディアと同じ扱い) + ...(payload.folder?.trim() + ? { noteContexts: [payload.folder.trim()] } + : {}), }); return { fileId }; }, From c76e1ac0dedec44ee587e3881d3ec4294424cf43 Mon Sep 17 00:00:00 2001 From: kumagallium Date: Thu, 3 Sep 2026 23:33:05 +0900 Subject: [PATCH 3/3] Move the send-into folder picker to the top of the mobile screen MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Buried in the settings sheet, the folder was easy to miss: you could send a dozen photos before noticing the setting existed. Where things are going has to be visible where you send them, so the picker now sits at the top of the capture screen, above the search box. Candidates come from two places: the folders this device's library knows about, and the folders used before on this device. Publishing the list from the desktop is not an option — the phone's Drive scope is drive.file, so it can only see files the app itself created, and a list synced up by the desktop client would be invisible to it. Remembering what was typed covers the inbox-only setup instead. The picker renders whether or not there is any history, so it is there on the very first capture; the search box next to it only appears once there is something to search. Co-Authored-By: Claude Opus 5 --- manual/ja/mobile.md | 2 +- manual/mobile.md | 2 +- .../mobile-capture/MobileCaptureView.tsx | 122 +++++++++++++++++- .../mobile-capture/MobileSettingsSheet.tsx | 28 ---- .../mobile-capture/inbox/push/config.test.ts | 41 ++++++ .../mobile-capture/inbox/push/config.ts | 33 +++++ src/i18n/en.ts | 7 +- src/i18n/ja.ts | 7 +- src/note-app.tsx | 1 + 9 files changed, 206 insertions(+), 37 deletions(-) diff --git a/manual/ja/mobile.md b/manual/ja/mobile.md index af0928a1e..4f241165e 100644 --- a/manual/ja/mobile.md +++ b/manual/ja/mobile.md @@ -88,7 +88,7 @@ Graphium はインストール可能な Web アプリ(PWA)です。スマホ 2. **記録して送る。** キューの **送信** をタップすると、自分の Drive の `Graphium/Inbox` フォルダに流れ込みます。転送は TLS を使い、ファイルは*自分が管理する*ストレージに普通のファイルとして置かれます。 3. **フォルダをデスクトップに同期する。** クラウドのクライアント(たとえば Google ドライブ デスクトップ版)を動かし、そのフォルダをデスクトップのディスク上に存在させます。 4. **Graphium にその場所を教える。** デスクトップの[設定 → ストレージ](/ja/settings)の **モバイル送信** セクションに **受信フォルダ** のピッカーがあります。クラウドストレージが同期しているフォルダを選ぶと、Graphium はその中の `Inbox` サブフォルダを読みます。同じセクションの **接続はスマホ側で行います** には、そもそもスマホで Graphium を開くための QR コード(**URL をコピー** 付き)があります。 -5. **取り込む。** ファイルが届くと、デスクトップのサイドバーの **モバイル** に未処理件数が表示されます。開いて 1 件ずつプレビューし、**全部取り込み** か **選択したものを取り込み** を実行します。写真・動画・音声メモは[素材](/ja/materials-and-citations)になり、書いた記録は通常のメモや URL のブックマークになります。スマホの設定で **送り先のフォルダ** を決めておくと、届いた素材はそのフォルダに入った状態になります(切り替えるまで覚えているので、送るたびに選ぶ必要はありません)。重複は自動でスキップされ、トーストが **取り込み・スキップ・失敗** の件数をまとめます。受信箱が空なら **新しいものはありません** と表示されるだけです。 +5. **取り込む。** ファイルが届くと、デスクトップのサイドバーの **モバイル** に未処理件数が表示されます。開いて 1 件ずつプレビューし、**全部取り込み** か **選択したものを取り込み** を実行します。写真・動画・音声メモは[素材](/ja/materials-and-citations)になり、書いた記録は通常のメモや URL のブックマークになります。スマホの画面上部にある **送り先** でフォルダを選んでおくと、届いた素材はそのフォルダに入った状態になります。一覧には、その端末で前に使ったフォルダと、スマホからも同じ保管場所に繋がっている場合はそのフォルダが並びます。**+ 新しいフォルダ…** でその場で作ることもできます。選んだフォルダは切り替えるまで覚えているので、送るたびに選ぶ必要はありません。重複は自動でスキップされ、トーストが **取り込み・スキップ・失敗** の件数をまとめます。受信箱が空なら **新しいものはありません** と表示されるだけです。 既定では、取り込んだファイルは受信箱から削除されます。中身はすでにライブラリに入っているためです。控えを残したい場合は、**処理済みファイルを _imported/ に残す**(受信フォルダの設定、または設定 → ストレージ)をオンにしてください。 diff --git a/manual/mobile.md b/manual/mobile.md index c61a9da14..8fc5ea190 100644 --- a/manual/mobile.md +++ b/manual/mobile.md @@ -88,7 +88,7 @@ The pipeline, end to end: 2. **Capture and send.** Tap **Send** on the queue and it drains into the `Graphium/Inbox` folder of your own Drive. Transfers use TLS, and the files sit as ordinary files in storage *you* control. 3. **Sync the folder to your desktop.** Run your cloud client (for example Google Drive for desktop) so that folder exists on the desktop's disk. 4. **Point Graphium at it.** In the desktop's [Settings → Storage](/settings), the **Mobile upload** section has the **Inbox folder** picker — choose the folder your cloud storage syncs to, and Graphium reads the `Inbox` subfolder inside it. The same section shows the QR code under **Connect on your phone** (with **Copy URL**) that opens Graphium on the phone in the first place. -5. **Import.** When files arrive, a **Mobile** entry in the desktop sidebar shows a pending count. Open it to preview each item, then **Import all** or **Import selected**. Photos, videos, and voice notes become [materials](/materials-and-citations); written captures become regular memos and URL bookmarks. Set **Send into folder** in the phone settings and what arrives is already in that folder — it is remembered until you change it, so there is nothing to pick each time you send. Duplicates are skipped automatically, a toast summarizes **Imported · skipped · failed**, and an empty inbox simply says **Nothing new**. +5. **Import.** When files arrive, a **Mobile** entry in the desktop sidebar shows a pending count. Open it to preview each item, then **Import all** or **Import selected**. Photos, videos, and voice notes become [materials](/materials-and-citations); written captures become regular memos and URL bookmarks. Pick a folder under **Send into** at the top of the phone screen and what arrives is already in that folder. The list holds the folders you have used before on that phone, plus the ones in your library if the phone is connected to the same storage; **+ New folder…** creates one on the spot. Your choice is remembered until you change it, so there is nothing to pick each time you send. Duplicates are skipped automatically, a toast summarizes **Imported · skipped · failed**, and an empty inbox simply says **Nothing new**. By default, imported files are deleted from the inbox — their content is already in your library. Check **Keep processed files in _imported/** (in the inbox's folder settings or in Settings → Storage) if you prefer an archive. diff --git a/src/features/mobile-capture/MobileCaptureView.tsx b/src/features/mobile-capture/MobileCaptureView.tsx index 1f050c73a..172a40c00 100644 --- a/src/features/mobile-capture/MobileCaptureView.tsx +++ b/src/features/mobile-capture/MobileCaptureView.tsx @@ -22,7 +22,15 @@ // スマホ単体で 接続/切断・client_id 上書きまで完結する。 import { useCallback, useEffect, useMemo, useRef, useState } from "react"; -import { StickyNote, Trash2, Search, X, RefreshCw, Settings as SettingsIcon } from "lucide-react"; +import { + StickyNote, + Trash2, + Search, + X, + RefreshCw, + Settings as SettingsIcon, + Folder as FolderIcon, +} from "lucide-react"; import type { CaptureIndex, CaptureEntry } from "./capture-store"; import type { MediaIndex, MediaIndexEntry } from "../asset-browser/media-index"; import { MediaPreview } from "../asset-browser/media-preview"; @@ -34,6 +42,12 @@ import { MobileCaptureBar } from "./MobileCaptureBar"; import { AudioRecorderSheet } from "./AudioRecorderSheet"; import { isAudioRecordingSupported } from "./audio-recorder"; import { MobileSettingsSheet } from "./MobileSettingsSheet"; +import { + getInboxFolder, + setInboxFolder, + getInboxFolderHistory, + rememberInboxFolder, +} from "./inbox/push/config"; import { StoragePickerSheet } from "./StoragePickerSheet"; import { CaptureHistorySection, type LocalCaptureItem } from "./inbox/CaptureHistorySection"; import { usePushQueue } from "./inbox/use-push-queue"; @@ -215,9 +229,13 @@ function MobileMediaPreviewModal({ ); } +/** select の「新しいフォルダ…」を表す番兵。フォルダ名には現れない形 */ +const NEW_FOLDER_OPTION = "\u0000new"; + export function MobileCaptureView({ captureIndex, mediaIndex, + vaultFolders, loading, onCreateCapture, onDeleteCapture, @@ -229,6 +247,11 @@ export function MobileCaptureView({ }: { captureIndex: CaptureIndex | null; mediaIndex?: MediaIndex | null; + /** + * この端末の vault が知っているフォルダ。送り先の候補に混ぜる。 + * 受信箱だけ使う構成では空になるので、候補はこの端末の履歴と合わせて作る。 + */ + vaultFolders?: readonly string[]; loading: boolean; onCreateCapture: (text: string) => Promise; onDeleteCapture?: (captureId: string) => Promise; @@ -247,6 +270,20 @@ export function MobileCaptureView({ const [refreshing, setRefreshing] = useState(false); const [pullDistance, setPullDistance] = useState(0); const [searchQuery, setSearchQuery] = useState(""); + // 送り先のフォルダ。設定シートと同じ localStorage を見るので、どちらで変えても揃う + const [sendFolder, setSendFolder] = useState(""); + const [folderHistory, setFolderHistory] = useState([]); + const [addingFolder, setAddingFolder] = useState(false); + const [newFolder, setNewFolder] = useState(""); + const applyFolder = useCallback((value: string) => { + const next = value.trim(); + setSendFolder(next); + setInboxFolder(next || null); + if (next) { + rememberInboxFolder(next); + setFolderHistory(getInboxFolderHistory()); + } + }, []); const scrollRef = useRef(null); const touchStartY = useRef(0); const pulling = useRef(false); @@ -473,6 +510,26 @@ export function MobileCaptureView({ // 最小設定シートのストレージ操作(状態・切断・client_id 上書き)の実体。 // ピッカー/シートが開いている間だけ push モジュールを引く。 const pushSettings = usePushSettings(showStoragePicker || showSettingsSheet); + // 初回と、設定シートを閉じた直後に読み直す(シート側で変えられていても揃う) + useEffect(() => { + if (showSettingsSheet) return; + setSendFolder(getInboxFolder() ?? ""); + setFolderHistory(getInboxFolderHistory()); + }, [showSettingsSheet]); + // 候補は vault のフォルダとこの端末の履歴の和集合。小文字で名寄せし、 + // 表示は先に出てきた形を残す(フォルダ名の突き合わせ規則と揃える) + const folderOptions = useMemo(() => { + const seen = new Set(); + const out: string[] = []; + for (const raw of [...(vaultFolders ?? []), ...folderHistory]) { + const value = raw.trim(); + const key = value.toLowerCase(); + if (!value || seen.has(key)) continue; + seen.add(key); + out.push(value); + } + return out; + }, [vaultFolders, folderHistory]); // ピッカーの Google 選択。**click から同期的に connect へ到達させる**(GIS 契約)。 // 接続成功 → 残キューを即送信(connectAndDrain)。 @@ -508,6 +565,67 @@ export function MobileCaptureView({ // ファイル選択にフォールバックする。判定は端末固定なので 1 回でよい。 const audioRecordingSupported = useMemo(() => isAudioRecordingSupported(), []); + // 送り先のフォルダ。設定の奥ではなく送る場所のすぐ上に出す — これから送るものが + // どこへ届くのかは、送る前に見えていないと意味がない。履歴が空でも出したいので + // 検索欄(履歴があるときだけ出る)とは別のブロックにしている + const folderPicker = push.configured ? ( +

+ + {t("mobile.sendFolder.label")} + {addingFolder ? ( + setNewFolder(e.target.value)} + onBlur={() => { + if (newFolder.trim()) applyFolder(newFolder); + setAddingFolder(false); + setNewFolder(""); + }} + onKeyDown={(e) => { + if (e.key === "Enter") { + applyFolder(newFolder); + setAddingFolder(false); + setNewFolder(""); + } else if (e.key === "Escape") { + setAddingFolder(false); + setNewFolder(""); + } + }} + placeholder={t("mobile.sendFolder.placeholder")} + className="min-w-0 flex-1 rounded-lg border border-border bg-background px-2 py-1.5 text-xs text-foreground outline-none placeholder:text-muted-foreground focus:border-primary" + /> + ) : ( + + )} +
+ ) : null; + // 検索入力(ヘッダー固定ではなくスクロール内 = 統合リストの直上に置く) const searchInput = (
@@ -611,6 +729,8 @@ export function MobileCaptureView({ 未送信ありのときだけセクション内の接続ボタンが主アクションになる。 捕獲の入口は画面下固定の捕獲バー(MobileCaptureBar)。履歴は IndexedDB 永続なので画面を離れても、送信し終えても消えない */} + {folderPicker} + {(timeline.length > 0 || push.items.length > 0) && (
{searchInput} diff --git a/src/features/mobile-capture/MobileSettingsSheet.tsx b/src/features/mobile-capture/MobileSettingsSheet.tsx index 1c67dc5c5..81b49e096 100644 --- a/src/features/mobile-capture/MobileSettingsSheet.tsx +++ b/src/features/mobile-capture/MobileSettingsSheet.tsx @@ -25,7 +25,6 @@ import { } from "lucide-react"; import { useLocale, type Locale } from "../../i18n"; import { getAppVersion } from "../../lib/updater"; -import { getInboxFolder, setInboxFolder } from "./inbox/push/config"; export type MobileSettingsSheetProps = { /** push モジュールのロードが済んだか。false の間はストレージ操作を無効化する。 */ @@ -62,11 +61,6 @@ export function MobileSettingsSheet({ const [version, setVersion] = useState(""); const [clientIdInput, setClientIdInput] = useState(clientIdOverride); const [clientIdSaved, setClientIdSaved] = useState(false); - // 送ったものを入れるフォルダ。localStorage に持つので、開いた時点の値を初期値にする - const [folderInput, setFolderInput] = useState(""); - useEffect(() => { - setFolderInput(getInboxFolder() ?? ""); - }, []); // バージョンは About タブと同じ経路(Tauri は実バージョン / PWA は package.json) useEffect(() => { @@ -179,28 +173,6 @@ export function MobileSettingsSheet({ )} - {/* 送り先のフォルダ。空なら未分類のまま届き、デスクトップで振り分ける。 - 毎回選ばせないのは、モバイルが「素早く放り込む」道具だから - (切り替えたいときだけここで変える)。 */} -
- - { - setFolderInput(e.target.value); - setInboxFolder(e.target.value); - }} - placeholder={t("mobile.settings.folderPlaceholder")} - className="w-full text-sm px-2.5 py-1.5 rounded-md border border-border bg-background text-foreground placeholder:text-muted-foreground outline-none focus:border-primary transition-colors" - /> -

- {t("mobile.settings.folderHint")} -

-
- {/* 同梱 ID の無いビルドでは自前 client ID が必須(設定モーダルと同じ注記) */} {!hasBundledId && (

diff --git a/src/features/mobile-capture/inbox/push/config.test.ts b/src/features/mobile-capture/inbox/push/config.test.ts index 25be7b545..163c7da61 100644 --- a/src/features/mobile-capture/inbox/push/config.test.ts +++ b/src/features/mobile-capture/inbox/push/config.test.ts @@ -5,10 +5,13 @@ // ある現在のビルドでは、上書き無し = 同梱 ID、上書きの解除 = 同梱 ID に戻る // - フォルダキャッシュは壊れた JSON / 形の違う JSON を無害に null 扱いする // - 選択プロバイダは未保存・不正値を google-drive に倒す(v1 の唯一の実体) +// - 送り先フォルダの履歴は新しい順・重複なし・上限あり。壊れた値は空として扱う import { describe, it, expect, beforeEach } from "vitest"; import { DEFAULT_GOOGLE_PUSH_CLIENT_ID, + getInboxFolderHistory, + rememberInboxFolder, getDriveFolderCache, getGoogleClientId, getGoogleClientIdOverride, @@ -85,3 +88,41 @@ describe("選択プロバイダの永続", () => { expect(getPushProvider()).toBe("google-drive"); }); }); + +describe("送り先フォルダの履歴", () => { + it("使った順に新しいものが先頭へ来る", () => { + rememberInboxFolder("材料X"); + rememberInboxFolder("実験B"); + expect(getInboxFolderHistory()).toEqual(["実験B", "材料X"]); + }); + + it("同じフォルダを使い直しても増えず、先頭へ上がる", () => { + rememberInboxFolder("材料X"); + rememberInboxFolder("実験B"); + rememberInboxFolder("材料X"); + expect(getInboxFolderHistory()).toEqual(["材料X", "実験B"]); + }); + + it("前後の空白は落とす。空文字は覚えない", () => { + rememberInboxFolder(" 材料X "); + rememberInboxFolder(" "); + expect(getInboxFolderHistory()).toEqual(["材料X"]); + }); + + it("上限を超えたら古いものから落ちる", () => { + for (let i = 0; i < 15; i++) rememberInboxFolder(`folder${i}`); + const history = getInboxFolderHistory(); + expect(history).toHaveLength(12); + expect(history[0]).toBe("folder14"); + expect(history).not.toContain("folder0"); + }); + + it("壊れた JSON / 形の違う JSON は空として扱う", () => { + localStorage.setItem("graphium-mobile-inbox-folder-history", "{"); + expect(getInboxFolderHistory()).toEqual([]); + localStorage.setItem("graphium-mobile-inbox-folder-history", JSON.stringify({ a: 1 })); + expect(getInboxFolderHistory()).toEqual([]); + localStorage.setItem("graphium-mobile-inbox-folder-history", JSON.stringify(["ok", 3, null])); + expect(getInboxFolderHistory()).toEqual(["ok"]); + }); +}); diff --git a/src/features/mobile-capture/inbox/push/config.ts b/src/features/mobile-capture/inbox/push/config.ts index c306427cf..16133aabe 100644 --- a/src/features/mobile-capture/inbox/push/config.ts +++ b/src/features/mobile-capture/inbox/push/config.ts @@ -57,6 +57,39 @@ export function setInboxFolder(folder: string | null): void { } } +const INBOX_FOLDER_HISTORY_KEY = "graphium-mobile-inbox-folder-history"; +const FOLDER_HISTORY_MAX = 12; + +/** + * この端末で送り先にしたことのあるフォルダ(新しい順)。 + * + * 受信箱だけ使う構成だと、この端末の vault は空でフォルダの候補が手元に無い。 + * PC から一覧を貰う道は Drive のスコープ(drive.file = アプリが作ったものだけ)で + * 塞がっているので、一度打ったものを覚えて次から選べるようにする。 + */ +export function getInboxFolderHistory(): string[] { + try { + const raw = localStorage.getItem(INBOX_FOLDER_HISTORY_KEY); + if (!raw) return []; + const parsed: unknown = JSON.parse(raw); + return Array.isArray(parsed) ? parsed.filter((v): v is string => typeof v === "string") : []; + } catch { + return []; + } +} + +/** 使ったフォルダを履歴の先頭へ。同じものは畳み、古いものから落とす。 */ +export function rememberInboxFolder(folder: string): void { + const value = folder.trim(); + if (!value) return; + try { + const next = [value, ...getInboxFolderHistory().filter((v) => v !== value)]; + localStorage.setItem(INBOX_FOLDER_HISTORY_KEY, JSON.stringify(next.slice(0, FOLDER_HISTORY_MAX))); + } catch { + // localStorage 不可の環境では黙って無視(他の設定と同じ非致命的挙動) + } +} + /** localStorage に保存された自前 client_id(未設定なら null)。 */ export function getGoogleClientIdOverride(): string | null { try { diff --git a/src/i18n/en.ts b/src/i18n/en.ts index a33a31741..373b36484 100644 --- a/src/i18n/en.ts +++ b/src/i18n/en.ts @@ -1018,9 +1018,10 @@ export const en: Record = { "nav.folders": "Folders", "nav.foldersTooltip": "Organize notes into folders. A note can live in more than one folder.", "nav.unfiled": "Unfiled", - "mobile.settings.folder": "Send into folder", - "mobile.settings.folderPlaceholder": "e.g. Material X (leave empty for none)", - "mobile.settings.folderHint": "What you send arrives already in this folder. Remembered until you change it.", + "mobile.sendFolder.label": "Send into", + "mobile.sendFolder.placeholder": "e.g. Material X (leave empty for none)", + "mobile.sendFolder.none": "No folder", + "mobile.sendFolder.new": "+ New folder…", "nav.newFolder": "New folder", "nav.newNoteInFolder": "New in folder", "nav.renameFolder": "Rename", diff --git a/src/i18n/ja.ts b/src/i18n/ja.ts index ca8579798..402222480 100644 --- a/src/i18n/ja.ts +++ b/src/i18n/ja.ts @@ -1019,9 +1019,10 @@ export const ja: Record = { "nav.folders": "フォルダ", "nav.foldersTooltip": "ノートをフォルダで整理します。1 つのノートを複数のフォルダに入れることもできます", "nav.unfiled": "未分類", - "mobile.settings.folder": "送り先のフォルダ", - "mobile.settings.folderPlaceholder": "例: 材料X(空ならフォルダなし)", - "mobile.settings.folderHint": "ここで決めたフォルダに入った状態で届きます。切り替えるまで覚えています。", + "mobile.sendFolder.label": "送り先", + "mobile.sendFolder.placeholder": "例: 材料X(空ならフォルダなし)", + "mobile.sendFolder.none": "フォルダなし", + "mobile.sendFolder.new": "+ 新しいフォルダ…", "nav.newFolder": "新しいフォルダ", "nav.newNoteInFolder": "このフォルダに新規", "nav.renameFolder": "名前を変更", diff --git a/src/note-app.tsx b/src/note-app.tsx index c664d939c..227d55dca 100644 --- a/src/note-app.tsx +++ b/src/note-app.tsx @@ -9624,6 +9624,7 @@ export function NoteApp() {