mirror of
https://github.com/aleksilassila/reiverr.git
synced 2026-04-22 00:35:12 +02:00
feat: Add context button with "open in" links in title pages & modals
This commit is contained in:
@@ -4,22 +4,26 @@
|
||||
|
||||
export let heading = '';
|
||||
export let disabled = false;
|
||||
export let position: 'absolute' | 'fixed' = 'fixed';
|
||||
let anchored = position === 'absolute';
|
||||
|
||||
const id = Symbol();
|
||||
export let id = Symbol();
|
||||
|
||||
let menu: HTMLDivElement;
|
||||
|
||||
let position = { x: 0, y: 0 };
|
||||
let fixedPosition = { x: 0, y: 0 };
|
||||
|
||||
function close() {
|
||||
contextMenu.hide();
|
||||
}
|
||||
|
||||
function handleOpen(event: MouseEvent) {
|
||||
if (disabled) return;
|
||||
export function handleOpen(event: MouseEvent) {
|
||||
if (disabled || (anchored && $contextMenu === id)) return; // Clicking button will close menu
|
||||
|
||||
position = { x: event.clientX, y: event.clientY };
|
||||
fixedPosition = { x: event.clientX, y: event.clientY };
|
||||
contextMenu.show(id);
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
function handleClickOutside(event: MouseEvent) {
|
||||
@@ -50,23 +54,25 @@
|
||||
<!-- <svelte:body bind:this={body} /> -->
|
||||
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
<div on:contextmenu|preventDefault={handleOpen}>
|
||||
<div on:contextmenu|preventDefault={handleOpen} on:click={(e) => anchored && e.stopPropagation()}>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
{#if $contextMenu === id}
|
||||
{#key position}
|
||||
{#key fixedPosition}
|
||||
<div
|
||||
class="fixed z-50 px-1 py-1 bg-zinc-800 bg-opacity-50 rounded-lg backdrop-blur-xl flex flex-col"
|
||||
style="left: {position.x}px; top: {position.y}px;"
|
||||
class={`${position} z-50 my-2 px-1 py-1 bg-zinc-800 bg-opacity-50 rounded-lg backdrop-blur-xl flex flex-col w-max`}
|
||||
style={position === 'fixed'
|
||||
? `left: ${fixedPosition.x}px; top: ${fixedPosition.y}px;`
|
||||
: 'left: 0px;'}
|
||||
bind:this={menu}
|
||||
in:fly|global={{ y: 5, duration: 100, delay: 100 }}
|
||||
in:fly|global={{ y: 5, duration: 100, delay: anchored ? 0 : 100 }}
|
||||
out:fly|global={{ y: 5, duration: 100 }}
|
||||
>
|
||||
<slot name="title">
|
||||
{#if heading}
|
||||
<h2
|
||||
class="text-xs text-zinc-200 opacity-60 tracking-wide font-semibold px-3 py-1 line-clamp-1"
|
||||
class="text-xs text-zinc-200 opacity-60 tracking-wide font-semibold px-3 py-1 line-clamp-1 text-left"
|
||||
>
|
||||
{heading}
|
||||
</h2>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
function createContextMenu() {
|
||||
const visibleItem = writable<Symbol | null>(null);
|
||||
const visibleItem = writable<symbol | null>(null);
|
||||
|
||||
return {
|
||||
subscribe: visibleItem.subscribe,
|
||||
show: (item: Symbol) => {
|
||||
show: (item: symbol) => {
|
||||
visibleItem.set(item);
|
||||
},
|
||||
hide: () => {
|
||||
|
||||
25
src/lib/components/ContextMenu/ContextMenuButton.svelte
Normal file
25
src/lib/components/ContextMenu/ContextMenuButton.svelte
Normal file
@@ -0,0 +1,25 @@
|
||||
<script lang="ts">
|
||||
import ContextMenu from './ContextMenu.svelte';
|
||||
import { contextMenu } from '../ContextMenu/ContextMenu';
|
||||
import Button from '../Button.svelte';
|
||||
import { DotsVertical } from 'radix-icons-svelte';
|
||||
|
||||
export let heading = '';
|
||||
|
||||
export let contextMenuId = Symbol();
|
||||
|
||||
function handleToggleVisibility() {
|
||||
if ($contextMenu === contextMenuId) contextMenu.hide();
|
||||
else contextMenu.show(contextMenuId);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="relative">
|
||||
<ContextMenu position="absolute" {heading} id={contextMenuId}>
|
||||
<slot name="menu" slot="menu" />
|
||||
|
||||
<Button slim on:click={handleToggleVisibility}>
|
||||
<DotsVertical size={24} />
|
||||
</Button>
|
||||
</ContextMenu>
|
||||
</div>
|
||||
1
src/lib/components/ContextMenu/ContextMenuDivider.svelte
Normal file
1
src/lib/components/ContextMenu/ContextMenuDivider.svelte
Normal file
@@ -0,0 +1 @@
|
||||
<div class="bg-zinc-200 bg-opacity-20 h-[1.5px] mx-3 my-1 rounded-full" />
|
||||
@@ -7,9 +7,9 @@
|
||||
<button
|
||||
on:click
|
||||
class={classNames(
|
||||
'text-sm font-medium px-3 py-1 hover:bg-zinc-200 hover:bg-opacity-10 rounded-md text-left cursor-default',
|
||||
'text-sm font-medium tracking-wide px-3 py-1 hover:bg-zinc-200 hover:bg-opacity-10 rounded-md text-left cursor-default',
|
||||
{
|
||||
'opacity-80 pointer-events-none': disabled
|
||||
'opacity-75 pointer-events-none': disabled
|
||||
}
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
PUBLIC_JELLYFIN_URL,
|
||||
PUBLIC_RADARR_BASE_URL,
|
||||
PUBLIC_SONARR_BASE_URL
|
||||
} from '$env/static/public';
|
||||
import { setJellyfinItemUnwatched, setJellyfinItemWatched } from '$lib/apis/jellyfin/jellyfinApi';
|
||||
import { library, type LibraryItemStore } from '$lib/stores/library.store';
|
||||
import type { TitleType } from '$lib/types';
|
||||
import ContextMenuDivider from './ContextMenuDivider.svelte';
|
||||
import ContextMenuItem from './ContextMenuItem.svelte';
|
||||
|
||||
export let itemStore: LibraryItemStore;
|
||||
export let type: TitleType;
|
||||
export let tmdbId: number;
|
||||
|
||||
let watched = false;
|
||||
itemStore.subscribe((i) => {
|
||||
if (i.item?.jellyfinItem) {
|
||||
watched =
|
||||
i.item.jellyfinItem.UserData?.Played !== undefined
|
||||
? i.item.jellyfinItem.UserData?.Played
|
||||
: watched;
|
||||
}
|
||||
});
|
||||
|
||||
function handleSetWatched() {
|
||||
if ($itemStore.item?.jellyfinId) {
|
||||
watched = true;
|
||||
setJellyfinItemWatched($itemStore.item?.jellyfinId).finally(() => library.refreshIn(3000));
|
||||
}
|
||||
}
|
||||
|
||||
function handleSetUnwatched() {
|
||||
if ($itemStore.item?.jellyfinId) {
|
||||
watched = false;
|
||||
setJellyfinItemUnwatched($itemStore.item?.jellyfinId).finally(() => library.refreshIn(3000));
|
||||
}
|
||||
}
|
||||
|
||||
function handleOpenInJellyfin() {
|
||||
window.open(
|
||||
PUBLIC_JELLYFIN_URL + '/web/index.html#!/details?id=' + $itemStore.item?.jellyfinItem?.Id
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if $itemStore.item}
|
||||
<ContextMenuItem on:click={handleSetWatched} disabled={!$itemStore.item?.jellyfinId || watched}>
|
||||
Mark as watched
|
||||
</ContextMenuItem>
|
||||
<ContextMenuItem
|
||||
on:click={handleSetUnwatched}
|
||||
disabled={!$itemStore.item?.jellyfinId || !watched}
|
||||
>
|
||||
Mark as unwatched
|
||||
</ContextMenuItem>
|
||||
<ContextMenuDivider />
|
||||
<ContextMenuItem disabled={!$itemStore.item.jellyfinItem} on:click={handleOpenInJellyfin}>
|
||||
Open in Jellyfin
|
||||
</ContextMenuItem>
|
||||
{#if $itemStore.item.type === 'movie'}
|
||||
<ContextMenuItem
|
||||
disabled={!$itemStore.item.radarrMovie}
|
||||
on:click={() =>
|
||||
window.open(PUBLIC_RADARR_BASE_URL + '/movie/' + $itemStore.item?.radarrMovie?.tmdbId)}
|
||||
>
|
||||
Open in Radarr
|
||||
</ContextMenuItem>
|
||||
{:else}
|
||||
<ContextMenuItem
|
||||
disabled={!$itemStore.item.sonarrSeries}
|
||||
on:click={() =>
|
||||
window.open(PUBLIC_SONARR_BASE_URL + '/series/' + $itemStore.item?.sonarrSeries?.titleSlug)}
|
||||
>
|
||||
Open in Sonarr
|
||||
</ContextMenuItem>
|
||||
{/if}
|
||||
{/if}
|
||||
<ContextMenuItem on:click={() => window.open(`https://www.themoviedb.org/${type}/${tmdbId}`)}>
|
||||
Open in TMDB
|
||||
</ContextMenuItem>
|
||||
Reference in New Issue
Block a user