Files
Watcharr/src/lib/Activity.svelte
T
IRHM 2202b11e39 fix all linting issues and upgrade the rest of devDependencies
now i should probably test the whole app waaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaauuuuuuuuuugugghghghghghghghghg
2026-07-30 02:42:35 +00:00

358 lines
8.8 KiB
Svelte

<script lang="ts">
import type { Activity } from "@/types";
import {
getOrdinalSuffix,
months,
seasonAndEpToReadable,
} from "./util/helpers";
import ActivityEditor from "./ActivityEditor.svelte";
import Icon from "./Icon.svelte";
import tooltip from "./actions/tooltip";
interface Props {
activity: Activity[] | undefined;
onRemoved: (activity: Activity) => void;
}
let { activity = undefined, onRemoved }: Props = $props();
let clickedActivity: Activity | undefined = $state();
let groupedActivities: { [index: string]: Activity[] } = $derived(
getGroupedActivity(activity),
);
function getMsg(a: Activity) {
switch (a?.type) {
case "ADDED_WATCHED":
if (a.data) {
const data = JSON.parse(a.data);
return `Added to Watched List${data?.status ? ` as ${toFullTitleCase(data.status)}` : ""}${
data?.rating ? ` with ${data.rating} Stars` : ""
}`;
}
return "Added to Watched List";
case "REMOVED_WATCHED":
return "Removed from Watched List";
case "RATING_CHANGED":
if (a.data) {
return `Rating Changed to ${a.data}`;
}
return "Rating Changed";
case "STATUS_CHANGED":
if (a.data) {
return `Status Changed to ${toFullTitleCase(a.data)}`;
}
return "Status Changed";
case "STATUS_CHANGED_AUTO":
if (a.data) {
const data = JSON.parse(a.data);
return `Status Changed to ${toFullTitleCase(data.status)}`;
}
return "Status Changed";
case "THOUGHTS_CHANGED":
return "Thoughts Changed";
case "THOUGHTS_REMOVED":
return "Thoughts Removed";
case "IMPORTED_WATCHED":
return "Imported";
case "IMPORTED_WATCHED_JF":
case "IMPORTED_WATCHED_PLEX":
return "Synced";
case "IMPORTED_RATING":
if (a.data) {
const data = JSON.parse(a.data);
if (data.rating) {
return `Rating Changed to ${data.rating}`;
} else {
return "Added to Watchlist with No Rating";
}
}
return "Imported Rating";
case "IMPORTED_ADDED_WATCHED":
case "IMPORTED_ADDED_WATCHED_JF":
case "IMPORTED_ADDED_WATCHED_PLEX":
return "Imported Watch Date";
case "SEASON_ADDED":
case "SEASON_ADDED_AUTO":
if (a.data) {
const data = JSON.parse(a.data);
return `Season ${data.season} Added as ${toFullTitleCase(data.status)}`;
}
return "Season Added";
case "SEASON_ADDED_JF":
case "SEASON_ADDED_PLEX":
if (a.data) {
const data = JSON.parse(a.data);
return `Season ${data.season} Synced as ${toFullTitleCase(data.status)}`;
}
return "Season Synced";
case "SEASON_RATING_CHANGED":
if (a.data) {
const data = JSON.parse(a.data);
return `Changed Season ${data.season} Rating to ${data.rating}`;
}
return "Season Rating Changed";
case "SEASON_STATUS_CHANGED":
case "SEASON_STATUS_CHANGED_AUTO":
if (a.data) {
const data = JSON.parse(a.data);
return `Changed Season ${data.season} Status to ${toFullTitleCase(data.status)}`;
}
return "Season Status Changed";
case "SEASON_REMOVED":
if (a.data) {
const data = JSON.parse(a.data);
return `Season ${data.season} Status Removed`;
}
return "Season Removed";
case "EPISODE_ADDED":
if (a.data) {
const data = JSON.parse(a.data);
return `${seasonAndEpToReadable(data.season, data.episode)} Added ${data.status ? `as ${toFullTitleCase(data.status)}` : data.rating ? `with Rating ${data.rating}` : ""}`;
}
return "Episode Added";
case "EPISODE_ADDED_JF":
case "EPISODE_ADDED_PLEX":
if (a.data) {
const data = JSON.parse(a.data);
return `${seasonAndEpToReadable(data.season, data.episode)} Synced ${data.status ? `as ${toFullTitleCase(data.status)}` : data.rating ? `with Rating ${data.rating}` : ""}`;
}
return "Episode Synced";
case "EPISODE_RATING_CHANGED":
if (a.data) {
const data = JSON.parse(a.data);
return `${seasonAndEpToReadable(data.season, data.episode)} Rating Changed to ${data.rating}`;
}
return "Episode Rating Changed";
case "EPISODE_STATUS_CHANGED":
if (a.data) {
const data = JSON.parse(a.data);
return `${seasonAndEpToReadable(data.season, data.episode)} Status Changed to ${toFullTitleCase(data.status)}`;
}
return "Episode Status Changed";
case "EPISODE_REMOVED":
if (a.data) {
const data = JSON.parse(a.data);
return `${seasonAndEpToReadable(data.season, data.episode)} Removed`;
}
return "Episode Removed";
default:
return a.type;
}
}
function toFullTitleCase(text: string | undefined) {
if (text) {
return text
.split(" ")
.map((l) => l[0].toUpperCase() + l.substring(1).toLowerCase())
.join(" ");
}
return "Unknown";
}
function toDayTime(d: Date) {
return `${d.getDate()}${getOrdinalSuffix(d.getDate())} at ${d.toLocaleTimeString()}`;
}
/**
* Get what will be the visible date the activity was 'created'.
* @returns customDate if defined or createdAt if not.
*/
function getCreatedAtVis(a: Activity) {
return Date.parse(a.customDate ?? a.createdAt);
}
function getGroupedActivity(activities?: Activity[]) {
activities = activities?.filter((a) => a.type);
const a = activities?.sort(
(a, b) => getCreatedAtVis(b) - getCreatedAtVis(a),
);
let grouped: { [index: string]: Activity[] } = {};
if (a) {
for (let i = 0; i < a.length; i++) {
const activity = a[i];
const date = new Date(getCreatedAtVis(activity));
const key = `${months[date.getMonth()]} ${date.getFullYear()}`;
if (grouped[key]) {
grouped[key].push(activity);
} else {
grouped[key] = [activity];
}
}
}
return grouped;
}
function openEditor(a: Activity) {
clickedActivity = a;
return;
}
function getActivityDataParsed(a: Activity) {
try {
if (a.data) {
return JSON.parse(a.data);
}
} catch (err) {
console.error("getActivityDataParsed: Failed!", err);
}
}
</script>
{#if clickedActivity}
<ActivityEditor
activity={clickedActivity}
activityMessage={getMsg(clickedActivity)}
onClose={() => (clickedActivity = undefined)}
onUpdated={(activityId, updatedActivity) => {
if (!activity) {
console.error(
"ActivityEditor->onUpdated: 'activity' doesn't exist somehow..",
);
return;
}
const ai = activity.findIndex((a) => a.id === activityId);
activity[ai] = updatedActivity;
}}
onRemoved={(a) => {
if (!activity) {
console.error(
"ActivityEditor->onRemoved: 'activity' doesn't exist somehow..",
);
return;
}
onRemoved(a);
}}
/>
{/if}
<div class="activity">
<h2>Activity</h2>
{#if groupedActivities && Object.keys(groupedActivities).length > 0}
<ul>
{#each Object.keys(groupedActivities) as k (k)}
<h3>{k}</h3>
{#each groupedActivities[k] as a (a.id)}
{@const d = new Date(getCreatedAtVis(a))}
<li>
<button class="plain" onclick={() => openEditor(a)}>
<span title={d.toDateString()}>{toDayTime(d)}</span>
<span>{getMsg(a)}</span>
</button>
{#if a.type?.endsWith("_AUTO")}
{@const data = getActivityDataParsed(a)}
<i
use:tooltip={{
text:
data && data.reason
? `Automated because ${data.reason}`
: "Completed by an automation.",
pos: "top",
}}
style="width: 20px; height: 20px;"
>
<Icon i="sparkles" wh={20} />
</i>
{/if}
{#if a.countAsPlay}
<i
use:tooltip={{
text: "Counts as a Play.",
pos: "top",
}}
style="width: 20px; height: 20px;"
>
<Icon i="play" wh={20} />
</i>
{/if}
</li>
{/each}
{/each}
</ul>
{:else}
<span>You Have No Activity!</span>
{/if}
</div>
<style lang="scss">
.activity {
width: 100%;
ul {
display: flex;
flex-flow: column;
gap: 8px;
margin-top: 8px;
margin-left: calc(30px + 8px);
list-style: none;
max-height: 250px;
overflow-y: auto;
overflow-x: hidden;
h3 {
position: sticky;
top: 0;
font-size: 16px;
font-family:
sans-serif,
system-ui,
-apple-system,
BlinkMacSystemFont;
padding-bottom: 1px;
background-color: $bg-color;
}
li {
display: flex;
flex-flow: row;
gap: 8px;
align-items: center;
button {
all: unset;
display: flex;
flex-flow: row;
align-items: center;
flex-shrink: 1;
gap: 8px;
width: max-content;
min-width: 0;
max-width: 100%;
cursor: pointer;
}
span {
width: max-content;
margin-left: 0px;
&:first-child {
min-width: max-content;
}
&:last-of-type {
background-color: $accent-color;
color: $text-color;
border-radius: 8px;
padding: 10px 12px;
}
}
}
}
span {
margin-left: calc(30px);
width: 100%;
display: flex;
justify-content: center;
}
}
h2 {
font-size: 30px;
font-weight: bold;
margin-left: 30px;
}
</style>