mirror of
https://github.com/sbondCo/Watcharr.git
synced 2026-08-07 07:14:44 +00:00
2202b11e39
now i should probably test the whole app waaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaauuuuuuuuuugugghghghghghghghghg
358 lines
8.8 KiB
Svelte
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>
|