mirror of
https://github.com/trafficlunar/jellyfin-spicetify.git
synced 2026-06-13 19:07:06 +00:00
feat: actual quick connect
i'm so stupid lMAO
This commit is contained in:
parent
6cc35f6100
commit
9cc104c6c9
2 changed files with 113 additions and 83 deletions
170
src/settings.tsx
170
src/settings.tsx
|
|
@ -1,19 +1,22 @@
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
|
|
||||||
|
import { getQuickConnectApi } from "@jellyfin/sdk/lib/utils/api/quick-connect-api";
|
||||||
import { getUserApi } from "@jellyfin/sdk/lib/utils/api/user-api";
|
import { getUserApi } from "@jellyfin/sdk/lib/utils/api/user-api";
|
||||||
import { jellyfin, setJellyfinApi, setJellyfinUser } from "./app";
|
|
||||||
|
import { jellyfin, jellyfinApi, setJellyfinApi, setJellyfinUser } from "./app";
|
||||||
import styles from "./styles.module.css";
|
import styles from "./styles.module.css";
|
||||||
|
|
||||||
|
type View = "url" | "password" | "quick-connect";
|
||||||
|
|
||||||
export default function SettingsModal() {
|
export default function SettingsModal() {
|
||||||
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
||||||
const [url, setUrl] = useState(Spicetify.LocalStorage.get("jellyfin-url") || "");
|
const [url, setUrl] = useState(Spicetify.LocalStorage.get("jellyfin-url") || "");
|
||||||
const [username, setUsername] = useState("");
|
const [username, setUsername] = useState("");
|
||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
const [isUsingQuickConnect, setIsUsingQuickConnect] = useState(false);
|
const [view, setView] = useState<View>("url");
|
||||||
const [quickConnectCode, setQuickConnectCode] = useState("");
|
const [quickConnectCode, setQuickConnectCode] = useState("");
|
||||||
|
|
||||||
const [isFocused, setIsFocused] = useState(false);
|
const createApi = async () => {
|
||||||
|
|
||||||
const login = async () => {
|
|
||||||
const servers = await jellyfin.discovery.getRecommendedServerCandidates(url);
|
const servers = await jellyfin.discovery.getRecommendedServerCandidates(url);
|
||||||
const best = jellyfin.discovery.findBestServer(servers);
|
const best = jellyfin.discovery.findBestServer(servers);
|
||||||
if (!best) {
|
if (!best) {
|
||||||
|
|
@ -21,45 +24,91 @@ export default function SettingsModal() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const api = jellyfin.createApi(best.address);
|
const api = jellyfin.createApi(best.address);
|
||||||
const userApi = getUserApi(api);
|
|
||||||
|
|
||||||
Spicetify.LocalStorage.set("jellyfin-url", url);
|
Spicetify.LocalStorage.set("jellyfin-url", url);
|
||||||
const savedToken = Spicetify.LocalStorage.get("jellyfin-token");
|
|
||||||
|
|
||||||
if (savedToken) {
|
setJellyfinApi(api);
|
||||||
api.accessToken = savedToken;
|
setView("password");
|
||||||
} else {
|
};
|
||||||
if (isUsingQuickConnect && quickConnectCode.length === 6) {
|
|
||||||
Spicetify.showNotification("Please enter the full quick connect code!", true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const auth = isUsingQuickConnect
|
const login = async () => {
|
||||||
? await userApi.authenticateWithQuickConnect({ quickConnectDto: { Secret: quickConnectCode } })
|
if (!jellyfinApi) return;
|
||||||
: await userApi.authenticateUserByName({ authenticateUserByName: { Username: username, Pw: password } });
|
const userApi = getUserApi(jellyfinApi);
|
||||||
|
|
||||||
|
const auth = await userApi.authenticateUserByName({ authenticateUserByName: { Username: username, Pw: password } });
|
||||||
|
|
||||||
if (!auth.data.AccessToken) {
|
if (!auth.data.AccessToken) {
|
||||||
Spicetify.showNotification("Failed to login!", true);
|
Spicetify.showNotification("Failed to login!", true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
api.accessToken = auth.data.AccessToken;
|
jellyfinApi!.accessToken = auth.data.AccessToken;
|
||||||
Spicetify.LocalStorage.set("jellyfin-token", auth.data.AccessToken);
|
Spicetify.LocalStorage.set("jellyfin-token", auth.data.AccessToken);
|
||||||
}
|
|
||||||
|
|
||||||
const user = await getUserApi(api).getCurrentUser();
|
const user = await getUserApi(jellyfinApi!).getCurrentUser();
|
||||||
if (user.data.Id) {
|
if (user.data.Id) {
|
||||||
setJellyfinUser(user.data.Id!);
|
setJellyfinUser(user.data.Id!);
|
||||||
Spicetify.LocalStorage.set("jellyfin-user", user.data.Id!);
|
Spicetify.LocalStorage.set("jellyfin-user", user.data.Id!);
|
||||||
}
|
}
|
||||||
|
|
||||||
setJellyfinApi(api);
|
|
||||||
setIsLoggedIn(true);
|
setIsLoggedIn(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (Spicetify.LocalStorage.get("jellyfin-token")) login();
|
if (view !== "quick-connect") return;
|
||||||
}, []);
|
if (!jellyfinApi) return;
|
||||||
|
|
||||||
|
const quickConnectApi = getQuickConnectApi(jellyfinApi);
|
||||||
|
let interval: NodeJS.Timeout;
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
const enabled = await quickConnectApi.getQuickConnectEnabled();
|
||||||
|
if (!enabled.data) {
|
||||||
|
Spicetify.showNotification("Quick Connect is not enabled on this server!", true);
|
||||||
|
setView("password");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const init = await quickConnectApi.initiateQuickConnect();
|
||||||
|
const secret = init.data.Secret!;
|
||||||
|
setQuickConnectCode(init.data.Code!);
|
||||||
|
|
||||||
|
interval = setInterval(async () => {
|
||||||
|
try {
|
||||||
|
const state = await quickConnectApi.getQuickConnectState({ secret });
|
||||||
|
if (!state.data.Authenticated) return;
|
||||||
|
|
||||||
|
clearInterval(interval);
|
||||||
|
|
||||||
|
const auth = await getUserApi(jellyfinApi!).authenticateWithQuickConnect({
|
||||||
|
quickConnectDto: { Secret: secret },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!auth.data.AccessToken) {
|
||||||
|
Spicetify.showNotification("Failed to login with Quick Connect!", true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
jellyfinApi!.accessToken = auth.data.AccessToken;
|
||||||
|
Spicetify.LocalStorage.set("jellyfin-token", auth.data.AccessToken);
|
||||||
|
|
||||||
|
const user = await getUserApi(jellyfinApi!).getCurrentUser();
|
||||||
|
if (user.data.Id) {
|
||||||
|
setJellyfinUser(user.data.Id!);
|
||||||
|
Spicetify.LocalStorage.set("jellyfin-user", user.data.Id!);
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoggedIn(true);
|
||||||
|
} catch {
|
||||||
|
clearInterval(interval);
|
||||||
|
Spicetify.showNotification("Quick Connect polling failed!", true);
|
||||||
|
setView("password");
|
||||||
|
}
|
||||||
|
}, 2000);
|
||||||
|
})();
|
||||||
|
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [view]);
|
||||||
|
|
||||||
if (isLoggedIn)
|
if (isLoggedIn)
|
||||||
return (
|
return (
|
||||||
|
|
@ -101,13 +150,20 @@ export default function SettingsModal() {
|
||||||
<option value="">Source</option>
|
<option value="">Source</option>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<hr style={{ width: "100%", margin: "1rem 0" }} className={styles.hr} />
|
<hr className={styles.hr} />
|
||||||
<button onClick={() => setIsLoggedIn(false)} className={styles.button}>
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
setIsLoggedIn(false);
|
||||||
|
setView("url");
|
||||||
|
}}
|
||||||
|
className={styles.button}
|
||||||
|
>
|
||||||
Log out
|
Log out
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (view === "url")
|
||||||
return (
|
return (
|
||||||
<div className={styles.modal}>
|
<div className={styles.modal}>
|
||||||
<div className={styles.inputContainer}>
|
<div className={styles.inputContainer}>
|
||||||
|
|
@ -115,36 +171,22 @@ export default function SettingsModal() {
|
||||||
<input id="url" type="text" placeholder="Enter Jellyfin URL..." value={url} onChange={(e) => setUrl(e.target.value)} />
|
<input id="url" type="text" placeholder="Enter Jellyfin URL..." value={url} onChange={(e) => setUrl(e.target.value)} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isUsingQuickConnect ? (
|
<hr className={styles.hr} />
|
||||||
|
<button onClick={createApi} className={styles.button}>
|
||||||
|
Next
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.modal}>
|
||||||
|
{view === "quick-connect" ? (
|
||||||
<div className={styles.inputContainer}>
|
<div className={styles.inputContainer}>
|
||||||
<label htmlFor="code">Code</label>
|
<label htmlFor="code">Code</label>
|
||||||
|
|
||||||
<div className={styles.quickConnectWrapper}>
|
<div className={styles.quickConnectWrapper}>
|
||||||
<input
|
|
||||||
id="quick-connect"
|
|
||||||
type="text"
|
|
||||||
inputMode="numeric"
|
|
||||||
maxLength={6}
|
|
||||||
value={quickConnectCode!}
|
|
||||||
onChange={(e) => setQuickConnectCode(e.target.value.replace(/\D/g, ""))}
|
|
||||||
onFocus={() => setIsFocused(true)}
|
|
||||||
onBlur={() => setIsFocused(false)}
|
|
||||||
// Force caret to always be at the end
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (["ArrowLeft", "ArrowRight", "Home", "End"].includes(e.key)) {
|
|
||||||
e.preventDefault();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
// Same here
|
|
||||||
onSelect={(e) => {
|
|
||||||
const element = e.target as HTMLInputElement;
|
|
||||||
element.setSelectionRange(element.value.length, element.value.length);
|
|
||||||
}}
|
|
||||||
className={styles.quickConnectInput}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{Array.from({ length: 6 }).map((_, i) => (
|
{Array.from({ length: 6 }).map((_, i) => (
|
||||||
<div key={i} className={`${styles.quickConnectBox} ${isFocused && quickConnectCode.length === i ? styles.quickConnectBoxActive : ""}`}>
|
<div key={i} className={styles.quickConnectBox}>
|
||||||
{quickConnectCode[i]}
|
{quickConnectCode[i]}
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
@ -161,31 +203,19 @@ export default function SettingsModal() {
|
||||||
<label htmlFor="password">Password</label>
|
<label htmlFor="password">Password</label>
|
||||||
<input id="password" type="password" placeholder="Enter password..." value={password} onChange={(e) => setPassword(e.target.value)} />
|
<input id="password" type="password" placeholder="Enter password..." value={password} onChange={(e) => setPassword(e.target.value)} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<button onClick={login} className={styles.button}>
|
||||||
|
Log in
|
||||||
|
</button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className={styles.separator}>
|
|
||||||
<hr className={styles.hr} />
|
<hr className={styles.hr} />
|
||||||
<span>or</span>
|
<button onClick={() => setView((prev) => (prev === "password" ? "quick-connect" : "password"))} className={`${styles.quickConnect} ${styles.button}`}>
|
||||||
<hr className={styles.hr} />
|
{view === "password" ? "Quick Connect" : "Username/Password"}
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={() => {
|
|
||||||
setIsUsingQuickConnect((prev) => {
|
|
||||||
if (!prev) {
|
|
||||||
document.getElementById("quick-connect")?.focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
return !prev;
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
className={`${styles.quickConnect} ${styles.button}`}
|
|
||||||
>
|
|
||||||
{isUsingQuickConnect ? "Username/Password" : "Quick Connect"}
|
|
||||||
</button>
|
</button>
|
||||||
<button onClick={login} className={styles.button}>
|
<button onClick={() => setView("url")} className={styles.button}>
|
||||||
Submit
|
Change URL
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -9,9 +9,10 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.hr {
|
.hr {
|
||||||
flex-grow: 1;
|
|
||||||
border: none;
|
border: none;
|
||||||
border-top: 1px solid var(--spice-button-disabled);
|
border-top: 1px solid var(--spice-button-disabled);
|
||||||
|
width: 100%;
|
||||||
|
margin: 0.5rem 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal {
|
.modal {
|
||||||
|
|
@ -47,8 +48,7 @@
|
||||||
transition: 200ms border-color;
|
transition: 200ms border-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
.inputContainer input:focus,
|
.inputContainer input:focus {
|
||||||
.quickConnectBoxActive {
|
|
||||||
border-color: var(--spice-button);
|
border-color: var(--spice-button);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue