feat: nicer notifications

This commit is contained in:
ThaUnknown 2023-07-15 21:50:28 +02:00
parent 2597e00f45
commit 4ca90751e6
16 changed files with 114 additions and 168 deletions

View file

@ -48,6 +48,7 @@
"svelte-keybinds": "1.0.5",
"svelte-loader": "^3.1.9",
"svelte-miniplayer": "1.0.3",
"svelte-sonner": "^0.1.1",
"webpack": "^5.85.0",
"webpack-cli": "^5.1.3",
"webpack-dev-server": "^4.15.0",

View file

@ -112,6 +112,9 @@ devDependencies:
svelte-miniplayer:
specifier: 1.0.3
version: 1.0.3
svelte-sonner:
specifier: ^0.1.1
version: 0.1.1(svelte@4.0.4)
webpack:
specifier: ^5.85.0
version: 5.86.0(webpack-cli@5.1.4)
@ -5485,6 +5488,14 @@ packages:
resolution: {integrity: sha512-++IvuENs/3x0SbHZ/jWNqfq+hmU/ZT73V/ETwn6TqCbHF7asXsZFxVcgyIL1lMaQMKxe227HrYghYRVNJXeFVw==}
dev: true
/svelte-sonner@0.1.1(svelte@4.0.4):
resolution: {integrity: sha512-jQ/coEZvJxImxxsT5FPnHeXitGx5wQRhAkAz6C8DY8yM1qFPvFowG6mDtC0KPPDEKlHCmocbWigk6zrG27OO/Q==}
peerDependencies:
svelte: ^4.0.0
dependencies:
svelte: 4.0.4
dev: true
/svelte@4.0.4:
resolution: {integrity: sha512-DDJavyX1mpNFLZ7jU9FwBKouemh6CJHZXwePBa5GXSaW5GuHZ361L2/1uznBqOCxu2UsUoWu8wRsB2iB8QG5sQ==}
engines: {node: '>=16'}

View file

@ -22,8 +22,8 @@
import ViewTrailer from './views/ViewAnime/ViewTrailer.svelte'
import RSSView from './views/RSSView.svelte'
import Menubar from './components/Menubar.svelte'
import Toasts from './components/Toasts.svelte'
import IspBlock from './views/IspBlock.svelte'
import { Toaster, toast } from 'svelte-sonner'
setContext('view', view)
@ -31,7 +31,7 @@
</script>
<div id='player' />
<Toasts />
<Toaster visibleToasts={3} position='top-right' theme='dark' richColors duration={10000} />
<div class='page-wrapper with-sidebar with-transitions bg-dark' data-sidebar-type='overlayed-all'>
<div class='sticky-alerts' />
<IspBlock />

View file

@ -3,7 +3,7 @@
import { alID } from '@/modules/anilist.js'
import { media } from '../views/Player/MediaHandler.svelte'
import { platformMap } from '../views/Settings.svelte'
import { addToast } from './Toasts.svelte'
import { toast } from 'svelte-sonner'
import { click } from '@/modules/click.js'
const view = getContext('view')
export let page
@ -17,11 +17,9 @@
} else {
window.IPC.emit('open', 'https://anilist.co/api/v2/oauth/authorize?client_id=4254&response_type=token') // Change redirect_url to miru://auth
if (platformMap[window.version.platform] === 'Linux') {
addToast({
text: "If your linux distribution doesn't support custom protocol handlers, you can simply paste the full URL into the app.",
title: 'Support Notification',
type: 'secondary',
duration: '300000'
toast('Support Notification', {
description: "If your linux distribution doesn't support custom protocol handlers, you can simply paste the full URL into the app.",
duration: 300000
})
}
}

View file

@ -1,56 +0,0 @@
<script context='module'>
import { writable } from 'svelte/store'
import { click } from '@/modules/click.js'
const toasts = writable({})
let index = 0
export function addToast (opts) {
// type, click, title, text
toasts.update(toasts => {
const i = ++index
toasts[i] = opts
setTimeout(() => {
close(i)
}, opts.duration || 10000)
return toasts
})
}
function close (index) {
toasts.update(toasts => {
if (toasts[index]) {
delete toasts[index]
}
return toasts
})
}
</script>
<div class='sticky-alerts d-flex flex-column-reverse'>
{#each Object.entries($toasts) as [index, toast] (index)}
<div class='alert alert-{toast.type} filled' class:pointer={toast.click} use:click={toast.click}>
<button class='close' type='button' use:click={() => close(index)}><span aria-hidden='true'>×</span></button>
<h4 class='alert-heading'>{toast.title}</h4>
{@html toast.text}
</div>
{/each}
</div>
<style>
.alert {
display: block !important;
animation: 0.3s ease 0s 1 fly-in;
right: 0;
}
.sticky-alerts {
top: 2.5rem
}
@keyframes fly-in {
from {
right: -50rem;
}
to {
right: 0;
}
}
</style>

View file

@ -28,6 +28,20 @@
--dm-button-secondary-bg-color-hover: #ddd !important;
}
[data-sonner-toaster][data-theme='dark'] {
--normal-bg: var(--dark-color) !important;
--normal-border: none !important;
--normal-text: var(--dm-base-text-color) !important;
/* --success-bg: var(--success-color) !important; */
--success-border: none !important;
/* --success-text: var(--lm-base-text-color) !important; */
/* --error-bg: hsl(358, 76%, 10%); */
--error-border: none !important;
/* --error-text: hsl(358, 100%, 81%); */
}
.material-symbols-outlined {
font-family: "Material Symbols Outlined Variable";
font-weight: normal;

View file

@ -3,7 +3,7 @@ import { writable } from 'simple-store-svelte'
import Bottleneck from 'bottleneck'
import { alToken } from '../views/Settings.svelte'
import { addToast } from '../components/Toasts.svelte'
import { toast } from 'svelte-sonner'
import { sleep } from './util.js'
const codes = {
@ -106,10 +106,8 @@ if (alToken) {
function printError (error) {
console.warn(error)
addToast({
text: /* html */`Failed making request to anilist!<br>Try again in a minute.<br>${error.status || 429} - ${error.message || codes[error.status || 429]}`,
title: 'Search Failed',
type: 'danger',
toast.error('Search Failed', {
description: `Failed making request to anilist!\nTry again in a minute.\n${error.status || 429} - ${error.message || codes[error.status || 429]}`,
duration: 3000
})
}

View file

@ -3,7 +3,7 @@ import { DOMPARSER, PromiseBatch, binarySearch } from './util.js'
import { alRequest, alSearch } from './anilist.js'
import anitomyscript from 'anitomyscript'
import { media } from '../views/Player/MediaHandler.svelte'
import { addToast } from '../components/Toasts.svelte'
import { toast } from 'svelte-sonner'
import { view } from '@/App.svelte'
import { playAnime } from '../views/RSSView.svelte'
@ -15,7 +15,17 @@ window.addEventListener('paste', ({ clipboardData }) => { // WAIT image lookup o
const item = clipboardData.items[0]
if (!item) return
const { type } = item
if (type.startsWith('image')) return traceAnime(item.getAsFile())
if (type.startsWith('image')) {
const promise = traceAnime(item.getAsFile())
toast.promise(promise, {
description: 'You can also paste an URL to an image.',
loading: 'Looking up anime for image...',
success: 'Found anime for image!',
error: 'Couldn\'t find anime for specified image! Try to remove black bars, or use a more detailed image.'
})
return
}
if (!type.startsWith('text')) return
item.getAsString(text => {
if (torrentRx.exec(text)) {
@ -28,26 +38,19 @@ window.addEventListener('paste', ({ clipboardData }) => { // WAIT image lookup o
} else if (imageRx.exec(text)) {
src = text
}
if (src) traceAnime(src)
if (src) {
const promise = traceAnime(src)
toast.promise(promise, {
description: 'You can also paste an URL to an image.',
loading: 'Looking up anime for image...',
success: 'Found anime for image!',
error: 'Couldn\'t find anime for specified image! Try to remove black bars, or use a more detailed image.'
})
}
}
})
})
export async function traceAnime (image) { // WAIT lookup logic
if (image instanceof Blob) {
const reader = new FileReader()
reader.onload = e => {
addToast({
title: 'Looking up anime for image',
text: /* html */`You can also paste an URL to an image!<br><img class="w-200 rounded pt-5" src="${e.target.result}">`
})
}
reader.readAsDataURL(image)
} else {
addToast({
title: 'Looking up anime for image',
text: /* html */`<img class="w-200 rounded pt-5" src="${image}">`
})
}
let options
let url = `https://api.trace.moe/search?cutBorders&url=${image}`
if (image instanceof Blob) {
@ -60,14 +63,12 @@ export async function traceAnime (image) { // WAIT lookup logic
}
const res = await fetch(url, options)
const { result } = await res.json()
if (result && result[0].similarity >= 0.85) {
if (result?.[0].similarity >= 0.85) {
const res = await alRequest({ method: 'SearchIDSingle', id: result[0].anilist })
view.set(res.data.Media)
} else {
addToast({
text: 'Couldn\'t find anime for specified image! Try to remove black bars, or use a more detailed image.',
title: 'Search Failed',
type: 'danger'
throw new Error('Search Failed', {
message: 'Couldn\'t find anime for specified image! Try to remove black bars, or use a more detailed image.'
})
}
}
@ -315,10 +316,8 @@ export async function resolveSeason (opts) {
const obj = { media, episode: episode - offset, offset, increment, rootMedia, failed: true }
if (!force) {
console.warn('Error in parsing!', obj)
addToast({
text: /* html */`Failed resolving anime episode!<br>${media.title.userPreferred} - ${episode - offset}`,
title: 'Parsing Error',
type: 'secondary'
toast('Parsing Error', {
description: `Failed resolving anime episode!\n${media.title.userPreferred} - ${episode - offset}`
})
}
return obj

View file

@ -45,6 +45,8 @@ export default async function tosho ({ media, episode }) {
found.seeders = complete
}
if (!mapped?.length) throw new Error('no entries found')
return mapped
}

View file

@ -1,6 +1,6 @@
import { DOMPARSER } from '@/modules/util.js'
import { set } from '@/views/Settings.svelte'
import { addToast } from '@/components/Toasts.svelte'
import { toast } from 'svelte-sonner'
import { add } from '@/modules/torrent.js'
import { resolveFileMedia, getEpisodeMetadataForMedia } from './anime.js'
import { hasNextPage } from '@/modules/sections.js'
@ -49,19 +49,15 @@ export async function getRSSContent (url) {
try {
const res = await fetch(url)
if (!res.ok) {
addToast({
text: 'Failed fetching RSS!<br>' + res.statusText,
title: 'Search Failed',
type: 'danger'
toast.error('Search Failed', {
description: 'Failed fetching RSS!\n' + res.statusText
})
console.error('Failed to fetch rss', res.statusText)
}
return DOMPARSER(await res.text(), 'text/xml')
} catch (e) {
addToast({
text: 'Failed fetching RSS!<br>' + e.message,
title: 'Search Failed',
type: 'danger'
toast.error('Search Failed', {
description: 'Failed fetching RSS!\n' + e.message
})
console.error('Failed to fetch rss', e)
}

View file

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

View file

@ -6,7 +6,7 @@
import { alEntry } from '@/modules/anilist.js'
import Subtitles from '@/modules/subtitles.js'
import { toTS, videoRx, fastPrettyBytes } from '@/modules/util.js'
import { addToast } from '../../components/Toasts.svelte'
import { toast } from 'svelte-sonner'
import { getChaptersAniSkip } from '@/modules/anime.js'
import Seekbar from 'perfect-seekbar'
import { click } from '@/modules/click.js'
@ -71,10 +71,8 @@
function checkAudio () {
if ('audioTracks' in HTMLVideoElement.prototype) {
if (!video.audioTracks.length) {
addToast({
text: "This torrent's audio codec is not supported, try a different release by disabling Autoplay Torrents in RSS settings.",
title: 'Audio Codec Unsupported',
type: 'danger'
toast.error('Audio Codec Unsupported', {
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)
@ -335,10 +333,8 @@
})
])
canvas.remove()
addToast({
text: 'Saved screenshot to clipboard.',
title: 'Screenshot',
type: 'success'
toast.success('Screenshot', {
description: 'Saved screenshot to clipboard.'
})
}
}
@ -830,31 +826,25 @@
break
case target.error.MEDIA_ERR_NETWORK:
console.warn('A network error caused the video download to fail part-way.', target.error)
addToast({
text: 'A network error caused the video download to fail part-way. Click here to reload the video.',
title: 'Video Network Error',
type: 'danger',
toast.error('Video Network Error', {
description: 'A network error caused the video download to fail part-way. Click here to reload the video.',
duration: 1000000,
click: () => target.load()
onClick: () => target.load()
})
break
case target.error.MEDIA_ERR_DECODE:
console.warn('The video playback was aborted due to a corruption problem or because the video used features your browser did not support.', target.error)
addToast({
text: 'The video playback was aborted due to a corruption problem. Click here to reload the video.',
title: 'Video Decode Error',
type: 'danger',
toast.error('Video Decode Error', {
description: 'The video playback was aborted due to a corruption problem. Click here to reload the video.',
duration: 1000000,
click: () => target.load()
onClick: () => target.load()
})
break
case target.error.MEDIA_ERR_SRC_NOT_SUPPORTED:
if (target.error.message !== 'MEDIA_ELEMENT_ERROR: Empty src attribute') {
console.warn('The video could not be loaded, either because the server or network failed or because the format is not supported.', target.error)
addToast({
text: 'The video could not be loaded, either because the server or network failed or because the format is not supported. Try a different release by disabling Autoplay Torrents in RSS settings.',
title: 'Video Codec Unsupported',
type: 'danger',
toast.error('Video Codec Unsupported', {
description: 'The video could not be loaded, either because the server or network failed or because the format is not supported. Try a different release by disabling Autoplay Torrents in RSS settings.',
duration: 300000
})
}
@ -881,8 +871,7 @@
on:keypress={resetImmerse}
on:mouseleave={immersePlayer}>
{#if showKeybinds && !miniplayer}
<div class='position-absolute bg-tp w-full h-full z-50 font-size-12 p-20 d-flex align-items-center justify-content-center'>
<button class='btn btn-square rounded-circle bg-dark font-size-16 top-0 right-0 m-10 position-absolute' type='button' use:click={() => (showKeybinds = false)}>&times;</button>
<div class='position-absolute bg-tp w-full h-full z-50 font-size-12 p-20 d-flex align-items-center justify-content-center pointer' on:pointerup|self={() => (showKeybinds = false)} tabindex='-1' role='button'>
<Keybinds let:prop={item} autosave={true} clickable={true}>
<div class:material-symbols-outlined={item?.type} class='bind'>{item?.id || ''}</div>
</Keybinds>

View file

@ -1,7 +1,7 @@
<script context='module'>
import { since } from '@/modules/util.js'
import { set } from './Settings.svelte'
import { addToast } from '../components/Toasts.svelte'
import { toast } from 'svelte-sonner'
import { findInCurrent } from './Player/MediaHandler.svelte'
import getRSSEntries from '@/modules/providers/tosho.js'
import { click } from '@/modules/click.js'
@ -73,15 +73,19 @@
async function loadRss ({ media, episode }) {
if (!media) return
const entries = await getRSSEntries({ media, episode })
if (!entries?.length) {
addToast({
text: /* html */`Couldn't find torrent for ${media.title.userPreferred} Episode ${parseInt(episode)}! Try specifying a torrent manually.`,
title: 'Search Failed',
type: 'danger'
})
return
const promise = getRSSEntries({ media, episode })
toast.promise(promise, {
loading: `Looking for torrents for ${media.title.userPreferred} Episode ${parseInt(episode)}...`,
success: `Found torrents for ${media.title.userPreferred} Episode ${parseInt(episode)}.`,
error: `Couldn't find torrents for ${media.title.userPreferred} Episode ${parseInt(episode)}! Try specifying a torrent manually.`
})
let entries
try {
entries = await promise
} catch (e) {
return e
}
entries.sort((a, b) => b.seeders - a.seeders)
if (settings.rssAutoplay) {
const best = entries.find(entry => entry.best)
@ -103,10 +107,8 @@
function play (entry) {
$media = $rss
if (entry.seeders !== '?' && entry.seeders <= 15) {
addToast({
text: 'This release is poorly seeded and likely will have playback issues such as buffering!',
title: 'Availability Warning',
type: 'secondary'
toast('Availability Warning', {
description: 'This release is poorly seeded and likely will have playback issues such as buffering!'
})
}
add(entry.link)

View file

@ -1,5 +1,6 @@
<script context='module'>
import { addToast } from '../components/Toasts.svelte'
import { toast } from 'svelte-sonner'
import { click } from '@/modules/click.js'
export let alToken = localStorage.getItem('ALtoken') || null
const defaults = {
@ -64,17 +65,14 @@
window.IPC.on('update-available', () => {
if (!wasUpdated) {
wasUpdated = true
addToast({
title: 'Auto Updater',
text: 'A new version of Miru is available. Downloading!'
toast('Auto Updater', {
description: 'A new version of Miru is available. Downloading!'
})
}
})
window.IPC.on('update-downloaded', () => {
addToast({
title: 'Auto Updater',
text: 'A new version of Miru has downloaded. You can restart to update!',
type: 'success'
toast.success('Auto Updater', {
description: 'A new version of Miru has downloaded. You can restart to update!'
})
})
function checkUpdate () {
@ -158,10 +156,8 @@
}
} catch (error) {
console.warn(error)
addToast({
text: /* html */`${error.message}<br>Try using a different font.`,
title: 'File Error',
type: 'secondary',
toast.error('File Error', {
description: `${error.message}<br>Try using a different font.`,
duration: 8000
})
}

View file

@ -2,7 +2,7 @@
import { getContext, setStatus } from 'svelte'
import { getMediaMaxEp, formatMap, playMedia } from '@/modules/anime.js'
import { playAnime } from '../RSSView.svelte'
import { addToast } from '../../components/Toasts.svelte'
import { toast } from 'svelte-sonner'
import { alRequest } from '@/modules/anilist.js'
import { click } from '@/modules/click.js'
import Details from './Details.svelte'
@ -64,11 +64,9 @@
}
function copyToClipboard (text) {
navigator.clipboard.writeText(text)
addToast({
title: 'Copied to clipboard',
text: 'Copied share URL to clipboard',
type: 'primary',
duration: '5000'
toast('Copied to clipboard', {
description: 'Copied share URL to clipboard',
duration: 5000
})
}
function openInBrowser (url) {

View file

@ -3,7 +3,7 @@
import { alID } from '@/modules/anilist.js'
import { add, client } from '@/modules/torrent.js'
import { generateRandomHexCode } from '@/modules/util.js'
import { addToast } from '../../components/Toasts.svelte'
import { toast } from 'svelte-sonner'
import { page } from '@/App.svelte'
import P2PT from 'p2pt'
import { click } from '@/modules/click.js'
@ -148,11 +148,9 @@
function invite () {
if (p2pt) {
navigator.clipboard.writeText(`https://miru.watch/w2g/${p2pt.identifierString}`)
addToast({
title: 'Copied to clipboard',
text: 'Copied invite URL to clipboard',
type: 'primary',
duration: '5000'
toast('Copied to clipboard', {
description: 'Copied invite URL to clipboard',
duration: 5000
})
}
}