Merge pull request 'themes' (#2) from themes into master
Reviewed-on: #2
This commit was merged in pull request #2.
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -42,4 +42,6 @@ data/logs/*
|
||||
|
||||
|
||||
todo.txt
|
||||
pytorrent/static/libs/*
|
||||
pytorrent/static/libs/*
|
||||
!pytorrent/static/libs/pytorrent-themes/
|
||||
!pytorrent/static/libs/pytorrent-themes/**
|
||||
|
||||
@@ -257,9 +257,7 @@ def torrent_file_export_link(torrent_hash: str):
|
||||
if not profile:
|
||||
return jsonify({"ok": False, "error": "No profile"}), 400
|
||||
try:
|
||||
# Note: Export availability is checked before the UI receives a temporary /download URL.
|
||||
item = rtorrent.export_torrent_file(profile, torrent_hash)
|
||||
_cleanup_staged_file(profile, item["path"], bool(item.get("local")))
|
||||
# Note: Create only a short-lived link here; the actual .torrent export runs once when the browser opens /download/<token>.
|
||||
link = pdf_preview_links.create_torrent_file_download_link(torrent_hash, int(profile.get("id") or 0), int(default_user_id() or 0))
|
||||
return ok({"url": url_for("main.temporary_download", token=link["token"]), "expires_in": link["expires_in"]})
|
||||
except Exception as exc:
|
||||
@@ -276,15 +274,7 @@ def torrent_files_export_zip_link():
|
||||
if not hashes:
|
||||
return jsonify({"ok": False, "error": "No torrents selected"}), 400
|
||||
try:
|
||||
# Note: Each hash is checked before the temporary ZIP export link is returned to the UI.
|
||||
staged_paths = []
|
||||
try:
|
||||
for h in hashes:
|
||||
item = rtorrent.export_torrent_file(profile, h)
|
||||
staged_paths.append((item["path"], bool(item.get("local"))))
|
||||
finally:
|
||||
for path, is_local in staged_paths:
|
||||
_cleanup_staged_file(profile, path, is_local)
|
||||
# Note: Store only the selected hashes in the temporary token; exporting each .torrent now happens once during the real ZIP download.
|
||||
link = pdf_preview_links.create_torrent_files_zip_download_link(hashes, int(profile.get("id") or 0), int(default_user_id() or 0))
|
||||
return ok({"url": url_for("main.temporary_download", token=link["token"]), "expires_in": link["expires_in"]})
|
||||
except Exception as exc:
|
||||
|
||||
@@ -40,18 +40,72 @@ def google_fonts_css_url() -> str:
|
||||
return f"https://fonts.googleapis.com/css2?{families}&display=swap"
|
||||
|
||||
|
||||
BOOTSTRAP_THEMES = (
|
||||
"default",
|
||||
"flatly",
|
||||
"litera",
|
||||
"lumen",
|
||||
"minty",
|
||||
"sketchy",
|
||||
"solar",
|
||||
"spacelab",
|
||||
"united",
|
||||
"zephyr",
|
||||
)
|
||||
DEVEXPRESS_BOOTSTRAP_THEMES = {
|
||||
"blazing-berry": "Blazing Berry",
|
||||
"office-white": "Office White",
|
||||
"purple": "Purple",
|
||||
}
|
||||
|
||||
PYTORRENT_APP_THEMES = {
|
||||
"adaptive": "pyTorrent Adaptive",
|
||||
"ocean": "pyTorrent Ocean",
|
||||
"graphite": "pyTorrent Graphite",
|
||||
"forest": "pyTorrent Forest",
|
||||
"amber": "pyTorrent Amber",
|
||||
"nord": "pyTorrent Nord",
|
||||
"crimson": "pyTorrent Crimson",
|
||||
"sky": "pyTorrent Sky",
|
||||
}
|
||||
|
||||
|
||||
BOOTSTRAP_THEME_DEFINITIONS = {
|
||||
"default": {
|
||||
"label": "Default Bootstrap",
|
||||
"local": f"{LIBS_STATIC_DIR}/bootstrap/{BOOTSTRAP_VERSION}/css/bootstrap.min.css",
|
||||
"cdn": f"https://cdn.jsdelivr.net/npm/bootstrap@{BOOTSTRAP_VERSION}/dist/css/bootstrap.min.css",
|
||||
},
|
||||
# Bootswatch themes.
|
||||
"flatly": {"label": "Bootswatch: Flatly", "provider": "bootswatch"},
|
||||
"litera": {"label": "Bootswatch: Litera", "provider": "bootswatch"},
|
||||
"lumen": {"label": "Bootswatch: Lumen", "provider": "bootswatch"},
|
||||
"minty": {"label": "Bootswatch: Minty", "provider": "bootswatch"},
|
||||
"sketchy": {"label": "Bootswatch: Sketchy", "provider": "bootswatch"},
|
||||
"spacelab": {"label": "Bootswatch: Spacelab", "provider": "bootswatch"},
|
||||
"united": {"label": "Bootswatch: United", "provider": "bootswatch"},
|
||||
"zephyr": {"label": "Bootswatch: Zephyr", "provider": "bootswatch"},
|
||||
# Complete DevExpress Bootstrap v5 dist.v5 set.
|
||||
**{
|
||||
f"dx-{theme}": {
|
||||
"label": f"DevExpress: {label}",
|
||||
"provider": "devexpress",
|
||||
"local": f"{LIBS_STATIC_DIR}/devexpress-bootstrap-themes/dist.v5/{theme}/bootstrap.min.css",
|
||||
"cdn": f"https://cdn.jsdelivr.net/gh/DevExpress/bootstrap-themes@master/dist.v5/{theme}/bootstrap.min.css",
|
||||
}
|
||||
for theme, label in DEVEXPRESS_BOOTSTRAP_THEMES.items()
|
||||
},
|
||||
# App-specific Bootstrap variable overrides. These sit on top of default Bootstrap.
|
||||
**{
|
||||
f"pytorrent-{theme}": {
|
||||
"label": f"Custom: {label}",
|
||||
"provider": "pytorrent",
|
||||
"local": f"{LIBS_STATIC_DIR}/pytorrent-themes/{theme}/bootstrap.min.css",
|
||||
"cdn": f"/static/{LIBS_STATIC_DIR}/pytorrent-themes/{theme}/bootstrap.min.css",
|
||||
}
|
||||
for theme, label in PYTORRENT_APP_THEMES.items()
|
||||
},
|
||||
}
|
||||
|
||||
def _theme_definition(theme: str | None) -> dict[str, str]:
|
||||
theme = theme if theme in BOOTSTRAP_THEME_DEFINITIONS else "default"
|
||||
item = dict(BOOTSTRAP_THEME_DEFINITIONS[theme])
|
||||
if item.get("provider") == "bootswatch":
|
||||
item["local"] = f"{LIBS_STATIC_DIR}/bootswatch/{BOOTSWATCH_VERSION}/{theme}/bootstrap.min.css"
|
||||
item["cdn"] = f"https://cdn.jsdelivr.net/npm/bootswatch@{BOOTSWATCH_VERSION}/dist/{theme}/bootstrap.min.css"
|
||||
return item
|
||||
|
||||
|
||||
BOOTSTRAP_THEMES = tuple(BOOTSTRAP_THEME_DEFINITIONS.keys())
|
||||
BOOTSTRAP_THEME_LABELS = {key: value["label"] for key, value in BOOTSTRAP_THEME_DEFINITIONS.items()}
|
||||
|
||||
STATIC_ASSETS = {
|
||||
"bootstrap_js": {
|
||||
@@ -86,16 +140,8 @@ STATIC_ASSETS = {
|
||||
|
||||
|
||||
def bootstrap_css_asset(theme: str | None = None) -> dict[str, str]:
|
||||
theme = theme if theme in BOOTSTRAP_THEMES else "default"
|
||||
if theme == "default":
|
||||
return {
|
||||
"local": f"{LIBS_STATIC_DIR}/bootstrap/{BOOTSTRAP_VERSION}/css/bootstrap.min.css",
|
||||
"cdn": f"https://cdn.jsdelivr.net/npm/bootstrap@{BOOTSTRAP_VERSION}/dist/css/bootstrap.min.css",
|
||||
}
|
||||
return {
|
||||
"local": f"{LIBS_STATIC_DIR}/bootswatch/{BOOTSWATCH_VERSION}/{theme}/bootstrap.min.css",
|
||||
"cdn": f"https://cdn.jsdelivr.net/npm/bootswatch@{BOOTSWATCH_VERSION}/dist/{theme}/bootstrap.min.css",
|
||||
}
|
||||
item = _theme_definition(theme)
|
||||
return {"local": item["local"], "cdn": item["cdn"]}
|
||||
|
||||
|
||||
def asset_path(key: str) -> str:
|
||||
|
||||
@@ -98,7 +98,10 @@ def record_job_event(profile_id: int, action: str, status: str, payload: dict |
|
||||
severity = "danger" if status == "failed" else "info"
|
||||
if action in {"add_magnet", "add_torrent_raw"}:
|
||||
name = str(payload.get("name") or payload.get("filename") or payload.get("uri") or "torrent")[:300]
|
||||
msg = f"{action} {status}: {name}"
|
||||
# Note: Keep the internal action name stable, but show a user-facing label instead of raw worker identifiers.
|
||||
source_label = "Torrent file" if action == "add_torrent_raw" else "Magnet link"
|
||||
status_label = {"started": "queued", "done": "added", "failed": "failed"}.get(str(status), str(status))
|
||||
msg = f"{source_label} {status_label}: {name}"
|
||||
record(profile_id, "torrent_added" if status == "done" else event_type, msg, severity=severity, source="job", action=action, details={"job_id": job_id, "status": status, "directory": payload.get("directory"), "label": payload.get("label"), "error": error, "result": result}, user_id=user_id)
|
||||
return
|
||||
if not hashes:
|
||||
|
||||
@@ -4,19 +4,9 @@ import json
|
||||
|
||||
from ..db import connect, utcnow, default_user_id
|
||||
from . import auth
|
||||
from .frontend_assets import BOOTSTRAP_THEME_LABELS
|
||||
|
||||
BOOTSTRAP_THEMES = {
|
||||
"default": "Default Bootstrap",
|
||||
"flatly": "Flatly",
|
||||
"litera": "Litera",
|
||||
"lumen": "Lumen",
|
||||
"minty": "Minty",
|
||||
"sketchy": "Sketchy",
|
||||
"solar": "Solar",
|
||||
"spacelab": "Spacelab",
|
||||
"united": "United",
|
||||
"zephyr": "Zephyr",
|
||||
}
|
||||
BOOTSTRAP_THEMES = BOOTSTRAP_THEME_LABELS
|
||||
|
||||
FONT_FAMILIES = {
|
||||
"default": "Theme default",
|
||||
|
||||
@@ -59,10 +59,17 @@ def _torrent_file_remote_path(profile: dict, torrent_hash: str, index: int) -> t
|
||||
if selected is None:
|
||||
available = ", ".join(str(f.get("index")) for f in files[:20]) or "none"
|
||||
raise ValueError(f"File index {index} not found. Available indexes: {available}")
|
||||
|
||||
base = _remote_clean_path(_torrent_data_path(c, torrent_hash))
|
||||
rel = str(selected.get("path") or "").lstrip("/")
|
||||
if len(files) == 1 and base and not base.endswith("/"):
|
||||
path = base
|
||||
|
||||
# Note: rTorrent can report d.base_path as either the payload file or the
|
||||
# containing data directory for a one-file torrent. Keep both existing
|
||||
# layouts working and avoid treating a directory as the media file.
|
||||
if len(files) == 1 and base and rel:
|
||||
base_name = posixpath.basename(base.rstrip("/"))
|
||||
rel_name = posixpath.basename(rel.rstrip("/"))
|
||||
path = base if base_name == rel_name else _remote_join(base, rel)
|
||||
else:
|
||||
path = _remote_join(base, rel)
|
||||
return selected, path
|
||||
@@ -176,25 +183,16 @@ def _media_info_sample_suffix(source_path: str) -> str:
|
||||
|
||||
|
||||
def _read_file_prefix(profile: dict, source_path: str, max_bytes: int) -> bytes:
|
||||
# Note: Small previews use a bounded prefix read, so text and image preview actions never load an entire large file into RAM.
|
||||
# Note: File info must read through rTorrent, not the pyTorrent process, because torrents may live on a remote host or under rTorrent-only permissions.
|
||||
limit = max(0, int(max_bytes or 0))
|
||||
chunks: list[bytes] = []
|
||||
collected = 0
|
||||
if int(profile.get("is_remote") or 0):
|
||||
for chunk in iter_remote_file_chunks(profile, source_path, size=limit, chunk_size=_MEDIA_INFO_CHUNK_BYTES):
|
||||
if collected >= limit:
|
||||
break
|
||||
data = bytes(chunk[: max(0, limit - collected)])
|
||||
chunks.append(data)
|
||||
collected += len(data)
|
||||
else:
|
||||
with open(source_path, "rb") as src:
|
||||
while collected < limit:
|
||||
data = src.read(min(_MEDIA_INFO_CHUNK_BYTES, limit - collected))
|
||||
if not data:
|
||||
break
|
||||
chunks.append(data)
|
||||
collected += len(data)
|
||||
for chunk in iter_remote_file_chunks(profile, source_path, size=limit, chunk_size=_MEDIA_INFO_CHUNK_BYTES):
|
||||
if collected >= limit:
|
||||
break
|
||||
data = bytes(chunk[: max(0, limit - collected)])
|
||||
chunks.append(data)
|
||||
collected += len(data)
|
||||
return b"".join(chunks)
|
||||
|
||||
|
||||
@@ -340,21 +338,12 @@ def _media_info_temp_sample(profile: dict, source_path: str, max_bytes: int) ->
|
||||
written = 0
|
||||
try:
|
||||
with os.fdopen(fd, "wb") as tmp:
|
||||
if int(profile.get("is_remote") or 0):
|
||||
for chunk in iter_remote_file_chunks(profile, source_path, size=max_bytes, chunk_size=_MEDIA_INFO_CHUNK_BYTES):
|
||||
if written >= max_bytes:
|
||||
break
|
||||
data = bytes(chunk[: max(0, max_bytes - written)])
|
||||
tmp.write(data)
|
||||
written += len(data)
|
||||
else:
|
||||
with open(source_path, "rb") as src:
|
||||
while written < max_bytes:
|
||||
data = src.read(min(_MEDIA_INFO_CHUNK_BYTES, max_bytes - written))
|
||||
if not data:
|
||||
break
|
||||
tmp.write(data)
|
||||
written += len(data)
|
||||
for chunk in iter_remote_file_chunks(profile, source_path, size=max_bytes, chunk_size=_MEDIA_INFO_CHUNK_BYTES):
|
||||
if written >= max_bytes:
|
||||
break
|
||||
data = bytes(chunk[: max(0, max_bytes - written)])
|
||||
tmp.write(data)
|
||||
written += len(data)
|
||||
return tmp_path, written
|
||||
except Exception:
|
||||
try:
|
||||
@@ -447,13 +436,25 @@ def _media_info_hachoir_imports():
|
||||
) from exc
|
||||
|
||||
|
||||
def _torrent_file_is_complete(selected: dict) -> bool:
|
||||
# Note: File info reads real file bytes, so incomplete payload files are blocked before any parser touches them.
|
||||
size = int(selected.get("size") or 0)
|
||||
completed_chunks = int(selected.get("completed_chunks") or 0)
|
||||
size_chunks = int(selected.get("size_chunks") or 0)
|
||||
progress = float(selected.get("progress") or 0)
|
||||
return size <= 0 or progress >= 100.0 or (size_chunks > 0 and completed_chunks >= size_chunks)
|
||||
|
||||
|
||||
def torrent_file_media_info(profile: dict, torrent_hash: str, index: int, max_bytes: int = _MEDIA_INFO_SAMPLE_BYTES) -> dict:
|
||||
# Note: This additive endpoint now acts as a smart file preview: media metadata, text/NFO reader, or image preview depending on file type.
|
||||
selected, remote_path = _torrent_file_remote_path(profile, torrent_hash, index)
|
||||
name = str(selected.get("path") or remote_path)
|
||||
size = int(selected.get("size") or 0)
|
||||
|
||||
err = remote_file_readability_error(profile, remote_path) if int(profile.get("is_remote") or 0) else None
|
||||
if not _torrent_file_is_complete(selected):
|
||||
raise RuntimeError("File info is available only after this file is fully downloaded.")
|
||||
|
||||
err = remote_file_readability_error(profile, remote_path)
|
||||
if err:
|
||||
raise RuntimeError(err)
|
||||
|
||||
@@ -541,17 +542,31 @@ def torrent_download_zip_items(profile: dict, torrent_hash: str, indexes: list[i
|
||||
return items
|
||||
|
||||
|
||||
def _remote_file_exists(c: ScgiRtorrentClient, source_path: str) -> bool:
|
||||
# Note: Export fallback checks candidate .torrent files on the rTorrent host before staging, avoiding stale tied-file paths.
|
||||
clean = _remote_clean_path(source_path)
|
||||
if not clean:
|
||||
return False
|
||||
script = 'p=$1; [ -f "$p" ] && [ -r "$p" ] && printf OK || true'
|
||||
try:
|
||||
return str(_rt_execute(c, "execute.capture", "sh", "-c", script, "pytorrent-file-exists", clean) or "").strip() == "OK"
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def _remote_stage_path(c: ScgiRtorrentClient, source_path: str, suffix: str = "") -> str:
|
||||
token = uuid.uuid4().hex
|
||||
safe_suffix = ''.join(ch if ch.isalnum() or ch in '.-_' else '_' for ch in str(suffix or ''))[:80]
|
||||
target = f"{download_tmp_dir().rstrip('/')}/pytorrent-download-{token}{safe_suffix}"
|
||||
script = (
|
||||
'src=$1; dst=$2; '
|
||||
'if [ ! -f "$src" ]; then echo "ERR\tmissing source"; exit 0; fi; '
|
||||
'if [ ! -f "$src" ]; then printf "ERR\tmissing source: %s\n" "$src"; exit 0; fi; '
|
||||
'if [ ! -r "$src" ]; then printf "ERR\tsource is not readable: %s\n" "$src"; exit 0; fi; '
|
||||
'cp -- "$src" "$dst" 2>/tmp/pytorrent-cp-err-$$ || { rc=$?; err=$(cat /tmp/pytorrent-cp-err-$$ 2>/dev/null); rm -f /tmp/pytorrent-cp-err-$$; printf "ERR\t%s\t%s\n" "$rc" "$err"; exit 0; }; '
|
||||
'rm -f /tmp/pytorrent-cp-err-$$; chmod 0644 "$dst" 2>/dev/null || true; printf "OK\t%s\n" "$dst"'
|
||||
)
|
||||
output = str(_rt_execute(c, "execute.capture", "sh", "-c", script, "pytorrent-stage-file", source_path, target) or "").strip()
|
||||
clean_source = _remote_clean_path(source_path)
|
||||
output = str(_rt_execute(c, "execute.capture", "sh", "-c", script, "pytorrent-stage-file", clean_source, target) or "").strip()
|
||||
parts = (output.splitlines()[0] if output else "").split("\t", 2)
|
||||
if len(parts) >= 2 and parts[0] == "OK":
|
||||
return parts[1]
|
||||
@@ -643,14 +658,48 @@ def _torrent_raw_from_method(c: ScgiRtorrentClient, torrent_hash: str) -> bytes
|
||||
return None
|
||||
|
||||
|
||||
def _torrent_source_file(c: ScgiRtorrentClient, torrent_hash: str) -> str:
|
||||
def _rtorrent_session_path(c: ScgiRtorrentClient) -> str:
|
||||
for method in ("session.path", "get_session"):
|
||||
try:
|
||||
value = str(c.call(method) or "").strip()
|
||||
except Exception:
|
||||
continue
|
||||
if value:
|
||||
return _remote_clean_path(value)
|
||||
return ""
|
||||
|
||||
|
||||
def _torrent_source_file_candidates(c: ScgiRtorrentClient, torrent_hash: str) -> list[str]:
|
||||
# Note: rTorrent may keep stale watch/tied paths; session candidates preserve .torrent export when the original source was moved.
|
||||
candidates: list[str] = []
|
||||
for method in ("d.tied_to_file", "d.get_tied_to_file", "d.loaded_file", "d.get_loaded_file", "d.session_file", "d.get_session_file"):
|
||||
try:
|
||||
value = str(c.call(method, torrent_hash) or "").strip()
|
||||
except Exception:
|
||||
continue
|
||||
if value:
|
||||
return value
|
||||
candidates.append(value)
|
||||
session_path = _rtorrent_session_path(c)
|
||||
hash_values = []
|
||||
clean_hash = str(torrent_hash or "").strip()
|
||||
if clean_hash:
|
||||
hash_values.extend([clean_hash, clean_hash.upper(), clean_hash.lower()])
|
||||
for h in dict.fromkeys(hash_values):
|
||||
if session_path:
|
||||
candidates.append(_remote_join(session_path, f"{h}.torrent"))
|
||||
candidates.append(f"/tmp/{h}.torrent")
|
||||
result = []
|
||||
for item in candidates:
|
||||
clean = _remote_clean_path(item)
|
||||
if clean and clean not in result:
|
||||
result.append(clean)
|
||||
return result
|
||||
|
||||
|
||||
def _torrent_source_file(c: ScgiRtorrentClient, torrent_hash: str) -> str:
|
||||
for source in _torrent_source_file_candidates(c, torrent_hash):
|
||||
if _remote_file_exists(c, source):
|
||||
return source
|
||||
return ""
|
||||
|
||||
|
||||
@@ -658,16 +707,16 @@ def export_torrent_file(profile: dict, torrent_hash: str) -> dict:
|
||||
c = client_for(profile)
|
||||
name = str(c.call("d.name", torrent_hash) or torrent_hash).strip() or torrent_hash
|
||||
filename = f"{name}.torrent" if not name.lower().endswith(".torrent") else name
|
||||
source = _torrent_source_file(c, torrent_hash)
|
||||
if source:
|
||||
# Note: Stream the existing .torrent source directly instead of copying it to a temporary staged file first.
|
||||
return {"path": source, "download_name": filename, "local": False}
|
||||
raw = _torrent_raw_from_method(c, torrent_hash)
|
||||
if raw:
|
||||
target = LocalPath(download_tmp_dir()) / f"pytorrent-download-{uuid.uuid4().hex}.torrent"
|
||||
target.write_bytes(raw)
|
||||
return {"path": str(target), "download_name": filename, "local": True}
|
||||
source = _torrent_source_file(c, torrent_hash)
|
||||
if not source:
|
||||
raise RuntimeError("Cannot find torrent source file in rTorrent")
|
||||
staged = _remote_stage_path(c, source, ".torrent")
|
||||
return {"path": staged, "download_name": filename, "local": False}
|
||||
raise RuntimeError("Cannot find torrent source file in rTorrent")
|
||||
|
||||
|
||||
def set_file_priorities(profile: dict, torrent_hash: str, files: list[dict]) -> dict:
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -3,6 +3,8 @@ export const messagesSource = `
|
||||
const APP_MESSAGES = {
|
||||
actions: {
|
||||
raw_torrent: 'Add torrent',
|
||||
add_torrent_raw: 'Add torrent file',
|
||||
add_magnet: 'Add magnet link',
|
||||
add: 'Add torrent',
|
||||
start: 'Start torrent',
|
||||
pause: 'Pause torrent',
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
66
pytorrent/static/libs/pytorrent-themes/adaptive/bootstrap.min.css
vendored
Normal file
66
pytorrent/static/libs/pytorrent-themes/adaptive/bootstrap.min.css
vendored
Normal file
@@ -0,0 +1,66 @@
|
||||
/* Balanced violet-blue theme with calm defaults. */
|
||||
:root, [data-bs-theme="light"] {
|
||||
color-scheme: light;
|
||||
--bs-primary: #6750a4;
|
||||
--bs-primary-rgb: 103,80,164;
|
||||
--bs-link-color: #6750a4;
|
||||
--bs-link-color-rgb: 103,80,164;
|
||||
--bs-link-hover-color: #4b2f8f;
|
||||
--bs-link-hover-color-rgb: 103,80,164;
|
||||
--bs-body-bg: #f6f7fb;
|
||||
--bs-body-bg-rgb: 246,247,251;
|
||||
--bs-body-color: #1f2937;
|
||||
--bs-secondary-bg: #ffffff;
|
||||
--bs-secondary-bg-rgb: 255,255,255;
|
||||
--bs-tertiary-bg: #eef1f7;
|
||||
--bs-border-color: #d9deea;
|
||||
--bs-secondary-color: #667085;
|
||||
--bs-primary-bg-subtle: #ece7ff;
|
||||
--bs-primary-text-emphasis: #4b2f8f;
|
||||
--torrent-progress-complete: #2f9e75;
|
||||
--pytorrent-page-bg: linear-gradient(180deg, #f7f8fc 0%, #eef1f7 100%);
|
||||
--pytorrent-shell-shadow: 0 16px 40px rgba(39, 45, 73, 0.12);
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] {
|
||||
color-scheme: dark;
|
||||
--bs-primary: #8f7cff;
|
||||
--bs-primary-rgb: 143,124,255;
|
||||
--bs-link-color: #8f7cff;
|
||||
--bs-link-color-rgb: 143,124,255;
|
||||
--bs-link-hover-color: #c6bdff;
|
||||
--bs-link-hover-color-rgb: 143,124,255;
|
||||
--bs-body-bg: #080b12;
|
||||
--bs-body-bg-rgb: 8,11,18;
|
||||
--bs-body-color: #dce3f0;
|
||||
--bs-secondary-bg: #101624;
|
||||
--bs-secondary-bg-rgb: 16,22,36;
|
||||
--bs-tertiary-bg: #151d2e;
|
||||
--bs-border-color: #273247;
|
||||
--bs-secondary-color: #97a4ba;
|
||||
--bs-primary-bg-subtle: #191735;
|
||||
--bs-primary-text-emphasis: #c6bdff;
|
||||
--torrent-progress-complete: #2f9e75;
|
||||
--pytorrent-page-bg: radial-gradient(circle at top left, rgba(143, 124, 255, 0.16), transparent 34%), #080b12;
|
||||
--pytorrent-shell-shadow: 0 18px 55px rgba(0, 0, 0, 0.48);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
--bs-btn-bg: var(--bs-primary);
|
||||
--bs-btn-border-color: var(--bs-primary);
|
||||
--bs-btn-hover-bg: var(--bs-primary-text-emphasis);
|
||||
--bs-btn-hover-border-color: var(--bs-primary-text-emphasis);
|
||||
}
|
||||
.btn-outline-primary {
|
||||
--bs-btn-color: var(--bs-primary);
|
||||
--bs-btn-border-color: rgba(var(--bs-primary-rgb), 0.72);
|
||||
--bs-btn-hover-bg: var(--bs-primary);
|
||||
--bs-btn-hover-border-color: var(--bs-primary);
|
||||
}
|
||||
.nav-pills {
|
||||
--bs-nav-pills-link-active-bg: var(--bs-primary);
|
||||
}
|
||||
.progress,
|
||||
.progress-stacked {
|
||||
--bs-progress-bg: rgba(var(--bs-secondary-bg-rgb), 0.82);
|
||||
}
|
||||
66
pytorrent/static/libs/pytorrent-themes/amber/bootstrap.min.css
vendored
Normal file
66
pytorrent/static/libs/pytorrent-themes/amber/bootstrap.min.css
vendored
Normal file
@@ -0,0 +1,66 @@
|
||||
/* Warm amber theme, good for light mode and softer dark mode. */
|
||||
:root, [data-bs-theme="light"] {
|
||||
color-scheme: light;
|
||||
--bs-primary: #b45309;
|
||||
--bs-primary-rgb: 180,83,9;
|
||||
--bs-link-color: #b45309;
|
||||
--bs-link-color-rgb: 180,83,9;
|
||||
--bs-link-hover-color: #92400e;
|
||||
--bs-link-hover-color-rgb: 180,83,9;
|
||||
--bs-body-bg: #fff8ed;
|
||||
--bs-body-bg-rgb: 255,248,237;
|
||||
--bs-body-color: #2c1d0b;
|
||||
--bs-secondary-bg: #ffffff;
|
||||
--bs-secondary-bg-rgb: 255,255,255;
|
||||
--bs-tertiary-bg: #ffefd6;
|
||||
--bs-border-color: #f3d7aa;
|
||||
--bs-secondary-color: #7a6750;
|
||||
--bs-primary-bg-subtle: #ffecd0;
|
||||
--bs-primary-text-emphasis: #92400e;
|
||||
--torrent-progress-complete: #16a34a;
|
||||
--pytorrent-page-bg: linear-gradient(180deg, #fffaf2 0%, #fff0d8 100%);
|
||||
--pytorrent-shell-shadow: 0 16px 38px rgba(146, 64, 14, 0.13);
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] {
|
||||
color-scheme: dark;
|
||||
--bs-primary: #f59e0b;
|
||||
--bs-primary-rgb: 245,158,11;
|
||||
--bs-link-color: #f59e0b;
|
||||
--bs-link-color-rgb: 245,158,11;
|
||||
--bs-link-hover-color: #fde68a;
|
||||
--bs-link-hover-color-rgb: 245,158,11;
|
||||
--bs-body-bg: #140d05;
|
||||
--bs-body-bg-rgb: 20,13,5;
|
||||
--bs-body-color: #f5e9d6;
|
||||
--bs-secondary-bg: #211607;
|
||||
--bs-secondary-bg-rgb: 33,22,7;
|
||||
--bs-tertiary-bg: #2b1d0b;
|
||||
--bs-border-color: #4c3515;
|
||||
--bs-secondary-color: #c3a780;
|
||||
--bs-primary-bg-subtle: #3a2609;
|
||||
--bs-primary-text-emphasis: #fde68a;
|
||||
--torrent-progress-complete: #22c55e;
|
||||
--pytorrent-page-bg: radial-gradient(circle at top left, rgba(245, 158, 11, 0.16), transparent 34%), #140d05;
|
||||
--pytorrent-shell-shadow: 0 18px 55px rgba(30, 15, 0, 0.58);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
--bs-btn-bg: var(--bs-primary);
|
||||
--bs-btn-border-color: var(--bs-primary);
|
||||
--bs-btn-hover-bg: var(--bs-primary-text-emphasis);
|
||||
--bs-btn-hover-border-color: var(--bs-primary-text-emphasis);
|
||||
}
|
||||
.btn-outline-primary {
|
||||
--bs-btn-color: var(--bs-primary);
|
||||
--bs-btn-border-color: rgba(var(--bs-primary-rgb), 0.72);
|
||||
--bs-btn-hover-bg: var(--bs-primary);
|
||||
--bs-btn-hover-border-color: var(--bs-primary);
|
||||
}
|
||||
.nav-pills {
|
||||
--bs-nav-pills-link-active-bg: var(--bs-primary);
|
||||
}
|
||||
.progress,
|
||||
.progress-stacked {
|
||||
--bs-progress-bg: rgba(var(--bs-secondary-bg-rgb), 0.82);
|
||||
}
|
||||
66
pytorrent/static/libs/pytorrent-themes/crimson/bootstrap.min.css
vendored
Normal file
66
pytorrent/static/libs/pytorrent-themes/crimson/bootstrap.min.css
vendored
Normal file
@@ -0,0 +1,66 @@
|
||||
/* High-contrast red accent theme without the previous purple feel. */
|
||||
:root, [data-bs-theme="light"] {
|
||||
color-scheme: light;
|
||||
--bs-primary: #be123c;
|
||||
--bs-primary-rgb: 190,18,60;
|
||||
--bs-link-color: #be123c;
|
||||
--bs-link-color-rgb: 190,18,60;
|
||||
--bs-link-hover-color: #9f1239;
|
||||
--bs-link-hover-color-rgb: 190,18,60;
|
||||
--bs-body-bg: #fff5f7;
|
||||
--bs-body-bg-rgb: 255,245,247;
|
||||
--bs-body-color: #2b1118;
|
||||
--bs-secondary-bg: #ffffff;
|
||||
--bs-secondary-bg-rgb: 255,255,255;
|
||||
--bs-tertiary-bg: #ffe4ea;
|
||||
--bs-border-color: #f5c6d1;
|
||||
--bs-secondary-color: #76545d;
|
||||
--bs-primary-bg-subtle: #ffe1e9;
|
||||
--bs-primary-text-emphasis: #9f1239;
|
||||
--torrent-progress-complete: #16a34a;
|
||||
--pytorrent-page-bg: linear-gradient(180deg, #fff8fa 0%, #ffe8ee 100%);
|
||||
--pytorrent-shell-shadow: 0 16px 38px rgba(159, 18, 57, 0.13);
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] {
|
||||
color-scheme: dark;
|
||||
--bs-primary: #fb7185;
|
||||
--bs-primary-rgb: 251,113,133;
|
||||
--bs-link-color: #fb7185;
|
||||
--bs-link-color-rgb: 251,113,133;
|
||||
--bs-link-hover-color: #fecdd3;
|
||||
--bs-link-hover-color-rgb: 251,113,133;
|
||||
--bs-body-bg: #13070a;
|
||||
--bs-body-bg-rgb: 19,7,10;
|
||||
--bs-body-color: #f7dce2;
|
||||
--bs-secondary-bg: #211014;
|
||||
--bs-secondary-bg-rgb: 33,16,20;
|
||||
--bs-tertiary-bg: #2b151b;
|
||||
--bs-border-color: #4b2430;
|
||||
--bs-secondary-color: #c096a1;
|
||||
--bs-primary-bg-subtle: #3b111b;
|
||||
--bs-primary-text-emphasis: #fecdd3;
|
||||
--torrent-progress-complete: #22c55e;
|
||||
--pytorrent-page-bg: radial-gradient(circle at top left, rgba(251, 113, 133, 0.15), transparent 35%), #13070a;
|
||||
--pytorrent-shell-shadow: 0 18px 55px rgba(28, 0, 8, 0.58);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
--bs-btn-bg: var(--bs-primary);
|
||||
--bs-btn-border-color: var(--bs-primary);
|
||||
--bs-btn-hover-bg: var(--bs-primary-text-emphasis);
|
||||
--bs-btn-hover-border-color: var(--bs-primary-text-emphasis);
|
||||
}
|
||||
.btn-outline-primary {
|
||||
--bs-btn-color: var(--bs-primary);
|
||||
--bs-btn-border-color: rgba(var(--bs-primary-rgb), 0.72);
|
||||
--bs-btn-hover-bg: var(--bs-primary);
|
||||
--bs-btn-hover-border-color: var(--bs-primary);
|
||||
}
|
||||
.nav-pills {
|
||||
--bs-nav-pills-link-active-bg: var(--bs-primary);
|
||||
}
|
||||
.progress,
|
||||
.progress-stacked {
|
||||
--bs-progress-bg: rgba(var(--bs-secondary-bg-rgb), 0.82);
|
||||
}
|
||||
66
pytorrent/static/libs/pytorrent-themes/forest/bootstrap.min.css
vendored
Normal file
66
pytorrent/static/libs/pytorrent-themes/forest/bootstrap.min.css
vendored
Normal file
@@ -0,0 +1,66 @@
|
||||
/* Green productivity theme with warm contrast. */
|
||||
:root, [data-bs-theme="light"] {
|
||||
color-scheme: light;
|
||||
--bs-primary: #2f7d32;
|
||||
--bs-primary-rgb: 47,125,50;
|
||||
--bs-link-color: #2f7d32;
|
||||
--bs-link-color-rgb: 47,125,50;
|
||||
--bs-link-hover-color: #256329;
|
||||
--bs-link-hover-color-rgb: 47,125,50;
|
||||
--bs-body-bg: #f4faf2;
|
||||
--bs-body-bg-rgb: 244,250,242;
|
||||
--bs-body-color: #172417;
|
||||
--bs-secondary-bg: #ffffff;
|
||||
--bs-secondary-bg-rgb: 255,255,255;
|
||||
--bs-tertiary-bg: #e7f4e4;
|
||||
--bs-border-color: #cce4c7;
|
||||
--bs-secondary-color: #61735e;
|
||||
--bs-primary-bg-subtle: #dcf2d7;
|
||||
--bs-primary-text-emphasis: #256329;
|
||||
--torrent-progress-complete: #22c55e;
|
||||
--pytorrent-page-bg: linear-gradient(180deg, #fbfff9 0%, #e9f6e5 100%);
|
||||
--pytorrent-shell-shadow: 0 16px 38px rgba(35, 84, 38, 0.13);
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] {
|
||||
color-scheme: dark;
|
||||
--bs-primary: #4ade80;
|
||||
--bs-primary-rgb: 74,222,128;
|
||||
--bs-link-color: #4ade80;
|
||||
--bs-link-color-rgb: 74,222,128;
|
||||
--bs-link-hover-color: #bbf7d0;
|
||||
--bs-link-hover-color-rgb: 74,222,128;
|
||||
--bs-body-bg: #071109;
|
||||
--bs-body-bg-rgb: 7,17,9;
|
||||
--bs-body-color: #d9f1dc;
|
||||
--bs-secondary-bg: #0f1f12;
|
||||
--bs-secondary-bg-rgb: 15,31,18;
|
||||
--bs-tertiary-bg: #152b18;
|
||||
--bs-border-color: #24472a;
|
||||
--bs-secondary-color: #95b79a;
|
||||
--bs-primary-bg-subtle: #13381a;
|
||||
--bs-primary-text-emphasis: #bbf7d0;
|
||||
--torrent-progress-complete: #4ade80;
|
||||
--pytorrent-page-bg: radial-gradient(circle at top left, rgba(74, 222, 128, 0.14), transparent 36%), #071109;
|
||||
--pytorrent-shell-shadow: 0 18px 55px rgba(0, 20, 4, 0.58);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
--bs-btn-bg: var(--bs-primary);
|
||||
--bs-btn-border-color: var(--bs-primary);
|
||||
--bs-btn-hover-bg: var(--bs-primary-text-emphasis);
|
||||
--bs-btn-hover-border-color: var(--bs-primary-text-emphasis);
|
||||
}
|
||||
.btn-outline-primary {
|
||||
--bs-btn-color: var(--bs-primary);
|
||||
--bs-btn-border-color: rgba(var(--bs-primary-rgb), 0.72);
|
||||
--bs-btn-hover-bg: var(--bs-primary);
|
||||
--bs-btn-hover-border-color: var(--bs-primary);
|
||||
}
|
||||
.nav-pills {
|
||||
--bs-nav-pills-link-active-bg: var(--bs-primary);
|
||||
}
|
||||
.progress,
|
||||
.progress-stacked {
|
||||
--bs-progress-bg: rgba(var(--bs-secondary-bg-rgb), 0.82);
|
||||
}
|
||||
66
pytorrent/static/libs/pytorrent-themes/graphite/bootstrap.min.css
vendored
Normal file
66
pytorrent/static/libs/pytorrent-themes/graphite/bootstrap.min.css
vendored
Normal file
@@ -0,0 +1,66 @@
|
||||
/* Neutral grey theme for users who want low saturation. */
|
||||
:root, [data-bs-theme="light"] {
|
||||
color-scheme: light;
|
||||
--bs-primary: #334155;
|
||||
--bs-primary-rgb: 51,65,85;
|
||||
--bs-link-color: #334155;
|
||||
--bs-link-color-rgb: 51,65,85;
|
||||
--bs-link-hover-color: #1f2937;
|
||||
--bs-link-hover-color-rgb: 51,65,85;
|
||||
--bs-body-bg: #f5f6f8;
|
||||
--bs-body-bg-rgb: 245,246,248;
|
||||
--bs-body-color: #18202f;
|
||||
--bs-secondary-bg: #ffffff;
|
||||
--bs-secondary-bg-rgb: 255,255,255;
|
||||
--bs-tertiary-bg: #eceff3;
|
||||
--bs-border-color: #d7dce3;
|
||||
--bs-secondary-color: #667085;
|
||||
--bs-primary-bg-subtle: #e4e8ef;
|
||||
--bs-primary-text-emphasis: #1f2937;
|
||||
--torrent-progress-complete: #16a34a;
|
||||
--pytorrent-page-bg: linear-gradient(180deg, #fafafa 0%, #edf0f4 100%);
|
||||
--pytorrent-shell-shadow: 0 16px 38px rgba(31, 41, 55, 0.13);
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] {
|
||||
color-scheme: dark;
|
||||
--bs-primary: #94a3b8;
|
||||
--bs-primary-rgb: 148,163,184;
|
||||
--bs-link-color: #94a3b8;
|
||||
--bs-link-color-rgb: 148,163,184;
|
||||
--bs-link-hover-color: #e2e8f0;
|
||||
--bs-link-hover-color-rgb: 148,163,184;
|
||||
--bs-body-bg: #07090d;
|
||||
--bs-body-bg-rgb: 7,9,13;
|
||||
--bs-body-color: #d7dde7;
|
||||
--bs-secondary-bg: #11151c;
|
||||
--bs-secondary-bg-rgb: 17,21,28;
|
||||
--bs-tertiary-bg: #171c25;
|
||||
--bs-border-color: #2b3442;
|
||||
--bs-secondary-color: #99a3b3;
|
||||
--bs-primary-bg-subtle: #1d2531;
|
||||
--bs-primary-text-emphasis: #e2e8f0;
|
||||
--torrent-progress-complete: #22c55e;
|
||||
--pytorrent-page-bg: radial-gradient(circle at top left, rgba(148, 163, 184, 0.11), transparent 34%), #07090d;
|
||||
--pytorrent-shell-shadow: 0 18px 55px rgba(0, 0, 0, 0.52);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
--bs-btn-bg: var(--bs-primary);
|
||||
--bs-btn-border-color: var(--bs-primary);
|
||||
--bs-btn-hover-bg: var(--bs-primary-text-emphasis);
|
||||
--bs-btn-hover-border-color: var(--bs-primary-text-emphasis);
|
||||
}
|
||||
.btn-outline-primary {
|
||||
--bs-btn-color: var(--bs-primary);
|
||||
--bs-btn-border-color: rgba(var(--bs-primary-rgb), 0.72);
|
||||
--bs-btn-hover-bg: var(--bs-primary);
|
||||
--bs-btn-hover-border-color: var(--bs-primary);
|
||||
}
|
||||
.nav-pills {
|
||||
--bs-nav-pills-link-active-bg: var(--bs-primary);
|
||||
}
|
||||
.progress,
|
||||
.progress-stacked {
|
||||
--bs-progress-bg: rgba(var(--bs-secondary-bg-rgb), 0.82);
|
||||
}
|
||||
66
pytorrent/static/libs/pytorrent-themes/nord/bootstrap.min.css
vendored
Normal file
66
pytorrent/static/libs/pytorrent-themes/nord/bootstrap.min.css
vendored
Normal file
@@ -0,0 +1,66 @@
|
||||
/* Cool blue-grey theme inspired by Nordic palettes. */
|
||||
:root, [data-bs-theme="light"] {
|
||||
color-scheme: light;
|
||||
--bs-primary: #4c6a92;
|
||||
--bs-primary-rgb: 76,106,146;
|
||||
--bs-link-color: #4c6a92;
|
||||
--bs-link-color-rgb: 76,106,146;
|
||||
--bs-link-hover-color: #365174;
|
||||
--bs-link-hover-color-rgb: 76,106,146;
|
||||
--bs-body-bg: #f4f7fb;
|
||||
--bs-body-bg-rgb: 244,247,251;
|
||||
--bs-body-color: #182334;
|
||||
--bs-secondary-bg: #ffffff;
|
||||
--bs-secondary-bg-rgb: 255,255,255;
|
||||
--bs-tertiary-bg: #e7edf5;
|
||||
--bs-border-color: #d0d9e8;
|
||||
--bs-secondary-color: #607089;
|
||||
--bs-primary-bg-subtle: #dfe8f4;
|
||||
--bs-primary-text-emphasis: #365174;
|
||||
--torrent-progress-complete: #3aa675;
|
||||
--pytorrent-page-bg: linear-gradient(180deg, #fbfdff 0%, #e9eff7 100%);
|
||||
--pytorrent-shell-shadow: 0 16px 38px rgba(47, 64, 87, 0.13);
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] {
|
||||
color-scheme: dark;
|
||||
--bs-primary: #88c0d0;
|
||||
--bs-primary-rgb: 136,192,208;
|
||||
--bs-link-color: #88c0d0;
|
||||
--bs-link-color-rgb: 136,192,208;
|
||||
--bs-link-hover-color: #b6e3ee;
|
||||
--bs-link-hover-color-rgb: 136,192,208;
|
||||
--bs-body-bg: #0b111a;
|
||||
--bs-body-bg-rgb: 11,17,26;
|
||||
--bs-body-color: #d8dee9;
|
||||
--bs-secondary-bg: #111927;
|
||||
--bs-secondary-bg-rgb: 17,25,39;
|
||||
--bs-tertiary-bg: #172233;
|
||||
--bs-border-color: #2e3a4d;
|
||||
--bs-secondary-color: #9aa8bc;
|
||||
--bs-primary-bg-subtle: #132b3a;
|
||||
--bs-primary-text-emphasis: #b6e3ee;
|
||||
--torrent-progress-complete: #a3be8c;
|
||||
--pytorrent-page-bg: radial-gradient(circle at top left, rgba(136, 192, 208, 0.14), transparent 35%), #0b111a;
|
||||
--pytorrent-shell-shadow: 0 18px 55px rgba(3, 8, 15, 0.55);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
--bs-btn-bg: var(--bs-primary);
|
||||
--bs-btn-border-color: var(--bs-primary);
|
||||
--bs-btn-hover-bg: var(--bs-primary-text-emphasis);
|
||||
--bs-btn-hover-border-color: var(--bs-primary-text-emphasis);
|
||||
}
|
||||
.btn-outline-primary {
|
||||
--bs-btn-color: var(--bs-primary);
|
||||
--bs-btn-border-color: rgba(var(--bs-primary-rgb), 0.72);
|
||||
--bs-btn-hover-bg: var(--bs-primary);
|
||||
--bs-btn-hover-border-color: var(--bs-primary);
|
||||
}
|
||||
.nav-pills {
|
||||
--bs-nav-pills-link-active-bg: var(--bs-primary);
|
||||
}
|
||||
.progress,
|
||||
.progress-stacked {
|
||||
--bs-progress-bg: rgba(var(--bs-secondary-bg-rgb), 0.82);
|
||||
}
|
||||
66
pytorrent/static/libs/pytorrent-themes/ocean/bootstrap.min.css
vendored
Normal file
66
pytorrent/static/libs/pytorrent-themes/ocean/bootstrap.min.css
vendored
Normal file
@@ -0,0 +1,66 @@
|
||||
/* Dark teal/navy with a bright, clean light mode. */
|
||||
:root, [data-bs-theme="light"] {
|
||||
color-scheme: light;
|
||||
--bs-primary: #0f766e;
|
||||
--bs-primary-rgb: 15,118,110;
|
||||
--bs-link-color: #0f766e;
|
||||
--bs-link-color-rgb: 15,118,110;
|
||||
--bs-link-hover-color: #095c55;
|
||||
--bs-link-hover-color-rgb: 15,118,110;
|
||||
--bs-body-bg: #f2fbfa;
|
||||
--bs-body-bg-rgb: 242,251,250;
|
||||
--bs-body-color: #102a2a;
|
||||
--bs-secondary-bg: #ffffff;
|
||||
--bs-secondary-bg-rgb: 255,255,255;
|
||||
--bs-tertiary-bg: #dff5f2;
|
||||
--bs-border-color: #bfe4df;
|
||||
--bs-secondary-color: #54706f;
|
||||
--bs-primary-bg-subtle: #d6f4ef;
|
||||
--bs-primary-text-emphasis: #095c55;
|
||||
--torrent-progress-complete: #14b8a6;
|
||||
--pytorrent-page-bg: linear-gradient(180deg, #f7fffe 0%, #e6f7f5 100%);
|
||||
--pytorrent-shell-shadow: 0 16px 38px rgba(10, 78, 74, 0.13);
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] {
|
||||
color-scheme: dark;
|
||||
--bs-primary: #2dd4bf;
|
||||
--bs-primary-rgb: 45,212,191;
|
||||
--bs-link-color: #2dd4bf;
|
||||
--bs-link-color-rgb: 45,212,191;
|
||||
--bs-link-hover-color: #99f6e4;
|
||||
--bs-link-hover-color-rgb: 45,212,191;
|
||||
--bs-body-bg: #031316;
|
||||
--bs-body-bg-rgb: 3,19,22;
|
||||
--bs-body-color: #d5f4f1;
|
||||
--bs-secondary-bg: #082326;
|
||||
--bs-secondary-bg-rgb: 8,35,38;
|
||||
--bs-tertiary-bg: #0d3034;
|
||||
--bs-border-color: #17494e;
|
||||
--bs-secondary-color: #8ab6b2;
|
||||
--bs-primary-bg-subtle: #063a3c;
|
||||
--bs-primary-text-emphasis: #99f6e4;
|
||||
--torrent-progress-complete: #34d399;
|
||||
--pytorrent-page-bg: radial-gradient(circle at top left, rgba(45, 212, 191, 0.16), transparent 36%), #031316;
|
||||
--pytorrent-shell-shadow: 0 18px 55px rgba(0, 20, 24, 0.58);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
--bs-btn-bg: var(--bs-primary);
|
||||
--bs-btn-border-color: var(--bs-primary);
|
||||
--bs-btn-hover-bg: var(--bs-primary-text-emphasis);
|
||||
--bs-btn-hover-border-color: var(--bs-primary-text-emphasis);
|
||||
}
|
||||
.btn-outline-primary {
|
||||
--bs-btn-color: var(--bs-primary);
|
||||
--bs-btn-border-color: rgba(var(--bs-primary-rgb), 0.72);
|
||||
--bs-btn-hover-bg: var(--bs-primary);
|
||||
--bs-btn-hover-border-color: var(--bs-primary);
|
||||
}
|
||||
.nav-pills {
|
||||
--bs-nav-pills-link-active-bg: var(--bs-primary);
|
||||
}
|
||||
.progress,
|
||||
.progress-stacked {
|
||||
--bs-progress-bg: rgba(var(--bs-secondary-bg-rgb), 0.82);
|
||||
}
|
||||
66
pytorrent/static/libs/pytorrent-themes/sky/bootstrap.min.css
vendored
Normal file
66
pytorrent/static/libs/pytorrent-themes/sky/bootstrap.min.css
vendored
Normal file
@@ -0,0 +1,66 @@
|
||||
/* Fresh lighter blue theme with a readable dark variant. */
|
||||
:root, [data-bs-theme="light"] {
|
||||
color-scheme: light;
|
||||
--bs-primary: #0284c7;
|
||||
--bs-primary-rgb: 2,132,199;
|
||||
--bs-link-color: #0284c7;
|
||||
--bs-link-color-rgb: 2,132,199;
|
||||
--bs-link-hover-color: #0369a1;
|
||||
--bs-link-hover-color-rgb: 2,132,199;
|
||||
--bs-body-bg: #f2f9ff;
|
||||
--bs-body-bg-rgb: 242,249,255;
|
||||
--bs-body-color: #102033;
|
||||
--bs-secondary-bg: #ffffff;
|
||||
--bs-secondary-bg-rgb: 255,255,255;
|
||||
--bs-tertiary-bg: #dff1ff;
|
||||
--bs-border-color: #bddff5;
|
||||
--bs-secondary-color: #567185;
|
||||
--bs-primary-bg-subtle: #d7efff;
|
||||
--bs-primary-text-emphasis: #0369a1;
|
||||
--torrent-progress-complete: #10b981;
|
||||
--pytorrent-page-bg: linear-gradient(180deg, #f8fcff 0%, #e5f4ff 100%);
|
||||
--pytorrent-shell-shadow: 0 16px 38px rgba(3, 105, 161, 0.13);
|
||||
}
|
||||
|
||||
[data-bs-theme="dark"] {
|
||||
color-scheme: dark;
|
||||
--bs-primary: #38bdf8;
|
||||
--bs-primary-rgb: 56,189,248;
|
||||
--bs-link-color: #38bdf8;
|
||||
--bs-link-color-rgb: 56,189,248;
|
||||
--bs-link-hover-color: #bae6fd;
|
||||
--bs-link-hover-color-rgb: 56,189,248;
|
||||
--bs-body-bg: #06111a;
|
||||
--bs-body-bg-rgb: 6,17,26;
|
||||
--bs-body-color: #d9ecf8;
|
||||
--bs-secondary-bg: #0d1c29;
|
||||
--bs-secondary-bg-rgb: 13,28,41;
|
||||
--bs-tertiary-bg: #12283a;
|
||||
--bs-border-color: #21435b;
|
||||
--bs-secondary-color: #91b4ca;
|
||||
--bs-primary-bg-subtle: #0b334d;
|
||||
--bs-primary-text-emphasis: #bae6fd;
|
||||
--torrent-progress-complete: #34d399;
|
||||
--pytorrent-page-bg: radial-gradient(circle at top left, rgba(56, 189, 248, 0.15), transparent 36%), #06111a;
|
||||
--pytorrent-shell-shadow: 0 18px 55px rgba(0, 13, 24, 0.58);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
--bs-btn-bg: var(--bs-primary);
|
||||
--bs-btn-border-color: var(--bs-primary);
|
||||
--bs-btn-hover-bg: var(--bs-primary-text-emphasis);
|
||||
--bs-btn-hover-border-color: var(--bs-primary-text-emphasis);
|
||||
}
|
||||
.btn-outline-primary {
|
||||
--bs-btn-color: var(--bs-primary);
|
||||
--bs-btn-border-color: rgba(var(--bs-primary-rgb), 0.72);
|
||||
--bs-btn-hover-bg: var(--bs-primary);
|
||||
--bs-btn-hover-border-color: var(--bs-primary);
|
||||
}
|
||||
.nav-pills {
|
||||
--bs-nav-pills-link-active-bg: var(--bs-primary);
|
||||
}
|
||||
.progress,
|
||||
.progress-stacked {
|
||||
--bs-progress-bg: rgba(var(--bs-secondary-bg-rgb), 0.82);
|
||||
}
|
||||
@@ -7,6 +7,8 @@
|
||||
--mobile-filterbar-height: 132px;
|
||||
--sidebar: calc(270px * var(--ui-scale));
|
||||
--torrent-progress-complete: #198754;
|
||||
--pytorrent-page-bg: var(--bs-body-bg);
|
||||
--pytorrent-shell-shadow: 0 12px 45px rgba(0, 0, 0, 0.38);
|
||||
}
|
||||
[data-bs-theme="dark"] {
|
||||
--bs-body-bg: #05070a;
|
||||
@@ -20,6 +22,7 @@
|
||||
--bs-primary-bg-subtle: #0d2238;
|
||||
--bs-primary-text-emphasis: #9ecbff;
|
||||
--torrent-progress-complete: #2f9e75;
|
||||
--pytorrent-page-bg: var(--bs-body-bg);
|
||||
}
|
||||
|
||||
html[data-app-font="adwaita-mono"] {
|
||||
@@ -109,7 +112,7 @@ body {
|
||||
min-height: 100vh;
|
||||
min-height: 100dvh;
|
||||
padding: calc(8px * var(--ui-scale));
|
||||
background: #05070a;
|
||||
background: var(--pytorrent-page-bg, var(--bs-body-bg));
|
||||
font-family: var(--app-font-family);
|
||||
}
|
||||
.app-shell {
|
||||
@@ -121,7 +124,7 @@ body {
|
||||
border: 1px solid var(--bs-border-color);
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 12px 45px rgba(0, 0, 0, 0.38);
|
||||
box-shadow: var(--pytorrent-shell-shadow, 0 12px 45px rgba(0, 0, 0, 0.38));
|
||||
}
|
||||
.topbar {
|
||||
display: flex;
|
||||
@@ -1524,6 +1527,10 @@ body.mobile-mode .mobile-card {
|
||||
padding-bottom: 0.1rem;
|
||||
padding-top: 0.1rem;
|
||||
}
|
||||
.file-priority-table .file-media-info-blocked {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.55;
|
||||
}
|
||||
.file-priority-table .file-check,
|
||||
.file-priority-table #fileSelectAll {
|
||||
display: block;
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
<title>pyTorrent</title>
|
||||
<link rel="icon" href="{{ static_url('favicon.svg') }}" type="image/svg+xml">
|
||||
<link rel="shortcut icon" href="{{ static_url('favicon.svg') }}" type="image/svg+xml">
|
||||
<link id="bootstrapBaseStylesheet" href="{{ bootstrap_theme_url('default') }}" rel="stylesheet">
|
||||
<link id="bootstrapThemeStylesheet" href="{{ bootstrap_theme_url(prefs.bootstrap_theme if prefs else 'default') }}" rel="stylesheet">
|
||||
<link href="{{ frontend_asset_url('fontawesome_css') }}" rel="stylesheet">
|
||||
<link href="{{ frontend_asset_url('font_css') }}" rel="stylesheet">
|
||||
|
||||
@@ -41,18 +41,71 @@ def google_fonts_css_url() -> str:
|
||||
return f"https://fonts.googleapis.com/css2?{families}&display=swap"
|
||||
|
||||
|
||||
BOOTSTRAP_THEMES = (
|
||||
"default",
|
||||
"flatly",
|
||||
"litera",
|
||||
"lumen",
|
||||
"minty",
|
||||
"sketchy",
|
||||
"solar",
|
||||
"spacelab",
|
||||
"united",
|
||||
"zephyr",
|
||||
)
|
||||
DEVEXPRESS_BOOTSTRAP_THEMES = {
|
||||
"blazing-berry": "Blazing Berry",
|
||||
"office-white": "Office White",
|
||||
"purple": "Purple",
|
||||
}
|
||||
|
||||
PYTORRENT_APP_THEMES = {
|
||||
"adaptive": "pyTorrent Adaptive",
|
||||
"ocean": "pyTorrent Ocean",
|
||||
"graphite": "pyTorrent Graphite",
|
||||
"forest": "pyTorrent Forest",
|
||||
"amber": "pyTorrent Amber",
|
||||
"nord": "pyTorrent Nord",
|
||||
"crimson": "pyTorrent Crimson",
|
||||
"sky": "pyTorrent Sky",
|
||||
}
|
||||
|
||||
|
||||
BOOTSTRAP_THEME_DEFINITIONS = {
|
||||
"default": {
|
||||
"label": "Default Bootstrap",
|
||||
"local": f"{LIBS_STATIC_DIR}/bootstrap/{BOOTSTRAP_VERSION}/css/bootstrap.min.css",
|
||||
"cdn": f"https://cdn.jsdelivr.net/npm/bootstrap@{BOOTSTRAP_VERSION}/dist/css/bootstrap.min.css",
|
||||
},
|
||||
# Bootswatch themes.
|
||||
"flatly": {"label": "Bootswatch: Flatly", "provider": "bootswatch"},
|
||||
"litera": {"label": "Bootswatch: Litera", "provider": "bootswatch"},
|
||||
"lumen": {"label": "Bootswatch: Lumen", "provider": "bootswatch"},
|
||||
"minty": {"label": "Bootswatch: Minty", "provider": "bootswatch"},
|
||||
"sketchy": {"label": "Bootswatch: Sketchy", "provider": "bootswatch"},
|
||||
"spacelab": {"label": "Bootswatch: Spacelab", "provider": "bootswatch"},
|
||||
"united": {"label": "Bootswatch: United", "provider": "bootswatch"},
|
||||
"zephyr": {"label": "Bootswatch: Zephyr", "provider": "bootswatch"},
|
||||
# Complete DevExpress Bootstrap v5 dist.v5 set.
|
||||
**{
|
||||
f"dx-{theme}": {
|
||||
"label": f"DevExpress: {label}",
|
||||
"provider": "devexpress",
|
||||
"local": f"{LIBS_STATIC_DIR}/devexpress-bootstrap-themes/dist.v5/{theme}/bootstrap.min.css",
|
||||
"cdn": f"https://cdn.jsdelivr.net/gh/DevExpress/bootstrap-themes@master/dist.v5/{theme}/bootstrap.min.css",
|
||||
}
|
||||
for theme, label in DEVEXPRESS_BOOTSTRAP_THEMES.items()
|
||||
},
|
||||
# App-specific Bootstrap variable overrides. These sit on top of default Bootstrap.
|
||||
**{
|
||||
f"pytorrent-{theme}": {
|
||||
"label": f"Custom: {label}",
|
||||
"provider": "pytorrent",
|
||||
"local": f"{LIBS_STATIC_DIR}/pytorrent-themes/{theme}/bootstrap.min.css",
|
||||
"cdn": f"/static/{LIBS_STATIC_DIR}/pytorrent-themes/{theme}/bootstrap.min.css",
|
||||
}
|
||||
for theme, label in PYTORRENT_APP_THEMES.items()
|
||||
},
|
||||
}
|
||||
|
||||
def _theme_definition(theme: str | None) -> dict[str, str]:
|
||||
theme = theme if theme in BOOTSTRAP_THEME_DEFINITIONS else "default"
|
||||
item = dict(BOOTSTRAP_THEME_DEFINITIONS[theme])
|
||||
if item.get("provider") == "bootswatch":
|
||||
item["local"] = f"{LIBS_STATIC_DIR}/bootswatch/{BOOTSWATCH_VERSION}/{theme}/bootstrap.min.css"
|
||||
item["cdn"] = f"https://cdn.jsdelivr.net/npm/bootswatch@{BOOTSWATCH_VERSION}/dist/{theme}/bootstrap.min.css"
|
||||
return item
|
||||
|
||||
|
||||
BOOTSTRAP_THEMES = tuple(BOOTSTRAP_THEME_DEFINITIONS.keys())
|
||||
STATIC_ASSETS = {
|
||||
"bootstrap_js": {
|
||||
"local": f"{LIBS_STATIC_DIR}/bootstrap/{BOOTSTRAP_VERSION}/js/bootstrap.bundle.min.js",
|
||||
@@ -88,15 +141,8 @@ ANY_URL_RE = re.compile(r"url\((['\"]?)(?!data:)([^)'\"]+)\1\)")
|
||||
|
||||
|
||||
def bootstrap_css_asset(theme: str) -> dict[str, str]:
|
||||
if theme == "default":
|
||||
return {
|
||||
"local": f"{LIBS_STATIC_DIR}/bootstrap/{BOOTSTRAP_VERSION}/css/bootstrap.min.css",
|
||||
"cdn": f"https://cdn.jsdelivr.net/npm/bootstrap@{BOOTSTRAP_VERSION}/dist/css/bootstrap.min.css",
|
||||
}
|
||||
return {
|
||||
"local": f"{LIBS_STATIC_DIR}/bootswatch/{BOOTSWATCH_VERSION}/{theme}/bootstrap.min.css",
|
||||
"cdn": f"https://cdn.jsdelivr.net/npm/bootswatch@{BOOTSWATCH_VERSION}/dist/{theme}/bootstrap.min.css",
|
||||
}
|
||||
item = _theme_definition(theme)
|
||||
return {"local": item["local"], "cdn": item["cdn"]}
|
||||
|
||||
|
||||
def download(url: str, dest: Path) -> None:
|
||||
@@ -169,7 +215,11 @@ def main() -> None:
|
||||
for item in items:
|
||||
url = item["cdn"]
|
||||
dest = ROOT / "pytorrent" / "static" / item["local"]
|
||||
if item.get("local") == STATIC_ASSETS["font_css"]["local"]:
|
||||
if url.startswith("/static/"):
|
||||
if not dest.is_file() or dest.stat().st_size <= 0:
|
||||
raise RuntimeError(f"Bundled app theme is missing: {dest.relative_to(ROOT)}")
|
||||
print(f"OK {dest.relative_to(ROOT)}")
|
||||
elif item.get("local") == STATIC_ASSETS["font_css"]["local"]:
|
||||
download_google_fonts_css(url, dest)
|
||||
elif dest.suffix == ".css":
|
||||
download_css_with_assets(url, dest)
|
||||
|
||||
Reference in New Issue
Block a user