IQ22c / index.html
PasoJ's picture
Upload index.html
de9df6d verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Image Search Query Builder</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
margin: 0;
padding: 20px;
background-color: #1a1a1a;
color: #e0e0e0;
display: flex;
flex-direction: column;
align-items: center;
min-height: 100vh;
box-sizing: border-box;
}
.container {
background-color: #1a1a1a;
padding: 30px;
border-radius: 10px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.4);
width: 90%;
max-width: 750px;
box-sizing: border-box;
}
h1, h2 {
color: #00ff00;
text-align: center;
margin-bottom: 25px;
text-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
font-weight: 300;
}
h1 {
border-bottom: 2px solid #00ff00;
padding-bottom: 0.5rem;
}
h2 {
margin-top: 40px;
border-top: 1px solid #444;
padding-top: 25px;
}
label {
display: block;
margin-bottom: 8px;
font-weight: bold;
color: #c0c0c0;
}
.label-group {
display: flex;
justify-content: space-between;
align-items: center;
}
input[type="text"],
textarea,
input[type="number"] {
width: calc(100% - 22px);
padding: 12px;
margin-bottom: 15px;
border: 1px solid #00ff00;
border-radius: 4px;
font-size: 16px;
box-sizing: border-box;
background-color: #2a2a2a;
color: #e0e0e0;
font-family: "Courier New", Courier, monospace;
}
textarea {
resize: vertical;
}
input[type="number"] {
width: 60px;
margin-bottom: 0;
padding: 8px;
}
input[type="text"]:focus,
textarea:focus,
input[type="number"]:focus {
outline: none;
border-color: #00ff00;
box-shadow: 0 0 5px #00ff00;
}
input[type="text"][readonly] {
background-color: #1f1f1f;
}
textarea {
min-height: 120px;
}
button {
display: block;
width: 100%;
padding: 12px 24px;
border: 1px solid #00ff00;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
transition: all 0.2s ease;
margin-bottom: 10px;
box-sizing: border-box;
font-weight: bold;
background-color: transparent;
color: #00ff00;
}
button:hover {
background-color: #00ff00;
color: #1a1a1a;
box-shadow: 0 0 8px #00ff00;
}
button:active {
background-color: #00e600;
}
.action-button-reset {
border-color: #999;
color: #999;
}
.action-button-reset:hover {
background-color: #999;
color: #1a1a1a;
box-shadow: none;
}
button:disabled {
background-color: #555;
cursor: not-allowed;
opacity: 0.7;
transform: none;
color: #aaa;
border-color: #555;
box-shadow: none;
}
button:disabled:hover {
background-color: #555;
color: #aaa;
}
.surprise-settings {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 15px 25px;
align-items: center;
margin-top: 15px;
margin-bottom: 25px;
padding: 15px;
border: 1px solid #3c3c3c;
border-radius: 8px;
}
.surprise-settings .label-group {
display: flex;
align-items: center;
gap: 10px;
}
.surprise-settings .label-group label,
.surprise-settings .lock-checkbox-group label {
margin-bottom: 0;
font-weight: normal;
font-size: 0.95em;
}
.surprise-settings .lock-checkbox-group {
margin: 0;
}
.main-action-buttons {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin-bottom: 15px;
justify-content: center;
}
.main-action-buttons button {
flex-grow: 1;
margin-bottom: 0;
font-size: 15px;
padding: 10px 15px;
min-width: 150px;
}
.action-buttons-group { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 15px; }
.action-buttons-group button { flex-grow: 1; font-size: 16px; padding: 12px 15px; min-width: 140px; }
.output-area { margin-top: 25px; position: relative; width: 100%; }
#outputContainer {
padding: 18px;
background-color: #1f1f1f;
border: 1px solid #3c3c3c;
border-radius: 8px;
white-space: pre-wrap;
word-wrap: break-word;
font-family: 'Consolas', 'Courier New', monospace;
font-size: 16px;
min-height: 60px;
color: #e0e0e0;
overflow-x: auto;
margin-bottom: 15px;
}
.form-group { margin-bottom: 25px; }
.item-adder-group, .prefix-input-group {
display: flex;
gap: 10px;
align-items: center;
margin-bottom: 10px;
}
.item-adder-group input[type="text"],
.prefix-input-group input[type="text"] {
flex-grow: 1;
margin-bottom: 0;
}
.item-adder-group button,
.prefix-input-group button {
width: auto;
padding: 10px 18px;
font-size: 15px;
margin-bottom: 0;
flex-shrink: 0;
}
.helper-tool, .history-log { margin-top: 30px; }
#helperResults {
background-color: #1f1f1f;
border: 1px solid #3c3c3c;
border-radius: 8px;
padding: 20px;
margin-top: 10px;
}
.helper-output-group { display: flex; align-items: center; gap: 10px; margin-bottom: 15px; }
.helper-output-group input[type="text"] {
flex-grow: 1;
font-family: 'Consolas', 'Courier New', monospace;
background-color: #2a2a2a;
margin-bottom: 0;
opacity: 0.8;
color: #e0e0e0;
}
.helper-output-group button { width: auto; padding: 10px 15px; font-size: 14px; margin-bottom: 0; }
#previousStarResult, #previousItemResult, #nextStarResult, #nextItemResult {
font-family: 'Consolas', 'Courier New', monospace;
font-weight: bold;
color: #00ff00;
}
.lock-checkbox-group { display: flex; align-items: center; gap: 8px; margin-top: -5px; margin-bottom: 18px; }
.lock-checkbox-group label {
font-weight: normal;
margin-bottom: 0;
color: #c0c0c0;
font-size: 0.95em;
cursor: pointer;
}
.lock-checkbox-group input[type="checkbox"] {
width: 18px;
height: 18px;
margin-bottom: 0;
accent-color: #00ff00;
}
.options-group, .star-separator-options {
display: flex;
flex-wrap: wrap;
gap: 10px 20px;
margin-top: -5px;
margin-bottom: 18px;
}
.options-group .lock-checkbox-group {
margin-bottom: 0;
}
.global-query-settings {
display: flex;
justify-content: center;
gap: 20px;
flex-wrap: wrap;
margin-top: 5px;
margin-bottom: 15px;
}
.global-query-settings .lock-checkbox-group {
margin-bottom: 0;
}
.search-engine-selector { display: flex; flex-wrap: wrap; justify-content: center; gap: 15px 25px; margin-top: 15px; font-size: 0.95em; }
.search-engine-selector label {
display: flex;
align-items: center;
gap: 8px;
font-weight: normal;
margin-bottom: 0;
cursor: pointer;
}
.separator-options {
display: flex;
flex-direction: column;
align-items: center;
gap: 10px;
margin-top: 15px;
margin-bottom: 25px;
}
.separator-group {
display: flex;
flex-wrap: wrap;
justify-content: center;
gap: 5px 15px;
}
.separator-options label, .suffix-options label, .star-separator-options label {
display: inline-flex;
align-items: center;
gap: 5px;
font-weight: normal;
font-size: 0.95em;
cursor: pointer;
}
.separator-options input[type="radio"], .suffix-options input[type="radio"], .star-separator-options input[type="radio"] {
width: 16px;
height: 16px;
accent-color: #00ff00;
margin-bottom: 0;
}
.suffix-options {
margin-top: 15px;
margin-bottom: 25px;
text-align: center;
}
.star-separator-options {
flex-direction: column;
align-items: flex-start;
gap: 10px;
margin-bottom: 18px;
width: 100%;
}
.star-separator-options > label {
font-weight: bold;
color: #c0c0c0;
margin-bottom: 0;
font-size: 1em;
}
.tab-nav {
display: flex;
border-bottom: 2px solid #3c3c3c;
margin-bottom: 25px;
}
.tab-button {
padding: 12px 20px;
cursor: pointer;
border: none;
background-color: transparent;
color: #c0c0c0;
font-size: 16px;
font-weight: bold;
transition: color 0.3s, border-bottom 0.3s;
border-bottom: 3px solid transparent;
margin-bottom: -2px;
}
.tab-button:hover {
color: #00ff00;
}
.tab-button.active {
color: #00ff00;
border-bottom: 3px solid #00ff00;
}
.tab-content {
display: none;
}
.tab-content.active {
display: block;
}
#surpriseTab.active {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
min-height: 50vh;
cursor: pointer;
position: relative;
}
.background-text {
font-size: 10vw;
color: rgba(0, 255, 0, 0.1);
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
pointer-events: none;
margin: 0;
}
.randomize-filters {
display: flex;
gap: 15px;
margin-bottom: 15px;
}
.randomize-filters .filter-group {
flex: 1;
}
.randomize-filters input {
margin-bottom: 0;
padding: 8px;
}
.full-list-container {
display: flex;
gap: 20px;
margin-top: 30px;
}
.full-list {
flex: 1;
background-color: #1f1f1f;
border: 1px solid #3c3c3c;
border-radius: 8px;
padding: 15px;
}
.full-list ol {
padding-left: 20px;
margin: 0;
}
.full-list li {
margin-bottom: 5px;
font-family: monospace;
font-size: 14px;
}
.list-total {
margin-top: -15px;
margin-bottom: 15px;
font-weight: bold;
color: #00ff00;
}
#manualLink {
font-size: 0.85em;
text-align: center;
display: block;
margin-top: 25px;
color: #999;
text-decoration: none;
transition: color 0.2s ease;
}
#manualLink:hover {
color: #00ff00;
text-decoration: underline;
}
.history-controls {
display: flex;
gap: 10px;
margin-top: 10px;
}
.history-controls button {
flex-grow: 1;
margin-bottom: 0;
padding: 8px 15px;
font-size: 14px;
}
#manualTab pre {
white-space: pre-wrap;
word-wrap: break-word;
color: #e0e0e0;
font-family: "Courier New", Courier, monospace;
font-size: 14px;
}
#historyDisplay {
background-color: #1f1f1f;
border: 1px solid #3c3c3c;
border-radius: 8px;
height: 400px;
overflow-y: auto;
font-family: 'Consolas', 'Courier New', monospace;
font-size: 14px;
}
.history-entry {
white-space: pre-wrap;
padding: 12px 15px;
border-bottom: 1px solid #333;
cursor: pointer;
transition: background-color 0.2s;
}
.history-entry:hover {
background-color: #2a2a2a;
}
.history-entry:last-child {
border-bottom: none;
}
</style>
</head>
<body>
<div class="container">
<h1>Image Search Query Builder</h1>
<div class="tab-nav">
<button class="tab-button active" data-tab="surpriseTab">Surprise Me!</button>
<button class="tab-button" data-tab="builderTab">Query Builder</button>
<button class="tab-button" data-tab="historyTab">History</button>
<button class="tab-button" data-tab="managerTab">List Manager</button>
<button class="tab-button" data-tab="manualTab">Manual</button>
</div>
<div id="surpriseTab" class="tab-content active">
<h1 class="background-text">Surprise Me!</h1>
</div>
<div id="builderTab" class="tab-content">
<button id="surpriseMeButton">Surprise Me!</button>
<div class="surprise-settings">
<div class="label-group">
<label for="surprisePositiveCount">Modifiers:</label>
<input type="number" id="surprisePositiveCount" min="1" max="5" value="1">
</div>
<div class="lock-checkbox-group">
<input type="checkbox" id="surpriseAppendMode">
<label for="surpriseAppendMode">Append Items</label>
</div>
</div>
<div class="form-group" style="margin-top: 25px; border-top: 1px solid #444; padding-top: 25px;">
<label for="outputPrefix">Primary Subject (The Star):</label>
<div class="prefix-input-group">
<input type="text" id="outputPrefix" list="prefixSuggestions" placeholder="e.g., Abella Danger, category, or keyword">
<button type="button" id="randomizePrefixBtn">Randomize</button>
</div>
<div class="randomize-filters">
<div class="filter-group">
<label for="startsWithFilter">Starts With:</label>
<input type="text" id="startsWithFilter" maxlength="1" placeholder="Letter">
</div>
<div class="filter-group">
<label for="containsFilter">Contains:</label>
<input type="text" id="containsFilter" placeholder="Text">
</div>
</div>
<div class="options-group">
<div class="lock-checkbox-group"><input type="checkbox" id="lockStarCheckbox"><label for="lockStarCheckbox">Lock</label></div>
<div class="lock-checkbox-group"><input type="checkbox" id="quoteStarCheckbox"><label for="quoteStarCheckbox">"Quote"</label></div>
<div class="lock-checkbox-group"><input type="checkbox" id="prefixWithPornstarCheckbox"><label for="prefixWithPornstarCheckbox">Add "Pornstar"</label></div>
<div class="lock-checkbox-group"><input type="checkbox" id="starSuffixSeparatorCheckbox"><label for="starSuffixSeparatorCheckbox">Add Separator</label></div>
<div class="lock-checkbox-group"><input type="checkbox" id="useItemsAsStarsCheckbox"><label for="useItemsAsStarsCheckbox">Use Items As Stars</label></div>
</div>
<div class="star-separator-options">
<label>Star Name Format:</label>
<div class="separator-group">
<label><input type="radio" name="starSeparator" value="space" checked> Space</label>
<label><input type="radio" name="starSeparator" value="hyphen"> Hyphen</label>
<label><input type="radio" name="starSeparator" value="underscore"> Underscore</label>
<label><input type="radio" name="starSeparator" value="nothing"> No Space</label>
<label><input type="radio" name="starSeparator" value="firstNameOnly"> First Name Only</label>
</div>
</div>
</div>
<div class="form-group">
<div class="label-group">
<label for="inputList">Modifiers (include these):</label>
<button type="button" id="addRandomItemBtn" style="width: auto; padding: 10px 18px; font-size: 15px;">Add Random</button>
</div>
<div class="randomize-filters">
<div class="filter-group">
<label for="itemStartsWithFilter">Starts With:</label>
<input type="text" id="itemStartsWithFilter" maxlength="1" placeholder="Letter">
</div>
<div class="filter-group">
<label for="itemContainsFilter">Contains:</label>
<input type="text" id="itemContainsFilter" placeholder="Text">
</div>
</div>
<div class="item-adder-group">
<input type="text" id="singleItemInput" list="itemSuggestions" placeholder="Add a single item and press Enter...">
<button type="button" id="addItemBtn">Add</button>
</div>
<textarea id="inputList" placeholder="Enter items to include, one per line or separated by commas..."></textarea>
<div class="options-group">
<div class="lock-checkbox-group"><input type="checkbox" id="lockPositiveCheckbox"><label for="lockPositiveCheckbox">Lock</label></div>
<div class="lock-checkbox-group"><input type="checkbox" id="quotePositiveCheckbox"><label for="quotePositiveCheckbox">"Quote"</label></div>
<div class="lock-checkbox-group"><input type="checkbox" id="includeStarsAsModifiersCheckbox"><label for="includeStarsAsModifiersCheckbox">Use Stars As Items</label></div>
</div>
<div class="star-separator-options">
<label>Modifier Word Format:</label>
<div class="separator-group">
<label><input type="radio" name="itemWordSeparator" value="space" checked> Space</label>
<label><input type="radio" name="itemWordSeparator" value="hyphen"> Hyphen</label>
<label><input type="radio" name="itemWordSeparator" value="underscore"> Underscore</label>
<label><input type="radio" name="itemWordSeparator" value="nothing"> No Space</label>
</div>
</div>
</div>
<div class="main-action-buttons">
<button id="generateQueryBtn">Generate Query</button>
<button id="resetAllBtn" class="action-button-reset">Reset</button>
<button id="fullscreenButton">Full Screen</button>
</div>
<div class="global-query-settings">
<div class="lock-checkbox-group"><input type="checkbox" id="useParenthesesCheckbox"><label for="useParenthesesCheckbox">Use Parentheses</label></div>
<div class="lock-checkbox-group"><input type="checkbox" id="quoteEntireQueryCheckbox"><label for="quoteEntireQueryCheckbox">"Quote" Entire Query</label></div>
</div>
<div class="separator-options">
<label>Global Separator:</label>
<div class="separator-group">
<label><input type="radio" name="separator" value=" " checked> Space</label>
<label><input type="radio" name="separator" value=", "> Comma</label>
<label><input type="radio" name="separator" value=" - "> Dash</label>
<label><input type="radio" name="separator" value="-"> Hyphen</label>
<label><input type="radio" name="separator" value="_"> Underscore</label>
<label><input type="radio" name="separator" value=": "> Colon</label>
<label><input type="radio" name="separator" value="; "> Semicolon</label>
</div>
<div class="separator-group">
<label><input type="radio" name="separator" value=" AND "> AND</label>
<label><input type="radio" name="separator" value=" OR "> OR</label>
<label><input type="radio" name="separator" value=" NOT "> NOT</label>
<label><input type="radio" name="separator" value=" WITH "> WITH</label>
<label><input type="radio" name="separator" value=" AS "> AS</label>
</div>
</div>
<div class="output-area">
<div id="outputContainer"></div>
<div class="suffix-options">
<label>Suffix Option:</label>
<label><input type="radio" name="suffixType" value="clear" checked> Clear</label>
<label><input type="radio" name="suffixType" value="numeric"> Numeric</label>
<label><input type="radio" name="suffixType" value="alpha"> Alphabetic</label>
</div>
<div class="search-engine-selector">
<label><input type="checkbox" id="googleCheckbox" checked> Google</label>
<label><input type="checkbox" id="bingCheckbox"> Bing</label>
<label><input type="checkbox" id="yandexCheckbox"> Yandex</label>
<label><input type="checkbox" id="yahooCheckbox"> Yahoo</label>
<label><input type="checkbox" id="duckDuckGoCheckbox"> DuckDuckGo</label>
<label><input type="checkbox" id="pholderCheckbox"> Pholder</label>
</div>
<div class="action-buttons-group">
<button id="copyButton" disabled>Copy Query</button>
<button data-engine="Google" class="search-btn" disabled>Search Google</button>
<button data-engine="Bing" class="search-btn" disabled>Search Bing</button>
<button data-engine="Yandex" class="search-btn" disabled>Search Yandex</button>
<button data-engine="Yahoo" class="search-btn" disabled>Search Yahoo</button>
<button data-engine="DuckDuckGo" class="search-btn" disabled>Search DuckDuckGo</button>
<button data-engine="Pholder" class="search-btn" disabled>Search Pholder</button>
</div>
</div>
</div>
<div id="managerTab" class="tab-content">
<div class="helper-tool">
<h2>List Manager</h2>
<div class="form-group" style="margin-bottom: 10px;">
<input type="text" id="helperInput" placeholder="Star Name (Synced with Query Builder)">
</div>
<div id="helperResults">
<label>Generated Entry to Paste into JSON file:</label>
<div class="helper-output-group">
<input type="text" id="htmlTagResult" readonly>
<button id="copyHelperBtn">Copy</button>
</div>
<p><strong>Previous Star:</strong> <span id="previousStarResult"></span></p>
<p><strong>Next Star:</strong> <span id="nextStarResult"></span></p>
<p><strong>Previous Item:</strong> <span id="previousItemResult"></span></p>
<p><strong>Next Item:</strong> <span id="nextItemResult"></span></p>
</div>
</div>
<div class="full-list-container">
<div class="full-list">
<h2>Full Star List</h2>
<p class="list-total">Total: <span id="starTotal">0</span></p>
<div id="fullStarListDisplay"></div>
</div>
<div class="full-list">
<h2>Full Item List</h2>
<p class="list-total">Total: <span id="itemTotal">0</span></p>
<div id="fullItemListDisplay"></div>
</div>
</div>
</div>
<div id="historyTab" class="tab-content">
<div class="history-log">
<h2>Search History</h2>
<div id="historyDisplay"></div>
<div class="history-controls">
<button id="downloadHistoryButton">Download History</button>
<button id="clearHistoryButton" class="action-button-reset">Clear History</button>
</div>
</div>
</div>
<div id="manualTab" class="tab-content"></div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
const QueryBuilderApp = {
// Store data, state, and cached DOM elements
data: {
stars: [],
items: [],
history: []
},
elements: {},
state: {},
// --- 1. APP INITIALIZATION ---
async init() {
this._cacheElements();
this._setupEventListeners();
await this._loadData();
this._loadHistory();
this.render();
},
_cacheElements() {
// Cache all DOM elements we'll need to interact with
const ids = [
'surpriseTab', 'surpriseMeButton', 'surprisePositiveCount', 'surpriseAppendMode',
'outputPrefix', 'randomizePrefixBtn', 'startsWithFilter', 'containsFilter',
'lockStarCheckbox', 'quoteStarCheckbox', 'prefixWithPornstarCheckbox', 'starSuffixSeparatorCheckbox',
'useItemsAsStarsCheckbox', 'addRandomItemBtn', 'itemStartsWithFilter', 'itemContainsFilter',
'singleItemInput', 'addItemBtn', 'inputList', 'lockPositiveCheckbox', 'quotePositiveCheckbox',
'includeStarsAsModifiersCheckbox', 'generateQueryBtn', 'resetAllBtn', 'fullscreenButton',
'useParenthesesCheckbox', 'quoteEntireQueryCheckbox', 'outputContainer', 'copyButton',
'helperInput', 'helperResults', 'htmlTagResult', 'copyHelperBtn', 'previousStarResult',
'nextStarResult', 'previousItemResult', 'nextItemResult', 'starTotal', 'fullStarListDisplay',
'itemTotal', 'fullItemListDisplay', 'historyDisplay', 'downloadHistoryButton', 'clearHistoryButton',
'manualTab'
];
ids.forEach(id => this.elements[id] = document.getElementById(id));
this.elements.tabButtons = document.querySelectorAll('.tab-button');
this.elements.searchButtons = document.querySelectorAll('.search-btn');
this.elements.allActionButtons = document.querySelectorAll('.action-buttons-group button');
this.elements.inputsForRender = document.querySelectorAll('textarea, input[type=checkbox], input[type=radio], #outputPrefix, #startsWithFilter, #containsFilter, #itemStartsWithFilter, #itemContainsFilter');
},
_setupEventListeners() {
// Centralize all event listeners
this.elements.tabButtons.forEach(btn => btn.addEventListener('click', () => this._showTab(btn.dataset.tab)));
this.elements.surpriseTab.addEventListener('click', () => this.surpriseMe());
this.elements.surpriseMeButton.addEventListener('click', () => this.surpriseMe());
// Main controls
this.elements.generateQueryBtn.addEventListener('click', () => this.render());
this.elements.resetAllBtn.addEventListener('click', () => this._resetAll());
this.elements.fullscreenButton.addEventListener('click', () => this._toggleFullScreen());
// Builder interactions
this.elements.randomizePrefixBtn.addEventListener('click', () => this._randomizePrefix());
this.elements.addRandomItemBtn.addEventListener('click', () => this._addRandomItem());
this.elements.addItemBtn.addEventListener('click', () => this._addItemToList());
this.elements.singleItemInput.addEventListener('keypress', e => { if (e.key === 'Enter') { e.preventDefault(); this._addItemToList(); }});
// Search and copy buttons
this.elements.searchButtons.forEach(btn => btn.addEventListener('click', () => this.search(btn.dataset.engine)));
this.elements.copyButton.addEventListener('click', () => this._copyToClipboard(this.elements.outputContainer.textContent, this.elements.copyButton));
this.elements.copyHelperBtn.addEventListener('click', () => this._copyToClipboard(this.elements.htmlTagResult.value, this.elements.copyHelperBtn));
// History controls
this.elements.downloadHistoryButton.addEventListener('click', () => this._downloadHistory());
this.elements.clearHistoryButton.addEventListener('click', () => this._clearHistory());
// Sync helper input with main prefix input
this.elements.outputPrefix.addEventListener('input', () => this._syncInputs(this.elements.outputPrefix, this.elements.helperInput));
this.elements.helperInput.addEventListener('input', () => this._syncInputs(this.elements.helperInput, this.elements.outputPrefix));
// Add listeners to all inputs that should trigger a re-render
this.elements.inputsForRender.forEach(el => el.addEventListener('input', () => this.render()));
},
async _loadData() {
try {
const [starsResponse, itemsResponse] = await Promise.all([fetch('stars.json'), fetch('items.json')]);
this.data.stars = await starsResponse.json();
this.data.items = await itemsResponse.json();
this._createDatalists();
} catch (error) {
console.error("Failed to load data:", error);
this.elements.outputContainer.textContent = "Error: Could not load data files.";
}
},
_loadHistory() {
const savedHistory = localStorage.getItem('searchHistoryLog');
this.data.history = savedHistory ? JSON.parse(savedHistory) : [];
},
// --- 2. CORE LOGIC ---
_updateStateFromUI() {
// Read all controls and update the central state object
this.state = {
prefix: this.elements.outputPrefix.value.trim(),
modifiers: this._parseAndCleanList(this.elements.inputList.value),
options: {
prefixWithPornstar: this.elements.prefixWithPornstarCheckbox.checked,
quoteStar: this.elements.quoteStarCheckbox.checked,
starSeparator: document.querySelector('input[name="starSeparator"]:checked').value,
addSeparatorAfterStar: this.elements.starSuffixSeparatorCheckbox.checked,
quotePositive: this.elements.quotePositiveCheckbox.checked,
itemWordSeparator: document.querySelector('input[name="itemWordSeparator"]:checked').value,
useParens: this.elements.useParenthesesCheckbox.checked,
quoteEntire: this.elements.quoteEntireQueryCheckbox.checked,
separator: document.querySelector('input[name="separator"]:checked').value,
suffixType: document.querySelector('input[name="suffixType"]:checked').value
}
};
},
_buildQueryString() {
// Build the query string based on the current state, not the DOM
const { prefix, modifiers, options } = this.state;
if (!prefix && modifiers.length === 0) return { error: "Please enter a subject or some modifiers." };
let processedPrefix = prefix;
if (options.prefixWithPornstar && processedPrefix) {
processedPrefix = `Pornstar ${processedPrefix}`;
}
if (processedPrefix) {
processedPrefix = this._formatWord(processedPrefix, options.starSeparator);
if (options.starSeparator === 'firstNameOnly') {
processedPrefix = processedPrefix.split(' ')[0];
}
}
if (options.quoteStar && processedPrefix) {
processedPrefix = `"${processedPrefix}"`;
}
let resultString = processedPrefix;
if (modifiers.length > 0) {
const processedModifiers = modifiers.map(item => {
const formatted = this._formatWord(item, options.itemWordSeparator);
return options.quotePositive ? `"${formatted}"` : formatted;
}).join(options.separator);
const finalPart = options.useParens && modifiers.length > 1 ? `(${processedModifiers})` : processedModifiers;
resultString = resultString ? `${resultString}${options.addSeparatorAfterStar ? options.separator : " "}${finalPart}` : finalPart;
}
if (options.quoteEntire && resultString) resultString = `"${resultString.trim()}"`;
let suffix = '';
if (options.suffixType === 'numeric') suffix = this._generateRandomNumericSuffix();
else if (options.suffixType === 'alpha') suffix = this._generateRandomAlphaSuffix();
return { query: `${resultString.trim().replace(/\s\s+/g, ' ')}${suffix ? ` ${suffix}` : ''}` };
},
search(engine, isFromSurprise = false) {
const query = this.elements.outputContainer.textContent;
if (!query || this.elements.outputContainer.textContent === this.state.error) return;
if (!isFromSurprise) this._logSearch(query, engine);
const urls = {
'Google': `https://www.google.com/search?tbm=isch&q=${encodeURIComponent(query)}`,
'Bing': `https://www.bing.com/images/search?q=${encodeURIComponent(query)}`,
'Yandex': `https://yandex.com/images/search?text=${encodeURIComponent(query)}`,
'Yahoo': `https://images.search.yahoo.com/search/images?p=${encodeURIComponent(query)}`,
'DuckDuckGo': `https://duckduckgo.com/?q=${encodeURIComponent(query)}&t=h_&iar=images`,
'Pholder': `https://pholder.com/search/${encodeURIComponent(query)}/`
};
if (urls[engine]) window.open(urls[engine], '_blank');
},
surpriseMe() {
const appendMode = this.elements.surpriseAppendMode.checked;
if (!this.elements.lockStarCheckbox.checked) {
this._randomizePrefix();
}
if (!this.elements.lockPositiveCheckbox.checked) {
const count = parseInt(this.elements.surprisePositiveCount.value, 10) || 1;
const filters = {
startsWith: this.elements.itemStartsWithFilter.value.toLowerCase().trim(),
contains: this.elements.itemContainsFilter.value.toLowerCase().trim()
};
let chosenItems = [];
const allUsedItems = appendMode ? this._parseAndCleanList(this.elements.inputList.value) : [];
for (let i = 0; i < count; i++) {
const item = this._getRandomUniqueItem(chosenItems.concat(allUsedItems), filters);
if (item) chosenItems.push(item); else break;
}
if (chosenItems.length > 0) {
const newItems = chosenItems.join('\n');
this.elements.inputList.value = (appendMode && this.elements.inputList.value.trim()) ? `${this.elements.inputList.value.trim()}\n${newItems}` : newItems;
} else if (!appendMode) {
this.elements.inputList.value = '';
}
}
this.render();
const selectedEngines = Array.from(document.querySelectorAll('.search-engine-selector input[type=checkbox]:checked'))
.map(cb => cb.id.replace('Checkbox',''));
if(selectedEngines.length > 0){
const randomEngine = selectedEngines[Math.floor(Math.random() * selectedEngines.length)];
const engineName = randomEngine.charAt(0).toUpperCase() + randomEngine.slice(1).replace('DuckDuckGo', 'DuckDuckGo');
this.search(engineName, true);
}
},
// --- 3. UI RENDERING & UPDATES ---
render() {
this._updateStateFromUI();
const result = this._buildQueryString();
if (result.error) {
this.elements.outputContainer.textContent = result.error;
this.elements.allActionButtons.forEach(btn => btn.disabled = true);
} else {
this.elements.outputContainer.textContent = result.query;
this.elements.allActionButtons.forEach(btn => btn.disabled = false);
}
this.elements.copyButton.textContent = 'Copy Query';
this._renderHelperTool();
this._renderFullLists();
this._renderHistory();
},
_renderHistory() {
const container = this.elements.historyDisplay;
container.innerHTML = '';
this.data.history.forEach((entry, index) => {
const entryDiv = document.createElement('div');
entryDiv.className = 'history-entry';
entryDiv.innerHTML = `${entry.timestamp}<br>- Engine: ${entry.engine}<br>- Star: ${entry.star}<br>- Modifiers: ${entry.modifiers}<br>- Query: <strong>${entry.query}</strong>`;
entryDiv.onclick = () => this._rerunSearch(index);
container.appendChild(entryDiv);
});
},
_renderFullLists() {
const starList = [...new Set(this.data.stars.filter(Boolean))].sort();
const itemList = [...new Set(this.data.items.filter(Boolean))].sort();
this.elements.fullStarListDisplay.innerHTML = `<ol>${starList.map(s => `<li>${s}</li>`).join('')}</ol>`;
this.elements.fullItemListDisplay.innerHTML = `<ol>${itemList.map(i => `<li>${i}</li>`).join('')}</ol>`;
this.elements.starTotal.textContent = starList.length;
this.elements.itemTotal.textContent = itemList.length;
},
_renderHelperTool() {
const targetName = this.elements.helperInput.value.trim();
this.elements.helperResults.style.display = targetName ? 'block' : 'none';
if (!targetName) return;
this.elements.htmlTagResult.value = `"${targetName}",`;
const starAdj = this._findAdjacentInList(targetName, this.data.stars);
this.elements.previousStarResult.textContent = starAdj.previous;
this.elements.nextStarResult.textContent = starAdj.next;
const itemAdj = this._findAdjacentInList(targetName, this.data.items);
this.elements.previousItemResult.textContent = itemAdj.previous;
this.elements.nextItemResult.textContent = itemAdj.next;
},
// --- 4. HELPER & UTILITY METHODS ---
_showTab(tabId) {
this.elements.tabButtons.forEach(btn => btn.classList.toggle('active', btn.dataset.tab === tabId));
document.querySelectorAll('.tab-content').forEach(content => content.classList.toggle('active', content.id === tabId));
if (tabId === 'manualTab' && !this.data.manualContentLoaded) this._loadManual();
},
_rerunSearch(index) {
const entry = this.data.history[index];
if (!entry) return;
this.elements.outputPrefix.value = entry.star === 'N/A' ? '' : entry.star;
this.elements.inputList.value = entry.modifiers === 'N/A' ? '' : entry.modifiers.split(', ').join('\n');
this._showTab('builderTab');
this.render();
this.search(entry.engine, true); // Don't re-log a re-run
},
_logSearch(query, engine) {
const now = new Date();
const timestamp = `[${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, '0')}-${String(now.getDate()).padStart(2, '0')} ${String(now.getHours()).padStart(2, '0')}:${String(now.getMinutes()).padStart(2, '0')}:${String(now.getSeconds()).padStart(2, '0')}]`;
const logEntry = {
timestamp, engine, query,
star: this.elements.outputPrefix.value.trim() || 'N/A',
modifiers: this._parseAndCleanList(this.elements.inputList.value).join(', ') || 'N/A'
};
this.data.history.unshift(logEntry);
if (this.data.history.length > 200) this.data.history.pop();
localStorage.setItem('searchHistoryLog', JSON.stringify(this.data.history));
this._renderHistory();
},
// ... other helpers from original code, now part of the app object
_parseAndCleanList(listText) { if (!listText.trim()) return []; return [...new Set(listText.split(/,|\n+/).map(item => item.trim()).filter(Boolean).map(item => { let cleaned = item; if (cleaned.startsWith('- ')) cleaned = cleaned.substring(2); else if (cleaned.startsWith('-')) cleaned = cleaned.substring(1); if (cleaned.startsWith('[')) cleaned = cleaned.substring(1); if (cleaned.endsWith(']')) cleaned = cleaned.slice(0, -1); return cleaned.trim(); }).filter(Boolean))]; },
_formatWord(word, separatorType) { switch (separatorType) { case 'hyphen': return word.replace(/\s+/g, '-'); case 'underscore': return word.replace(/\s+/g, '_'); case 'nothing': return word.replace(/\s+/g, ''); default: return word; }},
_generateRandomNumericSuffix() { const length = Math.floor(Math.random() * 6) + 1; let result = ''; for (let i = 0; i < length; i++) result += Math.floor(Math.random() * 10); return result; },
_generateRandomAlphaSuffix() { const consonants = 'bcdfghjklmnpqrstvwxyz', vowels = 'aeiou'; const length = Math.floor(Math.random() * 6) + 1; let result = ''; for (let i = 0; i < length; i++) { result += (i === 1 || i === 4) ? vowels[Math.floor(Math.random() * vowels.length)] : consonants[Math.floor(Math.random() * consonants.length)]; } return result.charAt(0).toUpperCase() + result.slice(1); },
_toggleFullScreen() { if (!document.fullscreenElement) { document.documentElement.requestFullscreen(); this.elements.fullscreenButton.textContent = 'Exit Full Screen'; } else { document.exitFullscreen(); this.elements.fullscreenButton.textContent = 'Full Screen'; }},
_copyToClipboard(text, buttonElement) { if (!text) return; navigator.clipboard.writeText(text).then(() => { const originalText = buttonElement.textContent; buttonElement.textContent = 'Copied!'; setTimeout(() => { buttonElement.textContent = originalText; }, 2000); }).catch(err => console.error('Failed to copy text: ', err)); },
_findAdjacentInList(targetName, listArray) { const names = listArray.filter(Boolean); const sortedList = [...new Set([targetName, ...names])].sort((a, b) => a.localeCompare(b, undefined, { sensitivity: 'base' })); const index = sortedList.indexOf(targetName); const prev = (index > 0) ? sortedList[index - 1] : (names.includes(targetName) ? '(Already first)' : '(First item)'); const next = (index > -1 && index < sortedList.length - 1) ? sortedList[index + 1] : (names.includes(targetName) ? '(Already last)' : '(Last item)'); return { previous: prev, next: next }; },
_syncInputs(source, destination) { if (document.activeElement === source) { destination.value = source.value; this.render(); }},
_createDatalists() { const starDatalist = document.createElement('datalist'); starDatalist.id = 'prefixSuggestions'; this.data.stars.forEach(item => { const option = document.createElement('option'); option.value = item; starDatalist.appendChild(option); }); const itemDatalist = document.createElement('datalist'); itemDatalist.id = 'itemSuggestions'; this.data.items.forEach(item => { const option = document.createElement('option'); option.value = item; itemDatalist.appendChild(option); }); document.body.appendChild(starDatalist); document.body.appendChild(itemDatalist); },
async _loadManual() { if(this.data.manualContentLoaded) return; try { const response = await fetch('readme.txt'); const text = await response.text(); this.elements.manualTab.innerHTML = response.ok ? `<h2>Manual</h2><pre>${text}</pre>` : `<h2>Manual</h2><p style="color: #ea4335;">Could not load readme.txt.</p>`; } catch (error) { this.elements.manualTab.innerHTML = `<h2>Manual</h2><p style="color: #ea4335;">Error loading readme.txt.</p>`;} this.data.manualContentLoaded = true; },
_downloadHistory() { const historyText = this.data.history.map(e => `${e.timestamp}\n- Engine: ${e.engine}\n- Star: ${e.star}\n- Modifiers: ${e.modifiers}\n- Query: ${e.query}`).join('\n----------------------------------------\n'); const blob = new Blob([historyText], { type: 'text/plain;charset=utf-8' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'search_history.txt'; a.click(); URL.revokeObjectURL(url); },
_clearHistory() { if (confirm("Are you sure?")) { this.data.history = []; localStorage.removeItem('searchHistoryLog'); this.render(); }},
_resetAll() { ['outputPrefix', 'inputList', 'singleItemInput', 'startsWithFilter', 'containsFilter', 'itemStartsWithFilter', 'itemContainsFilter'].forEach(id => this.elements[id].value = ''); document.querySelectorAll('input[type="checkbox"]').forEach(cb => cb.checked = (cb.id === 'googleCheckbox')); this.elements.surprisePositiveCount.value = 1; document.querySelector('input[name="separator"][value=" "]').checked = true; document.querySelector('input[name="starSeparator"][value="space"]').checked = true; document.querySelector('input[name="itemWordSeparator"][value="space"]').checked = true; document.querySelector('input[name="suffixType"][value="clear"]').checked = true; this.render(); },
_randomizePrefix() { if (this.elements.lockStarCheckbox.checked) return; let starOptions = [...this.data.stars]; if (this.elements.useItemsAsStarsCheckbox.checked) starOptions.push(...this.data.items); const startsWith = this.elements.startsWithFilter.value.toLowerCase().trim(); const contains = this.elements.containsFilter.value.toLowerCase().trim(); let filteredOptions = [...new Set(starOptions)].filter(Boolean); if (startsWith) filteredOptions = filteredOptions.filter(val => val.toLowerCase().startsWith(startsWith)); if (contains) filteredOptions = filteredOptions.filter(val => val.toLowerCase().includes(contains)); if (filteredOptions.length > 0) { this.elements.outputPrefix.value = filteredOptions[Math.floor(Math.random() * filteredOptions.length)]; this.render(); }},
_addRandomItem() { if (this.elements.lockPositiveCheckbox.checked) return; const filters = { startsWith: this.elements.itemStartsWithFilter.value.toLowerCase().trim(), contains: this.elements.itemContainsFilter.value.toLowerCase().trim() }; const item = this._getRandomUniqueItem([], filters); if (item) { this.elements.inputList.value = (this.elements.inputList.value.trim() ? this.elements.inputList.value.trim() + '\n' : '') + item; this.render(); } else { alert("No available items match your current filters."); }},
_addItemToList() { if (this.elements.lockPositiveCheckbox.checked) return; const item = this.elements.singleItemInput.value.trim(); if(item){ this.elements.inputList.value = (this.elements.inputList.value.trim() ? this.elements.inputList.value.trim() + '\n' : '') + item; this.elements.singleItemInput.value = ""; this.render();} this.elements.singleItemInput.focus();},
_getRandomUniqueItem(existingItemsToAvoid = [], filters = {}) { let possibleItems = [...this.data.items]; if (this.elements.includeStarsAsModifiersCheckbox.checked) possibleItems.push(...this.data.stars); const usedItems = new Set([...this._parseAndCleanList(this.elements.inputList.value), ...existingItemsToAvoid, this.elements.outputPrefix.value.trim()]); let availableItems = [...new Set(possibleItems)].filter(item => item && !usedItems.has(item)); if (filters.startsWith) availableItems = availableItems.filter(val => val.toLowerCase().startsWith(filters.startsWith)); if (filters.contains) availableItems = availableItems.filter(val => val.toLowerCase().includes(filters.contains)); if (availableItems.length === 0) return null; return availableItems[Math.floor(Math.random() * availableItems.length)]; }
};
QueryBuilderApp.init();
});
</script>
</body>
</html>