Skip to content
This repository has been archived by the owner on Oct 14, 2024. It is now read-only.

コメントとスタンプ流すやつ #199

Merged
merged 9 commits into from
Oct 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 0 additions & 7 deletions openapitools.json

This file was deleted.

2,422 changes: 174 additions & 2,248 deletions package-lock.json

Large diffs are not rendered by default.

6 changes: 2 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,17 @@
"name": "emoine_r-ui",
"version": "0.0.0",
"scripts": {
"dev": "concurrently \"vite\" \"npm run start-mock\"",
"dev": "vite",
"build": "vite build",
"lint": "eslint --ext .ts,.vue src",
"type-check": "vue-tsc --noEmit",
"gen-api": "buf generate https:/traPtitech/Emoine_R.git#subdir=api/proto",
"start-mock": "prism mock -p 8090 -d https://raw.githubusercontent.com/traPtitech/Emoine_R/main/docs/openapi.yml"
"gen-api": "buf generate https:/traPtitech/Emoine_R.git#subdir=api/proto"
},
"dependencies": {
"@bufbuild/connect": "^0.11.0",
"@bufbuild/connect-web": "^0.11.0",
"@bufbuild/protobuf": "^1.3.0",
"@iconify/vue": "^4.1.1",
"@stoplight/prism-cli": "^5.0.1",
"axios": "^1.4.0",
"dayjs": "^1.11.7",
"pinia": "^2.1.4",
Expand Down
13 changes: 4 additions & 9 deletions src/components/UI/DateChip.vue
Original file line number Diff line number Diff line change
@@ -1,21 +1,16 @@
<script setup lang="ts">
import { computed } from 'vue'
import { getDateDiffText } from '@/lib/date'
import { getEventStatus, getDateDiffText } from '@/lib/date'
import { Dayjs } from 'dayjs'

type LiveStatus = 'isPlanned' | 'isStreaming' | 'isArchived'

const props = defineProps<{
startedTime: Dayjs
endedTime: Dayjs
}>()

const status = computed((): LiveStatus => {
/* todo: endedTimeが存在しないときのサーバーからのデータの仕様に合わせる */
if (props.startedTime.isAfter(Date.now())) return 'isPlanned'
if (!props.endedTime.isValid()) return 'isStreaming'
return 'isArchived'
})
const status = computed(() =>
getEventStatus(props.startedTime, props.endedTime)
)

const dateDiffText = computed(() => {
switch (status.value) {
Expand Down
37 changes: 37 additions & 0 deletions src/components/VideoOverlay/OverlayComment.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<script setup lang="ts">
import { Comment } from '@/lib/apis'

defineProps<{
comment: Comment
}>()
</script>

<template>
<p
:class="$style.comment"
:style="{
color: comment.color,
top: `${Math.random() * 90}%`
}"
>
{{ comment.text }}
</p>
</template>

<style lang="scss" module>
.comment {
position: absolute;
font-size: 2rem;
right: 0;
animation: commentMove 8s linear;
pointer-events: none;
}
@keyframes commentMove {
0% {
right: 0;
}
100% {
right: 100%;
}
}
</style>
40 changes: 40 additions & 0 deletions src/components/VideoOverlay/OverlayReaction.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<script setup lang="ts">
import { Reaction } from '@/lib/apis'
import { ref } from 'vue'

defineProps<{
reaction: Reaction
}>()

// TODO: stampIdから画像を取得
const reactionImg = ref('https://q.trap.jp/api/v3/public/icon/mehm8128')
</script>

<template>
<img
:src="reactionImg"
:style="{
top: `${Math.random() * 90}%`,
left: `${Math.random() * 90}%`
}"
:class="$style.reaction"
/>
</template>

<style lang="scss" module>
.reaction {
position: absolute;
width: 100px;
height: 100px;
animation: stampZoom 0.4s linear;
pointer-events: none;
}
@keyframes stampZoom {
0% {
transform: scale(0.4);
}
100% {
transform: scale(1);
}
}
</style>
33 changes: 33 additions & 0 deletions src/components/VideoOverlay/VideoOverlay.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<script setup lang="ts">
import OverlayComment from './OverlayComment.vue'
import OverlayReaction from './OverlayReaction.vue'
import { Comment, Reaction } from '@/lib/apis'

defineProps<{
comments: Comment[]
reactions: Reaction[]
}>()
</script>

<template>
<div :class="$style.overlayContainer">
<slot />
<overlay-comment
v-for="comment in comments"
:key="comment.id"
:comment="comment"
/>
<overlay-reaction
v-for="reaction in reactions"
:key="reaction.stampId"
:reaction="reaction"
/>
</div>
</template>

<style lang="scss" module>
.overlayContainer {
position: relative;
width: 100%;
}
</style>
1 change: 1 addition & 0 deletions src/consts/status.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type LiveStatus = 'isPlanned' | 'isStreaming' | 'isArchived'
11 changes: 11 additions & 0 deletions src/lib/date.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { LiveStatus } from '@/consts/status'
import { Timestamp } from '@bufbuild/protobuf'
import dayjs, { Dayjs } from 'dayjs'

Expand Down Expand Up @@ -25,3 +26,13 @@ export const formatDateTime = (timeStamp: Timestamp | undefined): string => {
const date = toDayjs(timeStamp)
return date.format('YYYY/MM/DD HH:mm')
}

export const getEventStatus = (
startedTime: Dayjs,
endedTime: Dayjs
): LiveStatus => {
/* todo: endedTimeが存在しないときのサーバーからのデータの仕様に合わせる */
if (startedTime.isAfter(Date.now())) return 'isPlanned'
if (!endedTime.isValid()) return 'isStreaming'
return 'isArchived'
}
24 changes: 22 additions & 2 deletions src/lib/mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
} from '@/lib/apis/generated/proto/emoine_r/v1/admin_api_pb'
import { GeneralAPIService } from '@/lib/apis/generated/proto/emoine_r/v1/general_api_connect'
import {
ConnectToEventStreamResponse,
GetEventCommentsResponse,
GetEventReactionsResponse,
GetEventResponse,
Expand All @@ -14,7 +15,7 @@ import {
SendReactionResponse
} from '@/lib/apis/generated/proto/emoine_r/v1/general_api_pb'
import { Event, Token, Comment, Reaction } from '@/lib/apis'
import { randomDate, randomString } from '@/lib/random'
import { randomDate, randomSleep, randomString } from '@/lib/random'
import { ServiceImpl } from '@bufbuild/connect'
import { Timestamp } from '@bufbuild/protobuf'

Expand Down Expand Up @@ -99,7 +100,26 @@ export const generalApiMock: Partial<ServiceImpl<typeof GeneralAPIService>> = {
new GetEventReactionsResponse({
reactions: exampleReactions(10)
}),
// connectToEventStream: () => {}, todo: streamingのmockの書き方が分からなかった
connectToEventStream: async function* connectToEventStream() {
const comments = exampleComments(10)
const reactions = exampleReactions(10)
for (let i = 0; i < 10; i++) {
await randomSleep()
yield new ConnectToEventStreamResponse({
streamEvent: {
value: comments[i],
case: 'comment'
}
})
await randomSleep()
yield new ConnectToEventStreamResponse({
streamEvent: {
value: reactions[i],
case: 'reaction'
}
})
}
},
sendComment: () => new SendCommentResponse({ comment: exampleComment }),
sendReaction: () => new SendReactionResponse({ reaction: exampleReaction })
}
Expand Down
5 changes: 5 additions & 0 deletions src/lib/random.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,8 @@ export const randomDate = () => {
const end = dayjs().add(1, 'week')
return new Date(randomNum(start.valueOf(), end.valueOf()))
}

export const randomSleep = () => {
const ms = randomNum(1000, 5000)
return new Promise(resolve => setTimeout(resolve, ms))
}
105 changes: 92 additions & 13 deletions src/pages/EventPage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,19 @@ import { onMounted, ref } from 'vue'
import { useRoute } from 'vue-router'
import { getEventId } from '@/lib/parsePathParams'
import CommentPanel from '@/components/CommentPanel/CommentPanel.vue'
import StampList from '@/components/StampList/StampList.vue'
import { Stamp } from '@/components/StampList/StampList.vue'
import { useGeneralConnectClient } from '@/lib/connectClient'
import { Event, Comment } from '@/lib/apis'
import { Event, Comment, Reaction } from '@/lib/apis'
import StampList, { Stamp } from '@/components/StampList/StampList.vue'
import VideoOverlay from '@/components/VideoOverlay/VideoOverlay.vue'
import { getEventStatus, toDayjs } from '@/lib/date'

const route = useRoute()
const client = useGeneralConnectClient()

const eventId = getEventId(route.params.id)
const event = ref<Event>()
const comments = ref<Comment[]>()
const reactions = ref<Reaction[]>()

//TODO: スタンプをなんらかの方法で取ってくる
const stamps: Stamp[] = Array(10).fill({
Expand All @@ -22,37 +24,114 @@ const stamps: Stamp[] = Array(10).fill({
image: 'https://q.trap.jp/api/v3/public/icon/mehm8128'
})

const fetchevent = async () => {
const fetchEvent = async () => {
//todo: エラーハンドリング
const res = await client.getEvent({ id: eventId })
if (!res.event) {
throw new Error('res.event is undefined')
}
event.value = res.event
return res.event
}
const fetchComments = async () => {
//todo: エラーハンドリング
const res = await client.getEventComments({ eventId: eventId })
const res = await client.getEventComments({ eventId })
if (!res.comments) {
throw new Error('res.comment is undefined')
}
comments.value = res.comments
}
const fetchStamps = async () => {
//todo: エラーハンドリング
const res = await client.getEventReactions({ eventId })
if (!res.reactions) {
throw new Error('res.comment is undefined')
}
reactions.value = res.reactions
}

onMounted(() => {
fetchevent()
fetchComments()
const connectToEventStream = async () => {
for await (const res of client.connectToEventStream({
eventId
})) {
const type = res.streamEvent.case
switch (type) {
case 'event': {
const event = res.streamEvent.value
if (!event) {
throw new Error('event is undefined')
}
break
}
case 'comment': {
const comment = res.streamEvent.value
if (!comment) {
throw new Error('comment is undefined')
}
if (!comments.value) {
comments.value = [comment]
} else {
comments.value.push(comment)
}
break
}
case 'reaction': {
const reaction = res.streamEvent.value
if (!reaction) {
throw new Error('reaction is undefined')
}
if (!reactions.value) {
reactions.value = [reaction]
} else {
reactions.value.push(reaction)
}
break
}
case undefined:
throw new Error('res.event.case is undefined')
default: {
const exhaustivenessCheck: never = type
throw new Error(`Unexpected Type: ${exhaustivenessCheck}`)
}
}
}
}

onMounted(async () => {
const event = await fetchEvent()
const eventStatus = getEventStatus(
toDayjs(event.startedAt),
toDayjs(event.endedAt)
)
switch (eventStatus) {
case 'isPlanned':
//TODO: 表示を考える
break
case 'isStreaming':
connectToEventStream()
break
case 'isArchived':
fetchComments()
fetchStamps()
break
default: {
const exhaustivenessCheck: never = eventStatus
throw new Error(`Unexpected Type: ${exhaustivenessCheck}`)
}
}
})
</script>

<template>
<div :class="$style.container">
<div :class="$style.leftContainer">
<iframe
v-if="event"
:src="`https://www.youtube.com/embed/${event.videoId}`"
:class="$style.video"
/>
<video-overlay :reactions="reactions ?? []" :comments="comments ?? []">
<iframe
v-if="event"
:src="`https://www.youtube.com/embed/${event.videoId}`"
:class="$style.video"
/>
</video-overlay>
<div :class="$style.stampListContainer">
<stamp-list
:stamps="stamps"
Expand Down
Loading