Skip to content

Commit f226d36

Browse files
committed
✨ miraktest-annictに作品検索機能を実装
1 parent 7e42174 commit f226d36

File tree

5 files changed

+315
-26
lines changed

5 files changed

+315
-26
lines changed

src/miraktest-annict/components/AnnictTrack.tsx

+31-23
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import clsx from "clsx"
22
import React, { useEffect, useState } from "react"
3-
import { RefreshCcw } from "react-feather"
3+
import { Link } from "react-feather"
44
import Recoil, { useRecoilState } from "recoil"
55
import { ContentPlayerPlayingContent } from "../../@types/plugin"
66
import { AnnictRESTAPI, generateGqlClient } from "../annictAPI"
@@ -13,6 +13,7 @@ import {
1313
import { detectProgramInfo } from "../findWork"
1414
import { RatingState, StatusState, WorkFragment } from "../gql"
1515
import { ARM, SayaDefinition } from "../types"
16+
import { SearchWorkForm } from "./SeachWorkForm"
1617

1718
export const AnnictTrack: React.FC<{
1819
accessToken: string
@@ -30,6 +31,7 @@ export const AnnictTrack: React.FC<{
3031
facebookAtom,
3132
}) => {
3233
const [timing, setTiming] = useState(0)
34+
const [linkedAt, setLinkedAt] = useState(0)
3335
const [isLoading, setIsLoading] = useState(true)
3436
const rest = new AnnictRESTAPI(accessToken)
3537
const [workId, setWorkId] = useState<number | null>(null)
@@ -61,7 +63,7 @@ export const AnnictTrack: React.FC<{
6163
return
6264
}
6365
setWorkId(programInfo.annictId)
64-
if (programInfo.episode.id) {
66+
if (programInfo.episode?.id) {
6567
setEpisodeId(programInfo.episode.id)
6668
setEpisodeInfo(null)
6769
} else {
@@ -72,7 +74,7 @@ export const AnnictTrack: React.FC<{
7274
console.error(e)
7375
setIsLoading(false)
7476
})
75-
}, [accessToken, timing, arm])
77+
}, [accessToken, linkedAt, arm])
7678
const [episodeInfo, setEpisodeInfo] = useState<{
7779
number: number
7880
title: string
@@ -108,12 +110,12 @@ export const AnnictTrack: React.FC<{
108110
const work = result.searchWorks?.nodes?.[0]
109111
setWork(work || null)
110112
if (work) {
111-
setWatchStatus(work.viewerStatusState || StatusState.NO_STATE)
113+
setWatchStatus(work.viewerStatusState ?? StatusState.NO_STATE)
112114
}
113115
})
114116
.catch(console.error)
115117
.finally(() => setIsLoading(false))
116-
}, [workId, timing])
118+
}, [workId, linkedAt, timing])
117119
const [isStatusChanging, setIsStatusChanging] = useState(false)
118120
useEffect(() => {
119121
if (!work || work.viewerStatusState === watchStatus) {
@@ -174,25 +176,29 @@ export const AnnictTrack: React.FC<{
174176
"py-2",
175177
"bg-gray-800",
176178
"flex",
177-
"justify-between"
179+
"justify-between",
180+
"items-center"
178181
)}
179182
>
180183
<h1 className={clsx("text-lg")}>視聴記録</h1>
181-
<button
182-
className={clsx(
183-
"focus:outline-none",
184-
"rounded-md",
185-
"bg-gray-900",
186-
"hover:bg-gray-800",
187-
"p-1",
188-
"cursor-pointer",
189-
"transition-colors"
190-
)}
191-
title="再読み込み"
192-
onClick={() => setTiming(performance.now())}
193-
>
194-
<RefreshCcw size={18} />
195-
</button>
184+
<div className={clsx("flex", "items-center", "space-x-2")}>
185+
<button
186+
className={clsx(
187+
"focus:outline-none",
188+
"rounded-md",
189+
"bg-gray-900",
190+
"hover:bg-gray-800",
191+
"p-1",
192+
"cursor-pointer",
193+
"transition-colors"
194+
)}
195+
title="番組情報から取得"
196+
onClick={() => setLinkedAt(performance.now())}
197+
>
198+
<Link size={18} />
199+
</button>
200+
<SearchWorkForm accessToken={accessToken} setWorkId={setWorkId} />
201+
</div>
196202
</div>
197203
{work ? (
198204
<div className={clsx("w-full", "flex", "overflow-auto")}>
@@ -233,7 +239,7 @@ export const AnnictTrack: React.FC<{
233239
onChange={(e) => {
234240
setWatchStatus(e.target.value as never)
235241
}}
236-
defaultValue={work.viewerStatusState || undefined}
242+
value={watchStatus}
237243
disabled={isStatusChanging}
238244
>
239245
{Object.entries(STATUS_LABEL).map(([key, label]) => (
@@ -413,7 +419,9 @@ export const AnnictTrack: React.FC<{
413419
"flex",
414420
"justify-between",
415421
"cursor-pointer",
416-
episode?.viewerDidTrack && "text-gray-600"
422+
episode?.viewerDidTrack && "text-gray-600",
423+
"hover:text-gray-400",
424+
"transition-colors"
417425
)}
418426
>
419427
<span className={clsx("truncate")}>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import clsx from "clsx"
2+
import React, { useEffect, useState } from "react"
3+
import { Search } from "react-feather"
4+
import { useDebounce } from "react-use"
5+
import { generateGqlClient } from "../annictAPI"
6+
import { Work } from "../gql"
7+
8+
type QueryWork = Pick<Work, "id" | "annictId" | "title" | "image">
9+
10+
export const SearchWorkForm: React.FC<{
11+
accessToken: string
12+
setWorkId: React.Dispatch<React.SetStateAction<number | null>>
13+
}> = ({ accessToken, setWorkId }) => {
14+
const [localTerm, setLocalTerm] = useState("")
15+
const [searchTerm, setSearchTerm] = useState<string | null>(null)
16+
const [works, setWorks] = useState<QueryWork[] | null>(null)
17+
const [isVisible, setIsVisible] = useState(false)
18+
19+
useEffect(() => {
20+
if (!searchTerm) {
21+
setWorks(null)
22+
return
23+
}
24+
const sdk = generateGqlClient(accessToken)
25+
26+
sdk
27+
.searchWorksByTerm({
28+
term: searchTerm,
29+
count: 10,
30+
since: null,
31+
})
32+
.then((result) => {
33+
setWorks((result.searchWorks?.nodes || []) as QueryWork[])
34+
setIsVisible(true)
35+
})
36+
}, [searchTerm])
37+
useDebounce(
38+
() => {
39+
setSearchTerm(localTerm || null)
40+
},
41+
100,
42+
[localTerm]
43+
)
44+
45+
return (
46+
<div className={clsx("relative")}>
47+
<form
48+
className="flex items-center justify-center space-x-2"
49+
onSubmit={(e) => {
50+
e.preventDefault()
51+
setSearchTerm(localTerm || null)
52+
}}
53+
>
54+
<Search size={18} />
55+
<input
56+
type="text"
57+
placeholder="キーワードを入力…"
58+
className="block form-input rounded-md w-full text-gray-900"
59+
value={localTerm}
60+
onChange={(e) => setLocalTerm(e.target.value)}
61+
onKeyPress={(e) => {
62+
if (e.key === "Enter") {
63+
e.preventDefault()
64+
setSearchTerm(localTerm || null)
65+
}
66+
}}
67+
onFocus={() => setIsVisible(true)}
68+
onClick={() => setIsVisible(true)}
69+
/>
70+
</form>
71+
<div
72+
className={clsx(
73+
"absolute",
74+
"top-10",
75+
"w-full",
76+
"transition-opacity",
77+
!isVisible || works === null
78+
? "opacity-0 pointer-events-none"
79+
: "opacity-100"
80+
)}
81+
onMouseLeave={() => setIsVisible(false)}
82+
>
83+
{works !== null ? (
84+
0 < works.length ? (
85+
<div
86+
className={clsx(
87+
"w-full",
88+
"bg-gray-700",
89+
"overflow-auto",
90+
"p-2",
91+
"rounded-md"
92+
)}
93+
>
94+
{(works || []).map((work) => (
95+
<p
96+
key={work.id}
97+
className={clsx(
98+
"truncate",
99+
"cursor-pointer",
100+
"px-2",
101+
"py-1",
102+
"hover:bg-gray-600",
103+
"transition-colors"
104+
)}
105+
onClick={() => setWorkId(work.annictId)}
106+
>
107+
{work.title}
108+
</p>
109+
))}
110+
</div>
111+
) : (
112+
<div
113+
className={clsx(
114+
"flex",
115+
"items-center",
116+
"justify-center",
117+
"p-8",
118+
"text-gray-400",
119+
"bg-gray-700",
120+
"w-full",
121+
"text-sm"
122+
)}
123+
>
124+
<p>作品が見つかりません</p>
125+
</div>
126+
)
127+
) : (
128+
<></>
129+
)}
130+
</div>
131+
</div>
132+
)
133+
}

src/miraktest-annict/documents/work.graphql

+9-1
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ fragment Work on Work {
6161
...Cast
6262
}
6363
}
64-
episodes(orderBy: { field: SORT_NUMBER, direction: ASC }, first: 20) {
64+
episodes(orderBy: { field: SORT_NUMBER, direction: ASC }, last: 50) {
6565
nodes {
6666
...Episode
6767
}
@@ -81,6 +81,14 @@ query Work($annictId: Int!) {
8181
}
8282
}
8383

84+
query searchWorksByTerm($term: String!, $count: Int, $since: String) {
85+
searchWorks(titles: [$term], after: $since, first: $count) {
86+
nodes {
87+
...Work
88+
}
89+
}
90+
}
91+
8492
mutation updateWorkStatus($workId: ID!, $state: StatusState!) {
8593
updateStatus(
8694
input: { clientMutationId: "miraktest", workId: $workId, state: $state }

src/miraktest-annict/findWork.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ export const detectProgramInfo = async ({
2020
number: number
2121
title: string
2222
id?: number
23-
}
23+
} | null
2424
} | void> => {
2525
let startTime: dayjs.Dayjs
2626
let endTime: dayjs.Dayjs

0 commit comments

Comments
 (0)