Skip to content

Commit b8c3921

Browse files
committed
feat (spatial navigation): This allows for navigation using the arrow keys on keyboard and remote control. Also add support for play/pause button on webos
1 parent bddb6de commit b8c3921

File tree

11 files changed

+105
-10
lines changed

11 files changed

+105
-10
lines changed

frontend/assets/global.scss

+14
Original file line numberDiff line numberDiff line change
@@ -155,3 +155,17 @@ body {
155155
.overflow-hidden {
156156
overflow: hidden;
157157
}
158+
159+
.card-margin:focus-within,
160+
.card-margin:focus {
161+
box-shadow: 0 0 0 5px var(--v-primary-base);
162+
border-radius: 5px;
163+
outline: none;
164+
background-color: var(--v-primary-base);
165+
}
166+
button.primary:focus,
167+
button.primary:focus-visible {
168+
box-shadow: 0 0 0 5px white !important;
169+
outline: none !important;
170+
}
171+

frontend/components/Buttons/PlayButton.vue

+3-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
<div class="d-inline-flex">
33
<v-btn
44
v-if="canPlay(item) && (fab || iconOnly)"
5+
v-focus
56
:fab="fab"
67
:text="iconOnly"
78
:color="iconOnly ? null : 'primary'"
@@ -14,6 +15,7 @@
1415
</v-btn>
1516
<v-btn
1617
v-else-if="!fab"
18+
v-focus
1719
:disabled="disabled || !canPlay(item)"
1820
:loading="loading"
1921
class="mr-2"
@@ -39,7 +41,7 @@ import { BaseItemDto } from '@jellyfin/client-axios';
3941
import Vue from 'vue';
4042
import { mapStores } from 'pinia';
4143
import { playbackManagerStore } from '~/store';
42-
import { canResume, canPlay } from '~/utils/items';
44+
import { canPlay, canResume } from '~/utils/items';
4345
import { ticksToMs } from '~/utils/time';
4446
4547
export default Vue.extend({

frontend/components/Item/Card/Card.vue

+16-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
<template>
2-
<div :class="{ 'card-margin': margin }">
2+
<div
3+
v-focus="link"
4+
:class="{ 'card-margin': margin }"
5+
@keyup.enter="cardClicked()"
6+
>
37
<component
48
:is="link ? 'nuxt-link' : 'div'"
59
:to="link ? getItemDetailsLink(item) : null"
@@ -87,10 +91,10 @@ import Vue from 'vue';
8791
import { mapStores } from 'pinia';
8892
import { BaseItemDto, ImageType } from '@jellyfin/client-axios';
8993
import {
94+
canPlay,
9095
CardShapes,
91-
getShapeFromItemType,
9296
getItemDetailsLink,
93-
canPlay
97+
getShapeFromItemType
9498
} from '~/utils/items';
9599
import { taskManagerStore } from '~/store';
96100
@@ -250,6 +254,15 @@ export default Vue.extend({
250254
isFinePointer(): boolean {
251255
return window.matchMedia('(pointer:fine)').matches;
252256
},
257+
cardClicked() {
258+
if (this.link) {
259+
const itemLink = getItemDetailsLink(this.item);
260+
261+
if (itemLink) {
262+
this.$router.push(itemLink);
263+
}
264+
}
265+
},
253266
getItemDetailsLink,
254267
canPlay
255268
}

frontend/components/Item/SeasonTabs.vue

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<template>
22
<div>
33
<v-tabs v-model="currentTab" class="mb-3" background-color="transparent">
4-
<v-tab v-for="season in seasons" :key="season.Id">
4+
<v-tab v-for="season in seasons" :key="season.Id" v-focus>
55
{{ season.Name }}
66
</v-tab>
77
</v-tabs>

frontend/components/Layout/Navigation/NavigationDrawer.vue

+3-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
<v-list-item
1717
v-for="item in items"
1818
:key="item.Id"
19+
v-focus
1920
:to="item.to"
2021
router
2122
exact
@@ -31,6 +32,7 @@
3132
<v-list-item
3233
v-for="library in userViews.getNavigationDrawerItems"
3334
:key="library.Id"
35+
v-focus
3436
:to="library.to"
3537
router
3638
exact
@@ -52,7 +54,7 @@
5254
<script lang="ts">
5355
import Vue from 'vue';
5456
import { mapStores } from 'pinia';
55-
import { userViewsStore, pageStore } from '~/store';
57+
import { pageStore, userViewsStore } from '~/store';
5658
5759
interface LayoutButton {
5860
icon: string;

frontend/components/Players/PlayerManager.vue

+13-1
Original file line numberDiff line numberDiff line change
@@ -373,14 +373,18 @@ export default Vue.extend({
373373
374374
let spaceEnabled = false;
375375
376+
// Adds support for remote
377+
// Some keys are 'Unidentified' on LGtv.
378+
const key = e.key === 'Unidentified' ? `${e.keyCode}` : e.key;
379+
376380
if (e.key === 'Spacebar' || e.key === ' ') {
377381
spaceEnabled =
378382
focusEl?.classList.contains('v-dialog__content') ||
379383
focusEl?.classList.contains('hide-pointer') ||
380384
focusEl?.className === '';
381385
}
382386
383-
switch (e.key) {
387+
switch (key) {
384388
case 'Spacebar':
385389
case ' ':
386390
if (spaceEnabled) {
@@ -389,14 +393,22 @@ export default Vue.extend({
389393
390394
break;
391395
case 'k':
396+
case 'Enter':
397+
case '415':
398+
case '19':
399+
case '13':
392400
this.playbackManager.playPause();
393401
break;
394402
case 'ArrowRight':
395403
case 'l':
404+
case '417':
405+
case '39':
396406
this.playbackManager.skipForward();
397407
break;
398408
case 'ArrowLeft':
399409
case 'j':
410+
case '412':
411+
case '37':
400412
this.playbackManager.skipBackward();
401413
break;
402414
case 'f':

frontend/layouts/default.vue

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<template>
2-
<v-app>
2+
<v-app v-focus-section:app>
33
<backdrop />
44
<navigation-drawer />
55
<app-bar />

frontend/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
"uuid": "8.3.2",
5454
"vee-validate": "3.4.14",
5555
"vue-awesome-swiper": "4.1.1",
56+
"vue-js-spatial-navigation": "2.0.14",
5657
"vue-virtual-scroller": "1.0.10",
5758
"vuedraggable": "2.24.3"
5859
},

frontend/plugins/nuxt/browserDetectionPlugin.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,9 @@ export class BrowserDetector {
102102
isChrome(): boolean {
103103
// The Edge user agent will also contain the "Chrome" keyword, so we need
104104
// to make sure this is not Edge. Same happens for webos.
105-
return this.userAgentContains('Chrome') && !this.isEdge() && !this.isWebOS();
105+
return (
106+
this.userAgentContains('Chrome') && !this.isEdge() && !this.isWebOS()
107+
);
106108
}
107109

108110
/**

frontend/plugins/vue/components.ts

+27-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import Vue from 'vue';
2-
import { ValidationProvider, ValidationObserver } from 'vee-validate';
2+
import { ValidationObserver, ValidationProvider } from 'vee-validate';
33
import draggable from 'vuedraggable';
44
// @ts-expect-error - target typing doesn't exist as we declared it in params.
55
import { DynamicScroller, DynamicScrollerItem } from 'vue-virtual-scroller';
@@ -8,12 +8,38 @@ import VueAwesomeSwiper from 'vue-awesome-swiper';
88
import Swiper from 'swiper';
99
import 'swiper/css/swiper.css';
1010

11+
// @ts-expect-error - target typing doesn't exist in npm
12+
import vjsn from 'vue-js-spatial-navigation';
13+
1114
declare module 'vue/types/vue' {
1215
interface Vue {
1316
$swiper: Swiper;
1417
}
1518
}
1619

20+
const config = {
21+
// selector: 'a[href]',
22+
straightOnly: false,
23+
straightOverlapThreshold: 0.5,
24+
rememberSource: false,
25+
disabled: false,
26+
defaultElement: '',
27+
enterTo: '',
28+
leaveFor: null,
29+
restrict: 'self-first',
30+
tabIndexIgnoreList:
31+
'a, input, select, textarea, button, iframe, [contentEditable=true]',
32+
navigableFilter: (e: HTMLElement): boolean => {
33+
if (e?.parentElement?.parentElement?.classList?.contains('card-overlay')) {
34+
return false;
35+
}
36+
37+
return true;
38+
},
39+
scrollOptions: { behavior: 'smooth', block: 'center' }
40+
};
41+
42+
Vue.use(vjsn, config);
1743
Vue.use(VueAwesomeSwiper);
1844
Vue.component('ValidationProvider', ValidationProvider);
1945
Vue.component('ValidationObserver', ValidationObserver);

package-lock.json

+23
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)