1
0
Fork 0
mirror of https://gitlab.com/futo-org/fcast.git synced 2025-06-24 21:25:23 +00:00

Initial commit of WebOS receiver

This commit is contained in:
Michael Hollister 2024-12-09 00:56:55 -06:00
parent b7e304b987
commit 90e1f4de1a
118 changed files with 18279 additions and 1746 deletions

View file

@ -1,7 +1,7 @@
FROM node:22.10.0-bookworm
RUN dpkg --add-architecture i386
RUN apt update && apt install -y zip dpkg fakeroot rpm wget p7zip-full unzip rsync jq awscli
RUN apt update && apt install -y zip dpkg fakeroot rpm wget p7zip-full unzip jq awscli
RUN wget https://github.com/ebourg/jsign/releases/download/6.0/jsign_6.0_all.deb
RUN apt install -y ./jsign_6.0_all.deb

View file

@ -1,7 +0,0 @@
@font-face {
font-family: InterVariable;
font-style: normal;
font-weight: 100 900;
font-display: swap;
src: url("InterVariable.woff2") format("woff2");
}

View file

@ -1,7 +0,0 @@
@font-face {
font-family: Outfit;
font-style: normal;
font-weight: 100 900;
font-display: swap;
src: url("Outfit-VariableFont_wght.ttf") format("truetype");
}

View file

@ -1,269 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="111.96011"
height="114.46101"
viewBox="0 0 111.96011 114.46101"
fill="none"
version="1.1"
id="svg30"
sodipodi:docname="logo3.svg"
inkscape:version="1.4 (e7c3feb100, 2024-10-09)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="true"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="8"
inkscape:cx="36"
inkscape:cy="58.1875"
inkscape:window-width="1920"
inkscape:window-height="1128"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg30" />
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="m 84.763099,5.5827991 c 2.06,-2.0602 0.601,-5.58279997600248 -2.312,-5.58279997600248 H 3.2702997 C 1.4641997,-8.7600248e-7 -3.525637e-7,1.4641991 -3.525637e-7,3.2702991 V 82.451 c 0,2.913 3.5226000525637,4.373 5.5828000525637,2.312 L 20.366299,69.98 c 0.6134,-0.613 0.9579,-1.445 0.9579,-2.313 V 24.5965 c 0,-1.8062 1.4642,-3.2703 3.2703,-3.2703 h 43.0706 c 0.868,0 1.699,-0.3446 2.313,-0.9579 z"
fill="#ffffff"
id="path1"
style="fill:url(#linearGradient9);fill-opacity:1" />
<defs
id="defs30">
<linearGradient
id="linearGradient8"
inkscape:collect="always">
<stop
style="stop-color:#ffffff;stop-opacity:1;"
offset="0"
id="stop8" />
<stop
style="stop-color:#d3d3d3;stop-opacity:1;"
offset="1"
id="stop9" />
</linearGradient>
<linearGradient
id="paint0_linear_659_300"
x1="166.964"
y1="113.94"
x2="81.113297"
y2="113.94"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(-55.003901,-43.001)">
<stop
stop-color="#3004A0"
id="stop11" />
<stop
offset="1"
stop-color="#0567E8"
id="stop12" />
</linearGradient>
<linearGradient
id="paint1_linear_659_300"
x1="166.964"
y1="113.94"
x2="81.113297"
y2="113.94"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(-55.003901,-43.001)">
<stop
stop-color="#3004A0"
id="stop13" />
<stop
offset="1"
stop-color="#0567E8"
id="stop14" />
</linearGradient>
<linearGradient
id="paint2_linear_659_300"
x1="166.964"
y1="113.94"
x2="81.113297"
y2="113.94"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(-55.003901,-43.001)">
<stop
stop-color="#3004A0"
id="stop15" />
<stop
offset="1"
stop-color="#0567E8"
id="stop16" />
</linearGradient>
<linearGradient
id="paint3_linear_659_300"
x1="166.964"
y1="113.94"
x2="81.113297"
y2="113.94"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(-55.003901,-43.001)">
<stop
stop-color="#3004A0"
id="stop17" />
<stop
offset="1"
stop-color="#0567E8"
id="stop18" />
</linearGradient>
<linearGradient
id="paint4_linear_659_300"
x1="166.964"
y1="113.94"
x2="81.113297"
y2="113.94"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(-55.003901,-43.001)">
<stop
stop-color="#3004A0"
id="stop19" />
<stop
offset="1"
stop-color="#0567E8"
id="stop20" />
</linearGradient>
<linearGradient
id="paint5_linear_659_300"
x1="166.964"
y1="113.94"
x2="81.113297"
y2="113.94"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(-55.003901,-43.001)">
<stop
stop-color="#3004A0"
id="stop21" />
<stop
offset="1"
stop-color="#0567E8"
id="stop22" />
</linearGradient>
<linearGradient
id="paint6_linear_659_300"
x1="166.964"
y1="113.94"
x2="81.113297"
y2="113.94"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(-55.003901,-43.001)">
<stop
stop-color="#3004A0"
id="stop23" />
<stop
offset="1"
stop-color="#0567E8"
id="stop24" />
</linearGradient>
<linearGradient
id="paint7_linear_659_300"
x1="166.964"
y1="113.94"
x2="81.113297"
y2="113.94"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(-55.003901,-43.001)">
<stop
stop-color="#3004A0"
id="stop25" />
<stop
offset="1"
stop-color="#0567E8"
id="stop26" />
</linearGradient>
<linearGradient
id="paint8_linear_659_300"
x1="166.964"
y1="113.94"
x2="81.113297"
y2="113.94"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(-55.003901,-43.001)">
<stop
stop-color="#3004A0"
id="stop27" />
<stop
offset="1"
stop-color="#0567E8"
id="stop28" />
</linearGradient>
<linearGradient
id="paint9_linear_659_300"
x1="166.964"
y1="113.94"
x2="81.113297"
y2="113.94"
gradientUnits="userSpaceOnUse"
gradientTransform="translate(-55.003901,-43.001)">
<stop
stop-color="#3004A0"
id="stop29" />
<stop
offset="1"
stop-color="#0567E8"
id="stop30" />
</linearGradient>
<linearGradient
id="paint0_linear_200_827"
x1="29.933599"
y1="29.966999"
x2="59.579201"
y2="62.929798"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(2.4590919,0,0,2.4590868,-43.987224,-43.02245)">
<stop
stop-color="#26D1FF"
id="stop17-2" />
<stop
offset="0.545685"
stop-color="#0A62F5"
id="stop18-9" />
<stop
offset="1"
stop-color="#1E05FF"
id="stop19-1" />
</linearGradient>
<linearGradient
id="paint1_linear_200_827"
x1="45.967999"
y1="28.6367"
x2="45.967999"
y2="64.033302"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(2.4590919,0,0,2.4590868,-43.987224,-43.02245)">
<stop
stop-color="#EDEDED"
stop-opacity="0"
id="stop20-2" />
<stop
offset="1"
stop-color="#303030"
id="stop21-7" />
</linearGradient>
<linearGradient
inkscape:collect="always"
xlink:href="#linearGradient8"
id="linearGradient9"
x1="85.727425"
y1="42.863884"
x2="-2.508903e-06"
y2="42.863884"
gradientUnits="userSpaceOnUse" />
</defs>
<path
d="m 45.678643,73.496213 v 17.17967 c 0,0.67748 -0.549362,1.22662 -1.226841,1.22662 H 27.558578 c -0.67748,0 -1.226841,-0.54914 -1.226841,-1.22662 v -17.17967 c 0,-0.67748 0.549361,-1.22659 1.226841,-1.22659 h 16.893224 c 0.677479,0 1.226841,0.54911 1.226841,1.22659 z m 0,-22.333424 v 17.179674 c 0,0.67748 -0.549362,1.22659 -1.226841,1.22659 H 27.558578 c -0.67748,0 -1.226841,-0.54911 -1.226841,-1.22659 V 51.162789 c 0,-0.677477 0.549361,-1.226592 1.226841,-1.226592 h 16.893224 c 0.677479,0 1.226841,0.549115 1.226841,1.226592 z m 22.046984,0 v 17.179674 c 0,0.67748 -0.54912,1.22659 -1.2266,1.22659 H 49.605813 c -0.677481,0 -1.226841,-0.54911 -1.226841,-1.22659 V 51.162789 c 0,-0.677477 0.54936,-1.226592 1.226841,-1.226592 h 16.893214 c 0.67748,0 1.2266,0.549115 1.2266,1.226592 z m 0,-22.333425 v 17.179672 c 0,0.677477 -0.54912,1.226592 -1.2266,1.226592 H 49.605813 c -0.677481,0 -1.226841,-0.549115 -1.226841,-1.226592 V 28.829364 c 0,-0.67748 0.54936,-1.226593 1.226841,-1.226593 h 16.893214 c 0.67748,0 1.2266,0.549113 1.2266,1.226593 z m 22.04723,22.333425 v 17.179674 c 0,0.67748 -0.54912,1.22659 -1.2266,1.22659 h -16.89346 c -0.67748,0 -1.2266,-0.54911 -1.2266,-1.22659 V 51.162789 c 0,-0.677477 0.54912,-1.226592 1.2266,-1.226592 h 16.89346 c 0.67748,0 1.2266,0.549115 1.2266,1.226592 z m -22.04723,22.333424 v 17.17967 c 0,0.67748 -0.54912,1.22662 -1.2266,1.22662 H 49.605813 c -0.677481,0 -1.226841,-0.54914 -1.226841,-1.22662 v -17.17967 c 0,-0.67748 0.54936,-1.22659 1.226841,-1.22659 h 16.893214 c 0.67748,0 1.2266,0.54911 1.2266,1.22659 z m 22.04723,-44.666849 v 17.179672 c 0,0.677477 -0.54912,1.226592 -1.2266,1.226592 h -16.89346 c -0.67748,0 -1.2266,-0.549115 -1.2266,-1.226592 V 28.829364 c 0,-0.67748 0.54912,-1.226593 1.2266,-1.226593 h 16.89346 c 0.67748,0 1.2266,0.549113 1.2266,1.226593 z m -44.094214,0 v 17.179672 c 0,0.677477 -0.549362,1.226592 -1.226841,1.226592 H 27.558578 c -0.67748,0 -1.226841,-0.549115 -1.226841,-1.226592 V 28.829364 c 0,-0.67748 0.549361,-1.226593 1.226841,-1.226593 h 16.893224 c 0.677479,0 1.226841,0.549113 1.226841,1.226593 z m 0,67.000289 v 17.179677 c 0,0.67748 -0.549362,1.22659 -1.226841,1.22659 H 27.558578 c -0.67748,0 -1.226841,-0.54911 -1.226841,-1.22659 V 95.829653 c 0,-0.67748 0.549361,-1.22659 1.226841,-1.22659 h 16.893224 c 0.677479,0 1.226841,0.54911 1.226841,1.22659 z M 111.77289,28.829364 v 17.179672 c 0,0.677477 -0.54911,1.226592 -1.22659,1.226592 H 93.653067 c -0.67748,0 -1.22684,-0.549115 -1.22684,-1.226592 V 28.829364 c 0,-0.67748 0.54936,-1.226593 1.22684,-1.226593 H 110.5463 c 0.67748,0 1.22659,0.549113 1.22659,1.226593 z"
fill="url(#paint0_linear_200_827)"
stroke="url(#paint1_linear_200_827)"
stroke-width="0.40985"
id="path2-9"
style="fill:url(#paint0_linear_200_827);stroke:url(#paint1_linear_200_827)" />
</svg>

Before

Width:  |  Height:  |  Size: 9.9 KiB

View file

@ -1,3 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20.8 3.75H3.2C1.9867 3.75 1 4.67503 1 5.8125V18.1875C1 19.325 1.9867 20.25 3.2 20.25H20.8C22.0133 20.25 23 19.325 23 18.1875V5.8125C23 4.67503 22.0133 3.75 20.8 3.75ZM10.9 9.9375H7.6V14.0625H10.9V16.125H7.6C6.3867 16.125 5.4 15.2 5.4 14.0625V9.9375C5.4 8.80003 6.3867 7.875 7.6 7.875H10.9V9.9375ZM18.6 9.9375H15.3V14.0625H18.6V16.125H15.3C14.0867 16.125 13.1 15.2 13.1 14.0625V9.9375C13.1 8.80003 14.0867 7.875 15.3 7.875H18.6V9.9375Z" fill="#595959"/>
</svg>

Before

Width:  |  Height:  |  Size: 566 B

View file

@ -1,3 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20.8 3.75H3.2C1.9867 3.75 1 4.67503 1 5.8125V18.1875C1 19.325 1.9867 20.25 3.2 20.25H20.8C22.0133 20.25 23 19.325 23 18.1875V5.8125C23 4.67503 22.0133 3.75 20.8 3.75ZM10.9 9.9375H7.6V14.0625H10.9V16.125H7.6C6.3867 16.125 5.4 15.2 5.4 14.0625V9.9375C5.4 8.80003 6.3867 7.875 7.6 7.875H10.9V9.9375ZM18.6 9.9375H15.3V14.0625H18.6V16.125H15.3C14.0867 16.125 13.1 15.2 13.1 14.0625V9.9375C13.1 8.80003 14.0867 7.875 15.3 7.875H18.6V9.9375Z" fill="#8f8f8f"/>
</svg>

Before

Width:  |  Height:  |  Size: 566 B

View file

@ -1,3 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20.8 3.75H3.2C1.9867 3.75 1 4.67503 1 5.8125V18.1875C1 19.325 1.9867 20.25 3.2 20.25H20.8C22.0133 20.25 23 19.325 23 18.1875V5.8125C23 4.67503 22.0133 3.75 20.8 3.75ZM10.9 9.9375H7.6V14.0625H10.9V16.125H7.6C6.3867 16.125 5.4 15.2 5.4 14.0625V9.9375C5.4 8.80003 6.3867 7.875 7.6 7.875H10.9V9.9375ZM18.6 9.9375H15.3V14.0625H18.6V16.125H15.3C14.0867 16.125 13.1 15.2 13.1 14.0625V9.9375C13.1 8.80003 14.0867 7.875 15.3 7.875H18.6V9.9375Z" fill="#c9c9c9"/>
</svg>

Before

Width:  |  Height:  |  Size: 566 B

View file

@ -1,3 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20.8 3.75H3.2C1.9867 3.75 1 4.67503 1 5.8125V18.1875C1 19.325 1.9867 20.25 3.2 20.25H20.8C22.0133 20.25 23 19.325 23 18.1875V5.8125C23 4.67503 22.0133 3.75 20.8 3.75ZM10.9 9.9375H7.6V14.0625H10.9V16.125H7.6C6.3867 16.125 5.4 15.2 5.4 14.0625V9.9375C5.4 8.80003 6.3867 7.875 7.6 7.875H10.9V9.9375ZM18.6 9.9375H15.3V14.0625H18.6V16.125H15.3C14.0867 16.125 13.1 15.2 13.1 14.0625V9.9375C13.1 8.80003 14.0867 7.875 15.3 7.875H18.6V9.9375Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 564 B

View file

@ -1,3 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19.6652 6.35151C20.1116 6.82018 20.1116 7.58132 19.6652 8.04999L10.5234 17.6485C10.077 18.1172 9.35208 18.1172 8.9057 17.6485L4.33478 12.8492C3.88841 12.3806 3.88841 11.6194 4.33478 11.1508C4.78116 10.6821 5.50608 10.6821 5.95246 11.1508L9.71633 15.0989L18.0511 6.35151C18.4975 5.88283 19.2224 5.88283 19.6688 6.35151H19.6652Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 456 B

View file

@ -1,6 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.24976 1H2.24976C2.08558 0.999907 1.92299 1.03218 1.77129 1.09496C1.61959 1.15775 1.48176 1.24982 1.36566 1.36591C1.24957 1.482 1.1575 1.61984 1.09472 1.77154C1.03193 1.92323 0.999663 2.08582 0.999756 2.25V7.25C0.999756 7.58152 1.13145 7.89946 1.36587 8.13388C1.60029 8.3683 1.91824 8.5 2.24976 8.5C2.58128 8.5 2.89922 8.3683 3.13364 8.13388C3.36806 7.89946 3.49976 7.58152 3.49976 7.25V3.5H7.24976C7.58128 3.5 7.89922 3.3683 8.13364 3.13388C8.36806 2.89946 8.49976 2.58152 8.49976 2.25C8.49976 1.91848 8.36806 1.60054 8.13364 1.36612C7.89922 1.1317 7.58128 1 7.24976 1Z" fill="#c9c9c9"/>
<path d="M19.7498 1H14.7498C14.4182 1 14.1003 1.1317 13.8659 1.36612C13.6315 1.60054 13.4998 1.91848 13.4998 2.25C13.4998 2.58152 13.6315 2.89946 13.8659 3.13388C14.1003 3.3683 14.4182 3.5 14.7498 3.5H18.4998V7.25C18.4998 7.58152 18.6315 7.89946 18.8659 8.13388C19.1003 8.3683 19.4182 8.5 19.7498 8.5C20.0813 8.5 20.3992 8.3683 20.6336 8.13388C20.8681 7.89946 20.9998 7.58152 20.9998 7.25V2.25C20.9998 2.08582 20.9676 1.92323 20.9048 1.77154C20.842 1.61984 20.7499 1.482 20.6338 1.36591C20.5178 1.24982 20.3799 1.15775 20.2282 1.09496C20.0765 1.03218 19.9139 0.999907 19.7498 1Z" fill="#c9c9c9"/>
<path d="M7.24976 18.5H3.49976V14.75C3.49976 14.4185 3.36806 14.1005 3.13364 13.8661C2.89922 13.6317 2.58128 13.5 2.24976 13.5C1.91824 13.5 1.60029 13.6317 1.36587 13.8661C1.13145 14.1005 0.999756 14.4185 0.999756 14.75V19.75C0.999663 19.9142 1.03193 20.0768 1.09472 20.2285C1.1575 20.3802 1.24957 20.518 1.36566 20.6341C1.48176 20.7502 1.61959 20.8423 1.77129 20.905C1.92299 20.9678 2.08558 21.0001 2.24976 21H7.24976C7.58128 21 7.89922 20.8683 8.13364 20.6339C8.36806 20.3995 8.49976 20.0815 8.49976 19.75C8.49976 19.4185 8.36806 19.1005 8.13364 18.8661C7.89922 18.6317 7.58128 18.5 7.24976 18.5Z" fill="#c9c9c9"/>
<path d="M19.7498 13.5C19.5856 13.4999 19.423 13.5322 19.2713 13.595C19.1196 13.6577 18.9818 13.7498 18.8657 13.8659C18.7496 13.982 18.6575 14.1198 18.5947 14.2715C18.5319 14.4232 18.4997 14.5858 18.4998 14.75V18.5H14.7498C14.4182 18.5 14.1003 18.6317 13.8659 18.8661C13.6315 19.1005 13.4998 19.4185 13.4998 19.75C13.4998 20.0815 13.6315 20.3995 13.8659 20.6339C14.1003 20.8683 14.4182 21 14.7498 21H19.7498C19.9139 21.0001 20.0765 20.9678 20.2282 20.905C20.3799 20.8423 20.5178 20.7502 20.6338 20.6341C20.7499 20.518 20.842 20.3802 20.9048 20.2285C20.9676 20.0768 20.9998 19.9142 20.9998 19.75V14.75C20.9998 14.5858 20.9676 14.4232 20.9048 14.2715C20.842 14.1198 20.7499 13.982 20.6338 13.8659C20.5178 13.7498 20.3799 13.6577 20.2282 13.595C20.0765 13.5322 19.9139 13.4999 19.7498 13.5Z" fill="#c9c9c9"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.7 KiB

View file

@ -1,6 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M7.24976 1H2.24976C2.08558 0.999907 1.92299 1.03218 1.77129 1.09496C1.61959 1.15775 1.48176 1.24982 1.36566 1.36591C1.24957 1.482 1.1575 1.61984 1.09472 1.77154C1.03193 1.92323 0.999663 2.08582 0.999756 2.25V7.25C0.999756 7.58152 1.13145 7.89946 1.36587 8.13388C1.60029 8.3683 1.91824 8.5 2.24976 8.5C2.58128 8.5 2.89922 8.3683 3.13364 8.13388C3.36806 7.89946 3.49976 7.58152 3.49976 7.25V3.5H7.24976C7.58128 3.5 7.89922 3.3683 8.13364 3.13388C8.36806 2.89946 8.49976 2.58152 8.49976 2.25C8.49976 1.91848 8.36806 1.60054 8.13364 1.36612C7.89922 1.1317 7.58128 1 7.24976 1Z" fill="white"/>
<path d="M19.7498 1H14.7498C14.4182 1 14.1003 1.1317 13.8659 1.36612C13.6315 1.60054 13.4998 1.91848 13.4998 2.25C13.4998 2.58152 13.6315 2.89946 13.8659 3.13388C14.1003 3.3683 14.4182 3.5 14.7498 3.5H18.4998V7.25C18.4998 7.58152 18.6315 7.89946 18.8659 8.13388C19.1003 8.3683 19.4182 8.5 19.7498 8.5C20.0813 8.5 20.3992 8.3683 20.6336 8.13388C20.8681 7.89946 20.9998 7.58152 20.9998 7.25V2.25C20.9998 2.08582 20.9676 1.92323 20.9048 1.77154C20.842 1.61984 20.7499 1.482 20.6338 1.36591C20.5178 1.24982 20.3799 1.15775 20.2282 1.09496C20.0765 1.03218 19.9139 0.999907 19.7498 1Z" fill="white"/>
<path d="M7.24976 18.5H3.49976V14.75C3.49976 14.4185 3.36806 14.1005 3.13364 13.8661C2.89922 13.6317 2.58128 13.5 2.24976 13.5C1.91824 13.5 1.60029 13.6317 1.36587 13.8661C1.13145 14.1005 0.999756 14.4185 0.999756 14.75V19.75C0.999663 19.9142 1.03193 20.0768 1.09472 20.2285C1.1575 20.3802 1.24957 20.518 1.36566 20.6341C1.48176 20.7502 1.61959 20.8423 1.77129 20.905C1.92299 20.9678 2.08558 21.0001 2.24976 21H7.24976C7.58128 21 7.89922 20.8683 8.13364 20.6339C8.36806 20.3995 8.49976 20.0815 8.49976 19.75C8.49976 19.4185 8.36806 19.1005 8.13364 18.8661C7.89922 18.6317 7.58128 18.5 7.24976 18.5Z" fill="white"/>
<path d="M19.7498 13.5C19.5856 13.4999 19.423 13.5322 19.2713 13.595C19.1196 13.6577 18.9818 13.7498 18.8657 13.8659C18.7496 13.982 18.6575 14.1198 18.5947 14.2715C18.5319 14.4232 18.4997 14.5858 18.4998 14.75V18.5H14.7498C14.4182 18.5 14.1003 18.6317 13.8659 18.8661C13.6315 19.1005 13.4998 19.4185 13.4998 19.75C13.4998 20.0815 13.6315 20.3995 13.8659 20.6339C14.1003 20.8683 14.4182 21 14.7498 21H19.7498C19.9139 21.0001 20.0765 20.9678 20.2282 20.905C20.3799 20.8423 20.5178 20.7502 20.6338 20.6341C20.7499 20.518 20.842 20.3802 20.9048 20.2285C20.9676 20.0768 20.9998 19.9142 20.9998 19.75V14.75C20.9998 14.5858 20.9676 14.4232 20.9048 14.2715C20.842 14.1198 20.7499 13.982 20.6338 13.8659C20.5178 13.7498 20.3799 13.6577 20.2282 13.595C20.0765 13.5322 19.9139 13.4999 19.7498 13.5Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 2.7 KiB

View file

@ -1,3 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.84351 17.1171V14.5887H9.28498V20.97H6.69635V17.1171H2.84351ZM6.69635 6.88299V3.03015H9.28498V9.41142H2.84351V6.88299H6.69635ZM14.402 20.97V14.5887H20.8435V17.1171H16.9907V20.97H14.402ZM16.9907 6.88299H20.8435V9.41142H14.402V3.03015H16.9907V6.88299Z" fill="#c9c9c9"/>
</svg>

Before

Width:  |  Height:  |  Size: 382 B

View file

@ -1,3 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2.84351 17.1171V14.5887H9.28498V20.97H6.69635V17.1171H2.84351ZM6.69635 6.88299V3.03015H9.28498V9.41142H2.84351V6.88299H6.69635ZM14.402 20.97V14.5887H20.8435V17.1171H16.9907V20.97H14.402ZM16.9907 6.88299H20.8435V9.41142H14.402V3.03015H16.9907V6.88299Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 380 B

View file

@ -1,4 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.9924 14.5755V3.85875C14.9924 3.59609 14.9026 3.36832 14.7232 3.17544C14.5437 2.98255 14.3246 2.88611 14.0659 2.88611C13.8071 2.88611 13.5734 2.98871 13.3647 3.19391L8.444 8.03273L14.9924 14.5755ZM14.9924 17.4027V19.9381C14.9924 20.2007 14.9026 20.4244 14.7232 20.6091C14.5437 20.7938 14.3246 20.8861 14.0659 20.8861C13.7988 20.8861 13.565 20.7876 13.3647 20.5906L8.35661 15.6782H4.42524C4.15814 15.6782 3.93277 15.5879 3.74914 15.4073C3.56551 15.2267 3.47369 15.0051 3.47369 14.7425V9.05437C3.47369 8.79172 3.56551 8.5701 3.74914 8.38953C3.93277 8.20895 4.15814 8.11867 4.42524 8.11867H5.72523C5.76007 8.16978 5.80018 8.21839 5.84555 8.26372L14.9924 17.4027Z" fill="#c9c9c9"/>
<path d="M2.70729 1.71153C3.09775 1.32108 3.73078 1.321 4.12133 1.71137L22.062 19.6435C22.4527 20.034 22.4527 20.6673 22.0621 21.0579C21.6717 21.4483 21.0386 21.4484 20.6481 21.0581L2.70746 3.12592C2.31677 2.73542 2.3167 2.10212 2.70729 1.71153Z" fill="#c9c9c9"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -1,4 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd" d="M14.9924 14.5755V3.85875C14.9924 3.59609 14.9026 3.36832 14.7232 3.17544C14.5437 2.98255 14.3246 2.88611 14.0659 2.88611C13.8071 2.88611 13.5734 2.98871 13.3647 3.19391L8.444 8.03273L14.9924 14.5755ZM14.9924 17.4027V19.9381C14.9924 20.2007 14.9026 20.4244 14.7232 20.6091C14.5437 20.7938 14.3246 20.8861 14.0659 20.8861C13.7988 20.8861 13.565 20.7876 13.3647 20.5906L8.35661 15.6782H4.42524C4.15814 15.6782 3.93277 15.5879 3.74914 15.4073C3.56551 15.2267 3.47369 15.0051 3.47369 14.7425V9.05437C3.47369 8.79172 3.56551 8.5701 3.74914 8.38953C3.93277 8.20895 4.15814 8.11867 4.42524 8.11867H5.72523C5.76007 8.16978 5.80018 8.21839 5.84555 8.26372L14.9924 17.4027Z" fill="white"/>
<path d="M2.70729 1.71153C3.09775 1.32108 3.73078 1.321 4.12133 1.71137L22.062 19.6435C22.4527 20.034 22.4527 20.6673 22.0621 21.0579C21.6717 21.4483 21.0386 21.4484 20.6481 21.0581L2.70746 3.12592C2.31677 2.73542 2.3167 2.10212 2.70729 1.71153Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -1,3 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5 20V4H9.56376V20H5ZM14.1812 4H18.745V20H14.1812V4Z" fill="#c9c9c9"/>
</svg>

Before

Width:  |  Height:  |  Size: 183 B

View file

@ -1,3 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5 20V4H9.56376V20H5ZM14.1812 4H18.745V20H14.1812V4Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 181 B

View file

@ -1,3 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.34282 20C5.9769 20 5.67501 19.8658 5.43716 19.5975C5.19931 19.3291 5.08038 18.9693 5.08038 18.518V5.47284C5.08038 5.02154 5.19931 4.66476 5.43716 4.40252C5.67501 4.13417 5.9769 4 6.34282 4C6.53798 4 6.72399 4.03354 6.90085 4.10063C7.08381 4.16771 7.27897 4.2592 7.48633 4.37507L18.2719 10.6232C18.6683 10.8489 18.9519 11.0623 19.1227 11.2636C19.2935 11.4648 19.3788 11.7088 19.3788 11.9954C19.3788 12.2821 19.2935 12.526 19.1227 12.7273C18.9519 12.9285 18.6683 13.145 18.2719 13.3768L7.48633 19.6158C7.27897 19.7378 7.08381 19.8323 6.90085 19.8994C6.72399 19.9665 6.53798 20 6.34282 20Z" fill="#c9c9c9"/>
</svg>

Before

Width:  |  Height:  |  Size: 720 B

View file

@ -1,3 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.34282 20C5.9769 20 5.67501 19.8658 5.43716 19.5975C5.19931 19.3291 5.08038 18.9693 5.08038 18.518V5.47284C5.08038 5.02154 5.19931 4.66476 5.43716 4.40252C5.67501 4.13417 5.9769 4 6.34282 4C6.53798 4 6.72399 4.03354 6.90085 4.10063C7.08381 4.16771 7.27897 4.2592 7.48633 4.37507L18.2719 10.6232C18.6683 10.8489 18.9519 11.0623 19.1227 11.2636C19.2935 11.4648 19.3788 11.7088 19.3788 11.9954C19.3788 12.2821 19.2935 12.526 19.1227 12.7273C18.9519 12.9285 18.6683 13.145 18.2719 13.3768L7.48633 19.6158C7.27897 19.7378 7.08381 19.8323 6.90085 19.8994C6.72399 19.9665 6.53798 20 6.34282 20Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 718 B

View file

@ -1,17 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M 13.7129,1.20037 C 13.5599,1.1763 13.4038,1.18275 13.2534,1.21936 13.1029,1.25597 12.9611,1.32202 12.8361,1.41374 12.711,1.50546 12.6052,1.62105 12.5245,1.75392 c -0.0806,0.13287 -0.1344,0.28041 -0.1584,0.43419 -0.0239,0.15379 -0.0175,0.31081 0.0189,0.4621 0.0364,0.1513 0.1021,0.2939 0.1933,0.41967 0.0912,0.12576 0.2061,0.23223 0.3383,0.31333 0.1321,0.08109 0.2788,0.13522 0.4317,0.15929 3.389098,0.232344 5.207929,2.038111 6.604054,4.253237 1.118996,1.775427 1.325364,4.141017 0.792011,6.245906 -0.430385,1.678707 -1.358294,3.188482 -2.663099,4.333639 -1.304899,1.145121 -2.926554,1.872712 -4.654699,2.088365 -0.157845,0.01485 -0.311047,0.06109 -0.450441,0.135992 -0.139396,0.07491 -0.262211,0.176936 -0.361116,0.300045 -0.09891,0.123118 -0.171919,0.264807 -0.21471,0.416667 -0.04279,0.151859 -0.05447,0.310816 -0.03436,0.467418 0.0201,0.156603 0.07167,0.307691 0.151516,0.444297 0.07976,0.136572 0.186337,0.25591 0.31331,0.350903 0.126873,0.09498 0.271635,0.163699 0.42563,0.202072 0.153995,0.03837 0.314022,0.0456 0.470684,0.02128 2.551396,-0.319899 4.90852,-1.514905 6.665935,-3.379393 1.757318,-1.864512 2.80501,-4.281972 2.962701,-6.836347 C 23.513427,10.032106 22.806641,7.372284 21.257196,5.427038 19.461266,3.172344 17.377145,1.625939 13.7129,1.20037 Z"
fill="#c9c9c9" />
<path
d="M 8.07143,15.3046 V 9.35604 C 8.07157,9.08675 8.14013,8.82197 8.2706,8.58682 8.40107,8.35166 8.58912,8.15394 8.81691,8.0124 9.04469,7.87086 9.30467,7.79021 9.57215,7.77809 9.83964,7.76598 10.1058,7.8228 10.3453,7.94317 l 5.9148,2.97433 c 0.2607,0.1313 0.4799,0.3331 0.6331,0.5826 0.1531,0.2495 0.2343,0.537 0.2343,0.8302 0,0.2933 -0.0812,0.5807 -0.2343,0.8302 -0.1532,0.2496 -0.3724,0.4513 -0.6331,0.5827 l -5.9148,2.9743 C 10.1058,16.8378 9.83964,16.8947 9.57215,16.8826 9.30467,16.8704 9.04469,16.7898 8.81691,16.6482 8.58912,16.5067 8.40107,16.309 8.2706,16.0738 8.14013,15.8387 8.07157,15.5739 8.07143,15.3046 Z"
fill="#c9c9c9" />
<path
d="M 10.6863,2.31225 C 10.725,2.46314 10.7338,2.62022 10.7121,2.77452 10.6904,2.92881 10.6387,3.07729 10.5598,3.21146 10.481,3.34562 10.3767,3.46284 10.2528,3.55641 10.1289,3.64997 9.98781,3.71804 9.83771,3.75673 9.44403,3.8586 9.05806,3.98852 8.68271,4.1455 8.53981,4.20527 8.38659,4.23615 8.23182,4.23637 8.07705,4.23659 7.92375,4.20616 7.78068,4.14679 7.6376,4.08743 7.50755,4.00031 7.39796,3.8904 7.28836,3.7805 7.20136,3.64996 7.14193,3.50624 7.0825,3.36251 7.05179,3.20843 7.05157,3.05277 7.05135,2.89712 7.08162,2.74295 7.14064,2.59905 7.19967,2.45516 7.28629,2.32437 7.39558,2.21415 7.50486,2.10393 7.63467,2.01644 7.77757,1.95667 8.25214,1.75754 8.744,1.5916 9.25157,1.46043 9.40149,1.42171 9.55753,1.41307 9.71078,1.435 c 0.15324,0.02194 0.30072,0.07402 0.43392,0.15327 0.1332,0.07924 0.2496,0.18411 0.3425,0.3086 0.093,0.1245 0.1606,0.2646 0.1991,0.41538 z M 6.005,4.26087 C 6.2224,4.48645 6.34188,4.78959 6.33716,5.10371 6.33245,5.41782 6.20393,5.71721 5.97986,5.93608 5.39555,6.50882 4.89314,7.16036 4.487,7.87205 4.4123,8.01106 4.31071,8.13365 4.18821,8.2326 4.06572,8.33156 3.92478,8.40488 3.7737,8.44826 3.62261,8.49163 3.46443,8.50419 3.30845,8.48519 3.15247,8.46619 3.00185,8.41602 2.86543,8.33762 2.72901,8.25922 2.60956,8.15419 2.51409,8.02868 2.41862,7.90318 2.34907,7.75975 2.30953,7.60682 2.26999,7.45389 2.26125,7.29455 2.28382,7.13818 2.3064,6.9818 2.35984,6.83155 2.441,6.69625 2.95566,5.7904 3.59471,4.96203 4.33929,4.23558 4.56358,4.01694 4.86501,3.89679 5.17735,3.90153 5.48968,3.90627 5.78737,4.03552 6.005,4.26087 Z"
fill="#c9c9c9" />
<path
d="m 2.5268743,9.5228951 c 0.3098728,0.046129 0.589468,0.2134487 0.7773467,0.4652329 0.1878804,0.25177 0.2686758,0.567403 0.22464,0.877519 -0.1136188,0.810272 -0.1142716,1.633023 -0.00195,2.44471 0.025607,0.155718 0.019891,0.314829 -0.016815,0.467963 -0.036692,0.153135 -0.103645,0.297209 -0.1968964,0.423744 -0.093265,0.126534 -0.2109436,0.232979 -0.3461307,0.313074 -0.1351869,0.08009 -0.285141,0.132222 -0.4410639,0.153317 -0.155923,0.0211 -0.3146436,0.01076 -0.4668564,-0.03047 C 1.9069428,14.596772 1.7643374,14.525546 1.6397164,14.428487 1.5150953,14.331426 1.4109697,14.2105 1.3334655,14.072826 1.2559631,13.935137 1.2066531,13.783482 1.1884279,13.626763 1.043654,12.595027 1.0447093,11.548805 1.1915583,10.518968 1.2359088,10.2089 1.4014648,9.9298163 1.6518506,9.7430394 1.9022287,9.5562686 2.2169594,9.4770886 2.5268743,9.5228951 Z"
fill="#c9c9c9" />
<path
d="m 2.9263167,15.692773 c 0.2757306,-0.148735 0.5998338,-0.182299 0.9011147,-0.09328 0.3012739,0.089 0.5550776,0.293292 0.7056331,0.567965 0.3945227,0.716798 0.886966,1.375904 1.4632309,1.958459 0.1138029,0.10933 0.2045599,0.240142 0.266925,0.384737 0.062378,0.144591 0.095098,0.300056 0.096254,0.457235 0.00115,0.157187 -0.029296,0.312918 -0.089537,0.458044 -0.060243,0.145126 -0.1490677,0.276708 -0.2612635,0.387021 -0.112196,0.110313 -0.2454778,0.197121 -0.3920371,0.255324 -0.1465494,0.05821 -0.3033987,0.08662 -0.4613283,0.08359 -0.1579298,-0.003 -0.3137503,-0.03748 -0.4582919,-0.101261 C 4.5524683,19.986807 4.4221224,19.894932 4.3136299,19.780379 3.5795376,19.04109 2.9535201,18.202827 2.4540459,17.290329 2.3037709,17.015508 2.2691013,16.692871 2.3576556,16.39331 2.4462071,16.093759 2.6507458,15.841788 2.9263167,15.692773 Z m 4.1953049,4.873184 c 0.1254576,-0.286226 0.3601429,-0.510811 0.6524735,-0.624396 0.2923085,-0.113581 0.618309,-0.106861 0.9063581,0.01865 0.3756122,0.163711 0.762213,0.30108 1.156869,0.411084 0.3028188,0.08453 0.5598248,0.285213 0.7145048,0.55787 0.154676,0.272665 0.194271,0.594953 0.110218,0.89602 -0.08406,0.301066 -0.284866,0.556237 -0.55841,0.709339 -0.273548,0.153148 -0.5973332,0.19167 -0.9001528,0.107127 C 8.6966635,22.50016 8.2055345,22.325523 7.7348173,22.119051 7.4469054,21.993382 7.2204524,21.759141 7.1055001,21.467913 6.9904996,21.176672 6.9963029,20.852243 7.1216216,20.565957 Z"
fill="#c9c9c9" />
</svg>

Before

Width:  |  Height:  |  Size: 5.8 KiB

View file

@ -1,17 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M 13.7129,1.20037 C 13.5599,1.1763 13.4038,1.18275 13.2534,1.21936 13.1029,1.25597 12.9611,1.32202 12.8361,1.41374 12.711,1.50546 12.6052,1.62105 12.5245,1.75392 c -0.0806,0.13287 -0.1344,0.28041 -0.1584,0.43419 -0.0239,0.15379 -0.0175,0.31081 0.0189,0.4621 0.0364,0.1513 0.1021,0.2939 0.1933,0.41967 0.0912,0.12576 0.2061,0.23223 0.3383,0.31333 0.1321,0.08109 0.2788,0.13522 0.4317,0.15929 3.389098,0.232344 5.207929,2.038111 6.604054,4.253237 1.118996,1.775427 1.325364,4.141017 0.792011,6.245906 -0.430385,1.678707 -1.358294,3.188482 -2.663099,4.333639 -1.304899,1.145121 -2.926554,1.872712 -4.654699,2.088365 -0.157845,0.01485 -0.311047,0.06109 -0.450441,0.135992 -0.139396,0.07491 -0.262211,0.176936 -0.361116,0.300045 -0.09891,0.123118 -0.171919,0.264807 -0.21471,0.416667 -0.04279,0.151859 -0.05447,0.310816 -0.03436,0.467418 0.0201,0.156603 0.07167,0.307691 0.151516,0.444297 0.07976,0.136572 0.186337,0.25591 0.31331,0.350903 0.126873,0.09498 0.271635,0.163699 0.42563,0.202072 0.153995,0.03837 0.314022,0.0456 0.470684,0.02128 2.551396,-0.319899 4.90852,-1.514905 6.665935,-3.379393 1.757318,-1.864512 2.80501,-4.281972 2.962701,-6.836347 C 23.513427,10.032106 22.806641,7.372284 21.257196,5.427038 19.461266,3.172344 17.377145,1.625939 13.7129,1.20037 Z"
fill="#ffffff" />
<path
d="M 8.07143,15.3046 V 9.35604 C 8.07157,9.08675 8.14013,8.82197 8.2706,8.58682 8.40107,8.35166 8.58912,8.15394 8.81691,8.0124 9.04469,7.87086 9.30467,7.79021 9.57215,7.77809 9.83964,7.76598 10.1058,7.8228 10.3453,7.94317 l 5.9148,2.97433 c 0.2607,0.1313 0.4799,0.3331 0.6331,0.5826 0.1531,0.2495 0.2343,0.537 0.2343,0.8302 0,0.2933 -0.0812,0.5807 -0.2343,0.8302 -0.1532,0.2496 -0.3724,0.4513 -0.6331,0.5827 l -5.9148,2.9743 C 10.1058,16.8378 9.83964,16.8947 9.57215,16.8826 9.30467,16.8704 9.04469,16.7898 8.81691,16.6482 8.58912,16.5067 8.40107,16.309 8.2706,16.0738 8.14013,15.8387 8.07157,15.5739 8.07143,15.3046 Z"
fill="#ffffff" />
<path
d="M 10.6863,2.31225 C 10.725,2.46314 10.7338,2.62022 10.7121,2.77452 10.6904,2.92881 10.6387,3.07729 10.5598,3.21146 10.481,3.34562 10.3767,3.46284 10.2528,3.55641 10.1289,3.64997 9.98781,3.71804 9.83771,3.75673 9.44403,3.8586 9.05806,3.98852 8.68271,4.1455 8.53981,4.20527 8.38659,4.23615 8.23182,4.23637 8.07705,4.23659 7.92375,4.20616 7.78068,4.14679 7.6376,4.08743 7.50755,4.00031 7.39796,3.8904 7.28836,3.7805 7.20136,3.64996 7.14193,3.50624 7.0825,3.36251 7.05179,3.20843 7.05157,3.05277 7.05135,2.89712 7.08162,2.74295 7.14064,2.59905 7.19967,2.45516 7.28629,2.32437 7.39558,2.21415 7.50486,2.10393 7.63467,2.01644 7.77757,1.95667 8.25214,1.75754 8.744,1.5916 9.25157,1.46043 9.40149,1.42171 9.55753,1.41307 9.71078,1.435 c 0.15324,0.02194 0.30072,0.07402 0.43392,0.15327 0.1332,0.07924 0.2496,0.18411 0.3425,0.3086 0.093,0.1245 0.1606,0.2646 0.1991,0.41538 z M 6.005,4.26087 C 6.2224,4.48645 6.34188,4.78959 6.33716,5.10371 6.33245,5.41782 6.20393,5.71721 5.97986,5.93608 5.39555,6.50882 4.89314,7.16036 4.487,7.87205 4.4123,8.01106 4.31071,8.13365 4.18821,8.2326 4.06572,8.33156 3.92478,8.40488 3.7737,8.44826 3.62261,8.49163 3.46443,8.50419 3.30845,8.48519 3.15247,8.46619 3.00185,8.41602 2.86543,8.33762 2.72901,8.25922 2.60956,8.15419 2.51409,8.02868 2.41862,7.90318 2.34907,7.75975 2.30953,7.60682 2.26999,7.45389 2.26125,7.29455 2.28382,7.13818 2.3064,6.9818 2.35984,6.83155 2.441,6.69625 2.95566,5.7904 3.59471,4.96203 4.33929,4.23558 4.56358,4.01694 4.86501,3.89679 5.17735,3.90153 5.48968,3.90627 5.78737,4.03552 6.005,4.26087 Z"
fill="#ffffff" />
<path
d="m 2.5268743,9.5228951 c 0.3098728,0.046129 0.589468,0.2134487 0.7773467,0.4652329 0.1878804,0.25177 0.2686758,0.567403 0.22464,0.877519 -0.1136188,0.810272 -0.1142716,1.633023 -0.00195,2.44471 0.025607,0.155718 0.019891,0.314829 -0.016815,0.467963 -0.036692,0.153135 -0.103645,0.297209 -0.1968964,0.423744 -0.093265,0.126534 -0.2109436,0.232979 -0.3461307,0.313074 -0.1351869,0.08009 -0.285141,0.132222 -0.4410639,0.153317 -0.155923,0.0211 -0.3146436,0.01076 -0.4668564,-0.03047 C 1.9069428,14.596772 1.7643374,14.525546 1.6397164,14.428487 1.5150953,14.331426 1.4109697,14.2105 1.3334655,14.072826 1.2559631,13.935137 1.2066531,13.783482 1.1884279,13.626763 1.043654,12.595027 1.0447093,11.548805 1.1915583,10.518968 1.2359088,10.2089 1.4014648,9.9298163 1.6518506,9.7430394 1.9022287,9.5562686 2.2169594,9.4770886 2.5268743,9.5228951 Z"
fill="#ffffff" />
<path
d="m 2.9263167,15.692773 c 0.2757306,-0.148735 0.5998338,-0.182299 0.9011147,-0.09328 0.3012739,0.089 0.5550776,0.293292 0.7056331,0.567965 0.3945227,0.716798 0.886966,1.375904 1.4632309,1.958459 0.1138029,0.10933 0.2045599,0.240142 0.266925,0.384737 0.062378,0.144591 0.095098,0.300056 0.096254,0.457235 0.00115,0.157187 -0.029296,0.312918 -0.089537,0.458044 -0.060243,0.145126 -0.1490677,0.276708 -0.2612635,0.387021 -0.112196,0.110313 -0.2454778,0.197121 -0.3920371,0.255324 -0.1465494,0.05821 -0.3033987,0.08662 -0.4613283,0.08359 -0.1579298,-0.003 -0.3137503,-0.03748 -0.4582919,-0.101261 C 4.5524683,19.986807 4.4221224,19.894932 4.3136299,19.780379 3.5795376,19.04109 2.9535201,18.202827 2.4540459,17.290329 2.3037709,17.015508 2.2691013,16.692871 2.3576556,16.39331 2.4462071,16.093759 2.6507458,15.841788 2.9263167,15.692773 Z m 4.1953049,4.873184 c 0.1254576,-0.286226 0.3601429,-0.510811 0.6524735,-0.624396 0.2923085,-0.113581 0.618309,-0.106861 0.9063581,0.01865 0.3756122,0.163711 0.762213,0.30108 1.156869,0.411084 0.3028188,0.08453 0.5598248,0.285213 0.7145048,0.55787 0.154676,0.272665 0.194271,0.594953 0.110218,0.89602 -0.08406,0.301066 -0.284866,0.556237 -0.55841,0.709339 -0.273548,0.153148 -0.5973332,0.19167 -0.9001528,0.107127 C 8.6966635,22.50016 8.2055345,22.325523 7.7348173,22.119051 7.4469054,21.993382 7.2204524,21.759141 7.1055001,21.467913 6.9904996,21.176672 6.9963029,20.852243 7.1216216,20.565957 Z"
fill="#ffffff" />
</svg>

Before

Width:  |  Height:  |  Size: 5.8 KiB

View file

@ -1,4 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.0983 5.42385V18.2487C11.0983 18.4582 11.028 18.6366 10.8872 18.7839C10.7464 18.9312 10.5746 19.0049 10.3717 19.0049C10.1622 19.0049 9.97886 18.9263 9.82174 18.7692L5.89375 14.851H2.81028C2.60079 14.851 2.42403 14.779 2.28 14.635C2.13598 14.4909 2.06396 14.3142 2.06396 14.1047V9.56787C2.06396 9.35838 2.13598 9.18162 2.28 9.0376C2.42403 8.89357 2.60079 8.82156 2.81028 8.82156H5.89375L9.82174 4.89357C9.9854 4.7299 10.1687 4.64807 10.3717 4.64807C10.5746 4.64807 10.7464 4.72499 10.8872 4.87884C11.028 5.03269 11.0983 5.21436 11.0983 5.42385Z" fill="#c9c9c9"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M14 7.40483C15.6876 8.24862 16.8506 9.9818 16.8506 12C16.8506 14.0183 15.6876 15.7515 14 16.5839V7.40483Z" fill="#c9c9c9"/>
</svg>

Before

Width:  |  Height:  |  Size: 850 B

View file

@ -1,4 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.0983 5.42385V18.2487C11.0983 18.4582 11.028 18.6366 10.8872 18.7839C10.7464 18.9312 10.5746 19.0049 10.3717 19.0049C10.1622 19.0049 9.97886 18.9263 9.82174 18.7692L5.89375 14.851H2.81028C2.60079 14.851 2.42403 14.779 2.28 14.635C2.13598 14.4909 2.06396 14.3142 2.06396 14.1047V9.56787C2.06396 9.35838 2.13598 9.18162 2.28 9.0376C2.42403 8.89357 2.60079 8.82156 2.81028 8.82156H5.89375L9.82174 4.89357C9.9854 4.7299 10.1687 4.64807 10.3717 4.64807C10.5746 4.64807 10.7464 4.72499 10.8872 4.87884C11.028 5.03269 11.0983 5.21436 11.0983 5.42385Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M14 7.40483C15.6876 8.24862 16.8506 9.9818 16.8506 12C16.8506 14.0183 15.6876 15.7515 14 16.5839V7.40483Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 846 B

View file

@ -1,4 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.0983 5.42381V18.2487C11.0983 18.4582 11.028 18.6366 10.8872 18.7839C10.7464 18.9312 10.5746 19.0048 10.3717 19.0048C10.1622 19.0048 9.97886 18.9263 9.82174 18.7691L5.89375 14.851H2.81028C2.60079 14.851 2.42403 14.779 2.28 14.6349C2.13598 14.4909 2.06396 14.3142 2.06396 14.1047V9.56784C2.06396 9.35834 2.13598 9.18158 2.28 9.03756C2.42403 8.89353 2.60079 8.82152 2.81028 8.82152H5.89375L9.82174 4.89353C9.9854 4.72987 10.1687 4.64803 10.3717 4.64803C10.5746 4.64803 10.7464 4.72496 10.8872 4.8788C11.028 5.03265 11.0983 5.21432 11.0983 5.42381Z" fill="#c9c9c9"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M14 4.34892V2C18.5724 3.03763 21.9818 7.11973 21.9818 12C21.9818 16.8803 18.5724 20.9624 14 22V19.6511C17.2953 18.6705 19.7013 15.6146 19.7013 12C19.7013 8.38541 17.2953 5.32953 14 4.34892ZM14 7.4048C15.6876 8.24858 16.8506 9.98176 16.8506 12C16.8506 14.0183 15.6876 15.7514 14 16.5838V7.4048Z" fill="#c9c9c9"/>
</svg>

Before

Width:  |  Height:  |  Size: 1 KiB

View file

@ -1,4 +0,0 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M11.0983 5.42381V18.2487C11.0983 18.4582 11.028 18.6366 10.8872 18.7839C10.7464 18.9312 10.5746 19.0048 10.3717 19.0048C10.1622 19.0048 9.97886 18.9263 9.82174 18.7691L5.89375 14.851H2.81028C2.60079 14.851 2.42403 14.779 2.28 14.6349C2.13598 14.4909 2.06396 14.3142 2.06396 14.1047V9.56784C2.06396 9.35834 2.13598 9.18158 2.28 9.03756C2.42403 8.89353 2.60079 8.82152 2.81028 8.82152H5.89375L9.82174 4.89353C9.9854 4.72987 10.1687 4.64803 10.3717 4.64803C10.5746 4.64803 10.7464 4.72496 10.8872 4.8788C11.028 5.03265 11.0983 5.21432 11.0983 5.42381Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M14 4.34892V2C18.5724 3.03763 21.9818 7.11973 21.9818 12C21.9818 16.8803 18.5724 20.9624 14 22V19.6511C17.2953 18.6705 19.7013 15.6146 19.7013 12C19.7013 8.38541 17.2953 5.32953 14 4.34892ZM14 7.4048C15.6876 8.24858 16.8506 9.98176 16.8506 12C16.8506 14.0183 15.6876 15.7514 14 16.5838V7.4048Z" fill="white"/>
</svg>

Before

Width:  |  Height:  |  Size: 1 KiB

View file

@ -19,9 +19,9 @@
"https": "^1.0.0",
"log4js": "^6.9.1",
"qrcode": "^1.5.3",
"url": "^0.11.3",
"uuid": "^9.0.1",
"ws": "^8.14.2",
"url": "^0.11.4",
"uuid": "^11.0.3",
"ws": "^8.18.0",
"yargs": "^17.7.2"
},
"devDependencies": {
@ -44,6 +44,7 @@
"@types/workerpool": "^6.1.1",
"@types/ws": "^8.5.10",
"@types/yargs": "^17.0.33",
"copy-webpack-plugin": "^12.0.2",
"electron": "^32.2.1",
"eslint": "^9.10.0",
"globals": "^15.9.0",
@ -2504,6 +2505,19 @@
"url": "https://github.com/sindresorhus/is?sponsor=1"
}
},
"node_modules/@sindresorhus/merge-streams": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz",
"integrity": "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@sinonjs/commons": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz",
@ -3399,6 +3413,48 @@
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/ajv-formats": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz",
"integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==",
"dev": true,
"license": "MIT",
"dependencies": {
"ajv": "^8.0.0"
},
"peerDependencies": {
"ajv": "^8.0.0"
},
"peerDependenciesMeta": {
"ajv": {
"optional": true
}
}
},
"node_modules/ajv-formats/node_modules/ajv": {
"version": "8.17.1",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
"dev": true,
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
"json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/ajv-formats/node_modules/json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
"dev": true,
"license": "MIT"
},
"node_modules/ajv-keywords": {
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
@ -4436,6 +4492,88 @@
"dev": true,
"license": "MIT"
},
"node_modules/copy-webpack-plugin": {
"version": "12.0.2",
"resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-12.0.2.tgz",
"integrity": "sha512-SNwdBeHyII+rWvee/bTnAYyO8vfVdcSTud4EIb6jcZ8inLeWucJE0DnxXQBjlQ5zlteuuvooGQy3LIyGxhvlOA==",
"dev": true,
"license": "MIT",
"dependencies": {
"fast-glob": "^3.3.2",
"glob-parent": "^6.0.1",
"globby": "^14.0.0",
"normalize-path": "^3.0.0",
"schema-utils": "^4.2.0",
"serialize-javascript": "^6.0.2"
},
"engines": {
"node": ">= 18.12.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/webpack"
},
"peerDependencies": {
"webpack": "^5.1.0"
}
},
"node_modules/copy-webpack-plugin/node_modules/ajv": {
"version": "8.17.1",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
"dev": true,
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
"json-schema-traverse": "^1.0.0",
"require-from-string": "^2.0.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/copy-webpack-plugin/node_modules/ajv-keywords": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz",
"integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==",
"dev": true,
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.3"
},
"peerDependencies": {
"ajv": "^8.8.2"
}
},
"node_modules/copy-webpack-plugin/node_modules/json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
"dev": true,
"license": "MIT"
},
"node_modules/copy-webpack-plugin/node_modules/schema-utils": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz",
"integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/json-schema": "^7.0.9",
"ajv": "^8.9.0",
"ajv-formats": "^2.1.1",
"ajv-keywords": "^5.1.0"
},
"engines": {
"node": ">= 12.13.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/webpack"
}
},
"node_modules/create-jest": {
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz",
@ -6052,6 +6190,13 @@
"dev": true,
"license": "MIT"
},
"node_modules/fast-uri": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.3.tgz",
"integrity": "sha512-aLrHthzCjH5He4Z2H9YZ+v6Ujb9ocRuW6ZzkJQOrTxleEijANq4v1TsaPaVG1PZcuurEzrLcWRyYBYXD5cEiaw==",
"dev": true,
"license": "BSD-3-Clause"
},
"node_modules/fastest-levenshtein": {
"version": "1.0.16",
"resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz",
@ -6665,6 +6810,53 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/globby": {
"version": "14.0.2",
"resolved": "https://registry.npmjs.org/globby/-/globby-14.0.2.tgz",
"integrity": "sha512-s3Fq41ZVh7vbbe2PN3nrW7yC7U7MFVc5c98/iTl9c2GawNMKx/J648KQRW6WKkuU8GIbbh2IXfIRQjOZnXcTnw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@sindresorhus/merge-streams": "^2.1.0",
"fast-glob": "^3.3.2",
"ignore": "^5.2.4",
"path-type": "^5.0.0",
"slash": "^5.1.0",
"unicorn-magic": "^0.1.0"
},
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/globby/node_modules/path-type": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/path-type/-/path-type-5.0.0.tgz",
"integrity": "sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/globby/node_modules/slash": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz",
"integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=14.16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/gopd": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
@ -10256,6 +10448,16 @@
"node": ">=0.10.0"
}
},
"node_modules/require-from-string": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/require-main-filename": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz",
@ -11619,6 +11821,19 @@
"devOptional": true,
"license": "MIT"
},
"node_modules/unicorn-magic": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz",
"integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/unique-filename": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-2.0.1.tgz",
@ -11748,16 +11963,16 @@
"license": "MIT"
},
"node_modules/uuid": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",
"integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==",
"version": "11.0.3",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.3.tgz",
"integrity": "sha512-d0z310fCWv5dJwnX1Y/MncBAqGMKEzlBb1AOf7z9K8ALnd0utBX/msg/fA0+sbyN1ihbMsLhrBlnl1ak7Wa0rg==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": {
"uuid": "dist/bin/uuid"
"uuid": "dist/esm/bin/uuid"
}
},
"node_modules/v8-to-istanbul": {

View file

@ -6,7 +6,7 @@
"author": "FUTO",
"license": "MIT",
"scripts": {
"build": "rm -rf dist/ && webpack --config ./webpack.config.js && rsync -r src/player/* dist/player --exclude *.ts && rsync -r src/main/* dist/main --exclude *.ts && cp assets/icons/app/icon.ico dist/ && cp assets/icons/app/icon.png dist/ && cp assets/icons/app/icon512.png dist/",
"build": "rm -rf dist/ && webpack --config ./webpack.config.js",
"start": "electron-forge start",
"test": "jest",
"package": "electron-forge package",
@ -32,6 +32,7 @@
"@types/workerpool": "^6.1.1",
"@types/ws": "^8.5.10",
"@types/yargs": "^17.0.33",
"copy-webpack-plugin": "^12.0.2",
"electron": "^32.2.1",
"eslint": "^9.10.0",
"globals": "^15.9.0",
@ -55,9 +56,9 @@
"https": "^1.0.0",
"log4js": "^6.9.1",
"qrcode": "^1.5.3",
"url": "^0.11.3",
"uuid": "^9.0.1",
"ws": "^8.14.2",
"url": "^0.11.4",
"uuid": "^11.0.3",
"ws": "^8.18.0",
"yargs": "^17.7.2"
}
}

View file

@ -1,4 +1,4 @@
import { app } from 'electron';
import Main from './Main';
import { Main } from './Main';
await Main.main(app);

View file

@ -1,74 +0,0 @@
import mdns from 'mdns-js';
import * as log4js from "log4js";
const cp = require('child_process');
const os = require('os');
const logger = log4js.getLogger();
export class DiscoveryService {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private serviceTcp: any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
private serviceWs: any;
private static getComputerName() {
switch (process.platform) {
case "win32":
return process.env.COMPUTERNAME;
case "darwin":
return cp.execSync("scutil --get ComputerName").toString().trim();
case "linux": {
let hostname: string;
// Some distro's don't work with `os.hostname()`, but work with `hostnamectl` and vice versa...
try {
hostname = os.hostname();
}
catch (err) {
logger.warn('Error fetching hostname, trying different method...');
logger.warn(err);
try {
hostname = cp.execSync("hostnamectl hostname").toString().trim();
}
catch (err2) {
logger.warn('Error fetching hostname again, using generic name...');
logger.warn(err2);
hostname = 'linux device';
}
}
return hostname;
}
default:
return os.hostname();
}
}
start() {
if (this.serviceTcp || this.serviceWs) {
return;
}
const name = `FCast-${DiscoveryService.getComputerName()}`;
logger.info("Discovery service started.", name);
this.serviceTcp = mdns.createAdvertisement(mdns.tcp('_fcast'), 46899, { name: name });
this.serviceTcp.start();
this.serviceWs = mdns.createAdvertisement(mdns.tcp('_fcast-ws'), 46898, { name: name });
this.serviceWs.start();
}
stop() {
if (this.serviceTcp) {
this.serviceTcp.stop();
this.serviceTcp = null;
}
if (this.serviceWs) {
this.serviceWs.stop();
this.serviceWs = null;
}
}
}

View file

@ -1,208 +0,0 @@
import * as net from 'net';
import * as log4js from "log4js";
import { EventEmitter } from 'node:events';
import { PlaybackErrorMessage, PlaybackUpdateMessage, PlayMessage, SeekMessage, SetSpeedMessage, SetVolumeMessage, VersionMessage, VolumeUpdateMessage } from './Packets';
import { WebSocket } from 'ws';
const logger = log4js.getLogger();
enum SessionState {
Idle = 0,
WaitingForLength,
WaitingForData,
Disconnected,
};
export enum Opcode {
None = 0,
Play = 1,
Pause = 2,
Resume = 3,
Stop = 4,
Seek = 5,
PlaybackUpdate = 6,
VolumeUpdate = 7,
SetVolume = 8,
PlaybackError = 9,
SetSpeed = 10,
Version = 11,
Ping = 12,
Pong = 13
};
const LENGTH_BYTES = 4;
const MAXIMUM_PACKET_LENGTH = 32000;
export class FCastSession {
buffer: Buffer = Buffer.alloc(MAXIMUM_PACKET_LENGTH);
bytesRead = 0;
packetLength = 0;
socket: net.Socket | WebSocket;
writer: (data: Buffer) => void;
state: SessionState;
emitter = new EventEmitter();
constructor(socket: net.Socket | WebSocket, writer: (data: Buffer) => void) {
this.socket = socket;
this.writer = writer;
this.state = SessionState.WaitingForLength;
}
send(opcode: number, message = null) {
const json = message ? JSON.stringify(message) : null;
logger.info(`send (opcode: ${opcode}, body: ${json})`);
let data: Uint8Array;
if (json) {
const utf8Encode = new TextEncoder();
data = utf8Encode.encode(json);
} else {
data = new Uint8Array(0);
}
const size = 1 + data.length;
const header = Buffer.alloc(4 + 1);
header.writeUint32LE(size, 0);
header[4] = opcode;
let packet: Buffer;
if (data.length > 0) {
packet = Buffer.concat([ header, data ]);
} else {
packet = header;
}
this.writer(packet);
}
close() {
if (this.socket instanceof WebSocket) {
this.socket.close();
} else if (this.socket instanceof net.Socket) {
this.socket.end();
}
}
processBytes(receivedBytes: Buffer) {
//TODO: Multithreading?
if (receivedBytes.length == 0) {
return;
}
logger.info(`${receivedBytes.length} bytes received`);
switch (this.state) {
case SessionState.WaitingForLength:
this.handleLengthBytes(receivedBytes);
break;
case SessionState.WaitingForData:
this.handlePacketBytes(receivedBytes);
break;
default:
logger.info(`Data received is unhandled in current session state ${this.state}.`);
break;
}
}
private handleLengthBytes(receivedBytes: Buffer) {
const bytesToRead = Math.min(LENGTH_BYTES, receivedBytes.length);
const bytesRemaining = receivedBytes.length - bytesToRead;
receivedBytes.copy(this.buffer, this.bytesRead, 0, bytesToRead);
this.bytesRead += bytesToRead;
logger.info(`handleLengthBytes: Read ${bytesToRead} bytes from packet`);
if (this.bytesRead >= LENGTH_BYTES) {
this.state = SessionState.WaitingForData;
this.packetLength = this.buffer.readUInt32LE(0);
this.bytesRead = 0;
logger.info(`Packet length header received from: ${this.packetLength}`);
if (this.packetLength > MAXIMUM_PACKET_LENGTH) {
throw new Error(`Maximum packet length is 32kB: ${this.packetLength}`);
}
if (bytesRemaining > 0) {
logger.info(`${bytesRemaining} remaining bytes pushed to handlePacketBytes`);
this.handlePacketBytes(receivedBytes.slice(bytesToRead));
}
}
}
private handlePacketBytes(receivedBytes: Buffer) {
const bytesToRead = Math.min(this.packetLength, receivedBytes.length);
const bytesRemaining = receivedBytes.length - bytesToRead;
receivedBytes.copy(this.buffer, this.bytesRead, 0, bytesToRead);
this.bytesRead += bytesToRead;
logger.info(`handlePacketBytes: Read ${bytesToRead} bytes from packet`);
if (this.bytesRead >= this.packetLength) {
logger.info(`Packet finished receiving from of ${this.packetLength} bytes.`);
this.handleNextPacket();
this.state = SessionState.WaitingForLength;
this.packetLength = 0;
this.bytesRead = 0;
if (bytesRemaining > 0) {
logger.info(`${bytesRemaining} remaining bytes pushed to handleLengthBytes`);
this.handleLengthBytes(receivedBytes.slice(bytesToRead));
}
}
}
private handlePacket(opcode: number, body: string | undefined) {
logger.info(`handlePacket (opcode: ${opcode}, body: ${body})`);
try {
switch (opcode) {
case Opcode.Play:
this.emitter.emit("play", JSON.parse(body) as PlayMessage);
break;
case Opcode.Pause:
this.emitter.emit("pause");
break;
case Opcode.Resume:
this.emitter.emit("resume");
break;
case Opcode.Stop:
this.emitter.emit("stop");
break;
case Opcode.Seek:
this.emitter.emit("seek", JSON.parse(body) as SeekMessage);
break;
case Opcode.SetVolume:
this.emitter.emit("setvolume", JSON.parse(body) as SetVolumeMessage);
break;
case Opcode.SetSpeed:
this.emitter.emit("setspeed", JSON.parse(body) as SetSpeedMessage);
break;
case Opcode.Ping:
this.send(Opcode.Pong);
break;
}
} catch (e) {
logger.warn(`Error handling packet from.`, e);
}
}
private handleNextPacket() {
logger.info(`Processing packet of ${this.bytesRead} bytes from`);
const opcode = this.buffer[0];
const body = this.packetLength > 1 ? this.buffer.toString('utf8', 1, this.packetLength) : null;
logger.info('body', body);
this.handlePacket(opcode, body);
}
bindEvents(emitter: EventEmitter) {
this.emitter.on("play", (body: PlayMessage) => { emitter.emit("play", body) });
this.emitter.on("pause", () => { emitter.emit("pause") });
this.emitter.on("resume", () => { emitter.emit("resume") });
this.emitter.on("stop", () => { emitter.emit("stop") });
this.emitter.on("seek", (body: SeekMessage) => { emitter.emit("seek", body) });
this.emitter.on("setvolume", (body: SetVolumeMessage) => { emitter.emit("setvolume", body) });
this.emitter.on("setspeed", (body: SetSpeedMessage) => { emitter.emit("setspeed", body) });
}
}

View file

@ -1,21 +1,19 @@
import { BrowserWindow, ipcMain, IpcMainEvent, nativeImage, Tray, Menu, dialog } from 'electron';
import { TcpListenerService } from './TcpListenerService';
import { PlayMessage, PlaybackErrorMessage, PlaybackUpdateMessage, VolumeUpdateMessage } from './Packets';
import { DiscoveryService } from './DiscoveryService';
import { PlaybackErrorMessage, PlaybackUpdateMessage, VolumeUpdateMessage } from 'common/Packets';
import { DiscoveryService } from 'common/DiscoveryService';
import { TcpListenerService } from 'common/TcpListenerService';
import { WebSocketListenerService } from 'common/WebSocketListenerService';
import { NetworkService } from 'common/NetworkService';
import { Opcode } from 'common/FCastSession';
import { Updater } from './Updater';
import { WebSocketListenerService } from './WebSocketListenerService';
import { Opcode } from './FCastSession';
import * as os from 'os';
import * as path from 'path';
import * as http from 'http';
import * as url from 'url';
import * as log4js from "log4js";
import { AddressInfo } from 'ws';
import { v4 as uuidv4 } from 'uuid';
import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';
const cp = require('child_process');
export default class Main {
export class Main {
static shouldOpenMainWindow = true;
static startFullscreen = false;
static playerWindow: Electron.BrowserWindow;
@ -25,11 +23,6 @@ export default class Main {
static webSocketListenerService: WebSocketListenerService;
static discoveryService: DiscoveryService;
static tray: Tray;
static key: string = null;
static cert: string = null;
static proxyServer: http.Server;
static proxyServerAddress: AddressInfo;
static proxiedFiles: Map<string, { url: string, headers: { [key: string]: string } }> = new Map();
static logger: log4js.Logger;
private static toggleMainWindow() {
@ -80,7 +73,7 @@ export default class Main {
}
private static createTray() {
const icon = (process.platform === 'win32') ? path.join(__dirname, 'icon.ico') : path.join(__dirname, 'icon.png');
const icon = (process.platform === 'win32') ? path.join(__dirname, 'assets/icons/app/icon.ico') : path.join(__dirname, 'assets/icons/app/icon.png');
const trayicon = nativeImage.createFromPath(icon)
const tray = new Tray(trayicon.resize({ width: 16 }));
const contextMenu = Menu.buildFromTemplate([
@ -175,13 +168,13 @@ export default class Main {
Main.playerWindow.loadFile(path.join(__dirname, 'player/index.html'));
Main.playerWindow.on('ready-to-show', async () => {
Main.playerWindow?.webContents?.send("play", await Main.proxyPlayIfRequired(message));
Main.playerWindow?.webContents?.send("play", await NetworkService.proxyPlayIfRequired(message));
});
Main.playerWindow.on('closed', () => {
Main.playerWindow = null;
});
} else {
Main.playerWindow?.webContents?.send("play", await Main.proxyPlayIfRequired(message));
Main.playerWindow?.webContents?.send("play", await NetworkService.proxyPlayIfRequired(message));
}
});
@ -283,116 +276,6 @@ export default class Main {
}
}
private static setupProxyServer(): Promise<void> {
return new Promise<void>((resolve, reject) => {
try {
Main.logger.info(`Proxy server starting`);
const port = 0;
Main.proxyServer = http.createServer((req, res) => {
Main.logger.info(`Request received`);
const requestUrl = `http://${req.headers.host}${req.url}`;
const proxyInfo = Main.proxiedFiles.get(requestUrl);
if (!proxyInfo) {
res.writeHead(404);
res.end('Not found');
return;
}
const omitHeaders = new Set([
'host',
'connection',
'keep-alive',
'proxy-authenticate',
'proxy-authorization',
'te',
'trailers',
'transfer-encoding',
'upgrade'
]);
const filteredHeaders = Object.fromEntries(Object.entries(req.headers)
.filter(([key]) => !omitHeaders.has(key.toLowerCase()))
.map(([key, value]) => [key, Array.isArray(value) ? value.join(', ') : value]));
const parsedUrl = url.parse(proxyInfo.url);
const options: http.RequestOptions = {
... parsedUrl,
method: req.method,
headers: { ...filteredHeaders, ...proxyInfo.headers }
};
const proxyReq = http.request(options, (proxyRes) => {
res.writeHead(proxyRes.statusCode, proxyRes.headers);
proxyRes.pipe(res, { end: true });
});
req.pipe(proxyReq, { end: true });
proxyReq.on('error', (e) => {
Main.logger.error(`Problem with request: ${e.message}`);
res.writeHead(500);
res.end();
});
});
Main.proxyServer.on('error', e => {
reject(e);
});
Main.proxyServer.listen(port, '127.0.0.1', () => {
Main.proxyServerAddress = Main.proxyServer.address() as AddressInfo;
Main.logger.info(`Proxy server running at http://127.0.0.1:${Main.proxyServerAddress.port}/`);
resolve();
});
} catch (e) {
reject(e);
}
});
}
static streamingMediaTypes = [
"application/vnd.apple.mpegurl",
"application/x-mpegURL",
"application/dash+xml"
];
static async proxyPlayIfRequired(message: PlayMessage): Promise<PlayMessage> {
if (message.headers && message.url && !Main.streamingMediaTypes.find(v => v === message.container.toLocaleLowerCase())) {
return { ...message, url: await Main.proxyFile(message.url, message.headers) };
}
return message;
}
static async proxyFile(url: string, headers: { [key: string]: string }): Promise<string> {
if (!Main.proxyServer) {
await Main.setupProxyServer();
}
const proxiedUrl = `http://127.0.0.1:${Main.proxyServerAddress.port}/${uuidv4()}`;
Main.logger.info("Proxied url", { proxiedUrl, url, headers });
Main.proxiedFiles.set(proxiedUrl, { url: url, headers: headers });
return proxiedUrl;
}
static getAllIPv4Addresses() {
const interfaces = os.networkInterfaces();
const ipv4Addresses: string[] = [];
for (const interfaceName in interfaces) {
const addresses = interfaces[interfaceName];
if (!addresses) continue;
for (const addressInfo of addresses) {
if (addressInfo.family === 'IPv4' && !addressInfo.internal) {
ipv4Addresses.push(addressInfo.address);
}
}
}
return ipv4Addresses;
}
static openMainWindow() {
if (Main.mainWindow) {
Main.mainWindow.focus();
@ -419,7 +302,7 @@ export default class Main {
Main.mainWindow.show();
Main.mainWindow.on('ready-to-show', () => {
Main.mainWindow.webContents.send("device-info", {name: os.hostname(), addresses: Main.getAllIPv4Addresses()});
Main.mainWindow.webContents.send("device-info", {name: os.hostname(), addresses: NetworkService.getAllIPv4Addresses()});
});
}
@ -471,3 +354,59 @@ export default class Main {
}
}
}
export function getComputerName() {
switch (process.platform) {
case "win32":
return process.env.COMPUTERNAME;
case "darwin":
return cp.execSync("scutil --get ComputerName").toString().trim();
case "linux": {
let hostname: string;
// Some distro's don't work with `os.hostname()`, but work with `hostnamectl` and vice versa...
try {
hostname = os.hostname();
}
catch (err) {
Main.logger.warn('Error fetching hostname, trying different method...');
Main.logger.warn(err);
try {
hostname = cp.execSync("hostnamectl hostname").toString().trim();
}
catch (err2) {
Main.logger.warn('Error fetching hostname again, using generic name...');
Main.logger.warn(err2);
hostname = 'linux device';
}
}
return hostname;
}
default:
return os.hostname();
}
}
export async function errorHandler(err: NodeJS.ErrnoException) {
Main.logger.error("Application error:", err);
const restartPrompt = await dialog.showMessageBox({
type: 'error',
title: 'Failed to start',
message: 'The application failed to start properly.',
buttons: ['Restart', 'Close'],
defaultId: 0,
cancelId: 1
});
if (restartPrompt.response === 0) {
Main.application.relaunch();
Main.application.exit(0);
} else {
Main.application.exit(0);
}
}

View file

@ -1,57 +0,0 @@
export class PlayMessage {
constructor(
public container: string,
public url: string = null,
public content: string = null,
public time: number = null,
public speed: number = null,
public headers: { [key: string]: string } = null
) {}
}
export class SeekMessage {
constructor(
public time: number,
) {}
}
export class PlaybackUpdateMessage {
constructor(
public generationTime: number,
public time: number,
public duration: number,
public state: number,
public speed: number
) {}
}
export class PlaybackErrorMessage {
constructor(
public message: string
) {}
}
export class VolumeUpdateMessage {
constructor(
public generationTime: number,
public volume: number
) {}
}
export class SetVolumeMessage {
constructor(
public volume: number,
) {}
}
export class SetSpeedMessage {
constructor(
public speed: number,
) {}
}
export class VersionMessage {
constructor(
public version: number,
) {}
}

View file

@ -1,103 +0,0 @@
import * as net from 'net';
import { FCastSession, Opcode } from './FCastSession';
import { EventEmitter } from 'node:events';
import { dialog } from 'electron';
import Main from './Main';
export class TcpListenerService {
public static PORT = 46899;
emitter = new EventEmitter();
private server: net.Server;
private sessions: FCastSession[] = [];
start() {
if (this.server != null) {
return;
}
this.server = net.createServer()
.listen(TcpListenerService.PORT)
.on("connection", this.handleConnection.bind(this))
.on("error", this.handleServerError.bind(this));
}
stop() {
if (this.server == null) {
return;
}
const server = this.server;
this.server = null;
server.close();
}
send(opcode: number, message = null) {
this.sessions.forEach(session => {
try {
session.send(opcode, message);
} catch (e) {
Main.logger.warn("Failed to send error.", e);
session.close();
}
});
}
private async handleServerError(err: NodeJS.ErrnoException) {
Main.logger.error("Server error:", err);
const restartPrompt = await dialog.showMessageBox({
type: 'error',
title: 'Failed to start',
message: 'The application failed to start properly.',
buttons: ['Restart', 'Close'],
defaultId: 0,
cancelId: 1
});
if (restartPrompt.response === 0) {
Main.application.relaunch();
Main.application.exit(0);
} else {
Main.application.exit(0);
}
}
private handleConnection(socket: net.Socket) {
Main.logger.info(`new connection from ${socket.remoteAddress}:${socket.remotePort}`);
const session = new FCastSession(socket, (data) => socket.write(data));
session.bindEvents(this.emitter);
this.sessions.push(session);
socket.on("error", (err) => {
Main.logger.warn(`Error from ${socket.remoteAddress}:${socket.remotePort}.`, err);
socket.destroy();
});
socket.on("data", buffer => {
try {
session.processBytes(buffer);
} catch (e) {
Main.logger.warn(`Error while handling packet from ${socket.remoteAddress}:${socket.remotePort}.`, e);
socket.end();
}
});
socket.on("close", () => {
const index = this.sessions.indexOf(session);
if (index != -1) {
this.sessions.splice(index, 1);
}
});
try {
Main.logger.info('Sending version');
session.send(Opcode.Version, {version: 2});
} catch (e) {
Main.logger.info('Failed to send version', e);
}
}
}

View file

@ -1,108 +0,0 @@
import { FCastSession, Opcode } from './FCastSession';
import { EventEmitter } from 'node:events';
import { dialog } from 'electron';
import Main from './Main';
import { WebSocket, WebSocketServer } from 'ws';
export class WebSocketListenerService {
public static PORT = 46898;
emitter = new EventEmitter();
private server: WebSocketServer;
private sessions: FCastSession[] = [];
start() {
if (this.server != null) {
return;
}
this.server = new WebSocketServer({ port: WebSocketListenerService.PORT })
.on("connection", this.handleConnection.bind(this))
.on("error", this.handleServerError.bind(this));
}
stop() {
if (this.server == null) {
return;
}
const server = this.server;
this.server = null;
server.close();
}
send(opcode: number, message = null) {
this.sessions.forEach(session => {
try {
session.send(opcode, message);
} catch (e) {
Main.logger.warn("Failed to send error.", e);
session.close();
}
});
}
private async handleServerError(err: NodeJS.ErrnoException) {
Main.logger.error("Server error:", err);
const restartPrompt = await dialog.showMessageBox({
type: 'error',
title: 'Failed to start',
message: 'The application failed to start properly.',
buttons: ['Restart', 'Close'],
defaultId: 0,
cancelId: 1
});
if (restartPrompt.response === 0) {
Main.application.relaunch();
Main.application.exit(0);
} else {
Main.application.exit(0);
}
}
private handleConnection(socket: WebSocket) {
Main.logger.info('New WebSocket connection');
const session = new FCastSession(socket, (data) => socket.send(data));
session.bindEvents(this.emitter);
this.sessions.push(session);
socket.on("error", (err) => {
Main.logger.warn(`Error.`, err);
session.close();
});
socket.on('message', data => {
try {
if (data instanceof Buffer) {
session.processBytes(data);
} else {
Main.logger.warn("Received unhandled string message", data);
}
} catch (e) {
Main.logger.warn(`Error while handling packet.`, e);
session.close();
}
});
socket.on("close", () => {
Main.logger.info('WebSocket connection closed');
const index = this.sessions.indexOf(session);
if (index != -1) {
this.sessions.splice(index, 1);
}
});
try {
Main.logger.info('Sending version');
session.send(Opcode.Version, {version: 2});
} catch (e) {
Main.logger.info('Failed to send version');
}
}
}

View file

@ -1,17 +1,12 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { contextBridge, ipcRenderer } from 'electron';
let deviceInfo;
ipcRenderer.on("device-info", (_event, value) => {
deviceInfo = value;
})
import 'common/main/Preload';
contextBridge.exposeInMainWorld('electronAPI', {
updaterProgress: () => ipcRenderer.invoke('updater-progress'),
onDeviceInfo: (callback) => ipcRenderer.on("device-info", callback),
onUpdateAvailable: (callback) => ipcRenderer.on("update-available", callback),
onUpdateAvailable: (callback: any) => ipcRenderer.on("update-available", callback),
sendDownloadRequest: () => ipcRenderer.send('send-download-request'),
onDownloadComplete: (callback) => ipcRenderer.on("download-complete", callback),
onDownloadFailed: (callback) => ipcRenderer.on("download-failed", callback),
onDownloadComplete: (callback: any) => ipcRenderer.on("download-complete", callback),
onDownloadFailed: (callback: any) => ipcRenderer.on("download-failed", callback),
sendRestartRequest: () => ipcRenderer.send('send-restart-request'),
getDeviceInfo: () => deviceInfo,
});

View file

@ -1,4 +1,4 @@
import QRCode from 'qrcode';
import 'common/main/Renderer';
const updateView = document.getElementById("update-view");
const updateViewTitle = document.getElementById("update-view-title");
@ -10,51 +10,6 @@ const progressBar = document.getElementById("progress-bar");
const progressBarProgress = document.getElementById("progress-bar-progress");
let updaterProgressUIUpdateTimer = null;
window.electronAPI.onDeviceInfo(renderIPsAndQRCode);
if(window.electronAPI.getDeviceInfo()) {
console.log("device info already present");
renderIPsAndQRCode();
}
function renderIPsAndQRCode() {
const value = window.electronAPI.getDeviceInfo();
console.log("device info", value);
const ipsElement = document.getElementById('ips');
if (ipsElement) {
ipsElement.innerHTML = `IPs<br>${value.addresses.join('<br>')}`;
}
const fcastConfig = {
name: value.name,
addresses: value.addresses,
services: [
{ port: 46899, type: 0 }, //TCP
{ port: 46898, type: 1 }, //WS
]
};
const json = JSON.stringify(fcastConfig);
let base64 = btoa(json);
base64 = base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
const url = `fcast://r/${base64}`;
console.log("qr", {json, url, base64});
const qrCodeElement = document.getElementById('qr-code');
QRCode.toCanvas(qrCodeElement, url, {
margin: 0,
width: 256,
color: {
dark : "#000000",
light : "#ffffff",
},
errorCorrectionLevel : "M",
},
(e) => {
console.log(`Error rendering QR Code: ${e}`)
});
}
window.electronAPI.onUpdateAvailable(() => {
console.log(`Received UpdateAvailable event`);

View file

@ -1,16 +1,17 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="../../assets/fonts/outfit.css" />
<link rel="stylesheet" href="../../assets/fonts/inter.css" />
<link rel="stylesheet" href="./style.css" />
<title>FCast Receiver</title>
<meta charset="UTF-8">
<link rel="stylesheet" href="../assets/fonts/outfit.css" />
<link rel="stylesheet" href="../assets/fonts/inter.css" />
<link rel="stylesheet" href="./common.css" />
<link rel="stylesheet" href="./style.css" />
</head>
<body>
<div id="main-container">
<video id="video-player" class="video" autoplay loop>
<source src="../../assets/video/background.mp4" type="video/mp4">
<source src="../assets/video/background.mp4" type="video/mp4">
</video>
<div id="ui-container">
<div id="overlay">

View file

@ -1,50 +1,3 @@
body, html {
height: 100%;
margin: 0;
}
#main-container {
position: relative;
height: 100%;
overflow: hidden;
}
.video {
height: 100%;
width: 100%;
object-fit: cover;
}
.non-selectable {
user-select: none;
}
.card {
display: flex;
flex-direction: column;
text-align: center;
background-color: rgba(20, 20, 20, 0.5);
padding: 25px;
border-radius: 10px;
border: 1px solid #2E2E2E;
scrollbar-width: thin;
overflow: auto;
}
.card-title {
font-weight: 700;
line-height: 24px;
margin: 10px;
}
.card-title-separator {
height: 1px;
background: #2E2E2E;
margin-top: 3px;
margin-bottom: 3px;
}
.button {
display: inline-block;
align-items: center;
@ -83,98 +36,6 @@ body, html {
background: #3E3E3E;
}
#ui-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
#overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
color: white;
gap: 15vw;
font-family: InterVariable;
font-size: 20px;
font-style: normal;
font-weight: 400;
}
#title-container {
display: flex;
justify-content: center;
align-items: center;
}
#title-text {
font-family: Outfit;
font-size: 100px;
font-weight: 800;
text-align: center;
background-image: linear-gradient(180deg, #FFFFFF 5.9%, #D3D3D3 100%);
background-clip: text;
-webkit-text-fill-color: transparent;
}
#title-icon {
width: 84px;
height: 84px;
background-image: url(../../assets/icons/app/icon.svg);
background-size: cover;
margin-right: 15px;
}
#connection-status {
padding: 25px;
text-align: center;
}
#main-view {
padding: 25px;
}
#manual-connection-info {
font-weight: 700;
line-height: 24px;
margin: 10px;
}
#manual-connection-info-separator {
height: 1px;
background: #2E2E2E;
margin-top: 3px;
margin-bottom: 3px;
}
#qr-code {
display: flex;
margin: 20px auto;
flex-direction: column;
align-items: center;
padding: 20px;
background-color: white;
}
#scan-to-connect {
margin-top: 20px;
font-weight: bold;
}
#waiting-for-connection, #ips, #automatic-discovery {
margin-top: 20px;
}
#update-text {
margin-top: 20px;
width: 320px;
@ -188,10 +49,6 @@ body, html {
display: none;
}
#spinner {
padding: 20px;
}
#update-button-container {
display: flex;
flex-direction: row;
@ -225,51 +82,3 @@ body, html {
background-position: 0 0;
}
}
#window-can-be-closed {
color: #666666;
position: absolute;
bottom: 0;
margin-bottom: 20px;
font-family: InterVariable;
font-size: 18px;
font-style: normal;
font-weight: 400;
}
.lds-ring {
display: inline-block;
position: relative;
width: 80px;
height: 80px;
}
.lds-ring div {
box-sizing: border-box;
display: block;
position: absolute;
width: 64px;
height: 64px;
margin: 8px;
border: 8px solid #fff;
border-radius: 50%;
animation: lds-ring 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
border-color: #fff transparent transparent transparent;
}
.lds-ring div:nth-child(1) {
animation-delay: -0.45s;
}
.lds-ring div:nth-child(2) {
animation-delay: -0.3s;
}
.lds-ring div:nth-child(3) {
animation-delay: -0.15s;
}
@keyframes lds-ring {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}

View file

@ -1,235 +0,0 @@
import dashjs from 'dashjs';
import Hls from 'hls.js';
export enum PlayerType {
Html,
Dash,
Hls,
}
export class Player {
private player: dashjs.MediaPlayerClass | HTMLVideoElement
private hlsPlayer: Hls | undefined
public playerType: PlayerType
constructor(playerType: PlayerType, player: dashjs.MediaPlayerClass | HTMLVideoElement, hlsPlayer?: Hls) {
this.playerType = playerType;
this.player = player;
this.hlsPlayer = playerType === PlayerType.Hls ? hlsPlayer : null;
}
destroy() {
switch (this.playerType) {
case PlayerType.Dash:
try {
(this.player as dashjs.MediaPlayerClass).destroy();
} catch (e) {
console.warn("Failed to destroy dash player", e);
}
this.player = null;
this.playerType = null;
break;
case PlayerType.Hls:
// HLS also uses html player
try {
this.hlsPlayer.destroy();
} catch (e) {
console.warn("Failed to destroy hls player", e);
}
// fall through
case PlayerType.Html: {
const videoPlayer = this.player as HTMLVideoElement;
videoPlayer.src = "";
// videoPlayer.onerror = null;
videoPlayer.onloadedmetadata = null;
videoPlayer.ontimeupdate = null;
videoPlayer.onplay = null;
videoPlayer.onpause = null;
videoPlayer.onended = null;
videoPlayer.ontimeupdate = null;
videoPlayer.onratechange = null;
videoPlayer.onvolumechange = null;
this.player = null;
this.playerType = null;
break;
}
default:
break;
}
}
play() { console.log("Player: play"); this.player.play(); }
isPaused(): boolean {
if (this.playerType === PlayerType.Dash) {
return (this.player as dashjs.MediaPlayerClass).isPaused();
} else { // HLS, HTML
return (this.player as HTMLVideoElement).paused;
}
}
pause() { console.log("Player: pause"); this.player.pause(); }
getVolume(): number {
if (this.playerType === PlayerType.Dash) {
return (this.player as dashjs.MediaPlayerClass).getVolume();
} else { // HLS, HTML
return (this.player as HTMLVideoElement).volume;
}
}
setVolume(value: number) {
console.log(`Player: setVolume ${value}`);
const sanitizedVolume = Math.min(1.0, Math.max(0.0, value));
if (this.playerType === PlayerType.Dash) {
(this.player as dashjs.MediaPlayerClass).setVolume(sanitizedVolume);
} else { // HLS, HTML
(this.player as HTMLVideoElement).volume = sanitizedVolume;
}
}
isMuted(): boolean {
if (this.playerType === PlayerType.Dash) {
return (this.player as dashjs.MediaPlayerClass).isMuted();
} else { // HLS, HTML
return (this.player as HTMLVideoElement).muted;
}
}
setMute(value: boolean) {
console.log(`Player: setMute ${value}`);
if (this.playerType === PlayerType.Dash) {
(this.player as dashjs.MediaPlayerClass).setMute(value);
} else { // HLS, HTML
(this.player as HTMLVideoElement).muted = value;
}
}
getPlaybackRate(): number {
if (this.playerType === PlayerType.Dash) {
return (this.player as dashjs.MediaPlayerClass).getPlaybackRate();
} else { // HLS, HTML
return (this.player as HTMLVideoElement).playbackRate;
}
}
setPlaybackRate(value: number) {
console.log(`Player: setPlaybackRate ${value}`);
const sanitizedSpeed = Math.min(16.0, Math.max(0.0, value));
if (this.playerType === PlayerType.Dash) {
(this.player as dashjs.MediaPlayerClass).setPlaybackRate(sanitizedSpeed);
} else { // HLS, HTML
(this.player as HTMLVideoElement).playbackRate = sanitizedSpeed;
}
}
getDuration(): number {
if (this.playerType === PlayerType.Dash) {
const videoPlayer = this.player as dashjs.MediaPlayerClass;
return isFinite(videoPlayer.duration()) ? videoPlayer.duration() : 0;
} else { // HLS, HTML
const videoPlayer = this.player as HTMLVideoElement;
return isFinite(videoPlayer.duration) ? videoPlayer.duration : 0;
}
}
getCurrentTime(): number {
if (this.playerType === PlayerType.Dash) {
return (this.player as dashjs.MediaPlayerClass).time();
} else { // HLS, HTML
return (this.player as HTMLVideoElement).currentTime;
}
}
setCurrentTime(value: number) {
// console.log(`Player: setCurrentTime ${value}`);
const sanitizedTime = Math.min(this.getDuration(), Math.max(0.0, value));
if (this.playerType === PlayerType.Dash) {
(this.player as dashjs.MediaPlayerClass).seek(sanitizedTime);
const videoPlayer = this.player as dashjs.MediaPlayerClass;
if (!videoPlayer.isSeeking()) {
videoPlayer.seek(sanitizedTime);
}
} else { // HLS, HTML
(this.player as HTMLVideoElement).currentTime = sanitizedTime;
}
}
getSource(): string {
if (this.playerType === PlayerType.Dash) {
const videoPlayer = this.player as dashjs.MediaPlayerClass;
return videoPlayer.getSource() instanceof String ? videoPlayer.getSource() as string : JSON.stringify(videoPlayer.getSource());
} else { // HLS, HTML
return (this.player as HTMLVideoElement).src;
}
}
getBufferLength(): number {
if (this.playerType === PlayerType.Dash) {
const dashPlayer = this.player as dashjs.MediaPlayerClass;
let dashBufferLength = dashPlayer.getBufferLength("video")
?? dashPlayer.getBufferLength("audio")
?? dashPlayer.getBufferLength("text")
?? dashPlayer.getBufferLength("image")
?? 0;
if (Number.isNaN(dashBufferLength))
dashBufferLength = 0;
dashBufferLength += dashPlayer.time();
return dashBufferLength;
} else { // HLS, HTML
const videoPlayer = this.player as HTMLVideoElement;
let maxBuffer = 0;
if (videoPlayer.buffered) {
for (let i = 0; i < videoPlayer.buffered.length; i++) {
const start = videoPlayer.buffered.start(i);
const end = videoPlayer.buffered.end(i);
if (videoPlayer.currentTime >= start && videoPlayer.currentTime <= end) {
maxBuffer = end;
}
}
}
return maxBuffer;
}
}
isCaptionsSupported(): boolean {
if (this.playerType === PlayerType.Dash) {
return (this.player as dashjs.MediaPlayerClass).getTracksFor('text').length > 0;
} else if (this.playerType === PlayerType.Hls) {
return this.hlsPlayer.allSubtitleTracks.length > 0;
} else {
return false; // HTML captions not currently supported
}
}
isCaptionsEnabled(): boolean {
if (this.playerType === PlayerType.Dash) {
return (this.player as dashjs.MediaPlayerClass).isTextEnabled();
} else if (this.playerType === PlayerType.Hls) {
return this.hlsPlayer.subtitleDisplay;
} else {
return false; // HTML captions not currently supported
}
}
enableCaptions(enable: boolean) {
if (this.playerType === PlayerType.Dash) {
(this.player as dashjs.MediaPlayerClass).enableText(enable);
} else if (this.playerType === PlayerType.Hls) {
this.hlsPlayer.subtitleDisplay = enable;
}
// HTML captions not currently supported
}
}

View file

@ -1,24 +1,8 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import { contextBridge, ipcRenderer } from 'electron';
import { PlaybackErrorMessage, PlaybackUpdateMessage, VolumeUpdateMessage } from '../Packets';
declare global {
interface Window {
electronAPI: any;
}
}
import 'common/player/Preload';
contextBridge.exposeInMainWorld('electronAPI', {
isFullScreen: () => ipcRenderer.invoke('is-full-screen'),
toggleFullScreen: () => ipcRenderer.send('toggle-full-screen'),
exitFullScreen: () => ipcRenderer.send('exit-full-screen'),
sendPlaybackError: (error: PlaybackErrorMessage) => ipcRenderer.send('send-playback-error', error),
sendPlaybackUpdate: (update: PlaybackUpdateMessage) => ipcRenderer.send('send-playback-update', update),
sendVolumeUpdate: (update: VolumeUpdateMessage) => ipcRenderer.send('send-volume-update', update),
onPlay: (callback: any) => ipcRenderer.on("play", callback),
onPause: (callback: any) => ipcRenderer.on("pause", callback),
onResume: (callback: any) => ipcRenderer.on("resume", callback),
onSeek: (callback: any) => ipcRenderer.on("seek", callback),
onSetVolume: (callback: any) => ipcRenderer.on("setvolume", callback),
onSetSpeed: (callback: any) => ipcRenderer.on("setspeed", callback)
});

View file

@ -1,483 +1,11 @@
import dashjs from 'dashjs';
import Hls, { LevelLoadedData } from 'hls.js';
import { PlaybackUpdateMessage, PlayMessage, SeekMessage, SetSpeedMessage, SetVolumeMessage } from '../Packets';
import { Player, PlayerType } from './Player';
import { videoElement, PlayerControlEvent, playerCtrlStateUpdate } from 'common/player/Renderer';
function formatDuration(duration: number) {
const totalSeconds = Math.floor(duration);
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const seconds = Math.floor(totalSeconds % 60);
const paddedMinutes = String(minutes).padStart(2, '0');
const paddedSeconds = String(seconds).padStart(2, '0');
if (hours > 0) {
return `${hours}:${paddedMinutes}:${paddedSeconds}`;
} else {
return `${paddedMinutes}:${paddedSeconds}`;
}
}
function sendPlaybackUpdate(updateState: number) {
const updateMessage = new PlaybackUpdateMessage(Date.now(), player.getCurrentTime(), player.getDuration(), updateState, player.getPlaybackRate());
if (updateMessage.generationTime > lastPlayerUpdateGenerationTime) {
lastPlayerUpdateGenerationTime = updateMessage.generationTime;
window.electronAPI.sendPlaybackUpdate(updateMessage);
}
};
function onPlayerLoad(value: PlayMessage, currentPlaybackRate?: number, currentVolume?: number) {
playerCtrlStateUpdate(PlayerControlEvent.Load);
// Subtitles break when seeking post stream initialization for the DASH player.
// Its currently done on player initialization.
if (player.playerType === PlayerType.Hls || player.playerType === PlayerType.Html) {
if (value.time) {
player.setCurrentTime(value.time);
}
}
if (value.speed) {
player.setPlaybackRate(value.speed);
} else if (currentPlaybackRate) {
player.setPlaybackRate(currentPlaybackRate);
} else {
player.setPlaybackRate(1.0);
}
playerCtrlStateUpdate(PlayerControlEvent.SetPlaybackRate);
if (currentVolume) {
volumeChangeHandler(currentVolume);
}
else {
// FCast PlayMessage does not contain volume field and could result in the receiver
// getting out-of-sync with the sender on 1st playback.
volumeChangeHandler(1.0);
window.electronAPI.sendVolumeUpdate({ generationTime: Date.now(), volume: 1.0 });
}
player.play();
}
// HTML elements
const videoElement = document.getElementById("videoPlayer") as HTMLVideoElement;
const videoCaptions = document.getElementById("videoCaptions") as HTMLDivElement;
const playerControls = document.getElementById("controls");
const playerCtrlAction = document.getElementById("action");
const playerCtrlVolume = document.getElementById("volume");
const playerCtrlProgressBar = document.getElementById("progressBar");
const playerCtrlProgressBarBuffer = document.getElementById("progressBarBuffer");
const playerCtrlProgressBarProgress = document.getElementById("progressBarProgress");
const playerCtrlProgressBarPosition = document.getElementById("progressBarPosition");
const playerCtrlProgressBarHandle = document.getElementById("progressBarHandle");
const PlayerCtrlProgressBarInteractiveArea = document.getElementById("progressBarInteractiveArea");
const playerCtrlVolumeBar = document.getElementById("volumeBar");
const playerCtrlVolumeBarProgress = document.getElementById("volumeBarProgress");
const playerCtrlVolumeBarHandle = document.getElementById("volumeBarHandle");
const playerCtrlVolumeBarInteractiveArea = document.getElementById("volumeBarInteractiveArea");
const playerCtrlLiveBadge = document.getElementById("liveBadge");
const playerCtrlPosition = document.getElementById("position");
const playerCtrlDuration = document.getElementById("duration");
const playerCtrlCaptions = document.getElementById("captions");
const playerCtrlSpeed = document.getElementById("speed");
const playerCtrlFullscreen = document.getElementById("fullscreen");
playerCtrlFullscreen.onclick = () => { playerCtrlStateUpdate(PlayerControlEvent.ToggleFullscreen); };
videoElement.ondblclick = () => { playerCtrlStateUpdate(PlayerControlEvent.ToggleFullscreen); };
const playerCtrlSpeedMenu = document.getElementById("speedMenu");
let playerCtrlSpeedMenuShown = false;
const playbackRates = ["0.25", "0.50", "0.75", "1.00", "1.25", "1.50", "1.75", "2.00"];
const playbackUpdateInterval = 1.0;
const livePositionDelta = 5.0;
const livePositionWindow = livePositionDelta * 4;
let player: Player;
let playerPrevTime: number = 0;
let lastPlayerUpdateGenerationTime = 0;
let isLive = false;
let isLivePosition = false;
window.electronAPI.onPlay((_event, value: PlayMessage) => {
console.log("Handle play message renderer", JSON.stringify(value));
const currentVolume = player ? player.getVolume() : null;
const currentPlaybackRate = player ? player.getPlaybackRate() : null;
playerPrevTime = 0;
lastPlayerUpdateGenerationTime = 0;
isLive = false;
isLivePosition = false;
if (player) {
if (player.getSource() === value.url) {
if (value.time) {
if (Math.abs(value.time - player.getCurrentTime()) < 5000) {
console.warn(`Skipped changing video URL because URL and time is (nearly) unchanged: ${value.url}, ${player.getSource()}, ${formatDuration(value.time)}, ${formatDuration(player.getCurrentTime())}`);
} else {
console.info(`Skipped changing video URL because URL is the same, but time was changed, seeking instead: ${value.url}, ${player.getSource()}, ${formatDuration(value.time)}, ${formatDuration(player.getCurrentTime())}`);
player.setCurrentTime(value.time);
}
}
return;
}
player.destroy();
}
if ((value.url || value.content) && value.container && videoElement) {
if (value.container === 'application/dash+xml') {
console.log("Loading dash player");
const dashPlayer = dashjs.MediaPlayer().create();
player = new Player(PlayerType.Dash, dashPlayer);
dashPlayer.extend("RequestModifier", () => {
return {
modifyRequestHeader: function (xhr) {
if (value.headers) {
for (const [key, val] of Object.entries(value.headers)) {
xhr.setRequestHeader(key, val);
}
}
return xhr;
}
};
}, true);
// Player event handlers
dashPlayer.on(dashjs.MediaPlayer.events.PLAYBACK_PLAYING, () => { sendPlaybackUpdate(1); playerCtrlStateUpdate(PlayerControlEvent.Play); });
dashPlayer.on(dashjs.MediaPlayer.events.PLAYBACK_PAUSED, () => { sendPlaybackUpdate(2); playerCtrlStateUpdate(PlayerControlEvent.Pause); });
dashPlayer.on(dashjs.MediaPlayer.events.PLAYBACK_ENDED, () => { sendPlaybackUpdate(0) });
dashPlayer.on(dashjs.MediaPlayer.events.PLAYBACK_TIME_UPDATED, () => {
playerCtrlStateUpdate(PlayerControlEvent.TimeUpdate);
if (Math.abs(dashPlayer.time() - playerPrevTime) >= playbackUpdateInterval) {
sendPlaybackUpdate(dashPlayer.isPaused() ? 2 : 1);
playerPrevTime = dashPlayer.time();
}
});
dashPlayer.on(dashjs.MediaPlayer.events.PLAYBACK_RATE_CHANGED, () => { sendPlaybackUpdate(dashPlayer.isPaused() ? 2 : 1) });
// Buffering UI update when paused
dashPlayer.on(dashjs.MediaPlayer.events.PLAYBACK_PROGRESS, () => { playerCtrlStateUpdate(PlayerControlEvent.TimeUpdate); });
dashPlayer.on(dashjs.MediaPlayer.events.PLAYBACK_VOLUME_CHANGED, () => {
const updateVolume = dashPlayer.isMuted() ? 0 : dashPlayer.getVolume();
playerCtrlStateUpdate(PlayerControlEvent.VolumeChange);
window.electronAPI.sendVolumeUpdate({ generationTime: Date.now(), volume: updateVolume });
});
dashPlayer.on(dashjs.MediaPlayer.events.ERROR, (data) => { window.electronAPI.sendPlaybackError({
message: `DashJS ERROR: ${JSON.stringify(data)}`
})});
dashPlayer.on(dashjs.MediaPlayer.events.PLAYBACK_ERROR, (data) => { window.electronAPI.sendPlaybackError({
message: `DashJS PLAYBACK_ERROR: ${JSON.stringify(data)}`
})});
dashPlayer.on(dashjs.MediaPlayer.events.STREAM_INITIALIZED, () => { onPlayerLoad(value, currentPlaybackRate, currentVolume); });
dashPlayer.on(dashjs.MediaPlayer.events.CUE_ENTER, (e: any) => {
const subtitle = document.createElement("p")
subtitle.setAttribute("id", "subtitle-" + e.cueID)
subtitle.textContent = e.text;
videoCaptions.appendChild(subtitle);
});
dashPlayer.on(dashjs.MediaPlayer.events.CUE_EXIT, (e: any) => {
document.getElementById("subtitle-" + e.cueID)?.remove();
});
dashPlayer.updateSettings({
// debug: {
// logLevel: dashjs.LogLevel.LOG_LEVEL_INFO
// },
streaming: {
text: {
dispatchForManualRendering: true
}
}
});
if (value.content) {
dashPlayer.initialize(videoElement, `data:${value.container};base64,` + window.btoa(value.content), true, value.time);
// dashPlayer.initialize(videoElement, "https://dash.akamaized.net/akamai/test/caption_test/ElephantsDream/elephants_dream_480p_heaac5_1_https.mpd", true);
} else {
dashPlayer.initialize(videoElement, value.url, true, value.time);
}
} else if ((value.container === 'application/vnd.apple.mpegurl' || value.container === 'application/x-mpegURL') && !videoElement.canPlayType(value.container)) {
console.log("Loading hls player");
const config = {
xhrSetup: function (xhr: XMLHttpRequest) {
if (value.headers) {
for (const [key, val] of Object.entries(value.headers)) {
xhr.setRequestHeader(key, val);
}
}
},
};
const hlsPlayer = new Hls(config);
hlsPlayer.on(Hls.Events.ERROR, (eventName, data) => {
window.electronAPI.sendPlaybackError({
message: `HLS player error: ${JSON.stringify(data)}`
});
});
hlsPlayer.on(Hls.Events.LEVEL_LOADED, (eventName, level: LevelLoadedData) => {
isLive = level.details.live;
isLivePosition = isLive ? true : false;
});
player = new Player(PlayerType.Hls, videoElement, hlsPlayer);
// value.url = "https://devstreaming-cdn.apple.com/videos/streaming/examples/adv_dv_atmos/main.m3u8?ref=developerinsider.co";
hlsPlayer.loadSource(value.url);
hlsPlayer.attachMedia(videoElement);
// hlsPlayer.subtitleDisplay = true;
} else {
console.log("Loading html player");
player = new Player(PlayerType.Html, videoElement);
videoElement.src = value.url;
videoElement.load();
}
// Player event handlers
if (player.playerType === PlayerType.Hls || player.playerType === PlayerType.Html) {
videoElement.onplay = () => { sendPlaybackUpdate(1); playerCtrlStateUpdate(PlayerControlEvent.Play); };
videoElement.onpause = () => { sendPlaybackUpdate(2); playerCtrlStateUpdate(PlayerControlEvent.Pause); };
videoElement.onended = () => { sendPlaybackUpdate(0) };
videoElement.ontimeupdate = () => {
playerCtrlStateUpdate(PlayerControlEvent.TimeUpdate);
if (Math.abs(videoElement.currentTime - playerPrevTime) >= playbackUpdateInterval) {
sendPlaybackUpdate(videoElement.paused ? 2 : 1);
playerPrevTime = videoElement.currentTime;
}
};
// Buffering UI update when paused
videoElement.onprogress = () => { playerCtrlStateUpdate(PlayerControlEvent.TimeUpdate); };
videoElement.onratechange = () => { sendPlaybackUpdate(videoElement.paused ? 2 : 1) };
videoElement.onvolumechange = () => {
const updateVolume = videoElement.muted ? 0 : videoElement.volume;
playerCtrlStateUpdate(PlayerControlEvent.VolumeChange);
window.electronAPI.sendVolumeUpdate({ generationTime: Date.now(), volume: updateVolume });
};
videoElement.onerror = (event: Event | string, source?: string, lineno?: number, colno?: number, error?: Error) => {
console.error("Player error", {source, lineno, colno, error});
};
videoElement.onloadedmetadata = () => { onPlayerLoad(value, currentPlaybackRate, currentVolume); };
}
}
// Sender generated event handlers
window.electronAPI.onPause(() => { player.pause(); });
window.electronAPI.onResume(() => { player.play(); });
window.electronAPI.onSeek((_event, value: SeekMessage) => { player.setCurrentTime(value.time); });
window.electronAPI.onSetVolume((_event, value: SetVolumeMessage) => { volumeChangeHandler(value.volume); });
window.electronAPI.onSetSpeed((_event, value: SetSpeedMessage) => { player.setPlaybackRate(value.speed); playerCtrlStateUpdate(PlayerControlEvent.SetPlaybackRate); });
});
let scrubbing = false;
let volumeChanging = false;
enum PlayerControlEvent {
Load,
Pause,
Play,
VolumeChange,
TimeUpdate,
UiFadeOut,
UiFadeIn,
SetCaptions,
ToggleSpeedMenu,
SetPlaybackRate,
ToggleFullscreen,
ExitFullscreen,
}
// UI update handlers
function playerCtrlStateUpdate(event: PlayerControlEvent) {
export function targetPlayerCtrlStateUpdate(event: PlayerControlEvent) {
switch (event) {
case PlayerControlEvent.Load: {
playerCtrlProgressBarBuffer.setAttribute("style", "width: 0px");
playerCtrlProgressBarProgress.setAttribute("style", "width: 0px");
playerCtrlProgressBarHandle.setAttribute("style", `left: ${playerCtrlProgressBar.offsetLeft}px`);
const volume = Math.round(player.getVolume() * playerCtrlVolumeBar.offsetWidth);
playerCtrlVolumeBarProgress.setAttribute("style", `width: ${volume}px`);
playerCtrlVolumeBarHandle.setAttribute("style", `left: ${volume + 8}px`);
if (isLive) {
playerCtrlLiveBadge.setAttribute("style", "display: block");
playerCtrlPosition.setAttribute("style", "display: none");
playerCtrlDuration.setAttribute("style", "display: none");
}
else {
playerCtrlLiveBadge.setAttribute("style", "display: none");
playerCtrlPosition.setAttribute("style", "display: block");
playerCtrlDuration.setAttribute("style", "display: block");
playerCtrlPosition.textContent = formatDuration(player.getCurrentTime());
playerCtrlDuration.innerHTML = `/&nbsp&nbsp${formatDuration(player.getDuration())}`;
}
if (player.isCaptionsSupported()) {
playerCtrlCaptions.setAttribute("style", "display: block");
videoCaptions.setAttribute("style", "display: block");
}
else {
playerCtrlCaptions.setAttribute("style", "display: none");
videoCaptions.setAttribute("style", "display: none");
player.enableCaptions(false);
}
playerCtrlStateUpdate(PlayerControlEvent.SetCaptions);
break;
}
case PlayerControlEvent.Pause:
playerCtrlAction.setAttribute("class", "play");
stopUiHideTimer();
break;
case PlayerControlEvent.Play:
playerCtrlAction.setAttribute("class", "pause");
startUiHideTimer();
break;
case PlayerControlEvent.VolumeChange: {
// console.log(`VolumeChange: isMute ${player.isMuted()}, volume: ${player.getVolume()}`);
const volume = Math.round(player.getVolume() * playerCtrlVolumeBar.offsetWidth);
if (player.isMuted()) {
playerCtrlVolume.setAttribute("class", "mute");
playerCtrlVolumeBarProgress.setAttribute("style", `width: 0px`);
playerCtrlVolumeBarHandle.setAttribute("style", `left: 0px`);
}
else if (player.getVolume() >= 0.5) {
playerCtrlVolume.setAttribute("class", "volume_high");
playerCtrlVolumeBarProgress.setAttribute("style", `width: ${volume}px`);
playerCtrlVolumeBarHandle.setAttribute("style", `left: ${volume}px`);
} else {
playerCtrlVolume.setAttribute("class", "volume_low");
playerCtrlVolumeBarProgress.setAttribute("style", `width: ${volume}px`);
playerCtrlVolumeBarHandle.setAttribute("style", `left: ${volume}px`);
}
break;
}
case PlayerControlEvent.TimeUpdate: {
// console.log(`TimeUpdate: Position: ${player.getCurrentTime()}, Duration: ${player.getDuration()}`);
if (isLive) {
if (isLivePosition && player.getDuration() - player.getCurrentTime() > livePositionWindow) {
isLivePosition = false;
playerCtrlLiveBadge.setAttribute("style", `background-color: #595959`);
}
else if (!isLivePosition && player.getDuration() - player.getCurrentTime() <= livePositionWindow) {
isLivePosition = true;
playerCtrlLiveBadge.setAttribute("style", `background-color: red`);
}
}
if (isLivePosition) {
playerCtrlProgressBarProgress.setAttribute("style", `width: ${playerCtrlProgressBar.offsetWidth}px`);
playerCtrlProgressBarHandle.setAttribute("style", `left: ${playerCtrlProgressBar.offsetWidth + playerCtrlProgressBar.offsetLeft}px`);
}
else {
const buffer = Math.round((player.getBufferLength() / player.getDuration()) * playerCtrlProgressBar.offsetWidth);
const progress = Math.round((player.getCurrentTime() / player.getDuration()) * playerCtrlProgressBar.offsetWidth);
const handle = progress + playerCtrlProgressBar.offsetLeft;
playerCtrlProgressBarBuffer.setAttribute("style", `width: ${buffer}px`);
playerCtrlProgressBarProgress.setAttribute("style", `width: ${progress}px`);
playerCtrlProgressBarHandle.setAttribute("style", `left: ${handle}px`);
playerCtrlPosition.textContent = formatDuration(player.getCurrentTime());
}
break;
}
case PlayerControlEvent.UiFadeOut:
document.body.style.cursor = "none";
playerControls.setAttribute("style", "opacity: 0");
if (player.isCaptionsEnabled()) {
videoCaptions.setAttribute("style", "display: block; bottom: 75px;");
} else {
videoCaptions.setAttribute("style", "display: none; bottom: 75px;");
}
break;
case PlayerControlEvent.UiFadeIn:
document.body.style.cursor = "default";
playerControls.setAttribute("style", "opacity: 1");
if (player.isCaptionsEnabled()) {
videoCaptions.setAttribute("style", "display: block; bottom: 160px;");
} else {
videoCaptions.setAttribute("style", "display: none; bottom: 160px;");
}
break;
case PlayerControlEvent.SetCaptions:
if (player.isCaptionsEnabled()) {
playerCtrlCaptions.setAttribute("class", "captions_on");
videoCaptions.setAttribute("style", "display: block");
} else {
playerCtrlCaptions.setAttribute("class", "captions_off");
videoCaptions.setAttribute("style", "display: none");
}
break;
case PlayerControlEvent.ToggleSpeedMenu: {
if (playerCtrlSpeedMenuShown) {
playerCtrlSpeedMenu.setAttribute("style", "display: none");
} else {
playerCtrlSpeedMenu.setAttribute("style", "display: block");
}
playerCtrlSpeedMenuShown = !playerCtrlSpeedMenuShown;
break;
}
case PlayerControlEvent.SetPlaybackRate: {
const rate = player.getPlaybackRate().toFixed(2);
const entryElement = document.getElementById(`speedMenuEntry_${rate}_enabled`);
playbackRates.forEach(r => {
const entry = document.getElementById(`speedMenuEntry_${r}_enabled`);
entry.setAttribute("style", "opacity: 0");
});
// Ignore updating GUI for custom rates
if (entryElement !== null) {
entryElement.setAttribute("style", "opacity: 1");
}
break;
}
case PlayerControlEvent.ToggleFullscreen: {
window.electronAPI.toggleFullScreen();
@ -502,199 +30,8 @@ function playerCtrlStateUpdate(event: PlayerControlEvent) {
}
}
function scrubbingMouseUIHandler(e: MouseEvent) {
const progressBarOffset = e.offsetX - 8;
const progressBarWidth = PlayerCtrlProgressBarInteractiveArea.offsetWidth - 16;
let time = isLive ? Math.round((1 - (progressBarOffset / progressBarWidth)) * player.getDuration()) : Math.round((progressBarOffset / progressBarWidth) * player.getDuration());
time = Math.min(player.getDuration(), Math.max(0.0, time));
if (scrubbing && isLive && e.buttons === 1) {
isLivePosition = false;
playerCtrlLiveBadge.setAttribute("style", `background-color: #595959`);
}
const livePrefix = isLive && Math.floor(time) !== 0 ? "-" : "";
playerCtrlProgressBarPosition.textContent = isLive ? `${livePrefix}${formatDuration(time)}` : formatDuration(time);
let offset = e.offsetX - (playerCtrlProgressBarPosition.offsetWidth / 2);
offset = Math.min(PlayerCtrlProgressBarInteractiveArea.offsetWidth - (playerCtrlProgressBarPosition.offsetWidth / 1), Math.max(8, offset));
playerCtrlProgressBarPosition.setAttribute("style", `display: block; left: ${offset}px`);
}
// Receiver generated event handlers
playerCtrlAction.onclick = () => {
if (player.isPaused()) {
player.play();
} else {
player.pause();
}
};
playerCtrlVolume.onclick = () => { player.setMute(!player.isMuted()); };
PlayerCtrlProgressBarInteractiveArea.onmousedown = (e: MouseEvent) => { scrubbing = true; scrubbingMouseHandler(e) };
PlayerCtrlProgressBarInteractiveArea.onmouseup = () => { scrubbing = false; };
PlayerCtrlProgressBarInteractiveArea.onmouseenter = (e: MouseEvent) => {
if (e.buttons === 0) {
volumeChanging = false;
}
scrubbingMouseUIHandler(e);
};
PlayerCtrlProgressBarInteractiveArea.onmouseleave = () => { playerCtrlProgressBarPosition.setAttribute("style", "display: none"); };
PlayerCtrlProgressBarInteractiveArea.onmousemove = (e: MouseEvent) => { scrubbingMouseHandler(e) };
function scrubbingMouseHandler(e: MouseEvent) {
const progressBarOffset = e.offsetX - 8;
const progressBarWidth = PlayerCtrlProgressBarInteractiveArea.offsetWidth - 16;
let time = Math.round((progressBarOffset / progressBarWidth) * player.getDuration());
time = Math.min(player.getDuration(), Math.max(0.0, time));
if (scrubbing && e.buttons === 1) {
player.setCurrentTime(time);
}
scrubbingMouseUIHandler(e);
}
playerCtrlVolumeBarInteractiveArea.onmousedown = (e: MouseEvent) => { volumeChanging = true; volumeChangeMouseHandler(e) };
playerCtrlVolumeBarInteractiveArea.onmouseup = () => { volumeChanging = false; };
playerCtrlVolumeBarInteractiveArea.onmouseenter = (e: MouseEvent) => {
if (e.buttons === 0) {
scrubbing = false;
}
};
playerCtrlVolumeBarInteractiveArea.onmousemove = (e: MouseEvent) => { volumeChangeMouseHandler(e) };
playerCtrlVolumeBarInteractiveArea.onwheel = (e: WheelEvent) => {
const delta = -e.deltaY;
if (delta > 0 ) {
volumeChangeHandler(Math.min(player.getVolume() + volumeIncrement, 1));
} else if (delta < 0) {
volumeChangeHandler(Math.max(player.getVolume() - volumeIncrement, 0));
}
};
function volumeChangeMouseHandler(e: MouseEvent) {
if (volumeChanging && e.buttons === 1) {
const volumeBarOffsetX = e.offsetX - 8;
const volumeBarWidth = playerCtrlVolumeBarInteractiveArea.offsetWidth - 16;
const volume = volumeBarOffsetX / volumeBarWidth;
volumeChangeHandler(volume);
}
}
function volumeChangeHandler(volume: number) {
if (!player.isMuted() && volume <= 0) {
player.setMute(true);
}
else if (player.isMuted() && volume > 0) {
player.setMute(false);
}
player.setVolume(volume);
}
playerCtrlLiveBadge.onclick = () => { setLivePosition(); };
function setLivePosition() {
if (!isLivePosition) {
isLivePosition = true;
player.setCurrentTime(player.getDuration() - livePositionDelta);
playerCtrlLiveBadge.setAttribute("style", `background-color: red`);
if (player.isPaused()) {
player.play();
}
}
}
playerCtrlCaptions.onclick = () => { player.enableCaptions(!player.isCaptionsEnabled()); playerCtrlStateUpdate(PlayerControlEvent.SetCaptions); };
playerCtrlSpeed.onclick = () => { playerCtrlStateUpdate(PlayerControlEvent.ToggleSpeedMenu); };
playerCtrlFullscreen.onclick = () => { playerCtrlStateUpdate(PlayerControlEvent.ToggleFullscreen); };
playbackRates.forEach(r => {
const entry = document.getElementById(`speedMenuEntry_${r}`);
entry.onclick = () => {
player.setPlaybackRate(parseFloat(r));
playerCtrlStateUpdate(PlayerControlEvent.SetPlaybackRate);
playerCtrlStateUpdate(PlayerControlEvent.ToggleSpeedMenu);
};
});
videoElement.onclick = () => {
if (!playerCtrlSpeedMenuShown) {
if (player.isPaused()) {
player.play();
} else {
player.pause();
}
}
};
videoElement.ondblclick = () => { playerCtrlStateUpdate(PlayerControlEvent.ToggleFullscreen); };
// Component hiding
let uiHideTimer = null;
let uiVisible = true;
function startUiHideTimer() {
if (uiHideTimer === null) {
uiHideTimer = window.setTimeout(() => {
uiHideTimer = null;
uiVisible = false;
playerCtrlStateUpdate(PlayerControlEvent.UiFadeOut);
}, 3000);
}
}
function stopUiHideTimer() {
if (uiHideTimer) {
window.clearTimeout(uiHideTimer);
uiHideTimer = null;
}
if (!uiVisible) {
uiVisible = true;
playerCtrlStateUpdate(PlayerControlEvent.UiFadeIn);
}
}
document.onmouseout = () => {
if (uiHideTimer) {
window.clearTimeout(uiHideTimer);
uiHideTimer = null;
}
uiVisible = false;
playerCtrlStateUpdate(PlayerControlEvent.UiFadeOut);
}
document.onmousemove = () => {
stopUiHideTimer();
if (player && !player.isPaused()) {
startUiHideTimer();
}
};
window.onresize = () => { playerCtrlStateUpdate(PlayerControlEvent.TimeUpdate); };
// Listener for hiding speed menu when clicking outside element
document.addEventListener('click', (event: MouseEvent) => {
const node = event.target as Node;
if (playerCtrlSpeedMenuShown && !playerCtrlSpeed.contains(node) && !playerCtrlSpeedMenu.contains(node)){
playerCtrlStateUpdate(PlayerControlEvent.ToggleSpeedMenu);
}
});
// Add the keydown event listener to the document
const skipInterval = 10;
const volumeIncrement = 0.1;
document.addEventListener('keydown', (event) => {
// console.log("KeyDown", event);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function targetKeyDownEventListener(event: any) {
switch (event.code) {
case 'KeyF':
case 'F11':
@ -705,53 +42,7 @@ document.addEventListener('keydown', (event) => {
playerCtrlStateUpdate(PlayerControlEvent.ExitFullscreen);
event.preventDefault();
break;
case 'ArrowLeft':
// Skip back
player.setCurrentTime(Math.max(player.getCurrentTime() - skipInterval, 0));
event.preventDefault();
break;
case 'ArrowRight':
// Skip forward
if (!isLivePosition) {
player.setCurrentTime(Math.min(player.getCurrentTime() + skipInterval, player.getDuration()));
}
event.preventDefault();
break;
case "Home":
player.setCurrentTime(0);
event.preventDefault();
break;
case "End":
if (isLive) {
setLivePosition();
}
else {
player.setCurrentTime(player.getDuration());
}
event.preventDefault();
break;
case 'KeyK':
case 'Space':
case 'Enter':
// Pause/Continue
if (player.isPaused()) {
player.play();
} else {
player.pause();
}
event.preventDefault();
break;
case 'KeyM':
// Mute toggle
player.setMute(!player.isMuted());
break;
case 'ArrowUp':
// Volume up
volumeChangeHandler(Math.min(player.getVolume() + volumeIncrement, 1));
break;
case 'ArrowDown':
// Volume down
volumeChangeHandler(Math.max(player.getVolume() - volumeIncrement, 0));
default:
break;
}
});
};

View file

@ -1,10 +1,11 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="../../assets/fonts/inter.css" />
<link rel="stylesheet" href="./style.css" />
<title>FCast Receiver</title>
<meta charset="UTF-8">
<link rel="stylesheet" href="../assets/fonts/inter.css" />
<link rel="stylesheet" href="./common.css" />
<link rel="stylesheet" href="./style.css" />
</head>
<body>
<video id="videoPlayer" autoplay preload="auto"></video>

View file

@ -1,337 +1,15 @@
html {
margin: 0;
padding: 0;
overflow: hidden;
}
body {
margin: 0;
padding: 0;
background-color: black;
color: white;
width: 100vw;
max-width: 100%;
height: 100vh;
max-height: 100%;
}
#videoPlayer {
object-fit: contain;
width: 100%;
height: 100%;
}
*:focus {
outline: none;
box-shadow: none;
}
.container {
position: absolute;
bottom: 0px;
/* height: 100%; */
height: 120px;
width: 100%;
/* background: linear-gradient(to top, rgba(0, 0, 0, 0.8) 0%, rgba(0, 0, 0, 0) 100%); */
background: linear-gradient(to top, rgba(0, 0, 0, 0.8) 0%, rgba(0, 0, 0, 0.0) 35%);
background-size: 100% 300px;
background-repeat: no-repeat;
background-position: bottom;
opacity: 1;
transition: opacity 0.1s ease-in-out;
}
.volumeContainer {
position: relative;
height: 24px;
width: 92px;
flex-shrink: 0;
user-select: none;
}
.volumeBar {
position: absolute;
/* left: 12px; */
left: 8px;
top: 10px;
height: 4px;
/* width: 72px; */
width: 76px;
background-color: #999999;
border-radius: 3px;
pointer-events: none;
}
.volumeBarInteractiveArea {
position: absolute;
left: 0px;
/* left: 8px; */
top: 0px;
height: 24px;
width: 92px;
/* width: 84px; */
cursor: pointer;
}
.volumeBarHandle {
position: absolute;
left: 84px;
top: 4px;
width: 16px;
height: 16px;
/* background-color: #ffffff; */
background-color: #c9c9c9;
box-shadow: 0px 32px 64px 0px rgba(0, 0, 0, 0.56), 0px 2px 21px 0px rgba(0, 0, 0, 0.55);
border-radius: 50%;
pointer-events: none;
z-index: 10;
}
.volumeBarProgress {
position: absolute;
/* left: 12px; */
left: 8px;
top: 10px;
height: 4px;
width: 76px;
/* background-color: #ffffff; */
background-color: #c9c9c9;
border-radius: 3px;
pointer-events: none;
}
.progressBarContainer {
position: absolute;
bottom: 60px;
left: 16px;
right: 16px;
height: 4px;
padding-top: 10px;
padding-bottom: 10px;
border-radius: 3px;
cursor: pointer;
user-select: none;
}
.progressBarInteractiveArea {
position: absolute;
/* bottom: 60px; */
/* left: 24px; */
/* right: 24px; */
height: 4px;
width: 100%;
left: 0px;
bottom: 0px;
padding-top: 10px;
padding-bottom: 10px;
border-radius: 3px;
cursor: pointer;
z-index: 999;
}
.progressBarChapterContainer {
position: absolute;
bottom: 73px;
left: 24px;
right: 24px;
height: 4px;
border-radius: 3px;
cursor: pointer;
}
.progressBar {
/* position: absolute; */
position: relative;
/* bottom: 70px; */
/* left: 24px; */
/* right: 24px; */
left: 8px;
width: calc(100% - 16px);
height: 4px;
background-color: #99999945;
border-radius: 3px;
pointer-events: none;
}
.progressBarBuffer {
/* position: absolute; */
position: relative;
/* bottom: 70px; */
/* left: 24px; */
left: 8px;
bottom: 4px;
height: 4px;
background-color: #D9D9D945;
border-radius: 3px;
pointer-events: none;
}
.progressBarProgress {
/* position: absolute; */
position: relative;
/* bottom: 70px; */
/* left: 24px; */
left: 8px;
bottom: 8px;
height: 4px;
width: 0px;
background-color: #019BE7;
border-radius: 3px;
pointer-events: none;
}
.progressBarPosition {
display: none;
position: absolute;
bottom: 25px;
padding: 2px 5px;
font-family: InterVariable;
font-size: 16px;
font-style: normal;
font-weight: 400;
border-radius: 3px;
background-color: rgba(0, 0, 0, 0.5);
}
.progressBarHandle {
position: absolute;
/* bottom: 70px; */
bottom: 10px;
width: 20px;
height: 20px;
margin-left: -8px;
margin-bottom: -8px;
background-color: #019BE7;
border-radius: 50%;
pointer-events: none;
z-index: 10;
}
.positionContainer {
display: flex;
flex-direction: row;
flex-grow: 1;
align-items: center;
font-family: InterVariable;
font-size: 16px;
font-style: normal;
font-weight: 400;
user-select: text;
}
.position {
margin-right: 10px;
vertical-align: bottom;
color: #c9c9c9;
}
.duration {
opacity: 0.6;
color: #c9c9c9;
}
.liveBadge {
background-color: red;
/* margin-top: -2px; */
/* padding: 5px 5px; */
padding: 2px 5px;
border-radius: 4px;
/* margin-left: 10px; */
margin-right: 10px;
cursor: pointer;
}
.play {
width: 24px;
height: 24px;
cursor: pointer;
flex-shrink: 0;
background-image: url("../../assets/icons/player/icon24_play.svg");
transition: background-image 0.1s ease-in-out;
}
.play:hover {
background-image: url("../../assets/icons/player/icon24_play_active.svg");
}
.pause {
width: 24px;
height: 24px;
cursor: pointer;
flex-shrink: 0;
background-image: url("../../assets/icons/player/icon24_pause.svg");
transition: background-image 0.1s ease-in-out;
}
.pause:hover {
background-image: url("../../assets/icons/player/icon24_pause_active.svg");
}
.volume_high {
width: 24px;
height: 24px;
cursor: pointer;
flex-shrink: 0;
background-image: url("../../assets/icons/player/icon24_volume_more_50pct.svg");
transition: background-image 0.1s ease-in-out;
}
.volume_high:hover {
background-image: url("../../assets/icons/player/icon24_volume_more_50pct_active.svg");
}
.volume_low {
width: 24px;
height: 24px;
cursor: pointer;
flex-shrink: 0;
background-image: url("../../assets/icons/player/icon24_volume_less_50pct.svg");
transition: background-image 0.1s ease-in-out;
}
.volume_low:hover {
background-image: url("../../assets/icons/player/icon24_volume_less_50pct_active.svg");
}
.mute {
width: 24px;
height: 24px;
cursor: pointer;
flex-shrink: 0;
background-image: url("../../assets/icons/player/icon24_mute.svg");
transition: background-image 0.1s ease-in-out;
}
.mute:hover {
background-image: url("../../assets/icons/player/icon24_mute_active.svg");
}
.fullscreen_on {
width: 24px;
height: 24px;
cursor: pointer;
background-image: url("../../assets/icons/player/icon24_fullscreen_on.svg");
background-image: url("../assets/icons/player/icon24_fullscreen_on.svg");
transition: background-image 0.1s ease-in-out;
}
.fullscreen_on:hover {
background-image: url("../../assets/icons/player/icon24_fullscreen_on_active.svg");
background-image: url("../assets/icons/player/icon24_fullscreen_on_active.svg");
}
.fullscreen_off {
@ -339,157 +17,10 @@ body {
height: 24px;
cursor: pointer;
background-image: url("../../assets/icons/player/icon24_fullscreen_off.svg");
background-image: url("../assets/icons/player/icon24_fullscreen_off.svg");
transition: background-image 0.1s ease-in-out;
}
.fullscreen_off:hover {
background-image: url("../../assets/icons/player/icon24_fullscreen_off_active.svg");
}
.speed {
width: 24px;
height: 24px;
cursor: pointer;
background-image: url("../../assets/icons/player/icon24_speed.svg");
transition: background-image 0.1s ease-in-out;
}
.speed:hover {
background-image: url("../../assets/icons/player/icon24_speed_active.svg");
}
.captions_off {
width: 24px;
height: 24px;
cursor: pointer;
background-image: url("../../assets/icons/player/icon24_cc_off.svg");
transition: background-image 0.1s ease-in-out;
}
.captions_off:hover {
background-image: url("../../assets/icons/player/icon24_cc_off_active.svg");
}
.captions_on {
width: 24px;
height: 24px;
cursor: pointer;
background-image: url("../../assets/icons/player/icon24_cc_on.svg");
transition: background-image 0.1s ease-in-out;
}
.captions_on:hover {
background-image: url("../../assets/icons/player/icon24_cc_on_active.svg");
}
.leftButtonContainer {
position: absolute;
bottom: 24px;
left: 24px;
height: 24px;
/* width: calc(50% - 24px); */
right: 160px;
display: flex;
flex-direction: row;
align-items: center;
gap: 24px;
overflow: hidden;
user-select: none;
}
.buttonContainer {
position: absolute;
bottom: 24px;
right: 24px;
height: 24px;
/* width: calc(50% - 24px); */
align-items: center;
overflow: hidden;
display: flex;
flex-direction: row-reverse;
gap: 24px;
}
.captionsContainer {
/* display: none; */
position: relative;
/* top: -200px; */
bottom: 160px;
margin: auto;
text-align: center;
font-family: InterVariable;
font-size: 28px;
font-style: normal;
font-weight: 400;
background-color: rgba(0, 0, 0, 0.5);
padding: 0px 5px;
width: fit-content;
transition: bottom 0.2s ease-in-out;
}
.speedMenu {
position: absolute;
bottom: 80px;
right: 60px;
height: calc(55vh);
max-height: 368px;
background-color: #141414;
padding: 12px;
border-radius: 10px;
border: 1px solid #2E2E2E;
scrollbar-width: thin;
overflow: auto;
font-family: InterVariable;
font-size: 16px;
font-style: normal;
font-weight: 400;
box-shadow: 0px 1.852px 3.148px 0px rgba(0, 0, 0, 0.06), 0px 8.148px 6.519px 0px rgba(0, 0, 0, 0.10), 0px 20px 13px 0px rgba(0, 0, 0, 0.13), 0px 38.519px 25.481px 0px rgba(0, 0, 0, 0.15), 0px 64.815px 46.852px 0px rgba(0, 0, 0, 0.19), 0px 100px 80px 0px rgba(0, 0, 0, 0.25);
}
.speedMenuTitle {
font-weight: 700;
line-height: 24px;
margin: 10px;
}
.speedMenuEntry {
display: flex;
padding: 10px 15px;
}
.speedMenuEntry:hover {
cursor: pointer;
background-color: rgba(255, 255, 255, 0.1);
}
.speedMenuSeparator {
height: 1px;
background: #2E2E2E;
margin-top: 3px;
margin-bottom: 3px;
}
.speedMenuEntryEnabled {
width: 20px;
height: 20px;
margin-right: 10px;
background-image: url("../../assets/icons/player/icon24_check_thin.svg");
background-size: cover;
opacity: 0;
background-image: url("../assets/icons/player/icon24_fullscreen_off_active.svg");
}

View file

@ -9,7 +9,13 @@
"experimentalDecorators": true,
"removeComments": false,
"noImplicitAny": false,
"outDir": "dist"
"outDir": "dist",
"baseUrl": ".",
"paths": {
"src/*": ["./src/*"],
"modules/*": ["./node_modules/*"],
"common/*": ["../common/web/*"],
}
},
"exclude": [ "node_modules", "test" ]
}

View file

@ -1,8 +1,16 @@
const webpack = require('webpack');
const path = require('path');
const CopyWebpackPlugin = require("copy-webpack-plugin");
// const buildMode = 'production';
const buildMode = 'development';
const TARGET = 'electron';
// const TARGET = 'webOS';
// const TARGET = 'tizenOS';
module.exports = [
{
mode: 'development',
mode: buildMode,
entry: './src/App.ts',
target: 'electron-main',
module: {
@ -15,15 +23,43 @@ module.exports = [
],
},
resolve: {
alias: {
'src': path.resolve(__dirname, 'src'),
'modules': path.resolve(__dirname, 'node_modules'),
'common': path.resolve(__dirname, '../common/web'),
},
extensions: ['.tsx', '.ts', '.js'],
},
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
},
plugins: [
new CopyWebpackPlugin({
patterns: [
// Common assets
{
from: '../common/assets/**',
to: './[path][name][ext]',
context: path.resolve(__dirname, '..', 'common'),
globOptions: { ignore: ['**/*.txt'] }
},
// Target assets
{
from: '**',
to: './assets/[path][name][ext]',
context: path.resolve(__dirname, 'assets'),
globOptions: { ignore: [] }
}
],
}),
new webpack.DefinePlugin({
TARGET: JSON.stringify(TARGET)
})
]
},
{
mode: 'development',
mode: buildMode,
entry: {
preload: './src/main/Preload.ts',
renderer: './src/main/Renderer.ts',
@ -39,15 +75,38 @@ module.exports = [
],
},
resolve: {
alias: {
'src': path.resolve(__dirname, 'src'),
'modules': path.resolve(__dirname, 'node_modules'),
'common': path.resolve(__dirname, '../common/web'),
},
extensions: ['.tsx', '.ts', '.js'],
},
output: {
filename: '[name].js',
path: path.resolve(__dirname, 'dist/main'),
},
plugins: [
new CopyWebpackPlugin({
patterns: [
{
from: '../common/web/main/common.css',
to: '[name][ext]',
},
{
from: './src/main/*',
to: '[name][ext]',
globOptions: { ignore: ['**/*.ts'] }
}
],
}),
new webpack.DefinePlugin({
TARGET: JSON.stringify(TARGET)
})
]
},
{
mode: 'development',
mode: buildMode,
entry: {
preload: './src/player/Preload.ts',
renderer: './src/player/Renderer.ts',
@ -63,11 +122,34 @@ module.exports = [
],
},
resolve: {
alias: {
'src': path.resolve(__dirname, 'src'),
'modules': path.resolve(__dirname, 'node_modules'),
'common': path.resolve(__dirname, '../common/web'),
},
extensions: ['.tsx', '.ts', '.js'],
},
output: {
filename: '[name].js',
path: path.resolve(__dirname, 'dist/player'),
},
plugins: [
new CopyWebpackPlugin({
patterns: [
{
from: '../common/web/player/common.css',
to: '[name][ext]',
},
{
from: './src/player/*',
to: '[name][ext]',
globOptions: { ignore: ['**/*.ts'] }
}
],
}),
new webpack.DefinePlugin({
TARGET: JSON.stringify(TARGET)
})
]
}
];