-
Notifications
You must be signed in to change notification settings - Fork 350
fix: ノートをコピーして歌詞入力などのペーストしたときにノートオブジェクトがペーストされないようにする #2615
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 19 commits
4c07bdc
63a8655
5c39b4a
e27ae0e
a218d66
933a7b7
fdb29c5
8a6c7e8
6460bd4
b7f1402
f3a52dd
0bf2b52
6c6d6df
928b3ba
5b7452f
e081265
6cbdd98
cff813d
0bf5ffc
28a869c
aaa69cc
69510b8
05d7de8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,153 @@ | ||
| import { ActionContext, Note } from "../type"; | ||
| import { noteSchema } from "@/domain/project/schema"; | ||
|
|
||
| // VOICEVOXソングのノート専用のMIMEType | ||
| export const VOICEVOX_NOTES_MIME_TYPE = | ||
| "web application/vnd.voicevox.song-notes"; | ||
|
|
||
| /** | ||
| * 選択されたノートをクリップボードにコピーする | ||
| * @param context ActionContext | ||
| * @throws クリップボードへの書き込みに失敗した場合 | ||
| */ | ||
| export async function copyNotesToClipboard( | ||
| context: ActionContext, | ||
| ): Promise<void> { | ||
| const { getters } = context; | ||
| const selectedTrack = getters.SELECTED_TRACK; | ||
| const noteIds = getters.SELECTED_NOTE_IDS; | ||
|
|
||
| // 選択されたトラックがない場合は何もしない | ||
| if (!selectedTrack) return; | ||
|
Comment on lines
+22
to
+23
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. これがundefinedになることは無さそう? |
||
|
|
||
| // ノートが選択されていない場合は何もしない | ||
| if (noteIds.size === 0) return; | ||
|
|
||
| // 選択されたノートのみをコピーする | ||
| const selectedNotesForCopy = selectedTrack.notes | ||
| .filter((note: Note) => noteIds.has(note.id)) | ||
| .map((note: Note) => { | ||
| // idのみコピーしない | ||
| const { id, ...noteWithoutId } = note; | ||
| return noteWithoutId; | ||
| }); | ||
|
|
||
| // VOICEVOXのノートのペースト用としてノートをJSONにシリアライズ | ||
| const serializedNotes = JSON.stringify(selectedNotesForCopy); | ||
|
|
||
| await writeNotesToClipboard(serializedNotes); | ||
| } | ||
|
|
||
| /** | ||
| * クリップボードにデータを書き込む | ||
| * @param serializedNotes シリアライズされたノートデータオブジェクト | ||
| * @throws クリップボードへの書き込みに失敗した場合 | ||
| */ | ||
| async function writeNotesToClipboard(serializedNotes: string): Promise<void> { | ||
| try { | ||
| // 1. カスタムMIMEタイプを利用してコピー | ||
| // web application/vnd.voicevox.song-notes | ||
| // web からはじまる形式はChromeのみでサポート | ||
| const notesBlob = new Blob([serializedNotes], { | ||
| type: VOICEVOX_NOTES_MIME_TYPE, | ||
| }); | ||
| const clipboardItem = new ClipboardItem({ | ||
| [VOICEVOX_NOTES_MIME_TYPE]: notesBlob, | ||
| }); | ||
| await navigator.clipboard.write([clipboardItem]); | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. こちら修正しました! |
||
| } catch { | ||
| // 2. カスタムMIMEタイプが利用できない(Chrome以外のブラウザ環境)の場合のフォールバック | ||
| // VOICEVOXのシーケンサでは、ペースト時に text/html を読み取り、data-* 属性からノートデータを復元できる。 | ||
| // VOICEVOXの歌詞入力など他の入力inputや多くの他のアプリケーションでは、<i> タグや data-* 属性は無視され、 | ||
| // text/plainの空文字が優先されるため、結果的に何もペーストされないように見える | ||
sigprogramming marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| try { | ||
| // <i>のdata属性にノートオブジェクトを埋め込む | ||
| const encodedHtmlNotes = `<i data-voicevox-song-notes="${encodeURIComponent(serializedNotes)}" />`; | ||
| // ノートデータを持つtext/html | ||
| const textHtmlBlob = new Blob([encodedHtmlNotes], { | ||
| type: "text/html", | ||
| }); | ||
| // 多くの場合に表示される空文字 | ||
| const emptyTextBlob = new Blob([""], { | ||
|
||
| type: "text/plain", | ||
| }); | ||
| const clipboardItem = new ClipboardItem({ | ||
| "text/html": textHtmlBlob, | ||
| "text/plain": emptyTextBlob, | ||
| }); | ||
| await navigator.clipboard.write([clipboardItem]); | ||
| } catch (clipboardWriteError) { | ||
| // クリップボード書き込みに失敗した場合はエラー | ||
| throw new Error("Failed to copy notes to clipboard.", { | ||
| cause: clipboardWriteError, | ||
| }); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * クリップボードからノートオブジェクトを読み取る | ||
| * @returns 読み取ったノート配列(idは除外されている) | ||
| */ | ||
| export async function readNotesFromClipboard(): Promise<Omit<Note, "id">[]> { | ||
| try { | ||
| const clipboardItems = await navigator.clipboard.read(); | ||
|
|
||
| for (const item of clipboardItems) { | ||
| // 1. カスタムMIMEタイプがあればそれを優先してパース | ||
| if (item.types.includes(VOICEVOX_NOTES_MIME_TYPE)) { | ||
| const blob = await item.getType(VOICEVOX_NOTES_MIME_TYPE); | ||
| const notesText = await blob.text(); | ||
| return validateNotesForClipboard(notesText); | ||
| } | ||
| // 2. なければフォールバックとしてtext/htmlをチェックしてパース | ||
| if (item.types.includes("text/html")) { | ||
| const blob = await item.getType("text/html"); | ||
| const htmlText = await blob.text(); | ||
| const domParser = new DOMParser(); | ||
| const doc = domParser.parseFromString(htmlText, "text/html"); | ||
| // data-voicevox-song-notesデータ属性を持つ要素を取得 | ||
| const elementCandidate = doc.querySelector( | ||
| "[data-voicevox-song-notes]", | ||
| ); | ||
| // 要素が取得できないなら次のClipboardItemへ | ||
| if (!elementCandidate) continue; | ||
| // data-voicevox-song-notesデータ属性値を取得 | ||
| const encodedData = elementCandidate.getAttribute( | ||
| "data-voicevox-song-notes", | ||
| ); | ||
| // 属性値がないなら次のClipboardItemへ | ||
| if (!encodedData) continue; | ||
| const decodedData = decodeURIComponent(encodedData); | ||
| return validateNotesForClipboard(decodedData); | ||
| } | ||
| // 他のタイプはノートペーストにおいては無視 | ||
| } | ||
|
|
||
| // なにも見つからなければ空配列とし何もペーストされない | ||
| return []; | ||
| } catch (clipboardReadError) { | ||
| throw new Error("Failed to read notes from clipboard.", { | ||
| cause: clipboardReadError, | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * コピー&ペースト用のノートデータをバリデーションする | ||
| * @param clipboardText | ||
| * @returns バリデーション済みのノート配列(idは除外) | ||
| * @throws バリデーション失敗時にエラーをスロー | ||
| */ | ||
| function validateNotesForClipboard(clipboardText: string): Omit<Note, "id">[] { | ||
| try { | ||
| return noteSchema | ||
| .omit({ id: true }) | ||
| .array() | ||
| .parse(JSON.parse(clipboardText)); | ||
| } catch (validationError) { | ||
| throw new Error("Failed to validate notes for clipboard data.", { | ||
| cause: validationError, | ||
| }); | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.