Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.3 KiB |
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 3.3 KiB |
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 5.1 KiB |
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 4.5 KiB |
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 1.7 KiB |
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.2 KiB |
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.0 KiB |
Before Width: | Height: | Size: 840 B After Width: | Height: | Size: 644 B |
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 27 KiB |
Before Width: | Height: | Size: 92 KiB After Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 95 KiB After Width: | Height: | Size: 29 KiB |
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 97 KiB After Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 99 KiB After Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 132 KiB After Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 134 KiB After Width: | Height: | Size: 40 KiB |
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 7.6 KiB |
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 7.9 KiB |
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 27 KiB |
Before Width: | Height: | Size: 88 KiB After Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 31 KiB |
Before Width: | Height: | Size: 92 KiB After Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 27 KiB |
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 33 KiB |
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 81 KiB After Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 85 KiB After Width: | Height: | Size: 29 KiB |
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 33 KiB |
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 34 KiB |
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 29 KiB |
Before Width: | Height: | Size: 72 KiB After Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 24 KiB |
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 9.8 KiB |
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 18 KiB |
@ -0,0 +1,51 @@
@@ -0,0 +1,51 @@
|
||||
import { useEffect, useState } from "react"; |
||||
import type { DragEvent, ReactNode } from "react"; |
||||
|
||||
interface FileDropHandlerProps { |
||||
children: ReactNode; |
||||
className: string; |
||||
onDrop: (event: DragEvent<HTMLDivElement>) => void; |
||||
onDraggingChange: (isDragging: boolean) => void; |
||||
} |
||||
|
||||
export function FileDropHandler(props: FileDropHandlerProps) { |
||||
const [dragging, setDragging] = useState(false); |
||||
|
||||
const handleDragEnter = (event: DragEvent<HTMLDivElement>) => { |
||||
event.preventDefault(); |
||||
setDragging(true); |
||||
}; |
||||
|
||||
const handleDragLeave = (event: DragEvent<HTMLDivElement>) => { |
||||
if (!event.currentTarget.contains(event.relatedTarget as Node)) { |
||||
setDragging(false); |
||||
} |
||||
}; |
||||
|
||||
const handleDragOver = (event: DragEvent<HTMLDivElement>) => { |
||||
event.preventDefault(); |
||||
}; |
||||
|
||||
const handleDrop = (event: DragEvent<HTMLDivElement>) => { |
||||
event.preventDefault(); |
||||
setDragging(false); |
||||
|
||||
props.onDrop(event); |
||||
}; |
||||
|
||||
useEffect(() => { |
||||
props.onDraggingChange(dragging); |
||||
}, [dragging, props]); |
||||
|
||||
return ( |
||||
<div |
||||
onDragEnter={handleDragEnter} |
||||
onDragLeave={handleDragLeave} |
||||
onDragOver={handleDragOver} |
||||
onDrop={handleDrop} |
||||
className={props.className} |
||||
> |
||||
{props.children} |
||||
</div> |
||||
); |
||||
} |
@ -0,0 +1,65 @@
@@ -0,0 +1,65 @@
|
||||
import { useCallback } from "react"; |
||||
import { useTranslation } from "react-i18next"; |
||||
|
||||
import { FlagIcon } from "@/components/FlagIcon"; |
||||
import { Menu } from "@/components/player/internals/ContextMenu"; |
||||
import { useOverlayRouter } from "@/hooks/useOverlayRouter"; |
||||
import { AudioTrack } from "@/stores/player/slices/source"; |
||||
import { usePlayerStore } from "@/stores/player/store"; |
||||
import { getPrettyLanguageNameFromLocale } from "@/utils/language"; |
||||
|
||||
import { SelectableLink } from "../../internals/ContextMenu/Links"; |
||||
|
||||
export function AudioOption(props: { |
||||
langCode?: string; |
||||
children: React.ReactNode; |
||||
selected?: boolean; |
||||
onClick?: () => void; |
||||
}) { |
||||
return ( |
||||
<SelectableLink selected={props.selected} onClick={props.onClick}> |
||||
<span className="flex items-center"> |
||||
<span data-code={props.langCode} className="mr-3 inline-flex"> |
||||
<FlagIcon langCode={props.langCode} /> |
||||
</span> |
||||
<span>{props.children}</span> |
||||
</span> |
||||
</SelectableLink> |
||||
); |
||||
} |
||||
|
||||
export function AudioView({ id }: { id: string }) { |
||||
const { t } = useTranslation(); |
||||
const unknownChoice = t("player.menus.subtitles.unknownLanguage"); |
||||
|
||||
const router = useOverlayRouter(id); |
||||
const audioTracks = usePlayerStore((s) => s.audioTracks); |
||||
const currentAudioTrack = usePlayerStore((s) => s.currentAudioTrack); |
||||
const changeAudioTrack = usePlayerStore((s) => s.display?.changeAudioTrack); |
||||
|
||||
const change = useCallback( |
||||
(track: AudioTrack) => { |
||||
changeAudioTrack?.(track); |
||||
router.close(); |
||||
}, |
||||
[router, changeAudioTrack], |
||||
); |
||||
|
||||
return ( |
||||
<> |
||||
<Menu.BackLink onClick={() => router.navigate("/")}>Audio</Menu.BackLink> |
||||
<Menu.Section className="flex flex-col pb-4"> |
||||
{audioTracks.map((v) => ( |
||||
<AudioOption |
||||
key={v.id} |
||||
selected={v.id === currentAudioTrack?.id} |
||||
langCode={v.language} |
||||
onClick={audioTracks.includes(v) ? () => change(v) : undefined} |
||||
> |
||||
{getPrettyLanguageNameFromLocale(v.language) ?? unknownChoice} |
||||
</AudioOption> |
||||
))} |
||||
</Menu.Section> |
||||
</> |
||||
); |
||||
} |
@ -0,0 +1,20 @@
@@ -0,0 +1,20 @@
|
||||
import { isAllowedExtensionVersion } from "@/backend/extension/compatibility"; |
||||
import { extensionInfo } from "@/backend/extension/messaging"; |
||||
|
||||
export type ExtensionStatus = |
||||
| "unknown" |
||||
| "failed" |
||||
| "disallowed" |
||||
| "noperms" |
||||
| "outdated" |
||||
| "success"; |
||||
|
||||
export async function getExtensionState(): Promise<ExtensionStatus> { |
||||
const info = await extensionInfo(); |
||||
if (!info) return "unknown"; // cant talk to extension
|
||||
if (!info.success) return "failed"; // extension failed to respond
|
||||
if (!info.allowed) return "disallowed"; // extension is not enabled on this page
|
||||
if (!info.hasPermission) return "noperms"; // extension has no perms to do it's tasks
|
||||
if (!isAllowedExtensionVersion(info.version)) return "outdated"; // extension is too old
|
||||
return "success"; // no problems
|
||||
} |
@ -1,3 +1,49 @@
@@ -1,3 +1,49 @@
|
||||
{ |
||||
"routes": [{ "src": "/[^.]+", "dest": "/", "status": 200 }] |
||||
"rewrites": [ |
||||
{ |
||||
"source": "/(.*)", |
||||
"destination": "/" |
||||
} |
||||
], |
||||
"headers": [ |
||||
{ |
||||
"source": "/(.*)", |
||||
"headers": [ |
||||
{ |
||||
"key": "X-Content-Type-Options", |
||||
"value": "nosniff" |
||||
}, |
||||
{ |
||||
"key": "X-Frame-Options", |
||||
"value": "DENY" |
||||
}, |
||||
{ |
||||
"key": "X-XSS-Protection", |
||||
"value": "1; mode=block" |
||||
}, |
||||
{ |
||||
"key": "Cache-Control", |
||||
"value": "public, max-age=0, s-maxage=0, must-revalidate" |
||||
} |
||||
] |
||||
}, |
||||
{ |
||||
"source": "/manifest.webmanifest", |
||||
"headers": [ |
||||
{ |
||||
"key": "Content-Type", |
||||
"value": "application/manifest+json" |
||||
} |
||||
] |
||||
}, |
||||
{ |
||||
"source": "/assets/(.*)", |
||||
"headers": [ |
||||
{ |
||||
"key": "Cache-Control", |
||||
"value": "public, max-age=31536000, s-maxage=31536000, immutable" |
||||
} |
||||
] |
||||
} |
||||
] |
||||
} |
||||
|