feat: more responsive view anime page

fix: click listener behavior with touch
feat: bottom nabar
feat: hide android nav and status bars
feat: gamepad controls logic
This commit is contained in:
ThaUnknown 2023-11-15 01:04:18 +01:00
parent 3b5acf019b
commit 80050fa6e1
12 changed files with 393 additions and 58 deletions

View file

@ -36,6 +36,8 @@
"@capacitor/cli": "^5.5.1",
"@capacitor/core": "^5.5.1",
"@capacitor/ios": "^5.5.1",
"@capacitor/status-bar": "^5.0.6",
"@mauricewegner/capacitor-navigation-bar": "^2.0.3",
"@superfrogbe/cordova-plugin-chrome-apps-sockets-udp": "github:superfrogbe/cordova-plugin-chrome-apps-sockets-udp",
"common": "workspace:*",
"cordova-plugin-chrome-apps-common": "^1.0.7",

View file

@ -1,5 +1,23 @@
import TorrentClient from 'common/modules/webtorrent.js'
import { ipcRendererWebTorrent } from './ipc.js'
import { StatusBar, Style } from '@capacitor/status-bar'
import { NavigationBar } from '@mauricewegner/capacitor-navigation-bar'
StatusBar.hide()
StatusBar.setStyle({ style: Style.Dark })
StatusBar.setOverlaysWebView({ overlay: true })
NavigationBar.setColor({ color: '#17191c' })
function hideAndroidNavBar () {
NavigationBar.hide()
// NavigationBar.setTransparency({ isTransparent: true })
}
screen.orientation.addEventListener('change', () => {
hideAndroidNavBar()
})
hideAndroidNavBar()
globalThis.chrome.runtime = { lastError: false, id: 'something' }

View file

@ -25,11 +25,12 @@
import IspBlock from './views/IspBlock.svelte'
import { Toaster } from 'svelte-sonner'
import Logout from './components/Logout.svelte'
import Navbar from './components/Navbar.svelte'
setContext('view', view)
</script>
<div class='page-wrapper with-sidebar with-transitions bg-dark' data-sidebar-type='overlayed-all'>
<div class='page-wrapper with-transitions bg-dark position-relative' data-sidebar-type='overlayed-all'>
<IspBlock />
<Menubar bind:page={$page} />
<ViewAnime />
@ -40,6 +41,7 @@
<RSSView />
<Router bind:page={$page} />
</div>
<Navbar bind:page={$page} />
</div>
<style>
@ -50,8 +52,15 @@
.page-wrapper > .content-wrapper {
margin-left: var(--sidebar-minimised) !important;
position: unset !important;
width: calc(100% - var(--sidebar-minimised)) !important;
transition: none !important;
}
.page-wrapper {
height: calc(100% - var(--navbar-height)) !important;
}
@media (min-width: 769px) {
.page-wrapper {
padding-left: env(safe-area-inset-left) !important;
}
}
</style>

View file

@ -10,7 +10,7 @@
export let page = 'home'
</script>
<div class='wrapper h-full position-absolute overflow-hidden'>
<div class='w-full h-full position-absolute overflow-hidden'>
<Miniplayer active={page !== 'player'} class='bg-dark-light z-10 {page === 'player' ? 'h-full' : ''}' minwidth='35rem' maxwidth='60rem' width='300px' padding='2rem'>
<MediaHandler miniplayer={page !== 'player'} bind:page />
</Miniplayer>
@ -26,12 +26,3 @@
{:else if page === 'watchtogether'}
<WatchTogether />
{/if}
<style>
.wrapper {
width: calc(100% - var(--sidebar-minimised));
}
:global(:fullscreen) .wrapper {
width: 100%;
}
</style>

View file

@ -14,7 +14,7 @@
<div class='w-full z-101 navbar bg-transparent border-0 p-0 d-flex'>
<div class='d-flex h-full draggable align-items-center text-center'>
{#if window.version.platform !== 'darwin'}
<img src='./logo.ico' class='position-absolute w-50 h-50 m-10 pointer' alt='ico' use:click={close} />
<img src='./logo.ico' class='position-absolute w-50 h-50 m-10 pointer d-md-block d-none' alt='ico' use:click={close} />
{/if}
</div>
<div class='h-full bg-dark flex-grow-1'>
@ -56,4 +56,7 @@
path {
fill: currentColor;
}
.navbar {
left: unset !important
}
</style>

View file

@ -0,0 +1,199 @@
<script>
import { getContext } from 'svelte'
import { alID } from '@/modules/anilist.js'
import { media } from '../views/Player/MediaHandler.svelte'
import { platformMap } from '../views/Settings.svelte'
import { toast } from 'svelte-sonner'
import { click } from '@/modules/click.js'
import { logout } from './Logout.svelte'
import IPC from '@/modules/ipc.js'
const view = getContext('view')
export let page
const links = [
{
click: () => {
if (alID) {
$logout = true
} else {
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') {
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
})
}
}
},
icon: 'login',
text: 'Login With AniList',
css: 'ml-auto'
},
{
click: () => {
page = 'home'
},
page: 'home',
icon: 'home',
text: 'Home'
},
{
click: () => {
page = 'search'
},
page: 'search',
icon: 'search',
text: 'Search'
},
{
click: () => {
page = 'schedule'
},
page: 'schedule',
icon: 'schedule',
text: 'Schedule'
},
{
click: () => {
if ($media) $view = $media.media
},
icon: 'queue_music',
text: 'Now Playing'
},
{
click: () => {
page = 'watchtogether'
},
page: 'watchtogether',
icon: 'groups',
text: 'Watch Together'
},
{
click: () => {
IPC.emit('open', 'https://github.com/sponsors/ThaUnknown/')
},
icon: 'favorite',
text: 'Support This App',
css: 'ml-auto donate'
},
{
click: () => {
page = 'settings'
},
page: 'settings',
icon: 'settings',
text: 'Settings'
}
]
if (alID) {
alID.then(result => {
if (result?.data?.Viewer) {
links[0].image = result.data.Viewer.avatar.medium
links[0].text = 'Logout'
}
})
}
function close () {
$view = null
page = 'home'
}
</script>
<nav class='navbar navbar-fixed-bottom d-block d-md-none border-0 bg-dark'>
<div class='navbar-menu h-full d-flex flex-row justify-content-center align-items-center m-0 pb-5' class:animate={page !== 'player'}>
<img src='./logo.ico' class='w-50 h-50 m-10 pointer' alt='ico' use:click={close} />
{#each links as { click: _click, icon, text, image, css, page: _page }, i (i)}
<div
class='navbar-link navbar-link-with-icon pointer overflow-hidden {css}'
use:click={_click}>
{#if image}
<span class='material-symbols-outlined rounded' class:filled={page === _page}>
<img src={image} class='h-30 rounded' alt='logo' />
</span>
{:else}
<span class='material-symbols-outlined rounded' class:filled={page === _page}>{icon}</span>
{/if}
</div>
{/each}
</div>
</nav>
<style>
nav {
height: var(--navbar-height);
}
@keyframes glow {
from {
text-shadow: 0 0 2rem #fa68b6;
}
to {
text-shadow: 0 0 1rem #fa68b6;
}
}
.animate .donate .material-symbols-outlined {
animation: glow 1s ease-in-out infinite alternate;
}
.donate:hover .material-symbols-outlined {
background: #fff;
color: #fa68b6 !important;
}
.donate .material-symbols-outlined {
font-variation-settings: 'FILL' 1;
color: #fa68b6;
text-shadow: 0 0 1rem #fa68b6;
}
/* .sidebar-menu {
padding-top: 10rem;
} */
.text {
opacity: 1;
transition: opacity 0.8s cubic-bezier(0.25, 0.8, 0.25, 1);
display: inline-flex;
justify-content: center;
align-items: center;
}
.navbar-link > span {
color: #fff;
border-radius: 0.3rem;
}
.material-symbols-outlined {
color: #fff;
transition: background .8s cubic-bezier(0.25, 0.8, 0.25, 1), color .8s cubic-bezier(0.25, 0.8, 0.25, 1);
}
.navbar-link:hover > span {
background: #fff;
color: var(--dark-color);
}
.navbar-link {
font-size: 1.4rem;
padding: 0.75rem;
height: 5.5rem;
}
.material-symbols-outlined {
font-size: 2.2rem;
min-width: 4rem;
width: 4rem;
height: 4rem;
display: inline-flex;
justify-content: center;
align-items: center;
}
.navbar-link img {
font-size: 2.2rem;
width: 3rem;
height: 3rem;
margin: 0.5rem;
display: inline-flex;
justify-content: center;
align-items: center;
}
img {
margin-right: var(--sidebar-brand-image-margin-right);
}
</style>

View file

@ -95,7 +95,7 @@
}
</script>
<div class='sidebar z-30' class:animated={$settings.expandingSidebar}>
<div class='sidebar z-30 d-md-block' 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)}
@ -200,7 +200,8 @@
transition: width .8s cubic-bezier(0.25, 0.8, 0.25, 1), left .8s cubic-bezier(0.25, 0.8, 0.25, 1) !important;
background: none !important;
overflow-y: unset;
overflow-x: visible
overflow-x: visible;
left: unset;
}
.sidebar.animated:hover {
width: 22rem

View file

@ -10,8 +10,9 @@
--card-border-width: 0;
--sidebar-border-width: 0;
--input-border-width: 0;
--sidebar-minimised: 7rem;
--sidebar-width: 7rem;
--sidebar-minimised: 0px;
--sidebar-width: 0px;
--navbar-height: 7rem;
--dark-color-base-hue: 220deg !important;
--dark-color-base-saturation: 10% !important;
--dark-color-hsl: var(--dark-color-base-hue), var(--dark-color-base-saturation), 10% !important;
@ -24,6 +25,14 @@
color-scheme: dark;
}
@media (min-width: 769px) {
:root {
--sidebar-minimised: 7rem;
--sidebar-width: 7rem;
--navbar-height: 0px;
}
}
.btn-secondary {
--dm-button-secondary-bg-color: #fff !important;
--dm-button-secondary-bg-color-hover: #ddd !important;

View file

@ -1,19 +1,30 @@
let lastTapElement = null
let lastHoverElement = null
const noop = _ => {}
document.addEventListener('pointerup', () => {
lastTapElement?.(false)
lastTapElement = null
setTimeout(() => {
lastTapElement?.(false)
lastTapElement = null
lastHoverElement?.(false)
lastHoverElement = null
})
})
export function click (node, cb = noop) {
node.tabIndex = 0
node.role = 'button'
node.addEventListener('pointerdown', e => {
node.addEventListener('click', e => {
e.stopPropagation()
cb(e)
})
node.addEventListener('pointerup', e => {
e.stopPropagation()
})
node.addEventListener('pointerleave', e => {
e.stopPropagation()
})
node.addEventListener('keydown', e => { if (e.key === 'Enter') cb(e) })
}
@ -22,11 +33,13 @@ export function hoverClick (node, [cb = noop, hoverUpdate = noop]) {
node.tabIndex = 0
node.role = 'button'
node.addEventListener('pointerenter', e => {
lastHoverElement?.(false)
hoverUpdate(true)
lastTapElement?.(false)
lastHoverElement = hoverUpdate
pointerType = e.pointerType
})
node.addEventListener('pointerdown', e => {
node.addEventListener('click', e => {
e.stopPropagation()
if (pointerType === 'mouse') return cb(e)
lastTapElement?.(false)
if (lastTapElement === hoverUpdate) {
@ -39,9 +52,67 @@ export function hoverClick (node, [cb = noop, hoverUpdate = noop]) {
node.addEventListener('keydown', e => { if (e.key === 'Enter') cb(e) })
node.addEventListener('pointerup', e => {
e.stopPropagation()
if (e.pointerType === 'mouse') hoverUpdate(false)
if (e.pointerType === 'mouse') setTimeout(() => hoverUpdate(false))
})
node.addEventListener('pointerleave', e => {
setTimeout(() => { lastTapElement = hoverUpdate })
if (e.pointerType === 'mouse') hoverUpdate(false)
})
}
const Directions = { up: 1, right: 2, down: 3, left: 4 }
function getDirection (anchor, relative) {
return Math.round((Math.atan2(relative.y - anchor.y, relative.x - anchor.x) * 180 / Math.PI + 180) / 90)
}
function getDistance (anchor, relative) {
return Math.hypot(relative.x - anchor.x, relative.y - anchor.y)
}
/**
* Gets keyboard-focusable elements within a specified element
* @param {Element} [element=document.body] element
* @returns {Element[]}
*/
function getKeyboardFocusableElements (element = document.body) {
return [...element.querySelectorAll('a[href], button:not([disabled]), fieldset:not([disabled]), input:not([disabled]), optgroup:not([disabled]), option:not([disabled]), select:not([disabled]), textarea:not([disabled]), details, [tabindex]:not([tabindex="-1"]), [contenteditable], [controls]')].filter(
el => !el.getAttribute('aria-hidden')
)
}
/**
* @param {Element} element
*/
function getElementPosition (element) {
const { x, y, width, height } = element.getBoundingClientRect()
if (width || height) {
return { element, x: x + width * 0.5, y: y + height * 0.5 }
}
}
function getFocusableElementPositions () {
const elements = []
for (const element of getKeyboardFocusableElements()) {
const position = getElementPosition(element)
if (position) elements.push(position)
}
return elements
}
function navigateDPad (direction = 'up') {
const keyboardFocusable = getFocusableElementPositions()
const currentElement = !document.activeElement || document.activeElement === document.body ? keyboardFocusable[0] : getElementPosition(document.activeElement)
const elementsInDesiredDirection = keyboardFocusable.filter(position => {
return position.element !== currentElement.element && getDirection(currentElement, position) === Directions[direction]
})
const closestElement = elementsInDesiredDirection.reduce((reducer, position) => {
const distance = getDistance(currentElement, position)
if (distance < reducer.distance) return { distance, element: position.element }
return reducer
}, { distance: Infinity, element: null })
closestElement.element.focus()
}

View file

@ -20,7 +20,7 @@
</script>
{#if block}
<div class='w-full h-full left-0 z-50 position-absolute content-wrapper bg-dark d-flex align-items-center justify-content-center flex-column'>
<div class='w-full h-full left-0 z-50 px-10 position-absolute content-wrapper bg-dark d-flex align-items-center justify-content-center flex-column'>
<div>
<h1 class='font-weight-bold'>Could not connect to Tosho!</h1>
<div class='font-size-16'>This happens either because Tosho is down, or because your ISP blocks Tosho, the latter being more likely.</div>

View file

@ -82,39 +82,47 @@
<img class='w-full cover-img banner position-absolute' alt='banner' src={media.bannerImage || ' '} />
<div class='row'>
<div class='col-lg-7 col-12 pb-10'>
<div class='d-flex flex-md-row flex-column align-items-end pb-20 mb-15'>
<div class='d-flex flex-md-row flex-column align-items-md-end pb-20 mb-15'>
<div class='cover d-flex flex-row align-items-md-end align-items-center mw-full w-full mb-md-0 mb-20'>
<img class='rounded cover-img w-full overflow-hidden h-full' alt='cover-art' src={media.coverImage?.extraLarge || media.coverImage?.medium} />
</div>
<div class='pl-md-20 ml-md-20'>
<h1 class='font-weight-very-bold text-white select-all'>{media.title.userPreferred}</h1>
<p class='d-flex flex-row font-size-18'>
<h1 class='font-weight-very-bold text-white select-all mb-0'>{media.title.userPreferred}</h1>
<div class='d-flex flex-row font-size-18 flex-wrap mt-5'>
{#if media.averageScore}
<span class='material-symbols-outlined mx-10 font-size-24'> trending_up </span>
<span class='mr-20'>
Rating: {media.averageScore + '%'}
</span>
<div class='d-flex flex-row mt-10'>
<span class='material-symbols-outlined mx-10 font-size-24'> trending_up </span>
<span class='mr-20'>
Rating: {media.averageScore + '%'}
</span>
</div>
{/if}
{#if media.format}
<span class='material-symbols-outlined mx-10 font-size-24'> monitor </span>
<span class='mr-20 text-capitalize'>
Format: {formatMap[media.format]}
</span>
<div class='d-flex flex-row mt-10'>
<span class='material-symbols-outlined mx-10 font-size-24'> monitor </span>
<span class='mr-20 text-capitalize'>
Format: {formatMap[media.format]}
</span>
</div>
{/if}
{#if media.episodes !== 1 && getMediaMaxEp(media)}
<span class='material-symbols-outlined mx-10 font-size-24'> theaters </span>
<span class='mr-20'>
Episodes: {getMediaMaxEp(media)}
</span>
<div class='d-flex flex-row mt-10'>
<span class='material-symbols-outlined mx-10 font-size-24'> theaters </span>
<span class='mr-20'>
Episodes: {getMediaMaxEp(media)}
</span>
</div>
{:else if media.duration}
<span class='material-symbols-outlined mx-10 font-size-24'> timer </span>
<span class='mr-20'>
Length: {media.duration + ' min'}
</span>
<div class='d-flex flex-row mt-10'>
<span class='material-symbols-outlined mx-10 font-size-24'> timer </span>
<span class='mr-20'>
Length: {media.duration + ' min'}
</span>
</div>
{/if}
</p>
<div class='d-flex flex-row pt-5'>
<button class='btn btn-lg btn-secondary w-250 text-dark font-weight-bold shadow-none border-0 d-flex align-items-center justify-content-center'
</div>
<div class='d-flex flex-row flex-wrap'>
<button class='btn btn-lg btn-secondary w-250 text-dark font-weight-bold shadow-none border-0 d-flex align-items-center justify-content-center mr-10 mt-20'
use:click={() => play()}
disabled={media.status === 'NOT_YET_RELEASED'}>
<span class='material-symbols-outlined font-size-24 filled pr-10'>
@ -122,18 +130,20 @@
</span>
{playButtonText}
</button>
<button class='btn bg-dark btn-lg btn-square ml-10 material-symbols-outlined font-size-20 shadow-none border-0' class:filled={media.isFavourite} use:click={toggleFavourite}>
favorite
</button>
<button class='btn bg-dark btn-lg btn-square ml-10 material-symbols-outlined font-size-20 shadow-none border-0' class:filled={media.mediaListEntry} use:click={toggleStatus}>
bookmark
</button>
<button class='btn bg-dark btn-lg btn-square ml-10 material-symbols-outlined font-size-20 shadow-none border-0' use:click={() => copyToClipboard(`https://miru.watch/anime/${media.id}`)}>
share
</button>
<button class='btn bg-dark btn-lg btn-square ml-10 material-symbols-outlined font-size-20 shadow-none border-0' use:click={() => openInBrowser(`https://anilist.co/anime/${media.id}`)}>
open_in_new
</button>
<div class='mt-20'>
<button class='btn bg-dark btn-lg btn-square material-symbols-outlined font-size-20 shadow-none border-0' class:filled={media.isFavourite} use:click={toggleFavourite}>
favorite
</button>
<button class='btn bg-dark btn-lg btn-square ml-10 material-symbols-outlined font-size-20 shadow-none border-0' class:filled={media.mediaListEntry} use:click={toggleStatus}>
bookmark
</button>
<button class='btn bg-dark btn-lg btn-square ml-10 material-symbols-outlined font-size-20 shadow-none border-0' use:click={() => copyToClipboard(`https://miru.watch/anime/${media.id}`)}>
share
</button>
<button class='btn bg-dark btn-lg btn-square ml-10 material-symbols-outlined font-size-20 shadow-none border-0' use:click={() => openInBrowser(`https://anilist.co/anime/${media.id}`)}>
open_in_new
</button>
</div>
</div>
</div>
</div>

View file

@ -68,6 +68,12 @@ importers:
'@capacitor/ios':
specifier: ^5.5.1
version: 5.5.1(@capacitor/core@5.5.1)
'@capacitor/status-bar':
specifier: ^5.0.6
version: 5.0.6(@capacitor/core@5.5.1)
'@mauricewegner/capacitor-navigation-bar':
specifier: ^2.0.3
version: 2.0.3(@capacitor/core@5.5.1)
'@superfrogbe/cordova-plugin-chrome-apps-sockets-udp':
specifier: github:superfrogbe/cordova-plugin-chrome-apps-sockets-udp
version: github.com/superfrogbe/cordova-plugin-chrome-apps-sockets-udp/4b740017299c81cfc7d5b49c7d6122a6650b57d4
@ -292,6 +298,14 @@ packages:
'@capacitor/core': 5.5.1
dev: false
/@capacitor/status-bar@5.0.6(@capacitor/core@5.5.1):
resolution: {integrity: sha512-7od8CxsBnot1XMK3IeOkproFL4hgoKoWAc3pwUvmDOkQsXoxwQm4SR9mLwQavv1XfxtHbFV9Ukd7FwMxOPSViw==}
peerDependencies:
'@capacitor/core': ^5.0.0
dependencies:
'@capacitor/core': 5.5.1
dev: false
/@develar/schema-utils@2.6.5:
resolution: {integrity: sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig==}
engines: {node: '>= 8.9.0'}
@ -592,6 +606,14 @@ packages:
- supports-color
dev: true
/@mauricewegner/capacitor-navigation-bar@2.0.3(@capacitor/core@5.5.1):
resolution: {integrity: sha512-E8HTcVkZEqm4tLJ7MpkTlqc93mboUSbJL41fFbvEaVwBhpgenEezkK268RxwQDij5hkudZmo4/eRXG3aJQwrzQ==}
peerDependencies:
'@capacitor/core': ^4.0.1 || ^5.0.0
dependencies:
'@capacitor/core': 5.5.1
dev: false
/@nodelib/fs.scandir@2.1.5:
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
engines: {node: '>= 8'}