mirror of
https://github.com/ThaUnknown/miru.git
synced 2026-04-06 01:11:11 +00:00
feat: finish setup
feat: UI for toggle sync fix: better settings types fix: reorganize extension settings feat: finish extension search modal fix: use pick for media utils fix: change how native is loaded feat: debug ribbon wip: trace anime
This commit is contained in:
parent
5862e4d69b
commit
d5b9c8dee7
25 changed files with 313 additions and 176 deletions
1
src/app.d.ts
vendored
1
src/app.d.ts
vendored
|
|
@ -43,6 +43,7 @@ export interface Native {
|
|||
setActionHandler: (action: MediaSessionAction | 'enterpictureinpicture', handler: MediaSessionActionHandler | null) => void
|
||||
checkAvailableSpace: (_?: unknown) => Promise<number>
|
||||
checkIncomingConnections: (_?: unknown) => Promise<boolean>
|
||||
updatePeerCounts: (hashes: string[]) => Promise<Array<{ hash, complete, downloaded, incomplete }>>
|
||||
isApp: boolean
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
import * as Dialog from '$lib/components/ui/dialog'
|
||||
import { Input } from './ui/input'
|
||||
import { MagnifyingGlass } from 'svelte-radix'
|
||||
import { settings } from '$lib/modules/settings'
|
||||
import { settings, videoResolutions } from '$lib/modules/settings'
|
||||
import { SingleCombo } from './ui/combobox'
|
||||
import { title, type Media } from '$lib/modules/anilist'
|
||||
import type { AnitomyResult } from 'anitomyscript'
|
||||
|
|
@ -13,13 +13,6 @@
|
|||
import { BadgeCheck, Database } from 'lucide-svelte'
|
||||
import type { TorrentResult } from 'hayase-extensions'
|
||||
|
||||
const resolutions = {
|
||||
1080: '1080p',
|
||||
720: '720p',
|
||||
480: '480p',
|
||||
'': 'Any'
|
||||
}
|
||||
|
||||
const termMapping: Record<string, {text: string, color: string}> = {}
|
||||
termMapping['5.1'] = termMapping['5.1CH'] = { text: '5.1', color: '#f67255' }
|
||||
termMapping['TRUEHD5.1'] = { text: 'TrueHD 5.1', color: '#f67255' }
|
||||
|
|
@ -68,10 +61,11 @@
|
|||
<script lang='ts'>
|
||||
import ProgressButton from './ui/button/progress-button.svelte'
|
||||
import { Banner } from './ui/img'
|
||||
import { saved } from '$lib/modules/extensions'
|
||||
|
||||
$: open = !!$searchStore.media
|
||||
|
||||
$: searchResult = !!$searchStore.media && extensions.getResultsFromExtensions({ media: $searchStore.media, episode: $searchStore.episode, batch: $settings.searchBatch, resolution: $settings.searchQuality as '' | '1080' | '720' | '2160' | '540' | '480' })
|
||||
$: searchResult = !!$searchStore.media && extensions.getResultsFromExtensions({ media: $searchStore.media, episode: $searchStore.episode, batch: $settings.searchBatch, resolution: $settings.searchQuality })
|
||||
|
||||
function close (state: boolean) {
|
||||
if (!state) searchStore.set({})
|
||||
|
|
@ -79,7 +73,7 @@
|
|||
|
||||
let inputText = ''
|
||||
|
||||
function play (result: TorrentResult & { parseObject: AnitomyResult, extension: string[] }) {
|
||||
function play (result: TorrentResult & { parseObject: AnitomyResult, extension: Set<string> }) {
|
||||
close(false)
|
||||
// TODO
|
||||
}
|
||||
|
|
@ -91,17 +85,20 @@
|
|||
if (best) play(best)
|
||||
}
|
||||
|
||||
function filterAndSortResults (results: Array<TorrentResult & { parseObject: AnitomyResult, extension: string[] }>, searchText: string) {
|
||||
// TODO: sort preference such as size, quality, seeders, etc.
|
||||
function filterAndSortResults (results: Array<TorrentResult & { parseObject: AnitomyResult, extension: Set<string> }>, searchText: string) {
|
||||
const preference = $settings.lookupPreference
|
||||
return results
|
||||
.filter(({ title }) => title.toLowerCase().includes(searchText.toLowerCase()))
|
||||
.sort((a, b) => {
|
||||
// pre-emtively sort by deal breaker conditions
|
||||
// the higher the rank the worse the result... don't ask
|
||||
function getRank (res: typeof results[0]) {
|
||||
if (res.accuracy === 'low') return 3
|
||||
if ((res.type === 'best' || res.type === 'alt') && res.seeders > 15) return 0
|
||||
if (res.seeders > 15) return 1
|
||||
return 2
|
||||
if (res.seeders <= 15) return 2
|
||||
if ((res.type === 'best' || res.type === 'alt') && preference === 'quality') return 0
|
||||
return 1
|
||||
}
|
||||
|
||||
const rankA = getRank(a)
|
||||
const rankB = getRank(b)
|
||||
if (rankA !== rankB) return rankA - rankB
|
||||
|
|
@ -110,7 +107,9 @@
|
|||
const scoreB = b.accuracy === 'high' ? 1 : 0
|
||||
const diff = scoreB - scoreA
|
||||
if (diff !== 0) return diff
|
||||
// sort by seeders
|
||||
|
||||
// sort by preference, quality is sorted in rank, so quality and seeders is both as seeders here.
|
||||
if (preference === 'size') return a.size - b.size
|
||||
return b.seeders - a.seeders
|
||||
}
|
||||
return 0
|
||||
|
|
@ -119,18 +118,21 @@
|
|||
|
||||
let animating = false
|
||||
|
||||
function startAnimation () {
|
||||
async function startAnimation (searchRes: typeof searchResult) {
|
||||
if (!$settings.searchAutoSelect) return
|
||||
animating = true
|
||||
animating = false
|
||||
await searchRes
|
||||
if (searchRes === searchResult) animating = true
|
||||
}
|
||||
|
||||
function stopAnimation () {
|
||||
animating = false
|
||||
}
|
||||
|
||||
$: searchResult && searchResult.then(startAnimation)
|
||||
$: searchResult && startAnimation(searchResult)
|
||||
</script>
|
||||
|
||||
<!-- eslint-disable-next-line svelte/no-reactive-reassign -->
|
||||
<Dialog.Root bind:open onOpenChange={close}>
|
||||
<Dialog.Content class='bg-black h-full lg:border-x-4 border-b-0 max-w-5xl w-full max-h-[calc(100%-1rem)] mt-2 p-0 items-center flex lg:rounded-t-xl overflow-hidden'>
|
||||
<div class='absolute top-0 left-0 w-full h-full max-h-28 overflow-hidden'>
|
||||
|
|
@ -155,7 +157,7 @@
|
|||
</div>
|
||||
<div class='flex items-center space-x-2 grow'>
|
||||
<span>Resolution</span>
|
||||
<SingleCombo bind:value={$settings.searchQuality} items={resolutions} class='w-32 shrink-0 grow border-border border' />
|
||||
<SingleCombo bind:value={$settings.searchQuality} items={videoResolutions} class='w-32 shrink-0 grow border-border border' />
|
||||
</div>
|
||||
</div>
|
||||
<ProgressButton
|
||||
|
|
@ -166,15 +168,27 @@
|
|||
Auto Select Torrent
|
||||
</ProgressButton>
|
||||
</div>
|
||||
<div class='h-full overflow-y-auto px-4 sm:px-6 pt-2' role='menu' tabindex='-1' on:keydown={stopAnimation} on:pointerenter={stopAnimation}>
|
||||
<div class='h-full overflow-y-auto px-4 sm:px-6 pt-2' role='menu' tabindex='-1' on:keydown={stopAnimation} on:pointerenter={stopAnimation} on:pointermove={stopAnimation}>
|
||||
{#await searchResult}
|
||||
Loading...
|
||||
{#each Array.from({ length: 12 }) as _, i (i)}
|
||||
<div class='p-3 h-[104px] flex cursor-pointer mb-2 relative rounded-md overflow-hidden border border-border flex-col justify-between'>
|
||||
<div class='h-4 w-40 bg-primary/5 animate-pulse rounded mt-2' />
|
||||
<div class='bg-primary/5 animate-pulse rounded h-2 w-28 mt-1' />
|
||||
<div class='flex justify-between mb-1'>
|
||||
<div class='flex gap-2'>
|
||||
<div class='mt-2 bg-primary/5 animate-pulse rounded h-2 w-20' />
|
||||
<div class='mt-2 bg-primary/5 animate-pulse rounded h-2 w-20' />
|
||||
</div>
|
||||
<div class='mt-2 bg-primary/5 animate-pulse rounded h-2 w-20' />
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
{:then search}
|
||||
{@const media = $searchStore.media}
|
||||
{#if search && media}
|
||||
{@const { results, errors } = search}
|
||||
{#each filterAndSortResults(results, inputText) as result (result.hash)}
|
||||
<div class='p-3 flex cursor-pointer mb-2 relative rounded-md overflow-hidden border border-border select:ring-1 select:ring-ring select:bg-accent select:text-accent-foreground select:scale-[1.02] select:shadow-lg scale-100 transition-all' use:click={() => play(result)} title={result.parseObject.file_name}>
|
||||
<div class='p-3 flex cursor-pointer mb-2 relative rounded-md overflow-hidden border border-border select:ring-1 select:ring-ring select:bg-accent select:text-accent-foreground select:scale-[1.02] select:shadow-lg scale-100 transition-all' class:opacity-40={result.accuracy === 'low'} use:click={() => play(result)} title={result.parseObject.file_name}>
|
||||
{#if result.accuracy === 'high'}
|
||||
<div class='absolute top-0 left-0 w-full h-full -z-10'>
|
||||
<Banner {media} class='object-cover w-full h-full' />
|
||||
|
|
@ -182,13 +196,18 @@
|
|||
</div>
|
||||
{/if}
|
||||
<div class='flex pl-2 flex-col justify-between w-full h-20 relative min-w-0 text-[.7rem]'>
|
||||
<div class='flex w-full'>
|
||||
<div class='text-xl font-bold text-nowrap'>{result.parseObject.release_group && result.parseObject.release_group.length < 20 ? result.parseObject.release_group : 'No Group'}</div>
|
||||
<div class='flex w-full items-center'>
|
||||
{#if result.type === 'batch'}
|
||||
<Database size='1.2rem' class='ml-auto' />
|
||||
<Database class='mr-2' size='1.2rem' />
|
||||
{:else if result.accuracy === 'high'}
|
||||
<BadgeCheck size='1.2rem' class='ml-auto' style='color: #53da33' />
|
||||
<BadgeCheck size='1.2rem' class='mr-2' style='color: #53da33' />
|
||||
{/if}
|
||||
<div class='text-xl font-bold text-nowrap'>{result.parseObject.release_group && result.parseObject.release_group.length < 20 ? result.parseObject.release_group : 'No Group'}</div>
|
||||
<div class='ml-auto flex gap-2 self-start'>
|
||||
{#each result.extension as id (id)}
|
||||
<img src={$saved[id].icon} alt={id} class='size-4' title='Provided by {id}' />
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
<div class='text-muted-foreground text-ellipsis text-nowrap overflow-hidden'>{simplifyFilename(result.parseObject)}</div>
|
||||
<div class='flex flex-row leading-none'>
|
||||
|
|
@ -219,8 +238,30 @@
|
|||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
{#each errors as error, i (i)}
|
||||
<div class='p-5 flex items-center justify-center w-full h-80'>
|
||||
<div>
|
||||
<div class='mb-1 font-bold text-2xl text-center '>
|
||||
Extensions {error.extension} encountered an error
|
||||
</div>
|
||||
<div class='text-md text-center text-muted-foreground whitespace-pre-wrap'>
|
||||
{error.error.stack}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
|
||||
{:catch error}
|
||||
<div class='p-5 flex items-center justify-center w-full h-80'>
|
||||
<div>
|
||||
<div class='mb-1 font-bold text-4xl text-center '>
|
||||
Ooops!
|
||||
</div>
|
||||
<div class='text-lg text-center text-muted-foreground whitespace-pre-wrap'>
|
||||
{error.message}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/await}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@
|
|||
Ooops!
|
||||
</div>
|
||||
<div class='text-lg text-center text-muted-foreground'>
|
||||
Looks like something went wrong!.
|
||||
Looks like something went wrong!
|
||||
</div>
|
||||
<div class='text-lg text-center text-muted-foreground'>
|
||||
{$query.error.message}
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
export let media: Media
|
||||
export let size: NonNullable<$$Props['size']> = 'xs'
|
||||
function play () {
|
||||
const episode = progress(media) ?? 1
|
||||
const episode = (progress(media) ?? 0) + 1
|
||||
// TODO: set rewatch state
|
||||
searchStore.set({ media, episode })
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,16 +17,6 @@
|
|||
|
||||
return { destroy () { observer.unobserve(element) } }
|
||||
}
|
||||
|
||||
// TODO: each block sometimes errors with duplicate keys, why?
|
||||
// function dedupe (media: Array<Media|null|undefined>) {
|
||||
// const seen = new Set()
|
||||
// return media.filter((m) => {
|
||||
// if (seen.has(m?.id)) return false
|
||||
// seen.add(m?.id)
|
||||
// return true
|
||||
// })
|
||||
// }
|
||||
</script>
|
||||
|
||||
{#if $paused}
|
||||
|
|
@ -43,7 +33,7 @@
|
|||
Ooops!
|
||||
</div>
|
||||
<div class='text-lg text-center text-muted-foreground'>
|
||||
Looks like something went wrong!.
|
||||
Looks like something went wrong!
|
||||
</div>
|
||||
<div class='text-lg text-center text-muted-foreground'>
|
||||
{$query.error.message}
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@
|
|||
<CaretSort class='ml-2 h-4 w-4 shrink-0 opacity-50' />
|
||||
</Button>
|
||||
</Popover.Trigger>
|
||||
<Popover.Content class={cn('p-0 border-0')} sameWidth={true}>
|
||||
<Popover.Content class={cn('p-0 border-0 z-[1000]')} sameWidth={true}>
|
||||
<Command.Root>
|
||||
<Command.Input {placeholder} class='h-9 placeholder:opacity-50' />
|
||||
<Command.Empty>No results found.</Command.Empty>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
<script lang='ts'>
|
||||
import native from '$lib/modules/native'
|
||||
import { click } from '$lib/modules/navigate'
|
||||
import { persisted } from 'svelte-persisted-store'
|
||||
|
||||
const debug = false // TODO
|
||||
const debug = persisted('debug', '')
|
||||
</script>
|
||||
|
||||
<div class='w-[calc(100%-3.5rem)] left-[3.5rem] top-0 z-[2000] flex navbar absolute h-8'>
|
||||
|
|
@ -19,8 +20,8 @@
|
|||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{#if debug}
|
||||
<div class='ribbon z-10 text-center fixed font-bold pointer-events-none'>Debug Mode!</div>
|
||||
{#if $debug}
|
||||
<div class='ribbon z-[1000] text-center fixed font-bold pointer-events-none'>Debug Mode!</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
// TODO: update these
|
||||
|
||||
export const CHANGELOG_URL = 'https://api.github.com/repos/ThaUnknown/miru/releases'
|
||||
export const WEB_URL = 'https://miru.watch'
|
||||
|
|
|
|||
|
|
@ -3,9 +3,7 @@ import type { Episode, Episodes } from '../anizip/types'
|
|||
import type { Media } from './types'
|
||||
import type { ScheduleMedia } from './queries'
|
||||
|
||||
// TODO: use Pick<> for these
|
||||
|
||||
export function banner (media: Media): string | undefined {
|
||||
export function banner (media: Pick<Media, 'trailer' | 'bannerImage' | 'coverImage'>): string | undefined {
|
||||
if (media.bannerImage) return media.bannerImage
|
||||
if (media.trailer?.id) return `https://i.ytimg.com/vi/${media.trailer.id}/maxresdefault.jpg`
|
||||
return media.coverImage?.extraLarge as string | undefined
|
||||
|
|
@ -13,7 +11,7 @@ export function banner (media: Media): string | undefined {
|
|||
|
||||
const sizes = ['hq720', 'sddefault', 'hqdefault', 'mqdefault', 'default']
|
||||
|
||||
export async function safeBanner (media: Media): Promise<string | undefined> { // TODO: this needs to be a component
|
||||
export async function safeBanner (media: Pick<Media, 'trailer' | 'bannerImage' | 'coverImage'>): Promise<string | undefined> { // TODO: this needs to be a component
|
||||
const src = banner(media)
|
||||
if (!src?.startsWith('https://i.ytimg.com/')) return src
|
||||
|
||||
|
|
@ -42,12 +40,12 @@ export const STATUS_LABELS = {
|
|||
REPEATING: 'Re-Watching'
|
||||
}
|
||||
|
||||
export function cover (media: Media): string | undefined {
|
||||
export function cover (media: Pick<Media, 'trailer' | 'bannerImage' | 'coverImage'>): string | undefined {
|
||||
if (media.coverImage?.extraLarge) return media.coverImage.extraLarge
|
||||
return banner(media)
|
||||
}
|
||||
|
||||
export function title (media: Media): string {
|
||||
export function title (media: Pick<Media, 'title'>): string {
|
||||
return media.title?.userPreferred ?? 'TBA'
|
||||
}
|
||||
|
||||
|
|
@ -59,7 +57,7 @@ const STATUS_MAP = {
|
|||
HIATUS: 'Hiatus'
|
||||
}
|
||||
|
||||
export function status (media: Media): string {
|
||||
export function status (media: Pick<Media, 'status'>): string {
|
||||
if (media.status != null) return STATUS_MAP[media.status]
|
||||
|
||||
return 'N/A'
|
||||
|
|
@ -99,13 +97,13 @@ const FORMAT_MAP = {
|
|||
ONE_SHOT: 'One Shot'
|
||||
}
|
||||
|
||||
export function format (media: { format: Media['format'] }): string {
|
||||
export function format (media: Pick<Media, 'format'>): string {
|
||||
if (media.format != null) return FORMAT_MAP[media.format]
|
||||
|
||||
return 'N/A'
|
||||
}
|
||||
|
||||
export function episodes (media: Media): number | undefined {
|
||||
export function episodes (media: Pick<Media, 'aired' | 'notaired' | 'episodes' | 'mediaListEntry'>): number | undefined {
|
||||
if (media.episodes) return media.episodes
|
||||
|
||||
const upcoming = media.aired?.n?.[media.aired.n.length - 1]?.e ?? 0
|
||||
|
|
@ -115,16 +113,16 @@ export function episodes (media: Media): number | undefined {
|
|||
return Math.max(upcoming, past, progress)
|
||||
}
|
||||
|
||||
export function season (media: Media) {
|
||||
export function season (media: Pick<Media, 'season' | 'seasonYear'>) {
|
||||
return [media.season?.toLowerCase(), media.seasonYear].filter(s => s).join(' ')
|
||||
}
|
||||
|
||||
export function duration (media: Media) {
|
||||
export function duration (media: Pick<Media, 'duration'>) {
|
||||
if (!media.duration) return
|
||||
return `${media.duration} Minute${media.duration > 1 ? 's' : ''}`
|
||||
}
|
||||
|
||||
export function desc (media: Media) {
|
||||
export function desc (media: Pick<Media, 'description'>) {
|
||||
return notes(media.description?.replace(/<[^>]+>/g, '').replace(/\n+/g, '\n') ?? 'No description available.')
|
||||
}
|
||||
|
||||
|
|
@ -132,7 +130,7 @@ export function notes (string: string) {
|
|||
return string.replace(/\n?\(?Source: [^)]+\)?\n?/m, '').replace(/\n?Notes?:[ |\n][^\n]+\n?/m, '')
|
||||
}
|
||||
|
||||
export function isMovie (media: Media) {
|
||||
export function isMovie (media: Pick<Media, 'format' | 'title' | 'synonyms' | 'duration' | 'episodes'>) {
|
||||
if (media.format === 'MOVIE') return true
|
||||
if ([...Object.values(media.title ?? {}), ...media.synonyms ?? []].some(title => title?.toLowerCase().includes('movie'))) return true
|
||||
// if (!getParentForSpecial(media)) return true // this is good for checking movies, but false positives with normal TV shows
|
||||
|
|
|
|||
|
|
@ -1,31 +1,31 @@
|
|||
import { episodes, type Media } from '../anilist'
|
||||
|
||||
export function progress ({ mediaListEntry }: Media): number | undefined {
|
||||
export function progress ({ mediaListEntry }: Pick<Media, 'mediaListEntry'>): number | undefined {
|
||||
if (!mediaListEntry?.progress) return
|
||||
return mediaListEntry.progress
|
||||
}
|
||||
|
||||
export function fav (media: Media): boolean {
|
||||
export function fav (media: Pick<Media, 'isFavourite'>): boolean {
|
||||
return media.isFavourite
|
||||
}
|
||||
|
||||
export function list (media: Media): 'CURRENT' | 'PLANNING' | 'COMPLETED' | 'DROPPED' | 'PAUSED' | 'REPEATING' | null | undefined {
|
||||
export function list (media: Pick<Media, 'mediaListEntry'>): 'CURRENT' | 'PLANNING' | 'COMPLETED' | 'DROPPED' | 'PAUSED' | 'REPEATING' | null | undefined {
|
||||
return media.mediaListEntry?.status
|
||||
}
|
||||
|
||||
export function lists (media: Media): Array<{ enabled: boolean, name: string }> | undefined {
|
||||
export function lists (media: Pick<Media, 'mediaListEntry'>): Array<{ enabled: boolean, name: string }> | undefined {
|
||||
return media.mediaListEntry?.customLists as Array<{ enabled: boolean, name: string }> | undefined
|
||||
}
|
||||
|
||||
export function repeat (media: Media): number | null | undefined {
|
||||
export function repeat (media: Pick<Media, 'mediaListEntry'>): number | null | undefined {
|
||||
return media.mediaListEntry?.repeat
|
||||
}
|
||||
|
||||
export function score (media: Media): number | null | undefined {
|
||||
export function score (media: Pick<Media, 'mediaListEntry'>): number | null | undefined {
|
||||
return media.mediaListEntry?.score
|
||||
}
|
||||
|
||||
export function of (media: Media): string | undefined {
|
||||
export function of (media: Pick<Media, 'aired' | 'notaired' | 'episodes' | 'mediaListEntry'>): string | undefined {
|
||||
const count = episodes(media)
|
||||
if (count === 1 || !count) return
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
import { dedupeAiring, episodeByAirDate, isMovie, type Media, type MediaEdge } from '../anilist'
|
||||
import type { TorrentResult } from 'hayase-extensions'
|
||||
import { storage } from './storage'
|
||||
import { settings } from '../settings'
|
||||
import { settings, type videoResolutions } from '../settings'
|
||||
import { get } from 'svelte/store'
|
||||
import anitomyscript, { type AnitomyResult } from 'anitomyscript'
|
||||
import type { EpisodesResponse } from '../anizip/types'
|
||||
import { options as extensionOptions, saved } from '$lib/modules/extensions'
|
||||
import native from '../native'
|
||||
|
||||
const exclusions = ['DTS', 'TrueHD', '[EMBER]']
|
||||
const isDev = location.hostname === 'localhost'
|
||||
|
|
@ -57,7 +58,7 @@ export const extensions = new class Extensions {
|
|||
return titles
|
||||
}
|
||||
|
||||
async getResultsFromExtensions ({ media, episode, batch, resolution }: { media: Media, episode?: number, batch: boolean, resolution: '' | '1080' | '720' | '2160' | '540' | '480' }) {
|
||||
async getResultsFromExtensions ({ media, episode, batch, resolution }: { media: Media, episode?: number, batch: boolean, resolution: keyof typeof videoResolutions }) {
|
||||
await storage.modules
|
||||
const workers = storage.workers
|
||||
if (!Object.values(workers).length) {
|
||||
|
|
@ -85,7 +86,7 @@ export const extensions = new class Extensions {
|
|||
exclusions: get(settings).enableExternal ? [] : exclusions
|
||||
}
|
||||
|
||||
const results: Array<TorrentResult & { parseObject: AnitomyResult, extension: string[] }> = []
|
||||
const results: Array<TorrentResult & { parseObject: AnitomyResult, extension: Set<string> }> = []
|
||||
const errors: Array<{ error: Error, extension: string }> = []
|
||||
|
||||
const extopts = get(extensionOptions)
|
||||
|
|
@ -102,7 +103,7 @@ export const extensions = new class Extensions {
|
|||
|
||||
for (const result of await Promise.allSettled(promises)) {
|
||||
if (result.status === 'fulfilled') {
|
||||
results.push(...result.value.map(v => ({ ...v, extension: [id], parseObject: {} as unknown as AnitomyResult })))
|
||||
results.push(...result.value.map(v => ({ ...v, extension: new Set([id]), parseObject: {} as unknown as AnitomyResult })))
|
||||
} else {
|
||||
console.error(result.reason, id)
|
||||
errors.push({ error: result.reason as unknown as Error, extension: id })
|
||||
|
|
@ -117,45 +118,35 @@ export const extensions = new class Extensions {
|
|||
|
||||
const deduped = this.dedupe(results)
|
||||
|
||||
if (!deduped.length) throw new Error('No results found. Try specifying a torrent manually.')
|
||||
if (!deduped.length) throw new Error('No results found.\nTry specifying a torrent manually by pasting a magnet link or torrent file.')
|
||||
|
||||
const parseObjects = await anitomyscript(deduped.map(({ title }) => title))
|
||||
parseObjects.forEach((parseObject, index) => {
|
||||
deduped[index].parseObject = parseObject
|
||||
deduped[index]!.parseObject = parseObject
|
||||
})
|
||||
|
||||
console.log({ deduped })
|
||||
|
||||
return { results: deduped, errors }
|
||||
// return await this.updatePeerCounts(deduped) // TODO: re-enable
|
||||
return { results: await this.updatePeerCounts(deduped), errors }
|
||||
}
|
||||
|
||||
async updatePeerCounts (entries: TorrentResult[]) {
|
||||
// const id = Math.trunc(Math.random() * Number.MAX_SAFE_INTEGER).toString()
|
||||
async updatePeerCounts <T extends TorrentResult[]> (entries: T): Promise<T> {
|
||||
debug(`Updating peer counts for ${entries.length} entries`)
|
||||
|
||||
const updated = await new Promise<[] | null>(resolve => {
|
||||
resolve([])
|
||||
// function check ({ detail }) {
|
||||
// if (detail.id !== id) return
|
||||
// debug('Got scrape response')
|
||||
// client.removeListener('scrape', check)
|
||||
// resolve(detail.result)
|
||||
// }
|
||||
// client.on('scrape', check)
|
||||
// client.send('scrape', { id, infoHashes: entries.map(({ hash }) => hash) })
|
||||
})
|
||||
debug('Scrape complete')
|
||||
try {
|
||||
const updated = await native.updatePeerCounts(entries.map(({ hash }) => hash))
|
||||
debug('Scrape complete')
|
||||
for (const { hash, complete, downloaded, incomplete } of updated) {
|
||||
const found = entries.find(mapped => mapped.hash === hash)
|
||||
if (!found) continue
|
||||
found.downloads = downloaded
|
||||
found.leechers = incomplete
|
||||
found.seeders = complete
|
||||
}
|
||||
|
||||
for (const { hash, complete, downloaded, incomplete } of updated ?? []) {
|
||||
const found = entries.find(mapped => mapped.hash === hash)
|
||||
if (!found) continue
|
||||
found.downloads = downloaded
|
||||
found.leechers = incomplete
|
||||
found.seeders = complete
|
||||
debug(`Found ${updated.length} entries: ${JSON.stringify(updated)}`)
|
||||
} catch (err) {
|
||||
const error = err as Error
|
||||
debug('Failed to scrape\n' + error.stack)
|
||||
}
|
||||
|
||||
debug(`Found ${(updated ?? []).length} entries: ${JSON.stringify(updated)}`)
|
||||
return entries
|
||||
}
|
||||
|
||||
|
|
@ -200,12 +191,12 @@ export const extensions = new class Extensions {
|
|||
return episodeByAirDate(alDate, episodes!, episode)
|
||||
}
|
||||
|
||||
dedupe <T extends TorrentResult & { extension: string[] }> (entries: T[]): T[] {
|
||||
dedupe <T extends TorrentResult & { extension: Set<string> }> (entries: T[]): T[] {
|
||||
const deduped: Record<string, T> = {}
|
||||
for (const entry of entries) {
|
||||
if (entry.hash in deduped) {
|
||||
const dupe = deduped[entry.hash]
|
||||
dupe.extension.push(...entry.extension)
|
||||
const dupe = deduped[entry.hash]!
|
||||
for (const ext of entry.extension) dupe.extension.add(ext)
|
||||
dupe.accuracy = (['high', 'medium', 'low'].indexOf(entry.accuracy) <= ['high', 'medium', 'low'].indexOf(dupe.accuracy)
|
||||
? entry.accuracy
|
||||
: dupe.accuracy)
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { releaseProxy, type Remote } from 'abslink'
|
|||
import { persisted } from 'svelte-persisted-store'
|
||||
import { wrap } from 'abslink/worker'
|
||||
import { get } from 'svelte/store'
|
||||
import { toast } from 'svelte-sonner'
|
||||
|
||||
export const saved = persisted<Record<string, ExtensionConfig>>('extensions', {})
|
||||
export const options = persisted<Record<string, {options: Record<string, string | number | boolean | undefined>, enabled: boolean}>>('extensionoptions', {})
|
||||
|
|
@ -95,9 +96,9 @@ export const storage = new class Storage {
|
|||
|
||||
async import (url: string) {
|
||||
const config = await safejson<ExtensionConfig[]>(url)
|
||||
if (!config) throw new Error('Make sure the link you provided is a valid JSON config for Hayase.', { cause: 'Invalid extension URI.' })
|
||||
if (!config) throw new Error('Make sure the link you provided is a valid JSON config for Hayase', { cause: 'Invalid extension URI' })
|
||||
for (const c of config) {
|
||||
if (!this._validateConfig(c)) throw new Error('Make sure the link you provided is a valid extension config for Hayase.', { cause: 'Invalid extension config.' })
|
||||
if (!this._validateConfig(c)) throw new Error('Make sure the link you provided is a valid extension config for Hayase', { cause: 'Invalid extension config' })
|
||||
}
|
||||
for (const c of config) {
|
||||
saved.update(value => {
|
||||
|
|
@ -144,7 +145,7 @@ export const storage = new class Storage {
|
|||
} catch (e) {
|
||||
// worker.terminate()
|
||||
console.error(e, id)
|
||||
// TODO: what now?
|
||||
toast.error(`Failed to load extension ${config[id]!.name}`, { description: (e as Error).message })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -160,7 +161,7 @@ export const storage = new class Storage {
|
|||
const values = await Promise.all([...updateURLs].map(url => safejson<ExtensionConfig[]>(url)))
|
||||
const newconfig: Record<string, ExtensionConfig> = Object.fromEntries((values.flat().filter(f => this._validateConfig(f) && ids.includes(f!.id)) as ExtensionConfig[]).map((config) => [config.id, config]))
|
||||
|
||||
const toDelete = ids.filter(id => newconfig[id].version !== config[id].version)
|
||||
const toDelete = ids.filter(id => newconfig[id]?.version !== config[id]!.version)
|
||||
if (toDelete.length) {
|
||||
await delMany(toDelete)
|
||||
for (const id of toDelete) {
|
||||
|
|
|
|||
|
|
@ -1,13 +1,10 @@
|
|||
import type { AuthResponse, Native } from '../../app'
|
||||
|
||||
const native: Native = {
|
||||
export default Object.assign<Native, Partial<Native>>({
|
||||
authAL: (url: string) => {
|
||||
return new Promise<AuthResponse>((resolve, reject) => {
|
||||
const popup = open(url, 'authframe', 'popup')
|
||||
if (!popup) {
|
||||
reject(new Error('Failed to open popup'))
|
||||
return
|
||||
}
|
||||
if (!popup) return reject(new Error('Failed to open popup'))
|
||||
|
||||
popup.onload = () => {
|
||||
if (popup.location.hash.startsWith('#access_token=')) {
|
||||
|
|
@ -39,10 +36,6 @@ const native: Native = {
|
|||
setActionHandler: async (...args) => navigator.mediaSession.setActionHandler(...args as [action: MediaSessionAction, handler: MediaSessionActionHandler | null]),
|
||||
checkAvailableSpace: () => new Promise(resolve => setTimeout(() => resolve(Math.floor(Math.random() * (1e10 - 1e8 + 1) + 1e8)), 1000)),
|
||||
checkIncomingConnections: () => new Promise(resolve => setTimeout(() => resolve(Math.random() > 0.5), 5000)),
|
||||
updatePeerCounts: async () => [],
|
||||
isApp: false
|
||||
}
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
||||
globalThis.native ??= native
|
||||
|
||||
export default globalThis.native
|
||||
}, globalThis.native)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import SUPPORTS from './supports'
|
||||
import type { languageCodes, subtitleResolutions, videoResolutions } from './util'
|
||||
|
||||
export default {
|
||||
volume: 1,
|
||||
|
|
@ -7,10 +8,11 @@ export default {
|
|||
playerPause: true,
|
||||
playerAutocomplete: true,
|
||||
playerDeband: false,
|
||||
searchQuality: '1080',
|
||||
searchQuality: '1080' as keyof typeof videoResolutions,
|
||||
rssFeedsNew: SUPPORTS.isAndroid ? [['New Releases', 'SubsPlease']] : [],
|
||||
searchAutoSelect: true,
|
||||
searchBatch: true,
|
||||
lookupPreference: 'quality' as 'quality' | 'size' | 'seeders',
|
||||
torrentSpeed: 40,
|
||||
torrentPersist: false,
|
||||
torrentDHT: false,
|
||||
|
|
@ -20,18 +22,15 @@ export default {
|
|||
dhtPort: 0,
|
||||
missingFont: true,
|
||||
maxConns: 50,
|
||||
subtitleRenderHeight: SUPPORTS.isAndroid ? '720' : '0',
|
||||
subtitleLanguage: 'eng',
|
||||
audioLanguage: 'jpn',
|
||||
subtitleRenderHeight: SUPPORTS.isAndroid ? '720' : '0' as keyof typeof subtitleResolutions,
|
||||
subtitleLanguage: 'eng' as keyof typeof languageCodes,
|
||||
audioLanguage: 'jpn' as keyof typeof languageCodes,
|
||||
enableDoH: true,
|
||||
doHURL: 'https://cloudflare-dns.com/dns-query',
|
||||
disableSubtitleBlur: SUPPORTS.isAndroid,
|
||||
showDetailsInRPC: true,
|
||||
cards: 'small',
|
||||
torrentPath: '',
|
||||
angle: 'default',
|
||||
extensions: SUPPORTS.isAndroid ? ['anisearch'] : [],
|
||||
sources: {},
|
||||
angle: 'default' as 'default' | 'd3d11'| 'd3d9' | 'warp' | 'gl' | 'gles' | 'swiftshader' | 'vulkan' | 'metal',
|
||||
enableExternal: false,
|
||||
playerPath: '',
|
||||
playerSeek: 2,
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
/* eslint-disable quote-props */
|
||||
export const languageCodes = {
|
||||
eng: 'English',
|
||||
jpn: 'Japanese',
|
||||
|
|
@ -32,3 +33,25 @@ export const languageCodes = {
|
|||
mal: 'Malayalam',
|
||||
'': 'None'
|
||||
}
|
||||
|
||||
export const subtitleResolutions = {
|
||||
'0': 'None',
|
||||
'1440': '1440p',
|
||||
'1080': '1080p',
|
||||
'720': '720p',
|
||||
'480': '480p'
|
||||
}
|
||||
|
||||
export const videoResolutions = {
|
||||
'2160': '2160p',
|
||||
'1080': '1080p',
|
||||
'720': '720p',
|
||||
'480': '480p',
|
||||
'': 'Any'
|
||||
}
|
||||
|
||||
export const lookupPreferences = {
|
||||
quality: 'Quality',
|
||||
size: 'Size',
|
||||
seeders: 'Availability'
|
||||
}
|
||||
|
|
|
|||
|
|
@ -161,3 +161,56 @@ export function bindPiP (doc: Document, store: Writable<HTMLVideoElement | null>
|
|||
}, { signal: signal.signal })
|
||||
return { destroy: () => signal.abort() }
|
||||
}
|
||||
|
||||
interface TraceAnime {
|
||||
'anilist': number
|
||||
'filename': string
|
||||
'episode': number
|
||||
'from': number
|
||||
'to': number
|
||||
'similarity': number
|
||||
'video': string
|
||||
'image': string
|
||||
}
|
||||
|
||||
export async function traceAnime (image: File) { // WAIT lookup logic
|
||||
const options = {
|
||||
method: 'POST',
|
||||
body: image,
|
||||
headers: { 'Content-type': image.type }
|
||||
}
|
||||
const url = 'https://api.trace.moe/search'
|
||||
// let url = `https://api.trace.moe/search?cutBorders&url=${image}`
|
||||
|
||||
const res = await fetch(url, options)
|
||||
const { result } = await res.json() as { result: TraceAnime[] }
|
||||
|
||||
if (result.length) {
|
||||
return result
|
||||
// search.value = {
|
||||
// clearNext: true,
|
||||
// load: (page = 1, perPage = 50, variables = {}) => {
|
||||
// const res = anilistClient.searchIDS({ page, perPage, id: ids, ...SectionsManager.sanitiseObject(variables) }).then(res => {
|
||||
// for (const index in res.data?.Page?.media) {
|
||||
// const media = res.data.Page.media[index]
|
||||
// const counterpart = result.find(({ anilist }) => anilist === media.id)
|
||||
// res.data.Page.media[index] = {
|
||||
// media,
|
||||
// episode: counterpart.episode,
|
||||
// similarity: counterpart.similarity,
|
||||
// episodeData: {
|
||||
// image: counterpart.image,
|
||||
// video: counterpart.video
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// res.data?.Page?.media.sort((a, b) => b.similarity - a.similarity)
|
||||
// return res
|
||||
// })
|
||||
// return SectionsManager.wrapResponse(res, result.length, 'episode')
|
||||
// }
|
||||
// }
|
||||
} else {
|
||||
throw new Error('Search Failed \n Couldn\'t find anime for specified image! Try to remove black bars, or use a more detailed image.')
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -109,7 +109,7 @@
|
|||
Ooops!
|
||||
</div>
|
||||
<div class='text-lg text-center text-muted-foreground'>
|
||||
Looks like something went wrong!.
|
||||
Looks like something went wrong!
|
||||
</div>
|
||||
<div class='text-lg text-center text-muted-foreground'>
|
||||
{$query.error.message}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@
|
|||
import { genres, years, seasons, formats, status, sort } from './values'
|
||||
import { Button } from '$lib/components/ui/button'
|
||||
import { FileImage, Trash, X } from 'lucide-svelte'
|
||||
import { cn, debounce } from '$lib/utils'
|
||||
import { cn, debounce, traceAnime } from '$lib/utils'
|
||||
import { badgeVariants } from '$lib/components/ui/badge'
|
||||
import { click, dragScroll } from '$lib/modules/navigate'
|
||||
import { client } from '$lib/modules/anilist'
|
||||
|
|
@ -15,6 +15,7 @@
|
|||
import { page } from '$app/stores'
|
||||
import type { VariablesOf } from 'gql.tada'
|
||||
import type { Search } from '$lib/modules/anilist/queries'
|
||||
import { toast } from 'svelte-sonner'
|
||||
|
||||
// util
|
||||
|
||||
|
|
@ -118,8 +119,8 @@
|
|||
ids: filter.ids,
|
||||
search: filter.name,
|
||||
genre: filter.genres?.map(g => g.value),
|
||||
seasonYear: filter.years ? parseInt(filter.years[0].value) : undefined,
|
||||
season: filter.seasons?.[0].value as 'WINTER' | 'SPRING' | 'SUMMER' | 'FALL' | undefined,
|
||||
seasonYear: filter.years ? parseInt(filter.years[0]!.value) : undefined,
|
||||
season: filter.seasons?.[0]!.value as 'WINTER' | 'SPRING' | 'SUMMER' | 'FALL' | undefined,
|
||||
format: filter.formats?.map(f => f.value) as Array<'MUSIC' | 'MANGA' | 'TV' | 'TV_SHORT' | 'MOVIE' | 'SPECIAL' | 'OVA' | 'ONA' | 'NOVEL' | 'ONE_SHOT'>,
|
||||
status: filter.status?.map(s => s.value) as Array<'FINISHED' | 'RELEASING' | 'NOT_YET_RELEASED' | 'CANCELLED' | 'HIATUS' | null>,
|
||||
sort: [filter.sort?.[0]?.value ?? 'SEARCH_MATCH'] as Array<'TITLE_ROMAJI_DESC' | 'ID' | 'START_DATE_DESC' | 'SCORE_DESC' | 'POPULARITY_DESC' | 'TRENDING_DESC' | 'UPDATED_AT_DESC' | 'ID_DESC' | 'TITLE_ROMAJI' | 'TITLE_ENGLISH' | 'TITLE_ENGLISH_DESC' | null>
|
||||
|
|
@ -137,12 +138,37 @@
|
|||
// page = page + 1
|
||||
// media = [...media, searchQuery(filterEmpty(search), page)]
|
||||
// }
|
||||
// TODO: selects should turn into modals on mobile!
|
||||
// TODO: selects should turn into modals on mobile! like anilist
|
||||
// TODO: infinite scroll
|
||||
onMount(async () => {
|
||||
await tick()
|
||||
hideBanner.value = true
|
||||
})
|
||||
|
||||
async function imagePicker (e: Event) {
|
||||
const target = e.target as HTMLInputElement
|
||||
const { files } = target
|
||||
if (files?.[0]) {
|
||||
const promise = traceAnime(files[0])
|
||||
toast.promise(promise, {
|
||||
description: 'You can also paste an URL to an image.',
|
||||
loading: 'Looking up anime for image...',
|
||||
success: 'Found anime for image!',
|
||||
error: 'Couldn\'t find anime for specified image! Try to remove black bars, or use a more detailed image.'
|
||||
})
|
||||
target.value = ''
|
||||
|
||||
try {
|
||||
const res = await promise
|
||||
|
||||
clear()
|
||||
search.ids = [...new Set(res.map(r => r.anilist))]
|
||||
// TODO: sort by similarity
|
||||
} catch (e) {
|
||||
console.error(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class='flex flex-col h-full overflow-y-auto overflow-x-clip -ml-14 pl-14 z-20 min-w-0 grow pointer-events-none' use:dragScroll>
|
||||
|
|
@ -199,12 +225,15 @@
|
|||
<ComboBox items={sort} bind:value={search.sort} class='w-full' placeholder='Accuracy' />
|
||||
</div>
|
||||
<div class='flex-0 w-auto p-2 gap-4 grid grid-cols-2 items-end'>
|
||||
<Button variant='outline' size='icon' class='border-0'>
|
||||
<label for='search-image' class='contents'>
|
||||
<FileImage class='h-4 w-full cursor-pointer' />
|
||||
</label>
|
||||
<input type='file' class='hidden' id='search-image' accept='image/*' on:input|preventDefault|stopPropagation={imagePicker} />
|
||||
</Button>
|
||||
<Button variant='outline' size='icon' on:click={clear} class='border-0'>
|
||||
<Trash class={cn('h-4 w-4', empty(search) && 'text-muted-foreground opacity-50')} />
|
||||
</Button>
|
||||
<Button variant='outline' size='icon' class='border-0'>
|
||||
<FileImage class='h-4 w-4' />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class='flex flex-row flex-wrap mt-2 min-h-11 pb-3 px-1'>
|
||||
|
|
|
|||
|
|
@ -6,15 +6,8 @@
|
|||
import { Switch } from '$lib/components/ui/switch'
|
||||
|
||||
import native from '$lib/modules/native'
|
||||
import { settings, languageCodes } from '$lib/modules/settings'
|
||||
import { settings, languageCodes, subtitleResolutions } from '$lib/modules/settings'
|
||||
|
||||
const resolutions = {
|
||||
0: 'None',
|
||||
1440: '1440p',
|
||||
1080: '1080p',
|
||||
720: '720p',
|
||||
480: '480p'
|
||||
}
|
||||
async function selectPlayer () {
|
||||
$settings.playerPath = await native.selectPlayer()
|
||||
}
|
||||
|
|
@ -32,7 +25,7 @@
|
|||
<Switch {id} bind:checked={$settings.disableSubtitleBlur} />
|
||||
</SettingCard>
|
||||
<SettingCard title='Subtitle Render Resolution Limit' description="Max resolution to render subtitles at. If your resolution is higher than this setting the subtitles will be upscaled lineary. This will GREATLY improve rendering speeds for complex typesetting for slower devices. It's best to lower this on mobile devices which often have high pixel density where their effective resolution might be ~1440p while having small screens and slow processors.">
|
||||
<SingleCombo bind:value={$settings.subtitleRenderHeight} items={resolutions} class='w-32 shrink-0 border-input border' />
|
||||
<SingleCombo bind:value={$settings.subtitleRenderHeight} items={subtitleResolutions} class='w-32 shrink-0 border-input border' />
|
||||
</SettingCard>
|
||||
|
||||
<div class='font-weight-bold text-xl font-bold'>Language Settings</div>
|
||||
|
|
|
|||
|
|
@ -2,29 +2,37 @@
|
|||
import Anilist from '$lib/components/icons/Anilist.svelte'
|
||||
import * as Avatar from '$lib/components/ui/avatar'
|
||||
import { Button } from '$lib/components/ui/button'
|
||||
import { Label } from '$lib/components/ui/label'
|
||||
import { Switch } from '$lib/components/ui/switch'
|
||||
import { client } from '$lib/modules/anilist'
|
||||
import { persisted } from 'svelte-persisted-store'
|
||||
import native from '$lib/modules/native'
|
||||
import { click } from '$lib/modules/navigate'
|
||||
|
||||
const viewer = client.viewer
|
||||
|
||||
$: anilist = $viewer
|
||||
// TODO: allow disabling of syncing
|
||||
|
||||
const syncSettings = persisted('syncSettings', { al: true })
|
||||
</script>
|
||||
|
||||
<div class='space-y-3 pb-10 lg:max-w-4xl'>
|
||||
<div class='font-weight-bold text-xl font-bold'>Account Settings</div>
|
||||
<div>
|
||||
<div class='bg-neutral-900 px-6 py-4 rounded-t-md flex flex-row space-x-3'>
|
||||
<div class='bg-neutral-900 px-6 py-4 rounded-t-md flex flex-row gap-3'>
|
||||
{#if anilist?.viewer?.id}
|
||||
<Avatar.Root class='size-8 rounded-md'>
|
||||
<Avatar.Image src={anilist.viewer.avatar?.medium ?? ''} alt={anilist.viewer.name} />
|
||||
<Avatar.Fallback>{anilist.viewer.name}</Avatar.Fallback>
|
||||
</Avatar.Root>
|
||||
<div class='flex flex-col'>
|
||||
<div class='text-sm'>
|
||||
{anilist.viewer.name}
|
||||
</div>
|
||||
<div class='text-[9px] text-muted-foreground'>
|
||||
AniList
|
||||
<div use:click={() => native.openURL(`https://anilist.co/user/${anilist.viewer?.name}`)} class='flex flex-row gap-3'>
|
||||
<Avatar.Root class='size-8 rounded-md'>
|
||||
<Avatar.Image src={anilist.viewer.avatar?.medium ?? ''} alt={anilist.viewer.name} />
|
||||
<Avatar.Fallback>{anilist.viewer.name}</Avatar.Fallback>
|
||||
</Avatar.Root>
|
||||
<div class='flex flex-col'>
|
||||
<div class='text-sm'>
|
||||
{anilist.viewer.name}
|
||||
</div>
|
||||
<div class='text-[9px] text-muted-foreground leading-snug'>
|
||||
AniList
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
|
|
@ -32,12 +40,16 @@
|
|||
{/if}
|
||||
<Anilist class='size-6 !ml-auto' />
|
||||
</div>
|
||||
<div class='bg-neutral-950 px-6 py-4 rounded-b-md'>
|
||||
<div class='bg-neutral-950 px-6 py-4 rounded-b-md flex justify-between'>
|
||||
{#if anilist?.viewer?.id}
|
||||
<Button variant='secondary' on:click={() => client.logout()}>Logout</Button>
|
||||
{:else}
|
||||
<Button variant='secondary' on:click={() => client.auth()}>Login</Button>
|
||||
{/if}
|
||||
<div class='flex gap-2 items-center'>
|
||||
<Switch hideState={true} id='al-sync-switch' bind:checked={$syncSettings.al} />
|
||||
<Label for='al-sync-switch' class='cursor-pointer'>Enable Sync</Label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,11 +1,23 @@
|
|||
<script lang='ts'>
|
||||
import { Extensions } from '$lib/components/ui/extensions'
|
||||
import { SingleCombo } from '$lib/components/ui/combobox'
|
||||
import SettingCard from '$lib/components/SettingCard.svelte'
|
||||
import { Switch } from '$lib/components/ui/switch'
|
||||
import { lookupPreferences, settings, videoResolutions } from '$lib/modules/settings'
|
||||
</script>
|
||||
|
||||
<div class='space-y-3 pb-10'>
|
||||
<div class='font-weight-bold text-xl font-bold'>Extension Settings</div>
|
||||
<div class='space-y-3 pb-10 lg:max-w-4xl'>
|
||||
<div class='font-weight-bold text-xl font-bold'>Lookup Settings</div>
|
||||
<SettingCard title='Torrent Quality' description="What quality to use when trying to find torrents. None might rarely find less results than specific qualities. This doesn't exclude other qualities from being found like 4K or weird DVD resolutions. Non-1080p resolutions might not be available for all shows, or find way less results.">
|
||||
<SingleCombo bind:value={$settings.searchQuality} items={videoResolutions} class='w-32 shrink-0 border-input border' />
|
||||
</SettingCard>
|
||||
<SettingCard let:id title='Auto-Select Torrents' description='Automatically selects torrents based on quality and amount of seeders. Disable this to have more precise control over played torrents.'>
|
||||
<Switch {id} bind:checked={$settings.searchAutoSelect} />
|
||||
</SettingCard>
|
||||
<SettingCard title='Lookup Preference' description='What to prioritize when looking for and sorting results. Quality will focus on the best quality available which often means big file sizes, Size will focus on the smallest file size available, and Availability will pick results with the most peers regardless of size and quality.'>
|
||||
<SingleCombo bind:value={$settings.lookupPreference} items={lookupPreferences} class='w-32 shrink-0 border-input border' />
|
||||
</SettingCard>
|
||||
|
||||
<div class='space-y-3 lg:max-w-4xl h-full overflow-y-auto w-full'>
|
||||
<Extensions />
|
||||
</div>
|
||||
<div class='font-weight-bold text-xl font-bold'>Extension Settings</div>
|
||||
<Extensions />
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
<script lang='ts' context='module'>
|
||||
// @ts-expect-error internal methods
|
||||
// eslint-disable-next-line svelte/no-svelte-internal
|
||||
import { append, element } from 'svelte/internal'
|
||||
import { persisted } from 'svelte-persisted-store'
|
||||
|
||||
|
|
|
|||
|
|
@ -1,34 +1,20 @@
|
|||
<script lang='ts'>
|
||||
import SettingCard from '$lib/components/SettingCard.svelte'
|
||||
import { Button } from '$lib/components/ui/button'
|
||||
import { SingleCombo } from '$lib/components/ui/combobox'
|
||||
import { Input } from '$lib/components/ui/input'
|
||||
import { Switch } from '$lib/components/ui/switch'
|
||||
|
||||
import native from '$lib/modules/native'
|
||||
import { settings, SUPPORTS } from '$lib/modules/settings'
|
||||
|
||||
const resolutions = {
|
||||
1080: '1080p',
|
||||
720: '720p',
|
||||
480: '480p',
|
||||
'': 'Any'
|
||||
}
|
||||
|
||||
async function selectDownloadFolder () {
|
||||
$settings.torrentPath = await native.selectDownload()
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class='space-y-3 pb-10 lg:max-w-4xl'>
|
||||
<div class='font-weight-bold text-xl font-bold'>Lookup Settings</div>
|
||||
<SettingCard title='Torrent Quality' description="What quality to use when trying to find torrents. None might rarely find less results than specific qualities. This doesn't exclude other qualities from being found like 4K or weird DVD resolutions.">
|
||||
<SingleCombo bind:value={$settings.searchQuality} items={resolutions} class='w-32 shrink-0 border-input border' />
|
||||
</SettingCard>
|
||||
<SettingCard let:id title='Auto-Select Torrents' description='Automatically selects torrents based on quality and amount of seeders. Disable this to have more precise control over played torrents.'>
|
||||
<Switch {id} bind:checked={$settings.searchAutoSelect} />
|
||||
</SettingCard>
|
||||
{#if !SUPPORTS.isAndroid}
|
||||
<div class='font-weight-bold text-xl font-bold'>Security Settings</div>
|
||||
<SettingCard let:id title='Use DNS Over HTTPS' description='Enables DNS Over HTTPS, useful if your ISP blocks certain domains.'>
|
||||
<Switch {id} bind:checked={$settings.enableDoH} />
|
||||
</SettingCard>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,10 @@
|
|||
<script lang='ts'>
|
||||
import { WEB_URL } from '$lib'
|
||||
import { Button } from '$lib/components/ui/button'
|
||||
import { Checkbox } from '$lib/components/ui/checkbox'
|
||||
import { Label } from '$lib/components/ui/label'
|
||||
import { click } from '$lib/modules/navigate'
|
||||
import native from '$lib/modules/native'
|
||||
|
||||
let checked = false
|
||||
</script>
|
||||
|
|
@ -12,7 +15,7 @@
|
|||
<div class='flex items-center space-x-2 pt-12'>
|
||||
<Checkbox id='terms' bind:checked />
|
||||
<Label for='terms' class='text-md font-medium leading-none text-muted-foreground'>
|
||||
I agree to the <a href='/terms' class='text-primary underline'>Terms of Service</a> and <a href='/privacy' class='text-primary underline'>Privacy Policy</a>
|
||||
I agree to the <a use:click={() => native.openURL(`${WEB_URL}/terms`)} class='text-primary underline'>Terms of Service</a> and <a use:click={() => native.openURL(`${WEB_URL}/terms`)} class='text-primary underline'>Privacy Policy</a>
|
||||
</Label>
|
||||
</div>
|
||||
<Button class='mt-8 text-lg font-bold' disabled={!checked} size='lg' href={checked ? './storage' : undefined}>Start Setup</Button>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,9 @@
|
|||
import { Extensions } from '$lib/components/ui/extensions'
|
||||
import { saved, options } from '$lib/modules/extensions'
|
||||
import type { ExtensionConfig } from 'hayase-extensions'
|
||||
import SettingCard from '$lib/components/SettingCard.svelte'
|
||||
import { SingleCombo } from '$lib/components/ui/combobox'
|
||||
import { lookupPreferences, settings } from '$lib/modules/settings'
|
||||
|
||||
function checkExtensions (svd: Record<string, ExtensionConfig>, opts: Record<string, {
|
||||
options: Record<string, string | number | boolean | undefined>
|
||||
|
|
@ -30,8 +33,14 @@
|
|||
|
||||
<Progress step={2} />
|
||||
|
||||
<div class='space-y-3 lg:max-w-4xl h-full overflow-y-auto w-full px-6 py-8'>
|
||||
<Extensions />
|
||||
<div class='space-y-3 lg:max-w-4xl h-full overflow-y-auto w-full py-8'>
|
||||
<SettingCard title='Lookup Preference' description='What to prioritize when looking for and sorting results. Quality will focus on the best quality available which often means big file sizes, Size will focus on the smallest file size available, and Availability will pick results with the most peers regardless of size and quality.'>
|
||||
<SingleCombo bind:value={$settings.lookupPreference} items={lookupPreferences} class='w-32 shrink-0 border-input border' />
|
||||
</SettingCard>
|
||||
<div class='px-6'>
|
||||
<Extensions />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<Footer step={2} {checks} />
|
||||
|
|
|
|||
Loading…
Reference in a new issue