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 {
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 ) ) ) ;
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 ) ) ) ;
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 ( `
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 ( ) ;
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 ) ;
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 }
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 "" ;
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 / ^ \/ p e r f o r m e r s / . test ( path ) || / ^ \/ i m a g e s / . test ( path ) ;
17251799 }
17261800 function addFloatingButton ( ) {
17271801 if ( document . getElementById ( "hon-floating-btn" ) )
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 ;
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 ( ) ;
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 ( / \/ p e r f o r m e r s \/ ( \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 }
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