1
0
Fork 0
mirror of https://github.com/sussy-code/smov.git synced 2024-12-20 14:37:43 +01:00

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, "eslint.format.enable": true,
"[json]": { "[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode" "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 { useCallback, useEffect, useState } from "react";
import { Toggle } from "@/components/buttons/Toggle"; 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 }) { function CaptionSettingsView({ id }: { id: string }) {
const router = useOverlayRouter(id); const router = useOverlayRouter(id);
@ -102,12 +128,32 @@ function CaptionSettingsView({ id }: { id: string }) {
Custom captions Custom captions
</Context.BackLink> </Context.BackLink>
<Context.Section> <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> </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 }) { function SettingsOverlay({ id }: { id: string }) {
const router = useOverlayRouter(id); const router = useOverlayRouter(id);
const currentQuality = usePlayerStore((s) => s.currentQuality); const currentQuality = usePlayerStore((s) => s.currentQuality);
@ -165,15 +211,7 @@ function SettingsOverlay({ id }: { id: string }) {
</OverlayPage> </OverlayPage>
<OverlayPage id={id} path="/captions" width={343} height={431}> <OverlayPage id={id} path="/captions" width={343} height={431}>
<Context.Card> <Context.Card>
<Context.BackLink onClick={() => router.navigate("/")}> <CaptionsView id={id} />
Captions
</Context.BackLink>
<button
type="button"
onClick={() => router.navigate("/captions/settings")}
>
Go to caption settings
</button>
</Context.Card> </Context.Card>
</OverlayPage> </OverlayPage>
<OverlayPage id={id} path="/captions/settings" width={343} height={431}> <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 = { export const Context = {
Card,
CardWithScrollable, CardWithScrollable,
Title,
SectionTitle, SectionTitle,
BackLink,
Section,
Link,
LinkTitle,
LinkChevron, LinkChevron,
IconButton, IconButton,
Divider, FieldTitle,
SmallText, SmallText,
BackLink,
LinkTitle,
Section,
Divider,
Anchor, Anchor,
Title,
Link,
Card,
}; };

View file

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

View file

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

View file

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

View file

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

View file

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