mirror of
https://github.com/trafficlunar/jellyfin-spicetify.git
synced 2026-06-13 19:07:06 +00:00
feat: loading indicators, copy quick connect code
Also adds .editorconfig, formats most files, and remove archive false on workflow until nightly.link adds support for it
This commit is contained in:
parent
eee96c84af
commit
71c62c3f07
15 changed files with 2878 additions and 2817 deletions
11
.editorconfig
Normal file
11
.editorconfig
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
tab_width = 2
|
||||||
|
max_line_length = 160
|
||||||
|
insert_final_newline = true
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
end_of_line = lf
|
||||||
2
.github/workflows/build.yml
vendored
2
.github/workflows/build.yml
vendored
|
|
@ -13,4 +13,4 @@ jobs:
|
||||||
with:
|
with:
|
||||||
name: jellyfin-spicetify
|
name: jellyfin-spicetify
|
||||||
path: dist/jellyfin-spicetify.js
|
path: dist/jellyfin-spicetify.js
|
||||||
archive: false
|
# archive: false
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,6 @@ WIP: A Spicetify extension to integrate your Jellyfin music library into Spotify
|
||||||
|
|
||||||
The following are current limitations with the extension. They are not impossible to implement, but are rather time-consuming or require fragile solutions.
|
The following are current limitations with the extension. They are not impossible to implement, but are rather time-consuming or require fragile solutions.
|
||||||
|
|
||||||
- Non-Spotify tracks
|
### Non-Spotify tracks
|
||||||
|
|
||||||
Tracks that don't exist on Spotify can't be included in playlists, queue, etc. They can only be accessed via search and don't show up on the player interface.
|
Tracks that don't exist on Spotify can't be included in playlists, queue, etc. They can only be accessed via search and don't show up on the player interface.
|
||||||
|
|
|
||||||
3
build.ts
3
build.ts
|
|
@ -59,6 +59,8 @@ const options: BuildOptions = {
|
||||||
|
|
||||||
copyFileSync("./dist/jellyfin-spicetify.js", path);
|
copyFileSync("./dist/jellyfin-spicetify.js", path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log("Built!");
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
@ -67,6 +69,7 @@ const options: BuildOptions = {
|
||||||
|
|
||||||
if (isWatch) {
|
if (isWatch) {
|
||||||
const ctx = await context(options);
|
const ctx = await context(options);
|
||||||
|
console.log("Watching...");
|
||||||
await ctx.watch();
|
await ctx.watch();
|
||||||
} else {
|
} else {
|
||||||
await build(options);
|
await build(options);
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,9 @@
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "bun build.ts & spicetify apply",
|
"build": "bun build.ts && spicetify apply",
|
||||||
"build-local": "bun build.ts --local",
|
"build-local": "bun build.ts --local",
|
||||||
"watch": "bun build.ts --watch & spicetify watch -le"
|
"watch": "bun build.ts --watch && spicetify watch -le"
|
||||||
},
|
},
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,26 @@ import styles from "./styles.module.css";
|
||||||
|
|
||||||
type View = "url" | "password" | "quick-connect" | "settings";
|
type View = "url" | "password" | "quick-connect" | "settings";
|
||||||
|
|
||||||
|
const LoadingIndicatorButton = ({ children, onClick, isLoading }: { children: React.ReactNode; onClick: () => void; isLoading: boolean }) => (
|
||||||
|
<button onClick={onClick} className={styles.button}>
|
||||||
|
{isLoading && (
|
||||||
|
<svg width="17" height="18" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path fill="currentColor" d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z" opacity=".25" />
|
||||||
|
<path
|
||||||
|
fill="currentColor"
|
||||||
|
d="M10.14,1.16a11,11,0,0,0-9,8.92A1.59,1.59,0,0,0,2.46,12,1.52,1.52,0,0,0,4.11,10.7a8,8,0,0,1,6.66-6.61A1.42,1.42,0,0,0,12,2.69h0A1.57,1.57,0,0,0,10.14,1.16Z"
|
||||||
|
>
|
||||||
|
<animateTransform attributeName="transform" type="rotate" dur="0.75s" values="0 12 12;360 12 12" repeatCount="indefinite" />
|
||||||
|
</path>
|
||||||
|
</svg>
|
||||||
|
)}
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
|
||||||
export default function SettingsModal() {
|
export default function SettingsModal() {
|
||||||
|
const [isLoading, setIsLoading] = 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("");
|
||||||
|
|
@ -16,28 +35,33 @@ export default function SettingsModal() {
|
||||||
const [quickConnectCode, setQuickConnectCode] = useState("");
|
const [quickConnectCode, setQuickConnectCode] = useState("");
|
||||||
|
|
||||||
const createApi = async () => {
|
const createApi = async () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
const servers = await jellyfin.sdk.discovery.getRecommendedServerCandidates(url);
|
const servers = await jellyfin.sdk.discovery.getRecommendedServerCandidates(url);
|
||||||
const best = jellyfin.sdk.discovery.findBestServer(servers);
|
const best = jellyfin.sdk.discovery.findBestServer(servers);
|
||||||
if (!best) {
|
if (!best) {
|
||||||
Spicetify.showNotification("Failed to connect to server!", true);
|
Spicetify.showNotification("Failed to connect to server!", true);
|
||||||
|
setIsLoading(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const api = jellyfin.sdk.createApi(best.address);
|
const api = jellyfin.sdk.createApi(best.address);
|
||||||
|
|
||||||
Spicetify.LocalStorage.set("jellyfin-url", url);
|
Spicetify.LocalStorage.set("jellyfin-url", url);
|
||||||
|
|
||||||
jellyfin.setApi(api);
|
jellyfin.setApi(api);
|
||||||
|
|
||||||
setView("password");
|
setView("password");
|
||||||
|
setIsLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const login = async () => {
|
const login = async () => {
|
||||||
if (!jellyfin.api) return;
|
if (!jellyfin.api) return;
|
||||||
const userApi = getUserApi(jellyfin.api);
|
setIsLoading(true);
|
||||||
|
|
||||||
|
const userApi = getUserApi(jellyfin.api);
|
||||||
const auth = await userApi.authenticateUserByName({ authenticateUserByName: { Username: username, Pw: password } });
|
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);
|
||||||
|
setIsLoading(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -48,9 +72,11 @@ export default function SettingsModal() {
|
||||||
if (user.data.Id) jellyfin.setUser(user.data.Id);
|
if (user.data.Id) jellyfin.setUser(user.data.Id);
|
||||||
|
|
||||||
setView("settings");
|
setView("settings");
|
||||||
|
setIsLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const logout = () => {
|
const logout = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
|
e.stopPropagation();
|
||||||
Spicetify.LocalStorage.remove("jellyfin-token");
|
Spicetify.LocalStorage.remove("jellyfin-token");
|
||||||
setView("url");
|
setView("url");
|
||||||
};
|
};
|
||||||
|
|
@ -164,15 +190,16 @@ export default function SettingsModal() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr className={styles.hr} />
|
<hr className={styles.hr} />
|
||||||
<button onClick={createApi} className={styles.button}>
|
<LoadingIndicatorButton onClick={createApi} isLoading={isLoading}>
|
||||||
Next
|
Next
|
||||||
</button>
|
</LoadingIndicatorButton>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.modal}>
|
<div className={styles.modal}>
|
||||||
{view === "quick-connect" ? (
|
{view === "quick-connect" ? (
|
||||||
|
<>
|
||||||
<div className={styles.inputContainer}>
|
<div className={styles.inputContainer}>
|
||||||
<label htmlFor="code">Code</label>
|
<label htmlFor="code">Code</label>
|
||||||
|
|
||||||
|
|
@ -184,6 +211,17 @@ export default function SettingsModal() {
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
navigator.clipboard.writeText(quickConnectCode);
|
||||||
|
Spicetify.showNotification("Copied!");
|
||||||
|
}}
|
||||||
|
className={`${styles.button} ${styles.secondary}`}
|
||||||
|
>
|
||||||
|
Copy
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div className={styles.inputContainer}>
|
<div className={styles.inputContainer}>
|
||||||
|
|
@ -196,17 +234,23 @@ export default function SettingsModal() {
|
||||||
<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}>
|
<LoadingIndicatorButton onClick={login} isLoading={isLoading}>
|
||||||
Log in
|
Log in
|
||||||
</button>
|
</LoadingIndicatorButton>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<hr className={styles.hr} />
|
<hr className={styles.hr} />
|
||||||
<button onClick={() => setView((prev) => (prev === "password" ? "quick-connect" : "password"))} className={`${styles.quickConnect} ${styles.button}`}>
|
<button onClick={() => setView((prev) => (prev === "password" ? "quick-connect" : "password"))} className={`${styles.button} ${styles.secondary}`}>
|
||||||
{view === "password" ? "Quick Connect" : "Username/Password"}
|
{view === "password" ? "Quick Connect" : "Username/Password"}
|
||||||
</button>
|
</button>
|
||||||
<button onClick={() => setView("url")} className={styles.button}>
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setView("url");
|
||||||
|
}}
|
||||||
|
className={styles.button}
|
||||||
|
>
|
||||||
Change URL
|
Change URL
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,14 @@
|
||||||
outline: none;
|
outline: none;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
|
border-radius: 0.35rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.secondary {
|
||||||
|
background-color: var(--spice-main-elevated);
|
||||||
}
|
}
|
||||||
|
|
||||||
.hr {
|
.hr {
|
||||||
|
|
@ -46,6 +54,7 @@
|
||||||
padding: 0.5rem 0.6rem;
|
padding: 0.5rem 0.6rem;
|
||||||
font-size: 0.95rem;
|
font-size: 0.95rem;
|
||||||
transition: 200ms border-color;
|
transition: 200ms border-color;
|
||||||
|
border-radius: 0.35rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.inputContainer input:focus {
|
.inputContainer input:focus {
|
||||||
|
|
@ -65,10 +74,6 @@
|
||||||
color: var(--spice-subtext);
|
color: var(--spice-subtext);
|
||||||
}
|
}
|
||||||
|
|
||||||
.quick_connect {
|
|
||||||
background-color: var(--spice-main-elevated);
|
|
||||||
}
|
|
||||||
|
|
||||||
.quickConnectWrapper {
|
.quickConnectWrapper {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,7 @@
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"forceConsistentCasingInFileNames": true,
|
"forceConsistentCasingInFileNames": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"skipLibCheck": true
|
"skipLibCheck": true,
|
||||||
},
|
},
|
||||||
"include": [
|
"include": ["./src/**/*.*"],
|
||||||
"./src/**/*.*"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue