Compare commits

...
40 Commits
Author SHA1 Message Date
david f7107ff2a6 added changes 2026-01-17 12:14:36 -05:00
david f9639318e8 pulling 2026-01-14 17:59:25 +00:00
david 64186d7064 module update 2026-01-14 17:59:12 +00:00
david c5f100d747 module update 2026-01-14 17:59:09 +00:00
david 735757d58d Merge branch 'main' of http://truenas.local:30008/david/personal_development 2026-01-14 16:37:54 +00:00
david 64b593ac30 sync 2026-01-14 16:37:45 +00:00
david 68230235d1 sync 2026-01-14 16:36:06 +00:00
david c0c6cdd156 syncng 2026-01-14 10:47:16 -05:00
david c41ad77181 Merge branch 'main' of http://192.168.6.254:30008/david/personal_development 2026-01-13 10:33:36 -05:00
david 55a12c339c Merge branch 'main' of http://truenas.local:30008/david/personal_development 2026-01-13 15:27:30 +00:00
david 10de3ffd94 merging 2026-01-13 15:27:21 +00:00
david 0fdb758835 merging 2026-01-13 15:27:06 +00:00
david 5fd8cec0c2 merging 2026-01-13 15:27:05 +00:00
david 47f1d1311f sync 2026-01-13 10:26:20 -05:00
david 770390ff9b added logs 2026-01-12 16:02:20 -05:00
david 3daad96357 Merge branch 'main' of http://truenas.local:30008/david/personal_development 2026-01-12 16:01:23 -05:00
david 9befd9c28d added model c hange 2026-01-12 16:01:22 -05:00
david e3af9736b4 Merge branch 'main' of http://truenas.local:30008/david/personal_development 2026-01-12 15:25:30 -05:00
david c75814e837 syncing 2026-01-12 15:25:29 -05:00
david d180d43e18 added documentation 2026-01-12 15:25:11 -05:00
david 3196d3d761 fixed the script killer issue 2026-01-12 15:23:42 -05:00
david bdf8d27398 test merge 2026-01-12 15:17:32 -05:00
david 34e8221772 Merge branch 'main' of http://truenas.local:30008/david/personal_development 2026-01-12 15:15:38 -05:00
david e471d3e1f8 syncing latest revisions - dk123 2026-01-12 15:15:37 -05:00
david 7195dc1059 adding current revisions. 2026-01-12 15:11:23 -05:00
david 56494208bd Merge branch 'main' of http://truenas.local:30008/david/personal_development 2026-01-12 13:42:39 -05:00
david 417b70a6bc syncing current db info 2026-01-12 13:38:49 -05:00
david fab54845c8 syncing the current fixes 2026-01-12 13:38:36 -05:00
david bed73a0085 Merge branch 'main' of http://truenas.local:30008/david/personal_development 2026-01-12 12:27:36 -05:00
david 8ea92ea842 added 2026-01-12 12:18:42 -05:00
david 2a7878ddad adding latest documentation and changes to the script. 2026-01-12 11:47:39 -05:00
david 33441ea0fe first time run 2026-01-12 11:44:28 -05:00
david 7ad3715c52 added diag script 2026-01-12 10:53:06 -05:00
david 0a6e9ed1d8 sync 2026-01-12 10:05:57 -05:00
david 8181bebaa0 Added revisions to the translation app 2026-01-12 08:52:38 -05:00
david 64add85920 finished revising the script. 2026-01-11 17:37:45 -05:00
david 203fc69393 syncing scripts 2026-01-11 15:10:56 -05:00
david 362c06dab4 AI Transcriber tool 2026-01-10 17:08:51 -05:00
david c30b54f4b1 log files 2026-01-09 19:24:44 +00:00
david c0ac650625 major server manager overhaul 2026-01-09 19:24:41 +00:00
284 changed files with 290702 additions and 35270 deletions
+1
View File
@@ -0,0 +1 @@
.env_files/
+17
View File
@@ -0,0 +1,17 @@
#!/bin/bash
# Navigate to the script's directory
cd "$(dirname "$0")"
# Check for virtual environment and activate if found
if [ -d "venv" ]; then
source venv/bin/activate
elif [ -d ".venv" ]; then
source .venv/bin/activate
fi
echo "Starting DownloaderWebApp..."
python3 main.py &
PID=$!
echo $PID > app.pid
echo "Application started with PID: $PID"
+25
View File
@@ -0,0 +1,25 @@
#!/bin/bash
# Navigate to the script's directory
cd "$(dirname "$0")"
if [ -f "app.pid" ]; then
PID=$(cat app.pid)
if ps -p $PID > /dev/null; then
echo "Stopping app (PID $PID)..."
kill $PID
rm app.pid
echo "Stopped."
else
echo "PID file found ($PID) but process is not running. Removing stale PID file."
rm app.pid
fi
else
echo "No PID file found. Trying to find process by name..."
pkill -f "python3 main.py"
if [ $? -eq 0 ]; then
echo "Stopped via process name."
else
echo "No running process found."
fi
fi
+3 -2
View File
@@ -1,2 +1,3 @@
10546 1075850
10547 1075852
1075853
+662 -92
View File
@@ -1,9 +1,15 @@
import os import os
import re import re
from collections import defaultdict import shutil
import zipfile
import tempfile
import logging import logging
import cloudscraper
import difflib
from bs4 import BeautifulSoup
from collections import defaultdict
from .samba_manager import SambaManager from .samba_manager import SambaManager
from .config import Settings from .config import settings
from smb.smb_structs import OperationFailure from smb.smb_structs import OperationFailure
# --- Dedicated logger for ComicsManager --- # --- Dedicated logger for ComicsManager ---
@@ -11,117 +17,681 @@ comics_logger = logging.getLogger('comics_manager')
comics_logger.setLevel(logging.INFO) comics_logger.setLevel(logging.INFO)
comics_logger.propagate = False comics_logger.propagate = False
if not comics_logger.handlers: if not comics_logger.handlers:
os.makedirs("logs", exist_ok=True)
comics_log_handler = logging.FileHandler("logs/comics_organization.log", mode='a') comics_log_handler = logging.FileHandler("logs/comics_organization.log", mode='a')
comics_log_handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')) comics_log_handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s'))
comics_logger.addHandler(comics_log_handler) comics_logger.addHandler(comics_log_handler)
comics_logger.addHandler(logging.StreamHandler()) comics_logger.addHandler(logging.StreamHandler())
class ComicsManager: class ComicsManager:
def __init__(self):
# Staging settings (on 'isolation' share)
self.comics_root = "/comics/pendingorganization"
# Library defaults
self.library_share_default = "isolation"
self.library_path_default = "/comics/manga"
self.scraper = cloudscraper.create_scraper()
@staticmethod def _get_series_info(self, filename):
def _get_series_name(filename):
base_name, _ = os.path.splitext(filename) base_name, _ = os.path.splitext(filename)
pattern = re.compile(r'[-_\s]*(v(ol)?|c(h)?|chapter|issue|ep|episode)[-_\s]*\d+.*|[-_\s]+\d+$', re.IGNORECASE) artist = "Unknown"
match = pattern.search(base_name) doujin_id = None
id_match = re.search(r'[[\\](](\\d{5,7})[[\\])]', base_name)
if id_match:
doujin_id = id_match.group(1)
tags = re.findall(r'[[\\](](.*?)[[\\])]', base_name)
tags = [t for t in tags if t != doujin_id]
if tags:
artist = tags[0].strip()
name = re.sub(r'[[\\](].*?[[\\])]', '', base_name).strip()
pattern = re.compile(r'[-_\\s]*(v(ol)?\\. ?|c(h)?\\. ?|chapter|issue|ep(isode)?)\\s*\\d+.*$', re.IGNORECASE)
match = pattern.search(name)
if match: if match:
return base_name[:match.start()].strip('-_ ') name = name[:match.start()]
return base_name.strip('-_ ') else:
name = re.sub(r'[-_\\s]+\\d+\\s*$', '', name)
def cleanup_toberead(self, samba_manager: SambaManager): name = name.strip(' -_')
comics_logger.info("Starting cleanup of 'toberead' directory...") if not name and tags:
comics_root = "/comics/toberead" name = tags[0]
try: elif not name:
items_in_toberead = samba_manager.list_path(comics_root) name = base_name.strip()
if "error" in items_in_toberead:
raise Exception(f"Failed to list files in 'toberead': {items_in_toberead['error']}")
for item in items_in_toberead:
item_path = item["path"]
item_name = item["name"]
if item["is_directory"]:
comics_logger.info(f"Deleting directory '{item_name}' and all its contents.")
try:
samba_manager.delete_directory_recursive(item_path)
comics_logger.info(f"Successfully deleted directory: {item_name}")
except OperationFailure as e:
comics_logger.error(f"Failed to delete directory {item_name}: {e}")
else:
comics_logger.info(f"Deleting file '{item_name}'.")
try:
samba_manager.delete_file(item_path)
comics_logger.info(f"Successfully deleted file: {item_name}")
except OperationFailure as e:
comics_logger.error(f"Failed to delete file {item_name}: {e}")
comics_logger.info("Cleanup of 'toberead' finished.") name = name.replace(' ', '_')
artist = artist.replace(' ', '_')
return name, artist, doujin_id
def _fetch_metadata_from_url(self, target_url):
try:
comics_logger.info(f"Fetching metadata from: {target_url}")
response = self.scraper.get(target_url)
if response.status_code == 200:
soup = BeautifulSoup(response.content, 'html.parser')
data = {}
title_info = soup.select_one('#info')
if title_info:
data['title'] = title_info.select_one('h1.title').text.strip() if title_info.select_one('h1.title') else ""
data['original_title'] = title_info.select_one('h2.title').text.strip() if title_info.select_one('h2.title') else ""
tag_containers = soup.select('.tag-container')
for container in tag_containers:
label = container.text.split(':')[0].strip().lower() if ':' in container.text else ""
tags = [t.select_one('.name').text for t in container.select('.tag')]
if 'tags' in label: data['tags'] = tags
elif 'artists' in label: data['artist'] = tags
elif 'groups' in label: data['circle'] = tags
elif 'parodies' in label: data['parody'] = tags
time_tag = soup.select_one('#info time')
if time_tag and time_tag.get('datetime'):
year_match = re.search(r'(\\d{4})', time_tag['datetime'])
if year_match: data['year'] = year_match.group(1)
data['url'] = target_url
data['id'] = target_url.split('/g/')[1].strip('/')
return data
except Exception as e: except Exception as e:
comics_logger.error(f"An error occurred during 'toberead' cleanup: {e}", exc_info=True) comics_logger.error(f"Error parsing metadata: {e}")
return None
def _lookup_doujin_metadata(self, title, artist="Unknown", doujin_id=None):
base_url = "https://nhentai.net"
if doujin_id:
return self._fetch_metadata_from_url(f"{base_url}/g/{doujin_id}/")
clean_title = title.replace('_', ' ').strip()
clean_artist = artist.replace('_', ' ').strip() if artist != "Unknown" else ""
queries = []
if clean_artist: queries.append(f"{clean_artist} {clean_title}")
queries.append(clean_title)
for query in queries:
try:
search_url = f"{base_url}/search/?q={query.replace(' ', '+')}"
comics_logger.info(f"Searching metadata for: {query}")
response = self.scraper.get(search_url)
if response.status_code == 200:
soup = BeautifulSoup(response.content, 'html.parser')
results = soup.select('.gallery a.cover')
if results:
target_url = f"{base_url}{results[0]['href']}"
data = self._fetch_metadata_from_url(target_url)
if data: return data
except Exception as e:
comics_logger.error(f"Search failed for query '{query}': {e}")
return None
def _download_directory(self, samba_manager, remote_path, local_path):
os.makedirs(local_path, exist_ok=True)
items = samba_manager.list_path(remote_path)
if isinstance(items, dict) and "error" in items: raise Exception(items["error"])
for item in items:
if item['name'] in ['.', '..']: continue
local_item_path = os.path.join(local_path, item['name'])
if item['is_directory']: self._download_directory(samba_manager, item['path'], local_item_path)
else:
with open(local_item_path, 'wb') as f:
samba_manager.download_file(item['path'], f)
def _extract_archive(self, archive_path, extract_path):
if zipfile.is_zipfile(archive_path):
with zipfile.ZipFile(archive_path, 'r') as zf:
zf.extractall(extract_path)
else: raise Exception("Unsupported archive format")
def _create_cbz(self, source_folder, output_path):
with zipfile.ZipFile(output_path, 'w', zipfile.ZIP_DEFLATED) as zf:
for root, dirs, files in os.walk(source_folder):
for file in files:
file_path = os.path.join(root, file)
arcname = os.path.relpath(file_path, source_folder)
zf.write(file_path, arcname)
def _create_metadata_file(self, series_name, author="Unknown", artist="Unknown", api_data=None):
content = f"Title: {series_name}\n"
if api_data:
if api_data.get('title'): content = f"Title: {api_data['title']}\n"
if api_data.get('original_title'): content += f"Original Title: {api_data['original_title']}\n"
artists = ", ".join(api_data.get('artist', [])) or artist
circles = ", ".join(api_data.get('circle', [])) or "Unknown"
parodies = ", ".join(api_data.get('parody', [])) or "Original"
tags = ", ".join(api_data.get('tags', []))
content += f"Artist: {artists}\nCircle: {circles}\nParody: {parodies}\nTags: {tags}\nURL: {api_data.get('url', 'N/A')}\n"
else:
content += f"Artist: {artist}\nAuthor: {author}\n"
content += f"Processed by: ServerManagerWebApp\n"
return content
def _lookup_mangadex_metadata(self, title):
"""
Queries Mangadex API.
"""
try:
url = "https://api.mangadex.org/manga"
clean_title = title.replace('_', ' ').strip()
comics_logger.info(f"Searching Mangadex for: {clean_title}")
# Mangadex requires separate calls for author/artist usually, but we'll start with basic info
params = {
"title": clean_title,
"limit": 1,
"includes[]": ["author", "artist", "cover_art"]
}
response = self.scraper.get(url, params=params, timeout=10)
if response.status_code == 200:
data = response.json()
if data.get('data'):
manga = data['data'][0]
attr = manga['attributes']
# Extract authors/artists
authors = []
artists = []
for rel in manga['relationships']:
if rel['type'] == 'author': authors.append(rel.get('attributes', {}).get('name'))
if rel['type'] == 'artist': artists.append(rel.get('attributes', {}).get('name'))
# Tags
tags = [t['attributes']['name']['en'] for t in attr.get('tags', [])]
return {
"title": attr['title'].get('en') or list(attr['title'].values())[0],
"original_title": attr.get('altTitles', [{}])[0].get('en', "") if attr.get('altTitles') else "",
"year": attr.get('year'),
"url": f"https://mangadex.org/title/{manga['id']}",
"id": manga['id'],
"tags": tags,
"author": authors,
"artist": artists,
"source": "Mangadex"
}
except Exception as e:
comics_logger.error(f"Mangadex lookup failed: {e}")
return None
def _lookup_hentai2read_metadata(self, title):
"""
Scrapes Hentai2Read.
"""
try:
base_url = "https://hentai2read.com"
clean_title = title.replace('_', '+').strip()
search_url = f"{base_url}/search/?cmd={clean_title}"
comics_logger.info(f"Searching Hentai2Read for: {clean_title}")
response = self.scraper.get(search_url)
if response.status_code == 200:
soup = BeautifulSoup(response.content, 'html.parser')
# Hentai2Read search results structure
result_link = soup.select_one('.book-grid-item a')
if result_link:
target_url = result_link['href']
comics_logger.info(f"Fetching Hentai2Read details: {target_url}")
resp = self.scraper.get(target_url)
if resp.status_code == 200:
soup = BeautifulSoup(resp.content, 'html.parser')
data = {"source": "Hentai2Read", "url": target_url}
# Title
title_tag = soup.select_one('h3.block-title a')
if title_tag: data['title'] = title_tag.text.strip()
# Info list
info_items = soup.select('ul.list-simple-mini li')
for item in info_items:
text = item.text.strip()
if "Author" in text:
data['author'] = [a.strip() for a in text.replace("Author", "").strip(" :" ).split(',')]
elif "Artist" in text:
data['artist'] = [a.strip() for a in text.replace("Artist", "").strip(" :" ).split(',')]
elif "Parody" in text:
data['parody'] = [a.strip() for a in text.replace("Parody", "").strip(" :" ).split(',')]
elif "Storyline" in text or "Content" in text: # Tags
data['tags'] = [a.text.strip() for a in item.select('a')]
elif "Release" in text:
year_match = re.search(r'\\d{4}', text)
if year_match: data['year'] = year_match.group(0)
return data
except Exception as e:
comics_logger.error(f"Hentai2Read lookup failed: {e}")
return None
def _lookup_mangaupdates_metadata(self, title):
"""
Queries MangaUpdates API for metadata (good for Manhwa/Webtoons).
"""
try:
url = "https://api.mangaupdates.com/v1/series/search"
# Replace underscores with spaces for better search
clean_title = title.replace('_', ' ').strip()
comics_logger.info(f"Searching MangaUpdates for: {clean_title}")
response = self.scraper.post(url, json={"search": clean_title}, timeout=10)
if response.status_code == 200:
data = response.json()
if data.get('results'):
series = data['results'][0]['record']
return {
"title": series.get('title'),
"year": series.get('year'),
"url": series.get('url'),
"id": series.get('series_id'),
"tags": [g.get('genre') for g in series.get('genres', [])] if series.get('genres') else [],
"source": "MangaUpdates"
}
except Exception as e:
comics_logger.error(f"MangaUpdates lookup failed: {e}")
return None
def _process_item(self, samba_manager, item, is_archive=False):
item_path = item['path']
item_name = item['name']
name_part, artist_part, doujin_id = self._get_series_info(item_name)
api_data = None
# 1. Try Mangadex (Standard/Manhwa)
if not api_data:
api_data = self._lookup_mangadex_metadata(name_part)
# 2. Try nhentai (Doujinshi)
if not api_data:
# Need to re-add "source": "nhentai" to the existing method logic or wrapper
data = self._lookup_doujin_metadata(name_part, artist_part, doujin_id)
if data:
data['source'] = "nhentai"
api_data = data
# 3. Try Hentai2Read (Fallback Doujin)
if not api_data:
api_data = self._lookup_hentai2read_metadata(name_part)
# 4. Try MangaUpdates (Final Fallback)
if not api_data:
api_data = self._lookup_mangaupdates_metadata(name_part)
final_series_name = name_part
if api_data:
if api_data.get('title'):
title_clean = re.sub(r'[<>:"/\\|?*]', '', api_data['title']).strip()
final_series_name = title_clean.replace(' ', '_')
if api_data.get('year'): final_series_name = f"{final_series_name}_({api_data['year']})"
elif api_data.get('id'): final_series_name = f"{final_series_name}_({api_data['id']})"
final_series_name = final_series_name.replace(' ', '_')
series_folder_path = f"{self.comics_root}/{final_series_name}"
try:
samba_manager.create_directory(series_folder_path)
except OperationFailure: pass
if item_name == final_series_name: return
comics_logger.info(f"Processing: {item_name} -> {final_series_name}")
with tempfile.TemporaryDirectory() as temp_dir:
extraction_path = os.path.join(temp_dir, "extracted")
os.makedirs(extraction_path, exist_ok=True)
if is_archive:
local_archive = os.path.join(temp_dir, item_name)
with open(local_archive, 'wb') as f: samba_manager.download_file(item_path, f)
try: self._extract_archive(local_archive, extraction_path)
except Exception as e:
comics_logger.error(f"Failed to extract {item_name}: {e}")
return
else: self._download_directory(samba_manager, item_path, extraction_path)
has_images = False
for root, _, files in os.walk(extraction_path):
if any(f.lower().endswith(('.jpg', '.jpeg', '.png', '.webp')) for f in files):
has_images = True
break
if has_images:
cbz_name = f"{final_series_name}.cbz"
temp_cbz = os.path.join(temp_dir, cbz_name)
self._create_cbz(extraction_path, temp_cbz)
dest_cbz_path = f"{series_folder_path}/{cbz_name}"
with open(temp_cbz, 'rb') as f:
res = samba_manager.upload_file(dest_cbz_path, f)
if isinstance(res, dict) and "error" in res: raise Exception(f"Upload failed: {res['error']}")
meta_content = self._create_metadata_file(final_series_name, artist=artist_part, api_data=api_data)
meta_path = f"{series_folder_path}/{final_series_name}_info.txt"
with tempfile.NamedTemporaryFile(mode='w+', delete=False) as tmp_meta:
tmp_meta.write(meta_content)
tmp_meta.flush()
tmp_meta.seek(0)
with open(tmp_meta.name, 'rb') as f: samba_manager.upload_file(meta_path, f)
os.unlink(tmp_meta.name)
if is_archive: samba_manager.delete_file(item_path)
else: samba_manager.delete_directory_recursive(item_path)
else: comics_logger.warning(f"No images found in {item_name}, skipping.")
def organize_comics(self, samba_manager: SambaManager): def organize_comics(self, samba_manager: SambaManager):
comics_logger.info("Starting comics organization process...") comics_logger.info(f"Starting organization in {self.comics_root}...")
comics_root = "/comics/toberead" self._recursive_scan_and_process(samba_manager, self.comics_root)
comics_dest_root = "/comics/manga"
def _recursive_scan_and_process(self, samba_manager, current_path):
try: try:
items_in_toberead = samba_manager.list_path(comics_root) items = samba_manager.list_path(current_path)
if "error" in items_in_toberead: if isinstance(items, dict) and "error" in items: return
raise Exception(f"Failed to list files in 'toberead': {items_in_toberead['error']}") for item in items:
if item['name'] in ['.', '..']: continue
if item['is_directory']:
sub_items = samba_manager.list_path(item['path'])
if any(sub['name'].lower().endswith(('.jpg', '.jpeg', '.png', '.webp')) for sub in sub_items):
self._process_item(samba_manager, item, is_archive=False)
else: self._recursive_scan_and_process(samba_manager, item['path'])
elif item['name'].lower().endswith(('.zip', '.cbz')):
self._process_item(samba_manager, item, is_archive=True)
except Exception as e: comics_logger.error(f"Error scanning {current_path}: {e}")
series_chapters = defaultdict(list) def get_pending_comics(self, samba_manager: SambaManager):
for item in items_in_toberead: try:
if not item["is_directory"]: items = samba_manager.list_path(self.comics_root)
series_name = self._get_series_name(item["name"]) if isinstance(items, dict) and "error" in items: return []
series_chapters[series_name].append(item) return [{"name": i['name'], "path": i['path']} for i in items if i['is_directory'] and i['name'] not in ['.', '..']]
except Exception: return []
def move_series(self, src_samba: SambaManager, series_names, dest_share=None, dest_path=None):
"""Moves folders from isolation/staging to library share."""
share = dest_share if dest_share else self.library_share_default
path = dest_path if dest_path else self.library_path_default
# Connect to destination share
dest_samba = SambaManager(src_samba.server_ip, share, src_samba.username, src_samba.password)
results = {"success": [], "failed": []}
for series in series_names:
src_folder = f"{self.comics_root}/{series}"
target_folder = f"{path}/{series}"
comics_logger.info(f"Found {len(series_chapters)} series to process.") try:
# Ensure target directory exists
dest_samba.create_directory(target_folder)
# List files in source (on isolation share)
items = src_samba.list_path(src_folder)
for item in items:
if item['name'] in ['.', '..']: continue
# Cross-share move: Download -> Upload -> Delete
with tempfile.TemporaryDirectory() as temp_dir:
local_file = os.path.join(temp_dir, item['name'])
with open(local_file, 'wb') as f: src_samba.download_file(item['path'], f)
with open(local_file, 'rb') as f: dest_samba.upload_file(f"{target_folder}/{item['name']}", f)
src_samba.delete_file(item['path'])
src_samba.delete_directory(src_folder)
results["success"].append(series)
comics_logger.info(f"Moved {series} to {share}:{target_folder}")
except Exception as e:
results["failed"].append({"name": series, "error": str(e)})
comics_logger.error(f"Failed to move {series}: {e}")
dest_samba.close()
return results
for series_name, chapters in series_chapters.items(): def update_existing_metadata(self, samba_manager, target_path="/comics/manga", force=False):
comics_logger.info(f"Processing series: {series_name}") """
final_series_folder_path = f"{comics_dest_root}/{series_name}".replace("\\", "/") Scans an existing library directory for series folders and updates/creates metadata files.
"""
comics_logger.info(f"Starting metadata update in {target_path} (Force: {force})...")
try:
items = samba_manager.list_path(target_path)
if isinstance(items, dict) and "error" in items:
comics_logger.error(f"Error listing {target_path}: {items['error']}")
return
for item in items:
if item['name'] in ['.', '..']: continue
if not item['is_directory']: continue
series_name = item['name']
series_path = item['path']
# Check for existing metadata
meta_filename = f"{series_name}_info.txt"
meta_path = f"{series_path}/{meta_filename}"
# Check if meta exists
series_contents = samba_manager.list_path(series_path)
has_meta = False
if isinstance(series_contents, list):
for sub in series_contents:
if sub['name'] == meta_filename:
has_meta = True
break
if has_meta and not force:
continue
comics_logger.info(f"Updating metadata for: {series_name}")
# Parse series info from FOLDER NAME
clean_name = series_name.replace('_', ' ')
clean_name = re.sub(r'\s*\\(\\d+\\)$', '', clean_name).strip()
artist = "Unknown"
artist_match = re.match(r'^\\[(.*?)\\]', clean_name)
if artist_match:
artist = artist_match.group(1)
clean_name = clean_name[artist_match.end():].strip()
doujin_id = None
id_match = re.search(r'\\(\\d{5,7}\\)$', series_name)
if id_match:
doujin_id = id_match.group(1)
# 1. Try Mangadex
api_data = self._lookup_mangadex_metadata(clean_name)
# 2. Try nhentai
if not api_data:
data = self._lookup_doujin_metadata(clean_name, artist, doujin_id)
if data:
data['source'] = "nhentai"
api_data = data
# 3. Try Hentai2Read
if not api_data:
api_data = self._lookup_hentai2read_metadata(clean_name)
# 4. Try MangaUpdates
if not api_data:
api_data = self._lookup_mangaupdates_metadata(clean_name)
meta_content = self._create_metadata_file(clean_name, artist=artist, api_data=api_data)
with tempfile.NamedTemporaryFile(mode='w+', delete=False) as tmp_meta:
tmp_meta.write(meta_content)
tmp_meta.flush()
tmp_meta.seek(0)
with open(tmp_meta.name, 'rb') as f:
samba_manager.upload_file(meta_path, f)
os.unlink(tmp_meta.name)
except Exception as e:
comics_logger.error(f"Error updating metadata in {target_path}: {e}")
def _collect_all_folders(self, samba_manager, path):
folders = []
try:
items = samba_manager.list_path(path)
for item in items:
if item['name'] in ['.', '..']: continue
if item['is_directory']:
folders.append({'name': item['name'], 'path': item['path']})
# Recurse
folders.extend(self._collect_all_folders(samba_manager, item['path']))
except Exception as e:
comics_logger.error(f"Error listing path {path}: {e}")
return folders
def find_similar_folders(self, samba_manager: SambaManager, root_path="/comics/manga", threshold=0.9):
"""
Scans for folders with similar names.
"""
comics_logger.info(f"Scanning for duplicate folders in {root_path}...")
all_dirs = self._collect_all_folders(samba_manager, root_path)
comics_logger.info(f"Found {len(all_dirs)} directories. Comparing...")
groups = []
processed_indices = set()
for i in range(len(all_dirs)):
if i in processed_indices: continue
current_group = [all_dirs[i]]
for j in range(i + 1, len(all_dirs)):
if j in processed_indices: continue
name1 = all_dirs[i]['name'].lower().replace('_', ' ')
name2 = all_dirs[j]['name'].lower().replace('_', ' ')
ratio = difflib.SequenceMatcher(None, name1, name2).ratio()
if ratio >= threshold:
current_group.append(all_dirs[j])
processed_indices.add(j)
if len(current_group) > 1:
groups.append({
"name": all_dirs[i]['name'],
"folders": current_group
})
processed_indices.add(i)
return groups
def delete_folder(self, samba_manager: SambaManager, folder_path):
"""
Deletes a specific folder.
"""
try:
comics_logger.info(f"Deleting duplicate folder: {folder_path}")
samba_manager.delete_directory_recursive(folder_path)
return {"success": True}
except Exception as e:
comics_logger.error(f"Failed to delete folder {folder_path}: {e}")
return {"error": str(e)}
def _get_artist_from_info(self, samba_manager, series_path, series_name):
info_filename = f"{series_name}_info.txt"
info_path = f"{series_path}/{info_filename}"
try:
with tempfile.NamedTemporaryFile(mode='w+b', delete=False) as tmp:
samba_manager.download_file(info_path, tmp)
tmp.seek(0)
content = tmp.read().decode('utf-8', errors='ignore')
# Parse content
artist = "Unknown"
author = "Unknown"
for line in content.splitlines():
if line.startswith("Artist:"):
val = line.split(":", 1)[1].strip()
if val and val.lower() != "unknown":
artist = val.split(',')[0].strip() # Take first artist if multiple
elif line.startswith("Author:"):
val = line.split(":", 1)[1].strip()
if val and val.lower() != "unknown":
author = val.split(',')[0].strip()
if artist != "Unknown": return artist
if author != "Unknown": return author
except Exception as e:
# File might not exist or other error
pass
finally:
if 'tmp' in locals() and os.path.exists(tmp.name):
os.unlink(tmp.name)
return "_Unknown"
def sort_by_artist(self, samba_manager, root_path="/comics/manga"):
comics_logger.info(f"Sorting by Artist in {root_path}...")
try:
items = samba_manager.list_path(root_path)
if isinstance(items, dict) and "error" in items:
comics_logger.error(f"Error listing {root_path}: {items['error']}")
return
for item in items:
if item['name'] in ['.', '..', '_Unknown']: continue
if not item['is_directory']: continue
series_name = item['name']
series_path = item['path']
# Check if this is a Series Folder
# Criteria: Contains .cbz, .zip, or _info.txt
try:
sub_items = samba_manager.list_path(series_path)
if isinstance(sub_items, dict) and "error" in sub_items: continue
is_series = False
for sub in sub_items:
if sub['name'].lower().endswith(('.cbz', '.zip', '_info.txt')):
is_series = True
break
if not is_series:
comics_logger.info(f"Skipping potential Artist folder or empty folder: {series_name}")
continue
except Exception:
continue
# Attempt to get artist
artist = self._get_artist_from_info(samba_manager, series_path, series_name)
# Sanitize artist name for folder
clean_artist = re.sub(r'[<>:"/\\|?*]', '', artist).strip().replace(' ', '_')
if not clean_artist: clean_artist = "_Unknown"
# Target path: /comics/manga/Artist/Series
artist_folder = f"{root_path}/{clean_artist}"
target_path = f"{artist_folder}/{series_name}"
# Skip if already in place
if series_name == clean_artist:
continue
# Check if we are moving into itself (e.g. Root/Artist -> Root/Artist/Artist)
# This happens if 'SeriesName' == 'ArtistName' and it was already sorted?
# But we checked is_series. An Artist folder usually doesn't have cbz inside directly.
comics_logger.info(f"Moving '{series_name}' to Artist folder '{clean_artist}'")
try: try:
samba_manager.create_directory(final_series_folder_path) # Create Artist folder
except OperationFailure as e:
if "FILE_OBJECT_NAME_COLLISION" not in str(e):
comics_logger.error(f"Could not create directory {final_series_folder_path}: {e}")
for chapter_item in chapters:
original_path = chapter_item["path"].replace("\\", "/")
original_filename = chapter_item["name"]
path_to_move = ""
filename_to_move = ""
if original_filename.lower().endswith('.zip'):
cbz_filename = os.path.splitext(original_filename)[0] + '.cbz'
temp_cbz_path = f"{comics_root}/{cbz_filename}".replace("\\", "/")
try:
samba_manager.rename_file(original_path, temp_cbz_path)
path_to_move = temp_cbz_path
filename_to_move = cbz_filename
comics_logger.info(f"Renamed {original_filename} to {cbz_filename}")
except OperationFailure as e:
comics_logger.error(f"Failed to rename ZIP {original_filename}: {e}")
continue
elif original_filename.lower().endswith('.cbz'):
path_to_move = original_path
filename_to_move = original_filename
else:
continue
final_cbz_path = f"{final_series_folder_path}/{filename_to_move}".replace("\\", "/")
try: try:
samba_manager.rename_file(path_to_move, final_cbz_path) samba_manager.create_directory(artist_folder)
comics_logger.info(f"Moved {filename_to_move} to {final_series_folder_path}") except OperationFailure: pass # Exists
except OperationFailure as e:
comics_logger.error(f"Failed to move {filename_to_move} to {final_series_folder_path}: {e}") # Move Series folder
samba_manager.rename_file(series_path, target_path)
except Exception as e:
comics_logger.error(f"Failed to move {series_name}: {e}")
except Exception as e: except Exception as e:
comics_logger.error(f"An error occurred during comics organization: {e}", exc_info=True) comics_logger.error(f"Sort by artist failed: {e}")
comics_logger.info("Comics organization process finished.")
return
+44 -4
View File
@@ -2,6 +2,8 @@ from pydantic_settings import BaseSettings, SettingsConfigDict
import subprocess import subprocess
import logging import logging
import shutil import shutil
import json
import os
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -14,6 +16,8 @@ logging.basicConfig(
] ]
) )
SETTINGS_FILE = "resources/config/app_settings.json"
def check_gpu_support(): def check_gpu_support():
try: try:
logger.debug("Checking ffmpeg for CUDA support...") logger.debug("Checking ffmpeg for CUDA support...")
@@ -29,12 +33,48 @@ def check_gpu_support():
return False return False
class Settings(BaseSettings): class Settings(BaseSettings):
samba_server_ip: str samba_server_ip: str = "127.0.0.1"
samba_username: str samba_username: str = "guest"
samba_password: str samba_password: str = ""
comicvine_api_key: str | None = None
qbittorrent_url: str | None = None
qbittorrent_username: str | None = None
qbittorrent_password: str | None = None
gpu_enabled: bool = check_gpu_support() gpu_enabled: bool = check_gpu_support()
# Defaults
default_comics_path: str = "/comics/manga"
default_videos_path: str = "/videos"
model_config = SettingsConfigDict(env_file=".env") # Stash Integration
stash_enabled: bool = False
stash_db_path: str = "stash.sqlite"
stash_remote_db_path: str = "/appdata/stashapp/config/stash-go.sqlite"
stash_generated_path: str = "/media/stashapp/generated"
stash_remote_base: str = "/media/videos"
stash_container_base: str = "/data"
stash_share: str = "main"
model_config = SettingsConfigDict(env_file=".env", extra="ignore")
def load_from_json(self):
if os.path.exists(SETTINGS_FILE):
try:
with open(SETTINGS_FILE, 'r') as f:
data = json.load(f)
for key, value in data.items():
if hasattr(self, key):
setattr(self, key, value)
except Exception as e:
logger.error(f"Failed to load settings: {e}")
def save_to_json(self):
try:
os.makedirs(os.path.dirname(SETTINGS_FILE), exist_ok=True)
with open(SETTINGS_FILE, 'w') as f:
json.dump(self.model_dump(), f, indent=4)
except Exception as e:
logger.error(f"Failed to save settings: {e}")
settings = Settings() settings = Settings()
settings.load_from_json()
@@ -2,54 +2,96 @@ import logging
import os import os
import subprocess import subprocess
import tempfile import tempfile
import json
import time
import itertools
from PIL import Image from PIL import Image
import imagehash import imagehash
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from .samba_manager import SambaManager from .samba_manager import SambaManager
from .config import Settings from .config import Settings
from .stash_service import StashService
from . import models from . import models
import itertools
import logging
import time
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class CancellationException(Exception):
pass
class CancellableWriter:
def __init__(self, file_obj, check_cancel_func):
self.file_obj = file_obj
self.check_cancel_func = check_cancel_func
def write(self, data):
if self.check_cancel_func():
raise CancellationException("Scan canceled by user")
return self.file_obj.write(data)
def close(self):
return self.file_obj.close()
def flush(self):
return self.file_obj.flush()
def tell(self):
return self.file_obj.tell()
def seek(self, offset, whence=0):
return self.file_obj.seek(offset, whence)
class DuplicatesManager: class DuplicatesManager:
def __init__(self, settings: Settings, db: Session): def __init__(self, settings: Settings, db: Session):
self.settings = settings self.settings = settings
self.db = db self.db = db
self.videos_root = "/videos" self.stash_service = StashService(settings)
self.exclusions_file = "resources/config/exclusions.json"
self._load_exclusions()
def _load_exclusions(self):
if os.path.exists(self.exclusions_file):
try:
with open(self.exclusions_file, 'r') as f:
self.exclusions = json.load(f)
except:
self.exclusions = []
else:
self.exclusions = []
def _save_exclusions(self):
os.makedirs(os.path.dirname(self.exclusions_file), exist_ok=True)
with open(self.exclusions_file, 'w') as f:
json.dump(self.exclusions, f)
def add_exclusion(self, path):
if path not in self.exclusions:
self.exclusions.append(path)
self._save_exclusions()
def remove_exclusion(self, path):
if path in self.exclusions:
self.exclusions.remove(path)
self._save_exclusions()
def _is_excluded(self, path):
for excl in self.exclusions:
if path.startswith(excl):
return True
return False
def _get_video_duration(self, filepath): def _get_video_duration(self, filepath):
logger.debug(f"Running ffprobe for duration of {filepath}")
try: try:
command = [ command = [
"ffprobe", "ffprobe", "-v", "error", "-show_entries", "format=duration",
"-v", "-of", "default=noprint_wrappers=1:nokey=1", filepath
"error",
"-show_entries",
"format=duration",
"-of",
"default=noprint_wrappers=1:nokey=1",
filepath,
] ]
logger.debug(f"ffprobe command: {' '.join(command)}") result = subprocess.run(command, capture_output=True, text=True, check=True)
result = subprocess.run(
command,
capture_output=True,
text=True,
check=True,
)
logger.debug(f"ffprobe stdout: {result.stdout.strip()}")
logger.debug(f"ffprobe stderr: {result.stderr.strip()}")
return float(result.stdout) return float(result.stdout)
except (subprocess.CalledProcessError, FileNotFoundError) as e: except Exception as e:
logger.error(f"ffprobe failed for {filepath}: {e}") logger.error(f"ffprobe failed for {filepath}: {e}")
return None return None
def _get_frame_hash(self, filepath): def _get_frame_hash(self, filepath, algorithm='phash'):
logger.debug(f"Running ffmpeg for frame hash of {filepath}")
tmp_frame_path = "" tmp_frame_path = ""
try: try:
with tempfile.NamedTemporaryFile(suffix=".jpg", delete=False) as tmp_frame: with tempfile.NamedTemporaryFile(suffix=".jpg", delete=False) as tmp_frame:
@@ -57,264 +99,390 @@ class DuplicatesManager:
command = ["ffmpeg"] command = ["ffmpeg"]
if self.settings.gpu_enabled: if self.settings.gpu_enabled:
command.extend(["-hwaccel", "cuda"]) command.extend(["-hwaccel", "cuda"])
command.extend([
"-i",
filepath,
"-ss",
"00:00:10",
"-vframes",
"1",
"-y",
tmp_frame_path,
])
logger.debug(f"ffmpeg command: {' '.join(command)}") # Extract frame at 10s or 10%? Fixed 10s for now.
result = subprocess.run( command.extend(["-i", filepath, "-ss", "00:00:10", "-vframes", "1", "-y", tmp_frame_path])
command,
capture_output=True, subprocess.run(command, capture_output=True, check=True)
check=True,
)
logger.debug(f"ffmpeg stdout: {result.stdout.strip()}")
logger.debug(f"ffmpeg stderr: {result.stderr.strip()}")
if os.path.exists(tmp_frame_path): if os.path.exists(tmp_frame_path) and os.path.getsize(tmp_frame_path) > 0:
logger.debug(f"Temporary frame file exists: {tmp_frame_path}, size: {os.path.getsize(tmp_frame_path)} bytes") img = Image.open(tmp_frame_path)
phash = imagehash.phash(Image.open(tmp_frame_path)) if algorithm == 'ahash': h = imagehash.average_hash(img)
return str(phash) elif algorithm == 'dhash': h = imagehash.dhash(img)
else: else: h = imagehash.phash(img)
logger.warning(f"Temporary frame file was not created: {tmp_frame_path}") return str(h)
return None except Exception as e:
except (subprocess.CalledProcessError, FileNotFoundError) as e: logger.error(f"Hash generation failed for {filepath}: {e}")
logger.error(f"ffmpeg failed for {filepath}: {e}")
return None
except Image.UnidentifiedImageError as e:
logger.error(f"PIL.UnidentifiedImageError for {filepath} with temp file {tmp_frame_path}: {e}")
return None
finally: finally:
if os.path.exists(tmp_frame_path): if os.path.exists(tmp_frame_path):
os.remove(tmp_frame_path) os.remove(tmp_frame_path)
return None
def _is_video_file(self, filename): def generate_contact_sheet(self, video_path):
video_extensions = ['.mp4', '.mkv', '.avi', '.mov', '.wmv', '.flv', '.webm'] """
return any(filename.lower().endswith(ext) for ext in video_extensions) Generates a 3x3 contact sheet for the video.
Returns (relative_path, phash_str)
"""
try:
duration = self._get_video_duration(video_path)
if not duration or duration < 10: return None, None
# Extract 9 frames at intervals
interval = duration / 10
timestamps = [interval * i for i in range(1, 10)]
# We use a temp dir to store frames, then stitch
# ffmpeg tile filter is good but seeking is faster for sparse frames on large files?
# Actually, `ffmpeg -i ... -vf fps=... tile=...` reads the whole file which is slow over network/SMB.
# Best to seek.
# Since we have the file locally in tmp_path (downloaded), seeking is fast.
frames = []
with tempfile.TemporaryDirectory() as temp_frames_dir:
for idx, ts in enumerate(timestamps):
out_frame = os.path.join(temp_frames_dir, f"frame_{idx}.jpg")
# fast seek
subprocess.run(
["ffmpeg", "-ss", str(ts), "-i", video_path, "-vframes", "1", "-q:v", "5", "-vf", "scale=320:-1", "-y", out_frame],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=False
)
if os.path.exists(out_frame):
frames.append(Image.open(out_frame))
if len(frames) < 4: return None, None # Need at least some frames
# Stitch 3x3 (or adaptive)
# Create blank image
w, h = frames[0].size
grid_w = w * 3
grid_h = h * 3
contact_sheet = Image.new('RGB', (grid_w, grid_h))
for idx, frame in enumerate(frames):
if idx >= 9: break
x = (idx % 3) * w
y = (idx // 3) * h
contact_sheet.paste(frame, (x, y))
# Save
cache_dir = "resources/cache/thumbnails"
os.makedirs(cache_dir, exist_ok=True)
# Use hash of path to ensure uniqueness/retrievability
filename_hash = imagehash.hex_to_hash(os.path.basename(video_path)) # Just use random or md5
import hashlib
file_hash = hashlib.md5(video_path.encode()).hexdigest()
out_name = f"{file_hash}.jpg"
out_path = os.path.join(cache_dir, out_name)
contact_sheet.save(out_path, "JPEG", quality=80)
# Calculate Hash of the SHEET
sheet_hash = imagehash.phash(contact_sheet)
return out_name, str(sheet_hash)
except Exception as e:
logger.error(f"Contact sheet generation failed: {e}")
return None, None
def _process_video_file(self, samba_manager, filepath, filename, size, algorithm='phash', scan_type='fast', log_func=None, cancel_check_func=None):
if cancel_check_func and cancel_check_func(): return
if self._is_excluded(filepath):
return
def _process_video_file(self, samba_manager: SambaManager, filepath, filename, size):
logger.info(f"Processing video: {filepath} ({filename})")
existing_video = self.db.query(models.VideoFile).filter_by(filepath=filepath).first() existing_video = self.db.query(models.VideoFile).filter_by(filepath=filepath).first()
# If in scene mode, check if we already have the scene data
if existing_video: if existing_video:
if existing_video.size == size: if scan_type == 'scene' and not existing_video.contact_sheet_path:
logger.info(f"Skipping already processed and unaltered video: {filepath}") if log_func: log_func(f"Updating {filename} with contact sheet")
return # Continue to processing
elif existing_video.size == size:
return # Skip if unchanged
else: else:
logger.info(f"File {filepath} has altered size ({existing_video.size} -> {size}). Re-processing.")
self.db.delete(existing_video) self.db.delete(existing_video)
self.db.commit() self.db.commit()
existing_video = None
if log_func: log_func(f"Processing: {filename} (Mode: {scan_type})")
# --- Stash Integration ---
if self.settings.stash_enabled:
if cancel_check_func and cancel_check_func(): return
stash_phash, stash_oshash, scene_id, stash_duration = self.stash_service.get_file_metadata(filepath)
if stash_phash:
if log_func: log_func(f"Found Stash metadata for {filename}")
sheet_path = None
if scan_type == 'scene' and stash_oshash:
if cancel_check_func and cancel_check_func(): return
remote_sprite = self.stash_service.get_sprite_path(stash_oshash)
local_sheet_name = f"stash_{stash_oshash}.jpg"
local_sheet_path = os.path.join("resources/cache/thumbnails", local_sheet_name)
if not os.path.exists(local_sheet_path):
try:
# Ensure directory exists
os.makedirs(os.path.dirname(local_sheet_path), exist_ok=True)
# Use stash_share for sprite download
stash_samba = samba_manager
if samba_manager.share_name != self.settings.stash_share:
stash_samba = SambaManager(
self.settings.samba_server_ip,
self.settings.stash_share,
self.settings.samba_username,
self.settings.samba_password
)
try:
with open(local_sheet_path, "wb") as f:
stash_samba.download_file(remote_sprite, f)
sheet_path = local_sheet_name
finally:
if stash_samba != samba_manager:
stash_samba.close()
except Exception as e:
if log_func: log_func(f"Failed to download Stash sprite for {filename}: {e}")
else:
sheet_path = local_sheet_name
# Save to DB
video = models.VideoFile(
filepath=filepath, filename=filename, size=size,
duration=stash_duration or 0, phash=stash_phash,
contact_sheet_path=sheet_path,
scene_phash=None
)
self.db.add(video)
self.db.commit()
return
# --- End Stash Integration ---
if cancel_check_func and cancel_check_func(): return
# Download to temp
with tempfile.NamedTemporaryFile(delete=False) as tmp_video: with tempfile.NamedTemporaryFile(delete=False) as tmp_video:
try: try:
samba_manager.download_file(filepath, tmp_video) if log_func: log_func(f"Downloading {filename} ({size/1024/1024:.2f} MB)...")
# Wrap for cancellation during download
writer = tmp_video
if cancel_check_func:
writer = CancellableWriter(tmp_video, cancel_check_func)
samba_manager.download_file(filepath, writer)
tmp_path = tmp_video.name
except CancellationException:
if log_func: log_func(f"Download aborted for {filename}")
tmp_video.close()
os.remove(tmp_video.name)
return
except Exception as e: except Exception as e:
logger.error(f"Failed to download {filepath}: {e}") if log_func: log_func(f"Download failed: {filepath} - {e}")
tmp_video.close()
os.remove(tmp_video.name)
return return
duration = self._get_video_duration(tmp_video.name)
phash = self._get_frame_hash(tmp_video.name)
os.remove(tmp_video.name)
if duration is not None and phash is not None:
video_file = models.VideoFile(
filepath=filepath,
filename=filename,
size=size,
duration=duration,
phash=phash,
)
self.db.add(video_file)
self.db.commit()
logger.info(f"Processed video: {filepath}")
else:
logger.warning(f"Could not get duration or hash for {filepath}")
def scan_videos(self, samba_manager: SambaManager):
state_file = "logs/duplicates_scan.state"
progress_file = "logs/duplicates_scan.progress"
pause_file = "logs/duplicates_scan.pause"
dirs_to_scan = []
try: try:
if os.path.exists(progress_file): if cancel_check_func and cancel_check_func(): return
with open(progress_file, 'r') as f:
last_processed_dir = f.read().strip()
logger.info(f"Resuming scan from last in-progress directory: {last_processed_dir}")
dirs_to_scan.append(last_processed_dir)
if os.path.exists(state_file):
with open(state_file, 'r') as f:
dirs_to_scan.extend([line.strip() for line in f if line.strip()])
logger.info(f"Loaded {len(dirs_to_scan)} directories from state file.")
if not dirs_to_scan: duration = self._get_video_duration(tmp_path)
dirs_to_scan = [self.videos_root] phash = self._get_frame_hash(tmp_path, algorithm)
if cancel_check_func and cancel_check_func(): return
logger.info(f"Starting scan for videos in {self.videos_root} on share 'isolation'") sheet_path = None
scene_hash = None
if scan_type == 'scene':
sheet_path, scene_hash = self.generate_contact_sheet(tmp_path)
if duration is not None: # phash might be None if image generation failed
if not existing_video:
video = models.VideoFile(
filepath=filepath, filename=filename, size=size,
duration=duration, phash=phash or "",
contact_sheet_path=sheet_path,
scene_phash=scene_hash
)
self.db.add(video)
else:
# Update existing
existing_video.duration = duration
existing_video.phash = phash or ""
if sheet_path: existing_video.contact_sheet_path = sheet_path
if scene_hash: existing_video.scene_phash = scene_hash
self.db.commit()
finally:
if os.path.exists(tmp_path):
try: os.remove(tmp_path)
except: pass
def scan_videos(self, samba_manager, root_paths=["/videos"], algorithm='phash', scan_type='fast'):
state_file = "logs/duplicates_scan.state"
progress_file = "logs/duplicates_scan.json"
cancel_file = "logs/duplicates_scan.cancel"
log_file = "logs/duplicates_scan.log"
# Helper to log to file and console
def log(msg):
try:
with open(log_file, "a") as f:
f.write(f"{msg}\n")
except: pass
logger.info(msg)
is_cancelled = False
# Helper to check cancellation
def check_cancel():
nonlocal is_cancelled
if is_cancelled: return True
if os.path.exists(cancel_file):
log("Scan canceled by user.")
try:
os.remove(cancel_file)
except OSError:
pass
with open(progress_file, 'w') as f:
json.dump({"status": "canceled", "processed": processed_files}, f)
is_cancelled = True
return True
return False
# Clear log file
with open(log_file, "w") as f:
f.write("Scan started...\n")
# Ensure cancel file is gone before starting
if os.path.exists(cancel_file):
try: os.remove(cancel_file)
except OSError: pass
dirs_to_scan = list(root_paths)
processed_files = 0
try:
while dirs_to_scan: while dirs_to_scan:
while os.path.exists(pause_file): if check_cancel(): return
logger.info("Scan is paused. Waiting for resume signal...")
time.sleep(5)
current_path = dirs_to_scan.pop(0) current_path = dirs_to_scan.pop(0)
with open(progress_file, 'w') as f: with open(progress_file, 'w') as f:
f.write(current_path) json.dump({"status": "scanning", "current": current_path, "processed": processed_files}, f)
logger.info(f"Scanning directory: {current_path}") try:
files_and_dirs = samba_manager.list_path(current_path) log(f"Scanning directory: {current_path}")
items = samba_manager.list_path(current_path)
if "error" in files_and_dirs: except Exception as e:
logger.error(f"Failed to list path {current_path}: {files_and_dirs['error']}") log(f"Error listing {current_path}: {e}")
continue continue
subdirs = [] for item in items:
for item in files_and_dirs: if check_cancel(): return
while os.path.exists(pause_file):
logger.info("Scan is paused. Waiting for resume signal...")
time.sleep(5)
if item["is_directory"]: if item['name'] in ['.', '..']: continue
subdirs.append(item["path"])
elif self._is_video_file(item["name"]): if item['is_directory']:
self._process_video_file(samba_manager, item["path"], item["name"], item["size"]) if not self._is_excluded(item['path']):
dirs_to_scan.append(item['path'])
elif any(item['name'].lower().endswith(ext) for ext in ['.mp4', '.mkv', '.avi', '.mov', '.wmv']):
self._process_video_file(
samba_manager, item['path'], item['name'], item['size'],
algorithm, scan_type,
log_func=log, cancel_check_func=check_cancel
)
processed_files += 1
with open(progress_file, 'w') as f:
json.dump({"status": "scanning", "current": item['path'], "processed": processed_files}, f)
log("Scan completed.")
with open(progress_file, 'w') as f:
json.dump({"status": "completed", "processed": processed_files}, f)
dirs_to_scan = subdirs + dirs_to_scan
if os.path.exists(progress_file):
os.remove(progress_file)
with open(state_file, 'w') as f:
for d in dirs_to_scan:
f.write(d + '\n')
if os.path.exists(state_file):
os.remove(state_file)
logger.info("Video scan complete.")
return {"status": "Scan complete"}
except Exception as e: except Exception as e:
logger.error(f"An error occurred during video scan: {e}", exc_info=True) log(f"Scan failed: {e}")
return {"status": "Scan failed", "error": str(e)} with open(progress_file, 'w') as f:
json.dump({"status": "failed", "error": str(e)}, f)
def _hamming_distance(self, s1, s2): def find_duplicates(self, threshold=0.95, method='fast'):
return sum(c1 != c2 for c1, c2 in zip(s1, s2))
def _calculate_similarity(self, file1: models.VideoFile, file2: models.VideoFile):
size_similarity = 1 - (abs(file1.size - file2.size) / max(file1.size, file2.size))
duration_similarity = 1 - (abs(file1.duration - file2.duration) / max(file1.duration, file2.duration))
hash_similarity = 1 - (self._hamming_distance(file1.phash, file2.phash) / len(file1.phash))
return (size_similarity * 0.2) + (duration_similarity * 0.3) + (hash_similarity * 0.5)
def find_duplicates(self, threshold=0.95):
report = models.DuplicateReport(status="running") report = models.DuplicateReport(status="running")
self.db.add(report) self.db.add(report)
self.db.commit() self.db.commit()
videos = self.db.query(models.VideoFile).all() videos = self.db.query(models.VideoFile).all()
groups = [] groups = []
processed_videos = set() processed_ids = set()
for video1, video2 in itertools.combinations(videos, 2): for i in range(len(videos)):
if video1.id in processed_videos or video2.id in processed_videos: if videos[i].id in processed_ids: continue
continue
group = [videos[i]]
score = self._calculate_similarity(video1, video2) scores = []
if score >= threshold:
existing_group = None for j in range(i + 1, len(videos)):
for group in groups: if videos[j].id in processed_ids: continue
if video1.id in group["video_ids"] or video2.id in group["video_ids"]:
existing_group = group
break
if existing_group: v1, v2 = videos[i], videos[j]
existing_group["video_ids"].add(video1.id)
existing_group["video_ids"].add(video2.id) score = 0
existing_group["scores"].append(score) if method == 'scene' and v1.scene_phash and v2.scene_phash:
# Compare Scene Hashes
dist = imagehash.hex_to_hash(v1.scene_phash) - imagehash.hex_to_hash(v2.scene_phash)
score = max(0, 1.0 - (dist / 64.0)) # 64 is typical max distance for 8x8 hash
else: else:
groups.append({"video_ids": {video1.id, video2.id}, "scores": [score]}) # Standard Comparison
dist = imagehash.hex_to_hash(v1.phash) - imagehash.hex_to_hash(v2.phash) if v1.phash and v2.phash else 64
hash_sim = max(0, 1.0 - (dist / 64.0))
dur_sim = 1.0 - (abs(v1.duration - v2.duration) / max(v1.duration, v2.duration)) if max(v1.duration, v2.duration) > 0 else 1.0
size_sim = 1.0 - (abs(v1.size - v2.size) / max(v1.size, v2.size)) if max(v1.size, v2.size) > 0 else 1.0
score = (hash_sim * 0.6) + (dur_sim * 0.3) + (size_sim * 0.1)
processed_videos.add(video1.id) if score >= threshold:
processed_videos.add(video2.id) group.append(v2)
scores.append(score)
for group_data in groups: processed_ids.add(v2.id)
avg_score = sum(group_data["scores"]) / len(group_data["scores"])
db_group = models.DuplicateFileGroup(report_id=report.id, score=avg_score) if len(group) > 1:
self.db.add(db_group) processed_ids.add(videos[i].id)
self.db.commit() avg_score = sum(scores) / len(scores)
for video_id in group_data["video_ids"]: db_group = models.DuplicateFileGroup(report_id=report.id, score=avg_score)
db_file = models.DuplicateFile(group_id=db_group.id, video_file_id=video_id) self.db.add(db_group)
self.db.add(db_file) self.db.commit()
for v in group:
db_file = models.DuplicateFile(group_id=db_group.id, video_file_id=v.id)
self.db.add(db_file)
report.status = "completed" report.status = "completed"
self.db.commit() self.db.commit()
return {"report_id": report.id}
return {"report_id": report.id, "status": "completed"}
def get_reports(self): def get_reports(self):
return self.db.query(models.DuplicateReport).all() return self.db.query(models.DuplicateReport).order_by(models.DuplicateReport.created_at.desc()).all()
def get_duplicate_report(self, report_id: int): def get_report(self, report_id):
report = self.db.query(models.DuplicateReport).filter(models.DuplicateReport.id == report_id).first() report = self.db.query(models.DuplicateReport).filter_by(id=report_id).first()
if not report: if not report: return None
return {"error": "Report not found"}
groups = []
for group in report.groups:
files = []
for duplicate_file in group.files:
files.append(duplicate_file.video_file)
groups.append({
"group_id": group.id,
"score": group.score,
"files": files,
})
return { res = {
"report_id": report.id, "id": report.id,
"created_at": report.created_at,
"status": report.status, "status": report.status,
"groups": groups, "date": report.created_at,
"groups": []
} }
for g in report.groups:
def delete_files(self, filepaths: list[str]): files = [{
samba_manager = SambaManager( "id": f.video_file.id,
self.settings.samba_server_ip, "path": f.video_file.filepath,
"isolation", # Assuming all duplicates are in the isolation share "size": f.video_file.size,
self.settings.samba_username, "duration": f.video_file.duration,
self.settings.samba_password, "contact_sheet": f.video_file.contact_sheet_path
) } for f in g.files]
try: res["groups"].append({"id": g.id, "score": g.score, "files": files})
for filepath in filepaths: return res
# Delete from Samba
samba_manager.delete_file(filepath)
# Delete from database
video_file = self.db.query(models.VideoFile).filter_by(filepath=filepath).first()
if video_file:
# Delete associations in DuplicateFile
self.db.query(models.DuplicateFile).filter_by(video_file_id=video_file.id).delete()
self.db.delete(video_file)
self.db.commit()
return {"status": "success"}
except Exception as e:
self.db.rollback()
logger.error(f"Error deleting files: {e}", exc_info=True)
return {"status": "error", "message": str(e)}
finally:
samba_manager.close()
+33 -8
View File
@@ -1,20 +1,39 @@
from fastapi import FastAPI, Depends import os
import json
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from contextlib import asynccontextmanager
from . import models from . import models
from .database import SessionLocal, engine, get_db from .database import SessionLocal, engine, get_db
from .routers import samba, comics, duplicates, downloader from .routers import samba, comics, duplicates, downloader, qbittorrent, system, tasks, settings, stash, scheduler as scheduler_router
from .scheduler import start_scheduler, scheduler
models.Base.metadata.create_all(bind=engine) models.Base.metadata.create_all(bind=engine)
app = FastAPI() def reset_stuck_scans():
progress_file = "logs/duplicates_scan.json"
if os.path.exists(progress_file):
try:
with open(progress_file, 'r') as f:
data = json.load(f)
if data.get("status") == "scanning":
with open(progress_file, 'w') as f:
json.dump({"status": "failed", "error": "Scan interrupted by server restart"}, f)
except Exception:
pass
origins = [ @asynccontextmanager
"http://localhost:5173", # Assuming frontend runs on this port during development async def lifespan(app: FastAPI):
"http://127.0.0.1:5173", reset_stuck_scans()
# Add your production frontend URL(s) here when deploying start_scheduler()
] yield
scheduler.shutdown()
app = FastAPI(lifespan=lifespan)
origins = ["*"]
app.add_middleware( app.add_middleware(
CORSMiddleware, CORSMiddleware,
@@ -28,6 +47,12 @@ app.include_router(samba.router, prefix="/samba", tags=["samba"])
app.include_router(comics.router, prefix="/comics", tags=["comics"]) app.include_router(comics.router, prefix="/comics", tags=["comics"])
app.include_router(duplicates.router, prefix="/duplicates", tags=["duplicates"]) app.include_router(duplicates.router, prefix="/duplicates", tags=["duplicates"])
app.include_router(downloader.router, prefix="/downloader", tags=["downloader"]) app.include_router(downloader.router, prefix="/downloader", tags=["downloader"])
app.include_router(qbittorrent.router, prefix="/qbittorrent", tags=["qbittorrent"])
app.include_router(system.router, prefix="/system", tags=["system"])
app.include_router(tasks.router, prefix="/tasks", tags=["tasks"])
app.include_router(settings.router, prefix="/settings", tags=["settings"])
app.include_router(stash.router, prefix="/stash", tags=["stash"])
app.include_router(scheduler_router.router, prefix="/scheduler", tags=["scheduler"])
+14
View File
@@ -22,6 +22,8 @@ class VideoFile(Base):
size = Column(Integer) size = Column(Integer)
duration = Column(Float) duration = Column(Float)
phash = Column(String) phash = Column(String)
contact_sheet_path = Column(String, nullable=True)
scene_phash = Column(String, nullable=True)
class DuplicateFileGroup(Base): class DuplicateFileGroup(Base):
@@ -42,3 +44,15 @@ class DuplicateFile(Base):
video_file_id = Column(Integer, ForeignKey("video_files.id")) video_file_id = Column(Integer, ForeignKey("video_files.id"))
group = relationship("DuplicateFileGroup", back_populates="files") group = relationship("DuplicateFileGroup", back_populates="files")
video_file = relationship("VideoFile") video_file = relationship("VideoFile")
class TaskHistory(Base):
__tablename__ = "task_history"
id = Column(Integer, primary_key=True, index=True)
task_id = Column(String, index=True) # Huey Task ID
name = Column(String)
status = Column(String) # running, success, failed
start_time = Column(DateTime, default=datetime.datetime.utcnow)
end_time = Column(DateTime, nullable=True)
details = Column(String, nullable=True)
@@ -0,0 +1,70 @@
import qbittorrentapi
import logging
from .config import settings
logger = logging.getLogger(__name__)
class QbittorrentManager:
def __init__(self):
self.url = settings.qbittorrent_url
self.username = settings.qbittorrent_username
self.password = settings.qbittorrent_password
self.client = None
def connect(self):
if not self.url:
raise Exception("qBittorrent URL not configured")
try:
self.client = qbittorrentapi.Client(
host=self.url,
username=self.username,
password=self.password,
VERIFY_WEBUI_CERTIFICATE=False
)
self.client.auth_log_in()
except qbittorrentapi.LoginFailed as e:
logger.error(f"qBittorrent Login Failed: {e}")
raise Exception("qBittorrent Login Failed")
except Exception as e:
logger.error(f"qBittorrent Connection Failed: {e}")
raise
def get_torrents(self):
self.connect()
# Return list of dicts with relevant info
torrents = self.client.torrents_info()
return [
{
"hash": t.hash,
"name": t.name,
"state": t.state,
"save_path": t.save_path,
"progress": t.progress,
"size": t.size,
"dlspeed": t.dlspeed,
"upspeed": t.upspeed,
"eta": t.eta
}
for t in torrents
]
def pause_torrents(self, hashes):
self.connect()
self.client.torrents_pause(torrent_hashes=hashes)
def resume_torrents(self, hashes):
self.connect()
self.client.torrents_resume(torrent_hashes=hashes)
def delete_torrents(self, hashes, delete_files=False):
self.connect()
self.client.torrents_delete(torrent_hashes=hashes, delete_files=delete_files)
def recheck_torrents(self, hashes):
self.connect()
self.client.torrents_recheck(torrent_hashes=hashes)
def set_location(self, hashes, location):
self.connect()
self.client.torrents_set_location(location=location, torrent_hashes=hashes)
+103 -28
View File
@@ -1,40 +1,115 @@
from fastapi import APIRouter, BackgroundTasks from fastapi import APIRouter, HTTPException, WebSocket, WebSocketDisconnect
from pydantic import BaseModel
from typing import List, Optional
import asyncio
import os
from ..comics_manager import ComicsManager from ..comics_manager import ComicsManager
from ..config import settings from ..config import settings
from ..samba_manager import SambaManager from ..samba_manager import SambaManager
from ..tasks import task_organize_comics, task_move_comics, task_update_metadata, task_sort_by_artist
router = APIRouter() router = APIRouter()
def run_organize_comics_background(): class MoveRequest(BaseModel):
app_settings = settings series_names: List[str]
manager = ComicsManager() destination_share: Optional[str] = "comics"
samba_manager = SambaManager( destination_path: Optional[str] = "/manga"
app_settings.samba_server_ip,
"isolation", class MetadataUpdateRequest(BaseModel):
app_settings.samba_username, target_path: Optional[str] = "/comics/manga"
app_settings.samba_password, force_update: Optional[bool] = False
class FolderScanRequest(BaseModel):
root_path: Optional[str] = "/comics/manga"
threshold: float = 0.9
class DeleteFolderRequest(BaseModel):
folder_path: str
class SortRequest(BaseModel):
root_path: Optional[str] = "/comics/manga"
def get_samba_manager(share="isolation"):
return SambaManager(
settings.samba_server_ip,
share,
settings.samba_username,
settings.samba_password,
) )
manager.organize_comics(samba_manager)
samba_manager.close()
@router.post("/organize") @router.post("/organize")
def organize_comics(background_tasks: BackgroundTasks): def organize_comics():
background_tasks.add_task(run_organize_comics_background) task = task_organize_comics()
return {"message": "Comics organization started in the background."} return {"message": f"Organization queued (Task ID: {task.id}). Check logs."}
def run_cleanup_toberead_background(): @router.get("/pending")
app_settings = settings def get_pending_comics():
manager = ComicsManager() manager = ComicsManager()
samba_manager = SambaManager( samba_manager = get_samba_manager("isolation")
app_settings.samba_server_ip, try:
"isolation", return manager.get_pending_comics(samba_manager)
app_settings.samba_username, finally:
app_settings.samba_password, samba_manager.close()
)
manager.cleanup_toberead(samba_manager)
samba_manager.close()
@router.delete("/cleanup_toberead") @router.post("/move")
def cleanup_toberead(background_tasks: BackgroundTasks): def move_comics(request: MoveRequest):
background_tasks.add_task(run_cleanup_toberead_background) task = task_move_comics(request.series_names, request.destination_share, request.destination_path)
return {"message": "Comics cleanup started in the background."} return {"message": f"Move queued (Task ID: {task.id}). Check logs."}
@router.post("/update_metadata")
def update_metadata(request: MetadataUpdateRequest):
task = task_update_metadata(request.target_path, request.force_update)
return {"message": f"Metadata update queued (Task ID: {task.id}). Check logs."}
@router.post("/folders/scan")
def scan_folders(request: FolderScanRequest):
# This remains synchronous to return results immediately to UI
manager = ComicsManager()
samba_manager = get_samba_manager("isolation")
try:
groups = manager.find_similar_folders(samba_manager, request.root_path, request.threshold)
return groups
finally:
samba_manager.close()
@router.post("/folders/delete")
def delete_folder(request: DeleteFolderRequest):
manager = ComicsManager()
samba_manager = get_samba_manager("isolation")
try:
return manager.delete_folder(samba_manager, request.folder_path)
finally:
samba_manager.close()
@router.post("/sort_by_artist")
def sort_by_artist(request: SortRequest):
task = task_sort_by_artist(request.root_path)
return {"message": f"Sort by Artist queued (Task ID: {task.id}). Check logs."}
@router.websocket("/ws/logs")
async def websocket_endpoint(websocket: WebSocket):
await websocket.accept()
file_path = "logs/comics_organization.log"
# Ensure file exists
if not os.path.exists(file_path):
os.makedirs(os.path.dirname(file_path), exist_ok=True)
with open(file_path, "w") as f: f.write("")
try:
with open(file_path, "r") as f:
# Send last 20 lines for context
lines = f.readlines()
for line in lines[-20:]:
await websocket.send_text(line)
# Follow the file
f.seek(0, 2)
while True:
line = f.readline()
if line:
await websocket.send_text(line)
else:
await asyncio.sleep(0.1)
except WebSocketDisconnect:
pass
@@ -1,19 +1,30 @@
from fastapi import APIRouter, Depends, BackgroundTasks from fastapi import APIRouter, Depends, BackgroundTasks, HTTPException
from fastapi.responses import FileResponse
from sqlalchemy.orm import Session from sqlalchemy.orm import Session
from pydantic import BaseModel from pydantic import BaseModel
from ..duplicates_manager import DuplicatesManager from ..duplicates_manager import DuplicatesManager
from ..config import settings, Settings from ..config import settings, Settings
from ..database import get_db, SessionLocal from ..database import get_db, SessionLocal
from ..samba_manager import SambaManager from ..samba_manager import SambaManager
from .. import models
import os import os
import json
from typing import List from typing import List
router = APIRouter() router = APIRouter()
class DeleteFilesRequest(BaseModel): class ScanConfig(BaseModel):
filepaths: List[str] paths: List[str] = ["/videos"]
algorithm: str = "phash"
scan_type: str = "fast" # fast or scene
def run_scan_videos_background(): class DeleteRequest(BaseModel):
ids: List[int]
class ExclusionRequest(BaseModel):
path: str
def run_scan_videos_background(paths, algorithm, scan_type):
db = SessionLocal() db = SessionLocal()
app_settings = Settings() app_settings = Settings()
manager = DuplicatesManager(app_settings, db) manager = DuplicatesManager(app_settings, db)
@@ -24,42 +35,72 @@ def run_scan_videos_background():
app_settings.samba_password, app_settings.samba_password,
) )
try: try:
manager.scan_videos(samba_manager) manager.scan_videos(samba_manager, paths, algorithm, scan_type)
finally: finally:
samba_manager.close() samba_manager.close()
db.close() db.close()
@router.post("/scan") @router.post("/scan/start")
def scan_videos(background_tasks: BackgroundTasks): def start_scan(config: ScanConfig, background_tasks: BackgroundTasks):
background_tasks.add_task(run_scan_videos_background) background_tasks.add_task(run_scan_videos_background, config.paths, config.algorithm, config.scan_type)
return {"message": "Video scan started in the background."} return {"message": "Scan started"}
@router.post("/scan/pause") @router.post("/scan/cancel")
def pause_scan(): def cancel_scan():
with open("logs/duplicates_scan.pause", 'w') as f: try:
pass os.makedirs("logs", exist_ok=True)
return {"message": "Video scan paused."} with open("logs/duplicates_scan.cancel", 'w') as f:
f.write("cancel")
except Exception as e:
raise HTTPException(status_code=500, detail=f"Failed to cancel scan: {e}")
return {"message": "Cancellation requested"}
@router.post("/scan/resume") @router.get("/scan/log")
def resume_scan(): def get_scan_log(limit: int = 100):
if os.path.exists("logs/duplicates_scan.pause"): log_file = "logs/duplicates_scan.log"
os.remove("logs/duplicates_scan.pause") if not os.path.exists(log_file):
return {"message": "Video scan resumed."} return {"lines": []}
try:
# Simple read and tail (not efficient for huge files but fine here)
with open(log_file, "r", encoding="utf-8", errors="replace") as f:
lines = f.readlines()
return {"lines": lines[-limit:]}
except Exception as e:
return {"lines": [f"Error reading log: {e}"]}
@router.get("/scan/status") @router.get("/scan/progress")
def get_scan_status(): def get_scan_progress():
if os.path.exists("logs/duplicates_scan.pause"): path = "logs/duplicates_scan.json"
return {"status": "Paused"} if os.path.exists(path):
if os.path.exists("logs/duplicates_scan.progress") or os.path.exists("logs/duplicates_scan.state"): try:
return {"status": "Scanning"} with open(path, 'r') as f:
return {"status": "Idle"} return json.load(f)
except:
return {"status": "error", "message": "Read failed"}
return {"status": "idle"}
@router.get("/exclusions")
def get_exclusions(db: Session = Depends(get_db)):
manager = DuplicatesManager(settings, db)
return manager.exclusions
@router.post("/exclusions")
def add_exclusion(req: ExclusionRequest, db: Session = Depends(get_db)):
manager = DuplicatesManager(settings, db)
manager.add_exclusion(req.path)
return {"message": "Added"}
@router.delete("/exclusions")
def remove_exclusion(req: ExclusionRequest, db: Session = Depends(get_db)):
manager = DuplicatesManager(settings, db)
manager.remove_exclusion(req.path)
return {"message": "Removed"}
@router.post("/find") @router.post("/find")
def find_duplicates(threshold: float = 0.95, db: Session = Depends(get_db)): def find_duplicates(threshold: float = 0.95, method: str = "fast", db: Session = Depends(get_db)):
manager = DuplicatesManager(settings, db) manager = DuplicatesManager(settings, db)
result = manager.find_duplicates(threshold) return manager.find_duplicates(threshold, method)
return result
@router.get("/reports") @router.get("/reports")
def get_reports(db: Session = Depends(get_db)): def get_reports(db: Session = Depends(get_db)):
@@ -69,9 +110,24 @@ def get_reports(db: Session = Depends(get_db)):
@router.get("/report/{report_id}") @router.get("/report/{report_id}")
def get_report(report_id: int, db: Session = Depends(get_db)): def get_report(report_id: int, db: Session = Depends(get_db)):
manager = DuplicatesManager(settings, db) manager = DuplicatesManager(settings, db)
return manager.get_duplicate_report(report_id) res = manager.get_report(report_id)
if not res: raise HTTPException(status_code=404)
return res
@router.delete("/files") @router.delete("/files")
def delete_files(request: DeleteFilesRequest, db: Session = Depends(get_db)): def delete_files(request: DeleteRequest, db: Session = Depends(get_db)):
manager = DuplicatesManager(settings, db) manager = DuplicatesManager(settings, db)
return manager.delete_files(request.filepaths) deleted = manager.delete_files(request.ids)
return {"deleted_ids": deleted}
@router.get("/thumbnails/{video_id}")
def get_thumbnail(video_id: int, db: Session = Depends(get_db)):
video = db.query(models.VideoFile).get(video_id)
if not video or not video.contact_sheet_path:
raise HTTPException(status_code=404, detail="Thumbnail not found")
path = os.path.join("resources/cache/thumbnails", video.contact_sheet_path)
if not os.path.exists(path):
raise HTTPException(status_code=404, detail="Thumbnail file missing")
return FileResponse(path)
@@ -0,0 +1,63 @@
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
from typing import List
from ..qbittorrent_manager import QbittorrentManager
router = APIRouter()
manager = QbittorrentManager()
class TorrentActionRequest(BaseModel):
hashes: List[str]
class DeleteRequest(TorrentActionRequest):
delete_files: bool = False
class LocationRequest(TorrentActionRequest):
location: str
@router.get("/")
def list_torrents():
try:
return manager.get_torrents()
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/pause")
def pause_torrents(request: TorrentActionRequest):
try:
manager.pause_torrents(request.hashes)
return {"status": "success"}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/resume")
def resume_torrents(request: TorrentActionRequest):
try:
manager.resume_torrents(request.hashes)
return {"status": "success"}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/delete")
def delete_torrents(request: DeleteRequest):
try:
manager.delete_torrents(request.hashes, request.delete_files)
return {"status": "success"}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/recheck")
def recheck_torrents(request: TorrentActionRequest):
try:
manager.recheck_torrents(request.hashes)
return {"status": "success"}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.post("/location")
def set_location(request: LocationRequest):
try:
manager.set_location(request.hashes, request.location)
return {"status": "success"}
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@@ -215,3 +215,16 @@ def rename_file(share_name: str, request: RenameRequest):
result = manager.rename_file(request.old_path, request.new_path) result = manager.rename_file(request.old_path, request.new_path)
manager.close() manager.close()
return result return result
@router.get("/search/{share_name}")
def search_files(share_name: str, query: str, path: str = "/"):
manager = SambaManager(
settings.samba_server_ip,
share_name,
username=settings.samba_username,
password=settings.samba_password,
)
try:
return manager.search_files(query, path)
finally:
manager.close()
@@ -0,0 +1,34 @@
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
from typing import List, Optional, Dict, Any
from ..scheduler import add_job, get_jobs, remove_job
router = APIRouter()
class JobCreate(BaseModel):
task_name: str
cron_expression: str
args: Optional[List[Any]] = []
kwargs: Optional[Dict[str, Any]] = {}
@router.get("/")
def list_jobs():
return get_jobs()
@router.post("/")
def create_job(job: JobCreate):
try:
job_id = add_job(job.task_name, job.cron_expression, job.args, job.kwargs)
return {"id": job_id, "message": "Job scheduled"}
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@router.delete("/{job_id}")
def delete_job(job_id: str):
try:
remove_job(job_id)
return {"message": "Job deleted"}
except Exception:
raise HTTPException(status_code=404, detail="Job not found")
@@ -0,0 +1,34 @@
from fastapi import APIRouter
from pydantic import BaseModel
from typing import Optional
from ..config import settings
router = APIRouter()
class SettingsUpdate(BaseModel):
samba_server_ip: str
samba_username: str
samba_password: str
qbittorrent_url: Optional[str]
qbittorrent_username: Optional[str]
qbittorrent_password: Optional[str]
default_comics_path: str
default_videos_path: str
stash_enabled: Optional[bool]
stash_db_path: Optional[str]
stash_generated_path: Optional[str]
stash_remote_base: Optional[str]
stash_container_base: Optional[str]
stash_share: Optional[str]
@router.get("/")
def get_settings():
return settings.model_dump()
@router.post("/")
def update_settings(update: SettingsUpdate):
for key, value in update.model_dump().items():
if hasattr(settings, key):
setattr(settings, key, value)
settings.save_to_json()
return {"message": "Settings updated"}
@@ -0,0 +1,44 @@
from fastapi import APIRouter, Depends, BackgroundTasks, HTTPException
from sqlalchemy.orm import Session
from ..config import settings, Settings
from ..samba_manager import SambaManager
from ..database import get_db
import os
import logging
router = APIRouter()
logger = logging.getLogger(__name__)
def sync_stash_db_task():
samba_manager = SambaManager(
settings.samba_server_ip,
settings.stash_share,
settings.samba_username,
settings.samba_password,
)
try:
logger.info(f"Syncing Stash DB from {settings.stash_remote_db_path}")
with open(settings.stash_db_path, "wb") as f:
samba_manager.download_file(settings.stash_remote_db_path, f)
logger.info("Stash DB synced successfully")
except Exception as e:
logger.error(f"Stash DB sync failed: {e}")
finally:
samba_manager.close()
@router.post("/sync")
def sync_stash(background_tasks: BackgroundTasks):
background_tasks.add_task(sync_stash_db_task)
return {"message": "Stash sync started"}
@router.get("/status")
def get_stash_status():
exists = os.path.exists(settings.stash_db_path)
size = os.path.getsize(settings.stash_db_path) if exists else 0
mtime = os.path.getmtime(settings.stash_db_path) if exists else 0
return {
"enabled": settings.stash_enabled,
"db_exists": exists,
"db_size": size,
"db_mtime": mtime
}
@@ -0,0 +1,60 @@
from fastapi import APIRouter
import psutil
import time
import os
router = APIRouter()
def get_size(bytes, suffix="B"):
"""
Scale bytes to its proper format
e.g:
1253656 => '1.20MB'
1253656678 => '1.17GB'
"""
factor = 1024
for unit in ["", "K", "M", "G", "T", "P"]:
if bytes < factor:
return f"{bytes:.2f}{unit}{suffix}"
bytes /= factor
@router.get("/stats")
def get_system_stats():
# CPU
cpu_usage = psutil.cpu_percent(interval=1)
# Memory
svmem = psutil.virtual_memory()
memory_stats = {
"total": get_size(svmem.total),
"available": get_size(svmem.available),
"percent": svmem.percent,
"used": get_size(svmem.used)
}
# Disk (Root)
try:
partition = psutil.disk_usage("/")
disk_stats = {
"total": get_size(partition.total),
"free": get_size(partition.free),
"percent": partition.percent,
"used": get_size(partition.used)
}
except Exception:
disk_stats = {"error": "Unavailable"}
# Uptime
boot_time = psutil.boot_time()
uptime_seconds = time.time() - boot_time
uptime_string = time.strftime("%H:%M:%S", time.gmtime(uptime_seconds))
if uptime_seconds > 86400:
days = int(uptime_seconds // 86400)
uptime_string = f"{days} days, " + uptime_string
return {
"cpu": cpu_usage,
"memory": memory_stats,
"disk": disk_stats,
"uptime": uptime_string
}
@@ -0,0 +1,10 @@
from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
from ..database import get_db
from ..models import TaskHistory
router = APIRouter()
@router.get("/")
def get_tasks(limit: int = 50, db: Session = Depends(get_db)):
return db.query(TaskHistory).order_by(TaskHistory.start_time.desc()).limit(limit).all()
@@ -35,13 +35,54 @@ class SambaManager:
"name": f.filename, "name": f.filename,
"is_directory": f.isDirectory, "is_directory": f.isDirectory,
"size": f.file_size, "size": f.file_size,
"path": full_item_path "path": full_item_path,
"last_modified": f.last_write_time
}) })
return items return items
except Exception as e: except Exception as e:
logger.error(f"Error listing path '{current_dir_path}' on share '{self.share_name}': {e}", exc_info=True) logger.error(f"Error listing path '{current_dir_path}' on share '{self.share_name}': {e}", exc_info=True)
return [] return []
def search_files(self, query, start_path="/"):
"""
Recursively searches for files matching query (partial, case-insensitive).
"""
matches = []
try:
stack = [start_path]
while stack:
current_path = stack.pop()
try:
files = self.conn.listPath(self.share_name, current_path)
except Exception:
continue # Skip unreadable dirs
for f in files:
if f.filename in ['.', '..']: continue
normalized_current_path = current_path
if normalized_current_path != '/' and not normalized_current_path.endswith('/'):
normalized_current_path += '/'
full_item_path = f"{normalized_current_path}{f.filename}".replace("\\", "/")
if query.lower() in f.filename.lower():
matches.append({
"name": f.filename,
"is_directory": f.isDirectory,
"size": f.file_size,
"path": full_item_path,
"last_modified": f.last_write_time
})
if f.isDirectory:
stack.append(full_item_path)
except Exception as e:
logger.error(f"Error searching path '{start_path}': {e}")
return matches
def delete_file(self, path): def delete_file(self, path):
try: try:
@@ -63,6 +104,8 @@ class SambaManager:
self.conn.retrieveFile(self.share_name, path, file_obj) self.conn.retrieveFile(self.share_name, path, file_obj)
return {"success": True} return {"success": True}
except Exception as e: except Exception as e:
if "Scan canceled by user" in str(e):
raise e
return {"error": str(e)} return {"error": str(e)}
def download_file_range(self, path, file_obj, offset, max_length): def download_file_range(self, path, file_obj, offset, max_length):
@@ -0,0 +1,68 @@
from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.jobstores.sqlalchemy import SQLAlchemyJobStore
from .database import engine
from .tasks import task_organize_comics, task_update_metadata, task_sort_by_artist
import logging
logger = logging.getLogger(__name__)
# Map readable names to Huey task functions
TASK_MAP = {
"organize_comics": task_organize_comics,
"update_metadata": task_update_metadata,
"sort_by_artist": task_sort_by_artist
}
jobstores = {
'default': SQLAlchemyJobStore(engine=engine)
}
scheduler = BackgroundScheduler(jobstores=jobstores)
def start_scheduler():
try:
scheduler.start()
logger.info("Scheduler started.")
except Exception as e:
logger.error(f"Failed to start scheduler: {e}")
def add_job(task_name, cron_str, args=None, kwargs=None):
if task_name not in TASK_MAP:
raise ValueError(f"Unknown task: {task_name}")
func = TASK_MAP[task_name]
# Parse cron string (e.g. "* * * * *") -> minute, hour, day, month, day_of_week
# Simple implementation: expect "min hour day month day_of_week"
try:
parts = cron_str.split()
if len(parts) != 5:
raise ValueError("Invalid cron format. Expected 5 fields.")
trigger_args = {
'minute': parts[0],
'hour': parts[1],
'day': parts[2],
'month': parts[3],
'day_of_week': parts[4]
}
job = scheduler.add_job(func, 'cron', args=args, kwargs=kwargs, **trigger_args)
return job.id
except Exception as e:
logger.error(f"Failed to add job: {e}")
raise e
def get_jobs():
jobs = []
for job in scheduler.get_jobs():
jobs.append({
"id": job.id,
"name": job.name,
"next_run": str(job.next_run_time),
"trigger": str(job.trigger)
})
return jobs
def remove_job(job_id):
scheduler.remove_job(job_id)
@@ -0,0 +1,88 @@
import sqlite3
import os
import logging
from .config import Settings
logger = logging.getLogger(__name__)
class StashService:
def __init__(self, settings: Settings):
self.settings = settings
self.db_path = settings.stash_db_path
def get_db_connection(self):
if not os.path.exists(self.db_path):
logger.warning(f"Stash database not found at {self.db_path}")
return None
return sqlite3.connect(self.db_path)
def translate_to_stash_path(self, local_path):
"""
Translates a local/SMB path to Stash internal container path.
Example: /media/videos/Girl/Scene.mp4 -> /data/Girl/Scene.mp4
"""
remote_base = self.settings.stash_remote_base.rstrip('/')
container_base = self.settings.stash_container_base.rstrip('/')
if local_path.startswith(remote_base):
return local_path.replace(remote_base, container_base, 1)
# If it doesn't start with remote_base, maybe it's already relative or formatted differently?
# Stash also uses basenames in 'files' table.
return local_path
def get_file_metadata(self, local_path):
"""
Returns (phash, oshash, scene_id, duration) from Stash DB for a given file.
"""
stash_path = self.translate_to_stash_path(local_path)
conn = self.get_db_connection()
if not conn:
return None, None, None, None
try:
cursor = conn.cursor()
cursor.execute("SELECT id FROM files WHERE path = ?", (stash_path,))
row = cursor.fetchone()
if not row:
return None, None, None, None
file_id = row[0]
# Get fingerprints
cursor.execute("SELECT type, fingerprint FROM files_fingerprints WHERE file_id = ?", (file_id,))
fingerprints = cursor.fetchall()
phash = None
oshash = None
for f_type, f_val in fingerprints:
if f_type == 'phash':
phash = hex(int(f_val) & 0xffffffffffffffff)[2:].zfill(16)
elif f_type == 'oshash':
oshash = f_val
# Get scene_id
cursor.execute("SELECT scene_id FROM scenes_files WHERE file_id = ?", (file_id,))
scene_row = cursor.fetchone()
scene_id = scene_row[0] if scene_row else None
# Get duration
cursor.execute("SELECT duration FROM video_files WHERE file_id = ?", (file_id,))
dur_row = cursor.fetchone()
duration = dur_row[0] if dur_row else None
return phash, oshash, scene_id, duration
except Exception as e:
logger.error(f"Failed to query Stash metadata for {local_path}: {e}")
return None, None, None, None
finally:
conn.close()
def get_sprite_path(self, oshash):
"""
Returns the remote SMB path for the sprite.
Example: /media/stashapp/generated/vtt/{oshash}_sprite.jpg
"""
if not oshash: return None
return f"{self.settings.stash_generated_path}/vtt/{oshash}_sprite.jpg"
@@ -0,0 +1,30 @@
from .database import SessionLocal
from .models import TaskHistory
import datetime
def record_task_start(task_id, name, details=None):
db = SessionLocal()
try:
task = TaskHistory(task_id=str(task_id), name=name, status="running", details=str(details) if details else None)
db.add(task)
db.commit()
except Exception as e:
print(f"Error recording task start: {e}")
finally:
db.close()
def record_task_end(task_id, status, details=None):
db = SessionLocal()
try:
task = db.query(TaskHistory).filter_by(task_id=str(task_id)).first()
if task:
task.status = status
task.end_time = datetime.datetime.utcnow()
if details:
# Append details if existing? Or overwrite? Overwrite for now or simpler append
task.details = str(details)
db.commit()
except Exception as e:
print(f"Error recording task end: {e}")
finally:
db.close()
+83
View File
@@ -0,0 +1,83 @@
from huey import SqliteHuey
import os
import logging
from .comics_manager import ComicsManager
from .samba_manager import SambaManager
from .config import settings
from .task_tracker import record_task_start, record_task_end
# Ensure dir exists
os.makedirs("resources/config", exist_ok=True)
# Configure Huey with SQLite backend for persistence
huey = SqliteHuey(filename="resources/config/tasks.db")
logger = logging.getLogger('huey')
def get_samba_manager(share="isolation"):
return SambaManager(
settings.samba_server_ip,
share,
settings.samba_username,
settings.samba_password,
)
@huey.task(context=True)
def task_organize_comics(task=None):
record_task_start(task.id, "Organize Comics")
logger.info(f"Task {task.id}: Organize Comics started")
manager = ComicsManager()
samba_manager = get_samba_manager("isolation")
try:
manager.organize_comics(samba_manager)
record_task_end(task.id, "success")
except Exception as e:
logger.error(f"Task Organize Comics failed: {e}")
record_task_end(task.id, "failed", str(e))
finally:
samba_manager.close()
@huey.task(context=True)
def task_move_comics(series_names, share, path, task=None):
record_task_start(task.id, "Move Comics", f"Count: {len(series_names)} -> {share}:{path}")
logger.info(f"Task {task.id}: Move Comics started")
manager = ComicsManager()
samba_manager = get_samba_manager("isolation")
try:
manager.move_series(samba_manager, series_names, share, path)
record_task_end(task.id, "success")
except Exception as e:
logger.error(f"Task Move Comics failed: {e}")
record_task_end(task.id, "failed", str(e))
finally:
samba_manager.close()
@huey.task(context=True)
def task_update_metadata(target_path, force, task=None):
record_task_start(task.id, "Update Metadata", f"Path: {target_path}")
logger.info(f"Task {task.id}: Update Metadata started")
manager = ComicsManager()
samba_manager = get_samba_manager("isolation")
try:
manager.update_existing_metadata(samba_manager, target_path, force)
record_task_end(task.id, "success")
except Exception as e:
logger.error(f"Task Update Metadata failed: {e}")
record_task_end(task.id, "failed", str(e))
finally:
samba_manager.close()
@huey.task(context=True)
def task_sort_by_artist(root_path, task=None):
record_task_start(task.id, "Sort by Artist", f"Path: {root_path}")
logger.info(f"Task {task.id}: Sort by Artist started")
manager = ComicsManager()
samba_manager = get_samba_manager("isolation")
try:
manager.sort_by_artist(samba_manager, root_path)
record_task_end(task.id, "success")
except Exception as e:
logger.error(f"Task Sort by Artist failed: {e}")
record_task_end(task.id, "failed", str(e))
finally:
samba_manager.close()
File diff suppressed because it is too large Load Diff
@@ -0,0 +1 @@
{"status": "completed", "processed": 6970}
File diff suppressed because it is too large Load Diff
+19
View File
@@ -0,0 +1,19 @@
from app.database import engine
from sqlalchemy import text
def migrate():
with engine.connect() as conn:
try:
conn.execute(text("ALTER TABLE video_files ADD COLUMN contact_sheet_path VARCHAR"))
print("Added contact_sheet_path")
except Exception as e:
print(f"Skipped contact_sheet_path (probably exists): {e}")
try:
conn.execute(text("ALTER TABLE video_files ADD COLUMN scene_phash VARCHAR"))
print("Added scene_phash")
except Exception as e:
print(f"Skipped scene_phash (probably exists): {e}")
if __name__ == "__main__":
migrate()
+3 -1
View File
@@ -9,4 +9,6 @@ pydantic-settings
requests requests
pillow pillow
beautifulsoup4 beautifulsoup4
cloudscraper cloudscraper
huey
qbittorrent-api
-322
View File
@@ -1,322 +0,0 @@
import os
import zipfile
import re
import shutil
from PIL import Image
from pdf2image import convert_from_path
from datetime import datetime
from collections import defaultdict
import uuid # Library for generating unique IDs
# --- Utility Functions ---
def setup_error_log():
"""
Creates a uniquely named error log file.
"""
timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
log_filename = f"errorlog_{timestamp}.log"
return log_filename
def log_error(log_file, message):
"""
Appends an error message to the log file.
"""
with open(log_file, 'a') as f:
f.write(f"{datetime.now().strftime('%Y-%m-%d %H:%M:%S')} - {message}\n")
def convert_webp_to_jpg(file_path, log_file):
"""
Converts a .webp image file to .jpg format.
"""
try:
image = Image.open(file_path)
if image.mode == 'RGBA':
image = image.convert('RGB')
jpg_path = os.path.splitext(file_path)[0] + '.jpg'
image.save(jpg_path, 'JPEG')
os.remove(file_path)
return jpg_path
except Exception as e:
log_error(log_file, f"Error converting WEBP: {file_path} - {e}")
return None
def convert_pdf_to_jpgs(file_path, output_folder, log_file):
"""
Converts a PDF file into a series of JPG images.
"""
try:
pages = convert_from_path(file_path)
output_paths = []
for i, page in enumerate(pages):
jpg_path = os.path.join(output_folder, f"page_{i+1:03d}.jpg")
page.save(jpg_path, 'JPEG')
output_paths.append(jpg_path)
os.remove(file_path)
return output_paths
except Exception as e:
log_error(log_file, f"Error converting PDF: {file_path} - {e}")
return []
def create_cbz_archive(folder_path, output_path, log_file):
"""
Creates a .cbz archive from all image files in a folder.
"""
try:
with zipfile.ZipFile(output_path, 'w') as cbz_file:
for root, _, files in os.walk(folder_path):
# Sort files to ensure correct page order in the CBZ file
sorted_files = sorted(files, key=lambda f: re.split(r'(\d+)', f))
for filename in sorted_files:
if filename.lower().endswith(('.jpg', '.jpeg', '.png', '.gif')):
file_path = os.path.join(root, filename)
# Archive name should be relative to the content folder
arcname = os.path.relpath(file_path, folder_path)
cbz_file.write(file_path, arcname)
return True
except Exception as e:
log_error(log_file, f"Error creating CBZ for {folder_path} - {e}")
return False
def clean_up_temp_folders(folder_path):
"""
Removes a folder and its contents.
"""
try:
if os.path.exists(folder_path):
shutil.rmtree(folder_path)
except Exception as e:
print(f"Error cleaning up {folder_path}: {e}")
# --- Grouping Function ---
def get_series_name(file_or_folder_name):
"""
Uses regex to extract the common series name from a chapter folder/file name.
"""
base_name = os.path.splitext(file_or_folder_name)[0]
# Pattern to match and strip common chapter/volume suffixes at the end of a string.
pattern = re.compile(r'[-_]?((ch(apter)?|v(ol)?)[-_]?)?\d{1,4}(\.\d{1,2})?$', re.IGNORECASE)
# Find the match from the end
match = pattern.search(base_name)
if match:
# Return the string slice before the matched pattern
return base_name[:match.start()].strip('-_ ')
# Fallback: if no clear chapter pattern is found, return the original name
return base_name
# --- File/Folder Processing Function ---
def process_chapter(chapter_path, final_series_folder, temp_root_folder, log_file):
"""
Copies, converts, creates CBZ, and cleans up for a single chapter folder/archive.
Uses a unique temporary sub-folder for its operations.
"""
chapter_name = os.path.basename(chapter_path)
# Create a UNIQUE temporary folder for THIS chapter's processing
unique_id = str(uuid.uuid4())[:8] # Short unique identifier
temp_chapter_folder = os.path.join(temp_root_folder, f"proc_{unique_id}_{chapter_name}")
os.makedirs(temp_chapter_folder, exist_ok=True)
is_archive = os.path.isfile(chapter_path) and chapter_path.lower().endswith(('.zip', '.cbz'))
try:
if is_archive:
print(f" -> Extracting Archive: {chapter_name}")
with zipfile.ZipFile(chapter_path, 'r') as zip_ref:
zip_ref.extractall(temp_chapter_folder)
else:
print(f" -> Processing Folder: {chapter_name}")
# Copy original folder contents to the temporary folder
for item in os.listdir(chapter_path):
s = os.path.join(chapter_path, item)
d = os.path.join(temp_chapter_folder, item)
if os.path.isdir(s):
# Use a recursive copy for subdirectories
shutil.copytree(s, d)
else:
# Use a simple copy for files
shutil.copy2(s, d)
# B. Convert files inside the temporary folder
for root, _, files in os.walk(temp_chapter_folder):
for file_path in [os.path.join(root, f) for f in files]:
if file_path.lower().endswith('.webp'):
print(f" - Converting WEBP: {os.path.basename(file_path)}")
convert_webp_to_jpg(file_path, log_file)
elif file_path.lower().endswith('.pdf'):
print(f" - Converting PDF: {os.path.basename(file_path)}")
# Conversion places JPGs in the same folder as the PDF
convert_pdf_to_jpgs(file_path, root, log_file)
# C. Create CBZ archive in the final series destination
cbz_name = os.path.splitext(chapter_name)[0] + '.cbz'
output_cbz_path = os.path.join(final_series_folder, cbz_name)
if create_cbz_archive(temp_chapter_folder, output_cbz_path, log_file):
print(f" - Successfully created {cbz_name}")
except Exception as e:
log_error(log_file, f"Processing failed for {chapter_name} (Path: {chapter_path}): {e}")
print(f" - ERROR: Processing failed. Check log for details.")
return # Exit the function on error
finally:
# D. Clean up the temporary processing folder for this chapter
clean_up_temp_folders(temp_chapter_folder)
# --- New Move Function ---
def move_converted_files(source_dir, destination_root, log_file):
"""
Moves all contents (series folders) from source_dir (converted)
to destination_root (/mnt/isolation/comics).
"""
print("\n--- Starting Final Move Operation ---")
# The parent of /mnt/isolation/comics/toberead/converted is /mnt/isolation/comics/toberead
# We want to move to /mnt/isolation/comics
# 1. Identify the true final destination path
# Get the parent of the source_dir (which is /mnt/isolation/comics/toberead)
parent_of_source = os.path.dirname(source_dir)
# Get the parent of that (which is /mnt/isolation/comics)
final_destination = os.path.dirname(parent_of_source)
if destination_root != final_destination:
log_error(log_file, f"Configuration Error: Move destination root mismatch. Expected {final_destination}, got {destination_root}")
print(f"Error: Final destination paths do not match. Aborting move. Check log.")
return
total_moved = 0
# Iterate through each series folder in the 'converted' directory
for series_name in os.listdir(source_dir):
series_source_path = os.path.join(source_dir, series_name)
series_dest_path = os.path.join(destination_root, series_name)
if os.path.isdir(series_source_path):
try:
# Move the entire series folder (containing the CBZ files)
if not os.path.exists(series_dest_path):
shutil.move(series_source_path, series_dest_path)
print(f" -> Moved series folder: {series_name}")
total_moved += 1
else:
# If the series folder already exists in the destination, move its contents (the CBZ files)
for item in os.listdir(series_source_path):
item_s = os.path.join(series_source_path, item)
item_d = os.path.join(series_dest_path, item)
if item.lower().endswith('.cbz') and not os.path.exists(item_d):
shutil.move(item_s, item_d)
print(f" -> Moved CBZ: {item}")
total_moved += 1
# Remove the now-empty source folder
clean_up_temp_folders(series_source_path)
except Exception as e:
log_error(log_file, f"Error moving series {series_name}: {e}")
print(f"Error moving {series_name}. Check log for details.")
print(f"\nMove Complete. Total items moved: {total_moved}")
def main():
"""
Main function to run the comic organization script.
"""
# --- Configuration ---
source_directory = "/mnt/isolation/comics/toberead"
target_directory = os.path.join(source_directory, "converted")
temp_root_folder = os.path.join(source_directory, ".komga_processing_temp")
# The final destination root for the move operation (parent of source_directory)
destination_root = os.path.dirname(source_directory)
# ---------------------
error_log_file = setup_error_log()
print(f"Starting script. Source: {source_directory}")
print(f"Output will be in: {target_directory}")
print(f"Final destination root: {destination_root}")
print(f"Errors will be logged to: {error_log_file}")
os.makedirs(target_directory, exist_ok=True)
# Clean up the main temporary directory before starting
clean_up_temp_folders(temp_root_folder)
os.makedirs(temp_root_folder, exist_ok=True)
# 1. Group Chapter Folders and Archive Files by Series Name
series_chapters = defaultdict(list)
try:
items = os.listdir(source_directory)
except FileNotFoundError:
print(f"Error: Source directory not found: {source_directory}. Aborting.")
return
for item in items:
item_path = os.path.join(source_directory, item)
# Skip the output and temporary directories
if item_path.startswith(target_directory) or item_path.startswith(temp_root_folder):
continue
is_relevant_folder = os.path.isdir(item_path) and any(f.lower().endswith(('.jpg', '.jpeg', '.png', '.gif', '.webp', '.pdf'))
for f in os.listdir(item_path))
is_relevant_file = os.path.isfile(item_path) and item.lower().endswith(('.zip', '.cbz'))
if is_relevant_folder or is_relevant_file:
series_name = get_series_name(item)
series_chapters[series_name].append(item_path)
print(f"\nFound {len(series_chapters)} series to process.")
# 2. Process Each Series Group
for series_name, chapter_paths in series_chapters.items():
print(f"\n--- Processing Series: {series_name} ({len(chapter_paths)} Chapters/Archives) ---")
# Create the final parent folder for the series in the target directory
final_series_folder = os.path.join(target_directory, series_name)
os.makedirs(final_series_folder, exist_ok=True)
for chapter_path in chapter_paths:
chapter_name = os.path.basename(chapter_path)
if chapter_name.lower().endswith('.cbz') and os.path.isfile(chapter_path):
# Move existing CBZ files directly to the final destination
new_path = os.path.join(final_series_folder, chapter_name)
if not os.path.exists(new_path):
shutil.move(chapter_path, new_path)
print(f" -> Moved existing CBZ: {chapter_name}")
else:
print(f" -> Skipping CBZ (already exists in destination): {chapter_name}")
else:
# Process folders and ZIP files
process_chapter(chapter_path, final_series_folder, temp_root_folder, error_log_file)
# 3. Final Cleanup and Conditional Move
# Final cleanup of the main temp folder
clean_up_temp_folders(temp_root_folder)
print("\n--- Processing Finished ---")
# --- USER PROMPT FOR FINAL MOVE ---
prompt = f"Do you want to move all CBZ files from '{target_directory}' to the root directory '{destination_root}'? (yes/no): "
user_input = input(prompt).strip().lower()
if user_input == 'yes':
move_converted_files(target_directory, destination_root, error_log_file)
print("\nScript operation complete.")
if __name__ == "__main__":
main()
-129
View File
@@ -1,129 +0,0 @@
import os
import re
import sys
from collections import defaultdict
import logging
from .samba_manager import SambaManager
from .config import Settings
from smb.smb_structs import OperationFailure
# --- Dedicated logger for ComicsManager ---
comics_logger = logging.getLogger('comics_manager')
comics_logger.setLevel(logging.INFO)
comics_logger.propagate = False
if not comics_logger.handlers:
comics_log_handler = logging.FileHandler("logs/comics_organization.log", mode='a')
comics_log_handler.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s'))
comics_logger.addHandler(comics_log_handler)
comics_logger.addHandler(logging.StreamHandler())
# --- Disable noisy pysmb logger ---
# Temporarily set the pysmb logger levels to CRITICAL to silence its output.
# This needs to be set *after* basicConfig if pysmb initializes its loggers earlier.
try:
logging.getLogger("smb").setLevel(logging.CRITICAL)
logging.getLogger("smb.SMBConnection").setLevel(logging.CRITICAL)
except Exception:
# Handle cases where the logger might not be fully initialized yet
pass
# ---
class ComicsManager:
def __init__(self, settings: Settings):
self.settings = settings
self.db = None # ComicsManager doesn't use DB
self.samba_manager = SambaManager(
settings.samba_server_ip,
"isolation",
settings.samba_username,
settings.samba_password,
)
self.comics_root = "/comics/toberead"
self.comics_dest_root = "/comics/manga"
def _get_series_name(self, filename):
base_name, _ = os.path.splitext(filename)
# This regex is more aggressive at cleaning up chapter/volume numbers and other noise.
# It looks for common patterns like 'ch.10', 'v02', '123', etc.
pattern = re.compile(r'[-_\s]*(v(ol)?|c(h)?|chapter|issue|ep|episode)[-_\s]*\d+.*|[-_\s]+\d+$', re.IGNORECASE)
match = pattern.search(base_name)
if match:
return base_name[:match.start()].strip('-_ ')
return base_name.strip('-_ ')
def organize_comics(self):
comics_logger.info("Starting comics organization process...")
try:
items_in_toberead = self.samba_manager.list_path(self.comics_root)
if "error" in items_in_toberead:
raise Exception(f"Failed to list files in 'toberead': {items_in_toberead['error']}")
series_chapters = defaultdict(list)
for item in items_in_toberead:
if not item["is_directory"]:
series_name = self._get_series_name(item["name"])
series_chapters[series_name].append(item)
comics_logger.info(f"Found {len(series_chapters)} series to process.")
for series_name, chapters in series_chapters.items():
comics_logger.info(f"Processing series: {series_name}")
# Ensure paths use forward slashes for Samba operations
final_series_folder_path = f"{self.comics_dest_root}/{series_name}".replace("\", "/")
try:
self.samba_manager.create_directory(final_series_folder_path)
comics_logger.info(f"Created directory: {final_series_folder_path}")
except OperationFailure as e:
# Specific check for FILE_OBJECT_NAME_COLLISION if directory already exists
if "FILE_OBJECT_NAME_COLLISION" in str(e):
comics_logger.info(f"Directory {final_series_folder_path} already exists. Skipping creation.")
else:
comics_logger.error(f"Could not create directory {final_series_folder_path}: {e}")
for chapter_item in chapters:
# Sanitize original path coming from list_path
original_path = chapter_item["path"].replace("\", "/")
original_filename = chapter_item["name"]
path_to_move = ""
filename_to_move = ""
if original_filename.lower().endswith('.zip'):
cbz_filename = os.path.splitext(original_filename)[0] + '.cbz'
temp_cbz_path = f"{self.comics_root}/{cbz_filename}".replace("\", "/")
try:
self.samba_manager.rename_file(original_path, temp_cbz_path)
path_to_move = temp_cbz_path
filename_to_move = cbz_filename
comics_logger.info(f"Renamed {original_filename} to {cbz_filename}")
except OperationFailure as e:
comics_logger.error(f"Failed to rename ZIP {original_filename} to CBZ: {e}")
continue # Skip to next chapter if rename fails
elif original_filename.lower().endswith('.cbz'):
# If it's already a .cbz, it's ready to be moved
path_to_move = original_path
filename_to_move = original_filename
else:
comics_logger.warning(f"Skipping non-zip/cbz file: {original_filename}")
continue
# Final destination path for the CBZ file
final_cbz_path = f"{final_series_folder_path}/{filename_to_move}".replace("\", "/")
try:
self.samba_manager.rename_file(path_to_move, final_cbz_path)
comics_logger.info(f"Moved {filename_to_move} to {final_series_folder_path}")
except OperationFailure as e:
comics_logger.error(f"Failed to move {filename_to_move} to {final_series_folder_path}: {e}")
except Exception as e:
comics_logger.error(f"An unexpected error occurred during comics organization: {e}", exc_info=True)
finally:
comics_logger.info("Comics organization process finished.")
self.samba_manager.close()
return
+337 -277
View File
@@ -1,367 +1,394 @@
{ {
"hash": "58793c98", "hash": "95aedb35",
"configHash": "939bcc17", "configHash": "7be18a1c",
"lockfileHash": "b10062e0", "lockfileHash": "b10062e0",
"browserHash": "e6aae6df", "browserHash": "9f13f699",
"optimized": { "optimized": {
"vue": { "vue": {
"src": "../../vue/dist/vue.runtime.esm-bundler.js", "src": "../../vue/dist/vue.runtime.esm-bundler.js",
"file": "vue.js", "file": "vue.js",
"fileHash": "c9954258", "fileHash": "8eb41ee8",
"needsInterop": false "needsInterop": false
}, },
"vue-router": { "vue-router": {
"src": "../../vue-router/dist/vue-router.mjs", "src": "../../vue-router/dist/vue-router.mjs",
"file": "vue-router.js", "file": "vue-router.js",
"fileHash": "e764d986", "fileHash": "1427789a",
"needsInterop": false "needsInterop": false
}, },
"vuetify": { "vuetify": {
"src": "../../vuetify/lib/framework.js", "src": "../../vuetify/lib/framework.js",
"file": "vuetify.js", "file": "vuetify.js",
"fileHash": "369c86f0", "fileHash": "8dbe3ca9",
"needsInterop": false "needsInterop": false
}, },
"vuetify/components": { "vuetify/components": {
"src": "../../vuetify/lib/components/index.js", "src": "../../vuetify/lib/components/index.js",
"file": "vuetify_components.js", "file": "vuetify_components.js",
"fileHash": "721553c0", "fileHash": "c679badd",
"needsInterop": false "needsInterop": false
}, },
"vuetify/directives": { "vuetify/directives": {
"src": "../../vuetify/lib/directives/index.js", "src": "../../vuetify/lib/directives/index.js",
"file": "vuetify_directives.js", "file": "vuetify_directives.js",
"fileHash": "cf4c59e7", "fileHash": "1b05ee8f",
"needsInterop": false "needsInterop": false
}, },
"vuetify/iconsets/mdi": { "vuetify/iconsets/mdi": {
"src": "../../vuetify/lib/iconsets/mdi.js", "src": "../../vuetify/lib/iconsets/mdi.js",
"file": "vuetify_iconsets_mdi.js", "file": "vuetify_iconsets_mdi.js",
"fileHash": "590e0963", "fileHash": "af4fe77a",
"needsInterop": false "needsInterop": false
}, },
"vuetify/components/VApp": { "vuetify/components/VApp": {
"src": "../../vuetify/lib/components/VApp/index.js", "src": "../../vuetify/lib/components/VApp/index.js",
"file": "vuetify_components_VApp.js", "file": "vuetify_components_VApp.js",
"fileHash": "f6cb1574", "fileHash": "67472430",
"needsInterop": false "needsInterop": false
}, },
"vuetify/components/VAppBar": { "vuetify/components/VAppBar": {
"src": "../../vuetify/lib/components/VAppBar/index.js", "src": "../../vuetify/lib/components/VAppBar/index.js",
"file": "vuetify_components_VAppBar.js", "file": "vuetify_components_VAppBar.js",
"fileHash": "bd33378b", "fileHash": "1c27af94",
"needsInterop": false "needsInterop": false
}, },
"vuetify/components/VBtn": { "vuetify/components/VBtn": {
"src": "../../vuetify/lib/components/VBtn/index.js", "src": "../../vuetify/lib/components/VBtn/index.js",
"file": "vuetify_components_VBtn.js", "file": "vuetify_components_VBtn.js",
"fileHash": "b2541520", "fileHash": "9ff6101b",
"needsInterop": false "needsInterop": false
}, },
"vuetify/components/VDivider": { "vuetify/components/VDivider": {
"src": "../../vuetify/lib/components/VDivider/index.js", "src": "../../vuetify/lib/components/VDivider/index.js",
"file": "vuetify_components_VDivider.js", "file": "vuetify_components_VDivider.js",
"fileHash": "1201295e", "fileHash": "d9ee236f",
"needsInterop": false "needsInterop": false
}, },
"vuetify/components/VGrid": { "vuetify/components/VGrid": {
"src": "../../vuetify/lib/components/VGrid/index.js", "src": "../../vuetify/lib/components/VGrid/index.js",
"file": "vuetify_components_VGrid.js", "file": "vuetify_components_VGrid.js",
"fileHash": "dec97038", "fileHash": "74768afe",
"needsInterop": false "needsInterop": false
}, },
"vuetify/components/VList": { "vuetify/components/VList": {
"src": "../../vuetify/lib/components/VList/index.js", "src": "../../vuetify/lib/components/VList/index.js",
"file": "vuetify_components_VList.js", "file": "vuetify_components_VList.js",
"fileHash": "92076a71", "fileHash": "dc9b9e42",
"needsInterop": false "needsInterop": false
}, },
"vuetify/components/VMain": { "vuetify/components/VMain": {
"src": "../../vuetify/lib/components/VMain/index.js", "src": "../../vuetify/lib/components/VMain/index.js",
"file": "vuetify_components_VMain.js", "file": "vuetify_components_VMain.js",
"fileHash": "aea7930b", "fileHash": "41fdc34f",
"needsInterop": false "needsInterop": false
}, },
"vuetify/components/VNavigationDrawer": { "vuetify/components/VNavigationDrawer": {
"src": "../../vuetify/lib/components/VNavigationDrawer/index.js", "src": "../../vuetify/lib/components/VNavigationDrawer/index.js",
"file": "vuetify_components_VNavigationDrawer.js", "file": "vuetify_components_VNavigationDrawer.js",
"fileHash": "fa7c5e03", "fileHash": "e3294e44",
"needsInterop": false
},
"vuetify/components/VAlert": {
"src": "../../vuetify/lib/components/VAlert/index.js",
"file": "vuetify_components_VAlert.js",
"fileHash": "5dc7580a",
"needsInterop": false
},
"vuetify/components/VCard": {
"src": "../../vuetify/lib/components/VCard/index.js",
"file": "vuetify_components_VCard.js",
"fileHash": "90108afc",
"needsInterop": false
},
"vuetify/components/VDialog": {
"src": "../../vuetify/lib/components/VDialog/index.js",
"file": "vuetify_components_VDialog.js",
"fileHash": "e56ad233",
"needsInterop": false
},
"vuetify/components/VIcon": {
"src": "../../vuetify/lib/components/VIcon/index.js",
"file": "vuetify_components_VIcon.js",
"fileHash": "ccd96f39",
"needsInterop": false
},
"vuetify/components/VToolbar": {
"src": "../../vuetify/lib/components/VToolbar/index.js",
"file": "vuetify_components_VToolbar.js",
"fileHash": "f4e18631",
"needsInterop": false "needsInterop": false
}, },
"vuetify/components/VBreadcrumbs": { "vuetify/components/VBreadcrumbs": {
"src": "../../vuetify/lib/components/VBreadcrumbs/index.js", "src": "../../vuetify/lib/components/VBreadcrumbs/index.js",
"file": "vuetify_components_VBreadcrumbs.js", "file": "vuetify_components_VBreadcrumbs.js",
"fileHash": "b9da5690", "fileHash": "cce8769a",
"needsInterop": false "needsInterop": false
}, },
"vuetify/components/VDataTable": { "vuetify/components/VCard": {
"src": "../../vuetify/lib/components/VDataTable/index.js", "src": "../../vuetify/lib/components/VCard/index.js",
"file": "vuetify_components_VDataTable.js", "file": "vuetify_components_VCard.js",
"fileHash": "d06d0545", "fileHash": "ea6e451a",
"needsInterop": false
},
"vuetify/components/VFileInput": {
"src": "../../vuetify/lib/components/VFileInput/index.js",
"file": "vuetify_components_VFileInput.js",
"fileHash": "9b6d1640",
"needsInterop": false
},
"vuetify/components/VMenu": {
"src": "../../vuetify/lib/components/VMenu/index.js",
"file": "vuetify_components_VMenu.js",
"fileHash": "6401d9c7",
"needsInterop": false
},
"vuetify/components/VProgressLinear": {
"src": "../../vuetify/lib/components/VProgressLinear/index.js",
"file": "vuetify_components_VProgressLinear.js",
"fileHash": "f6998592",
"needsInterop": false
},
"vuetify/components/VSelect": {
"src": "../../vuetify/lib/components/VSelect/index.js",
"file": "vuetify_components_VSelect.js",
"fileHash": "fff4e409",
"needsInterop": false
},
"vuetify/components/VSnackbar": {
"src": "../../vuetify/lib/components/VSnackbar/index.js",
"file": "vuetify_components_VSnackbar.js",
"fileHash": "e8e1d7d3",
"needsInterop": false
},
"vuetify/components/VCheckbox": {
"src": "../../vuetify/lib/components/VCheckbox/index.js",
"file": "vuetify_components_VCheckbox.js",
"fileHash": "6c2a3d1f",
"needsInterop": false "needsInterop": false
}, },
"vuetify/components/VChip": { "vuetify/components/VChip": {
"src": "../../vuetify/lib/components/VChip/index.js", "src": "../../vuetify/lib/components/VChip/index.js",
"file": "vuetify_components_VChip.js", "file": "vuetify_components_VChip.js",
"fileHash": "149d1552", "fileHash": "3ec2d4cf",
"needsInterop": false
},
"vuetify/components/VDataTable": {
"src": "../../vuetify/lib/components/VDataTable/index.js",
"file": "vuetify_components_VDataTable.js",
"fileHash": "1d4ec1bb",
"needsInterop": false
},
"vuetify/components/VDialog": {
"src": "../../vuetify/lib/components/VDialog/index.js",
"file": "vuetify_components_VDialog.js",
"fileHash": "3fa692d0",
"needsInterop": false
},
"vuetify/components/VFileInput": {
"src": "../../vuetify/lib/components/VFileInput/index.js",
"file": "vuetify_components_VFileInput.js",
"fileHash": "85476177",
"needsInterop": false
},
"vuetify/components/VIcon": {
"src": "../../vuetify/lib/components/VIcon/index.js",
"file": "vuetify_components_VIcon.js",
"fileHash": "aa188e74",
"needsInterop": false
},
"vuetify/components/VMenu": {
"src": "../../vuetify/lib/components/VMenu/index.js",
"file": "vuetify_components_VMenu.js",
"fileHash": "ca24447e",
"needsInterop": false
},
"vuetify/components/VProgressLinear": {
"src": "../../vuetify/lib/components/VProgressLinear/index.js",
"file": "vuetify_components_VProgressLinear.js",
"fileHash": "da3b21f8",
"needsInterop": false
},
"vuetify/components/VSelect": {
"src": "../../vuetify/lib/components/VSelect/index.js",
"file": "vuetify_components_VSelect.js",
"fileHash": "23a7041e",
"needsInterop": false
},
"vuetify/components/VSnackbar": {
"src": "../../vuetify/lib/components/VSnackbar/index.js",
"file": "vuetify_components_VSnackbar.js",
"fileHash": "11f33cbb",
"needsInterop": false
},
"vuetify/components/VToolbar": {
"src": "../../vuetify/lib/components/VToolbar/index.js",
"file": "vuetify_components_VToolbar.js",
"fileHash": "fb071d70",
"needsInterop": false
},
"vuetify/components/VAlert": {
"src": "../../vuetify/lib/components/VAlert/index.js",
"file": "vuetify_components_VAlert.js",
"fileHash": "1e0ea25e",
"needsInterop": false
},
"vuetify/components/VCheckbox": {
"src": "../../vuetify/lib/components/VCheckbox/index.js",
"file": "vuetify_components_VCheckbox.js",
"fileHash": "32261bb2",
"needsInterop": false "needsInterop": false
}, },
"vuetify/components/VExpansionPanel": { "vuetify/components/VExpansionPanel": {
"src": "../../vuetify/lib/components/VExpansionPanel/index.js", "src": "../../vuetify/lib/components/VExpansionPanel/index.js",
"file": "vuetify_components_VExpansionPanel.js", "file": "vuetify_components_VExpansionPanel.js",
"fileHash": "e8175e4b", "fileHash": "13bd06bb",
"needsInterop": false "needsInterop": false
}, },
"vuetify/components/VSlider": { "vuetify/components/VSlider": {
"src": "../../vuetify/lib/components/VSlider/index.js", "src": "../../vuetify/lib/components/VSlider/index.js",
"file": "vuetify_components_VSlider.js", "file": "vuetify_components_VSlider.js",
"fileHash": "70d54e33", "fileHash": "e6b50146",
"needsInterop": false "needsInterop": false
}, },
"vuetify/components/transitions": { "vuetify/components/transitions": {
"src": "../../vuetify/lib/components/transitions/index.js", "src": "../../vuetify/lib/components/transitions/index.js",
"file": "vuetify_components_transitions.js", "file": "vuetify_components_transitions.js",
"fileHash": "1ae42e15", "fileHash": "1f8b3903",
"needsInterop": false
},
"vuetify/components/VImg": {
"src": "../../vuetify/lib/components/VImg/index.js",
"file": "vuetify_components_VImg.js",
"fileHash": "55902c55",
"needsInterop": false
},
"vuetify/components/VProgressCircular": {
"src": "../../vuetify/lib/components/VProgressCircular/index.js",
"file": "vuetify_components_VProgressCircular.js",
"fileHash": "4ac970e2",
"needsInterop": false
},
"vuetify/components/VSheet": {
"src": "../../vuetify/lib/components/VSheet/index.js",
"file": "vuetify_components_VSheet.js",
"fileHash": "60500776",
"needsInterop": false "needsInterop": false
}, },
"vuetify/components/VForm": { "vuetify/components/VForm": {
"src": "../../vuetify/lib/components/VForm/index.js", "src": "../../vuetify/lib/components/VForm/index.js",
"file": "vuetify_components_VForm.js", "file": "vuetify_components_VForm.js",
"fileHash": "6098d2ad", "fileHash": "ae2d51af",
"needsInterop": false "needsInterop": false
}, },
"vuetify/components/VTextField": { "vuetify/components/VSheet": {
"src": "../../vuetify/lib/components/VTextField/index.js", "src": "../../vuetify/lib/components/VSheet/index.js",
"file": "vuetify_components_VTextField.js", "file": "vuetify_components_VSheet.js",
"fileHash": "6c3b6c9f", "fileHash": "61437271",
"needsInterop": false
},
"vuetify/components/VBtnToggle": {
"src": "../../vuetify/lib/components/VBtnToggle/index.js",
"file": "vuetify_components_VBtnToggle.js",
"fileHash": "92b719ca",
"needsInterop": false "needsInterop": false
}, },
"vuetify/components/VTextarea": { "vuetify/components/VTextarea": {
"src": "../../vuetify/lib/components/VTextarea/index.js", "src": "../../vuetify/lib/components/VTextarea/index.js",
"file": "vuetify_components_VTextarea.js", "file": "vuetify_components_VTextarea.js",
"fileHash": "c4710e99", "fileHash": "59ca68d2",
"needsInterop": false
},
"vuetify/components/VBtnToggle": {
"src": "../../vuetify/lib/components/VBtnToggle/index.js",
"file": "vuetify_components_VBtnToggle.js",
"fileHash": "15a21fa2",
"needsInterop": false
},
"vuetify/components/VProgressCircular": {
"src": "../../vuetify/lib/components/VProgressCircular/index.js",
"file": "vuetify_components_VProgressCircular.js",
"fileHash": "f97f0ffe",
"needsInterop": false
},
"vuetify/components/VTextField": {
"src": "../../vuetify/lib/components/VTextField/index.js",
"file": "vuetify_components_VTextField.js",
"fileHash": "ef211ea3",
"needsInterop": false
},
"vuetify/components/VTooltip": {
"src": "../../vuetify/lib/components/VTooltip/index.js",
"file": "vuetify_components_VTooltip.js",
"fileHash": "37f415ca",
"needsInterop": false
},
"vuetify/components/VBottomNavigation": {
"src": "../../vuetify/lib/components/VBottomNavigation/index.js",
"file": "vuetify_components_VBottomNavigation.js",
"fileHash": "122f68ff",
"needsInterop": false
},
"vuetify/components/VOverlay": {
"src": "../../vuetify/lib/components/VOverlay/index.js",
"file": "vuetify_components_VOverlay.js",
"fileHash": "6830175b",
"needsInterop": false
},
"vuetify/components/VCombobox": {
"src": "../../vuetify/lib/components/VCombobox/index.js",
"file": "vuetify_components_VCombobox.js",
"fileHash": "144d5e6c",
"needsInterop": false
},
"vuetify/components/VAvatar": {
"src": "../../vuetify/lib/components/VAvatar/index.js",
"file": "vuetify_components_VAvatar.js",
"fileHash": "81b98f53",
"needsInterop": false
},
"vuetify/components/VImg": {
"src": "../../vuetify/lib/components/VImg/index.js",
"file": "vuetify_components_VImg.js",
"fileHash": "55ad767e",
"needsInterop": false
},
"vuetify/components/VSwitch": {
"src": "../../vuetify/lib/components/VSwitch/index.js",
"file": "vuetify_components_VSwitch.js",
"fileHash": "7937a40d",
"needsInterop": false "needsInterop": false
} }
}, },
"chunks": { "chunks": {
"chunk-KPWCGWEX": { "chunk-244ON7Z6": {
"file": "chunk-KPWCGWEX.js" "file": "chunk-244ON7Z6.js"
}, },
"chunk-5LZQZ6A5": { "chunk-5LZQZ6A5": {
"file": "chunk-5LZQZ6A5.js" "file": "chunk-5LZQZ6A5.js"
}, },
"chunk-F6LZGKE2": { "chunk-T4XKRUDE": {
"file": "chunk-F6LZGKE2.js" "file": "chunk-T4XKRUDE.js"
}, },
"chunk-4MMPKXON": { "chunk-4MMPKXON": {
"file": "chunk-4MMPKXON.js" "file": "chunk-4MMPKXON.js"
}, },
"chunk-LHMOJDYM": { "chunk-F6LZGKE2": {
"file": "chunk-LHMOJDYM.js" "file": "chunk-F6LZGKE2.js"
}, },
"chunk-TSTWIJCO": { "chunk-LFKLAYDH": {
"file": "chunk-TSTWIJCO.js" "file": "chunk-LFKLAYDH.js"
},
"chunk-YDVSSFCH": {
"file": "chunk-YDVSSFCH.js"
},
"chunk-RNFCLGZV": {
"file": "chunk-RNFCLGZV.js"
},
"chunk-WNCBJDZZ": {
"file": "chunk-WNCBJDZZ.js"
},
"chunk-34XWDQGB": {
"file": "chunk-34XWDQGB.js"
}, },
"chunk-RH43OVWP": { "chunk-RH43OVWP": {
"file": "chunk-RH43OVWP.js" "file": "chunk-RH43OVWP.js"
}, },
"chunk-PEUMXYT2": { "chunk-QMLWKRPM": {
"file": "chunk-PEUMXYT2.js" "file": "chunk-QMLWKRPM.js"
}, },
"chunk-4HCXUCLO": { "chunk-SCY7DFFY": {
"file": "chunk-4HCXUCLO.js" "file": "chunk-SCY7DFFY.js"
}, },
"chunk-ERI3RTQS": { "chunk-WGWEQK64": {
"file": "chunk-ERI3RTQS.js" "file": "chunk-WGWEQK64.js"
}, },
"chunk-VSEC5QGQ": { "chunk-7M5DDQ46": {
"file": "chunk-VSEC5QGQ.js" "file": "chunk-7M5DDQ46.js"
}, },
"chunk-BYBKODOB": { "chunk-3NOJ7Z44": {
"file": "chunk-BYBKODOB.js" "file": "chunk-3NOJ7Z44.js"
}, },
"chunk-SHBPUR7N": { "chunk-FCTFWI5H": {
"file": "chunk-SHBPUR7N.js" "file": "chunk-FCTFWI5H.js"
}, },
"chunk-HGIC64KW": { "chunk-RB6DAYHX": {
"file": "chunk-HGIC64KW.js" "file": "chunk-RB6DAYHX.js"
}, },
"chunk-BXEVD6W6": { "chunk-QV3QXWPM": {
"file": "chunk-BXEVD6W6.js" "file": "chunk-QV3QXWPM.js"
}, },
"chunk-LPZZJ62X": { "chunk-IZDIYS2S": {
"file": "chunk-LPZZJ62X.js" "file": "chunk-IZDIYS2S.js"
}, },
"chunk-UIXKNP5E": { "chunk-6MO6MKTY": {
"file": "chunk-UIXKNP5E.js" "file": "chunk-6MO6MKTY.js"
},
"chunk-DRSCVHAQ": {
"file": "chunk-DRSCVHAQ.js"
},
"chunk-TSYQL5U3": {
"file": "chunk-TSYQL5U3.js"
},
"chunk-MVBFAXEB": {
"file": "chunk-MVBFAXEB.js"
},
"chunk-QC7VWQER": {
"file": "chunk-QC7VWQER.js"
},
"chunk-NURQ3473": {
"file": "chunk-NURQ3473.js"
},
"chunk-ET5GCC5W": {
"file": "chunk-ET5GCC5W.js"
},
"chunk-W6DJL42A": {
"file": "chunk-W6DJL42A.js"
},
"chunk-HV7P7R7J": {
"file": "chunk-HV7P7R7J.js"
},
"chunk-MPVSDM4V": {
"file": "chunk-MPVSDM4V.js"
},
"chunk-HOEZWRBG": {
"file": "chunk-HOEZWRBG.js"
},
"chunk-Z3677HMS": {
"file": "chunk-Z3677HMS.js"
},
"chunk-ADXDXPPR": {
"file": "chunk-ADXDXPPR.js"
},
"chunk-7SKEB5LN": {
"file": "chunk-7SKEB5LN.js"
},
"chunk-PCKQGDLK": {
"file": "chunk-PCKQGDLK.js"
},
"chunk-6CRS5HI4": {
"file": "chunk-6CRS5HI4.js"
},
"chunk-X7YBE6PF": {
"file": "chunk-X7YBE6PF.js"
},
"chunk-YJD2PNP6": {
"file": "chunk-YJD2PNP6.js"
},
"chunk-I4SZJCEY": {
"file": "chunk-I4SZJCEY.js"
},
"chunk-OI3EYDCB": {
"file": "chunk-OI3EYDCB.js"
}, },
"chunk-IZQZFEAL": { "chunk-IZQZFEAL": {
"file": "chunk-IZQZFEAL.js" "file": "chunk-IZQZFEAL.js"
}, },
"chunk-J7Q2NMV7": { "chunk-6HLLVLOR": {
"file": "chunk-J7Q2NMV7.js" "file": "chunk-6HLLVLOR.js"
}, },
"chunk-4VABFUPG": { "chunk-RWSHRKUK": {
"file": "chunk-4VABFUPG.js" "file": "chunk-RWSHRKUK.js"
},
"chunk-EKMPZMZF": {
"file": "chunk-EKMPZMZF.js"
},
"chunk-OI3EYDCB": {
"file": "chunk-OI3EYDCB.js"
},
"chunk-ITO3JWXJ": {
"file": "chunk-ITO3JWXJ.js"
},
"chunk-PLNIG6WP": {
"file": "chunk-PLNIG6WP.js"
},
"chunk-LPDOFVDK": {
"file": "chunk-LPDOFVDK.js"
},
"chunk-CCNWGDBL": {
"file": "chunk-CCNWGDBL.js"
},
"chunk-P6EQK5CJ": {
"file": "chunk-P6EQK5CJ.js"
},
"chunk-H2QFYTJQ": {
"file": "chunk-H2QFYTJQ.js"
},
"chunk-CMTCIV56": {
"file": "chunk-CMTCIV56.js"
},
"chunk-MQSMF5CI": {
"file": "chunk-MQSMF5CI.js"
},
"chunk-SXI5CS7S": {
"file": "chunk-SXI5CS7S.js"
},
"chunk-X3WY3J75": {
"file": "chunk-X3WY3J75.js"
},
"chunk-Q5BHIW5H": {
"file": "chunk-Q5BHIW5H.js"
},
"chunk-ZB5S5BI5": {
"file": "chunk-ZB5S5BI5.js"
},
"chunk-TTIGZHWR": {
"file": "chunk-TTIGZHWR.js"
},
"chunk-PCKQGDLK": {
"file": "chunk-PCKQGDLK.js"
},
"chunk-TN4XZECZ": {
"file": "chunk-TN4XZECZ.js"
},
"chunk-L7BQJLEW": {
"file": "chunk-L7BQJLEW.js"
},
"chunk-KHWE4FHS": {
"file": "chunk-KHWE4FHS.js"
},
"chunk-IK67YVHF": {
"file": "chunk-IK67YVHF.js"
}, },
"chunk-GAYXO4FM": { "chunk-GAYXO4FM": {
"file": "chunk-GAYXO4FM.js" "file": "chunk-GAYXO4FM.js"
@@ -369,11 +396,83 @@
"chunk-B4JFJDWM": { "chunk-B4JFJDWM": {
"file": "chunk-B4JFJDWM.js" "file": "chunk-B4JFJDWM.js"
}, },
"chunk-U2DSRSME": { "chunk-WGYWYQOA": {
"file": "chunk-U2DSRSME.js" "file": "chunk-WGYWYQOA.js"
}, },
"chunk-FVDWV4NO": { "chunk-NJV3GUE4": {
"file": "chunk-FVDWV4NO.js" "file": "chunk-NJV3GUE4.js"
},
"chunk-KPWCGWEX": {
"file": "chunk-KPWCGWEX.js"
},
"chunk-2AYDXCXT": {
"file": "chunk-2AYDXCXT.js"
},
"chunk-EXUNAYJA": {
"file": "chunk-EXUNAYJA.js"
},
"chunk-CL6G3ICF": {
"file": "chunk-CL6G3ICF.js"
},
"chunk-YGX2Z63P": {
"file": "chunk-YGX2Z63P.js"
},
"chunk-BVUPSGRF": {
"file": "chunk-BVUPSGRF.js"
},
"chunk-5ONGSJQ2": {
"file": "chunk-5ONGSJQ2.js"
},
"chunk-GUNW7S2L": {
"file": "chunk-GUNW7S2L.js"
},
"chunk-CAGZXMOS": {
"file": "chunk-CAGZXMOS.js"
},
"chunk-N4LJHDYD": {
"file": "chunk-N4LJHDYD.js"
},
"chunk-47GEKY7C": {
"file": "chunk-47GEKY7C.js"
},
"chunk-KQXZAEJP": {
"file": "chunk-KQXZAEJP.js"
},
"chunk-MVBFAXEB": {
"file": "chunk-MVBFAXEB.js"
},
"chunk-UQALH6SL": {
"file": "chunk-UQALH6SL.js"
},
"chunk-5AQLTWWS": {
"file": "chunk-5AQLTWWS.js"
},
"chunk-W6DJL42A": {
"file": "chunk-W6DJL42A.js"
},
"chunk-HOEZWRBG": {
"file": "chunk-HOEZWRBG.js"
},
"chunk-ADXDXPPR": {
"file": "chunk-ADXDXPPR.js"
},
"chunk-X7YBE6PF": {
"file": "chunk-X7YBE6PF.js"
},
"chunk-YJD2PNP6": {
"file": "chunk-YJD2PNP6.js"
},
"chunk-GSWLJLRT": {
"file": "chunk-GSWLJLRT.js"
},
"chunk-MH76ZJYE": {
"file": "chunk-MH76ZJYE.js"
},
"chunk-UXZ55AO7": {
"file": "chunk-UXZ55AO7.js"
},
"chunk-6RFSM2OD": {
"file": "chunk-6RFSM2OD.js"
}, },
"chunk-AJ7MUB7M": { "chunk-AJ7MUB7M": {
"file": "chunk-AJ7MUB7M.js" "file": "chunk-AJ7MUB7M.js"
@@ -384,69 +483,36 @@
"chunk-TM3MIDYC": { "chunk-TM3MIDYC": {
"file": "chunk-TM3MIDYC.js" "file": "chunk-TM3MIDYC.js"
}, },
"chunk-CMZ4UYUX": { "chunk-4XZHKYGH": {
"file": "chunk-CMZ4UYUX.js" "file": "chunk-4XZHKYGH.js"
}, },
"chunk-PIT7IT55": { "chunk-PIT7IT55": {
"file": "chunk-PIT7IT55.js" "file": "chunk-PIT7IT55.js"
}, },
"chunk-VH7QIDM4": {
"file": "chunk-VH7QIDM4.js"
},
"chunk-AQK3CYVW": { "chunk-AQK3CYVW": {
"file": "chunk-AQK3CYVW.js" "file": "chunk-AQK3CYVW.js"
}, },
"chunk-PAT7NP2Z": {
"file": "chunk-PAT7NP2Z.js"
},
"chunk-2HZIACSB": { "chunk-2HZIACSB": {
"file": "chunk-2HZIACSB.js" "file": "chunk-2HZIACSB.js"
}, },
"chunk-WGYWYQOA": { "chunk-OJDZBU5D": {
"file": "chunk-WGYWYQOA.js" "file": "chunk-OJDZBU5D.js"
},
"chunk-NJV3GUE4": {
"file": "chunk-NJV3GUE4.js"
},
"chunk-6VKWM6MN": {
"file": "chunk-6VKWM6MN.js"
},
"chunk-TS6Y6OVT": {
"file": "chunk-TS6Y6OVT.js"
},
"chunk-YGX2Z63P": {
"file": "chunk-YGX2Z63P.js"
},
"chunk-5BPWKCNA": {
"file": "chunk-5BPWKCNA.js"
},
"chunk-BVUPSGRF": {
"file": "chunk-BVUPSGRF.js"
}, },
"chunk-573D4TYC": { "chunk-573D4TYC": {
"file": "chunk-573D4TYC.js" "file": "chunk-573D4TYC.js"
}, },
"chunk-GUNW7S2L": { "chunk-DS4J67XG": {
"file": "chunk-GUNW7S2L.js" "file": "chunk-DS4J67XG.js"
}, },
"chunk-5ONGSJQ2": { "chunk-BHV5Z34M": {
"file": "chunk-5ONGSJQ2.js" "file": "chunk-BHV5Z34M.js"
},
"chunk-CAGZXMOS": {
"file": "chunk-CAGZXMOS.js"
}, },
"chunk-6EZKHCRW": { "chunk-6EZKHCRW": {
"file": "chunk-6EZKHCRW.js" "file": "chunk-6EZKHCRW.js"
}, },
"chunk-N4LJHDYD": {
"file": "chunk-N4LJHDYD.js"
},
"chunk-OJDZBU5D": {
"file": "chunk-OJDZBU5D.js"
},
"chunk-IYFY4IHW": {
"file": "chunk-IYFY4IHW.js"
},
"chunk-KQXZAEJP": {
"file": "chunk-KQXZAEJP.js"
},
"chunk-XDSAO7OT": { "chunk-XDSAO7OT": {
"file": "chunk-XDSAO7OT.js" "file": "chunk-XDSAO7OT.js"
}, },
@@ -456,12 +522,6 @@
"chunk-KA77II4V": { "chunk-KA77II4V": {
"file": "chunk-KA77II4V.js" "file": "chunk-KA77II4V.js"
}, },
"chunk-WXMECWZN": {
"file": "chunk-WXMECWZN.js"
},
"chunk-BHV5Z34M": {
"file": "chunk-BHV5Z34M.js"
},
"chunk-3DSR7NCB": { "chunk-3DSR7NCB": {
"file": "chunk-3DSR7NCB.js" "file": "chunk-3DSR7NCB.js"
}, },
+663
View File
@@ -0,0 +1,663 @@
import {
highlightResult,
makeFilterProps,
useFilter
} from "./chunk-RB6DAYHX.js";
import {
VVirtualScroll,
makeSelectProps,
useMenuActivator,
useScrolling
} from "./chunk-IZDIYS2S.js";
import {
VTextField,
makeVTextFieldProps
} from "./chunk-6MO6MKTY.js";
import {
VCheckboxBtn
} from "./chunk-6HLLVLOR.js";
import {
VChip
} from "./chunk-PLNIG6WP.js";
import {
VMenu
} from "./chunk-LPDOFVDK.js";
import {
VList,
VListItem,
VListSubheader,
transformItem,
useItems
} from "./chunk-MQSMF5CI.js";
import {
VAvatar
} from "./chunk-SXI5CS7S.js";
import {
VDivider
} from "./chunk-X3WY3J75.js";
import {
useInputIcon
} from "./chunk-IK67YVHF.js";
import {
useForm
} from "./chunk-GAYXO4FM.js";
import {
forwardRefs
} from "./chunk-GUNW7S2L.js";
import {
VIcon
} from "./chunk-4XZHKYGH.js";
import {
VDefaultsProvider
} from "./chunk-PAT7NP2Z.js";
import {
useTextColor
} from "./chunk-BHV5Z34M.js";
import {
useLocale
} from "./chunk-ENTLCOCZ.js";
import {
useProxiedModel
} from "./chunk-KA77II4V.js";
import {
IN_BROWSER,
checkPrintable,
deepEqual,
ensureValidVNode,
escapeForRegex,
genericComponent,
isComposingIgnoreKey,
noop,
omit,
propsFactory,
useRender,
wrapInArray
} from "./chunk-LZ5T2ERG.js";
import {
Fragment,
computed,
createBaseVNode,
createTextVNode,
createVNode,
mergeProps,
nextTick,
normalizeClass,
normalizeStyle,
ref,
shallowRef,
toRef,
watch
} from "./chunk-2MKFL3BX.js";
// node_modules/vuetify/lib/components/VCombobox/VCombobox.js
import "/home/david/gitea_repo/personal_development/ServerManagerWebApp/frontend/node_modules/vuetify/lib/components/VCombobox/VCombobox.css";
var makeVComboboxProps = propsFactory({
alwaysFilter: Boolean,
autoSelectFirst: {
type: [Boolean, String]
},
clearOnSelect: {
type: Boolean,
default: true
},
delimiters: Array,
...makeFilterProps({
filterKeys: ["title"]
}),
...makeSelectProps({
hideNoData: true,
returnObject: true
}),
...omit(makeVTextFieldProps({
modelValue: null,
role: "combobox"
}), ["validationValue", "dirty"])
}, "VCombobox");
var VCombobox = genericComponent()({
name: "VCombobox",
props: makeVComboboxProps(),
emits: {
"update:focused": (focused) => true,
"update:modelValue": (value) => true,
"update:search": (value) => true,
"update:menu": (value) => true
},
setup(props, _ref) {
let {
emit,
slots
} = _ref;
const {
t
} = useLocale();
const vTextFieldRef = ref();
const isFocused = shallowRef(false);
const isPristine = shallowRef(true);
const listHasFocus = shallowRef(false);
const vMenuRef = ref();
const vVirtualScrollRef = ref();
const selectionIndex = shallowRef(-1);
let cleared = false;
const {
items,
transformIn,
transformOut
} = useItems(props);
const {
textColorClasses,
textColorStyles
} = useTextColor(() => vTextFieldRef.value?.color);
const {
InputIcon
} = useInputIcon(props);
const model = useProxiedModel(props, "modelValue", [], (v) => transformIn(wrapInArray(v)), (v) => {
const transformed = transformOut(v);
return props.multiple ? transformed : transformed[0] ?? null;
});
const form = useForm(props);
const closableChips = toRef(() => props.closableChips && !form.isReadonly.value && !form.isDisabled.value);
const hasChips = computed(() => !!(props.chips || slots.chip));
const hasSelectionSlot = computed(() => hasChips.value || !!slots.selection);
const _search = shallowRef(!props.multiple && !hasSelectionSlot.value ? model.value[0]?.title ?? "" : "");
const _searchLock = shallowRef(null);
const search = computed({
get: () => {
return _search.value;
},
set: async (val) => {
_search.value = val ?? "";
if (val === null || val === "" && !props.multiple && !hasSelectionSlot.value) {
model.value = [];
} else if (!props.multiple && !hasSelectionSlot.value) {
model.value = [transformItem(props, val)];
nextTick(() => vVirtualScrollRef.value?.scrollToIndex(0));
}
if (val && props.multiple && props.delimiters?.length) {
const values = splitByDelimiters(val);
if (values.length > 1) {
selectMultiple(values);
_search.value = "";
}
}
if (!val) selectionIndex.value = -1;
isPristine.value = !val;
}
});
const counterValue = computed(() => {
return typeof props.counterValue === "function" ? props.counterValue(model.value) : typeof props.counterValue === "number" ? props.counterValue : props.multiple ? model.value.length : search.value.length;
});
const {
filteredItems,
getMatches
} = useFilter(props, items, () => _searchLock.value ?? (props.alwaysFilter || !isPristine.value ? search.value : ""));
const displayItems = computed(() => {
if (props.hideSelected && _searchLock.value === null) {
return filteredItems.value.filter((filteredItem) => !model.value.some((s) => s.value === filteredItem.value));
}
return filteredItems.value;
});
const menuDisabled = computed(() => props.hideNoData && !displayItems.value.length || form.isReadonly.value || form.isDisabled.value);
const _menu = useProxiedModel(props, "menu");
const menu = computed({
get: () => _menu.value,
set: (v) => {
if (_menu.value && !v && vMenuRef.value?.ΨopenChildren.size) return;
if (v && menuDisabled.value) return;
_menu.value = v;
}
});
const {
menuId,
ariaExpanded,
ariaControls
} = useMenuActivator(props, menu);
watch(_search, (value) => {
if (cleared) {
nextTick(() => cleared = false);
} else if (isFocused.value && !menu.value) {
menu.value = true;
}
emit("update:search", value);
});
watch(model, (value) => {
if (!props.multiple && !hasSelectionSlot.value) {
_search.value = value[0]?.title ?? "";
}
});
const selectedValues = computed(() => model.value.map((selection) => selection.value));
const firstSelectableItem = computed(() => displayItems.value.find((x) => x.type === "item" && !x.props.disabled));
const highlightFirst = computed(() => {
const selectFirst = props.autoSelectFirst === true || props.autoSelectFirst === "exact" && search.value === firstSelectableItem.value?.title;
return selectFirst && displayItems.value.length > 0 && !isPristine.value && !listHasFocus.value;
});
const listRef = ref();
const listEvents = useScrolling(listRef, vTextFieldRef);
function onClear(e) {
cleared = true;
nextTick(() => cleared = false);
if (props.openOnClear) {
menu.value = true;
}
}
function onMousedownControl() {
if (menuDisabled.value) return;
menu.value = true;
}
function onMousedownMenuIcon(e) {
if (menuDisabled.value) return;
if (isFocused.value) {
e.preventDefault();
e.stopPropagation();
}
menu.value = !menu.value;
}
function onListKeydown(e) {
if (checkPrintable(e) || e.key === "Backspace") {
vTextFieldRef.value?.focus();
}
}
function onKeydown(e) {
if (isComposingIgnoreKey(e) || form.isReadonly.value) return;
const selectionStart = vTextFieldRef.value?.selectionStart;
const length = model.value.length;
if (["Enter", "ArrowDown", "ArrowUp"].includes(e.key)) {
e.preventDefault();
}
if (["Enter", "ArrowDown"].includes(e.key)) {
menu.value = true;
}
if (["Escape"].includes(e.key)) {
menu.value = false;
}
if (highlightFirst.value && ["Enter", "Tab"].includes(e.key) && firstSelectableItem.value && !model.value.some((_ref2) => {
let {
value
} = _ref2;
return value === firstSelectableItem.value.value;
})) {
select(firstSelectableItem.value);
}
if (e.key === "ArrowDown" && highlightFirst.value) {
listRef.value?.focus("next");
}
if (e.key === "Enter" && search.value) {
select(transformItem(props, search.value), true, true);
if (hasSelectionSlot.value) _search.value = "";
}
if (["Backspace", "Delete"].includes(e.key)) {
if (!props.multiple && hasSelectionSlot.value && model.value.length > 0 && !search.value) return select(model.value[0], false);
if (~selectionIndex.value) {
e.preventDefault();
const originalSelectionIndex = selectionIndex.value;
select(model.value[selectionIndex.value], false);
selectionIndex.value = originalSelectionIndex >= length - 1 ? length - 2 : originalSelectionIndex;
} else if (e.key === "Backspace" && !search.value) {
selectionIndex.value = length - 1;
}
return;
}
if (!props.multiple) return;
if (e.key === "ArrowLeft") {
if (selectionIndex.value < 0 && selectionStart && selectionStart > 0) return;
const prev = selectionIndex.value > -1 ? selectionIndex.value - 1 : length - 1;
if (model.value[prev]) {
selectionIndex.value = prev;
} else {
selectionIndex.value = -1;
vTextFieldRef.value?.setSelectionRange(search.value.length, search.value.length);
}
} else if (e.key === "ArrowRight") {
if (selectionIndex.value < 0) return;
const next = selectionIndex.value + 1;
if (model.value[next]) {
selectionIndex.value = next;
} else {
selectionIndex.value = -1;
vTextFieldRef.value?.setSelectionRange(0, 0);
}
} else if (~selectionIndex.value && checkPrintable(e)) {
selectionIndex.value = -1;
}
}
function onPaste(e) {
const clipboardText = e?.clipboardData?.getData("Text") ?? "";
const values = splitByDelimiters(clipboardText);
if (values.length > 1 && props.multiple) {
e.preventDefault();
selectMultiple(values);
}
}
function onAfterEnter() {
if (props.eager) {
vVirtualScrollRef.value?.calculateVisibleItems();
}
}
function onAfterLeave() {
if (isFocused.value) {
vTextFieldRef.value?.focus();
}
isPristine.value = true;
_searchLock.value = null;
}
function select(item) {
let set = arguments.length > 1 && arguments[1] !== void 0 ? arguments[1] : true;
let keepMenu = arguments.length > 2 && arguments[2] !== void 0 ? arguments[2] : false;
if (!item || item.props.disabled) return;
if (props.multiple) {
const index = model.value.findIndex((selection) => (props.valueComparator || deepEqual)(selection.value, item.value));
const add = set == null ? !~index : set;
if (~index) {
const value = add ? [...model.value, item] : [...model.value];
value.splice(index, 1);
model.value = value;
} else if (add) {
model.value = [...model.value, item];
}
if (props.clearOnSelect) {
search.value = "";
}
} else {
const add = set !== false;
model.value = add ? [item] : [];
if ((!isPristine.value || props.alwaysFilter) && _search.value) {
_searchLock.value = _search.value;
}
_search.value = add && !hasSelectionSlot.value ? item.title : "";
nextTick(() => {
menu.value = keepMenu;
isPristine.value = true;
});
}
}
function splitByDelimiters(val) {
const effectiveDelimiters = ["\n", ...props.delimiters ?? []];
const signsToMatch = effectiveDelimiters.map(escapeForRegex).join("|");
return val.split(new RegExp(`(?:${signsToMatch})+`));
}
async function selectMultiple(values) {
for (let value of values) {
value = value.trim();
if (value) {
select(transformItem(props, value));
await nextTick();
}
}
}
function onFocusin(e) {
isFocused.value = true;
setTimeout(() => {
listHasFocus.value = true;
});
}
function onFocusout(e) {
listHasFocus.value = false;
}
watch(isFocused, (val, oldVal) => {
if (val || val === oldVal) return;
selectionIndex.value = -1;
menu.value = false;
if (search.value) {
if (props.multiple) {
select(transformItem(props, search.value));
return;
}
if (!hasSelectionSlot.value) return;
if (model.value.some((_ref3) => {
let {
title
} = _ref3;
return title === search.value;
})) {
_search.value = "";
} else {
select(transformItem(props, search.value));
}
}
});
watch(menu, (val) => {
if (!props.hideSelected && val && model.value.length && isPristine.value) {
const index = displayItems.value.findIndex((item) => model.value.some((s) => (props.valueComparator || deepEqual)(s.value, item.value)));
IN_BROWSER && window.requestAnimationFrame(() => {
index >= 0 && vVirtualScrollRef.value?.scrollToIndex(index);
});
}
if (val) _searchLock.value = null;
});
watch(items, (newVal, oldVal) => {
if (menu.value) return;
if (isFocused.value && !oldVal.length && newVal.length) {
menu.value = true;
}
});
useRender(() => {
const hasList = !!(!props.hideNoData || displayItems.value.length || slots["prepend-item"] || slots["append-item"] || slots["no-data"]);
const isDirty = model.value.length > 0;
const textFieldProps = VTextField.filterProps(props);
return createVNode(VTextField, mergeProps({
"ref": vTextFieldRef
}, textFieldProps, {
"modelValue": search.value,
"onUpdate:modelValue": ($event) => search.value = $event,
"focused": isFocused.value,
"onUpdate:focused": ($event) => isFocused.value = $event,
"validationValue": model.externalValue,
"counterValue": counterValue.value,
"dirty": isDirty,
"class": ["v-combobox", {
"v-combobox--active-menu": menu.value,
"v-combobox--chips": !!props.chips,
"v-combobox--selection-slot": !!hasSelectionSlot.value,
"v-combobox--selecting-index": selectionIndex.value > -1,
[`v-combobox--${props.multiple ? "multiple" : "single"}`]: true
}, props.class],
"style": props.style,
"readonly": form.isReadonly.value,
"placeholder": isDirty ? void 0 : props.placeholder,
"onClick:clear": onClear,
"onMousedown:control": onMousedownControl,
"onKeydown": onKeydown,
"onPaste": onPaste,
"aria-expanded": ariaExpanded.value,
"aria-controls": ariaControls.value
}), {
...slots,
default: (_ref4) => {
let {
id
} = _ref4;
return createBaseVNode(Fragment, null, [createVNode(VMenu, mergeProps({
"id": menuId.value,
"ref": vMenuRef,
"modelValue": menu.value,
"onUpdate:modelValue": ($event) => menu.value = $event,
"activator": "parent",
"contentClass": "v-combobox__content",
"disabled": menuDisabled.value,
"eager": props.eager,
"maxHeight": 310,
"openOnClick": false,
"closeOnContentClick": false,
"onAfterEnter": onAfterEnter,
"onAfterLeave": onAfterLeave
}, props.menuProps), {
default: () => [hasList && createVNode(VList, mergeProps({
"ref": listRef,
"filterable": true,
"selected": selectedValues.value,
"selectStrategy": props.multiple ? "independent" : "single-independent",
"onMousedown": (e) => e.preventDefault(),
"selectable": true,
"onKeydown": onListKeydown,
"onFocusin": onFocusin,
"onFocusout": onFocusout,
"tabindex": "-1",
"aria-live": "polite",
"aria-labelledby": `${id.value}-label`,
"aria-multiselectable": props.multiple,
"color": props.itemColor ?? props.color
}, listEvents, props.listProps), {
default: () => [slots["prepend-item"]?.(), !displayItems.value.length && !props.hideNoData && (slots["no-data"]?.() ?? createVNode(VListItem, {
"key": "no-data",
"title": t(props.noDataText)
}, null)), createVNode(VVirtualScroll, {
"ref": vVirtualScrollRef,
"renderless": true,
"items": displayItems.value,
"itemKey": "value"
}, {
default: (_ref5) => {
let {
item,
index,
itemRef
} = _ref5;
const itemProps = mergeProps(item.props, {
ref: itemRef,
key: item.value,
active: highlightFirst.value && item === firstSelectableItem.value ? true : void 0,
onClick: () => select(item, null),
"aria-posinset": index + 1,
"aria-setsize": displayItems.value.length
});
if (item.type === "divider") {
return slots.divider?.({
props: item.raw,
index
}) ?? createVNode(VDivider, mergeProps(item.props, {
"key": `divider-${index}`
}), null);
}
if (item.type === "subheader") {
return slots.subheader?.({
props: item.raw,
index
}) ?? createVNode(VListSubheader, mergeProps(item.props, {
"key": `subheader-${index}`
}), null);
}
return slots.item?.({
item,
index,
props: itemProps
}) ?? createVNode(VListItem, mergeProps(itemProps, {
"role": "option"
}), {
prepend: (_ref6) => {
let {
isSelected
} = _ref6;
return createBaseVNode(Fragment, null, [props.multiple && !props.hideSelected ? createVNode(VCheckboxBtn, {
"key": item.value,
"modelValue": isSelected,
"ripple": false,
"tabindex": "-1",
"aria-hidden": true,
"onClick": (event) => event.preventDefault()
}, null) : void 0, item.props.prependAvatar && createVNode(VAvatar, {
"image": item.props.prependAvatar
}, null), item.props.prependIcon && createVNode(VIcon, {
"icon": item.props.prependIcon
}, null)]);
},
title: () => {
return isPristine.value ? item.title : highlightResult("v-combobox", item.title, getMatches(item)?.title);
}
});
}
}), slots["append-item"]?.()]
})]
}), model.value.map((item, index) => {
function onChipClose(e) {
e.stopPropagation();
e.preventDefault();
select(item, false);
}
const slotProps = mergeProps(VChip.filterProps(item.props), {
"onClick:close": onChipClose,
onKeydown(e) {
if (e.key !== "Enter" && e.key !== " ") return;
e.preventDefault();
e.stopPropagation();
onChipClose(e);
},
onMousedown(e) {
e.preventDefault();
e.stopPropagation();
},
modelValue: true,
"onUpdate:modelValue": void 0
});
const hasSlot = hasChips.value ? !!slots.chip : !!slots.selection;
const slotContent = hasSlot ? ensureValidVNode(hasChips.value ? slots.chip({
item,
index,
props: slotProps
}) : slots.selection({
item,
index
})) : void 0;
if (hasSlot && !slotContent) return void 0;
return createBaseVNode("div", {
"key": item.value,
"class": normalizeClass(["v-combobox__selection", index === selectionIndex.value && ["v-combobox__selection--selected", textColorClasses.value]]),
"style": normalizeStyle(index === selectionIndex.value ? textColorStyles.value : {})
}, [hasChips.value ? !slots.chip ? createVNode(VChip, mergeProps({
"key": "chip",
"closable": closableChips.value,
"size": "small",
"text": item.title,
"disabled": item.props.disabled
}, slotProps), null) : createVNode(VDefaultsProvider, {
"key": "chip-defaults",
"defaults": {
VChip: {
closable: closableChips.value,
size: "small",
text: item.title
}
}
}, {
default: () => [slotContent]
}) : slotContent ?? createBaseVNode("span", {
"class": "v-combobox__selection-text"
}, [item.title, props.multiple && index < model.value.length - 1 && createBaseVNode("span", {
"class": "v-combobox__selection-comma"
}, [createTextVNode(",")])])]);
})]);
},
"append-inner": function() {
for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key];
}
return createBaseVNode(Fragment, null, [slots["append-inner"]?.(...args), (!props.hideNoData || props.items.length) && props.menuIcon ? createVNode(VIcon, {
"class": "v-combobox__menu-icon",
"color": vTextFieldRef.value?.fieldIconColor,
"icon": props.menuIcon,
"onMousedown": onMousedownMenuIcon,
"onClick": noop,
"aria-hidden": true,
"tabindex": "-1"
}, null) : void 0, props.appendInnerIcon && createVNode(InputIcon, {
"key": "append-icon",
"name": "appendInner",
"color": args[0].iconColor.value
}, null)]);
}
});
});
return forwardRefs({
isFocused,
isPristine,
menu,
search,
selectionIndex,
filteredItems,
select
}, vTextFieldRef);
}
});
export {
VCombobox
};
//# sourceMappingURL=chunk-244ON7Z6.js.map
File diff suppressed because one or more lines are too long
@@ -1,31 +1,6 @@
import { import {
VOverlay, keys
makeVOverlayProps
} from "./chunk-TS6Y6OVT.js";
import {
useScopeId
} from "./chunk-BVUPSGRF.js";
import {
forwardRefs
} from "./chunk-GUNW7S2L.js";
import {
useProxiedModel
} from "./chunk-KA77II4V.js";
import {
genericComponent,
keys,
omit,
propsFactory,
useRender
} from "./chunk-LZ5T2ERG.js"; } from "./chunk-LZ5T2ERG.js";
import {
computed,
createVNode,
mergeProps,
ref,
toRef,
useId
} from "./chunk-2MKFL3BX.js";
// node_modules/vuetify/lib/directives/resize/index.js // node_modules/vuetify/lib/directives/resize/index.js
function mounted(el, binding) { function mounted(el, binding) {
@@ -163,95 +138,10 @@ var Touch = {
}; };
var touch_default = Touch; var touch_default = Touch;
// node_modules/vuetify/lib/components/VTooltip/VTooltip.js
import "/home/david/Documents/vscode/personal_development/ServerManagerWebApp/frontend/node_modules/vuetify/lib/components/VTooltip/VTooltip.css";
var makeVTooltipProps = propsFactory({
id: String,
interactive: Boolean,
text: String,
...omit(makeVOverlayProps({
closeOnBack: false,
location: "end",
locationStrategy: "connected",
eager: true,
minWidth: 0,
offset: 10,
openOnClick: false,
openOnHover: true,
origin: "auto",
scrim: false,
scrollStrategy: "reposition",
transition: null
}), ["absolute", "retainFocus", "captureFocus", "disableInitialFocus"])
}, "VTooltip");
var VTooltip = genericComponent()({
name: "VTooltip",
props: makeVTooltipProps(),
emits: {
"update:modelValue": (value) => true
},
setup(props, _ref) {
let {
slots
} = _ref;
const isActive = useProxiedModel(props, "modelValue");
const {
scopeId
} = useScopeId();
const uid = useId();
const id = toRef(() => props.id || `v-tooltip-${uid}`);
const overlay = ref();
const location = computed(() => {
return props.location.split(" ").length > 1 ? props.location : props.location + " center";
});
const origin = computed(() => {
return props.origin === "auto" || props.origin === "overlap" || props.origin.split(" ").length > 1 || props.location.split(" ").length > 1 ? props.origin : props.origin + " center";
});
const transition = toRef(() => {
if (props.transition != null) return props.transition;
return isActive.value ? "scale-transition" : "fade-transition";
});
const activatorProps = computed(() => mergeProps({
"aria-describedby": id.value
}, props.activatorProps));
useRender(() => {
const overlayProps = VOverlay.filterProps(props);
return createVNode(VOverlay, mergeProps({
"ref": overlay,
"class": ["v-tooltip", {
"v-tooltip--interactive": props.interactive
}, props.class],
"style": props.style,
"id": id.value
}, overlayProps, {
"modelValue": isActive.value,
"onUpdate:modelValue": ($event) => isActive.value = $event,
"transition": transition.value,
"absolute": true,
"location": location.value,
"origin": origin.value,
"role": "tooltip",
"activatorProps": activatorProps.value,
"_disableGlobalStack": true
}, scopeId), {
activator: slots.activator,
default: function() {
for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key];
}
return slots.default?.(...args) ?? props.text;
}
});
});
return forwardRefs({}, overlay);
}
});
export { export {
Resize, Resize,
resize_default, resize_default,
Touch, Touch,
touch_default, touch_default
VTooltip
}; };
//# sourceMappingURL=chunk-6VKWM6MN.js.map //# sourceMappingURL=chunk-2AYDXCXT.js.map
File diff suppressed because one or more lines are too long
@@ -1,18 +1,18 @@
import {
VAvatar
} from "./chunk-SXI5CS7S.js";
import {
VImg
} from "./chunk-TTIGZHWR.js";
import { import {
makePositionProps, makePositionProps,
usePosition usePosition
} from "./chunk-HOEZWRBG.js"; } from "./chunk-HOEZWRBG.js";
import {
VAvatar
} from "./chunk-Z3677HMS.js";
import { import {
genOverlays, genOverlays,
makeVariantProps, makeVariantProps,
useVariant useVariant
} from "./chunk-ADXDXPPR.js"; } from "./chunk-ADXDXPPR.js";
import {
VImg
} from "./chunk-6CRS5HI4.js";
import { import {
makeBorderProps, makeBorderProps,
useBorder useBorder
@@ -25,7 +25,7 @@ import {
LoaderSlot, LoaderSlot,
makeLoaderProps, makeLoaderProps,
useLoader useLoader
} from "./chunk-U2DSRSME.js"; } from "./chunk-UXZ55AO7.js";
import { import {
makeLocationProps, makeLocationProps,
useLocation useLocation
@@ -36,31 +36,31 @@ import {
} from "./chunk-TM3MIDYC.js"; } from "./chunk-TM3MIDYC.js";
import { import {
VIcon VIcon
} from "./chunk-CMZ4UYUX.js"; } from "./chunk-4XZHKYGH.js";
import {
VDefaultsProvider
} from "./chunk-VH7QIDM4.js";
import { import {
makeRoundedProps, makeRoundedProps,
useRounded useRounded
} from "./chunk-AQK3CYVW.js"; } from "./chunk-AQK3CYVW.js";
import {
VDefaultsProvider
} from "./chunk-PAT7NP2Z.js";
import { import {
makeTagProps makeTagProps
} from "./chunk-2HZIACSB.js"; } from "./chunk-2HZIACSB.js";
import { import {
ripple_default IconValue
} from "./chunk-5BPWKCNA.js"; } from "./chunk-OJDZBU5D.js";
import { import {
makeRouterProps, makeRouterProps,
useLink useLink
} from "./chunk-573D4TYC.js"; } from "./chunk-573D4TYC.js";
import {
ripple_default
} from "./chunk-DS4J67XG.js";
import { import {
makeDimensionProps, makeDimensionProps,
useDimension useDimension
} from "./chunk-6EZKHCRW.js"; } from "./chunk-6EZKHCRW.js";
import {
IconValue
} from "./chunk-OJDZBU5D.js";
import { import {
makeThemeProps, makeThemeProps,
provideTheme provideTheme
@@ -87,7 +87,7 @@ import {
} from "./chunk-2MKFL3BX.js"; } from "./chunk-2MKFL3BX.js";
// node_modules/vuetify/lib/components/VCard/VCard.js // node_modules/vuetify/lib/components/VCard/VCard.js
import "/home/david/Documents/vscode/personal_development/ServerManagerWebApp/frontend/node_modules/vuetify/lib/components/VCard/VCard.css"; import "/home/david/gitea_repo/personal_development/ServerManagerWebApp/frontend/node_modules/vuetify/lib/components/VCard/VCard.css";
// node_modules/vuetify/lib/components/VCard/VCardActions.js // node_modules/vuetify/lib/components/VCard/VCardActions.js
var makeVCardActionsProps = propsFactory({ var makeVCardActionsProps = propsFactory({
@@ -441,4 +441,4 @@ export {
VCardText, VCardText,
VCard VCard
}; };
//# sourceMappingURL=chunk-MPVSDM4V.js.map //# sourceMappingURL=chunk-3NOJ7Z44.js.map
@@ -23,7 +23,7 @@ import {
} from "./chunk-2MKFL3BX.js"; } from "./chunk-2MKFL3BX.js";
// node_modules/vuetify/lib/components/VApp/VApp.js // node_modules/vuetify/lib/components/VApp/VApp.js
import "/home/david/Documents/vscode/personal_development/ServerManagerWebApp/frontend/node_modules/vuetify/lib/components/VApp/VApp.css"; import "/home/david/gitea_repo/personal_development/ServerManagerWebApp/frontend/node_modules/vuetify/lib/components/VApp/VApp.css";
var makeVAppProps = propsFactory({ var makeVAppProps = propsFactory({
...makeComponentProps(), ...makeComponentProps(),
...omit(makeLayoutProps(), ["fullHeight"]), ...omit(makeLayoutProps(), ["fullHeight"]),
@@ -67,4 +67,4 @@ var VApp = genericComponent()({
export { export {
VApp VApp
}; };
//# sourceMappingURL=chunk-IYFY4IHW.js.map //# sourceMappingURL=chunk-47GEKY7C.js.map
File diff suppressed because one or more lines are too long
@@ -33,7 +33,7 @@ import {
} from "./chunk-2MKFL3BX.js"; } from "./chunk-2MKFL3BX.js";
// node_modules/vuetify/lib/components/VIcon/VIcon.js // node_modules/vuetify/lib/components/VIcon/VIcon.js
import "/home/david/Documents/vscode/personal_development/ServerManagerWebApp/frontend/node_modules/vuetify/lib/components/VIcon/VIcon.css"; import "/home/david/gitea_repo/personal_development/ServerManagerWebApp/frontend/node_modules/vuetify/lib/components/VIcon/VIcon.css";
var makeVIconProps = propsFactory({ var makeVIconProps = propsFactory({
color: String, color: String,
disabled: Boolean, disabled: Boolean,
@@ -106,4 +106,4 @@ var VIcon = genericComponent()({
export { export {
VIcon VIcon
}; };
//# sourceMappingURL=chunk-CMZ4UYUX.js.map //# sourceMappingURL=chunk-4XZHKYGH.js.map
@@ -44,7 +44,7 @@ import {
} from "./chunk-2MKFL3BX.js"; } from "./chunk-2MKFL3BX.js";
// node_modules/vuetify/lib/components/VBtnGroup/VBtnGroup.js // node_modules/vuetify/lib/components/VBtnGroup/VBtnGroup.js
import "/home/david/Documents/vscode/personal_development/ServerManagerWebApp/frontend/node_modules/vuetify/lib/components/VBtnGroup/VBtnGroup.css"; import "/home/david/gitea_repo/personal_development/ServerManagerWebApp/frontend/node_modules/vuetify/lib/components/VBtnGroup/VBtnGroup.css";
var makeVBtnGroupProps = propsFactory({ var makeVBtnGroupProps = propsFactory({
baseColor: String, baseColor: String,
divided: Boolean, divided: Boolean,
@@ -105,7 +105,7 @@ var VBtnGroup = genericComponent()({
}); });
// node_modules/vuetify/lib/components/VBtnToggle/VBtnToggle.js // node_modules/vuetify/lib/components/VBtnToggle/VBtnToggle.js
import "/home/david/Documents/vscode/personal_development/ServerManagerWebApp/frontend/node_modules/vuetify/lib/components/VBtnToggle/VBtnToggle.css"; import "/home/david/gitea_repo/personal_development/ServerManagerWebApp/frontend/node_modules/vuetify/lib/components/VBtnToggle/VBtnToggle.css";
var VBtnToggleSymbol = /* @__PURE__ */ Symbol.for("vuetify:v-btn-toggle"); var VBtnToggleSymbol = /* @__PURE__ */ Symbol.for("vuetify:v-btn-toggle");
var makeVBtnToggleProps = propsFactory({ var makeVBtnToggleProps = propsFactory({
...makeVBtnGroupProps(), ...makeVBtnGroupProps(),
@@ -157,4 +157,4 @@ export {
VBtnToggleSymbol, VBtnToggleSymbol,
VBtnToggle VBtnToggle
}; };
//# sourceMappingURL=chunk-NURQ3473.js.map //# sourceMappingURL=chunk-5AQLTWWS.js.map
+162
View File
@@ -0,0 +1,162 @@
import {
VSelectionControl,
makeVSelectionControlProps
} from "./chunk-KHWE4FHS.js";
import {
VInput,
makeVInputProps,
useFocus
} from "./chunk-IK67YVHF.js";
import {
forwardRefs
} from "./chunk-GUNW7S2L.js";
import {
IconValue
} from "./chunk-OJDZBU5D.js";
import {
useProxiedModel
} from "./chunk-KA77II4V.js";
import {
filterInputAttrs,
genericComponent,
omit,
propsFactory,
useRender
} from "./chunk-LZ5T2ERG.js";
import {
createVNode,
mergeProps,
ref,
toRef,
useId
} from "./chunk-2MKFL3BX.js";
// node_modules/vuetify/lib/components/VCheckbox/VCheckbox.js
import "/home/david/gitea_repo/personal_development/ServerManagerWebApp/frontend/node_modules/vuetify/lib/components/VCheckbox/VCheckbox.css";
// node_modules/vuetify/lib/components/VCheckbox/VCheckboxBtn.js
var makeVCheckboxBtnProps = propsFactory({
indeterminate: Boolean,
indeterminateIcon: {
type: IconValue,
default: "$checkboxIndeterminate"
},
...makeVSelectionControlProps({
falseIcon: "$checkboxOff",
trueIcon: "$checkboxOn"
})
}, "VCheckboxBtn");
var VCheckboxBtn = genericComponent()({
name: "VCheckboxBtn",
props: makeVCheckboxBtnProps(),
emits: {
"update:modelValue": (value) => true,
"update:indeterminate": (value) => true
},
setup(props, _ref) {
let {
slots
} = _ref;
const indeterminate = useProxiedModel(props, "indeterminate");
const model = useProxiedModel(props, "modelValue");
function onChange(v) {
if (indeterminate.value) {
indeterminate.value = false;
}
}
const falseIcon = toRef(() => {
return indeterminate.value ? props.indeterminateIcon : props.falseIcon;
});
const trueIcon = toRef(() => {
return indeterminate.value ? props.indeterminateIcon : props.trueIcon;
});
useRender(() => {
const controlProps = omit(VSelectionControl.filterProps(props), ["modelValue"]);
return createVNode(VSelectionControl, mergeProps(controlProps, {
"modelValue": model.value,
"onUpdate:modelValue": [($event) => model.value = $event, onChange],
"class": ["v-checkbox-btn", props.class],
"style": props.style,
"type": "checkbox",
"falseIcon": falseIcon.value,
"trueIcon": trueIcon.value,
"aria-checked": indeterminate.value ? "mixed" : void 0
}), slots);
});
return {};
}
});
// node_modules/vuetify/lib/components/VCheckbox/VCheckbox.js
var makeVCheckboxProps = propsFactory({
...makeVInputProps(),
...omit(makeVCheckboxBtnProps(), ["inline"])
}, "VCheckbox");
var VCheckbox = genericComponent()({
name: "VCheckbox",
inheritAttrs: false,
props: makeVCheckboxProps(),
emits: {
"update:modelValue": (value) => true,
"update:focused": (focused) => true
},
setup(props, _ref) {
let {
attrs,
slots
} = _ref;
const model = useProxiedModel(props, "modelValue");
const {
isFocused,
focus,
blur
} = useFocus(props);
const inputRef = ref();
const uid = useId();
useRender(() => {
const [rootAttrs, controlAttrs] = filterInputAttrs(attrs);
const inputProps = VInput.filterProps(props);
const checkboxProps = VCheckboxBtn.filterProps(props);
return createVNode(VInput, mergeProps({
"ref": inputRef,
"class": ["v-checkbox", props.class]
}, rootAttrs, inputProps, {
"modelValue": model.value,
"onUpdate:modelValue": ($event) => model.value = $event,
"id": props.id || `checkbox-${uid}`,
"focused": isFocused.value,
"style": props.style
}), {
...slots,
default: (_ref2) => {
let {
id,
messagesId,
isDisabled,
isReadonly,
isValid
} = _ref2;
return createVNode(VCheckboxBtn, mergeProps(checkboxProps, {
"id": id.value,
"aria-describedby": messagesId.value,
"disabled": isDisabled.value,
"readonly": isReadonly.value
}, controlAttrs, {
"error": isValid.value === false,
"modelValue": model.value,
"onUpdate:modelValue": ($event) => model.value = $event,
"onFocus": focus,
"onBlur": blur
}), slots);
}
});
});
return forwardRefs({}, inputRef);
}
});
export {
VCheckboxBtn,
VCheckbox
};
//# sourceMappingURL=chunk-6HLLVLOR.js.map
File diff suppressed because one or more lines are too long
@@ -7,18 +7,18 @@ import {
VCounter, VCounter,
VField, VField,
makeVFieldProps makeVFieldProps
} from "./chunk-J7Q2NMV7.js"; } from "./chunk-ITO3JWXJ.js";
import { import {
VInput, VInput,
makeVInputProps, makeVInputProps,
useFocus useFocus
} from "./chunk-4VABFUPG.js"; } from "./chunk-IK67YVHF.js";
import {
forwardRefs
} from "./chunk-GUNW7S2L.js";
import { import {
intersect_default intersect_default
} from "./chunk-5ONGSJQ2.js"; } from "./chunk-5ONGSJQ2.js";
import {
forwardRefs
} from "./chunk-GUNW7S2L.js";
import { import {
useProxiedModel useProxiedModel
} from "./chunk-KA77II4V.js"; } from "./chunk-KA77II4V.js";
@@ -44,7 +44,7 @@ import {
} from "./chunk-2MKFL3BX.js"; } from "./chunk-2MKFL3BX.js";
// node_modules/vuetify/lib/components/VTextField/VTextField.js // node_modules/vuetify/lib/components/VTextField/VTextField.js
import "/home/david/Documents/vscode/personal_development/ServerManagerWebApp/frontend/node_modules/vuetify/lib/components/VTextField/VTextField.css"; import "/home/david/gitea_repo/personal_development/ServerManagerWebApp/frontend/node_modules/vuetify/lib/components/VTextField/VTextField.css";
var activeTypes = ["color", "file", "time", "date", "datetime-local", "week", "month"]; var activeTypes = ["color", "file", "time", "date", "datetime-local", "week", "month"];
var makeVTextFieldProps = propsFactory({ var makeVTextFieldProps = propsFactory({
autofocus: Boolean, autofocus: Boolean,
@@ -267,4 +267,4 @@ export {
makeVTextFieldProps, makeVTextFieldProps,
VTextField VTextField
}; };
//# sourceMappingURL=chunk-VSEC5QGQ.js.map //# sourceMappingURL=chunk-6MO6MKTY.js.map
@@ -12,6 +12,10 @@ import {
import { import {
makeTagProps makeTagProps
} from "./chunk-2HZIACSB.js"; } from "./chunk-2HZIACSB.js";
import {
useBackgroundColor,
useTextColor
} from "./chunk-BHV5Z34M.js";
import { import {
useResizeObserver useResizeObserver
} from "./chunk-XDSAO7OT.js"; } from "./chunk-XDSAO7OT.js";
@@ -22,10 +26,6 @@ import {
useProxiedModel, useProxiedModel,
useToggleScope useToggleScope
} from "./chunk-KA77II4V.js"; } from "./chunk-KA77II4V.js";
import {
useBackgroundColor,
useTextColor
} from "./chunk-BHV5Z34M.js";
import { import {
makeThemeProps, makeThemeProps,
provideTheme provideTheme
@@ -53,7 +53,7 @@ import {
} from "./chunk-2MKFL3BX.js"; } from "./chunk-2MKFL3BX.js";
// node_modules/vuetify/lib/components/VProgressLinear/VProgressLinear.js // node_modules/vuetify/lib/components/VProgressLinear/VProgressLinear.js
import "/home/david/Documents/vscode/personal_development/ServerManagerWebApp/frontend/node_modules/vuetify/lib/components/VProgressLinear/VProgressLinear.css"; import "/home/david/gitea_repo/personal_development/ServerManagerWebApp/frontend/node_modules/vuetify/lib/components/VProgressLinear/VProgressLinear.css";
// node_modules/vuetify/lib/components/VProgressLinear/chunks.js // node_modules/vuetify/lib/components/VProgressLinear/chunks.js
var makeChunksProps = propsFactory({ var makeChunksProps = propsFactory({
@@ -318,4 +318,4 @@ var VProgressLinear = genericComponent()({
export { export {
VProgressLinear VProgressLinear
}; };
//# sourceMappingURL=chunk-FVDWV4NO.js.map //# sourceMappingURL=chunk-6RFSM2OD.js.map
File diff suppressed because one or more lines are too long
@@ -21,13 +21,13 @@ import {
import { import {
makeTagProps makeTagProps
} from "./chunk-2HZIACSB.js"; } from "./chunk-2HZIACSB.js";
import {
useBackgroundColor
} from "./chunk-BHV5Z34M.js";
import { import {
makeDimensionProps, makeDimensionProps,
useDimension useDimension
} from "./chunk-6EZKHCRW.js"; } from "./chunk-6EZKHCRW.js";
import {
useBackgroundColor
} from "./chunk-BHV5Z34M.js";
import { import {
makeThemeProps, makeThemeProps,
provideTheme provideTheme
@@ -45,7 +45,7 @@ import {
} from "./chunk-2MKFL3BX.js"; } from "./chunk-2MKFL3BX.js";
// node_modules/vuetify/lib/components/VSheet/VSheet.js // node_modules/vuetify/lib/components/VSheet/VSheet.js
import "/home/david/Documents/vscode/personal_development/ServerManagerWebApp/frontend/node_modules/vuetify/lib/components/VSheet/VSheet.css"; import "/home/david/gitea_repo/personal_development/ServerManagerWebApp/frontend/node_modules/vuetify/lib/components/VSheet/VSheet.css";
var makeVSheetProps = propsFactory({ var makeVSheetProps = propsFactory({
color: String, color: String,
...makeBorderProps(), ...makeBorderProps(),
@@ -102,4 +102,4 @@ export {
makeVSheetProps, makeVSheetProps,
VSheet VSheet
}; };
//# sourceMappingURL=chunk-RNFCLGZV.js.map //# sourceMappingURL=chunk-7M5DDQ46.js.map
File diff suppressed because one or more lines are too long
@@ -1,3 +1,17 @@
import {
VOverlay,
makeVOverlayProps
} from "./chunk-CL6G3ICF.js";
import {
useScopeId
} from "./chunk-BVUPSGRF.js";
import {
forwardRefs
} from "./chunk-GUNW7S2L.js";
import {
VuetifyLayoutKey,
useLayout
} from "./chunk-KQXZAEJP.js";
import { import {
makePositionProps, makePositionProps,
usePosition usePosition
@@ -9,31 +23,17 @@ import {
} from "./chunk-ADXDXPPR.js"; } from "./chunk-ADXDXPPR.js";
import { import {
VProgressLinear VProgressLinear
} from "./chunk-FVDWV4NO.js"; } from "./chunk-6RFSM2OD.js";
import { import {
makeLocationProps makeLocationProps
} from "./chunk-KTTVT6V2.js"; } from "./chunk-KTTVT6V2.js";
import {
VDefaultsProvider
} from "./chunk-VH7QIDM4.js";
import { import {
makeRoundedProps, makeRoundedProps,
useRounded useRounded
} from "./chunk-AQK3CYVW.js"; } from "./chunk-AQK3CYVW.js";
import { import {
VOverlay, VDefaultsProvider
makeVOverlayProps } from "./chunk-PAT7NP2Z.js";
} from "./chunk-TS6Y6OVT.js";
import {
useScopeId
} from "./chunk-BVUPSGRF.js";
import {
forwardRefs
} from "./chunk-GUNW7S2L.js";
import {
VuetifyLayoutKey,
useLayout
} from "./chunk-KQXZAEJP.js";
import { import {
useProxiedModel, useProxiedModel,
useToggleScope useToggleScope
@@ -65,7 +65,7 @@ import {
} from "./chunk-2MKFL3BX.js"; } from "./chunk-2MKFL3BX.js";
// node_modules/vuetify/lib/components/VSnackbar/VSnackbar.js // node_modules/vuetify/lib/components/VSnackbar/VSnackbar.js
import "/home/david/Documents/vscode/personal_development/ServerManagerWebApp/frontend/node_modules/vuetify/lib/components/VSnackbar/VSnackbar.css"; import "/home/david/gitea_repo/personal_development/ServerManagerWebApp/frontend/node_modules/vuetify/lib/components/VSnackbar/VSnackbar.css";
function useCountdown(milliseconds) { function useCountdown(milliseconds) {
const time = shallowRef(milliseconds()); const time = shallowRef(milliseconds());
let timer = -1; let timer = -1;
@@ -275,4 +275,4 @@ export {
makeVSnackbarProps, makeVSnackbarProps,
VSnackbar VSnackbar
}; };
//# sourceMappingURL=chunk-YDVSSFCH.js.map //# sourceMappingURL=chunk-CCNWGDBL.js.map
@@ -9,21 +9,24 @@ import {
useFocusTrap, useFocusTrap,
useScopeId useScopeId
} from "./chunk-BVUPSGRF.js"; } from "./chunk-BVUPSGRF.js";
import {
useBackButton,
useRouter
} from "./chunk-573D4TYC.js";
import { import {
MaybeTransition, MaybeTransition,
makeTransitionProps makeTransitionProps
} from "./chunk-CAGZXMOS.js"; } from "./chunk-CAGZXMOS.js";
import {
useDisplay
} from "./chunk-N4LJHDYD.js";
import {
useBackButton,
useRouter
} from "./chunk-573D4TYC.js";
import {
useBackgroundColor
} from "./chunk-BHV5Z34M.js";
import { import {
makeDimensionProps, makeDimensionProps,
useDimension useDimension
} from "./chunk-6EZKHCRW.js"; } from "./chunk-6EZKHCRW.js";
import {
useDisplay
} from "./chunk-N4LJHDYD.js";
import { import {
useRtl useRtl
} from "./chunk-ENTLCOCZ.js"; } from "./chunk-ENTLCOCZ.js";
@@ -31,9 +34,6 @@ import {
useProxiedModel, useProxiedModel,
useToggleScope useToggleScope
} from "./chunk-KA77II4V.js"; } from "./chunk-KA77II4V.js";
import {
useBackgroundColor
} from "./chunk-BHV5Z34M.js";
import { import {
makeThemeProps, makeThemeProps,
provideTheme provideTheme
@@ -103,79 +103,8 @@ import {
withDirectives withDirectives
} from "./chunk-2MKFL3BX.js"; } from "./chunk-2MKFL3BX.js";
// node_modules/vuetify/lib/directives/click-outside/index.js
function defaultConditional() {
return true;
}
function checkEvent(e, el, binding) {
if (!e || checkIsActive(e, binding) === false) return false;
const root = attachedRoot(el);
if (typeof ShadowRoot !== "undefined" && root instanceof ShadowRoot && root.host === e.target) return false;
const elements = (typeof binding.value === "object" && binding.value.include || (() => []))();
elements.push(el);
return !elements.some((el2) => el2?.contains(e.target));
}
function checkIsActive(e, binding) {
const isActive = typeof binding.value === "object" && binding.value.closeConditional || defaultConditional;
return isActive(e);
}
function directive(e, el, binding) {
const handler = typeof binding.value === "function" ? binding.value : binding.value.handler;
e.shadowTarget = e.target;
el._clickOutside.lastMousedownWasOutside && checkEvent(e, el, binding) && setTimeout(() => {
checkIsActive(e, binding) && handler && handler(e);
}, 0);
}
function handleShadow(el, callback) {
const root = attachedRoot(el);
callback(document);
if (typeof ShadowRoot !== "undefined" && root instanceof ShadowRoot) {
callback(root);
}
}
var ClickOutside = {
// [data-app] may not be found
// if using bind, inserted makes
// sure that the root element is
// available, iOS does not support
// clicks on body
mounted(el, binding) {
const onClick = (e) => directive(e, el, binding);
const onMousedown = (e) => {
el._clickOutside.lastMousedownWasOutside = checkEvent(e, el, binding);
};
handleShadow(el, (app) => {
app.addEventListener("click", onClick, true);
app.addEventListener("mousedown", onMousedown, true);
});
if (!el._clickOutside) {
el._clickOutside = {
lastMousedownWasOutside: false
};
}
el._clickOutside[binding.instance.$.uid] = {
onClick,
onMousedown
};
},
beforeUnmount(el, binding) {
if (!el._clickOutside) return;
handleShadow(el, (app) => {
if (!app || !el._clickOutside?.[binding.instance.$.uid]) return;
const {
onClick,
onMousedown
} = el._clickOutside[binding.instance.$.uid];
app.removeEventListener("click", onClick, true);
app.removeEventListener("mousedown", onMousedown, true);
});
delete el._clickOutside[binding.instance.$.uid];
}
};
var click_outside_default = ClickOutside;
// node_modules/vuetify/lib/components/VOverlay/VOverlay.js // node_modules/vuetify/lib/components/VOverlay/VOverlay.js
import "/home/david/Documents/vscode/personal_development/ServerManagerWebApp/frontend/node_modules/vuetify/lib/components/VOverlay/VOverlay.css"; import "/home/david/gitea_repo/personal_development/ServerManagerWebApp/frontend/node_modules/vuetify/lib/components/VOverlay/VOverlay.css";
// node_modules/vuetify/lib/components/VOverlay/util/point.js // node_modules/vuetify/lib/components/VOverlay/util/point.js
function elementToViewport(point, offset) { function elementToViewport(point, offset) {
@@ -1111,6 +1040,77 @@ function useTeleport(target) {
}; };
} }
// node_modules/vuetify/lib/directives/click-outside/index.js
function defaultConditional() {
return true;
}
function checkEvent(e, el, binding) {
if (!e || checkIsActive(e, binding) === false) return false;
const root = attachedRoot(el);
if (typeof ShadowRoot !== "undefined" && root instanceof ShadowRoot && root.host === e.target) return false;
const elements = (typeof binding.value === "object" && binding.value.include || (() => []))();
elements.push(el);
return !elements.some((el2) => el2?.contains(e.target));
}
function checkIsActive(e, binding) {
const isActive = typeof binding.value === "object" && binding.value.closeConditional || defaultConditional;
return isActive(e);
}
function directive(e, el, binding) {
const handler = typeof binding.value === "function" ? binding.value : binding.value.handler;
e.shadowTarget = e.target;
el._clickOutside.lastMousedownWasOutside && checkEvent(e, el, binding) && setTimeout(() => {
checkIsActive(e, binding) && handler && handler(e);
}, 0);
}
function handleShadow(el, callback) {
const root = attachedRoot(el);
callback(document);
if (typeof ShadowRoot !== "undefined" && root instanceof ShadowRoot) {
callback(root);
}
}
var ClickOutside = {
// [data-app] may not be found
// if using bind, inserted makes
// sure that the root element is
// available, iOS does not support
// clicks on body
mounted(el, binding) {
const onClick = (e) => directive(e, el, binding);
const onMousedown = (e) => {
el._clickOutside.lastMousedownWasOutside = checkEvent(e, el, binding);
};
handleShadow(el, (app) => {
app.addEventListener("click", onClick, true);
app.addEventListener("mousedown", onMousedown, true);
});
if (!el._clickOutside) {
el._clickOutside = {
lastMousedownWasOutside: false
};
}
el._clickOutside[binding.instance.$.uid] = {
onClick,
onMousedown
};
},
beforeUnmount(el, binding) {
if (!el._clickOutside) return;
handleShadow(el, (app) => {
if (!app || !el._clickOutside?.[binding.instance.$.uid]) return;
const {
onClick,
onMousedown
} = el._clickOutside[binding.instance.$.uid];
app.removeEventListener("click", onClick, true);
app.removeEventListener("mousedown", onMousedown, true);
});
delete el._clickOutside[binding.instance.$.uid];
}
};
var click_outside_default = ClickOutside;
// node_modules/vuetify/lib/components/VOverlay/VOverlay.js // node_modules/vuetify/lib/components/VOverlay/VOverlay.js
function Scrim(props) { function Scrim(props) {
const { const {
@@ -1412,10 +1412,10 @@ var VOverlay = genericComponent()({
}); });
export { export {
ClickOutside,
VMenuSymbol, VMenuSymbol,
useHydration, useHydration,
ClickOutside,
makeVOverlayProps, makeVOverlayProps,
VOverlay VOverlay
}; };
//# sourceMappingURL=chunk-TS6Y6OVT.js.map //# sourceMappingURL=chunk-CL6G3ICF.js.map
File diff suppressed because one or more lines are too long
@@ -1,3 +1,6 @@
import {
breakpoints
} from "./chunk-N4LJHDYD.js";
import { import {
makeTagProps makeTagProps
} from "./chunk-2HZIACSB.js"; } from "./chunk-2HZIACSB.js";
@@ -5,9 +8,6 @@ import {
makeDimensionProps, makeDimensionProps,
useDimension useDimension
} from "./chunk-6EZKHCRW.js"; } from "./chunk-6EZKHCRW.js";
import {
breakpoints
} from "./chunk-N4LJHDYD.js";
import { import {
useRtl useRtl
} from "./chunk-ENTLCOCZ.js"; } from "./chunk-ENTLCOCZ.js";
@@ -28,7 +28,7 @@ import {
} from "./chunk-2MKFL3BX.js"; } from "./chunk-2MKFL3BX.js";
// node_modules/vuetify/lib/components/VGrid/VContainer.js // node_modules/vuetify/lib/components/VGrid/VContainer.js
import "/home/david/Documents/vscode/personal_development/ServerManagerWebApp/frontend/node_modules/vuetify/lib/components/VGrid/VGrid.css"; import "/home/david/gitea_repo/personal_development/ServerManagerWebApp/frontend/node_modules/vuetify/lib/components/VGrid/VGrid.css";
var makeVContainerProps = propsFactory({ var makeVContainerProps = propsFactory({
fluid: { fluid: {
type: Boolean, type: Boolean,
@@ -62,7 +62,7 @@ var VContainer = genericComponent()({
}); });
// node_modules/vuetify/lib/components/VGrid/VCol.js // node_modules/vuetify/lib/components/VGrid/VCol.js
import "/home/david/Documents/vscode/personal_development/ServerManagerWebApp/frontend/node_modules/vuetify/lib/components/VGrid/VGrid.css"; import "/home/david/gitea_repo/personal_development/ServerManagerWebApp/frontend/node_modules/vuetify/lib/components/VGrid/VGrid.css";
var breakpointProps = (() => { var breakpointProps = (() => {
return breakpoints.reduce((props, val) => { return breakpoints.reduce((props, val) => {
props[val] = { props[val] = {
@@ -176,7 +176,7 @@ var VCol = genericComponent()({
}); });
// node_modules/vuetify/lib/components/VGrid/VRow.js // node_modules/vuetify/lib/components/VGrid/VRow.js
import "/home/david/Documents/vscode/personal_development/ServerManagerWebApp/frontend/node_modules/vuetify/lib/components/VGrid/VGrid.css"; import "/home/david/gitea_repo/personal_development/ServerManagerWebApp/frontend/node_modules/vuetify/lib/components/VGrid/VGrid.css";
var ALIGNMENT = ["start", "end", "center"]; var ALIGNMENT = ["start", "end", "center"];
var SPACE = ["space-between", "space-around", "space-evenly"]; var SPACE = ["space-between", "space-around", "space-evenly"];
function makeRowProps(prefix, def) { function makeRowProps(prefix, def) {
@@ -287,7 +287,7 @@ var VRow = genericComponent()({
}); });
// node_modules/vuetify/lib/components/VGrid/VSpacer.js // node_modules/vuetify/lib/components/VGrid/VSpacer.js
import "/home/david/Documents/vscode/personal_development/ServerManagerWebApp/frontend/node_modules/vuetify/lib/components/VGrid/VGrid.css"; import "/home/david/gitea_repo/personal_development/ServerManagerWebApp/frontend/node_modules/vuetify/lib/components/VGrid/VGrid.css";
var VSpacer = createSimpleFunctional("v-spacer", "div", "VSpacer"); var VSpacer = createSimpleFunctional("v-spacer", "div", "VSpacer");
export { export {
@@ -296,4 +296,4 @@ export {
VRow, VRow,
VSpacer VSpacer
}; };
//# sourceMappingURL=chunk-UIXKNP5E.js.map //# sourceMappingURL=chunk-CMTCIV56.js.map
@@ -3,7 +3,7 @@ import {
} from "./chunk-LZ5T2ERG.js"; } from "./chunk-LZ5T2ERG.js";
// node_modules/vuetify/lib/directives/ripple/index.js // node_modules/vuetify/lib/directives/ripple/index.js
import "/home/david/Documents/vscode/personal_development/ServerManagerWebApp/frontend/node_modules/vuetify/lib/directives/ripple/VRipple.css"; import "/home/david/gitea_repo/personal_development/ServerManagerWebApp/frontend/node_modules/vuetify/lib/directives/ripple/VRipple.css";
var stopSymbol = /* @__PURE__ */ Symbol("rippleStop"); var stopSymbol = /* @__PURE__ */ Symbol("rippleStop");
var DELAY_RIPPLE = 80; var DELAY_RIPPLE = 80;
function transform(el, value) { function transform(el, value) {
@@ -287,4 +287,4 @@ export {
Ripple, Ripple,
ripple_default ripple_default
}; };
//# sourceMappingURL=chunk-5BPWKCNA.js.map //# sourceMappingURL=chunk-DS4J67XG.js.map
@@ -1,16 +1,16 @@
import {
VChip
} from "./chunk-SHBPUR7N.js";
import { import {
VCounter, VCounter,
VField, VField,
makeVFieldProps makeVFieldProps
} from "./chunk-J7Q2NMV7.js"; } from "./chunk-ITO3JWXJ.js";
import {
VChip
} from "./chunk-PLNIG6WP.js";
import { import {
VInput, VInput,
makeVInputProps, makeVInputProps,
useFocus useFocus
} from "./chunk-4VABFUPG.js"; } from "./chunk-IK67YVHF.js";
import { import {
forwardRefs forwardRefs
} from "./chunk-GUNW7S2L.js"; } from "./chunk-GUNW7S2L.js";
@@ -44,7 +44,7 @@ import {
} from "./chunk-2MKFL3BX.js"; } from "./chunk-2MKFL3BX.js";
// node_modules/vuetify/lib/components/VFileInput/VFileInput.js // node_modules/vuetify/lib/components/VFileInput/VFileInput.js
import "/home/david/Documents/vscode/personal_development/ServerManagerWebApp/frontend/node_modules/vuetify/lib/components/VFileInput/VFileInput.css"; import "/home/david/gitea_repo/personal_development/ServerManagerWebApp/frontend/node_modules/vuetify/lib/components/VFileInput/VFileInput.css";
// node_modules/vuetify/lib/composables/fileDrop.js // node_modules/vuetify/lib/composables/fileDrop.js
function useFileDrop() { function useFileDrop() {
@@ -423,4 +423,4 @@ var VFileInput = genericComponent()({
export { export {
VFileInput VFileInput
}; };
//# sourceMappingURL=chunk-PEUMXYT2.js.map //# sourceMappingURL=chunk-EKMPZMZF.js.map
File diff suppressed because one or more lines are too long
+116
View File
@@ -0,0 +1,116 @@
import {
VOverlay,
makeVOverlayProps
} from "./chunk-CL6G3ICF.js";
import {
useScopeId
} from "./chunk-BVUPSGRF.js";
import {
forwardRefs
} from "./chunk-GUNW7S2L.js";
import {
useProxiedModel
} from "./chunk-KA77II4V.js";
import {
genericComponent,
omit,
propsFactory,
useRender
} from "./chunk-LZ5T2ERG.js";
import {
computed,
createVNode,
mergeProps,
ref,
toRef,
useId
} from "./chunk-2MKFL3BX.js";
// node_modules/vuetify/lib/components/VTooltip/VTooltip.js
import "/home/david/gitea_repo/personal_development/ServerManagerWebApp/frontend/node_modules/vuetify/lib/components/VTooltip/VTooltip.css";
var makeVTooltipProps = propsFactory({
id: String,
interactive: Boolean,
text: String,
...omit(makeVOverlayProps({
closeOnBack: false,
location: "end",
locationStrategy: "connected",
eager: true,
minWidth: 0,
offset: 10,
openOnClick: false,
openOnHover: true,
origin: "auto",
scrim: false,
scrollStrategy: "reposition",
transition: null
}), ["absolute", "retainFocus", "captureFocus", "disableInitialFocus"])
}, "VTooltip");
var VTooltip = genericComponent()({
name: "VTooltip",
props: makeVTooltipProps(),
emits: {
"update:modelValue": (value) => true
},
setup(props, _ref) {
let {
slots
} = _ref;
const isActive = useProxiedModel(props, "modelValue");
const {
scopeId
} = useScopeId();
const uid = useId();
const id = toRef(() => props.id || `v-tooltip-${uid}`);
const overlay = ref();
const location = computed(() => {
return props.location.split(" ").length > 1 ? props.location : props.location + " center";
});
const origin = computed(() => {
return props.origin === "auto" || props.origin === "overlap" || props.origin.split(" ").length > 1 || props.location.split(" ").length > 1 ? props.origin : props.origin + " center";
});
const transition = toRef(() => {
if (props.transition != null) return props.transition;
return isActive.value ? "scale-transition" : "fade-transition";
});
const activatorProps = computed(() => mergeProps({
"aria-describedby": id.value
}, props.activatorProps));
useRender(() => {
const overlayProps = VOverlay.filterProps(props);
return createVNode(VOverlay, mergeProps({
"ref": overlay,
"class": ["v-tooltip", {
"v-tooltip--interactive": props.interactive
}, props.class],
"style": props.style,
"id": id.value
}, overlayProps, {
"modelValue": isActive.value,
"onUpdate:modelValue": ($event) => isActive.value = $event,
"transition": transition.value,
"absolute": true,
"location": location.value,
"origin": origin.value,
"role": "tooltip",
"activatorProps": activatorProps.value,
"_disableGlobalStack": true
}, scopeId), {
activator: slots.activator,
default: function() {
for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key];
}
return slots.default?.(...args) ?? props.text;
}
});
});
return forwardRefs({}, overlay);
}
});
export {
VTooltip
};
//# sourceMappingURL=chunk-EXUNAYJA.js.map
@@ -0,0 +1,7 @@
{
"version": 3,
"sources": ["../../vuetify/src/components/VTooltip/VTooltip.tsx"],
"sourcesContent": ["// Styles\nimport './VTooltip.sass'\n\n// Components\nimport { VOverlay } from '@/components/VOverlay'\nimport { makeVOverlayProps } from '@/components/VOverlay/VOverlay'\n\n// Composables\nimport { forwardRefs } from '@/composables/forwardRefs'\nimport { useProxiedModel } from '@/composables/proxiedModel'\nimport { useScopeId } from '@/composables/scopeId'\n\n// Utilities\nimport { computed, mergeProps, ref, toRef, useId } from 'vue'\nimport { genericComponent, omit, propsFactory, useRender } from '@/util'\n\n// Types\nimport type { StrategyProps } from '@/components/VOverlay/locationStrategies'\nimport type { OverlaySlots } from '@/components/VOverlay/VOverlay'\n\nexport const makeVTooltipProps = propsFactory({\n id: String,\n interactive: Boolean,\n text: String,\n\n ...omit(makeVOverlayProps({\n closeOnBack: false,\n location: 'end' as const,\n locationStrategy: 'connected' as const,\n eager: true,\n minWidth: 0,\n offset: 10,\n openOnClick: false,\n openOnHover: true,\n origin: 'auto' as const,\n scrim: false,\n scrollStrategy: 'reposition' as const,\n transition: null,\n }), [\n 'absolute',\n 'retainFocus',\n 'captureFocus',\n 'disableInitialFocus',\n ]),\n}, 'VTooltip')\n\nexport const VTooltip = genericComponent<OverlaySlots>()({\n name: 'VTooltip',\n\n props: makeVTooltipProps(),\n\n emits: {\n 'update:modelValue': (value: boolean) => true,\n },\n\n setup (props, { slots }) {\n const isActive = useProxiedModel(props, 'modelValue')\n const { scopeId } = useScopeId()\n\n const uid = useId()\n const id = toRef(() => props.id || `v-tooltip-${uid}`)\n\n const overlay = ref<VOverlay>()\n\n const location = computed(() => {\n return props.location.split(' ').length > 1\n ? props.location\n : props.location + ' center' as StrategyProps['location']\n })\n\n const origin = computed(() => {\n return (\n props.origin === 'auto' ||\n props.origin === 'overlap' ||\n props.origin.split(' ').length > 1 ||\n props.location.split(' ').length > 1\n ) ? props.origin\n : props.origin + ' center' as StrategyProps['origin']\n })\n\n const transition = toRef(() => {\n if (props.transition != null) return props.transition\n return isActive.value ? 'scale-transition' : 'fade-transition'\n })\n\n const activatorProps = computed(() =>\n mergeProps({\n 'aria-describedby': id.value,\n }, props.activatorProps)\n )\n\n useRender(() => {\n const overlayProps = VOverlay.filterProps(props)\n\n return (\n <VOverlay\n ref={ overlay }\n class={[\n 'v-tooltip',\n { 'v-tooltip--interactive': props.interactive },\n props.class,\n ]}\n style={ props.style }\n id={ id.value }\n { ...overlayProps }\n v-model={ isActive.value }\n transition={ transition.value }\n absolute\n location={ location.value }\n origin={ origin.value }\n role=\"tooltip\"\n activatorProps={ activatorProps.value }\n _disableGlobalStack\n { ...scopeId }\n >\n {{\n activator: slots.activator,\n default: (...args) => slots.default?.(...args) ?? props.text,\n }}\n </VOverlay>\n )\n })\n\n return forwardRefs({}, overlay)\n },\n})\n\nexport type VTooltip = InstanceType<typeof VTooltip>\n"],
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;AACA,OAAA;AAmBO,IAAMA,oBAAoBC,aAAa;EAC5CC,IAAIC;EACJC,aAAaC;EACbC,MAAMH;EAEN,GAAGI,KAAKC,kBAAkB;IACxBC,aAAa;IACbC,UAAU;IACVC,kBAAkB;IAClBC,OAAO;IACPC,UAAU;IACVC,QAAQ;IACRC,aAAa;IACbC,aAAa;IACbC,QAAQ;IACRC,OAAO;IACPC,gBAAgB;IAChBC,YAAY;EACd,CAAC,GAAG,CACF,YACA,eACA,gBACA,qBAAqB,CACtB;AACH,GAAG,UAAU;AAEN,IAAMC,WAAWC,iBAA+B,EAAE;EACvDC,MAAM;EAENC,OAAOxB,kBAAkB;EAEzByB,OAAO;IACL,qBAAsBC,WAAmB;EAC3C;EAEAC,MAAOH,OAAKI,MAAa;AAAA,QAAX;MAAEC;IAAM,IAACD;AACrB,UAAME,WAAWC,gBAAgBP,OAAO,YAAY;AACpD,UAAM;MAAEQ;IAAQ,IAAIC,WAAW;AAE/B,UAAMC,MAAMC,MAAM;AAClB,UAAMjC,KAAKkC,MAAM,MAAMZ,MAAMtB,MAAM,aAAagC,GAAG,EAAE;AAErD,UAAMG,UAAUC,IAAc;AAE9B,UAAM5B,WAAW6B,SAAS,MAAM;AAC9B,aAAOf,MAAMd,SAAS8B,MAAM,GAAG,EAAEC,SAAS,IACtCjB,MAAMd,WACNc,MAAMd,WAAW;IACvB,CAAC;AAED,UAAMO,SAASsB,SAAS,MAAM;AAC5B,aACEf,MAAMP,WAAW,UACjBO,MAAMP,WAAW,aACjBO,MAAMP,OAAOuB,MAAM,GAAG,EAAEC,SAAS,KACjCjB,MAAMd,SAAS8B,MAAM,GAAG,EAAEC,SAAS,IACjCjB,MAAMP,SACNO,MAAMP,SAAS;IACrB,CAAC;AAED,UAAMG,aAAagB,MAAM,MAAM;AAC7B,UAAIZ,MAAMJ,cAAc,KAAM,QAAOI,MAAMJ;AAC3C,aAAOU,SAASJ,QAAQ,qBAAqB;IAC/C,CAAC;AAED,UAAMgB,iBAAiBH,SAAS,MAC9BI,WAAW;MACT,oBAAoBzC,GAAGwB;IACzB,GAAGF,MAAMkB,cAAc,CACzB;AAEAE,cAAU,MAAM;AACd,YAAMC,eAAeC,SAASC,YAAYvB,KAAK;AAE/C,aAAAwB,YAAAF,UAAAG,WAAA;QAAA,OAEUZ;QAAO,SACN,CACL,aACA;UAAE,0BAA0Bb,MAAMpB;QAAY,GAC9CoB,MAAM0B,KAAK;QACZ,SACO1B,MAAM2B;QAAK,MACdjD,GAAGwB;MAAK,GACRmB,cAAY;QAAA,cACPf,SAASJ;QAAK,uBAAA0B,YAAdtB,SAASJ,QAAK0B;QAAA,cACXhC,WAAWM;QAAK,YAAA;QAAA,YAElBhB,SAASgB;QAAK,UAChBT,OAAOS;QAAK,QAAA;QAAA,kBAEJgB,eAAehB;QAAK,uBAAA;MAAA,GAEhCM,OAAO,GAAA;QAGVqB,WAAWxB,MAAMwB;QACjBC,SAAS,WAAA;AAAA,mBAAAC,OAAAC,UAAAf,QAAIgB,OAAI,IAAAC,MAAAH,IAAA,GAAAI,OAAA,GAAAA,OAAAJ,MAAAI,QAAA;AAAJF,iBAAIE,IAAA,IAAAH,UAAAG,IAAA;UAAA;AAAA,iBAAK9B,MAAMyB,UAAU,GAAGG,IAAI,KAAKjC,MAAMlB;QAAI;MAAA,CAAA;IAIpE,CAAC;AAED,WAAOsD,YAAY,CAAC,GAAGvB,OAAO;EAChC;AACF,CAAC;",
"names": ["makeVTooltipProps", "propsFactory", "id", "String", "interactive", "Boolean", "text", "omit", "makeVOverlayProps", "closeOnBack", "location", "locationStrategy", "eager", "minWidth", "offset", "openOnClick", "openOnHover", "origin", "scrim", "scrollStrategy", "transition", "VTooltip", "genericComponent", "name", "props", "emits", "value", "setup", "_ref", "slots", "isActive", "useProxiedModel", "scopeId", "useScopeId", "uid", "useId", "toRef", "overlay", "ref", "computed", "split", "length", "activatorProps", "mergeProps", "useRender", "overlayProps", "VOverlay", "filterProps", "_createVNode", "_mergeProps", "class", "style", "$event", "activator", "default", "_len", "arguments", "args", "Array", "_key", "forwardRefs"]
}
@@ -1,18 +1,29 @@
import {
makeFilterProps,
useFilter
} from "./chunk-RB6DAYHX.js";
import { import {
VSelect, VSelect,
VVirtualScrollItem, VVirtualScrollItem,
makeVirtualProps, makeVirtualProps,
useVirtual useVirtual
} from "./chunk-ERI3RTQS.js"; } from "./chunk-IZDIYS2S.js";
import { import {
VCheckboxBtn VCheckboxBtn
} from "./chunk-BYBKODOB.js"; } from "./chunk-6HLLVLOR.js";
import { import {
VChip VChip
} from "./chunk-SHBPUR7N.js"; } from "./chunk-PLNIG6WP.js";
import {
VDivider
} from "./chunk-X3WY3J75.js";
import {
makeDisplayProps,
useDisplay
} from "./chunk-N4LJHDYD.js";
import { import {
VBtn VBtn
} from "./chunk-QC7VWQER.js"; } from "./chunk-UQALH6SL.js";
import { import {
makeVariantProps makeVariantProps
} from "./chunk-ADXDXPPR.js"; } from "./chunk-ADXDXPPR.js";
@@ -26,14 +37,14 @@ import {
LoaderSlot, LoaderSlot,
makeLoaderProps, makeLoaderProps,
useLoader useLoader
} from "./chunk-U2DSRSME.js"; } from "./chunk-UXZ55AO7.js";
import { import {
makeDensityProps, makeDensityProps,
useDensity useDensity
} from "./chunk-TM3MIDYC.js"; } from "./chunk-TM3MIDYC.js";
import { import {
VIcon VIcon
} from "./chunk-CMZ4UYUX.js"; } from "./chunk-4XZHKYGH.js";
import { import {
makeSizeProps makeSizeProps
} from "./chunk-PIT7IT55.js"; } from "./chunk-PIT7IT55.js";
@@ -43,13 +54,12 @@ import {
import { import {
makeTagProps makeTagProps
} from "./chunk-2HZIACSB.js"; } from "./chunk-2HZIACSB.js";
import {
makeDisplayProps,
useDisplay
} from "./chunk-N4LJHDYD.js";
import { import {
IconValue IconValue
} from "./chunk-OJDZBU5D.js"; } from "./chunk-OJDZBU5D.js";
import {
useBackgroundColor
} from "./chunk-BHV5Z34M.js";
import { import {
useResizeObserver useResizeObserver
} from "./chunk-XDSAO7OT.js"; } from "./chunk-XDSAO7OT.js";
@@ -60,12 +70,6 @@ import {
import { import {
useProxiedModel useProxiedModel
} from "./chunk-KA77II4V.js"; } from "./chunk-KA77II4V.js";
import {
VDivider
} from "./chunk-WXMECWZN.js";
import {
useBackgroundColor
} from "./chunk-BHV5Z34M.js";
import { import {
makeThemeProps, makeThemeProps,
provideTheme provideTheme
@@ -116,20 +120,19 @@ import {
toRef, toRef,
toRefs, toRefs,
toValue, toValue,
unref,
watch, watch,
watchEffect, watchEffect,
withModifiers withModifiers
} from "./chunk-2MKFL3BX.js"; } from "./chunk-2MKFL3BX.js";
// node_modules/vuetify/lib/components/VDataTable/VDataTable.js // node_modules/vuetify/lib/components/VDataTable/VDataTable.js
import "/home/david/Documents/vscode/personal_development/ServerManagerWebApp/frontend/node_modules/vuetify/lib/components/VDataTable/VDataTable.css"; import "/home/david/gitea_repo/personal_development/ServerManagerWebApp/frontend/node_modules/vuetify/lib/components/VDataTable/VDataTable.css";
// node_modules/vuetify/lib/components/VDataTable/VDataTableFooter.js // node_modules/vuetify/lib/components/VDataTable/VDataTableFooter.js
import "/home/david/Documents/vscode/personal_development/ServerManagerWebApp/frontend/node_modules/vuetify/lib/components/VDataTable/VDataTableFooter.css"; import "/home/david/gitea_repo/personal_development/ServerManagerWebApp/frontend/node_modules/vuetify/lib/components/VDataTable/VDataTableFooter.css";
// node_modules/vuetify/lib/components/VPagination/VPagination.js // node_modules/vuetify/lib/components/VPagination/VPagination.js
import "/home/david/Documents/vscode/personal_development/ServerManagerWebApp/frontend/node_modules/vuetify/lib/components/VPagination/VPagination.css"; import "/home/david/gitea_repo/personal_development/ServerManagerWebApp/frontend/node_modules/vuetify/lib/components/VPagination/VPagination.css";
// node_modules/vuetify/lib/composables/refs.js // node_modules/vuetify/lib/composables/refs.js
function useRefs() { function useRefs() {
@@ -2243,7 +2246,7 @@ var VDataTableRows = genericComponent()({
}); });
// node_modules/vuetify/lib/components/VTable/VTable.js // node_modules/vuetify/lib/components/VTable/VTable.js
import "/home/david/Documents/vscode/personal_development/ServerManagerWebApp/frontend/node_modules/vuetify/lib/components/VTable/VTable.css"; import "/home/david/gitea_repo/personal_development/ServerManagerWebApp/frontend/node_modules/vuetify/lib/components/VTable/VTable.css";
var makeVTableProps = propsFactory({ var makeVTableProps = propsFactory({
fixedHeader: Boolean, fixedHeader: Boolean,
fixedFooter: Boolean, fixedFooter: Boolean,
@@ -2373,160 +2376,6 @@ function useOptions(_ref) {
}); });
} }
// node_modules/vuetify/lib/composables/filter.js
var defaultFilter = (value, query, item) => {
if (value == null || query == null) return -1;
if (!query.length) return 0;
value = value.toString().toLocaleLowerCase();
query = query.toString().toLocaleLowerCase();
const result = [];
let idx = value.indexOf(query);
while (~idx) {
result.push([idx, idx + query.length]);
idx = value.indexOf(query, idx + query.length);
}
return result.length ? result : -1;
};
function normaliseMatch(match, query) {
if (match == null || typeof match === "boolean" || match === -1) return;
if (typeof match === "number") return [[match, match + query.length]];
if (Array.isArray(match[0])) return match;
return [match];
}
var makeFilterProps = propsFactory({
customFilter: Function,
customKeyFilter: Object,
filterKeys: [Array, String],
filterMode: {
type: String,
default: "intersection"
},
noFilter: Boolean
}, "filter");
function filterItems(items, query, options) {
const array = [];
const filter = options?.default ?? defaultFilter;
const keys = options?.filterKeys ? wrapInArray(options.filterKeys) : false;
const customFiltersLength = Object.keys(options?.customKeyFilter ?? {}).length;
if (!items?.length) return array;
let lookAheadItem = null;
loop: for (let i = 0; i < items.length; i++) {
const [item, transformed = item] = wrapInArray(items[i]);
const customMatches = {};
const defaultMatches = {};
let match = -1;
if ((query || customFiltersLength > 0) && !options?.noFilter) {
let hasOnlyCustomFilters = false;
if (typeof item === "object") {
if (item.type === "divider" || item.type === "subheader") {
if (lookAheadItem?.type === "divider" && item.type === "subheader") {
array.push(lookAheadItem);
}
lookAheadItem = {
index: i,
matches: {},
type: item.type
};
continue;
}
const filterKeys = keys || Object.keys(transformed);
hasOnlyCustomFilters = filterKeys.length === customFiltersLength;
for (const key of filterKeys) {
const value = getPropertyFromItem(transformed, key);
const keyFilter = options?.customKeyFilter?.[key];
match = keyFilter ? keyFilter(value, query, item) : filter(value, query, item);
if (match !== -1 && match !== false) {
if (keyFilter) customMatches[key] = normaliseMatch(match, query);
else defaultMatches[key] = normaliseMatch(match, query);
} else if (options?.filterMode === "every") {
continue loop;
}
}
} else {
match = filter(item, query, item);
if (match !== -1 && match !== false) {
defaultMatches.title = normaliseMatch(match, query);
}
}
const defaultMatchesLength = Object.keys(defaultMatches).length;
const customMatchesLength = Object.keys(customMatches).length;
if (!defaultMatchesLength && !customMatchesLength) continue;
if (options?.filterMode === "union" && customMatchesLength !== customFiltersLength && !defaultMatchesLength) continue;
if (options?.filterMode === "intersection" && (customMatchesLength !== customFiltersLength || !defaultMatchesLength && customFiltersLength > 0 && !hasOnlyCustomFilters)) continue;
}
if (lookAheadItem) {
array.push(lookAheadItem);
lookAheadItem = null;
}
array.push({
index: i,
matches: {
...defaultMatches,
...customMatches
}
});
}
return array;
}
function useFilter(props, items, query, options) {
const filteredItems = shallowRef([]);
const filteredMatches = shallowRef(/* @__PURE__ */ new Map());
const transformedItems = computed(() => options?.transform ? unref(items).map((item) => [item, options.transform(item)]) : unref(items));
watchEffect(() => {
const _query = typeof query === "function" ? query() : unref(query);
const strQuery = typeof _query !== "string" && typeof _query !== "number" ? "" : String(_query);
const results = filterItems(transformedItems.value, strQuery, {
customKeyFilter: {
...props.customKeyFilter,
...unref(options?.customKeyFilter)
},
default: props.customFilter,
filterKeys: props.filterKeys,
filterMode: props.filterMode,
noFilter: props.noFilter
});
const originalItems = unref(items);
const _filteredItems = [];
const _filteredMatches = /* @__PURE__ */ new Map();
results.forEach((_ref) => {
let {
index,
matches
} = _ref;
const item = originalItems[index];
_filteredItems.push(item);
_filteredMatches.set(item.value, matches);
});
filteredItems.value = _filteredItems;
filteredMatches.value = _filteredMatches;
});
function getMatches(item) {
return filteredMatches.value.get(item.value);
}
return {
filteredItems,
filteredMatches,
getMatches
};
}
function highlightResult(name, text, matches) {
if (matches == null || !matches.length) return text;
return matches.map((match, i) => {
const start = i === 0 ? 0 : matches[i - 1][1];
const result = [createBaseVNode("span", {
"class": normalizeClass(`${name}__unmask`)
}, [text.slice(start, match[0])]), createBaseVNode("span", {
"class": normalizeClass(`${name}__mask`)
}, [text.slice(match[0], match[1])])];
if (i === matches.length - 1) {
result.push(createBaseVNode("span", {
"class": normalizeClass(`${name}__unmask`)
}, [text.slice(match[1])]));
}
return createBaseVNode(Fragment, null, [result]);
});
}
// node_modules/vuetify/lib/components/VDataTable/VDataTable.js // node_modules/vuetify/lib/components/VDataTable/VDataTable.js
var makeDataTableProps = propsFactory({ var makeDataTableProps = propsFactory({
...makeVDataTableRowsProps(), ...makeVDataTableRowsProps(),
@@ -3157,9 +3006,6 @@ var VDataTableServer = genericComponent()({
}); });
export { export {
makeFilterProps,
useFilter,
highlightResult,
makeDataTableExpandProps, makeDataTableExpandProps,
provideExpanded, provideExpanded,
makeDataTableGroupProps, makeDataTableGroupProps,
@@ -3186,4 +3032,4 @@ export {
VDataTableVirtual, VDataTableVirtual,
VDataTableServer VDataTableServer
}; };
//# sourceMappingURL=chunk-4HCXUCLO.js.map //# sourceMappingURL=chunk-FCTFWI5H.js.map
@@ -0,0 +1,7 @@
{
"version": 3,
"sources": [],
"sourcesContent": [],
"mappings": "",
"names": []
}
@@ -1,6 +1,9 @@
import { import {
VImg VImg
} from "./chunk-6CRS5HI4.js"; } from "./chunk-TTIGZHWR.js";
import {
VExpandTransition
} from "./chunk-B4JFJDWM.js";
import { import {
makeBorderProps, makeBorderProps,
useBorder useBorder
@@ -9,25 +12,22 @@ import {
makeElevationProps, makeElevationProps,
useElevation useElevation
} from "./chunk-YJD2PNP6.js"; } from "./chunk-YJD2PNP6.js";
import {
VExpandTransition
} from "./chunk-B4JFJDWM.js";
import {
VDefaultsProvider
} from "./chunk-VH7QIDM4.js";
import { import {
makeRoundedProps, makeRoundedProps,
useRounded useRounded
} from "./chunk-AQK3CYVW.js"; } from "./chunk-AQK3CYVW.js";
import {
VDefaultsProvider
} from "./chunk-PAT7NP2Z.js";
import { import {
makeTagProps makeTagProps
} from "./chunk-2HZIACSB.js"; } from "./chunk-2HZIACSB.js";
import {
useRtl
} from "./chunk-ENTLCOCZ.js";
import { import {
useBackgroundColor useBackgroundColor
} from "./chunk-BHV5Z34M.js"; } from "./chunk-BHV5Z34M.js";
import {
useRtl
} from "./chunk-ENTLCOCZ.js";
import { import {
makeThemeProps, makeThemeProps,
provideTheme provideTheme
@@ -78,7 +78,7 @@ var VToolbarTitle = genericComponent()({
}); });
// node_modules/vuetify/lib/components/VToolbar/VToolbar.js // node_modules/vuetify/lib/components/VToolbar/VToolbar.js
import "/home/david/Documents/vscode/personal_development/ServerManagerWebApp/frontend/node_modules/vuetify/lib/components/VToolbar/VToolbar.css"; import "/home/david/gitea_repo/personal_development/ServerManagerWebApp/frontend/node_modules/vuetify/lib/components/VToolbar/VToolbar.css";
var allowedDensities = [null, "prominent", "default", "comfortable", "compact"]; var allowedDensities = [null, "prominent", "default", "comfortable", "compact"];
var makeVToolbarProps = propsFactory({ var makeVToolbarProps = propsFactory({
absolute: Boolean, absolute: Boolean,
@@ -236,4 +236,4 @@ export {
makeVToolbarProps, makeVToolbarProps,
VToolbar VToolbar
}; };
//# sourceMappingURL=chunk-LPZZJ62X.js.map //# sourceMappingURL=chunk-H2QFYTJQ.js.map
@@ -4,24 +4,27 @@ import {
import { import {
VSlideYTransition VSlideYTransition
} from "./chunk-B4JFJDWM.js"; } from "./chunk-B4JFJDWM.js";
import {
MaybeTransition,
makeTransitionProps
} from "./chunk-CAGZXMOS.js";
import { import {
makeDensityProps, makeDensityProps,
useDensity useDensity
} from "./chunk-TM3MIDYC.js"; } from "./chunk-TM3MIDYC.js";
import { import {
VIcon VIcon
} from "./chunk-CMZ4UYUX.js"; } from "./chunk-4XZHKYGH.js";
import { import {
MaybeTransition, IconValue
makeTransitionProps } from "./chunk-OJDZBU5D.js";
} from "./chunk-CAGZXMOS.js"; import {
useTextColor
} from "./chunk-BHV5Z34M.js";
import { import {
makeDimensionProps, makeDimensionProps,
useDimension useDimension
} from "./chunk-6EZKHCRW.js"; } from "./chunk-6EZKHCRW.js";
import {
IconValue
} from "./chunk-OJDZBU5D.js";
import { import {
useLocale, useLocale,
useRtl useRtl
@@ -30,9 +33,6 @@ import {
useProxiedModel, useProxiedModel,
useToggleScope useToggleScope
} from "./chunk-KA77II4V.js"; } from "./chunk-KA77II4V.js";
import {
useTextColor
} from "./chunk-BHV5Z34M.js";
import { import {
makeThemeProps, makeThemeProps,
provideTheme provideTheme
@@ -69,34 +69,8 @@ import {
watch watch
} from "./chunk-2MKFL3BX.js"; } from "./chunk-2MKFL3BX.js";
// node_modules/vuetify/lib/components/VLabel/VLabel.js
import "/home/david/Documents/vscode/personal_development/ServerManagerWebApp/frontend/node_modules/vuetify/lib/components/VLabel/VLabel.css";
var makeVLabelProps = propsFactory({
text: String,
onClick: EventProp(),
...makeComponentProps(),
...makeThemeProps()
}, "VLabel");
var VLabel = genericComponent()({
name: "VLabel",
props: makeVLabelProps(),
setup(props, _ref) {
let {
slots
} = _ref;
useRender(() => createBaseVNode("label", {
"class": normalizeClass(["v-label", {
"v-label--clickable": !!props.onClick
}, props.class]),
"style": normalizeStyle(props.style),
"onClick": props.onClick
}, [props.text, slots.default?.()]));
return {};
}
});
// node_modules/vuetify/lib/components/VMessages/VMessages.js // node_modules/vuetify/lib/components/VMessages/VMessages.js
import "/home/david/Documents/vscode/personal_development/ServerManagerWebApp/frontend/node_modules/vuetify/lib/components/VMessages/VMessages.css"; import "/home/david/gitea_repo/personal_development/ServerManagerWebApp/frontend/node_modules/vuetify/lib/components/VMessages/VMessages.css";
var makeVMessagesProps = propsFactory({ var makeVMessagesProps = propsFactory({
active: Boolean, active: Boolean,
color: String, color: String,
@@ -143,7 +117,7 @@ var VMessages = genericComponent()({
}); });
// node_modules/vuetify/lib/components/VInput/VInput.js // node_modules/vuetify/lib/components/VInput/VInput.js
import "/home/david/Documents/vscode/personal_development/ServerManagerWebApp/frontend/node_modules/vuetify/lib/components/VInput/VInput.css"; import "/home/david/gitea_repo/personal_development/ServerManagerWebApp/frontend/node_modules/vuetify/lib/components/VInput/VInput.css";
// node_modules/vuetify/lib/components/VInput/InputIcon.js // node_modules/vuetify/lib/components/VInput/InputIcon.js
function useInputIcon(props) { function useInputIcon(props) {
@@ -548,15 +522,41 @@ var VInput = genericComponent()({
} }
}); });
// node_modules/vuetify/lib/components/VLabel/VLabel.js
import "/home/david/gitea_repo/personal_development/ServerManagerWebApp/frontend/node_modules/vuetify/lib/components/VLabel/VLabel.css";
var makeVLabelProps = propsFactory({
text: String,
onClick: EventProp(),
...makeComponentProps(),
...makeThemeProps()
}, "VLabel");
var VLabel = genericComponent()({
name: "VLabel",
props: makeVLabelProps(),
setup(props, _ref) {
let {
slots
} = _ref;
useRender(() => createBaseVNode("label", {
"class": normalizeClass(["v-label", {
"v-label--clickable": !!props.onClick
}, props.class]),
"style": normalizeStyle(props.style),
"onClick": props.onClick
}, [props.text, slots.default?.()]));
return {};
}
});
export { export {
VLabel,
useInputIcon, useInputIcon,
VMessages,
makeFocusProps, makeFocusProps,
useFocus, useFocus,
VMessages,
makeValidationProps, makeValidationProps,
useValidation, useValidation,
makeVInputProps, makeVInputProps,
VInput VInput,
VLabel
}; };
//# sourceMappingURL=chunk-4VABFUPG.js.map //# sourceMappingURL=chunk-IK67YVHF.js.map
File diff suppressed because one or more lines are too long
@@ -3,37 +3,37 @@ import {
makeFocusProps, makeFocusProps,
useFocus, useFocus,
useInputIcon useInputIcon
} from "./chunk-4VABFUPG.js"; } from "./chunk-IK67YVHF.js";
import { import {
VExpandXTransition, VExpandXTransition,
VSlideYTransition VSlideYTransition
} from "./chunk-B4JFJDWM.js"; } from "./chunk-B4JFJDWM.js";
import {
MaybeTransition,
makeTransitionProps
} from "./chunk-CAGZXMOS.js";
import { import {
LoaderSlot, LoaderSlot,
makeLoaderProps, makeLoaderProps,
useLoader useLoader
} from "./chunk-U2DSRSME.js"; } from "./chunk-UXZ55AO7.js";
import {
VDefaultsProvider
} from "./chunk-VH7QIDM4.js";
import { import {
makeRoundedProps, makeRoundedProps,
useRounded useRounded
} from "./chunk-AQK3CYVW.js"; } from "./chunk-AQK3CYVW.js";
import { import {
MaybeTransition, VDefaultsProvider
makeTransitionProps } from "./chunk-PAT7NP2Z.js";
} from "./chunk-CAGZXMOS.js";
import { import {
IconValue IconValue
} from "./chunk-OJDZBU5D.js"; } from "./chunk-OJDZBU5D.js";
import {
useRtl
} from "./chunk-ENTLCOCZ.js";
import { import {
useBackgroundColor, useBackgroundColor,
useTextColor useTextColor
} from "./chunk-BHV5Z34M.js"; } from "./chunk-BHV5Z34M.js";
import {
useRtl
} from "./chunk-ENTLCOCZ.js";
import { import {
makeThemeProps, makeThemeProps,
provideTheme provideTheme
@@ -67,7 +67,7 @@ import {
} from "./chunk-2MKFL3BX.js"; } from "./chunk-2MKFL3BX.js";
// node_modules/vuetify/lib/components/VCounter/VCounter.js // node_modules/vuetify/lib/components/VCounter/VCounter.js
import "/home/david/Documents/vscode/personal_development/ServerManagerWebApp/frontend/node_modules/vuetify/lib/components/VCounter/VCounter.css"; import "/home/david/gitea_repo/personal_development/ServerManagerWebApp/frontend/node_modules/vuetify/lib/components/VCounter/VCounter.css";
var makeVCounterProps = propsFactory({ var makeVCounterProps = propsFactory({
active: Boolean, active: Boolean,
disabled: Boolean, disabled: Boolean,
@@ -135,7 +135,7 @@ var VFieldLabel = genericComponent()({
}); });
// node_modules/vuetify/lib/components/VField/VField.js // node_modules/vuetify/lib/components/VField/VField.js
import "/home/david/Documents/vscode/personal_development/ServerManagerWebApp/frontend/node_modules/vuetify/lib/components/VField/VField.css"; import "/home/david/gitea_repo/personal_development/ServerManagerWebApp/frontend/node_modules/vuetify/lib/components/VField/VField.css";
var allowedVariants = ["underlined", "outlined", "filled", "solo", "solo-inverted", "solo-filled", "plain"]; var allowedVariants = ["underlined", "outlined", "filled", "solo", "solo-inverted", "solo-filled", "plain"];
var makeVFieldProps = propsFactory({ var makeVFieldProps = propsFactory({
appendInnerIcon: IconValue, appendInnerIcon: IconValue,
@@ -452,4 +452,4 @@ export {
makeVFieldProps, makeVFieldProps,
VField VField
}; };
//# sourceMappingURL=chunk-J7Q2NMV7.js.map //# sourceMappingURL=chunk-ITO3JWXJ.js.map
@@ -1,60 +1,63 @@
import { import {
VTextField, VTextField,
makeVTextFieldProps makeVTextFieldProps
} from "./chunk-VSEC5QGQ.js"; } from "./chunk-6MO6MKTY.js";
import {
useAutocomplete
} from "./chunk-IZQZFEAL.js";
import { import {
VCheckboxBtn VCheckboxBtn
} from "./chunk-BYBKODOB.js"; } from "./chunk-6HLLVLOR.js";
import { import {
VChip VChip
} from "./chunk-SHBPUR7N.js"; } from "./chunk-PLNIG6WP.js";
import { import {
VMenu VMenu
} from "./chunk-HGIC64KW.js"; } from "./chunk-LPDOFVDK.js";
import { import {
VList, VList,
VListItem, VListItem,
VListSubheader, VListSubheader,
makeItemsProps, makeItemsProps,
useItems useItems
} from "./chunk-HV7P7R7J.js"; } from "./chunk-MQSMF5CI.js";
import { import {
VAvatar VAvatar
} from "./chunk-Z3677HMS.js"; } from "./chunk-SXI5CS7S.js";
import { import {
useAutocomplete VDivider
} from "./chunk-IZQZFEAL.js"; } from "./chunk-X3WY3J75.js";
import { import {
useInputIcon useInputIcon
} from "./chunk-4VABFUPG.js"; } from "./chunk-IK67YVHF.js";
import { import {
useForm useForm
} from "./chunk-GAYXO4FM.js"; } from "./chunk-GAYXO4FM.js";
import { import {
VDialogTransition VDialogTransition
} from "./chunk-B4JFJDWM.js"; } from "./chunk-B4JFJDWM.js";
import {
VIcon
} from "./chunk-CMZ4UYUX.js";
import {
VDefaultsProvider
} from "./chunk-VH7QIDM4.js";
import { import {
forwardRefs forwardRefs
} from "./chunk-GUNW7S2L.js"; } from "./chunk-GUNW7S2L.js";
import { import {
makeTransitionProps makeTransitionProps
} from "./chunk-CAGZXMOS.js"; } from "./chunk-CAGZXMOS.js";
import {
makeDimensionProps,
useDimension
} from "./chunk-6EZKHCRW.js";
import { import {
useDisplay useDisplay
} from "./chunk-N4LJHDYD.js"; } from "./chunk-N4LJHDYD.js";
import {
VIcon
} from "./chunk-4XZHKYGH.js";
import {
VDefaultsProvider
} from "./chunk-PAT7NP2Z.js";
import { import {
IconValue IconValue
} from "./chunk-OJDZBU5D.js"; } from "./chunk-OJDZBU5D.js";
import {
makeDimensionProps,
useDimension
} from "./chunk-6EZKHCRW.js";
import { import {
useResizeObserver useResizeObserver
} from "./chunk-XDSAO7OT.js"; } from "./chunk-XDSAO7OT.js";
@@ -65,9 +68,6 @@ import {
useProxiedModel, useProxiedModel,
useToggleScope useToggleScope
} from "./chunk-KA77II4V.js"; } from "./chunk-KA77II4V.js";
import {
VDivider
} from "./chunk-WXMECWZN.js";
import { import {
IN_BROWSER, IN_BROWSER,
camelizeProps, camelizeProps,
@@ -109,11 +109,8 @@ import {
watchEffect watchEffect
} from "./chunk-2MKFL3BX.js"; } from "./chunk-2MKFL3BX.js";
// node_modules/vuetify/lib/components/VSelect/VSelect.js
import "/home/david/Documents/vscode/personal_development/ServerManagerWebApp/frontend/node_modules/vuetify/lib/components/VSelect/VSelect.css";
// node_modules/vuetify/lib/components/VVirtualScroll/VVirtualScroll.js // node_modules/vuetify/lib/components/VVirtualScroll/VVirtualScroll.js
import "/home/david/Documents/vscode/personal_development/ServerManagerWebApp/frontend/node_modules/vuetify/lib/components/VVirtualScroll/VVirtualScroll.css"; import "/home/david/gitea_repo/personal_development/ServerManagerWebApp/frontend/node_modules/vuetify/lib/components/VVirtualScroll/VVirtualScroll.css";
// node_modules/vuetify/lib/components/VVirtualScroll/VVirtualScrollItem.js // node_modules/vuetify/lib/components/VVirtualScroll/VVirtualScrollItem.js
var makeVVirtualScrollItemProps = propsFactory({ var makeVVirtualScrollItemProps = propsFactory({
@@ -494,6 +491,9 @@ var VVirtualScroll = genericComponent()({
} }
}); });
// node_modules/vuetify/lib/components/VSelect/VSelect.js
import "/home/david/gitea_repo/personal_development/ServerManagerWebApp/frontend/node_modules/vuetify/lib/components/VSelect/VSelect.css";
// node_modules/vuetify/lib/components/VSelect/useScrolling.js // node_modules/vuetify/lib/components/VSelect/useScrolling.js
function useScrolling(listRef, textFieldRef) { function useScrolling(listRef, textFieldRef) {
const isScrolling = shallowRef(false); const isScrolling = shallowRef(false);
@@ -1079,4 +1079,4 @@ export {
makeSelectProps, makeSelectProps,
VSelect VSelect
}; };
//# sourceMappingURL=chunk-ERI3RTQS.js.map //# sourceMappingURL=chunk-IZDIYS2S.js.map
File diff suppressed because one or more lines are too long
@@ -1,32 +1,26 @@
import { import {
VInput, VLabel
VLabel, } from "./chunk-IK67YVHF.js";
makeVInputProps,
useFocus
} from "./chunk-4VABFUPG.js";
import { import {
makeDensityProps, makeDensityProps,
useDensity useDensity
} from "./chunk-TM3MIDYC.js"; } from "./chunk-TM3MIDYC.js";
import { import {
VIcon VIcon
} from "./chunk-CMZ4UYUX.js"; } from "./chunk-4XZHKYGH.js";
import {
ripple_default
} from "./chunk-5BPWKCNA.js";
import {
forwardRefs
} from "./chunk-GUNW7S2L.js";
import { import {
IconValue IconValue
} from "./chunk-OJDZBU5D.js"; } from "./chunk-OJDZBU5D.js";
import { import {
useProxiedModel ripple_default
} from "./chunk-KA77II4V.js"; } from "./chunk-DS4J67XG.js";
import { import {
useBackgroundColor, useBackgroundColor,
useTextColor useTextColor
} from "./chunk-BHV5Z34M.js"; } from "./chunk-BHV5Z34M.js";
import {
useProxiedModel
} from "./chunk-KA77II4V.js";
import { import {
makeThemeProps makeThemeProps
} from "./chunk-3DSR7NCB.js"; } from "./chunk-3DSR7NCB.js";
@@ -36,7 +30,6 @@ import {
genericComponent, genericComponent,
makeComponentProps, makeComponentProps,
matchesSelector, matchesSelector,
omit,
propsFactory, propsFactory,
provideDefaults, provideDefaults,
useRender, useRender,
@@ -61,14 +54,8 @@ import {
withDirectives withDirectives
} from "./chunk-2MKFL3BX.js"; } from "./chunk-2MKFL3BX.js";
// node_modules/vuetify/lib/components/VCheckbox/VCheckbox.js
import "/home/david/Documents/vscode/personal_development/ServerManagerWebApp/frontend/node_modules/vuetify/lib/components/VCheckbox/VCheckbox.css";
// node_modules/vuetify/lib/components/VSelectionControl/VSelectionControl.js
import "/home/david/Documents/vscode/personal_development/ServerManagerWebApp/frontend/node_modules/vuetify/lib/components/VSelectionControl/VSelectionControl.css";
// node_modules/vuetify/lib/components/VSelectionControlGroup/VSelectionControlGroup.js // node_modules/vuetify/lib/components/VSelectionControlGroup/VSelectionControlGroup.js
import "/home/david/Documents/vscode/personal_development/ServerManagerWebApp/frontend/node_modules/vuetify/lib/components/VSelectionControlGroup/VSelectionControlGroup.css"; import "/home/david/gitea_repo/personal_development/ServerManagerWebApp/frontend/node_modules/vuetify/lib/components/VSelectionControlGroup/VSelectionControlGroup.css";
var VSelectionControlGroupSymbol = /* @__PURE__ */ Symbol.for("vuetify:selection-control-group"); var VSelectionControlGroupSymbol = /* @__PURE__ */ Symbol.for("vuetify:selection-control-group");
var makeSelectionControlGroupProps = propsFactory({ var makeSelectionControlGroupProps = propsFactory({
color: String, color: String,
@@ -167,6 +154,7 @@ var VSelectionControlGroup = genericComponent()({
}); });
// node_modules/vuetify/lib/components/VSelectionControl/VSelectionControl.js // node_modules/vuetify/lib/components/VSelectionControl/VSelectionControl.js
import "/home/david/gitea_repo/personal_development/ServerManagerWebApp/frontend/node_modules/vuetify/lib/components/VSelectionControl/VSelectionControl.css";
var makeVSelectionControlProps = propsFactory({ var makeVSelectionControlProps = propsFactory({
label: String, label: String,
baseColor: String, baseColor: String,
@@ -369,133 +357,10 @@ var VSelectionControl = genericComponent()({
} }
}); });
// node_modules/vuetify/lib/components/VCheckbox/VCheckboxBtn.js
var makeVCheckboxBtnProps = propsFactory({
indeterminate: Boolean,
indeterminateIcon: {
type: IconValue,
default: "$checkboxIndeterminate"
},
...makeVSelectionControlProps({
falseIcon: "$checkboxOff",
trueIcon: "$checkboxOn"
})
}, "VCheckboxBtn");
var VCheckboxBtn = genericComponent()({
name: "VCheckboxBtn",
props: makeVCheckboxBtnProps(),
emits: {
"update:modelValue": (value) => true,
"update:indeterminate": (value) => true
},
setup(props, _ref) {
let {
slots
} = _ref;
const indeterminate = useProxiedModel(props, "indeterminate");
const model = useProxiedModel(props, "modelValue");
function onChange(v) {
if (indeterminate.value) {
indeterminate.value = false;
}
}
const falseIcon = toRef(() => {
return indeterminate.value ? props.indeterminateIcon : props.falseIcon;
});
const trueIcon = toRef(() => {
return indeterminate.value ? props.indeterminateIcon : props.trueIcon;
});
useRender(() => {
const controlProps = omit(VSelectionControl.filterProps(props), ["modelValue"]);
return createVNode(VSelectionControl, mergeProps(controlProps, {
"modelValue": model.value,
"onUpdate:modelValue": [($event) => model.value = $event, onChange],
"class": ["v-checkbox-btn", props.class],
"style": props.style,
"type": "checkbox",
"falseIcon": falseIcon.value,
"trueIcon": trueIcon.value,
"aria-checked": indeterminate.value ? "mixed" : void 0
}), slots);
});
return {};
}
});
// node_modules/vuetify/lib/components/VCheckbox/VCheckbox.js
var makeVCheckboxProps = propsFactory({
...makeVInputProps(),
...omit(makeVCheckboxBtnProps(), ["inline"])
}, "VCheckbox");
var VCheckbox = genericComponent()({
name: "VCheckbox",
inheritAttrs: false,
props: makeVCheckboxProps(),
emits: {
"update:modelValue": (value) => true,
"update:focused": (focused) => true
},
setup(props, _ref) {
let {
attrs,
slots
} = _ref;
const model = useProxiedModel(props, "modelValue");
const {
isFocused,
focus,
blur
} = useFocus(props);
const inputRef = ref();
const uid = useId();
useRender(() => {
const [rootAttrs, controlAttrs] = filterInputAttrs(attrs);
const inputProps = VInput.filterProps(props);
const checkboxProps = VCheckboxBtn.filterProps(props);
return createVNode(VInput, mergeProps({
"ref": inputRef,
"class": ["v-checkbox", props.class]
}, rootAttrs, inputProps, {
"modelValue": model.value,
"onUpdate:modelValue": ($event) => model.value = $event,
"id": props.id || `checkbox-${uid}`,
"focused": isFocused.value,
"style": props.style
}), {
...slots,
default: (_ref2) => {
let {
id,
messagesId,
isDisabled,
isReadonly,
isValid
} = _ref2;
return createVNode(VCheckboxBtn, mergeProps(checkboxProps, {
"id": id.value,
"aria-describedby": messagesId.value,
"disabled": isDisabled.value,
"readonly": isReadonly.value
}, controlAttrs, {
"error": isValid.value === false,
"modelValue": model.value,
"onUpdate:modelValue": ($event) => model.value = $event,
"onFocus": focus,
"onBlur": blur
}), slots);
}
});
});
return forwardRefs({}, inputRef);
}
});
export { export {
makeSelectionControlGroupProps, makeSelectionControlGroupProps,
VSelectionControlGroup, VSelectionControlGroup,
makeVSelectionControlProps, makeVSelectionControlProps,
VSelectionControl, VSelectionControl
VCheckboxBtn,
VCheckbox
}; };
//# sourceMappingURL=chunk-BYBKODOB.js.map //# sourceMappingURL=chunk-KHWE4FHS.js.map
File diff suppressed because one or more lines are too long
+226
View File
@@ -0,0 +1,226 @@
import {
VSelectionControl,
makeVSelectionControlProps
} from "./chunk-KHWE4FHS.js";
import {
VInput,
makeVInputProps,
useFocus
} from "./chunk-IK67YVHF.js";
import {
VScaleTransition
} from "./chunk-B4JFJDWM.js";
import {
forwardRefs
} from "./chunk-GUNW7S2L.js";
import {
VProgressCircular
} from "./chunk-MH76ZJYE.js";
import {
LoaderSlot,
useLoader
} from "./chunk-UXZ55AO7.js";
import {
VIcon
} from "./chunk-4XZHKYGH.js";
import {
VDefaultsProvider
} from "./chunk-PAT7NP2Z.js";
import {
useProxiedModel
} from "./chunk-KA77II4V.js";
import {
SUPPORTS_MATCH_MEDIA,
filterInputAttrs,
genericComponent,
propsFactory,
useRender
} from "./chunk-LZ5T2ERG.js";
import {
Fragment,
createBaseVNode,
createVNode,
mergeProps,
normalizeClass,
normalizeStyle,
ref,
toRef,
useId
} from "./chunk-2MKFL3BX.js";
// node_modules/vuetify/lib/components/VSwitch/VSwitch.js
import "/home/david/gitea_repo/personal_development/ServerManagerWebApp/frontend/node_modules/vuetify/lib/components/VSwitch/VSwitch.css";
var makeVSwitchProps = propsFactory({
indeterminate: Boolean,
inset: Boolean,
flat: Boolean,
loading: {
type: [Boolean, String],
default: false
},
...makeVInputProps(),
...makeVSelectionControlProps()
}, "VSwitch");
var VSwitch = genericComponent()({
name: "VSwitch",
inheritAttrs: false,
props: makeVSwitchProps(),
emits: {
"update:focused": (focused) => true,
"update:modelValue": (value) => true,
"update:indeterminate": (value) => true
},
setup(props, _ref) {
let {
attrs,
slots
} = _ref;
const indeterminate = useProxiedModel(props, "indeterminate");
const model = useProxiedModel(props, "modelValue");
const {
loaderClasses
} = useLoader(props);
const {
isFocused,
focus,
blur
} = useFocus(props);
const control = ref();
const inputRef = ref();
const isForcedColorsModeActive = SUPPORTS_MATCH_MEDIA && window.matchMedia("(forced-colors: active)").matches;
const loaderColor = toRef(() => {
return typeof props.loading === "string" && props.loading !== "" ? props.loading : props.color;
});
const uid = useId();
const id = toRef(() => props.id || `switch-${uid}`);
function onChange() {
if (indeterminate.value) {
indeterminate.value = false;
}
}
function onTrackClick(e) {
e.stopPropagation();
e.preventDefault();
control.value?.input?.click();
}
useRender(() => {
const [rootAttrs, controlAttrs] = filterInputAttrs(attrs);
const inputProps = VInput.filterProps(props);
const controlProps = VSelectionControl.filterProps(props);
return createVNode(VInput, mergeProps({
"ref": inputRef,
"class": ["v-switch", {
"v-switch--flat": props.flat
}, {
"v-switch--inset": props.inset
}, {
"v-switch--indeterminate": indeterminate.value
}, loaderClasses.value, props.class]
}, rootAttrs, inputProps, {
"modelValue": model.value,
"onUpdate:modelValue": ($event) => model.value = $event,
"id": id.value,
"focused": isFocused.value,
"style": props.style
}), {
...slots,
default: (_ref2) => {
let {
id: id2,
messagesId,
isDisabled,
isReadonly,
isValid
} = _ref2;
const slotProps = {
model,
isValid
};
return createVNode(VSelectionControl, mergeProps({
"ref": control
}, controlProps, {
"modelValue": model.value,
"onUpdate:modelValue": [($event) => model.value = $event, onChange],
"id": id2.value,
"aria-describedby": messagesId.value,
"type": "checkbox",
"aria-checked": indeterminate.value ? "mixed" : void 0,
"disabled": isDisabled.value,
"readonly": isReadonly.value,
"onFocus": focus,
"onBlur": blur
}, controlAttrs), {
...slots,
default: (_ref3) => {
let {
backgroundColorClasses,
backgroundColorStyles
} = _ref3;
return createBaseVNode("div", {
"class": normalizeClass(["v-switch__track", !isForcedColorsModeActive ? backgroundColorClasses.value : void 0]),
"style": normalizeStyle(backgroundColorStyles.value),
"onClick": onTrackClick
}, [slots["track-true"] && createBaseVNode("div", {
"key": "prepend",
"class": "v-switch__track-true"
}, [slots["track-true"](slotProps)]), slots["track-false"] && createBaseVNode("div", {
"key": "append",
"class": "v-switch__track-false"
}, [slots["track-false"](slotProps)])]);
},
input: (_ref4) => {
let {
inputNode,
icon,
backgroundColorClasses,
backgroundColorStyles
} = _ref4;
return createBaseVNode(Fragment, null, [inputNode, createBaseVNode("div", {
"class": normalizeClass(["v-switch__thumb", {
"v-switch__thumb--filled": icon || props.loading
}, props.inset || isForcedColorsModeActive ? void 0 : backgroundColorClasses.value]),
"style": normalizeStyle(props.inset ? void 0 : backgroundColorStyles.value)
}, [slots.thumb ? createVNode(VDefaultsProvider, {
"defaults": {
VIcon: {
icon,
size: "x-small"
}
}
}, {
default: () => [slots.thumb({
...slotProps,
icon
})]
}) : createVNode(VScaleTransition, null, {
default: () => [!props.loading ? icon && createVNode(VIcon, {
"key": String(icon),
"icon": icon,
"size": "x-small"
}, null) : createVNode(LoaderSlot, {
"name": "v-switch",
"active": true,
"color": isValid.value === false ? void 0 : loaderColor.value
}, {
default: (slotProps2) => slots.loader ? slots.loader(slotProps2) : createVNode(VProgressCircular, {
"active": slotProps2.isActive,
"color": slotProps2.color,
"indeterminate": true,
"size": "16",
"width": "2"
}, null)
})]
})])]);
}
});
}
});
});
return forwardRefs({}, inputRef);
}
});
export {
VSwitch
};
//# sourceMappingURL=chunk-L7BQJLEW.js.map
File diff suppressed because one or more lines are too long
+177
View File
@@ -0,0 +1,177 @@
import {
useSsrBoot
} from "./chunk-PCKQGDLK.js";
import {
makeLayoutItemProps,
useLayoutItem
} from "./chunk-KQXZAEJP.js";
import {
VBtnToggleSymbol
} from "./chunk-5AQLTWWS.js";
import {
makeGroupProps,
useGroup
} from "./chunk-W6DJL42A.js";
import {
makeBorderProps,
useBorder
} from "./chunk-X7YBE6PF.js";
import {
makeElevationProps,
useElevation
} from "./chunk-YJD2PNP6.js";
import {
makeDensityProps,
useDensity
} from "./chunk-TM3MIDYC.js";
import {
makeRoundedProps,
useRounded
} from "./chunk-AQK3CYVW.js";
import {
makeTagProps
} from "./chunk-2HZIACSB.js";
import {
useBackgroundColor
} from "./chunk-BHV5Z34M.js";
import {
useProxiedModel
} from "./chunk-KA77II4V.js";
import {
makeThemeProps,
useTheme
} from "./chunk-3DSR7NCB.js";
import {
convertToUnit,
genericComponent,
makeComponentProps,
propsFactory,
provideDefaults,
useRender
} from "./chunk-LZ5T2ERG.js";
import {
computed,
createBaseVNode,
createVNode,
normalizeClass,
normalizeStyle,
toRef
} from "./chunk-2MKFL3BX.js";
// node_modules/vuetify/lib/components/VBottomNavigation/VBottomNavigation.js
import "/home/david/gitea_repo/personal_development/ServerManagerWebApp/frontend/node_modules/vuetify/lib/components/VBottomNavigation/VBottomNavigation.css";
var makeVBottomNavigationProps = propsFactory({
baseColor: String,
bgColor: String,
color: String,
grow: Boolean,
mode: {
type: String,
validator: (v) => !v || ["horizontal", "shift"].includes(v)
},
height: {
type: [Number, String],
default: 56
},
active: {
type: Boolean,
default: true
},
...makeBorderProps(),
...makeComponentProps(),
...makeDensityProps(),
...makeElevationProps(),
...makeRoundedProps(),
...makeLayoutItemProps({
name: "bottom-navigation"
}),
...makeTagProps({
tag: "header"
}),
...makeGroupProps({
selectedClass: "v-btn--selected"
}),
...makeThemeProps()
}, "VBottomNavigation");
var VBottomNavigation = genericComponent()({
name: "VBottomNavigation",
props: makeVBottomNavigationProps(),
emits: {
"update:active": (value) => true,
"update:modelValue": (value) => true
},
setup(props, _ref) {
let {
slots
} = _ref;
const {
themeClasses
} = useTheme();
const {
borderClasses
} = useBorder(props);
const {
backgroundColorClasses,
backgroundColorStyles
} = useBackgroundColor(() => props.bgColor);
const {
densityClasses
} = useDensity(props);
const {
elevationClasses
} = useElevation(props);
const {
roundedClasses
} = useRounded(props);
const {
ssrBootStyles
} = useSsrBoot();
const height = computed(() => Number(props.height) - (props.density === "comfortable" ? 8 : 0) - (props.density === "compact" ? 16 : 0));
const isActive = useProxiedModel(props, "active", props.active);
const {
layoutItemStyles
} = useLayoutItem({
id: props.name,
order: computed(() => parseInt(props.order, 10)),
position: toRef(() => "bottom"),
layoutSize: toRef(() => isActive.value ? height.value : 0),
elementSize: height,
active: isActive,
absolute: toRef(() => props.absolute)
});
useGroup(props, VBtnToggleSymbol);
provideDefaults({
VBtn: {
baseColor: toRef(() => props.baseColor),
color: toRef(() => props.color),
density: toRef(() => props.density),
stacked: toRef(() => props.mode !== "horizontal"),
variant: "text"
}
}, {
scoped: true
});
useRender(() => {
return createVNode(props.tag, {
"class": normalizeClass(["v-bottom-navigation", {
"v-bottom-navigation--active": isActive.value,
"v-bottom-navigation--grow": props.grow,
"v-bottom-navigation--shift": props.mode === "shift"
}, themeClasses.value, backgroundColorClasses.value, borderClasses.value, densityClasses.value, elevationClasses.value, roundedClasses.value, props.class]),
"style": normalizeStyle([backgroundColorStyles.value, layoutItemStyles.value, {
height: convertToUnit(height.value)
}, ssrBootStyles.value, props.style])
}, {
default: () => [slots.default && createBaseVNode("div", {
"class": "v-bottom-navigation__content"
}, [slots.default()])]
});
});
return {};
}
});
export {
VBottomNavigation
};
//# sourceMappingURL=chunk-LFKLAYDH.js.map
@@ -0,0 +1,7 @@
{
"version": 3,
"sources": ["../../vuetify/src/components/VBottomNavigation/VBottomNavigation.tsx"],
"sourcesContent": ["// Styles\nimport './VBottomNavigation.sass'\n\n// Components\nimport { VBtnToggleSymbol } from '@/components/VBtnToggle/VBtnToggle'\n\n// Composables\nimport { makeBorderProps, useBorder } from '@/composables/border'\nimport { useBackgroundColor } from '@/composables/color'\nimport { makeComponentProps } from '@/composables/component'\nimport { provideDefaults } from '@/composables/defaults'\nimport { makeDensityProps, useDensity } from '@/composables/density'\nimport { makeElevationProps, useElevation } from '@/composables/elevation'\nimport { makeGroupProps, useGroup } from '@/composables/group'\nimport { makeLayoutItemProps, useLayoutItem } from '@/composables/layout'\nimport { useProxiedModel } from '@/composables/proxiedModel'\nimport { makeRoundedProps, useRounded } from '@/composables/rounded'\nimport { useSsrBoot } from '@/composables/ssrBoot'\nimport { makeTagProps } from '@/composables/tag'\nimport { makeThemeProps, useTheme } from '@/composables/theme'\n\n// Utilities\nimport { computed, toRef } from 'vue'\nimport { convertToUnit, genericComponent, propsFactory, useRender } from '@/util'\n\n// Types\nimport type { GenericProps } from '@/util'\n\nexport const makeVBottomNavigationProps = propsFactory({\n baseColor: String,\n bgColor: String,\n color: String,\n grow: Boolean,\n mode: {\n type: String,\n validator: (v: any) => !v || ['horizontal', 'shift'].includes(v),\n },\n height: {\n type: [Number, String],\n default: 56,\n },\n active: {\n type: Boolean,\n default: true,\n },\n\n ...makeBorderProps(),\n ...makeComponentProps(),\n ...makeDensityProps(),\n ...makeElevationProps(),\n ...makeRoundedProps(),\n ...makeLayoutItemProps({ name: 'bottom-navigation' }),\n ...makeTagProps({ tag: 'header' }),\n ...makeGroupProps({ selectedClass: 'v-btn--selected' }),\n ...makeThemeProps(),\n}, 'VBottomNavigation')\n\nexport const VBottomNavigation = genericComponent<new <T>(\n props: {\n modelValue?: T\n 'onUpdate:modelValue'?: (value: T) => void\n },\n slots: { default: never },\n) => GenericProps<typeof props, typeof slots>>()({\n name: 'VBottomNavigation',\n\n props: makeVBottomNavigationProps(),\n\n emits: {\n 'update:active': (value: any) => true,\n 'update:modelValue': (value: any) => true,\n },\n\n setup (props, { slots }) {\n const { themeClasses } = useTheme()\n const { borderClasses } = useBorder(props)\n const { backgroundColorClasses, backgroundColorStyles } = useBackgroundColor(() => props.bgColor)\n const { densityClasses } = useDensity(props)\n const { elevationClasses } = useElevation(props)\n const { roundedClasses } = useRounded(props)\n const { ssrBootStyles } = useSsrBoot()\n const height = computed(() => (\n Number(props.height) -\n (props.density === 'comfortable' ? 8 : 0) -\n (props.density === 'compact' ? 16 : 0)\n ))\n const isActive = useProxiedModel(props, 'active', props.active)\n const { layoutItemStyles } = useLayoutItem({\n id: props.name,\n order: computed(() => parseInt(props.order, 10)),\n position: toRef(() => 'bottom'),\n layoutSize: toRef(() => isActive.value ? height.value : 0),\n elementSize: height,\n active: isActive,\n absolute: toRef(() => props.absolute),\n })\n\n useGroup(props, VBtnToggleSymbol)\n\n provideDefaults({\n VBtn: {\n baseColor: toRef(() => props.baseColor),\n color: toRef(() => props.color),\n density: toRef(() => props.density),\n stacked: toRef(() => props.mode !== 'horizontal'),\n variant: 'text',\n },\n }, { scoped: true })\n\n useRender(() => {\n return (\n <props.tag\n class={[\n 'v-bottom-navigation',\n {\n 'v-bottom-navigation--active': isActive.value,\n 'v-bottom-navigation--grow': props.grow,\n 'v-bottom-navigation--shift': props.mode === 'shift',\n },\n themeClasses.value,\n backgroundColorClasses.value,\n borderClasses.value,\n densityClasses.value,\n elevationClasses.value,\n roundedClasses.value,\n props.class,\n ]}\n style={[\n backgroundColorStyles.value,\n layoutItemStyles.value,\n {\n height: convertToUnit(height.value),\n },\n ssrBootStyles.value,\n props.style,\n ]}\n >\n { slots.default && (\n <div class=\"v-bottom-navigation__content\">\n { slots.default() }\n </div>\n )}\n </props.tag>\n )\n })\n\n return {}\n },\n})\n\nexport type VBottomNavigation = InstanceType<typeof VBottomNavigation>\n"],
"mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AACA,OAAA;AA2BO,IAAMA,6BAA6BC,aAAa;EACrDC,WAAWC;EACXC,SAASD;EACTE,OAAOF;EACPG,MAAMC;EACNC,MAAM;IACJC,MAAMN;IACNO,WAAYC,OAAW,CAACA,KAAK,CAAC,cAAc,OAAO,EAAEC,SAASD,CAAC;EACjE;EACAE,QAAQ;IACNJ,MAAM,CAACK,QAAQX,MAAM;IACrBY,SAAS;EACX;EACAC,QAAQ;IACNP,MAAMF;IACNQ,SAAS;EACX;EAEA,GAAGE,gBAAgB;EACnB,GAAGC,mBAAmB;EACtB,GAAGC,iBAAiB;EACpB,GAAGC,mBAAmB;EACtB,GAAGC,iBAAiB;EACpB,GAAGC,oBAAoB;IAAEC,MAAM;EAAoB,CAAC;EACpD,GAAGC,aAAa;IAAEC,KAAK;EAAS,CAAC;EACjC,GAAGC,eAAe;IAAEC,eAAe;EAAkB,CAAC;EACtD,GAAGC,eAAe;AACpB,GAAG,mBAAmB;AAEf,IAAMC,oBAAoBC,iBAMc,EAAE;EAC/CP,MAAM;EAENQ,OAAO/B,2BAA2B;EAElCgC,OAAO;IACL,iBAAkBC,WAAe;IACjC,qBAAsBA,WAAe;EACvC;EAEAC,MAAOH,OAAKI,MAAa;AAAA,QAAX;MAAEC;IAAM,IAACD;AACrB,UAAM;MAAEE;IAAa,IAAIC,SAAS;AAClC,UAAM;MAAEC;IAAc,IAAIC,UAAUT,KAAK;AACzC,UAAM;MAAEU;MAAwBC;IAAsB,IAAIC,mBAAmB,MAAMZ,MAAM3B,OAAO;AAChG,UAAM;MAAEwC;IAAe,IAAIC,WAAWd,KAAK;AAC3C,UAAM;MAAEe;IAAiB,IAAIC,aAAahB,KAAK;AAC/C,UAAM;MAAEiB;IAAe,IAAIC,WAAWlB,KAAK;AAC3C,UAAM;MAAEmB;IAAc,IAAIC,WAAW;AACrC,UAAMtC,SAASuC,SAAS,MACtBtC,OAAOiB,MAAMlB,MAAM,KAClBkB,MAAMsB,YAAY,gBAAgB,IAAI,MACtCtB,MAAMsB,YAAY,YAAY,KAAK,EACrC;AACD,UAAMC,WAAWC,gBAAgBxB,OAAO,UAAUA,MAAMf,MAAM;AAC9D,UAAM;MAAEwC;IAAiB,IAAIC,cAAc;MACzCC,IAAI3B,MAAMR;MACVoC,OAAOP,SAAS,MAAMQ,SAAS7B,MAAM4B,OAAO,EAAE,CAAC;MAC/CE,UAAUC,MAAM,MAAM,QAAQ;MAC9BC,YAAYD,MAAM,MAAMR,SAASrB,QAAQpB,OAAOoB,QAAQ,CAAC;MACzD+B,aAAanD;MACbG,QAAQsC;MACRW,UAAUH,MAAM,MAAM/B,MAAMkC,QAAQ;IACtC,CAAC;AAEDC,aAASnC,OAAOoC,gBAAgB;AAEhCC,oBAAgB;MACdC,MAAM;QACJnE,WAAW4D,MAAM,MAAM/B,MAAM7B,SAAS;QACtCG,OAAOyD,MAAM,MAAM/B,MAAM1B,KAAK;QAC9BgD,SAASS,MAAM,MAAM/B,MAAMsB,OAAO;QAClCiB,SAASR,MAAM,MAAM/B,MAAMvB,SAAS,YAAY;QAChD+D,SAAS;MACX;IACF,GAAG;MAAEC,QAAQ;IAAK,CAAC;AAEnBC,cAAU,MAAM;AACd,aAAAC,YAAA3C,MAAAN,KAAA;QAAA,SAAAkD,eAEW,CACL,uBACA;UACE,+BAA+BrB,SAASrB;UACxC,6BAA6BF,MAAMzB;UACnC,8BAA8ByB,MAAMvB,SAAS;QAC/C,GACA6B,aAAaJ,OACbQ,uBAAuBR,OACvBM,cAAcN,OACdW,eAAeX,OACfa,iBAAiBb,OACjBe,eAAef,OACfF,MAAM6C,KAAK,CACZ;QAAA,SAAAC,eACM,CACLnC,sBAAsBT,OACtBuB,iBAAiBvB,OACjB;UACEpB,QAAQiE,cAAcjE,OAAOoB,KAAK;QACpC,GACAiB,cAAcjB,OACdF,MAAMgD,KAAK,CACZ;MAAA,GAAA;QAAAhE,SAAAA,MAAA,CAECqB,MAAMrB,WAAOiE,gBAAA,OAAA;UAAA,SAAA;QAAA,GAAA,CAET5C,MAAMrB,QAAQ,CAAC,CAAA,CAEpB;MAAA,CAAA;IAGP,CAAC;AAED,WAAO,CAAC;EACV;AACF,CAAC;",
"names": ["makeVBottomNavigationProps", "propsFactory", "baseColor", "String", "bgColor", "color", "grow", "Boolean", "mode", "type", "validator", "v", "includes", "height", "Number", "default", "active", "makeBorderProps", "makeComponentProps", "makeDensityProps", "makeElevationProps", "makeRoundedProps", "makeLayoutItemProps", "name", "makeTagProps", "tag", "makeGroupProps", "selectedClass", "makeThemeProps", "VBottomNavigation", "genericComponent", "props", "emits", "value", "setup", "_ref", "slots", "themeClasses", "useTheme", "borderClasses", "useBorder", "backgroundColorClasses", "backgroundColorStyles", "useBackgroundColor", "densityClasses", "useDensity", "elevationClasses", "useElevation", "roundedClasses", "useRounded", "ssrBootStyles", "useSsrBoot", "computed", "density", "isActive", "useProxiedModel", "layoutItemStyles", "useLayoutItem", "id", "order", "parseInt", "position", "toRef", "layoutSize", "elementSize", "absolute", "useGroup", "VBtnToggleSymbol", "provideDefaults", "VBtn", "stacked", "variant", "scoped", "useRender", "_createVNode", "_normalizeClass", "class", "_normalizeStyle", "convertToUnit", "style", "_createElementVNode"]
}
@@ -1,20 +1,20 @@
import { import {
VDialogTransition VDialogTransition
} from "./chunk-B4JFJDWM.js"; } from "./chunk-B4JFJDWM.js";
import {
VDefaultsProvider
} from "./chunk-VH7QIDM4.js";
import { import {
VMenuSymbol, VMenuSymbol,
VOverlay, VOverlay,
makeVOverlayProps makeVOverlayProps
} from "./chunk-TS6Y6OVT.js"; } from "./chunk-CL6G3ICF.js";
import { import {
useScopeId useScopeId
} from "./chunk-BVUPSGRF.js"; } from "./chunk-BVUPSGRF.js";
import { import {
forwardRefs forwardRefs
} from "./chunk-GUNW7S2L.js"; } from "./chunk-GUNW7S2L.js";
import {
VDefaultsProvider
} from "./chunk-PAT7NP2Z.js";
import { import {
useRtl useRtl
} from "./chunk-ENTLCOCZ.js"; } from "./chunk-ENTLCOCZ.js";
@@ -47,7 +47,7 @@ import {
} from "./chunk-2MKFL3BX.js"; } from "./chunk-2MKFL3BX.js";
// node_modules/vuetify/lib/components/VMenu/VMenu.js // node_modules/vuetify/lib/components/VMenu/VMenu.js
import "/home/david/Documents/vscode/personal_development/ServerManagerWebApp/frontend/node_modules/vuetify/lib/components/VMenu/VMenu.css"; import "/home/david/gitea_repo/personal_development/ServerManagerWebApp/frontend/node_modules/vuetify/lib/components/VMenu/VMenu.css";
var makeVMenuProps = propsFactory({ var makeVMenuProps = propsFactory({
// TODO // TODO
// disableKeys: Boolean, // disableKeys: Boolean,
@@ -203,4 +203,4 @@ export {
makeVMenuProps, makeVMenuProps,
VMenu VMenu
}; };
//# sourceMappingURL=chunk-HGIC64KW.js.map //# sourceMappingURL=chunk-LPDOFVDK.js.map
@@ -8,12 +8,12 @@ import {
import { import {
makeTagProps makeTagProps
} from "./chunk-2HZIACSB.js"; } from "./chunk-2HZIACSB.js";
import {
useResizeObserver
} from "./chunk-XDSAO7OT.js";
import { import {
useTextColor useTextColor
} from "./chunk-BHV5Z34M.js"; } from "./chunk-BHV5Z34M.js";
import {
useResizeObserver
} from "./chunk-XDSAO7OT.js";
import { import {
makeThemeProps, makeThemeProps,
provideTheme provideTheme
@@ -39,7 +39,7 @@ import {
} from "./chunk-2MKFL3BX.js"; } from "./chunk-2MKFL3BX.js";
// node_modules/vuetify/lib/components/VProgressCircular/VProgressCircular.js // node_modules/vuetify/lib/components/VProgressCircular/VProgressCircular.js
import "/home/david/Documents/vscode/personal_development/ServerManagerWebApp/frontend/node_modules/vuetify/lib/components/VProgressCircular/VProgressCircular.css"; import "/home/david/gitea_repo/personal_development/ServerManagerWebApp/frontend/node_modules/vuetify/lib/components/VProgressCircular/VProgressCircular.css";
var makeVProgressCircularProps = propsFactory({ var makeVProgressCircularProps = propsFactory({
bgColor: String, bgColor: String,
color: String, color: String,
@@ -168,4 +168,4 @@ var VProgressCircular = genericComponent()({
export { export {
VProgressCircular VProgressCircular
}; };
//# sourceMappingURL=chunk-ET5GCC5W.js.map //# sourceMappingURL=chunk-MH76ZJYE.js.map
@@ -1,14 +1,23 @@
import { import {
VAvatar VAvatar
} from "./chunk-Z3677HMS.js"; } from "./chunk-SXI5CS7S.js";
import {
VDivider
} from "./chunk-X3WY3J75.js";
import {
useSsrBoot
} from "./chunk-PCKQGDLK.js";
import {
VExpandTransition
} from "./chunk-B4JFJDWM.js";
import {
MaybeTransition
} from "./chunk-CAGZXMOS.js";
import { import {
genOverlays, genOverlays,
makeVariantProps, makeVariantProps,
useVariant useVariant
} from "./chunk-ADXDXPPR.js"; } from "./chunk-ADXDXPPR.js";
import {
useSsrBoot
} from "./chunk-PCKQGDLK.js";
import { import {
makeBorderProps, makeBorderProps,
useBorder useBorder
@@ -17,53 +26,44 @@ import {
makeElevationProps, makeElevationProps,
useElevation useElevation
} from "./chunk-YJD2PNP6.js"; } from "./chunk-YJD2PNP6.js";
import {
VExpandTransition
} from "./chunk-B4JFJDWM.js";
import { import {
makeDensityProps, makeDensityProps,
useDensity useDensity
} from "./chunk-TM3MIDYC.js"; } from "./chunk-TM3MIDYC.js";
import { import {
VIcon VIcon
} from "./chunk-CMZ4UYUX.js"; } from "./chunk-4XZHKYGH.js";
import {
VDefaultsProvider
} from "./chunk-VH7QIDM4.js";
import { import {
makeRoundedProps, makeRoundedProps,
useRounded useRounded
} from "./chunk-AQK3CYVW.js"; } from "./chunk-AQK3CYVW.js";
import {
VDefaultsProvider
} from "./chunk-PAT7NP2Z.js";
import { import {
makeTagProps makeTagProps
} from "./chunk-2HZIACSB.js"; } from "./chunk-2HZIACSB.js";
import { import {
ripple_default IconValue
} from "./chunk-5BPWKCNA.js"; } from "./chunk-OJDZBU5D.js";
import { import {
makeRouterProps, makeRouterProps,
useLink useLink
} from "./chunk-573D4TYC.js"; } from "./chunk-573D4TYC.js";
import { import {
MaybeTransition ripple_default
} from "./chunk-CAGZXMOS.js"; } from "./chunk-DS4J67XG.js";
import {
useBackgroundColor,
useTextColor
} from "./chunk-BHV5Z34M.js";
import { import {
makeDimensionProps, makeDimensionProps,
useDimension useDimension
} from "./chunk-6EZKHCRW.js"; } from "./chunk-6EZKHCRW.js";
import {
IconValue
} from "./chunk-OJDZBU5D.js";
import { import {
useProxiedModel useProxiedModel
} from "./chunk-KA77II4V.js"; } from "./chunk-KA77II4V.js";
import {
VDivider
} from "./chunk-WXMECWZN.js";
import {
useBackgroundColor,
useTextColor
} from "./chunk-BHV5Z34M.js";
import { import {
makeThemeProps, makeThemeProps,
provideTheme provideTheme
@@ -116,7 +116,7 @@ import {
} from "./chunk-2MKFL3BX.js"; } from "./chunk-2MKFL3BX.js";
// node_modules/vuetify/lib/components/VList/VList.js // node_modules/vuetify/lib/components/VList/VList.js
import "/home/david/Documents/vscode/personal_development/ServerManagerWebApp/frontend/node_modules/vuetify/lib/components/VList/VList.css"; import "/home/david/gitea_repo/personal_development/ServerManagerWebApp/frontend/node_modules/vuetify/lib/components/VList/VList.css";
// node_modules/vuetify/lib/components/VList/list.js // node_modules/vuetify/lib/components/VList/list.js
var ListKey = /* @__PURE__ */ Symbol.for("vuetify:list"); var ListKey = /* @__PURE__ */ Symbol.for("vuetify:list");
@@ -1015,7 +1015,7 @@ var VListGroup = genericComponent()({
}); });
// node_modules/vuetify/lib/components/VList/VListItem.js // node_modules/vuetify/lib/components/VList/VListItem.js
import "/home/david/Documents/vscode/personal_development/ServerManagerWebApp/frontend/node_modules/vuetify/lib/components/VList/VListItem.css"; import "/home/david/gitea_repo/personal_development/ServerManagerWebApp/frontend/node_modules/vuetify/lib/components/VList/VListItem.css";
// node_modules/vuetify/lib/components/VList/VListItemSubtitle.js // node_modules/vuetify/lib/components/VList/VListItemSubtitle.js
var makeVListItemSubtitleProps = propsFactory({ var makeVListItemSubtitleProps = propsFactory({
@@ -1893,4 +1893,4 @@ export {
VListItemAction, VListItemAction,
VListItemMedia VListItemMedia
}; };
//# sourceMappingURL=chunk-HV7P7R7J.js.map //# sourceMappingURL=chunk-MQSMF5CI.js.map
@@ -3,11 +3,7 @@ import {
VToolbarTitle, VToolbarTitle,
makeVToolbarProps, makeVToolbarProps,
makeVToolbarTitleProps makeVToolbarTitleProps
} from "./chunk-LPZZJ62X.js"; } from "./chunk-H2QFYTJQ.js";
import {
VBtn,
makeVBtnProps
} from "./chunk-QC7VWQER.js";
import { import {
useSsrBoot useSsrBoot
} from "./chunk-PCKQGDLK.js"; } from "./chunk-PCKQGDLK.js";
@@ -15,6 +11,10 @@ import {
makeLayoutItemProps, makeLayoutItemProps,
useLayoutItem useLayoutItem
} from "./chunk-KQXZAEJP.js"; } from "./chunk-KQXZAEJP.js";
import {
VBtn,
makeVBtnProps
} from "./chunk-UQALH6SL.js";
import { import {
useProxiedModel, useProxiedModel,
useToggleScope useToggleScope
@@ -41,7 +41,7 @@ import {
} from "./chunk-2MKFL3BX.js"; } from "./chunk-2MKFL3BX.js";
// node_modules/vuetify/lib/components/VAppBar/VAppBar.js // node_modules/vuetify/lib/components/VAppBar/VAppBar.js
import "/home/david/Documents/vscode/personal_development/ServerManagerWebApp/frontend/node_modules/vuetify/lib/components/VAppBar/VAppBar.css"; import "/home/david/gitea_repo/personal_development/ServerManagerWebApp/frontend/node_modules/vuetify/lib/components/VAppBar/VAppBar.css";
// node_modules/vuetify/lib/composables/scroll.js // node_modules/vuetify/lib/composables/scroll.js
var makeScrollProps = propsFactory({ var makeScrollProps = propsFactory({
@@ -357,4 +357,4 @@ export {
VAppBarNavIcon, VAppBarNavIcon,
VAppBarTitle VAppBarTitle
}; };
//# sourceMappingURL=chunk-BXEVD6W6.js.map //# sourceMappingURL=chunk-P6EQK5CJ.js.map
@@ -42,4 +42,4 @@ var VDefaultsProvider = genericComponent(false)({
export { export {
VDefaultsProvider VDefaultsProvider
}; };
//# sourceMappingURL=chunk-VH7QIDM4.js.map //# sourceMappingURL=chunk-PAT7NP2Z.js.map

Some files were not shown because too many files have changed in this diff Show More