diff --git a/CHANGELOG.md b/CHANGELOG.md index 4544e5acba..b4c8f577df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -39,6 +39,8 @@ - Feat: 絵文字をミュート可能にする機能 - 絵文字(ユニコードの絵文字・カスタム絵文字)毎にミュートし、不可視化することができるようになりました - Feat: モバイルデバイスで折りたたまれたUIの展開表示に全画面ページを使用できるように(実験的) +- Feat: 画像のアップロード時にウォーターマークを適用できるように + (Based on https://github.com/MisskeyIO/misskey/pull/785) - Enhance: メモリ使用量を軽減しました - Enhance: 画像の高品質なプレースホルダを無効化してパフォーマンスを向上させるオプションを追加 - Enhance: 招待されているが参加していないルームを開いたときに、招待を承認するかどうか尋ねるように diff --git a/locales/index.d.ts b/locales/index.d.ts index 21da0c171a..2fadd94163 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -1918,6 +1918,26 @@ export interface Locale extends ILocale { * 既定アップロード先 */ "uploadFolder": string; + /** + * ウォーターマーク + */ + "watermark": string; + /** + * ウォーターマークをつけますか? + */ + "watermarkConfirm": string; + /** + * ウォーターマークをつける + */ + "useWatermark": string; + /** + * 画像のアップロード時にデフォルトでウォーターマークをつけるようにします。 + */ + "useWatermarkDescription": string; + /** + * デフォルトの値にかかわらず、アップロードメニューの「ウォーターマークをつける」スイッチを操作して、一回限りの設定を適用することができます。 + */ + "useWatermarkInfo": string; /** * すべての通知を既読にする */ @@ -3175,13 +3195,25 @@ export interface Locale extends ILocale { */ "duplicate": string; /** - * 左 + * 上 */ - "left": string; + "top": string; + /** + * 下 + */ + "bottom": string; /** * 中央 */ "center": string; + /** + * 左 + */ + "left": string; + /** + * 右 + */ + "right": string; /** * 広い */ @@ -4502,18 +4534,38 @@ export interface Locale extends ILocale { * 通知の表示 */ "notificationDisplay": string; + /** + * 配置 + */ + "placement": string; /** * 左上 */ "leftTop": string; + /** + * 中上 + */ + "centerTop": string; /** * 右上 */ "rightTop": string; + /** + * 左中 + */ + "leftCenter": string; + /** + * 右中 + */ + "rightCenter": string; /** * 左下 */ "leftBottom": string; + /** + * 中下 + */ + "centerBottom": string; /** * 右下 */ @@ -4534,6 +4586,14 @@ export interface Locale extends ILocale { * 位置 */ "position": string; + /** + * 回転 + */ + "rotate": string; + /** + * 透明度 + */ + "transparency": string; /** * サーバールール */ @@ -5386,18 +5446,6 @@ export interface Locale extends ILocale { * 圧縮 */ "compress": string; - /** - * 右 - */ - "right": string; - /** - * 下 - */ - "bottom": string; - /** - * 上 - */ - "top": string; /** * 埋め込み */ @@ -5477,6 +5525,14 @@ export interface Locale extends ILocale { * 全ての「ヒントとコツ」を非表示 */ "hideAllTips": string; + /** + * 常に確認する + */ + "alwaysConfirm": string; + /** + * デフォルトの設定を適用する + */ + "useDefaultSettings": string; "_chat": { /** * まだメッセージはありません @@ -12016,6 +12072,56 @@ export interface Locale extends ILocale { */ "tip": string; }; + "_watermarkEditor": { + /** + * ウォーターマークをカスタマイズ + */ + "title": string; + /** + * このファイルは対応していません + */ + "driveFileTypeWarn": string; + /** + * 画像ファイルを選択してください + */ + "driveFileTypeWarnDescription": string; + /** + * 設定が不十分です + */ + "settingInvalidWarn": string; + /** + * プレビューが正常に表示されることを確認してから保存してください + */ + "settingInvalidWarnDescription": string; + /** + * ウォーターマーク用画像のファイルサイズが大きいと、処理の際にウォーターマークを読み込む時間が長くなり、アップロードに時間がかかるようになります。あらかじめ解像度を低くしたり、ファイルを圧縮したりしておくことを推奨します。 + */ + "useSmallFile": string; + /** + * 描画モード + */ + "repeatSetting": string; + /** + * 全体を埋め尽くす + */ + "repeat": string; + /** + * 余白 + */ + "padding": string; + /** + * 回転した分の面積を確保する + */ + "preserveBoundingRect": string; + /** + * 通常はオンで問題ありません。ウォーターマークを回転させた際に余白が不自然になった場合はオフにしてみてください。 + */ + "preserveBoundingRectDescription": string; + /** + * クリップボード経由でのアップロード時の動作 + */ + "clipboardUploadBehavior": string; + }; } declare const locales: { [lang: string]: Locale; diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index b81529790f..364e548095 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -475,6 +475,11 @@ share: "共有" notFound: "見つかりません" notFoundDescription: "指定されたURLに該当するページはありませんでした。" uploadFolder: "既定アップロード先" +watermark: "ウォーターマーク" +watermarkConfirm: "ウォーターマークをつけますか?" +useWatermark: "ウォーターマークをつける" +useWatermarkDescription: "画像のアップロード時にデフォルトでウォーターマークをつけるようにします。" +useWatermarkInfo: "デフォルトの値にかかわらず、アップロードメニューの「ウォーターマークをつける」スイッチを操作して、一回限りの設定を適用することができます。" markAsReadAllNotifications: "すべての通知を既読にする" markAsReadAllUnreadNotes: "すべての投稿を既読にする" markAsReadAllTalkMessages: "すべてのチャットを既読にする" @@ -789,8 +794,11 @@ developer: "開発者" makeExplorable: "アカウントを見つけやすくする" makeExplorableDescription: "オフにすると、「みつける」にアカウントが載らなくなります。" duplicate: "複製" -left: "左" +top: "上" +bottom: "下" center: "中央" +left: "左" +right: "右" wide: "広い" narrow: "狭い" reloadToApplySetting: "設定はページリロード後に反映されます。" @@ -1121,14 +1129,21 @@ editMemo: "メモを編集" reactionsList: "リアクション一覧" renotesList: "リノート一覧" notificationDisplay: "通知の表示" +placement: "配置" leftTop: "左上" +centerTop: "中上" rightTop: "右上" +leftCenter: "左中" +rightCenter: "右中" leftBottom: "左下" +centerBottom: "中下" rightBottom: "右下" stackAxis: "スタック方向" vertical: "縦" horizontal: "横" position: "位置" +rotate: "回転" +transparency: "透明度" serverRules: "サーバールール" pleaseConfirmBelowBeforeSignup: "このサーバーに登録するには、以下の内容を確認し同意する必要があります。" pleaseAgreeAllToContinue: "続けるには、全ての「同意する」にチェックが入っている必要があります。" @@ -1342,9 +1357,6 @@ chat: "チャット" migrateOldSettings: "旧設定情報を移行" migrateOldSettings_description: "通常これは自動で行われていますが、何らかの理由により上手く移行されなかった場合は手動で移行処理をトリガーできます。現在の設定情報は上書きされます。" compress: "圧縮" -right: "右" -bottom: "下" -top: "上" embed: "埋め込み" settingsMigrating: "設定を移行しています。しばらくお待ちください... (後ほど、設定→その他→旧設定情報を移行 で手動で移行することもできます)" readonly: "読み取り専用" @@ -1364,6 +1376,8 @@ abort: "中止" tip: "ヒントとコツ" redisplayAllTips: "全ての「ヒントとコツ」を再表示" hideAllTips: "全ての「ヒントとコツ」を非表示" +alwaysConfirm: "常に確認する" +useDefaultSettings: "デフォルトの設定を適用する" _chat: noMessagesYet: "まだメッセージはありません" @@ -3217,3 +3231,17 @@ _clip: _userLists: tip: "任意のユーザーが含まれるリストを作成できます。作成したリストはタイムラインとして表示可能です。" + +_watermarkEditor: + title: "ウォーターマークをカスタマイズ" + driveFileTypeWarn: "このファイルは対応していません" + driveFileTypeWarnDescription: "画像ファイルを選択してください" + settingInvalidWarn: "設定が不十分です" + settingInvalidWarnDescription: "プレビューが正常に表示されることを確認してから保存してください" + useSmallFile: "ウォーターマーク用画像のファイルサイズが大きいと、処理の際にウォーターマークを読み込む時間が長くなり、アップロードに時間がかかるようになります。あらかじめ解像度を低くしたり、ファイルを圧縮したりしておくことを推奨します。" + repeatSetting: "描画モード" + repeat: "全体を埋め尽くす" + padding: "余白" + preserveBoundingRect: "回転した分の面積を確保する" + preserveBoundingRectDescription: "通常はオンで問題ありません。ウォーターマークを回転させた際に余白が不自然になった場合はオフにしてみてください。" + clipboardUploadBehavior: "クリップボード経由でのアップロード時の動作" diff --git a/packages/frontend/assets/hill.webp b/packages/frontend/assets/hill.webp new file mode 100644 index 0000000000..20877449b4 Binary files /dev/null and b/packages/frontend/assets/hill.webp differ diff --git a/packages/frontend/src/components/MkFormDialog.file.vue b/packages/frontend/src/components/MkFormDialog.file.vue index a11075c342..182ff3ccf5 100644 --- a/packages/frontend/src/components/MkFormDialog.file.vue +++ b/packages/frontend/src/components/MkFormDialog.file.vue @@ -51,7 +51,10 @@ if (props.fileId) { } function selectButton(ev: MouseEvent) { - selectFile(ev.currentTarget ?? ev.target).then(async (file) => { + selectFile({ + anchorElement: ev.currentTarget ?? ev.target, + multiple: false, + }).then(async (file) => { if (!file) return; if (props.validate && !await props.validate(file)) return; diff --git a/packages/frontend/src/components/MkInput.vue b/packages/frontend/src/components/MkInput.vue index b34b7aaf60..766703c448 100644 --- a/packages/frontend/src/components/MkInput.vue +++ b/packages/frontend/src/components/MkInput.vue @@ -27,8 +27,8 @@ SPDX-License-Identifier: AGPL-3.0-only :list="id" :min="min" :max="max" - @focus="focused = true" - @blur="focused = false" + @focus="onFocus" + @blur="onBlur" @keydown="onKeydown($event)" @input="onInput" > @@ -82,6 +82,8 @@ const emit = defineEmits<{ (ev: 'change', _ev: KeyboardEvent): void; (ev: 'keydown', _ev: KeyboardEvent): void; (ev: 'enter', _ev: KeyboardEvent): void; + (ev: 'focus', _ev: FocusEvent): void; + (ev: 'blur', _ev: FocusEvent): void; (ev: 'update:modelValue', value: string | number): void; }>(); @@ -116,6 +118,14 @@ const onKeydown = (ev: KeyboardEvent) => { emit('enter', ev); } }; +const onFocus = (ev: FocusEvent) => { + focused.value = true; + emit('focus', ev); +}; +const onBlur = (ev: FocusEvent) => { + focused.value = false; + emit('blur', ev); +}; const updated = () => { changed.value = false; diff --git a/packages/frontend/src/components/MkPostForm.vue b/packages/frontend/src/components/MkPostForm.vue index 982ed88003..cd4fabea02 100644 --- a/packages/frontend/src/components/MkPostForm.vue +++ b/packages/frontend/src/components/MkPostForm.vue @@ -120,7 +120,7 @@ import { formatTimeString } from '@/utility/format-time-string.js'; import { Autocomplete } from '@/utility/autocomplete.js'; import * as os from '@/os.js'; import { misskeyApi } from '@/utility/misskey-api.js'; -import { selectFiles } from '@/utility/drive.js'; +import { selectFile } from '@/utility/drive.js'; import { store } from '@/store.js'; import MkInfo from '@/components/MkInfo.vue'; import { i18n } from '@/i18n.js'; @@ -437,7 +437,11 @@ function focus() { function chooseFileFrom(ev) { if (props.mock) return; - selectFiles(ev.currentTarget ?? ev.target, i18n.ts.attachFile).then(files_ => { + selectFile({ + anchorElement: ev.currentTarget ?? ev.target, + multiple: true, + label: i18n.ts.attachFile, + }).then(files_ => { for (const file of files_) { files.value.push(file); } diff --git a/packages/frontend/src/components/MkUploaderDialog.vue b/packages/frontend/src/components/MkUploaderDialog.vue index 3f5f0776a8..a3f5c14ae1 100644 --- a/packages/frontend/src/components/MkUploaderDialog.vue +++ b/packages/frontend/src/components/MkUploaderDialog.vue @@ -92,6 +92,13 @@ SPDX-License-Identifier: AGPL-3.0-only + + + + diff --git a/packages/frontend/src/components/MkWatermarkEditorDialog.padding.vue b/packages/frontend/src/components/MkWatermarkEditorDialog.padding.vue new file mode 100644 index 0000000000..374dd8fa5b --- /dev/null +++ b/packages/frontend/src/components/MkWatermarkEditorDialog.padding.vue @@ -0,0 +1,66 @@ + + + + + + + diff --git a/packages/frontend/src/components/MkWatermarkEditorDialog.vue b/packages/frontend/src/components/MkWatermarkEditorDialog.vue new file mode 100644 index 0000000000..a652f3e7bd --- /dev/null +++ b/packages/frontend/src/components/MkWatermarkEditorDialog.vue @@ -0,0 +1,398 @@ + + + + + + + diff --git a/packages/frontend/src/components/form/link.vue b/packages/frontend/src/components/form/link.vue index e60155f4af..1fb60d4fd5 100644 --- a/packages/frontend/src/components/form/link.vue +++ b/packages/frontend/src/components/form/link.vue @@ -5,33 +5,33 @@ SPDX-License-Identifier: AGPL-3.0-only diff --git a/packages/frontend/src/os.ts b/packages/frontend/src/os.ts index a513ae4902..4daf9adf43 100644 --- a/packages/frontend/src/os.ts +++ b/packages/frontend/src/os.ts @@ -13,6 +13,7 @@ import type { ComponentProps as CP } from 'vue-component-type-helpers'; import type { Form, GetFormResultType } from '@/utility/form.js'; import type { MenuItem } from '@/types/menu.js'; import type { PostFormProps } from '@/types/post-form.js'; +import type { UploaderDialogFeatures } from '@/components/MkUploaderDialog.vue'; import type MkRoleSelectDialog_TypeReferenceOnly from '@/components/MkRoleSelectDialog.vue'; import type MkEmojiPickerDialog_TypeReferenceOnly from '@/components/MkEmojiPickerDialog.vue'; import { misskeyApi } from '@/utility/misskey-api.js'; @@ -773,6 +774,7 @@ export function launchUploader( options?: { folderId?: string | null; multiple?: boolean; + features?: UploaderDialogFeatures; }, ): Promise { return new Promise((res, rej) => { @@ -781,6 +783,7 @@ export function launchUploader( files: markRaw(files), folderId: options?.folderId, multiple: options?.multiple, + features: options?.features, }, { done: driveFiles => { if (driveFiles.length === 0) return rej(); diff --git a/packages/frontend/src/pages/admin/custom-emojis-manager.local.list.vue b/packages/frontend/src/pages/admin/custom-emojis-manager.local.list.vue index 68c7048ae1..172a4d09cc 100644 --- a/packages/frontend/src/pages/admin/custom-emojis-manager.local.list.vue +++ b/packages/frontend/src/pages/admin/custom-emojis-manager.local.list.vue @@ -174,7 +174,10 @@ function setupGrid(): GridSetting { { bindTo: 'url', icon: 'ti-icons', type: 'image', editable: true, width: 'auto', validators: [required], async customValueEditor(row, col, value, cellElement) { - const file = await selectFile(cellElement); + const file = await selectFile({ + anchorElement: cellElement, + multiple: false, + }); gridItems.value[row.index].url = file.url; gridItems.value[row.index].fileId = file.id; diff --git a/packages/frontend/src/pages/channel-editor.vue b/packages/frontend/src/pages/channel-editor.vue index 355b5464a1..72281ea882 100644 --- a/packages/frontend/src/pages/channel-editor.vue +++ b/packages/frontend/src/pages/channel-editor.vue @@ -188,7 +188,10 @@ async function archive() { } function setBannerImage(evt) { - selectFile(evt.currentTarget ?? evt.target, null).then(file => { + selectFile({ + anchorElement: evt.currentTarget ?? evt.target, + multiple: false, + }).then(file => { bannerId.value = file.id; }); } diff --git a/packages/frontend/src/pages/chat/room.form.vue b/packages/frontend/src/pages/chat/room.form.vue index 7e3be67230..17b68d6eb9 100644 --- a/packages/frontend/src/pages/chat/room.form.vue +++ b/packages/frontend/src/pages/chat/room.form.vue @@ -168,7 +168,11 @@ function onKeydown(ev: KeyboardEvent) { } function chooseFile(ev: MouseEvent) { - selectFile(ev.currentTarget ?? ev.target, i18n.ts.selectFile).then(selectedFile => { + selectFile({ + anchorElement: ev.currentTarget ?? ev.target, + multiple: false, + label: i18n.ts.selectFile, + }).then(selectedFile => { file.value = selectedFile; }); } diff --git a/packages/frontend/src/pages/custom-emojis-manager.vue b/packages/frontend/src/pages/custom-emojis-manager.vue index c2bc621f6a..4d78ff14be 100644 --- a/packages/frontend/src/pages/custom-emojis-manager.vue +++ b/packages/frontend/src/pages/custom-emojis-manager.vue @@ -214,7 +214,10 @@ const menu = (ev: MouseEvent) => { icon: 'ti ti-upload', text: i18n.ts.import, action: async () => { - const file = await selectFile(ev.currentTarget ?? ev.target); + const file = await selectFile({ + anchorElement: ev.currentTarget ?? ev.target, + multiple: false, + }); misskeyApi('admin/emoji/import-zip', { fileId: file.id, }) diff --git a/packages/frontend/src/pages/emoji-edit-dialog.vue b/packages/frontend/src/pages/emoji-edit-dialog.vue index 41de457427..b4fc4a46d9 100644 --- a/packages/frontend/src/pages/emoji-edit-dialog.vue +++ b/packages/frontend/src/pages/emoji-edit-dialog.vue @@ -121,7 +121,10 @@ watch(roleIdsThatCanBeUsedThisEmojiAsReaction, async () => { const imgUrl = computed(() => file.value ? file.value.url : props.emoji ? props.emoji.url : null); async function changeImage(ev: Event) { - file.value = await selectFile(ev.currentTarget ?? ev.target, null); + file.value = await selectFile({ + anchorElement: ev.currentTarget ?? ev.target, + multiple: false, + }); const candidate = file.value.name.replace(/\.(.+)$/, ''); if (candidate.match(/^[a-z0-9_]+$/)) { name.value = candidate; diff --git a/packages/frontend/src/pages/gallery/edit.vue b/packages/frontend/src/pages/gallery/edit.vue index 1b8c14a156..f4d528ac6e 100644 --- a/packages/frontend/src/pages/gallery/edit.vue +++ b/packages/frontend/src/pages/gallery/edit.vue @@ -20,7 +20,7 @@ SPDX-License-Identifier: AGPL-3.0-only
{{ file.name }}
- {{ i18n.ts.attachFile }} + {{ i18n.ts.attachFile }} {{ i18n.ts.markAsSensitive }} @@ -44,7 +44,7 @@ import MkInput from '@/components/MkInput.vue'; import MkTextarea from '@/components/MkTextarea.vue'; import MkSwitch from '@/components/MkSwitch.vue'; import FormSuspense from '@/components/form/suspense.vue'; -import { selectFiles } from '@/utility/drive.js'; +import { selectFile } from '@/utility/drive.js'; import * as os from '@/os.js'; import { misskeyApi } from '@/utility/misskey-api.js'; import { definePage } from '@/page.js'; @@ -63,8 +63,11 @@ const description = ref(null); const title = ref(null); const isSensitive = ref(false); -function selectFile(evt) { - selectFiles(evt.currentTarget ?? evt.target, null).then(selected => { +function chooseFile(evt) { + selectFile({ + anchorElement: evt.currentTarget ?? evt.target, + multiple: false, + }).then(selected => { files.value = files.value.concat(selected); }); } diff --git a/packages/frontend/src/pages/page-editor/page-editor.vue b/packages/frontend/src/pages/page-editor/page-editor.vue index 49d9150852..f2ffdf645a 100644 --- a/packages/frontend/src/pages/page-editor/page-editor.vue +++ b/packages/frontend/src/pages/page-editor/page-editor.vue @@ -205,7 +205,10 @@ async function add() { } function setEyeCatchingImage(img: Event) { - selectFile(img.currentTarget ?? img.target, null).then(file => { + selectFile({ + anchorElement: img.currentTarget ?? img.target, + multiple: false, + }).then(file => { eyeCatchingImageId.value = file.id; }); } diff --git a/packages/frontend/src/pages/settings/account-data.vue b/packages/frontend/src/pages/settings/account-data.vue index d175c0dc32..5a00d7a9d7 100644 --- a/packages/frontend/src/pages/settings/account-data.vue +++ b/packages/frontend/src/pages/settings/account-data.vue @@ -233,7 +233,10 @@ const exportAntennas = () => { }; const importFollowing = async (ev) => { - const file = await selectFile(ev.currentTarget ?? ev.target); + const file = await selectFile({ + anchorElement: ev.currentTarget ?? ev.target, + multiple: false, + }); misskeyApi('i/import-following', { fileId: file.id, withReplies: withReplies.value, @@ -241,22 +244,34 @@ const importFollowing = async (ev) => { }; const importUserLists = async (ev) => { - const file = await selectFile(ev.currentTarget ?? ev.target); + const file = await selectFile({ + anchorElement: ev.currentTarget ?? ev.target, + multiple: false, + }); misskeyApi('i/import-user-lists', { fileId: file.id }).then(onImportSuccess).catch(onError); }; const importMuting = async (ev) => { - const file = await selectFile(ev.currentTarget ?? ev.target); + const file = await selectFile({ + anchorElement: ev.currentTarget ?? ev.target, + multiple: false, + }); misskeyApi('i/import-muting', { fileId: file.id }).then(onImportSuccess).catch(onError); }; const importBlocking = async (ev) => { - const file = await selectFile(ev.currentTarget ?? ev.target); + const file = await selectFile({ + anchorElement: ev.currentTarget ?? ev.target, + multiple: false, + }); misskeyApi('i/import-blocking', { fileId: file.id }).then(onImportSuccess).catch(onError); }; const importAntennas = async (ev) => { - const file = await selectFile(ev.currentTarget ?? ev.target); + const file = await selectFile({ + anchorElement: ev.currentTarget ?? ev.target, + multiple: false, + }); misskeyApi('i/import-antennas', { fileId: file.id }).then(onImportSuccess).catch(onError); }; diff --git a/packages/frontend/src/pages/settings/deck.vue b/packages/frontend/src/pages/settings/deck.vue index 22bd8cbc80..ae882d1ee2 100644 --- a/packages/frontend/src/pages/settings/deck.vue +++ b/packages/frontend/src/pages/settings/deck.vue @@ -114,7 +114,10 @@ watch(wallpaper, async () => { }); function setWallpaper(ev: MouseEvent) { - selectFile(ev.currentTarget ?? ev.target, null).then(file => { + selectFile({ + anchorElement: ev.currentTarget ?? ev.target, + multiple: false, + }).then(file => { wallpaper.value = file.url; }); } diff --git a/packages/frontend/src/pages/settings/drive.vue b/packages/frontend/src/pages/settings/drive.vue index d62e487341..48b189302c 100644 --- a/packages/frontend/src/pages/settings/drive.vue +++ b/packages/frontend/src/pages/settings/drive.vue @@ -53,6 +53,29 @@ SPDX-License-Identifier: AGPL-3.0-only {{ i18n.ts.drivecleaner }} + + + + +
+
+ {{ i18n.ts.useWatermarkInfo }} + + + + + +
+ +
+ + + + {{ i18n.ts._watermarkEditor.title }} + +
+
+ @@ -81,10 +104,12 @@ SPDX-License-Identifier: AGPL-3.0-only