Skip to content

Commit 0da5c75

Browse files
committed
✨ miraktest-local
1 parent d3a928d commit 0da5c75

File tree

8 files changed

+428
-0
lines changed

8 files changed

+428
-0
lines changed

README.md

+2
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@
3838
スクリーンショットを Gyazo にアップロードします。
3939
- [Twitter](./src/miraktest-twitter)<br />
4040
視聴中の番組に関連するツイートを投稿します。
41+
- [Local](./src/miraktest-local/)<br />
42+
ローカルファイルの再生を行います。
4143

4244
## ビルド
4345

src/miraktest-local/LocalRenderer.tsx

+69
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import React, { useEffect } from "react"
2+
import { useRecoilValue, useSetRecoilState } from "recoil"
3+
import { ContentPlayerPlayingContent, InitPlugin } from "../@types/plugin"
4+
import tailwind from "../tailwind.scss"
5+
import { FileSelector } from "./components/FileSelector"
6+
import { LOCAL_META, Local_RECORDS_WINDOW_ID } from "./constants"
7+
8+
export const LocalRenderer: InitPlugin["renderer"] = ({
9+
appInfo,
10+
functions,
11+
atoms,
12+
rpc,
13+
}) => {
14+
return {
15+
...LOCAL_META,
16+
exposedAtoms: [],
17+
sharedAtoms: [],
18+
storedAtoms: [],
19+
setup() {
20+
return
21+
},
22+
components: [],
23+
destroy() {
24+
return
25+
},
26+
windows: {
27+
[Local_RECORDS_WINDOW_ID]: () => {
28+
const activeId = useRecoilValue(
29+
atoms.globalActiveContentPlayerIdSelector
30+
)
31+
const setPlayingContent = useSetRecoilState(
32+
atoms.globalContentPlayerPlayingContentFamily(activeId ?? 0)
33+
)
34+
const services = useRecoilValue(atoms.mirakurunServicesSelector)
35+
useEffect(() => {
36+
rpc.setWindowTitle(`ローカル - ${appInfo.name}`)
37+
}, [])
38+
39+
return (
40+
<>
41+
<style>{tailwind}</style>
42+
<div className="w-full h-screen bg-gray-100 text-gray-900 flex leading-loose">
43+
{services ? (
44+
<FileSelector
45+
services={services}
46+
setPlayingContent={setPlayingContent}
47+
openContentPlayer={(
48+
playingContent: ContentPlayerPlayingContent
49+
) => {
50+
return functions.openContentPlayerWindow({
51+
playingContent,
52+
})
53+
}}
54+
requestDialog={rpc.requestDialog}
55+
/>
56+
) : (
57+
<div className="w-full h-full flex items-center justify-center">
58+
<div className="text-gray-600 px-4 text-center">
59+
<p>読み込み中</p>
60+
</div>
61+
</div>
62+
)}
63+
</div>
64+
</>
65+
)
66+
},
67+
},
68+
}
69+
}

src/miraktest-local/README.md

+7
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# miraktest-local
2+
3+
ローカルファイルの再生を行うプラグインです。
4+
5+
## ダウンロード
6+
7+
<https://github.com/ci7lus/miraktest-plugins/releases>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
import { Switch } from "@headlessui/react"
2+
import clsx from "clsx"
3+
import dayjs from "dayjs"
4+
import React, { useState } from "react"
5+
import { File } from "react-feather"
6+
import {
7+
ContentPlayerPlayingContent,
8+
InitPluginInRenderer,
9+
Program,
10+
Service,
11+
} from "../../@types/plugin"
12+
13+
export const FileSelector: React.VFC<{
14+
services: Service[]
15+
setPlayingContent: React.Dispatch<
16+
React.SetStateAction<ContentPlayerPlayingContent | null>
17+
>
18+
openContentPlayer: (_: ContentPlayerPlayingContent) => Promise<number>
19+
requestDialog: Parameters<InitPluginInRenderer>[0]["rpc"]["requestDialog"]
20+
}> = ({ services, setPlayingContent, openContentPlayer, requestDialog }) => {
21+
const [filePath, setFilePath] = useState("")
22+
const [startAt, setStartAt] = useState("")
23+
const [duration, setDuration] = useState(30)
24+
const [serviceId, setServiceId] = useState(-1)
25+
26+
const [isOpenWithNewWindow, setIsOpenWithNewWindow] = useState(false)
27+
28+
return (
29+
<div className="w-full h-full flex flex-col">
30+
<div className="w-full bg-gray-800 text-gray-200">
31+
<div className="w-full py-2 pl-4 pr-2 flex items-center justify-between">
32+
<h2 className="font-semibold text-lg">ローカルファイル再生</h2>
33+
</div>
34+
</div>
35+
<div className="w-full flex overflow-auto p-4">
36+
<form
37+
onSubmit={(e) => {
38+
e.preventDefault()
39+
if (!filePath) {
40+
return
41+
}
42+
const contentType = "Local"
43+
const url = "file://" + filePath
44+
const service = services.find(
45+
(service) => service.serviceId === serviceId
46+
)
47+
const program: Program | undefined = startAt
48+
? {
49+
startAt: dayjs(startAt).unix() * 1000,
50+
duration: duration * 1000 * 60,
51+
id: -1,
52+
networkId: -1,
53+
serviceId,
54+
eventId: -1,
55+
isFree: true,
56+
}
57+
: undefined
58+
const contentPlayload = {
59+
contentType,
60+
url,
61+
program,
62+
service,
63+
}
64+
console.info("再生します:", contentPlayload)
65+
if (isOpenWithNewWindow) {
66+
openContentPlayer(contentPlayload)
67+
} else {
68+
setPlayingContent(contentPlayload)
69+
}
70+
}}
71+
>
72+
<label className="block w-full">
73+
<span>ファイルを選択</span>
74+
<div className="flex justify-center flex-grow">
75+
<input
76+
type="text"
77+
className={clsx(
78+
"block mt-1 form-input rounded-l-md w-full text-gray-900 focus:outline-none cursor-pointer"
79+
)}
80+
value={filePath || ""}
81+
onChange={(e) => setFilePath(e.target.value)}
82+
spellCheck={false}
83+
/>
84+
<button
85+
className={clsx(
86+
`px-4 py-2 mt-1 rounded-r-md flex items-center justify-center bg-gray-200 text-gray-900 focus:outline-none cursor-pointer`
87+
)}
88+
onClick={async () => {
89+
const dialog = await requestDialog({
90+
properties: ["openFile"],
91+
})
92+
if (dialog.canceled) {
93+
return
94+
}
95+
const path = dialog.filePaths.slice(0).shift()
96+
if (!path) {
97+
return
98+
}
99+
setFilePath(path)
100+
}}
101+
>
102+
<File className="pointer-events-none" size="1.75rem" />
103+
</button>
104+
</div>
105+
</label>
106+
<div className={clsx("flex", "space-x-2")}>
107+
<label className="block mt-2">
108+
<span>開始時間</span>
109+
<input
110+
type="datetime-local"
111+
className="block mt-1 form-input rounded-md w-full text-gray-900"
112+
value={startAt || ""}
113+
onChange={(e) => setStartAt(e.target.value)}
114+
/>
115+
</label>
116+
<label className="block mt-2">
117+
<span>長さ</span>
118+
<input
119+
type="number"
120+
className="block mt-1 form-input rounded-md w-full text-gray-900"
121+
value={duration}
122+
onChange={(e) => {
123+
const p = parseInt(e.target.value)
124+
if (Number.isNaN(p)) {
125+
return
126+
}
127+
setDuration(p)
128+
}}
129+
/>
130+
{startAt ? (
131+
<span>{dayjs(startAt).add(duration, "minutes").format()}</span>
132+
) : (
133+
<span>未選択</span>
134+
)}
135+
</label>
136+
</div>
137+
<select
138+
className="appearance-none border rounded py-2 px-2 mt-2 leading-tight focus:outline-none"
139+
value={serviceId}
140+
onChange={(e) => {
141+
const selectedServiceId = parseInt(e.target.value)
142+
if (Number.isNaN(selectedServiceId)) {
143+
setServiceId(-1)
144+
return
145+
}
146+
setServiceId(selectedServiceId)
147+
}}
148+
>
149+
<option value="-1" defaultChecked>
150+
選択解除
151+
</option>
152+
{services.map((service) => {
153+
return (
154+
<option key={service.serviceId} value={service.serviceId}>
155+
{service.name}
156+
</option>
157+
)
158+
})}
159+
</select>
160+
<Switch.Group>
161+
<div className="flex items-center mt-4">
162+
<Switch
163+
checked={isOpenWithNewWindow}
164+
onChange={(e) => setIsOpenWithNewWindow(e)}
165+
className={`${
166+
isOpenWithNewWindow ? "bg-blue-600" : "bg-gray-300"
167+
} relative inline-flex items-center h-6 rounded-full w-11`}
168+
>
169+
<span
170+
className={`${
171+
isOpenWithNewWindow ? "translate-x-6" : "translate-x-1"
172+
} inline-block w-4 h-4 transform bg-white rounded-full transition ease-in-out duration-200`}
173+
/>
174+
</Switch>
175+
<Switch.Label className="ml-2">
176+
新しいウィンドウで開く
177+
</Switch.Label>
178+
</div>
179+
</Switch.Group>
180+
<button
181+
type="submit"
182+
className="bg-blue-500 text-gray-100 p-2 px-3 my-4 rounded-md focus:outline-none cursor-pointer active:bg-gray-200"
183+
>
184+
再生
185+
</button>
186+
</form>
187+
</div>
188+
</div>
189+
)
190+
}

src/miraktest-local/constants.ts

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
export const LOCAL_ID = "io.github.ci7lus.miraktest-plugins.local"
2+
export const LOCAL_PREFIX = "plugins.ci7lus.local"
3+
export const LOCAL_META = {
4+
id: LOCAL_ID,
5+
name: "Local",
6+
author: "ci7lus",
7+
version: "0.0.1",
8+
description: "ローカルファイルの再生を行うプラグインです。",
9+
}
10+
export const Local_RECORDS_WINDOW_ID = `${LOCAL_ID}.records`

src/miraktest-local/index.tsx

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { InitPlugin } from "../@types/plugin"
2+
import { LOCAL_META, Local_RECORDS_WINDOW_ID } from "./constants"
3+
4+
/**
5+
* MirakTest Local
6+
* ローカルファイルの再生を行うプラグイン
7+
*/
8+
9+
const main: InitPlugin = {
10+
renderer:
11+
typeof window !== "undefined"
12+
? // eslint-disable-next-line @typescript-eslint/no-var-requires
13+
require("./LocalRenderer").LocalRenderer
14+
: undefined,
15+
main: ({ functions }) => {
16+
return {
17+
...LOCAL_META,
18+
setup: () => {
19+
return
20+
},
21+
destroy: () => {
22+
return
23+
},
24+
appMenu: {
25+
label: "ローカルファイル再生",
26+
click: () => {
27+
functions.openWindow({
28+
name: Local_RECORDS_WINDOW_ID,
29+
isSingletone: true,
30+
args: {
31+
width: 800,
32+
height: 600,
33+
},
34+
})
35+
},
36+
},
37+
contextMenu: {
38+
label: "ローカルファイル再生",
39+
click: () => {
40+
functions.openWindow({
41+
name: Local_RECORDS_WINDOW_ID,
42+
isSingletone: true,
43+
args: {
44+
width: 800,
45+
height: 600,
46+
},
47+
})
48+
},
49+
},
50+
}
51+
},
52+
}
53+
54+
export default main

0 commit comments

Comments
 (0)