major server manager overhaul
This commit is contained in:
@@ -1,2 +1,3 @@
|
||||
10546
|
||||
10547
|
||||
26015
|
||||
26017
|
||||
26018
|
||||
|
||||
@@ -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}")
|
||||
@@ -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
|
||||
@@ -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"])
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
@@ -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
@@ -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()
|
||||
@@ -9,4 +9,6 @@ pydantic-settings
|
||||
requests
|
||||
pillow
|
||||
beautifulsoup4
|
||||
cloudscraper
|
||||
cloudscraper
|
||||
huey
|
||||
qbittorrent-api
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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()
|
||||
@@ -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
@@ -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
@@ -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
|
||||
+7
File diff suppressed because one or more lines are too long
Generated
Vendored
+3
-113
@@ -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
|
||||
+7
File diff suppressed because one or more lines are too long
Generated
Vendored
+18
-18
@@ -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
|
||||
Generated
Vendored
Generated
Vendored
+2
-2
@@ -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
|
||||
Generated
Vendored
-7
File diff suppressed because one or more lines are too long
Generated
Vendored
+2
-2
@@ -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
|
||||
Generated
Vendored
Generated
Vendored
+3
-3
@@ -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
|
||||
Generated
Vendored
+162
@@ -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
|
||||
+7
File diff suppressed because one or more lines are too long
Generated
Vendored
+7
-7
@@ -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
|
||||
Generated
Vendored
Generated
Vendored
+6
-6
@@ -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
|
||||
Generated
Vendored
-7
File diff suppressed because one or more lines are too long
Generated
Vendored
+5
-5
@@ -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
|
||||
Generated
Vendored
-7
File diff suppressed because one or more lines are too long
Generated
Vendored
+19
-19
@@ -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
|
||||
Generated
Vendored
Generated
Vendored
+84
-84
@@ -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
|
||||
+7
File diff suppressed because one or more lines are too long
Generated
Vendored
+8
-8
@@ -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
|
||||
Generated
Vendored
Generated
Vendored
+2
-2
@@ -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
|
||||
Generated
Vendored
Generated
Vendored
+7
-7
@@ -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
|
||||
Generated
Vendored
-7
File diff suppressed because one or more lines are too long
+116
@@ -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
|
||||
+7
@@ -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"]
|
||||
}
|
||||
Generated
Vendored
+25
-179
@@ -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
|
||||
Generated
Vendored
+4
-4
File diff suppressed because one or more lines are too long
+7
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"version": 3,
|
||||
"sources": [],
|
||||
"sourcesContent": [],
|
||||
"mappings": "",
|
||||
"names": []
|
||||
}
|
||||
Generated
Vendored
+12
-12
@@ -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
|
||||
Generated
Vendored
Generated
Vendored
+42
-42
@@ -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
|
||||
+7
File diff suppressed because one or more lines are too long
Generated
Vendored
+14
-14
@@ -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
|
||||
Generated
Vendored
Generated
Vendored
+27
-27
@@ -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
|
||||
+7
File diff suppressed because one or more lines are too long
Generated
Vendored
+12
-147
@@ -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
|
||||
+7
File diff suppressed because one or more lines are too long
+226
@@ -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
|
||||
+7
File diff suppressed because one or more lines are too long
+177
@@ -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
|
||||
+7
@@ -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"]
|
||||
}
|
||||
Generated
Vendored
+6
-6
@@ -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
|
||||
Generated
Vendored
Generated
Vendored
+5
-5
@@ -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
|
||||
Generated
Vendored
Generated
Vendored
+28
-28
@@ -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
|
||||
Generated
Vendored
Generated
Vendored
+7
-7
@@ -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
|
||||
Generated
Vendored
Generated
Vendored
+1
-1
@@ -42,4 +42,4 @@ var VDefaultsProvider = genericComponent(false)({
|
||||
export {
|
||||
VDefaultsProvider
|
||||
};
|
||||
//# sourceMappingURL=chunk-VH7QIDM4.js.map
|
||||
//# sourceMappingURL=chunk-PAT7NP2Z.js.map
|
||||
Generated
Vendored
Generated
Vendored
+26
-26
@@ -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
|
||||
Generated
Vendored
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user