Fixed #239
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "spotizerr-ui",
|
"name": "spotizerr-ui",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "3.0.5",
|
"version": "3.0.6",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -40,11 +40,33 @@ const deleteCredential = async ({ service, name }: { service: Service; name: str
|
|||||||
return response;
|
return response;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// --- Error helpers ---
|
||||||
|
function extractApiErrorMessage(error: unknown): string {
|
||||||
|
const fallback = "Failed to add account.";
|
||||||
|
try {
|
||||||
|
// Axios-style error
|
||||||
|
const anyErr: any = error as any;
|
||||||
|
const resp = anyErr?.response;
|
||||||
|
if (resp?.data) {
|
||||||
|
const data = resp.data;
|
||||||
|
if (typeof data === "string") return data;
|
||||||
|
if (typeof data?.detail === "string") return data.detail;
|
||||||
|
if (typeof data?.message === "string") return data.message;
|
||||||
|
if (typeof data?.error === "string") return data.error;
|
||||||
|
}
|
||||||
|
if (typeof anyErr?.message === "string") return anyErr.message;
|
||||||
|
return fallback;
|
||||||
|
} catch {
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- Component ---
|
// --- Component ---
|
||||||
export function AccountsTab() {
|
export function AccountsTab() {
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const [activeService, setActiveService] = useState<Service>("spotify");
|
const [activeService, setActiveService] = useState<Service>("spotify");
|
||||||
const [isAdding, setIsAdding] = useState(false);
|
const [isAdding, setIsAdding] = useState(false);
|
||||||
|
const [submitError, setSubmitError] = useState<string | null>(null);
|
||||||
|
|
||||||
const { data: credentials, isLoading } = useQuery({
|
const { data: credentials, isLoading } = useQuery({
|
||||||
queryKey: ["credentials", activeService],
|
queryKey: ["credentials", activeService],
|
||||||
@@ -64,10 +86,13 @@ export function AccountsTab() {
|
|||||||
toast.success("Account added successfully!");
|
toast.success("Account added successfully!");
|
||||||
queryClient.invalidateQueries({ queryKey: ["credentials", activeService] });
|
queryClient.invalidateQueries({ queryKey: ["credentials", activeService] });
|
||||||
setIsAdding(false);
|
setIsAdding(false);
|
||||||
|
setSubmitError(null);
|
||||||
reset();
|
reset();
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
toast.error(`Failed to add account: ${error.message}`);
|
const msg = extractApiErrorMessage(error);
|
||||||
|
setSubmitError(msg);
|
||||||
|
toast.error(msg);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -78,17 +103,26 @@ export function AccountsTab() {
|
|||||||
queryClient.invalidateQueries({ queryKey: ["credentials", activeService] });
|
queryClient.invalidateQueries({ queryKey: ["credentials", activeService] });
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
toast.error(`Failed to delete account: ${error.message}`);
|
const msg = extractApiErrorMessage(error);
|
||||||
|
toast.error(msg);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const onSubmit: SubmitHandler<AccountFormData> = (data) => {
|
const onSubmit: SubmitHandler<AccountFormData> = (data) => {
|
||||||
|
setSubmitError(null);
|
||||||
addMutation.mutate({ service: activeService, data });
|
addMutation.mutate({ service: activeService, data });
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderAddForm = () => (
|
const renderAddForm = () => (
|
||||||
<form onSubmit={handleSubmit(onSubmit)} className="p-4 border border-line dark:border-border-dark rounded-lg mt-4 space-y-4">
|
<form onSubmit={handleSubmit(onSubmit)} className="p-4 border border-line dark:border-border-dark rounded-lg mt-4 space-y-4">
|
||||||
<h4 className="font-semibold text-content-primary dark:text-content-primary-dark">Add New {activeService === "spotify" ? "Spotify" : "Deezer"} Account</h4>
|
<h4 className="font-semibold text-content-primary dark:text-content-primary-dark">Add New {activeService === "spotify" ? "Spotify" : "Deezer"} Account</h4>
|
||||||
|
|
||||||
|
{submitError && (
|
||||||
|
<div className="text-error-text bg-error-muted border border-error rounded p-2 text-sm">
|
||||||
|
{submitError}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<label htmlFor="accountName" className="text-content-primary dark:text-content-primary-dark">Account Name</label>
|
<label htmlFor="accountName" className="text-content-primary dark:text-content-primary-dark">Account Name</label>
|
||||||
<input
|
<input
|
||||||
|
|||||||
@@ -52,6 +52,8 @@ export type FlatAppSettings = {
|
|||||||
skipExisting: boolean;
|
skipExisting: boolean;
|
||||||
m3u: boolean;
|
m3u: boolean;
|
||||||
hlsThreads: number;
|
hlsThreads: number;
|
||||||
|
// Frontend-only flag used in DownloadsTab
|
||||||
|
recursiveQuality: boolean;
|
||||||
// Add defaults for the new formatting properties
|
// Add defaults for the new formatting properties
|
||||||
track: string;
|
track: string;
|
||||||
album: string;
|
album: string;
|
||||||
@@ -85,6 +87,8 @@ const defaultSettings: FlatAppSettings = {
|
|||||||
skipExisting: true,
|
skipExisting: true,
|
||||||
m3u: false,
|
m3u: false,
|
||||||
hlsThreads: 8,
|
hlsThreads: 8,
|
||||||
|
// Frontend-only default
|
||||||
|
recursiveQuality: false,
|
||||||
// Add defaults for the new formatting properties
|
// Add defaults for the new formatting properties
|
||||||
track: "{artist_name}/{album_name}/{track_number} - {track_name}",
|
track: "{artist_name}/{album_name}/{track_number} - {track_name}",
|
||||||
album: "{artist_name}/{album_name}",
|
album: "{artist_name}/{album_name}",
|
||||||
@@ -117,7 +121,13 @@ const fetchSettings = async (): Promise<FlatAppSettings> => {
|
|||||||
// Transform the keys before returning the data
|
// Transform the keys before returning the data
|
||||||
const camelData = convertKeysToCamelCase(combinedConfig) as FetchedCamelCaseSettings;
|
const camelData = convertKeysToCamelCase(combinedConfig) as FetchedCamelCaseSettings;
|
||||||
|
|
||||||
return camelData as unknown as FlatAppSettings;
|
const withDefaults: FlatAppSettings = {
|
||||||
|
...(camelData as unknown as FlatAppSettings),
|
||||||
|
// Ensure required frontend-only fields exist
|
||||||
|
recursiveQuality: Boolean((camelData as any).recursiveQuality ?? false),
|
||||||
|
};
|
||||||
|
|
||||||
|
return withDefaults;
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
// If we get authentication errors, return default settings
|
// If we get authentication errors, return default settings
|
||||||
if (error.response?.status === 401 || error.response?.status === 403) {
|
if (error.response?.status === 401 || error.response?.status === 403) {
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export interface AppSettings {
|
|||||||
skipExisting: boolean;
|
skipExisting: boolean;
|
||||||
m3u: boolean;
|
m3u: boolean;
|
||||||
hlsThreads: number;
|
hlsThreads: number;
|
||||||
|
recursiveQuality: boolean;
|
||||||
// Properties from the old 'formatting' object
|
// Properties from the old 'formatting' object
|
||||||
track: string;
|
track: string;
|
||||||
album: string;
|
album: string;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { useSearch } from "@tanstack/react-router";
|
|||||||
import { useSettings } from "../contexts/settings-context";
|
import { useSettings } from "../contexts/settings-context";
|
||||||
import { useAuth } from "../contexts/auth-context";
|
import { useAuth } from "../contexts/auth-context";
|
||||||
import { LoginScreen } from "../components/auth/LoginScreen";
|
import { LoginScreen } from "../components/auth/LoginScreen";
|
||||||
|
import pkgJson from "../../package.json";
|
||||||
|
|
||||||
// Lazy load config tab components for better code splitting
|
// Lazy load config tab components for better code splitting
|
||||||
const GeneralTab = lazy(() => import("../components/config/GeneralTab").then(m => ({ default: m.GeneralTab })));
|
const GeneralTab = lazy(() => import("../components/config/GeneralTab").then(m => ({ default: m.GeneralTab })));
|
||||||
@@ -28,6 +29,8 @@ const ConfigComponent = () => {
|
|||||||
// Get settings from the context instead of fetching here
|
// Get settings from the context instead of fetching here
|
||||||
const { settings: config, isLoading } = useSettings();
|
const { settings: config, isLoading } = useSettings();
|
||||||
|
|
||||||
|
const appVersion = (pkgJson as any)?.version as string;
|
||||||
|
|
||||||
// Determine initial tab based on URL parameter, user role, and auth state
|
// Determine initial tab based on URL parameter, user role, and auth state
|
||||||
const getInitialTab = () => {
|
const getInitialTab = () => {
|
||||||
if (tab) {
|
if (tab) {
|
||||||
@@ -174,9 +177,12 @@ const ConfigComponent = () => {
|
|||||||
return (
|
return (
|
||||||
<div className="max-w-7xl mx-auto p-4 md:p-6 lg:p-8 space-y-8">
|
<div className="max-w-7xl mx-auto p-4 md:p-6 lg:p-8 space-y-8">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<h1 className="text-3xl font-bold text-content-primary dark:text-content-primary-dark">
|
<div className="flex items-center justify-between">
|
||||||
{authEnabled && !isAdmin ? "Profile Settings" : "Configuration"}
|
<h1 className="text-3xl font-bold text-content-primary dark:text-content-primary-dark">
|
||||||
</h1>
|
{authEnabled && !isAdmin ? "Profile Settings" : "Configuration"}
|
||||||
|
</h1>
|
||||||
|
<span className="px-2 py-1 text-xs rounded bg-surface-muted dark:bg-surface-muted-dark text-content-secondary dark:text-content-secondary-dark border border-border dark:border-border-dark">v{appVersion}</span>
|
||||||
|
</div>
|
||||||
<p className="text-content-muted dark:text-content-muted-dark">
|
<p className="text-content-muted dark:text-content-muted-dark">
|
||||||
{authEnabled && !isAdmin
|
{authEnabled && !isAdmin
|
||||||
? "Manage your profile and account settings."
|
? "Manage your profile and account settings."
|
||||||
@@ -219,7 +225,7 @@ const ConfigComponent = () => {
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleTabChange("formatting")}
|
onClick={() => handleTabChange("formatting")}
|
||||||
className={`px-4 py-3 rounded-lg text-left transition-all whitespace-nowrap ${activeTab === "formatting" ? "bg-surface-accent dark:bg-surface-accent-dark font-semibold text-content-primary dark:text-content-primary-dark shadow-sm" : "text-content-secondary dark:text-content-secondary-dark hover:bg-surface-muted dark:hover:bg-surface-muted-dark hover:text-content-primary dark:hover:text-content-primary-dark"}`}
|
className={`px-4 py-3 rounded-lg text-left transition-all whitespace-nowrap ${activeTab === "formatting" ? "bg-surface-accent dark:bg-surface-accent-dark font-semibold text-content-primary dark:text-content-primary-dark shadowsm" : "text-content-secondary dark:text-content-secondary-dark hover:bg-surface-muted dark:hover:bg-surface-muted-dark hover:text-content-primary dark:hover:text-content-primary-dark"}`}
|
||||||
>
|
>
|
||||||
Formatting
|
Formatting
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
Reference in New Issue
Block a user