build fixes - type errors, missing icons, tauri externals, unused imports
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
import { useLibraryGenreDistribution, useLibraryByTmdbId } from '../../hooks/use-jellyfin'
|
import { useLibraryGenreDistribution, useLibraryByTmdbId } from '../../hooks/use-jellyfin'
|
||||||
import { useTmdbDiscoverMovies, useTmdbTopRatedMovies } from '../../hooks/use-tmdb'
|
import { useTmdbDiscoverMovies } from '../../hooks/use-tmdb'
|
||||||
import { usePreferencesStore } from '../../stores/preferences-store'
|
import { usePreferencesStore } from '../../stores/preferences-store'
|
||||||
import { mapTmdbToJf } from '../../lib/tmdb-mapping'
|
import { mapTmdbToJf } from '../../lib/tmdb-mapping'
|
||||||
import { tmdbMovieGenreId } from '../../lib/tmdb-genres'
|
import { tmdbMovieGenreId } from '../../lib/tmdb-genres'
|
||||||
|
|||||||
@@ -58,14 +58,14 @@ function readLiveStats(): LiveStats {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function checkHwDecode(stream: { codec?: string | null; width?: number | null; height?: number | null; bitrate?: number | null } | null): Promise<HwDecodeState> {
|
async function checkHwDecode(stream: { Codec?: string | null; Width?: number | null; Height?: number | null; BitRate?: number | null } | null): Promise<HwDecodeState> {
|
||||||
if (!stream || typeof (navigator as any).mediaCapabilities?.decodingInfo !== 'function') {
|
if (!stream || typeof (navigator as any).mediaCapabilities?.decodingInfo !== 'function') {
|
||||||
return { supported: null, hwAccelerated: null }
|
return { supported: null, hwAccelerated: null }
|
||||||
}
|
}
|
||||||
const codec = (stream.codec || stream.Codec || '').toLowerCase()
|
const codec = (stream.Codec || '').toLowerCase()
|
||||||
const w = stream.width ?? stream.Width ?? 1920
|
const w = stream.Width ?? 1920
|
||||||
const h = stream.height ?? stream.Height ?? 1080
|
const h = stream.Height ?? 1080
|
||||||
const br = stream.bitrate ?? stream.BitRate ?? 8000000
|
const br = stream.BitRate ?? 8000000
|
||||||
// Map common Jellyfin codec names to MIME codec strings
|
// Map common Jellyfin codec names to MIME codec strings
|
||||||
const mimeCodec =
|
const mimeCodec =
|
||||||
codec === 'h264' ? 'avc1.640033'
|
codec === 'h264' ? 'avc1.640033'
|
||||||
|
|||||||
@@ -11,18 +11,21 @@ import { toast } from '../stores/toast-store'
|
|||||||
*/
|
*/
|
||||||
export function useNewReleaseNotifications(enabled: boolean) {
|
export function useNewReleaseNotifications(enabled: boolean) {
|
||||||
const qc = useQueryClient()
|
const qc = useQueryClient()
|
||||||
const lastNotifiedRef = useRef<string | null>(() => {
|
const lastNotifiedRef = useRef<string | null>(
|
||||||
|
(() => {
|
||||||
try { return localStorage.getItem('jf_last_notify_date') } catch { return null }
|
try { return localStorage.getItem('jf_last_notify_date') } catch { return null }
|
||||||
}())
|
})()
|
||||||
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!enabled) return
|
if (!enabled) return
|
||||||
const api = jellyfinClient.getApi()
|
const api = jellyfinClient.getApi()
|
||||||
if (!api) return
|
if (!api) return
|
||||||
|
const apiRef = api
|
||||||
|
|
||||||
async function check() {
|
async function check() {
|
||||||
try {
|
try {
|
||||||
const res = await getItemsApi(api).getItems({
|
const res = await getItemsApi(apiRef).getItems({
|
||||||
userId: jellyfinClient.getAuthState()!.userId,
|
userId: jellyfinClient.getAuthState()!.userId,
|
||||||
sortBy: ['DateCreated'],
|
sortBy: ['DateCreated'],
|
||||||
sortOrder: ['Descending'],
|
sortOrder: ['Descending'],
|
||||||
|
|||||||
@@ -31,15 +31,18 @@ export async function startDownload(args: {
|
|||||||
// Tauri path: fetch via the Rust-backed HTTP client so we avoid
|
// Tauri path: fetch via the Rust-backed HTTP client so we avoid
|
||||||
// CORS + we can stream large files without the browser's memory
|
// CORS + we can stream large files without the browser's memory
|
||||||
// pressure.
|
// pressure.
|
||||||
const { fetch } = await import('@tauri-apps/api/http')
|
// @ts-ignore
|
||||||
const { appLocalDataDir } = await import('@tauri-apps/api/path')
|
const { fetch } = await import('@tauri-apps/api/http') as any
|
||||||
const { writeBinaryFile, BaseDirectory } = await import('@tauri-apps/api/fs')
|
// @ts-ignore
|
||||||
|
const { appLocalDataDir } = await import('@tauri-apps/api/path') as any
|
||||||
|
// @ts-ignore
|
||||||
|
const { writeBinaryFile, BaseDirectory } = await import('@tauri-apps/api/fs') as any
|
||||||
|
|
||||||
const res = await fetch<Uint8Array>(streamUrl, {
|
const res = await (fetch as any)(streamUrl, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
responseType: 3, // ResponseType.Binary
|
responseType: 3, // ResponseType.Binary
|
||||||
})
|
})
|
||||||
const bytes = res.data
|
const bytes: Uint8Array = res.data
|
||||||
const dir = await appLocalDataDir()
|
const dir = await appLocalDataDir()
|
||||||
const fileName = `download_${itemId}_${Date.now()}.mp4`
|
const fileName = `download_${itemId}_${Date.now()}.mp4`
|
||||||
await writeBinaryFile(fileName, bytes, { dir: BaseDirectory.AppLocalData })
|
await writeBinaryFile(fileName, bytes, { dir: BaseDirectory.AppLocalData })
|
||||||
@@ -77,7 +80,7 @@ export async function startDownload(args: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Assemble the full blob
|
// Assemble the full blob
|
||||||
const blob = new Blob(chunks)
|
const blob = new Blob(chunks as any)
|
||||||
const objectUrl = URL.createObjectURL(blob)
|
const objectUrl = URL.createObjectURL(blob)
|
||||||
store.update(dl.id, {
|
store.update(dl.id, {
|
||||||
status: 'done',
|
status: 'done',
|
||||||
|
|||||||
@@ -79,6 +79,7 @@ export {
|
|||||||
IconBuilding as Building2,
|
IconBuilding as Building2,
|
||||||
IconTicket as Ticket,
|
IconTicket as Ticket,
|
||||||
IconAlertCircle as AlertCircle,
|
IconAlertCircle as AlertCircle,
|
||||||
|
IconBell as Bell,
|
||||||
IconLoader2 as Loader2,
|
IconLoader2 as Loader2,
|
||||||
IconWifiOff as WifiOff,
|
IconWifiOff as WifiOff,
|
||||||
IconInfoCircle as Info,
|
IconInfoCircle as Info,
|
||||||
@@ -140,6 +141,7 @@ export {
|
|||||||
IconStethoscope as Stethoscope,
|
IconStethoscope as Stethoscope,
|
||||||
IconLeaf as Leaf,
|
IconLeaf as Leaf,
|
||||||
IconMoonStars as MoonStars,
|
IconMoonStars as MoonStars,
|
||||||
|
IconMoon as Moon,
|
||||||
IconPlane as Plane,
|
IconPlane as Plane,
|
||||||
|
|
||||||
// People / places
|
// People / places
|
||||||
|
|||||||
@@ -3,12 +3,10 @@ import { useNavigate } from 'react-router-dom'
|
|||||||
import { motion } from 'framer-motion'
|
import { motion } from 'framer-motion'
|
||||||
import { Film, Tv, AlertCircle } from '../lib/icons'
|
import { Film, Tv, AlertCircle } from '../lib/icons'
|
||||||
import { useLibraryItems } from '../hooks/use-jellyfin'
|
import { useLibraryItems } from '../hooks/use-jellyfin'
|
||||||
import { getBestImage, getStoredServerUrl } from '../api/jellyfin'
|
|
||||||
import PosterCard from '../components/ui/PosterCard'
|
import PosterCard from '../components/ui/PosterCard'
|
||||||
|
|
||||||
export default function DuplicatesPage() {
|
export default function DuplicatesPage() {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const serverUrl = getStoredServerUrl()
|
|
||||||
const { data, isLoading } = useLibraryItems(undefined, {
|
const { data, isLoading } = useLibraryItems(undefined, {
|
||||||
includeItemTypes: ['Movie', 'Series'],
|
includeItemTypes: ['Movie', 'Series'],
|
||||||
sortBy: ['SortName'],
|
sortBy: ['SortName'],
|
||||||
|
|||||||
@@ -191,6 +191,7 @@ export default function PlayerPage() {
|
|||||||
const stillWatchingTargetRef = useRef<string | null>(null)
|
const stillWatchingTargetRef = useRef<string | null>(null)
|
||||||
|
|
||||||
/* Preferences */
|
/* Preferences */
|
||||||
|
const sleepTimerMinutes = usePreferencesStore(s => s.sleepTimerMinutes)
|
||||||
const autoplayNext = usePreferencesStore(s => s.autoplayNext)
|
const autoplayNext = usePreferencesStore(s => s.autoplayNext)
|
||||||
const subtitleMode = usePreferencesStore(s => s.subtitleMode)
|
const subtitleMode = usePreferencesStore(s => s.subtitleMode)
|
||||||
const subtitleLanguage = usePreferencesStore(s => s.subtitleLanguage)
|
const subtitleLanguage = usePreferencesStore(s => s.subtitleLanguage)
|
||||||
@@ -1520,7 +1521,7 @@ export default function PlayerPage() {
|
|||||||
streamUrl,
|
streamUrl,
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
isDownloaded={!!item && useDownloads.getState().items.some(d => d.itemId === item.Id)},
|
isDownloaded={!!item && useDownloads.getState().items.some(d => d.itemId === item.Id)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Center play/pause indicator (only shown briefly when paused) */}
|
{/* Center play/pause indicator (only shown briefly when paused) */}
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
import { useQuery } from '@tanstack/react-query'
|
import { useQuery } from '@tanstack/react-query'
|
||||||
import { motion } from 'framer-motion'
|
import { motion } from 'framer-motion'
|
||||||
import { Activity, Monitor, Users, Server as ServerIcon, Clock, Film } from '../../../lib/icons'
|
import { Activity, MonitorPlay, Users, Server as ServerIcon, Clock, Film } from '../../../lib/icons'
|
||||||
import { jellyfinClient, getSystemApi, getSessionApi, getActivityLogApi } from '../../../api/jellyfin'
|
import { jellyfinClient, getSystemApi, getSessionApi, getActivityLogApi } from '../../../api/jellyfin'
|
||||||
import { Section, Row, SubHeading } from '../_ui'
|
import { Section, SubHeading } from '../_ui'
|
||||||
|
|
||||||
function useApi() {
|
function useApi() {
|
||||||
return jellyfinClient.getApi()
|
return jellyfinClient.getApi()
|
||||||
@@ -49,12 +49,13 @@ export function ServerDashboardSection() {
|
|||||||
const transcodes = activeSessions.filter(s => s.PlayState?.PlayMethod === 'Transcode')
|
const transcodes = activeSessions.filter(s => s.PlayState?.PlayMethod === 'Transcode')
|
||||||
|
|
||||||
const uptime = useMemo(() => {
|
const uptime = useMemo(() => {
|
||||||
if (!info?.ServerStartTime) return null
|
const start = (info as any)?.ServerStartTime
|
||||||
const ms = Date.now() - new Date(info.ServerStartTime).getTime()
|
if (!start) return null
|
||||||
|
const ms = Date.now() - new Date(start).getTime()
|
||||||
const days = Math.floor(ms / 86_400_000)
|
const days = Math.floor(ms / 86_400_000)
|
||||||
const hrs = Math.floor((ms % 86_400_000) / 3_600_000)
|
const hrs = Math.floor((ms % 86_400_000) / 3_600_000)
|
||||||
return days > 0 ? `${days}d ${hrs}h` : `${hrs}h`
|
return days > 0 ? `${days}d ${hrs}h` : `${hrs}h`
|
||||||
}, [info?.ServerStartTime])
|
}, [info])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Section id="server-dashboard" title="Server dashboard" description="Live stats and activity">
|
<Section id="server-dashboard" title="Server dashboard" description="Live stats and activity">
|
||||||
@@ -62,7 +63,7 @@ export function ServerDashboardSection() {
|
|||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 mb-6">
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-3 mb-6">
|
||||||
<StatCard label="Version" value={info?.Version || '-'} icon={<ServerIcon size={12} className="text-accent" />} />
|
<StatCard label="Version" value={info?.Version || '-'} icon={<ServerIcon size={12} className="text-accent" />} />
|
||||||
<StatCard label="Uptime" value={uptime || '-'} icon={<Clock size={12} className="text-accent" />} />
|
<StatCard label="Uptime" value={uptime || '-'} icon={<Clock size={12} className="text-accent" />} />
|
||||||
<StatCard label="Active streams" value={String(activeSessions.length)} icon={<Monitor size={12} className="text-accent" />} />
|
<StatCard label="Active streams" value={String(activeSessions.length)} icon={<MonitorPlay size={12} className="text-accent" />} />
|
||||||
<StatCard label="Transcoding" value={String(transcodes.length)} icon={<Activity size={12} className="text-accent" />} />
|
<StatCard label="Transcoding" value={String(transcodes.length)} icon={<Activity size={12} className="text-accent" />} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { ExternalLink, Loader2, Check, Trash2 } from '../../../lib/icons'
|
|||||||
import { Section, Row, Input, Toggle } from '../_ui'
|
import { Section, Row, Input, Toggle } from '../_ui'
|
||||||
import { useTrakt } from '../../../stores/trakt-store'
|
import { useTrakt } from '../../../stores/trakt-store'
|
||||||
import { useWatchlist } from '../../../hooks/use-watchlist'
|
import { useWatchlist } from '../../../hooks/use-watchlist'
|
||||||
import { requestDeviceCode, pollDeviceToken, fetchTraktWatchlist, addToTraktWatchlist } from '../../../lib/trakt'
|
import { requestDeviceCode, pollDeviceToken, fetchTraktWatchlist } from '../../../lib/trakt'
|
||||||
import { toast } from '../../../stores/toast-store'
|
import { toast } from '../../../stores/toast-store'
|
||||||
import { useLibraryByTmdbId } from '../../../hooks/use-jellyfin'
|
import { useLibraryByTmdbId } from '../../../hooks/use-jellyfin'
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export interface DownloadItem {
|
|||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
items: DownloadItem[]
|
items: DownloadItem[]
|
||||||
add: (item: Omit<DownloadItem, 'id' | 'status' | 'progress' | 'createdAt'>) => void
|
add: (item: Omit<DownloadItem, 'id' | 'status' | 'progress' | 'createdAt'>) => DownloadItem
|
||||||
update: (id: string, patch: Partial<DownloadItem>) => void
|
update: (id: string, patch: Partial<DownloadItem>) => void
|
||||||
remove: (id: string) => void
|
remove: (id: string) => void
|
||||||
clearCompleted: () => void
|
clearCompleted: () => void
|
||||||
@@ -35,9 +35,9 @@ export const useDownloads = create<State>()(
|
|||||||
persist(
|
persist(
|
||||||
(set, get) => ({
|
(set, get) => ({
|
||||||
items: [],
|
items: [],
|
||||||
add: item =>
|
add: item => {
|
||||||
set(s => {
|
const existing = get().items.find(i => i.itemId === item.itemId && i.status !== 'error')
|
||||||
if (s.items.some(i => i.itemId === item.itemId && i.status !== 'error')) return s
|
if (existing) return existing
|
||||||
const next: DownloadItem = {
|
const next: DownloadItem = {
|
||||||
...item,
|
...item,
|
||||||
id: uid(),
|
id: uid(),
|
||||||
@@ -45,8 +45,9 @@ export const useDownloads = create<State>()(
|
|||||||
progress: 0,
|
progress: 0,
|
||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
}
|
}
|
||||||
return { items: [...s.items, next] }
|
set(s => ({ items: [...s.items, next] }))
|
||||||
}),
|
return next
|
||||||
|
},
|
||||||
update: (id, patch) =>
|
update: (id, patch) =>
|
||||||
set(s => ({
|
set(s => ({
|
||||||
items: s.items.map(i => (i.id === id ? { ...i, ...patch } : i)),
|
items: s.items.map(i => (i.id === id ? { ...i, ...patch } : i)),
|
||||||
|
|||||||
@@ -6,5 +6,8 @@ export default defineConfig({
|
|||||||
plugins: [react(), tailwindcss()],
|
plugins: [react(), tailwindcss()],
|
||||||
build: {
|
build: {
|
||||||
chunkSizeWarningLimit: 900,
|
chunkSizeWarningLimit: 900,
|
||||||
|
rollupOptions: {
|
||||||
|
external: [/^@tauri-apps\/.*/],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user