mirror of
https://github.com/ThaUnknown/miru.git
synced 2026-03-11 21:05:34 +00:00
feat: library and files filtering
feat: library rescan and delete buttons [disabled for now]
This commit is contained in:
parent
7e8ac47cdf
commit
9e1cdb7017
21 changed files with 1232 additions and 511 deletions
51
package.json
51
package.json
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "ui",
|
"name": "ui",
|
||||||
"version": "6.4.111",
|
"version": "6.4.112",
|
||||||
"license": "BUSL-1.1",
|
"license": "BUSL-1.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"packageManager": "pnpm@9.15.5",
|
"packageManager": "pnpm@9.15.5",
|
||||||
|
|
@ -20,8 +20,8 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@gql.tada/svelte-support": "^1.0.1",
|
"@gql.tada/svelte-support": "^1.0.1",
|
||||||
"@sveltejs/adapter-static": "^3.0.8",
|
"@sveltejs/adapter-static": "^3.0.9",
|
||||||
"@sveltejs/kit": "^2.21.0",
|
"@sveltejs/kit": "^2.37.0",
|
||||||
"@sveltejs/vite-plugin-svelte": "^3.1.2",
|
"@sveltejs/vite-plugin-svelte": "^3.1.2",
|
||||||
"@types/debug": "^4.1.12",
|
"@types/debug": "^4.1.12",
|
||||||
"@types/semver": "^7.7.0",
|
"@types/semver": "^7.7.0",
|
||||||
|
|
@ -29,8 +29,8 @@
|
||||||
"autoprefixer": "^10.4.21",
|
"autoprefixer": "^10.4.21",
|
||||||
"bits-ui": "^0.22.0",
|
"bits-ui": "^0.22.0",
|
||||||
"cmdk-sv": "^0.0.19",
|
"cmdk-sv": "^0.0.19",
|
||||||
"eslint-config-standard-universal": "^1.0.8",
|
"eslint-config-standard-universal": "^1.0.9",
|
||||||
"gql.tada": "^1.8.10",
|
"gql.tada": "^1.8.13",
|
||||||
"hayase-extensions": "github:hayase-app/extensions",
|
"hayase-extensions": "github:hayase-app/extensions",
|
||||||
"jassub": "^1.8.6",
|
"jassub": "^1.8.6",
|
||||||
"ms": "^2.1.3",
|
"ms": "^2.1.3",
|
||||||
|
|
@ -38,44 +38,45 @@
|
||||||
"rollup-plugin-license": "^3.6.0",
|
"rollup-plugin-license": "^3.6.0",
|
||||||
"simple-copy": "^2.2.1",
|
"simple-copy": "^2.2.1",
|
||||||
"svelte": "^4.2.19",
|
"svelte": "^4.2.19",
|
||||||
"svelte-check": "^4.2.1",
|
"svelte-check": "^4.3.1",
|
||||||
"svelte-radix": "^1.1.1",
|
"svelte-radix": "^1.1.1",
|
||||||
"svelte-sonner": "^0.3.28",
|
"svelte-sonner": "^0.3.28",
|
||||||
"tailwindcss": "^3.4.17",
|
"tailwindcss": "^3.4.17",
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^5.9.2",
|
||||||
"vaul-svelte": "^0.3.2",
|
"vaul-svelte": "^0.3.2",
|
||||||
"vite": "^5.4.11",
|
"vite": "^5.4.11",
|
||||||
"vite-plugin-static-copy": "^3.0.2"
|
"vite-plugin-devtools-json": "^1.0.0",
|
||||||
|
"vite-plugin-static-copy": "^3.1.2"
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cloudflare/speedtest": "^1.4.1",
|
"@cloudflare/speedtest": "^1.6.0",
|
||||||
"@fontsource-variable/nunito": "^5.2.5",
|
"@fontsource-variable/nunito": "^5.2.6",
|
||||||
"@fontsource/geist-mono": "^5.2.6",
|
"@fontsource/geist-mono": "^5.2.6",
|
||||||
"@prgm/sveltekit-progress-bar": "2.0.0",
|
"@prgm/sveltekit-progress-bar": "2.0.0",
|
||||||
"@thaunknown/web-irc": "^1.0.3",
|
"@thaunknown/web-irc": "^1.0.3",
|
||||||
"@urql/core": "^5.2.0",
|
"@urql/core": "^6.0.1",
|
||||||
"@urql/exchange-auth": "^2.2.1",
|
"@urql/exchange-auth": "^3.0.0",
|
||||||
"@urql/exchange-graphcache": "^7.2.3",
|
"@urql/exchange-graphcache": "^8.1.0",
|
||||||
"@urql/exchange-refocus": "^1.1.1",
|
"@urql/exchange-refocus": "^2.0.0",
|
||||||
"@urql/exchange-request-policy": "^1.2.1",
|
"@urql/exchange-request-policy": "^2.0.0",
|
||||||
"@urql/exchange-retry": "^1.3.1",
|
"@urql/exchange-retry": "^2.0.0",
|
||||||
"@urql/svelte": "^4.2.3",
|
"@urql/svelte": "^5.0.0",
|
||||||
"abslink": "^1.1.0",
|
"abslink": "^1.1.2",
|
||||||
"anitomyscript": "github:thaunknown/anitomyscript",
|
"anitomyscript": "github:thaunknown/anitomyscript",
|
||||||
"bittorrent-tracker": "10.0.12",
|
"bittorrent-tracker": "11.2.1",
|
||||||
"bottleneck": "^2.19.5",
|
"bottleneck": "^2.19.5",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"cobe": "0.6.3",
|
"cobe": "0.6.3",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"debug": "^4.4.1",
|
"debug": "^4.4.1",
|
||||||
"doc999tor-fast-geoip": "^1.1.335",
|
"doc999tor-fast-geoip": "^1.1.360",
|
||||||
"dompurify": "^3.2.5",
|
"dompurify": "^3.2.6",
|
||||||
"events": "^3.3.0",
|
"events": "^3.3.0",
|
||||||
"idb-keyval": "^6.2.2",
|
"idb-keyval": "^6.2.2",
|
||||||
"js-levenshtein": "^1.1.6",
|
"js-levenshtein": "^1.1.6",
|
||||||
"lucide-svelte": "^0.511.0",
|
"lucide-svelte": "^0.542.0",
|
||||||
"marked": "^15.0.11",
|
"marked": "^16.2.1",
|
||||||
"overtype": "^1.2.3",
|
"overtype": "^1.2.3",
|
||||||
"p2pt": "github:ThaUnknown/p2pt#modernise",
|
"p2pt": "github:ThaUnknown/p2pt#modernise",
|
||||||
"semver": "^7.7.2",
|
"semver": "^7.7.2",
|
||||||
|
|
@ -83,10 +84,10 @@
|
||||||
"svelte-headless-table": "^0.18.3",
|
"svelte-headless-table": "^0.18.3",
|
||||||
"svelte-keybinds": "^1.0.9",
|
"svelte-keybinds": "^1.0.9",
|
||||||
"svelte-persisted-store": "^0.12.0",
|
"svelte-persisted-store": "^0.12.0",
|
||||||
"tailwind-merge": "^3.3.0",
|
"tailwind-merge": "^3.3.1",
|
||||||
"tailwind-variants": "^1.0.0",
|
"tailwind-variants": "^1.0.0",
|
||||||
"uint8-util": "^2.2.5",
|
"uint8-util": "^2.2.5",
|
||||||
"urql": "^4.2.2",
|
"urql": "^5.0.1",
|
||||||
"video-deband": "^1.0.9",
|
"video-deband": "^1.0.9",
|
||||||
"wonka": "^6.3.5",
|
"wonka": "^6.3.5",
|
||||||
"workbox-core": "^7.3.0",
|
"workbox-core": "^7.3.0",
|
||||||
|
|
|
||||||
1456
pnpm-lock.yaml
1456
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
|
|
@ -167,7 +167,7 @@ a {
|
||||||
-webkit-user-drag: none;
|
-webkit-user-drag: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
:fullscreen {
|
#episodeListTarget:fullscreen {
|
||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@
|
||||||
const key = 'active-settings-tab'
|
const key = 'active-settings-tab'
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<nav class={cn('md:flex grid grid-cols-2 md:flex-row lg:flex-col gap-y-1 gap-x-2', className)}>
|
<nav class={cn('md:flex grid grid-cols-2 md:flex-row lg:flex-col gap-y-1 gap-x-2 pb-2 sm:pb-0', className)}>
|
||||||
{#each items as { href, title }, i (i)}
|
{#each items as { href, title }, i (i)}
|
||||||
{@const isActive = $page.url.pathname === href}
|
{@const isActive = $page.url.pathname === href}
|
||||||
<Button {href} variant='ghost' data-sveltekit-noscroll class='relative font-semibold justify-start last:odd:col-span-2'>
|
<Button {href} variant='ghost' data-sveltekit-noscroll class='relative font-semibold justify-start last:odd:col-span-2'>
|
||||||
|
|
|
||||||
43
src/lib/components/icons/animated/foldersync.svelte
Normal file
43
src/lib/components/icons/animated/foldersync.svelte
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
<script lang='ts'>
|
||||||
|
import { cn } from '$lib/utils'
|
||||||
|
|
||||||
|
export let color = 'currentColor'
|
||||||
|
export let size = 24
|
||||||
|
export let strokeWidth = 2
|
||||||
|
let className = ''
|
||||||
|
|
||||||
|
export { className as class }
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svg
|
||||||
|
xmlns='http://www.w3.org/2000/svg'
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
viewBox='0 0 24 24'
|
||||||
|
fill='none'
|
||||||
|
stroke={color}
|
||||||
|
stroke-width={strokeWidth}
|
||||||
|
stroke-linecap='round'
|
||||||
|
stroke-linejoin='round'
|
||||||
|
class={cn(className, 'overflow-visible')}
|
||||||
|
{...$$restProps}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d='M9 20H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H20a2 2 0 0 1 2 2v.5'
|
||||||
|
/>
|
||||||
|
<g class='target-animated-icon'>
|
||||||
|
<path d='M12 10v4h4' />
|
||||||
|
<path d='m12 14 1.535-1.605a5 5 0 0 1 8 1.5' />
|
||||||
|
<path d='M22 22v-4h-4' />
|
||||||
|
<path d='m22 18-1.535 1.605a5 5 0 0 1-8-1.5' />
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.target-animated-icon {
|
||||||
|
transform-origin: center;
|
||||||
|
transform-box: fill-box;
|
||||||
|
transition: transform 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275);
|
||||||
|
transform: rotate(-50deg);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -16,3 +16,4 @@ export { default as Trash } from './trash.svelte'
|
||||||
export { default as FileImage } from './fileimage.svelte'
|
export { default as FileImage } from './fileimage.svelte'
|
||||||
export { default as Minimize } from './minimize.svelte'
|
export { default as Minimize } from './minimize.svelte'
|
||||||
export { default as Maximize } from './maximize.svelte'
|
export { default as Maximize } from './maximize.svelte'
|
||||||
|
export { default as FolderSync } from './foldersync.svelte'
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@
|
||||||
<slot />
|
<slot />
|
||||||
</Button>
|
</Button>
|
||||||
</Dialog.Trigger>
|
</Dialog.Trigger>
|
||||||
<Dialog.Content tabindex={null} class='gap-4 bottom-0 border-b-0 !translate-y-[unset] p-0 top-[unset] !pb-4 flex flex-col h-full sm:h-1/2'>
|
<Dialog.Content tabindex={null} class='gap-4 bottom-0 border-b-0 !translate-y-[unset] p-0 top-[unset] !pb-4 flex flex-col h-[90%] sm:h-1/2'>
|
||||||
<Markdown class='form-control w-full shrink-0 min-h-56 rounded-none flex-grow' {placeholder} bind:value />
|
<Markdown class='form-control w-full shrink-0 min-h-56 rounded-none flex-grow' {placeholder} bind:value />
|
||||||
<div class='flex gap-2 justify-end flex-grow-0 px-4'>
|
<div class='flex gap-2 justify-end flex-grow-0 px-4'>
|
||||||
<Dialog.Close asChild let:builder>
|
<Dialog.Close asChild let:builder>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
<script lang='ts'>
|
<script lang='ts'>
|
||||||
import { Render, Subscribe, createRender, createTable } from 'svelte-headless-table'
|
import { Render, Subscribe, createRender, createTable } from 'svelte-headless-table'
|
||||||
import { addSortBy } from 'svelte-headless-table/plugins'
|
import { addSortBy, addTableFilter } from 'svelte-headless-table/plugins'
|
||||||
|
import MagnifyingGlass from 'svelte-radix/MagnifyingGlass.svelte'
|
||||||
|
|
||||||
|
import { Input } from '../../input'
|
||||||
import Columnheader from '../columnheader.svelte'
|
import Columnheader from '../columnheader.svelte'
|
||||||
|
|
||||||
import { NameCell, ProgressCell } from './cells'
|
import { NameCell, ProgressCell } from './cells'
|
||||||
|
|
@ -11,7 +13,10 @@
|
||||||
import { cn, fastPrettyBytes } from '$lib/utils'
|
import { cn, fastPrettyBytes } from '$lib/utils'
|
||||||
|
|
||||||
const table = createTable(server.files, {
|
const table = createTable(server.files, {
|
||||||
sort: addSortBy({ toggleOrder: ['asc', 'desc'] })
|
sort: addSortBy({ toggleOrder: ['asc', 'desc'] }),
|
||||||
|
filter: addTableFilter({
|
||||||
|
fn: ({ filterValue, value }) => value.toLowerCase().includes(filterValue.toLowerCase())
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
const columns = table.createColumns([
|
const columns = table.createColumns([
|
||||||
|
|
@ -25,12 +30,14 @@
|
||||||
accessor: 'size',
|
accessor: 'size',
|
||||||
header: 'Size',
|
header: 'Size',
|
||||||
id: 'size',
|
id: 'size',
|
||||||
|
plugins: { filter: { exclude: true } },
|
||||||
cell: ({ value }) => fastPrettyBytes(value)
|
cell: ({ value }) => fastPrettyBytes(value)
|
||||||
}),
|
}),
|
||||||
table.column({
|
table.column({
|
||||||
accessor: 'progress',
|
accessor: 'progress',
|
||||||
header: 'Progress',
|
header: 'Progress',
|
||||||
id: 'progress',
|
id: 'progress',
|
||||||
|
plugins: { filter: { exclude: true } },
|
||||||
cell: ({ value }) => createRender(ProgressCell, { value })
|
cell: ({ value }) => createRender(ProgressCell, { value })
|
||||||
}),
|
}),
|
||||||
table.column({ accessor: 'selections', header: 'Streams', id: 'selections' })
|
table.column({ accessor: 'selections', header: 'Streams', id: 'selections' })
|
||||||
|
|
@ -38,9 +45,18 @@
|
||||||
|
|
||||||
const tableModel = table.createViewModel(columns)
|
const tableModel = table.createViewModel(columns)
|
||||||
|
|
||||||
const { headerRows, pageRows, tableAttrs, tableBodyAttrs } = tableModel
|
const { headerRows, pageRows, tableAttrs, tableBodyAttrs, pluginStates } = tableModel
|
||||||
|
|
||||||
|
const filterValue = pluginStates.filter.filterValue
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<div class='flex items-center scale-parent relative pb-2 overflow-visible'>
|
||||||
|
<Input
|
||||||
|
class='pl-9 bg-black select:bg-accent select:text-accent-foreground shadow-sm no-scale placeholder:opacity-50'
|
||||||
|
placeholder='Search by File Name...'
|
||||||
|
bind:value={$filterValue} />
|
||||||
|
<MagnifyingGlass class='h-4 w-4 shrink-0 opacity-50 absolute left-3 text-muted-foreground z-10 pointer-events-none' />
|
||||||
|
</div>
|
||||||
<div class='rounded-md border size-full overflow-clip contain-strict'>
|
<div class='rounded-md border size-full overflow-clip contain-strict'>
|
||||||
<Table.Root {...$tableAttrs} class='max-h-full'>
|
<Table.Root {...$tableAttrs} class='max-h-full'>
|
||||||
<Table.Header class='px-5'>
|
<Table.Header class='px-5'>
|
||||||
|
|
@ -74,7 +90,7 @@
|
||||||
{#if $pageRows.length}
|
{#if $pageRows.length}
|
||||||
{#each $pageRows as row (row.id)}
|
{#each $pageRows as row (row.id)}
|
||||||
<Subscribe rowAttrs={row.attrs()} let:rowAttrs>
|
<Subscribe rowAttrs={row.attrs()} let:rowAttrs>
|
||||||
<Table.Row {...rowAttrs} class='h-12'>
|
<Table.Row {...rowAttrs} class='h-12 [content-visibility:auto] [contain-intrinsic-height:auto_48px] contain-strict'>
|
||||||
{#each row.cells as cell (cell.id)}
|
{#each row.cells as cell (cell.id)}
|
||||||
<Subscribe attrs={cell.attrs()} let:attrs>
|
<Subscribe attrs={cell.attrs()} let:attrs>
|
||||||
<Table.Cell {...attrs} class={cn('px-4 h-14 first:pl-6 last:pr-6 text-nowrap', cell.id === 'name' && 'text-wrap break-all')}>
|
<Table.Cell {...attrs} class={cn('px-4 h-14 first:pl-6 last:pr-6 text-nowrap', cell.id === 'name' && 'text-wrap break-all')}>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
<script lang='ts'>
|
||||||
|
import type { HTMLButtonAttributes } from 'svelte/elements'
|
||||||
|
import type { Writable } from 'svelte/store'
|
||||||
|
|
||||||
|
import { Checkbox } from '$lib/components/ui/checkbox'
|
||||||
|
|
||||||
|
type $$Props = HTMLButtonAttributes & {
|
||||||
|
checked: Writable<boolean>
|
||||||
|
}
|
||||||
|
export let checked: Writable<boolean>
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class='contents' on:click|stopPropagation|stopImmediatePropagation>
|
||||||
|
<Checkbox bind:checked={$checked} {...$$restProps} class='mx-4' />
|
||||||
|
</div>
|
||||||
|
|
@ -2,3 +2,4 @@ export { default as StatusCell } from './status.svelte'
|
||||||
export { default as NameCell } from './name.svelte'
|
export { default as NameCell } from './name.svelte'
|
||||||
export { default as MediaCell } from './mediatitle.svelte'
|
export { default as MediaCell } from './mediatitle.svelte'
|
||||||
export { default as DateCell } from './date.svelte'
|
export { default as DateCell } from './date.svelte'
|
||||||
|
export { default as CheckboxCell } from './checkboxcell.svelte'
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,33 @@
|
||||||
<script lang='ts'>
|
<script lang='ts'>
|
||||||
import { DataBodyRow, Render, Subscribe, createRender, createTable } from 'svelte-headless-table'
|
import { DataBodyRow, Render, Subscribe, createRender, createTable } from 'svelte-headless-table'
|
||||||
import { addSortBy } from 'svelte-headless-table/plugins'
|
import { addSelectedRows, addSortBy, addTableFilter } from 'svelte-headless-table/plugins'
|
||||||
|
import MagnifyingGlass from 'svelte-radix/MagnifyingGlass.svelte'
|
||||||
|
import { toast } from 'svelte-sonner'
|
||||||
|
|
||||||
|
import { Button } from '../../button'
|
||||||
import Columnheader from '../columnheader.svelte'
|
import Columnheader from '../columnheader.svelte'
|
||||||
|
|
||||||
import { MediaCell, NameCell, StatusCell, DateCell } from './cells'
|
import { MediaCell, NameCell, StatusCell, DateCell, CheckboxCell } from './cells'
|
||||||
|
|
||||||
import type { LibraryEntry } from 'native'
|
import type { LibraryEntry } from 'native'
|
||||||
|
|
||||||
import { goto } from '$app/navigation'
|
import { goto } from '$app/navigation'
|
||||||
|
import { FolderSync, Trash } from '$lib/components/icons/animated'
|
||||||
|
import { Input } from '$lib/components/ui/input'
|
||||||
import * as Table from '$lib/components/ui/table'
|
import * as Table from '$lib/components/ui/table'
|
||||||
import { client } from '$lib/modules/anilist'
|
import { client } from '$lib/modules/anilist'
|
||||||
|
import native from '$lib/modules/native'
|
||||||
import { server } from '$lib/modules/torrent'
|
import { server } from '$lib/modules/torrent'
|
||||||
import { cn, fastPrettyBytes } from '$lib/utils'
|
import { cn, fastPrettyBytes } from '$lib/utils'
|
||||||
|
|
||||||
const lib = server.library
|
const lib = server.library
|
||||||
|
|
||||||
const table = createTable(lib, {
|
const table = createTable(lib, {
|
||||||
sort: addSortBy({ toggleOrder: ['asc', 'desc'] })
|
select: addSelectedRows(),
|
||||||
|
sort: addSortBy({ toggleOrder: ['asc', 'desc'] }),
|
||||||
|
filter: addTableFilter({
|
||||||
|
fn: ({ filterValue, value }) => value.toLowerCase().includes(filterValue.toLowerCase())
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
const columns = table.createColumns([
|
const columns = table.createColumns([
|
||||||
|
|
@ -25,36 +35,36 @@
|
||||||
accessor: 'mediaID',
|
accessor: 'mediaID',
|
||||||
header: 'Series',
|
header: 'Series',
|
||||||
id: 'series',
|
id: 'series',
|
||||||
plugins: { sort: { getSortValue: e => e ?? 0 } },
|
plugins: { sort: { getSortValue: e => e ?? 0 }, filter: { exclude: true } },
|
||||||
cell: ({ value }) => value ? createRender(MediaCell, { value }) : '?'
|
cell: ({ value }) => value ? createRender(MediaCell, { value }) : '?'
|
||||||
}),
|
}),
|
||||||
table.column({
|
table.column({
|
||||||
accessor: 'episode',
|
accessor: 'episode',
|
||||||
header: 'Episode',
|
header: 'Episode',
|
||||||
id: 'episode',
|
id: 'episode',
|
||||||
plugins: { sort: { getSortValue: e => e ?? 0 } },
|
plugins: { sort: { getSortValue: e => e ?? 0 }, filter: { exclude: true } },
|
||||||
cell: ({ value }) => value?.toString() ?? '?'
|
cell: ({ value }) => value?.toString() ?? '?'
|
||||||
}),
|
}),
|
||||||
table.column({ accessor: 'files', header: 'Files', id: 'files' }),
|
table.column({ accessor: 'files', header: 'Files', id: 'files', plugins: { filter: { exclude: true } } }),
|
||||||
table.column({
|
table.column({
|
||||||
accessor: 'size',
|
accessor: 'size',
|
||||||
header: 'Size',
|
header: 'Size',
|
||||||
id: 'size',
|
id: 'size',
|
||||||
plugins: { sort: { getSortValue: e => e ?? 0 } },
|
plugins: { sort: { getSortValue: e => e ?? 0 }, filter: { exclude: true } },
|
||||||
cell: ({ value }) => value ? fastPrettyBytes(value) : '?'
|
cell: ({ value }) => value ? fastPrettyBytes(value) : '?'
|
||||||
}),
|
}),
|
||||||
table.column({
|
table.column({
|
||||||
accessor: 'progress',
|
accessor: 'progress',
|
||||||
header: 'Status',
|
header: 'Status',
|
||||||
id: 'completed',
|
id: 'completed',
|
||||||
plugins: { sort: { getSortValue: e => e ?? 0 } },
|
plugins: { sort: { getSortValue: e => e ?? 0 }, filter: { exclude: true } },
|
||||||
cell: ({ value }) => value ? createRender(StatusCell, { value: value === 1 }) : '?'
|
cell: ({ value }) => value ? createRender(StatusCell, { value: value === 1 }) : '?'
|
||||||
}),
|
}),
|
||||||
table.column({
|
table.column({
|
||||||
accessor: 'date',
|
accessor: 'date',
|
||||||
header: 'Date',
|
header: 'Date',
|
||||||
id: 'date',
|
id: 'date',
|
||||||
plugins: { sort: { getSortValue: e => e ?? 0 } },
|
plugins: { sort: { getSortValue: e => e ?? 0 }, filter: { exclude: true } },
|
||||||
cell: ({ value }) => value ? createRender(DateCell, { value }) : '?'
|
cell: ({ value }) => value ? createRender(DateCell, { value }) : '?'
|
||||||
}),
|
}),
|
||||||
table.column({
|
table.column({
|
||||||
|
|
@ -63,12 +73,35 @@
|
||||||
id: 'name',
|
id: 'name',
|
||||||
plugins: { sort: { getSortValue: e => e ?? '' } },
|
plugins: { sort: { getSortValue: e => e ?? '' } },
|
||||||
cell: ({ value }) => createRender(NameCell, { value })
|
cell: ({ value }) => createRender(NameCell, { value })
|
||||||
|
}),
|
||||||
|
table.display({
|
||||||
|
id: 'select',
|
||||||
|
header: (_, { pluginStates }) => {
|
||||||
|
const { allPageRowsSelected } = pluginStates.select
|
||||||
|
return createRender(CheckboxCell, {
|
||||||
|
checked: allPageRowsSelected,
|
||||||
|
'aria-label': 'Select all'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
cell: ({ row }, { pluginStates }) => {
|
||||||
|
const { getRowState } = pluginStates.select
|
||||||
|
const { isSelected } = getRowState(row)
|
||||||
|
return createRender(CheckboxCell, {
|
||||||
|
checked: isSelected,
|
||||||
|
'aria-label': 'Select row'
|
||||||
|
})
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
sort: {
|
||||||
|
disable: true
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
])
|
])
|
||||||
|
|
||||||
const tableModel = table.createViewModel(columns)
|
const tableModel = table.createViewModel(columns)
|
||||||
|
|
||||||
const { headerRows, pageRows, tableAttrs, tableBodyAttrs } = tableModel
|
const { headerRows, pageRows, tableAttrs, tableBodyAttrs, pluginStates } = tableModel
|
||||||
|
|
||||||
async function playEntry ({ mediaID, episode, hash }: LibraryEntry) {
|
async function playEntry ({ mediaID, episode, hash }: LibraryEntry) {
|
||||||
if (!mediaID || !hash) return
|
if (!mediaID || !hash) return
|
||||||
|
|
@ -78,10 +111,60 @@
|
||||||
goto('/app/player/')
|
goto('/app/player/')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { filterValue } = pluginStates.filter
|
||||||
|
const { selectedDataIds } = pluginStates.select
|
||||||
|
|
||||||
|
function getSelected () {
|
||||||
|
return Object.keys($selectedDataIds).map(id => $lib[id as unknown as number]?.hash).filter(e => e) as string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: enable
|
||||||
|
function rescanTorrents () {
|
||||||
|
return null
|
||||||
|
// eslint-disable-next-line no-unreachable
|
||||||
|
toast.promise(native.rescanTorrents(getSelected()), {
|
||||||
|
loading: 'Rescanning torrents...',
|
||||||
|
success: 'Rescan complete',
|
||||||
|
error: e => {
|
||||||
|
console.error(e)
|
||||||
|
return 'Failed to rescan torrents\n' + ('stack' in (e as object) ? (e as Error).stack : 'Unknown error')
|
||||||
|
},
|
||||||
|
description: 'This may take a long while depending on the number of torrents.'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
function deleteTorrents () {
|
||||||
|
return null
|
||||||
|
// eslint-disable-next-line no-unreachable
|
||||||
|
toast.promise(native.deleteTorrents(getSelected()), {
|
||||||
|
loading: 'Deleting torrents...',
|
||||||
|
success: 'Torrents deleted',
|
||||||
|
error: e => {
|
||||||
|
console.error(e)
|
||||||
|
return 'Failed to delete torrents\n' + ('stack' in (e as object) ? (e as Error).stack : 'Unknown error')
|
||||||
|
},
|
||||||
|
description: 'This may take a while depending on the number of torrents.'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// TODO once new resolver is implemented
|
// TODO once new resolver is implemented
|
||||||
// $: allIDsPromise = client.multiple($lib.map(e => e.mediaID))
|
// $: allIDsPromise = client.multiple($lib.map(e => e.mediaID))
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<div class='flex gap-2'>
|
||||||
|
<div class='flex items-center scale-parent relative pb-2 overflow-visible grow'>
|
||||||
|
<Input
|
||||||
|
class='pl-9 bg-black select:bg-accent select:text-accent-foreground shadow-sm no-scale placeholder:opacity-50'
|
||||||
|
placeholder='Search by Torrent Name...'
|
||||||
|
bind:value={$filterValue} />
|
||||||
|
<MagnifyingGlass class='h-4 w-4 shrink-0 opacity-50 absolute left-3 text-muted-foreground z-10 pointer-events-none' />
|
||||||
|
</div>
|
||||||
|
<Button variant='secondary' size='icon' class='border-0 animated-icon !pointer-events-auto cursor-not-allowed' disabled on:click={rescanTorrents}>
|
||||||
|
<FolderSync class={cn('size-4')} />
|
||||||
|
</Button>
|
||||||
|
<Button variant='destructive' size='icon' class='border-0 animated-icon !pointer-events-auto cursor-not-allowed' disabled on:click={deleteTorrents}>
|
||||||
|
<Trash class={cn('size-4')} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
<div class='rounded-md border size-full overflow-clip contain-strict'>
|
<div class='rounded-md border size-full overflow-clip contain-strict'>
|
||||||
<Table.Root {...$tableAttrs} class='max-h-full'>
|
<Table.Root {...$tableAttrs} class='max-h-full'>
|
||||||
<Table.Header class='px-5'>
|
<Table.Header class='px-5'>
|
||||||
|
|
@ -109,10 +192,15 @@
|
||||||
{#if $pageRows.length}
|
{#if $pageRows.length}
|
||||||
{#each $pageRows as row (row.id)}
|
{#each $pageRows as row (row.id)}
|
||||||
<Subscribe rowAttrs={row.attrs()} let:rowAttrs>
|
<Subscribe rowAttrs={row.attrs()} let:rowAttrs>
|
||||||
<Table.Row {...rowAttrs} class={cn('h-14', (row instanceof DataBodyRow) && row.original.mediaID ? 'cursor-pointer' : 'cursor-not-allowed')} on:click={() => { if (row instanceof DataBodyRow) playEntry(row.original) }}>
|
<Table.Row {...rowAttrs} class={cn('h-14 [content-visibility:auto] [contain-intrinsic-height:auto_56px] contain-strict', (row instanceof DataBodyRow) && row.original.mediaID ? 'cursor-pointer' : 'cursor-not-allowed')} on:click={() => { if (row instanceof DataBodyRow) playEntry(row.original) }}>
|
||||||
{#each row.cells as cell (cell.id)}
|
{#each row.cells as cell (cell.id)}
|
||||||
<Subscribe attrs={cell.attrs()} let:attrs>
|
<Subscribe attrs={cell.attrs()} let:attrs>
|
||||||
<Table.Cell {...attrs} class={cn('px-4 min-h-14 first:pl-6 last:pr-6 text-nowrap', (cell.id === 'episode') && 'text-muted-foreground', (cell.id === 'series' || cell.id === 'name') && 'min-w-80 text-wrap break-all')}>
|
<Table.Cell {...attrs} class={cn(
|
||||||
|
'px-4 min-h-14 first:pl-6 last:pr-6 text-nowrap',
|
||||||
|
(cell.id === 'episode') && 'text-muted-foreground',
|
||||||
|
(cell.id === 'series' || cell.id === 'name') && 'min-w-80 text-wrap break-all',
|
||||||
|
cell.id === 'select' && 'p-0'
|
||||||
|
)}>
|
||||||
<Render of={cell.render()} />
|
<Render of={cell.render()} />
|
||||||
</Table.Cell>
|
</Table.Cell>
|
||||||
</Subscribe>
|
</Subscribe>
|
||||||
|
|
|
||||||
|
|
@ -110,6 +110,7 @@ export default new class URQLClient extends Client {
|
||||||
constructor () {
|
constructor () {
|
||||||
super({
|
super({
|
||||||
url: 'https://graphql.anilist.co',
|
url: 'https://graphql.anilist.co',
|
||||||
|
preferGetMethod: false,
|
||||||
// fetch: dev ? fetch : (req: RequestInfo | URL, opts?: RequestInit) => this.handleRequest(req, opts),
|
// fetch: dev ? fetch : (req: RequestInfo | URL, opts?: RequestInit) => this.handleRequest(req, opts),
|
||||||
fetch: (req: RequestInfo | URL, opts?: RequestInit) => this.handleRequest(req, opts),
|
fetch: (req: RequestInfo | URL, opts?: RequestInit) => this.handleRequest(req, opts),
|
||||||
exchanges: [
|
exchanges: [
|
||||||
|
|
|
||||||
|
|
@ -102,6 +102,8 @@ const ENDPOINTS = {
|
||||||
API_ANIME: 'https://api.myanimelist.net/v2/anime'
|
API_ANIME: 'https://api.myanimelist.net/v2/anime'
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
|
const clientID = 'd93b624a92e431a9b6dfe7a66c0c5bbb'
|
||||||
|
|
||||||
export default new class MALSync {
|
export default new class MALSync {
|
||||||
auth = persisted<MALOAuth | undefined>('malAuth', undefined)
|
auth = persisted<MALOAuth | undefined>('malAuth', undefined)
|
||||||
viewer = persisted<ResultOf<typeof UserFrag> | undefined>('malViewer', undefined)
|
viewer = persisted<ResultOf<typeof UserFrag> | undefined>('malViewer', undefined)
|
||||||
|
|
@ -169,7 +171,7 @@ export default new class MALSync {
|
||||||
if (auth) {
|
if (auth) {
|
||||||
const expiresAt = (auth.created_at + auth.expires_in) * 1000
|
const expiresAt = (auth.created_at + auth.expires_in) * 1000
|
||||||
|
|
||||||
if (expiresAt < Date.now() - 1000 * 60 * 5) { // 5 minutes before expiry
|
if (expiresAt < Date.now() - 1000 * 60 * 5 && !body?.get('refresh_token')) { // 5 minutes before expiry
|
||||||
await this._refresh()
|
await this._refresh()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -248,6 +250,7 @@ export default new class MALSync {
|
||||||
const data = await this._post<MALOAuth>(
|
const data = await this._post<MALOAuth>(
|
||||||
ENDPOINTS.API_OAUTH,
|
ENDPOINTS.API_OAUTH,
|
||||||
{
|
{
|
||||||
|
client_id: clientID,
|
||||||
grant_type: 'refresh_token',
|
grant_type: 'refresh_token',
|
||||||
refresh_token: auth.refresh_token
|
refresh_token: auth.refresh_token
|
||||||
}
|
}
|
||||||
|
|
@ -265,7 +268,6 @@ export default new class MALSync {
|
||||||
debug('Logging in to MAL')
|
debug('Logging in to MAL')
|
||||||
const state = crypto.randomUUID().replaceAll('-', '')
|
const state = crypto.randomUUID().replaceAll('-', '')
|
||||||
const challenge = (crypto.randomUUID() + crypto.randomUUID()).replaceAll('-', '')
|
const challenge = (crypto.randomUUID() + crypto.randomUUID()).replaceAll('-', '')
|
||||||
const clientID = 'd93b624a92e431a9b6dfe7a66c0c5bbb'
|
|
||||||
|
|
||||||
const redirect = dev ? 'http://localhost:7344/authorize' : 'https://hayase.app/authorize'
|
const redirect = dev ? 'http://localhost:7344/authorize' : 'https://hayase.app/authorize'
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@ export async function encryptMessage (message: string) {
|
||||||
iv: key
|
iv: key
|
||||||
},
|
},
|
||||||
await derived,
|
await derived,
|
||||||
text2arr(message)
|
text2arr(message).buffer as ArrayBuffer
|
||||||
))))
|
))))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -47,6 +47,6 @@ export async function decryptMessage (encryptedMessage: string) {
|
||||||
iv: key
|
iv: key
|
||||||
},
|
},
|
||||||
await derived,
|
await derived,
|
||||||
hex2arr(bin2hex(encryptedMessage))
|
hex2arr(bin2hex(encryptedMessage)).buffer as ArrayBuffer
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -116,6 +116,8 @@ export default Object.assign<Native, Partial<Native>>({
|
||||||
updatePeerCounts: async () => [],
|
updatePeerCounts: async () => [],
|
||||||
isApp: false,
|
isApp: false,
|
||||||
playTorrent: async () => dummyFiles,
|
playTorrent: async () => dummyFiles,
|
||||||
|
rescanTorrents: async () => undefined,
|
||||||
|
deleteTorrents: async () => undefined,
|
||||||
library: async () => [],
|
library: async () => [],
|
||||||
attachments: async () => [],
|
attachments: async () => [],
|
||||||
tracks: async () => [],
|
tracks: async () => [],
|
||||||
|
|
|
||||||
|
|
@ -59,10 +59,10 @@
|
||||||
<aside class='lg:grow lg:max-w-60 flex flex-col'>
|
<aside class='lg:grow lg:max-w-60 flex flex-col'>
|
||||||
<SettingsNav {items} />
|
<SettingsNav {items} />
|
||||||
<div class='mt-auto text-xs text-muted-foreground px-4 sm:px-2 py-3 md:py-5 flex-row lg:flex-col font-light gap-0.5 gap-x-4 flex-wrap hidden sm:flex'>
|
<div class='mt-auto text-xs text-muted-foreground px-4 sm:px-2 py-3 md:py-5 flex-row lg:flex-col font-light gap-0.5 gap-x-4 flex-wrap hidden sm:flex'>
|
||||||
<div>WebTorrent v2.6.8</div>
|
<div>WebTorrent v2.8.4</div>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
<div class='flex-1 overflow-y-scroll' use:dragScroll>
|
<div class='flex-1' use:dragScroll>
|
||||||
{#if !SUPPORTS.isUnderPowered}
|
{#if !SUPPORTS.isUnderPowered}
|
||||||
<Globe />
|
<Globe />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,6 @@
|
||||||
import { Overview } from '$lib/components/ui/torrentclient'
|
import { Overview } from '$lib/components/ui/torrentclient'
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class='flex flex-col h-full'>
|
<div class='flex flex-col h-full overflow-y-scroll'>
|
||||||
<Overview />
|
<Overview />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -2,4 +2,6 @@
|
||||||
import { FilesTable } from '$lib/components/ui/torrentclient'
|
import { FilesTable } from '$lib/components/ui/torrentclient'
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<FilesTable />
|
<div class='flex flex-col size-full'>
|
||||||
|
<FilesTable />
|
||||||
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -2,4 +2,6 @@
|
||||||
import { LibraryTable } from '$lib/components/ui/torrentclient'
|
import { LibraryTable } from '$lib/components/ui/torrentclient'
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<LibraryTable />
|
<div class='flex flex-col size-full'>
|
||||||
|
<LibraryTable />
|
||||||
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -18,8 +18,6 @@
|
||||||
import native from '$lib/modules/native'
|
import native from '$lib/modules/native'
|
||||||
import { w2globby } from '$lib/modules/w2g/lobby'
|
import { w2globby } from '$lib/modules/w2g/lobby'
|
||||||
|
|
||||||
export let data
|
|
||||||
|
|
||||||
$: users = $w2globby!.peers
|
$: users = $w2globby!.peers
|
||||||
$: messages = $w2globby!.messages
|
$: messages = $w2globby!.messages
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { resolve } from 'node:path'
|
||||||
import { sveltekit } from '@sveltejs/kit/vite'
|
import { sveltekit } from '@sveltejs/kit/vite'
|
||||||
import license from 'rollup-plugin-license'
|
import license from 'rollup-plugin-license'
|
||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
|
import devtoolsJson from 'vite-plugin-devtools-json'
|
||||||
import { viteStaticCopy } from 'vite-plugin-static-copy'
|
import { viteStaticCopy } from 'vite-plugin-static-copy'
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
|
|
@ -22,7 +23,8 @@ export default defineConfig({
|
||||||
dest: 'geoip/'
|
dest: 'geoip/'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
})
|
}),
|
||||||
|
devtoolsJson()
|
||||||
],
|
],
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue