mirror of
https://git.datalinker.icu/comfyanonymous/ComfyUI
synced 2026-09-04 06:57:16 +08:00
feat: Add gallery feature for user images
This commit introduces a new gallery feature that allows you to mark images
in your user data folder and view them in a dedicated gallery page.
Key changes:
Backend (`app/user_manager.py`):
- Added a `GALLERY_SUFFIX` constant (`.gallery`).
- Implemented `toggle_gallery_status` to add/remove the gallery suffix from
filenames (e.g., `image.png` <-> `image.gallery.png`).
- Added a POST route `/userdata/{file}/gallery` to toggle an image's gallery
status.
- Implemented `list_gallery_files` to retrieve all images marked for the
gallery.
- Added a GET route `/gallery` to list gallery items for you.
Frontend:
- Created `web/gallery.html` as the main page for the gallery view.
- Created `web/css/gallery.css` for basic styling of the gallery.
- Created `web/scripts/gallery.js` to fetch and display gallery items,
allowing you to remove items from the gallery.
- Created `web/scripts/gallery_integration.js` to dynamically add a "Gallery"
button/link to the main application UI, which navigates to `gallery.html`.
- Updated `web/index.html` to include the gallery integration script.
Tests (`tests-unit/app_test/user_manager_gallery_routes_test.py`):
- Added comprehensive unit tests for the new backend routes, covering:
- Listing gallery items (empty, single, multiple, subdirectories).
- Toggling gallery status (add, remove, file not found, invalid params).
- Correct filename transformations and path handling.
This commit is contained in:
parent
37fda45c09
commit
9dc31cd71e
@ -14,6 +14,7 @@ from .app_settings import AppSettings
|
||||
from typing import TypedDict
|
||||
|
||||
default_user = "default"
|
||||
GALLERY_SUFFIX = ".gallery"
|
||||
|
||||
|
||||
class FileInfo(TypedDict):
|
||||
@ -268,6 +269,62 @@ class UserManager():
|
||||
|
||||
return web.json_response(resp)
|
||||
|
||||
async def toggle_gallery_status(self, request):
|
||||
file = request.match_info.get("file", None)
|
||||
if not file:
|
||||
return web.Response(status=400, text="File not specified")
|
||||
|
||||
filepath = self.get_request_user_filepath(request, file)
|
||||
if not filepath or not os.path.exists(filepath):
|
||||
return web.Response(status=404, text="File not found")
|
||||
|
||||
filename_no_ext, ext = os.path.splitext(filepath)
|
||||
|
||||
if filename_no_ext.endswith(GALLERY_SUFFIX):
|
||||
# Remove gallery status by removing suffix from filename part
|
||||
new_filename_no_ext = filename_no_ext[:-len(GALLERY_SUFFIX)]
|
||||
new_filepath = new_filename_no_ext + ext
|
||||
else:
|
||||
# Add gallery status by adding suffix to filename part
|
||||
new_filepath = filename_no_ext + GALLERY_SUFFIX + ext
|
||||
|
||||
os.rename(filepath, new_filepath)
|
||||
new_filename = os.path.basename(new_filepath)
|
||||
return web.json_response({"filename": new_filename}, status=200)
|
||||
|
||||
@routes.post("/userdata/{file}/gallery")
|
||||
async def post_toggle_gallery_status(request):
|
||||
return await self.toggle_gallery_status(request)
|
||||
|
||||
async def list_gallery_files(self, request):
|
||||
user_root_dir = self.get_request_user_filepath(request, None)
|
||||
if not user_root_dir or not os.path.isdir(user_root_dir):
|
||||
return web.json_response({"error": "User directory not found"}, status=404)
|
||||
|
||||
gallery_files_info = []
|
||||
# Search for files like *.gallery.png, *.gallery.jpg etc. in all subdirectories
|
||||
pattern = os.path.join(glob.escape(user_root_dir), '**', '*' + GALLERY_SUFFIX + '.*')
|
||||
|
||||
for filepath in glob.glob(pattern, recursive=True):
|
||||
if os.path.isfile(filepath):
|
||||
original_filename_with_ext = os.path.basename(filepath)
|
||||
# Remove .gallery suffix to get original filename
|
||||
original_filename = original_filename_with_ext.replace(GALLERY_SUFFIX, "")
|
||||
|
||||
file_info = {
|
||||
"filename": original_filename,
|
||||
"path": os.path.relpath(filepath, user_root_dir).replace(os.sep, '/'),
|
||||
"size": os.path.getsize(filepath),
|
||||
"modified": os.path.getmtime(filepath)
|
||||
}
|
||||
gallery_files_info.append(file_info)
|
||||
|
||||
return web.json_response(gallery_files_info)
|
||||
|
||||
@routes.get("/gallery")
|
||||
async def get_gallery_files(request):
|
||||
return await self.list_gallery_files(request)
|
||||
|
||||
@routes.delete("/userdata/{file}")
|
||||
async def delete_userdata(request):
|
||||
path = get_user_data_path(request, check_exists=True)
|
||||
|
||||
240
tests-unit/app_test/user_manager_gallery_routes_test.py
Normal file
240
tests-unit/app_test/user_manager_gallery_routes_test.py
Normal file
@ -0,0 +1,240 @@
|
||||
import pytest
|
||||
import os
|
||||
import shutil
|
||||
import json
|
||||
import time
|
||||
from unittest import IsolatedAsyncioTestCase # Using IsolatedAsyncioTestCase for async test methods
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
from aiohttp import web
|
||||
|
||||
# Assuming UserManager is in app.user_manager
|
||||
# Adjust the import path if your project structure is different
|
||||
from app.user_manager import UserManager, GALLERY_SUFFIX
|
||||
|
||||
# Mock comfy.cli_args and folder_paths before they are imported by UserManager
|
||||
# This is a common pattern if these modules are read at import time by the tested code.
|
||||
mock_args = MagicMock()
|
||||
mock_args.multi_user = False # Default to single-user mode for simplicity in most tests
|
||||
|
||||
mock_folder_paths = MagicMock()
|
||||
|
||||
# We'll set get_user_directory dynamically in test setup using tmp_path
|
||||
|
||||
# Apply patches at the module level if they need to be active before UserManager is imported
|
||||
# or within specific test classes/methods if more fine-grained control is needed.
|
||||
# For now, let's assume UserManager can be instantiated after these are patched.
|
||||
|
||||
@pytest.fixture
|
||||
def app_client_factory(event_loop): # event_loop is a pytest-asyncio fixture
|
||||
"""Factory to create aiohttp test clients."""
|
||||
async def _create_client(routes_def_func, *args_for_func):
|
||||
app = web.Application(loop=event_loop)
|
||||
routes = web.RouteTableDef()
|
||||
routes_def_func(routes, *args_for_func) # Call the function that defines routes
|
||||
app.add_routes(routes)
|
||||
return await event_loop.create_task(pytest.aiohttp.plugin.make_aiohttp_client(app))
|
||||
return _create_client
|
||||
|
||||
|
||||
class TestUserManagerGalleryRoutes(IsolatedAsyncioTestCase):
|
||||
|
||||
def setUp(self):
|
||||
# Create a temporary directory for user data
|
||||
self.test_user_dir_root = "temp_test_user_data"
|
||||
os.makedirs(self.test_user_dir_root, exist_ok=True)
|
||||
self.default_user_path = os.path.join(self.test_user_dir_root, "default")
|
||||
os.makedirs(self.default_user_path, exist_ok=True)
|
||||
|
||||
# Patch folder_paths.get_user_directory and args
|
||||
self.patch_folder_paths = patch('app.user_manager.folder_paths', mock_folder_paths)
|
||||
self.patch_args = patch('app.user_manager.args', mock_args)
|
||||
|
||||
self.mock_folder_paths = self.patch_folder_paths.start()
|
||||
self.mock_args = self.patch_args.start()
|
||||
|
||||
self.mock_folder_paths.get_user_directory.return_value = self.test_user_dir_root
|
||||
self.mock_args.multi_user = False # Explicitly set for each test run
|
||||
|
||||
self.user_manager = UserManager()
|
||||
|
||||
# Setup routes for the user_manager
|
||||
self.app = web.Application()
|
||||
self.user_manager.add_routes(self.app.router)
|
||||
|
||||
|
||||
async def asyncSetUp(self):
|
||||
# Create a test client for making requests
|
||||
self.client = await pytest.aiohttp.plugin.make_aiohttp_client(self.app)
|
||||
|
||||
|
||||
async def asyncTearDown(self):
|
||||
await self.client.close() # Close the client
|
||||
self.patch_folder_paths.stop()
|
||||
self.patch_args.stop()
|
||||
if os.path.exists(self.test_user_dir_root):
|
||||
shutil.rmtree(self.test_user_dir_root)
|
||||
|
||||
# --- Helper Methods ---
|
||||
def _create_file(self, filename, content="test", user="default", subdir=None):
|
||||
user_specific_path = os.path.join(self.test_user_dir_root, user)
|
||||
if subdir:
|
||||
user_specific_path = os.path.join(user_specific_path, subdir)
|
||||
os.makedirs(user_specific_path, exist_ok=True)
|
||||
|
||||
filepath = os.path.join(user_specific_path, filename)
|
||||
with open(filepath, "w") as f:
|
||||
f.write(content)
|
||||
return filepath
|
||||
|
||||
def _get_user_data_path(self, filename, user="default", subdir=None):
|
||||
user_specific_path = os.path.join(self.test_user_dir_root, user)
|
||||
if subdir:
|
||||
user_specific_path = os.path.join(user_specific_path, subdir)
|
||||
return os.path.join(user_specific_path, filename)
|
||||
|
||||
# --- Test Cases for /gallery (GET) ---
|
||||
async def test_list_gallery_empty(self):
|
||||
resp = await self.client.get("/gallery")
|
||||
assert resp.status == 200
|
||||
data = await resp.json()
|
||||
assert data == []
|
||||
|
||||
async def test_list_one_gallery_item(self):
|
||||
filename_orig = "image.png"
|
||||
filename_gallery = f"image{GALLERY_SUFFIX}.png"
|
||||
self._create_file(filename_gallery) # Create the .gallery.png file
|
||||
|
||||
resp = await self.client.get("/gallery")
|
||||
assert resp.status == 200
|
||||
data = await resp.json()
|
||||
|
||||
assert len(data) == 1
|
||||
item = data[0]
|
||||
assert item["filename"] == "image.png" # Original name without .gallery
|
||||
assert item["path"] == filename_gallery # Path includes .gallery
|
||||
assert item["size"] == 4 # "test"
|
||||
assert "modified" in item
|
||||
|
||||
async def test_list_multiple_gallery_items_and_subdirs(self):
|
||||
self._create_file(f"img1{GALLERY_SUFFIX}.jpg")
|
||||
self._create_file(f"img2{GALLERY_SUFFIX}.jpeg", subdir="photos")
|
||||
self._create_file(f"document{GALLERY_SUFFIX}.pdf", subdir="docs/work")
|
||||
self._create_file("not_gallery.txt") # Should not be listed
|
||||
self._create_file(f"also_not_gallery{GALLERY_SUFFIX}") # No extension, should not match *.gallery.*
|
||||
self._create_file(f"another.gallery#fake.png") # Invalid char, but testing suffix rule
|
||||
|
||||
resp = await self.client.get("/gallery")
|
||||
assert resp.status == 200
|
||||
data = await resp.json()
|
||||
|
||||
assert len(data) == 3 # Only 3 valid gallery items
|
||||
filenames_found = sorted([item["filename"] for item in data])
|
||||
expected_filenames = sorted(["img1.jpg", "img2.jpeg", "document.pdf"])
|
||||
assert filenames_found == expected_filenames
|
||||
|
||||
paths_found = sorted([item["path"] for item in data])
|
||||
expected_paths = sorted([
|
||||
f"img1{GALLERY_SUFFIX}.jpg",
|
||||
f"photos/img2{GALLERY_SUFFIX}.jpeg",
|
||||
f"docs/work/document{GALLERY_SUFFIX}.pdf"
|
||||
])
|
||||
assert paths_found == expected_paths
|
||||
|
||||
async def test_list_non_gallery_items_not_listed(self):
|
||||
self._create_file("textfile.txt")
|
||||
self._create_file(f"image_not_gallery.png") # No .gallery suffix in name
|
||||
self._create_file(f"image_with_gallery_suffix_only{GALLERY_SUFFIX}") # No further extension
|
||||
|
||||
resp = await self.client.get("/gallery")
|
||||
assert resp.status == 200
|
||||
data = await resp.json()
|
||||
assert data == []
|
||||
|
||||
# --- Test Cases for /userdata/{file}/gallery (POST) ---
|
||||
async def test_toggle_gallery_add(self):
|
||||
filename = "add_me.png"
|
||||
created_path = self._create_file(filename)
|
||||
|
||||
resp = await self.client.post(f"/userdata/{filename}/gallery")
|
||||
assert resp.status == 200
|
||||
data = await resp.json()
|
||||
|
||||
expected_new_filename = f"add_me{GALLERY_SUFFIX}.png"
|
||||
assert data["filename"] == expected_new_filename
|
||||
|
||||
assert not os.path.exists(created_path)
|
||||
assert os.path.exists(self._get_user_data_path(expected_new_filename))
|
||||
|
||||
async def test_toggle_gallery_add_with_subdir(self):
|
||||
filename = "add_me_subdir.jpg"
|
||||
subdir = "level1/level2"
|
||||
created_path = self._create_file(filename, subdir=subdir)
|
||||
|
||||
# Path in URL needs to be URL encoded if it has slashes
|
||||
url_path = f"{subdir}/{filename}"
|
||||
|
||||
resp = await self.client.post(f"/userdata/{url_path}/gallery")
|
||||
assert resp.status == 200
|
||||
data = await resp.json()
|
||||
|
||||
expected_new_filename = f"add_me_subdir{GALLERY_SUFFIX}.jpg"
|
||||
assert data["filename"] == expected_new_filename # Response is basename
|
||||
|
||||
assert not os.path.exists(created_path)
|
||||
assert os.path.exists(self._get_user_data_path(expected_new_filename, subdir=subdir))
|
||||
|
||||
|
||||
async def test_toggle_gallery_remove(self):
|
||||
original_filename_part = "remove_me"
|
||||
ext = ".jpeg"
|
||||
gallery_filename = f"{original_filename_part}{GALLERY_SUFFIX}{ext}"
|
||||
created_gallery_path = self._create_file(gallery_filename)
|
||||
|
||||
resp = await self.client.post(f"/userdata/{gallery_filename}/gallery")
|
||||
assert resp.status == 200
|
||||
data = await resp.json()
|
||||
|
||||
expected_new_filename = f"{original_filename_part}{ext}"
|
||||
assert data["filename"] == expected_new_filename
|
||||
|
||||
assert not os.path.exists(created_gallery_path)
|
||||
assert os.path.exists(self._get_user_data_path(expected_new_filename))
|
||||
|
||||
async def test_toggle_gallery_file_not_found(self):
|
||||
resp = await self.client.post("/userdata/nonexistentfile.png/gallery")
|
||||
assert resp.status == 404 # Or 400 if file not specified, but here it is specified
|
||||
data = await resp.json() # Assuming error responses are JSON
|
||||
assert "File not found" in data.get("error", "") or "File not found" in await resp.text()
|
||||
|
||||
|
||||
async def test_toggle_gallery_invalid_file_param(self):
|
||||
# Test with an empty file parameter or one that might be problematic
|
||||
# The route itself might catch this before user_manager logic if path is malformed
|
||||
# Depending on aiohttp's routing, this might result in a 404 for the route itself
|
||||
# or a 400 if the handler's file extraction fails.
|
||||
# UserManager's toggle_gallery_status expects `file` from `request.match_info`.
|
||||
# If `file` is empty, it returns 400 "File not specified".
|
||||
|
||||
# This test is more about how aiohttp handles empty path parameters
|
||||
# For instance, a route like /userdata//gallery might not match or might pass an empty string.
|
||||
# Let's assume it passes an empty string if the route matches /userdata/{file}/gallery
|
||||
# For this, we'd need to register a route that can produce an empty 'file' match_info.
|
||||
# The current route definition /userdata/{file}/gallery will likely not match /userdata//gallery.
|
||||
# So, let's test the handler directly with a mock request if we want to ensure "File not specified".
|
||||
|
||||
mock_request = MagicMock(spec=web.Request)
|
||||
mock_request.match_info = {} # No 'file'
|
||||
mock_request.headers = {} # For get_request_user_id
|
||||
|
||||
# Mock get_request_user_id if it's called before file check
|
||||
# self.user_manager.get_request_user_id = MagicMock(return_value="default")
|
||||
|
||||
response = await self.user_manager.toggle_gallery_status(mock_request)
|
||||
assert response.status == 400
|
||||
# text_response = await response.text() # Not needed if using response.text
|
||||
assert "File not specified" in response.text
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main()
|
||||
37
web/css/gallery.css
vendored
Normal file
37
web/css/gallery.css
vendored
Normal file
@ -0,0 +1,37 @@
|
||||
#gallery-container {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 15px; /* spacing between items */
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.gallery-item {
|
||||
margin: 10px;
|
||||
border: 1px solid #ccc;
|
||||
padding: 5px;
|
||||
box-shadow: 2px 2px 5px rgba(0,0,0,0.1);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
width: calc(200px + 10px); /* width of thumbnail + padding */
|
||||
}
|
||||
|
||||
.gallery-thumbnail {
|
||||
width: 200px;
|
||||
height: 200px;
|
||||
object-fit: cover; /* scales the image to cover the container while maintaining aspect ratio */
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.gallery-item button {
|
||||
padding: 8px 12px;
|
||||
background-color: #f44336; /* Red */
|
||||
color: white;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.gallery-item button:hover {
|
||||
background-color: #d32f2f; /* Darker red */
|
||||
}
|
||||
16
web/gallery.html
vendored
Normal file
16
web/gallery.html
vendored
Normal file
@ -0,0 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Gallery</title>
|
||||
<link rel="stylesheet" href="css/gallery.css">
|
||||
</head>
|
||||
<body>
|
||||
<h1>My Gallery</h1>
|
||||
<div id="gallery-container">
|
||||
<!-- Gallery items will be loaded here by JavaScript -->
|
||||
</div>
|
||||
<script src="scripts/gallery.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
1
web/index.html
vendored
1
web/index.html
vendored
@ -8,6 +8,7 @@
|
||||
<link rel="stylesheet" type="text/css" href="materialdesignicons.min.css" />
|
||||
<script type="module" crossorigin src="./assets/index-DIU5yZe9.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="./assets/index-1vLlIVor.css">
|
||||
<script src="scripts/gallery_integration.js" defer></script>
|
||||
</head>
|
||||
<body class="litegraph grid">
|
||||
<div id="vue-app"></div>
|
||||
|
||||
77
web/scripts/gallery.js
vendored
Normal file
77
web/scripts/gallery.js
vendored
Normal file
@ -0,0 +1,77 @@
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
initGallery();
|
||||
});
|
||||
|
||||
async function initGallery() {
|
||||
try {
|
||||
const items = await fetchGalleryItems();
|
||||
renderGalleryItems(items);
|
||||
} catch (error) {
|
||||
console.error("Error initializing gallery:", error);
|
||||
const galleryContainer = document.getElementById('gallery-container');
|
||||
if (galleryContainer) {
|
||||
galleryContainer.innerHTML = '<p>Error loading gallery items. Please try again later.</p>';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchGalleryItems() {
|
||||
const response = await fetch('/gallery');
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
function renderGalleryItems(items) {
|
||||
const galleryContainer = document.getElementById('gallery-container');
|
||||
if (!galleryContainer) {
|
||||
console.error("Gallery container not found!");
|
||||
return;
|
||||
}
|
||||
|
||||
galleryContainer.innerHTML = ''; // Clear previous items
|
||||
|
||||
if (!items || items.length === 0) {
|
||||
galleryContainer.innerHTML = '<p>No items in the gallery.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
items.forEach(item => {
|
||||
const itemDiv = document.createElement('div');
|
||||
itemDiv.className = 'gallery-item';
|
||||
|
||||
const img = document.createElement('img');
|
||||
// Assuming 'item.path' is the correct relative path including any necessary subdirectories
|
||||
// and the '.gallery' part of the filename.
|
||||
img.src = `userdata/${item.path}`;
|
||||
img.alt = item.filename;
|
||||
img.className = 'gallery-thumbnail';
|
||||
img.onerror = () => { // Basic error handling for broken images
|
||||
img.alt = 'Image not found';
|
||||
// Optionally, display a placeholder or hide the item
|
||||
};
|
||||
|
||||
const removeButton = document.createElement('button');
|
||||
removeButton.textContent = 'Remove from Gallery';
|
||||
removeButton.onclick = async () => {
|
||||
try {
|
||||
// item.path should be the full relative path including any .gallery part
|
||||
const response = await fetch(`/userdata/${item.path}/gallery`, { method: 'POST' });
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(`Failed to remove item: ${errorData.error || response.status}`);
|
||||
}
|
||||
// Refresh the gallery to show changes
|
||||
initGallery();
|
||||
} catch (error) {
|
||||
console.error('Error removing item from gallery:', error);
|
||||
alert(`Error: ${error.message}`);
|
||||
}
|
||||
};
|
||||
|
||||
itemDiv.appendChild(img);
|
||||
itemDiv.appendChild(removeButton);
|
||||
galleryContainer.appendChild(itemDiv);
|
||||
});
|
||||
}
|
||||
92
web/scripts/gallery_integration.js
vendored
Normal file
92
web/scripts/gallery_integration.js
vendored
Normal file
@ -0,0 +1,92 @@
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
// Attempt to find a known menu element
|
||||
let menuElement = document.querySelector(".comfy-menu-buttons");
|
||||
|
||||
if (!menuElement) {
|
||||
menuElement = document.querySelector(".comfy-horizontal-menu");
|
||||
}
|
||||
|
||||
if (!menuElement) {
|
||||
const queueButton = document.getElementById("queue-button");
|
||||
if (queueButton && queueButton.parentElement) {
|
||||
menuElement = queueButton.parentElement;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: try to find any element with 'menu' in its class or id
|
||||
if (!menuElement) {
|
||||
const allElements = document.getElementsByTagName('*');
|
||||
for (let i = 0; i < allElements.length; i++) {
|
||||
const el = allElements[i];
|
||||
if ((el.className && typeof el.className === 'string' && el.className.includes('menu')) ||
|
||||
(el.id && el.id.includes('menu'))) {
|
||||
// Check if it's a plausible candidate (e.g., not too deep, visible)
|
||||
// This is a very rough heuristic
|
||||
if (el.children.length > 0 && el.children.length < 10 && el.offsetParent !== null) {
|
||||
menuElement = el;
|
||||
console.log("Found a generic menu element:", menuElement);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (menuElement) {
|
||||
const galleryLink = document.createElement('a');
|
||||
galleryLink.href = 'gallery.html';
|
||||
galleryLink.textContent = 'Gallery';
|
||||
galleryLink.id = 'gallery-button'; // Added an ID for easier selection/styling if needed
|
||||
|
||||
// Basic styling to make it look like other buttons if possible
|
||||
// This is highly dependent on the existing CSS of the application
|
||||
// Attempt to copy styles from an existing button if one exists
|
||||
const existingButton = menuElement.querySelector('button') || menuElement.querySelector('a');
|
||||
if (existingButton) {
|
||||
galleryLink.className = existingButton.className; // Copy class
|
||||
// Copy some inline styles if they exist (might not be ideal but can work)
|
||||
if (existingButton.style.padding) galleryLink.style.padding = existingButton.style.padding;
|
||||
if (existingButton.style.margin) galleryLink.style.margin = existingButton.style.margin;
|
||||
if (existingButton.style.textDecoration) galleryLink.style.textDecoration = existingButton.style.textDecoration;
|
||||
if (existingButton.style.color) galleryLink.style.color = existingButton.style.color;
|
||||
if (existingButton.style.backgroundColor) galleryLink.style.backgroundColor = existingButton.style.backgroundColor;
|
||||
if (existingButton.style.border) galleryLink.style.border = existingButton.style.border;
|
||||
if (existingButton.style.borderRadius) galleryLink.style.borderRadius = existingButton.style.borderRadius;
|
||||
|
||||
} else {
|
||||
// Default minimal styling
|
||||
galleryLink.style.padding = '5px 10px';
|
||||
galleryLink.style.margin = '0 5px';
|
||||
galleryLink.style.textDecoration = 'none';
|
||||
galleryLink.style.border = '1px solid #333';
|
||||
galleryLink.style.borderRadius = '4px';
|
||||
galleryLink.style.color = '#333';
|
||||
galleryLink.style.backgroundColor = '#f0f0f0';
|
||||
}
|
||||
|
||||
// Specific style for our gallery button if not overridden by copied styles
|
||||
if (!galleryLink.style.display) galleryLink.style.display = 'inline-block'; // Ensure it's displayed
|
||||
|
||||
menuElement.appendChild(galleryLink);
|
||||
console.log('Gallery link added to menu:', menuElement);
|
||||
} else {
|
||||
console.warn('Could not find a suitable menu element to add the gallery link.');
|
||||
// Fallback: Add it to the body or a prominent header if nothing else is found
|
||||
const body = document.body;
|
||||
const galleryLink = document.createElement('a');
|
||||
galleryLink.href = 'gallery.html';
|
||||
galleryLink.textContent = 'Open Gallery';
|
||||
galleryLink.style.position = 'fixed';
|
||||
galleryLink.style.top = '10px';
|
||||
galleryLink.style.right = '10px';
|
||||
galleryLink.style.padding = '10px';
|
||||
galleryLink.style.backgroundColor = '#007bff';
|
||||
galleryLink.style.color = 'white';
|
||||
galleryLink.style.textDecoration = 'none';
|
||||
galleryLink.style.zIndex = '1000';
|
||||
galleryLink.style.border = '1px solid #0056b3'
|
||||
galleryLink.style.borderRadius = '5px';
|
||||
body.appendChild(galleryLink);
|
||||
console.log('Gallery link added as a fallback floating button.');
|
||||
}
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user