Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

登録イベントの通知機能を実装 #41

Merged
merged 6 commits into from
Oct 3, 2024
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: 3 additions & 4 deletions src/components/menu/bookmarkedEvent/BookmarkedEventButton.vue
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,16 @@ import BookmarkedEventPopover from "./BookmarkedEventPopover.vue";
import type { LiverEvent } from "@/services/api";
import { usePopover } from "@/composable/usePopover";

import { useBookmarkStore } from "@/store/bookmarkStore";
import { useEventListStore } from "@/store/eventListStore";
import { useStorageStore } from "@/store/storageStore";

const eventListStore = useEventListStore();
const storageStore = useStorageStore();

const bookmarkStore = useBookmarkStore();
const popover = usePopover();

const bookmarkEventList = computed(() => {
const list: LiverEvent[] = [];
storageStore.bookmarkEventSet.forEach((id) => {
bookmarkStore.bookmarkEventMap.forEach((_value, id) => {
const liverEvent = eventListStore.liverEventMap.get(id);
if (liverEvent) {
list.push(liverEvent);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import { computed, onMounted } from "vue";
import type { LiverEvent } from "@/services/api";
import { scrollToLiverEventTop } from "@/lib/scroll";
import { getThumnail } from "@/lib/youtube";
import { useBookmarkStore } from "@/store/bookmarkStore";
import { useDateStore } from "@/store/dateStore";
import { useFocusStore } from "@/store/focusStore";
import { useStorageStore } from "@/store/storageStore";
import { compareDate, getDateTime } from "@/utils/date";
import { hhmmDateFormatter, toRelativeTime } from "@/utils/dateFormat";
import { closePopover } from "@/utils/popover";
Expand All @@ -16,7 +16,7 @@ const props = defineProps<{

const focusStore = useFocusStore();
const dateStore = useDateStore();
const storageStore = useStorageStore();
const bookmarkStore = useBookmarkStore();

const bookmarkCount = computed(() => props.bookmarkEventList.length);

Expand Down Expand Up @@ -99,7 +99,7 @@ onMounted(() => {
<div class="flex justify-center p-2">
<button
class="rounded bg-gray-200 px-4 py-2 text-sm hover:bg-gray-300"
@click.prevent="storageStore.resetBookmarkEventSet"
@click.prevent="bookmarkStore.bookmarkEventMap.clear"
>
clear all
</button>
Expand Down
101 changes: 21 additions & 80 deletions src/components/streams/LiverEventCard.vue
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
<script setup lang="ts">
import { computed, toRaw } from "vue";
import { computed, toRaw, toRef } from "vue";
import { useLiverEvent } from "./useLiverEvent";
import type { LiverEvent } from "@/services/api";
import { getThumnail } from "@/lib/youtube";
import { useDateStore } from "@/store/dateStore";
import { useEventListStore } from "@/store/eventListStore";
import { useFocusStore } from "@/store/focusStore";
import { useSearchStore } from "@/store/searchStore";
import { useStorageStore } from "@/store/storageStore";
import { hhss } from "@/utils/dateFormat";
import { getChannelIcon } from "@/utils/icons";

Expand All @@ -15,40 +13,10 @@ const props = defineProps<{
}>();

const focusStore = useFocusStore();
const dateStore = useDateStore();
const eventListStore = useEventListStore();
const storageStore = useStorageStore();
const searchStore = useSearchStore();

const oneHour = 60 * 60 * 1000;

// ホロライブのライブ判定開始用
const liveStartDuration = 20 * 60 * 1000;
const liveEndDuration = 60 * 60 * 1000;

const elapsedTime = computed(() => {
const { isLive, endAt } = props.liverEvent;

const time = (() => {
// 終了時間があれば終了時間から開始時間を引く
if (endAt) {
return endAt.getTime() - props.liverEvent.startAt.getTime();
}
// ライブ中なら現在時刻から開始時間を引く
if (isLive) {
return dateStore.currentTime - props.liverEvent.startAt.getTime();
}
return 0;
})();

if (time === 0) return null;

const hour = time / oneHour;
return {
fixed: hour.toFixed(1),
count: Math.min(12, Math.max(1, Math.round(hour))),
};
});
const { isFinished, elapsedTime, isNew, hasBookmark, hasNotify } = useLiverEvent(
toRef(props.liverEvent),
);

const timeDisplay = computed(() => {
const { isLive, startAt } = props.liverEvent;
Expand All @@ -68,34 +36,6 @@ const timeDisplay = computed(() => {
return strs.join(" ");
});

// 配信終了判定
const isFinished = computed(() => {
// 終了時刻が設定されているか(にじさんじのみ)
if (props.liverEvent.endAt) return true;
// 配信中か
if (props.liverEvent.isLive) return false;

// 配信していない場合
const now = dateStore.currentTime;
const startTime = props.liverEvent.startAt.getTime();
const elapsed = now - startTime;
// 現在時刻を過ぎていなければ開始前
if (elapsed < 0) return false;
// ホロライブの場合
if (props.liverEvent.affilication === "hololive") {
// 配信開始直後は開始時間が更新されてもliveになっていない場合があるので一定時間判定しない
if (elapsed < liveStartDuration) return false;

// startTimeの秒数が0以外あれば配信開始済み
if (props.liverEvent.startAt.getSeconds() !== 0) return true;

// 秒数が0の場合、1時間経過していたら終了と見なす
if (elapsed > liveEndDuration) return true;
}
// それ以外の場合:未終了
return false;
});

const isHovered = computed(() => {
if (!focusStore.hoveredTalent) return false;

Expand All @@ -122,14 +62,6 @@ const hasHoveredHash = computed(() => {
return hashSet.intersection(focusStore.hoveredHashSet).size > 0;
});

const isNew = computed(() => {
return eventListStore.addedEventIdSet.has(props.liverEvent.id);
});

const isBookmark = computed(() => {
return storageStore.bookmarkEventSet.has(props.liverEvent.id);
});

// 通常クリック時はpreventしてダイアログを開き、ホイールクリックはリンクを開く
function onClickCard(evt: MouseEvent) {
evt.preventDefault();
Expand Down Expand Up @@ -233,13 +165,22 @@ function setSearchString(str: string) {
>
<i class="i-mdi-sparkles size-7 text-purple-600" />
</div>
<div
v-if="isBookmark"
class="grid size-10 place-items-center rounded-full border-2 border-green-800 bg-white shadow-md"
title="bookmark"
>
<i class="i-mdi-bookmark size-7 text-green-600" />
</div>
<template v-if="hasBookmark">
<div
v-if="hasNotify"
class="grid size-10 place-items-center rounded-full border-2 border-yellow-800 bg-white shadow-md"
title="bookmark"
>
<i class="i-mdi-bell size-7 text-yellow-600" />
</div>
<div
v-else
class="grid size-10 place-items-center rounded-full border-2 border-green-800 bg-white shadow-md"
title="bookmark"
>
<i class="i-mdi-bookmark size-7 text-green-600" />
</div>
</template>
</div>

<div
Expand Down
111 changes: 91 additions & 20 deletions src/components/streams/detail/LiverEventDetailPopover.vue
Original file line number Diff line number Diff line change
@@ -1,33 +1,39 @@
<script setup lang="ts">
import { computed } from "vue";
import { computed, toRef } from "vue";
import { useLiverEvent } from "../useLiverEvent";
import type { LiverEvent } from "@/services/api";
import { usePopover } from "@/composable/usePopover";
import { parseSegment } from "@/lib/text";
import { getThumnail } from "@/lib/youtube";
import { useBookmarkStore } from "@/store/bookmarkStore";
import { useFocusStore } from "@/store/focusStore";
import { useNotificationStore } from "@/store/notificationStore";
import { useSearchStore } from "@/store/searchStore";
import { useStorageStore } from "@/store/storageStore";
import { fullDateFormatter } from "@/utils/dateFormat";
import { closePopover } from "@/utils/popover";

const props = defineProps<{
liverEvent: LiverEvent;
}>();

const bookmarkStore = useBookmarkStore();
const focusStore = useFocusStore();
const storageStore = useStorageStore();
const searchStore = useSearchStore();
const notificationStore = useNotificationStore();

const { isFinished, hasBookmark, hasNotify, beforeStartTime } = useLiverEvent(
toRef(props.liverEvent),
);
const permissionPopover = usePopover();

const isBookmark = computed(() => {
return storageStore.bookmarkEventSet.has(props.liverEvent.id);
});
const fullDate = computed(() => {
return fullDateFormatter.format(props.liverEvent.startAt);
});

// セグメント化したタイトル
const segmentList = computed(() => {
const { title, keywordList, hashtagList: hashList } = props.liverEvent;
return parseSegment(title, keywordList, hashList);
const { title, keywordList, hashtagList } = props.liverEvent;
return parseSegment(title, keywordList, hashtagList);
});

function setSearchString(str: string) {
Expand All @@ -40,6 +46,33 @@ function setSearchString(str: string) {
}
searchStore.setSearchString(formattedStr);
}

// 通知機能が使えるか
const canSetNotify = computed(() => {
if (!notificationStore.isSupported) return false;
// 開始時間前なら通知可能
return beforeStartTime.value && !isFinished.value;
});

async function onClickNotify(id: string) {
if (!notificationStore.isSupported) return;

// 通知許可済みなら通知をトグル
if (notificationStore.permissionGranted) {
bookmarkStore.toggleNotifyEvent(id);
return;
}

// 通知許可ポップオーバーを表示
permissionPopover.showPopover();
// 通知許可を求める
const permission = await notificationStore.ensurePermissions();
if (!permission) return;

// 許可されたらポップオーバーを閉じて通知をトグル
permissionPopover.hidePopover();
bookmarkStore.toggleNotifyEvent(id);
}
</script>

<template>
Expand Down Expand Up @@ -118,21 +151,47 @@ function setSearchString(str: string) {
</button>
</div>
</div>
<button
class="group/fav grid size-11 place-items-center"
@click="storageStore.toggleBookmarkEvent(liverEvent.id)"
title="bookmark"
>
<div
:class="`size-10 place-items-center bg-white rounded-full grid border-2 ${isBookmark ? 'border-green-800' : 'border-gray-400'} group-hover/fav:bg-gray-100`"
<div class="flex flex-col place-items-center">
<button
class="group/fav grid size-11 place-items-center"
@click="bookmarkStore.toggleBookmarkEvent(liverEvent.id)"
title="add bookmark"
>
<i
:class="`size-7 ${isBookmark ? 'i-mdi-bookmark text-green-600' : 'i-mdi-bookmark-outline text-gray-400'}`"
/>
</div>
</button>
<div
:class="`size-10 place-items-center bg-white rounded-full grid border-2 group-hover/fav:bg-gray-100 ${hasBookmark ? 'border-green-800' : 'border-gray-400'} `"
>
<i
:class="`size-7 ${hasBookmark ? 'i-mdi-bookmark text-green-600' : 'i-mdi-bookmark-outline text-gray-400'}`"
/>
</div>
</button>
<button
v-if="canSetNotify"
class="group/fav grid size-11 place-items-center"
@click="onClickNotify(liverEvent.id)"
title="add notification"
>
<div
:class="`size-10 place-items-center bg-white rounded-full grid border-2 border-gray-400 group-hover/fav:bg-gray-100
${hasNotify ? 'border-yellow-800' : 'border-gray-400'}
`"
>
<i
:class="`size-7 ${hasNotify ? 'i-mdi-bell text-yellow-600' : 'i-mdi-bell-outline text-gray-400'}`"
/>
</div>
</button>
</div>
</div>
</div>
<permissionPopover.PopOver class="bottom-4 top-auto overflow-visible bg-transparent p-0">
<button
class="rounded-full bg-yellow-400 p-4 text-sm shadow-md"
@click="permissionPopover.hidePopover()"
>
通知を許可してください
</button>
</permissionPopover.PopOver>
</div>
</template>

Expand All @@ -143,4 +202,16 @@ function setSearchString(str: string) {
opacity: 0;
}
}

[popover] {
&:popover-open {
animation: fadeIn 0.2s ease-in-out forwards;
}
}

@keyframes fadeIn {
from {
translate: 0 50%;
}
}
</style>
Loading