summaryrefslogtreecommitdiff
path: root/packages/frontend
diff options
context:
space:
mode:
authorかっこかり <67428053+kakkokari-gtyih@users.noreply.github.com>2025-01-14 21:05:34 +0900
committerGitHub <noreply@github.com>2025-01-14 12:05:34 +0000
commit40f8b5e7f574ef24a9674297ae5912e3961dac2d (patch)
treeac59cada0b16a76f2f3460b9d277c8f238ad1483 /packages/frontend
parentfeat(backend): pgroongaに対応(configの構成変更あり) (#14978) (diff)
downloadsharkey-40f8b5e7f574ef24a9674297ae5912e3961dac2d.tar.gz
sharkey-40f8b5e7f574ef24a9674297ae5912e3961dac2d.tar.bz2
sharkey-40f8b5e7f574ef24a9674297ae5912e3961dac2d.zip
enhance(frontend): ユーザーページに「ファイル」タブを新設 (#15130)
* 投稿したファイルの一覧をプロフィールページ内のタブで見れるようにしてみた (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>
Diffstat (limited to 'packages/frontend')
-rw-r--r--packages/frontend/src/components/MkContainer.vue10
-rw-r--r--packages/frontend/src/components/MkDriveFileThumbnail.vue28
-rw-r--r--packages/frontend/src/components/MkNoteMediaGrid.vue99
-rw-r--r--packages/frontend/src/pages/user/files.vue56
-rw-r--r--packages/frontend/src/pages/user/home.vue8
-rw-r--r--packages/frontend/src/pages/user/index.files.vue58
-rw-r--r--packages/frontend/src/pages/user/index.vue8
7 files changed, 214 insertions, 53 deletions
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'));
@@ -104,6 +106,10 @@ const headerTabs = computed(() => user.value ? [{
title: i18n.ts.notes,
icon: 'ti ti-pencil',
}, {
+ key: 'files',
+ title: i18n.ts.files,
+ icon: 'ti ti-photo',
+}, {
key: 'activity',
title: i18n.ts.activity,
icon: 'ti ti-chart-line',