drop oauth plan (copy config)

This commit is contained in:
2026-08-08 07:10:59 -04:00
parent 13a31aabdc
commit 3ef19f80b8
20 changed files with 530 additions and 1592 deletions

View File

@@ -1,178 +0,0 @@
/**
* Login screen component for PodTUI
* Email/password login with links to code validation and OAuth
*/
import { createSignal } from "solid-js";
import { useAuthStore } from "@/stores/auth";
import { useTheme } from "@/context/ThemeContext";
import { AUTH_CONFIG } from "@/config/auth";
interface LoginScreenProps {
focused?: boolean;
onNavigateToCode?: () => void;
onNavigateToOAuth?: () => void;
}
type FocusField = "email" | "password" | "submit" | "code" | "oauth";
export function LoginScreen(props: LoginScreenProps) {
const auth = useAuthStore();
const { theme } = useTheme();
const [email, setEmail] = createSignal("");
const [password, setPassword] = createSignal("");
const [focusField, setFocusField] = createSignal<FocusField>("email");
const [emailError, setEmailError] = createSignal<string | null>(null);
const [passwordError, setPasswordError] = createSignal<string | null>(null);
const fields: FocusField[] = ["email", "password", "submit", "code", "oauth"];
const validateEmail = (value: string): boolean => {
if (!value) {
setEmailError("Email is required");
return false;
}
if (!AUTH_CONFIG.email.pattern.test(value)) {
setEmailError("Invalid email format");
return false;
}
setEmailError(null);
return true;
};
const validatePassword = (value: string): boolean => {
if (!value) {
setPasswordError("Password is required");
return false;
}
if (value.length < AUTH_CONFIG.password.minLength) {
setPasswordError(`Minimum ${AUTH_CONFIG.password.minLength} characters`);
return false;
}
setPasswordError(null);
return true;
};
const handleSubmit = async () => {
const isEmailValid = validateEmail(email());
const isPasswordValid = validatePassword(password());
if (!isEmailValid || !isPasswordValid) {
return;
}
await auth.login({ email: email(), password: password() });
};
const handleKeyPress = (key: { name: string; shift?: boolean }) => {
if (key.name === "tab") {
const currentIndex = fields.indexOf(focusField());
const nextIndex = key.shift
? (currentIndex - 1 + fields.length) % fields.length
: (currentIndex + 1) % fields.length;
setFocusField(fields[nextIndex]);
} else if (key.name === "return") {
if (focusField() === "submit") {
handleSubmit();
} else if (focusField() === "code" && props.onNavigateToCode) {
props.onNavigateToCode();
} else if (focusField() === "oauth" && props.onNavigateToOAuth) {
props.onNavigateToOAuth();
}
}
};
return (
<box flexDirection="column" border borderColor={theme.border} padding={2} gap={1}>
<text fg={theme.text}>
<strong>Sign In</strong>
</text>
<box height={1} />
{/* Email field */}
<box flexDirection="column" gap={0}>
<text fg={focusField() === "email" ? theme.primary : theme.textMuted}>
Email:
</text>
<input
value={email()}
onInput={setEmail}
placeholder="your@email.com"
focused={props.focused && focusField() === "email"}
width={30}
/>
{emailError() && <text fg={theme.error}>{emailError()}</text>}
</box>
{/* Password field */}
<box flexDirection="column" gap={0}>
<text fg={focusField() === "password" ? theme.primary : theme.textMuted}>
Password:
</text>
<input
value={password()}
onInput={setPassword}
placeholder="********"
focused={props.focused && focusField() === "password"}
width={30}
/>
{passwordError() && <text fg={theme.error}>{passwordError()}</text>}
</box>
<box height={1} />
{/* Submit button */}
<box flexDirection="row" gap={2}>
<box
border
borderColor={theme.border}
padding={1}
backgroundColor={
focusField() === "submit" ? theme.primary : undefined
}
>
<text fg={focusField() === "submit" ? theme.text : undefined}>
{auth.isLoading ? "Signing in..." : "[Enter] Sign In"}
</text>
</box>
</box>
{/* Auth error message */}
{auth.error && <text fg={theme.error}>{auth.error.message}</text>}
<box height={1} />
{/* Alternative auth options */}
<text fg={theme.textMuted}>Or authenticate with:</text>
<box flexDirection="row" gap={2}>
<box
border
borderColor={theme.border}
padding={1}
backgroundColor={focusField() === "code" ? theme.primary : undefined}
>
<text fg={focusField() === "code" ? theme.accent : theme.textMuted}>
[C] Sync Code
</text>
</box>
<box
border
borderColor={theme.border}
padding={1}
backgroundColor={focusField() === "oauth" ? theme.primary : undefined}
>
<text fg={focusField() === "oauth" ? theme.accent : theme.textMuted}>
[O] OAuth Info
</text>
</box>
</box>
<box height={1} />
<text fg={theme.textMuted}>Tab to navigate, Enter to select</text>
</box>
);
}

View File

@@ -1,123 +0,0 @@
/**
* OAuth placeholder component for PodTUI
* Displays OAuth limitations and alternative authentication methods
*/
import { createSignal } from "solid-js";
import { OAUTH_PROVIDERS, OAUTH_LIMITATION_MESSAGE } from "@/config/auth";
import { useTheme } from "@/context/ThemeContext";
interface OAuthPlaceholderProps {
focused?: boolean;
onBack?: () => void;
onNavigateToCode?: () => void;
}
type FocusField = "code" | "back";
export function OAuthPlaceholder(props: OAuthPlaceholderProps) {
const { theme } = useTheme();
const [focusField, setFocusField] = createSignal<FocusField>("code");
const fields: FocusField[] = ["code", "back"];
const handleKeyPress = (key: { name: string; shift?: boolean }) => {
if (key.name === "tab") {
const currentIndex = fields.indexOf(focusField());
const nextIndex = key.shift
? (currentIndex - 1 + fields.length) % fields.length
: (currentIndex + 1) % fields.length;
setFocusField(fields[nextIndex]);
} else if (key.name === "return") {
if (focusField() === "code" && props.onNavigateToCode) {
props.onNavigateToCode();
} else if (focusField() === "back" && props.onBack) {
props.onBack();
}
} else if (key.name === "escape" && props.onBack) {
props.onBack();
}
};
return (
<box flexDirection="column" border padding={2} gap={1} borderColor={theme.border}>
<text fg={theme.text}>
<strong>OAuth Authentication</strong>
</text>
<box height={1} />
{/* OAuth providers list */}
<text fg={theme.primary}>Available OAuth Providers:</text>
<box flexDirection="column" gap={0} paddingLeft={2}>
{OAUTH_PROVIDERS.map((provider) => (
<box flexDirection="row" gap={1}>
<text fg={provider.enabled ? theme.success : theme.textMuted}>
{provider.enabled ? "[+]" : "[-]"} {provider.name}
</text>
<text fg={theme.textMuted}>- {provider.description}</text>
</box>
))}
</box>
<box height={1} />
{/* Limitation message */}
<box border padding={1} borderColor={theme.warning}>
<text fg={theme.warning}>Terminal Limitations</text>
</box>
<box paddingLeft={1}>
{OAUTH_LIMITATION_MESSAGE.split("\n").map((line) => (
<text fg={theme.textMuted}>{line}</text>
))}
</box>
<box height={1} />
{/* Alternative options */}
<text fg={theme.primary}>Recommended Alternatives:</text>
<box flexDirection="column" gap={0} paddingLeft={2}>
<box flexDirection="row" gap={1}>
<text fg={theme.success}>[1]</text>
<text fg={theme.text}>Use a sync code from the web portal</text>
<text fg={theme.success}>[2]</text>
<text fg={theme.text}>Use email/password authentication</text>
<text fg={theme.success}>[3]</text>
<text fg={theme.text}>Use file-based sync (no account needed)</text>
</box>
</box>
<box height={1} />
{/* Action buttons */}
<box flexDirection="row" gap={2}>
<box
border
padding={1}
backgroundColor={focusField() === "code" ? theme.backgroundElement : undefined}
>
<text fg={focusField() === "code" ? theme.primary : undefined}>
[C] Enter Sync Code
</text>
</box>
<box
border
padding={1}
backgroundColor={focusField() === "back" ? theme.backgroundElement : undefined}
>
<text fg={focusField() === "back" ? theme.warning : theme.textMuted}>
[Esc] Back to Login
</text>
</box>
</box>
<box height={1} />
<text fg={theme.textMuted}>Tab to navigate, Enter to select, Esc to go back</text>
</box>
);
}

View File

@@ -63,18 +63,12 @@ const SECTIONS: SettingsSectionDef[] = [
},
{
id: 4,
label: "Account",
description: "Account login & OAuth (not yet implemented).",
},
{
id: 5,
label: "Downloads",
description: "Manage downloaded episodes — delete by show or individually.",
},
];
/** Resolve the items for a section id at render time. Section 4 (Account) has
* no items yet. */
/** Resolve the items for a section id at render time. */
function sectionItems(sectionId: number): SettingItem[] {
switch (sectionId) {
case 0:
@@ -85,7 +79,7 @@ function sectionItems(sectionId: number): SettingItem[] {
return usePreferencesItems();
case 3:
return useVisualizerItems();
case 5:
case 4:
return useDownloadItems();
default:
return [];

View File

@@ -1,157 +0,0 @@
/**
* Sync profile component for PodTUI
* Displays user profile information and sync status
*/
import { createSignal } from "solid-js";
import { useAuthStore } from "@/stores/auth";
import { format } from "date-fns";
import { useTheme } from "@/context/ThemeContext";
interface SyncProfileProps {
focused?: boolean;
onLogout?: () => void;
onManageSync?: () => void;
}
type FocusField = "sync" | "export" | "logout";
export function SyncProfile(props: SyncProfileProps) {
const auth = useAuthStore();
const { theme } = useTheme();
const [focusField, setFocusField] = createSignal<FocusField>("sync");
const [lastSyncTime] = createSignal<Date | null>(new Date());
const fields: FocusField[] = ["sync", "export", "logout"];
const handleKeyPress = (key: { name: string; shift?: boolean }) => {
if (key.name === "tab") {
const currentIndex = fields.indexOf(focusField());
const nextIndex = key.shift
? (currentIndex - 1 + fields.length) % fields.length
: (currentIndex + 1) % fields.length;
setFocusField(fields[nextIndex]);
} else if (key.name === "return") {
if (focusField() === "sync" && props.onManageSync) {
props.onManageSync();
} else if (focusField() === "logout" && props.onLogout) {
handleLogout();
}
}
};
const handleLogout = () => {
auth.logout();
if (props.onLogout) {
props.onLogout();
}
};
const formatDate = (date: Date | null | undefined): string => {
if (!date) return "Never";
return format(date, "MMM d, yyyy HH:mm");
};
const user = () => auth.state().user;
// Get user initials for avatar
const userInitials = () => {
const name = user()?.name || "?";
return name.slice(0, 2).toUpperCase();
};
return (
<box flexDirection="column" border padding={2} gap={1} borderColor={theme.border}>
<text fg={theme.text}>
<strong>User Profile</strong>
</text>
<box height={1} />
{/* User avatar and info */}
<box flexDirection="row" gap={2}>
{/* ASCII avatar */}
<box
border
padding={1}
width={8}
height={4}
justifyContent="center"
alignItems="center"
>
<text fg={theme.primary}>{userInitials()}</text>
</box>
{/* User details */}
<box flexDirection="column" gap={0}>
<text fg={theme.text}>{user()?.name || "Guest User"}</text>
<text fg={theme.textMuted}>{user()?.email || "No email"}</text>
<text fg={theme.textMuted}>Joined: {formatDate(user()?.createdAt)}</text>
</box>
</box>
<box height={1} />
{/* Sync status section */}
<box border padding={1} flexDirection="column" gap={0} borderColor={theme.border}>
<text fg={theme.primary}>Sync Status</text>
<box flexDirection="row" gap={1}>
<text fg={theme.textMuted}>Status:</text>
<text fg={user()?.syncEnabled ? theme.success : theme.warning}>
{user()?.syncEnabled ? "Enabled" : "Disabled"}
</text>
</box>
<box flexDirection="row" gap={1}>
<text fg={theme.textMuted}>Last Sync:</text>
<text fg={theme.text}>{formatDate(lastSyncTime())}</text>
</box>
<box flexDirection="row" gap={1}>
<text fg={theme.textMuted}>Method:</text>
<text fg={theme.text}>File-based (JSON/XML)</text>
</box>
</box>
<box height={1} />
{/* Action buttons */}
<box flexDirection="row" gap={2}>
<box
border
padding={1}
backgroundColor={focusField() === "sync" ? theme.backgroundElement : undefined}
>
<text fg={focusField() === "sync" ? theme.primary : undefined}>
[S] Manage Sync
</text>
</box>
<box
border
padding={1}
backgroundColor={focusField() === "export" ? theme.backgroundElement : undefined}
>
<text fg={focusField() === "export" ? theme.primary : undefined}>
[E] Export Data
</text>
</box>
<box
border
padding={1}
backgroundColor={focusField() === "logout" ? theme.backgroundElement : undefined}
>
<text fg={focusField() === "logout" ? theme.error : theme.textMuted}>
[L] Logout
</text>
</box>
</box>
<box height={1} />
<text fg={theme.textMuted}>Tab to navigate, Enter to select</text>
</box>
);
}