Skip to content

Commit 536a2aa

Browse files
committed
feat: hide/show ui based on hero carousel focus
1 parent 58bbe26 commit 536a2aa

18 files changed

+358
-184
lines changed

src/lib/components/DetachedPage/DetachedPage.svelte

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
<script lang="ts">
2-
import Container from '../Container.svelte';
3-
import { type KeyEvent, type NavigateEvent, useRegistrar } from '../../selectable.js';
4-
import { get } from 'svelte/store';
5-
import Sidebar from '../Sidebar/Sidebar.svelte';
62
import classNames from 'classnames';
3+
import { get } from 'svelte/store';
4+
import { useRegistrar } from '../../selectable.js';
5+
import Container from '../Container.svelte';
76
import type { ContainerProps } from '../Container.type';
7+
import Sidebar from '../Sidebar/Sidebar.svelte';
88
99
interface $$Props extends ContainerProps {
1010
topmost?: boolean;

src/lib/components/HeroCarousel/HeroBackground.svelte

+11-12
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
<script lang="ts">
2-
import { PLATFORM_TV } from '../../constants';
2+
import { localSettings } from '$lib/stores/localstorage.store';
3+
import { getUiVisibilityContext } from '$lib/stores/ui-visibility.store';
34
import classNames from 'classnames';
45
import { onDestroy } from 'svelte';
5-
import { isFirefox } from '../../utils/browser-detection';
6+
import { PLATFORM_TV } from '../../constants';
67
import YouTubeVideo from '../YoutubeVideo.svelte';
7-
import { fade } from 'svelte/transition';
8-
import { localSettings } from '$lib/stores/localstorage.store';
8+
9+
const { visibleStyle } = getUiVisibilityContext();
910
1011
export let items: Promise<{ backdropUrl: string; videoUrl?: string }[]>;
1112
export let index: number;
1213
export let hasFocus = true;
13-
export let heroHasFocus = false;
14-
export let hideInterface = false;
14+
export let videoVisible = false;
1515
let visibleIndex = -2;
1616
let visibleIndexTimeout: ReturnType<typeof setTimeout>;
1717
@@ -47,15 +47,16 @@
4747
class={classNames('absolute inset-0 bg-center bg-cover', {
4848
'opacity-100': visibleIndex === i,
4949
'opacity-0': visibleIndex !== i,
50-
'scale-110': !hasFocus
50+
'scale-110': !hasFocus && !PLATFORM_TV
5151
})}
5252
style={`background-image: url('${backdropUrl}'); transition: opacity 500ms, transform 500ms;`}
5353
>
5454
{#if videoUrl && i === visibleIndex && $localSettings.enableTrailers}
5555
<YouTubeVideo
5656
videoId={videoUrl}
5757
autoplay={$localSettings.autoplayTrailers}
58-
visible={$localSettings.autoplayTrailers ? heroHasFocus : hasFocus}
58+
visible={videoVisible}
59+
muted={!hasFocus}
5960
/>
6061
{/if}
6162
</div>
@@ -92,10 +93,8 @@
9293
{/if}
9394
</div>
9495
<div
95-
class={classNames('absolute inset-0 flex flex-col transition-opacity', {
96-
'opacity-0': hideInterface
97-
})}
98-
style="-webkit-transform: translate3d(0,0,0);"
96+
class={classNames('absolute inset-0 flex flex-col')}
97+
style={`-webkit-transform: translate3d(0,0,0); ${$visibleStyle}`}
9998
>
10099
<div class="h-screen bg-gradient-to-b from-transparent to-secondary-900" />
101100
<div class="flex-1 bg-secondary-900" />

src/lib/components/HeroCarousel/HeroCarousel.svelte

+39-18
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,26 @@
11
<script lang="ts">
2+
import { getUiVisibilityContext } from '$lib/stores/ui-visibility.store';
3+
import classNames from 'classnames';
4+
import { ChevronRight } from 'radix-icons-svelte';
5+
import { createEventDispatcher, tick } from 'svelte';
6+
import { get, type Readable, type Writable } from 'svelte/store';
27
import Container from '../Container.svelte';
3-
import HeroBackground from './HeroBackground.svelte';
48
import IconButton from '../FloatingIconButton.svelte';
5-
import { ChevronRight, ChevronUp } from 'radix-icons-svelte';
69
import PageDots from '../HeroShowcase/PageDots.svelte';
7-
import type { Readable, Writable } from 'svelte/store';
8-
import { createEventDispatcher } from 'svelte';
9-
import classNames from 'classnames';
10+
import HeroBackground from './HeroBackground.svelte';
11+
import { Selectable } from '$lib/selectable';
12+
import { localSettings } from '$lib/stores/localstorage.store';
13+
import { getScrollContext } from '$lib/stores/scroll.store';
1014
1115
const dispatch = createEventDispatcher();
16+
const { visibleStyle, visible } = getUiVisibilityContext();
17+
const { topVisible } = getScrollContext();
1218
1319
export let items: Promise<{ backdropUrl: string; videoUrl?: string }[]>;
1420
export let index = 0;
15-
export let hideInterface = false;
21+
export let hasFocus = false;
22+
23+
let selectable: Selectable;
1624
1725
let length = 0;
1826
@@ -44,7 +52,19 @@
4452
4553
let heroHasFocusWithin: Readable<boolean>;
4654
let focusIndex: Writable<number>;
47-
$: backgroundHasFocus = $heroHasFocusWithin && $focusIndex === 0;
55+
$: hasFocus = $heroHasFocusWithin && $focusIndex === 0;
56+
$: visible?.set(!hasFocus || !$topVisible);
57+
58+
let focusedObject: Selectable | undefined = undefined;
59+
topVisible?.subscribe((v) => !v && handleClickFocus(true));
60+
function handleClickFocus(unfocusOnly = false) {
61+
if (hasFocus && focusedObject) {
62+
tick().then(() => focusedObject?.focus());
63+
} else if (!unfocusOnly) {
64+
focusedObject = get(Selectable.focusedObject);
65+
selectable?.focusChild(0);
66+
}
67+
}
4868
</script>
4969

5070
<Container
@@ -53,7 +73,7 @@
5373
on:select
5474
on:navigate={(event) => {
5575
const detail = event.detail;
56-
if (!backgroundHasFocus) return;
76+
if (!hasFocus) return;
5777
if (detail.direction === 'right') {
5878
if (onNext()) {
5979
detail.preventNavigation();
@@ -70,27 +90,28 @@
7090
}}
7191
bind:hasFocusWithin={heroHasFocusWithin}
7292
bind:focusIndex
93+
bind:selectable
7394
>
7495
<HeroBackground
7596
{items}
7697
{index}
77-
hasFocus={backgroundHasFocus}
78-
heroHasFocus={$heroHasFocusWithin}
79-
{hideInterface}
98+
{hasFocus}
99+
videoVisible={$localSettings.autoplayTrailers ? $topVisible ?? true : !$visible}
80100
/>
81-
<div
82-
class={classNames('flex flex-1 z-10 transition-opacity', {
83-
'opacity-0': hideInterface
84-
})}
85-
>
86-
<slot />
101+
<div class={classNames('flex flex-1 z-10')}>
102+
<!-- svelte-ignore a11y-click-events-have-key-events -->
103+
<div class="flex-1 flex flex-col justify-end" on:click|self={() => handleClickFocus()}>
104+
<div style={$visibleStyle}>
105+
<slot />
106+
</div>
107+
</div>
87108
<!-- <div
88109
class="absolute inset-x-1/2 -translate-x-1/2 top-16 min-w-fit flex flex-col items-center justify-center"
89110
>
90111
<ChevronUp size={38} />
91112
<div class="whitespace-nowrap">View Trailer</div>
92113
</div> -->
93-
<div class="flex flex-col justify-end ml-4">
114+
<div class="flex flex-col justify-end ml-4" style={$visibleStyle}>
94115
<div class="flex flex-1 justify-end items-center">
95116
<IconButton on:click={onNext}>
96117
<ChevronRight size={38} />

src/lib/components/HeroShowcase/HeroShowcase.svelte

+34-36
Original file line numberDiff line numberDiff line change
@@ -52,41 +52,39 @@
5252
}}
5353
on:select={openItem}
5454
>
55-
<div class="h-full flex-1 flex overflow-hidden z-10 relative">
56-
{#await items}
57-
<!-- <div class="flex-1 flex items-end">-->
58-
<!-- <CardPlaceholder orientation="portrait" />-->
59-
<!-- <div class="flex flex-col">-->
60-
<!-- <div>stats</div>-->
61-
<!-- <div>title</div>-->
62-
<!-- <div>genres</div>-->
63-
<!-- </div>-->
64-
<!-- </div>-->
65-
{:then items}
66-
{@const item = items[showcaseIndex]}
67-
{#if item}
68-
<div class="flex-1 flex items-end">
69-
<div class="mr-8">
70-
<!-- <Card orientation="portrait" backdropUrl={TMDB_POSTER_SMALL + item.posterUrl} />-->
71-
<!-- svelte-ignore a11y-click-events-have-key-events -->
72-
<div
73-
class="bg-center bg-cover rounded-xl w-44 h-64 cursor-pointer"
74-
style={`background-image: url("${TMDB_POSTER_SMALL + item.posterUri}")`}
75-
on:click={openItem}
76-
/>
77-
</div>
78-
<div class="flex flex-col">
79-
<HeroTitleInfo
80-
title={item.title}
81-
properties={item.infoProperties}
82-
overview={item.overview ?? ''}
83-
onClickTitle={openItem}
84-
/>
85-
</div>
55+
{#await items}
56+
<!-- <div class="flex-1 flex items-end">-->
57+
<!-- <CardPlaceholder orientation="portrait" />-->
58+
<!-- <div class="flex flex-col">-->
59+
<!-- <div>stats</div>-->
60+
<!-- <div>title</div>-->
61+
<!-- <div>genres</div>-->
62+
<!-- </div>-->
63+
<!-- </div>-->
64+
{:then items}
65+
{@const item = items[showcaseIndex]}
66+
{#if item}
67+
<div class="flex-1 flex items-end">
68+
<div class="mr-8">
69+
<!-- <Card orientation="portrait" backdropUrl={TMDB_POSTER_SMALL + item.posterUrl} />-->
70+
<!-- svelte-ignore a11y-click-events-have-key-events -->
71+
<div
72+
class="bg-center bg-cover rounded-xl w-44 h-64 cursor-pointer"
73+
style={`background-image: url("${TMDB_POSTER_SMALL + item.posterUri}")`}
74+
on:click={openItem}
75+
/>
8676
</div>
87-
{/if}
88-
{:catch error}
89-
<p>{error.message}</p>
90-
{/await}
91-
</div>
77+
<div class="flex flex-col">
78+
<HeroTitleInfo
79+
title={item.title}
80+
properties={item.infoProperties}
81+
overview={item.overview ?? ''}
82+
onClickTitle={openItem}
83+
/>
84+
</div>
85+
</div>
86+
{/if}
87+
{:catch error}
88+
<p>{error.message}</p>
89+
{/await}
9290
</HeroCarousel>

src/lib/components/ScrollHelper.svelte

+7
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
11
<script lang="ts">
22
import { onMount } from 'svelte';
33
import { getScrollParent } from '../utils';
4+
import { getScrollContext } from '$lib/stores/scroll.store';
45
56
export let scrollTop: number = 0;
67
export let scrollLeft: number = 0;
78
9+
const scrollStore = getScrollContext();
10+
if (!scrollStore.scrollLeft || !scrollStore.scrollTop) console.error('ScrollHelper requires ScrollStore');
11+
12+
$: scrollStore.scrollTop?.set(scrollTop);
13+
$: scrollStore.scrollLeft?.set(scrollLeft);
14+
815
let div: HTMLElement;
916
1017
onMount(() => {

src/lib/components/Sidebar/Sidebar.svelte

+5-1
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import { useTabs } from '../Tab/Tab';
1818
import { user } from '../../stores/user.store';
1919
import { sessions } from '../../stores/session.store';
20+
import { getUiVisibilityContext } from '$lib/stores/ui-visibility.store';
2021
2122
enum Tabs {
2223
Users,
@@ -29,6 +30,8 @@
2930
3031
const tab = useTabs(Tabs.Series);
3132
33+
const { visibleStyle } = getUiVisibilityContext();
34+
3235
let selectedIndex = 0;
3336
let activeIndex = -1;
3437
@@ -52,7 +55,7 @@
5255
// if (index === activeIndex) {
5356
// if (get(selectable.hasFocusWithin)) Selectable.giveFocus('right');
5457
// }
55-
selectable.focusChild(index);
58+
selectable.focusChild(index, { setFocusedElement: false });
5659
const path =
5760
{
5861
[Tabs.Users]: '/users',
@@ -101,6 +104,7 @@
101104
bind:focusIndex
102105
bind:selectable
103106
on:mount={registrars.sidebar.registrar}
107+
style={$visibleStyle}
104108
>
105109
<!-- Background -->
106110
<div

src/lib/components/StackRouter/StackRouter.ts

+30-31
Original file line numberDiff line numberDiff line change
@@ -46,12 +46,16 @@ export function useStackRouter({
4646
maxDepth?: number;
4747
}) {
4848
const { initialPages, initialIndexes } = getInitialValues();
49-
const indexes = writable<Indexes>(initialIndexes);
50-
const pageStack = writable<Page[]>(initialPages);
51-
const visibleStack = derived([indexes, pageStack], ([$indexes, $stack]) => {
52-
return $stack.slice(
53-
maxDepth ? Math.max($indexes.bottom, $indexes.top - maxDepth + 1) : $indexes.bottom,
54-
$indexes.top + 1
49+
const pageStack = writable<{ pages: Page[]; indexes: Indexes }>({
50+
pages: initialPages,
51+
indexes: initialIndexes
52+
});
53+
const visibleStack = derived(pageStack, ($stack) => {
54+
return $stack.pages.slice(
55+
maxDepth
56+
? Math.max($stack.indexes.bottom, $stack.indexes.top - maxDepth + 1)
57+
: $stack.indexes.bottom,
58+
$stack.indexes.top + 1
5559
);
5660
});
5761

@@ -126,46 +130,41 @@ export function useStackRouter({
126130
const replaceStack = page.route.root || options.replaceStack || false;
127131

128132
pageStack.update((prev) => {
129-
const idxs = get(indexes);
130-
if (replaceStack) return [page];
131-
else {
132-
prev.splice(idxs.top + 1, Infinity, page);
133-
return prev;
134-
}
135-
});
133+
let pages = prev.pages;
134+
let idxs = prev.indexes;
136135

137-
if (replaceStack) {
138-
const stack = get(pageStack);
139-
indexes.update((prev) => {
136+
if (replaceStack) pages = [page];
137+
else pages.splice(idxs.top + 1, Infinity, page);
138+
139+
if (replaceStack) {
140140
const indexes: Indexes = {
141141
id: Math.random().toString(36).slice(2),
142-
bottom: stack.length - 1,
143-
top: stack.length - 1
142+
bottom: pages.length - 1,
143+
top: pages.length - 1
144144
};
145145
history.pushState(indexes, '', routeString);
146-
return indexes;
147-
});
148-
} else {
149-
indexes.update((prev) => {
150-
const indexes: Indexes = { id: prev.id, bottom: prev.bottom, top: prev.top + 1 };
146+
idxs = indexes;
147+
} else {
148+
const indexes: Indexes = { id: idxs.id, bottom: idxs.bottom, top: idxs.top + 1 };
151149
history.pushState(indexes, '', routeString);
152-
return indexes;
153-
});
154-
}
150+
idxs = indexes;
151+
}
152+
153+
return { pages, indexes: idxs };
154+
});
155155
};
156156

157157
const handlePopState = (e: PopStateEvent) => {
158158
const newIndexes: Indexes = e.state;
159-
const prevIndexes = get(indexes);
159+
const prevIndexes = get(pageStack);
160160

161161
modalStack.reset();
162162

163-
if (prevIndexes.id === newIndexes.id) {
164-
indexes.set(newIndexes);
163+
if (prevIndexes.indexes.id === newIndexes.id) {
164+
pageStack.update((p) => ({ ...p, indexes: newIndexes }));
165165
} else {
166166
const initialValues = getInitialValues();
167-
indexes.set(initialValues.initialIndexes);
168-
pageStack.set(initialValues.initialPages);
167+
pageStack.set({ indexes: initialValues.initialIndexes, pages: initialValues.initialPages });
169168
}
170169
};
171170

0 commit comments

Comments
 (0)