mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-06-16 04:27:09 +08:00
903 lines
37 KiB
Python
903 lines
37 KiB
Python
#!/usr/bin/env python3
|
|
import glob
|
|
import io
|
|
import json
|
|
import os
|
|
import random
|
|
import requests
|
|
import shutil
|
|
import subprocess
|
|
import zipfile
|
|
|
|
from datetime import date, timedelta
|
|
from dateutil import easter
|
|
from pathlib import Path
|
|
from urllib.parse import quote_plus
|
|
|
|
from openpilot.starpilot.common.starpilot_download_utilities import GITLAB_URL, download_file, get_repository_url, handle_error, verify_download
|
|
from openpilot.starpilot.common.theme_asset_names import find_matching_theme_asset_file, find_matching_theme_asset_name
|
|
from openpilot.starpilot.common.starpilot_utilities import delete_file, extract_zip, load_json_file, update_json_file
|
|
from openpilot.starpilot.common.starpilot_variables import ACTIVE_THEME_PATH, RANDOM_EVENTS_PATH, RESOURCES_REPO, THEME_SAVE_PATH
|
|
|
|
CANCEL_DOWNLOAD_PARAM = "CancelThemeDownload"
|
|
DOWNLOAD_PROGRESS_PARAM = "ThemeDownloadProgress"
|
|
|
|
HOLIDAY_THEME_PATH = Path(__file__).parent / "holiday_themes"
|
|
STOCKOP_THEME_PATH = Path(__file__).parent / "stock_theme"
|
|
LOCAL_RESOURCES_PATH = Path(os.getenv("STARPILOT_LOCAL_RESOURCES_PATH", "~/StarPilot-Resources")).expanduser()
|
|
|
|
HOLIDAY_SLUGS = {
|
|
"new_years": "New Year's",
|
|
"valentines_day": "Valentine's Day",
|
|
"st_patricks_day": "St. Patrick's Day",
|
|
"world_frog_day": "World Frog Day",
|
|
"april_fools": "April Fools",
|
|
"easter_week": "Easter",
|
|
"may_the_fourth": "May the Fourth",
|
|
"cinco_de_mayo": "Cinco de Mayo",
|
|
"stitch_day": "Stitch Day",
|
|
"fourth_of_july": "Fourth of July",
|
|
"halloween_week": "Halloween",
|
|
"thanksgiving_week": "Thanksgiving",
|
|
"christmas_week": "Christmas"
|
|
}
|
|
|
|
THEME_COMPONENT_PARAMS = {
|
|
"boot_logos": "BootLogoToDownload",
|
|
"colors": "ColorToDownload",
|
|
"distance_icons": "DistanceIconToDownload",
|
|
"icons": "IconToDownload",
|
|
"signals": "SignalToDownload",
|
|
"sounds": "SoundToDownload",
|
|
"steering_wheels": "WheelToDownload"
|
|
}
|
|
|
|
class ThemeManager:
|
|
def __init__(self, params, params_memory, boot_run=False):
|
|
self.params = params
|
|
self.params_memory = params_memory
|
|
|
|
self.downloading_theme = False
|
|
self.theme_updated = False
|
|
|
|
self.holiday_theme = "stock"
|
|
|
|
self.previous_asset_mappings = {}
|
|
|
|
self.theme_sizes_path = THEME_SAVE_PATH / "theme_sizes.json"
|
|
|
|
# Ensure theme storage layout exists on desktop and device alike.
|
|
(THEME_SAVE_PATH / "bootlogos").mkdir(parents=True, exist_ok=True)
|
|
(THEME_SAVE_PATH / "theme_packs").mkdir(parents=True, exist_ok=True)
|
|
(THEME_SAVE_PATH / "steering_wheels").mkdir(parents=True, exist_ok=True)
|
|
self.sync_local_resources()
|
|
|
|
self.theme_sizes = load_json_file(self.theme_sizes_path)
|
|
|
|
self.session = requests.Session()
|
|
self.session.headers.update({
|
|
"Accept": "application/vnd.github.v3+json",
|
|
"Accept-Language": "en",
|
|
"User-Agent": "starpilot-theme-downloader/1.0 (https://github.com/FrogAi/StarPilot)"
|
|
})
|
|
|
|
if boot_run:
|
|
self.copy_default_theme()
|
|
|
|
@staticmethod
|
|
def _local_resources_available():
|
|
return LOCAL_RESOURCES_PATH.is_dir() and (LOCAL_RESOURCES_PATH / ".git").exists()
|
|
|
|
@staticmethod
|
|
def _git_list_tree(ref):
|
|
result = subprocess.run(
|
|
["git", "-C", str(LOCAL_RESOURCES_PATH), "ls-tree", "-r", "--name-only", ref],
|
|
capture_output=True,
|
|
text=True,
|
|
check=True,
|
|
)
|
|
return [line for line in result.stdout.splitlines() if line]
|
|
|
|
@staticmethod
|
|
def _git_show_bytes(ref, path):
|
|
result = subprocess.run(
|
|
["git", "-C", str(LOCAL_RESOURCES_PATH), "show", f"{ref}:{path}"],
|
|
capture_output=True,
|
|
check=True,
|
|
)
|
|
return result.stdout
|
|
|
|
@staticmethod
|
|
def _directory_has_files(path):
|
|
return path.is_dir() and any(path.iterdir())
|
|
|
|
@staticmethod
|
|
def _write_file_if_missing(destination, data):
|
|
if destination.exists() and destination.stat().st_size > 0:
|
|
return False
|
|
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
destination.write_bytes(data)
|
|
return True
|
|
|
|
@staticmethod
|
|
def _extract_zip_if_missing(zip_bytes, destination):
|
|
if ThemeManager._directory_has_files(destination):
|
|
return False
|
|
destination.mkdir(parents=True, exist_ok=True)
|
|
with zipfile.ZipFile(io.BytesIO(zip_bytes)) as archive:
|
|
archive.extractall(destination)
|
|
return True
|
|
|
|
def sync_local_resources(self):
|
|
if not self._local_resources_available():
|
|
return
|
|
|
|
try:
|
|
imported_assets = 0
|
|
|
|
for path in self._git_list_tree("Steering-Wheels"):
|
|
suffix = Path(path).suffix.lower()
|
|
if suffix not in {".gif", ".png", ".webp", ".jpg", ".jpeg"}:
|
|
continue
|
|
destination = THEME_SAVE_PATH / "steering_wheels" / Path(path).name
|
|
imported_assets += int(self._write_file_if_missing(destination, self._git_show_bytes("Steering-Wheels", path)))
|
|
|
|
for path in self._git_list_tree("Themes"):
|
|
path_obj = Path(path)
|
|
suffix = path_obj.suffix.lower()
|
|
|
|
if path_obj.parts[:1] == ("bootlogo",) and suffix in {".png", ".jpg", ".jpeg"}:
|
|
destination = THEME_SAVE_PATH / "bootlogos" / path_obj.name
|
|
imported_assets += int(self._write_file_if_missing(destination, self._git_show_bytes("Themes", path)))
|
|
continue
|
|
|
|
if suffix != ".zip" or len(path_obj.parts) != 2:
|
|
continue
|
|
|
|
theme_name, archive_name = path_obj.parts
|
|
component = Path(archive_name).stem.lower()
|
|
if component not in {"colors", "distance_icons", "icons", "signals", "sounds"}:
|
|
continue
|
|
|
|
destination = THEME_SAVE_PATH / "theme_packs" / theme_name / component
|
|
imported_assets += int(self._extract_zip_if_missing(self._git_show_bytes("Themes", path), destination))
|
|
|
|
for path in self._git_list_tree("Distance-Icons"):
|
|
path_obj = Path(path)
|
|
if path_obj.suffix.lower() != ".zip":
|
|
continue
|
|
destination = THEME_SAVE_PATH / "theme_packs" / path_obj.stem / "distance_icons"
|
|
imported_assets += int(self._extract_zip_if_missing(self._git_show_bytes("Distance-Icons", path), destination))
|
|
|
|
if imported_assets:
|
|
print(f"Imported {imported_assets} local theme assets from {LOCAL_RESOURCES_PATH}")
|
|
except (FileNotFoundError, subprocess.CalledProcessError, zipfile.BadZipFile, OSError) as error:
|
|
print(f"Failed to sync local theme resources from {LOCAL_RESOURCES_PATH}: {error}")
|
|
|
|
@staticmethod
|
|
def calculate_thanksgiving(year):
|
|
november_first = date(year, 11, 1)
|
|
days_to_thursday = (3 - november_first.weekday()) % 7
|
|
first_thursday = november_first + timedelta(days=days_to_thursday)
|
|
return first_thursday + timedelta(days=21)
|
|
|
|
@staticmethod
|
|
def copy_default_theme():
|
|
world_frog_day_theme_path = HOLIDAY_THEME_PATH / "world_frog_day"
|
|
|
|
for theme_subfolder_name, save_subfolder_path in [
|
|
("colors", "theme_packs/frog/colors"),
|
|
("distance_icons", "theme_packs/frog-animated/distance_icons"),
|
|
("icons", "theme_packs/frog-animated/icons"),
|
|
("signals", "theme_packs/frog/signals"),
|
|
("sounds", "theme_packs/frog/sounds"),
|
|
]:
|
|
source_folder_path = world_frog_day_theme_path / theme_subfolder_name
|
|
destination_folder_path = THEME_SAVE_PATH / save_subfolder_path
|
|
destination_folder_path.mkdir(parents=True, exist_ok=True)
|
|
shutil.copytree(source_folder_path, destination_folder_path, dirs_exist_ok=True)
|
|
|
|
steering_wheel_image_path = world_frog_day_theme_path / "steering_wheel/wheel.png"
|
|
steering_wheel_save_path = THEME_SAVE_PATH / "steering_wheels/frog.png"
|
|
steering_wheel_save_path.parent.mkdir(parents=True, exist_ok=True)
|
|
shutil.copy2(steering_wheel_image_path, steering_wheel_save_path)
|
|
|
|
default_boot_logo_path = Path(__file__).parent / "other_images/starpilot_boot_logo.jpg"
|
|
boot_logo_save_path = THEME_SAVE_PATH / "bootlogos/starpilot.jpg"
|
|
boot_logo_save_path.parent.mkdir(parents=True, exist_ok=True)
|
|
if default_boot_logo_path.exists():
|
|
should_refresh_default_boot_logo = not boot_logo_save_path.exists()
|
|
if not should_refresh_default_boot_logo:
|
|
try:
|
|
should_refresh_default_boot_logo = default_boot_logo_path.read_bytes() != boot_logo_save_path.read_bytes()
|
|
except OSError:
|
|
should_refresh_default_boot_logo = True
|
|
|
|
if should_refresh_default_boot_logo:
|
|
shutil.copy2(default_boot_logo_path, boot_logo_save_path)
|
|
|
|
def download_theme(self, theme_component, theme_name, asset_param, starpilot_toggles):
|
|
self.downloading_theme = True
|
|
|
|
repo_url = get_repository_url(self.session)
|
|
if not repo_url:
|
|
handle_error(None, asset_param, "Repository unavailable", "GitHub and GitLab are offline...", self.params_memory, DOWNLOAD_PROGRESS_PARAM)
|
|
self.downloading_theme = False
|
|
return
|
|
|
|
if theme_component == "boot_logos":
|
|
download_link = f"{repo_url}/Themes/bootlogo"
|
|
download_path = THEME_SAVE_PATH / "bootlogos" / theme_name
|
|
extensions = [".png", ".jpg", ".jpeg"]
|
|
name_candidates = list(dict.fromkeys([theme_name, theme_name.replace("_", "-"), theme_name.replace("-", "_")]))
|
|
elif theme_component == "distance_icons":
|
|
download_link = f"{repo_url}/Distance-Icons/{theme_name}"
|
|
download_path = THEME_SAVE_PATH / "theme_packs" / theme_name / theme_component
|
|
extensions = [".zip"]
|
|
name_candidates = [theme_name]
|
|
elif theme_component == "steering_wheels":
|
|
download_link = f"{repo_url}/Steering-Wheels/{theme_name}"
|
|
download_path = THEME_SAVE_PATH / theme_component / theme_name
|
|
extensions = [".gif", ".png"]
|
|
name_candidates = [theme_name]
|
|
else:
|
|
download_link = f"{repo_url}/Themes/{theme_name}/{theme_component}"
|
|
download_path = THEME_SAVE_PATH / "theme_packs" / theme_name / theme_component
|
|
extensions = [".zip"]
|
|
name_candidates = [theme_name]
|
|
|
|
for extension in extensions:
|
|
theme_path = download_path.with_suffix(extension)
|
|
theme_urls = [f"{download_link}/{candidate}{extension}" for candidate in name_candidates] if theme_component == "boot_logos" else [download_link + extension]
|
|
|
|
for theme_url in theme_urls:
|
|
delete_file(theme_path)
|
|
|
|
print(f"Downloading theme from GitHub: {theme_name}")
|
|
download_file(CANCEL_DOWNLOAD_PARAM, theme_path, asset_param, self.params_memory, DOWNLOAD_PROGRESS_PARAM, self.session, theme_url)
|
|
|
|
if self.params_memory.get_bool(CANCEL_DOWNLOAD_PARAM):
|
|
delete_file(theme_path)
|
|
handle_error(None, asset_param, "Download cancelled...", "Download cancelled...", self.params_memory, DOWNLOAD_PROGRESS_PARAM)
|
|
|
|
self.downloading_theme = False
|
|
return
|
|
|
|
if verify_download(theme_path, self.params_memory, self.session, theme_url):
|
|
print(f"Theme {theme_name} downloaded and verified successfully from GitHub!")
|
|
self.update_theme_size(theme_component, theme_name, theme_path.stat().st_size)
|
|
|
|
if extension == ".zip":
|
|
self.params_memory.put(DOWNLOAD_PROGRESS_PARAM, "Unpacking theme...")
|
|
extract_zip(theme_path, download_path)
|
|
|
|
self.params_memory.put(DOWNLOAD_PROGRESS_PARAM, "Downloaded!")
|
|
self.params_memory.remove(asset_param)
|
|
|
|
self.downloading_theme = False
|
|
|
|
self.update_themes(starpilot_toggles)
|
|
return
|
|
|
|
if self.handle_verification_failure(extension, theme_component, theme_name, asset_param, theme_path, download_path, starpilot_toggles):
|
|
return
|
|
|
|
handle_error(download_path, asset_param, "Download failed...", "Download failed...", self.params_memory, DOWNLOAD_PROGRESS_PARAM)
|
|
self.downloading_theme = False
|
|
|
|
def fetch_assets(self, repo_url, starpilot_toggles):
|
|
is_github = "github" in repo_url
|
|
is_gitlab = "gitlab" in repo_url
|
|
|
|
repo_encoded = quote_plus(RESOURCES_REPO)
|
|
|
|
assets = {"boot_logos": [], "themes": {}, "wheels": []}
|
|
try:
|
|
def list_files(branch):
|
|
if is_github:
|
|
response = self.session.get(f"https://api.github.com/repos/{RESOURCES_REPO}/git/trees/{branch}?recursive=1", timeout=10)
|
|
response.raise_for_status()
|
|
return [
|
|
{
|
|
"path": item.get("path", ""),
|
|
"name": Path(item.get("path", "")).name,
|
|
"type": item.get("type"),
|
|
"size": item.get("size", 0),
|
|
}
|
|
for item in response.json().get("tree", [])
|
|
if item.get("type") == "blob"
|
|
]
|
|
if is_gitlab:
|
|
response = self.session.get(f"https://gitlab.com/api/v4/projects/{repo_encoded}/repository/tree?ref={branch}&recursive=true", timeout=10)
|
|
response.raise_for_status()
|
|
return [
|
|
{
|
|
"path": item.get("path", ""),
|
|
"name": item.get("name", ""),
|
|
"type": item.get("type"),
|
|
"size": 0,
|
|
}
|
|
for item in response.json()
|
|
if item.get("type") in ("blob", "file")
|
|
]
|
|
print(f"Unsupported repository URL: {repo_url}")
|
|
return []
|
|
|
|
def file_size(branch, path, fallback):
|
|
if is_github:
|
|
return int(fallback or 0)
|
|
response = self.session.head(f"https://gitlab.com/api/v4/projects/{repo_encoded}/repository/files/{quote_plus(path)}/raw?ref={branch}", timeout=10)
|
|
return int(response.headers.get("content-length", 0)) if response.ok else 0
|
|
|
|
for branch in ["Distance-Icons", "Steering-Wheels"]:
|
|
for item in list_files(branch):
|
|
if item.get("type") not in ("file", "blob"):
|
|
continue
|
|
|
|
path = item["path"]
|
|
size = file_size(branch, path, item.get("size", 0))
|
|
|
|
if branch == "Steering-Wheels":
|
|
assets["wheels"].append(path)
|
|
theme_name = Path(path).stem
|
|
local_files = list((THEME_SAVE_PATH / "steering_wheels").glob(f"{theme_name}.*"))
|
|
if local_files and size > 0:
|
|
local_size = self.theme_sizes.get("wheels", {}).get(theme_name)
|
|
if local_size != size:
|
|
self.download_theme("steering_wheels", theme_name, THEME_COMPONENT_PARAMS["steering_wheels"], starpilot_toggles)
|
|
|
|
elif branch == "Distance-Icons":
|
|
component_name = "distance_icons"
|
|
theme_name = Path(path).stem
|
|
assets["themes"].setdefault(theme_name, set()).add(component_name)
|
|
|
|
local_path = THEME_SAVE_PATH / "theme_packs" / theme_name / component_name
|
|
if local_path.exists() and size > 0:
|
|
local_size = self.theme_sizes.get("themes", {}).get(theme_name, {}).get(component_name)
|
|
if local_size != size:
|
|
self.download_theme(component_name, theme_name, THEME_COMPONENT_PARAMS[component_name], starpilot_toggles)
|
|
|
|
branch = "Themes"
|
|
for item in list_files(branch):
|
|
if item.get("type") not in ("file", "blob") or "/" not in item["path"]:
|
|
continue
|
|
|
|
expected_size = file_size(branch, item["path"], item.get("size", 0))
|
|
|
|
theme_name, sub_path = item["path"].split("/", 1)
|
|
theme_path = sub_path.lower()
|
|
|
|
if theme_name.lower() == "bootlogo":
|
|
if Path(sub_path).suffix.lower() not in (".png", ".jpg", ".jpeg"):
|
|
continue
|
|
|
|
assets["boot_logos"].append(sub_path)
|
|
logo_name = Path(sub_path).stem
|
|
local_files = list((THEME_SAVE_PATH / "bootlogos").glob(f"{logo_name}.*"))
|
|
if local_files and expected_size > 0:
|
|
local_size = self.theme_sizes.get("boot_logos", {}).get(logo_name)
|
|
if local_size != expected_size:
|
|
print(f"boot logo {logo_name} is outdated, redownloading...")
|
|
self.download_theme("boot_logos", logo_name, THEME_COMPONENT_PARAMS["boot_logos"], starpilot_toggles)
|
|
continue
|
|
|
|
for key in ("colors", "icons", "signals", "sounds"):
|
|
if key in theme_path:
|
|
assets["themes"].setdefault(theme_name, set()).add(key)
|
|
|
|
local_path = THEME_SAVE_PATH / "theme_packs" / theme_name / key
|
|
if local_path.exists():
|
|
local_size = self.theme_sizes.get("themes", {}).get(theme_name, {}).get(key)
|
|
if local_size != expected_size:
|
|
print(f"{key} {theme_name} is outdated, redownloading...")
|
|
self.download_theme(key, theme_name, THEME_COMPONENT_PARAMS[key], starpilot_toggles)
|
|
break
|
|
|
|
assets["boot_logos"].sort()
|
|
assets["themes"] = {key: sorted(list(value)) for key, value in assets["themes"].items()}
|
|
assets["wheels"].sort()
|
|
return assets
|
|
|
|
except requests.exceptions.RequestException as error:
|
|
print(f"Failed to fetch theme sizes from {'GitHub' if is_github else 'GitLab'}: {error}")
|
|
return {}
|
|
|
|
@staticmethod
|
|
def format_name(name, component):
|
|
base = Path(name).stem
|
|
creator = ""
|
|
if "~" in base:
|
|
base, creator = base.split("~", 1)
|
|
|
|
parts = base.replace("_", " ").replace("-", " ").split()
|
|
display = " ".join(part.capitalize() for part in parts)
|
|
|
|
if creator:
|
|
return f"{display} - by: {creator}"
|
|
return display
|
|
|
|
@staticmethod
|
|
def get_full_themes():
|
|
theme_packs_path = THEME_SAVE_PATH / "theme_packs"
|
|
if not theme_packs_path.exists():
|
|
return []
|
|
|
|
valid_themes = set()
|
|
for theme_directory in theme_packs_path.iterdir():
|
|
if not theme_directory.is_dir():
|
|
continue
|
|
|
|
base_name = theme_directory.name.replace("-animated", "")
|
|
|
|
animated_path = theme_packs_path / f"{base_name}-animated"
|
|
base_path = theme_packs_path / base_name
|
|
|
|
base_valid = all((base_path / asset).is_dir() for asset in {"colors", "sounds"})
|
|
animated_icons_exist = (animated_path / "icons").is_dir()
|
|
base_icons_exist = (base_path / "icons").is_dir()
|
|
|
|
if base_valid and (animated_icons_exist or base_icons_exist):
|
|
if animated_icons_exist:
|
|
valid_themes.add(f"{base_name}-animated")
|
|
else:
|
|
valid_themes.add(base_name)
|
|
|
|
return sorted(valid_themes)
|
|
|
|
@staticmethod
|
|
def get_holiday_theme_dates(year):
|
|
return {
|
|
"new_years": date(year, 1, 1),
|
|
"valentines_day": date(year, 2, 14),
|
|
"st_patricks_day": date(year, 3, 17),
|
|
"world_frog_day": date(year, 3, 20),
|
|
"april_fools": date(year, 4, 1),
|
|
"easter_week": easter.easter(year),
|
|
"may_the_fourth": date(year, 5, 4),
|
|
"cinco_de_mayo": date(year, 5, 5),
|
|
"stitch_day": date(year, 6, 26),
|
|
"fourth_of_july": date(year, 7, 4),
|
|
"halloween_week": date(year, 10, 31),
|
|
"thanksgiving_week": ThemeManager.calculate_thanksgiving(year),
|
|
"christmas_week": date(year, 12, 25)
|
|
}
|
|
|
|
def handle_verification_failure(self, extension, theme_component, theme_name, asset_param, theme_path, download_path, starpilot_toggles):
|
|
if theme_component == "boot_logos":
|
|
download_link = f"{GITLAB_URL}/Themes/bootlogo"
|
|
name_candidates = list(dict.fromkeys([theme_name, theme_name.replace("_", "-"), theme_name.replace("-", "_")]))
|
|
elif theme_component == "distance_icons":
|
|
download_link = f"{GITLAB_URL}/Distance-Icons/{theme_name}"
|
|
name_candidates = [theme_name]
|
|
elif theme_component == "steering_wheels":
|
|
download_link = f"{GITLAB_URL}/Steering-Wheels/{theme_name}"
|
|
name_candidates = [theme_name]
|
|
else:
|
|
download_link = f"{GITLAB_URL}/Themes/{theme_name}/{theme_component}"
|
|
name_candidates = [theme_name]
|
|
|
|
for candidate in name_candidates:
|
|
delete_file(theme_path)
|
|
|
|
theme_url = f"{download_link}/{candidate}{extension}" if theme_component == "boot_logos" else download_link + extension
|
|
print(f"Downloading theme from GitLab: {theme_name}")
|
|
download_file(CANCEL_DOWNLOAD_PARAM, theme_path, asset_param, self.params_memory, DOWNLOAD_PROGRESS_PARAM, self.session, theme_url)
|
|
|
|
if verify_download(theme_path, self.params_memory, self.session, theme_url):
|
|
print(f"Theme {theme_name} downloaded and verified successfully from GitLab!")
|
|
self.update_theme_size(theme_component, theme_name, theme_path.stat().st_size)
|
|
|
|
if extension == ".zip":
|
|
self.params_memory.put(DOWNLOAD_PROGRESS_PARAM, "Unpacking theme...")
|
|
extract_zip(theme_path, download_path)
|
|
|
|
self.params_memory.put(DOWNLOAD_PROGRESS_PARAM, "Downloaded!")
|
|
self.params_memory.remove(asset_param)
|
|
|
|
self.downloading_theme = False
|
|
|
|
self.update_themes(starpilot_toggles)
|
|
return True
|
|
|
|
return False
|
|
|
|
@staticmethod
|
|
def is_within_week_of(target_date, current_date):
|
|
start_of_week = target_date - timedelta(days=target_date.weekday())
|
|
return start_of_week <= current_date < target_date
|
|
|
|
@staticmethod
|
|
def randomize_distance_icons(available_themes, selected_theme):
|
|
theme_packs_path = THEME_SAVE_PATH / "theme_packs"
|
|
if not theme_packs_path.exists():
|
|
return "stock"
|
|
|
|
candidates = []
|
|
for theme_pack in theme_packs_path.iterdir():
|
|
if not theme_pack.is_dir():
|
|
continue
|
|
|
|
distance_icons_dir = theme_pack / "distance_icons"
|
|
if not distance_icons_dir.is_dir():
|
|
continue
|
|
|
|
icon_name = theme_pack.name.lower()
|
|
|
|
theme_association = [theme for theme in available_themes if theme.replace("-animated", "") in icon_name]
|
|
if theme_association and selected_theme not in icon_name:
|
|
continue
|
|
|
|
weight = 5 if selected_theme in icon_name else 1
|
|
candidates.extend([theme_pack.name] * weight)
|
|
|
|
return random.choice(candidates) if candidates else "stock"
|
|
|
|
@staticmethod
|
|
def randomize_theme_asset(available_themes):
|
|
if not available_themes:
|
|
return "stock"
|
|
|
|
return random.choice(available_themes)
|
|
|
|
@staticmethod
|
|
def randomize_wheel_image(available_themes, selected_theme):
|
|
steering_wheels_path = THEME_SAVE_PATH / "steering_wheels"
|
|
if not steering_wheels_path.exists():
|
|
return "stock"
|
|
|
|
candidates = []
|
|
for wheel_file in steering_wheels_path.iterdir():
|
|
if not wheel_file.is_file():
|
|
continue
|
|
|
|
name = wheel_file.stem.lower()
|
|
|
|
theme_association = [theme for theme in available_themes if theme.replace("-animated", "") in name]
|
|
if theme_association and selected_theme not in name:
|
|
continue
|
|
|
|
weight = 5 if selected_theme in name else 1
|
|
candidates.extend([wheel_file.stem] * weight)
|
|
|
|
return random.choice(candidates) if candidates else "stock"
|
|
|
|
def update_active_theme(self, time_validated, starpilot_toggles, boot_run=False, randomize_theme=False):
|
|
boot_logo = getattr(starpilot_toggles, "boot_logo", "starpilot")
|
|
|
|
if time_validated and starpilot_toggles.holiday_themes:
|
|
self.holiday_theme = self.update_holiday()
|
|
else:
|
|
self.holiday_theme = "stock"
|
|
|
|
if self.holiday_theme != "stock":
|
|
asset_mappings = {
|
|
"boot_logo": ("boot_logo", boot_logo),
|
|
"color_scheme": ("colors", self.holiday_theme),
|
|
"distance_icons": ("distance_icons", self.holiday_theme),
|
|
"icon_pack": ("icons", self.holiday_theme),
|
|
"sound_pack": ("sounds", self.holiday_theme),
|
|
"turn_signal_pack": ("signals", self.holiday_theme),
|
|
"wheel_image": ("wheel_image", self.holiday_theme)
|
|
}
|
|
elif (boot_run or randomize_theme) and starpilot_toggles.random_themes:
|
|
available_themes = self.get_full_themes()
|
|
|
|
if starpilot_toggles.random_themes_holidays:
|
|
available_themes.extend(HOLIDAY_SLUGS.keys())
|
|
|
|
selected_theme = self.randomize_theme_asset(available_themes)
|
|
|
|
asset_mappings = {
|
|
"boot_logo": ("boot_logo", boot_logo),
|
|
"color_scheme": ("colors", selected_theme.replace("-animated", "")),
|
|
"distance_icons": ("distance_icons", self.randomize_distance_icons(available_themes, selected_theme.replace("-animated", ""))),
|
|
"icon_pack": ("icons", selected_theme),
|
|
"sound_pack": ("sounds", selected_theme.replace("-animated", "")),
|
|
"turn_signal_pack": ("signals", selected_theme.replace("-animated", "")),
|
|
"wheel_image": ("wheel_image", self.randomize_wheel_image(available_themes, selected_theme.replace("-animated", "")))
|
|
}
|
|
elif not starpilot_toggles.random_themes:
|
|
asset_mappings = {
|
|
"boot_logo": ("boot_logo", boot_logo),
|
|
"color_scheme": ("colors", starpilot_toggles.color_scheme),
|
|
"distance_icons": ("distance_icons", starpilot_toggles.distance_icons),
|
|
"icon_pack": ("icons", starpilot_toggles.icon_pack),
|
|
"sound_pack": ("sounds", starpilot_toggles.sound_pack),
|
|
"turn_signal_pack": ("signals", starpilot_toggles.signal_icons),
|
|
"wheel_image": ("wheel_image", starpilot_toggles.wheel_image)
|
|
}
|
|
else:
|
|
return
|
|
|
|
if asset_mappings != self.previous_asset_mappings:
|
|
for asset, (asset_type, current_value) in asset_mappings.items():
|
|
print(f"Updating {asset}: {asset_type} with value {current_value}")
|
|
|
|
if asset_type == "boot_logo":
|
|
self.update_boot_logo(current_value)
|
|
elif asset_type == "wheel_image":
|
|
self.update_wheel_image(current_value, boot_run=boot_run)
|
|
else:
|
|
self.update_theme_asset(asset_type, current_value, boot_run=boot_run)
|
|
|
|
self.previous_asset_mappings = asset_mappings
|
|
|
|
self.theme_updated = True
|
|
|
|
def update_holiday(self):
|
|
current_date = date.today()
|
|
|
|
holidays = self.get_holiday_theme_dates(current_date.year)
|
|
for holiday, holiday_date in holidays.items():
|
|
if (holiday.endswith("_week") and self.is_within_week_of(holiday_date, current_date)) or (current_date == holiday_date):
|
|
return holiday
|
|
|
|
return "stock"
|
|
|
|
def update_theme_asset(self, asset_type, theme, boot_run=False):
|
|
save_location = ACTIVE_THEME_PATH / asset_type
|
|
|
|
if self.holiday_theme != "stock":
|
|
asset_location = HOLIDAY_THEME_PATH / self.holiday_theme / asset_type
|
|
elif theme in HOLIDAY_SLUGS:
|
|
asset_location = HOLIDAY_THEME_PATH / theme / asset_type
|
|
elif f"{theme}_week" in HOLIDAY_SLUGS:
|
|
asset_location = HOLIDAY_THEME_PATH / f"{theme}_week" / asset_type
|
|
else:
|
|
asset_location = THEME_SAVE_PATH / "theme_packs" / theme / asset_type
|
|
|
|
if not asset_location.exists() or theme == "stock":
|
|
asset_location = STOCKOP_THEME_PATH / asset_type
|
|
print(f"Using the stock {asset_type[:-1]} instead")
|
|
|
|
delete_file(save_location, print_error=not boot_run)
|
|
|
|
save_location.parent.mkdir(parents=True, exist_ok=True)
|
|
save_location.symlink_to(asset_location, target_is_directory=True)
|
|
print(f"Linked {save_location} to {asset_location}")
|
|
|
|
def update_theme_params(self, downloadable_boot_logos, downloadable_colors, downloadable_distance_icons, downloadable_icons, downloadable_signals, downloadable_sounds, downloadable_wheels):
|
|
boot_logos_dir = THEME_SAVE_PATH / "bootlogos"
|
|
theme_packs_dir = THEME_SAVE_PATH / "theme_packs"
|
|
steering_wheels_dir = THEME_SAVE_PATH / "steering_wheels"
|
|
boot_logos_dir.mkdir(parents=True, exist_ok=True)
|
|
theme_packs_dir.mkdir(parents=True, exist_ok=True)
|
|
steering_wheels_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
def update_param(key, assets, subfolder):
|
|
if subfolder == "boot_logos":
|
|
existing_assets = {item.stem.lower() for item in boot_logos_dir.glob("*") if item.is_file()}
|
|
pending_assets = [asset for asset in assets if asset.lower() not in existing_assets]
|
|
self.params.put(key, ",".join(sorted(set(pending_assets))))
|
|
print(f"{key} updated successfully")
|
|
return
|
|
if subfolder == "steering_wheels":
|
|
themes_path = steering_wheels_dir
|
|
existing_assets = {self.format_name(item.name, "steering_wheels") for item in themes_path.glob("*") if item.is_file()}
|
|
else:
|
|
themes_path = theme_packs_dir
|
|
existing_assets = {self.format_name(item.parent.name, subfolder) for item in themes_path.glob(f"*/{subfolder}") if item.is_dir()}
|
|
|
|
self.params.put(key, ",".join(sorted(set(assets) - existing_assets)))
|
|
print(f"{key} updated successfully")
|
|
|
|
update_param("DownloadableBootLogos", downloadable_boot_logos, "boot_logos")
|
|
update_param("DownloadableColors", downloadable_colors, "colors")
|
|
update_param("DownloadableDistanceIcons", downloadable_distance_icons, "distance_icons")
|
|
update_param("DownloadableIcons", downloadable_icons, "icons")
|
|
update_param("DownloadableSignals", downloadable_signals, "signals")
|
|
update_param("DownloadableSounds", downloadable_sounds, "sounds")
|
|
update_param("DownloadableWheels", downloadable_wheels, "steering_wheels")
|
|
|
|
downloaded_themes = {}
|
|
for theme_dir in theme_packs_dir.iterdir():
|
|
components = []
|
|
for component in ["colors", "distance_icons", "icons", "signals", "sounds"]:
|
|
if (theme_dir / component).is_dir():
|
|
components.append(component)
|
|
|
|
if components:
|
|
theme_name = self.format_name(theme_dir.name, "theme_packs")
|
|
downloaded_themes[theme_name] = sorted(components)
|
|
|
|
downloaded_boot_logos = []
|
|
for boot_logo_file in boot_logos_dir.iterdir():
|
|
if boot_logo_file.is_file():
|
|
downloaded_boot_logos.append(self.format_name(boot_logo_file.name, "boot_logos"))
|
|
|
|
downloaded_wheels = []
|
|
for wheel_file in steering_wheels_dir.iterdir():
|
|
if wheel_file.is_file():
|
|
downloaded_wheels.append(self.format_name(wheel_file.name, "steering_wheels"))
|
|
|
|
self.params.put("ThemesDownloaded", {
|
|
"boot_logos": sorted(downloaded_boot_logos),
|
|
"themes": {key: downloaded_themes[key] for key in sorted(downloaded_themes)},
|
|
"steering_wheels": sorted(downloaded_wheels)
|
|
})
|
|
|
|
print("ThemesDownloaded updated successfully")
|
|
|
|
def update_theme_size(self, theme_component, theme_name, file_size):
|
|
if theme_component == "boot_logos":
|
|
key = "boot_logos"
|
|
elif theme_component == "steering_wheels":
|
|
key = "wheels"
|
|
else:
|
|
key = "themes"
|
|
|
|
if key not in self.theme_sizes:
|
|
self.theme_sizes[key] = {}
|
|
|
|
if key in {"boot_logos", "wheels"}:
|
|
self.theme_sizes[key][theme_name] = file_size
|
|
else:
|
|
if theme_name not in self.theme_sizes[key]:
|
|
self.theme_sizes[key][theme_name] = {}
|
|
self.theme_sizes[key][theme_name][theme_component] = file_size
|
|
|
|
update_json_file(self.theme_sizes_path, self.theme_sizes)
|
|
|
|
def update_themes(self, starpilot_toggles, boot_run=False):
|
|
if self.downloading_theme:
|
|
return
|
|
|
|
self.sync_local_resources()
|
|
|
|
repo_url = get_repository_url(self.session)
|
|
if repo_url is None:
|
|
print("GitHub and GitLab are offline...")
|
|
self.update_theme_params([], [], [], [], [], [], [])
|
|
return
|
|
|
|
assets = self.fetch_assets(repo_url, starpilot_toggles)
|
|
if not assets:
|
|
return
|
|
|
|
downloadable_boot_logos = []
|
|
downloadable_colors = []
|
|
downloadable_distance_icons = []
|
|
downloadable_icons = []
|
|
downloadable_signals = []
|
|
downloadable_sounds = []
|
|
|
|
for theme, available_assets in assets["themes"].items():
|
|
theme_name = self.format_name(theme, "theme_packs")
|
|
print(f"Theme found: {theme_name}")
|
|
|
|
if "colors" in available_assets:
|
|
downloadable_colors.append(theme_name)
|
|
if "distance_icons" in available_assets:
|
|
downloadable_distance_icons.append(theme_name)
|
|
if "icons" in available_assets:
|
|
downloadable_icons.append(theme_name)
|
|
if "signals" in available_assets:
|
|
downloadable_signals.append(theme_name)
|
|
if "sounds" in available_assets:
|
|
downloadable_sounds.append(theme_name)
|
|
|
|
downloadable_boot_logos = [Path(boot_logo).stem for boot_logo in assets["boot_logos"]]
|
|
downloadable_wheels = [self.format_name(wheel, "steering_wheels") for wheel in assets["wheels"]]
|
|
|
|
print(f"Downloadable Boot Logos: {downloadable_boot_logos}")
|
|
print(f"Downloadable Colors: {downloadable_colors}")
|
|
print(f"Downloadable Icons: {downloadable_icons}")
|
|
print(f"Downloadable Signals: {downloadable_signals}")
|
|
print(f"Downloadable Sounds: {downloadable_sounds}")
|
|
print(f"Downloadable Distance Icons: {downloadable_distance_icons}")
|
|
print(f"Downloadable Wheels: {downloadable_wheels}")
|
|
|
|
if boot_run:
|
|
self.validate_themes(downloadable_boot_logos, downloadable_colors, downloadable_distance_icons, downloadable_icons, downloadable_signals, downloadable_sounds, downloadable_wheels, starpilot_toggles)
|
|
|
|
self.update_theme_params(downloadable_boot_logos, downloadable_colors, downloadable_distance_icons, downloadable_icons, downloadable_signals, downloadable_sounds, downloadable_wheels)
|
|
|
|
@staticmethod
|
|
def update_boot_logo(image):
|
|
default_boot_logo = Path(__file__).parent / "other_images/starpilot_boot_logo.jpg"
|
|
|
|
if not default_boot_logo.exists():
|
|
return
|
|
|
|
source_file = find_matching_theme_asset_file(THEME_SAVE_PATH / "bootlogos", image) or default_boot_logo
|
|
|
|
if source_file.resolve() == default_boot_logo.resolve():
|
|
print(f"Boot logo unchanged: {default_boot_logo}")
|
|
return
|
|
|
|
shutil.copy2(source_file, default_boot_logo)
|
|
print(f"Copied {source_file} to {default_boot_logo}")
|
|
|
|
def update_wheel_image(self, image, boot_run=False, random_event=False):
|
|
wheel_save_location = ACTIVE_THEME_PATH / "steering_wheel"
|
|
|
|
if self.holiday_theme != "stock":
|
|
wheel_location = HOLIDAY_THEME_PATH / self.holiday_theme / "steering_wheel"
|
|
elif random_event:
|
|
wheel_location = RANDOM_EVENTS_PATH / "steering_wheels"
|
|
elif image == "stock":
|
|
wheel_location = STOCKOP_THEME_PATH / "steering_wheel"
|
|
elif image in HOLIDAY_SLUGS:
|
|
wheel_location = HOLIDAY_THEME_PATH / image / "steering_wheel"
|
|
elif f"{image}_week" in HOLIDAY_SLUGS:
|
|
wheel_location = HOLIDAY_THEME_PATH / f"{image}_week" / "steering_wheel"
|
|
else:
|
|
wheel_location = THEME_SAVE_PATH / "steering_wheels"
|
|
|
|
if not wheel_location.exists():
|
|
wheel_location = STOCKOP_THEME_PATH / "steering_wheel"
|
|
print("Using the stock steering wheel instead")
|
|
|
|
delete_file(wheel_save_location, print_error=not boot_run)
|
|
wheel_save_location.mkdir(parents=True, exist_ok=True)
|
|
|
|
image_name = image.replace(" ", "_").lower()
|
|
matching_files = [images for images in wheel_location.iterdir() if images.stem.lower() in {image_name, "wheel"}]
|
|
if not matching_files:
|
|
stock_location = STOCKOP_THEME_PATH / "steering_wheel"
|
|
matching_files = [images for images in stock_location.iterdir() if images.stem.lower() == "wheel"]
|
|
if matching_files:
|
|
print(f"Steering wheel '{image}' not found, using the stock steering wheel instead")
|
|
|
|
if not matching_files:
|
|
print(f"No steering wheel asset found for '{image}'")
|
|
return
|
|
|
|
source_file = matching_files[0]
|
|
destination_file = wheel_save_location / f"wheel{source_file.suffix}"
|
|
destination_file.symlink_to(source_file)
|
|
print(f"Linked {destination_file} to {source_file}")
|
|
|
|
def validate_themes(self, downloadable_boot_logos, downloadable_colors, downloadable_distance_icons, downloadable_icons, downloadable_signals, downloadable_sounds, downloadable_wheels, starpilot_toggles):
|
|
downloaded_data = self.params.get("ThemesDownloaded")
|
|
if isinstance(downloaded_data, (bytes, bytearray)):
|
|
downloaded_data = downloaded_data.decode("utf-8", "ignore")
|
|
if isinstance(downloaded_data, str):
|
|
try:
|
|
downloaded_data = json.loads(downloaded_data)
|
|
except json.JSONDecodeError:
|
|
downloaded_data = {}
|
|
if not isinstance(downloaded_data, dict):
|
|
downloaded_data = {}
|
|
|
|
boot_logos_path = THEME_SAVE_PATH / "bootlogos"
|
|
for display_name in downloaded_data.get("boot_logos", []):
|
|
if not find_matching_theme_asset_file(boot_logos_path, display_name):
|
|
file_stem = find_matching_theme_asset_name(downloadable_boot_logos, display_name) or display_name.replace(" ", "_").lower()
|
|
print(f"Missing boot logo '{display_name}'. Downloading...")
|
|
self.download_theme("boot_logos", file_stem, THEME_COMPONENT_PARAMS["boot_logos"], starpilot_toggles)
|
|
self.update_active_theme(True, starpilot_toggles)
|
|
|
|
for display_name, components in downloaded_data.get("themes", {}).items():
|
|
raw_name = display_name.lower().replace(" ", "_").replace("(", "").replace(")", "")
|
|
theme_folder_name = raw_name.replace("_animated", "-animated")
|
|
|
|
for component in components:
|
|
component_path = THEME_SAVE_PATH / "theme_packs" / theme_folder_name / component
|
|
if not component_path.is_dir() or not any(component_path.iterdir()):
|
|
print(f"Missing or empty component '{component}' for theme '{theme_folder_name}'. Downloading...")
|
|
self.download_theme(component, theme_folder_name, THEME_COMPONENT_PARAMS.get(component), starpilot_toggles)
|
|
self.update_active_theme(True, starpilot_toggles)
|
|
|
|
wheels_path = THEME_SAVE_PATH / "steering_wheels"
|
|
for display_name in downloaded_data.get("steering_wheels", []):
|
|
file_stem = display_name.replace(" ", "_").lower()
|
|
matching_files = list(wheels_path.glob(f"{file_stem}.*"))
|
|
if not matching_files:
|
|
print(f"Missing steering wheel '{display_name}'. Downloading...")
|
|
self.download_theme("steering_wheels", file_stem, THEME_COMPONENT_PARAMS["steering_wheels"], starpilot_toggles)
|
|
self.update_active_theme(True, starpilot_toggles)
|
|
|
|
protected_dirs = {THEME_SAVE_PATH / "bootlogos", THEME_SAVE_PATH / "theme_packs", THEME_SAVE_PATH / "steering_wheels"}
|
|
for dir_path in THEME_SAVE_PATH.glob("**/*"):
|
|
if dir_path.is_dir() and not any(dir_path.iterdir()):
|
|
if dir_path in protected_dirs:
|
|
continue
|
|
print(f"Deleting empty folder: {dir_path}")
|
|
delete_file(dir_path)
|
|
elif dir_path.is_file() and dir_path.name.startswith("tmp"):
|
|
print(f"Deleting temp file: {dir_path}")
|
|
delete_file(dir_path)
|
|
|
|
print("Theme validation complete.")
|