Skip to content

Commit 50d24c3

Browse files
vladjercatrakt-bot[bot]
authored andcommitted
feat(pwa): flag queued offline actions with a pending pill
Renders a yellow 'Queued' pill next to text/dropdown tracking controls and a corner dot on icon-only action buttons whenever isQueued is set, so an action saved offline reads as pending rather than merely disabled. A queued action also un-sticks the control (Sefer's change pins isUpdating until sync) so the user can still toggle it back. Adds the queued-tag color tokens and i18n.
1 parent 05dbc6b commit 50d24c3

14 files changed

Lines changed: 172 additions & 24 deletions

File tree

projects/client/i18n/meta/en.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9070,6 +9070,14 @@
90709070
"default": "Add the offline plays anyway",
90719071
"description": "Accessible label for the button confirming that duplicate offline watches should be replayed."
90729072
},
9073+
"tag_text_queued": {
9074+
"default": "Queued",
9075+
"description": "Pill label on a tracking action (watched, watchlist, rating, favorite) that was saved offline and is waiting to sync."
9076+
},
9077+
"label_queued_action": {
9078+
"default": "Saved offline, waiting to sync",
9079+
"description": "Accessible label / tooltip for the queued indicator shown on an action that was saved offline."
9080+
},
90739081
"error_text_sync_load_failed": {
90749082
"default": "We couldn't load this right now. Please try again.",
90759083
"description": "Inline error message shown when a streaming sync detail or its items fail to load."
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<script lang="ts">
2+
import * as m from "$lib/features/i18n/messages.ts";
3+
import type { Snippet } from "svelte";
4+
5+
type QueuedIndicatorProps = {
6+
isQueued: boolean;
7+
children: Snippet;
8+
};
9+
10+
const { isQueued, children }: QueuedIndicatorProps = $props();
11+
</script>
12+
13+
<div class="trakt-queued-indicator" class:is-queued={isQueued}>
14+
{@render children()}
15+
{#if isQueued}
16+
<span class="queued-dot" role="status" aria-label={m.label_queued_action()}
17+
></span>
18+
{/if}
19+
</div>
20+
21+
<style lang="scss">
22+
.trakt-queued-indicator {
23+
position: relative;
24+
display: inline-flex;
25+
26+
// A queued action disables its button. Chromium ignores `cursor` on a
27+
// disabled <button> and falls back to the ancestor's, so set it here to
28+
// cover the whole control (not just the icon, which inherits it).
29+
&.is-queued {
30+
cursor: not-allowed;
31+
}
32+
33+
.queued-dot {
34+
position: absolute;
35+
inset-block-start: 0;
36+
inset-inline-end: 0;
37+
38+
width: var(--ni-10);
39+
height: var(--ni-10);
40+
box-sizing: border-box;
41+
42+
border-radius: 50%;
43+
background-color: var(--color-background-queued-tag);
44+
border: var(--border-thickness-xs) solid var(--color-background);
45+
46+
pointer-events: none;
47+
}
48+
}
49+
</style>
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<script lang="ts">
2+
import * as m from "$lib/features/i18n/messages.ts";
3+
import StemTag from "../tags/StemTag.svelte";
4+
import Tooltip from "../tooltip/Tooltip.svelte";
5+
</script>
6+
7+
<Tooltip content={m.label_queued_action()}>
8+
<div class="trakt-queued-tag" role="status" aria-label={m.label_queued_action()}>
9+
<StemTag
10+
--color-background-stem-tag="var(--color-background-queued-tag)"
11+
--color-foreground-stem-tag="var(--color-text-queued-tag)"
12+
text={m.tag_text_queued()}
13+
/>
14+
</div>
15+
</Tooltip>
16+
17+
<style>
18+
.trakt-queued-tag {
19+
user-select: none;
20+
}
21+
</style>

projects/client/src/lib/components/buttons/favorite/FavoriteButton.svelte

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
<script lang="ts">
2+
import QueuedIndicator from "$lib/components/badge/QueuedIndicator.svelte";
3+
import QueuedTag from "$lib/components/badge/QueuedTag.svelte";
4+
import * as m from "$lib/features/i18n/messages.ts";
25
import Button from "$lib/components/buttons/Button.svelte";
36
import DropdownItem from "$lib/components/dropdown/DropdownItem.svelte";
47
import FavoriteIcon from "$lib/components/icons/FavoriteIcon.svelte";
@@ -11,6 +14,7 @@
1114
title,
1215
isFavoriteUpdating,
1316
isFavorited,
17+
isQueued = false,
1418
style,
1519
onAdd,
1620
onRemove,
@@ -22,11 +26,19 @@
2226
const handler = $derived(isFavorited ? onRemove : onAdd);
2327
const state = $derived(isFavorited ? "filled" : "open");
2428
29+
const label = $derived(
30+
isQueued
31+
? `${i18n.label({ isFavorited, title })} (${m.label_queued_action()})`
32+
: i18n.label({ isFavorited, title }),
33+
);
34+
2535
const commonProps: Omit<ButtonProps, "children"> = $derived({
26-
label: i18n.label({ isFavorited, title }),
36+
label,
2737
variant: "primary",
2838
onclick: handler,
29-
disabled: isFavoriteUpdating,
39+
// A queued action stays disabled so it can't be re-triggered while it
40+
// waits to sync - the pending pill signals it's already been actioned.
41+
disabled: isFavoriteUpdating || isQueued,
3042
navigationType,
3143
size,
3244
});
@@ -35,21 +47,25 @@
3547
{#if style === "normal"}
3648
<Button {...commonProps} {...props} style="ghost" color="orange">
3749
{i18n.text({ isFavorited, title })}
50+
{#if isQueued}<QueuedTag />{/if}
3851
{#snippet icon()}
3952
<FavoriteIcon {state} />
4053
{/snippet}
4154
</Button>
4255
{/if}
4356

4457
{#if style === "action"}
45-
<ActionButton {...commonProps} {...props} style="ghost" color="default">
46-
<FavoriteIcon {state} --icon-color="var(--color-background-orange)" />
47-
</ActionButton>
58+
<QueuedIndicator {isQueued}>
59+
<ActionButton {...commonProps} {...props} style="ghost" color="default">
60+
<FavoriteIcon {state} --icon-color="var(--color-background-orange)" />
61+
</ActionButton>
62+
</QueuedIndicator>
4863
{/if}
4964

5065
{#if style === "dropdown-item"}
5166
<DropdownItem {...commonProps} style="flat" color="default">
5267
{i18n.text({ isFavorited, title })}
68+
{#if isQueued}<QueuedTag />{/if}
5369
{#snippet icon()}
5470
<FavoriteIcon {state} />
5571
{/snippet}

projects/client/src/lib/components/buttons/favorite/FavoriteButtonProps.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export type FavoriteButtonProps = {
66
title: string;
77
isFavoriteUpdating: boolean;
88
isFavorited: boolean;
9+
isQueued?: boolean;
910
style: 'action' | 'normal' | 'dropdown-item';
1011
size?: 'small' | 'normal';
1112
onAdd: () => void;

projects/client/src/lib/components/buttons/mark-as-watched/MarkAsWatchedButton.svelte

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
<script lang="ts">
2+
import QueuedIndicator from "$lib/components/badge/QueuedIndicator.svelte";
3+
import QueuedTag from "$lib/components/badge/QueuedTag.svelte";
4+
import * as m from "$lib/features/i18n/messages.ts";
25
import DropdownItem from "$lib/components/dropdown/DropdownItem.svelte";
36
import LoadingIndicator from "$lib/components/icons/LoadingIndicator.svelte";
47
import TrackIcon from "$lib/components/icons/TrackIcon.svelte";
@@ -18,6 +21,7 @@
1821
onAsk,
1922
isMarkingAsWatched,
2023
isWatched,
24+
isQueued = false,
2125
isLoading = false,
2226
style,
2327
mode = "hybrid",
@@ -52,12 +56,20 @@
5256
);
5357
const state = $derived(isRemovable ? "watched" : "unwatched");
5458
59+
const label = $derived(
60+
isQueued
61+
? `${i18n.label({ title, isWatched, isRewatching })} (${m.label_queued_action()})`
62+
: i18n.label({ title, isWatched, isRewatching }),
63+
);
64+
5565
const commonProps: Omit<ButtonProps, "children"> = $derived({
56-
label: i18n.label({ title, isWatched, isRewatching }),
66+
label,
5767
color: $color,
5868
variant: mode === "ask" ? "primary" : variant,
5969
onclick: handler,
60-
disabled: isMarkingAsWatched || isLoading,
70+
// A queued action stays disabled so it can't be re-triggered while it
71+
// waits to sync - the pending pill signals it's already been actioned.
72+
disabled: isMarkingAsWatched || isQueued || isLoading,
6173
...events,
6274
});
6375
@@ -92,6 +104,7 @@
92104
navigationType={DpadNavigationType.Item}
93105
>
94106
{buttonText}
107+
{#if isQueued}<QueuedTag />{/if}
95108
{#snippet icon()}
96109
{@render watchIcon()}
97110
{/snippet}
@@ -100,14 +113,17 @@
100113
{/if}
101114

102115
{#if style === "action"}
103-
<ActionButton style="ghost" {...commonProps} {...props}>
104-
{@render watchIcon("small")}
105-
</ActionButton>
116+
<QueuedIndicator {isQueued}>
117+
<ActionButton style="ghost" {...commonProps} {...props}>
118+
{@render watchIcon("small")}
119+
</ActionButton>
120+
</QueuedIndicator>
106121
{/if}
107122

108123
{#if style === "dropdown-item"}
109124
<DropdownItem {...commonProps} style="flat">
110125
{buttonText}
126+
{#if isQueued}<QueuedTag />{/if}
111127
{#snippet icon()}
112128
{@render watchIcon()}
113129
{/snippet}

projects/client/src/lib/components/buttons/mark-as-watched/MarkAsWatchedButtonProps.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export type MarkAsWatchedButtonProps = {
55
title: string;
66
isMarkingAsWatched: boolean;
77
isWatched: boolean;
8+
isQueued?: boolean;
89
isLoading?: boolean;
910
style: 'action' | 'normal' | 'dropdown-item';
1011
size: 'normal' | 'small';

projects/client/src/lib/components/buttons/watchlist/WatchlistButton.svelte

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
<script lang="ts">
2+
import QueuedIndicator from "$lib/components/badge/QueuedIndicator.svelte";
3+
import QueuedTag from "$lib/components/badge/QueuedTag.svelte";
4+
import * as m from "$lib/features/i18n/messages.ts";
25
import Button from "$lib/components/buttons/Button.svelte";
36
import DropdownItem from "$lib/components/dropdown/DropdownItem.svelte";
47
import BookmarkIcon from "$lib/components/icons/BookmarkIcon.svelte";
@@ -14,6 +17,7 @@
1417
title,
1518
isWatchlistUpdating,
1619
isWatchlisted,
20+
isQueued = false,
1721
type,
1822
onAdd,
1923
onRemove,
@@ -31,10 +35,18 @@
3135
);
3236
const state = $derived(isWatchlisted ? "added" : "missing");
3337
38+
const label = $derived(
39+
isQueued
40+
? `${i18n.label({ isWatchlisted, title })} (${m.label_queued_action()})`
41+
: i18n.label({ isWatchlisted, title }),
42+
);
43+
3444
const actionProps = $derived({
35-
label: i18n.label({ isWatchlisted, title }),
45+
label,
3646
onclick: handler,
37-
disabled: isWatchlistUpdating,
47+
// A queued action stays disabled so it can't be re-triggered while it
48+
// waits to sync - the pending pill signals it's already been actioned.
49+
disabled: isWatchlistUpdating || isQueued,
3850
...events,
3951
});
4052
@@ -53,6 +65,7 @@
5365
navigationType={DpadNavigationType.Item}
5466
>
5567
{i18n.text({ isWatchlisted, title })}
68+
{#if isQueued}<QueuedTag />{/if}
5669
{#snippet icon()}
5770
<BookmarkIcon {state} size="normal" />
5871
{/snippet}
@@ -61,17 +74,20 @@
6174
{/if}
6275

6376
{#if type === "action"}
64-
<ActionButton style="ghost" {...actionProps} {...props}>
65-
<BookmarkIcon {state} />
66-
</ActionButton>
77+
<QueuedIndicator {isQueued}>
78+
<ActionButton style="ghost" {...actionProps} {...props}>
79+
<BookmarkIcon {state} />
80+
</ActionButton>
81+
</QueuedIndicator>
6782
{/if}
6883

6984
{#if type === "dropdown-item"}
7085
<DropdownItem {...commonProps} style="flat">
7186
{i18n.text({ isWatchlisted, title })}
87+
{#if isQueued}<QueuedTag />{/if}
7288
{#snippet icon()}
7389
<!-- FIXME: generalize this and do this for all applicable buttons/items -->
74-
{#if isWatchlistUpdating}
90+
{#if isWatchlistUpdating && !isQueued}
7591
<LoadingIndicator />
7692
{:else}
7793
<BookmarkIcon {state} size="normal" />

projects/client/src/lib/components/buttons/watchlist/WatchlistButtonProps.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export type WatchlistButtonProps = {
55
title: string;
66
isWatchlistUpdating: boolean;
77
isWatchlisted: boolean;
8+
isQueued?: boolean;
89
type: 'action' | 'normal' | 'dropdown-item';
910
size: 'small' | 'normal';
1011
onAdd: () => void;

projects/client/src/lib/sections/media-actions/favorite/FavoriteAction.svelte

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
const {
3636
isUpdatingFavorite,
3737
isFavorited,
38+
isQueued,
3839
addToFavorites: doAddToFavorites,
3940
removeFromFavorites,
4041
} = $derived(useFavorites({ type, id, title }));
@@ -91,6 +92,7 @@
9192
{size}
9293
isFavorited={$isFavorited}
9394
isFavoriteUpdating={$isUpdatingFavorite}
95+
isQueued={$isQueued}
9496
onAdd={() => handler("add")}
9597
onRemove={() => handler("remove")}
9698
/>

0 commit comments

Comments
 (0)