feat: update user lists every 15 minutes

feat: allow custom order and sections on home screen
fix: improve settings code
This commit is contained in:
ThaUnknown 2023-11-11 20:22:43 +01:00
parent ab1d14eee6
commit a806bd1f08
22 changed files with 422 additions and 240 deletions

View file

@ -16,6 +16,7 @@ const flags = [
['disable-features', 'Vulkan,CalculateNativeWinOcclusion,WidgetLayering'],
['disable-color-correct-rendering'],
['autoplay-policy', 'no-user-gesture-required'], ['disable-notifications'], ['disable-logging'], ['disable-permissions-api'], ['no-sandbox'], ['no-zygote'],
['bypasscsp-schemes'],
['force-color-profile', 'srgb']
]
for (const [flag, value] of flags) {

View file

@ -10,7 +10,7 @@
<script>
import { traceAnime } from '@/modules/anime.js'
import { set } from '../views/Settings.svelte'
import { settings } from '@/modules/settings.js'
import { click } from '@/modules/click.js'
import { page } from '@/App.svelte'
import { toast } from 'svelte-sonner'
@ -49,7 +49,7 @@
}
}
function changeCardMode (type) {
set.cards = type
$settings.cards = type
form.dispatchEvent(new Event('input', { bubbles: true }))
}
</script>
@ -201,8 +201,8 @@
<span class='badge bg-light border-0 py-5 px-10 text-capitalize mr-20 text-white text-nowrap'>{('' + badge).replace(/_/g, ' ').toLowerCase()}</span>
{/each}
{/if}
<span class='material-symbols-outlined font-size-24 mr-10 filled ml-auto text-dark-light pointer' class:text-muted={set.cards === 'small'} use:click={() => changeCardMode('small')}>grid_on</span>
<span class='material-symbols-outlined font-size-24 filled text-dark-light pointer' class:text-muted={set.cards === 'full'} use:click={() => changeCardMode('full')}>grid_view</span>
<span class='material-symbols-outlined font-size-24 mr-10 filled ml-auto text-dark-light pointer' class:text-muted={$settings.cards === 'small'} use:click={() => changeCardMode('small')}>grid_on</span>
<span class='material-symbols-outlined font-size-24 filled text-dark-light pointer' class:text-muted={$settings.cards === 'full'} use:click={() => changeCardMode('full')}>grid_view</span>
</div>
</form>

View file

@ -2,7 +2,8 @@
import { getContext } from 'svelte'
import { alID } from '@/modules/anilist.js'
import { media } from '../views/Player/MediaHandler.svelte'
import { platformMap, set } from '../views/Settings.svelte'
import { platformMap } from '../views/Settings.svelte'
import { settings } from '@/modules/settings.js'
import { toast } from 'svelte-sonner'
import { click } from '@/modules/click.js'
import { logout } from './Logout.svelte'
@ -93,7 +94,7 @@
}
</script>
<div class='sidebar z-30' class:animated={set.expandingSidebar}>
<div class='sidebar z-30' class:animated={$settings.expandingSidebar}>
<div class='sidebar-overlay pointer-events-none h-full position-absolute' />
<div class='sidebar-menu h-full d-flex flex-column justify-content-center align-items-center m-0 pb-5' class:animate={page !== 'player'}>
{#each links as { click: _click, icon, text, image, css, page: _page }, i (i)}

View file

@ -5,11 +5,11 @@
import FullCard from './FullCard.svelte'
import EpisodeCard from './EpisodeCard.svelte'
import FullSkeletonCard from './FullSkeletonCard.svelte'
import { set } from '../../views/Settings.svelte'
import { settings } from '@/modules/settings.js'
export let card
const type = card.type || set.cards
const type = card.type || $settings.cards
</script>
{#if type === 'episode'}

View file

@ -2,7 +2,7 @@ import lavenshtein from 'js-levenshtein'
import { writable } from 'simple-store-svelte'
import Bottleneck from 'bottleneck'
import { alToken } from '../views/Settings.svelte'
import { alToken } from '@/modules/settings.js'
import { toast } from 'svelte-sonner'
import { sleep } from './util.js'
@ -158,6 +158,10 @@ export async function alEntry (filemedia) {
}
}
const date = new Date()
export const currentSeason = ['WINTER', 'SPRING', 'SUMMER', 'FALL'][Math.floor((date.getMonth() / 12) * 4) % 4]
export const currentYear = date.getFullYear()
function getDistanceFromTitle (media, name) {
if (media) {
const titles = Object.values(media.title).filter(v => v).map(title => lavenshtein(title.toLowerCase(), name.toLowerCase()))
@ -529,3 +533,8 @@ mutation($lists: [String]){
}
export const userLists = writable(alToken && alRequest({ method: 'UserLists' }))
if (alToken) {
// update userLists every 15 mins
setInterval(() => { userLists.value = alRequest({ method: 'UserLists' }) }, 1000 * 60 * 15)
}

View file

@ -2,7 +2,7 @@ import { DOMPARSER, PromiseBatch, binarySearch } from './util.js'
import { alRequest, alSearch } from './anilist.js'
import _anitomyscript from 'anitomyscript'
import { toast } from 'svelte-sonner'
import Sections from './sections.js'
import SectionsManager from './sections.js'
import { page } from '@/App.svelte'
import clipboard from './clipboard.js'
@ -63,7 +63,7 @@ export async function traceAnime (image) { // WAIT lookup logic
search.value = {
clearNext: true,
load: (page = 1, perPage = 50, variables = {}) => {
const res = alRequest({ method: 'SearchIDS', page, perPage, id: ids, ...Sections.sanitiseObject(variables) }).then(res => {
const res = alRequest({ method: '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)
@ -80,7 +80,7 @@ export async function traceAnime (image) { // WAIT lookup logic
res.data?.Page?.media.sort((a, b) => b.similarity - a.similarity)
return res
})
return Sections.wrapResponse(res, result.length, 'episode')
return SectionsManager.wrapResponse(res, result.length, 'episode')
}
}
key.value = {}

View file

@ -1,5 +1,5 @@
import { alRequest } from '@/modules/anilist.js'
import { set } from '@/views/Settings.svelte'
import { settings } from '@/modules/settings.js'
import { findEdge, resolveSeason, getMediaMaxEp, mapBestRelease } from '../anime.js'
import { exclusions, getRSSContent, parseRSSNodes } from '../rss.js'
@ -34,8 +34,8 @@ export default async function getRSSEntries ({ media, episode, mode, ignoreQuali
}
const excl = exclusions.join('|')
const quality = (!ignoreQuality && (`"${set.rssQuality}"` || '"1080"')) || ''
const url = new URL(`${set.catURL}/?page=rss&c=1_2&f=0&s=seeders&o=desc&q=(${titles})${ep}${quality}-(${excl})`)
const quality = (!ignoreQuality && (`"${settings.value.rssQuality}"` || '"1080"')) || ''
const url = new URL(`${settings.value.catURL}/?page=rss&c=1_2&f=0&s=seeders&o=desc&q=(${titles})${ep}${quality}-(${excl})`)
let nodes = [...(await getRSSContent(url)).querySelectorAll('item')]
@ -45,7 +45,7 @@ export default async function getRSSEntries ({ media, episode, mode, ignoreQuali
// we want the dates of the target media as the S1 title might be used for SX releases
const titles = createTitle(absolute.media).join(')|(')
const url = new URL(`${set.catURL}/?page=rss&c=1_2&f=0&s=seeders&o=desc&q=(${titles})${epstring(absoluteep)}${quality}-(${excl})`)
const url = new URL(`${settings.value.catURL}/?page=rss&c=1_2&f=0&s=seeders&o=desc&q=(${titles})${epstring(absoluteep)}${quality}-(${excl})`)
nodes = [...nodes, ...(await getRSSContent(url)).querySelectorAll('item')]
}

View file

@ -1,7 +1,7 @@
import { mapBestRelease, anitomyscript } from '../anime.js'
import { fastPrettyBytes } from '../util.js'
import { exclusions } from '../rss.js'
import { set } from '@/views/Settings.svelte'
import { settings } from '@/modules/settings.js'
import { alRequest } from '../anilist.js'
import { client } from '@/modules/torrent.js'
@ -14,7 +14,7 @@ export default async function ({ media, episode }) {
const movie = isMovie(media) // don't query movies with qualities, to allow 4k
let entries = await getToshoEntries(media, aniDBEpisode, json, !movie && set.rssQuality)
let entries = await getToshoEntries(media, aniDBEpisode, json, !movie && settings.value.rssQuality)
if (!entries.length && !movie) entries = await getToshoEntries(media, aniDBEpisode, json)
@ -243,7 +243,7 @@ function buildQuery (quality) {
async function fetchBatches ({ episodeCount, id, quality, movie = null }) {
try {
const queryString = buildQuery(quality)
const torrents = await fetch(set.toshoURL + 'json?order=size-d&aid=' + id + queryString)
const torrents = await fetch(settings.value.toshoURL + 'json?order=size-d&aid=' + id + queryString)
// safe if AL includes EP 0 or doesn't
const batches = (await torrents.json()).filter(entry => entry.num_files >= episodeCount)
@ -263,7 +263,7 @@ async function fetchBatches ({ episodeCount, id, quality, movie = null }) {
async function fetchSingleEpisode ({ id, quality }) {
try {
const queryString = buildQuery(quality)
const torrents = await fetch(set.toshoURL + 'json?eid=' + id + queryString)
const torrents = await fetch(settings.value.toshoURL + 'json?eid=' + id + queryString)
const episodes = await torrents.json()
console.log({ episodes })

View file

@ -1,5 +1,5 @@
import { DOMPARSER } from '@/modules/util.js'
import { set } from '@/views/Settings.svelte'
import { settings } from '@/modules/settings.js'
import { toast } from 'svelte-sonner'
import { add } from '@/modules/torrent.js'
import { resolveFileMedia, getEpisodeMetadataForMedia } from './anime.js'
@ -35,13 +35,13 @@ export function parseRSSNodes (nodes) {
}
const rssmap = {
SubsPlease: set.toshoURL + 'rss2?qx=1&q="[SubsPlease] "',
'NC-Raws': set.toshoURL + 'rss2?qx=1&q="[NC-Raws] "',
'Erai-raws [Multi-Sub]': set.toshoURL + 'rss2?qx=1&q="[Erai-raws] "'
SubsPlease: settings.value.toshoURL + 'rss2?qx=1&q="[SubsPlease] "',
'NC-Raws': settings.value.toshoURL + 'rss2?qx=1&q="[NC-Raws] "',
'Erai-raws [Multi-Sub]': settings.value.toshoURL + 'rss2?qx=1&q="[Erai-raws] "'
}
export function getReleasesRSSurl (val) {
const rss = rssmap[val] || val
return rss && new URL(rssmap[val] ? `${rss}${set.rssQuality ? `"${set.rssQuality}"` : ''}` : rss)
return rss && new URL(rssmap[val] ? `${rss}${settings.value.rssQuality ? `"${settings.value.rssQuality}"` : ''}` : rss)
}
export async function getRSSContent (url) {

View file

@ -1,7 +1,7 @@
import { set } from '@/views/Settings.svelte'
import { settings } from '@/modules/settings.js'
export default function (t, { speed = 120, smooth = 10 } = {}) {
if (!set.smoothScroll) return
if (!settings.value.smoothScroll) return
let moving = false
let pos = 0
let scrollTop = 0

View file

@ -1,25 +1,32 @@
import { alRequest } from '@/modules/anilist.js'
import { alRequest, currentSeason, currentYear, userLists } from '@/modules/anilist.js'
import { writable } from 'simple-store-svelte'
import { settings, alToken } from '@/modules/settings.js'
import { RSSManager } from '@/modules/rss.js'
export const hasNextPage = writable(true)
export default class Sections {
export default class SectionsManager {
constructor (data = []) {
this.sections = []
this.add(data)
for (const section of data) this.add(section)
}
/**
* @param {object} data
*/
add (data) {
for (const { title, variables = {}, type, load = Sections.createFallbackLoad(variables, type), preview = writable() } of data) {
this.sections.push({ load, title, preview, variables })
}
if (!data) return
const { title, variables = {}, type, load = SectionsManager.createFallbackLoad(variables, type), preview = writable() } = data
const section = { ...data, load, title, preview, variables }
this.sections.push(section)
return section
}
static createFallbackLoad (variables, type) {
return (page = 1, perPage = 50, search = variables) => {
const options = { method: 'Search', page, perPage, ...Sections.sanitiseObject(search) }
const options = { method: 'Search', page, perPage, ...SectionsManager.sanitiseObject(search) }
const res = alRequest(options)
return Sections.wrapResponse(res, perPage, type)
return SectionsManager.wrapResponse(res, perPage, type)
}
}
@ -27,7 +34,7 @@ export default class Sections {
res.then(res => {
hasNextPage.value = res?.data?.Page.pageInfo.hasNextPage
})
return Array.from({ length }, (_, i) => ({ type, data: Sections.fromPending(res, i) }))
return Array.from({ length }, (_, i) => ({ type, data: SectionsManager.fromPending(res, i) }))
}
static sanitiseObject (object = {}) {
@ -43,3 +50,129 @@ export default class Sections {
return data.Page.media[i]
}
}
// list of all possible home screen sections
export let sections = createSections()
settings.subscribe(() => {
sections = createSections()
})
function createSections () {
return [
// RSS feeds
...[...settings.value.rssFeedsNew].reverse().map(([title, url]) => {
const section = {
title,
load: (page = 1, perPage = 8) => RSSManager.getMediaForRSS(page, perPage, url),
preview: writable(RSSManager.getMediaForRSS(1, 8, url)),
variables: { disableSearch: true }
}
// update every 30 seconds
setInterval(async () => {
if (await RSSManager.getContentChanged(1, 8, url)) {
section.preview.value = RSSManager.getMediaForRSS(1, 8, url, true)
}
}, 30000)
return section
}),
// user specific sections
{
title: 'Continue Watching',
load: (page = 1, perPage = 50, variables = {}) => {
const res = userLists.value.then(res => {
const mediaList = res.data.MediaListCollection.lists.reduce((filtered, { status, entries }) => {
return (status === 'CURRENT' || status === 'REPEATING') ? filtered.concat(entries) : filtered
}, [])
const ids = mediaList.filter(({ media }) => {
if (media.status === 'FINISHED') return true
return media.mediaListEntry?.progress < media.nextAiringEpisode?.episode - 1
}).map(({ media }) => media.id)
return alRequest({ method: 'SearchIDS', page, perPage, id: ids, ...SectionsManager.sanitiseObject(variables) })
})
return SectionsManager.wrapResponse(res, perPage)
},
hide: !alToken
},
{
title: 'Sequels You Missed',
load: (page = 1, perPage = 50, variables = {}) => {
const res = userLists.value.then(res => {
const mediaList = res.data.MediaListCollection.lists.find(({ status }) => status === 'COMPLETED').entries
const ids = mediaList.flatMap(({ media }) => {
return media.relations.edges.filter(edge => edge.relationType === 'SEQUEL')
}).map(({ node }) => node.id)
return alRequest({ method: 'SearchIDS', page, perPage, id: ids, ...SectionsManager.sanitiseObject(variables), status: ['FINISHED', 'RELEASING'], onList: false })
})
return SectionsManager.wrapResponse(res, perPage)
},
hide: !alToken
},
{
title: 'Your List',
load: (page = 1, perPage = 50, variables = {}) => {
const res = userLists.value.then(res => {
const ids = res.data.MediaListCollection.lists.find(({ status }) => status === 'PLANNING').entries.map(({ media }) => media.id)
return alRequest({ method: 'SearchIDS', page, perPage, id: ids, ...SectionsManager.sanitiseObject(variables) })
})
return SectionsManager.wrapResponse(res, perPage)
},
hide: !alToken
},
{
title: 'Completed List',
load: (page = 1, perPage = 50, variables = {}) => {
const res = userLists.value.then(res => {
const ids = res.data.MediaListCollection.lists.find(({ status }) => status === 'COMPLETED').entries.map(({ media }) => media.id)
return alRequest({ method: 'SearchIDS', page, perPage, id: ids, ...SectionsManager.sanitiseObject(variables) })
})
return SectionsManager.wrapResponse(res, perPage)
},
hide: !alToken
},
{
title: 'Paused List',
load: (page = 1, perPage = 50, variables = {}) => {
const res = userLists.value.then(res => {
const ids = res.data.MediaListCollection.lists.find(({ status }) => status === 'PAUSED').entries.map(({ media }) => media.id)
return alRequest({ method: 'SearchIDS', page, perPage, id: ids, ...SectionsManager.sanitiseObject(variables) })
})
return SectionsManager.wrapResponse(res, perPage)
},
hide: !alToken
},
{
title: 'Dropped List',
load: (page = 1, perPage = 50, variables = {}) => {
const res = userLists.value.then(res => {
const ids = res.data.MediaListCollection.lists.find(({ status }) => status === 'DROPPED').entries.map(({ media }) => media.id)
return alRequest({ method: 'SearchIDS', page, perPage, id: ids, ...SectionsManager.sanitiseObject(variables) })
})
return SectionsManager.wrapResponse(res, perPage)
},
hide: !alToken
},
{
title: 'Currently Watching List',
load: (page = 1, perPage = 50, variables = {}) => {
const res = userLists.value.then(res => {
const ids = res.data.MediaListCollection.lists.find(({ status }) => status === 'CURRENT').entries.map(({ media }) => media.id)
return alRequest({ method: 'SearchIDS', page, perPage, id: ids, ...SectionsManager.sanitiseObject(variables) })
})
return SectionsManager.wrapResponse(res, perPage)
},
hide: !alToken
},
// common, non-user specific sections
{ title: 'Popular This Season', variables: { sort: 'POPULARITY_DESC', season: currentSeason, year: currentYear } },
{ title: 'Trending Now', variables: { sort: 'TRENDING_DESC' } },
{ title: 'All Time Popular', variables: { sort: 'POPULARITY_DESC' } },
{ title: 'Romance', variables: { sort: 'TRENDING_DESC', genre: 'Romance' } },
{ title: 'Action', variables: { sort: 'TRENDING_DESC', genre: 'Action' } },
{ title: 'Adventure', variables: { sort: 'TRENDING_DESC', genre: 'Adventure' } },
{ title: 'Fantasy', variables: { sort: 'TRENDING_DESC', genre: 'Fantasy' } },
{ title: 'Comedy', variables: { sort: 'TRENDING_DESC', genre: 'Comedy' } }
]
}

View file

@ -0,0 +1,46 @@
import { writable } from 'simple-store-svelte'
import { defaults } from '@/../common/util.js'
export let alToken = localStorage.getItem('ALtoken') || null
let storedSettings = { ...defaults }
try {
storedSettings = JSON.parse(localStorage.getItem('settings'))
} catch (e) {}
const scopedDefaults = {
homeSections: [...[...storedSettings.rssFeedsNew.reverse()].map(([title]) => title), 'Continue Watching', 'Sequels You Missed', 'Your List', 'Popular This Season', 'Trending Now', 'All Time Popular', 'Romance', 'Action', 'Adventure', 'Fantasy', 'Comedy']
}
/**
* @type {import('svelte/store').Writable & { value: any }}
*/
export const settings = writable({ ...defaults, ...scopedDefaults, ...storedSettings })
settings.subscribe(value => {
localStorage.setItem('settings', JSON.stringify(value))
})
export function resetSettings () {
settings.value = { ...defaults, ...scopedDefaults }
}
window.addEventListener('paste', ({ clipboardData }) => {
if (clipboardData.items?.[0]) {
if (clipboardData.items[0].type === 'text/plain' && clipboardData.items[0].kind === 'string') {
clipboardData.items[0].getAsString(text => {
let token = text.split('access_token=')?.[1]?.split('&token_type')?.[0]
if (token) {
if (token.endsWith('/')) token = token.slice(0, -1)
handleToken(token)
}
})
}
}
})
window.IPC.on('altoken', handleToken)
function handleToken (data) {
localStorage.setItem('ALtoken', data)
alToken = data
location.reload()
}

View file

@ -1,7 +1,7 @@
import JASSUB from 'jassub'
import { toTS } from './util.js'
import { subRx, videoRx } from '@/../common/util.js'
import { set } from '../views/Settings.svelte'
import { settings } from '@/modules/settings.js'
import { client } from '@/modules/torrent.js'
import clipboard from './clipboard.js'
@ -15,7 +15,7 @@ ScaledBorderAndShadow: yes
[V4+ Styles]
Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding
Style: Default, ${set.font?.name.toLowerCase() || 'Roboto Medium'},52,&H00FFFFFF,&H00FFFFFF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2.6,0,2,20,20,46,1
Style: Default, ${settings.value.font?.name.toLowerCase() || 'Roboto Medium'},52,&H00FFFFFF,&H00FFFFFF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2.6,0,2,20,20,46,1
[Events]
`
@ -82,13 +82,13 @@ export default class Subtitles {
}
this.initSubtitleRenderer()
const tracks = this.headers?.filter(t => t)
if (tracks?.length && set.subtitleLanguage) {
if (tracks?.length && settings.value.subtitleLanguage) {
if (tracks.length === 1) {
this.selectCaptions(tracks[0].number)
} else {
const wantedTrack = tracks.find(({ language }) => {
if (language == null) language = 'eng'
return language === set.subtitleLanguage
return language === settings.value.subtitleLanguage
})
if (wantedTrack) return this.selectCaptions(wantedTrack.number)
@ -159,7 +159,7 @@ export default class Subtitles {
video: this.video,
subContent: defaultHeader,
fonts: this.fonts,
fallbackFont: set.font?.name || 'roboto medium',
fallbackFont: settings.value.font?.name || 'roboto medium',
availableFonts: {
'roboto medium': './Roboto.ttf'
},
@ -167,12 +167,12 @@ export default class Subtitles {
wasmUrl: new URL('jassub/dist/jassub-worker.wasm', import.meta.url).toString(),
legacyWasmUrl: new URL('jassub/dist/jassub-worker.wasm.js', import.meta.url).toString(),
modernWasmUrl: new URL('jassub/dist/jassub-worker-modern.wasm', import.meta.url).toString(),
useLocalFonts: set.missingFont,
dropAllBlur: set.disableSubtitleBlur
useLocalFonts: settings.value.missingFont,
dropAllBlur: settings.value.disableSubtitleBlur
}
if (set.font) {
options.availableFonts[set.font.name.toLowerCase()] = new Uint8Array(set.font.data)
this.fonts.push(new Uint8Array(set.font.data))
if (settings.value.font) {
options.availableFonts[settings.value.font.name.toLowerCase()] = new Uint8Array(settings.value.font.data)
this.fonts.push(new Uint8Array(settings.value.font.data))
}
this.renderer = new JASSUB(options)
}

View file

@ -1,19 +1,15 @@
<script context='module'>
import Sections from '@/modules/sections.js'
import SectionsManager from '@/modules/sections.js'
import Search, { search } from './Search.svelte'
import { alRequest } from '@/modules/anilist.js'
import { alRequest, currentSeason, currentYear } from '@/modules/anilist.js'
const date = new Date()
const seasons = ['WINTER', 'SPRING', 'SUMMER', 'FALL']
const getSeason = d => seasons[Math.floor((d.getMonth() / 12) * 4) % 4]
const vars = { format: 'TV', season: getSeason(date), year: date.getFullYear() }
const vars = { format: 'TV', season: currentSeason, year: currentYear }
async function fetchAllScheduleEntries (_variables) {
const variables = { ..._variables }
const results = { data: { Page: { media: [], pageInfo: { hasNextPage: false } } } }
for (let page = 1, hasNextPage = true; hasNextPage && page < 5; ++page) {
const res = await alRequest({ method: 'Search', page, perPage: 50, ...vars, ...Sections.sanitiseObject(variables) })
const res = await alRequest({ method: 'Search', page, perPage: 50, ...vars, ...SectionsManager.sanitiseObject(variables) })
hasNextPage = res.data.Page.pageInfo.hasNextPage
results.data.Page.media = results.data.Page.media.concat(res.data.Page.media)
}
@ -26,7 +22,7 @@
<script>
$search = {
...vars,
load: (_, __, variables) => Sections.wrapResponse(fetchAllScheduleEntries(variables), 150)
load: (_, __, variables) => SectionsManager.wrapResponse(fetchAllScheduleEntries(variables), 150)
}
</script>

View file

@ -1,101 +1,28 @@
<script context='module'>
import { writable } from 'simple-store-svelte'
import Sections from '@/modules/sections.js'
import { alToken, set } from '../Settings.svelte'
import { alRequest, userLists } from '@/modules/anilist.js'
import { RSSManager } from '@/modules/rss.js'
import SectionsManager, { sections } from '@/modules/sections.js'
import { settings } from '@/modules/settings.js'
import { alRequest, currentSeason, currentYear, userLists } from '@/modules/anilist.js'
const seasons = ['WINTER', 'SPRING', 'SUMMER', 'FALL']
const getSeason = d => seasons[Math.floor((d.getMonth() / 12) * 4) % 4]
const bannerData = alRequest({ method: 'Search', sort: 'POPULARITY_DESC', perPage: 1, onList: false, season: currentSeason, year: currentYear })
const date = new Date()
const manager = new SectionsManager()
const bannerData = alRequest({ method: 'Search', sort: 'POPULARITY_DESC', perPage: 1, onList: false, season: getSeason(date), year: date.getFullYear() })
const mappedSections = {}
const manager = new Sections()
for (const [title, url] of set.rssFeedsNew.reverse()) {
const load = (page = 1, perPage = 8) => RSSManager.getMediaForRSS(page, perPage, url)
manager.add([
{
title,
load,
preview: writable(RSSManager.getMediaForRSS(1, 8, url)),
variables: { disableSearch: true }
}
])
const entry = manager.sections.find(section => section.load === load)
setInterval(async () => {
if (await RSSManager.getContentChanged(1, 8, url)) {
entry.preview.value = RSSManager.getMediaForRSS(1, 8, url, true)
}
}, 30000)
for (const section of sections) {
mappedSections[section.title] = section
}
if (alToken) {
const sections = [
{
title: 'Continue Watching',
load: (page = 1, perPage = 50, variables = {}) => {
const res = userLists.value.then(res => {
const mediaList = res.data.MediaListCollection.lists.reduce((filtered, { status, entries }) => {
return (status === 'CURRENT' || status === 'REPEATING') ? filtered.concat(entries) : filtered
}, [])
const ids = mediaList.filter(({ media }) => {
if (media.status === 'FINISHED') return true
return media.mediaListEntry?.progress < media.nextAiringEpisode?.episode - 1
}).map(({ media }) => media.id)
return alRequest({ method: 'SearchIDS', page, perPage, id: ids, ...Sections.sanitiseObject(variables) })
})
return Sections.wrapResponse(res, perPage)
}
},
{
title: 'Sequels You Missed',
load: (page = 1, perPage = 50, variables = {}) => {
const res = userLists.value.then(res => {
const mediaList = res.data.MediaListCollection.lists.find(({ status }) => status === 'COMPLETED').entries
const ids = mediaList.flatMap(({ media }) => {
return media.relations.edges.filter(edge => {
return edge.relationType === 'SEQUEL'
})
}).map(({ node }) => node.id)
return alRequest({ method: 'SearchIDS', page, perPage, id: ids, ...Sections.sanitiseObject(variables), status: ['FINISHED', 'RELEASING'], onList: false })
})
return Sections.wrapResponse(res, perPage)
}
},
{
title: 'Your List',
load: (page = 1, perPage = 50, variables = {}) => {
const res = userLists.value.then(res => {
const ids = res.data.MediaListCollection.lists.find(({ status }) => status === 'PLANNING').entries.map(({ media }) => media.id)
return alRequest({ method: 'SearchIDS', page, perPage, id: ids, ...Sections.sanitiseObject(variables) })
})
return Sections.wrapResponse(res, perPage)
}
}
]
userLists.subscribe(() => {
const titles = sections.map(({ title }) => title)
for (const section of manager.sections) {
if (titles.includes(section.title)) section.preview.value = undefined
}
})
manager.add(sections)
}
manager.add([
{
title: 'Popular This Season',
variables: { sort: 'POPULARITY_DESC', season: getSeason(date), year: date.getFullYear() }
},
{ title: 'Trending Now', variables: { sort: 'TRENDING_DESC' } },
{ title: 'All Time Popular', variables: { sort: 'POPULARITY_DESC' } },
{ title: 'Romance', variables: { sort: 'TRENDING_DESC', genre: 'Romance' } },
{ title: 'Action', variables: { sort: 'TRENDING_DESC', genre: 'Action' } },
{ title: 'Adventure', variables: { sort: 'TRENDING_DESC', genre: 'Adventure' } },
{ title: 'Fantasy', variables: { sort: 'TRENDING_DESC', genre: 'Fantasy' } },
{ title: 'Comedy', variables: { sort: 'TRENDING_DESC', genre: 'Comedy' } }
])
for (const sectionTitle of settings.value.homeSections) manager.add(mappedSections[sectionTitle])
const userSections = ['Continue Watching', 'Sequels You Missed', 'Your List', 'Completed List', 'Paused List', 'Dropped List', 'Currently Watching List']
userLists.subscribe(() => {
for (const section of manager.sections) {
// remove preview value, to force UI to re-request data, which updates it once in viewport
if (userSections.includes(section.title)) section.preview.value = undefined
}
})
</script>
<script>
@ -108,7 +35,9 @@
<Banner data={bannerData} />
<div class='d-flex flex-column h-full w-full'>
{#each manager.sections as section, i (i)}
<Section bind:opts={section} />
{#if !section.hide}
<Section bind:opts={section} />
{/if}
{/each}
</div>
</div>

View file

@ -1,5 +1,5 @@
<script>
import { set } from './Settings.svelte'
import { settings } from '@/modules/settings.js'
import { click } from '@/modules/click.js'
let block = false
@ -7,7 +7,7 @@
try {
for (let i = 0; i < 2; ++i) {
// fetch twice, sometimes it will go tru once if ISP is shitty
await fetch(set.toshoURL + 'json?show=torrent&id=1')
await fetch($settings.toshoURL + 'json?show=torrent&id=1')
}
block = false
} catch (e) {

View file

@ -1,5 +1,5 @@
<script>
import { set } from '../../views/Settings.svelte'
import { settings } from '@/modules/settings.js'
import { playAnime } from '../RSSView.svelte'
import { client } from '@/modules/torrent.js'
import { createEventDispatcher } from 'svelte'
@ -76,7 +76,7 @@
description: "This torrent's audio codec is not supported, try a different release by disabling Autoplay Torrents in RSS settings."
})
} else if (video.audioTracks.length > 1) {
const preferredTrack = [...video.audioTracks].find(({ language }) => language === set.audioLanguage)
const preferredTrack = [...video.audioTracks].find(({ language }) => language === settings.audioLanguage)
if (preferredTrack) return selectAudio(preferredTrack.id)
const japaneseTrack = [...video.audioTracks].find(({ language }) => language === 'jpn')
@ -218,7 +218,7 @@
}
let visibilityPaused = true
document.addEventListener('visibilitychange', () => {
if (!video?.ended && set.playerPause && !pip) {
if (!video?.ended && $settings.playerPause && !pip) {
if (document.visibilityState === 'hidden') {
visibilityPaused = paused
paused = true
@ -228,7 +228,7 @@
}
})
function tryPlayNext () {
if (set.playerAutoplay && !state.value) playNext()
if ($settings.playerAutoplay && !state.value) playNext()
}
function playNext () {
if (hasNext) {
@ -814,7 +814,7 @@
let completed = false
function checkCompletion () {
if (!completed && set.playerAutocomplete) {
if (!completed && $settings.playerAutocomplete) {
const fromend = Math.max(180, safeduration / 10)
if (safeduration && currentTime && video?.readyState && safeduration - fromend < currentTime) {
if (media?.media?.episodes || media?.media?.nextAiringEpisode?.episode) {

View file

@ -1,6 +1,6 @@
<script context='module'>
import { since } from '@/modules/util.js'
import { set } from './Settings.svelte'
import { settings } from '@/modules/settings.js'
import { toast } from 'svelte-sonner'
import { findInCurrent } from './Player/MediaHandler.svelte'
import getRSSEntries from '@/modules/providers/tosho.js'
@ -10,8 +10,6 @@
const rss = writable({})
const settings = set
export function playAnime (media, episode = 1, force) {
episode = Number(episode)
episode = isNaN(episode) ? 1 : episode
@ -87,7 +85,7 @@
const entries = await promise
entries.sort((a, b) => b.seeders - a.seeders)
if (settings.rssAutoplay) {
if ($settings.rssAutoplay) {
const best = entries.find(entry => entry.best)
if (best?.seeders >= 15) { // only play best if it actually has a lot of seeders, 20 might be too little for those overkill blurays
play(best)

View file

@ -1,6 +1,6 @@
<script context='module'>
import { writable } from 'simple-store-svelte'
import Sections from '@/modules/sections.js'
import SectionsManager from '@/modules/sections.js'
export const search = writable({})
@ -21,7 +21,7 @@
let container = null
function loadSearchData () {
const load = $search.load || Sections.createFallbackLoad()
const load = $search.load || SectionsManager.createFallbackLoad()
const nextData = load(++page, undefined, searchCleanup($search))
$items = [...$items, ...nextData]
return nextData[nextData.length - 1].data

View file

@ -1,36 +1,9 @@
<script context='module'>
import { toast } from 'svelte-sonner'
import { click } from '@/modules/click.js'
import { defaults } from '@/../common/util.js'
export let alToken = localStorage.getItem('ALtoken') || null
import { resetSettings, settings } from '@/modules/settings.js'
let storedSettings = { ...defaults }
try {
storedSettings = JSON.parse(localStorage.getItem('settings'))
} catch (e) {}
export const set = { ...defaults, ...storedSettings }
if (set.enableDoH) window.IPC.emit('doh', set.doHURL)
window.addEventListener('paste', ({ clipboardData }) => {
if (clipboardData.items?.[0]) {
if (clipboardData.items[0].type === 'text/plain' && clipboardData.items[0].kind === 'string') {
clipboardData.items[0].getAsString(text => {
let token = text.split('access_token=')?.[1]?.split('&token_type')?.[0]
if (token) {
if (token.endsWith('/')) token = token.slice(0, -1)
handleToken(token)
}
})
}
}
})
window.IPC.on('altoken', handleToken)
function handleToken (data) {
localStorage.setItem('ALtoken', data)
alToken = data
location.reload()
}
if (settings.value.enableDoH) window.IPC.emit('doh', settings.value.doHURL)
export const platformMap = {
aix: 'Aix',
darwin: 'MacOS',
@ -44,7 +17,7 @@
window.IPC.on('version', data => (version = data))
window.IPC.emit('version')
function updateAngle () {
window.IPC.emit('angle', set.angle)
window.IPC.emit('angle', settings.value.angle)
}
let wasUpdated = false
@ -71,7 +44,7 @@
const json = await res.json()
return json.map(({ body, tag_name: version }) => ({ body, version }))
})()
window.IPC.emit('discord_status', set.showDetailsInRPC)
window.IPC.emit('discord_status', settings.value.showDetailsInRPC)
</script>
<script>
@ -79,6 +52,8 @@
import FontSelect from '../components/FontSelect.svelte'
import { onDestroy } from 'svelte'
import { variables } from '@/modules/themes.js'
import { defaults } from '@/../common/util.js'
import HomeSections from './Settings/HomeSectionsSettings.svelte'
onDestroy(() => {
window.IPC.off('path')
@ -116,27 +91,18 @@
desc: 'Version change log.'
}
}
let settings = set
$: saveSettings(settings)
$: window.IPC.emit('discord_status', settings.showDetailsInRPC)
function saveSettings (settings) {
localStorage.setItem('settings', JSON.stringify(settings))
}
function restoreSettings () {
localStorage.removeItem('settings')
settings = { ...defaults }
}
$: window.IPC.emit('discord_status', $settings.showDetailsInRPC)
function handleFolder () {
window.IPC.emit('dialog')
}
window.IPC.on('path', data => {
settings.torrentPath = data
$settings.torrentPath = data
})
async function changeFont ({ detail }) {
try {
const blob = await detail.blob()
const data = await blob.arrayBuffer()
settings.font = {
$settings.font = {
name: detail.fullName,
value: detail.postscriptName,
data: [...new Uint8Array(data)]
@ -163,7 +129,7 @@
</script>
<Tabs>
<div class='d-flex w-full h-full'>
<div class='d-flex w-full h-full position-relative'>
<div class='d-flex flex-column h-full w-300 bg-dark position-relative'>
<div class='px-20 py-15 font-size-20 font-weight-semi-bold border-bottom root'>Settings</div>
{#each Object.values(groups) as group}
@ -202,7 +168,7 @@
Check For Updates
</button>
<button
use:click={restoreSettings}
use:click={resetSettings}
class='btn btn-danger mx-20 mt-10'
type='button'
data-toggle='tooltip'
@ -222,14 +188,14 @@
<div class='material-symbols-outlined mr-10 font-size-30'>font_download</div>
Default Subtitle Font
</div>
<FontSelect class='form-control bg-dark shadow-lg w-300' on:change={changeFont} value={settings.font?.value} />
<FontSelect class='form-control bg-dark shadow-lg w-300' on:change={changeFont} value={$settings.font?.value} />
</div>
<div
class='custom-switch mb-10 pl-10 font-size-16 w-300'
data-toggle='tooltip'
data-placement='bottom'
data-title="Automatically Finds Fonts That Are Missing From A Video's Subtitles">
<input type='checkbox' id='player-missingFont' bind:checked={settings.missingFont} />
<input type='checkbox' id='player-missingFont' bind:checked={$settings.missingFont} />
<label for='player-missingFont'>Find Missing Fonts</label>
</div>
<div class='col p-10 d-flex flex-column justify-content-end'>
@ -243,7 +209,7 @@
data-toggle='tooltip'
data-placement='bottom'
data-title='Automatically Starts Playing Next Episode When A Video Ends'>
<input type='checkbox' id='player-autoplay' bind:checked={settings.playerAutoplay} />
<input type='checkbox' id='player-autoplay' bind:checked={$settings.playerAutoplay} />
<label for='player-autoplay'>Autoplay Next Episode</label>
</div>
<div
@ -251,7 +217,7 @@
data-toggle='tooltip'
data-placement='bottom'
data-title='Pauses/Resumes Video Playback When Tabbing In/Out Of The App'>
<input type='checkbox' id='player-pause' bind:checked={settings.playerPause} />
<input type='checkbox' id='player-pause' bind:checked={$settings.playerPause} />
<label for='player-pause'>Pause When Tabbing Out</label>
</div>
<div
@ -259,7 +225,7 @@
data-toggle='tooltip'
data-placement='bottom'
data-title='Disables Blur When Rendering Subtitles Reducing Lag, Will Cause Text And Subtitle Edges To Appear Sharper'>
<input type='checkbox' id='player-sub-blur' bind:checked={settings.disableSubtitleBlur} />
<input type='checkbox' id='player-sub-blur' bind:checked={$settings.disableSubtitleBlur} />
<label for='player-sub-blur'>Fast Subtitle Rendering</label>
</div>
<div class='col p-10 d-flex flex-column justify-content-end'>
@ -273,7 +239,7 @@
data-toggle='tooltip'
data-placement='bottom'
data-title='Automatically Marks Episodes As Complete On AniList When You Finish Watching Them, Requires AniList Login'>
<input type='checkbox' id='player-autocomplete' bind:checked={settings.playerAutocomplete} />
<input type='checkbox' id='player-autocomplete' bind:checked={$settings.playerAutocomplete} />
<label for='player-autocomplete'>Autocomplete Episodes</label>
</div>
<div class='col p-10 d-flex flex-column justify-content-end'>
@ -286,7 +252,7 @@
<div class='input-group-prepend'>
<span class='input-group-text w-250 justify-content-center'>Preferred Subtitle Language</span>
</div>
<select class='form-control form-control-lg' bind:value={settings.subtitleLanguage}>
<select class='form-control form-control-lg' bind:value={$settings.subtitleLanguage}>
<option value=''>None</option>
<option value='eng' selected>English</option>
<option value='jpn'>Japanese</option>
@ -316,7 +282,7 @@
<div class='input-group-prepend'>
<span class='input-group-text w-250 justify-content-center'>Preferred Audio Language</span>
</div>
<select class='form-control form-control-lg' bind:value={settings.audioLanguage}>
<select class='form-control form-control-lg' bind:value={$settings.audioLanguage}>
<option value='eng'>English</option>
<option value='jpn' selected>Japanese</option>
<option value='chi'>Chinese</option>
@ -345,7 +311,7 @@
</Tab>
<Tab>
<div class='root p-20 m-20'>
{#each settings.rssFeedsNew as _, i}
{#each $settings.rssFeedsNew as _, i}
<div
class='input-group mb-10 w-700 form-control-lg'
data-toggle='tooltip'
@ -354,26 +320,26 @@
<div class='input-group-prepend'>
<span class='input-group-text w-100 justify-content-center'>Feed</span>
</div>
<input type='text' class='form-control form-control-lg w-150 flex-reset' placeholder='New Releases' autocomplete='off' bind:value={settings.rssFeedsNew[i][0]} />
<input id='rss-feed-{i}' type='text' list='rss-feed-list-{i}' class='w-400 form-control form-control-lg' placeholder={set.toshoURL + 'rss2?qx=1&q="[SubsPlease] "'} autocomplete='off' bind:value={settings.rssFeedsNew[i][1]} />
<input type='text' class='form-control form-control-lg w-150 flex-reset' placeholder='New Releases' autocomplete='off' bind:value={$settings.rssFeedsNew[i][0]} />
<input id='rss-feed-{i}' type='text' list='rss-feed-list-{i}' class='w-400 form-control form-control-lg' placeholder={$settings.toshoURL + 'rss2?qx=1&q="[SubsPlease] "'} autocomplete='off' bind:value={$settings.rssFeedsNew[i][1]} />
<datalist id='rss-feed-list-{i}'>
<option value='SubsPlease'>{set.toshoURL + 'rss2?qx=1&q="[SubsPlease] "'}</option>
<option value='NC-Raws'>{set.toshoURL + 'rss2?qx=1&q="[NC-Raws] "'}</option>
<option value='Erai-raws [Multi-Sub]'>{set.toshoURL + 'rss2?qx=1&q="[Erai-raws] "'}</option>
<option value='SubsPlease'>{$settings.toshoURL + 'rss2?qx=1&q="[SubsPlease] "'}</option>
<option value='NC-Raws'>{$settings.toshoURL + 'rss2?qx=1&q="[NC-Raws] "'}</option>
<option value='Erai-raws [Multi-Sub]'>{$settings.toshoURL + 'rss2?qx=1&q="[Erai-raws] "'}</option>
</datalist>
<div class='input-group-append'>
<button type='button' use:click={() => { settings.rssFeedsNew.splice(i, 1); settings.rssFeedsNew = settings.rssFeedsNew }} class='btn btn-danger btn-lg input-group-append'>Remove</button>
<button type='button' use:click={() => { $settings.rssFeedsNew.splice(i, 1); $settings.rssFeedsNew = $settings.rssFeedsNew }} class='btn btn-danger btn-lg input-group-append'>Remove</button>
</div>
</div>
{/each}
<div class='input-group input-group-lg form-control-lg mb-10 w-500'>
<button type='button' use:click={() => { settings.rssFeedsNew[settings.rssFeedsNew.length] = ['New Releases', null] }} class='btn btn-lg btn-primary mb-10'>Add Feed</button>
<button type='button' use:click={() => { $settings.rssFeedsNew[$settings.rssFeedsNew.length] = ['New Releases', null] }} class='btn btn-lg btn-primary mb-10'>Add Feed</button>
</div>
<div class='input-group mb-10 w-300 form-control-lg' data-toggle='tooltip' data-placement='top' data-title='What Quality To Find Torrents In'>
<div class='input-group-prepend'>
<span class='input-group-text w-100 justify-content-center'>Quality</span>
</div>
<select class='form-control form-control-lg' bind:value={settings.rssQuality}>
<select class='form-control form-control-lg' bind:value={$settings.rssQuality}>
<option value='1080' selected>1080p</option>
<option value='720'>720p</option>
<option value='480||540'>SD</option>
@ -386,7 +352,7 @@
data-placement='bottom'
data-title='Skips The Torrent Selection Popup, Might Lead To Unwanted Videos Being
Played'>
<input type='checkbox' id='rss-autoplay' bind:checked={settings.rssAutoplay} />
<input type='checkbox' id='rss-autoplay' bind:checked={$settings.rssAutoplay} />
<label for='rss-autoplay'>Auto-Play Torrents</label>
</div>
<div
@ -394,7 +360,7 @@
data-toggle='tooltip'
data-placement='bottom'
data-title='Enables DNS Over HTTPS, Useful If Your ISP Blocks Certain Domains'>
<input type='checkbox' id='rss-dohtoggle' bind:checked={settings.enableDoH} />
<input type='checkbox' id='rss-dohtoggle' bind:checked={$settings.enableDoH} />
<label for='rss-dohtoggle'>Enable DoH</label>
</div>
<div
@ -405,7 +371,7 @@
<div class='input-group-prepend'>
<span class='input-group-text w-150 justify-content-center'>DoH URL</span>
</div>
<input type='text' class='form-control' bind:value={settings.doHURL} placeholder={defaults.doHURL} />
<input type='text' class='form-control' bind:value={$settings.doHURL} placeholder={defaults.doHURL} />
</div>
<div
class='input-group input-group-lg form-control-lg mb-10 w-500'
@ -415,7 +381,7 @@
<div class='input-group-prepend'>
<span class='input-group-text w-150 justify-content-center'>Tosho URL</span>
</div>
<input type='text' class='form-control' bind:value={settings.toshoURL} placeholder={defaults.toshoURL} />
<input type='text' class='form-control' bind:value={$settings.toshoURL} placeholder={defaults.toshoURL} />
</div>
</div>
</Tab>
@ -429,7 +395,7 @@
<div class='input-group-prepend'>
<button type='button' use:click={handleFolder} class='btn btn-primary input-group-append'>Select Folder</button>
</div>
<input type='text' class='form-control' bind:value={settings.torrentPath} placeholder='Folder Path' />
<input type='text' class='form-control' bind:value={$settings.torrentPath} placeholder='Folder Path' />
</div>
<div
class='input-group w-300 form-control-lg mb-10'
@ -439,7 +405,7 @@
<div class='input-group-prepend'>
<span class='input-group-text w-150 justify-content-center'>Max Speed</span>
</div>
<input type='number' bind:value={settings.torrentSpeed} min='0' max='50' class='form-control text-right form-control-lg' />
<input type='number' bind:value={$settings.torrentSpeed} min='0' max='50' class='form-control text-right form-control-lg' />
<div class='input-group-append'>
<span class='input-group-text'>MB/s</span>
</div>
@ -452,7 +418,7 @@
<div class='input-group-prepend'>
<span class='input-group-text w-200 justify-content-center'>Max Connections</span>
</div>
<input type='number' bind:value={settings.maxConns} min='1' max='512' class='form-control text-right form-control-lg' />
<input type='number' bind:value={$settings.maxConns} min='1' max='512' class='form-control text-right form-control-lg' />
</div>
<div
class='input-group w-300 form-control-lg mb-10'
@ -462,7 +428,7 @@
<div class='input-group-prepend'>
<span class='input-group-text w-150 justify-content-center'>DHT Port</span>
</div>
<input type='number' bind:value={settings.dhtPort} min='0' max='65536' class='form-control text-right form-control-lg' />
<input type='number' bind:value={$settings.dhtPort} min='0' max='65536' class='form-control text-right form-control-lg' />
</div>
<div
class='input-group w-300 form-control-lg mb-10'
@ -472,14 +438,14 @@
<div class='input-group-prepend'>
<span class='input-group-text w-150 justify-content-center'>Torrent Port</span>
</div>
<input type='number' bind:value={settings.torrentPort} min='0' max='65536' class='form-control text-right form-control-lg' />
<input type='number' bind:value={$settings.torrentPort} min='0' max='65536' class='form-control text-right form-control-lg' />
</div>
<div
class='custom-switch mb-10 pl-10 font-size-16 w-300'
data-toggle='tooltip'
data-placement='bottom'
data-title="Doesn't Delete Files Of Old Torrents When A New Torrent Is Played">
<input type='checkbox' id='torrent-persist' bind:checked={settings.torrentPersist} />
<input type='checkbox' id='torrent-persist' bind:checked={$settings.torrentPersist} />
<label for='torrent-persist'>Persist Files</label>
</div>
<div
@ -487,7 +453,7 @@
data-toggle='tooltip'
data-placement='bottom'
data-title='Disables Distributed Hash Tables For Use In Private Trackers To Improve Privacy'>
<input type='checkbox' id='torrent-dht' bind:checked={settings.torrentDHT} />
<input type='checkbox' id='torrent-dht' bind:checked={$settings.torrentDHT} />
<label for='torrent-dht'>Disable DHT</label>
</div>
<div
@ -495,7 +461,7 @@
data-toggle='tooltip'
data-placement='bottom'
data-title='Disables Peer Exchange For Use In Private Trackers To Improve Privacy'>
<input type='checkbox' id='torrent-pex' bind:checked={settings.torrentPeX} />
<input type='checkbox' id='torrent-pex' bind:checked={$settings.torrentPeX} />
<label for='torrent-pex'>Disable PeX</label>
</div>
</div>
@ -507,7 +473,7 @@
data-toggle='tooltip'
data-placement='bottom'
data-title='Shows Currently Played Anime And Episode in Discord Rich Presence.'>
<input type='checkbox' id='rpc-details' bind:checked={settings.showDetailsInRPC} />
<input type='checkbox' id='rpc-details' bind:checked={$settings.showDetailsInRPC} />
<label for='rpc-details'>Show Details in Discord Rich Presence</label>
</div>
</div>
@ -519,7 +485,7 @@
data-toggle='tooltip'
data-placement='bottom'
data-title='Enables Smooth Scrolling For Long Vertical Containers. Impacts Performance.'>
<input type='checkbox' id='smooth-scroll' bind:checked={settings.smoothScroll} />
<input type='checkbox' id='smooth-scroll' bind:checked={$settings.smoothScroll} />
<label for='smooth-scroll'>Enable Smooth Scrolling</label>
</div>
<div
@ -527,7 +493,7 @@
data-toggle='tooltip'
data-placement='bottom'
data-title='Enables Sidebar Hover Animations'>
<input type='checkbox' id='disable-sidebar' bind:checked={settings.expandingSidebar} />
<input type='checkbox' id='disable-sidebar' bind:checked={$settings.expandingSidebar} />
<label for='disable-sidebar'>Enable Sidebar Animations</label>
</div>
<div class='form-group mb-10 pl-10 font-size-16 w-500'
@ -541,7 +507,7 @@
<div class='input-group-prepend'>
<span class='input-group-text w-200 justify-content-center'>ANGLE Backend</span>
</div>
<select class='form-control form-control-lg' bind:value={settings.angle} on:change={updateAngle}>
<select class='form-control form-control-lg' bind:value={$settings.angle} on:change={updateAngle}>
<option value='default' selected>Default</option>
<option value='d3d9'>D3D9</option>
<option value='d3d11'>D3D11</option>
@ -553,6 +519,7 @@
<option value='metal'>Metal</option>
</select>
</div>
<HomeSections bind:homeSections={$settings.homeSections} />
</div>
</Tab>
<Tab>

View file

@ -0,0 +1,102 @@
<script>
import { click } from '@/modules/click.js'
import { sections } from '@/modules/sections.js'
const allowedHomeSections = sections.map(({ title }) => title)
export let homeSections
let mouseYCoordinate = null // pointer y coordinate within client
let distanceTopGrabbedVsPointer = null
let draggingItem = null
let draggingItemIndex = null
let hoveredItemIndex = null
$: {
if (draggingItemIndex != null && hoveredItemIndex != null && draggingItemIndex !== hoveredItemIndex) {
[homeSections[draggingItemIndex], homeSections[hoveredItemIndex]] = [homeSections[hoveredItemIndex], homeSections[draggingItemIndex]]
draggingItemIndex = hoveredItemIndex
}
}
</script>
<div class='position-relative'>
<div class='col p-10 d-flex flex-column justify-content-end'>
<div class='font-size-24 font-weight-semi-bold d-flex'>
<div class='material-symbols-outlined mr-10 font-size-30'>list</div>
Home Sections Order
</div>
</div>
{#if mouseYCoordinate}
<div
class='input-group mb-10 form-control-lg ghost w-full'
style='top: {mouseYCoordinate + distanceTopGrabbedVsPointer}px;'>
<div class='input-group-prepend'>
<span class='input-group-text w-100 justify-content-center'>Feed</span>
</div>
<select class='form-control form-control-lg' value={draggingItem}>
{#each allowedHomeSections as section}
<option>{section}</option>
{/each}
</select>
<div class='input-group-append'>
<button type='button' class='btn btn-danger btn-lg input-group-append'>Remove</button>
</div>
</div>
{/if}
{#each homeSections as item, index}
<div
class='input-group mb-10 form-control-lg'
class:tp={draggingItem === item}
draggable='true'
role='menuitem'
tabindex='0'
on:dragstart={({ clientY, target }) => {
mouseYCoordinate = clientY
draggingItem = item
draggingItemIndex = index
distanceTopGrabbedVsPointer = target.offsetTop - clientY
}}
on:drag={e => { mouseYCoordinate = e.clientY }}
on:dragover={() => { hoveredItemIndex = index }}
on:dragend={() => {
mouseYCoordinate = null
draggingItem = null
hoveredItemIndex = null
}}>
<div class='input-group-prepend grab'>
<span class='input-group-text w-100 justify-content-center'>Feed</span>
</div>
<select class='form-control form-control-lg' bind:value={homeSections[index]}>
{#each allowedHomeSections as section}
<option>{section}</option>
{/each}
</select>
<div class='input-group-append'>
<button type='button' use:click={() => { homeSections.splice(index, 1); homeSections = homeSections }} class='btn btn-danger btn-lg input-group-append'>Remove</button>
</div>
</div>
{/each}
<div class='input-group input-group-lg form-control-lg mb-10 w-500'>
<button type='button' use:click={() => { homeSections[homeSections.length] = 'Trending Now' }} class='btn btn-lg btn-primary mb-10'>Add Section</button>
</div>
</div>
<style>
.ghost {
margin-bottom: 10px;
pointer-events: none;
z-index: 9999;
position: absolute !important;
}
.tp {
opacity: 0;
}
.grab{
cursor: grab;
}
</style>

View file

@ -1,5 +1,5 @@
<script>
import { alToken } from '../../views/Settings.svelte'
import { alToken } from '@/modules/settings.js'
import { alRequest } from '@/modules/anilist.js'
import { click } from '@/modules/click.js'
export let media = null