Bug fixes.
This commit is contained in:
27
config.py
27
config.py
@@ -8,31 +8,22 @@ import os
|
|||||||
# Base directory of the repo (this file lives in the project root)
|
# Base directory of the repo (this file lives in the project root)
|
||||||
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
|
||||||
# Paths to the folders that contain source images. Can be overridden at runtime
|
# Paths to the folders that contain source images. Add as many as you like.
|
||||||
# via the IMAGE_DIRS environment variable (colon-separated paths).
|
IMAGE_DIRS = [
|
||||||
_IMAGE_DIRS_DEFAULT = [
|
"/mnt/secret-items/sd-outputs/Sorted/Images/Portrait",
|
||||||
"/media/Portrait",
|
"/mnt/secret-items/sd-outputs/Sorted/Images/Landscape",
|
||||||
"/media/Landscape",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
_IMAGE_DIRS_ENV = os.getenv("IMAGE_DIRS")
|
|
||||||
if _IMAGE_DIRS_ENV:
|
|
||||||
IMAGE_DIRS = _IMAGE_DIRS_ENV.split(":")
|
|
||||||
else:
|
|
||||||
IMAGE_DIRS = _IMAGE_DIRS_DEFAULT
|
|
||||||
|
|
||||||
# Backwards-compatibility: first directory
|
# Backwards-compatibility: first directory
|
||||||
IMAGE_DIR = IMAGE_DIRS[0] if IMAGE_DIRS else ""
|
IMAGE_DIR = IMAGE_DIRS[0]
|
||||||
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
# Data directory (override with DATA_DIR env var)
|
|
||||||
DATA_DIR = os.getenv("DATA_DIR", BASE_DIR)
|
|
||||||
|
|
||||||
# SQLite database file that stores selections & metadata
|
# SQLite database file that stores selections & metadata
|
||||||
DB_PATH = os.path.join(DATA_DIR, "image_selections.db")
|
DB_PATH = os.path.join(BASE_DIR, "data/image_selections.db")
|
||||||
|
|
||||||
# Default port for the HTTP server (override with PORT env var)
|
# Default port for the HTTP server
|
||||||
PORT = int(os.getenv("PORT", 8888))
|
PORT = 8000
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# NSFW detection configuration
|
# NSFW detection configuration
|
||||||
|
|||||||
83
config_custom.py
Normal file
83
config_custom.py
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
|
# Configuration constants for the SWIPER application
|
||||||
|
# --------------------------------------------------
|
||||||
|
# Centralising these values avoids circular imports
|
||||||
|
# and makes it easy to update paths / ports later
|
||||||
|
|
||||||
|
# Base directory of the repo (this file lives in the project root)
|
||||||
|
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
|
||||||
|
# Paths to the folders that contain source images. Can be overridden at runtime
|
||||||
|
# via the IMAGE_DIRS environment variable (colon-separated paths).
|
||||||
|
_IMAGE_DIRS_DEFAULT = [
|
||||||
|
"/media/Portrait",
|
||||||
|
"/media/Landscape",
|
||||||
|
]
|
||||||
|
|
||||||
|
_IMAGE_DIRS_ENV = os.getenv("IMAGE_DIRS")
|
||||||
|
if _IMAGE_DIRS_ENV:
|
||||||
|
IMAGE_DIRS = _IMAGE_DIRS_ENV.split(":")
|
||||||
|
else:
|
||||||
|
IMAGE_DIRS = _IMAGE_DIRS_DEFAULT
|
||||||
|
|
||||||
|
# Backwards-compatibility: first directory
|
||||||
|
IMAGE_DIR = IMAGE_DIRS[0] if IMAGE_DIRS else ""
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
# Data directory (override with DATA_DIR env var)
|
||||||
|
DATA_DIR = os.getenv("DATA_DIR", BASE_DIR)
|
||||||
|
|
||||||
|
# SQLite database file that stores selections & metadata
|
||||||
|
DB_PATH = os.path.join(DATA_DIR, "image_selections.db")
|
||||||
|
|
||||||
|
# Default port for the HTTP server (override with PORT env var)
|
||||||
|
PORT = int(os.getenv("PORT", 8888))
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# NSFW detection configuration
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# List of keywords that, if present in an image's prompt data, should mark the
|
||||||
|
# image as NSFW. Feel free to customise this list as appropriate for your own
|
||||||
|
# needs.
|
||||||
|
NSFW_KEYWORDS = [
|
||||||
|
"nude",
|
||||||
|
"nudity",
|
||||||
|
"porn",
|
||||||
|
"explicit",
|
||||||
|
"sexual",
|
||||||
|
"sex",
|
||||||
|
"boobs",
|
||||||
|
"nipples",
|
||||||
|
"penis",
|
||||||
|
"vagina",
|
||||||
|
"pussy",
|
||||||
|
"cum",
|
||||||
|
"fellatio",
|
||||||
|
"blowjob",
|
||||||
|
"cunnilingus",
|
||||||
|
"paizuri",
|
||||||
|
"rape",
|
||||||
|
"handjob",
|
||||||
|
"lingerie",
|
||||||
|
"bikini",
|
||||||
|
"latex",
|
||||||
|
"saliva",
|
||||||
|
"ass",
|
||||||
|
"condom",
|
||||||
|
]
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Utility helpers
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
def find_image_file(rel_path: str) -> Optional[str]:
|
||||||
|
"""Return absolute path to `rel_path` by searching all IMAGE_DIRS.
|
||||||
|
|
||||||
|
Returns None if file is not found in any configured directory.
|
||||||
|
"""
|
||||||
|
for base in IMAGE_DIRS:
|
||||||
|
abs_path = os.path.join(base, rel_path)
|
||||||
|
if os.path.exists(abs_path):
|
||||||
|
return abs_path
|
||||||
|
return None
|
||||||
@@ -30,4 +30,4 @@ services:
|
|||||||
- ./data:/data
|
- ./data:/data
|
||||||
# Uncomment the line below if you maintain a custom config.py alongside
|
# Uncomment the line below if you maintain a custom config.py alongside
|
||||||
# the compose file and want to override the image copy at runtime.
|
# the compose file and want to override the image copy at runtime.
|
||||||
- ./config.py:/app/config.py:ro
|
- ./config_custom.py:/app/config.py:ro
|
||||||
|
|||||||
64
handler.py
64
handler.py
@@ -65,6 +65,8 @@ class ImageSwipeHandler(BaseHTTPRequestHandler):
|
|||||||
self.serve_random_image()
|
self.serve_random_image()
|
||||||
elif path == "/selections":
|
elif path == "/selections":
|
||||||
self.serve_selections()
|
self.serve_selections()
|
||||||
|
elif path == "/image-count":
|
||||||
|
self.serve_image_count()
|
||||||
elif path.startswith("/images/"):
|
elif path.startswith("/images/"):
|
||||||
self.serve_image(path[8:])
|
self.serve_image(path[8:])
|
||||||
elif path == "/favicon.ico":
|
elif path == "/favicon.ico":
|
||||||
@@ -249,6 +251,68 @@ class ImageSwipeHandler(BaseHTTPRequestHandler):
|
|||||||
data = {"selections": get_selections()}
|
data = {"selections": get_selections()}
|
||||||
self._json_response(data)
|
self._json_response(data)
|
||||||
|
|
||||||
|
def serve_image_count(self) -> None:
|
||||||
|
"""Return the total count of images available for the current filter."""
|
||||||
|
parsed = urllib.parse.urlparse(self.path)
|
||||||
|
query_params = urllib.parse.parse_qs(parsed.query)
|
||||||
|
orientation_str = query_params.get("orientation", ["all"])[0]
|
||||||
|
orientations = [o.strip() for o in orientation_str.split(',')]
|
||||||
|
search_keywords_str = query_params.get("search", [""])[0].strip()
|
||||||
|
allow_nsfw = query_params.get("allow_nsfw", ["0"])[0] == "1"
|
||||||
|
search_keywords = [kw.strip() for kw in search_keywords_str.split(',') if kw.strip()]
|
||||||
|
actions_str = query_params.get("actions", ["Unactioned"])[0]
|
||||||
|
actions = [a.strip() for a in actions_str.split(',') if a.strip()]
|
||||||
|
|
||||||
|
conn = sqlite3.connect(DB_PATH)
|
||||||
|
cur = conn.cursor()
|
||||||
|
query = """
|
||||||
|
SELECT COUNT(*) FROM image_metadata meta
|
||||||
|
LEFT JOIN prompt_details pd ON meta.path = pd.image_path
|
||||||
|
"""
|
||||||
|
params: List[str] = []
|
||||||
|
where_clauses = ["(meta.actioned IS NULL OR meta.actioned != 'purged')"]
|
||||||
|
|
||||||
|
# Action filter
|
||||||
|
action_conditions = []
|
||||||
|
action_params = []
|
||||||
|
if "Unactioned" in actions:
|
||||||
|
action_conditions.append("meta.actioned IS NULL")
|
||||||
|
actions.remove("Unactioned")
|
||||||
|
if actions:
|
||||||
|
placeholders = ", ".join("?" for _ in actions)
|
||||||
|
action_conditions.append(f"meta.actioned IN ({placeholders})")
|
||||||
|
action_params.extend(actions)
|
||||||
|
|
||||||
|
if action_conditions:
|
||||||
|
where_clauses.append(f"({' OR '.join(action_conditions)})")
|
||||||
|
params.extend(action_params)
|
||||||
|
|
||||||
|
# Orientation filter
|
||||||
|
if "all" not in orientations and orientations:
|
||||||
|
placeholders = ", ".join("?" for _ in orientations)
|
||||||
|
where_clauses.append(f"meta.orientation IN ({placeholders})")
|
||||||
|
params.extend(orientations)
|
||||||
|
|
||||||
|
# NSFW filter
|
||||||
|
if not allow_nsfw:
|
||||||
|
where_clauses.append("meta.nsfw = 0")
|
||||||
|
|
||||||
|
# Keyword filter
|
||||||
|
if search_keywords:
|
||||||
|
for keyword in search_keywords:
|
||||||
|
# Search only the positive prompt (pd.positive_prompt)
|
||||||
|
where_clauses.append("pd.positive_prompt LIKE ?")
|
||||||
|
params.append(f"%{keyword}%")
|
||||||
|
|
||||||
|
if where_clauses:
|
||||||
|
query += " WHERE " + " AND ".join(where_clauses)
|
||||||
|
|
||||||
|
cur.execute(query, params)
|
||||||
|
count = cur.fetchone()[0]
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
self._json_response({"count": count})
|
||||||
|
|
||||||
def serve_resolutions(self) -> None:
|
def serve_resolutions(self) -> None:
|
||||||
# Collect resolutions across all configured directories
|
# Collect resolutions across all configured directories
|
||||||
resolutions_set = set()
|
resolutions_set = set()
|
||||||
|
|||||||
@@ -165,6 +165,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="js/history.js" type="module"></script>
|
<script src="js/enhanced-history.js" type="module"></script>
|
||||||
|
<script src="js/ui-enhancements.js" type="module"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -114,6 +114,7 @@
|
|||||||
<img src="static/icons/fullscreen.svg" class="btn-icon" alt="Fullscreen">
|
<img src="static/icons/fullscreen.svg" class="btn-icon" alt="Fullscreen">
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<script src="js/main.js" type="module"></script>
|
<script src="js/enhanced-main.js" type="module"></script>
|
||||||
|
<script src="js/ui-enhancements.js" type="module"></script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
const selectionGrid = document.getElementById('selection-grid');
|
const selectionGrid = document.getElementById('selection-grid');
|
||||||
const filterButtons = document.querySelectorAll('.filter-buttons .filter-btn');
|
const filterButtons = document.querySelectorAll('.filter-buttons .filter-btn');
|
||||||
const orientationButtons = document.querySelectorAll('.orientation-filters .filter-btn');
|
const orientationButtons = document.querySelectorAll('.orientation-filters .filter-btn');
|
||||||
const resolutionFilter = document.getElementById('resolution-filter');
|
const resolutionFilter = document.getElementById('resolution-select');
|
||||||
const selectAllBtn = document.getElementById('select-all');
|
const selectAllBtn = document.getElementById('select-all');
|
||||||
const deselectAllBtn = document.getElementById('deselect-all');
|
const deselectAllBtn = document.getElementById('deselect-all');
|
||||||
const downloadSelectedBtn = document.getElementById('download-selected');
|
const downloadSelectedBtn = document.getElementById('download-selected');
|
||||||
@@ -176,7 +176,9 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
<p>No selections match the current filters</p>
|
<p>No selections match the current filters</p>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
filteredCountEl.textContent = `0 images match your filters (out of ${selections.length} total)`;
|
if (filteredCountEl) {
|
||||||
|
filteredCountEl.textContent = `0 images match your filters (out of ${selections.length} total)`;
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -259,8 +261,19 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const updateDownloadButton = () => {
|
const updateDownloadButton = () => {
|
||||||
|
if (!downloadSelectedBtn) return;
|
||||||
|
|
||||||
downloadSelectedBtn.disabled = selectedItems.length === 0;
|
downloadSelectedBtn.disabled = selectedItems.length === 0;
|
||||||
downloadSelectedBtn.querySelector('.label').textContent = selectedItems.length > 0 ? `Download (${selectedItems.length})` : 'Download';
|
const labelEl = downloadSelectedBtn.querySelector('.label');
|
||||||
|
if (labelEl) {
|
||||||
|
labelEl.textContent = selectedItems.length > 0 ? `Download (${selectedItems.length})` : 'Download';
|
||||||
|
} else {
|
||||||
|
// If there's no label element, update the count element instead
|
||||||
|
const countEl = downloadSelectedBtn.querySelector('.count');
|
||||||
|
if (countEl) {
|
||||||
|
countEl.textContent = selectedItems.length > 0 ? selectedItems.length : '0';
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// Enhanced selection item click handler
|
// Enhanced selection item click handler
|
||||||
@@ -379,34 +392,53 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
confirmDialog.remove();
|
confirmDialog.remove();
|
||||||
}, 400);
|
}, 400);
|
||||||
|
|
||||||
// Simulate delete functionality (replace with actual API call)
|
// Delete the selection via API
|
||||||
setTimeout(() => {
|
fetch('/delete-selection', {
|
||||||
selectionItem.remove();
|
method: 'POST',
|
||||||
showToast('Selection removed');
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ id: selectionId })
|
||||||
// Update selected items if this was selected
|
})
|
||||||
if (selectedItems.some(item => item.id === selectionId)) {
|
.then(response => {
|
||||||
selectedItems = selectedItems.filter(item => item.id !== selectionId);
|
if (!response.ok) {
|
||||||
updateDownloadButton();
|
throw new Error('Failed to delete selection');
|
||||||
}
|
}
|
||||||
|
return response.json();
|
||||||
// Update allSelections array
|
})
|
||||||
allSelections = allSelections.filter(s => s.id !== selectionId);
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
// Update stats
|
selectionItem.remove();
|
||||||
const stats = calculateStats(allSelections);
|
showToast('Selection removed');
|
||||||
updateStats(stats);
|
|
||||||
|
// Update selected items if this was selected
|
||||||
// Update filtered count
|
if (selectedItems.some(item => item.id === selectionId)) {
|
||||||
if (filteredCountEl) {
|
selectedItems = selectedItems.filter(item => item.id !== selectionId);
|
||||||
const filteredSelections = allSelections.filter(s =>
|
updateDownloadButton();
|
||||||
(currentFilter === 'all' || s.action === currentFilter) &&
|
}
|
||||||
(currentOrientation === 'all' || s.orientation === currentOrientation) &&
|
|
||||||
(currentResolution === 'all' || s.resolution === currentResolution)
|
// Update allSelections array
|
||||||
);
|
allSelections = allSelections.filter(s => s.id !== selectionId);
|
||||||
filteredCountEl.textContent = `Showing ${filteredSelections.length} of ${allSelections.length} images`;
|
|
||||||
|
// Update stats
|
||||||
|
const stats = calculateStats(allSelections);
|
||||||
|
updateStats(stats);
|
||||||
|
|
||||||
|
// Update filtered count
|
||||||
|
if (filteredCountEl) {
|
||||||
|
const filteredSelections = allSelections.filter(s =>
|
||||||
|
(currentFilter === 'all' || s.action === currentFilter) &&
|
||||||
|
(currentOrientation === 'all' || s.orientation === currentOrientation) &&
|
||||||
|
(currentResolution === 'all' || s.resolution === currentResolution)
|
||||||
|
);
|
||||||
|
filteredCountEl.textContent = `Showing ${filteredSelections.length} of ${allSelections.length} images`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
showToast('Error removing selection', 'error');
|
||||||
}
|
}
|
||||||
}, 300);
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error deleting selection:', error);
|
||||||
|
showToast('Error removing selection', 'error');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
confirmDialog.querySelector('.cancel-delete-btn').addEventListener('click', () => {
|
confirmDialog.querySelector('.cancel-delete-btn').addEventListener('click', () => {
|
||||||
@@ -523,44 +555,65 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
modalMessage.textContent = `Updating action to ${getActionName(action)}...`;
|
modalMessage.textContent = `Updating action to ${getActionName(action)}...`;
|
||||||
modalMessage.style.color = '#3498db';
|
modalMessage.style.color = '#3498db';
|
||||||
|
|
||||||
// Simulate API call (replace with actual implementation)
|
// Update the selection via API
|
||||||
setTimeout(() => {
|
fetch('/update-selection', {
|
||||||
modalMessage.textContent = `Action updated successfully!`;
|
method: 'POST',
|
||||||
modalMessage.style.color = '#2ecc71';
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ id: currentSelectionId, action })
|
||||||
// Close modal after success
|
})
|
||||||
setTimeout(() => {
|
.then(response => {
|
||||||
actionModal.classList.remove('show');
|
if (!response.ok) {
|
||||||
setTimeout(() => {
|
throw new Error('Failed to update selection');
|
||||||
actionModal.style.display = 'none';
|
}
|
||||||
modalMessage.textContent = '';
|
return response.json();
|
||||||
}, 400);
|
})
|
||||||
|
.then(data => {
|
||||||
|
if (data.success) {
|
||||||
|
modalMessage.textContent = `Action updated successfully!`;
|
||||||
|
modalMessage.style.color = '#2ecc71';
|
||||||
|
|
||||||
// Update the UI to reflect the change
|
// Close modal after success
|
||||||
const selectionItem = document.querySelector(`.selection-item[data-id="${currentSelectionId}"]`);
|
setTimeout(() => {
|
||||||
if (selectionItem) {
|
actionModal.classList.remove('show');
|
||||||
const actionEl = selectionItem.querySelector('.selection-action');
|
setTimeout(() => {
|
||||||
const oldAction = actionEl.classList[1].replace('action-', '');
|
actionModal.style.display = 'none';
|
||||||
|
modalMessage.textContent = '';
|
||||||
|
}, 400);
|
||||||
|
|
||||||
// Update the action element
|
// Update the UI to reflect the change
|
||||||
actionEl.className = `selection-action action-${action}`;
|
const selectionItem = document.querySelector(`.selection-item[data-id="${currentSelectionId}"]`);
|
||||||
actionEl.innerHTML = `<i class="fa-solid ${getActionIcon(action)}"></i> ${getActionName(action)}`;
|
if (selectionItem) {
|
||||||
|
const actionEl = selectionItem.querySelector('.selection-action');
|
||||||
// Update the selection in allSelections
|
const oldAction = actionEl.classList[1].replace('action-', '');
|
||||||
const selectionIndex = allSelections.findIndex(s => s.id === currentSelectionId);
|
|
||||||
if (selectionIndex !== -1) {
|
// Update the action element
|
||||||
allSelections[selectionIndex].action = action;
|
actionEl.className = `selection-action action-${action}`;
|
||||||
|
actionEl.innerHTML = `<i class="fa-solid ${getActionIcon(action)}"></i> ${getActionName(action)}`;
|
||||||
|
|
||||||
|
// Update the selection in allSelections
|
||||||
|
const selectionIndex = allSelections.findIndex(s => s.id === currentSelectionId);
|
||||||
|
if (selectionIndex !== -1) {
|
||||||
|
allSelections[selectionIndex].action = action;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update stats
|
||||||
|
const stats = calculateStats(allSelections);
|
||||||
|
updateStats(stats);
|
||||||
|
|
||||||
|
// Show toast notification
|
||||||
|
showToast(`Updated to ${getActionName(action)}`);
|
||||||
}
|
}
|
||||||
|
}, 1000);
|
||||||
// Update stats
|
} else {
|
||||||
const stats = calculateStats(allSelections);
|
modalMessage.textContent = `Error updating action`;
|
||||||
updateStats(stats);
|
modalMessage.style.color = '#e74c3c';
|
||||||
|
}
|
||||||
// Show toast notification
|
})
|
||||||
showToast(`Updated to ${getActionName(action)}`);
|
.catch(error => {
|
||||||
}
|
console.error('Error updating selection:', error);
|
||||||
}, 1000);
|
modalMessage.textContent = `Error: ${error.message}`;
|
||||||
}, 800);
|
modalMessage.style.color = '#e74c3c';
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -579,40 +632,64 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
resetMessage.textContent = 'Deleting all selections...';
|
resetMessage.textContent = 'Deleting all selections...';
|
||||||
resetMessage.style.color = '#3498db';
|
resetMessage.style.color = '#3498db';
|
||||||
|
|
||||||
// Simulate API call (replace with actual implementation)
|
// Reset database via API
|
||||||
setTimeout(() => {
|
fetch('/reset-database', {
|
||||||
resetMessage.textContent = 'All selections have been deleted successfully!';
|
method: 'POST',
|
||||||
resetMessage.style.color = '#2ecc71';
|
headers: { 'Content-Type': 'application/json' }
|
||||||
|
})
|
||||||
// Close modal after success
|
.then(response => {
|
||||||
setTimeout(() => {
|
if (!response.ok) {
|
||||||
resetModal.classList.remove('show');
|
throw new Error('Failed to reset database');
|
||||||
|
}
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
if (data.status === 'success') {
|
||||||
|
resetMessage.textContent = 'All selections have been deleted successfully!';
|
||||||
|
resetMessage.style.color = '#2ecc71';
|
||||||
|
|
||||||
|
// Close modal after success
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
resetModal.style.display = 'none';
|
resetModal.classList.remove('show');
|
||||||
confirmResetBtn.disabled = false;
|
setTimeout(() => {
|
||||||
confirmResetBtn.textContent = 'Yes, Delete All';
|
resetModal.style.display = 'none';
|
||||||
resetMessage.textContent = '';
|
confirmResetBtn.disabled = false;
|
||||||
|
confirmResetBtn.textContent = 'Yes, Delete All';
|
||||||
// Clear the grid and update state
|
resetMessage.textContent = '';
|
||||||
selectionGrid.innerHTML = `
|
|
||||||
<div class="no-selections">
|
// Clear the grid and update state
|
||||||
<i class="fa-solid fa-image-slash fa-3x"></i>
|
selectionGrid.innerHTML = `
|
||||||
<p>No selections found</p>
|
<div class="no-selections">
|
||||||
</div>
|
<i class="fa-solid fa-image-slash fa-3x"></i>
|
||||||
`;
|
<p>No selections found</p>
|
||||||
selectedItems = [];
|
</div>
|
||||||
allSelections = [];
|
`;
|
||||||
updateDownloadButton();
|
selectedItems = [];
|
||||||
|
allSelections = [];
|
||||||
// Update stats
|
updateDownloadButton();
|
||||||
const stats = calculateStats([]);
|
|
||||||
updateStats(stats);
|
// Update stats
|
||||||
|
const stats = calculateStats([]);
|
||||||
// Show toast notification
|
updateStats(stats);
|
||||||
showToast('All selections have been deleted');
|
|
||||||
}, 400);
|
// Show toast notification
|
||||||
}, 1000);
|
showToast('All selections have been deleted');
|
||||||
}, 1500);
|
}, 400);
|
||||||
|
}, 1000);
|
||||||
|
} else {
|
||||||
|
resetMessage.textContent = 'Error resetting database';
|
||||||
|
resetMessage.style.color = '#e74c3c';
|
||||||
|
confirmResetBtn.disabled = false;
|
||||||
|
confirmResetBtn.textContent = 'Yes, Delete All';
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error resetting database:', error);
|
||||||
|
resetMessage.textContent = `Error: ${error.message}`;
|
||||||
|
resetMessage.style.color = '#e74c3c';
|
||||||
|
confirmResetBtn.disabled = false;
|
||||||
|
confirmResetBtn.textContent = 'Yes, Delete All';
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
cancelResetBtn.addEventListener('click', () => {
|
cancelResetBtn.addEventListener('click', () => {
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function updateProgressBar() {
|
function updateProgressBar() {
|
||||||
if (progressState.totalImages > 0) {
|
if (progressBar && progressState.totalImages > 0) {
|
||||||
const percentage = (progressState.processedImages / progressState.totalImages) * 100;
|
const percentage = (progressState.processedImages / progressState.totalImages) * 100;
|
||||||
progressBar.style.width = `${Math.min(percentage, 100)}%`;
|
progressBar.style.width = `${Math.min(percentage, 100)}%`;
|
||||||
}
|
}
|
||||||
|
|||||||
449
js/main.js
449
js/main.js
@@ -16,6 +16,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
moveY: 0,
|
moveY: 0,
|
||||||
touchStartTime: 0,
|
touchStartTime: 0,
|
||||||
hasMoved: false,
|
hasMoved: false,
|
||||||
|
isAnimating: false, // Flag to prevent actions during animation
|
||||||
};
|
};
|
||||||
|
|
||||||
const card = document.getElementById('current-card');
|
const card = document.getElementById('current-card');
|
||||||
@@ -31,11 +32,30 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
const keywordPillsContainer = document.getElementById('keyword-pills-container');
|
const keywordPillsContainer = document.getElementById('keyword-pills-container');
|
||||||
|
|
||||||
const SWIPE_THRESHOLD = 100;
|
const SWIPE_THRESHOLD = 100;
|
||||||
|
const ACTION_ANGLE_THRESHOLD = 15; // Degrees of rotation to commit to swipe
|
||||||
|
|
||||||
|
function resetCardPosition(animated = true) {
|
||||||
|
if (state.isAnimating && animated) return;
|
||||||
|
|
||||||
|
card.style.transition = animated ? 'transform 0.4s cubic-bezier(0.25, 0.8, 0.25, 1)' : 'none';
|
||||||
|
card.style.transform = 'translate(0, 0) rotate(0deg)';
|
||||||
|
|
||||||
|
if (animated) {
|
||||||
|
state.isAnimating = true;
|
||||||
|
card.addEventListener('transitionend', () => {
|
||||||
|
state.isAnimating = false;
|
||||||
|
card.style.transition = 'none';
|
||||||
|
}, { once: true });
|
||||||
|
} else {
|
||||||
|
card.style.transition = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const performSwipe = (direction) => {
|
const performSwipe = (direction) => {
|
||||||
if (!state.currentImageInfo) return;
|
if (!state.currentImageInfo || state.isAnimating) return;
|
||||||
|
|
||||||
card.classList.add(`swipe-${direction}`);
|
state.isAnimating = true;
|
||||||
|
|
||||||
const actionNameMap = { left: 'Discarded', right: 'Kept', up: 'Favourited', down: 'Reviewing' };
|
const actionNameMap = { left: 'Discarded', right: 'Kept', up: 'Favourited', down: 'Reviewing' };
|
||||||
const actionName = actionNameMap[direction] || direction;
|
const actionName = actionNameMap[direction] || direction;
|
||||||
lastActionText.textContent = `Last action: ${actionName}`;
|
lastActionText.textContent = `Last action: ${actionName}`;
|
||||||
@@ -44,310 +64,247 @@ document.addEventListener('DOMContentLoaded', () => {
|
|||||||
|
|
||||||
recordSelection(state.currentImageInfo, actionName);
|
recordSelection(state.currentImageInfo, actionName);
|
||||||
|
|
||||||
setTimeout(() => {
|
// Animate swipe out
|
||||||
card.classList.remove(`swipe-${direction}`);
|
let rotation, translateX, translateY;
|
||||||
|
const windowWidth = window.innerWidth;
|
||||||
|
const windowHeight = window.innerHeight;
|
||||||
|
|
||||||
|
switch (direction) {
|
||||||
|
case 'left':
|
||||||
|
rotation = -30;
|
||||||
|
translateX = -windowWidth;
|
||||||
|
translateY = state.moveY;
|
||||||
|
break;
|
||||||
|
case 'right':
|
||||||
|
rotation = 30;
|
||||||
|
translateX = windowWidth;
|
||||||
|
translateY = state.moveY;
|
||||||
|
break;
|
||||||
|
case 'up':
|
||||||
|
rotation = 0;
|
||||||
|
translateX = state.moveX;
|
||||||
|
translateY = -windowHeight;
|
||||||
|
break;
|
||||||
|
case 'down':
|
||||||
|
rotation = 0;
|
||||||
|
translateX = state.moveX;
|
||||||
|
translateY = windowHeight;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
card.style.transition = 'transform 0.5s cubic-bezier(0.6, -0.28, 0.735, 0.045)';
|
||||||
|
card.style.transform = `translate(${translateX}px, ${translateY}px) rotate(${rotation}deg)`;
|
||||||
|
|
||||||
|
card.addEventListener('transitionend', () => {
|
||||||
loadNewImage();
|
loadNewImage();
|
||||||
}, 500);
|
state.isAnimating = false;
|
||||||
|
}, { once: true });
|
||||||
};
|
};
|
||||||
|
|
||||||
const recordSelection = async (imageInfo, action) => {
|
async function getNextImage() {
|
||||||
try {
|
const queryParams = new URLSearchParams({
|
||||||
const response = await fetch('/selection', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({
|
|
||||||
image_path: imageInfo.path,
|
|
||||||
action,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
if (!response.ok) {
|
|
||||||
console.error('Error recording selection. Status:', response.status);
|
|
||||||
} else {
|
|
||||||
const data = await response.json();
|
|
||||||
console.log('Selection recorded:', data);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error recording selection:', err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const loadNewImage = () => {
|
|
||||||
if (state.isLoading) return;
|
|
||||||
state.isLoading = true;
|
|
||||||
card.classList.add('loading');
|
|
||||||
|
|
||||||
const params = new URLSearchParams({
|
|
||||||
orientation: state.currentOrientation.join(','),
|
orientation: state.currentOrientation.join(','),
|
||||||
t: new Date().getTime(),
|
actions: state.currentActions.join(','),
|
||||||
});
|
nsfw: state.allowNsfw,
|
||||||
|
keywords: state.searchKeywords.join(','),
|
||||||
|
}).toString();
|
||||||
|
|
||||||
// NSFW param
|
const response = await fetch(`/next-image?${queryParams}`);
|
||||||
params.append('allow_nsfw', state.allowNsfw ? '1' : '0');
|
if (!response.ok) {
|
||||||
|
throw new Error(`HTTP error! status: ${response.status}`);
|
||||||
if (state.searchKeywords.length > 0) {
|
|
||||||
params.append('search', state.searchKeywords.join(','));
|
|
||||||
}
|
}
|
||||||
|
return await response.json();
|
||||||
|
}
|
||||||
|
|
||||||
if (state.currentActions.length > 0) {
|
function recordSelection(imageInfo, action) {
|
||||||
params.append('actions', state.currentActions.join(','));
|
fetch('/record-selection', {
|
||||||
}
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ filename: imageInfo.filename, action: action }),
|
||||||
|
}).catch(error => console.error('Failed to record selection:', error));
|
||||||
|
}
|
||||||
|
|
||||||
fetch(`/random-image?${params.toString()}`)
|
function handleNoImageAvailable() {
|
||||||
.then(response => response.json())
|
const imageElement = card.querySelector('img');
|
||||||
.then(data => {
|
imageElement.src = `data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D%22400%22%20height%3D%22400%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Crect%20width%3D%22400%22%20height%3D%22400%22%20fill%3D%22%23e0e0e0%22%2F%3E%3Ctext%20x%3D%22200%22%20y%3D%22200%22%20font-size%3D%2220%22%20text-anchor%3D%22middle%22%20alignment-baseline%3D%22middle%22%20fill%3D%22%23999%22%3ENo%20images%20found%3C%2Ftext%3E%3C%2Fsvg%3E`;
|
||||||
state.isLoading = false;
|
state.currentImageInfo = null;
|
||||||
// card.classList.remove('loading'); // moved to image load handler
|
updateImageInfo(null);
|
||||||
if (data && data.path) {
|
showToast('No more images matching your criteria.', 'info');
|
||||||
state.currentImageInfo = data;
|
}
|
||||||
const cardImage = card.querySelector('img');
|
|
||||||
// Use load event to ensure indicator hides after image fully loads
|
|
||||||
cardImage.onload = () => {
|
|
||||||
card.classList.remove('loading');
|
|
||||||
};
|
|
||||||
cardImage.src = data.path;
|
|
||||||
updateImageInfo(data);
|
|
||||||
adjustContainerToImage(data.orientation);
|
|
||||||
} else {
|
|
||||||
const placeholder = 'static/no-image.png';
|
|
||||||
const imgEl = card.querySelector('img');
|
|
||||||
if (imgEl) {
|
|
||||||
imgEl.onload = () => card.classList.remove('loading');
|
|
||||||
imgEl.src = placeholder;
|
|
||||||
}
|
|
||||||
updateImageInfo({ filename:'No image', creation_date:'', resolution:'', prompt_data:''});
|
|
||||||
state.currentImageInfo = null; // disables swipe actions
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error('Error fetching image:', error);
|
|
||||||
state.isLoading = false;
|
|
||||||
card.classList.remove('loading');
|
|
||||||
card.innerHTML = '<div class="no-images-message">Error loading image.</div>';
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const adjustContainerToImage = (orientation) => {
|
async function loadNewImage() {
|
||||||
const container = document.querySelector('.swipe-container');
|
if (state.isLoading) return;
|
||||||
if (window.innerWidth < 992) { // Only on desktop
|
resetCardPosition(false); // Reset position without animation for new image
|
||||||
container.style.transition = 'all 0.5s ease-in-out';
|
state.isLoading = true;
|
||||||
if (orientation === 'landscape') {
|
const loadingIndicator = card.querySelector('.loading-indicator');
|
||||||
container.style.flex = '4';
|
loadingIndicator.style.display = 'block';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const data = await getNextImage();
|
||||||
|
if (data && data.image_data) {
|
||||||
|
const imageUrl = `data:image/jpeg;base64,${data.image_data}`;
|
||||||
|
const imageElement = card.querySelector('img');
|
||||||
|
imageElement.src = imageUrl;
|
||||||
|
state.currentImageInfo = data;
|
||||||
|
updateImageInfo(data);
|
||||||
} else {
|
} else {
|
||||||
container.style.flex = '2';
|
handleNoImageAvailable();
|
||||||
}
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error loading image:', error);
|
||||||
|
showToast('Failed to load image.', 'error');
|
||||||
|
handleNoImageAvailable();
|
||||||
|
} finally {
|
||||||
|
state.isLoading = false;
|
||||||
|
loadingIndicator.style.display = 'none';
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
const handlePointerDown = (x, y) => {
|
// --- Event Listeners ---
|
||||||
|
|
||||||
|
// Swipe and Drag
|
||||||
|
card.addEventListener('mousedown', onDragStart);
|
||||||
|
card.addEventListener('touchstart', onDragStart, { passive: true });
|
||||||
|
|
||||||
|
function onDragStart(e) {
|
||||||
|
if (state.isLoading || state.isAnimating) return;
|
||||||
state.isDragging = true;
|
state.isDragging = true;
|
||||||
state.startX = x;
|
|
||||||
state.startY = y;
|
|
||||||
state.hasMoved = false;
|
state.hasMoved = false;
|
||||||
|
card.style.transition = 'none';
|
||||||
|
state.startX = e.clientX || e.touches[0].clientX;
|
||||||
|
state.startY = e.clientY || e.touches[0].clientY;
|
||||||
state.touchStartTime = Date.now();
|
state.touchStartTime = Date.now();
|
||||||
card.classList.add('swiping');
|
document.addEventListener('mousemove', onDragMove);
|
||||||
};
|
document.addEventListener('touchmove', onDragMove, { passive: false });
|
||||||
|
document.addEventListener('mouseup', onDragEnd);
|
||||||
|
document.addEventListener('touchend', onDragEnd);
|
||||||
|
}
|
||||||
|
|
||||||
const handlePointerMove = (x, y) => {
|
function onDragMove(e) {
|
||||||
if (!state.isDragging) return;
|
if (!state.isDragging) return;
|
||||||
|
e.preventDefault();
|
||||||
|
state.hasMoved = true;
|
||||||
|
|
||||||
|
const currentX = e.clientX || e.touches[0].clientX;
|
||||||
|
const currentY = e.clientY || e.touches[0].clientY;
|
||||||
|
|
||||||
|
state.moveX = currentX - state.startX;
|
||||||
|
state.moveY = currentY - state.startY;
|
||||||
|
|
||||||
state.moveX = x - state.startX;
|
const rotation = state.moveX * 0.1;
|
||||||
state.moveY = y - state.startY;
|
card.style.transform = `translate(${state.moveX}px, ${state.moveY}px) rotate(${rotation}deg)`;
|
||||||
|
}
|
||||||
|
|
||||||
if (Math.abs(state.moveX) > 10 || Math.abs(state.moveY) > 10) {
|
function onDragEnd() {
|
||||||
state.hasMoved = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
card.style.transform = `translate(${state.moveX}px, ${state.moveY}px) rotate(${state.moveX * 0.05}deg)`;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePointerUp = () => {
|
|
||||||
if (!state.isDragging) return;
|
if (!state.isDragging) return;
|
||||||
state.isDragging = false;
|
state.isDragging = false;
|
||||||
card.classList.remove('swiping');
|
|
||||||
|
|
||||||
const absX = Math.abs(state.moveX);
|
document.removeEventListener('mousemove', onDragMove);
|
||||||
const absY = Math.abs(state.moveY);
|
document.removeEventListener('touchmove', onDragMove);
|
||||||
|
document.removeEventListener('mouseup', onDragEnd);
|
||||||
|
document.removeEventListener('touchend', onDragEnd);
|
||||||
|
|
||||||
if (state.hasMoved && (absX > SWIPE_THRESHOLD || absY > SWIPE_THRESHOLD)) {
|
if (!state.hasMoved) return;
|
||||||
if (absX > absY) {
|
|
||||||
performSwipe(state.moveX > 0 ? 'right' : 'left');
|
const rotation = state.moveX * 0.1;
|
||||||
} else {
|
|
||||||
performSwipe(state.moveY > 0 ? 'down' : 'up');
|
if (Math.abs(rotation) > ACTION_ANGLE_THRESHOLD || Math.abs(state.moveX) > SWIPE_THRESHOLD || Math.abs(state.moveY) > SWIPE_THRESHOLD) {
|
||||||
|
const angle = Math.atan2(state.moveY, state.moveX) * 180 / Math.PI;
|
||||||
|
let direction;
|
||||||
|
if (angle > -45 && angle <= 45) {
|
||||||
|
direction = 'right';
|
||||||
|
} else if (angle > 45 && angle <= 135) {
|
||||||
|
direction = 'down';
|
||||||
|
} else if (angle > 135 || angle <= -135) {
|
||||||
|
direction = 'left';
|
||||||
|
} else if (angle > -135 && angle <= -45) {
|
||||||
|
direction = 'up';
|
||||||
}
|
}
|
||||||
|
performSwipe(direction);
|
||||||
} else {
|
} else {
|
||||||
card.style.transform = '';
|
resetCardPosition();
|
||||||
}
|
}
|
||||||
|
|
||||||
state.moveX = 0;
|
state.moveX = 0;
|
||||||
state.moveY = 0;
|
state.moveY = 0;
|
||||||
};
|
}
|
||||||
|
|
||||||
card.addEventListener('mousedown', e => handlePointerDown(e.clientX, e.clientY));
|
|
||||||
document.addEventListener('mousemove', e => handlePointerMove(e.clientX, e.clientY));
|
|
||||||
document.addEventListener('mouseup', () => handlePointerUp());
|
|
||||||
|
|
||||||
card.addEventListener('touchstart', e => handlePointerDown(e.touches[0].clientX, e.touches[0].clientY), { passive: true });
|
|
||||||
card.addEventListener('touchmove', e => handlePointerMove(e.touches[0].clientX, e.touches[0].clientY), { passive: true });
|
|
||||||
card.addEventListener('touchend', () => handlePointerUp());
|
|
||||||
|
|
||||||
document.getElementById('btn-left').addEventListener('click', () => performSwipe('left'));
|
|
||||||
document.getElementById('btn-right').addEventListener('click', () => performSwipe('right'));
|
|
||||||
document.getElementById('btn-up').addEventListener('click', () => performSwipe('up'));
|
|
||||||
document.getElementById('btn-down').addEventListener('click', () => performSwipe('down'));
|
|
||||||
|
|
||||||
document.addEventListener('keydown', (e) => {
|
|
||||||
if (state.isLoading || document.activeElement === searchInput) return;
|
|
||||||
|
|
||||||
const keyMap = {
|
|
||||||
ArrowLeft: 'left',
|
|
||||||
ArrowRight: 'right',
|
|
||||||
ArrowUp: 'up',
|
|
||||||
ArrowDown: 'down',
|
|
||||||
};
|
|
||||||
|
|
||||||
if (keyMap[e.key]) {
|
|
||||||
e.preventDefault(); // Prevent scrolling
|
|
||||||
performSwipe(keyMap[e.key]);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
|
// Filter buttons
|
||||||
orientationFilters.addEventListener('click', (e) => {
|
orientationFilters.addEventListener('click', (e) => {
|
||||||
const button = e.target.closest('button');
|
const button = e.target.closest('button');
|
||||||
if (!button) return;
|
if (!button) return;
|
||||||
|
state.currentOrientation = [button.dataset.value];
|
||||||
const clickedOrientation = button.dataset.orientation;
|
|
||||||
|
|
||||||
if (clickedOrientation === 'all') {
|
|
||||||
state.currentOrientation = ['all'];
|
|
||||||
} else {
|
|
||||||
// If 'all' was the only active filter, start a new selection
|
|
||||||
if (state.currentOrientation.length === 1 && state.currentOrientation[0] === 'all') {
|
|
||||||
state.currentOrientation = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const index = state.currentOrientation.indexOf(clickedOrientation);
|
|
||||||
if (index > -1) {
|
|
||||||
// Already selected, so deselect
|
|
||||||
state.currentOrientation.splice(index, 1);
|
|
||||||
} else {
|
|
||||||
// Not selected, so select
|
|
||||||
state.currentOrientation.push(clickedOrientation);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If no filters are selected after interaction, default to 'all'
|
|
||||||
if (state.currentOrientation.length === 0) {
|
|
||||||
state.currentOrientation = ['all'];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update UI based on the state
|
|
||||||
orientationFilters.querySelectorAll('button').forEach(btn => {
|
|
||||||
if (state.currentOrientation.includes(btn.dataset.orientation)) {
|
|
||||||
btn.classList.add('active');
|
|
||||||
} else {
|
|
||||||
btn.classList.remove('active');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
loadNewImage();
|
loadNewImage();
|
||||||
});
|
});
|
||||||
|
|
||||||
actionFilters.addEventListener('click', (e) => {
|
actionFilters.addEventListener('click', (e) => {
|
||||||
const button = e.target.closest('button');
|
const button = e.target.closest('button');
|
||||||
if (!button) return;
|
if (!button) return;
|
||||||
|
state.currentActions = [button.dataset.value];
|
||||||
const clickedAction = button.dataset.action;
|
|
||||||
|
|
||||||
if (state.currentActions.length === 1 && state.currentActions[0] === 'Unactioned' && clickedAction !== 'Unactioned') {
|
|
||||||
state.currentActions = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const index = state.currentActions.indexOf(clickedAction);
|
|
||||||
if (index > -1) {
|
|
||||||
state.currentActions.splice(index, 1);
|
|
||||||
} else {
|
|
||||||
state.currentActions.push(clickedAction);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state.currentActions.length === 0) {
|
|
||||||
state.currentActions = ['Unactioned'];
|
|
||||||
}
|
|
||||||
|
|
||||||
actionFilters.querySelectorAll('button').forEach(btn => {
|
|
||||||
if (state.currentActions.includes(btn.dataset.action)) {
|
|
||||||
btn.classList.add('active');
|
|
||||||
} else {
|
|
||||||
btn.classList.remove('active');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
loadNewImage();
|
loadNewImage();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Modal
|
||||||
|
card.addEventListener('click', (e) => {
|
||||||
|
if (!state.hasMoved && state.currentImageInfo) {
|
||||||
|
fullscreenImage.src = card.querySelector('img').src;
|
||||||
|
modal.style.display = 'flex';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
closeModal.addEventListener('click', () => {
|
||||||
|
modal.style.display = 'none';
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener('click', (e) => {
|
||||||
|
if (e.target === modal) {
|
||||||
|
modal.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Search
|
||||||
|
const addKeyword = () => {
|
||||||
|
const keyword = searchInput.value.trim();
|
||||||
|
if (keyword && !state.searchKeywords.includes(keyword)) {
|
||||||
|
state.searchKeywords.push(keyword);
|
||||||
|
renderKeywordPills();
|
||||||
|
searchInput.value = '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeKeyword = (keywordToRemove) => {
|
||||||
|
state.searchKeywords = state.searchKeywords.filter(k => k !== keywordToRemove);
|
||||||
|
renderKeywordPills();
|
||||||
|
};
|
||||||
|
|
||||||
const renderKeywordPills = () => {
|
const renderKeywordPills = () => {
|
||||||
keywordPillsContainer.innerHTML = '';
|
keywordPillsContainer.innerHTML = '';
|
||||||
state.searchKeywords.forEach(keyword => {
|
state.searchKeywords.forEach(keyword => {
|
||||||
const pill = document.createElement('div');
|
const pill = document.createElement('div');
|
||||||
pill.className = 'keyword-pill';
|
pill.className = 'keyword-pill';
|
||||||
pill.textContent = keyword;
|
pill.textContent = keyword;
|
||||||
|
const removeBtn = document.createElement('span');
|
||||||
const removeBtn = document.createElement('button');
|
removeBtn.textContent = 'x';
|
||||||
removeBtn.className = 'remove-keyword';
|
removeBtn.onclick = () => removeKeyword(keyword);
|
||||||
removeBtn.innerHTML = '×';
|
|
||||||
removeBtn.dataset.keyword = keyword;
|
|
||||||
pill.appendChild(removeBtn);
|
pill.appendChild(removeBtn);
|
||||||
|
|
||||||
keywordPillsContainer.appendChild(pill);
|
keywordPillsContainer.appendChild(pill);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const addSearchKeyword = () => {
|
searchInput.addEventListener('keydown', (e) => {
|
||||||
const newKeyword = searchInput.value.trim();
|
|
||||||
if (newKeyword && !state.searchKeywords.includes(newKeyword)) {
|
|
||||||
state.searchKeywords.push(newKeyword);
|
|
||||||
renderKeywordPills();
|
|
||||||
loadNewImage();
|
|
||||||
}
|
|
||||||
searchInput.value = '';
|
|
||||||
searchInput.focus();
|
|
||||||
};
|
|
||||||
|
|
||||||
searchButton.addEventListener('click', addSearchKeyword);
|
|
||||||
|
|
||||||
searchInput.addEventListener('keypress', (e) => {
|
|
||||||
if (e.key === 'Enter') {
|
if (e.key === 'Enter') {
|
||||||
addSearchKeyword();
|
addKeyword();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
keywordPillsContainer.addEventListener('click', (e) => {
|
searchButton.addEventListener('click', () => {
|
||||||
if (e.target.classList.contains('remove-keyword')) {
|
loadNewImage();
|
||||||
const keywordToRemove = e.target.dataset.keyword;
|
|
||||||
state.searchKeywords = state.searchKeywords.filter(k => k !== keywordToRemove);
|
|
||||||
renderKeywordPills();
|
|
||||||
loadNewImage();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
card.addEventListener('click', () => {
|
|
||||||
if (!state.hasMoved && state.currentImageInfo) {
|
|
||||||
fullscreenImage.src = state.currentImageInfo.path;
|
|
||||||
document.getElementById('modal-resolution').textContent = `Resolution: ${state.currentImageInfo.resolution}`;
|
|
||||||
document.getElementById('modal-filename').textContent = `Filename: ${state.currentImageInfo.filename || 'N/A'}`;
|
|
||||||
document.getElementById('modal-creation-date').textContent = `Creation Date: ${state.currentImageInfo.creation_date || 'N/A'}`;
|
|
||||||
document.getElementById('modal-prompt-data').textContent = `Prompt: ${state.currentImageInfo.prompt_data || 'N/A'}`;
|
|
||||||
modal.style.display = 'flex';
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
closeModal.addEventListener('click', () => modal.style.display = 'none');
|
|
||||||
modal.addEventListener('click', (e) => {
|
|
||||||
if (e.target === modal) {
|
|
||||||
modal.style.display = 'none';
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Keyboard shortcuts
|
||||||
document.addEventListener('keydown', (e) => {
|
document.addEventListener('keydown', (e) => {
|
||||||
|
if (searchInput === document.activeElement) return;
|
||||||
|
|
||||||
if (modal.style.display === 'flex' && e.key === 'Escape') {
|
if (modal.style.display === 'flex' && e.key === 'Escape') {
|
||||||
modal.style.display = 'none';
|
modal.style.display = 'none';
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -7,6 +7,9 @@
|
|||||||
function addSwipeDecisionIndicators() {
|
function addSwipeDecisionIndicators() {
|
||||||
const swipeContainer = document.querySelector('.swipe-container');
|
const swipeContainer = document.querySelector('.swipe-container');
|
||||||
|
|
||||||
|
// Only proceed if swipe container exists (not on history page)
|
||||||
|
if (!swipeContainer) return;
|
||||||
|
|
||||||
// Create decision indicators for each direction
|
// Create decision indicators for each direction
|
||||||
const directions = ['left', 'right', 'up', 'down'];
|
const directions = ['left', 'right', 'up', 'down'];
|
||||||
const icons = ['fa-trash', 'fa-folder-plus', 'fa-star', 'fa-clock'];
|
const icons = ['fa-trash', 'fa-folder-plus', 'fa-star', 'fa-clock'];
|
||||||
@@ -34,6 +37,9 @@ function enhanceLoadingIndicator() {
|
|||||||
function enhanceDirectionArrows() {
|
function enhanceDirectionArrows() {
|
||||||
const arrows = document.querySelectorAll('.direction-arrow');
|
const arrows = document.querySelectorAll('.direction-arrow');
|
||||||
|
|
||||||
|
// Only proceed if direction arrows exist
|
||||||
|
if (arrows.length === 0) return;
|
||||||
|
|
||||||
arrows.forEach(arrow => {
|
arrows.forEach(arrow => {
|
||||||
arrow.addEventListener('mouseenter', function() {
|
arrow.addEventListener('mouseenter', function() {
|
||||||
this.style.transform = this.classList.contains('arrow-left') || this.classList.contains('arrow-right')
|
this.style.transform = this.classList.contains('arrow-left') || this.classList.contains('arrow-right')
|
||||||
@@ -54,13 +60,13 @@ function enhanceDirectionArrows() {
|
|||||||
// Show swipe decision indicator
|
// Show swipe decision indicator
|
||||||
function showSwipeDecision(direction) {
|
function showSwipeDecision(direction) {
|
||||||
const indicator = document.querySelector(`.decision-${direction}`);
|
const indicator = document.querySelector(`.decision-${direction}`);
|
||||||
if (indicator) {
|
if (!indicator) return;
|
||||||
indicator.classList.add('visible');
|
|
||||||
|
indicator.classList.add('visible');
|
||||||
setTimeout(() => {
|
|
||||||
indicator.classList.remove('visible');
|
setTimeout(() => {
|
||||||
}, 800);
|
indicator.classList.remove('visible');
|
||||||
}
|
}, 800);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enhance the performSwipe function
|
// Enhance the performSwipe function
|
||||||
@@ -68,6 +74,7 @@ function enhancePerformSwipe() {
|
|||||||
// Store the original performSwipe function
|
// Store the original performSwipe function
|
||||||
const originalPerformSwipe = window.performSwipe;
|
const originalPerformSwipe = window.performSwipe;
|
||||||
|
|
||||||
|
// Only proceed if performSwipe exists (main page only)
|
||||||
if (typeof originalPerformSwipe === 'function') {
|
if (typeof originalPerformSwipe === 'function') {
|
||||||
// Override with enhanced version
|
// Override with enhanced version
|
||||||
window.performSwipe = function(direction) {
|
window.performSwipe = function(direction) {
|
||||||
|
|||||||
36
js/utils.js
36
js/utils.js
@@ -1,11 +1,45 @@
|
|||||||
export function showToast(message) {
|
export function showToast(message, type = 'info') {
|
||||||
const toastEl = document.getElementById('toast');
|
const toastEl = document.getElementById('toast');
|
||||||
if (!toastEl) return;
|
if (!toastEl) return;
|
||||||
|
|
||||||
|
// Clear any existing classes
|
||||||
|
toastEl.className = 'toast';
|
||||||
|
|
||||||
|
// Add the appropriate class based on type
|
||||||
|
if (type === 'error') {
|
||||||
|
toastEl.classList.add('toast-error');
|
||||||
|
} else if (type === 'success') {
|
||||||
|
toastEl.classList.add('toast-success');
|
||||||
|
} else {
|
||||||
|
toastEl.classList.add('toast-info');
|
||||||
|
}
|
||||||
|
|
||||||
toastEl.textContent = message;
|
toastEl.textContent = message;
|
||||||
toastEl.classList.add('show');
|
toastEl.classList.add('show');
|
||||||
setTimeout(() => toastEl.classList.remove('show'), 3000);
|
setTimeout(() => toastEl.classList.remove('show'), 3000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function addRippleEffect(button) {
|
||||||
|
if (!button) return;
|
||||||
|
|
||||||
|
button.addEventListener('click', function(e) {
|
||||||
|
const ripple = document.createElement('span');
|
||||||
|
ripple.classList.add('ripple');
|
||||||
|
this.appendChild(ripple);
|
||||||
|
|
||||||
|
const rect = button.getBoundingClientRect();
|
||||||
|
const size = Math.max(rect.width, rect.height);
|
||||||
|
|
||||||
|
ripple.style.width = ripple.style.height = `${size}px`;
|
||||||
|
ripple.style.left = `${e.clientX - rect.left - size/2}px`;
|
||||||
|
ripple.style.top = `${e.clientY - rect.top - size/2}px`;
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
ripple.remove();
|
||||||
|
}, 600);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export function updateImageInfo(data) {
|
export function updateImageInfo(data) {
|
||||||
const resolutionEl = document.getElementById('image-resolution');
|
const resolutionEl = document.getElementById('image-resolution');
|
||||||
if (resolutionEl) {
|
if (resolutionEl) {
|
||||||
|
|||||||
30
styles.css
30
styles.css
@@ -579,6 +579,36 @@ html, body {
|
|||||||
transform: translateX(-50%) translateY(-10px);
|
transform: translateX(-50%) translateY(-10px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Toast types */
|
||||||
|
.toast-error {
|
||||||
|
background-color: rgba(231, 76, 60, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-success {
|
||||||
|
background-color: rgba(46, 204, 113, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-info {
|
||||||
|
background-color: rgba(52, 152, 219, 0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Ripple effect for buttons */
|
||||||
|
.ripple {
|
||||||
|
position: absolute;
|
||||||
|
background: rgba(255, 255, 255, 0.3);
|
||||||
|
border-radius: 50%;
|
||||||
|
transform: scale(0);
|
||||||
|
animation: ripple 0.6s linear;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes ripple {
|
||||||
|
to {
|
||||||
|
transform: scale(4);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.modal {
|
.modal {
|
||||||
display: none;
|
display: none;
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
|||||||
Reference in New Issue
Block a user