enhance: ウォーターマーク機能をロールで制御可能に

This commit is contained in:
syuilo 2025-07-12 15:13:35 +09:00
parent d2c4f79886
commit 5ecaf5095e
11 changed files with 49 additions and 3 deletions

View File

@ -5,6 +5,7 @@
- Feat: クリップ内でノートを検索できるように - Feat: クリップ内でノートを検索できるように
- Feat: Playを検索できるように - Feat: Playを検索できるように
- Feat: モデレーションにおいて、特定のドライブファイルを添付しているチャットメッセージを一覧できるように - Feat: モデレーションにおいて、特定のドライブファイルを添付しているチャットメッセージを一覧できるように
- Enhance: ウォーターマーク機能をロールで制御可能に
### Client ### Client
- Feat: モデログを検索できるように - Feat: モデログを検索できるように

4
locales/index.d.ts vendored
View File

@ -7795,6 +7795,10 @@ export interface Locale extends ILocale {
* *
*/ */
"noteDraftLimit": string; "noteDraftLimit": string;
/**
* 使
*/
"watermarkAvailable": string;
}; };
"_condition": { "_condition": {
/** /**

View File

@ -2019,6 +2019,7 @@ _role:
uploadableFileTypes_caption: "MIMEタイプを指定します。改行で区切って複数指定できるほか、アスタリスク(*)でワイルドカード指定できます。(例: image/*)" uploadableFileTypes_caption: "MIMEタイプを指定します。改行で区切って複数指定できるほか、アスタリスク(*)でワイルドカード指定できます。(例: image/*)"
uploadableFileTypes_caption2: "ファイルによっては種別を判定できないことがあります。そのようなファイルを許可する場合は {x} を指定に追加してください。" uploadableFileTypes_caption2: "ファイルによっては種別を判定できないことがあります。そのようなファイルを許可する場合は {x} を指定に追加してください。"
noteDraftLimit: "サーバーサイドのノートの下書きの作成可能数" noteDraftLimit: "サーバーサイドのノートの下書きの作成可能数"
watermarkAvailable: "ウォーターマーク機能の使用可否"
_condition: _condition:
roleAssignedTo: "マニュアルロールにアサイン済み" roleAssignedTo: "マニュアルロールにアサイン済み"
isLocal: "ローカルユーザー" isLocal: "ローカルユーザー"

View File

@ -67,6 +67,7 @@ export type RolePolicies = {
chatAvailability: 'available' | 'readonly' | 'unavailable'; chatAvailability: 'available' | 'readonly' | 'unavailable';
uploadableFileTypes: string[]; uploadableFileTypes: string[];
noteDraftLimit: number; noteDraftLimit: number;
watermarkAvailable: boolean;
}; };
export const DEFAULT_POLICIES: RolePolicies = { export const DEFAULT_POLICIES: RolePolicies = {
@ -111,6 +112,7 @@ export const DEFAULT_POLICIES: RolePolicies = {
'audio/*', 'audio/*',
], ],
noteDraftLimit: 10, noteDraftLimit: 10,
watermarkAvailable: true,
}; };
@Injectable() @Injectable()
@ -433,6 +435,7 @@ export class RoleService implements OnApplicationShutdown, OnModuleInit {
return [...set]; return [...set];
}), }),
noteDraftLimit: calc('noteDraftLimit', vs => Math.max(...vs)), noteDraftLimit: calc('noteDraftLimit', vs => Math.max(...vs)),
watermarkAvailable: calc('watermarkAvailable', vs => vs.some(v => v === true)),
}; };
} }

View File

@ -313,6 +313,10 @@ export const packedRolePoliciesSchema = {
type: 'integer', type: 'integer',
optional: false, nullable: false, optional: false, nullable: false,
}, },
watermarkAvailable: {
type: 'boolean',
optional: false, nullable: false,
},
}, },
} as const; } as const;

View File

@ -112,6 +112,7 @@ export const ROLE_POLICIES = [
'chatAvailability', 'chatAvailability',
'uploadableFileTypes', 'uploadableFileTypes',
'noteDraftLimit', 'noteDraftLimit',
'watermarkAvailable',
] as const; ] as const;
export const MFM_TAGS = ['tada', 'jelly', 'twitch', 'shake', 'spin', 'jump', 'bounce', 'flip', 'x2', 'x3', 'x4', 'scale', 'position', 'fg', 'bg', 'border', 'font', 'blur', 'rainbow', 'sparkle', 'rotate', 'ruby', 'unixtime']; export const MFM_TAGS = ['tada', 'jelly', 'twitch', 'shake', 'spin', 'jump', 'bounce', 'flip', 'x2', 'x3', 'x4', 'scale', 'position', 'fg', 'bg', 'border', 'font', 'blur', 'rainbow', 'sparkle', 'rotate', 'ruby', 'unixtime'];

View File

@ -104,6 +104,8 @@ export function useUploader(options: {
multiple?: boolean; multiple?: boolean;
features?: UploaderFeatures; features?: UploaderFeatures;
} = {}) { } = {}) {
const $i = ensureSignin();
const events = new EventEmitter<{ const events = new EventEmitter<{
'itemUploaded': (ctx: { item: UploaderItem; }) => void; 'itemUploaded': (ctx: { item: UploaderItem; }) => void;
}>(); }>();
@ -132,7 +134,7 @@ export function useUploader(options: {
uploaded: null, uploaded: null,
uploadFailed: false, uploadFailed: false,
compressionLevel: prefer.s.defaultImageCompressionLevel, compressionLevel: prefer.s.defaultImageCompressionLevel,
watermarkPresetId: uploaderFeatures.value.watermark ? prefer.s.defaultWatermarkPresetId : null, watermarkPresetId: uploaderFeatures.value.watermark && $i.policies.watermarkAvailable ? prefer.s.defaultWatermarkPresetId : null,
file: markRaw(file), file: markRaw(file),
}); });
const reactiveItem = items.value.at(-1)!; const reactiveItem = items.value.at(-1)!;
@ -264,6 +266,7 @@ export function useUploader(options: {
if ( if (
uploaderFeatures.value.watermark && uploaderFeatures.value.watermark &&
$i.policies.watermarkAvailable &&
WATERMARK_SUPPORTED_TYPES.includes(item.file.type) && WATERMARK_SUPPORTED_TYPES.includes(item.file.type) &&
!item.preprocessing && !item.preprocessing &&
!item.uploading && !item.uploading &&
@ -500,7 +503,7 @@ export function useUploader(options: {
let preprocessedFile: Blob | File = item.file; let preprocessedFile: Blob | File = item.file;
const needsWatermark = item.watermarkPresetId != null && WATERMARK_SUPPORTED_TYPES.includes(preprocessedFile.type); const needsWatermark = item.watermarkPresetId != null && WATERMARK_SUPPORTED_TYPES.includes(preprocessedFile.type) && $i.policies.watermarkAvailable;
const preset = prefer.s.watermarkPresets.find(p => p.id === item.watermarkPresetId); const preset = prefer.s.watermarkPresets.find(p => p.id === item.watermarkPresetId);
if (needsWatermark && preset != null) { if (needsWatermark && preset != null) {
const canvas = window.document.createElement('canvas'); const canvas = window.document.createElement('canvas');

View File

@ -780,6 +780,26 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkRange> </MkRange>
</div> </div>
</MkFolder> </MkFolder>
<MkFolder v-if="matchQuery([i18n.ts._role._options.watermarkAvailable, 'watermarkAvailable'])">
<template #label>{{ i18n.ts._role._options.watermarkAvailable }}</template>
<template #suffix>
<span v-if="role.policies.watermarkAvailable.useDefault" :class="$style.useDefaultLabel">{{ i18n.ts._role.useBaseValue }}</span>
<span v-else>{{ role.policies.watermarkAvailable.value ? i18n.ts.yes : i18n.ts.no }}</span>
<span :class="$style.priorityIndicator"><i :class="getPriorityIcon(role.policies.watermarkAvailable)"></i></span>
</template>
<div class="_gaps">
<MkSwitch v-model="role.policies.watermarkAvailable.useDefault" :readonly="readonly">
<template #label>{{ i18n.ts._role.useBaseValue }}</template>
</MkSwitch>
<MkSwitch v-model="role.policies.watermarkAvailable.value" :disabled="role.policies.watermarkAvailable.useDefault" :readonly="readonly">
<template #label>{{ i18n.ts.enable }}</template>
</MkSwitch>
<MkRange v-model="role.policies.watermarkAvailable.priority" :min="0" :max="2" :step="1" easing :textConverter="(v) => v === 0 ? i18n.ts._role._priority.low : v === 1 ? i18n.ts._role._priority.middle : v === 2 ? i18n.ts._role._priority.high : ''">
<template #label>{{ i18n.ts._role.priority }}</template>
</MkRange>
</div>
</MkFolder>
</div> </div>
</FormSlot> </FormSlot>
</div> </div>

View File

@ -291,6 +291,14 @@ SPDX-License-Identifier: AGPL-3.0-only
<MkInput v-model="policies.noteDraftLimit" type="number" :min="0"> <MkInput v-model="policies.noteDraftLimit" type="number" :min="0">
</MkInput> </MkInput>
</MkFolder> </MkFolder>
<MkFolder v-if="matchQuery([i18n.ts._role._options.watermarkAvailable, 'watermarkAvailable'])">
<template #label>{{ i18n.ts._role._options.watermarkAvailable }}</template>
<template #suffix>{{ policies.watermarkAvailable ? i18n.ts.yes : i18n.ts.no }}</template>
<MkSwitch v-model="policies.watermarkAvailable">
<template #label>{{ i18n.ts.enable }}</template>
</MkSwitch>
</MkFolder>
</div> </div>
</MkFolder> </MkFolder>
<MkButton primary rounded @click="create"><i class="ti ti-plus"></i> {{ i18n.ts._role.new }}</MkButton> <MkButton primary rounded @click="create"><i class="ti ti-plus"></i> {{ i18n.ts._role.new }}</MkButton>

View File

@ -87,7 +87,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<div class="_gaps_m"> <div class="_gaps_m">
<SearchMarker :keywords="['watermark', 'credit']"> <SearchMarker :keywords="['watermark', 'credit']">
<MkFolder> <MkFolder v-if="$i.policies.watermarkAvailable">
<template #icon><i class="ti ti-copyright"></i></template> <template #icon><i class="ti ti-copyright"></i></template>
<template #label><SearchLabel>{{ i18n.ts.watermark }}</SearchLabel></template> <template #label><SearchLabel>{{ i18n.ts.watermark }}</SearchLabel></template>
<template #caption>{{ i18n.ts._watermarkEditor.tip }}</template> <template #caption>{{ i18n.ts._watermarkEditor.tip }}</template>

View File

@ -5225,6 +5225,7 @@ export type components = {
/** @enum {string} */ /** @enum {string} */
chatAvailability: 'available' | 'readonly' | 'unavailable'; chatAvailability: 'available' | 'readonly' | 'unavailable';
noteDraftLimit: number; noteDraftLimit: number;
watermarkAvailable: boolean;
}; };
ReversiGameLite: { ReversiGameLite: {
/** Format: id */ /** Format: id */