mirror of
https://github.com/NoCrypt/migu.git
synced 2026-04-19 15:42:05 +00:00
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:
parent
ab1d14eee6
commit
a806bd1f08
22 changed files with 422 additions and 240 deletions
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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)}
|
||||
|
|
|
|||
|
|
@ -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'}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 = {}
|
||||
|
|
|
|||
|
|
@ -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')]
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 })
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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' } }
|
||||
]
|
||||
}
|
||||
|
|
|
|||
46
src/renderer/modules/settings.js
Normal file
46
src/renderer/modules/settings.js
Normal 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()
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
102
src/renderer/views/Settings/HomeSectionsSettings.svelte
Normal file
102
src/renderer/views/Settings/HomeSectionsSettings.svelte
Normal 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>
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue