Skip to content

Commit

Permalink
enhance(frontend): ユーザーページに「ファイル」タブを新設 (#167)
Browse files Browse the repository at this point in the history
  • Loading branch information
kakkokari-gtyih authored Dec 16, 2024
1 parent d696cbb commit 01df944
Show file tree
Hide file tree
Showing 5 changed files with 198 additions and 50 deletions.
33 changes: 26 additions & 7 deletions packages/frontend/src/components/MkDriveFileThumbnail.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,21 @@ SPDX-License-Identifier: AGPL-3.0-only

<template>
<div
ref="thumbnail"
:class="[
$style.root,
{ [$style.sensitiveHighlight]: highlightWhenSensitive && file.isSensitive },
]"
:class="[$style.root, {
[$style.sensitiveHighlight]: highlightWhenSensitive && file.isSensitive,
[$style.bgIsPanel]: bgIsPanel,
[$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>
Expand All @@ -34,6 +42,9 @@ const props = defineProps<{
file: Misskey.entities.DriveFile;
fit: 'cover' | 'contain';
highlightWhenSensitive?: boolean;
bgIsPanel?: boolean;
forceBlurhash?: boolean;
large?: boolean;
}>();

const is = computed(() => {
Expand All @@ -60,7 +71,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>
Expand All @@ -72,6 +83,10 @@ const isThumbnailAvailable = computed(() => {
background: var(--MI_THEME-panel);
border-radius: 8px;
overflow: clip;

&.bgIsPanel {
background: var(--MI_THEME-bg);
}
}

.sensitiveHighlight::after {
Expand Down Expand Up @@ -101,4 +116,8 @@ const isThumbnailAvailable = computed(() => {
font-size: 32px;
color: #777;
}

.large .icon {
font-size: 40px;
}
</style>
102 changes: 102 additions & 0 deletions packages/frontend/src/components/MkNoteMediaGrid.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
<!--
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"
:bgIsPanel="bgIsPanel"
: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"
:bgIsPanel="bgIsPanel"
: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;
bgIsPanel?: 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>
56 changes: 56 additions & 0 deletions packages/frontend/src/pages/user/files.vue
Original file line number Diff line number Diff line change
@@ -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>
51 changes: 8 additions & 43 deletions packages/frontend/src/pages/user/index.files.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,71 +9,36 @@ SPDX-License-Identifier: AGPL-3.0-only
<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" bgIsPanel/>
</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>

<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 fetching = ref(true);
const files = ref<{
note: Misskey.entities.Note;
file: Misskey.entities.DriveFile;
}[]>([]);
const showingFiles = ref<string[]>([]);

function thumbnail(image: Misskey.entities.DriveFile): string {
return defaultStore.state.disableShowingAnimatedImages
? getStaticImageUrl(image.url)
: image.thumbnailUrl;
}
const notes = ref<Misskey.entities.Note[]>([]);

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;
});
});
Expand Down
6 changes: 6 additions & 0 deletions packages/frontend/src/pages/user/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
<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"/>
Expand Down Expand Up @@ -43,6 +44,7 @@ import { getServerContext } 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'));
Expand Down Expand Up @@ -102,6 +104,10 @@ const headerTabs = computed(() => user.value ? [{
key: 'notes',
title: i18n.ts.notes,
icon: 'ti ti-pencil',
}, {
key: 'file',
title: i18n.ts.files,
icon: 'ti ti-photo',
}, {
key: 'activity',
title: i18n.ts.activity,
Expand Down

0 comments on commit 01df944

Please sign in to comment.