diff --git a/locales/index.d.ts b/locales/index.d.ts index 0ae188f1f7..beccac4a1d 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -1902,6 +1902,18 @@ export interface Locale extends ILocale { * 既定アップロード先 */ "uploadFolder": string; + /** + * ウォーターマーク + */ + "watermark": string; + /** + * ウォーターマークをつける + */ + "useWatermark": string; + /** + * 画像にウォーターマークを追加します + */ + "useWatermarkDescription": string; /** * すべての通知を既読にする */ @@ -3143,13 +3155,25 @@ export interface Locale extends ILocale { */ "duplicate": string; /** - * 左 + * 上 */ - "left": string; + "top": string; + /** + * 下 + */ + "bottom": string; /** * 中央 */ "center": string; + /** + * 左 + */ + "left": string; + /** + * 右 + */ + "right": string; /** * 広い */ @@ -4458,18 +4482,38 @@ export interface Locale extends ILocale { * 通知の表示 */ "notificationDisplay": string; + /** + * 配置 + */ + "placement": string; /** * 左上 */ "leftTop": string; + /** + * 中上 + */ + "centerTop": string; /** * 右上 */ "rightTop": string; + /** + * 左中 + */ + "leftCenter": string; + /** + * 右中 + */ + "rightCenter": string; /** * 左下 */ "leftBottom": string; + /** + * 中下 + */ + "centerBottom": string; /** * 右下 */ @@ -4490,6 +4534,22 @@ export interface Locale extends ILocale { * 位置 */ "position": string; + /** + * 繰り返し + */ + "repeat": string; + /** + * 引き伸ばし + */ + "enlargement": string; + /** + * 回転 + */ + "rotate": string; + /** + * 透明度 + */ + "opacity": string; /** * サーバールール */ diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 1b59708d85..01fdd36abe 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -471,6 +471,9 @@ share: "共有" notFound: "見つかりません" notFoundDescription: "指定されたURLに該当するページはありませんでした。" uploadFolder: "既定アップロード先" +watermark: "ウォーターマーク" +useWatermark: "ウォーターマークをつける" +useWatermarkDescription: "画像にウォーターマークを追加します" markAsReadAllNotifications: "すべての通知を既読にする" markAsReadAllUnreadNotes: "すべての投稿を既読にする" markAsReadAllTalkMessages: "すべてのチャットを既読にする" @@ -781,8 +784,11 @@ makeExplorable: "アカウントを見つけやすくする" makeExplorableDescription: "オフにすると、「みつける」にアカウントが載らなくなります。" showGapBetweenNotesInTimeline: "タイムラインのノートを離して表示" duplicate: "複製" -left: "左" +top: "上" +bottom: "下" center: "中央" +left: "左" +right: "右" wide: "広い" narrow: "狭い" reloadToApplySetting: "設定はページリロード後に反映されます。" @@ -1110,14 +1116,23 @@ editMemo: "メモを編集" reactionsList: "リアクション一覧" renotesList: "リノート一覧" notificationDisplay: "通知の表示" +placement: "配置" leftTop: "左上" +centerTop: "中上" rightTop: "右上" +leftCenter: "左中" +rightCenter: "右中" leftBottom: "左下" +centerBottom: "中下" rightBottom: "右下" stackAxis: "スタック方向" vertical: "縦" horizontal: "横" position: "位置" +repeat: "繰り返し" +enlargement: "引き伸ばし" +rotate: "回転" +opacity: "透明度" serverRules: "サーバールール" pleaseConfirmBelowBeforeSignup: "このサーバーに登録するには、以下の内容を確認し同意する必要があります。" pleaseAgreeAllToContinue: "続けるには、全ての「同意する」にチェックが入っている必要があります。" diff --git a/packages/frontend-shared/js/const.ts b/packages/frontend-shared/js/const.ts index 4fe5cbb205..7253bc180c 100644 --- a/packages/frontend-shared/js/const.ts +++ b/packages/frontend-shared/js/const.ts @@ -86,6 +86,7 @@ export const ROLE_POLICIES = [ 'canManageCustomEmojis', 'canManageAvatarDecorations', 'canSearchNotes', + 'canUseReaction', 'canUseTranslator', 'canHideAds', 'driveCapacityMb', diff --git a/packages/frontend/assets/default-watermark.png b/packages/frontend/assets/default-watermark.png new file mode 100644 index 0000000000..e8edd64f69 Binary files /dev/null and b/packages/frontend/assets/default-watermark.png differ diff --git a/packages/frontend/src/components/MkDrive.vue b/packages/frontend/src/components/MkDrive.vue index 8be6d6f53d..fe44c33c6b 100644 --- a/packages/frontend/src/components/MkDrive.vue +++ b/packages/frontend/src/components/MkDrive.vue @@ -106,6 +106,7 @@ import XFile from '@/components/MkDrive.file.vue'; import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { useStream } from '@/stream.js'; +import { $i } from '@/account.js'; import { defaultStore } from '@/store.js'; import { i18n } from '@/i18n.js'; import { uploadFile, uploads } from '@/scripts/upload.js'; @@ -143,6 +144,7 @@ const selectedFolders = ref([]); const uploadings = uploads; const connection = useStream().useChannel('drive'); const keepOriginal = ref(defaultStore.state.keepOriginalUploading); // 外部渡しが多いので$refは使わないほうがよい +const useWatermark = ref(defaultStore.state.useWatermark); // ドロップされようとしているか const draghover = ref(false); @@ -391,7 +393,7 @@ function onChangeFileInput() { } function upload(file: File, folderToUpload?: Misskey.entities.DriveFolder | null) { - uploadFile(file, (folderToUpload && typeof folderToUpload === 'object') ? folderToUpload.id : null, undefined, keepOriginal.value).then(res => { + uploadFile(file, (folderToUpload && typeof folderToUpload === 'object') ? folderToUpload.id : null, undefined, keepOriginal.value, useWatermark.value).then(res => { addFile(res, true); }); } @@ -633,7 +635,12 @@ function getMenu() { type: 'switch', text: i18n.ts.keepOriginalUploading, ref: keepOriginal, - }, { type: 'divider' }, { + }, ...($i?.policies.canUseWatermark ? [{ + type: 'switch', + text: i18n.ts.useWatermark, + ref: useWatermark, + }] as MenuItem[] : [] + ), { type: 'divider' }, { text: i18n.ts.addFile, type: 'label', }, { diff --git a/packages/frontend/src/pages/admin/roles.vue b/packages/frontend/src/pages/admin/roles.vue index b1cbdad137..cc1546e0ef 100644 --- a/packages/frontend/src/pages/admin/roles.vue +++ b/packages/frontend/src/pages/admin/roles.vue @@ -89,14 +89,6 @@ SPDX-License-Identifier: AGPL-3.0-only - - - - - - - - @@ -105,6 +97,14 @@ SPDX-License-Identifier: AGPL-3.0-only + + + + + + + + diff --git a/packages/frontend/src/pages/settings/drive.vue b/packages/frontend/src/pages/settings/drive.vue index 0e66b93f1c..1eeb14b854 100644 --- a/packages/frontend/src/pages/settings/drive.vue +++ b/packages/frontend/src/pages/settings/drive.vue @@ -33,11 +33,36 @@ SPDX-License-Identifier: AGPL-3.0-only
+ {{ i18n.ts.uploadFolder }} - + + + +
+ + + + + + +
+
+ +
+
+ +
+ {{ i18n.ts.selectFile }} + {{ i18n.ts.defa }} +
+
+
+
+
+ {{ i18n.ts.drivecleaner }} @@ -77,6 +102,9 @@ import MkChart from '@/components/MkChart.vue'; import { i18n } from '@/i18n.js'; import { definePageMetadata } from '@/scripts/page-metadata.js'; import { signinRequired } from '@/account.js'; +import MkInfo from "@/components/MkInfo.vue"; +import MkButton from "@/components/MkButton.vue"; +import MkFolder from "@/components/MkFolder.vue"; const $i = signinRequired(); @@ -102,6 +130,9 @@ const meterStyle = computed(() => { const keepOriginalUploading = computed(defaultStore.makeGetterSetter('keepOriginalUploading')); const keepOriginalFilename = computed(defaultStore.makeGetterSetter('keepOriginalFilename')); +const useWatermark = computed(defaultStore.makeGetterSetter('useWatermark')); +const watermarkConfig = computed(defaultStore.makeGetterSetter('watermarkConfig')); + misskeyApi('drive').then(info => { capacity.value = info.capacity; usage.value = info.usage; diff --git a/packages/frontend/src/scripts/select-file.ts b/packages/frontend/src/scripts/select-file.ts index b037aa8acc..d0d874d428 100644 --- a/packages/frontend/src/scripts/select-file.ts +++ b/packages/frontend/src/scripts/select-file.ts @@ -9,17 +9,19 @@ import * as os from '@/os.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import { useStream } from '@/stream.js'; import { i18n } from '@/i18n.js'; +import { $i } from '@/account.js'; import { defaultStore } from '@/store.js'; import { uploadFile } from '@/scripts/upload.js'; +import type { MenuItem } from '@/types/menu.js'; -export function chooseFileFromPc(multiple: boolean, keepOriginal = false): Promise { +export function chooseFileFromPc(multiple: boolean, keepOriginal = false, useWatermark = false): Promise { return new Promise((res, rej) => { const input = document.createElement('input'); input.type = 'file'; input.multiple = multiple; input.onchange = () => { if (!input.files) return res([]); - const promises = Array.from(input.files, file => uploadFile(file, defaultStore.state.uploadFolder, undefined, keepOriginal)); + const promises = Array.from(input.files, file => uploadFile(file, defaultStore.state.uploadFolder, undefined, keepOriginal, useWatermark)); Promise.all(promises).then(driveFiles => { res(driveFiles); @@ -83,6 +85,7 @@ export function chooseFileFromUrl(): Promise { function select(src: HTMLElement | EventTarget | null, label: string | null, multiple: boolean): Promise { return new Promise((res, rej) => { const keepOriginal = ref(defaultStore.state.keepOriginalUploading); + const useWatermark = ref(defaultStore.state.useWatermark); os.popupMenu([label ? { text: label, @@ -91,10 +94,15 @@ function select(src: HTMLElement | EventTarget | null, label: string | null, mul type: 'switch', text: i18n.ts.keepOriginalUploading, ref: keepOriginal, - }, { + }, ...($i?.policies.canUseWatermark ? [{ + type: 'switch', + text: i18n.ts.useWatermark, + ref: useWatermark, + }] as MenuItem[] : [] + ), { text: i18n.ts.upload, icon: 'ti ti-upload', - action: () => chooseFileFromPc(multiple, keepOriginal.value).then(files => res(files)), + action: () => chooseFileFromPc(multiple, keepOriginal.value, useWatermark.value).then(files => res(files)), }, { text: i18n.ts.fromDrive, icon: 'ti ti-cloud', diff --git a/packages/frontend/src/scripts/upload.ts b/packages/frontend/src/scripts/upload.ts index 713573a377..e9991ba69c 100644 --- a/packages/frontend/src/scripts/upload.ts +++ b/packages/frontend/src/scripts/upload.ts @@ -35,6 +35,7 @@ export function uploadFile( folder?: string | Misskey.entities.DriveFolder, name?: string, keepOriginal: boolean = defaultStore.state.keepOriginalUploading, + watermark: boolean = defaultStore.state.useWatermark, ): Promise { if ($i == null) throw new Error('Not logged in'); diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts index 1d981e897b..3b2ac3b61d 100644 --- a/packages/frontend/src/store.ts +++ b/packages/frontend/src/store.ts @@ -474,6 +474,30 @@ export const defaultStore = markRaw(new Storage('base', { where: 'device', default: true, }, + useWatermark: { + where: 'device', + default: false, + }, + watermarkConfig: { + where: 'account', + default: null as { + fileId: string | null; + fileUrl: string | null; + width: number | null; + height: number | null; + enlargement: 'scale-down' | 'contain' | 'cover' | 'crop' | 'pad'; + gravity: 'auto' | 'left' | 'right' | 'top' | 'bottom'; + opacity: number; + repeat: true | false | 'x' | 'y'; + anchor: 'center' | 'top' | 'left' | 'bottom' | 'right' | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'; + offsetTop: number | null; + offsetLeft: number | null; + offsetBottom: number | null; + offsetRight: number | null; + backgroundColor: string | null; + rotate: number | null; + } | null, + }, sound_masterVolume: { where: 'device',