Bookmarking/continue watching + sorting, color options in caption settings

Co-authored-by: mrjvs <mistrjvs@gmail.com>
This commit is contained in:
Jip Fr 2023-10-17 17:04:03 +02:00
parent 18ec79af07
commit 09c52d9f37
8 changed files with 119 additions and 53 deletions

View file

@ -4,5 +4,8 @@
"eslint.format.enable": true,
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[typescriptreact]": {
"editor.defaultFormatter": "ms-vsliveshare.vsliveshare"
}
}

View file

@ -1,3 +1,4 @@
import classNames from "classnames";
import { useCallback, useEffect, useState } from "react";
import { Toggle } from "@/components/buttons/Toggle";
@ -93,6 +94,31 @@ function QualityView({ id }: { id: string }) {
);
}
function ColorOption(props: {
color: string;
active?: boolean;
onClick: () => void;
}) {
return (
<div
className={classNames(
"p-1.5 bg-video-context-buttonFocus rounded transition-colors duration-100",
props.active ? "bg-opacity-100" : "bg-opacity-0 cursor-pointer"
)}
onClick={props.onClick}
>
<div
className="w-6 h-6 rounded-full flex justify-center items-center"
style={{ backgroundColor: props.color }}
>
{props.active ? (
<Icon className="text-sm text-black" icon={Icons.CHECKMARK} />
) : null}
</div>
</div>
);
}
function CaptionSettingsView({ id }: { id: string }) {
const router = useOverlayRouter(id);
@ -102,12 +128,32 @@ function CaptionSettingsView({ id }: { id: string }) {
Custom captions
</Context.BackLink>
<Context.Section>
<Context.SmallText>Hello!</Context.SmallText>
<div className="flex justify-between items-center">
<Context.FieldTitle>Color</Context.FieldTitle>
<div className="flex justify-center items-center">
<ColorOption onClick={() => {}} color="#FFFFFF" active />
<ColorOption onClick={() => {}} color="#80B1FA" />
<ColorOption onClick={() => {}} color="#E2E535" />
</div>
</div>
</Context.Section>
</>
);
}
function CaptionsView({ id }: { id: string }) {
const router = useOverlayRouter(id);
return (
<>
<Context.BackLink onClick={() => router.navigate("/captions")}>
Captions
</Context.BackLink>
<Context.Section>Yee!</Context.Section>
</>
);
}
function SettingsOverlay({ id }: { id: string }) {
const router = useOverlayRouter(id);
const currentQuality = usePlayerStore((s) => s.currentQuality);
@ -165,15 +211,7 @@ function SettingsOverlay({ id }: { id: string }) {
</OverlayPage>
<OverlayPage id={id} path="/captions" width={343} height={431}>
<Context.Card>
<Context.BackLink onClick={() => router.navigate("/")}>
Captions
</Context.BackLink>
<button
type="button"
onClick={() => router.navigate("/captions/settings")}
>
Go to caption settings
</button>
<CaptionsView id={id} />
</Context.Card>
</OverlayPage>
<OverlayPage id={id} path="/captions/settings" width={343} height={431}>

View file

@ -154,18 +154,23 @@ function Anchor(props: { children: React.ReactNode; onClick: () => void }) {
);
}
function FieldTitle(props: { children: React.ReactNode }) {
return <p className="font-medium">{props.children}</p>;
}
export const Context = {
Card,
CardWithScrollable,
Title,
SectionTitle,
BackLink,
Section,
Link,
LinkTitle,
LinkChevron,
IconButton,
Divider,
FieldTitle,
SmallText,
BackLink,
LinkTitle,
Section,
Divider,
Anchor,
Title,
Link,
Card,
};

View file

@ -8,26 +8,38 @@ import { SectionHeading } from "@/components/layout/SectionHeading";
import { MediaGrid } from "@/components/media/MediaGrid";
import { WatchedMediaCard } from "@/components/media/WatchedMediaCard";
import { useBookmarkStore } from "@/stores/bookmarks";
import { useProgressStore } from "@/stores/progress";
import { MediaItem } from "@/utils/mediaTypes";
export function BookmarksPart() {
const { t } = useTranslation();
const progressItems = useProgressStore((s) => s.items);
const bookmarks = useBookmarkStore((s) => s.bookmarks);
const removeBookmark = useBookmarkStore((s) => s.removeBookmark);
const [editing, setEditing] = useState(false);
const [gridRef] = useAutoAnimate<HTMLDivElement>();
// TODO sort on last watched
const items = useMemo(() => {
const output: MediaItem[] = [];
let output: MediaItem[] = [];
Object.entries(bookmarks).forEach((entry) => {
output.push({
id: entry[0],
...entry[1],
});
});
output = output.sort((a, b) => {
const bookmarkA = bookmarks[a.id];
const bookmarkB = bookmarks[b.id];
const progressA = progressItems[a.id];
const progressB = progressItems[b.id];
const dateA = Math.max(bookmarkA.updatedAt, progressA?.updatedAt ?? 0);
const dateB = Math.max(bookmarkB.updatedAt, progressB?.updatedAt ?? 0);
return dateB - dateA;
});
return output;
}, [bookmarks]);
}, [bookmarks, progressItems]);
if (items.length === 0) return null;

View file

@ -21,7 +21,9 @@ export function WatchingPart() {
const sortedProgressItems = useMemo(() => {
let output: MediaItem[] = [];
Object.entries(progressItems).forEach((entry) => {
Object.entries(progressItems)
.sort((a, b) => b[1].updatedAt - a[1].updatedAt)
.forEach((entry) => {
output.push({
id: entry[0],
...entry[1],
@ -32,7 +34,6 @@ export function WatchingPart() {
const isBookMarked = !!bookmarks[v.id];
return !isBookMarked;
});
// TODO sort on last modified date
return output;
}, [progressItems, bookmarks]);

View file

@ -9,6 +9,7 @@ export interface BookmarkMediaItem {
year: number;
poster?: string;
type: "show" | "movie";
updatedAt: number;
}
export interface ProgressStore {
@ -34,6 +35,7 @@ export const useBookmarkStore = create(
title: meta.title,
year: meta.releaseYear,
poster: meta.poster,
updatedAt: Date.now(),
};
});
},

View file

@ -29,6 +29,7 @@ export interface ProgressMediaItem {
poster?: string;
type: "show" | "movie";
progress?: ProgressItem;
updatedAt: number;
seasons: Record<string, ProgressSeasonItem>;
episodes: Record<string, ProgressEpisodeItem>;
}
@ -61,11 +62,14 @@ export const useProgressStore = create(
type: meta.type,
episodes: {},
seasons: {},
updatedAt: 0,
title: meta.title,
year: meta.releaseYear,
poster: meta.poster,
};
const item = s.items[meta.tmdbId];
item.updatedAt = Date.now();
if (meta.type === "movie") {
if (!item.progress)
item.progress = {

View file

@ -26,23 +26,23 @@ module.exports = {
"ash-400": "#3D394D",
"ash-300": "#2C293A",
"ash-200": "#2B2836",
"ash-100": "#1E1C26"
"ash-100": "#1E1C26",
},
/* fonts */
fontFamily: {
"open-sans": "'Open Sans'"
"open-sans": "'Open Sans'",
},
/* animations */
keyframes: {
"loading-pin": {
"0%, 40%, 100%": { height: "0.5em", "background-color": "#282336" },
"20%": { height: "1em", "background-color": "white" }
}
"20%": { height: "1em", "background-color": "white" },
},
},
animation: { "loading-pin": "loading-pin 1.8s ease-in-out infinite" },
},
animation: { "loading-pin": "loading-pin 1.8s ease-in-out infinite" }
}
},
plugins: [
require("tailwind-scrollbar"),
@ -52,31 +52,31 @@ module.exports = {
colors: {
// Branding
pill: {
background: "#1C1C36"
background: "#1C1C36",
},
// meta data for the theme itself
global: {
accentA: "#505DBD",
accentB: "#3440A1"
accentB: "#3440A1",
},
// light bar
lightBar: {
light: "#2A2A71"
light: "#2A2A71",
},
// Buttons
buttons: {
toggle: "#8D44D6",
toggleDisabled: "#202836"
toggleDisabled: "#202836",
},
// only used for body colors/textures
background: {
main: "#0A0A10",
accentA: "#6E3B80",
accentB: "#1F1F50"
accentB: "#1F1F50",
},
// typography
@ -85,7 +85,7 @@ module.exports = {
text: "#73739D",
dimmed: "#926CAD",
divider: "#262632",
secondary: "#64647B"
secondary: "#64647B",
},
// search bar
@ -94,7 +94,7 @@ module.exports = {
focused: "#24243C",
placeholder: "#4A4A71",
icon: "#545476",
text: "#FFFFFF"
text: "#FFFFFF",
},
// media cards
@ -106,7 +106,7 @@ module.exports = {
barColor: "#4B4B63",
barFillColor: "#BA7FD6",
badge: "#151522",
badgeText: "#5F5F7A"
badgeText: "#5F5F7A",
},
// video player
@ -118,34 +118,35 @@ module.exports = {
error: "#E44F4F",
success: "#40B44B",
loading: "#B759D8",
noresult: "#64647B"
noresult: "#64647B",
},
progress: {
background: "#8787A8",
preloaded: "#8787A8",
watched: "#A75FC9"
watched: "#A75FC9",
},
audio: {
set: "#A75FC9"
set: "#A75FC9",
},
context: {
background: "#0C1216",
light: "#4D79A8",
border: "#4F5C66",
buttonFocus: "#202836",
type: {
main: "#617A8A",
secondary: "#374A56",
accent: "#A570FA"
}
}
}
}
}
}
})
]
accent: "#A570FA",
},
},
},
},
},
},
}),
],
};