Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

enhance(client): 1枚だけのメディアリストの画像のアスペクト比を画像に応じて縦長にする #10452

Merged
merged 45 commits into from
Apr 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
99135b3
:v:
tamaina Apr 2, 2023
2f39c08
fix
tamaina Apr 2, 2023
73d43f4
Merge branch 'develop' into img-max
tamaina Apr 4, 2023
9306ba7
:v:
tamaina Apr 4, 2023
ac384b5
Merge branch 'develop' into img-max
tamaina Apr 4, 2023
13c8885
Merge branch 'develop' into img-max
tamaina Apr 6, 2023
d5653ba
422px上限
tamaina Apr 6, 2023
238c71c
334
tamaina Apr 6, 2023
b75a4cc
min-height: 130px
tamaina Apr 6, 2023
ce094c5
64px
tamaina Apr 6, 2023
47b90f0
fix
tamaina Apr 6, 2023
6fe0ce9
wip
tamaina Apr 9, 2023
64d4d3c
Merge branch 'develop' into img-max
tamaina Apr 9, 2023
91e2a4f
:v:
tamaina Apr 9, 2023
4751512
fix
tamaina Apr 9, 2023
4c3aa63
max-height: none
tamaina Apr 9, 2023
c9dd511
Merge branch 'develop' into img-max
tamaina Apr 12, 2023
08046ad
MkImgWithBlurHashでratioを計算する
tamaina Apr 12, 2023
af7d86f
wip
tamaina Apr 12, 2023
4188cc7
Merge branch 'develop' into img-max
tamaina Apr 12, 2023
15be36b
fix
tamaina Apr 12, 2023
e39d832
fix?
tamaina Apr 12, 2023
9cb4fbf
Revert "fix?"
tamaina Apr 12, 2023
c1d94a4
Revert "fix"
tamaina Apr 12, 2023
bb0036a
Revert "wip"
tamaina Apr 12, 2023
b01c22c
fix
tamaina Apr 12, 2023
22abfb2
Revert "Revert "wip""
tamaina Apr 12, 2023
0560487
Revert "Revert "fix""
tamaina Apr 12, 2023
3d5c068
Revert "Revert "fix?""
tamaina Apr 12, 2023
92201e7
fix
tamaina Apr 12, 2023
4af1f6b
use clamp
tamaina Apr 13, 2023
2cc7f5c
Merge branch 'develop' into img-max
tamaina Apr 13, 2023
e90da7d
readable
tamaina Apr 13, 2023
3a1b67c
Merge branch 'develop' into img-max
tamaina Apr 14, 2023
8dced67
Merge branch 'develop' into img-max
tamaina Apr 14, 2023
5e5f46f
add 1:1, 3:4
tamaina Apr 14, 2023
f45be59
moveComment
tamaina Apr 14, 2023
6c8da24
3:4 → 2:3
tamaina Apr 15, 2023
fc8ab48
fix
tamaina Apr 15, 2023
697808c
default
tamaina Apr 15, 2023
741717d
fallback
tamaina Apr 15, 2023
8e0fcd1
Revert "fallback"
tamaina Apr 15, 2023
4bbd7db
Merge branch 'develop' into img-max
tamaina Apr 15, 2023
1b08d51
Fix?(server): Content-Dispositionのパースでエラーが発生した場合にもダウンロードが完了するように
tamaina Apr 15, 2023
cc68f19
Merge branch 'develop' into img-max
tamaina Apr 15, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions locales/ja-JP.yml
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,8 @@ createAccount: "アカウントを作成"
existingAccount: "既存のアカウント"
regenerate: "再生成"
fontSize: "フォントサイズ"
mediaListWithOneImageAppearance: "画像が1枚のみのメディアリストの高さ"
limitTo: "{x}を上限に"
noFollowRequests: "フォロー申請はありません"
openImageInNewTab: "画像を新しいタブで開く"
dashboard: "ダッシュボード"
Expand Down
47 changes: 34 additions & 13 deletions packages/frontend/src/components/MkImgWithBlurhash.vue
Original file line number Diff line number Diff line change
@@ -1,46 +1,67 @@
<template>
<div :class="[$style.root, { [$style.cover]: cover }]" :title="title">
<canvas v-if="!loaded || forceBlurhash" ref="canvas" :class="$style.canvas" :width="size" :height="size" :title="title"/>
<img v-if="src && !forceBlurhash" :class="$style.img" :src="src" :title="title" :alt="alt" @load="onLoad"/>
<div :class="[$style.root, { [$style.cover]: cover }]" :title="title ?? ''">
<canvas v-if="!loaded || forceBlurhash" ref="canvas" :class="$style.canvas" :width="width" :height="height" :title="title ?? ''"/>
<img v-if="src && !forceBlurhash" v-show="loaded" :class="$style.img" :src="src" :title="title ?? ''" :alt="alt ?? ''" @load="onLoad"/>
</div>
</template>
<script lang="ts" setup>
import { onMounted } from 'vue';
import { onMounted, watch } from 'vue';
import { decode } from 'blurhash';
const props = withDefaults(defineProps<{
src?: string | null;
hash?: string;
alt?: string | null;
title?: string | null;
size?: number;
height?: number;
width?: number;
cover?: boolean;
forceBlurhash?: boolean;
}>(), {
src: null,
alt: '',
title: null,
size: 64,
height: 64,
width: 64,
cover: true,
forceBlurhash: false,
});
const canvas = $shallowRef<HTMLCanvasElement>();
let loaded = $ref(false);
let width = $ref(props.width);
let height = $ref(props.height);
function onLoad() {
loaded = true;
}
watch([() => props.width, () => props.height], () => {
const ratio = props.width / props.height;
if (ratio > 1) {
width = Math.round(64 * ratio);
height = 64;
} else {
width = 64;
height = Math.round(64 / ratio);
}
}, {
immediate: true,
});
function draw() {
if (props.hash == null) return;
const pixels = decode(props.hash, props.size, props.size);
const pixels = decode(props.hash, width, height);
const ctx = canvas.getContext('2d');
const imageData = ctx!.createImageData(props.size, props.size);
const imageData = ctx!.createImageData(width, height);
imageData.data.set(pixels);
ctx!.putImageData(imageData, 0, 0);
}
function onLoad() {
loaded = true;
}
watch(() => props.hash, () => {
draw();
});
onMounted(() => {
draw();
Expand All @@ -54,6 +75,7 @@ onMounted(() => {
height: 100%;
&.cover {
> .canvas,
> .img {
object-fit: cover;
}
Expand All @@ -68,8 +90,7 @@ onMounted(() => {
}
.canvas {
position: absolute;
object-fit: cover;
object-fit: contain;
}
.img {
Expand Down
9 changes: 5 additions & 4 deletions packages/frontend/src/components/MkMediaImage.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<template>
<div v-if="hide" :class="$style.hidden" @click="hide = false">
<ImgWithBlurhash style="filter: brightness(0.5);" :hash="image.blurhash" :title="image.comment" :alt="image.comment" :force-blurhash="defaultStore.state.enableDataSaverMode" />
<ImgWithBlurhash style="filter: brightness(0.5);" :hash="image.blurhash" :title="image.comment" :alt="image.comment" :width="image.properties.width" :height="image.properties.height" :force-blurhash="defaultStore.state.enableDataSaverMode" />
<div :class="$style.hiddenText">
<div :class="$style.hiddenTextWrapper">
<b v-if="image.isSensitive" style="display: block;"><i class="ti ti-alert-triangle"></i> {{ i18n.ts.sensitive }}{{ defaultStore.state.enableDataSaverMode ? ` (${i18n.ts.image}${image.size ? ' ' + bytes(image.size) : ''})` : '' }}</b>
Expand All @@ -15,7 +15,7 @@
:href="image.url"
:title="image.name"
>
<ImgWithBlurhash :hash="image.blurhash" :src="url" :alt="image.comment || image.name" :title="image.comment || image.name" :cover="false"/>
<ImgWithBlurhash :hash="image.blurhash" :src="url" :alt="image.comment || image.name" :title="image.comment || image.name" :width="image.properties.width" :height="image.properties.height" :cover="false"/>
</a>
<div :class="$style.indicators">
<div v-if="['image/gif', 'image/apng'].includes(image.type)" :class="$style.indicator">GIF</div>
Expand All @@ -42,11 +42,12 @@ const props = defineProps<{
let hide = $ref(true);
let darkMode: boolean = $ref(defaultStore.state.darkMode);
const url = (props.raw || defaultStore.state.loadRawImages)
const url = $computed(() => (props.raw || defaultStore.state.loadRawImages)
? props.image.url
: defaultStore.state.disableShowingAnimatedImages
? getStaticImageUrl(props.image.url)
: props.image.thumbnailUrl;
: props.image.thumbnailUrl
);
// Plugin:register_note_view_interruptor を使って書き換えられる可能性があるためwatchする
watch(() => props.image, () => {
Expand Down
77 changes: 70 additions & 7 deletions packages/frontend/src/components/MkMediaList.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,25 @@
<div>
<XBanner v-for="media in mediaList.filter(media => !previewable(media))" :key="media.id" :media="media"/>
<div v-if="mediaList.filter(media => previewable(media)).length > 0" :class="$style.container">
<div ref="gallery" :class="[$style.medias, count <= 4 ? $style['n' + count] : $style.nMany]">
<div
ref="gallery"
:class="[
$style.medias,
count <= 4 ? $style['n' + count] : $style.nMany,
$style[`n1${defaultStore.reactiveState.mediaListWithOneImageAppearance.value}`]
]"
>
<template v-for="media in mediaList.filter(media => previewable(media))">
<XVideo v-if="media.type.startsWith('video')" :key="media.id" :class="$style.media" :video="media"/>
<XImage v-else-if="media.type.startsWith('image')" :key="media.id" :class="$style.media" class="image" :data-id="media.id" :image="media" :raw="raw"/>
<XVideo v-if="media.type.startsWith('video')" :key="`video:${media.id}`" :class="$style.media" :video="media"/>
<XImage v-else-if="media.type.startsWith('image')" :key="`image:${media.id}`" :class="$style.media" class="image" :data-id="media.id" :image="media" :raw="raw"/>
</template>
</div>
</div>
</div>
</template>

<script lang="ts" setup>
import { onMounted, ref, useCssModule } from 'vue';
import { onMounted, ref, useCssModule, watch } from 'vue';
import * as misskey from 'misskey-js';
import PhotoSwipeLightbox from 'photoswipe/lightbox';
import PhotoSwipe from 'photoswipe';
Expand All @@ -23,6 +30,7 @@ import XImage from '@/components/MkMediaImage.vue';
import XVideo from '@/components/MkMediaVideo.vue';
import * as os from '@/os';
import { FILE_TYPE_BROWSERSAFE } from '@/const';
import { defaultStore } from '@/store';
const props = defineProps<{
mediaList: misskey.entities.DriveFile[];
Expand All @@ -31,11 +39,42 @@ const props = defineProps<{
const $style = useCssModule();
const gallery = ref(null);
const gallery = ref<HTMLDivElement>();
const pswpZIndex = os.claimZIndex('middle');
document.documentElement.style.setProperty('--mk-pswp-root-z-index', pswpZIndex.toString());
const count = $computed(() => props.mediaList.filter(media => previewable(media)).length);
function calcAspectRatio() {
if (!gallery.value) return;
let img = props.mediaList[0];
if (props.mediaList.length !== 1 || !(img.properties.width && img.properties.height)) {
gallery.value.style.aspectRatio = '';
return;
}
// アスペクト比上限設定では、横長の場合は高さを縮小させる
const ratioMax = (ratio: number) => `${Math.max(ratio, img.properties.width / img.properties.height).toString()} / 1`;
switch (defaultStore.state.mediaListWithOneImageAppearance) {
case '16_9':
gallery.value.style.aspectRatio = ratioMax(16 / 9);
break;
case '1_1':
gallery.value.style.aspectRatio = ratioMax(1);
break;
case '2_3':
gallery.value.style.aspectRatio = ratioMax(2 / 3);
break;
default:
gallery.value.style.aspectRatio = '';
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

この場合イメージのロードが終わってない時どうなりますか?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

canvasがwidth, height付きで挿入されているため問題ない

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

あ、aspectRatioの代入はイメージのロード関係ない

(img.propertiesは画像投稿時にデータベースに保存されている値であるため)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

意外と関係あったりします
(設定しないとロード終わるまでブラウザがサイズを知らないので)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ほんまか?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

要するとこうしたいです

const limit = defaultStore.state.mediaListWithOneImageAppearance === '16_9' ? (16 / 9) : 0;
gallery.value.style.aspectRatio = Math.max(limit, img.properties.width / img.properties.height).toString() + '/1';

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

出先でコード読めてないけどfileIdsしかもらわないんだっけ

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

一時的にイメージがないようにみえる

devtools inspectorでimgのsrc消してみたらシミュレートできます

  • 現在:サイズが変わらない
  • PR:小さくなる

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

あっ、でもロードする前にはblurhashを見せるようになっているので結局問題ないかも

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

canvasがwidth, height付きで挿入されているため問題ない

break;
}
}
watch([defaultStore.reactiveState.mediaListWithOneImageAppearance, gallery], () => calcAspectRatio());
onMounted(() => {
const lightbox = new PhotoSwipeLightbox({
dataSource: props.mediaList
Expand Down Expand Up @@ -155,12 +194,36 @@ const previewable = (file: misskey.entities.DriveFile): boolean => {
display: grid;
grid-gap: 8px;
// for webkit
height: 100%;
&.n1 {
aspect-ratio: 16/9;
grid-template-rows: 1fr;
// default (expand)
min-height: 64px;
max-height: clamp(
64px,
calc(var(--containerHeight, 100svh) * 0.5), // but --containerHeight can broken (too big)
min(334px, 50vh)
);
&.n116_9 {
min-height: none;
max-height: none;
aspect-ratio: 16 / 9; // fallback
}
&.n11_1{
min-height: none;
max-height: none;
aspect-ratio: 1 / 1; // fallback
}
&.n12_3 {
min-height: none;
max-height: none;
aspect-ratio: 2 / 3; // fallback
}
}
&.n2 {
Expand Down
2 changes: 1 addition & 1 deletion packages/frontend/src/components/MkRadio.vue
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import { } from 'vue';
const props = defineProps<{
modelValue: any;
value: any;
disabled: boolean;
disabled?: boolean;
}>();
const emit = defineEmits<{
Expand Down
14 changes: 8 additions & 6 deletions packages/frontend/src/components/MkRadios.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script lang="ts">
import { defineComponent, h } from 'vue';
import { VNode, defineComponent, h } from 'vue';
import MkRadio from './MkRadio.vue';
export default defineComponent({
Expand All @@ -22,31 +22,33 @@ export default defineComponent({
},
},
render() {
console.log(this.$slots, this.$slots.label && this.$slots.label());
if (!this.$slots.default) return null;
let options = this.$slots.default();
const label = this.$slots.label && this.$slots.label();
const caption = this.$slots.caption && this.$slots.caption();
// なぜかFragmentになることがあるため
if (options.length === 1 && options[0].props == null) options = options[0].children;
if (options.length === 1 && options[0].props == null) options = options[0].children as VNode[];
return h('div', {
class: 'novjtcto',
}, [
...(label ? [h('div', {
class: 'label',
}, [label])] : []),
}, label)] : []),
h('div', {
class: 'body',
}, options.map(option => h(MkRadio, {
key: option.key,
value: option.props.value,
value: option.props?.value,
modelValue: this.value,
'onUpdate:modelValue': value => this.value = value,
}, option.children)),
}, () => option.children)),
),
...(caption ? [h('div', {
class: 'caption',
}, [caption])] : []),
}, caption)] : []),
]);
},
});
Expand Down
9 changes: 9 additions & 0 deletions packages/frontend/src/pages/settings/general.vue
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,14 @@
<option value="2"><span style="font-size: 16px;">Aa</span></option>
<option value="3"><span style="font-size: 17px;">Aa</span></option>
</MkRadios>

<MkRadios v-model="mediaListWithOneImageAppearance">
<template #label>{{ i18n.ts.mediaListWithOneImageAppearance }}</template>
<option value="expand">{{ i18n.ts.default }}</option>
<option value="16_9">{{ i18n.t('limitTo', { x: '16:9' }) }}</option>
<option value="1_1">{{ i18n.t('limitTo', { x: '1:1' }) }}</option>
<option value="2_3">{{ i18n.t('limitTo', { x: '2:3' }) }}</option>
</MkRadios>
</div>
</FormSection>

Expand Down Expand Up @@ -172,6 +180,7 @@ const enableInfiniteScroll = computed(defaultStore.makeGetterSetter('enableInfin
const useReactionPickerForContextMenu = computed(defaultStore.makeGetterSetter('useReactionPickerForContextMenu'));
const squareAvatars = computed(defaultStore.makeGetterSetter('squareAvatars'));
const aiChanMode = computed(defaultStore.makeGetterSetter('aiChanMode'));
const mediaListWithOneImageAppearance = computed(defaultStore.makeGetterSetter('mediaListWithOneImageAppearance'));
watch(lang, () => {
miLocalStorage.setItem('lang', lang.value as string);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ const defaultStoreSaveKeys: (keyof typeof defaultStore['state'])[] = [
'squareAvatars',
'numberOfPageCache',
'aiChanMode',
'mediaListWithOneImageAppearance',
];
const coldDeviceStorageSaveKeys: (keyof typeof ColdDeviceStorage.default)[] = [
'lightTheme',
Expand Down
4 changes: 4 additions & 0 deletions packages/frontend/src/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,10 @@ export const defaultStore = markRaw(new Storage('base', {
where: 'device',
default: false,
},
mediaListWithOneImageAppearance: {
where: 'device',
default: 'expand' as 'expand' | '16_9' | '1_1' | '2_3',
},
}));

// TODO: 他のタブと永続化されたstateを同期
Expand Down