diff --git a/locales/en-US.yml b/locales/en-US.yml index e83883d3c..c6befb512 100644 --- a/locales/en-US.yml +++ b/locales/en-US.yml @@ -464,6 +464,9 @@ share: "Share" notFound: "Not found" notFoundDescription: "No page corresponding to this URL could be found." uploadFolder: "Default folder for uploads" +watermark: "Watermark" +useWatermark: "Use watermark" +useWatermarkDescription: "Add a watermark to images" markAsReadAllNotifications: "Mark all notifications as read" markAsReadAllUnreadNotes: "Mark all notes as read" markAsReadAllTalkMessages: "Mark all messages as read" @@ -778,8 +781,11 @@ makeExplorable: "Make account visible in \"Explore\"" makeExplorableDescription: "If you turn this off, your account will not show up in the \"Explore\" section." showGapBetweenNotesInTimeline: "Show a gap between posts on the timeline" duplicate: "Duplicate" -left: "Left" +top: "Top" +bottom: "Bottom" center: "Center" +left: "Left" +right: "Right" wide: "Wide" narrow: "Narrow" reloadToApplySetting: "This setting will only apply after a page reload. Reload now?" @@ -1104,14 +1110,23 @@ editMemo: "Edit memo" reactionsList: "Reactions" renotesList: "Renotes" notificationDisplay: "Notifications" +placement: "Placement" leftTop: "Top left" +centerTop: "Top center" rightTop: "Top right" +leftCenter: "Center left" +rightCenter: "Center right" leftBottom: "Bottom left" +centerBottom: "Bottom center" rightBottom: "Bottom right" stackAxis: "Stacking direction" vertical: "Vertical" horizontal: "Horizontal" position: "Position" +repeat: "Repeat" +enlargement: "Enlargement" +rotate: "Rotate" +opacity: "Opacity" serverRules: "Server rules" pleaseConfirmBelowBeforeSignup: "To register on this server, you must review and agree to the following:" pleaseAgreeAllToContinue: "You must agree to all above fields to continue." @@ -1811,10 +1826,11 @@ _role: descriptionOfRateLimitFactor: "Lower rate limits are less restrictive, higher ones more restrictive. " canHideAds: "Can hide ads" canSearchNotes: "Usage of note search" - canUseTranslator: "Can use Translator" - avatarDecorationLimit: "Maximum number of avatar decorations that can be applied" - canUseDriveFileInSoundSettings: "Can use Drive File in Sound Settings" canUseReaction: "Can use reactions" + canUseTranslator: "Can use Translator" + canUseDriveFileInSoundSettings: "Can use Drive File in Sound Settings" + canUseWatermark: "Can use Watermark" + avatarDecorationLimit: "Maximum number of avatar decorations that can be applied" mutualLinkSectionLimit: "Maximum number of mutual link sections" mutualLinkLimit: "Maximum number of mutual links in a section" _condition: diff --git a/locales/index.d.ts b/locales/index.d.ts index eba90728a..c5edc1b7e 100644 --- a/locales/index.d.ts +++ b/locales/index.d.ts @@ -1884,6 +1884,18 @@ export interface Locale extends ILocale { * 既定アップロード先 */ "uploadFolder": string; + /** + * ウォーターマーク + */ + "watermark": string; + /** + * ウォーターマークをつける + */ + "useWatermark": string; + /** + * 画像にウォーターマークを追加します + */ + "useWatermarkDescription": string; /** * すべての通知を既読にする */ @@ -3137,13 +3149,25 @@ export interface Locale extends ILocale { */ "duplicate": string; /** - * 左 + * 上 */ - "left": string; + "top": string; + /** + * 下 + */ + "bottom": string; /** * 中央 */ "center": string; + /** + * 左 + */ + "left": string; + /** + * 右 + */ + "right": string; /** * 広い */ @@ -4443,18 +4467,38 @@ export interface Locale extends ILocale { * 通知の表示 */ "notificationDisplay": string; + /** + * 配置 + */ + "placement": string; /** * 左上 */ "leftTop": string; + /** + * 中上 + */ + "centerTop": string; /** * 右上 */ "rightTop": string; + /** + * 左中 + */ + "leftCenter": string; + /** + * 右中 + */ + "rightCenter": string; /** * 左下 */ "leftBottom": string; + /** + * 中下 + */ + "centerBottom": string; /** * 右下 */ @@ -4475,6 +4519,22 @@ export interface Locale extends ILocale { * 位置 */ "position": string; + /** + * 繰り返し + */ + "repeat": string; + /** + * 引き伸ばし + */ + "enlargement": string; + /** + * 回転 + */ + "rotate": string; + /** + * 透明度 + */ + "opacity": string; /** * サーバールール */ @@ -7091,6 +7151,10 @@ export interface Locale extends ILocale { * ノート検索の利用 */ "canSearchNotes": string; + /** + * リアクションの利用 + */ + "canUseReaction": string; /** * 翻訳機能の利用 */ @@ -7100,9 +7164,9 @@ export interface Locale extends ILocale { */ "canUseDriveFileInSoundSettings": string; /** - * リアクションの利用 + * ウォーターマークの利用 */ - "canUseReaction": string; + "canUseWatermark": string; /** * アイコンデコレーションの最大取付個数 */ diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 746854467..298666845 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -467,6 +467,9 @@ share: "共有" notFound: "見つかりません" notFoundDescription: "指定されたURLに該当するページはありませんでした。" uploadFolder: "既定アップロード先" +watermark: "ウォーターマーク" +useWatermark: "ウォーターマークをつける" +useWatermarkDescription: "画像にウォーターマークを追加します" markAsReadAllNotifications: "すべての通知を既読にする" markAsReadAllUnreadNotes: "すべての投稿を既読にする" markAsReadAllTalkMessages: "すべてのチャットを既読にする" @@ -780,8 +783,11 @@ makeExplorable: "アカウントを見つけやすくする" makeExplorableDescription: "オフにすると、「みつける」にアカウントが載らなくなります。" showGapBetweenNotesInTimeline: "タイムラインのノートを離して表示" duplicate: "複製" -left: "左" +top: "上" +bottom: "下" center: "中央" +left: "左" +right: "右" wide: "広い" narrow: "狭い" reloadToApplySetting: "設定はページリロード後に反映されます。今すぐリロードしますか?" @@ -1106,14 +1112,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: "続けるには、全ての「同意する」にチェックが入っている必要があります。" @@ -1826,9 +1841,10 @@ _role: descriptionOfRateLimitFactor: "小さいほど制限が緩和され、大きいほど制限が強化されます。" canHideAds: "広告の非表示" canSearchNotes: "ノート検索の利用" + canUseReaction: "リアクションの利用" canUseTranslator: "翻訳機能の利用" canUseDriveFileInSoundSettings: "サウンド設定でドライブのファイルを利用" - canUseReaction: "リアクションの利用" + canUseWatermark: "ウォーターマークの利用" avatarDecorationLimit: "アイコンデコレーションの最大取付個数" mutualLinkSectionLimit: "相互リンクのセクションの最大数" mutualLinkLimit: "セクション内の相互リンクの最大数" diff --git a/locales/ko-KR.yml b/locales/ko-KR.yml index e65ddaea2..567ecff21 100644 --- a/locales/ko-KR.yml +++ b/locales/ko-KR.yml @@ -463,6 +463,9 @@ share: "공유" notFound: "찾을 수 없습니다" notFoundDescription: "지정한 URL에 해당하는 페이지가 존재하지 않습니다." uploadFolder: "기본 업로드 위치" +watermark: "워터마크" +useWatermark: "워터마크 사용" +useWatermarkDescription: "이미지에 워터마크를 추가합니다" markAsReadAllNotifications: "모든 알림을 읽은 상태로 표시" markAsReadAllUnreadNotes: "모든 글을 읽은 상태로 표시" markAsReadAllTalkMessages: "모든 대화를 읽은 상태로 표시" @@ -777,8 +780,11 @@ makeExplorable: "\"발견하기\"에 내 계정 보이기" makeExplorableDescription: "비활성화하면 \"발견하기\"에 나의 계정을 표시하지 않습니다." showGapBetweenNotesInTimeline: "타임라인의 노트 사이를 띄워서 표시" duplicate: "복제" -left: "왼쪽" +top: "상단" +bottom: "하단" center: "가운데" +left: "왼쪽" +right: "오른쪽" wide: "넓게" narrow: "좁게" reloadToApplySetting: "이 설정을 적용하려면 페이지를 새로고침해야 합니다. 바로 새로고침하시겠습니까?" @@ -1103,14 +1109,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: "계속하시려면 모든 항목에 동의하십시오." @@ -1805,10 +1820,11 @@ _role: descriptionOfRateLimitFactor: "작을수록 제한이 완화되고, 클수록 제한이 강화됩니다." canHideAds: "광고 숨기기" canSearchNotes: "노트 검색 이용 가능 여부" + canUseReaction: "리액션 사용" canUseTranslator: "번역 기능의 사용" canUseDriveFileInSoundSettings: "사운드 설정에서 드라이브의 파일 사용 가능 여부" + canUseWatermark: "워터마크 기능의 사용" avatarDecorationLimit: "아바타 장식의 최대 붙임 개수" - canUseReaction: "리액션 사용" mutualLinkSectionLimit: "서로링크 섹션의 최대 수" mutualLinkLimit: "섹션 내의 서로링크의 최대 수" _condition: diff --git a/packages/backend/src/core/RoleService.ts b/packages/backend/src/core/RoleService.ts index 45e8b641c..ce3e71031 100644 --- a/packages/backend/src/core/RoleService.ts +++ b/packages/backend/src/core/RoleService.ts @@ -51,9 +51,10 @@ export type RolePolicies = { canManageCustomEmojis: boolean; canManageAvatarDecorations: boolean; canSearchNotes: boolean; + canUseReaction: boolean; canUseTranslator: boolean; canUseDriveFileInSoundSettings: boolean; - canUseReaction: boolean; + canUseWatermark: boolean; canHideAds: boolean; driveCapacityMb: number; alwaysMarkNsfw: boolean; @@ -92,9 +93,10 @@ export const DEFAULT_POLICIES: RolePolicies = { canManageCustomEmojis: false, canManageAvatarDecorations: false, canSearchNotes: false, + canUseReaction: true, canUseTranslator: true, canUseDriveFileInSoundSettings: false, - canUseReaction: true, + canUseWatermark: false, canHideAds: false, driveCapacityMb: 100, alwaysMarkNsfw: false, @@ -406,9 +408,10 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit { canManageCustomEmojis: calc('canManageCustomEmojis', vs => vs.some(v => v === true)), canManageAvatarDecorations: calc('canManageAvatarDecorations', vs => vs.some(v => v === true)), canSearchNotes: calc('canSearchNotes', vs => vs.some(v => v === true)), + canUseReaction: calc('canUseReaction', vs => vs.some(v => v === true)), canUseTranslator: calc('canUseTranslator', vs => vs.some(v => v === true)), canUseDriveFileInSoundSettings: calc('canUseDriveFileInSoundSettings', vs => vs.some(v => v === true)), - canUseReaction: calc('canUseReaction', vs => vs.some(v => v === true)), + canUseWatermark: calc('canUseWatermark', vs => vs.some(v => v === true)), canHideAds: calc('canHideAds', vs => vs.some(v => v === true)), driveCapacityMb: calc('driveCapacityMb', vs => Math.max(...vs)), alwaysMarkNsfw: calc('alwaysMarkNsfw', vs => vs.some(v => v === true)), diff --git a/packages/backend/src/models/json-schema/role.ts b/packages/backend/src/models/json-schema/role.ts index 166a085f5..6afc62a67 100644 --- a/packages/backend/src/models/json-schema/role.ts +++ b/packages/backend/src/models/json-schema/role.ts @@ -240,6 +240,10 @@ export const packedRolePoliciesSchema = { type: 'boolean', optional: false, nullable: false, }, + canUseReaction: { + type: 'boolean', + optional: false, nullable: false, + }, canUseTranslator: { type: 'boolean', optional: false, nullable: false, @@ -248,7 +252,7 @@ export const packedRolePoliciesSchema = { type: 'boolean', optional: false, nullable: false, }, - canUseReaction: { + canUseWatermark: { type: 'boolean', optional: false, nullable: false, }, diff --git a/packages/backend/src/server/api/EndpointsModule.ts b/packages/backend/src/server/api/EndpointsModule.ts index 7b564affe..b12eb9ea6 100644 --- a/packages/backend/src/server/api/EndpointsModule.ts +++ b/packages/backend/src/server/api/EndpointsModule.ts @@ -396,6 +396,7 @@ import * as ep___reversi_invitations from './endpoints/reversi/invitations.js'; import * as ep___reversi_showGame from './endpoints/reversi/show-game.js'; import * as ep___reversi_surrender from './endpoints/reversi/surrender.js'; import * as ep___reversi_verify from './endpoints/reversi/verify.js'; +import * as ep___watermark_update from './endpoints/watermark/update.js'; import { GetterService } from './GetterService.js'; import { ApiLoggerService } from './ApiLoggerService.js'; import type { Provider } from '@nestjs/common'; @@ -790,6 +791,7 @@ const $reversi_invitations: Provider = { provide: 'ep:reversi/invitations', useC const $reversi_showGame: Provider = { provide: 'ep:reversi/show-game', useClass: ep___reversi_showGame.default }; const $reversi_surrender: Provider = { provide: 'ep:reversi/surrender', useClass: ep___reversi_surrender.default }; const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep___reversi_verify.default }; +const $watermark_update: Provider = { provide: 'ep:watermark/update', useClass: ep___watermark_update.default }; @Module({ imports: [ @@ -1188,6 +1190,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__ $reversi_showGame, $reversi_surrender, $reversi_verify, + $watermark_update, ], exports: [ $admin_meta, @@ -1578,6 +1581,7 @@ const $reversi_verify: Provider = { provide: 'ep:reversi/verify', useClass: ep__ $reversi_showGame, $reversi_surrender, $reversi_verify, + $watermark_update, ], }) export class EndpointsModule {} diff --git a/packages/backend/src/server/api/endpoints.ts b/packages/backend/src/server/api/endpoints.ts index a603a7077..9e8cf642f 100644 --- a/packages/backend/src/server/api/endpoints.ts +++ b/packages/backend/src/server/api/endpoints.ts @@ -396,6 +396,7 @@ import * as ep___reversi_invitations from './endpoints/reversi/invitations.js'; import * as ep___reversi_showGame from './endpoints/reversi/show-game.js'; import * as ep___reversi_surrender from './endpoints/reversi/surrender.js'; import * as ep___reversi_verify from './endpoints/reversi/verify.js'; +import * as ep___watermark_update from './endpoints/watermark/update.js'; const eps = [ ['admin/meta', ep___admin_meta], @@ -788,6 +789,7 @@ const eps = [ ['reversi/show-game', ep___reversi_showGame], ['reversi/surrender', ep___reversi_surrender], ['reversi/verify', ep___reversi_verify], + ['watermark/update', ep___watermark_update], ]; interface IEndpointMetaBase { diff --git a/packages/backend/src/server/api/endpoints/drive/files/create.ts b/packages/backend/src/server/api/endpoints/drive/files/create.ts index 3276d99f5..039e83592 100644 --- a/packages/backend/src/server/api/endpoints/drive/files/create.ts +++ b/packages/backend/src/server/api/endpoints/drive/files/create.ts @@ -86,6 +86,7 @@ export const paramDef = { comment: { type: 'string', nullable: true, maxLength: DB_MAX_IMAGE_COMMENT_LENGTH, default: null }, isSensitive: { type: 'boolean', default: false }, force: { type: 'boolean', default: false }, + watermark: { type: 'boolean' }, }, required: [], } as const; diff --git a/packages/backend/src/server/api/endpoints/watermark/update.ts b/packages/backend/src/server/api/endpoints/watermark/update.ts new file mode 100644 index 000000000..a88318fc7 --- /dev/null +++ b/packages/backend/src/server/api/endpoints/watermark/update.ts @@ -0,0 +1,42 @@ +import { Injectable } from '@nestjs/common'; +import { Endpoint } from '@/server/api/endpoint-base.js'; + +export const meta = { + tags: ['account'], + + requireCredential: true, + requireRolePolicy: 'canUpdateContent', + + kind: 'write:account', +} as const; + +export const paramDef = { + type: 'object', + properties: { + fileId: { type: 'string', format: 'misskey:id' }, + width: { type: 'integer' }, + height: { type: 'integer' }, + fit: { type: 'string', enum: ['scale-down', 'contain', 'cover', 'crop', 'pad'] }, + gravity: { type: 'string', enum: ['auto', 'left', 'right', 'top', 'bottom'] }, + opacity: { type: 'number' }, + repeat: { type: 'string', enum: ['true', 'x', 'y'] }, + top: { type: 'number' }, + left: { type: 'number' }, + bottom: { type: 'number' }, + right: { type: 'number' }, + background: { type: 'string' }, + rotate: { type: 'number' }, + }, + required: ['fileId'], +} as const; + +// eslint-disable-next-line import/no-default-export +@Injectable() +export default class extends Endpoint { + constructor( + ) { + super(meta, paramDef, async (ps, me) => { + // THIS ENDPOINT IS STUB + }); + } +} diff --git a/packages/frontend/assets/default-watermark.png b/packages/frontend/assets/default-watermark.png new file mode 100644 index 000000000..e8edd64f6 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 19e2c67b0..3e9b83c5f 100644 --- a/packages/frontend/src/components/MkDrive.vue +++ b/packages/frontend/src/components/MkDrive.vue @@ -105,6 +105,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'; @@ -142,6 +143,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); @@ -385,7 +387,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); }); } @@ -618,7 +620,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/const.ts b/packages/frontend/src/const.ts index 32b0bb353..01d0f9d62 100644 --- a/packages/frontend/src/const.ts +++ b/packages/frontend/src/const.ts @@ -90,9 +90,10 @@ export const ROLE_POLICIES = [ 'canManageCustomEmojis', 'canManageAvatarDecorations', 'canSearchNotes', + 'canUseReaction', 'canUseTranslator', 'canUseDriveFileInSoundSettings', - 'canUseReaction', + 'canUseWatermark', 'canHideAds', 'driveCapacityMb', 'alwaysMarkNsfw', diff --git a/packages/frontend/src/pages/admin/roles.editor.vue b/packages/frontend/src/pages/admin/roles.editor.vue index 2665bfb90..d707f6923 100644 --- a/packages/frontend/src/pages/admin/roles.editor.vue +++ b/packages/frontend/src/pages/admin/roles.editor.vue @@ -523,6 +523,26 @@ SPDX-License-Identifier: AGPL-3.0-only + + + +
+ + + + + + + + + +
+
+