1
1
mirror of https://github.com/kokonect-link/cherrypick synced 2024-11-23 22:56:53 +09:00

feat: 캡션 미설정 안내 표시 (1673beta/cherrypick#142)

- 노트를 게시하기 전에 첨부한 파일에 캡션이 없으면 경고를 표시합니다.
  - 이 변경으로 이미지 뷰어의 파일 이름 영역에는 더 이상 캡션이 아닌 실제 파일 이름이 표시됩니다.
This commit is contained in:
NoriDev 2024-10-03 10:31:03 +09:00
parent 1a5f3d963b
commit 7702ff29c1
15 changed files with 142 additions and 13 deletions

View File

@ -45,6 +45,9 @@ Misskey의 전체 변경 사항을 확인하려면, [CHANGELOG.md#2024xx](CHANGE
- Feat: 답글 대상 노트의 반투명 옵션을 선택할 수 있음 (kokonect-link/cherrypick#495)
- Feat: 사용자 페이지의 미디어 탭을 그리드 레이아웃으로 설정할 수 있음 (kokonect-link/cherrypick#494)
- Feat: 검색 위젯 (1673beta/cherrypick#125)
- Feat: 캡션 미설정 안내 표시 (1673beta/cherrypick#142)
- 노트를 게시하기 전에 첨부한 파일에 캡션이 없으면 경고를 표시합니다.
- 이 변경으로 이미지 뷰어의 파일 이름 영역에는 더 이상 캡션이 아닌 실제 파일 이름이 표시됩니다.
### Client
- Enhance: CherryPick 업데이트 페이지를 제어판 목록에 추가함
@ -55,7 +58,13 @@ Misskey의 전체 변경 사항을 확인하려면, [CHANGELOG.md#2024xx](CHANGE
- Enhance: 설정 페이지 개선
- 일반 설정에 있던 설정 중 디자인과 관련된 설정을 모양으로 옮겼습니다.
- Enhance: 외부 사이트로 이동할 때 경고 표시 (MisskeyIO/misskey#558)
- Enhance: 이미지의 확장자를 더욱 정확하게 표시함
- APNG 형식의 이미지가 GIF로 표시되던 것을 APNG로 표시하도록 변경
- Enhance: 이미지 뷰어가 파일 이름과 캡션을 동시에 표시하도록 변경
- Enhance: 노트를 작성할 때 첨부한 파일에 캡션이 존재하는 경우 파일 목록에 아이콘을 표시함
- Enhance: 미디어 숨기기 버튼의 디자인을 개선함
- Fix: 환경설정 백업 시 일부 설정이 누락되어 백업될 수 있음
- Fix: 이미지 자르기를 할 때 이미지 크기 전체를 표시하지 못할 수 있음
---

View File

@ -1,5 +1,7 @@
---
_lang_: "English"
showNoAltWarning: "Show caption unset warning"
showNoAltWarningDescription: "Display a warning when no alternate text is set in the image"
filesGridLayoutInUserPage: "Change the media tab to grid layout"
filesGridLayoutInUserPageDescription: "When this function is turned on, the Media tab on your page is shown in album format.\nTurning it off will change to the original note timeline."
showReplyTargetNoteInSemiTransparent: "Show reply target note in semi-transparent"
@ -1479,8 +1481,8 @@ _cherrypick:
reactableRemoteReaction: "Allow remote custom emoji reactions to react if there is an emoji with the same name on this server."
showFollowingMessageInsteadOfButton: "Do not show the follow button in the notification field if you are already following someone"
mobileHeaderChange: "Header design change in mobile environment"
renameTheButtonInPostFormToNya: "Change the \"Note\" button on the note-posting form to \"Nyan!\""
renameTheButtonInPostFormToNyaDescription: "Outside of the note-posting form, they are still as \"Note\"."
renameTheButtonInPostFormToNya: "Change the \"Note\" button on the posting form to \"Nyan!\""
renameTheButtonInPostFormToNyaDescription: "Outside of the posting form, they are still as \"Note\"."
enableWidgetsArea: "Enable the widgets area"
disableWidgetsArea: "Disable the widgets area"
friendlyUiEnableNotificationsArea: "Enable the notifications area"
@ -3016,3 +3018,6 @@ _externalNavigationWarning:
title: "Navigate to an external site"
description: "Leave {host} and go to an external site"
trustThisDomain: "Trust this domain on this device in the future"
_altWarning:
noAltWarning: "No alternate text is configured in the file."
noAltWarningDescription: "You can change this setting in \"Settings - Appearance\"."

18
locales/index.d.ts vendored
View File

@ -13,6 +13,14 @@ export interface Locale extends ILocale {
*
*/
"_lang_": string;
/**
*
*/
"showNoAltWarning": string;
/**
*
*/
"showNoAltWarningDescription": string;
/**
*
*/
@ -11794,6 +11802,16 @@ export interface Locale extends ILocale {
*/
"trustThisDomain": string;
};
"_altWarning": {
/**
*
*/
"noAltWarning": string;
/**
* -
*/
"noAltWarningDescription": string;
};
}
declare const locales: {
[lang: string]: Locale;

View File

@ -1,5 +1,7 @@
_lang_: "日本語"
showNoAltWarning: "キャプション未設定案内を表示"
showNoAltWarningDescription: "画像に代替テキストが設定されていない場合に警告を表示する"
filesGridLayoutInUserPage: "メディアタブをグリッドレイアウトに変更"
filesGridLayoutInUserPageDescription: "この設定をオンにすると、ユーザーページのメディアタブがアルバム形式で表示されます。\nオフにすると、元のートのタイムラインに変更されます。"
showReplyTargetNoteInSemiTransparent: "返信対象ノートを半透明に表示"
@ -3140,3 +3142,7 @@ _externalNavigationWarning:
title: "外部サイトに移動します"
description: "{host}を離れて外部サイトに移動します"
trustThisDomain: "このデバイスで今後このドメインを信頼する"
_altWarning:
noAltWarning: "ファイルに代替テキストが設定されていません。"
noAltWarningDescription: "この設定は「設定 - アピアランス」で変更できます。"

View File

@ -1,5 +1,7 @@
---
_lang_: "한국어"
showNoAltWarning: "캡션 미설정 안내 표시"
showNoAltWarningDescription: "이미지에 캡션이 설정되어 있지 않으면 경고를 표시해요"
filesGridLayoutInUserPage: "미디어 탭을 그리드 레이아웃으로 변경"
filesGridLayoutInUserPageDescription: "이 설정을 켜면 사용자 페이지의 미디어 탭이 앨범 형식으로 보여져요.\n끄면 원래의 노트 타임라인으로 변경돼요."
showReplyTargetNoteInSemiTransparent: "답글 대상 노트를 반투명하게 표시"
@ -3047,3 +3049,6 @@ _externalNavigationWarning:
title: "외부 사이트로 이동할까요?"
description: "{host}을(를) 떠나 외부 사이트로 이동하려고 해요"
trustThisDomain: "이 장치에서 앞으로 이 도메인을 신뢰할게요"
_altWarning:
noAltWarning: "파일에 캡션이 설정되어 있지 않아요."
noAltWarningDescription: "이 설정은 [설정 - 모양]에서 변경할 수 있어요."

View File

@ -113,6 +113,7 @@ const onImageLoad = () => {
loading.value = false;
if (cropper) {
cropper.getCropperCanvas();
cropper.getCropperImage()!.$center('contain');
cropper.getCropperSelection()!.$center();
}
@ -159,6 +160,7 @@ onMounted(() => {
width: var(--vw);
height: var(--vh);
position: relative;
object-fit: contain;
> .loading {
position: absolute;
@ -183,6 +185,7 @@ onMounted(() => {
> ::v-deep(cropper-canvas) {
width: 100%;
height: 100%;
object-fit: contain;
> cropper-selection > cropper-handle[action="move"] {
background: transparent;

View File

@ -11,7 +11,7 @@ SPDX-License-Identifier: AGPL-3.0-only
{ [$style.sensitiveHighlight]: highlightWhenSensitive && file.isSensitive },
]"
>
<ImgWithBlurhash v-if="isThumbnailAvailable" :hash="file.blurhash" :src="file.thumbnailUrl" :alt="file.name" :title="file.name" :cover="fit !== 'contain'"/>
<ImgWithBlurhash v-if="isThumbnailAvailable" :hash="file.blurhash" :src="file.thumbnailUrl" :alt="file.comment" :title="file.name" :cover="fit !== 'contain'" :showAltIndicator="showAltIndicator"/>
<i v-else-if="is === 'image'" class="ti ti-photo" :class="$style.icon"></i>
<i v-else-if="is === 'video'" class="ti ti-video" :class="$style.icon"></i>
<i v-else-if="is === 'audio' || is === 'midi'" class="ti ti-file-music" :class="$style.icon"></i>
@ -34,6 +34,7 @@ const props = defineProps<{
file: Misskey.entities.DriveFile;
fit: 'cover' | 'contain';
highlightWhenSensitive?: boolean;
showAltIndicator?: boolean;
}>();
const is = computed(() => {

View File

@ -16,6 +16,7 @@ SPDX-License-Identifier: AGPL-3.0-only
>
<canvas v-show="hide" key="canvas" ref="canvas" :class="$style.canvas" :width="canvasWidth" :height="canvasHeight" :title="title ?? undefined" tabindex="-1"/>
<img v-show="!hide" key="img" ref="img" :height="imgHeight ?? undefined" :width="imgWidth ?? undefined" :class="[$style.img, { [$style.noDrag]: noDrag }]" :src="src ?? undefined" :title="title ?? undefined" :alt="alt ?? undefined" loading="eager" decoding="async" tabindex="-1"/>
<i v-if="alt && showAltIndicator" :class="$style.altIndicator" class="ti ti-alt"></i>
</TransitionGroup>
</div>
</template>
@ -82,6 +83,7 @@ const props = withDefaults(defineProps<{
forceBlurhash?: boolean;
onlyAvgColor?: boolean; // Blurhash使
noDrag?: boolean;
showAltIndicator?: boolean;
}>(), {
transition: null,
src: null,
@ -93,6 +95,7 @@ const props = withDefaults(defineProps<{
forceBlurhash: false,
onlyAvgColor: false,
noDrag: false,
showAltIndicator: false,
});
const viewId = uuid();
@ -269,4 +272,21 @@ onUnmounted(() => {
-webkit-user-drag: none;
}
}
.altIndicator {
display: flex;
gap: 4px;
position: absolute;
border-radius: 8px;
overflow: hidden;
top: 2.5px;
right: 2px;
background-color: var(--bg);
-webkit-backdrop-filter: var(--blur, blur(15px));
backdrop-filter: var(--blur, blur(15px));
color: var(--accent);
font-size: 1em;
padding: 2px 4px;
text-align: center;
}
</style>

View File

@ -23,7 +23,7 @@ SPDX-License-Identifier: AGPL-3.0-only
:forceBlurhash="hide"
:cover="hide || cover"
:alt="image.comment || image.name"
:title="image.comment || image.name"
:title="image.name"
:width="image.properties.width"
:height="image.properties.height"
:style="hide ? 'filter: brightness(0.7);' : null"
@ -44,7 +44,8 @@ SPDX-License-Identifier: AGPL-3.0-only
</template>
<template v-else-if="controls">
<div :class="$style.indicators">
<div v-if="['image/gif', 'image/apng'].includes(image.type)" :class="$style.indicator">GIF</div>
<div v-if="['image/gif'].includes(image.type)" :class="$style.indicator">GIF</div>
<div v-if="['image/apng'].includes(image.type)" :class="$style.indicator">APNG</div>
<div v-if="image.comment" :class="$style.indicator">ALT</div>
<div v-if="image.isSensitive" :class="$style.indicator" style="color: var(--warn);" :title="i18n.ts.sensitive"><i class="ti ti-eye-exclamation"></i></div>
</div>
@ -232,10 +233,10 @@ onUnmounted(() => {
display: block;
position: absolute;
border-radius: 6px;
background-color: var(--fg);
background-color: var(--bg);
color: var(--accentLighten);
font-size: 12px;
opacity: .5;
font-size: 18px;
opacity: .7;
padding: 5px 8px;
text-align: center;
cursor: pointer;

View File

@ -153,8 +153,9 @@ onMounted(() => {
[itemData.w, itemData.h] = [itemData.h, itemData.w];
}
itemData.msrc = file.thumbnailUrl ?? undefined;
itemData.alt = file.comment ?? file.name;
itemData.alt = file.comment ?? undefined;
itemData.comment = file.comment ?? file.name;
itemData.title = file.name;
itemData.thumbCropped = true;
return itemData;
@ -180,6 +181,25 @@ onMounted(() => {
pswp.on('change', () => {
textBox.textContent = pswp.currSlide?.data.comment;
const altText = pswp.currSlide?.data.alt || null;
textBox.textContent = altText;
if (!altText) {
el.style.display = 'none';
}
});
},
});
lightbox?.pswp?.ui?.registerElement({
name: 'fileName',
className: 'pswp__file-name-container',
appendTo: 'wrapper',
onInit: (el, pswp) => {
const textBox = document.createElement('p');
textBox.className = 'pswp__file-name _acrylic';
el.appendChild(textBox);
pswp.on('change', () => {
textBox.textContent = pswp.currSlide?.data.title;
});
},
});
@ -339,12 +359,20 @@ defineExpose({
align-items: center;
position: absolute;
bottom: 20px;
bottom: 100px;
left: 50%;
transform: translateX(-50%);
width: 75%;
max-width: 800px;
z-index: 2;
}
.pswp__file-name-container {
@extend .pswp__alt-text-container;
bottom: 20px;
z-index: 1;
}
.pswp__alt-text {
@ -358,4 +386,9 @@ defineExpose({
text-shadow: var(--bg) 0 0 10px, var(--bg) 0 0 3px, var(--bg) 0 0 3px;
white-space: pre-line;
}
.pswp__file-name {
@extend .pswp__alt-text;
max-height: 16em;
}
</style>

View File

@ -186,7 +186,7 @@ const visibilityButton = shallowRef<HTMLElement>();
const posting = ref(false);
const posted = ref(false);
const text = ref(props.initialText ?? '');
const files = ref(props.initialFiles ?? []);
const files = ref(props.initialFiles ?? ([] as Misskey.entities.DriveFile[]));
const poll = ref<PollEditorModelValue | null>(null);
const event = ref<{
title: string;
@ -785,6 +785,17 @@ async function post(ev?: MouseEvent) {
return;
}
if (defaultStore.state.showNoAltTextWarning && files.value.some((f) => f.comment == null || f.comment.length === 0)) {
const { canceled } = await os.confirm({
type: 'warning',
text: i18n.ts._altWarning.noAltWarning,
caption: i18n.ts._altWarning.noAltWarningDescription,
okText: i18n.ts.goBack,
cancelText: i18n.ts.thisPostMayBeAnnoyingIgnore,
});
if (!canceled) return;
}
if (ev) {
const el = (ev.currentTarget ?? ev.target) as HTMLElement | null;

View File

@ -8,7 +8,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<Sortable :modelValue="props.modelValue" :class="$style.files" itemKey="id" :animation="150" :delay="100" :delayOnTouchOnly="true" @update:modelValue="v => emit('update:modelValue', v)">
<template #item="{element}">
<div :class="$style.file" @click="showFileMenu(element, $event)" @contextmenu.prevent="showFileMenu(element, $event)">
<MkDriveFileThumbnail :data-id="element.id" :class="$style.thumbnail" :file="element" fit="cover"/>
<MkDriveFileThumbnail :data-id="element.id" :class="$style.thumbnail" :file="element" fit="cover" :showAltIndicator="true"/>
<div v-if="element.isSensitive" :class="$style.sensitive">
<i class="ti ti-eye-exclamation" style="margin: auto;"></i>
</div>

View File

@ -204,7 +204,7 @@ const showForm = ref(false);
const posting = ref(false);
const posted = ref(false);
const text = ref(props.initialText ?? '');
const files = ref(props.initialFiles ?? []);
const files = ref(props.initialFiles ?? ([] as Misskey.entities.DriveFile[]));
const poll = ref<PollEditorModelValue | null>(null);
const event = ref<{
title: string;
@ -774,6 +774,17 @@ async function post(ev?: MouseEvent) {
return;
}
if (defaultStore.state.showNoAltTextWarning && files.value.some((f) => f.comment == null || f.comment.length === 0)) {
const { canceled } = await os.confirm({
type: 'warning',
text: i18n.ts._altWarning.noAltWarning,
caption: i18n.ts._altWarning.noAltWarningDescription,
okText: i18n.ts.goBack,
cancelText: i18n.ts.thisPostMayBeAnnoyingIgnore,
});
if (!canceled) return;
}
if (ev) {
const el = (ev.currentTarget ?? ev.target) as HTMLElement | null;

View File

@ -180,6 +180,7 @@ SPDX-License-Identifier: AGPL-3.0-only
</MkSelect>
<MkSwitch v-model="showFixedPostFormInReplies">{{ i18n.ts.showFixedPostFormInReplies }}<template #caption>{{ i18n.ts.showFixedPostFormInRepliesDescription }}</template> <span class="_beta">CherryPick</span></MkSwitch>
<MkSwitch v-model="allMediaNoteCollapse">{{ i18n.ts.allMediaNoteCollapse }} <span class="_beta">CherryPick</span></MkSwitch>
<MkSwitch v-model="showNoAltTextWarning">{{ i18n.ts.showNoAltWarning }}<template #caption>{{ i18n.ts.showNoAltWarningDescription }}</template> <span class="_beta">CherryPick</span></MkSwitch>
<MkSwitch v-model="alwaysShowCw">{{ i18n.ts.alwaysShowCw }} <span class="_beta">CherryPick</span></MkSwitch>
<MkSwitch v-model="showReplyTargetNoteInSemiTransparent">{{ i18n.ts.showReplyTargetNoteInSemiTransparent }} <span class="_beta">CherryPick</span></MkSwitch>
</div>
@ -330,6 +331,7 @@ const renoteQuoteButtonSeparation = computed(defaultStore.makeGetterSetter('reno
const showFixedPostFormInReplies = computed(defaultStore.makeGetterSetter('showFixedPostFormInReplies'));
const showingAnimatedImages = computed(defaultStore.makeGetterSetter('showingAnimatedImages'));
const allMediaNoteCollapse = computed(defaultStore.makeGetterSetter('allMediaNoteCollapse'));
const showNoAltTextWarning = computed(defaultStore.makeGetterSetter('showNoAltTextWarning'));
const alwaysShowCw = computed(defaultStore.makeGetterSetter('alwaysShowCw'));
const showReplyTargetNoteInSemiTransparent = computed(defaultStore.makeGetterSetter('showReplyTargetNoteInSemiTransparent'));
const nsfwOpenBehavior = computed(defaultStore.makeGetterSetter('nsfwOpenBehavior'));

View File

@ -614,6 +614,10 @@ export const defaultStore = markRaw(new Storage('base', {
where: 'device',
default: false,
},
showNoAltTextWarning: {
where: 'device',
default: true,
},
alwaysShowCw: {
where: 'device',
default: false,