major server manager overhaul

This commit is contained in:
2026-01-09 19:24:41 +00:00
parent 6a667d1019
commit c0ac650625
202 changed files with 325323 additions and 6929 deletions
+3 -2
View File
@@ -1,2 +1,3 @@
10546
10547
26015
26017
26018
+662 -92
View File
@@ -1,9 +1,15 @@
import os
import re
from collections import defaultdict
import shutil
import zipfile
import tempfile
import logging
import cloudscraper
import difflib
from bs4 import BeautifulSoup
from collections import defaultdict
from .samba_manager import SambaManager
from .config import Settings
from .config import settings
from smb.smb_structs import OperationFailure
# --- Dedicated logger for ComicsManager ---
@@ -11,117 +17,681 @@ comics_logger = logging.getLogger('comics_manager')
comics_logger.setLevel(logging.INFO)
comics_logger.propagate = False
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.setFormatter(logging.Formatter('%(asctime)s - %(levelname)s - %(message)s'))
comics_logger.addHandler(comics_log_handler)
comics_logger.addHandler(logging.StreamHandler())
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_name(filename):
def _get_series_info(self, 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)
match = pattern.search(base_name)
artist = "Unknown"
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:
return base_name[:match.start()].strip('-_ ')
return base_name.strip('-_ ')
name = name[:match.start()]
else:
name = re.sub(r'[-_\\s]+\\d+\\s*$', '', name)
def cleanup_toberead(self, samba_manager: SambaManager):
comics_logger.info("Starting cleanup of 'toberead' directory...")
comics_root = "/comics/toberead"
try:
items_in_toberead = samba_manager.list_path(comics_root)
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}")
name = name.strip(' -_')
if not name and tags:
name = tags[0]
elif not name:
name = base_name.strip()
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:
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):
comics_logger.info("Starting comics organization process...")
comics_root = "/comics/toberead"
comics_dest_root = "/comics/manga"
comics_logger.info(f"Starting organization in {self.comics_root}...")
self._recursive_scan_and_process(samba_manager, self.comics_root)
def _recursive_scan_and_process(self, samba_manager, current_path):
try:
items_in_toberead = samba_manager.list_path(comics_root)
if "error" in items_in_toberead:
raise Exception(f"Failed to list files in 'toberead': {items_in_toberead['error']}")
items = samba_manager.list_path(current_path)
if isinstance(items, dict) and "error" in items: return
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)
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)
def get_pending_comics(self, samba_manager: SambaManager):
try:
items = samba_manager.list_path(self.comics_root)
if isinstance(items, dict) and "error" in items: return []
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():
comics_logger.info(f"Processing series: {series_name}")
final_series_folder_path = f"{comics_dest_root}/{series_name}".replace("\\", "/")
def update_existing_metadata(self, samba_manager, target_path="/comics/manga", force=False):
"""
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:
samba_manager.create_directory(final_series_folder_path)
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("\\", "/")
# Create Artist folder
try:
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}")
samba_manager.create_directory(artist_folder)
except OperationFailure: pass # Exists
# 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:
comics_logger.error(f"An error occurred during comics organization: {e}", exc_info=True)
comics_logger.info("Comics organization process finished.")
return
comics_logger.error(f"Sort by artist failed: {e}")
+44 -4
View File
@@ -2,6 +2,8 @@ from pydantic_settings import BaseSettings, SettingsConfigDict
import subprocess
import logging
import shutil
import json
import os
logger = logging.getLogger(__name__)
@@ -14,6 +16,8 @@ logging.basicConfig(
]
)
SETTINGS_FILE = "resources/config/app_settings.json"
def check_gpu_support():
try:
logger.debug("Checking ffmpeg for CUDA support...")
@@ -29,12 +33,48 @@ def check_gpu_support():
return False
class Settings(BaseSettings):
samba_server_ip: str
samba_username: str
samba_password: str
samba_server_ip: str = "127.0.0.1"
samba_username: str = "guest"
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()
# 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.load_from_json()
@@ -2,54 +2,96 @@ import logging
import os
import subprocess
import tempfile
import json
import time
import itertools
from PIL import Image
import imagehash
from sqlalchemy.orm import Session
from .samba_manager import SambaManager
from .config import Settings
from .stash_service import StashService
from . import models
import itertools
import logging
import time
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:
def __init__(self, settings: Settings, db: Session):
self.settings = settings
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):
logger.debug(f"Running ffprobe for duration of {filepath}")
try:
command = [
"ffprobe",
"-v",
"error",
"-show_entries",
"format=duration",
"-of",
"default=noprint_wrappers=1:nokey=1",
filepath,
"ffprobe", "-v", "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,
)
logger.debug(f"ffprobe stdout: {result.stdout.strip()}")
logger.debug(f"ffprobe stderr: {result.stderr.strip()}")
result = subprocess.run(command, capture_output=True, text=True, check=True)
return float(result.stdout)
except (subprocess.CalledProcessError, FileNotFoundError) as e:
except Exception as e:
logger.error(f"ffprobe failed for {filepath}: {e}")
return None
def _get_frame_hash(self, filepath):
logger.debug(f"Running ffmpeg for frame hash of {filepath}")
def _get_frame_hash(self, filepath, algorithm='phash'):
tmp_frame_path = ""
try:
with tempfile.NamedTemporaryFile(suffix=".jpg", delete=False) as tmp_frame:
@@ -57,264 +99,390 @@ class DuplicatesManager:
command = ["ffmpeg"]
if self.settings.gpu_enabled:
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)}")
result = subprocess.run(
command,
capture_output=True,
check=True,
)
logger.debug(f"ffmpeg stdout: {result.stdout.strip()}")
logger.debug(f"ffmpeg stderr: {result.stderr.strip()}")
# Extract frame at 10s or 10%? Fixed 10s for now.
command.extend(["-i", filepath, "-ss", "00:00:10", "-vframes", "1", "-y", tmp_frame_path])
subprocess.run(command, capture_output=True, check=True)
if os.path.exists(tmp_frame_path):
logger.debug(f"Temporary frame file exists: {tmp_frame_path}, size: {os.path.getsize(tmp_frame_path)} bytes")
phash = imagehash.phash(Image.open(tmp_frame_path))
return str(phash)
else:
logger.warning(f"Temporary frame file was not created: {tmp_frame_path}")
return None
except (subprocess.CalledProcessError, FileNotFoundError) as 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
if os.path.exists(tmp_frame_path) and os.path.getsize(tmp_frame_path) > 0:
img = Image.open(tmp_frame_path)
if algorithm == 'ahash': h = imagehash.average_hash(img)
elif algorithm == 'dhash': h = imagehash.dhash(img)
else: h = imagehash.phash(img)
return str(h)
except Exception as e:
logger.error(f"Hash generation failed for {filepath}: {e}")
finally:
if os.path.exists(tmp_frame_path):
os.remove(tmp_frame_path)
return None
def _is_video_file(self, filename):
video_extensions = ['.mp4', '.mkv', '.avi', '.mov', '.wmv', '.flv', '.webm']
return any(filename.lower().endswith(ext) for ext in video_extensions)
def generate_contact_sheet(self, video_path):
"""
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()
# If in scene mode, check if we already have the scene data
if existing_video:
if existing_video.size == size:
logger.info(f"Skipping already processed and unaltered video: {filepath}")
return
if scan_type == 'scene' and not existing_video.contact_sheet_path:
if log_func: log_func(f"Updating {filename} with contact sheet")
# Continue to processing
elif existing_video.size == size:
return # Skip if unchanged
else:
logger.info(f"File {filepath} has altered size ({existing_video.size} -> {size}). Re-processing.")
self.db.delete(existing_video)
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:
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:
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
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:
if os.path.exists(progress_file):
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 cancel_check_func and cancel_check_func(): return
if not dirs_to_scan:
dirs_to_scan = [self.videos_root]
duration = self._get_video_duration(tmp_path)
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 os.path.exists(pause_file):
logger.info("Scan is paused. Waiting for resume signal...")
time.sleep(5)
if check_cancel(): return
current_path = dirs_to_scan.pop(0)
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}")
files_and_dirs = samba_manager.list_path(current_path)
if "error" in files_and_dirs:
logger.error(f"Failed to list path {current_path}: {files_and_dirs['error']}")
try:
log(f"Scanning directory: {current_path}")
items = samba_manager.list_path(current_path)
except Exception as e:
log(f"Error listing {current_path}: {e}")
continue
subdirs = []
for item in files_and_dirs:
while os.path.exists(pause_file):
logger.info("Scan is paused. Waiting for resume signal...")
time.sleep(5)
for item in items:
if check_cancel(): return
if item["is_directory"]:
subdirs.append(item["path"])
elif self._is_video_file(item["name"]):
self._process_video_file(samba_manager, item["path"], item["name"], item["size"])
if item['name'] in ['.', '..']: continue
if item['is_directory']:
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:
logger.error(f"An error occurred during video scan: {e}", exc_info=True)
return {"status": "Scan failed", "error": str(e)}
log(f"Scan failed: {e}")
with open(progress_file, 'w') as f:
json.dump({"status": "failed", "error": str(e)}, f)
def _hamming_distance(self, s1, s2):
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):
def find_duplicates(self, threshold=0.95, method='fast'):
report = models.DuplicateReport(status="running")
self.db.add(report)
self.db.commit()
videos = self.db.query(models.VideoFile).all()
groups = []
processed_videos = set()
processed_ids = set()
for video1, video2 in itertools.combinations(videos, 2):
if video1.id in processed_videos or video2.id in processed_videos:
continue
score = self._calculate_similarity(video1, video2)
if score >= threshold:
existing_group = None
for group in groups:
if video1.id in group["video_ids"] or video2.id in group["video_ids"]:
existing_group = group
break
for i in range(len(videos)):
if videos[i].id in processed_ids: continue
group = [videos[i]]
scores = []
for j in range(i + 1, len(videos)):
if videos[j].id in processed_ids: continue
if existing_group:
existing_group["video_ids"].add(video1.id)
existing_group["video_ids"].add(video2.id)
existing_group["scores"].append(score)
v1, v2 = videos[i], videos[j]
score = 0
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:
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)
processed_videos.add(video2.id)
for group_data in groups:
avg_score = sum(group_data["scores"]) / len(group_data["scores"])
db_group = models.DuplicateFileGroup(report_id=report.id, score=avg_score)
self.db.add(db_group)
self.db.commit()
for video_id in group_data["video_ids"]:
db_file = models.DuplicateFile(group_id=db_group.id, video_file_id=video_id)
self.db.add(db_file)
if score >= threshold:
group.append(v2)
scores.append(score)
processed_ids.add(v2.id)
if len(group) > 1:
processed_ids.add(videos[i].id)
avg_score = sum(scores) / len(scores)
db_group = models.DuplicateFileGroup(report_id=report.id, score=avg_score)
self.db.add(db_group)
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"
self.db.commit()
return {"report_id": report.id, "status": "completed"}
return {"report_id": report.id}
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):
report = self.db.query(models.DuplicateReport).filter(models.DuplicateReport.id == report_id).first()
if not report:
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,
})
def get_report(self, report_id):
report = self.db.query(models.DuplicateReport).filter_by(id=report_id).first()
if not report: return None
return {
"report_id": report.id,
"created_at": report.created_at,
res = {
"id": report.id,
"status": report.status,
"groups": groups,
"date": report.created_at,
"groups": []
}
def delete_files(self, filepaths: list[str]):
samba_manager = SambaManager(
self.settings.samba_server_ip,
"isolation", # Assuming all duplicates are in the isolation share
self.settings.samba_username,
self.settings.samba_password,
)
try:
for filepath in filepaths:
# 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()
for g in report.groups:
files = [{
"id": f.video_file.id,
"path": f.video_file.filepath,
"size": f.video_file.size,
"duration": f.video_file.duration,
"contact_sheet": f.video_file.contact_sheet_path
} for f in g.files]
res["groups"].append({"id": g.id, "score": g.score, "files": files})
return res
+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 sqlalchemy.orm import Session
from contextlib import asynccontextmanager
from . import models
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)
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 = [
"http://localhost:5173", # Assuming frontend runs on this port during development
"http://127.0.0.1:5173",
# Add your production frontend URL(s) here when deploying
]
@asynccontextmanager
async def lifespan(app: FastAPI):
reset_stuck_scans()
start_scheduler()
yield
scheduler.shutdown()
app = FastAPI(lifespan=lifespan)
origins = ["*"]
app.add_middleware(
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(duplicates.router, prefix="/duplicates", tags=["duplicates"])
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)
duration = Column(Float)
phash = Column(String)
contact_sheet_path = Column(String, nullable=True)
scene_phash = Column(String, nullable=True)
class DuplicateFileGroup(Base):
@@ -42,3 +44,15 @@ class DuplicateFile(Base):
video_file_id = Column(Integer, ForeignKey("video_files.id"))
group = relationship("DuplicateFileGroup", back_populates="files")
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 ..config import settings
from ..samba_manager import SambaManager
from ..tasks import task_organize_comics, task_move_comics, task_update_metadata, task_sort_by_artist
router = APIRouter()
def run_organize_comics_background():
app_settings = settings
manager = ComicsManager()
samba_manager = SambaManager(
app_settings.samba_server_ip,
"isolation",
app_settings.samba_username,
app_settings.samba_password,
class MoveRequest(BaseModel):
series_names: List[str]
destination_share: Optional[str] = "comics"
destination_path: Optional[str] = "/manga"
class MetadataUpdateRequest(BaseModel):
target_path: Optional[str] = "/comics/manga"
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")
def organize_comics(background_tasks: BackgroundTasks):
background_tasks.add_task(run_organize_comics_background)
return {"message": "Comics organization started in the background."}
def organize_comics():
task = task_organize_comics()
return {"message": f"Organization queued (Task ID: {task.id}). Check logs."}
def run_cleanup_toberead_background():
app_settings = settings
@router.get("/pending")
def get_pending_comics():
manager = ComicsManager()
samba_manager = SambaManager(
app_settings.samba_server_ip,
"isolation",
app_settings.samba_username,
app_settings.samba_password,
)
manager.cleanup_toberead(samba_manager)
samba_manager.close()
samba_manager = get_samba_manager("isolation")
try:
return manager.get_pending_comics(samba_manager)
finally:
samba_manager.close()
@router.delete("/cleanup_toberead")
def cleanup_toberead(background_tasks: BackgroundTasks):
background_tasks.add_task(run_cleanup_toberead_background)
return {"message": "Comics cleanup started in the background."}
@router.post("/move")
def move_comics(request: MoveRequest):
task = task_move_comics(request.series_names, request.destination_share, request.destination_path)
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 pydantic import BaseModel
from ..duplicates_manager import DuplicatesManager
from ..config import settings, Settings
from ..database import get_db, SessionLocal
from ..samba_manager import SambaManager
from .. import models
import os
import json
from typing import List
router = APIRouter()
class DeleteFilesRequest(BaseModel):
filepaths: List[str]
class ScanConfig(BaseModel):
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()
app_settings = Settings()
manager = DuplicatesManager(app_settings, db)
@@ -24,42 +35,72 @@ def run_scan_videos_background():
app_settings.samba_password,
)
try:
manager.scan_videos(samba_manager)
manager.scan_videos(samba_manager, paths, algorithm, scan_type)
finally:
samba_manager.close()
db.close()
@router.post("/scan")
def scan_videos(background_tasks: BackgroundTasks):
background_tasks.add_task(run_scan_videos_background)
return {"message": "Video scan started in the background."}
@router.post("/scan/start")
def start_scan(config: ScanConfig, background_tasks: BackgroundTasks):
background_tasks.add_task(run_scan_videos_background, config.paths, config.algorithm, config.scan_type)
return {"message": "Scan started"}
@router.post("/scan/pause")
def pause_scan():
with open("logs/duplicates_scan.pause", 'w') as f:
pass
return {"message": "Video scan paused."}
@router.post("/scan/cancel")
def cancel_scan():
try:
os.makedirs("logs", exist_ok=True)
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")
def resume_scan():
if os.path.exists("logs/duplicates_scan.pause"):
os.remove("logs/duplicates_scan.pause")
return {"message": "Video scan resumed."}
@router.get("/scan/log")
def get_scan_log(limit: int = 100):
log_file = "logs/duplicates_scan.log"
if not os.path.exists(log_file):
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")
def get_scan_status():
if os.path.exists("logs/duplicates_scan.pause"):
return {"status": "Paused"}
if os.path.exists("logs/duplicates_scan.progress") or os.path.exists("logs/duplicates_scan.state"):
return {"status": "Scanning"}
return {"status": "Idle"}
@router.get("/scan/progress")
def get_scan_progress():
path = "logs/duplicates_scan.json"
if os.path.exists(path):
try:
with open(path, 'r') as f:
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")
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)
result = manager.find_duplicates(threshold)
return result
return manager.find_duplicates(threshold, method)
@router.get("/reports")
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}")
def get_report(report_id: int, db: Session = Depends(get_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")
def delete_files(request: DeleteFilesRequest, db: Session = Depends(get_db)):
def delete_files(request: DeleteRequest, db: Session = Depends(get_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)
manager.close()
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,
"is_directory": f.isDirectory,
"size": f.file_size,
"path": full_item_path
"path": full_item_path,
"last_modified": f.last_write_time
})
return items
except Exception as e:
logger.error(f"Error listing path '{current_dir_path}' on share '{self.share_name}': {e}", exc_info=True)
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):
try:
@@ -63,6 +104,8 @@ class SambaManager:
self.conn.retrieveFile(self.share_name, path, file_obj)
return {"success": True}
except Exception as e:
if "Scan canceled by user" in str(e):
raise e
return {"error": str(e)}
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": "scanning", "current": "/videos/jav/incest/HUNTC/hhd800.com@HUNTC-328.mp4", "processed": 4309}
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
pillow
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",
"configHash": "939bcc17",
"hash": "95aedb35",
"configHash": "7be18a1c",
"lockfileHash": "b10062e0",
"browserHash": "e6aae6df",
"browserHash": "9f13f699",
"optimized": {
"vue": {
"src": "../../vue/dist/vue.runtime.esm-bundler.js",
"file": "vue.js",
"fileHash": "c9954258",
"fileHash": "8eb41ee8",
"needsInterop": false
},
"vue-router": {
"src": "../../vue-router/dist/vue-router.mjs",
"file": "vue-router.js",
"fileHash": "e764d986",
"fileHash": "1427789a",
"needsInterop": false
},
"vuetify": {
"src": "../../vuetify/lib/framework.js",
"file": "vuetify.js",
"fileHash": "369c86f0",
"fileHash": "8dbe3ca9",
"needsInterop": false
},
"vuetify/components": {
"src": "../../vuetify/lib/components/index.js",
"file": "vuetify_components.js",
"fileHash": "721553c0",
"fileHash": "c679badd",
"needsInterop": false
},
"vuetify/directives": {
"src": "../../vuetify/lib/directives/index.js",
"file": "vuetify_directives.js",
"fileHash": "cf4c59e7",
"fileHash": "1b05ee8f",
"needsInterop": false
},
"vuetify/iconsets/mdi": {
"src": "../../vuetify/lib/iconsets/mdi.js",
"file": "vuetify_iconsets_mdi.js",
"fileHash": "590e0963",
"fileHash": "af4fe77a",
"needsInterop": false
},
"vuetify/components/VApp": {
"src": "../../vuetify/lib/components/VApp/index.js",
"file": "vuetify_components_VApp.js",
"fileHash": "f6cb1574",
"fileHash": "67472430",
"needsInterop": false
},
"vuetify/components/VAppBar": {
"src": "../../vuetify/lib/components/VAppBar/index.js",
"file": "vuetify_components_VAppBar.js",
"fileHash": "bd33378b",
"fileHash": "1c27af94",
"needsInterop": false
},
"vuetify/components/VBtn": {
"src": "../../vuetify/lib/components/VBtn/index.js",
"file": "vuetify_components_VBtn.js",
"fileHash": "b2541520",
"fileHash": "9ff6101b",
"needsInterop": false
},
"vuetify/components/VDivider": {
"src": "../../vuetify/lib/components/VDivider/index.js",
"file": "vuetify_components_VDivider.js",
"fileHash": "1201295e",
"fileHash": "d9ee236f",
"needsInterop": false
},
"vuetify/components/VGrid": {
"src": "../../vuetify/lib/components/VGrid/index.js",
"file": "vuetify_components_VGrid.js",
"fileHash": "dec97038",
"fileHash": "74768afe",
"needsInterop": false
},
"vuetify/components/VList": {
"src": "../../vuetify/lib/components/VList/index.js",
"file": "vuetify_components_VList.js",
"fileHash": "92076a71",
"fileHash": "dc9b9e42",
"needsInterop": false
},
"vuetify/components/VMain": {
"src": "../../vuetify/lib/components/VMain/index.js",
"file": "vuetify_components_VMain.js",
"fileHash": "aea7930b",
"fileHash": "41fdc34f",
"needsInterop": false
},
"vuetify/components/VNavigationDrawer": {
"src": "../../vuetify/lib/components/VNavigationDrawer/index.js",
"file": "vuetify_components_VNavigationDrawer.js",
"fileHash": "fa7c5e03",
"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",
"fileHash": "e3294e44",
"needsInterop": false
},
"vuetify/components/VBreadcrumbs": {
"src": "../../vuetify/lib/components/VBreadcrumbs/index.js",
"file": "vuetify_components_VBreadcrumbs.js",
"fileHash": "b9da5690",
"fileHash": "cce8769a",
"needsInterop": false
},
"vuetify/components/VDataTable": {
"src": "../../vuetify/lib/components/VDataTable/index.js",
"file": "vuetify_components_VDataTable.js",
"fileHash": "d06d0545",
"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",
"vuetify/components/VCard": {
"src": "../../vuetify/lib/components/VCard/index.js",
"file": "vuetify_components_VCard.js",
"fileHash": "ea6e451a",
"needsInterop": false
},
"vuetify/components/VChip": {
"src": "../../vuetify/lib/components/VChip/index.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
},
"vuetify/components/VExpansionPanel": {
"src": "../../vuetify/lib/components/VExpansionPanel/index.js",
"file": "vuetify_components_VExpansionPanel.js",
"fileHash": "e8175e4b",
"fileHash": "13bd06bb",
"needsInterop": false
},
"vuetify/components/VSlider": {
"src": "../../vuetify/lib/components/VSlider/index.js",
"file": "vuetify_components_VSlider.js",
"fileHash": "70d54e33",
"fileHash": "e6b50146",
"needsInterop": false
},
"vuetify/components/transitions": {
"src": "../../vuetify/lib/components/transitions/index.js",
"file": "vuetify_components_transitions.js",
"fileHash": "1ae42e15",
"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",
"fileHash": "1f8b3903",
"needsInterop": false
},
"vuetify/components/VForm": {
"src": "../../vuetify/lib/components/VForm/index.js",
"file": "vuetify_components_VForm.js",
"fileHash": "6098d2ad",
"fileHash": "ae2d51af",
"needsInterop": false
},
"vuetify/components/VTextField": {
"src": "../../vuetify/lib/components/VTextField/index.js",
"file": "vuetify_components_VTextField.js",
"fileHash": "6c3b6c9f",
"needsInterop": false
},
"vuetify/components/VBtnToggle": {
"src": "../../vuetify/lib/components/VBtnToggle/index.js",
"file": "vuetify_components_VBtnToggle.js",
"fileHash": "92b719ca",
"vuetify/components/VSheet": {
"src": "../../vuetify/lib/components/VSheet/index.js",
"file": "vuetify_components_VSheet.js",
"fileHash": "61437271",
"needsInterop": false
},
"vuetify/components/VTextarea": {
"src": "../../vuetify/lib/components/VTextarea/index.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
}
},
"chunks": {
"chunk-KPWCGWEX": {
"file": "chunk-KPWCGWEX.js"
"chunk-244ON7Z6": {
"file": "chunk-244ON7Z6.js"
},
"chunk-5LZQZ6A5": {
"file": "chunk-5LZQZ6A5.js"
},
"chunk-F6LZGKE2": {
"file": "chunk-F6LZGKE2.js"
"chunk-T4XKRUDE": {
"file": "chunk-T4XKRUDE.js"
},
"chunk-4MMPKXON": {
"file": "chunk-4MMPKXON.js"
},
"chunk-LHMOJDYM": {
"file": "chunk-LHMOJDYM.js"
"chunk-F6LZGKE2": {
"file": "chunk-F6LZGKE2.js"
},
"chunk-TSTWIJCO": {
"file": "chunk-TSTWIJCO.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-LFKLAYDH": {
"file": "chunk-LFKLAYDH.js"
},
"chunk-RH43OVWP": {
"file": "chunk-RH43OVWP.js"
},
"chunk-PEUMXYT2": {
"file": "chunk-PEUMXYT2.js"
"chunk-QMLWKRPM": {
"file": "chunk-QMLWKRPM.js"
},
"chunk-4HCXUCLO": {
"file": "chunk-4HCXUCLO.js"
"chunk-SCY7DFFY": {
"file": "chunk-SCY7DFFY.js"
},
"chunk-ERI3RTQS": {
"file": "chunk-ERI3RTQS.js"
"chunk-WGWEQK64": {
"file": "chunk-WGWEQK64.js"
},
"chunk-VSEC5QGQ": {
"file": "chunk-VSEC5QGQ.js"
"chunk-7M5DDQ46": {
"file": "chunk-7M5DDQ46.js"
},
"chunk-BYBKODOB": {
"file": "chunk-BYBKODOB.js"
"chunk-3NOJ7Z44": {
"file": "chunk-3NOJ7Z44.js"
},
"chunk-SHBPUR7N": {
"file": "chunk-SHBPUR7N.js"
"chunk-FCTFWI5H": {
"file": "chunk-FCTFWI5H.js"
},
"chunk-HGIC64KW": {
"file": "chunk-HGIC64KW.js"
"chunk-RB6DAYHX": {
"file": "chunk-RB6DAYHX.js"
},
"chunk-BXEVD6W6": {
"file": "chunk-BXEVD6W6.js"
"chunk-QV3QXWPM": {
"file": "chunk-QV3QXWPM.js"
},
"chunk-LPZZJ62X": {
"file": "chunk-LPZZJ62X.js"
"chunk-IZDIYS2S": {
"file": "chunk-IZDIYS2S.js"
},
"chunk-UIXKNP5E": {
"file": "chunk-UIXKNP5E.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-6MO6MKTY": {
"file": "chunk-6MO6MKTY.js"
},
"chunk-IZQZFEAL": {
"file": "chunk-IZQZFEAL.js"
},
"chunk-J7Q2NMV7": {
"file": "chunk-J7Q2NMV7.js"
"chunk-6HLLVLOR": {
"file": "chunk-6HLLVLOR.js"
},
"chunk-4VABFUPG": {
"file": "chunk-4VABFUPG.js"
"chunk-RWSHRKUK": {
"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": {
"file": "chunk-GAYXO4FM.js"
@@ -369,11 +396,83 @@
"chunk-B4JFJDWM": {
"file": "chunk-B4JFJDWM.js"
},
"chunk-U2DSRSME": {
"file": "chunk-U2DSRSME.js"
"chunk-WGYWYQOA": {
"file": "chunk-WGYWYQOA.js"
},
"chunk-FVDWV4NO": {
"file": "chunk-FVDWV4NO.js"
"chunk-NJV3GUE4": {
"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": {
"file": "chunk-AJ7MUB7M.js"
@@ -384,69 +483,36 @@
"chunk-TM3MIDYC": {
"file": "chunk-TM3MIDYC.js"
},
"chunk-CMZ4UYUX": {
"file": "chunk-CMZ4UYUX.js"
"chunk-4XZHKYGH": {
"file": "chunk-4XZHKYGH.js"
},
"chunk-PIT7IT55": {
"file": "chunk-PIT7IT55.js"
},
"chunk-VH7QIDM4": {
"file": "chunk-VH7QIDM4.js"
},
"chunk-AQK3CYVW": {
"file": "chunk-AQK3CYVW.js"
},
"chunk-PAT7NP2Z": {
"file": "chunk-PAT7NP2Z.js"
},
"chunk-2HZIACSB": {
"file": "chunk-2HZIACSB.js"
},
"chunk-WGYWYQOA": {
"file": "chunk-WGYWYQOA.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-OJDZBU5D": {
"file": "chunk-OJDZBU5D.js"
},
"chunk-573D4TYC": {
"file": "chunk-573D4TYC.js"
},
"chunk-GUNW7S2L": {
"file": "chunk-GUNW7S2L.js"
"chunk-DS4J67XG": {
"file": "chunk-DS4J67XG.js"
},
"chunk-5ONGSJQ2": {
"file": "chunk-5ONGSJQ2.js"
},
"chunk-CAGZXMOS": {
"file": "chunk-CAGZXMOS.js"
"chunk-BHV5Z34M": {
"file": "chunk-BHV5Z34M.js"
},
"chunk-6EZKHCRW": {
"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": {
"file": "chunk-XDSAO7OT.js"
},
@@ -456,12 +522,6 @@
"chunk-KA77II4V": {
"file": "chunk-KA77II4V.js"
},
"chunk-WXMECWZN": {
"file": "chunk-WXMECWZN.js"
},
"chunk-BHV5Z34M": {
"file": "chunk-BHV5Z34M.js"
},
"chunk-3DSR7NCB": {
"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 {
VOverlay,
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
keys
} from "./chunk-LZ5T2ERG.js";
import {
computed,
createVNode,
mergeProps,
ref,
toRef,
useId
} from "./chunk-2MKFL3BX.js";
// node_modules/vuetify/lib/directives/resize/index.js
function mounted(el, binding) {
@@ -163,95 +138,10 @@ var 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 {
Resize,
resize_default,
Touch,
touch_default,
VTooltip
touch_default
};
//# 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 {
makePositionProps,
usePosition
} from "./chunk-HOEZWRBG.js";
import {
VAvatar
} from "./chunk-Z3677HMS.js";
import {
genOverlays,
makeVariantProps,
useVariant
} from "./chunk-ADXDXPPR.js";
import {
VImg
} from "./chunk-6CRS5HI4.js";
import {
makeBorderProps,
useBorder
@@ -25,7 +25,7 @@ import {
LoaderSlot,
makeLoaderProps,
useLoader
} from "./chunk-U2DSRSME.js";
} from "./chunk-UXZ55AO7.js";
import {
makeLocationProps,
useLocation
@@ -36,31 +36,31 @@ import {
} from "./chunk-TM3MIDYC.js";
import {
VIcon
} from "./chunk-CMZ4UYUX.js";
import {
VDefaultsProvider
} from "./chunk-VH7QIDM4.js";
} from "./chunk-4XZHKYGH.js";
import {
makeRoundedProps,
useRounded
} from "./chunk-AQK3CYVW.js";
import {
VDefaultsProvider
} from "./chunk-PAT7NP2Z.js";
import {
makeTagProps
} from "./chunk-2HZIACSB.js";
import {
ripple_default
} from "./chunk-5BPWKCNA.js";
IconValue
} from "./chunk-OJDZBU5D.js";
import {
makeRouterProps,
useLink
} from "./chunk-573D4TYC.js";
import {
ripple_default
} from "./chunk-DS4J67XG.js";
import {
makeDimensionProps,
useDimension
} from "./chunk-6EZKHCRW.js";
import {
IconValue
} from "./chunk-OJDZBU5D.js";
import {
makeThemeProps,
provideTheme
@@ -87,7 +87,7 @@ import {
} from "./chunk-2MKFL3BX.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
var makeVCardActionsProps = propsFactory({
@@ -441,4 +441,4 @@ export {
VCardText,
VCard
};
//# sourceMappingURL=chunk-MPVSDM4V.js.map
//# sourceMappingURL=chunk-3NOJ7Z44.js.map
@@ -23,7 +23,7 @@ import {
} from "./chunk-2MKFL3BX.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({
...makeComponentProps(),
...omit(makeLayoutProps(), ["fullHeight"]),
@@ -67,4 +67,4 @@ var VApp = genericComponent()({
export {
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";
// 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({
color: String,
disabled: Boolean,
@@ -106,4 +106,4 @@ var VIcon = genericComponent()({
export {
VIcon
};
//# sourceMappingURL=chunk-CMZ4UYUX.js.map
//# sourceMappingURL=chunk-4XZHKYGH.js.map
@@ -44,7 +44,7 @@ import {
} from "./chunk-2MKFL3BX.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({
baseColor: String,
divided: Boolean,
@@ -105,7 +105,7 @@ var VBtnGroup = genericComponent()({
});
// 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 makeVBtnToggleProps = propsFactory({
...makeVBtnGroupProps(),
@@ -157,4 +157,4 @@ export {
VBtnToggleSymbol,
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,
VField,
makeVFieldProps
} from "./chunk-J7Q2NMV7.js";
} from "./chunk-ITO3JWXJ.js";
import {
VInput,
makeVInputProps,
useFocus
} from "./chunk-4VABFUPG.js";
import {
forwardRefs
} from "./chunk-GUNW7S2L.js";
} from "./chunk-IK67YVHF.js";
import {
intersect_default
} from "./chunk-5ONGSJQ2.js";
import {
forwardRefs
} from "./chunk-GUNW7S2L.js";
import {
useProxiedModel
} from "./chunk-KA77II4V.js";
@@ -44,7 +44,7 @@ import {
} from "./chunk-2MKFL3BX.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 makeVTextFieldProps = propsFactory({
autofocus: Boolean,
@@ -267,4 +267,4 @@ export {
makeVTextFieldProps,
VTextField
};
//# sourceMappingURL=chunk-VSEC5QGQ.js.map
//# sourceMappingURL=chunk-6MO6MKTY.js.map
@@ -12,6 +12,10 @@ import {
import {
makeTagProps
} from "./chunk-2HZIACSB.js";
import {
useBackgroundColor,
useTextColor
} from "./chunk-BHV5Z34M.js";
import {
useResizeObserver
} from "./chunk-XDSAO7OT.js";
@@ -22,10 +26,6 @@ import {
useProxiedModel,
useToggleScope
} from "./chunk-KA77II4V.js";
import {
useBackgroundColor,
useTextColor
} from "./chunk-BHV5Z34M.js";
import {
makeThemeProps,
provideTheme
@@ -53,7 +53,7 @@ import {
} from "./chunk-2MKFL3BX.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
var makeChunksProps = propsFactory({
@@ -318,4 +318,4 @@ var VProgressLinear = genericComponent()({
export {
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 {
makeTagProps
} from "./chunk-2HZIACSB.js";
import {
useBackgroundColor
} from "./chunk-BHV5Z34M.js";
import {
makeDimensionProps,
useDimension
} from "./chunk-6EZKHCRW.js";
import {
useBackgroundColor
} from "./chunk-BHV5Z34M.js";
import {
makeThemeProps,
provideTheme
@@ -45,7 +45,7 @@ import {
} from "./chunk-2MKFL3BX.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({
color: String,
...makeBorderProps(),
@@ -102,4 +102,4 @@ export {
makeVSheetProps,
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 {
makePositionProps,
usePosition
@@ -9,31 +23,17 @@ import {
} from "./chunk-ADXDXPPR.js";
import {
VProgressLinear
} from "./chunk-FVDWV4NO.js";
} from "./chunk-6RFSM2OD.js";
import {
makeLocationProps
} from "./chunk-KTTVT6V2.js";
import {
VDefaultsProvider
} from "./chunk-VH7QIDM4.js";
import {
makeRoundedProps,
useRounded
} from "./chunk-AQK3CYVW.js";
import {
VOverlay,
makeVOverlayProps
} from "./chunk-TS6Y6OVT.js";
import {
useScopeId
} from "./chunk-BVUPSGRF.js";
import {
forwardRefs
} from "./chunk-GUNW7S2L.js";
import {
VuetifyLayoutKey,
useLayout
} from "./chunk-KQXZAEJP.js";
VDefaultsProvider
} from "./chunk-PAT7NP2Z.js";
import {
useProxiedModel,
useToggleScope
@@ -65,7 +65,7 @@ import {
} from "./chunk-2MKFL3BX.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) {
const time = shallowRef(milliseconds());
let timer = -1;
@@ -275,4 +275,4 @@ export {
makeVSnackbarProps,
VSnackbar
};
//# sourceMappingURL=chunk-YDVSSFCH.js.map
//# sourceMappingURL=chunk-CCNWGDBL.js.map
@@ -9,21 +9,24 @@ import {
useFocusTrap,
useScopeId
} from "./chunk-BVUPSGRF.js";
import {
useBackButton,
useRouter
} from "./chunk-573D4TYC.js";
import {
MaybeTransition,
makeTransitionProps
} from "./chunk-CAGZXMOS.js";
import {
useDisplay
} from "./chunk-N4LJHDYD.js";
import {
useBackButton,
useRouter
} from "./chunk-573D4TYC.js";
import {
useBackgroundColor
} from "./chunk-BHV5Z34M.js";
import {
makeDimensionProps,
useDimension
} from "./chunk-6EZKHCRW.js";
import {
useDisplay
} from "./chunk-N4LJHDYD.js";
import {
useRtl
} from "./chunk-ENTLCOCZ.js";
@@ -31,9 +34,6 @@ import {
useProxiedModel,
useToggleScope
} from "./chunk-KA77II4V.js";
import {
useBackgroundColor
} from "./chunk-BHV5Z34M.js";
import {
makeThemeProps,
provideTheme
@@ -103,79 +103,8 @@ import {
withDirectives
} 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
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
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
function Scrim(props) {
const {
@@ -1412,10 +1412,10 @@ var VOverlay = genericComponent()({
});
export {
ClickOutside,
VMenuSymbol,
useHydration,
ClickOutside,
makeVOverlayProps,
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 {
makeTagProps
} from "./chunk-2HZIACSB.js";
@@ -5,9 +8,6 @@ import {
makeDimensionProps,
useDimension
} from "./chunk-6EZKHCRW.js";
import {
breakpoints
} from "./chunk-N4LJHDYD.js";
import {
useRtl
} from "./chunk-ENTLCOCZ.js";
@@ -28,7 +28,7 @@ import {
} from "./chunk-2MKFL3BX.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({
fluid: {
type: Boolean,
@@ -62,7 +62,7 @@ var VContainer = genericComponent()({
});
// 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 = (() => {
return breakpoints.reduce((props, val) => {
props[val] = {
@@ -176,7 +176,7 @@ var VCol = genericComponent()({
});
// 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 SPACE = ["space-between", "space-around", "space-evenly"];
function makeRowProps(prefix, def) {
@@ -287,7 +287,7 @@ var VRow = genericComponent()({
});
// 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");
export {
@@ -296,4 +296,4 @@ export {
VRow,
VSpacer
};
//# sourceMappingURL=chunk-UIXKNP5E.js.map
//# sourceMappingURL=chunk-CMTCIV56.js.map
@@ -3,7 +3,7 @@ import {
} from "./chunk-LZ5T2ERG.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 DELAY_RIPPLE = 80;
function transform(el, value) {
@@ -287,4 +287,4 @@ export {
Ripple,
ripple_default
};
//# sourceMappingURL=chunk-5BPWKCNA.js.map
//# sourceMappingURL=chunk-DS4J67XG.js.map
@@ -1,16 +1,16 @@
import {
VChip
} from "./chunk-SHBPUR7N.js";
import {
VCounter,
VField,
makeVFieldProps
} from "./chunk-J7Q2NMV7.js";
} from "./chunk-ITO3JWXJ.js";
import {
VChip
} from "./chunk-PLNIG6WP.js";
import {
VInput,
makeVInputProps,
useFocus
} from "./chunk-4VABFUPG.js";
} from "./chunk-IK67YVHF.js";
import {
forwardRefs
} from "./chunk-GUNW7S2L.js";
@@ -44,7 +44,7 @@ import {
} from "./chunk-2MKFL3BX.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
function useFileDrop() {
@@ -423,4 +423,4 @@ var VFileInput = genericComponent()({
export {
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 {
VSelect,
VVirtualScrollItem,
makeVirtualProps,
useVirtual
} from "./chunk-ERI3RTQS.js";
} from "./chunk-IZDIYS2S.js";
import {
VCheckboxBtn
} from "./chunk-BYBKODOB.js";
} from "./chunk-6HLLVLOR.js";
import {
VChip
} from "./chunk-SHBPUR7N.js";
} from "./chunk-PLNIG6WP.js";
import {
VDivider
} from "./chunk-X3WY3J75.js";
import {
makeDisplayProps,
useDisplay
} from "./chunk-N4LJHDYD.js";
import {
VBtn
} from "./chunk-QC7VWQER.js";
} from "./chunk-UQALH6SL.js";
import {
makeVariantProps
} from "./chunk-ADXDXPPR.js";
@@ -26,14 +37,14 @@ import {
LoaderSlot,
makeLoaderProps,
useLoader
} from "./chunk-U2DSRSME.js";
} from "./chunk-UXZ55AO7.js";
import {
makeDensityProps,
useDensity
} from "./chunk-TM3MIDYC.js";
import {
VIcon
} from "./chunk-CMZ4UYUX.js";
} from "./chunk-4XZHKYGH.js";
import {
makeSizeProps
} from "./chunk-PIT7IT55.js";
@@ -43,13 +54,12 @@ import {
import {
makeTagProps
} from "./chunk-2HZIACSB.js";
import {
makeDisplayProps,
useDisplay
} from "./chunk-N4LJHDYD.js";
import {
IconValue
} from "./chunk-OJDZBU5D.js";
import {
useBackgroundColor
} from "./chunk-BHV5Z34M.js";
import {
useResizeObserver
} from "./chunk-XDSAO7OT.js";
@@ -60,12 +70,6 @@ import {
import {
useProxiedModel
} from "./chunk-KA77II4V.js";
import {
VDivider
} from "./chunk-WXMECWZN.js";
import {
useBackgroundColor
} from "./chunk-BHV5Z34M.js";
import {
makeThemeProps,
provideTheme
@@ -116,20 +120,19 @@ import {
toRef,
toRefs,
toValue,
unref,
watch,
watchEffect,
withModifiers
} from "./chunk-2MKFL3BX.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
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
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
function useRefs() {
@@ -2243,7 +2246,7 @@ var VDataTableRows = genericComponent()({
});
// 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({
fixedHeader: 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
var makeDataTableProps = propsFactory({
...makeVDataTableRowsProps(),
@@ -3157,9 +3006,6 @@ var VDataTableServer = genericComponent()({
});
export {
makeFilterProps,
useFilter,
highlightResult,
makeDataTableExpandProps,
provideExpanded,
makeDataTableGroupProps,
@@ -3186,4 +3032,4 @@ export {
VDataTableVirtual,
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 {
VImg
} from "./chunk-6CRS5HI4.js";
} from "./chunk-TTIGZHWR.js";
import {
VExpandTransition
} from "./chunk-B4JFJDWM.js";
import {
makeBorderProps,
useBorder
@@ -9,25 +12,22 @@ import {
makeElevationProps,
useElevation
} from "./chunk-YJD2PNP6.js";
import {
VExpandTransition
} from "./chunk-B4JFJDWM.js";
import {
VDefaultsProvider
} from "./chunk-VH7QIDM4.js";
import {
makeRoundedProps,
useRounded
} from "./chunk-AQK3CYVW.js";
import {
VDefaultsProvider
} from "./chunk-PAT7NP2Z.js";
import {
makeTagProps
} from "./chunk-2HZIACSB.js";
import {
useRtl
} from "./chunk-ENTLCOCZ.js";
import {
useBackgroundColor
} from "./chunk-BHV5Z34M.js";
import {
useRtl
} from "./chunk-ENTLCOCZ.js";
import {
makeThemeProps,
provideTheme
@@ -78,7 +78,7 @@ var VToolbarTitle = genericComponent()({
});
// 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 makeVToolbarProps = propsFactory({
absolute: Boolean,
@@ -236,4 +236,4 @@ export {
makeVToolbarProps,
VToolbar
};
//# sourceMappingURL=chunk-LPZZJ62X.js.map
//# sourceMappingURL=chunk-H2QFYTJQ.js.map
@@ -4,24 +4,27 @@ import {
import {
VSlideYTransition
} from "./chunk-B4JFJDWM.js";
import {
MaybeTransition,
makeTransitionProps
} from "./chunk-CAGZXMOS.js";
import {
makeDensityProps,
useDensity
} from "./chunk-TM3MIDYC.js";
import {
VIcon
} from "./chunk-CMZ4UYUX.js";
} from "./chunk-4XZHKYGH.js";
import {
MaybeTransition,
makeTransitionProps
} from "./chunk-CAGZXMOS.js";
IconValue
} from "./chunk-OJDZBU5D.js";
import {
useTextColor
} from "./chunk-BHV5Z34M.js";
import {
makeDimensionProps,
useDimension
} from "./chunk-6EZKHCRW.js";
import {
IconValue
} from "./chunk-OJDZBU5D.js";
import {
useLocale,
useRtl
@@ -30,9 +33,6 @@ import {
useProxiedModel,
useToggleScope
} from "./chunk-KA77II4V.js";
import {
useTextColor
} from "./chunk-BHV5Z34M.js";
import {
makeThemeProps,
provideTheme
@@ -69,34 +69,8 @@ import {
watch
} 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
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({
active: Boolean,
color: String,
@@ -143,7 +117,7 @@ var VMessages = genericComponent()({
});
// 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
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 {
VLabel,
useInputIcon,
VMessages,
makeFocusProps,
useFocus,
VMessages,
makeValidationProps,
useValidation,
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,
useFocus,
useInputIcon
} from "./chunk-4VABFUPG.js";
} from "./chunk-IK67YVHF.js";
import {
VExpandXTransition,
VSlideYTransition
} from "./chunk-B4JFJDWM.js";
import {
MaybeTransition,
makeTransitionProps
} from "./chunk-CAGZXMOS.js";
import {
LoaderSlot,
makeLoaderProps,
useLoader
} from "./chunk-U2DSRSME.js";
import {
VDefaultsProvider
} from "./chunk-VH7QIDM4.js";
} from "./chunk-UXZ55AO7.js";
import {
makeRoundedProps,
useRounded
} from "./chunk-AQK3CYVW.js";
import {
MaybeTransition,
makeTransitionProps
} from "./chunk-CAGZXMOS.js";
VDefaultsProvider
} from "./chunk-PAT7NP2Z.js";
import {
IconValue
} from "./chunk-OJDZBU5D.js";
import {
useRtl
} from "./chunk-ENTLCOCZ.js";
import {
useBackgroundColor,
useTextColor
} from "./chunk-BHV5Z34M.js";
import {
useRtl
} from "./chunk-ENTLCOCZ.js";
import {
makeThemeProps,
provideTheme
@@ -67,7 +67,7 @@ import {
} from "./chunk-2MKFL3BX.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({
active: Boolean,
disabled: Boolean,
@@ -135,7 +135,7 @@ var VFieldLabel = genericComponent()({
});
// 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 makeVFieldProps = propsFactory({
appendInnerIcon: IconValue,
@@ -452,4 +452,4 @@ export {
makeVFieldProps,
VField
};
//# sourceMappingURL=chunk-J7Q2NMV7.js.map
//# sourceMappingURL=chunk-ITO3JWXJ.js.map
@@ -1,60 +1,63 @@
import {
VTextField,
makeVTextFieldProps
} from "./chunk-VSEC5QGQ.js";
} from "./chunk-6MO6MKTY.js";
import {
useAutocomplete
} from "./chunk-IZQZFEAL.js";
import {
VCheckboxBtn
} from "./chunk-BYBKODOB.js";
} from "./chunk-6HLLVLOR.js";
import {
VChip
} from "./chunk-SHBPUR7N.js";
} from "./chunk-PLNIG6WP.js";
import {
VMenu
} from "./chunk-HGIC64KW.js";
} from "./chunk-LPDOFVDK.js";
import {
VList,
VListItem,
VListSubheader,
makeItemsProps,
useItems
} from "./chunk-HV7P7R7J.js";
} from "./chunk-MQSMF5CI.js";
import {
VAvatar
} from "./chunk-Z3677HMS.js";
} from "./chunk-SXI5CS7S.js";
import {
useAutocomplete
} from "./chunk-IZQZFEAL.js";
VDivider
} from "./chunk-X3WY3J75.js";
import {
useInputIcon
} from "./chunk-4VABFUPG.js";
} from "./chunk-IK67YVHF.js";
import {
useForm
} from "./chunk-GAYXO4FM.js";
import {
VDialogTransition
} from "./chunk-B4JFJDWM.js";
import {
VIcon
} from "./chunk-CMZ4UYUX.js";
import {
VDefaultsProvider
} from "./chunk-VH7QIDM4.js";
import {
forwardRefs
} from "./chunk-GUNW7S2L.js";
import {
makeTransitionProps
} from "./chunk-CAGZXMOS.js";
import {
makeDimensionProps,
useDimension
} from "./chunk-6EZKHCRW.js";
import {
useDisplay
} from "./chunk-N4LJHDYD.js";
import {
VIcon
} from "./chunk-4XZHKYGH.js";
import {
VDefaultsProvider
} from "./chunk-PAT7NP2Z.js";
import {
IconValue
} from "./chunk-OJDZBU5D.js";
import {
makeDimensionProps,
useDimension
} from "./chunk-6EZKHCRW.js";
import {
useResizeObserver
} from "./chunk-XDSAO7OT.js";
@@ -65,9 +68,6 @@ import {
useProxiedModel,
useToggleScope
} from "./chunk-KA77II4V.js";
import {
VDivider
} from "./chunk-WXMECWZN.js";
import {
IN_BROWSER,
camelizeProps,
@@ -109,11 +109,8 @@ import {
watchEffect
} 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
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
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
function useScrolling(listRef, textFieldRef) {
const isScrolling = shallowRef(false);
@@ -1079,4 +1079,4 @@ export {
makeSelectProps,
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 {
VInput,
VLabel,
makeVInputProps,
useFocus
} from "./chunk-4VABFUPG.js";
VLabel
} from "./chunk-IK67YVHF.js";
import {
makeDensityProps,
useDensity
} from "./chunk-TM3MIDYC.js";
import {
VIcon
} from "./chunk-CMZ4UYUX.js";
import {
ripple_default
} from "./chunk-5BPWKCNA.js";
import {
forwardRefs
} from "./chunk-GUNW7S2L.js";
} from "./chunk-4XZHKYGH.js";
import {
IconValue
} from "./chunk-OJDZBU5D.js";
import {
useProxiedModel
} from "./chunk-KA77II4V.js";
ripple_default
} from "./chunk-DS4J67XG.js";
import {
useBackgroundColor,
useTextColor
} from "./chunk-BHV5Z34M.js";
import {
useProxiedModel
} from "./chunk-KA77II4V.js";
import {
makeThemeProps
} from "./chunk-3DSR7NCB.js";
@@ -36,7 +30,6 @@ import {
genericComponent,
makeComponentProps,
matchesSelector,
omit,
propsFactory,
provideDefaults,
useRender,
@@ -61,14 +54,8 @@ import {
withDirectives
} 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
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 makeSelectionControlGroupProps = propsFactory({
color: String,
@@ -167,6 +154,7 @@ var VSelectionControlGroup = genericComponent()({
});
// 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({
label: 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 {
makeSelectionControlGroupProps,
VSelectionControlGroup,
makeVSelectionControlProps,
VSelectionControl,
VCheckboxBtn,
VCheckbox
VSelectionControl
};
//# 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 {
VDialogTransition
} from "./chunk-B4JFJDWM.js";
import {
VDefaultsProvider
} from "./chunk-VH7QIDM4.js";
import {
VMenuSymbol,
VOverlay,
makeVOverlayProps
} from "./chunk-TS6Y6OVT.js";
} from "./chunk-CL6G3ICF.js";
import {
useScopeId
} from "./chunk-BVUPSGRF.js";
import {
forwardRefs
} from "./chunk-GUNW7S2L.js";
import {
VDefaultsProvider
} from "./chunk-PAT7NP2Z.js";
import {
useRtl
} from "./chunk-ENTLCOCZ.js";
@@ -47,7 +47,7 @@ import {
} from "./chunk-2MKFL3BX.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({
// TODO
// disableKeys: Boolean,
@@ -203,4 +203,4 @@ export {
makeVMenuProps,
VMenu
};
//# sourceMappingURL=chunk-HGIC64KW.js.map
//# sourceMappingURL=chunk-LPDOFVDK.js.map
@@ -8,12 +8,12 @@ import {
import {
makeTagProps
} from "./chunk-2HZIACSB.js";
import {
useResizeObserver
} from "./chunk-XDSAO7OT.js";
import {
useTextColor
} from "./chunk-BHV5Z34M.js";
import {
useResizeObserver
} from "./chunk-XDSAO7OT.js";
import {
makeThemeProps,
provideTheme
@@ -39,7 +39,7 @@ import {
} from "./chunk-2MKFL3BX.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({
bgColor: String,
color: String,
@@ -168,4 +168,4 @@ var VProgressCircular = genericComponent()({
export {
VProgressCircular
};
//# sourceMappingURL=chunk-ET5GCC5W.js.map
//# sourceMappingURL=chunk-MH76ZJYE.js.map
@@ -1,14 +1,23 @@
import {
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 {
genOverlays,
makeVariantProps,
useVariant
} from "./chunk-ADXDXPPR.js";
import {
useSsrBoot
} from "./chunk-PCKQGDLK.js";
import {
makeBorderProps,
useBorder
@@ -17,53 +26,44 @@ import {
makeElevationProps,
useElevation
} from "./chunk-YJD2PNP6.js";
import {
VExpandTransition
} from "./chunk-B4JFJDWM.js";
import {
makeDensityProps,
useDensity
} from "./chunk-TM3MIDYC.js";
import {
VIcon
} from "./chunk-CMZ4UYUX.js";
import {
VDefaultsProvider
} from "./chunk-VH7QIDM4.js";
} from "./chunk-4XZHKYGH.js";
import {
makeRoundedProps,
useRounded
} from "./chunk-AQK3CYVW.js";
import {
VDefaultsProvider
} from "./chunk-PAT7NP2Z.js";
import {
makeTagProps
} from "./chunk-2HZIACSB.js";
import {
ripple_default
} from "./chunk-5BPWKCNA.js";
IconValue
} from "./chunk-OJDZBU5D.js";
import {
makeRouterProps,
useLink
} from "./chunk-573D4TYC.js";
import {
MaybeTransition
} from "./chunk-CAGZXMOS.js";
ripple_default
} from "./chunk-DS4J67XG.js";
import {
useBackgroundColor,
useTextColor
} from "./chunk-BHV5Z34M.js";
import {
makeDimensionProps,
useDimension
} from "./chunk-6EZKHCRW.js";
import {
IconValue
} from "./chunk-OJDZBU5D.js";
import {
useProxiedModel
} from "./chunk-KA77II4V.js";
import {
VDivider
} from "./chunk-WXMECWZN.js";
import {
useBackgroundColor,
useTextColor
} from "./chunk-BHV5Z34M.js";
import {
makeThemeProps,
provideTheme
@@ -116,7 +116,7 @@ import {
} from "./chunk-2MKFL3BX.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
var ListKey = /* @__PURE__ */ Symbol.for("vuetify:list");
@@ -1015,7 +1015,7 @@ var VListGroup = genericComponent()({
});
// 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
var makeVListItemSubtitleProps = propsFactory({
@@ -1893,4 +1893,4 @@ export {
VListItemAction,
VListItemMedia
};
//# sourceMappingURL=chunk-HV7P7R7J.js.map
//# sourceMappingURL=chunk-MQSMF5CI.js.map
@@ -3,11 +3,7 @@ import {
VToolbarTitle,
makeVToolbarProps,
makeVToolbarTitleProps
} from "./chunk-LPZZJ62X.js";
import {
VBtn,
makeVBtnProps
} from "./chunk-QC7VWQER.js";
} from "./chunk-H2QFYTJQ.js";
import {
useSsrBoot
} from "./chunk-PCKQGDLK.js";
@@ -15,6 +11,10 @@ import {
makeLayoutItemProps,
useLayoutItem
} from "./chunk-KQXZAEJP.js";
import {
VBtn,
makeVBtnProps
} from "./chunk-UQALH6SL.js";
import {
useProxiedModel,
useToggleScope
@@ -41,7 +41,7 @@ import {
} from "./chunk-2MKFL3BX.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
var makeScrollProps = propsFactory({
@@ -357,4 +357,4 @@ export {
VAppBarNavIcon,
VAppBarTitle
};
//# sourceMappingURL=chunk-BXEVD6W6.js.map
//# sourceMappingURL=chunk-P6EQK5CJ.js.map
@@ -42,4 +42,4 @@ var VDefaultsProvider = genericComponent(false)({
export {
VDefaultsProvider
};
//# sourceMappingURL=chunk-VH7QIDM4.js.map
//# sourceMappingURL=chunk-PAT7NP2Z.js.map
@@ -1,12 +1,23 @@
import {
VAvatar
} from "./chunk-SXI5CS7S.js";
import {
VExpandXTransition,
VFadeTransition
} from "./chunk-B4JFJDWM.js";
import {
useGoTo
} from "./chunk-NJV3GUE4.js";
import {
makeDisplayProps,
useDisplay
} from "./chunk-N4LJHDYD.js";
import {
makeGroupItemProps,
makeGroupProps,
useGroup,
useGroupItem
} from "./chunk-W6DJL42A.js";
import {
VAvatar
} from "./chunk-Z3677HMS.js";
import {
genOverlays,
makeVariantProps,
@@ -20,48 +31,37 @@ import {
makeElevationProps,
useElevation
} from "./chunk-YJD2PNP6.js";
import {
VExpandXTransition,
VFadeTransition
} from "./chunk-B4JFJDWM.js";
import {
makeDensityProps,
useDensity
} from "./chunk-TM3MIDYC.js";
import {
VIcon
} from "./chunk-CMZ4UYUX.js";
} from "./chunk-4XZHKYGH.js";
import {
makeSizeProps,
useSize
} from "./chunk-PIT7IT55.js";
import {
VDefaultsProvider
} from "./chunk-VH7QIDM4.js";
import {
makeRoundedProps,
useRounded
} from "./chunk-AQK3CYVW.js";
import {
VDefaultsProvider
} from "./chunk-PAT7NP2Z.js";
import {
makeTagProps
} from "./chunk-2HZIACSB.js";
import {
useGoTo
} from "./chunk-NJV3GUE4.js";
import {
ripple_default
} from "./chunk-5BPWKCNA.js";
IconValue
} from "./chunk-OJDZBU5D.js";
import {
makeRouterProps,
useLink
} from "./chunk-573D4TYC.js";
import {
makeDisplayProps,
useDisplay
} from "./chunk-N4LJHDYD.js";
import {
IconValue
} from "./chunk-OJDZBU5D.js";
ripple_default
} from "./chunk-DS4J67XG.js";
import {
useResizeObserver
} from "./chunk-XDSAO7OT.js";
@@ -104,13 +104,13 @@ import {
} from "./chunk-2MKFL3BX.js";
// node_modules/vuetify/lib/components/VChip/VChip.js
import "/home/david/Documents/vscode/personal_development/ServerManagerWebApp/frontend/node_modules/vuetify/lib/components/VChip/VChip.css";
import "/home/david/gitea_repo/personal_development/ServerManagerWebApp/frontend/node_modules/vuetify/lib/components/VChip/VChip.css";
// node_modules/vuetify/lib/components/VChipGroup/VChipGroup.js
import "/home/david/Documents/vscode/personal_development/ServerManagerWebApp/frontend/node_modules/vuetify/lib/components/VChipGroup/VChipGroup.css";
import "/home/david/gitea_repo/personal_development/ServerManagerWebApp/frontend/node_modules/vuetify/lib/components/VChipGroup/VChipGroup.css";
// node_modules/vuetify/lib/components/VSlideGroup/VSlideGroup.js
import "/home/david/Documents/vscode/personal_development/ServerManagerWebApp/frontend/node_modules/vuetify/lib/components/VSlideGroup/VSlideGroup.css";
import "/home/david/gitea_repo/personal_development/ServerManagerWebApp/frontend/node_modules/vuetify/lib/components/VSlideGroup/VSlideGroup.css";
// node_modules/vuetify/lib/components/VSlideGroup/helpers.js
function calculateUpdatedTarget(_ref) {
@@ -862,4 +862,4 @@ export {
VChipGroup,
VChip
};
//# sourceMappingURL=chunk-SHBPUR7N.js.map
//# sourceMappingURL=chunk-PLNIG6WP.js.map

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