Skip to content

Commit a1dc38f

Browse files
authored
Add files via upload
1 parent 12c9137 commit a1dc38f

2 files changed

Lines changed: 148 additions & 24 deletions

File tree

plugins/hot_or_not/hot_or_not.css

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -83,9 +83,10 @@
8383
.hon-vs-container {
8484
display: grid;
8585
grid-template-columns: 1fr auto 1fr;
86-
gap: 20px;
86+
gap: 12px;
8787
align-items: stretch;
8888
width: 100%;
89+
max-width: 960px;
8990
}
9091
.hon-scene-card {
9192
background: #1a1a1a;
@@ -201,7 +202,7 @@
201202
.hon-scene-image-container {
202203
position: relative;
203204
width: 100%;
204-
max-height: 70vh;
205+
max-height: 52vh;
205206
overflow: hidden;
206207
cursor: pointer;
207208
display: flex;
@@ -211,7 +212,7 @@
211212
.hon-performer-image-container {
212213
position: relative;
213214
width: 100%;
214-
max-height: 60vh;
215+
max-height: 46vh;
215216
overflow: hidden;
216217
display: flex;
217218
align-items: center;
@@ -220,7 +221,7 @@
220221
}
221222
.hon-scene-image {
222223
max-width: 100%;
223-
max-height: 70vh;
224+
max-height: 52vh;
224225
width: auto;
225226
height: auto;
226227
object-fit: contain;
@@ -230,7 +231,7 @@
230231
.hon-performer-image {
231232
display: block;
232233
max-width: 100%;
233-
max-height: 60vh;
234+
max-height: 46vh;
234235
width: auto;
235236
height: auto;
236237
object-fit: contain;

plugins/hot_or_not/hot_or_not.js

Lines changed: 142 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -773,6 +773,29 @@
773773
});
774774

775775
// api-client.js
776+
var api_client_exports = {};
777+
__export(api_client_exports, {
778+
IMAGE_FRAGMENT: () => IMAGE_FRAGMENT,
779+
PERFORMER_FRAGMENT: () => PERFORMER_FRAGMENT,
780+
SCENE_FRAGMENT: () => SCENE_FRAGMENT,
781+
fetchAllPerformerStats: () => fetchAllPerformerStats,
782+
fetchImageCount: () => fetchImageCount,
783+
fetchPerformerById: () => fetchPerformerById,
784+
fetchPerformerCount: () => fetchPerformerCount,
785+
fetchRandomImages: () => fetchRandomImages,
786+
fetchRandomPerformers: () => fetchRandomPerformers,
787+
fetchRandomScenes: () => fetchRandomScenes,
788+
fetchSceneCount: () => fetchSceneCount,
789+
getHotOrNotConfig: () => getHotOrNotConfig,
790+
getPerformerBattleRank: () => getPerformerBattleRank,
791+
graphqlQuery: () => graphqlQuery,
792+
handleComparison: () => handleComparison,
793+
isBattleRankBadgeEnabled: () => isBattleRankBadgeEnabled,
794+
updateImageRating: () => updateImageRating,
795+
updateItemRating: () => updateItemRating,
796+
updatePerformerRating: () => updatePerformerRating,
797+
updateSceneRating: () => updateSceneRating
798+
});
776799
async function graphqlQuery(query, variables = {}) {
777800
if (typeof PluginApi !== "undefined" && PluginApi.utils?.StashService?.getClient && PluginApi.libraries?.Apollo) {
778801
try {
@@ -877,7 +900,7 @@
877900
const isFallingWinner = state.gauntletFalling && state.gauntletFallingItem && winnerId === state.gauntletFallingItem.id;
878901
const isChampionLoser = state.gauntletChampion && loserId === state.gauntletChampion.id;
879902
const isFallingLoser = state.gauntletFalling && state.gauntletFallingItem && loserId === state.gauntletFallingItem.id;
880-
const expectedWinner = 1 / (1 + Math.pow(10, ratingDiff / 40));
903+
const expectedWinner = 1 / (1 + Math.pow(10, ratingDiff / 400));
881904
const kFactor = getKFactor(winnerRating, winnerMatchCount, "gauntlet");
882905
if (isChampionWinner || isFallingWinner) {
883906
winnerGain = Math.max(0, Math.round(kFactor * (1 - expectedWinner)));
@@ -889,13 +912,13 @@
889912
loserLoss = 1;
890913
}
891914
} else if (state.currentMode === "champion") {
892-
const expectedWinner = 1 / (1 + Math.pow(10, ratingDiff / 40));
915+
const expectedWinner = 1 / (1 + Math.pow(10, ratingDiff / 400));
893916
const winnerK = getKFactor(winnerRating, winnerMatchCount, "champion");
894917
const loserK = getKFactor(loserRating, loserMatchCount, "champion");
895918
winnerGain = Math.max(0, Math.round(winnerK * (1 - expectedWinner)));
896919
loserLoss = Math.max(0, Math.round(loserK * expectedWinner));
897920
} else {
898-
const expectedWinner = 1 / (1 + Math.pow(10, ratingDiff / 40));
921+
const expectedWinner = 1 / (1 + Math.pow(10, ratingDiff / 400));
899922
const winnerK = getKFactor(winnerRating, winnerMatchCount, "swiss");
900923
const loserK = getKFactor(loserRating, loserMatchCount, "swiss");
901924
winnerGain = Math.max(0, Math.round(winnerK * (1 - expectedWinner)));
@@ -1004,6 +1027,17 @@
10041027
i: { id, rating100: Math.max(1, Math.min(100, Math.round(rating))) }
10051028
});
10061029
}
1030+
async function getHotOrNotConfig() {
1031+
if (pluginConfigCache)
1032+
return pluginConfigCache;
1033+
const result = await graphqlQuery(`query { configuration { plugins } }`);
1034+
pluginConfigCache = (result.configuration.plugins || {})["HotOrNot"] || {};
1035+
return pluginConfigCache;
1036+
}
1037+
async function isBattleRankBadgeEnabled() {
1038+
const config = await getHotOrNotConfig();
1039+
return config.showBattleRankBadge !== false;
1040+
}
10071041
async function getPerformerBattleRank(performerId) {
10081042
try {
10091043
const result = await graphqlQuery(`
@@ -1041,7 +1075,7 @@
10411075
return null;
10421076
}
10431077
}
1044-
var SCENE_FRAGMENT, PERFORMER_FRAGMENT, IMAGE_FRAGMENT;
1078+
var SCENE_FRAGMENT, PERFORMER_FRAGMENT, IMAGE_FRAGMENT, pluginConfigCache;
10451079
var init_api_client = __esm({
10461080
"api-client.js"() {
10471081
init_parsers();
@@ -1050,10 +1084,18 @@
10501084
SCENE_FRAGMENT = `id title date rating100 paths { screenshot preview } files { duration path } studio { name } performers { name } tags { name }`;
10511085
PERFORMER_FRAGMENT = `id name image_path rating100 details custom_fields birthdate ethnicity country gender`;
10521086
IMAGE_FRAGMENT = `id rating100 paths { thumbnail image }`;
1087+
pluginConfigCache = null;
10531088
}
10541089
});
10551090

10561091
// gauntlet-selection.js
1092+
var gauntlet_selection_exports = {};
1093+
__export(gauntlet_selection_exports, {
1094+
fetchPerformersForSelection: () => fetchPerformersForSelection,
1095+
loadPerformerSelection: () => loadPerformerSelection,
1096+
showPerformerSelection: () => showPerformerSelection,
1097+
showPlacementScreen: () => showPlacementScreen
1098+
});
10571099
async function fetchPerformersForSelection(count = 5) {
10581100
const filter = getPerformerFilter(state.cachedUrlFilter, state.selectedGenders);
10591101
const total = await fetchPerformerCount(filter);
@@ -1402,7 +1444,14 @@
14021444
btn.onclick = () => {
14031445
state.gauntletChampion = null;
14041446
state.gauntletWins = 0;
1405-
loadNewPair();
1447+
state.gauntletDefeated = [];
1448+
state.gauntletFalling = false;
1449+
state.gauntletFallingItem = null;
1450+
if (state.currentMode === "gauntlet" && state.battleType === "performers") {
1451+
Promise.resolve().then(() => (init_gauntlet_selection(), gauntlet_selection_exports)).then((m) => m.showPerformerSelection());
1452+
} else {
1453+
loadNewPair();
1454+
}
14061455
};
14071456
}
14081457
}
@@ -1663,7 +1712,31 @@
16631712
return groups.join("");
16641713
}
16651714
function generateBarGroups(ratingBuckets) {
1715+
const totalPerformers = ratingBuckets.reduce((s, c) => s + c, 0);
16661716
const maxBucket = Math.max(...ratingBuckets, 1);
1717+
const isClustered = totalPerformers > 0 && maxBucket / totalPerformers > 0.5;
1718+
if (isClustered) {
1719+
const grouped = [];
1720+
for (let i = 0; i <= 100; i += 5) {
1721+
const count = ratingBuckets.slice(i, i + 5).reduce((s, c) => s + c, 0);
1722+
grouped.push({ label: `${i}\u2013${Math.min(i + 4, 100)}`, count });
1723+
}
1724+
const groupMax = Math.max(...grouped.map((g) => g.count), 1);
1725+
return grouped.map(({ label, count }) => {
1726+
if (count === 0)
1727+
return "";
1728+
const percentage = count / groupMax * 100;
1729+
return `
1730+
<div class="hon-bar-container" title="Rating ${label}: ${count} performers">
1731+
<div class="hon-bar-label" style="min-width:60px">${label}</div>
1732+
<div class="hon-bar-wrapper">
1733+
<div class="hon-bar" style="width: ${percentage}%">
1734+
${count > 2 ? `<span class="hon-bar-count">${count}</span>` : ""}
1735+
</div>
1736+
</div>
1737+
</div>`;
1738+
}).join("");
1739+
}
16671740
return ratingBuckets.map((count, i) => {
16681741
if (count === 0)
16691742
return "";
@@ -1721,7 +1794,8 @@
17211794
shouldShowButton: () => shouldShowButton
17221795
});
17231796
function shouldShowButton() {
1724-
return ["/performers", "/performers/", "/images", "/images/"].includes(window.location.pathname);
1797+
const path = window.location.pathname;
1798+
return /^\/performers/.test(path) || /^\/images/.test(path);
17251799
}
17261800
function addFloatingButton() {
17271801
if (document.getElementById("hon-floating-btn"))
@@ -1736,7 +1810,7 @@
17361810
document.body.appendChild(btn);
17371811
}
17381812
function handleGlobalKeys(e) {
1739-
const activeModal = document.getElementById("hon-modal-container");
1813+
const activeModal = document.getElementById("hon-modal");
17401814
if (!activeModal) {
17411815
document.removeEventListener("keydown", handleGlobalKeys);
17421816
return;
@@ -1746,20 +1820,20 @@
17461820
e.stopImmediatePropagation();
17471821
e.preventDefault();
17481822
if (e.key === "ArrowLeft") {
1749-
activeModal.querySelector('.hon-scene-card[data-side="left"] .hon-scene-body')?.click();
1823+
const leftCard = activeModal.querySelector('.hon-scene-card[data-side="left"]');
1824+
leftCard?.querySelector(".hon-scene-body")?.click();
17501825
}
17511826
if (e.key === "ArrowRight") {
1752-
activeModal.querySelector('.hon-scene-card[data-side="right"] .hon-scene-body')?.click();
1827+
const rightCard = activeModal.querySelector('.hon-scene-card[data-side="right"]');
1828+
rightCard?.querySelector(".hon-scene-body")?.click();
17531829
}
17541830
if (e.key === " " || e.code === "Space") {
17551831
document.getElementById("hon-skip-btn")?.click();
17561832
}
17571833
}
17581834
}
1759-
function openRankingModal() {
1835+
function _buildAndOpenModal() {
17601836
try {
1761-
const path = window.location.pathname;
1762-
state.battleType = path.includes("/images") ? "images" : "performers";
17631837
const existing = document.getElementById("hon-modal");
17641838
if (existing)
17651839
existing.remove();
@@ -1777,11 +1851,53 @@
17771851
modal.querySelector(".hon-modal-backdrop").onclick = () => closeRankingModal();
17781852
attachEventListeners(modal);
17791853
if (state.currentMode === "gauntlet") {
1780-
window.showPerformerSelection();
1854+
if (state.gauntletChampion) {
1855+
const selEl = document.getElementById("hon-performer-selection");
1856+
const compEl = document.getElementById("hon-comparison-area");
1857+
const actEl = document.querySelector(".hon-actions");
1858+
if (selEl)
1859+
selEl.style.display = "none";
1860+
if (compEl)
1861+
compEl.style.display = "";
1862+
if (actEl)
1863+
actEl.style.display = "";
1864+
loadNewPair();
1865+
} else {
1866+
window.showPerformerSelection();
1867+
}
17811868
} else {
17821869
loadNewPair();
17831870
}
17841871
document.addEventListener("keydown", handleGlobalKeys);
1872+
} catch (err) {
1873+
console.error("CRASH in _buildAndOpenModal:", err);
1874+
}
1875+
}
1876+
function openRankingModal() {
1877+
try {
1878+
const path = window.location.pathname;
1879+
state.battleType = path.includes("/images") ? "images" : "performers";
1880+
const performerMatch = path.match(/\/performers\/(\d+)/);
1881+
if (performerMatch && state.currentMode === "gauntlet") {
1882+
const performerId = performerMatch[1];
1883+
Promise.resolve().then(() => (init_api_client(), api_client_exports)).then(async ({ fetchPerformerById: fetchPerformerById2 }) => {
1884+
try {
1885+
const performer = await fetchPerformerById2(performerId);
1886+
if (performer) {
1887+
state.gauntletChampion = performer;
1888+
state.gauntletWins = 0;
1889+
state.gauntletDefeated = [];
1890+
state.gauntletFalling = false;
1891+
state.gauntletFallingItem = null;
1892+
}
1893+
} catch (e) {
1894+
console.warn("[HotOrNot] Could not pre-seed performer for gauntlet:", e);
1895+
}
1896+
_buildAndOpenModal();
1897+
});
1898+
return;
1899+
}
1900+
_buildAndOpenModal();
17851901
} catch (err) {
17861902
console.error("CRASH in openRankingModal:", err);
17871903
}
@@ -1806,16 +1922,17 @@
18061922
// ui-dashboard.js
18071923
function createMainUI() {
18081924
const isPerformers = state.battleType === "performers";
1809-
const MODE_LABELS = {
1810-
swiss: "\u2696\uFE0F Swiss",
1811-
gauntlet: "\u{1F94A} Gauntlet",
1812-
champion: "\u{1F451} Champion"
1925+
const MODE_CONFIG = {
1926+
swiss: { icon: "\u2696\uFE0F", label: "Swiss" },
1927+
gauntlet: { icon: "\u{1F94A}", label: "Gauntlet" },
1928+
champion: { icon: "\u{1F451}", label: "Champion" }
18131929
};
18141930
const modeToggleHTML = state.battleType !== "images" ? `
18151931
<div class="hon-mode-toggle">
18161932
${["swiss", "gauntlet", "champion"].map((mode) => `
18171933
<button class="hon-mode-btn ${state.currentMode === mode ? "active" : ""}" data-mode="${mode}">
1818-
${MODE_LABELS[mode]}
1934+
<span class="hon-mode-icon">${MODE_CONFIG[mode].icon}</span>
1935+
<span class="hon-mode-title">${MODE_CONFIG[mode].label}</span>
18191936
</button>`).join("")}
18201937
</div>` : "";
18211938
const genderFilterHTML = isPerformers ? `
@@ -2122,6 +2239,7 @@ Match Stats:`;
21222239
// main.js
21232240
init_state();
21242241
init_ui_manager();
2242+
init_ui_modal();
21252243
init_gauntlet_selection();
21262244
init_match_handler();
21272245
init_api_client();
@@ -2135,7 +2253,12 @@ Match Stats:`;
21352253
var lastPath = "";
21362254
var observer = new MutationObserver(() => {
21372255
const currentPath = window.location.pathname;
2138-
if (!document.getElementById("hon-floating-btn")) {
2256+
const existingBtn = document.getElementById("hon-floating-btn");
2257+
if (existingBtn) {
2258+
if (!shouldShowButton()) {
2259+
existingBtn.remove();
2260+
}
2261+
} else if (shouldShowButton()) {
21392262
addFloatingButton();
21402263
}
21412264
if (isOnSinglePerformerPage()) {

0 commit comments

Comments
 (0)