mirror of
https://github.com/WonderAliceUwU/OmniArr.git
synced 2026-11-11 04:01:01 +00:00
⭐️ feature: add tv networks
This commit is contained in:
1 parent
bc69468130
commit
ca2976a783
10 files changed
+1473
-351
No files matched your search
Binary file not shown.
|
After Width: | Height: | Size: 1.8 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" id="Layer_1" viewBox="0 0 283.18 204.15"><defs><style>.cls-1{fill:#000000;}</style></defs><g><polygon class="cls-1" points="52.09 44.37 31.96 44.37 31.96 1.91 0 1.91 0 116.05 31.96 116.05 31.96 71.77 52.09 71.77 52.09 116.05 83.09 116.05 83.09 1.91 52.09 1.91 52.09 44.37"/><path class="cls-1" d="M225.19,0c-31.94-.02-48.26,23.49-52.3,32.98,.04-14.25-14-31.06-30.43-31.08h-52.92V116.05h49.34c19.89,0,34.05-17.23,34.07-32.01,4.54,9.3,20.29,32.59,52.23,32.58,31.98-.01,58-26.58,58-58.57C283.18,25.39,257.17,.02,225.19,0Zm-90.44,89.47h-15.69v-19.29h15.69c4.57,0,8.16,4.51,8.16,9.65s-3.6,9.65-8.16,9.65Zm.06-42.86h-15.69V27.31h15.69c4.57,0,8.16,4.51,8.16,9.64s-3.6,9.65-8.16,9.65Zm20.89,11.47c3.6-.46,9.59-4.57,11.7-7.19-.74,3.25-.8,12.67,.07,15.92-2.41-3.71-8.12-8.28-11.77-8.73Zm69.51,29.06c-16.05,0-29.06-13.01-29.06-29.06s13.01-29.06,29.06-29.06,29.06,13.01,29.06,29.06-13.01,29.06-29.06,29.06Z"/><path class="cls-1" d="M225.21,35.23c-12.62,0-22.84,10.23-22.84,22.84s10.23,22.84,22.84,22.84,22.84-10.23,22.84-22.84-10.23-22.84-22.84-22.84Z"/></g><g><path class="cls-1" d="M82.95,129.8c-9.18,0-18.97,7.86-24.36,12.76-2.69-8.5-8.69-12.76-19.47-12.76-8.2,0-16.77,6.18-22.28,10.95v-9.28H.19v72.09H16.84v-48.25c5.39-4.77,13.22-10.57,19.1-10.57,6.98,0,8.08,5.41,8.08,14.43v44.39h16.65v-44.39c0-1.29,0-2.58-.12-3.74,5.39-4.77,13.34-10.69,19.22-10.69,6.98,0,8.08,5.41,8.08,14.43v44.39h16.77v-44.39c0-19.71-5.39-29.38-21.67-29.38Z"/><path class="cls-1" d="M243.16,166.52c11.01-10.47,20.95-22.15,29.47-35.05h-20.95c-5.15,8.8-11.36,16.39-19,24.28-7.64-7.89-14.38-15.48-19-24.28h-20.95c8.52,12.9,18.47,24.58,29.47,35.05-11.72,10.93-22.02,23.37-31.25,37.02h20.95c5.68-9.56,12.61-17.75,20.77-26.1,8.17,8.35,15.09,16.54,20.77,26.1h20.95c-9.23-13.66-19.53-26.1-31.25-37.02Z"/><path class="cls-1" d="M170.19,131.47h-.38v8.71c-6.31-7.29-14.4-10.67-23.73-10.67-19.36,0-35.42,16.65-35.42,37.31s16.06,37.31,35.42,37.31c9.33,0,17.42-3.38,23.73-10.67v10.08h16.65v-72.09h-16.27Zm-6.43,50.63c-4.36,4.26-9.51,6.4-15.47,6.4-2.84,0-5.66-.58-8.45-1.73-2.8-1.15-5.2-2.73-7.19-4.72-4.24-4.14-6.35-9.31-6.35-15.49s2.06-11.01,6.18-15.19c4.32-4.38,9.51-6.57,15.59-6.57s11.17,2.11,15.53,6.34c4.28,4.15,6.42,9.54,6.42,16.19,0,5.79-2.08,10.72-6.23,14.78Z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 90 KiB |
@@ -0,0 +1,74 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||
|
||||
<svg
|
||||
width="124.35394mm"
|
||||
height="73.636108mm"
|
||||
viewBox="0 0 124.35394 73.636108"
|
||||
version="1.1"
|
||||
id="svg1"
|
||||
xml:space="preserve"
|
||||
inkscape:version="1.3.2 (091e20e, 2023-11-25, custom)"
|
||||
sodipodi:docname="Prime-2024.svg"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"><sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:document-units="mm"
|
||||
showguides="true"
|
||||
inkscape:zoom="1.4004209"
|
||||
inkscape:cx="206.0095"
|
||||
inkscape:cy="44.986476"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:window-height="1017"
|
||||
inkscape:window-x="-8"
|
||||
inkscape:window-y="-8"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="layer1" /><defs
|
||||
id="defs1" /><g
|
||||
inkscape:label="Layer 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1"
|
||||
transform="translate(170.34596,187.1882)"><path
|
||||
d="m -154.09649,-137.56369 c -0.21429,0.0209 -0.40278,0.0942 -0.57003,0.21968 -0.45995,0.3345 -0.56172,0.92533 -0.29237,1.75583 0.23255,0.71703 0.8361,1.58907 1.71418,2.67622 3.51232,4.34857 7.69349,7.98634 12.58565,10.87145 4.85034,2.88512 10.07716,5.01792 15.63833,6.39775 5.60298,1.37985 9.65904,2.09067 17.1018,2.09067 7.44277,0 14.551192,-1.08746 21.283133,-3.26176 6.731939,-2.13247 12.376898,-5.18452 16.892734,-9.07316 1.588904,-1.42165 2.38303,-2.5088 2.38303,-3.34507 0,-0.29268 -0.125606,-0.62768 -0.376482,-0.92038 -0.37632,-0.33449 -0.794289,-0.45945 -1.296049,-0.33402 -0.501759,0.12548 -1.212586,0.41799 -2.090665,0.87793 -4.306769,2.25792 -9.575571,4.09769 -15.805751,5.43572 -6.188368,1.37985 -11.038057,2.04902 -19.40072,2.04902 -8.36266,0 -16.43278,-1.12962 -24.21006,-3.38754 -7.77727,-2.29971 -14.67647,-5.85385 -20.69758,-10.66238 -0.87808,-0.66901 -1.58874,-1.08698 -2.13232,-1.29606 -0.27178,-0.0836 -0.51254,-0.11489 -0.72683,-0.0939 z"
|
||||
id="path908"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="scssccsccscsccscccs"
|
||||
style="fill:#0578ff;fill-opacity:1;stroke-width:0.418133" /><path
|
||||
d="m -65.143602,-139.22561 c -2.968743,0 -5.854025,0.71066 -8.530075,2.13231 -0.961706,0.54358 -1.714671,1.04518 -2.216431,1.50512 -0.501759,0.50176 -0.65932,0.95698 -0.418132,1.46346 0.177054,0.37181 0.502085,0.58539 0.962032,0.62721 0.459945,0.0836 1.128469,0.0423 2.006548,-0.125 1.421652,-0.2927 2.88577,-0.50176 4.432861,-0.62721 1.547093,-0.0836 2.884955,-0.0422 4.013914,0.0833 1.128959,0.16725 1.839623,0.41813 2.132315,0.83627 0.418133,0.66901 0.292856,2.00671 -0.459784,4.05556 -0.710825,2.04886 -1.714344,4.05621 -2.92693,5.97963 -0.459947,0.79446 -0.752639,1.38001 -0.836266,1.83995 -0.125432,0.45995 -0.04796,0.86412 0.237002,1.15173 0.2081,0.21002 0.476558,0.31174 0.808331,0.31174 0.669012,0 1.672369,-0.58571 2.96858,-1.79831 2.341545,-2.04884 3.972589,-4.43172 4.934296,-7.23321 0.543572,-1.46346 0.835776,-2.9689 0.961216,-4.55781 0.125432,-1.54709 -0.02962,-2.75784 -0.543084,-3.38671 -1.659089,-2.03197 -5.091998,-2.23919 -7.526393,-2.25808 z"
|
||||
id="path898"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="sccsccccccccsscccsss"
|
||||
style="fill:#0578ff;fill-opacity:1;stroke-width:0.418133" /><path
|
||||
d="m -118.84955,-187.1882 c -1.15447,0 -2.18952,0.39825 -2.98571,1.15463 -0.79619,0.71657 -1.19429,1.79127 -1.19429,3.02536 0,1.2341 0.43775,2.26899 1.23395,3.02538 0.83599,0.75637 1.83122,1.15461 2.9857,1.15461 1.19428,0 2.14988,-0.39824 2.94605,-1.15461 0.83601,-0.75639 1.23395,-1.75146 1.23395,-3.02538 0,-1.2739 -0.43776,-2.26897 -1.23395,-3.02536 -0.79617,-0.75638 -1.83122,-1.15463 -2.9857,-1.15463 z m -2.66771,10.58917 c -0.27866,0 -0.47756,0.0794 -0.63679,0.23869 -0.15923,0.15925 -0.2387,0.35891 -0.2387,0.63759 v 26.3139 c 0,0.27866 0.0794,0.51721 0.2387,0.67645 0.15923,0.15924 0.35813,0.23871 0.63679,0.23871 h 5.41472 c 0.27867,0 0.47756,-0.0794 0.6368,-0.23871 0.15923,-0.15924 0.23869,-0.35812 0.23869,-0.6368 v -26.35355 c 0,-0.59715 -0.27835,-0.87628 -0.87549,-0.87628 z"
|
||||
id="path7"
|
||||
inkscape:connector-curvature="0"
|
||||
style="fill:#0578ff;fill-opacity:1;stroke-width:0.418133" /><path
|
||||
class="color-health-green"
|
||||
d="m -156.01455,-177.27579 c 1.91086,0 3.58287,0.59716 5.0558,1.83125 1.47295,1.23408 2.58763,2.94589 3.42362,5.17522 0.7962,2.22933 1.19429,4.81696 1.19429,7.72305 0,2.90609 -0.39809,5.4937 -1.19429,7.68322 -0.79619,2.22933 -1.95067,3.94114 -3.42362,5.17524 -1.47293,1.23409 -3.14494,1.87104 -5.0558,1.87104 -1.91085,0 -2.82647,-0.39809 -3.94114,-1.15447 -1.11467,-0.75639 -2.1099,-1.95067 -2.94591,-3.50324 h -0.27865 v 11.94116 c 0,0.27865 -0.0796,0.47772 -0.23886,0.63694 -0.15925,0.15923 -0.35829,0.23886 -0.63696,0.23886 h -5.4141 c -0.23885,0 -0.45648,-0.0677 -0.63673,-0.24789 -0.18023,-0.18024 -0.23906,-0.38906 -0.23906,-0.62791 v -35.18989 c 0,-0.23885 0.0796,-0.4379 0.27865,-0.59715 0.15924,-0.19905 0.35829,-0.27868 0.59714,-0.27868 h 4.45867 c 0.47771,0 0.79619,0.27868 0.95543,0.87583 l 0.67676,3.30419 h 0.27866 c 0.59715,-1.47296 1.55258,-2.62743 2.86628,-3.54304 1.27392,-0.87583 2.70706,-1.31373 4.21982,-1.31373 z m -2.508,6.01123 c -1.51276,0 -2.70705,0.75639 -3.54304,2.26914 -0.83601,1.51276 -1.2341,3.66248 -1.2341,6.44915 0,2.78665 0.39809,4.93636 1.2341,6.44913 0.83599,1.51276 2.03028,2.26913 3.54304,2.26913 1.51277,0 2.70705,-0.75637 3.50323,-2.26913 0.79619,-1.51277 1.2341,-3.66248 1.2341,-6.44913 0,-2.78667 -0.39809,-4.93639 -1.2341,-6.44915 -0.79618,-1.51275 -1.99046,-2.26914 -3.50323,-2.26914 z"
|
||||
id="path9"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="sscscsssccssssssscssccccsssssssscs"
|
||||
style="fill:#0578ff;fill-opacity:1;stroke-width:0.418133" /><path
|
||||
id="path11"
|
||||
style="fill:#0578ff;fill-opacity:1;stroke-width:0.418133"
|
||||
d="m -57.616635,-177.27548 c -2.388569,0 -4.498608,0.63658 -6.329847,1.91047 -1.831239,1.27391 -3.263907,3.06543 -4.338753,5.33456 -1.035048,2.26914 -1.552877,4.81749 -1.552877,7.68377 0,2.82647 0.477512,5.37383 1.512569,7.56336 1.035057,2.18951 2.508041,3.9015 4.33927,5.13561 1.831241,1.23408 3.702434,1.83141 6.329847,1.83141 2.627431,0 4.896479,-0.67686 6.807336,-1.99058 1.910857,-1.35351 3.304335,-3.26425 4.140316,-5.77225 0.199046,-0.59713 -2.53e-4,-0.95596 -0.59738,-1.07539 l -5.015714,-1.11414 c -0.517529,-0.11947 -0.875851,0.11916 -0.995288,0.67644 -0.597138,2.42838 -1.512794,3.62252 -4.33927,3.62252 -2.826469,0 -4.418715,-2.30932 -4.737179,-6.96702 h 15.446086 c 0.278653,0 0.517227,-0.0794 0.676444,-0.23874 0.199055,-0.15923 0.279053,-0.35798 0.279053,-0.63666 v -1.43298 c 0,-2.78665 -0.478047,-5.29477 -1.473295,-7.4843 -0.955419,-2.22934 -2.308579,-3.9413 -4.100008,-5.1754 -1.751602,-1.23409 -3.662744,-1.87068 -6.05131,-1.87068 z m -0.03979,5.65288 c 1.234102,0 2.22951,0.55708 2.946073,1.63194 0.756394,1.07485 1.233952,2.70738 1.432988,4.81728 h -9.036658 c 0.238855,-2.07008 0.796465,-3.66232 1.592667,-4.77697 0.835982,-1.11466 1.830837,-1.67225 3.06493,-1.67225 z" /><path
|
||||
class="color-health-green"
|
||||
d="m -109.4982,-148.53318 c -0.23885,0 -0.4546,-0.0581 -0.62513,-0.24758 -0.1883,-0.20922 -0.25067,-0.4292 -0.25067,-0.62825 v -26.31407 c 0,-0.27866 0.0796,-0.47771 0.23886,-0.63694 0.19904,-0.15923 0.39809,-0.23885 0.63694,-0.23885 h 4.45866 c 0.51753,0 0.83601,0.27865 0.95543,0.87579 l 0.67676,3.3042 h 0.27867 c 0.59715,-1.59239 1.47295,-2.78667 2.62743,-3.62266 1.15448,-0.83601 2.508011,-1.2341 4.060578,-1.2341 1.552577,0 3.065326,0.4379 4.25961,1.2739 1.194285,0.83601 2.109916,2.03028 2.746862,3.58286 h 0.278665 c 0.995249,-1.6322 2.149714,-2.82649 3.383818,-3.66247 1.273883,-0.7962 2.667214,-1.2341 4.219791,-1.2341 1.552568,0 3.105145,0.4379 4.339239,1.35351 1.234104,0.91563 2.189524,2.18953 2.786661,3.90134 0.636947,1.672 0.995239,3.62267 0.995239,5.89181 v 16.75978 c 0,0.23885 -0.07962,0.47771 -0.238865,0.63696 -0.159227,0.15924 -0.358282,0.23887 -0.636937,0.23887 h -5.414095 c -0.238865,0 -0.437911,-0.0796 -0.597138,-0.27868 -0.199046,-0.19904 -0.278674,-0.3981 -0.278674,-0.59715 v -16.20245 c 0,-1.672 -0.318473,-2.94591 -0.95543,-3.82171 -0.636946,-0.87582 -1.512758,-1.31371 -2.707043,-1.31371 -1.194284,0 -2.34876,0.51751 -3.025516,1.55256 -0.676766,1.03505 -0.995239,2.54781 -0.995239,4.53829 v 15.24702 c 0,0.23885 -0.07962,0.47771 -0.238865,0.63696 -0.159227,0.15924 -0.358272,0.23887 -0.636946,0.23887 h -5.414086 c -0.238865,0 -0.437911,-0.0796 -0.597148,-0.27868 -0.199035,-0.19904 -0.278664,-0.3981 -0.278664,-0.59715 v -16.20245 c 0,-1.672 -0.318473,-2.94591 -0.95543,-3.82171 -0.636946,-0.87582 -1.512758,-1.31371 -2.707043,-1.31371 -1.194304,0 -2.348774,0.51751 -3.025544,1.55256 -0.67676,1.03505 -0.99523,2.54781 -0.99523,4.53829 v 15.24702 c 0,0.23885 -0.0796,0.47771 -0.23886,0.63696 -0.15923,0.15924 -0.35828,0.23887 -0.63695,0.23887 z"
|
||||
id="path13"
|
||||
inkscape:connector-curvature="0"
|
||||
sodipodi:nodetypes="sssscsscccssscccsscssssscsssssssssscssssssssss"
|
||||
style="fill:#0578ff;fill-opacity:1;stroke-width:0.418133" /><path
|
||||
id="path14"
|
||||
style="fill:#0578ff;fill-opacity:1;stroke-width:0.418133"
|
||||
class="color-health-green"
|
||||
d="m -128.53789,-177.27548 c -1.55256,0 -2.90574,0.39802 -4.06022,1.23403 -1.15448,0.83599 -2.03061,2.03013 -2.62774,3.62252 h -0.27854 l -0.67696,-3.30419 c -0.11942,-0.59714 -0.43797,-0.87592 -0.9555,-0.87592 h -4.45864 c -0.23885,0 -0.43762,0.08 -0.63666,0.23926 -0.15926,0.15923 -0.23874,0.358 -0.23874,0.63666 v 26.31415 c 0,0.19905 0.0623,0.41917 0.25063,0.62839 0.17053,0.18948 0.38592,0.24753 0.62477,0.24753 h 5.49372 c 0.27867,0 0.47794,-0.0795 0.63717,-0.23875 0.15926,-0.15925 0.23875,-0.39832 0.23875,-0.63717 v -15.24713 c 0,-1.99048 0.31852,-3.42049 0.99528,-4.45554 0.67677,-1.03505 1.83136,-1.5167 3.02566,-1.5167 1.234,0 2.6962,0.4392 3.87625,1.07228 0.32367,0.17364 0.96781,0.26748 1.13791,-0.42943 l 1.38907,-5.69113 c 0.0454,-0.18512 0.14733,-0.6098 -0.47388,-0.89555 -0.99358,-0.45702 -2.11506,-0.70331 -3.26233,-0.70331 z"
|
||||
sodipodi:nodetypes="sscccsscsssssssssssscss" /></g></svg>
|
||||
|
After Width: | Height: | Size: 10 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 41 KiB |
+26
-7
@@ -9,6 +9,7 @@ import {
|
||||
getDashboard,
|
||||
getDownloads,
|
||||
getHubPage,
|
||||
getTmdbLogos,
|
||||
getServiceOptions,
|
||||
loadJellyfinUsers,
|
||||
proxyJellyfinImage,
|
||||
@@ -196,13 +197,15 @@ async function routeRequest(request: IncomingMessage, response: ServerResponse):
|
||||
|
||||
if (url.pathname.startsWith('/api/hubs/') && method === 'GET') {
|
||||
const config = await readConfig(session!.encryptionKey);
|
||||
const page = decodeURIComponent(url.pathname.replace('/api/hubs/', '')) as
|
||||
| 'stream'
|
||||
| 'home'
|
||||
| 'anime'
|
||||
| 'dorama'
|
||||
| 'library';
|
||||
sendJson(response, 200, await getHubPage(config, page));
|
||||
const page = decodeURIComponent(url.pathname.replace('/api/hubs/', ''));
|
||||
console.log(`[API] GET /api/hubs/${page}`);
|
||||
try {
|
||||
const result = await getHubPage(config, page as any);
|
||||
sendJson(response, 200, result);
|
||||
} catch (err) {
|
||||
console.error(`[API] Error in GET /api/hubs/${page}:`, err);
|
||||
sendJson(response, 500, { error: err instanceof Error ? err.message : 'Internal server error' });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -251,6 +254,22 @@ async function routeRequest(request: IncomingMessage, response: ServerResponse):
|
||||
return;
|
||||
}
|
||||
|
||||
if (url.pathname === '/api/tmdb/logo' && method === 'GET') {
|
||||
const config = await readConfig(session!.encryptionKey);
|
||||
const tmdbId = Number(url.searchParams.get('tmdbId') || 0);
|
||||
const mediaTypeParam = url.searchParams.get('mediaType');
|
||||
const mediaType = mediaTypeParam === 'tv' ? 'tv' : mediaTypeParam === 'movie' ? 'movie' : null;
|
||||
|
||||
if (!tmdbId || !mediaType) {
|
||||
sendJson(response, 400, { error: 'tmdbId and mediaType are required.' });
|
||||
return;
|
||||
}
|
||||
|
||||
const logos = await getTmdbLogos(config, tmdbId, mediaType);
|
||||
sendJson(response, 200, { logos });
|
||||
return;
|
||||
}
|
||||
|
||||
await serveApp(url.pathname, response);
|
||||
}
|
||||
|
||||
|
||||
+671
-189
File diff suppressed because it is too large.
Load diff
+18
-1
@@ -3,6 +3,11 @@ export type AppRoute =
|
||||
| 'home'
|
||||
| 'anime'
|
||||
| 'dorama'
|
||||
| 'netflix'
|
||||
| 'primevideo'
|
||||
| 'hbo'
|
||||
| 'viki'
|
||||
| 'disney'
|
||||
| 'library'
|
||||
| 'downloads'
|
||||
| 'settings';
|
||||
@@ -89,11 +94,23 @@ export interface MediaCard {
|
||||
mediaType: 'movie' | 'tv' | 'episode' | 'series' | 'mixed';
|
||||
posterUrl: string;
|
||||
backdropUrl: string;
|
||||
logoUrl?: string;
|
||||
badge?: string;
|
||||
progress?: number;
|
||||
secondaryValue?: string;
|
||||
hoverLabel?: string;
|
||||
actionLabel?: string;
|
||||
accent?: string;
|
||||
popularity?: number;
|
||||
voteAverage?: number;
|
||||
voteCount?: number;
|
||||
originalLanguage?: string;
|
||||
releaseDate?: string;
|
||||
firstAirDate?: string;
|
||||
lastAirDate?: string;
|
||||
seasonCount?: number;
|
||||
mediaStatus?: string;
|
||||
genreIds?: number[];
|
||||
}
|
||||
|
||||
export interface DownloadItem {
|
||||
@@ -126,7 +143,7 @@ export interface HubSection {
|
||||
}
|
||||
|
||||
export interface HubPageResponse {
|
||||
page: 'stream' | 'home' | 'anime' | 'dorama' | 'library';
|
||||
page: 'stream' | 'home' | 'anime' | 'dorama' | 'netflix' | 'primevideo' | 'hbo' | 'viki' | 'disney' | 'library';
|
||||
hero: MediaCard[];
|
||||
continueWatching?: MediaCard[];
|
||||
sections: HubSection[];
|
||||
|
||||
+223
-65
@@ -29,6 +29,14 @@ const sidebarItems: Array<{ route: AppRoute; label: string; short: string }> = [
|
||||
{ route: 'downloads', label: 'Downloads', short: 'D' },
|
||||
]
|
||||
|
||||
const networkSidebarItems: Array<{ route: AppRoute; label: string; icon: string; iconClassName?: string }> = [
|
||||
{ route: 'netflix', label: 'Netflix', icon: '/networks/netflix.png' },
|
||||
{ route: 'primevideo', label: 'Prime Video', icon: '/networks/prime.svg' },
|
||||
{ route: 'hbo', label: 'HBO', icon: '/networks/hbo.svg', iconClassName: 'sidebar-network-icon-image white-network-icon' },
|
||||
{ route: 'viki', label: 'Viki', icon: '/networks/viki.png' },
|
||||
{ route: 'disney', label: 'Disney+', icon: '/networks/disney.webp' },
|
||||
]
|
||||
|
||||
function App() {
|
||||
const [authState, setAuthState] = useState<AuthState>('loading')
|
||||
const [entryStage, setEntryStage] = useState<EntryStage>('profiles')
|
||||
@@ -67,15 +75,19 @@ function App() {
|
||||
setEntryStage(resolveEntryStage(settings, serviceOptions))
|
||||
}
|
||||
|
||||
async function loadHub(page: HubPageResponse['page']) {
|
||||
setHubLoading(true)
|
||||
async function loadHub(page: HubPageResponse['page'], options?: { silent?: boolean }) {
|
||||
if (!options?.silent) {
|
||||
setHubLoading(true)
|
||||
}
|
||||
try {
|
||||
const hub = await api.getHub(page)
|
||||
setHubPages((current) => ({ ...current, [page]: hub }))
|
||||
} catch (loadError) {
|
||||
setError(loadError instanceof Error ? loadError.message : 'Failed to load this section.')
|
||||
} finally {
|
||||
setHubLoading(false)
|
||||
if (!options?.silent) {
|
||||
setHubLoading(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,6 +138,7 @@ function App() {
|
||||
|
||||
const page = route as HubPageResponse['page']
|
||||
let cancelled = false
|
||||
setHubLoading(true)
|
||||
|
||||
void api.getHub(page)
|
||||
.then((hub) => {
|
||||
@@ -154,7 +167,7 @@ function App() {
|
||||
const interval = window.setInterval(() => {
|
||||
void refreshBaseData()
|
||||
if (route !== 'downloads' && route !== 'settings') {
|
||||
void loadHub(route as HubPageResponse['page'])
|
||||
void loadHub(route as HubPageResponse['page'], { silent: true })
|
||||
}
|
||||
}, 30000)
|
||||
|
||||
@@ -255,7 +268,7 @@ function App() {
|
||||
await api.requestSeries(card.tmdbId)
|
||||
}
|
||||
if (route !== 'downloads' && route !== 'settings') {
|
||||
await loadHub(route as HubPageResponse['page'])
|
||||
await loadHub(route as HubPageResponse['page'], { silent: true })
|
||||
}
|
||||
setNotice(`${card.title} sent to ${card.mediaType === 'movie' ? 'Radarr' : 'Sonarr'}.`)
|
||||
} catch (requestError) {
|
||||
@@ -351,6 +364,7 @@ function App() {
|
||||
<SettingsView
|
||||
busy={busy}
|
||||
config={config}
|
||||
connections={connections}
|
||||
jellyfinUsersBusy={jellyfinUsersBusy}
|
||||
onLoadJellyfinUsers={handleLoadJellyfinUsers}
|
||||
onSave={handleSaveSettings}
|
||||
@@ -366,6 +380,7 @@ function App() {
|
||||
onOpenCard={setSelectedCard}
|
||||
onRequest={handleRequest}
|
||||
route={route}
|
||||
tmdbToken={config.tmdb.bearerToken}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
@@ -392,6 +407,8 @@ function Sidebar(props: {
|
||||
busy: boolean
|
||||
onExitProfile: () => void
|
||||
}) {
|
||||
const hasServiceFailure = props.connections.some((connection) => !connection.ok)
|
||||
|
||||
return (
|
||||
<aside className="omniarr-sidebar">
|
||||
<div className="sidebar-wallpaper" />
|
||||
@@ -417,21 +434,29 @@ function Sidebar(props: {
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="sidebar-services">
|
||||
<p className="eyebrow">Connections</p>
|
||||
<div className="service-status-list">
|
||||
{props.connections.map((connection) => (
|
||||
<div className="service-status" key={connection.key} title={connection.message}>
|
||||
<span className={connection.ok ? 'status-dot ok' : 'status-dot down'} />
|
||||
<span>{connection.label}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="sidebar-links">
|
||||
{networkSidebarItems.map((item) => (
|
||||
<button
|
||||
className={item.route === props.route ? 'sidebar-link active' : 'sidebar-link'}
|
||||
key={item.route}
|
||||
onClick={() => startTransition(() => props.setRoute(item.route))}
|
||||
type="button"
|
||||
>
|
||||
<span className="sidebar-link-short sidebar-network-icon" aria-hidden="true">
|
||||
<img alt="" className={item.iconClassName || 'sidebar-network-icon-image'} src={item.icon} />
|
||||
</span>
|
||||
<span>{item.label}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="sidebar-footer">
|
||||
<button className="ghost-button sidebar-settings" onClick={() => startTransition(() => props.setRoute('settings'))} type="button">
|
||||
<span className="door-icon sidebar-action-icon" aria-hidden="true">S</span>
|
||||
<span
|
||||
className={hasServiceFailure ? 'status-dot down sidebar-settings-status' : 'status-dot ok sidebar-settings-status'}
|
||||
title={hasServiceFailure ? 'One or more services are failing.' : 'All services are healthy.'}
|
||||
/>
|
||||
<span>Settings</span>
|
||||
</button>
|
||||
<button className="ghost-button sidebar-exit" disabled={props.busy} onClick={props.onExitProfile} type="button">
|
||||
@@ -617,6 +642,7 @@ function HubPageView(props: {
|
||||
onRequest: (card: MediaCard) => Promise<void>
|
||||
onOpenCard: (card: MediaCard) => void
|
||||
busy: boolean
|
||||
tmdbToken: string
|
||||
}) {
|
||||
if (props.loading) {
|
||||
return <LoadingHub />
|
||||
@@ -646,7 +672,7 @@ function HubPageView(props: {
|
||||
|
||||
return (
|
||||
<div className="hub-page">
|
||||
<HeroShowcase hero={props.hub.hero} onOpenCard={props.onOpenCard} />
|
||||
<HeroShowcase hero={props.hub.hero} onOpenCard={props.onOpenCard} tmdbToken={props.tmdbToken} />
|
||||
{props.hub.continueWatching?.length ? (
|
||||
<SectionCarousel busy={props.busy} onOpenCard={props.onOpenCard} onRequest={props.onRequest} section={{ id: 'continue', title: 'Continue Watching', style: 'episode', items: props.hub.continueWatching }} />
|
||||
) : null}
|
||||
@@ -657,8 +683,79 @@ function HubPageView(props: {
|
||||
)
|
||||
}
|
||||
|
||||
function HeroShowcase({ hero, onOpenCard }: { hero: MediaCard[]; onOpenCard: (card: MediaCard) => void }) {
|
||||
const featured = hero[0]
|
||||
function HeroShowcase({ hero, onOpenCard, tmdbToken }: { hero: MediaCard[]; onOpenCard: (card: MediaCard) => void; tmdbToken: string }) {
|
||||
const [activeIndex, setActiveIndex] = useState(0)
|
||||
const [activeLogoUrl, setActiveLogoUrl] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
setActiveIndex((current) => (hero.length ? Math.min(current, hero.length - 1) : 0))
|
||||
}, [hero])
|
||||
|
||||
useEffect(() => {
|
||||
if (hero.length <= 1) {
|
||||
return
|
||||
}
|
||||
|
||||
const interval = window.setInterval(() => {
|
||||
setActiveIndex((current) => (current + 1) % hero.length)
|
||||
}, 9000)
|
||||
|
||||
return () => window.clearInterval(interval)
|
||||
}, [hero])
|
||||
|
||||
const featured = hero[activeIndex]
|
||||
|
||||
useEffect(() => {
|
||||
if (!featured?.tmdbId) {
|
||||
setActiveLogoUrl('')
|
||||
return
|
||||
}
|
||||
|
||||
const mediaType = featured.mediaType === 'movie' ? 'movie' : featured.mediaType === 'tv' || featured.mediaType === 'series' ? 'tv' : ''
|
||||
if (!mediaType) {
|
||||
setActiveLogoUrl('')
|
||||
return
|
||||
}
|
||||
|
||||
let cancelled = false
|
||||
|
||||
const fetchImages = tmdbToken
|
||||
? fetch(`https://api.themoviedb.org/3/${mediaType}/${featured.tmdbId}/images`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${tmdbToken}`,
|
||||
},
|
||||
})
|
||||
: fetch(`/api/tmdb/logo?tmdbId=${featured.tmdbId}&mediaType=${mediaType}`)
|
||||
|
||||
void fetchImages
|
||||
.then(async (response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to load logo')
|
||||
}
|
||||
|
||||
return response.json() as Promise<{
|
||||
logos?: Array<{ file_path: string; iso_639_1: string | null }>
|
||||
backdrops?: Array<{ file_path: string; iso_639_1: string | null }>
|
||||
}>
|
||||
})
|
||||
.then((payload) => {
|
||||
if (!cancelled) {
|
||||
const englishLogos = payload.logos?.filter((logo) => logo.iso_639_1 === 'en') || []
|
||||
const logo = englishLogos[0] || payload.logos?.[0]
|
||||
setActiveLogoUrl(logo?.file_path ? `https://image.tmdb.org/t/p/original${logo.file_path}` : '')
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
if (!cancelled) {
|
||||
setActiveLogoUrl('')
|
||||
}
|
||||
})
|
||||
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [featured])
|
||||
|
||||
if (!featured) {
|
||||
return (
|
||||
<div className="hero-showcase empty">
|
||||
@@ -671,30 +768,63 @@ function HeroShowcase({ hero, onOpenCard }: { hero: MediaCard[]; onOpenCard: (ca
|
||||
)
|
||||
}
|
||||
|
||||
const heroImage = featured.backdropUrl || featured.posterUrl
|
||||
|
||||
return (
|
||||
<section
|
||||
className="hero-showcase"
|
||||
onClick={() => onOpenCard(featured)}
|
||||
style={heroImage
|
||||
? { backgroundImage: `linear-gradient(180deg, rgba(6,10,18,0.12), rgba(7,7,10,0.94)), url("${heroImage}")` }
|
||||
: undefined}
|
||||
>
|
||||
<div className="hero-copy">
|
||||
<span className="badge">{featured.badge || 'Featured'}</span>
|
||||
<h1>{featured.title}</h1>
|
||||
<p>{featured.overview || featured.subtitle}</p>
|
||||
<div className="hero-media" aria-hidden="true">
|
||||
{hero.slice(0, 6).map((card, index) => (
|
||||
<div
|
||||
className={index === activeIndex ? 'hero-slide active' : 'hero-slide'}
|
||||
key={card.id}
|
||||
style={resolveHeroSlideStyle(card)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="hero-overlay" />
|
||||
<div className="hero-content">
|
||||
<div className="hero-copy">
|
||||
{activeLogoUrl ? (
|
||||
<img
|
||||
alt={featured.title}
|
||||
className="hero-logo-image"
|
||||
loading="eager"
|
||||
src={activeLogoUrl}
|
||||
/>
|
||||
) : (
|
||||
<div className="hero-logo" aria-label={featured.title}>{featured.title}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="hero-dots">
|
||||
{hero.slice(0, 6).map((card) => (
|
||||
<div className="hero-dot" key={card.id} title={card.title} />
|
||||
{hero.slice(0, 6).map((card, index) => (
|
||||
<button
|
||||
aria-label={`Show ${card.title}`}
|
||||
className={index === activeIndex ? 'hero-dot active' : 'hero-dot'}
|
||||
key={card.id}
|
||||
onClick={(event) => {
|
||||
event.stopPropagation()
|
||||
setActiveIndex(index)
|
||||
}}
|
||||
type="button"
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
function resolveHeroSlideStyle(card: MediaCard | undefined) {
|
||||
if (!card) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const slideImage = card.backdropUrl || card.posterUrl
|
||||
|
||||
return slideImage ? { backgroundImage: `url("${slideImage}")` } : undefined
|
||||
}
|
||||
|
||||
function SectionCarousel(props: {
|
||||
section: HubPageResponse['sections'][number]
|
||||
onRequest: (card: MediaCard) => Promise<void>
|
||||
@@ -704,12 +834,14 @@ function SectionCarousel(props: {
|
||||
if (!props.section.items.length) return null
|
||||
|
||||
return (
|
||||
<section className="carousel-section">
|
||||
<section className={props.section.id === 'continue' ? 'carousel-row continue-row' : 'carousel-row'}>
|
||||
<div className="carousel-title">{props.section.title}</div>
|
||||
<div className="carousel-track">
|
||||
{props.section.items.map((card) => (
|
||||
<MediaTile busy={props.busy} card={card} key={card.id} onOpenCard={props.onOpenCard} onRequest={props.onRequest} styleType={props.section.style || 'landscape'} />
|
||||
))}
|
||||
<div className="carousel-section">
|
||||
<div className="carousel-track">
|
||||
{props.section.items.map((card) => (
|
||||
<MediaTile busy={props.busy} card={card} isContinueWatching={props.section.id === 'continue'} key={card.id} onOpenCard={props.onOpenCard} onRequest={props.onRequest} styleType={props.section.style || 'landscape'} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
@@ -717,32 +849,23 @@ function SectionCarousel(props: {
|
||||
|
||||
function MediaTile(props: {
|
||||
card: MediaCard
|
||||
isContinueWatching: boolean
|
||||
styleType: 'poster' | 'landscape' | 'profile' | 'genre' | 'network' | 'episode'
|
||||
busy: boolean
|
||||
onOpenCard: (card: MediaCard) => void
|
||||
onRequest: (card: MediaCard) => Promise<void>
|
||||
}) {
|
||||
const actionable = Boolean(props.card.tmdbId && (props.card.mediaType === 'movie' || props.card.mediaType === 'tv'))
|
||||
const imageUrl = props.styleType === 'landscape' || props.styleType === 'episode'
|
||||
? (props.card.backdropUrl || props.card.posterUrl)
|
||||
: props.card.posterUrl
|
||||
|
||||
return (
|
||||
<article className={`media-tile ${props.styleType}`}>
|
||||
<article className={props.isContinueWatching ? `media-tile ${props.styleType} continue-tile` : `media-tile ${props.styleType}`}>
|
||||
<button className="media-art media-trigger" onClick={() => props.onOpenCard(props.card)} type="button">
|
||||
{imageUrl ? <img alt={props.card.title} loading="lazy" src={imageUrl} /> : <div className="media-fallback" />}
|
||||
{props.card.hoverLabel ? <div className="media-hover-label">{props.card.hoverLabel}</div> : null}
|
||||
{props.card.progress ? <div className="tile-progress"><div className="tile-progress-fill" style={{ width: `${props.card.progress}%` }} /></div> : null}
|
||||
</button>
|
||||
<div className="tile-copy">
|
||||
<span className="badge subtle">{props.card.badge || props.card.subtitle}</span>
|
||||
<h3>{props.card.title}</h3>
|
||||
<p>{props.card.secondaryValue || props.card.subtitle}</p>
|
||||
{actionable ? (
|
||||
<button className="tile-action" disabled={props.busy} onClick={() => void props.onRequest(props.card)} type="button">
|
||||
{props.card.actionLabel || 'Add'}
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
</article>
|
||||
)
|
||||
}
|
||||
@@ -754,36 +877,56 @@ function TitleDetailsModal(props: {
|
||||
onRequest: (card: MediaCard) => Promise<void>
|
||||
}) {
|
||||
const canPlay = Boolean(props.card.providerId && props.config.jellyfin.baseUrl)
|
||||
const canRequest = Boolean(props.card.tmdbId && (props.card.mediaType === 'movie' || props.card.mediaType === 'tv'))
|
||||
const canRequest = Boolean(props.card.tmdbId && props.card.mediaType !== 'mixed')
|
||||
const backgroundImage = props.card.backdropUrl || props.card.posterUrl
|
||||
|
||||
function handlePlay() {
|
||||
if (!props.card.providerId || !props.config.jellyfin.baseUrl) return
|
||||
const baseUrl = props.config.jellyfin.baseUrl.replace(/\/$/, '')
|
||||
window.open(`${baseUrl}/web/#/details?id=${encodeURIComponent(props.card.providerId)}`, '_blank', 'noopener,noreferrer')
|
||||
}
|
||||
const [playerOpen, setPlayerOpen] = useState(false)
|
||||
const requestLabel = props.card.mediaType === 'movie' ? 'Add to Radarr' : 'Add to Sonarr'
|
||||
const jellyfinPlayerUrl = canPlay
|
||||
? `${props.config.jellyfin.baseUrl.replace(/\/$/, '')}/web/#/details?id=${encodeURIComponent(props.card.providerId!)}&autoplay=true`
|
||||
: ''
|
||||
|
||||
return (
|
||||
<div className="modal-backdrop" onClick={props.onClose} role="presentation">
|
||||
<div className="title-modal" onClick={(event) => event.stopPropagation()} role="dialog" aria-modal="true">
|
||||
<button className="modal-close" onClick={props.onClose} type="button">Close</button>
|
||||
<div className="title-modal-hero" style={backgroundImage ? { backgroundImage: `linear-gradient(180deg, rgba(5, 8, 14, 0.18), rgba(5, 8, 14, 0.96)), url("${backgroundImage}")` } : undefined}>
|
||||
<div className="title-modal-grid">
|
||||
<div className="title-modal-poster">
|
||||
<div className="title-modal omniarr-title-modal" onClick={(event) => event.stopPropagation()} role="dialog" aria-modal="true">
|
||||
<div className="title-modal-hero omniarr-title-hero" style={backgroundImage ? { backgroundImage: `url("${backgroundImage}")` } : undefined}>
|
||||
<div className="omniarr-title-darken" />
|
||||
<button className="modal-close omniarr-modal-close" onClick={props.onClose} type="button">Close</button>
|
||||
<div className="omniarr-title-top">
|
||||
<div className="omniarr-title-shell">
|
||||
<div className="omniarr-title-copy">
|
||||
<span className="badge">{props.card.badge || props.card.subtitle || 'Title'}</span>
|
||||
<h2>{props.card.title}</h2>
|
||||
{props.card.subtitle ? <p className="omniarr-title-subtitle">{props.card.subtitle}</p> : null}
|
||||
<div className="title-modal-actions omniarr-title-actions">
|
||||
{canPlay ? <button className="primary-button" onClick={() => setPlayerOpen(true)} type="button">Play</button> : null}
|
||||
{canRequest ? <button className="secondary-button" onClick={() => void props.onRequest(props.card)} type="button">{props.card.actionLabel || requestLabel}</button> : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{playerOpen && jellyfinPlayerUrl ? (
|
||||
<div className="omniarr-player-shell">
|
||||
<iframe
|
||||
allow="autoplay; fullscreen; encrypted-media"
|
||||
className="omniarr-player-frame"
|
||||
src={jellyfinPlayerUrl}
|
||||
title={`${props.card.title} player`}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="omniarr-title-bottom">
|
||||
<div className="omniarr-title-shell omniarr-title-bottom-grid">
|
||||
<div className="title-modal-poster omniarr-title-poster">
|
||||
{props.card.posterUrl ? <img alt={props.card.title} src={props.card.posterUrl} /> : <div className="media-fallback" />}
|
||||
</div>
|
||||
<div className="title-modal-copy">
|
||||
<span className="badge">{props.card.badge || props.card.subtitle || 'Title'}</span>
|
||||
<h2>{props.card.title}</h2>
|
||||
<div className="omniarr-title-info">
|
||||
<h3>{props.card.title}</h3>
|
||||
<p>{props.card.overview || props.card.subtitle || 'No details available yet.'}</p>
|
||||
<div className="title-modal-meta">
|
||||
{props.card.subtitle ? <span>{props.card.subtitle}</span> : null}
|
||||
{props.card.secondaryValue ? <span>{props.card.secondaryValue}</span> : null}
|
||||
</div>
|
||||
<div className="title-modal-actions">
|
||||
{canPlay ? <button className="primary-button" onClick={handlePlay} type="button">Play</button> : null}
|
||||
{canRequest ? <button className="secondary-button" onClick={() => void props.onRequest(props.card)} type="button">{props.card.actionLabel || 'Add'}</button> : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -824,6 +967,7 @@ function DownloadRow({ item }: { item: DownloadItem }) {
|
||||
|
||||
function SettingsView(props: {
|
||||
config: AppConfig
|
||||
connections: ConnectionStatus[]
|
||||
jellyfinUsersBusy: boolean
|
||||
onLoadJellyfinUsers: () => Promise<void>
|
||||
updateConfig: <K extends keyof AppConfig>(key: K, value: AppConfig[K]) => void
|
||||
@@ -833,6 +977,20 @@ function SettingsView(props: {
|
||||
}) {
|
||||
return (
|
||||
<form className="settings-page" onSubmit={(event) => void props.onSave(event)}>
|
||||
<SettingsCard title="Connections">
|
||||
<div className="settings-connections">
|
||||
{props.connections.map((connection) => (
|
||||
<div className="settings-connection" key={connection.key} title={connection.message}>
|
||||
<div className="settings-connection-label">
|
||||
<span className={connection.ok ? 'status-dot ok' : 'status-dot down'} />
|
||||
<strong>{connection.label}</strong>
|
||||
</div>
|
||||
<span className="settings-connection-message">{connection.message}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</SettingsCard>
|
||||
|
||||
<SettingsCard title="Preferences">
|
||||
<label>
|
||||
Language
|
||||
|
||||
+460
-89
@@ -22,9 +22,7 @@ body,
|
||||
}
|
||||
|
||||
body {
|
||||
background:
|
||||
radial-gradient(circle at top left, rgba(255, 188, 102, 0.18), transparent 22%),
|
||||
linear-gradient(180deg, #070709 0%, #111216 100%);
|
||||
background: #000;
|
||||
}
|
||||
|
||||
button,
|
||||
@@ -62,6 +60,7 @@ img {
|
||||
padding: 1rem;
|
||||
overflow: hidden;
|
||||
transition: width 220ms ease;
|
||||
filter: drop-shadow(0 1.4rem 2rem rgba(0, 0, 0, 0.34));
|
||||
}
|
||||
|
||||
.omniarr-sidebar:hover {
|
||||
@@ -73,7 +72,9 @@ img {
|
||||
inset: 1rem;
|
||||
border-radius: 1rem;
|
||||
background: linear-gradient(180deg, rgba(10, 12, 16, 0.96), rgba(16, 18, 24, 0.96));
|
||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04);
|
||||
box-shadow:
|
||||
0 1.2rem 2.6rem rgba(0, 0, 0, 0.34),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
|
||||
.sidebar-inner {
|
||||
@@ -83,7 +84,7 @@ img {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
padding: 0.75rem;
|
||||
padding: 0.425rem;
|
||||
border: 1px solid rgba(255, 255, 255, 0.07);
|
||||
border-radius: 1rem;
|
||||
backdrop-filter: blur(12px);
|
||||
@@ -99,13 +100,11 @@ img {
|
||||
width: 2.9rem;
|
||||
min-width: 2.9rem;
|
||||
max-width: 2.9rem;
|
||||
justify-self: center;
|
||||
align-self: center;
|
||||
border: 0;
|
||||
color: #f5f7fb;
|
||||
background: transparent;
|
||||
text-align: left;
|
||||
padding: 0.55rem;
|
||||
padding: 0.525rem;
|
||||
border-radius: 0.8rem;
|
||||
transition: background-color 180ms ease, transform 180ms ease, box-shadow 180ms ease;
|
||||
}
|
||||
@@ -133,10 +132,17 @@ img {
|
||||
box-shadow: -3px 6px 13px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.sidebar-services .eyebrow {
|
||||
white-space: nowrap;
|
||||
opacity: 0;
|
||||
max-width: 0;
|
||||
overflow: hidden;
|
||||
transform: translateX(-0.2rem);
|
||||
transition: opacity 180ms ease, transform 180ms ease, max-width 180ms ease;
|
||||
}
|
||||
|
||||
.sidebar-profile-copy,
|
||||
.sidebar-link > span:last-child,
|
||||
.sidebar-services .eyebrow,
|
||||
.service-status span,
|
||||
.sidebar-settings > span:last-child,
|
||||
.sidebar-exit > span:last-child {
|
||||
white-space: nowrap;
|
||||
@@ -149,8 +155,6 @@ img {
|
||||
|
||||
.omniarr-sidebar:hover .sidebar-profile-copy,
|
||||
.omniarr-sidebar:hover .sidebar-link > span:last-child,
|
||||
.omniarr-sidebar:hover .sidebar-services .eyebrow,
|
||||
.omniarr-sidebar:hover .service-status span,
|
||||
.omniarr-sidebar:hover .sidebar-settings > span:last-child,
|
||||
.omniarr-sidebar:hover .sidebar-exit > span:last-child {
|
||||
opacity: 1;
|
||||
@@ -161,8 +165,7 @@ img {
|
||||
.omniarr-sidebar:hover .sidebar-profile,
|
||||
.omniarr-sidebar:hover .sidebar-link,
|
||||
.omniarr-sidebar:hover .sidebar-settings,
|
||||
.omniarr-sidebar:hover .sidebar-exit,
|
||||
.omniarr-sidebar:hover .service-status {
|
||||
.omniarr-sidebar:hover .sidebar-exit {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
@@ -173,11 +176,19 @@ img {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
max-width: none;
|
||||
justify-self: stretch;
|
||||
align-self: stretch;
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.omniarr-sidebar:not(:hover) .sidebar-profile,
|
||||
.omniarr-sidebar:not(:hover) .sidebar-link,
|
||||
.omniarr-sidebar:not(:hover) .sidebar-settings,
|
||||
.omniarr-sidebar:not(:hover) .sidebar-exit {
|
||||
gap: 0;
|
||||
justify-content: center;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
.sidebar-profile {
|
||||
min-height: 4.25rem;
|
||||
width: 2.9rem;
|
||||
@@ -213,41 +224,14 @@ img {
|
||||
margin-top: 0.75rem;
|
||||
}
|
||||
|
||||
.sidebar-links + .sidebar-links {
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
.sidebar-link {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.sidebar-services {
|
||||
display: grid;
|
||||
gap: 0.5rem;
|
||||
margin-top: 0.75rem;
|
||||
padding: 0 0.45rem;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.service-status-list {
|
||||
display: grid;
|
||||
gap: 0.45rem;
|
||||
}
|
||||
|
||||
.service-status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.7rem;
|
||||
width: 2.4rem;
|
||||
align-self: center;
|
||||
min-height: 1.9rem;
|
||||
color: #dfe7f6;
|
||||
font-size: 0.92rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.omniarr-sidebar:hover .service-status {
|
||||
width: 100%;
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 0.75rem;
|
||||
height: 0.75rem;
|
||||
@@ -282,7 +266,7 @@ img {
|
||||
background: linear-gradient(135deg, #ffd166, #ff9f5a);
|
||||
color: #13151a;
|
||||
font-weight: 800;
|
||||
flex-shrink: 0;
|
||||
flex: 0 0 1.85rem;
|
||||
}
|
||||
|
||||
.sidebar-settings,
|
||||
@@ -297,11 +281,9 @@ img {
|
||||
width: 2.9rem;
|
||||
min-width: 2.9rem;
|
||||
max-width: 2.9rem;
|
||||
justify-self: center;
|
||||
align-self: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
padding: 0.55rem;
|
||||
padding: 0.525rem;
|
||||
}
|
||||
|
||||
.omniarr-sidebar:hover .sidebar-settings,
|
||||
@@ -309,6 +291,13 @@ img {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.sidebar-settings-status {
|
||||
width: 0.6rem;
|
||||
height: 0.6rem;
|
||||
margin-left: -0.4rem;
|
||||
margin-right: 0.05rem;
|
||||
}
|
||||
|
||||
.door-icon {
|
||||
display: inline-grid;
|
||||
place-items: center;
|
||||
@@ -334,6 +323,22 @@ img {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.sidebar-network-icon {
|
||||
overflow: hidden;
|
||||
padding: 0.2rem;
|
||||
}
|
||||
|
||||
.sidebar-network-icon-image {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.white-network-icon {
|
||||
filter: brightness(0) invert(1);
|
||||
}
|
||||
|
||||
.omniarr-sidebar:not(:hover) .sidebar-link,
|
||||
.omniarr-sidebar:not(:hover) .sidebar-settings,
|
||||
.omniarr-sidebar:not(:hover) .sidebar-exit {
|
||||
@@ -348,15 +353,13 @@ img {
|
||||
|
||||
.content-shell {
|
||||
height: 100%;
|
||||
width: calc(100% - 5.75rem);
|
||||
min-width: 0;
|
||||
padding: 1rem;
|
||||
padding: calc(1rem - 1px) 0 1rem;
|
||||
margin-left: 5.75rem;
|
||||
transition: margin-left 220ms ease, width 220ms ease;
|
||||
transition: margin-left 220ms ease;
|
||||
}
|
||||
|
||||
.omniarr-app:has(.omniarr-sidebar:hover) .content-shell {
|
||||
width: calc(100% - 15rem);
|
||||
margin-left: 15rem;
|
||||
}
|
||||
|
||||
@@ -365,17 +368,17 @@ img {
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
min-width: 0;
|
||||
border-radius: 1.4rem;
|
||||
border-radius: 0;
|
||||
background: #000;
|
||||
overflow: hidden;
|
||||
box-shadow: -10px 11px 13px rgba(0, 0, 0, 0.37);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.content-scroll {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
padding: 1.5rem 0 2.5rem;
|
||||
padding: 1px 0 2.5rem;
|
||||
}
|
||||
|
||||
.content-status {
|
||||
@@ -420,20 +423,60 @@ img {
|
||||
display: grid;
|
||||
gap: 2.25rem;
|
||||
min-width: 0;
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
.hero-showcase {
|
||||
min-height: 72vh;
|
||||
margin: 0 1rem;
|
||||
border-radius: 0 0 1.25rem 1.25rem;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
aspect-ratio: 16 / 9;
|
||||
min-height: 0;
|
||||
max-height: 42rem;
|
||||
margin: 0;
|
||||
border-radius: 1.5rem;
|
||||
position: relative;
|
||||
display: grid;
|
||||
align-content: end;
|
||||
gap: 2rem;
|
||||
padding: 0 1.25rem 1.5rem;
|
||||
overflow: hidden;
|
||||
cursor: pointer;
|
||||
box-shadow:
|
||||
0 1.8rem 3.6rem rgba(0, 0, 0, 0.42),
|
||||
0 0 0 1px rgba(255, 255, 255, 0.12),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.08);
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.hero-media,
|
||||
.hero-slide {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
}
|
||||
|
||||
.hero-media {
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.hero-slide {
|
||||
background-size: cover;
|
||||
background-position: center 28%;
|
||||
opacity: 0;
|
||||
transform: scale(1.02);
|
||||
transition: opacity 900ms ease, transform 9000ms ease;
|
||||
}
|
||||
|
||||
.hero-slide.active {
|
||||
opacity: 1;
|
||||
transform: scale(1.085);
|
||||
}
|
||||
|
||||
@media (min-width: 981px) {
|
||||
.hero-showcase {
|
||||
width: calc(100% - 2.5rem);
|
||||
margin-inline: 1.25rem;
|
||||
aspect-ratio: auto;
|
||||
min-height: min(42rem, 56vw);
|
||||
max-height: none;
|
||||
}
|
||||
}
|
||||
|
||||
.hero-showcase.empty {
|
||||
@@ -441,10 +484,67 @@ img {
|
||||
background: rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
|
||||
.hero-overlay {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background:
|
||||
linear-gradient(90deg, rgba(7, 9, 14, 0.52) 0%, rgba(7, 9, 14, 0.2) 44%, rgba(7, 9, 14, 0.03) 100%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.hero-content {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: grid;
|
||||
align-items: end;
|
||||
gap: 1.5rem;
|
||||
animation: hero-copy-in 420ms ease;
|
||||
}
|
||||
|
||||
.hero-copy {
|
||||
position: relative;
|
||||
max-width: 48rem;
|
||||
}
|
||||
|
||||
.hero-copy::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: -1.1rem;
|
||||
top: -0.5rem;
|
||||
width: min(42rem, 92vw);
|
||||
height: 15rem;
|
||||
background: radial-gradient(ellipse at left center, rgba(0, 0, 0, 0.42) 0%, rgba(0, 0, 0, 0.22) 38%, rgba(0, 0, 0, 0) 78%);
|
||||
pointer-events: none;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.hero-logo {
|
||||
margin: 0.5rem 0 0.95rem;
|
||||
font-family: "Space Grotesk", "Segoe UI", sans-serif;
|
||||
font-size: clamp(3rem, 9vw, 7.2rem);
|
||||
line-height: 0.9;
|
||||
letter-spacing: -0.08em;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
text-wrap: balance;
|
||||
text-shadow: 0 0.2rem 1.2rem rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
.hero-logo-image {
|
||||
display: block;
|
||||
position: relative;
|
||||
width: auto;
|
||||
max-width: min(40rem, 88vw);
|
||||
max-height: 13rem;
|
||||
margin: 0.75rem 0 1rem;
|
||||
object-fit: contain;
|
||||
object-position: left center;
|
||||
filter:
|
||||
drop-shadow(0 0.25rem 0.4rem rgba(0, 0, 0, 0.5))
|
||||
drop-shadow(0 0.9rem 1.8rem rgba(0, 0, 0, 0.42));
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.hero-copy h1 {
|
||||
margin: 0.35rem 0 0.8rem;
|
||||
font-family: "Space Grotesk", "Segoe UI", sans-serif;
|
||||
@@ -460,6 +560,8 @@ img {
|
||||
}
|
||||
|
||||
.hero-dots {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
@@ -467,20 +569,52 @@ img {
|
||||
.hero-dot {
|
||||
width: 0.65rem;
|
||||
height: 0.65rem;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.45);
|
||||
}
|
||||
|
||||
.carousel-section {
|
||||
.hero-dot.active {
|
||||
width: 1.8rem;
|
||||
background: #ffd166;
|
||||
}
|
||||
|
||||
@keyframes hero-copy-in {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateY(0.65rem);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.carousel-row {
|
||||
display: grid;
|
||||
gap: 0.9rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.carousel-section {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
margin: 0 1.25rem;
|
||||
padding: 1rem 0 1.15rem;
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
border-radius: 1.9rem;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.035), rgba(255, 255, 255, 0.015));
|
||||
box-shadow:
|
||||
0 1.4rem 2.8rem rgba(0, 0, 0, 0.22),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.03);
|
||||
}
|
||||
|
||||
.carousel-title {
|
||||
padding: 0 1.5rem;
|
||||
padding: 0 1.25rem;
|
||||
color: #d6d8de;
|
||||
font-size: 1.1rem;
|
||||
font-size: 1.35rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
@@ -489,7 +623,7 @@ img {
|
||||
gap: 1rem;
|
||||
overflow-x: auto;
|
||||
overflow-y: visible;
|
||||
padding: 0.2rem 1.25rem 0.2rem;
|
||||
padding: 0.45rem 1.25rem 0.7rem;
|
||||
scrollbar-width: none;
|
||||
}
|
||||
|
||||
@@ -497,51 +631,142 @@ img {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.hero-showcase {
|
||||
aspect-ratio: 16 / 10;
|
||||
min-height: 0;
|
||||
max-height: none;
|
||||
border-radius: 0 0 1.25rem 1.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
.media-tile {
|
||||
flex: 0 0 auto;
|
||||
display: grid;
|
||||
gap: 0.75rem;
|
||||
display: block;
|
||||
color: #edf1fb;
|
||||
min-width: 0;
|
||||
overflow: visible;
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
padding: 0;
|
||||
border-radius: 1.3rem;
|
||||
background: transparent;
|
||||
transition: transform 220ms ease, box-shadow 220ms ease;
|
||||
}
|
||||
|
||||
.media-tile::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
border-radius: inherit;
|
||||
pointer-events: none;
|
||||
transition: border-color 220ms ease, box-shadow 220ms ease;
|
||||
}
|
||||
|
||||
.media-tile.landscape,
|
||||
.media-tile.episode {
|
||||
width: min(21rem, 72vw);
|
||||
width: min(24rem, 78vw);
|
||||
}
|
||||
|
||||
.media-tile.poster {
|
||||
width: 12.5rem;
|
||||
width: 14rem;
|
||||
}
|
||||
|
||||
.media-tile.profile,
|
||||
.media-tile.genre,
|
||||
.media-tile.network {
|
||||
width: 11rem;
|
||||
width: 12rem;
|
||||
}
|
||||
|
||||
.media-art {
|
||||
display: block;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
background: rgba(255, 255, 255, 0.08);
|
||||
box-shadow: 0 16px 36px rgba(0, 0, 0, 0.28);
|
||||
box-shadow:
|
||||
0 1rem 2.2rem rgba(0, 0, 0, 0.24),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.media-hover-label {
|
||||
position: absolute;
|
||||
left: 0.85rem;
|
||||
right: 0.85rem;
|
||||
bottom: 1.5rem;
|
||||
z-index: 2;
|
||||
padding: 0;
|
||||
color: #f2f6ff;
|
||||
font-size: 0.95rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.015em;
|
||||
line-height: 1.15;
|
||||
text-shadow:
|
||||
0 0.18rem 0.8rem rgba(0, 0, 0, 0.78),
|
||||
0 0.04rem 0.12rem rgba(0, 0, 0, 0.7);
|
||||
opacity: 0;
|
||||
transform: translateY(0.45rem);
|
||||
transition: opacity 180ms ease, transform 180ms ease;
|
||||
pointer-events: none;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.media-trigger {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
}
|
||||
|
||||
.media-tile:hover,
|
||||
.media-tile:focus-within {
|
||||
z-index: 4;
|
||||
transform: translateY(-0.22rem) scale(1.03);
|
||||
}
|
||||
|
||||
.media-tile:hover::after,
|
||||
.media-tile:focus-within::after {
|
||||
border-color: rgba(255, 255, 255, 0.2);
|
||||
box-shadow: 0 0 0 1px rgba(255, 255, 255, 0.04);
|
||||
}
|
||||
|
||||
.media-tile:hover .media-art,
|
||||
.media-tile:focus-within .media-art {
|
||||
box-shadow:
|
||||
0 1.45rem 3rem rgba(0, 0, 0, 0.28),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.continue-tile .media-art::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: linear-gradient(180deg, rgba(4, 6, 10, 0) 45%, rgba(4, 6, 10, 0.78) 100%);
|
||||
opacity: 0;
|
||||
transition: opacity 180ms ease;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.continue-tile:hover .media-art::after,
|
||||
.continue-tile:focus-within .media-art::after {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.media-trigger:hover .media-hover-label,
|
||||
.media-trigger:focus-visible .media-hover-label {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
.landscape .media-art,
|
||||
.episode .media-art {
|
||||
aspect-ratio: 16 / 9;
|
||||
border-radius: 1rem;
|
||||
border-radius: 1.3rem;
|
||||
}
|
||||
|
||||
.poster .media-art {
|
||||
aspect-ratio: 2 / 3;
|
||||
border-radius: 1rem;
|
||||
border-radius: 1.3rem;
|
||||
}
|
||||
|
||||
.profile .media-art {
|
||||
@@ -552,7 +777,7 @@ img {
|
||||
.genre .media-art,
|
||||
.network .media-art {
|
||||
aspect-ratio: 21 / 9;
|
||||
border-radius: 1rem;
|
||||
border-radius: 1.3rem;
|
||||
}
|
||||
|
||||
.media-art img {
|
||||
@@ -567,17 +792,6 @@ img {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.tile-copy h3 {
|
||||
margin: 0.15rem 0 0.3rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.tile-copy p {
|
||||
margin: 0;
|
||||
color: #9ca4b8;
|
||||
font-size: 0.92rem;
|
||||
}
|
||||
|
||||
.badge {
|
||||
display: inline-flex;
|
||||
width: fit-content;
|
||||
@@ -646,11 +860,11 @@ img {
|
||||
.omniarr-sidebar:not(:hover) .sidebar-link.active::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 50% auto auto calc(50% - 0.425rem);
|
||||
inset: 50% auto auto 50%;
|
||||
width: 2.9rem;
|
||||
height: 2.9rem;
|
||||
transform: translate(-50%, -50%);
|
||||
border-radius: 999px;
|
||||
border-radius: 0.8rem;
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
box-shadow: -2px 4px 10px rgba(0, 0, 0, 0.18);
|
||||
z-index: 0;
|
||||
@@ -674,6 +888,7 @@ img {
|
||||
left: 0.7rem;
|
||||
right: 0.7rem;
|
||||
bottom: 0.7rem;
|
||||
z-index: 3;
|
||||
height: 0.35rem;
|
||||
border-radius: 999px;
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
@@ -706,6 +921,13 @@ img {
|
||||
box-shadow: 0 30px 80px rgba(0, 0, 0, 0.45);
|
||||
}
|
||||
|
||||
.omniarr-title-modal {
|
||||
width: min(88rem, 100%);
|
||||
max-height: calc(100vh - 4rem);
|
||||
border-radius: 1.75rem;
|
||||
background: #090b10;
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
position: absolute;
|
||||
top: 1.25rem;
|
||||
@@ -718,12 +940,119 @@ img {
|
||||
color: #eef4ff;
|
||||
}
|
||||
|
||||
.omniarr-modal-close {
|
||||
top: 1.5rem;
|
||||
right: 1.5rem;
|
||||
}
|
||||
|
||||
.title-modal-hero {
|
||||
position: relative;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
.omniarr-title-hero {
|
||||
min-height: min(70vh, 44rem);
|
||||
background-position: center 24%;
|
||||
}
|
||||
|
||||
.omniarr-title-darken {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background:
|
||||
linear-gradient(180deg, rgba(5, 8, 14, 0.18) 0%, rgba(5, 8, 14, 0.58) 58%, #090b10 100%);
|
||||
}
|
||||
|
||||
.omniarr-title-top {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
min-height: inherit;
|
||||
display: flex;
|
||||
align-items: end;
|
||||
padding: 5.5rem 1.5rem 2rem;
|
||||
}
|
||||
|
||||
.omniarr-title-shell {
|
||||
width: min(100%, 84rem);
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.omniarr-title-copy {
|
||||
max-width: 44rem;
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.omniarr-title-copy h2 {
|
||||
margin: 0;
|
||||
font-family: "Space Grotesk", "Segoe UI", sans-serif;
|
||||
font-size: clamp(2.5rem, 7vw, 5.6rem);
|
||||
line-height: 0.92;
|
||||
letter-spacing: -0.06em;
|
||||
}
|
||||
|
||||
.omniarr-title-subtitle {
|
||||
margin: 0;
|
||||
color: #c7cede;
|
||||
font-size: 1rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.14em;
|
||||
}
|
||||
|
||||
.omniarr-title-actions {
|
||||
margin-top: 0.4rem;
|
||||
}
|
||||
|
||||
.omniarr-player-shell {
|
||||
padding: 0 1.5rem 1.5rem;
|
||||
background: #090b10;
|
||||
}
|
||||
|
||||
.omniarr-player-frame {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: min(70vh, 46rem);
|
||||
border: 0;
|
||||
border-radius: 1.35rem;
|
||||
background: #000;
|
||||
box-shadow: 0 1.6rem 3rem rgba(0, 0, 0, 0.32);
|
||||
}
|
||||
|
||||
.omniarr-title-bottom {
|
||||
padding: 0 1.5rem 2rem;
|
||||
background: #090b10;
|
||||
}
|
||||
|
||||
.omniarr-title-bottom-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 15rem minmax(0, 1fr);
|
||||
gap: 2rem;
|
||||
align-items: start;
|
||||
}
|
||||
|
||||
.omniarr-title-poster {
|
||||
align-self: start;
|
||||
}
|
||||
|
||||
.omniarr-title-info {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
padding-top: 0.5rem;
|
||||
}
|
||||
|
||||
.omniarr-title-info h3 {
|
||||
margin: 0;
|
||||
font-size: clamp(1.5rem, 3vw, 2.25rem);
|
||||
letter-spacing: -0.04em;
|
||||
}
|
||||
|
||||
.omniarr-title-info p {
|
||||
margin: 0;
|
||||
max-width: 48rem;
|
||||
color: #d0d7e6;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.title-modal-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 16rem minmax(0, 1fr);
|
||||
@@ -849,6 +1178,33 @@ img {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.settings-connections {
|
||||
display: grid;
|
||||
gap: 0.85rem;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.settings-connection {
|
||||
display: grid;
|
||||
gap: 0.28rem;
|
||||
padding: 0.85rem 0.95rem;
|
||||
border: 1px solid rgba(255, 255, 255, 0.06);
|
||||
border-radius: 1rem;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
}
|
||||
|
||||
.settings-connection-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.65rem;
|
||||
color: #f4f7ff;
|
||||
}
|
||||
|
||||
.settings-connection-message {
|
||||
color: #98a8c4;
|
||||
font-size: 0.92rem;
|
||||
}
|
||||
|
||||
.settings-card label,
|
||||
.auth-form label {
|
||||
display: grid;
|
||||
@@ -1071,6 +1427,21 @@ img {
|
||||
min-height: auto;
|
||||
}
|
||||
|
||||
.omniarr-title-hero {
|
||||
min-height: 28rem;
|
||||
}
|
||||
|
||||
.omniarr-title-top,
|
||||
.omniarr-player-shell,
|
||||
.omniarr-title-bottom {
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
|
||||
.omniarr-title-bottom-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.title-modal-poster {
|
||||
max-width: 16rem;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user