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