Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 64 additions & 23 deletions public/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -651,27 +651,60 @@ function showMatch(card, reason) {
showScreen("screen-match");
}

// Shared Overseerr request flow: ask for the PIN once (stored locally), POST, toast.
async function overseerrRequest(path, extraBody) {
// Masked PIN modal. Resolves with the entered PIN, or null if cancelled.
function askPin(errorMsg) {
return new Promise((resolve) => {
const overlay = $("pin-overlay");
const input = $("pin-input");
input.value = "";
$("pin-error").textContent = errorMsg || "";
overlay.classList.remove("hidden");
setTimeout(() => input.focus(), 50);
const done = (val) => {
overlay.classList.add("hidden");
$("pin-ok").onclick = null;
$("pin-cancel").onclick = null;
input.onkeydown = null;
resolve(val);
};
$("pin-ok").onclick = () => done(input.value.trim() || null);
$("pin-cancel").onclick = () => done(null);
input.onkeydown = (e) => {
if (e.key === "Enter") done(input.value.trim() || null);
else if (e.key === "Escape") done(null);
};
});
}

// Shared Overseerr request flow: masked PIN (re-asks on a wrong PIN), success alert with title.
async function overseerrRequest(path, extraBody, label) {
let pin = localStorage.getItem("cinemate_request_pin");
if (!pin) {
pin = (window.prompt("Request PIN") || "").trim();
if (!pin) return;
localStorage.setItem("cinemate_request_pin", pin);
}
try {
await api(path, { method: "POST", body: JSON.stringify({ user_id: state.userId, pin, ...extraBody }) });
toast("✅ Requested in Overseerr");
} catch (e) {
if (e.message === "invalid_pin") {
localStorage.removeItem("cinemate_request_pin");
toast("Wrong PIN — try again");
} else if (e.message === "requests_disabled") {
toast("Requests are disabled");
} else if (e.message === "not_configured") {
toast("Overseerr not set up");
} else {
toast("Overseerr request failed");
let errorMsg = "";
for (;;) {
if (!pin) {
pin = await askPin(errorMsg);
if (!pin) return; // cancelled
localStorage.setItem("cinemate_request_pin", pin);
}
try {
await api(path, { method: "POST", body: JSON.stringify({ user_id: state.userId, pin, ...extraBody }) });
toast(`✅ ${label ? `"${label}" ` : ""}added to Overseerr`);
return;
} catch (e) {
if (e.message === "invalid_pin") {
localStorage.removeItem("cinemate_request_pin");
pin = null;
errorMsg = "Wrong PIN — try again";
continue; // re-open the modal with the error
}
toast(
e.message === "requests_disabled"
? "Requests are disabled"
: e.message === "not_configured"
? "Overseerr not set up"
: "Overseerr request failed",
);
return;
}
}
}
Expand All @@ -680,7 +713,11 @@ async function addToOverseerr() {
if (!currentMatchCard) return;
const btn = $("add-overseerr-btn");
btn.disabled = true;
await overseerrRequest(`/api/rooms/${state.room.id}/request`, { tmdb_id: currentMatchCard.tmdb_id });
await overseerrRequest(
`/api/rooms/${state.room.id}/request`,
{ tmdb_id: currentMatchCard.tmdb_id },
currentMatchCard.title,
);
btn.disabled = false;
}

Expand Down Expand Up @@ -781,7 +818,7 @@ async function showMatchesList() {
try {
const res = await api(`/api/rooms/${state.room.id}/matches`);
renderCardList(list, res.matches || [], "No matches yet.", (m) =>
overseerrRequest(`/api/rooms/${state.room.id}/request`, { tmdb_id: m.tmdb_id }),
overseerrRequest(`/api/rooms/${state.room.id}/request`, { tmdb_id: m.tmdb_id }, (m.card || {}).title),
);
} catch (e) {
list.innerHTML = "<p class='error'>Error: " + e.message + "</p>";
Expand All @@ -795,7 +832,11 @@ async function showWatchlist() {
try {
const res = await api(`/api/users/${state.userId}/watchlist`);
renderCardList(list, res.watchlist || [], "Your watchlist is empty. Like titles in solo mode.", (m) =>
overseerrRequest(`/api/users/${state.userId}/request`, { tmdb_id: m.tmdb_id, media_type: m.media_type }),
overseerrRequest(
`/api/users/${state.userId}/request`,
{ tmdb_id: m.tmdb_id, media_type: m.media_type },
(m.card || {}).title,
),
);
} catch (e) {
list.innerHTML = "<p class='error'>Error: " + e.message + "</p>";
Expand Down
13 changes: 13 additions & 0 deletions public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,19 @@ <h2>Profile</h2>
</footer>
</main>

<!-- PIN modal (Overseerr request) -->
<div id="pin-overlay" class="overlay hidden">
<div class="modal">
<h3>Enter request PIN</h3>
<input id="pin-input" class="input" type="password" inputmode="numeric" autocomplete="off" placeholder="PIN" />
<p id="pin-error" class="error"></p>
<div class="modal-actions">
<button id="pin-cancel" class="btn">Cancel</button>
<button id="pin-ok" class="btn btn-primary">OK</button>
</div>
</div>
</div>

<!-- Trailer overlay -->
<div id="trailer-overlay" class="overlay hidden">
<button id="trailer-close" class="overlay-close" aria-label="Close">✕</button>
Expand Down
17 changes: 17 additions & 0 deletions public/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -534,6 +534,23 @@ select.input {
z-index: 20;
padding: 1rem;
}
.modal {
background: var(--surface);
border-radius: var(--radius);
padding: 1.2rem;
width: 100%;
max-width: 320px;
box-shadow: var(--shadow);
}
.modal h3 {
margin: 0 0 0.8rem;
text-align: center;
}
.modal-actions {
display: flex;
gap: 0.6rem;
margin-top: 0.4rem;
}
.overlay-close {
position: absolute;
top: 1rem;
Expand Down
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export { Room } from "./durable-objects/room";

const app = new Hono<{ Bindings: Env }>();

app.get("/api/health", (c) => c.json({ ok: true, service: "cinemate", version: "v3.2" }));
app.get("/api/health", (c) => c.json({ ok: true, service: "cinemate", version: "v3.3" }));

app.route("/api/users", users);
app.route("/api/profile", profile);
Expand Down
Loading