feat (client): add "Media" tab to user page
https://maniakey.com/notes/9rsfrmqm1a
66a2d2fa5c
Co-authored-by: naskya <m@naskya.net>
This commit is contained in:
parent
d0901d77ab
commit
ea357b71a0
7 changed files with 375 additions and 1 deletions
|
@ -708,6 +708,7 @@ display: "Display"
|
|||
copy: "Copy"
|
||||
metrics: "Metrics"
|
||||
overview: "Overview"
|
||||
media: "Media"
|
||||
logs: "Logs"
|
||||
delayed: "Delayed"
|
||||
database: "Database"
|
||||
|
|
|
@ -638,6 +638,7 @@ display: "表示"
|
|||
copy: "コピー"
|
||||
metrics: "メトリクス"
|
||||
overview: "概要"
|
||||
media: "メディア"
|
||||
logs: "ログ"
|
||||
delayed: "遅延"
|
||||
database: "データベース"
|
||||
|
|
291
packages/client/src/components/MkNoteMedia.vue
Normal file
291
packages/client/src/components/MkNoteMedia.vue
Normal file
|
@ -0,0 +1,291 @@
|
|||
<template>
|
||||
<div v-size="{ max: [350] }" class="media">
|
||||
<button v-if="hide" class="hidden" @click="hide = false">
|
||||
<ImgWithBlurhash
|
||||
:hash="media.blurhash"
|
||||
:title="media.comment"
|
||||
:alt="media.comment"
|
||||
/>
|
||||
<div class="text">
|
||||
<div class="wrapper">
|
||||
<b style="display: block"
|
||||
><i :class="icon('ph-warning')"></i>
|
||||
{{ i18n.ts.sensitive }}</b
|
||||
>
|
||||
<span style="display: block">{{
|
||||
i18n.ts.clickToShow
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
<template v-else>
|
||||
<MkA :to="notePage(note)">
|
||||
<div v-if="media.type.startsWith('image')">
|
||||
<ImgWithBlurhash
|
||||
:hash="media.blurhash"
|
||||
:src="url"
|
||||
:alt="media.comment"
|
||||
:type="media.type"
|
||||
:cover="false"
|
||||
:largest-dimension="largestDimension"
|
||||
/>
|
||||
</div>
|
||||
<VuePlyr
|
||||
v-if="media.type.startsWith('video')"
|
||||
ref="plyr"
|
||||
:options="{
|
||||
controls: [],
|
||||
disableContextMenu: false,
|
||||
}"
|
||||
>
|
||||
<video
|
||||
:poster="media.thumbnailUrl"
|
||||
:aria-label="media.comment ?? undefined"
|
||||
preload="none"
|
||||
controls
|
||||
playsinline
|
||||
@contextmenu.stop
|
||||
>
|
||||
<source :src="media.url" :type="mediaType" />
|
||||
</video>
|
||||
</VuePlyr>
|
||||
</MkA>
|
||||
</template>
|
||||
<div class="buttons">
|
||||
<button
|
||||
v-if="media.comment"
|
||||
v-tooltip.noLabel="
|
||||
`${i18n.ts.alt}: ${
|
||||
media.comment.length > 200
|
||||
? media.comment.trim().slice(0, 200) + '...'
|
||||
: media.comment.trim()
|
||||
}`
|
||||
"
|
||||
:aria-label="i18n.ts.alt"
|
||||
class="_button"
|
||||
@click.stop="captionPopup"
|
||||
>
|
||||
<i :class="icon('ph-subtitles')"></i>
|
||||
</button>
|
||||
<button
|
||||
v-if="!hide"
|
||||
v-tooltip="i18n.ts.hide"
|
||||
class="_button"
|
||||
@click.stop="hide = true"
|
||||
>
|
||||
<i :class="icon('ph-eye-slash')"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { computed, ref, watch } from "vue";
|
||||
import VuePlyr from "vue-plyr";
|
||||
import "vue-plyr/dist/vue-plyr.css";
|
||||
import type { entities } from "firefish-js";
|
||||
import { getStaticImageUrl } from "@/scripts/get-static-image-url";
|
||||
import ImgWithBlurhash from "@/components/MkImgWithBlurhash.vue";
|
||||
import { defaultStore } from "@/store";
|
||||
import { i18n } from "@/i18n";
|
||||
import * as os from "@/os";
|
||||
import icon from "@/scripts/icon";
|
||||
import { notePage } from "@/filters/note";
|
||||
|
||||
const props = defineProps<{
|
||||
note: entities.Note;
|
||||
media: entities.DriveFile;
|
||||
loadRawFiles?: boolean;
|
||||
}>();
|
||||
|
||||
const hide = ref(true);
|
||||
|
||||
const plyr = ref();
|
||||
|
||||
const url =
|
||||
props.loadRawFiles || defaultStore.state.loadRawImages
|
||||
? props.media.url
|
||||
: defaultStore.state.disableShowingAnimatedImages &&
|
||||
props.media.type.startsWith("image")
|
||||
? getStaticImageUrl(props.media.thumbnailUrl)
|
||||
: props.media.thumbnailUrl;
|
||||
|
||||
const mediaType = computed(() => {
|
||||
return props.media.type === "video/quicktime"
|
||||
? "video/mp4"
|
||||
: props.media.type;
|
||||
});
|
||||
|
||||
let largestDimension: "width" | "height";
|
||||
|
||||
if (
|
||||
props.media.type.startsWith("image") &&
|
||||
props.media.properties?.width &&
|
||||
props.media.properties?.height
|
||||
) {
|
||||
largestDimension =
|
||||
props.media.properties.width > props.media.properties.height
|
||||
? "width"
|
||||
: "height";
|
||||
}
|
||||
function captionPopup() {
|
||||
os.alert({
|
||||
type: "info",
|
||||
text: props.media.comment,
|
||||
isPlaintext: true,
|
||||
});
|
||||
}
|
||||
|
||||
// Plugin:register_note_view_interruptor を使って書き換えられる可能性があるためwatchする
|
||||
watch(
|
||||
() => props.media,
|
||||
() => {
|
||||
hide.value =
|
||||
defaultStore.state.nsfw === "force"
|
||||
? true
|
||||
: props.media.isSensitive && defaultStore.state.nsfw !== "ignore";
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
immediate: true,
|
||||
},
|
||||
);
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.hidden {
|
||||
all: unset;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
|
||||
> .text {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 30px;
|
||||
box-sizing: border-box;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
|
||||
> .wrapper {
|
||||
display: table-cell;
|
||||
text-align: center;
|
||||
font-size: 0.8em;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
&:focus-visible {
|
||||
border: 2px solid var(--accent);
|
||||
}
|
||||
}
|
||||
|
||||
.media {
|
||||
position: relative;
|
||||
background: var(--bg);
|
||||
|
||||
--plyr-color-main: var(--accent);
|
||||
|
||||
> .buttons {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
position: absolute;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
z-index: 1;
|
||||
> * {
|
||||
background-color: var(--accentedBg);
|
||||
-webkit-backdrop-filter: var(--blur, blur(15px));
|
||||
backdrop-filter: var(--blur, blur(15px));
|
||||
color: var(--accent);
|
||||
font-size: 0.8em;
|
||||
padding: 6px 8px;
|
||||
text-align: center;
|
||||
}
|
||||
}
|
||||
|
||||
> a {
|
||||
display: flex;
|
||||
cursor: zoom-in;
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-position: center;
|
||||
background-size: contain;
|
||||
background-repeat: no-repeat;
|
||||
box-sizing: border-box;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
|
||||
&:focus-visible {
|
||||
border: 2px solid var(--accent);
|
||||
}
|
||||
|
||||
> .gif {
|
||||
background-color: var(--fg);
|
||||
border-radius: 6px;
|
||||
color: var(--accentLighten);
|
||||
display: inline-block;
|
||||
font-size: 14px;
|
||||
font-weight: bold;
|
||||
left: 12px;
|
||||
opacity: 0.5;
|
||||
padding: 0 6px;
|
||||
text-align: center;
|
||||
top: 12px;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
:deep(.plyr__controls) {
|
||||
contain: strict;
|
||||
height: 24px;
|
||||
box-sizing: content-box;
|
||||
}
|
||||
:deep(.plyr__volume) {
|
||||
display: flex;
|
||||
min-width: max-content;
|
||||
width: 110px;
|
||||
transition: width 0.2s cubic-bezier(0, 0, 0, 1);
|
||||
[data-plyr="volume"] {
|
||||
width: 0;
|
||||
flex-grow: 1;
|
||||
transition:
|
||||
margin 0.3s,
|
||||
opacity 0.2s 0.2s;
|
||||
}
|
||||
&:not(:hover):not(:focus-within) {
|
||||
width: 0px;
|
||||
transition: width 0.2s;
|
||||
[data-plyr="volume"] {
|
||||
margin-inline: 0px;
|
||||
opacity: 0;
|
||||
transition:
|
||||
margin 0.3s,
|
||||
opacity 0.1s;
|
||||
}
|
||||
}
|
||||
}
|
||||
&.max-width_350px {
|
||||
:deep(.plyr:not(:fullscreen)) {
|
||||
min-width: unset !important;
|
||||
.plyr__control--overlaid,
|
||||
.plyr__progress__container,
|
||||
.plyr__volume,
|
||||
[data-plyr="download"] {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
:deep(.plyr__time) {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
24
packages/client/src/components/MkNoteMediaList.vue
Normal file
24
packages/client/src/components/MkNoteMediaList.vue
Normal file
|
@ -0,0 +1,24 @@
|
|||
<template>
|
||||
<template v-for="file in note.files">
|
||||
<XNoteMedia
|
||||
v-if="
|
||||
file.type.startsWith('video') ||
|
||||
file.type.startsWith('image')
|
||||
"
|
||||
:key="file.id"
|
||||
:class="{ image: file.type.startsWith('image') }"
|
||||
:data-id="file.id"
|
||||
:media="file"
|
||||
:note="note"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import type { entities } from "firefish-js";
|
||||
import XNoteMedia from "@/components/MkNoteMedia.vue";
|
||||
|
||||
defineProps<{
|
||||
note: entities.Note;
|
||||
}>();
|
||||
</script>
|
|
@ -152,7 +152,7 @@ interface Tab {
|
|||
}
|
||||
|
||||
const props = defineProps<{
|
||||
tabs?: Tab[];
|
||||
tabs?: Tab[] | null;
|
||||
tab?: string;
|
||||
actions?: {
|
||||
text: string;
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
@refresh="fetchUser()"
|
||||
/>
|
||||
<XReactions v-else-if="tab === 'reactions'" :user="user" />
|
||||
<XMediaList v-else-if="tab === 'media'" :user="user"/>
|
||||
<XClips v-else-if="tab === 'clips'" :user="user" />
|
||||
<XPages v-else-if="tab === 'pages'" :user="user" />
|
||||
<XGallery v-else-if="tab === 'gallery'" :user="user" />
|
||||
|
@ -40,6 +41,7 @@ import icon from "@/scripts/icon";
|
|||
|
||||
const XHome = defineAsyncComponent(() => import("./home.vue"));
|
||||
const XReactions = defineAsyncComponent(() => import("./reactions.vue"));
|
||||
const XMediaList = defineAsyncComponent(() => import("./media-list.vue"));
|
||||
const XClips = defineAsyncComponent(() => import("./clips.vue"));
|
||||
const XPages = defineAsyncComponent(() => import("./pages.vue"));
|
||||
const XGallery = defineAsyncComponent(() => import("./gallery.vue"));
|
||||
|
@ -86,6 +88,11 @@ const headerTabs = computed(() =>
|
|||
title: i18n.ts.overview,
|
||||
icon: `${icon("ph-user")}`,
|
||||
},
|
||||
{
|
||||
key: "media",
|
||||
title: i18n.ts.media,
|
||||
icon: `${icon("ph-grid-four")}`,
|
||||
},
|
||||
...((isSignedIn && me.id === user.value.id) ||
|
||||
user.value.publicReactions
|
||||
? [
|
||||
|
|
50
packages/client/src/pages/user/media-list.vue
Normal file
50
packages/client/src/pages/user/media-list.vue
Normal file
|
@ -0,0 +1,50 @@
|
|||
<template>
|
||||
<MkSpacer :contentMax="1100">
|
||||
<div :class="$style.root">
|
||||
<MkPagination v-slot="{items}" :pagination="pagination">
|
||||
<div :class="$style.stream">
|
||||
<MkNoteMediaList v-for="note in items" :note="note"/>
|
||||
</div>
|
||||
</MkPagination>
|
||||
</div>
|
||||
</MkSpacer>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import MkNoteMediaList from "@/components/MkNoteMediaList.vue";
|
||||
import MkPagination from "@/components/MkPagination.vue";
|
||||
import { computed } from "vue";
|
||||
import type { entities } from "firefish-js";
|
||||
|
||||
const props = defineProps<{
|
||||
user: entities.UserDetailed;
|
||||
}>();
|
||||
|
||||
const pagination = {
|
||||
endpoint: "users/notes" as const,
|
||||
limit: 10,
|
||||
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(250px, 1fr));
|
||||
grid-auto-rows: 160px;
|
||||
grid-gap: 6px;
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.stream {
|
||||
grid-template-columns: repeat(auto-fill, minmax(160px, 1fr));
|
||||
}
|
||||
}
|
||||
</style>
|
Loading…
Reference in a new issue