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:
parent
18ec79af07
commit
09c52d9f37
8 changed files with 119 additions and 53 deletions
3
.vscode/settings.json
vendored
3
.vscode/settings.json
vendored
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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}>
|
||||||
|
|
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
|
@ -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;
|
||||||
|
|
||||||
|
|
|
@ -21,7 +21,9 @@ export function WatchingPart() {
|
||||||
|
|
||||||
const sortedProgressItems = useMemo(() => {
|
const sortedProgressItems = useMemo(() => {
|
||||||
let output: MediaItem[] = [];
|
let output: MediaItem[] = [];
|
||||||
Object.entries(progressItems).forEach((entry) => {
|
Object.entries(progressItems)
|
||||||
|
.sort((a, b) => b[1].updatedAt - a[1].updatedAt)
|
||||||
|
.forEach((entry) => {
|
||||||
output.push({
|
output.push({
|
||||||
id: entry[0],
|
id: entry[0],
|
||||||
...entry[1],
|
...entry[1],
|
||||||
|
@ -32,7 +34,6 @@ export function WatchingPart() {
|
||||||
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]);
|
||||||
|
|
||||||
|
|
|
@ -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(),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
|
@ -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 = {
|
||||||
|
|
|
@ -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",
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
})
|
}),
|
||||||
]
|
],
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in a new issue