-
+
{props.icon ? (
diff --git a/src/components/media/EpisodeButton.tsx b/src/components/media/EpisodeButton.tsx
deleted file mode 100644
index 76e38c85..00000000
--- a/src/components/media/EpisodeButton.tsx
+++ /dev/null
@@ -1,25 +0,0 @@
-export interface EpisodeProps {
- progress?: number;
- episodeNumber: number;
- onClick?: () => void;
- active?: boolean;
-}
-
-export function Episode(props: EpisodeProps) {
- return (
-
-
-
{props.episodeNumber}
-
- );
-}
diff --git a/src/components/player/atoms/AutoPlayStart.tsx b/src/components/player/atoms/AutoPlayStart.tsx
index a0019391..b5d02f29 100644
--- a/src/components/player/atoms/AutoPlayStart.tsx
+++ b/src/components/player/atoms/AutoPlayStart.tsx
@@ -23,7 +23,7 @@ export function AutoPlayStart() {
return (
) => {
- setTimeout(() => {
- // pause on mouse click
- if (e.pointerType === "mouse") {
- if (e.button !== 0) return;
- if (isPaused) display?.play();
- else display?.pause();
- return;
- }
+ // pause on mouse click
+ if (e.pointerType === "mouse") {
+ if (e.button !== 0) return;
+ if (isPaused) display?.play();
+ else display?.pause();
+ return;
+ }
- // toggle on other types of clicks
- if (hovering !== PlayerHoverState.MOBILE_TAPPED)
- updateInterfaceHovering(PlayerHoverState.MOBILE_TAPPED);
- else updateInterfaceHovering(PlayerHoverState.NOT_HOVERING);
- }, 10); // TODO this is dirty workaround, without this, tapping on something where a button will be will trigger it immediately
+ // toggle on other types of clicks
+ if (hovering !== PlayerHoverState.MOBILE_TAPPED)
+ updateInterfaceHovering(PlayerHoverState.MOBILE_TAPPED);
+ else updateInterfaceHovering(PlayerHoverState.NOT_HOVERING);
},
[display, isPaused, hovering, updateInterfaceHovering]
);
diff --git a/src/components/text/ArrowLink.tsx b/src/components/text/ArrowLink.tsx
index ad51319e..3690c1fc 100644
--- a/src/components/text/ArrowLink.tsx
+++ b/src/components/text/ArrowLink.tsx
@@ -27,7 +27,7 @@ export function ArrowLink(props: ArrowLinkProps) {
const isExternal = !!(props as IArrowLinkPropsExternal).url;
const isInternal = !!(props as IArrowLinkPropsInternal).to;
const content = (
-
+
{direction === "left" ? (
diff --git a/src/components/text/DotList.tsx b/src/components/text/DotList.tsx
index cda5be09..bb328a6d 100644
--- a/src/components/text/DotList.tsx
+++ b/src/components/text/DotList.tsx
@@ -5,7 +5,7 @@ export interface DotListProps {
export function DotList(props: DotListProps) {
return (
-
+
{props.content.map((item, index) => (
{index !== 0 ? (
diff --git a/src/components/text/Link.tsx b/src/components/text/Link.tsx
index 1451114e..a4e6b2e8 100644
--- a/src/components/text/Link.tsx
+++ b/src/components/text/Link.tsx
@@ -22,7 +22,7 @@ export function Link(props: LinkProps) {
const isExternal = !!(props as ILinkPropsExternal).url;
const isInternal = !!(props as ILinkPropsInternal).to;
const content = (
-
+
{props.children}
);
diff --git a/src/hooks/auth/useAuth.ts b/src/hooks/auth/useAuth.ts
index ddc44a2a..b0af9b25 100644
--- a/src/hooks/auth/useAuth.ts
+++ b/src/hooks/auth/useAuth.ts
@@ -1,5 +1,6 @@
import { useCallback } from "react";
+import { SessionResponse } from "@/backend/accounts/auth";
import { bookmarkMediaToInput } from "@/backend/accounts/bookmarks";
import {
bytesToBase64,
@@ -16,7 +17,13 @@ import {
registerAccount,
} from "@/backend/accounts/register";
import { removeSession } from "@/backend/accounts/sessions";
-import { getBookmarks, getProgress, getUser } from "@/backend/accounts/user";
+import { getSettings } from "@/backend/accounts/settings";
+import {
+ UserResponse,
+ getBookmarks,
+ getProgress,
+ getUser,
+} from "@/backend/accounts/user";
import { useAuthData } from "@/hooks/auth/useAuthData";
import { useBackendUrl } from "@/hooks/auth/useBackendUrl";
import { AccountWithToken, useAuthStore } from "@/stores/auth";
@@ -90,7 +97,7 @@ export function useAuth() {
} catch {
// we dont care about failing to delete session
}
- userDataLogout();
+ await userDataLogout();
}, [userDataLogout, backendUrl, currentAccount]);
const register = useCallback(
@@ -148,18 +155,32 @@ export function useAuth() {
[backendUrl]
);
- const restore = useCallback(async () => {
- if (!currentAccount) {
- return;
- }
+ const restore = useCallback(
+ async (account: AccountWithToken) => {
+ let user: { user: UserResponse; session: SessionResponse };
+ try {
+ user = await getUser(backendUrl, account.token);
+ } catch (err) {
+ const anyError: any = err;
+ if (
+ anyError?.response?.status === 401 ||
+ anyError?.response?.status === 403
+ ) {
+ await logout();
+ return;
+ }
+ console.error(err);
+ throw err;
+ }
- // TODO if fail to get user, log them out
- const user = await getUser(backendUrl, currentAccount.token);
- const bookmarks = await getBookmarks(backendUrl, currentAccount);
- const progress = await getProgress(backendUrl, currentAccount);
+ const bookmarks = await getBookmarks(backendUrl, account);
+ const progress = await getProgress(backendUrl, account);
+ const settings = await getSettings(backendUrl, account);
- syncData(user.user, user.session, progress, bookmarks);
- }, [backendUrl, currentAccount, syncData]);
+ syncData(user.user, user.session, progress, bookmarks, settings);
+ },
+ [backendUrl, syncData, logout]
+ );
return {
loggedIn,
diff --git a/src/hooks/auth/useAuthData.ts b/src/hooks/auth/useAuthData.ts
index 9fef5839..993e25c2 100644
--- a/src/hooks/auth/useAuthData.ts
+++ b/src/hooks/auth/useAuthData.ts
@@ -1,6 +1,7 @@
import { useCallback } from "react";
import { LoginResponse, SessionResponse } from "@/backend/accounts/auth";
+import { SettingsResponse } from "@/backend/accounts/settings";
import {
BookmarkResponse,
ProgressResponse,
@@ -10,7 +11,10 @@ import {
} from "@/backend/accounts/user";
import { useAuthStore } from "@/stores/auth";
import { useBookmarkStore } from "@/stores/bookmarks";
+import { useLanguageStore } from "@/stores/language";
import { useProgressStore } from "@/stores/progress";
+import { useSubtitleStore } from "@/stores/subtitles";
+import { useThemeStore } from "@/stores/theme";
export function useAuthData() {
const loggedIn = !!useAuthStore((s) => s.account);
@@ -18,6 +22,9 @@ export function useAuthData() {
const removeAccount = useAuthStore((s) => s.removeAccount);
const clearBookmarks = useBookmarkStore((s) => s.clear);
const clearProgress = useProgressStore((s) => s.clear);
+ const setTheme = useThemeStore((s) => s.setTheme);
+ const setAppLanguage = useLanguageStore((s) => s.setLanguage);
+ const setCaptionLanguage = useSubtitleStore((s) => s.setLanguage);
const replaceBookmarks = useBookmarkStore((s) => s.replaceBookmarks);
const replaceItems = useProgressStore((s) => s.replaceItems);
@@ -47,7 +54,6 @@ export function useAuthData() {
removeAccount();
clearBookmarks();
clearProgress();
- // TODO clear settings
}, [removeAccount, clearBookmarks, clearProgress]);
const syncData = useCallback(
@@ -55,13 +61,31 @@ export function useAuthData() {
_user: UserResponse,
_session: SessionResponse,
progress: ProgressResponse[],
- bookmarks: BookmarkResponse[]
+ bookmarks: BookmarkResponse[],
+ settings: SettingsResponse
) => {
- // TODO sync user settings
replaceBookmarks(bookmarkResponsesToEntries(bookmarks));
replaceItems(progressResponsesToEntries(progress));
+
+ if (settings.applicationLanguage) {
+ setAppLanguage(settings.applicationLanguage);
+ }
+
+ if (settings.defaultSubtitleLanguage) {
+ setCaptionLanguage(settings.defaultSubtitleLanguage);
+ }
+
+ if (settings.applicationTheme) {
+ setTheme(settings.applicationTheme);
+ }
},
- [replaceBookmarks, replaceItems]
+ [
+ replaceBookmarks,
+ replaceItems,
+ setAppLanguage,
+ setCaptionLanguage,
+ setTheme,
+ ]
);
return {
diff --git a/src/hooks/auth/useAuthRestore.ts b/src/hooks/auth/useAuthRestore.ts
index b3d67ed7..f0c8bb02 100644
--- a/src/hooks/auth/useAuthRestore.ts
+++ b/src/hooks/auth/useAuthRestore.ts
@@ -2,20 +2,22 @@ import { useRef } from "react";
import { useAsync, useInterval } from "react-use";
import { useAuth } from "@/hooks/auth/useAuth";
+import { useAuthStore } from "@/stores/auth";
const AUTH_CHECK_INTERVAL = 12 * 60 * 60 * 1000;
export function useAuthRestore() {
+ const { account } = useAuthStore();
const { restore } = useAuth();
const hasRestored = useRef(false);
useInterval(() => {
- restore();
+ if (account) restore(account);
}, AUTH_CHECK_INTERVAL);
const result = useAsync(async () => {
- if (hasRestored.current) return;
- await restore().finally(() => {
+ if (hasRestored.current || !account) return;
+ await restore(account).finally(() => {
hasRestored.current = true;
});
}, []); // no deps because we don't want to it ever rerun after the first time
diff --git a/src/hooks/useSettingsState.ts b/src/hooks/useSettingsState.ts
new file mode 100644
index 00000000..d2058ab8
--- /dev/null
+++ b/src/hooks/useSettingsState.ts
@@ -0,0 +1,79 @@
+import { useCallback, useEffect, useMemo, useState } from "react";
+
+import { SubtitleStyling } from "@/stores/subtitles";
+
+export function useDerived(
+ initial: T
+): [T, (v: T) => void, () => void, boolean] {
+ const [overwrite, setOverwrite] = useState(undefined);
+ useEffect(() => {
+ setOverwrite(undefined);
+ }, [initial]);
+
+ const changed = overwrite !== initial && overwrite !== undefined;
+ const data = overwrite === undefined ? initial : overwrite;
+
+ const reset = useCallback(() => setOverwrite(undefined), [setOverwrite]);
+
+ return [data, setOverwrite, reset, changed];
+}
+
+export function useSettingsState(
+ theme: string | null,
+ appLanguage: string,
+ subtitleStyling: SubtitleStyling,
+ deviceName?: string
+) {
+ const [themeState, setTheme, resetTheme, themeChanged] = useDerived(theme);
+ const [
+ appLanguageState,
+ setAppLanguage,
+ resetAppLanguage,
+ appLanguageChanged,
+ ] = useDerived(appLanguage);
+ const [subStylingState, setSubStyling, resetSubStyling, subStylingChanged] =
+ useDerived(subtitleStyling);
+ const [
+ deviceNameState,
+ setDeviceNameState,
+ resetDeviceName,
+ deviceNameChanged,
+ ] = useDerived(deviceName);
+
+ function reset() {
+ resetTheme();
+ resetAppLanguage();
+ resetSubStyling();
+ resetDeviceName();
+ }
+
+ const changed = useMemo(
+ () =>
+ themeChanged ||
+ appLanguageChanged ||
+ subStylingChanged ||
+ deviceNameChanged,
+ [themeChanged, appLanguageChanged, subStylingChanged, deviceNameChanged]
+ );
+
+ return {
+ reset,
+ changed,
+ theme: {
+ state: themeState,
+ set: setTheme,
+ },
+ appLanguage: {
+ state: appLanguageState,
+ set: setAppLanguage,
+ },
+ subtitleStyling: {
+ state: subStylingState,
+ set: setSubStyling,
+ },
+ deviceName: {
+ state: deviceNameState,
+ set: setDeviceNameState,
+ },
+ };
+}
diff --git a/src/index.tsx b/src/index.tsx
index a5886a41..5eebd773 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -3,7 +3,7 @@ import "./stores/__old/imports";
import "@/setup/ga";
import "@/setup/index.css";
-import React, { Suspense } from "react";
+import React, { Suspense, useCallback } from "react";
import type { ReactNode } from "react";
import ReactDOM from "react-dom";
import { HelmetProvider } from "react-helmet-async";
@@ -11,15 +11,22 @@ import { BrowserRouter, HashRouter } from "react-router-dom";
import { useAsync } from "react-use";
import { registerSW } from "virtual:pwa-register";
+import { Button } from "@/components/Button";
+import { Icon, Icons } from "@/components/Icon";
+import { Loading } from "@/components/layout/Loading";
import { useAuthRestore } from "@/hooks/auth/useAuthRestore";
+import { useBackendUrl } from "@/hooks/auth/useBackendUrl";
import { ErrorBoundary } from "@/pages/errors/ErrorBoundary";
import { MigrationPart } from "@/pages/parts/migrations/MigrationPart";
+import { LargeTextPart } from "@/pages/parts/util/LargeTextPart";
import App from "@/setup/App";
import { conf } from "@/setup/config";
import i18n from "@/setup/i18n";
+import { useAuthStore } from "@/stores/auth";
import { BookmarkSyncer } from "@/stores/bookmarks/BookmarkSyncer";
import { useLanguageStore } from "@/stores/language";
import { ProgressSyncer } from "@/stores/progress/ProgressSyncer";
+import { SettingsSyncer } from "@/stores/subtitles/SettingsSyncer";
import { useThemeStore } from "@/stores/theme";
import { initializeChromecast } from "./setup/chromecast";
@@ -37,15 +44,52 @@ registerSW({
});
function LoadingScreen(props: { type: "user" | "lazy" }) {
- return Loading: {props.type}
;
+ return (
+ }>Loading {props.type}
+ );
+}
+
+function ErrorScreen(props: {
+ children: ReactNode;
+ showResetButton?: boolean;
+}) {
+ const setBackendUrl = useAuthStore((s) => s.setBackendUrl);
+ const resetBackend = useCallback(() => {
+ setBackendUrl(null);
+ // eslint-disable-next-line no-restricted-globals
+ location.reload();
+ }, [setBackendUrl]);
+
+ return (
+
+ }
+ >
+ {props.children}
+ {props.showResetButton ? (
+
+
+
+ ) : null}
+
+ );
}
function AuthWrapper() {
const status = useAuthRestore();
+ const backendUrl = conf().BACKEND_URL;
+ const userBackendUrl = useBackendUrl();
- // TODO what to do when failing to load user data?
if (status.loading) return ;
- if (status.error) return Failed to fetch user data
;
+ if (status.error)
+ return (
+
+ Failed to fetch user data. Try resetting the backend URL.
+
+ );
return ;
}
@@ -56,7 +100,8 @@ function MigrationRunner() {
}, []);
if (status.loading) return ;
- if (status.error) return Failed to migrate
;
+ if (status.error)
+ return Failed to migrate your data.;
return ;
}
@@ -82,6 +127,7 @@ ReactDOM.render(
+
diff --git a/src/pages/Settings.tsx b/src/pages/Settings.tsx
index 15f94e2c..e046a168 100644
--- a/src/pages/Settings.tsx
+++ b/src/pages/Settings.tsx
@@ -3,28 +3,32 @@ import { useEffect } from "react";
import { useAsyncFn } from "react-use";
import { getSessions } from "@/backend/accounts/sessions";
+import { Button } from "@/components/Button";
import { WideContainer } from "@/components/layout/WideContainer";
import { Heading1 } from "@/components/utils/Text";
import { useBackendUrl } from "@/hooks/auth/useBackendUrl";
import { useIsMobile } from "@/hooks/useIsMobile";
-import { AccountActionsPart } from "@/pages/settings/AccountActionsPart";
-import { AccountEditPart } from "@/pages/settings/AccountEditPart";
-import { CaptionsPart } from "@/pages/settings/CaptionsPart";
-import { DeviceListPart } from "@/pages/settings/DeviceListPart";
-import { RegisterCalloutPart } from "@/pages/settings/RegisterCalloutPart";
-import { SidebarPart } from "@/pages/settings/SidebarPart";
-import { ThemePart } from "@/pages/settings/ThemePart";
+import { useSettingsState } from "@/hooks/useSettingsState";
+import { AccountActionsPart } from "@/pages/parts/settings/AccountActionsPart";
+import { AccountEditPart } from "@/pages/parts/settings/AccountEditPart";
+import { CaptionsPart } from "@/pages/parts/settings/CaptionsPart";
+import { DeviceListPart } from "@/pages/parts/settings/DeviceListPart";
+import { RegisterCalloutPart } from "@/pages/parts/settings/RegisterCalloutPart";
+import { SidebarPart } from "@/pages/parts/settings/SidebarPart";
+import { ThemePart } from "@/pages/parts/settings/ThemePart";
import { AccountWithToken, useAuthStore } from "@/stores/auth";
+import { useLanguageStore } from "@/stores/language";
+import { useSubtitleStore } from "@/stores/subtitles";
import { useThemeStore } from "@/stores/theme";
import { SubPageLayout } from "./layouts/SubPageLayout";
-import { LocalePart } from "./settings/LocalePart";
+import { LocalePart } from "./parts/settings/LocalePart";
function SettingsLayout(props: { children: React.ReactNode }) {
const { isMobile } = useIsMobile();
return (
-
+
s.theme);
const setTheme = useThemeStore((s) => s.setTheme);
+
+ const appLanguage = useLanguageStore((s) => s.language);
+
+ const subStyling = useSubtitleStore((s) => s.styling);
+
+ const deviceName = useAuthStore((s) => s.account?.deviceName);
+
const user = useAuthStore();
+ const state = useSettingsState(
+ activeTheme,
+ appLanguage,
+ subStyling,
+ deviceName
+ );
+
return (
@@ -81,15 +99,31 @@ export function SettingsPage() {
)}
-
+
-
+
-
+ s}
+ />
+
+ {state.changed ? (
+
+
You have unsaved changes
+
+
+
+
+
+ ) : null}
);
}
diff --git a/src/pages/parts/auth/LoginFormPart.tsx b/src/pages/parts/auth/LoginFormPart.tsx
index 6e603f0e..90898c05 100644
--- a/src/pages/parts/auth/LoginFormPart.tsx
+++ b/src/pages/parts/auth/LoginFormPart.tsx
@@ -40,7 +40,7 @@ export function LoginFormPart(props: LoginFormPartProps) {
await importData(account, progressItems, bookmarkItems);
- await restore();
+ await restore(account);
props.onLogin?.();
},
diff --git a/src/pages/parts/auth/VerifyPassphrasePart.tsx b/src/pages/parts/auth/VerifyPassphrasePart.tsx
index b7458741..3f5e017a 100644
--- a/src/pages/parts/auth/VerifyPassphrasePart.tsx
+++ b/src/pages/parts/auth/VerifyPassphrasePart.tsx
@@ -73,7 +73,7 @@ export function VerifyPassphrase(props: VerifyPassphraseProps) {
applicationTheme: applicationTheme ?? undefined,
});
- await restore();
+ await restore(account);
props.onNext?.();
},
diff --git a/src/pages/parts/migrations/MigrationPart.tsx b/src/pages/parts/migrations/MigrationPart.tsx
index da8c027e..fcf7ccbf 100644
--- a/src/pages/parts/migrations/MigrationPart.tsx
+++ b/src/pages/parts/migrations/MigrationPart.tsx
@@ -17,10 +17,6 @@ export function MigrationPart() {
Please hold, we are migrating your data. This shouldn't take long.
Also, fuck you.
-
- 25%
);
}
diff --git a/src/pages/parts/search/SearchListPart.tsx b/src/pages/parts/search/SearchListPart.tsx
index a4be8231..5fff2484 100644
--- a/src/pages/parts/search/SearchListPart.tsx
+++ b/src/pages/parts/search/SearchListPart.tsx
@@ -21,7 +21,9 @@ function SearchSuffix(props: { failed?: boolean; results?: number }) {
{/* standard suffix */}
diff --git a/src/pages/settings/AccountActionsPart.tsx b/src/pages/parts/settings/AccountActionsPart.tsx
similarity index 100%
rename from src/pages/settings/AccountActionsPart.tsx
rename to src/pages/parts/settings/AccountActionsPart.tsx
diff --git a/src/pages/settings/AccountEditPart.tsx b/src/pages/parts/settings/AccountEditPart.tsx
similarity index 68%
rename from src/pages/settings/AccountEditPart.tsx
rename to src/pages/parts/settings/AccountEditPart.tsx
index 35f9e645..754821de 100644
--- a/src/pages/settings/AccountEditPart.tsx
+++ b/src/pages/parts/settings/AccountEditPart.tsx
@@ -2,30 +2,37 @@ import { UserAvatar } from "@/components/Avatar";
import { Button } from "@/components/Button";
import { Icon, Icons } from "@/components/Icon";
import { SettingsCard } from "@/components/layout/SettingsCard";
+import { useModal } from "@/components/overlays/Modal";
import { AuthInputBox } from "@/components/text-inputs/AuthInputBox";
import { useAuth } from "@/hooks/auth/useAuth";
+import { ProfileEditModal } from "@/pages/parts/settings/ProfileEditModal";
export function AccountEditPart() {
const { logout } = useAuth();
+ const profileEditModal = useModal("profile-edit");
return (
+
-
diff --git a/src/pages/settings/CaptionsPart.tsx b/src/pages/parts/settings/CaptionsPart.tsx
similarity index 83%
rename from src/pages/settings/CaptionsPart.tsx
rename to src/pages/parts/settings/CaptionsPart.tsx
index 43e3b351..e7afba30 100644
--- a/src/pages/settings/CaptionsPart.tsx
+++ b/src/pages/parts/settings/CaptionsPart.tsx
@@ -62,10 +62,11 @@ export function CaptionPreview(props: {
);
}
-export function CaptionsPart() {
- const styling = useSubtitleStore((s) => s.styling);
+export function CaptionsPart(props: {
+ styling: SubtitleStyling;
+ setStyling: (s: SubtitleStyling) => void;
+}) {
const [fullscreenPreview, setFullscreenPreview] = useState(false);
- const updateStyling = useSubtitleStore((s) => s.updateStyling);
return (
@@ -76,8 +77,10 @@ export function CaptionsPart() {
label="Background opacity"
max={100}
min={0}
- onChange={(v) => updateStyling({ backgroundOpacity: v / 100 })}
- value={styling.backgroundOpacity * 100}
+ onChange={(v) =>
+ props.setStyling({ ...props.styling, backgroundOpacity: v / 100 })
+ }
+ value={props.styling.backgroundOpacity * 100}
textTransformer={(s) => `${s}%`}
/>
`${s}%`}
- onChange={(v) => updateStyling({ size: v / 100 })}
- value={styling.size * 100}
+ onChange={(v) =>
+ props.setStyling({ ...props.styling, size: v / 100 })
+ }
+ value={props.styling.size * 100}
/>
Color
{colors.map((v) => (
updateStyling({ color: v })}
+ onClick={() =>
+ props.setStyling({ ...props.styling, color: v })
+ }
color={v}
- active={styling.color === v}
+ active={props.styling.color === v}
key={v}
/>
))}
@@ -104,13 +111,13 @@ export function CaptionsPart() {
setFullscreenPreview((s) => !s)}
/>
setFullscreenPreview((s) => !s)}
/>
diff --git a/src/pages/settings/DeviceListPart.tsx b/src/pages/parts/settings/DeviceListPart.tsx
similarity index 70%
rename from src/pages/settings/DeviceListPart.tsx
rename to src/pages/parts/settings/DeviceListPart.tsx
index 1b0b48c9..1fc1ca68 100644
--- a/src/pages/settings/DeviceListPart.tsx
+++ b/src/pages/parts/settings/DeviceListPart.tsx
@@ -1,3 +1,4 @@
+import { useMemo } from "react";
import { useAsyncFn } from "react-use";
import { SessionResponse } from "@/backend/accounts/auth";
@@ -50,7 +51,25 @@ export function DeviceListPart(props: {
onChange?: () => void;
}) {
const seed = useAuthStore((s) => s.account?.seed);
+ const sessions = props.sessions;
const currentSessionId = useAuthStore((s) => s.account?.sessionId);
+ const deviceListSorted = useMemo(() => {
+ if (!seed) return [];
+ let list = sessions.map((session) => {
+ const decryptedName = decryptData(session.device, base64ToBuffer(seed));
+ return {
+ current: session.id === currentSessionId,
+ id: session.id,
+ name: decryptedName,
+ };
+ });
+ list = list.sort((a, b) => {
+ if (a.current) return -1;
+ if (b.current) return 1;
+ return a.name.localeCompare(b.name);
+ });
+ return list;
+ }, [seed, sessions, currentSessionId]);
if (!seed) return null;
return (
@@ -64,21 +83,15 @@ export function DeviceListPart(props: {
) : (
- {props.sessions.map((session) => {
- const decryptedName = decryptData(
- session.device,
- base64ToBuffer(seed)
- );
- return (
-
- );
- })}
+ {deviceListSorted.map((session) => (
+
+ ))}
)}
diff --git a/src/pages/settings/LocalePart.tsx b/src/pages/parts/settings/LocalePart.tsx
similarity index 78%
rename from src/pages/settings/LocalePart.tsx
rename to src/pages/parts/settings/LocalePart.tsx
index b1a1951e..93e9f7e9 100644
--- a/src/pages/settings/LocalePart.tsx
+++ b/src/pages/parts/settings/LocalePart.tsx
@@ -2,12 +2,13 @@ import { Dropdown } from "@/components/Dropdown";
import { FlagIcon } from "@/components/FlagIcon";
import { Heading1 } from "@/components/utils/Text";
import { appLanguageOptions } from "@/setup/i18n";
-import { useLanguageStore } from "@/stores/language";
import { sortLangCodes } from "@/utils/sortLangCodes";
-export function LocalePart() {
+export function LocalePart(props: {
+ language: string;
+ setLanguage: (l: string) => void;
+}) {
const sorted = sortLangCodes(appLanguageOptions.map((t) => t.id));
- const { language, setLanguage } = useLanguageStore();
const options = appLanguageOptions
.sort((a, b) => sorted.indexOf(a.id) - sorted.indexOf(b.id))
@@ -17,7 +18,7 @@ export function LocalePart() {
leftIcon:
,
}));
- const selected = options.find((t) => t.id === language);
+ const selected = options.find((t) => t.id === props.language);
return (
@@ -29,7 +30,7 @@ export function LocalePart() {
setLanguage(opt.id)}
+ setSelectedItem={(opt) => props.setLanguage(opt.id)}
/>
);
diff --git a/src/pages/parts/settings/ProfileEditModal.tsx b/src/pages/parts/settings/ProfileEditModal.tsx
new file mode 100644
index 00000000..b292a259
--- /dev/null
+++ b/src/pages/parts/settings/ProfileEditModal.tsx
@@ -0,0 +1,15 @@
+import { Button } from "@/components/Button";
+import { Modal, ModalCard } from "@/components/overlays/Modal";
+import { Heading2 } from "@/components/utils/Text";
+
+export function ProfileEditModal(props: { id: string }) {
+ return (
+
+
+ Edit profile?
+ I am existing
+
+
+
+ );
+}
diff --git a/src/pages/settings/RegisterCalloutPart.tsx b/src/pages/parts/settings/RegisterCalloutPart.tsx
similarity index 94%
rename from src/pages/settings/RegisterCalloutPart.tsx
rename to src/pages/parts/settings/RegisterCalloutPart.tsx
index 790fbdd1..9744876d 100644
--- a/src/pages/settings/RegisterCalloutPart.tsx
+++ b/src/pages/parts/settings/RegisterCalloutPart.tsx
@@ -11,7 +11,7 @@ export function RegisterCalloutPart() {
Sync to the cloud
diff --git a/src/pages/settings/SidebarPart.tsx b/src/pages/parts/settings/SidebarPart.tsx
similarity index 98%
rename from src/pages/settings/SidebarPart.tsx
rename to src/pages/parts/settings/SidebarPart.tsx
index 3cc73049..d955cc78 100644
--- a/src/pages/settings/SidebarPart.tsx
+++ b/src/pages/parts/settings/SidebarPart.tsx
@@ -28,6 +28,7 @@ export function SidebarPart() {
const windowHeight =
window.innerHeight || document.documentElement.clientHeight;
+ // TODO this detection does not work
const viewList = settingLinks
.map((link) => {
const el = document.getElementById(link.id);
diff --git a/src/pages/settings/ThemePart.tsx b/src/pages/parts/settings/ThemePart.tsx
similarity index 100%
rename from src/pages/settings/ThemePart.tsx
rename to src/pages/parts/settings/ThemePart.tsx
diff --git a/src/pages/parts/util/LargeTextPart.tsx b/src/pages/parts/util/LargeTextPart.tsx
new file mode 100644
index 00000000..e537916b
--- /dev/null
+++ b/src/pages/parts/util/LargeTextPart.tsx
@@ -0,0 +1,23 @@
+import { BrandPill } from "@/components/layout/BrandPill";
+import { BlurEllipsis } from "@/pages/layouts/SubPageLayout";
+
+export function LargeTextPart(props: {
+ iconSlot?: React.ReactNode;
+ children: React.ReactNode;
+}) {
+ return (
+
+ {/* Overlayed elements */}
+
+
+
+
+
+ {/* Content */}
+ {props.iconSlot ? props.iconSlot : null}
+
+ {props.children}
+
+
+ );
+}
diff --git a/src/setup/index.css b/src/setup/index.css
index 6317bc34..3cfffd78 100644
--- a/src/setup/index.css
+++ b/src/setup/index.css
@@ -4,7 +4,7 @@
html,
body {
- @apply bg-background-main font-open-sans text-denim-700 overflow-x-hidden;
+ @apply bg-background-main font-open-sans text-type-text overflow-x-hidden;
min-height: 100vh;
min-height: 100dvh;
position: relative;
diff --git a/src/stores/__old/DONT_TOUCH_THIS_FOLDER b/src/stores/__old/DONT_TOUCH_THIS_FOLDER
new file mode 100644
index 00000000..fd7bd968
--- /dev/null
+++ b/src/stores/__old/DONT_TOUCH_THIS_FOLDER
@@ -0,0 +1 @@
+just dont, it's old stuff that needs to stay for legacy localstorage
diff --git a/src/stores/auth/index.ts b/src/stores/auth/index.ts
index 164a8a0f..aa908ac0 100644
--- a/src/stores/auth/index.ts
+++ b/src/stores/auth/index.ts
@@ -26,6 +26,7 @@ interface AuthStore {
setAccount(acc: AccountWithToken): void;
updateDeviceName(deviceName: string): void;
updateAccount(acc: Account): void;
+ setBackendUrl(url: null | string): void;
}
export const useAuthStore = create(
@@ -44,6 +45,11 @@ export const useAuthStore = create(
s.account = null;
});
},
+ setBackendUrl(v) {
+ set((s) => {
+ s.backendUrl = v;
+ });
+ },
updateAccount(acc) {
set((s) => {
if (!s.account) return;
diff --git a/src/stores/bookmarks/BookmarkSyncer.tsx b/src/stores/bookmarks/BookmarkSyncer.tsx
index bd471f45..a37e8207 100644
--- a/src/stores/bookmarks/BookmarkSyncer.tsx
+++ b/src/stores/bookmarks/BookmarkSyncer.tsx
@@ -17,7 +17,7 @@ async function syncBookmarks(
// complete it beforehand so it doesn't get handled while in progress
finish(item.id);
- if (!account) return; // not logged in, dont sync to server
+ if (!account) continue; // not logged in, dont sync to server
try {
if (item.action === "delete") {
diff --git a/src/stores/bookmarks/index.ts b/src/stores/bookmarks/index.ts
index b3825a10..94bef5a1 100644
--- a/src/stores/bookmarks/index.ts
+++ b/src/stores/bookmarks/index.ts
@@ -80,7 +80,9 @@ export const useBookmarkStore = create(
});
},
clear() {
- this.replaceBookmarks({});
+ set((s) => {
+ s.bookmarks = {};
+ });
},
clearUpdateQueue() {
set((s) => {
diff --git a/src/stores/progress/ProgressSyncer.tsx b/src/stores/progress/ProgressSyncer.tsx
index 974b00e3..ee4fae87 100644
--- a/src/stores/progress/ProgressSyncer.tsx
+++ b/src/stores/progress/ProgressSyncer.tsx
@@ -21,7 +21,7 @@ async function syncProgress(
// complete it beforehand so it doesn't get handled while in progress
finish(item.id);
- if (!account) return; // not logged in, dont sync to server
+ if (!account) continue; // not logged in, dont sync to server
try {
if (item.action === "delete") {
diff --git a/src/stores/progress/index.ts b/src/stores/progress/index.ts
index 359d1d9f..941d75b4 100644
--- a/src/stores/progress/index.ts
+++ b/src/stores/progress/index.ts
@@ -159,7 +159,9 @@ export const useProgressStore = create(
});
},
clear() {
- this.replaceItems({});
+ set((s) => {
+ s.items = {};
+ });
},
clearUpdateQueue() {
set((s) => {
diff --git a/src/stores/subtitles/SettingsSyncer.tsx b/src/stores/subtitles/SettingsSyncer.tsx
new file mode 100644
index 00000000..bd21e0dd
--- /dev/null
+++ b/src/stores/subtitles/SettingsSyncer.tsx
@@ -0,0 +1,38 @@
+import { useEffect } from "react";
+
+import { updateSettings } from "@/backend/accounts/settings";
+import { useBackendUrl } from "@/hooks/auth/useBackendUrl";
+import { useAuthStore } from "@/stores/auth";
+import { useSubtitleStore } from "@/stores/subtitles";
+
+const syncIntervalMs = 5 * 1000;
+
+export function SettingsSyncer() {
+ const importSubtitleLanguage = useSubtitleStore(
+ (s) => s.importSubtitleLanguage
+ );
+ const url = useBackendUrl();
+
+ useEffect(() => {
+ const interval = setInterval(() => {
+ (async () => {
+ const state = useSubtitleStore.getState();
+ const user = useAuthStore.getState();
+ if (state.lastSync.lastSelectedLanguage === state.lastSelectedLanguage)
+ return; // only sync if there is a difference
+ if (!user.account) return;
+ if (!state.lastSelectedLanguage) return;
+ await updateSettings(url, user.account, {
+ defaultSubtitleLanguage: state.lastSelectedLanguage,
+ });
+ importSubtitleLanguage(state.lastSelectedLanguage);
+ })();
+ }, syncIntervalMs);
+
+ return () => {
+ clearInterval(interval);
+ };
+ }, [importSubtitleLanguage, url]);
+
+ return null;
+}
diff --git a/src/stores/subtitles/index.ts b/src/stores/subtitles/index.ts
index 6bf93737..1652ee93 100644
--- a/src/stores/subtitles/index.ts
+++ b/src/stores/subtitles/index.ts
@@ -20,6 +20,9 @@ export interface SubtitleStyling {
}
export interface SubtitleStore {
+ lastSync: {
+ lastSelectedLanguage: string | null;
+ };
enabled: boolean;
lastSelectedLanguage: string | null;
styling: SubtitleStyling;
@@ -37,6 +40,9 @@ export const useSubtitleStore = create(
persist(
immer
((set) => ({
enabled: false,
+ lastSync: {
+ lastSelectedLanguage: null,
+ },
lastSelectedLanguage: null,
overrideCasing: false,
delay: 0,
@@ -80,6 +86,7 @@ export const useSubtitleStore = create(
importSubtitleLanguage(lang) {
set((s) => {
s.lastSelectedLanguage = lang;
+ s.lastSync.lastSelectedLanguage = lang;
});
},
})),
diff --git a/tailwind.config.ts b/tailwind.config.ts
index 70e4454a..83da3298 100644
--- a/tailwind.config.ts
+++ b/tailwind.config.ts
@@ -8,31 +8,6 @@ const config: Config = {
safelist: safeThemeList,
theme: {
extend: {
- // TODO remove old colors
- /* colors */
- colors: {
- "bink-100": "#432449",
- "bink-200": "#412B57",
- "bink-300": "#533670",
- "bink-400": "#714C97",
- "bink-500": "#8D66B5",
- "bink-600": "#A87FD1",
- "bink-700": "#CD97D6",
- "denim-100": "#120F1D",
- "denim-200": "#191526",
- "denim-300": "#211D30",
- "denim-400": "#2B263D",
- "denim-500": "#38334A",
- "denim-600": "#504B64",
- "denim-700": "#7A758F",
- "ash-600": "#817998",
- "ash-500": "#9C93B5",
- "ash-400": "#3D394D",
- "ash-300": "#2C293A",
- "ash-200": "#2B2836",
- "ash-100": "#1E1C26"
- },
-
/* fonts */
fontFamily: {
"open-sans": "'Open Sans'"
diff --git a/themes/default.ts b/themes/default.ts
index be3755ff..b682c971 100644
--- a/themes/default.ts
+++ b/themes/default.ts
@@ -10,26 +10,28 @@ export const defaultTheme = {
// Branding
pill: {
background: "#1C1C36",
+ backgroundHover: "#1C1C36",
+ highlight: "#714C97",
},
-
+
// meta data for the theme itself
global: {
accentA: "#505DBD",
accentB: "#3440A1",
},
-
+
// light bar
lightBar: {
light: "#2A2A71",
},
-
+
// Buttons
buttons: {
toggle: "#8D44D6",
toggleDisabled: "#202836",
danger: "#792131",
dangerHover: "#8a293b",
-
+
secondary: "#161F25",
secondaryText: "#8EA3B0",
secondaryHover: "#1B262E",
@@ -41,22 +43,27 @@ export const defaultTheme = {
cancel: "#252533",
cancelHover: "#3C3C4A",
},
-
+
// only used for body colors/textures
background: {
main: "#0A0A10",
+ secondary: "#151529",
+ secondaryHover: "#252542",
accentA: "#6E3B80",
accentB: "#1F1F50",
},
-
+
// typography
type: {
+ logo: "#A87FD1",
emphasis: "#FFFFFF",
text: "#73739D",
dimmed: "#926CAD",
divider: "#262632",
secondary: "#64647B",
danger: "#F46E6E",
+ link: "#A87FD1",
+ linkHover: "#A87FD1",
},
// search bar
@@ -127,6 +134,10 @@ export const defaultTheme = {
background: "#29243D",
altBackground: "#29243D",
},
+
+ saveBar: {
+ background: "#0F0E17"
+ }
},
utils: {