From 40f8b5e7f574ef24a9674297ae5912e3961dac2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?= <67428053+kakkokari-gtyih@users.noreply.github.com> Date: Tue, 14 Jan 2025 21:05:34 +0900 Subject: [PATCH] =?UTF-8?q?enhance(frontend):=20=E3=83=A6=E3=83=BC?= =?UTF-8?q?=E3=82=B6=E3=83=BC=E3=83=9A=E3=83=BC=E3=82=B8=E3=81=AB=E3=80=8C?= =?UTF-8?q?=E3=83=95=E3=82=A1=E3=82=A4=E3=83=AB=E3=80=8D=E3=82=BF=E3=83=96?= =?UTF-8?q?=E3=82=92=E6=96=B0=E8=A8=AD=20(#15130)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 投稿ã—ãŸãƒ•ã‚¡ã‚¤ãƒ«ã®ä¸€è¦§ã‚’プãƒãƒ•ã‚£ãƒ¼ãƒ«ãƒšãƒ¼ã‚¸å†…ã®ã‚¿ãƒ–ã§è¦‹ã‚Œã‚‹ã‚ˆã†ã«ã—ã¦ã¿ãŸ (Otaku-Social#14) * ギャラリー(ノート)ã®å–得方法を変更ã€ãƒšãƒ¼ã‚¸ãƒãƒ¼ã‚·ãƒ§ãƒ³ã«å¯¾å¿œ * ギャラリー(ノート)ãŒå‹•ä½œã—ãªã„å•é¡Œã‚’ä¿®æ£ * ギャラリー(ノート)ã®å称変更 * styles * GalleryFromPosts -> Files * fix * enhance: æ—¢å˜ã®ãƒ•ã‚¡ã‚¤ãƒ«ã‚³ãƒ³ãƒ†ãƒŠã®ã€Œã‚‚ã£ã¨è¦‹ã‚‹ã€ã‚’クリックã—ãŸã‚‰ãƒ•ã‚¡ã‚¤ãƒ«ã‚¿ãƒ–ã«é£›ã¹ã‚‹ã‚ˆã†ã« * Update Changelog * 共通化 * spdx * ãã®ä»–ã®ãƒ¡ãƒ‡ã‚£ã‚¢ãŒã¡ã‚ƒã‚“ã¨ãƒ—レビューã•ã‚Œã‚‹ã‚ˆã†ã« * fix(frontend): リストãŒã‚»ãƒ³ã‚·ãƒ†ã‚£ãƒ–è¨å®šã‚’考慮ã™ã‚‹ã‚ˆã†ã« * arrayã‚’setã«å¤‰æ›´ * remove unused imports * :art: * :art: * ç”»åƒä»¥å¤–ã®ãƒ•ã‚¡ã‚¤ãƒ«ã®ãƒ—レビューã«å¯¾å¿œã—ãŸã®ã§ã‚³ãƒ¡ãƒ³ãƒˆã‚’削除 * サムãƒã‚¤ãƒ«ã‚’MkDriveFileThumbnailã«çµ±ä¸€ * v-panelã«ç½®ãæ›ãˆ * lint --------- Co-authored-by: tmorio <morikapusan@morikapu-denki.com> Co-authored-by: tmorio <20278135+tmorio@users.noreply.github.com> Co-authored-by: syuilo <4439005+syuilo@users.noreply.github.com> --- CHANGELOG.md | 2 + .../frontend/src/components/MkContainer.vue | 10 +- .../src/components/MkDriveFileThumbnail.vue | 28 ++++-- .../src/components/MkNoteMediaGrid.vue | 99 +++++++++++++++++++ packages/frontend/src/pages/user/files.vue | 56 +++++++++++ packages/frontend/src/pages/user/home.vue | 8 +- .../frontend/src/pages/user/index.files.vue | 58 +++-------- packages/frontend/src/pages/user/index.vue | 8 +- 8 files changed, 216 insertions(+), 53 deletions(-) create mode 100644 packages/frontend/src/components/MkNoteMediaGrid.vue create mode 100644 packages/frontend/src/pages/user/files.vue diff --git a/CHANGELOG.md b/CHANGELOG.md index e44c15005e..90b0611af3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,8 @@ - Enhance: 照会ã«å¤±æ•—ã—ãŸå ´åˆã€ãã®ç†ç”±ã‚’表示ã™ã‚‹ã‚ˆã†ã« - Enhance: 連åˆãŒãƒ›ãƒ¯ã‚¤ãƒˆãƒªã‚¹ãƒˆåŒ–・無効化ã•ã‚Œã¦ã„るサーãƒãƒ¼å‘ã‘ã®ãƒ‡ã‚¶ã‚¤ãƒ³ä¿®æ£ - Enhance: AiScriptã®ã‚»ãƒ¼ãƒ–データを明示的ã«å‰Šé™¤ã™ã‚‹é–¢æ•°`Mk:remove`ã‚’è¿½åŠ +- Enhance: ノートã®æ·»ä»˜ãƒ•ã‚¡ã‚¤ãƒ«ã‚’一覧ã§é¡ã‚Œã‚‹ã€Œãƒ•ã‚¡ã‚¤ãƒ«ã€ã‚¿ãƒ–ã‚’è¿½åŠ + (Based on https://github.com/Otaku-Social/maniakey/pull/14) - Enhance: AiScriptã®æ‹¡å¼µAPI関数ã«ãŠã„ã¦å¼•æ•°ã®åž‹ãƒã‚§ãƒƒã‚¯ã‚’ã‚ˆã‚ŠåŽ³æ ¼ã« - Fix: ç”»é¢ã‚µã‚¤ã‚ºãŒå¤‰ã‚ã£ãŸéš›ã«ãƒŠãƒ“ゲーションãƒãƒ¼ãŒè‡ªå‹•ã§æŠ˜ã‚ŠãŸãŸã¾ã‚Œãªã„å•é¡Œã‚’ä¿®æ£ - Fix: サーãƒãƒ¼æƒ…å ±ãƒ¡ãƒ‹ãƒ¥ãƒ¼ã«åŒºåˆ‡ã‚Šç·šãŒä¸è¶³ã—ã¦ã„ãŸã®ã‚’ä¿®æ£ diff --git a/packages/frontend/src/components/MkContainer.vue b/packages/frontend/src/components/MkContainer.vue index f513795c56..6a278250fa 100644 --- a/packages/frontend/src/components/MkContainer.vue +++ b/packages/frontend/src/components/MkContainer.vue @@ -30,7 +30,7 @@ SPDX-License-Identifier: AGPL-3.0-only > <div v-show="showBody" ref="contentEl" :class="[$style.content, { [$style.omitted]: omitted }]"> <slot></slot> - <button v-if="omitted" :class="$style.fade" class="_button" @click="() => { ignoreOmit = true; omitted = false; }"> + <button v-if="omitted" :class="$style.fade" class="_button" @click="showMore"> <span :class="$style.fadeLabel">{{ i18n.ts.showMore }}</span> </button> </div> @@ -48,6 +48,7 @@ const props = withDefaults(defineProps<{ thin?: boolean; naked?: boolean; foldable?: boolean; + onUnfold?: () => boolean; // return false to prevent unfolding scrollable?: boolean; expanded?: boolean; maxHeight?: number | null; @@ -101,6 +102,13 @@ const omitObserver = new ResizeObserver((entries, observer) => { calcOmit(); }); +function showMore() { + if (props.onUnfold && !props.onUnfold()) return; + + ignoreOmit.value = true; + omitted.value = false; +} + onMounted(() => { watch(showBody, v => { if (!rootEl.value) return; diff --git a/packages/frontend/src/components/MkDriveFileThumbnail.vue b/packages/frontend/src/components/MkDriveFileThumbnail.vue index 3410a915c3..874d9b04cf 100644 --- a/packages/frontend/src/components/MkDriveFileThumbnail.vue +++ b/packages/frontend/src/components/MkDriveFileThumbnail.vue @@ -5,13 +5,21 @@ SPDX-License-Identifier: AGPL-3.0-only <template> <div - ref="thumbnail" - :class="[ - $style.root, - { [$style.sensitiveHighlight]: highlightWhenSensitive && file.isSensitive }, - ]" + v-panel + :class="[$style.root, { + [$style.sensitiveHighlight]: highlightWhenSensitive && file.isSensitive, + [$style.large]: large, + }]" > - <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.name" + :title="file.name" + :cover="fit !== 'contain'" + :forceBlurHash="forceBlurhash" + /> <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 +42,8 @@ const props = defineProps<{ file: Misskey.entities.DriveFile; fit: 'cover' | 'contain'; highlightWhenSensitive?: boolean; + forceBlurhash?: boolean; + large?: boolean; }>(); const is = computed(() => { @@ -60,7 +70,7 @@ const is = computed(() => { const isThumbnailAvailable = computed(() => { return props.file.thumbnailUrl - ? (is.value === 'image' as const || is.value === 'video') + ? (is.value === 'image' || is.value === 'video') : false; }); </script> @@ -101,4 +111,8 @@ const isThumbnailAvailable = computed(() => { font-size: 32px; color: #777; } + +.large .icon { + font-size: 40px; +} </style> diff --git a/packages/frontend/src/components/MkNoteMediaGrid.vue b/packages/frontend/src/components/MkNoteMediaGrid.vue new file mode 100644 index 0000000000..520421bfb7 --- /dev/null +++ b/packages/frontend/src/components/MkNoteMediaGrid.vue @@ -0,0 +1,99 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> + <template v-for="file in note.files"> + <div + v-if="(defaultStore.state.nsfw === 'force' || file.isSensitive) && defaultStore.state.nsfw !== 'ignore' && !showingFiles.has(file.id)" + :class="[$style.filePreview, { [$style.square]: square }]" + @click="showingFiles.add(file.id)" + > + <MkDriveFileThumbnail + :file="file" + fit="cover" + :highlightWhenSensitive="defaultStore.state.highlightSensitiveMedia" + :forceBlurhash="true" + :large="true" + :class="$style.file" + /> + <div :class="$style.sensitive"> + <div> + <div><i class="ti ti-eye-exclamation"></i> {{ i18n.ts.sensitive }}</div> + <div>{{ i18n.ts.clickToShow }}</div> + </div> + </div> + </div> + <MkA v-else :class="[$style.filePreview, { [$style.square]: square }]" :to="notePage(note)"> + <MkDriveFileThumbnail + :file="file" + fit="cover" + :highlightWhenSensitive="defaultStore.state.highlightSensitiveMedia" + :large="true" + :class="$style.file" + /> + </MkA> + </template> +</template> + +<script lang="ts" setup> +import { ref } from 'vue'; +import { notePage } from '@/filters/note.js'; +import { i18n } from '@/i18n.js'; +import * as Misskey from 'misskey-js'; +import { defaultStore } from '@/store.js'; + +import MkDriveFileThumbnail from '@/components/MkDriveFileThumbnail.vue'; + +defineProps<{ + note: Misskey.entities.Note; + square?: boolean; +}>(); + +const showingFiles = ref<Set<string>>(new Set()); +</script> + +<style lang="scss" module> +.square { + width: 100%; + height: auto; + aspect-ratio: 1; +} + +.filePreview { + position: relative; + height: 128px; + border-radius: calc(var(--MI-radius) / 2); + overflow: clip; + + &:hover { + text-decoration: none; + } + + &.square { + height: 100%; + } +} + +.file { + width: 100%; + height: 100%; + border-radius: calc(var(--MI-radius) / 2); +} + +.sensitive { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + display: grid; + place-items: center; + font-size: 0.8em; + color: #fff; + background: rgba(0, 0, 0, 0.5); + backdrop-filter: blur(5px); + cursor: pointer; +} +</style> diff --git a/packages/frontend/src/pages/user/files.vue b/packages/frontend/src/pages/user/files.vue new file mode 100644 index 0000000000..b6c7c1c777 --- /dev/null +++ b/packages/frontend/src/pages/user/files.vue @@ -0,0 +1,56 @@ +<!-- +SPDX-FileCopyrightText: syuilo and misskey-project +SPDX-License-Identifier: AGPL-3.0-only +--> + +<template> + <MkSpacer :contentMax="1100"> + <div :class="$style.root"> + <MkPagination v-slot="{items}" :pagination="pagination"> + <div :class="$style.stream"> + <MkNoteMediaGrid v-for="note in items" :note="note" square/> + </div> + </MkPagination> + </div> + </MkSpacer> +</template> + +<script lang="ts" setup> +import { computed } from 'vue'; +import * as Misskey from 'misskey-js'; + +import MkNoteMediaGrid from '@/components/MkNoteMediaGrid.vue'; +import MkPagination from '@/components/MkPagination.vue'; + +const props = defineProps<{ + user: Misskey.entities.UserDetailed; +}>(); + +const pagination = { + endpoint: 'users/notes' as const, + limit: 15, + params: computed(() => ({ + userId: props.user.id, + withFiles: true, + })), +}; +</script> + +<style lang="scss" module> +.root { + padding: 8px; +} + +.stream { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); + gap: var(--MI-marginHalf); +} + +@media screen and (min-width: 600px) { + .stream { + grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); + } + +} +</style> diff --git a/packages/frontend/src/pages/user/home.vue b/packages/frontend/src/pages/user/home.vue index 2794db2821..a6a49f0ab9 100644 --- a/packages/frontend/src/pages/user/home.vue +++ b/packages/frontend/src/pages/user/home.vue @@ -136,7 +136,7 @@ SPDX-License-Identifier: AGPL-3.0-only <MkInfo v-else-if="$i && $i.id === user.id">{{ i18n.ts.userPagePinTip }}</MkInfo> <template v-if="narrow"> <MkLazy> - <XFiles :key="user.id" :user="user"/> + <XFiles :key="user.id" :user="user" @unfold="emit('unfoldFiles')"/> </MkLazy> <MkLazy> <XActivity :key="user.id" :user="user"/> @@ -150,7 +150,7 @@ SPDX-License-Identifier: AGPL-3.0-only </div> </div> <div v-if="!narrow" class="sub _gaps" style="container-type: inline-size;"> - <XFiles :key="user.id" :user="user"/> + <XFiles :key="user.id" :user="user" @unfold="emit('unfoldFiles')"/> <XActivity :key="user.id" :user="user"/> </div> </div> @@ -212,6 +212,10 @@ const props = withDefaults(defineProps<{ disableNotes: false, }); +const emit = defineEmits<{ + (ev: 'unfoldFiles'): void; +}>(); + const router = useRouter(); const user = ref(props.user); diff --git a/packages/frontend/src/pages/user/index.files.vue b/packages/frontend/src/pages/user/index.files.vue index ce4d113cad..b5e5f29ade 100644 --- a/packages/frontend/src/pages/user/index.files.vue +++ b/packages/frontend/src/pages/user/index.files.vue @@ -4,30 +4,15 @@ SPDX-License-Identifier: AGPL-3.0-only --> <template> -<MkContainer :max-height="300" :foldable="true"> +<MkContainer :max-height="300" :foldable="true" :onUnfold="unfoldContainer"> <template #icon><i class="ti ti-photo"></i></template> <template #header>{{ i18n.ts.files }}</template> <div :class="$style.root"> <MkLoading v-if="fetching"/> - <div v-if="!fetching && files.length > 0" :class="$style.stream"> - <template v-for="file in files" :key="file.note.id + file.file.id"> - <div v-if="file.file.isSensitive && !showingFiles.includes(file.file.id)" :class="$style.img" @click="showingFiles.push(file.file.id)"> - <!-- TODO: ç”»åƒä»¥å¤–ã®ãƒ•ã‚¡ã‚¤ãƒ«ã«å¯¾å¿œ --> - <ImgWithBlurhash :class="$style.sensitiveImg" :hash="file.file.blurhash" :src="thumbnail(file.file)" :title="file.file.name" :forceBlurhash="true"/> - <div :class="$style.sensitive"> - <div> - <div><i class="ti ti-eye-exclamation"></i> {{ i18n.ts.sensitive }}</div> - <div>{{ i18n.ts.clickToShow }}</div> - </div> - </div> - </div> - <MkA v-else :class="$style.img" :to="notePage(file.note)"> - <!-- TODO: ç”»åƒä»¥å¤–ã®ãƒ•ã‚¡ã‚¤ãƒ«ã«å¯¾å¿œ --> - <ImgWithBlurhash :hash="file.file.blurhash" :src="thumbnail(file.file)" :title="file.file.name"/> - </MkA> - </template> + <div v-if="!fetching && notes.length > 0" :class="$style.stream"> + <MkNoteMediaGrid v-for="note in notes" :note="note"/> </div> - <p v-if="!fetching && files.length == 0" :class="$style.empty">{{ i18n.ts.nothing }}</p> + <p v-if="!fetching && notes.length == 0" :class="$style.empty">{{ i18n.ts.nothing }}</p> </div> </MkContainer> </template> @@ -35,45 +20,34 @@ SPDX-License-Identifier: AGPL-3.0-only <script lang="ts" setup> import { onMounted, ref } from 'vue'; import * as Misskey from 'misskey-js'; -import { getStaticImageUrl } from '@/scripts/media-proxy.js'; -import { notePage } from '@/filters/note.js'; import { misskeyApi } from '@/scripts/misskey-api.js'; import MkContainer from '@/components/MkContainer.vue'; -import ImgWithBlurhash from '@/components/MkImgWithBlurhash.vue'; -import { defaultStore } from '@/store.js'; import { i18n } from '@/i18n.js'; +import MkNoteMediaGrid from '@/components/MkNoteMediaGrid.vue'; const props = defineProps<{ user: Misskey.entities.UserDetailed; }>(); +const emit = defineEmits<{ + (ev: 'unfold'): void; +}>(); + const fetching = ref(true); -const files = ref<{ - note: Misskey.entities.Note; - file: Misskey.entities.DriveFile; -}[]>([]); -const showingFiles = ref<string[]>([]); +const notes = ref<Misskey.entities.Note[]>([]); -function thumbnail(image: Misskey.entities.DriveFile): string { - return defaultStore.state.disableShowingAnimatedImages - ? getStaticImageUrl(image.url) - : image.thumbnailUrl; +function unfoldContainer(): boolean { + emit('unfold'); + return false; } onMounted(() => { misskeyApi('users/notes', { userId: props.user.id, withFiles: true, - limit: 15, - }).then(notes => { - for (const note of notes) { - for (const file of note.files) { - files.value.push({ - note, - file, - }); - } - } + limit: 10, + }).then(_notes => { + notes.value = _notes; fetching.value = false; }); }); diff --git a/packages/frontend/src/pages/user/index.vue b/packages/frontend/src/pages/user/index.vue index 4787db5dbe..c43f6c76d9 100644 --- a/packages/frontend/src/pages/user/index.vue +++ b/packages/frontend/src/pages/user/index.vue @@ -9,10 +9,11 @@ SPDX-License-Identifier: AGPL-3.0-only <div> <div v-if="user"> <MkHorizontalSwipe v-model:tab="tab" :tabs="headerTabs"> - <XHome v-if="tab === 'home'" key="home" :user="user"/> + <XHome v-if="tab === 'home'" key="home" :user="user" @unfoldFiles="() => { tab = 'files'; }"/> <MkSpacer v-else-if="tab === 'notes'" key="notes" :contentMax="800" style="padding-top: 0"> <XTimeline :user="user"/> </MkSpacer> + <XFiles v-else-if="tab === 'files'" :user="user"/> <XActivity v-else-if="tab === 'activity'" key="activity" :user="user"/> <XAchievements v-else-if="tab === 'achievements'" key="achievements" :user="user"/> <XReactions v-else-if="tab === 'reactions'" key="reactions" :user="user"/> @@ -43,6 +44,7 @@ import { serverContext, assertServerContext } from '@/server-context.js'; const XHome = defineAsyncComponent(() => import('./home.vue')); const XTimeline = defineAsyncComponent(() => import('./index.timeline.vue')); +const XFiles = defineAsyncComponent(() => import('./files.vue')); const XActivity = defineAsyncComponent(() => import('./activity.vue')); const XAchievements = defineAsyncComponent(() => import('./achievements.vue')); const XReactions = defineAsyncComponent(() => import('./reactions.vue')); @@ -103,6 +105,10 @@ const headerTabs = computed(() => user.value ? [{ key: 'notes', title: i18n.ts.notes, icon: 'ti ti-pencil', +}, { + key: 'files', + title: i18n.ts.files, + icon: 'ti ti-photo', }, { key: 'activity', title: i18n.ts.activity, -- GitLab