diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile
index 9795567..a48c086 100644
--- a/.devcontainer/Dockerfile
+++ b/.devcontainer/Dockerfile
@@ -4,7 +4,7 @@ ENV PATH="$PATH:/opt/flutter/bin"
# Install system dependencies
RUN apt-get update && apt-get install -y \
- wget curl unzip git xz-utils zip cmake clang pkg-config mesa-utils lld libgtk-3-dev ninja-build libglu1-mesa openjdk-17-jdk\
+ wget curl unzip git xz-utils zip cmake clang pkg-config mesa-utils python3 pip pipx lld libgtk-3-dev ninja-build libglu1-mesa openjdk-17-jdk\
&& rm -rf /var/lib/apt/lists/*
# Become non-root
diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json
index c6f7f3b..79f3f46 100644
--- a/.devcontainer/devcontainer.json
+++ b/.devcontainer/devcontainer.json
@@ -1,7 +1,7 @@
{
"name": "kryz-go",
"mounts": [
- "source=/dev/dri,target=/dev/dri,type=bind"
+ // "source=/dev/dri,target=/dev/dri,type=bind"
],
"build": {
"dockerfile": "Dockerfile"
@@ -14,5 +14,10 @@
]
}
},
- "postCreateCommand": "flutter doctor -v"
+ "postCreateCommand": "flutter pub get && python3 -m venv .venv && . .venv/bin/activate && pip install --upgrade pip && pip install -r requirements.txt && curl -fsSL https://claude.ai/install.sh | bash",
+ "containerEnv": {
+ "ANTHROPIC_BASE_URL": "http://192.168.1.108:1234",
+ "ANTHROPIC_API_KEY": "lmstudio",
+ "ANTHROPIC_BASE_MODEL": "qwen/qwen3.6-27b"
+ }
}
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index 266f640..cef469a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,6 @@
+# Generated whitelabel config — rebuilt by build_whitelabel.py
+lib/generated/
+
# ---> Dart
# See https://www.dartlang.org/guides/libraries/private-files
@@ -69,7 +72,7 @@ google-services.json
.LSOverride
# Icon must end with two \r
-Icon
+Icon
# Thumbnails
._*
@@ -90,3 +93,8 @@ Network Trash Folder
Temporary Items
.apdisk
+# Python venv
+.venv/
+
+# Whitelabel app theming
+theme.json
\ No newline at end of file
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 84c4687..104ab1a 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -8,7 +8,7 @@
= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
NOTIFICATION_CHANNEL_ID,
- "KRYZ Auto Playback",
+ "KMountain Flower Radio Auto Playback",
NotificationManager.IMPORTANCE_LOW,
).apply {
- description = "KRYZ Radio Android Auto playback"
+ description = "KMountain Flower Radio Android Auto playback"
setShowBadge(false)
}
val mgr = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
@@ -314,7 +314,7 @@ class KryzAutoMediaBrowserService : MediaBrowserServiceCompat() {
else -> "Stopped"
}
return NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID)
- .setContentTitle("KRYZ Radio")
+ .setContentTitle("KMountain Radio")
.setContentText(statusText)
.setSmallIcon(R.mipmap.ic_launcher)
.setOngoing(isPlaying || isBuffering)
@@ -333,8 +333,8 @@ class KryzAutoMediaBrowserService : MediaBrowserServiceCompat() {
val iconUri = Uri.parse("android.resource://${packageName}/drawable/kryz_auto_art")
mediaSession.setMetadata(
MediaMetadataCompat.Builder()
- .putString(MediaMetadataCompat.METADATA_KEY_TITLE, title ?: "KRYZ Live Stream")
- .putString(MediaMetadataCompat.METADATA_KEY_ARTIST, subtitle ?: "KRYZ Radio")
+ .putString(MediaMetadataCompat.METADATA_KEY_TITLE, title ?: "KMTN Live Stream")
+ .putString(MediaMetadataCompat.METADATA_KEY_ARTIST, subtitle ?: "KMountain Radio")
.putString(MediaMetadataCompat.METADATA_KEY_ALBUM, album ?: "Live")
.putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, MEDIA_ID_LIVE)
.putString(MediaMetadataCompat.METADATA_KEY_MEDIA_URI, mediaUri ?: STREAM_URL)
@@ -347,8 +347,8 @@ class KryzAutoMediaBrowserService : MediaBrowserServiceCompat() {
private fun applyNowPlayingMetadataFromFlutter(metadata: MediaMetadataCompat?) {
if (metadata == null) {
applyNowPlayingMetadata(
- title = "KRYZ Live Stream",
- subtitle = "KRYZ Radio",
+ title = "KMTN Live Stream",
+ subtitle = "KMountain Radio",
album = "Live",
mediaUri = STREAM_URL,
)
@@ -430,8 +430,8 @@ class KryzAutoMediaBrowserService : MediaBrowserServiceCompat() {
private fun buildLiveStreamMetadata(): MediaMetadataCompat {
val iconUri = Uri.parse("android.resource://${packageName}/mipmap/ic_launcher")
return MediaMetadataCompat.Builder()
- .putString(MediaMetadataCompat.METADATA_KEY_TITLE, "KRYZ Live Stream")
- .putString(MediaMetadataCompat.METADATA_KEY_ARTIST, "KRYZ Radio")
+ .putString(MediaMetadataCompat.METADATA_KEY_TITLE, "KMTN Live Stream")
+ .putString(MediaMetadataCompat.METADATA_KEY_ARTIST, "KMountain Radio")
.putString(MediaMetadataCompat.METADATA_KEY_ALBUM, "Live")
.putString(MediaMetadataCompat.METADATA_KEY_MEDIA_ID, MEDIA_ID_LIVE)
.putString(MediaMetadataCompat.METADATA_KEY_MEDIA_URI, STREAM_URL)
diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
index 51b2b81..1baa9c5 100644
Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
index d1d6a5f..bc274b8 100644
Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
index 651bf24..2d83039 100644
Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
index 4ee9f9e..284ccbc 100644
Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
index 167958a..39276bc 100644
Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/values/automotive_colors.xml b/android/app/src/main/res/values/automotive_colors.xml
index 59add22..779ccf7 100644
--- a/android/app/src/main/res/values/automotive_colors.xml
+++ b/android/app/src/main/res/values/automotive_colors.xml
@@ -5,7 +5,7 @@
#E36A18 matches AppThemeBusinessObject.lightColors.primarySeed (brand orange).
-->
- #E36A18
- #B84E08
- #1663C7
+ #000000
+ #0d0d0d
+ #ff7a33
diff --git a/assets/icon/app_icon.png b/assets/icon/app_icon.png
index e5817ad..f396189 100644
Binary files a/assets/icon/app_icon.png and b/assets/icon/app_icon.png differ
diff --git a/build_whitelabel.py b/build_whitelabel.py
new file mode 100644
index 0000000..37bbf52
--- /dev/null
+++ b/build_whitelabel.py
@@ -0,0 +1,504 @@
+#!/usr/bin/env python3
+"""
+Whitelabel build script for KRYZ Go Flutter.
+
+Reads theme.json and generates:
+ - lib/generated/app_config.dart (compile-time constants)
+ - lib/generated/theme_colors.dart (light + dark AppThemeColors)
+ - assets/icon/app_icon.png (from embedded logo SVG)
+Patches native platform files (AndroidManifest, Info.plist, automotive_colors.xml, Kotlin).
+Invokes flutter_launcher_icons and flutter_name_manager.
+
+Usage:
+ python3 build_whitelabel.py [--theme theme.json] [--no-tools]
+"""
+
+import argparse
+import base64
+import json
+import math
+import os
+import re
+import subprocess
+import sys
+
+# ---------------------------------------------------------------------------
+# Helpers
+# ---------------------------------------------------------------------------
+
+PROJECT_ROOT = os.path.dirname(os.path.abspath(__file__))
+
+
+def hex_to_rgba(hex_color: str, alpha: float = 1.0) -> str:
+ """Convert '#RRGGBB' hex to '0xAARRGGBB' Dart Color format."""
+ hex_color = hex_color.lstrip("#")
+ assert len(hex_color) == 6, f"Expected 6 hex digits, got {len(hex_color)}"
+ r = int(hex_color[0:2], 16)
+ g = int(hex_color[2:4], 16)
+ b = int(hex_color[4:6], 16)
+ a = int(round(alpha * 255))
+ return f"Color(0x{a:02X}{r:02X}{g:02X}{b:02X})"
+
+
+def darken(hex_color: str, factor: float) -> str:
+ """Darken a hex color by multiplying RGB channels by factor (0-1)."""
+ hex_color = hex_color.lstrip("#")
+ r = int(hex_color[0:2], 16)
+ g = int(hex_color[2:4], 16)
+ b = int(hex_color[4:6], 16)
+ r = int(min(r * factor, 255))
+ g = int(min(g * factor, 255))
+ b = int(min(b * factor, 255))
+ return f"Color(0xFF{r:02X}{g:02X}{b:02X})"
+
+
+def dart_escape(value: str) -> str:
+ """Escape a string for use in a Dart const string literal."""
+ return value.replace("\\", "\\\\").replace("'", "\\'").replace("\n", "\\n")
+
+
+def read_theme(path: str) -> dict:
+ with open(path, "r", encoding="utf-8") as f:
+ return json.load(f)
+
+
+# ---------------------------------------------------------------------------
+# Config generation
+# ---------------------------------------------------------------------------
+
+def generate_app_config(theme: dict) -> str:
+ station = theme["station"]
+ streams = theme["streams"]
+
+ name_primary = station.get("name_primary", "")
+ name_secondary = station.get("name_secondary", "")
+ callsign = station.get("callsign", "")
+ tagline = station.get("tagline", "")
+ frequency = station.get("frequency", "")
+ stream_url = streams.get("url", "")
+ metadata_url = streams.get("metadata_url")
+
+ app_name = f"{name_primary} {name_secondary}".strip()
+ default_title = f"{callsign or name_primary} Live Stream"
+ default_artist = f"{name_primary} Radio"
+
+ metadata_url_line = ""
+ if metadata_url:
+ metadata_url_line = f" static const String metadataUrl = '{dart_escape(metadata_url)}';\n"
+ else:
+ metadata_url_line = " static const String? metadataUrl = null;\n"
+
+ return f'''// GENERATED FILE — DO NOT EDIT.
+// Run `python3 build_whitelabel.py` to regenerate.
+
+class AppConfig {{
+ static const String callsign = '{dart_escape(callsign)}';
+ static const String appName = '{dart_escape(app_name)}';
+ static const String tagline = '{dart_escape(tagline)}';
+ static const String frequency = '{dart_escape(frequency)}';
+ static const String streamUrl = '{dart_escape(stream_url)}';
+{metadata_url_line.rstrip()}
+ static const String defaultTitle = '{dart_escape(default_title)}';
+ static const String defaultArtist = '{dart_escape(default_artist)}';
+}}
+'''
+
+
+# ---------------------------------------------------------------------------
+# Theme colors generation
+# ---------------------------------------------------------------------------
+
+def generate_theme_colors(theme: dict) -> str:
+ c = theme["colors"]
+
+ # Light mode
+ light_props = [
+ ("primarySeed", hex_to_rgba(c["primary"])),
+ ("secondarySeed", hex_to_rgba(c["accent"])),
+ ("scaffoldGradientStart", hex_to_rgba(c["background"])),
+ ("scaffoldGradientEnd", hex_to_rgba(c["light"])),
+ ("shellSurface", hex_to_rgba(c["white"], 0.97)),
+ ("shellBorder", hex_to_rgba(c["medium"], 0.10)),
+ ("accent", hex_to_rgba(c["accent_dark"])),
+ ("webViewBorder", hex_to_rgba(c["medium"], 0.15)),
+ ("webViewBackground", hex_to_rgba(c["white"])),
+ ("controlsGradientStart", hex_to_rgba(c["primary_muted"])),
+ ("controlsGradientEnd", hex_to_rgba(c["accent_muted"])),
+ ("controlsShadow", hex_to_rgba(c["black"], 0.14)),
+ ("statusText", hex_to_rgba(c["text"])),
+ ]
+
+ # Dark mode
+ dark_props = [
+ ("primarySeed", hex_to_rgba(c["primary_light"])),
+ ("secondarySeed", hex_to_rgba(c["accent_light"])),
+ ("scaffoldGradientStart", hex_to_rgba(c["black"])),
+ ("scaffoldGradientEnd", darken(c["primary_dark"], 0.30)),
+ ("shellSurface", hex_to_rgba(c["black"], 0.85)),
+ ("shellBorder", hex_to_rgba(c["accent_light"], 0.20)),
+ ("accent", hex_to_rgba(c["primary_light"])),
+ ("webViewBorder", hex_to_rgba(c["accent_light"], 0.20)),
+ ("webViewBackground", hex_to_rgba(c["black"])),
+ ("controlsGradientStart", hex_to_rgba(c["primary_dark"])),
+ ("controlsGradientEnd", hex_to_rgba(c["accent_dark"])),
+ ("controlsShadow", hex_to_rgba(c["black"], 0.27)),
+ ("statusText", hex_to_rgba(c["light"])),
+ ]
+
+ def props_block(props, indent: str = " ") -> str:
+ lines = []
+ for name, color_expr in props:
+ lines.append(f"{indent}{name}: {color_expr},")
+ return "\n".join(lines)
+
+ return f'''// GENERATED FILE — DO NOT EDIT.
+// Run `python3 build_whitelabel.py` to regenerate.
+
+import 'package:flutter/material.dart';
+
+import '../app_theme_business_object.dart';
+
+class GeneratedThemeColors {{
+ static const AppThemeColors lightColors = AppThemeColors(
+{props_block(light_props)}
+ );
+
+ static const AppThemeColors darkColors = AppThemeColors(
+{props_block(dark_props)}
+ );
+}}
+'''
+
+
+# ---------------------------------------------------------------------------
+# Icon generation
+# ---------------------------------------------------------------------------
+
+def generate_icon(theme: dict, dry_run: bool = False) -> None:
+ """Decode embedded logo (JPEG, PNG, or SVG) and write app_icon.png."""
+ embedded = theme.get("images_embedded", {})
+ logo = embedded.get("logo", {})
+ data_b64 = logo.get("data", "")
+ content_type = logo.get("content_type", "")
+ if not data_b64:
+ print(" [icon] No embedded logo data — skipping icon generation")
+ return
+
+ raw_bytes = base64.b64decode(data_b64)
+ icon_path = os.path.join(PROJECT_ROOT, "assets", "icon", "app_icon.png")
+
+ if dry_run:
+ print(f" [icon] Would generate PNG from embedded {content_type or 'image'} at assets/icon/app_icon.png")
+ return
+
+ # --- JPEG / PNG: convert to PNG via Pillow, or write as-is ---
+ if content_type in ("image/jpeg", "image/png"):
+ try:
+ from PIL import Image
+ import io
+ img = Image.open(io.BytesIO(raw_bytes)).convert("RGBA")
+ img = img.resize((1024, 1024), Image.LANCZOS)
+ img.save(icon_path, "PNG")
+ print(f" [icon] Converted {content_type} → PNG (Pillow, 1024×1024)")
+ return
+ except ImportError:
+ pass
+ except Exception as e:
+ print(f" [icon] Pillow conversion failed: {e} — writing raw bytes")
+
+ # Fallback: write raw bytes directly (flutter_launcher_icons accepts JPEG)
+ if content_type == "image/jpeg":
+ fallback = os.path.join(PROJECT_ROOT, "assets", "icon", "app_icon.jpg")
+ else:
+ fallback = icon_path
+ with open(fallback, "wb") as f:
+ f.write(raw_bytes)
+ print(f" [icon] Wrote embedded {content_type} (no Pillow — using raw bytes)")
+ # If we wrote a JPEG, rename the pubspec image_path is still .png,
+ # so try to at least rename it
+ if fallback != icon_path:
+ os.replace(fallback, icon_path)
+ return
+
+ # --- SVG: render to PNG via cairosvg or rsvg-convert ---
+ try:
+ import cairosvg
+ cairosvg.svg2png(bytestring=raw_bytes, write_to=icon_path, output_width=1024, output_height=1024)
+ print(" [icon] Rendered SVG → PNG (cairosvg)")
+ return
+ except ImportError:
+ pass
+ except Exception as e:
+ print(f" [icon] cairosvg failed: {e} — trying rsvg-convert")
+
+ try:
+ import tempfile
+ with tempfile.NamedTemporaryFile(suffix=".svg", delete=False) as tmp:
+ tmp.write(raw_bytes)
+ tmp_path = tmp.name
+ subprocess.run(
+ ["rsvg-convert", "-w", "1024", "-h", "1024", "-o", icon_path, tmp_path],
+ check=True,
+ capture_output=True,
+ )
+ os.unlink(tmp_path)
+ print(" [icon] Rendered SVG → PNG (rsvg-convert)")
+ except Exception as e:
+ print(f" [icon] WARNING: Could not render icon: {e}")
+ print(" [icon] Install cairosvg (`pip install cairosvg`) or rsvg-convert")
+
+
+# ---------------------------------------------------------------------------
+# Native file patching
+# ---------------------------------------------------------------------------
+
+def patch_android_manifest(theme: dict, dry_run: bool = False) -> None:
+ path = os.path.join(PROJECT_ROOT, "android", "app", "src", "main", "AndroidManifest.xml")
+ if not os.path.exists(path):
+ print(" [android] AndroidManifest.xml not found — skipping")
+ return
+
+ station = theme["station"]
+ app_name = f"{station.get('name_primary','')} {station.get('name_secondary','')}".strip()
+
+ with open(path, "r", encoding="utf-8") as f:
+ content = f.read()
+
+ new_content = re.sub(
+ r'android:label="[^"]*"',
+ f'android:label="{app_name}"',
+ content,
+ )
+
+ if dry_run:
+ print(f" [android] Would patch AndroidManifest.xml label → '{app_name}'")
+ return
+
+ with open(path, "w", encoding="utf-8") as f:
+ f.write(new_content)
+ print(f" [android] Patched AndroidManifest.xml label → '{app_name}'")
+
+
+def patch_info_plist(theme: dict, dry_run: bool = False) -> None:
+ path = os.path.join(PROJECT_ROOT, "ios", "Runner", "Info.plist")
+ if not os.path.exists(path):
+ print(" [ios] Info.plist not found — skipping")
+ return
+
+ station = theme["station"]
+ app_name = f"{station.get('name_primary','')} {station.get('name_secondary','')}".strip()
+
+ with open(path, "r", encoding="utf-8") as f:
+ content = f.read()
+
+ # Replace CFBundleDisplayName value
+ content = re.sub(
+ r'(CFBundleDisplayName\s*)[^<]*()',
+ rf'\1{app_name}\2',
+ content,
+ )
+ # Replace CFBundleName value
+ content = re.sub(
+ r'(CFBundleName\s*)[^<]*()',
+ rf'\1{app_name}\2',
+ content,
+ )
+
+ if dry_run:
+ print(f" [ios] Would patch Info.plist CFBundleDisplayName/CFBundleName → '{app_name}'")
+ return
+
+ with open(path, "w", encoding="utf-8") as f:
+ f.write(content)
+ print(f" [ios] Patched Info.plist → '{app_name}'")
+
+
+def patch_automotive_colors(theme: dict, dry_run: bool = False) -> None:
+ path = os.path.join(PROJECT_ROOT, "android", "app", "src", "main", "res", "values", "automotive_colors.xml")
+ if not os.path.exists(path):
+ print(" [android-auto] automotive_colors.xml not found — skipping")
+ return
+
+ c = theme["colors"]
+
+ with open(path, "r", encoding="utf-8") as f:
+ content = f.read()
+
+ # Replace hex values in color tags
+ content = re.sub(
+ r'()#[^<]*',
+ rf'\1{c["primary"]}',
+ content,
+ )
+ content = re.sub(
+ r'()#[^<]*',
+ rf'\1{c["primary_dark"]}',
+ content,
+ )
+ content = re.sub(
+ r'()#[^<]*',
+ rf'\1{c["accent"]}',
+ content,
+ )
+
+ if dry_run:
+ print(" [android-auto] Would patch automotive_colors.xml")
+ return
+
+ with open(path, "w", encoding="utf-8") as f:
+ f.write(content)
+ print(" [android-auto] Patched automotive_colors.xml")
+
+
+def patch_kotlin_service(theme: dict, dry_run: bool = False) -> None:
+ kts = list(
+ os.path.join(PROJECT_ROOT, "android", "app", "src", "main", "kotlin", "com", "example", "kryz_go_flutter", f)
+ for f in os.listdir(
+ os.path.join(PROJECT_ROOT, "android", "app", "src", "main", "kotlin", "com", "example", "kryz_go_flutter")
+ )
+ ) if os.path.isdir(
+ os.path.join(PROJECT_ROOT, "android", "app", "src", "main", "kotlin", "com", "example", "kryz_go_flutter")
+ ) else []
+
+ if not kts:
+ print(" [kotlin] No Kotlin files found — skipping")
+ return
+
+ station = theme["station"]
+ streams = theme["streams"]
+ name_primary = station.get("name_primary", "KRYZ")
+ callsign = station.get("callsign", name_primary)
+ app_name = f"{name_primary} {station.get('name_secondary','')}".strip()
+ default_title = f"{callsign} Live Stream"
+ default_artist = f"{name_primary} Radio"
+ stream_url = streams.get("url", "")
+
+ replacements = [
+ # STREAM_URL constant
+ (r'(private const val STREAM_URL = ")[^"]*(")', rf'\g<1>{stream_url}\2'),
+ # "KRYZ Live Stream" → default title
+ (r'"KRYZ Live Stream"', f'"{default_title}"'),
+ # "KRYZ Radio" → default artist
+ (r'"KRYZ Radio"', f'"{default_artist}"'),
+ # "KRYZ Auto Playback" → app name + " Auto Playback"
+ (r'"KRYZ Auto Playback"', f'"{app_name} Auto Playback"'),
+ # "KRYZ Radio Android Auto playback" → descriptive text
+ (r'"KRYZ Radio Android Auto playback"', f'"{app_name} Android Auto playback"'),
+ ]
+
+ for kfile in kts:
+ if not kfile.endswith(".kt"):
+ continue
+ with open(kfile, "r", encoding="utf-8") as f:
+ content = f.read()
+
+ original = content
+ for pattern, replacement in replacements:
+ content = re.sub(pattern, replacement, content)
+
+ if content == original:
+ continue
+
+ if dry_run:
+ print(f" [kotlin] Would patch {os.path.basename(kfile)}")
+ return
+
+ with open(kfile, "w", encoding="utf-8") as f:
+ f.write(content)
+ print(f" [kotlin] Patched {os.path.basename(kfile)}")
+
+
+# ---------------------------------------------------------------------------
+# Main
+# ---------------------------------------------------------------------------
+
+def main():
+ parser = argparse.ArgumentParser(description="Whitelabel build script")
+ parser.add_argument("--theme", default=os.path.join(PROJECT_ROOT, "theme.json"), help="Path to theme.json")
+ parser.add_argument("--dry-run", action="store_true", help="Show what would be done without writing files")
+ parser.add_argument("--no-tools", action="store_true", help="Skip invoking flutter_launcher_icons and flutter_name_manager")
+ args = parser.parse_args()
+
+ theme_path = args.theme
+ if not os.path.exists(theme_path):
+ print(f"Error: theme.json not found at {theme_path}")
+ sys.exit(1)
+
+ print(f"Reading {theme_path} ...")
+ theme = read_theme(theme_path)
+
+ station = theme.get("station", {})
+ if not station.get("name_primary"):
+ print("Error: theme.json is missing station.name_primary")
+ sys.exit(1)
+
+ app_name = f"{station.get('name_primary','')} {station.get('name_secondary','')}".strip()
+ print(f"Whitelabel: {app_name}")
+ print()
+
+ # Generate Dart files
+ gen_dir = os.path.join(PROJECT_ROOT, "lib", "generated")
+ os.makedirs(gen_dir, exist_ok=True)
+
+ app_config_path = os.path.join(gen_dir, "app_config.dart")
+ theme_colors_path = os.path.join(gen_dir, "theme_colors.dart")
+
+ if args.dry_run:
+ print(" [gen] Would generate lib/generated/app_config.dart")
+ print(" [gen] Would generate lib/generated/theme_colors.dart")
+ else:
+ with open(app_config_path, "w", encoding="utf-8") as f:
+ f.write(generate_app_config(theme))
+ print(" [gen] Generated lib/generated/app_config.dart")
+
+ with open(theme_colors_path, "w", encoding="utf-8") as f:
+ f.write(generate_theme_colors(theme))
+ print(" [gen] Generated lib/generated/theme_colors.dart")
+
+ # Generate icon
+ print()
+ generate_icon(theme, dry_run=args.dry_run)
+
+ # Patch native files
+ print()
+ patch_android_manifest(theme, dry_run=args.dry_run)
+ patch_info_plist(theme, dry_run=args.dry_run)
+ patch_automotive_colors(theme, dry_run=args.dry_run)
+ patch_kotlin_service(theme, dry_run=args.dry_run)
+
+ # Invoke downstream tools
+ if not args.dry_run and not args.no_tools:
+ print()
+ print("Running flutter_launcher_icons ...")
+ try:
+ subprocess.run(
+ ["dart", "run", "flutter_launcher_icons"],
+ cwd=PROJECT_ROOT,
+ check=True,
+ )
+ print(" flutter_launcher_icons succeeded")
+ except subprocess.CalledProcessError as e:
+ print(f" WARNING: flutter_launcher_icons failed: {e}")
+ except FileNotFoundError:
+ print(" WARNING: 'dart' not found — run `flutter pub get` first")
+
+ print()
+ print("Running flutter_name_manager ...")
+ try:
+ subprocess.run(
+ ["dart", "run", "flutter_name_manager", "--app-name", app_name],
+ cwd=PROJECT_ROOT,
+ check=True,
+ )
+ print(" flutter_name_manager succeeded")
+ except subprocess.CalledProcessError as e:
+ print(f" WARNING: flutter_name_manager failed: {e}")
+ except FileNotFoundError:
+ print(" WARNING: 'dart' not found — run `flutter pub get` first")
+
+ print()
+ print("Done!")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
index dc74b54..5d8744f 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
index 0f4a6d9..2424474 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
index bfceeaf..2d82c68 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
index 6002b16..5a9d918 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
index 54b4fff..f3c5e99 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
index 394214b..d18ab03 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
index 8c5638f..d566292 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
index bfceeaf..2d82c68 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
index 4061a5f..775a286 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
index aa462a1..98fba58 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png
index 18d8937..f0951b0 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png
index d5c6d22..15a601f 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png
index 251a88c..dd31617 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png
index 9143b86..40c2450 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
index aa462a1..98fba58 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
index b6bc3c9..47e5e81 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png
index 51b2b81..1baa9c5 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png
index 4ee9f9e..284ccbc 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
index e9e7a15..b159fdb 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
index d3d906b..f614c4c 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
index 3f62d68..08df380 100644
Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ
diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist
index 2c003f3..f7f1410 100644
--- a/ios/Runner/Info.plist
+++ b/ios/Runner/Info.plist
@@ -7,7 +7,7 @@
CFBundleDevelopmentRegion
$(DEVELOPMENT_LANGUAGE)
CFBundleDisplayName
- KRYZ Go!
+ Fire FM
CFBundleExecutable
$(EXECUTABLE_NAME)
CFBundleIdentifier
@@ -15,7 +15,7 @@
CFBundleInfoDictionaryVersion
6.0
CFBundleName
- KRYZ Go!
+ Fire FM
CFBundlePackageType
APPL
CFBundleShortVersionString
diff --git a/lib/app_theme_business_object.dart b/lib/app_theme_business_object.dart
index 3341bba..9a58370 100644
--- a/lib/app_theme_business_object.dart
+++ b/lib/app_theme_business_object.dart
@@ -1,5 +1,7 @@
import 'package:flutter/material.dart';
+import 'generated/theme_colors.dart';
+
class AppThemeColors {
const AppThemeColors({
required this.primarySeed,
@@ -33,37 +35,8 @@ class AppThemeColors {
}
class AppThemeBusinessObject {
- static const AppThemeColors lightColors = AppThemeColors(
- primarySeed: Color(0xFFE36A18),
- secondarySeed: Color(0xFF1663C7),
- scaffoldGradientStart: Color(0xFFFFF3E3),
- scaffoldGradientEnd: Color(0xFFE6F2FF),
- shellSurface: Color(0xF7FFFFFF),
- shellBorder: Color(0x1A1A4D86),
- accent: Color(0xFF0F5DB6),
- webViewBorder: Color(0x261A4D86),
- webViewBackground: Color(0xFFFFFFFF),
- controlsGradientStart: Color(0xFFFFB465),
- controlsGradientEnd: Color(0xFF69ADFF),
- controlsShadow: Color(0x24114B7A),
- statusText: Color(0xFF13243D),
- );
-
- static const AppThemeColors darkColors = AppThemeColors(
- primarySeed: Color(0xFFFF9D59),
- secondarySeed: Color(0xFF76B8FF),
- scaffoldGradientStart: Color(0xFF0F1C30),
- scaffoldGradientEnd: Color(0xFF31190C),
- shellSurface: Color(0xD9162438),
- shellBorder: Color(0x3366AAFF),
- accent: Color(0xFFFFBC7E),
- webViewBorder: Color(0x3378B7FF),
- webViewBackground: Color(0xFF0C1420),
- controlsGradientStart: Color(0xFF275792),
- controlsGradientEnd: Color(0xFF9A4F1F),
- controlsShadow: Color(0x44000000),
- statusText: Color(0xFFF4F7FF),
- );
+ static const AppThemeColors lightColors = GeneratedThemeColors.lightColors;
+ static const AppThemeColors darkColors = GeneratedThemeColors.darkColors;
static bool isDarkMode(BuildContext context) {
return MediaQuery.platformBrightnessOf(context) == Brightness.dark;
diff --git a/lib/kryz_audio_handler.dart b/lib/kryz_audio_handler.dart
index 90836f6..0034717 100644
--- a/lib/kryz_audio_handler.dart
+++ b/lib/kryz_audio_handler.dart
@@ -6,6 +6,7 @@ import 'package:audio_session/audio_session.dart';
import 'package:http/http.dart' as http;
import 'package:just_audio/just_audio.dart';
+import 'generated/app_config.dart';
import 'live_info_business_object.dart';
class KryzAudioHandler extends BaseAudioHandler with SeekHandler {
@@ -16,16 +17,16 @@ class KryzAudioHandler extends BaseAudioHandler with SeekHandler {
);
}
- static const String streamUrl = 'https://kryz.out.airtime.pro/kryz_a';
+ static const String streamUrl = AppConfig.streamUrl;
static const String mediaId = 'kryz-live-stream';
- static const String _liveInfoEndpoint = 'http://kryz.airtime.pro/api/live-info';
+ static const String _liveInfoEndpoint = AppConfig.metadataUrl ?? '';
static const Duration _liveInfoTimeout = Duration(seconds: 8);
static const Duration _metadataRefreshInterval = Duration(seconds: 20);
static const MediaItem _defaultMediaItem = MediaItem(
id: mediaId,
- title: 'KRYZ Live Stream',
- artist: 'KRYZ Radio',
+ title: AppConfig.defaultTitle,
+ artist: AppConfig.defaultArtist,
album: 'Live',
extras: {
'isLive': true,
@@ -52,7 +53,7 @@ class KryzAudioHandler extends BaseAudioHandler with SeekHandler {
MediaItem(
id: mediaId,
title: current.displayTitle,
- artist: subtitle.isEmpty ? 'KRYZ Radio' : subtitle,
+ artist: subtitle.isEmpty ? AppConfig.defaultArtist : subtitle,
album: 'Live',
extras: {
'isLive': true,
diff --git a/lib/live_info_business_object.dart b/lib/live_info_business_object.dart
index d46dd55..6e4e6be 100644
--- a/lib/live_info_business_object.dart
+++ b/lib/live_info_business_object.dart
@@ -4,17 +4,31 @@ import 'dart:convert';
import 'package:flutter/foundation.dart';
import 'package:http/http.dart' as http;
+import 'generated/app_config.dart';
+
class LiveInfoBusinessObject extends ChangeNotifier {
LiveInfoBusinessObject({
- this.endpoint = const String.fromEnvironment(
- 'KRYZ_LIVE_INFO_URL',
- defaultValue: 'http://kryz.airtime.pro/api/live-info',
- ),
+ String? endpoint,
int refreshIntervalSeconds = 20,
this.requestTimeout = const Duration(seconds: 8),
- }) : _refreshIntervalSeconds = refreshIntervalSeconds;
+ }) : _endpointOverride = endpoint,
+ _refreshIntervalSeconds = refreshIntervalSeconds;
- final String endpoint;
+ /// Explicit endpoint passed at construction, if any.
+ final String? _endpointOverride;
+
+ /// The effective polling endpoint: construction override > env var > AppConfig.
+ String get endpoint {
+ final override = _endpointOverride;
+ if (override != null && override.isNotEmpty) {
+ return override;
+ }
+ const envUrl = String.fromEnvironment('KRYZ_LIVE_INFO_URL', defaultValue: '');
+ if (envUrl.isNotEmpty) {
+ return envUrl;
+ }
+ return AppConfig.metadataUrl ?? '';
+ }
final Duration requestTimeout;
Timer? _pollTimer;
@@ -34,6 +48,10 @@ class LiveInfoBusinessObject extends ChangeNotifier {
List get recentPlayed => List.unmodifiable(_recentPlayed);
Future start() async {
+ // Skip polling if no endpoint is configured
+ if (endpoint.isEmpty && (AppConfig.metadataUrl ?? '').isEmpty) {
+ return;
+ }
await refresh();
_restartTimer();
}
diff --git a/lib/main.dart b/lib/main.dart
index 546ed90..9b59f3c 100644
--- a/lib/main.dart
+++ b/lib/main.dart
@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:timezone/data/latest.dart' as tz;
+import 'generated/app_config.dart';
import 'app_theme_business_object.dart';
import 'casting_business_object.dart';
import 'live_info_business_object.dart';
@@ -53,7 +54,7 @@ class _MyAppState extends State {
@override
Widget build(BuildContext context) {
return MaterialApp(
- title: 'KRYZ Go!',
+ title: AppConfig.appName,
theme: AppThemeBusinessObject.lightTheme(),
darkTheme: AppThemeBusinessObject.darkTheme(),
themeMode: ThemeMode.system,
@@ -394,7 +395,7 @@ class _MainPageState extends State with WidgetsBindingObserver {
const SizedBox(width: 14),
Expanded(
child: Text(
- 'KRYZ Go!',
+ AppConfig.appName,
textAlign: TextAlign.center,
style: theme.textTheme.headlineMedium?.copyWith(
fontWeight: FontWeight.w900,
diff --git a/lib/streaming_audio_business_object.dart b/lib/streaming_audio_business_object.dart
index d364047..b0eb805 100644
--- a/lib/streaming_audio_business_object.dart
+++ b/lib/streaming_audio_business_object.dart
@@ -3,6 +3,7 @@ import 'dart:async';
import 'package:audio_service/audio_service.dart';
import 'package:flutter/foundation.dart';
+import 'generated/app_config.dart';
import 'kryz_audio_handler.dart';
import 'live_info_business_object.dart';
@@ -38,7 +39,7 @@ class StreamingAudioBusinessObject extends ChangeNotifier {
builder: () => KryzAudioHandler(),
config: const AudioServiceConfig(
androidNotificationChannelId: 'com.kryzgoflutter.audio.channel',
- androidNotificationChannelName: 'KRYZ Audio Playback',
+ androidNotificationChannelName: '${AppConfig.appName} Audio Playback',
androidNotificationOngoing: true,
androidStopForegroundOnPause: true,
),
@@ -60,8 +61,8 @@ class StreamingAudioBusinessObject extends ChangeNotifier {
StreamingPlaybackStatus _playbackStatus = StreamingPlaybackStatus.stopped;
bool _isDisposed = false;
bool _isAttached = false;
- String _currentTitle = 'KRYZ Live Stream';
- String _currentSubtitle = 'KRYZ Radio';
+ String _currentTitle = AppConfig.defaultTitle;
+ String _currentSubtitle = AppConfig.defaultArtist;
StreamingPlaybackStatus get playbackStatus => _playbackStatus;
bool get isPlaying => _playbackStatus == StreamingPlaybackStatus.playing;
@@ -145,9 +146,9 @@ class StreamingAudioBusinessObject extends ChangeNotifier {
return;
}
- final title = item.title.trim().isEmpty ? 'KRYZ Live Stream' : item.title;
+ final title = item.title.trim().isEmpty ? AppConfig.defaultTitle : item.title;
final subtitle =
- (item.artist ?? '').trim().isEmpty ? 'KRYZ Radio' : item.artist!.trim();
+ (item.artist ?? '').trim().isEmpty ? AppConfig.defaultArtist : item.artist!.trim();
if (title == _currentTitle && subtitle == _currentSubtitle) {
return;
diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png
index dc74b54..5d8744f 100644
Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ
diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png
index 9ecbd77..c4356f5 100644
Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ
diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png
index c51211a..b43361e 100644
Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ
diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png
index 9c20ede..0b5cab6 100644
Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ
diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png
index 4581a99..a4b96a6 100644
Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ
diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png
index 23e7f5c..fb88455 100644
Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ
diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png
index ff588e1..9e374a5 100644
Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png and b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..93934a8
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,3 @@
+# build_whitelabel.py dependencies
+Pillow>=9.0.0
+cairosvg>=2.7.0
diff --git a/web/favicon.png b/web/favicon.png
index c51211a..b43361e 100644
Binary files a/web/favicon.png and b/web/favicon.png differ
diff --git a/web/icons/Icon-192.png b/web/icons/Icon-192.png
index 167958a..39276bc 100644
Binary files a/web/icons/Icon-192.png and b/web/icons/Icon-192.png differ
diff --git a/web/icons/Icon-512.png b/web/icons/Icon-512.png
index 23e7f5c..fb88455 100644
Binary files a/web/icons/Icon-512.png and b/web/icons/Icon-512.png differ
diff --git a/web/icons/Icon-maskable-192.png b/web/icons/Icon-maskable-192.png
index 167958a..39276bc 100644
Binary files a/web/icons/Icon-maskable-192.png and b/web/icons/Icon-maskable-192.png differ
diff --git a/web/icons/Icon-maskable-512.png b/web/icons/Icon-maskable-512.png
index 23e7f5c..fb88455 100644
Binary files a/web/icons/Icon-maskable-512.png and b/web/icons/Icon-maskable-512.png differ
diff --git a/windows/runner/resources/app_icon.ico b/windows/runner/resources/app_icon.ico
index b0c3ccb..c853d39 100644
Binary files a/windows/runner/resources/app_icon.ico and b/windows/runner/resources/app_icon.ico differ