fix: touch support for miniplayer and fastforward
Some checks are pending
Check / check (push) Waiting to run

fix: mobile responsive UI for player episodelist, and settings pages
This commit is contained in:
ThaUnknown 2025-07-13 15:48:23 +02:00
parent 571efea41e
commit f078ae8f19
No known key found for this signature in database
19 changed files with 47 additions and 197 deletions

View file

@ -1,6 +1,6 @@
{
"name": "ui",
"version": "6.4.30",
"version": "6.4.31",
"license": "BUSL-1.1",
"private": true,
"packageManager": "pnpm@9.14.4",
@ -73,6 +73,7 @@
"js-levenshtein": "^1.1.6",
"lucide-svelte": "^0.511.0",
"marked": "^15.0.11",
"native": "github:hayase-app/native",
"p2pt": "github:ThaUnknown/p2pt#modernise",
"semver": "^7.7.2",
"simple-store-svelte": "^1.0.6",

View file

@ -89,6 +89,9 @@ importers:
marked:
specifier: ^15.0.11
version: 15.0.11
native:
specifier: github:hayase-app/native
version: https://codeload.github.com/hayase-app/native/tar.gz/151ca360188ffd431143f1b9ba2f33ae0a87480b
p2pt:
specifier: github:ThaUnknown/p2pt#modernise
version: https://codeload.github.com/ThaUnknown/p2pt/tar.gz/9ad7a56ed6ee43f5664ebad33b803702ee349316
@ -1897,6 +1900,10 @@ packages:
engines: {node: ^18 || >=20}
hasBin: true
native@https://codeload.github.com/hayase-app/native/tar.gz/151ca360188ffd431143f1b9ba2f33ae0a87480b:
resolution: {tarball: https://codeload.github.com/hayase-app/native/tar.gz/151ca360188ffd431143f1b9ba2f33ae0a87480b}
version: 1.0.0
natural-compare@1.4.0:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
@ -4543,6 +4550,8 @@ snapshots:
nanoid@5.1.5: {}
native@https://codeload.github.com/hayase-app/native/tar.gz/151ca360188ffd431143f1b9ba2f33ae0a87480b: {}
natural-compare@1.4.0: {}
no-case@3.0.4:

162
src/app.d.ts vendored
View file

@ -1,16 +1,8 @@
// See https://kit.svelte.dev/docs/types#app
import type { SessionMetadata } from '$lib/components/ui/player/util'
import type { Search } from '$lib/modules/anilist/queries'
import type { VariablesOf } from 'gql.tada'
import type { CompositionEventHandler } from 'svelte/elements'
// for information about these interfaces
export interface AuthResponse {
access_token: string
expires_in: string // seconds
token_type: 'Bearer'
}
export interface Track {
selected: boolean
enabled: boolean
@ -20,160 +12,6 @@ export interface Track {
language: string
}
export interface TorrentFile {
name: string
hash: string
type: string
size: number
path: string
url: string
id: number
}
export interface Attachment {
filename: string
mimetype: string
id: number
url: string
}
export interface TorrentInfo {
name: string
progress: number
size: {
total: number
downloaded: number
uploaded: number
}
speed: {
down: number
up: number
}
time: {
remaining: number
elapsed: number
}
peers: {
seeders: number
leechers: number
wires: number
}
pieces: {
total: number
size: number
}
hash: string
}
export interface PeerInfo {
ip: string
seeder: boolean
client: string
progress: number
size: {
downloaded: number
uploaded: number
}
speed: {
down: number
up: number
}
flags: Array<'incoming' | 'outgoing' | 'utp' | 'encrypted'>
time: number
}
export interface FileInfo {
name: string
size: number
progress: number
selections: number
}
export interface TorrentSettings {
torrentPersist: boolean
torrentDHT: boolean
torrentStreamedDownload: boolean
torrentSpeed: number
maxConns: number
torrentPort: number
dhtPort: number
torrentPeX: boolean
}
export interface LibraryEntry {
mediaID: number
episode: number
files: number
hash: string
progress: number
date: number
size: number
name: string
}
export interface Native {
authAL: (url: string) => Promise<AuthResponse>
authMAL: (url: string) => Promise<{ code: string, state: string }>
restart: () => Promise<void>
openURL: (url: string) => Promise<void>
share: Navigator['share']
minimise: () => Promise<void>
maximise: () => Promise<void>
focus: () => Promise<void>
close: () => Promise<void>
selectPlayer: () => Promise<string>
selectDownload: () => Promise<string>
setAngle: (angle: string) => Promise<void>
getLogs: () => Promise<string>
getDeviceInfo: () => Promise<unknown>
openUIDevtools: () => Promise<void>
openTorrentDevtools: () => Promise<void>
checkUpdate: () => Promise<void>
updateAndRestart: () => Promise<void>
updateReady: () => Promise<void>
toggleDiscordDetails: (enabled: boolean) => Promise<void>
setMediaSession: (metadata: SessionMetadata, mediaId: number) => Promise<void>
setPositionState: (state?: MediaPositionState) => Promise<void>
setPlayBackState: (paused: 'none' | 'paused' | 'playing') => Promise<void>
setActionHandler: (action: MediaSessionAction | 'enterpictureinpicture', handler: MediaSessionActionHandler | null) => void
checkAvailableSpace: (_?: unknown) => Promise<number>
checkIncomingConnections: (port: number) => Promise<boolean>
updatePeerCounts: (hashes: string[]) => Promise<Array<{ hash: string, complete: string, downloaded: string, incomplete: string }>>
playTorrent: (id: string, mediaID: number, episode: number) => Promise<TorrentFile[]>
library: () => Promise<LibraryEntry[]>
attachments: (hash: string, id: number) => Promise<Attachment[]>
tracks: (hash: string, id: number) => Promise<Array<{ number: string, language?: string, type: string, header?: string, name?: string }>>
subtitles: (hash: string, id: number, cb: (subtitle: { text: string, time: number, duration: number }, trackNumber: number) => void) => Promise<void>
errors: (cb: (error: Error) => void) => Promise<void>
chapters: (hash: string, id: number) => Promise<Array<{ start: number, end: number, text: string }>>
torrentInfo: (hash: string) => Promise<TorrentInfo>
peerInfo: (hash: string) => Promise<PeerInfo[]>
fileInfo: (hash: string) => Promise<FileInfo[]>
protocolStatus: (hash: string) => Promise<{
dht: boolean
lsd: boolean
pex: boolean
nat: boolean
forwarding: boolean
persisting: boolean
streaming: boolean
}>
setDOH: (dns: string) => Promise<void>
cachedTorrents: () => Promise<string[]>
downloadProgress: (percent: number) => Promise<void>
updateSettings: (settings: TorrentSettings) => Promise<void>
updateProgress: (cb: (progress: number) => void) => Promise<void>
spawnPlayer: (url: string) => Promise<void>
setHideToTray: (enabled: boolean) => Promise<void>
transparency: (enabled: boolean) => Promise<void>
setZoom: (scale: number) => Promise<void>
isApp: boolean
version: () => Promise<string>
navigate: (cb: (data: { target: string, value: string | undefined }) => void) => Promise<void>
defaultTransparency: () => boolean
debug: (levels: string) => Promise<void>
}
declare global {
namespace App {

View file

@ -49,7 +49,7 @@
</script>
<div class='flex flex-col w-full relative h-full overflow-clip'>
<div class='space-y-0.5 px-10 pt-10'>
<div class='space-y-0.5 p-3 md:p-10 pb-0'>
<h2 class='text-2xl font-bold'>Global App Chat</h2>
<p class='text-muted-foreground'>
Chat with other users of the app, share your thoughts, ask questions and have fun!

View file

@ -8,7 +8,7 @@
import type { ResolvedFile } from './resolver'
import type { MediaInfo } from './util'
import type { TorrentFile } from '../../../../app'
import type { TorrentFile } from 'native'
import { goto } from '$app/navigation'
import { page } from '$app/stores'

View file

@ -40,7 +40,7 @@
import Volume from './volume.svelte'
import type { ResolvedFile } from './resolver'
import type { TorrentFile } from '../../../../app'
import type { TorrentFile } from 'native'
import type { SvelteMediaTimeRange } from 'svelte/elements'
import { beforeNavigate, goto } from '$app/navigation'
@ -743,8 +743,8 @@
<svelte:document bind:fullscreenElement bind:visibilityState use:holdToFF={'key'} />
<div class='w-full h-full relative content-center bg-black overflow-clip text-left' class:fitWidth class:seeking class:pip={pictureInPictureElement} bind:this={wrapper} on:navigate={resetMove}>
<video class='w-full h-full' preload='metadata' class:cursor-none={immersed} class:cursor-pointer={isMiniplayer} class:object-cover={fitWidth} class:opacity-0={$settings.playerDeband || seeking || pictureInPictureElement} class:absolute={$settings.playerDeband} class:top-0={$settings.playerDeband}
<div class='w-full h-full relative content-center bg-black overflow-clip text-left touch-none' class:fitWidth class:seeking class:pip={pictureInPictureElement} bind:this={wrapper} on:navigate={resetMove}>
<video class='w-full h-full touch-none' preload='metadata' class:cursor-none={immersed} class:cursor-pointer={isMiniplayer} class:object-cover={fitWidth} class:opacity-0={$settings.playerDeband || seeking || pictureInPictureElement} class:absolute={$settings.playerDeband} class:top-0={$settings.playerDeband}
use:createSubtitles
use:createDeband={$settings.playerDeband}
use:holdToFF={'pointer'}
@ -857,7 +857,7 @@
<a class='text-white text-lg font-normal leading-none line-clamp-1 hover:text-neutral-300 hover:underline' href='/app/anime/{mediaInfo.media.id}'>{mediaInfo.session.title}</a>
<Sheet.Root portal={wrapper}>
<Sheet.Trigger id='episode-list-button' class='text-[rgba(217,217,217,0.6)] hover:text-neutral-500 text-sm leading-none font-light line-clamp-1 text-left hover:underline'>{mediaInfo.session.description}</Sheet.Trigger>
<Sheet.Content class='w-[550px] sm:max-w-full h-full overflow-y-scroll flex flex-col pb-0 shrink-0 gap-0 bg-black justify-between'>
<Sheet.Content class='w-full sm:w-[550px] p-3 sm:p-6 max-w-full sm:max-w-full h-full overflow-y-scroll flex flex-col pb-0 shrink-0 gap-0 bg-black justify-between'>
{#if mediaInfo.media}
{#await Promise.all([episodes(mediaInfo.media.id), client.single(mediaInfo.media.id)]) then [eps, media]}
{#if media.data?.Media}

View file

@ -1,9 +1,9 @@
import anitomyscript from 'anitomyscript'
import type { TorrentFile } from '$lib/../app'
import type { MediaEdgeFrag } from '$lib/modules/anilist/queries'
import type { AnitomyResult } from 'anitomyscript'
import type { ResultOf } from 'gql.tada'
import type { TorrentFile } from 'native'
import { client, episodes, type Media } from '$lib/modules/anilist'
import { videoRx } from '$lib/utils'

View file

@ -3,7 +3,7 @@ import { writable } from 'simple-store-svelte'
import { get } from 'svelte/store'
import type { ResolvedFile } from './resolver'
import type { TorrentFile } from '../../../../app'
import type { TorrentFile } from 'native'
import native from '$lib/modules/native'
import { type defaults, settings, SUPPORTS } from '$lib/modules/settings'

View file

@ -1,6 +1,7 @@
import type { Media } from '$lib/modules/anilist'
import type { ResolvedFile } from './resolver'
import type { Track } from '../../../../app'
import type { SessionMetadata } from 'native'
export interface Chapter {
start: number
@ -8,12 +9,6 @@ export interface Chapter {
text: string
}
export interface SessionMetadata {
title: string
description: string
image: string
}
export interface MediaInfo {
file: ResolvedFile
media: Media

View file

@ -63,7 +63,7 @@
}
</script>
<div class={cn('w-full h-full', isMiniplayer && 'z-[49] absolute top-0 left-0 pointer-events-none cursor-grabbing')}
<div class={cn('w-full h-full', isMiniplayer && 'z-[49] absolute top-0 left-0 pointer-events-none cursor-grabbing touch-none')}
bind:this={wrapper}
on:pointerdown={startDragging}
on:pointerup|self={endDragging}

View file

@ -6,7 +6,7 @@
import { MediaCell, NameCell, StatusCell } from './cells'
import type { LibraryEntry } from '$lib/../app'
import type { LibraryEntry } from 'native'
import { goto } from '$app/navigation'
import * as Table from '$lib/components/ui/table'

View file

@ -11,21 +11,23 @@ import { storage } from './storage'
import type { EpisodesResponse } from '../anizip/types'
import type { TorrentResult } from 'hayase-extensions'
import { dev } from '$app/environment'
import { options as extensionOptions, saved } from '$lib/modules/extensions'
// TODO: ember exclusions might not be needed anymore as parser was improved
const exclusions = ['DTS', 'TrueHD', '[EMBER]']
const isDev = location.hostname === 'localhost'
const video = document.createElement('video')
if (!isDev && !video.canPlayType('video/mp4; codecs="hev1.1.6.L93.B0"')) {
if (!dev && !video.canPlayType('video/mp4; codecs="hev1.1.6.L93.B0"')) {
exclusions.push('HEVC', 'x265', 'H.265')
}
if (!isDev && !video.canPlayType('audio/mp4; codecs="ac-3"')) {
if (!dev && !video.canPlayType('audio/mp4; codecs="ac-3"')) {
exclusions.push('AC3', 'AC-3')
}
if (!('audioTracks' in HTMLVideoElement.prototype)) {
exclusions.push('DUAL')
exclusions.push('MULTI')
}
video.remove()

View file

@ -1,4 +1,4 @@
import type { AuthResponse, Native, TorrentInfo } from '../../app'
import type { AuthResponse, Native, TorrentInfo } from 'native'
import { sleep } from '$lib/utils'

View file

@ -5,8 +5,8 @@ import { persisted } from 'svelte-persisted-store'
import native from '../native'
import { w2globby } from '../w2g/lobby'
import type { TorrentFile, TorrentInfo } from '$lib/../app'
import type { Media } from '../anilist'
import type { TorrentFile, TorrentInfo } from 'native'
const defaultTorrentInfo: TorrentInfo = {
name: '',

View file

@ -46,7 +46,7 @@
}
</script>
<div class='space-y-6 p-10 pb-0 w-full h-full flex flex-col min-w-0'>
<div class='space-y-6 p-3 md:p-10 pb-0 w-full h-full flex flex-col min-w-0'>
<div class='space-y-0.5'>
<h2 class='text-2xl font-bold'>{overview.title}</h2>
<p class='text-muted-foreground'>

View file

@ -79,7 +79,7 @@
const _list = list
</script>
<div class='flex flex-col items-center w-full h-full overflow-y-auto p-10 min-w-0' use:dragScroll>
<div class='flex flex-col items-center w-full h-full overflow-y-auto p-3 md:p-10 min-w-0' use:dragScroll>
<div class='space-y-0.5 self-start mb-6'>
<h2 class='text-2xl font-bold'>Airing Calendar</h2>
<p class='text-muted-foreground'>
@ -138,8 +138,8 @@
{#each aggregate($query.data, dayList) as { day, episodes } (day.date)}
{@const sameMonth = isSameMonth(now, day.date)}
<div>
<div class='flex flex-col text-xs py-3 h-48' class:opacity-30={!sameMonth}>
{#if !$breakpoints.md}
<div class='flex flex-col text-xs py-3 h-24 lg:h-48' class:opacity-30={!sameMonth}>
{#if !$breakpoints.lg}
<Drawer.Root shouldScaleBackground portal='html'>
<Drawer.Trigger class='h-full flex flex-col'>
<div class={cn('w-6 h-6 flex items-center justify-center font-bold mx-3', isToday(day.date) && 'bg-[rgb(61,180,242)] rounded-full')}>
@ -185,12 +185,12 @@
<ButtonPrimitive.Root class={cn('flex items-center h-4 w-full group mt-1.5 px-3', +episode.airTime < Date.now() && 'opacity-30')} href='/app/anime/{episode.id}'>
<div class='font-medium text-nowrap text-ellipsis overflow-hidden pr-2' title={episode.title?.userPreferred}>
{#if status}
<StatusDot variant={status} class='hidden lg:inline-flex' />
<StatusDot variant={status} class='hidden xl:inline-flex' />
{/if}
{episode.title?.userPreferred}
</div>
<div class='ml-auto mr-1 text-nowrap hidden lg:inline-flex'>#{episode.episode}</div>
<div class='text-neutral-400 group-select:text-neutral-200 ml-auto lg:ml-0'>{format(episode.airTime, 'HH:mm')}</div>
<div class='ml-auto mr-1 text-nowrap hidden xl:inline-flex'>#{episode.episode}</div>
<div class='text-neutral-400 group-select:text-neutral-200 ml-auto xl:ml-0'>{format(episode.airTime, 'HH:mm')}</div>
</ButtonPrimitive.Root>
{/each}
{#if episodes.length > 6}
@ -204,12 +204,12 @@
<ButtonPrimitive.Root class={cn('flex items-center h-4 w-full group', +episode.airTime < Date.now() && 'text-neutral-300')} href='/app/anime/{episode.id}'>
<div class='font-medium text-nowrap text-ellipsis overflow-hidden pr-2' title={episode.title?.userPreferred}>
{#if status}
<StatusDot variant={status} class='hidden lg:inline-flex' />
<StatusDot variant={status} class='hidden xl:inline-flex' />
{/if}
{episode.title?.userPreferred}
</div>
<div class='ml-auto mr-1 text-nowrap hidden lg:inline-flex'>#{episode.episode}</div>
<div class='text-neutral-400 group-select:text-neutral-900 ml-auto lg:ml-0'>{format(episode.airTime, 'HH:mm')}</div>
<div class='ml-auto mr-1 text-nowrap hidden xl:inline-flex'>#{episode.episode}</div>
<div class='text-neutral-400 group-select:text-neutral-900 ml-auto xl:ml-0'>{format(episode.airTime, 'HH:mm')}</div>
</ButtonPrimitive.Root>
{/each}
</Tooltip.Content>

View file

@ -38,7 +38,7 @@
]
</script>
<div class='space-y-6 p-10 pb-0 w-full h-full flex flex-col'>
<div class='space-y-6 p-3 md:p-10 pb-0 w-full h-full flex flex-col'>
<div class='space-y-0.5'>
<h2 class='text-2xl font-bold'>Settings</h2>
<p class='text-muted-foreground'>

View file

@ -57,7 +57,7 @@
</script>
<div class='flex flex-col w-full relative h-full overflow-clip'>
<div class='space-y-0.5 px-10 pt-10'>
<div class='space-y-0.5 p-3 md:p-10 pb-0'>
<h2 class='text-2xl font-bold'>Watch Together <span class='text-muted-foreground text-lg ml-4 font-semibold'>{$w2globby?.code}</span></h2>
<p class='text-muted-foreground'>
Watch videos together with friends in real-time. You can invite others to your lobby and chat while watching.

View file

@ -41,7 +41,8 @@ const config: Config = {
center: true,
padding: '2rem',
screens: {
'2xl': '1400px'
'2xl': '1400px',
xs: '480px'
}
},
extend: {
@ -88,6 +89,10 @@ const config: Config = {
},
fontFamily: {
sans: [...fontFamily.sans]
},
screens: {
'2xl': '1400px',
xs: '480px'
}
}
}