diff --git a/LICENSE b/LICENSE
index c3139e2..97e9a4f 100644
--- a/LICENSE
+++ b/LICENSE
@@ -2,17 +2,20 @@ MIT License
Copyright (c) 2026 iGoX
-Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
-associated documentation files (the "Software"), to deal in the Software without restriction, including
-without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
-following conditions:
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
-The above copyright notice and this permission notice shall be included in all copies or substantial
-portions of the Software.
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
-THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
-LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
-EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
-IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
-USE OR OTHER DEALINGS IN THE SOFTWARE.
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
index 3240ba6..d1965b4 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,191 @@
-# busylight-buddy
+# BusyLight Buddy
+Multiplatform Flutter app to control your DIY [BusyLight](https://github.com/igox/busylight) (ESP32 + MicroPython + Microdot).
+
+Supports **iOS**, **ipadOS**, **Android**, **macOS**, and **Windows**.
+
+---
+
+## Downloads
+
+[](https://github.com/igox/busylight-buddy/releases/download/v0.0.1/BusyLight-Buddy-Installer.exe)
+[](https://github.com/igox/busylight-buddy/releases/download/v0.0.1/org.igox.apps.android.busylight-buddy-release.apk)
+
+Or browse all releases on the [Releases page](https://github.com/igox/busylight-buddy/releases).
+
+---
+
+## Screenshots (iOS)
+
+
+
+---
+
+## Features
+
+### Status control
+- Quick status presets: Available (green), Away (yellow), Busy (red), On, Off
+- Live color preview circle with glow effect, synced with current status
+- Status label displayed below the preview circle
+
+### Custom color presets
+- Color picker
+- **Save & Apply** — pick a color, name it, save as a reusable preset and apply to BusyLight
+- **Apply only** — apply color without saving
+- Custom preset chips displayed in a horizontal scrollable row
+- Long press on a preset chip to **Edit** (color + name) or **Delete**
+- Edit mode updates the preset locally without applying to the BusyLight
+
+### Brightness
+- Brightness slider (0–100%)
+
+### Background polling
+- Automatically pulls status + color from device at a configurable interval
+- Silent updates — no loading screen interruption
+- Configurable in Settings (default: every 5 seconds, can be disabled)
+
+### Settings
+- Device address (hostname or IP, e.g. `http://igox-busylight.local`)
+- Polling interval slider (Off → 1 min)
+- Start with session (macOS and Windows only) — launch automatically at login
+
+### UX & feedback
+- Loading spinner per button during API calls (no full-screen takeover)
+- User-friendly error screen with collapsible technical details
+
+---
+
+## Getting started
+
+```bash
+flutter pub get
+flutter run
+```
+
+### Run on specific platform
+
+```bash
+flutter run -d iphone # iOS simulator
+open -a Simulator # open iOS simulator first if needed
+flutter run -d macos # macOS
+flutter run -d android # Android emulator
+flutter run -d windows # Windows
+```
+
+---
+
+## Project structure
+
+```
+busylight_app/
+├── assets/
+│ └── icon.png # App icon (all platforms)
+├── lib/
+│ ├── main.dart # App entry point
+│ ├── models/
+│ │ ├── busylight_status.dart # Status enum + API paths
+│ │ ├── busylight_color.dart # Color model (r/g/b/brightness)
+│ │ └── color_preset.dart # Named color preset model
+│ ├── services/
+│ │ ├── busylight_service.dart # All REST API calls
+│ │ └── autostart_service.dart # Start with session (macOS + Windows)
+│ ├── providers/
+│ │ ├── busylight_provider.dart # Status, color, brightness, polling, config
+│ │ └── presets_provider.dart # Custom color presets (CRUD + persistence)
+│ ├── screens/
+│ │ ├── home_screen.dart # Main screen
+│ │ └── settings_screen.dart # Configuration screen
+│ └── widgets/
+│ ├── status_button.dart # Animated status button with pending spinner
+│ └── brightness_slider.dart # Brightness control slider
+└── pubspec.yaml
+```
+
+---
+
+## API endpoints used
+
+| Endpoint | Method | Description |
+|---|---|---|
+| `/api/status` | GET | Get current status |
+| `/api/status/available` | POST/GET | Set available (green) |
+| `/api/status/away` | POST/GET | Set away (yellow) |
+| `/api/status/busy` | POST/GET | Set busy (red) |
+| `/api/status/on` | POST/GET | Turn on (white) |
+| `/api/status/off` | POST/GET | Turn off |
+| `/api/color` | GET | Get current color + brightness |
+| `/api/color` | POST | Set custom color (r, g, b, brightness) |
+
+---
+
+## Platform setup
+
+### Android — build APK
+
+Two helper scripts are available in the `android/` folder to build and rename the APK in one step — one for macOS/Linux, one for Windows.
+
+**macOS / Linux:**
+```bash
+# Debug build (default)
+./flutter-build-apk.sh
+
+# Release build
+./flutter-build-apk.sh release
+```
+
+**Windows (PowerShell):**
+```powershell
+# Debug build (default)
+.\flutter-build-apk.ps1
+
+# Release build
+.\flutter-build-apk.ps1 release
+```
+
+Both scripts build the APK with `flutter build apk`, then rename the output from `app-.apk` to `org.igox.apps.android.busylight-buddy-.apk` (and its `.sha1` file if present) in `build/app/outputs/flutter-apk/`.
+
+### Windows — build release
+
+```bash
+flutter build windows --release
+```
+
+### Windows — build installer
+
+The repository includes an [Inno Setup](https://jrsoftware.org/isdl.php) configuration file at the root of the repo to package the app as a Windows installer.
+
+1. Download and install [Inno Setup](https://jrsoftware.org/isdl.php)
+2. Build the release app first:
+ ```bash
+ flutter build windows --release
+ ```
+3. Open `busylight-buddy-windows-installer-builder.iss` in Inno Setup Compiler and click **Compile**, or run from the command line:
+ ```bash
+ iscc busylight-buddy-windows-installer-builder.iss
+ ```
+
+This generates a standalone `.exe` installer in the `windows/installer/` folder.
+
+### App icon (all platforms)
+
+Uses `flutter_launcher_icons`. Icon source: `assets/icon.png`.
+
+```bash
+dart run flutter_launcher_icons
+```
+
+---
+
+## Default device address
+
+`http://igox-busylight.local` — configurable in Settings.
+
+---
+
+## A note on AI & vibe coding
+
+This app was built entirely through **vibe coding** — a collaborative session with [Claude](https://claude.ai) (Anthropic), where the architecture, features, bug fixes, and UI decisions were developed iteratively through natural conversation, without writing a single line of code manually.
+
+The full Flutter project — models, providers, screens, widgets, platform config — was generated, debugged, and refined through back-and-forth dialogue, screenshot feedback, and incremental feature requests.
+
+> "Vibe coding" is a term for AI-assisted development where you describe what you want, review the result, and iterate — focusing on product decisions rather than syntax.
\ No newline at end of file
diff --git a/analysis_options.yaml b/analysis_options.yaml
new file mode 100644
index 0000000..f9b3034
--- /dev/null
+++ b/analysis_options.yaml
@@ -0,0 +1 @@
+include: package:flutter_lints/flutter.yaml
diff --git a/android/.gitignore b/android/.gitignore
new file mode 100644
index 0000000..be3943c
--- /dev/null
+++ b/android/.gitignore
@@ -0,0 +1,14 @@
+gradle-wrapper.jar
+/.gradle
+/captures/
+/gradlew
+/gradlew.bat
+/local.properties
+GeneratedPluginRegistrant.java
+.cxx/
+
+# Remember to never publicly share your keystore.
+# See https://flutter.dev/to/reference-keystore
+key.properties
+**/*.keystore
+**/*.jks
diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts
new file mode 100644
index 0000000..d8e3090
--- /dev/null
+++ b/android/app/build.gradle.kts
@@ -0,0 +1,65 @@
+import java.util.Properties
+import java.io.FileInputStream
+
+plugins {
+ id("com.android.application")
+ id("kotlin-android")
+ // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
+ id("dev.flutter.flutter-gradle-plugin")
+}
+
+val keyPropertiesFile = rootProject.file("key.properties")
+val keyProperties = Properties()
+keyProperties.load(FileInputStream(keyPropertiesFile))
+
+android {
+ namespace = "com.example.busylight_buddy"
+ compileSdk = flutter.compileSdkVersion
+ ndkVersion = flutter.ndkVersion
+
+ compileOptions {
+ sourceCompatibility = JavaVersion.VERSION_17
+ targetCompatibility = JavaVersion.VERSION_17
+ }
+
+ kotlinOptions {
+ jvmTarget = JavaVersion.VERSION_17.toString()
+ }
+
+ defaultConfig {
+ // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
+ applicationId = "com.example.busylight_buddy"
+ // You can update the following values to match your application needs.
+ // For more information, see: https://flutter.dev/to/review-gradle-config.
+ minSdk = flutter.minSdkVersion
+ targetSdk = flutter.targetSdkVersion
+ versionCode = flutter.versionCode
+ versionName = flutter.versionName
+ }
+
+ signingConfigs {
+ create("release") {
+ keyAlias = keyProperties["keyAlias"] as String
+ keyPassword = keyProperties["keyPassword"] as String
+ storeFile = keyProperties["storeFile"]?.let { file(it) }
+ storePassword = keyProperties["storePassword"] as String
+ }
+ }
+
+ buildTypes {
+ release {
+ // TODO: Add your own signing config for the release build.
+ // Signing with the debug keys for now, so `flutter run --release` works.
+ signingConfig = signingConfigs.getByName("release")
+ }
+ debug {
+ // TODO: Add your own signing config for the release build.
+ // Signing with the debug keys for now, so `flutter run --release` works.
+ signingConfig = signingConfigs.getByName("debug")
+ }
+ }
+}
+
+flutter {
+ source = "../.."
+}
\ No newline at end of file
diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml
new file mode 100644
index 0000000..399f698
--- /dev/null
+++ b/android/app/src/debug/AndroidManifest.xml
@@ -0,0 +1,7 @@
+
+
+
+
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..abec0d5
--- /dev/null
+++ b/android/app/src/main/AndroidManifest.xml
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/android/app/src/main/kotlin/com/example/busylight_buddy/MainActivity.kt b/android/app/src/main/kotlin/com/example/busylight_buddy/MainActivity.kt
new file mode 100644
index 0000000..4a02e13
--- /dev/null
+++ b/android/app/src/main/kotlin/com/example/busylight_buddy/MainActivity.kt
@@ -0,0 +1,5 @@
+package com.example.busylight_buddy
+
+import io.flutter.embedding.android.FlutterActivity
+
+class MainActivity : FlutterActivity()
diff --git a/android/app/src/main/res/drawable-v21/launch_background.xml b/android/app/src/main/res/drawable-v21/launch_background.xml
new file mode 100644
index 0000000..f74085f
--- /dev/null
+++ b/android/app/src/main/res/drawable-v21/launch_background.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
diff --git a/android/app/src/main/res/drawable/launch_background.xml b/android/app/src/main/res/drawable/launch_background.xml
new file mode 100644
index 0000000..304732f
--- /dev/null
+++ b/android/app/src/main/res/drawable/launch_background.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..ce4406f
Binary files /dev/null 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
new file mode 100644
index 0000000..0a3bd75
Binary files /dev/null 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
new file mode 100644
index 0000000..326cac5
Binary files /dev/null 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
new file mode 100644
index 0000000..6685832
Binary files /dev/null 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
new file mode 100644
index 0000000..882ca66
Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/android/app/src/main/res/values-night/styles.xml b/android/app/src/main/res/values-night/styles.xml
new file mode 100644
index 0000000..06952be
--- /dev/null
+++ b/android/app/src/main/res/values-night/styles.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml
new file mode 100644
index 0000000..cb1ef88
--- /dev/null
+++ b/android/app/src/main/res/values/styles.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
diff --git a/android/app/src/profile/AndroidManifest.xml b/android/app/src/profile/AndroidManifest.xml
new file mode 100644
index 0000000..399f698
--- /dev/null
+++ b/android/app/src/profile/AndroidManifest.xml
@@ -0,0 +1,7 @@
+
+
+
+
diff --git a/android/build.gradle.kts b/android/build.gradle.kts
new file mode 100644
index 0000000..dbee657
--- /dev/null
+++ b/android/build.gradle.kts
@@ -0,0 +1,24 @@
+allprojects {
+ repositories {
+ google()
+ mavenCentral()
+ }
+}
+
+val newBuildDir: Directory =
+ rootProject.layout.buildDirectory
+ .dir("../../build")
+ .get()
+rootProject.layout.buildDirectory.value(newBuildDir)
+
+subprojects {
+ val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
+ project.layout.buildDirectory.value(newSubprojectBuildDir)
+}
+subprojects {
+ project.evaluationDependsOn(":app")
+}
+
+tasks.register("clean") {
+ delete(rootProject.layout.buildDirectory)
+}
diff --git a/android/flutter-build-apk.ps1 b/android/flutter-build-apk.ps1
new file mode 100644
index 0000000..db768ac
--- /dev/null
+++ b/android/flutter-build-apk.ps1
@@ -0,0 +1,23 @@
+$buildType = if ($args[0]) { $args[0] } else { "debug" }
+$apkDir = "..\build\app\outputs\flutter-apk"
+$baseName = "org.igox.apps.android.busylight-buddy"
+
+flutter build apk --$buildType
+
+# Rename APK
+$oldApk = "$apkDir\app-$buildType.apk"
+$newApk = "$apkDir\$baseName-$buildType.apk"
+if (Test-Path $oldApk) {
+ if (Test-Path $newApk) { Remove-Item $newApk -Force }
+ Rename-Item -Path $oldApk -NewName "$baseName-$buildType.apk"
+ Write-Host "APK renamed to: $baseName-$buildType.apk"
+}
+
+# Rename SHA1 (if exists)
+$oldSha1 = "$apkDir\app-$buildType.apk.sha1"
+$newSha1 = "$apkDir\$baseName-$buildType.apk.sha1"
+if (Test-Path $oldSha1) {
+ if (Test-Path $newSha1) { Remove-Item $newSha1 -Force }
+ Rename-Item -Path $oldSha1 -NewName "$baseName-$buildType.apk.sha1"
+ Write-Host "SHA1 renamed to: $baseName-$buildType.apk.sha1"
+}
\ No newline at end of file
diff --git a/android/flutter-build-apk.sh b/android/flutter-build-apk.sh
new file mode 100755
index 0000000..4d6cbf9
--- /dev/null
+++ b/android/flutter-build-apk.sh
@@ -0,0 +1,24 @@
+#!/bin/bash
+BUILD_TYPE=${1:-debug}
+APK_DIR="../build/app/outputs/flutter-apk"
+BASE_NAME="org.igox.apps.android.busylight-buddy"
+
+flutter build apk --$BUILD_TYPE
+
+# Rename APK
+OLD_APK="$APK_DIR/app-$BUILD_TYPE.apk"
+NEW_APK="$APK_DIR/$BASE_NAME-$BUILD_TYPE.apk"
+if [ -f "$OLD_APK" ]; then
+ [ -f "$NEW_APK" ] && rm -f "$NEW_APK"
+ mv "$OLD_APK" "$NEW_APK"
+ echo "APK renamed to: $BASE_NAME-$BUILD_TYPE.apk"
+fi
+
+# Rename SHA1 (if exists)
+OLD_SHA1="$APK_DIR/app-$BUILD_TYPE.apk.sha1"
+NEW_SHA1="$APK_DIR/$BASE_NAME-$BUILD_TYPE.apk.sha1"
+if [ -f "$OLD_SHA1" ]; then
+ [ -f "$NEW_SHA1" ] && rm -f "$NEW_SHA1"
+ mv "$OLD_SHA1" "$NEW_SHA1"
+ echo "SHA1 renamed to: $BASE_NAME-$BUILD_TYPE.apk.sha1"
+fi
\ No newline at end of file
diff --git a/android/gradle.properties b/android/gradle.properties
new file mode 100644
index 0000000..fbee1d8
--- /dev/null
+++ b/android/gradle.properties
@@ -0,0 +1,2 @@
+org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
+android.useAndroidX=true
diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..e4ef43f
--- /dev/null
+++ b/android/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,5 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip
diff --git a/android/settings.gradle.kts b/android/settings.gradle.kts
new file mode 100644
index 0000000..ca7fe06
--- /dev/null
+++ b/android/settings.gradle.kts
@@ -0,0 +1,26 @@
+pluginManagement {
+ val flutterSdkPath =
+ run {
+ val properties = java.util.Properties()
+ file("local.properties").inputStream().use { properties.load(it) }
+ val flutterSdkPath = properties.getProperty("flutter.sdk")
+ require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
+ flutterSdkPath
+ }
+
+ includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
+
+ repositories {
+ google()
+ mavenCentral()
+ gradlePluginPortal()
+ }
+}
+
+plugins {
+ id("dev.flutter.flutter-plugin-loader") version "1.0.0"
+ id("com.android.application") version "8.11.1" apply false
+ id("org.jetbrains.kotlin.android") version "2.2.20" apply false
+}
+
+include(":app")
diff --git a/assets/icon.png b/assets/icon.png
new file mode 100644
index 0000000..eaccaaf
Binary files /dev/null and b/assets/icon.png differ
diff --git a/assets/icon_macos.png b/assets/icon_macos.png
new file mode 100644
index 0000000..36eb967
Binary files /dev/null and b/assets/icon_macos.png differ
diff --git a/busyligth-buddy-windows-installer-builder.iss b/busyligth-buddy-windows-installer-builder.iss
new file mode 100644
index 0000000..abf3631
--- /dev/null
+++ b/busyligth-buddy-windows-installer-builder.iss
@@ -0,0 +1,61 @@
+; Script generated by the Inno Setup Script Wizard.
+; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES!
+; Non-commercial use only
+
+#define MyAppName "BusyLight Buddy"
+#define MyAppVersion "0.1"
+#define MyAppPublisher "iGoX"
+#define MyAppURL "https://github.com/igox/busylight-buddy"
+#define MyAppExeName "busylight_buddy.exe"
+
+[Setup]
+; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications.
+; (To generate a new GUID, click Tools | Generate GUID inside the IDE.)
+AppId={{0E33DC67-F87E-4363-917F-B4FE941C7677}
+AppName={#MyAppName}
+AppVersion={#MyAppVersion}
+;AppVerName={#MyAppName} {#MyAppVersion}
+AppPublisher={#MyAppPublisher}
+AppPublisherURL={#MyAppURL}
+AppSupportURL={#MyAppURL}
+AppUpdatesURL={#MyAppURL}
+DefaultDirName={autopf}\{#MyAppName}
+UninstallDisplayIcon={app}\{#MyAppExeName}
+; "ArchitecturesAllowed=x64compatible" specifies that Setup cannot run
+; on anything but x64 and Windows 11 on Arm.
+ArchitecturesAllowed=x64compatible
+; "ArchitecturesInstallIn64BitMode=x64compatible" requests that the
+; install be done in "64-bit mode" on x64 or Windows 11 on Arm,
+; meaning it should use the native 64-bit Program Files directory and
+; the 64-bit view of the registry.
+ArchitecturesInstallIn64BitMode=x64compatible
+DisableProgramGroupPage=yes
+LicenseFile="LICENSE"
+; Uncomment the following line to run in non administrative install mode (install for current user only).
+;PrivilegesRequired=lowest
+PrivilegesRequiredOverridesAllowed=dialog
+OutputDir="windows\installer"
+OutputBaseFilename=BusyLight-Buddy-Installer
+SetupIconFile="windows\runner\resources\app_icon.ico"
+SolidCompression=yes
+WizardStyle=modern dynamic
+
+[Languages]
+Name: "english"; MessagesFile: "compiler:Default.isl"
+
+[Tasks]
+Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
+
+[Files]
+Source: "build\windows\x64\runner\Release\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion
+Source: "build\windows\x64\runner\Release\flutter_windows.dll"; DestDir: "{app}"; Flags: ignoreversion
+Source: "build\windows\x64\runner\Release\data\*"; DestDir: "{app}\data"; Flags: ignoreversion recursesubdirs createallsubdirs
+; NOTE: Don't use "Flags: ignoreversion" on any shared system files
+
+[Icons]
+Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
+Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
+
+[Run]
+Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent
+
diff --git a/devtools_options.yaml b/devtools_options.yaml
new file mode 100644
index 0000000..fa0b357
--- /dev/null
+++ b/devtools_options.yaml
@@ -0,0 +1,3 @@
+description: This file stores settings for Dart & Flutter DevTools.
+documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
+extensions:
diff --git a/doc/screenshots/ios-screenshot-config.png b/doc/screenshots/ios-screenshot-config.png
new file mode 100644
index 0000000..6128958
Binary files /dev/null and b/doc/screenshots/ios-screenshot-config.png differ
diff --git a/doc/screenshots/ios-screenshot-main.png b/doc/screenshots/ios-screenshot-main.png
new file mode 100644
index 0000000..54ab48b
Binary files /dev/null and b/doc/screenshots/ios-screenshot-main.png differ
diff --git a/downloads/BusyLight-Buddy-Installer.exe b/downloads/BusyLight-Buddy-Installer.exe
new file mode 100644
index 0000000..5603144
Binary files /dev/null and b/downloads/BusyLight-Buddy-Installer.exe differ
diff --git a/downloads/org.igox.apps.android.busylight-buddy-release.apk b/downloads/org.igox.apps.android.busylight-buddy-release.apk
new file mode 100644
index 0000000..459ed76
Binary files /dev/null and b/downloads/org.igox.apps.android.busylight-buddy-release.apk differ
diff --git a/downloads/org.igox.apps.android.busylight-buddy-release.apk.sha1 b/downloads/org.igox.apps.android.busylight-buddy-release.apk.sha1
new file mode 100644
index 0000000..5d7bef9
--- /dev/null
+++ b/downloads/org.igox.apps.android.busylight-buddy-release.apk.sha1
@@ -0,0 +1 @@
+17b71b5af077e19e826bdd5d84ec2de014cd91fe
\ No newline at end of file
diff --git a/ios/.gitignore b/ios/.gitignore
new file mode 100644
index 0000000..7a7f987
--- /dev/null
+++ b/ios/.gitignore
@@ -0,0 +1,34 @@
+**/dgph
+*.mode1v3
+*.mode2v3
+*.moved-aside
+*.pbxuser
+*.perspectivev3
+**/*sync/
+.sconsign.dblite
+.tags*
+**/.vagrant/
+**/DerivedData/
+Icon?
+**/Pods/
+**/.symlinks/
+profile
+xcuserdata
+**/.generated/
+Flutter/App.framework
+Flutter/Flutter.framework
+Flutter/Flutter.podspec
+Flutter/Generated.xcconfig
+Flutter/ephemeral/
+Flutter/app.flx
+Flutter/app.zip
+Flutter/flutter_assets/
+Flutter/flutter_export_environment.sh
+ServiceDefinitions.json
+Runner/GeneratedPluginRegistrant.*
+
+# Exceptions to above rules.
+!default.mode1v3
+!default.mode2v3
+!default.pbxuser
+!default.perspectivev3
diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist
new file mode 100644
index 0000000..391a902
--- /dev/null
+++ b/ios/Flutter/AppFrameworkInfo.plist
@@ -0,0 +1,24 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ en
+ CFBundleExecutable
+ App
+ CFBundleIdentifier
+ io.flutter.flutter.app
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ App
+ CFBundlePackageType
+ FMWK
+ CFBundleShortVersionString
+ 1.0
+ CFBundleSignature
+ ????
+ CFBundleVersion
+ 1.0
+
+
diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig
new file mode 100644
index 0000000..ec97fc6
--- /dev/null
+++ b/ios/Flutter/Debug.xcconfig
@@ -0,0 +1,2 @@
+#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
+#include "Generated.xcconfig"
diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig
new file mode 100644
index 0000000..c4855bf
--- /dev/null
+++ b/ios/Flutter/Release.xcconfig
@@ -0,0 +1,2 @@
+#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
+#include "Generated.xcconfig"
diff --git a/ios/Podfile b/ios/Podfile
new file mode 100644
index 0000000..620e46e
--- /dev/null
+++ b/ios/Podfile
@@ -0,0 +1,43 @@
+# Uncomment this line to define a global platform for your project
+# platform :ios, '13.0'
+
+# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
+ENV['COCOAPODS_DISABLE_STATS'] = 'true'
+
+project 'Runner', {
+ 'Debug' => :debug,
+ 'Profile' => :release,
+ 'Release' => :release,
+}
+
+def flutter_root
+ generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
+ unless File.exist?(generated_xcode_build_settings_path)
+ raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
+ end
+
+ File.foreach(generated_xcode_build_settings_path) do |line|
+ matches = line.match(/FLUTTER_ROOT\=(.*)/)
+ return matches[1].strip if matches
+ end
+ raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
+end
+
+require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
+
+flutter_ios_podfile_setup
+
+target 'Runner' do
+ use_frameworks!
+
+ flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
+ target 'RunnerTests' do
+ inherit! :search_paths
+ end
+end
+
+post_install do |installer|
+ installer.pods_project.targets.each do |target|
+ flutter_additional_ios_build_settings(target)
+ end
+end
diff --git a/ios/Podfile.lock b/ios/Podfile.lock
new file mode 100644
index 0000000..ebac102
--- /dev/null
+++ b/ios/Podfile.lock
@@ -0,0 +1,23 @@
+PODS:
+ - Flutter (1.0.0)
+ - shared_preferences_foundation (0.0.1):
+ - Flutter
+ - FlutterMacOS
+
+DEPENDENCIES:
+ - Flutter (from `Flutter`)
+ - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
+
+EXTERNAL SOURCES:
+ Flutter:
+ :path: Flutter
+ shared_preferences_foundation:
+ :path: ".symlinks/plugins/shared_preferences_foundation/darwin"
+
+SPEC CHECKSUMS:
+ Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
+ shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
+
+PODFILE CHECKSUM: 3c63482e143d1b91d2d2560aee9fb04ecc74ac7e
+
+COCOAPODS: 1.16.2
diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj
new file mode 100644
index 0000000..b2445d6
--- /dev/null
+++ b/ios/Runner.xcodeproj/project.pbxproj
@@ -0,0 +1,732 @@
+// !$*UTF8*$!
+{
+ archiveVersion = 1;
+ classes = {
+ };
+ objectVersion = 54;
+ objects = {
+
+/* Begin PBXBuildFile section */
+ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
+ 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
+ 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
+ 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
+ 7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */; };
+ 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
+ 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
+ 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
+ CA146C24B12DCCDB3315C539 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FC9C4E8990B82FAA4F04314B /* Pods_RunnerTests.framework */; };
+ CF2A6911AB3F29129BA9A20C /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D70E62231295DA8B787211BD /* Pods_Runner.framework */; };
+/* End PBXBuildFile section */
+
+/* Begin PBXContainerItemProxy section */
+ 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = {
+ isa = PBXContainerItemProxy;
+ containerPortal = 97C146E61CF9000F007C117D /* Project object */;
+ proxyType = 1;
+ remoteGlobalIDString = 97C146ED1CF9000F007C117D;
+ remoteInfo = Runner;
+ };
+/* End PBXContainerItemProxy section */
+
+/* Begin PBXCopyFilesBuildPhase section */
+ 9705A1C41CF9048500538489 /* Embed Frameworks */ = {
+ isa = PBXCopyFilesBuildPhase;
+ buildActionMask = 2147483647;
+ dstPath = "";
+ dstSubfolderSpec = 10;
+ files = (
+ );
+ name = "Embed Frameworks";
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXCopyFilesBuildPhase section */
+
+/* Begin PBXFileReference section */
+ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; };
+ 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; };
+ 2F030225A20A2250224AAF02 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; };
+ 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; };
+ 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
+ 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; };
+ 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; };
+ 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; };
+ 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; };
+ 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; };
+ 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; };
+ 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; };
+ 976FAD9681BB7D2A26540682 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; };
+ 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
+ 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; };
+ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; };
+ 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; };
+ 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; };
+ B02049EF18B96DBE0D50CDF5 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; };
+ BEDB1CF85E45F77E6E6E2132 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; };
+ CBB709FCB5B2AB086AEBDF9C /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; };
+ CF86EA389FF5C80B9A44263F /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; };
+ D70E62231295DA8B787211BD /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+ FC9C4E8990B82FAA4F04314B /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
+/* End PBXFileReference section */
+
+/* Begin PBXFrameworksBuildPhase section */
+ 97C146EB1CF9000F007C117D /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ CF2A6911AB3F29129BA9A20C /* Pods_Runner.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ A7CCCE24DE2A75A07DDC5E35 /* Frameworks */ = {
+ isa = PBXFrameworksBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ CA146C24B12DCCDB3315C539 /* Pods_RunnerTests.framework in Frameworks */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXFrameworksBuildPhase section */
+
+/* Begin PBXGroup section */
+ 331C8082294A63A400263BE5 /* RunnerTests */ = {
+ isa = PBXGroup;
+ children = (
+ 331C807B294A618700263BE5 /* RunnerTests.swift */,
+ );
+ path = RunnerTests;
+ sourceTree = "";
+ };
+ 5F26977DA5C7A2570A05D594 /* Frameworks */ = {
+ isa = PBXGroup;
+ children = (
+ D70E62231295DA8B787211BD /* Pods_Runner.framework */,
+ FC9C4E8990B82FAA4F04314B /* Pods_RunnerTests.framework */,
+ );
+ name = Frameworks;
+ sourceTree = "";
+ };
+ 94C5EE211A18272B3A975505 /* Pods */ = {
+ isa = PBXGroup;
+ children = (
+ 976FAD9681BB7D2A26540682 /* Pods-Runner.debug.xcconfig */,
+ CBB709FCB5B2AB086AEBDF9C /* Pods-Runner.release.xcconfig */,
+ 2F030225A20A2250224AAF02 /* Pods-Runner.profile.xcconfig */,
+ B02049EF18B96DBE0D50CDF5 /* Pods-RunnerTests.debug.xcconfig */,
+ CF86EA389FF5C80B9A44263F /* Pods-RunnerTests.release.xcconfig */,
+ BEDB1CF85E45F77E6E6E2132 /* Pods-RunnerTests.profile.xcconfig */,
+ );
+ name = Pods;
+ path = Pods;
+ sourceTree = "";
+ };
+ 9740EEB11CF90186004384FC /* Flutter */ = {
+ isa = PBXGroup;
+ children = (
+ 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
+ 9740EEB21CF90195004384FC /* Debug.xcconfig */,
+ 7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
+ 9740EEB31CF90195004384FC /* Generated.xcconfig */,
+ );
+ name = Flutter;
+ sourceTree = "";
+ };
+ 97C146E51CF9000F007C117D = {
+ isa = PBXGroup;
+ children = (
+ 9740EEB11CF90186004384FC /* Flutter */,
+ 97C146F01CF9000F007C117D /* Runner */,
+ 97C146EF1CF9000F007C117D /* Products */,
+ 331C8082294A63A400263BE5 /* RunnerTests */,
+ 94C5EE211A18272B3A975505 /* Pods */,
+ 5F26977DA5C7A2570A05D594 /* Frameworks */,
+ );
+ sourceTree = "";
+ };
+ 97C146EF1CF9000F007C117D /* Products */ = {
+ isa = PBXGroup;
+ children = (
+ 97C146EE1CF9000F007C117D /* Runner.app */,
+ 331C8081294A63A400263BE5 /* RunnerTests.xctest */,
+ );
+ name = Products;
+ sourceTree = "";
+ };
+ 97C146F01CF9000F007C117D /* Runner */ = {
+ isa = PBXGroup;
+ children = (
+ 97C146FA1CF9000F007C117D /* Main.storyboard */,
+ 97C146FD1CF9000F007C117D /* Assets.xcassets */,
+ 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
+ 97C147021CF9000F007C117D /* Info.plist */,
+ 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
+ 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
+ 74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
+ 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */,
+ 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
+ );
+ path = Runner;
+ sourceTree = "";
+ };
+/* End PBXGroup section */
+
+/* Begin PBXNativeTarget section */
+ 331C8080294A63A400263BE5 /* RunnerTests */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
+ buildPhases = (
+ 49C46BDD1C94FFAE1E5C9608 /* [CP] Check Pods Manifest.lock */,
+ 331C807D294A63A400263BE5 /* Sources */,
+ 331C807F294A63A400263BE5 /* Resources */,
+ A7CCCE24DE2A75A07DDC5E35 /* Frameworks */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ 331C8086294A63A400263BE5 /* PBXTargetDependency */,
+ );
+ name = RunnerTests;
+ productName = RunnerTests;
+ productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
+ productType = "com.apple.product-type.bundle.unit-test";
+ };
+ 97C146ED1CF9000F007C117D /* Runner */ = {
+ isa = PBXNativeTarget;
+ buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
+ buildPhases = (
+ 3D9C074EBBEAE39379E335DC /* [CP] Check Pods Manifest.lock */,
+ 9740EEB61CF901F6004384FC /* Run Script */,
+ 97C146EA1CF9000F007C117D /* Sources */,
+ 97C146EB1CF9000F007C117D /* Frameworks */,
+ 97C146EC1CF9000F007C117D /* Resources */,
+ 9705A1C41CF9048500538489 /* Embed Frameworks */,
+ 3B06AD1E1E4923F5004D2608 /* Thin Binary */,
+ 8C6A254FE2A6317D46769E53 /* [CP] Embed Pods Frameworks */,
+ );
+ buildRules = (
+ );
+ dependencies = (
+ );
+ name = Runner;
+ productName = Runner;
+ productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
+ productType = "com.apple.product-type.application";
+ };
+/* End PBXNativeTarget section */
+
+/* Begin PBXProject section */
+ 97C146E61CF9000F007C117D /* Project object */ = {
+ isa = PBXProject;
+ attributes = {
+ BuildIndependentTargetsInParallel = YES;
+ LastUpgradeCheck = 1510;
+ ORGANIZATIONNAME = "";
+ TargetAttributes = {
+ 331C8080294A63A400263BE5 = {
+ CreatedOnToolsVersion = 14.0;
+ TestTargetID = 97C146ED1CF9000F007C117D;
+ };
+ 97C146ED1CF9000F007C117D = {
+ CreatedOnToolsVersion = 7.3.1;
+ LastSwiftMigration = 1100;
+ };
+ };
+ };
+ buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */;
+ compatibilityVersion = "Xcode 9.3";
+ developmentRegion = en;
+ hasScannedForEncodings = 0;
+ knownRegions = (
+ en,
+ Base,
+ );
+ mainGroup = 97C146E51CF9000F007C117D;
+ productRefGroup = 97C146EF1CF9000F007C117D /* Products */;
+ projectDirPath = "";
+ projectRoot = "";
+ targets = (
+ 97C146ED1CF9000F007C117D /* Runner */,
+ 331C8080294A63A400263BE5 /* RunnerTests */,
+ );
+ };
+/* End PBXProject section */
+
+/* Begin PBXResourcesBuildPhase section */
+ 331C807F294A63A400263BE5 /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 97C146EC1CF9000F007C117D /* Resources */ = {
+ isa = PBXResourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */,
+ 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
+ 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
+ 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXResourcesBuildPhase section */
+
+/* Begin PBXShellScriptBuildPhase section */
+ 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
+ isa = PBXShellScriptBuildPhase;
+ alwaysOutOfDate = 1;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
+ );
+ name = "Thin Binary";
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
+ };
+ 3D9C074EBBEAE39379E335DC /* [CP] Check Pods Manifest.lock */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ );
+ inputPaths = (
+ "${PODS_PODFILE_DIR_PATH}/Podfile.lock",
+ "${PODS_ROOT}/Manifest.lock",
+ );
+ name = "[CP] Check Pods Manifest.lock";
+ outputFileListPaths = (
+ );
+ outputPaths = (
+ "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
+ showEnvVarsInLog = 0;
+ };
+ 49C46BDD1C94FFAE1E5C9608 /* [CP] Check Pods Manifest.lock */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ );
+ inputPaths = (
+ "${PODS_PODFILE_DIR_PATH}/Podfile.lock",
+ "${PODS_ROOT}/Manifest.lock",
+ );
+ name = "[CP] Check Pods Manifest.lock";
+ outputFileListPaths = (
+ );
+ outputPaths = (
+ "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
+ showEnvVarsInLog = 0;
+ };
+ 8C6A254FE2A6317D46769E53 /* [CP] Embed Pods Frameworks */ = {
+ isa = PBXShellScriptBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputFileListPaths = (
+ "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
+ );
+ name = "[CP] Embed Pods Frameworks";
+ outputFileListPaths = (
+ "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
+ showEnvVarsInLog = 0;
+ };
+ 9740EEB61CF901F6004384FC /* Run Script */ = {
+ isa = PBXShellScriptBuildPhase;
+ alwaysOutOfDate = 1;
+ buildActionMask = 2147483647;
+ files = (
+ );
+ inputPaths = (
+ );
+ name = "Run Script";
+ outputPaths = (
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ shellPath = /bin/sh;
+ shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
+ };
+/* End PBXShellScriptBuildPhase section */
+
+/* Begin PBXSourcesBuildPhase section */
+ 331C807D294A63A400263BE5 /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+ 97C146EA1CF9000F007C117D /* Sources */ = {
+ isa = PBXSourcesBuildPhase;
+ buildActionMask = 2147483647;
+ files = (
+ 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
+ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
+ 7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */,
+ );
+ runOnlyForDeploymentPostprocessing = 0;
+ };
+/* End PBXSourcesBuildPhase section */
+
+/* Begin PBXTargetDependency section */
+ 331C8086294A63A400263BE5 /* PBXTargetDependency */ = {
+ isa = PBXTargetDependency;
+ target = 97C146ED1CF9000F007C117D /* Runner */;
+ targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
+ };
+/* End PBXTargetDependency section */
+
+/* Begin PBXVariantGroup section */
+ 97C146FA1CF9000F007C117D /* Main.storyboard */ = {
+ isa = PBXVariantGroup;
+ children = (
+ 97C146FB1CF9000F007C117D /* Base */,
+ );
+ name = Main.storyboard;
+ sourceTree = "";
+ };
+ 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
+ isa = PBXVariantGroup;
+ children = (
+ 97C147001CF9000F007C117D /* Base */,
+ );
+ name = LaunchScreen.storyboard;
+ sourceTree = "";
+ };
+/* End PBXVariantGroup section */
+
+/* Begin XCBuildConfiguration section */
+ 249021D3217E4FDB00AE95B9 /* Profile */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ ENABLE_NS_ASSERTIONS = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_USER_SCRIPT_SANDBOXING = NO;
+ GCC_C_LANGUAGE_STANDARD = gnu99;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 13.0;
+ MTL_ENABLE_DEBUG_INFO = NO;
+ SDKROOT = iphoneos;
+ SUPPORTED_PLATFORMS = iphoneos;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ VALIDATE_PRODUCT = YES;
+ };
+ name = Profile;
+ };
+ 249021D4217E4FDB00AE95B9 /* Profile */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CLANG_ENABLE_MODULES = YES;
+ CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
+ ENABLE_BITCODE = NO;
+ INFOPLIST_FILE = Runner/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = com.example.busylightApp;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
+ SWIFT_VERSION = 5.0;
+ VERSIONING_SYSTEM = "apple-generic";
+ };
+ name = Profile;
+ };
+ 331C8088294A63A400263BE5 /* Debug */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = B02049EF18B96DBE0D50CDF5 /* Pods-RunnerTests.debug.xcconfig */;
+ buildSettings = {
+ BUNDLE_LOADER = "$(TEST_HOST)";
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ GENERATE_INFOPLIST_FILE = YES;
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = com.example.busylightApp.RunnerTests;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ SWIFT_VERSION = 5.0;
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
+ };
+ name = Debug;
+ };
+ 331C8089294A63A400263BE5 /* Release */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = CF86EA389FF5C80B9A44263F /* Pods-RunnerTests.release.xcconfig */;
+ buildSettings = {
+ BUNDLE_LOADER = "$(TEST_HOST)";
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ GENERATE_INFOPLIST_FILE = YES;
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = com.example.busylightApp.RunnerTests;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_VERSION = 5.0;
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
+ };
+ name = Release;
+ };
+ 331C808A294A63A400263BE5 /* Profile */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = BEDB1CF85E45F77E6E6E2132 /* Pods-RunnerTests.profile.xcconfig */;
+ buildSettings = {
+ BUNDLE_LOADER = "$(TEST_HOST)";
+ CODE_SIGN_STYLE = Automatic;
+ CURRENT_PROJECT_VERSION = 1;
+ GENERATE_INFOPLIST_FILE = YES;
+ MARKETING_VERSION = 1.0;
+ PRODUCT_BUNDLE_IDENTIFIER = com.example.busylightApp.RunnerTests;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_VERSION = 5.0;
+ TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner";
+ };
+ name = Profile;
+ };
+ 97C147031CF9000F007C117D /* Debug */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = dwarf;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_TESTABILITY = YES;
+ ENABLE_USER_SCRIPT_SANDBOXING = NO;
+ GCC_C_LANGUAGE_STANDARD = gnu99;
+ GCC_DYNAMIC_NO_PIC = NO;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_OPTIMIZATION_LEVEL = 0;
+ GCC_PREPROCESSOR_DEFINITIONS = (
+ "DEBUG=1",
+ "$(inherited)",
+ );
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 13.0;
+ MTL_ENABLE_DEBUG_INFO = YES;
+ ONLY_ACTIVE_ARCH = YES;
+ SDKROOT = iphoneos;
+ TARGETED_DEVICE_FAMILY = "1,2";
+ };
+ name = Debug;
+ };
+ 97C147041CF9000F007C117D /* Release */ = {
+ isa = XCBuildConfiguration;
+ buildSettings = {
+ ALWAYS_SEARCH_USER_PATHS = NO;
+ ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon;
+ CLANG_ANALYZER_NONNULL = YES;
+ CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
+ CLANG_CXX_LIBRARY = "libc++";
+ CLANG_ENABLE_MODULES = YES;
+ CLANG_ENABLE_OBJC_ARC = YES;
+ CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
+ CLANG_WARN_BOOL_CONVERSION = YES;
+ CLANG_WARN_COMMA = YES;
+ CLANG_WARN_CONSTANT_CONVERSION = YES;
+ CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
+ CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
+ CLANG_WARN_EMPTY_BODY = YES;
+ CLANG_WARN_ENUM_CONVERSION = YES;
+ CLANG_WARN_INFINITE_RECURSION = YES;
+ CLANG_WARN_INT_CONVERSION = YES;
+ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
+ CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
+ CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
+ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
+ CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
+ CLANG_WARN_STRICT_PROTOTYPES = YES;
+ CLANG_WARN_SUSPICIOUS_MOVE = YES;
+ CLANG_WARN_UNREACHABLE_CODE = YES;
+ CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
+ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer";
+ COPY_PHASE_STRIP = NO;
+ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
+ ENABLE_NS_ASSERTIONS = NO;
+ ENABLE_STRICT_OBJC_MSGSEND = YES;
+ ENABLE_USER_SCRIPT_SANDBOXING = NO;
+ GCC_C_LANGUAGE_STANDARD = gnu99;
+ GCC_NO_COMMON_BLOCKS = YES;
+ GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
+ GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
+ GCC_WARN_UNDECLARED_SELECTOR = YES;
+ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
+ GCC_WARN_UNUSED_FUNCTION = YES;
+ GCC_WARN_UNUSED_VARIABLE = YES;
+ IPHONEOS_DEPLOYMENT_TARGET = 13.0;
+ MTL_ENABLE_DEBUG_INFO = NO;
+ SDKROOT = iphoneos;
+ SUPPORTED_PLATFORMS = iphoneos;
+ SWIFT_COMPILATION_MODE = wholemodule;
+ SWIFT_OPTIMIZATION_LEVEL = "-O";
+ TARGETED_DEVICE_FAMILY = "1,2";
+ VALIDATE_PRODUCT = YES;
+ };
+ name = Release;
+ };
+ 97C147061CF9000F007C117D /* Debug */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CLANG_ENABLE_MODULES = YES;
+ CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
+ ENABLE_BITCODE = NO;
+ INFOPLIST_FILE = Runner/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = com.example.busylightApp;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
+ SWIFT_OPTIMIZATION_LEVEL = "-Onone";
+ SWIFT_VERSION = 5.0;
+ VERSIONING_SYSTEM = "apple-generic";
+ };
+ name = Debug;
+ };
+ 97C147071CF9000F007C117D /* Release */ = {
+ isa = XCBuildConfiguration;
+ baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */;
+ buildSettings = {
+ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
+ CLANG_ENABLE_MODULES = YES;
+ CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)";
+ ENABLE_BITCODE = NO;
+ INFOPLIST_FILE = Runner/Info.plist;
+ LD_RUNPATH_SEARCH_PATHS = (
+ "$(inherited)",
+ "@executable_path/Frameworks",
+ );
+ PRODUCT_BUNDLE_IDENTIFIER = com.example.busylightApp;
+ PRODUCT_NAME = "$(TARGET_NAME)";
+ SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
+ SWIFT_VERSION = 5.0;
+ VERSIONING_SYSTEM = "apple-generic";
+ };
+ name = Release;
+ };
+/* End XCBuildConfiguration section */
+
+/* Begin XCConfigurationList section */
+ 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 331C8088294A63A400263BE5 /* Debug */,
+ 331C8089294A63A400263BE5 /* Release */,
+ 331C808A294A63A400263BE5 /* Profile */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 97C147031CF9000F007C117D /* Debug */,
+ 97C147041CF9000F007C117D /* Release */,
+ 249021D3217E4FDB00AE95B9 /* Profile */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+ 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = {
+ isa = XCConfigurationList;
+ buildConfigurations = (
+ 97C147061CF9000F007C117D /* Debug */,
+ 97C147071CF9000F007C117D /* Release */,
+ 249021D4217E4FDB00AE95B9 /* Profile */,
+ );
+ defaultConfigurationIsVisible = 0;
+ defaultConfigurationName = Release;
+ };
+/* End XCConfigurationList section */
+ };
+ rootObject = 97C146E61CF9000F007C117D /* Project object */;
+}
diff --git a/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000..919434a
--- /dev/null
+++ b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,7 @@
+
+
+
+
+
diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 0000000..18d9810
--- /dev/null
+++ b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ IDEDidComputeMac32BitWarning
+
+
+
diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
new file mode 100644
index 0000000..f9b0d7c
--- /dev/null
+++ b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
@@ -0,0 +1,8 @@
+
+
+
+
+ PreviewsEnabled
+
+
+
diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
new file mode 100644
index 0000000..e3773d4
--- /dev/null
+++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
@@ -0,0 +1,101 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ios/Runner.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcworkspace/contents.xcworkspacedata
new file mode 100644
index 0000000..21a3cc1
--- /dev/null
+++ b/ios/Runner.xcworkspace/contents.xcworkspacedata
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
diff --git a/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
new file mode 100644
index 0000000..18d9810
--- /dev/null
+++ b/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist
@@ -0,0 +1,8 @@
+
+
+
+
+ IDEDidComputeMac32BitWarning
+
+
+
diff --git a/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
new file mode 100644
index 0000000..f9b0d7c
--- /dev/null
+++ b/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings
@@ -0,0 +1,8 @@
+
+
+
+
+ PreviewsEnabled
+
+
+
diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift
new file mode 100644
index 0000000..c30b367
--- /dev/null
+++ b/ios/Runner/AppDelegate.swift
@@ -0,0 +1,16 @@
+import Flutter
+import UIKit
+
+@main
+@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
+ override func application(
+ _ application: UIApplication,
+ didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
+ ) -> Bool {
+ return super.application(application, didFinishLaunchingWithOptions: launchOptions)
+ }
+
+ func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
+ GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
+ }
+}
diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
new file mode 100644
index 0000000..d0d98aa
--- /dev/null
+++ b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
@@ -0,0 +1 @@
+{"images":[{"size":"20x20","idiom":"iphone","filename":"Icon-App-20x20@2x.png","scale":"2x"},{"size":"20x20","idiom":"iphone","filename":"Icon-App-20x20@3x.png","scale":"3x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@2x.png","scale":"2x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@3x.png","scale":"3x"},{"size":"40x40","idiom":"iphone","filename":"Icon-App-40x40@2x.png","scale":"2x"},{"size":"40x40","idiom":"iphone","filename":"Icon-App-40x40@3x.png","scale":"3x"},{"size":"57x57","idiom":"iphone","filename":"Icon-App-57x57@1x.png","scale":"1x"},{"size":"57x57","idiom":"iphone","filename":"Icon-App-57x57@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"Icon-App-60x60@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"Icon-App-60x60@3x.png","scale":"3x"},{"size":"20x20","idiom":"ipad","filename":"Icon-App-20x20@1x.png","scale":"1x"},{"size":"20x20","idiom":"ipad","filename":"Icon-App-20x20@2x.png","scale":"2x"},{"size":"29x29","idiom":"ipad","filename":"Icon-App-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"ipad","filename":"Icon-App-29x29@2x.png","scale":"2x"},{"size":"40x40","idiom":"ipad","filename":"Icon-App-40x40@1x.png","scale":"1x"},{"size":"40x40","idiom":"ipad","filename":"Icon-App-40x40@2x.png","scale":"2x"},{"size":"50x50","idiom":"ipad","filename":"Icon-App-50x50@1x.png","scale":"1x"},{"size":"50x50","idiom":"ipad","filename":"Icon-App-50x50@2x.png","scale":"2x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@1x.png","scale":"1x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@2x.png","scale":"2x"},{"size":"76x76","idiom":"ipad","filename":"Icon-App-76x76@1x.png","scale":"1x"},{"size":"76x76","idiom":"ipad","filename":"Icon-App-76x76@2x.png","scale":"2x"},{"size":"83.5x83.5","idiom":"ipad","filename":"Icon-App-83.5x83.5@2x.png","scale":"2x"},{"size":"1024x1024","idiom":"ios-marketing","filename":"Icon-App-1024x1024@1x.png","scale":"1x"}],"info":{"version":1,"author":"xcode"}}
\ No newline at end of file
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
new file mode 100644
index 0000000..180fb15
Binary files /dev/null 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
new file mode 100644
index 0000000..3b1585e
Binary files /dev/null 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
new file mode 100644
index 0000000..b751375
Binary files /dev/null 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
new file mode 100644
index 0000000..5fbb336
Binary files /dev/null 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
new file mode 100644
index 0000000..ee5c497
Binary files /dev/null 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
new file mode 100644
index 0000000..89fde9b
Binary files /dev/null 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
new file mode 100644
index 0000000..7d87dfc
Binary files /dev/null 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
new file mode 100644
index 0000000..b751375
Binary files /dev/null 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
new file mode 100644
index 0000000..dfce2f3
Binary files /dev/null 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
new file mode 100644
index 0000000..92ebae7
Binary files /dev/null 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
new file mode 100644
index 0000000..6fd7995
Binary files /dev/null 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
new file mode 100644
index 0000000..0490633
Binary files /dev/null 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
new file mode 100644
index 0000000..dd71a99
Binary files /dev/null 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
new file mode 100644
index 0000000..eb9541b
Binary files /dev/null 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
new file mode 100644
index 0000000..92ebae7
Binary files /dev/null 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
new file mode 100644
index 0000000..1132cfe
Binary files /dev/null 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
new file mode 100644
index 0000000..ce4406f
Binary files /dev/null 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
new file mode 100644
index 0000000..6685832
Binary files /dev/null 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
new file mode 100644
index 0000000..7da9f46
Binary files /dev/null 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
new file mode 100644
index 0000000..f0d6c9b
Binary files /dev/null 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
new file mode 100644
index 0000000..8bde092
Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
new file mode 100644
index 0000000..0bedcf2
--- /dev/null
+++ b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
@@ -0,0 +1,23 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "filename" : "LaunchImage.png",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "LaunchImage@2x.png",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "universal",
+ "filename" : "LaunchImage@3x.png",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}
diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
new file mode 100644
index 0000000..9da19ea
Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ
diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
new file mode 100644
index 0000000..9da19ea
Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ
diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
new file mode 100644
index 0000000..9da19ea
Binary files /dev/null and b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ
diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
new file mode 100644
index 0000000..89c2725
--- /dev/null
+++ b/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
@@ -0,0 +1,5 @@
+# Launch Screen Assets
+
+You can customize the launch screen with your own desired assets by replacing the image files in this directory.
+
+You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.
\ No newline at end of file
diff --git a/ios/Runner/Base.lproj/LaunchScreen.storyboard b/ios/Runner/Base.lproj/LaunchScreen.storyboard
new file mode 100644
index 0000000..f2e259c
--- /dev/null
+++ b/ios/Runner/Base.lproj/LaunchScreen.storyboard
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ios/Runner/Base.lproj/Main.storyboard b/ios/Runner/Base.lproj/Main.storyboard
new file mode 100644
index 0000000..f3c2851
--- /dev/null
+++ b/ios/Runner/Base.lproj/Main.storyboard
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist
new file mode 100644
index 0000000..212148b
--- /dev/null
+++ b/ios/Runner/Info.plist
@@ -0,0 +1,70 @@
+
+
+
+
+ CADisableMinimumFrameDurationOnPhone
+
+ CFBundleDevelopmentRegion
+ $(DEVELOPMENT_LANGUAGE)
+ CFBundleDisplayName
+ BusyLight Buddy
+ CFBundleExecutable
+ $(EXECUTABLE_NAME)
+ CFBundleIdentifier
+ $(PRODUCT_BUNDLE_IDENTIFIER)
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ busylight_buddy
+ CFBundlePackageType
+ APPL
+ CFBundleShortVersionString
+ $(FLUTTER_BUILD_NAME)
+ CFBundleSignature
+ ????
+ CFBundleVersion
+ $(FLUTTER_BUILD_NUMBER)
+ LSRequiresIPhoneOS
+
+ UIApplicationSceneManifest
+
+ UIApplicationSupportsMultipleScenes
+
+ UISceneConfigurations
+
+ UIWindowSceneSessionRoleApplication
+
+
+ UISceneClassName
+ UIWindowScene
+ UISceneConfigurationName
+ flutter
+ UISceneDelegateClassName
+ $(PRODUCT_MODULE_NAME).SceneDelegate
+ UISceneStoryboardFile
+ Main
+
+
+
+
+ UIApplicationSupportsIndirectInputEvents
+
+ UILaunchStoryboardName
+ LaunchScreen
+ UIMainStoryboardFile
+ Main
+ UISupportedInterfaceOrientations
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+ UISupportedInterfaceOrientations~ipad
+
+ UIInterfaceOrientationPortrait
+ UIInterfaceOrientationPortraitUpsideDown
+ UIInterfaceOrientationLandscapeLeft
+ UIInterfaceOrientationLandscapeRight
+
+
+
diff --git a/ios/Runner/Runner-Bridging-Header.h b/ios/Runner/Runner-Bridging-Header.h
new file mode 100644
index 0000000..308a2a5
--- /dev/null
+++ b/ios/Runner/Runner-Bridging-Header.h
@@ -0,0 +1 @@
+#import "GeneratedPluginRegistrant.h"
diff --git a/ios/Runner/SceneDelegate.swift b/ios/Runner/SceneDelegate.swift
new file mode 100644
index 0000000..b9ce8ea
--- /dev/null
+++ b/ios/Runner/SceneDelegate.swift
@@ -0,0 +1,6 @@
+import Flutter
+import UIKit
+
+class SceneDelegate: FlutterSceneDelegate {
+
+}
diff --git a/ios/RunnerTests/RunnerTests.swift b/ios/RunnerTests/RunnerTests.swift
new file mode 100644
index 0000000..86a7c3b
--- /dev/null
+++ b/ios/RunnerTests/RunnerTests.swift
@@ -0,0 +1,12 @@
+import Flutter
+import UIKit
+import XCTest
+
+class RunnerTests: XCTestCase {
+
+ func testExample() {
+ // If you add code to the Runner application, consider adding tests here.
+ // See https://developer.apple.com/documentation/xctest for more information about using XCTest.
+ }
+
+}
diff --git a/lib/main.dart b/lib/main.dart
new file mode 100644
index 0000000..f205b8e
--- /dev/null
+++ b/lib/main.dart
@@ -0,0 +1,27 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import '../screens/home_screen.dart';
+
+void main() {
+ runApp(const ProviderScope(child: BusylightApp()));
+}
+
+class BusylightApp extends StatelessWidget {
+ const BusylightApp({super.key});
+
+ @override
+ Widget build(BuildContext context) {
+ return MaterialApp(
+ title: 'BusyLight',
+ debugShowCheckedModeBanner: false,
+ theme: ThemeData.dark().copyWith(
+ colorScheme: ColorScheme.dark(
+ primary: Colors.amber,
+ secondary: Colors.amber.shade700,
+ ),
+ useMaterial3: true,
+ ),
+ home: const HomeScreen(),
+ );
+ }
+}
diff --git a/lib/models/busylight_color.dart b/lib/models/busylight_color.dart
new file mode 100644
index 0000000..840d97d
--- /dev/null
+++ b/lib/models/busylight_color.dart
@@ -0,0 +1,59 @@
+import 'package:flutter/material.dart';
+
+class BusylightColor {
+ final int r;
+ final int g;
+ final int b;
+ final double brightness;
+
+ const BusylightColor({
+ required this.r,
+ required this.g,
+ required this.b,
+ this.brightness = 1.0,
+ });
+
+ factory BusylightColor.fromJson(Map json) {
+ // GET /api/color returns { "colors": { r, g, b }, "brightness": 0.3 }
+ final colors = json['colors'] as Map? ?? json;
+ return BusylightColor(
+ r: (colors['r'] as num?)?.toInt() ?? 0,
+ g: (colors['g'] as num?)?.toInt() ?? 0,
+ b: (colors['b'] as num?)?.toInt() ?? 0,
+ brightness: (json['brightness'] as num?)?.toDouble() ?? 0.3,
+ );
+ }
+
+ Map toJson() => {
+ 'r': r,
+ 'g': g,
+ 'b': b,
+ 'brightness': brightness,
+ };
+
+ Color toFlutterColor() => Color.fromARGB(255, r, g, b);
+
+ factory BusylightColor.fromFlutterColor(Color color, {double brightness = 1.0}) {
+ return BusylightColor(
+ r: color.red,
+ g: color.green,
+ b: color.blue,
+ brightness: brightness,
+ );
+ }
+
+ BusylightColor copyWith({int? r, int? g, int? b, double? brightness}) {
+ return BusylightColor(
+ r: r ?? this.r,
+ g: g ?? this.g,
+ b: b ?? this.b,
+ brightness: brightness ?? this.brightness,
+ );
+ }
+
+ static const green = BusylightColor(r: 0, g: 255, b: 0);
+ static const red = BusylightColor(r: 255, g: 0, b: 0);
+ static const yellow = BusylightColor(r: 255, g: 200, b: 0);
+ static const white = BusylightColor(r: 255, g: 255, b: 255);
+ static const off = BusylightColor(r: 0, g: 0, b: 0, brightness: 0);
+}
\ No newline at end of file
diff --git a/lib/models/busylight_status.dart b/lib/models/busylight_status.dart
new file mode 100644
index 0000000..7f0ec92
--- /dev/null
+++ b/lib/models/busylight_status.dart
@@ -0,0 +1,37 @@
+enum BusylightStatus {
+ on,
+ off,
+ available,
+ away,
+ busy,
+ colored;
+
+ String get apiPath {
+ switch (this) {
+ case BusylightStatus.on: return '/api/status/on';
+ case BusylightStatus.off: return '/api/status/off';
+ case BusylightStatus.available: return '/api/status/available';
+ case BusylightStatus.away: return '/api/status/away';
+ case BusylightStatus.busy: return '/api/status/busy';
+ case BusylightStatus.colored: return '/api/status';
+ }
+ }
+
+ String get label {
+ switch (this) {
+ case BusylightStatus.on: return 'On';
+ case BusylightStatus.off: return 'Off';
+ case BusylightStatus.available: return 'Available';
+ case BusylightStatus.away: return 'Away';
+ case BusylightStatus.busy: return 'Busy';
+ case BusylightStatus.colored: return 'Custom';
+ }
+ }
+
+ static BusylightStatus fromString(String value) {
+ return BusylightStatus.values.firstWhere(
+ (e) => e.name == value,
+ orElse: () => BusylightStatus.off,
+ );
+ }
+}
diff --git a/lib/models/color_preset.dart b/lib/models/color_preset.dart
new file mode 100644
index 0000000..2dc68ab
--- /dev/null
+++ b/lib/models/color_preset.dart
@@ -0,0 +1,42 @@
+import 'dart:convert';
+import 'busylight_color.dart';
+
+class ColorPreset {
+ final String id;
+ final String name;
+ final BusylightColor color;
+
+ const ColorPreset({
+ required this.id,
+ required this.name,
+ required this.color,
+ });
+
+ factory ColorPreset.fromJson(Map json) {
+ return ColorPreset(
+ id: json['id'] as String,
+ name: json['name'] as String,
+ color: BusylightColor.fromJson(json['color'] as Map),
+ );
+ }
+
+ Map toJson() => {
+ 'id': id,
+ 'name': name,
+ 'color': {
+ 'r': color.r,
+ 'g': color.g,
+ 'b': color.b,
+ 'brightness': color.brightness,
+ },
+ };
+
+ static List listFromJson(String raw) {
+ final list = jsonDecode(raw) as List;
+ return list.map((e) => ColorPreset.fromJson(e as Map)).toList();
+ }
+
+ static String listToJson(List presets) {
+ return jsonEncode(presets.map((p) => p.toJson()).toList());
+ }
+}
\ No newline at end of file
diff --git a/lib/providers/busylight_provider.dart b/lib/providers/busylight_provider.dart
new file mode 100644
index 0000000..fa775f3
--- /dev/null
+++ b/lib/providers/busylight_provider.dart
@@ -0,0 +1,198 @@
+import 'dart:async';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:shared_preferences/shared_preferences.dart';
+import '../models/busylight_color.dart';
+import '../models/busylight_status.dart';
+import '../services/busylight_service.dart';
+
+// ── Device config ────────────────────────────────────────────────────────────
+
+const _kHostKey = 'busylight_host';
+const _kDefaultHost = 'http://igox-busylight.local';
+const _kPollIntervalKey = 'busylight_poll_interval';
+const _kDefaultPollInterval = 5; // seconds
+
+final sharedPreferencesProvider = FutureProvider(
+ (_) => SharedPreferences.getInstance(),
+);
+
+final deviceHostProvider = StateProvider((ref) {
+ final prefs = ref.watch(sharedPreferencesProvider).valueOrNull;
+ return prefs?.getString(_kHostKey) ?? _kDefaultHost;
+});
+
+final pollIntervalProvider = StateProvider((ref) {
+ final prefs = ref.watch(sharedPreferencesProvider).valueOrNull;
+ return prefs?.getInt(_kPollIntervalKey) ?? _kDefaultPollInterval;
+});
+
+// ── Service ──────────────────────────────────────────────────────────────────
+
+final busylightServiceProvider = Provider((ref) {
+ final host = ref.watch(deviceHostProvider);
+ return BusylightService(baseUrl: host);
+});
+
+// ── Startup snapshot ─────────────────────────────────────────────────────────
+// Loads status + color (which includes brightness) in 2 parallel calls.
+// No separate GET /api/brightness needed — the color response already has it.
+
+class BusylightSnapshot {
+ final BusylightStatus status;
+ final BusylightColor color;
+
+ const BusylightSnapshot({
+ required this.status,
+ required this.color,
+ });
+
+ double get brightness => color.brightness;
+}
+
+final busylightSnapshotProvider = FutureProvider((ref) async {
+ final service = ref.watch(busylightServiceProvider);
+ final results = await Future.wait([
+ service.getStatus(),
+ service.getColor(),
+ ]);
+ return BusylightSnapshot(
+ status: results[0] as BusylightStatus,
+ color: results[1] as BusylightColor,
+ );
+});
+
+// ── State notifiers ──────────────────────────────────────────────────────────
+
+class BusylightStateNotifier extends StateNotifier> {
+ BusylightStateNotifier(this._service, BusylightStatus? initial)
+ : super(initial != null
+ ? AsyncValue.data(initial)
+ : const AsyncValue.loading()) {
+ if (initial == null) refresh();
+ }
+
+ final BusylightService _service;
+
+ Future refresh() async {
+ state = const AsyncValue.loading();
+ state = await AsyncValue.guard(_service.getStatus);
+ }
+
+ Future setStatus(BusylightStatus status) async {
+ // Keep current value visible during the API call — no loading state
+ final result = await AsyncValue.guard(() => _service.setStatus(status));
+ state = result;
+ }
+
+ /// Update state locally without making an API call (e.g. for `colored`)
+ void setLocalStatus(BusylightStatus status) {
+ state = AsyncValue.data(status);
+ }
+}
+
+final busylightStatusProvider =
+ StateNotifierProvider>(
+ (ref) {
+ final snapshot = ref.watch(busylightSnapshotProvider).valueOrNull;
+ return BusylightStateNotifier(
+ ref.watch(busylightServiceProvider),
+ snapshot?.status,
+ );
+ },
+);
+
+// ── Brightness ───────────────────────────────────────────────────────────────
+
+class BrightnessNotifier extends StateNotifier {
+ BrightnessNotifier(this._service, double initial) : super(initial);
+ final BusylightService _service;
+
+ Future set(double value) async {
+ state = value;
+ await _service.setBrightness(value);
+ }
+
+ void silentSet(double value) => state = value;
+}
+
+final brightnessProvider = StateNotifierProvider(
+ (ref) {
+ final snapshot = ref.watch(busylightSnapshotProvider).valueOrNull;
+ return BrightnessNotifier(
+ ref.watch(busylightServiceProvider),
+ snapshot?.brightness ?? 0.3,
+ );
+ },
+);
+
+// ── Color ─────────────────────────────────────────────────────────────────────
+
+class ColorNotifier extends StateNotifier {
+ ColorNotifier(this._service, BusylightColor initial) : super(initial);
+ final BusylightService _service;
+
+ Future set(BusylightColor color) async {
+ state = color;
+ await _service.setColor(color);
+ }
+
+ void silentSet(BusylightColor color) => state = color;
+}
+
+final colorProvider = StateNotifierProvider(
+ (ref) {
+ final snapshot = ref.watch(busylightSnapshotProvider).valueOrNull;
+ return ColorNotifier(
+ ref.watch(busylightServiceProvider),
+ snapshot?.color ?? BusylightColor.white,
+ );
+ },
+);
+
+// ── Background polling ────────────────────────────────────────────────────────
+// Periodically pulls status + color from the device and silently updates state.
+
+class PollingNotifier extends StateNotifier {
+ PollingNotifier(this._ref) : super(null) {
+ _start();
+ }
+
+ final Ref _ref;
+ Timer? _timer;
+
+ void _start() {
+ final interval = _ref.read(pollIntervalProvider);
+ _timer?.cancel();
+ if (interval <= 0) return;
+ _timer = Timer.periodic(Duration(seconds: interval), (_) => _poll());
+ }
+
+ void restart() => _start();
+
+ Future _poll() async {
+ try {
+ final service = _ref.read(busylightServiceProvider);
+ final results = await Future.wait([
+ service.getStatus(),
+ service.getColor(),
+ ]);
+ final status = results[0] as BusylightStatus;
+ final color = results[1] as BusylightColor;
+ _ref.read(busylightStatusProvider.notifier).setLocalStatus(status);
+ _ref.read(colorProvider.notifier).silentSet(color);
+ _ref.read(brightnessProvider.notifier).silentSet(color.brightness);
+ } catch (_) {
+ // Silently ignore poll errors — connection issues are shown on manual refresh
+ }
+ }
+
+ @override
+ void dispose() {
+ _timer?.cancel();
+ super.dispose();
+ }
+}
+
+final pollingProvider = StateNotifierProvider(
+ (ref) => PollingNotifier(ref),
+);
\ No newline at end of file
diff --git a/lib/providers/presets_provider.dart b/lib/providers/presets_provider.dart
new file mode 100644
index 0000000..d76988d
--- /dev/null
+++ b/lib/providers/presets_provider.dart
@@ -0,0 +1,57 @@
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:shared_preferences/shared_preferences.dart';
+import 'package:uuid/uuid.dart';
+import '../models/color_preset.dart';
+import '../models/busylight_color.dart';
+
+const _kPresetsKey = 'busylight_color_presets';
+const _uuid = Uuid();
+
+class PresetsNotifier extends StateNotifier> {
+ PresetsNotifier() : super([]) {
+ _load();
+ }
+
+ Future _load() async {
+ final prefs = await SharedPreferences.getInstance();
+ final raw = prefs.getString(_kPresetsKey);
+ if (raw != null) {
+ try {
+ state = ColorPreset.listFromJson(raw);
+ } catch (_) {
+ state = [];
+ }
+ }
+ }
+
+ Future _save() async {
+ final prefs = await SharedPreferences.getInstance();
+ await prefs.setString(_kPresetsKey, ColorPreset.listToJson(state));
+ }
+
+ Future add(String name, BusylightColor color) async {
+ final preset = ColorPreset(
+ id: _uuid.v4(),
+ name: name.trim(),
+ color: color,
+ );
+ state = [...state, preset];
+ await _save();
+ }
+
+ Future update(String id, String name, BusylightColor color) async {
+ state = state.map((p) => p.id == id
+ ? ColorPreset(id: id, name: name.trim(), color: color)
+ : p).toList();
+ await _save();
+ }
+
+ Future remove(String id) async {
+ state = state.where((p) => p.id != id).toList();
+ await _save();
+ }
+}
+
+final presetsProvider = StateNotifierProvider>(
+ (_) => PresetsNotifier(),
+);
\ No newline at end of file
diff --git a/lib/screens/home_screen.dart b/lib/screens/home_screen.dart
new file mode 100644
index 0000000..3235863
--- /dev/null
+++ b/lib/screens/home_screen.dart
@@ -0,0 +1,817 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_colorpicker/flutter_colorpicker.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import '../models/busylight_color.dart';
+import '../models/busylight_status.dart';
+import '../models/color_preset.dart';
+import '../providers/busylight_provider.dart';
+import '../providers/presets_provider.dart';
+import '../widgets/brightness_slider.dart';
+import '../widgets/status_button.dart';
+import 'settings_screen.dart';
+
+// ── App bar shared between screens ───────────────────────────────────────────
+
+AppBar _buildAppBar(BuildContext context) => AppBar(
+ backgroundColor: Colors.black,
+ title: const Text('BusyLight', style: TextStyle(color: Colors.white)),
+ actions: [
+ IconButton(
+ icon: const Icon(Icons.settings_outlined, color: Colors.white),
+ onPressed: () => Navigator.push(
+ context,
+ MaterialPageRoute(builder: (_) => const SettingsScreen()),
+ ),
+ ),
+ ],
+);
+
+// ── HomeScreen ────────────────────────────────────────────────────────────────
+
+class HomeScreen extends ConsumerWidget {
+ const HomeScreen({super.key});
+
+ @override
+ Widget build(BuildContext context, WidgetRef ref) {
+ final snapshot = ref.watch(busylightSnapshotProvider);
+ final statusAsync = ref.watch(busylightStatusProvider);
+
+ // Start background polling (no-op if already running)
+ ref.watch(pollingProvider);
+
+ if (!snapshot.hasValue && !snapshot.hasError) {
+ return Scaffold(
+ backgroundColor: Colors.black,
+ appBar: _buildAppBar(context),
+ body: const Center(child: CircularProgressIndicator(color: Colors.amber)),
+ );
+ }
+ if (snapshot.hasError && !statusAsync.hasValue) {
+ return Scaffold(
+ backgroundColor: Colors.black,
+ appBar: _buildAppBar(context),
+ body: _ErrorView(
+ message: snapshot.error.toString(),
+ onRetry: () => ref.invalidate(busylightSnapshotProvider),
+ ),
+ );
+ }
+
+ return Scaffold(
+ backgroundColor: Colors.black,
+ appBar: _buildAppBar(context),
+ // _Body reads all providers itself — no props passed down
+ body: statusAsync.when(
+ loading: () => const Center(child: CircularProgressIndicator(color: Colors.amber)),
+ error: (e, _) => _ErrorView(
+ message: e.toString(),
+ onRetry: () => ref.read(busylightStatusProvider.notifier).refresh(),
+ ),
+ data: (_) => const _Body(),
+ ),
+ );
+ }
+}
+
+// ── Body ──────────────────────────────────────────────────────────────────────
+// ConsumerStatefulWidget so ref is stable across rebuilds and dialogs.
+// Reads all providers itself — receives NO props from HomeScreen.
+
+class _Body extends ConsumerStatefulWidget {
+ const _Body();
+
+ @override
+ ConsumerState<_Body> createState() => _BodyState();
+}
+
+class _BodyState extends ConsumerState<_Body> {
+ BusylightStatus? _pendingStatus;
+ String? _pendingPresetId;
+
+ Color _statusColor(BusylightStatus status, BusylightColor color) {
+ switch (status) {
+ case BusylightStatus.available: return Colors.green;
+ case BusylightStatus.away: return Colors.orange;
+ case BusylightStatus.busy: return Colors.red;
+ case BusylightStatus.on: return Colors.white;
+ case BusylightStatus.off: return Colors.grey.shade900;
+ case BusylightStatus.colored: return color.toFlutterColor();
+ }
+ }
+
+ Future _setStatus(BusylightStatus s) async {
+ setState(() => _pendingStatus = s);
+ await ref.read(busylightStatusProvider.notifier).setStatus(s);
+ if (mounted) setState(() => _pendingStatus = null);
+ }
+
+ Future _applyPreset(ColorPreset preset) async {
+ setState(() => _pendingPresetId = preset.id);
+ await ref.read(colorProvider.notifier).set(preset.color);
+ ref.read(busylightStatusProvider.notifier).setLocalStatus(BusylightStatus.colored);
+ if (mounted) setState(() => _pendingPresetId = null);
+ }
+
+ void _editPreset(ColorPreset preset) {
+ _openColorPicker(preset.color, ref.read(brightnessProvider), editingPreset: preset);
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final statusAsync = ref.watch(busylightStatusProvider);
+ final brightness = ref.watch(brightnessProvider);
+ final color = ref.watch(colorProvider);
+ final presets = ref.watch(presetsProvider);
+
+ final status = statusAsync.valueOrNull ?? BusylightStatus.off;
+ final displayColor = _statusColor(status, color);
+
+ return ListView(
+ padding: const EdgeInsets.all(24),
+ children: [
+ // Live color preview dot
+ Center(
+ child: AnimatedContainer(
+ duration: const Duration(milliseconds: 400),
+ width: 100,
+ height: 100,
+ decoration: BoxDecoration(
+ shape: BoxShape.circle,
+ color: status == BusylightStatus.off
+ ? Colors.grey.shade900
+ : displayColor.withOpacity(brightness),
+ boxShadow: status != BusylightStatus.off
+ ? [BoxShadow(
+ color: displayColor.withOpacity(0.5 * brightness),
+ blurRadius: 40,
+ spreadRadius: 8,
+ )]
+ : null,
+ ),
+ ),
+ ),
+ const SizedBox(height: 12),
+ Center(
+ child: Text(
+ status.label.toUpperCase(),
+ style: const TextStyle(color: Colors.grey, letterSpacing: 2, fontSize: 13),
+ ),
+ ),
+ const SizedBox(height: 36),
+
+ // Quick status
+ const _SectionLabel('Quick status'),
+ const SizedBox(height: 12),
+ GridView.count(
+ shrinkWrap: true,
+ physics: const NeverScrollableScrollPhysics(),
+ crossAxisCount: 3,
+ mainAxisSpacing: 10,
+ crossAxisSpacing: 10,
+ childAspectRatio: 1.1,
+ children: [
+ BusylightStatus.available,
+ BusylightStatus.away,
+ BusylightStatus.busy,
+ BusylightStatus.on,
+ BusylightStatus.off,
+ ].map((s) => StatusButton(
+ status: s,
+ isActive: status == s,
+ isPending: _pendingStatus == s,
+ onTap: _pendingStatus == null ? () => _setStatus(s) : () {},
+ )).toList(),
+ ),
+ const SizedBox(height: 32),
+
+ // Custom presets + add button (horizontal scroll, never pushes content down)
+ _PresetsScroller(
+ presets: presets,
+ pendingPresetId: _pendingPresetId,
+ onPresetTap: (_pendingStatus == null && _pendingPresetId == null)
+ ? _applyPreset
+ : (_) {},
+ onPresetDelete: (preset) => ref.read(presetsProvider.notifier).remove(preset.id),
+ onPresetEdit: _editPreset,
+ onAddTap: () => _openColorPicker(color, brightness),
+ ),
+ const SizedBox(height: 32),
+
+ // Brightness
+ const _SectionLabel('Brightness'),
+ const SizedBox(height: 8),
+ BrightnessSlider(
+ value: brightness,
+ onChanged: (v) => ref.read(brightnessProvider.notifier).set(v),
+ ),
+ const SizedBox(height: 40),
+ ],
+ );
+ }
+
+ // ── Color picker dialog ───────────────────────────────────────────────────
+
+ void _openColorPicker(BusylightColor currentColor, double currentBrightness, {ColorPreset? editingPreset}) {
+ Color pickerColor = currentColor.toFlutterColor();
+
+ showDialog(
+ context: context,
+ builder: (ctx) => StatefulBuilder(
+ builder: (ctx, setDialogState) => AlertDialog(
+ backgroundColor: Colors.grey.shade900,
+ title: Text(
+ editingPreset != null ? 'Edit color' : 'Pick a color',
+ style: const TextStyle(color: Colors.white),
+ ),
+ content: SingleChildScrollView(
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ ColorPicker(
+ pickerColor: pickerColor,
+ onColorChanged: (c) => setDialogState(() => pickerColor = c),
+ pickerAreaHeightPercent: 0.7,
+ enableAlpha: false,
+ displayThumbColor: true,
+ labelTypes: const [],
+ ),
+ SlidePicker(
+ pickerColor: pickerColor,
+ onColorChanged: (c) => setDialogState(() => pickerColor = c),
+ colorModel: ColorModel.rgb,
+ enableAlpha: false,
+ displayThumbColor: true,
+ showParams: true,
+ showIndicator: false,
+ ),
+ const SizedBox(height: 16),
+ // Buttons stacked vertically — avoids collision on narrow macOS dialogs
+ Row(
+ mainAxisAlignment: MainAxisAlignment.end,
+ children: [
+ TextButton(
+ onPressed: () => Navigator.pop(ctx),
+ style: TextButton.styleFrom(
+ foregroundColor: Colors.grey,
+ shape: const StadiumBorder(),
+ ),
+ child: const Text('Cancel'),
+ ),
+ ],
+ ),
+ if (editingPreset == null)
+ SizedBox(
+ width: double.infinity,
+ child: OutlinedButton(
+ onPressed: () {
+ final picked = BusylightColor.fromFlutterColor(
+ pickerColor, brightness: currentBrightness,
+ );
+ Navigator.pop(ctx);
+ ref.read(colorProvider.notifier).set(picked);
+ ref.read(busylightStatusProvider.notifier).setLocalStatus(BusylightStatus.colored);
+ },
+ style: OutlinedButton.styleFrom(
+ foregroundColor: Colors.white,
+ side: BorderSide(color: Colors.grey.shade600),
+ shape: const StadiumBorder(),
+ ),
+ child: const Text('Apply only'),
+ ),
+ ),
+ const SizedBox(height: 8),
+ SizedBox(
+ width: double.infinity,
+ child: ElevatedButton.icon(
+ onPressed: () {
+ final picked = BusylightColor.fromFlutterColor(
+ pickerColor, brightness: currentBrightness,
+ );
+ Navigator.pop(ctx);
+ WidgetsBinding.instance.addPostFrameCallback((_) {
+ _openNameDialog(picked, editingPreset: editingPreset);
+ });
+ },
+ icon: const Icon(Icons.bookmark_outline, size: 16, color: Colors.black),
+ label: Text(
+ editingPreset != null ? 'Save' : 'Save & Apply',
+ style: const TextStyle(color: Colors.black),
+ ),
+ style: ElevatedButton.styleFrom(
+ backgroundColor: Colors.amber,
+ shape: const StadiumBorder(),
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ ),
+ ),
+ );
+ }
+
+ // ── Preset name dialog ────────────────────────────────────────────────────
+
+ void _openNameDialog(BusylightColor color, {ColorPreset? editingPreset}) {
+ showDialog(
+ context: context,
+ builder: (ctx) => _NamePresetDialog(
+ initialName: editingPreset?.name ?? '',
+ onSave: (name) {
+ if (editingPreset != null) {
+ // Edit mode: only update the saved preset, do not touch the BusyLight
+ ref.read(presetsProvider.notifier).update(editingPreset.id, name, color);
+ } else {
+ // Create mode: save preset and apply color to BusyLight
+ ref.read(presetsProvider.notifier).add(name, color);
+ ref.read(colorProvider.notifier).set(color);
+ ref.read(busylightStatusProvider.notifier).setLocalStatus(BusylightStatus.colored);
+ }
+ },
+ ),
+ );
+ }
+}
+
+// ── Presets scroller with overflow indicator ──────────────────────────────────
+
+class _PresetsScroller extends StatefulWidget {
+ final List presets;
+ final String? pendingPresetId;
+ final ValueChanged onPresetTap;
+ final ValueChanged onPresetDelete;
+ final ValueChanged onPresetEdit;
+ final VoidCallback onAddTap;
+
+ const _PresetsScroller({
+ required this.presets,
+ required this.onPresetTap,
+ required this.onPresetDelete,
+ required this.onPresetEdit,
+ required this.onAddTap,
+ this.pendingPresetId,
+ });
+
+ @override
+ State<_PresetsScroller> createState() => _PresetsScrollerState();
+}
+
+class _PresetsScrollerState extends State<_PresetsScroller> {
+ final _scrollController = ScrollController();
+ bool _hasOverflow = false;
+ int _hiddenCount = 0;
+
+ @override
+ void initState() {
+ super.initState();
+ _scrollController.addListener(_onScroll);
+ WidgetsBinding.instance.addPostFrameCallback((_) => _updateOverflow());
+ }
+
+ @override
+ void didUpdateWidget(_PresetsScroller old) {
+ super.didUpdateWidget(old);
+ WidgetsBinding.instance.addPostFrameCallback((_) => _updateOverflow());
+ }
+
+ @override
+ void dispose() {
+ _scrollController.removeListener(_onScroll);
+ _scrollController.dispose();
+ super.dispose();
+ }
+
+ void _onScroll() => _updateOverflow();
+
+ void _updateOverflow() {
+ if (!_scrollController.hasClients) return;
+ final pos = _scrollController.position;
+ final overflow = pos.maxScrollExtent - pos.pixels;
+ final hasMore = overflow > 10;
+
+ // Estimate hidden count: avg chip ~110px wide + 10px gap
+ // Subtract 1 to exclude the "+ New" chip from the count
+ final hidden = hasMore ? ((overflow / 120).ceil() - 1).clamp(0, 999) : 0;
+
+ if (hasMore != _hasOverflow || hidden != _hiddenCount) {
+ setState(() {
+ _hasOverflow = hasMore;
+ _hiddenCount = hidden;
+ });
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ // Header row: "Custom" label + overflow count on the same line
+ Row(
+ children: [
+ Text(
+ 'Custom',
+ style: TextStyle(
+ color: Colors.grey.shade300,
+ fontSize: 13,
+ letterSpacing: 0.3,
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ if (_hasOverflow && _hiddenCount > 0) ...[
+ const SizedBox(width: 8),
+ Text(
+ '· +$_hiddenCount more',
+ style: TextStyle(
+ color: Colors.grey.shade500,
+ fontSize: 12,
+ letterSpacing: 1.2,
+ fontWeight: FontWeight.w500,
+ ),
+ ),
+ ],
+ ],
+ ),
+ const SizedBox(height: 12),
+ Stack(
+ children: [
+ SingleChildScrollView(
+ controller: _scrollController,
+ scrollDirection: Axis.horizontal,
+ child: Row(
+ children: [
+ ...widget.presets.map((preset) => Padding(
+ padding: const EdgeInsets.only(right: 10),
+ child: _PresetChip(
+ preset: preset,
+ isPending: widget.pendingPresetId == preset.id,
+ onTap: () => widget.onPresetTap(preset),
+ onDelete: () => widget.onPresetDelete(preset),
+ onEdit: widget.onPresetEdit,
+ ),
+ )),
+ GestureDetector(
+ onTap: widget.onAddTap,
+ child: Container(
+ height: 44,
+ padding: const EdgeInsets.symmetric(horizontal: 14),
+ decoration: BoxDecoration(
+ border: Border.all(color: Colors.grey.shade800, width: 1.5),
+ borderRadius: BorderRadius.circular(12),
+ ),
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Icon(Icons.add, color: Colors.grey.shade600, size: 16),
+ const SizedBox(width: 6),
+ Text('New', style: TextStyle(color: Colors.grey.shade600, fontSize: 13)),
+ ],
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ // Fade + arrow overlay on the right edge
+ if (_hasOverflow)
+ Positioned(
+ right: 0,
+ top: 0,
+ bottom: 0,
+ child: IgnorePointer(
+ child: Container(
+ width: 60,
+ decoration: BoxDecoration(
+ gradient: LinearGradient(
+ begin: Alignment.centerLeft,
+ end: Alignment.centerRight,
+ colors: [Colors.black.withOpacity(0), Colors.black],
+ ),
+ ),
+ alignment: Alignment.centerRight,
+ padding: const EdgeInsets.only(right: 6),
+ child: Icon(Icons.chevron_right, color: Colors.grey.shade500, size: 20),
+ ),
+ ),
+ ),
+ ],
+ ),
+ ],
+ );
+ }
+}
+
+// ── Name preset dialog ────────────────────────────────────────────────────────
+
+class _NamePresetDialog extends StatefulWidget {
+ final ValueChanged onSave;
+ final String initialName;
+ const _NamePresetDialog({required this.onSave, this.initialName = ''});
+
+ @override
+ State<_NamePresetDialog> createState() => _NamePresetDialogState();
+}
+
+class _NamePresetDialogState extends State<_NamePresetDialog> {
+ late final TextEditingController _controller;
+
+ @override
+ void initState() {
+ super.initState();
+ _controller = TextEditingController(text: widget.initialName);
+ // Select all text so user can type a new name immediately
+ _controller.selection = TextSelection(
+ baseOffset: 0, extentOffset: widget.initialName.length,
+ );
+ }
+
+ @override
+ void dispose() {
+ _controller.dispose();
+ super.dispose();
+ }
+
+ void _submit() {
+ final name = _controller.text.trim();
+ if (name.isEmpty) return;
+ Navigator.pop(context);
+ widget.onSave(name);
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return AlertDialog(
+ backgroundColor: Colors.grey.shade900,
+ title: Text(
+ widget.initialName.isNotEmpty ? 'Rename preset' : 'Name this preset',
+ style: const TextStyle(color: Colors.white),
+ ),
+ content: TextField(
+ controller: _controller,
+ autofocus: true,
+ maxLength: 20,
+ style: const TextStyle(color: Colors.white),
+ decoration: InputDecoration(
+ hintText: 'e.g. Love, Focus, Chill…',
+ hintStyle: TextStyle(color: Colors.grey.shade600),
+ counterStyle: TextStyle(color: Colors.grey.shade600),
+ filled: true,
+ fillColor: Colors.grey.shade800,
+ border: OutlineInputBorder(
+ borderRadius: BorderRadius.circular(8),
+ borderSide: BorderSide.none,
+ ),
+ ),
+ onSubmitted: (_) => _submit(),
+ ),
+ actions: [
+ TextButton(
+ onPressed: () => Navigator.pop(context),
+ child: const Text('Cancel', style: TextStyle(color: Colors.grey)),
+ ),
+ ElevatedButton(
+ onPressed: _submit,
+ style: ElevatedButton.styleFrom(backgroundColor: Colors.amber),
+ child: const Text('Save', style: TextStyle(color: Colors.black)),
+ ),
+ ],
+ );
+ }
+}
+
+// ── Preset chip ───────────────────────────────────────────────────────────────
+
+class _PresetChip extends StatelessWidget {
+ final ColorPreset preset;
+ final bool isPending;
+ final VoidCallback onTap;
+ final VoidCallback onDelete;
+ final ValueChanged onEdit;
+
+ const _PresetChip({
+ required this.preset,
+ required this.onTap,
+ required this.onDelete,
+ required this.onEdit,
+ this.isPending = false,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ final chipColor = preset.color.toFlutterColor();
+ return GestureDetector(
+ onTap: onTap,
+ onLongPress: () => _showOptions(context),
+ child: Container(
+ height: 44,
+ padding: const EdgeInsets.symmetric(horizontal: 14),
+ decoration: BoxDecoration(
+ color: chipColor.withOpacity(0.08),
+ border: Border.all(color: chipColor.withOpacity(0.5), width: 1.5),
+ borderRadius: BorderRadius.circular(12),
+ ),
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ isPending
+ ? SizedBox(
+ width: 9,
+ height: 9,
+ child: CircularProgressIndicator(
+ strokeWidth: 1.5,
+ valueColor: AlwaysStoppedAnimation(chipColor),
+ ),
+ )
+ : Container(
+ width: 9,
+ height: 9,
+ decoration: BoxDecoration(color: chipColor, shape: BoxShape.circle),
+ ),
+ const SizedBox(width: 8),
+ Text(preset.name, style: TextStyle(color: Colors.grey.shade200, fontSize: 13)),
+ ],
+ ),
+ ),
+ );
+ }
+
+ void _showOptions(BuildContext context) {
+ showModalBottomSheet(
+ context: context,
+ backgroundColor: Colors.grey.shade900,
+ shape: const RoundedRectangleBorder(
+ borderRadius: BorderRadius.vertical(top: Radius.circular(16)),
+ ),
+ builder: (ctx) => SafeArea(
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Container(
+ width: 36, height: 4,
+ margin: const EdgeInsets.only(top: 12, bottom: 16),
+ decoration: BoxDecoration(
+ color: Colors.grey.shade700,
+ borderRadius: BorderRadius.circular(2),
+ ),
+ ),
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 20),
+ child: Row(
+ children: [
+ Container(
+ width: 10, height: 10,
+ decoration: BoxDecoration(
+ color: preset.color.toFlutterColor(),
+ shape: BoxShape.circle,
+ ),
+ ),
+ const SizedBox(width: 10),
+ Text(
+ preset.name,
+ style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w600, fontSize: 15),
+ ),
+ ],
+ ),
+ ),
+ const SizedBox(height: 16),
+ const Divider(height: 1, color: Color(0xFF2a2a2a)),
+ ListTile(
+ leading: const Icon(Icons.edit_outlined, color: Colors.white),
+ title: const Text('Edit', style: TextStyle(color: Colors.white)),
+ onTap: () {
+ Navigator.pop(ctx);
+ onEdit(preset);
+ },
+ ),
+ ListTile(
+ leading: Icon(Icons.delete_outline, color: Colors.red.shade400),
+ title: Text('Delete', style: TextStyle(color: Colors.red.shade400)),
+ onTap: () {
+ Navigator.pop(ctx);
+ onDelete();
+ },
+ ),
+ const SizedBox(height: 8),
+ ],
+ ),
+ ),
+ );
+ }
+}
+
+// ── Section label ─────────────────────────────────────────────────────────────
+
+class _SectionLabel extends StatelessWidget {
+ final String text;
+ const _SectionLabel(this.text);
+
+ @override
+ Widget build(BuildContext context) => Text(
+ text,
+ style: TextStyle(
+ color: Colors.grey.shade300,
+ fontSize: 13,
+ letterSpacing: 0.3,
+ fontWeight: FontWeight.w600,
+ ),
+ );
+}
+
+// ── Error view ────────────────────────────────────────────────────────────────
+
+class _ErrorView extends StatefulWidget {
+ final String message;
+ final VoidCallback onRetry;
+
+ const _ErrorView({required this.message, required this.onRetry});
+
+ @override
+ State<_ErrorView> createState() => _ErrorViewState();
+}
+
+class _ErrorViewState extends State<_ErrorView> {
+ bool _showDetails = false;
+
+ String get _friendlyMessage {
+ const hint = '\nAlso double-check the device address in ⚙ Settings.';
+ final m = widget.message.toLowerCase();
+ if (m.contains('socket') || m.contains('network') || m.contains('connection refused'))
+ return 'Make sure your BusyLight is powered on and connected to the same Wi-Fi network.$hint';
+ if (m.contains('timeout'))
+ return 'Connection timed out. Your BusyLight may be out of range or busy.$hint';
+ if (m.contains('404') || m.contains('not found'))
+ return 'BusyLight was reached but returned an unexpected response.$hint';
+ if (m.contains('host') || m.contains('lookup'))
+ return 'Could not find your BusyLight on the network.$hint';
+ return 'Could not connect to your BusyLight.$hint';
+ }
+
+ @override
+ Widget build(BuildContext context) => Center(
+ child: Padding(
+ padding: const EdgeInsets.all(32),
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ const Icon(Icons.wifi_off, color: Colors.grey, size: 48),
+ const SizedBox(height: 16),
+ const Text('Cannot reach BusyLight',
+ style: TextStyle(color: Colors.white, fontSize: 18)),
+ const SizedBox(height: 8),
+ Text(_friendlyMessage,
+ style: TextStyle(color: Colors.grey.shade500, fontSize: 13),
+ textAlign: TextAlign.center),
+ const SizedBox(height: 16),
+
+ // Collapsible details
+ GestureDetector(
+ onTap: () => setState(() => _showDetails = !_showDetails),
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ Text(
+ 'Details',
+ style: TextStyle(color: Colors.grey.shade600, fontSize: 12),
+ ),
+ const SizedBox(width: 4),
+ Icon(
+ _showDetails ? Icons.expand_less : Icons.expand_more,
+ color: Colors.grey.shade600,
+ size: 16,
+ ),
+ ],
+ ),
+ ),
+ if (_showDetails) ...[
+ const SizedBox(height: 8),
+ Container(
+ width: double.infinity,
+ padding: const EdgeInsets.all(12),
+ decoration: BoxDecoration(
+ color: Colors.grey.shade900,
+ borderRadius: BorderRadius.circular(8),
+ border: Border.all(color: Colors.grey.shade800),
+ ),
+ child: Text(
+ widget.message,
+ style: TextStyle(
+ color: Colors.grey.shade400,
+ fontSize: 11,
+ fontFamily: 'monospace',
+ ),
+ ),
+ ),
+ ],
+
+ const SizedBox(height: 24),
+ ElevatedButton(
+ onPressed: widget.onRetry,
+ style: ElevatedButton.styleFrom(backgroundColor: Colors.amber),
+ child: const Text('Retry', style: TextStyle(color: Colors.black)),
+ ),
+ ],
+ ),
+ ),
+ );
+}
\ No newline at end of file
diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart
new file mode 100644
index 0000000..2b66bbb
--- /dev/null
+++ b/lib/screens/settings_screen.dart
@@ -0,0 +1,200 @@
+import 'dart:io';
+import 'package:flutter/material.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:shared_preferences/shared_preferences.dart';
+import '../providers/busylight_provider.dart';
+import '../services/autostart_service.dart';
+
+class SettingsScreen extends ConsumerStatefulWidget {
+ const SettingsScreen({super.key});
+
+ @override
+ ConsumerState createState() => _SettingsScreenState();
+}
+
+class _SettingsScreenState extends ConsumerState {
+ late TextEditingController _hostController;
+ late int _pollInterval;
+ bool _startWithSession = false;
+
+ @override
+ void initState() {
+ super.initState();
+ _hostController = TextEditingController(text: ref.read(deviceHostProvider));
+ _pollInterval = ref.read(pollIntervalProvider);
+ _loadAutostart();
+ }
+
+ Future _loadAutostart() async {
+ final enabled = await AutostartService.isEnabled();
+ if (mounted) setState(() => _startWithSession = enabled);
+ }
+
+ @override
+ void dispose() {
+ _hostController.dispose();
+ super.dispose();
+ }
+
+ Future _save() async {
+ final host = _hostController.text.trim();
+ if (host.isEmpty) return;
+
+ ref.read(deviceHostProvider.notifier).state = host;
+ ref.read(pollIntervalProvider.notifier).state = _pollInterval;
+ ref.read(pollingProvider.notifier).restart();
+
+ await AutostartService.setEnabled(_startWithSession);
+
+ final prefs = await SharedPreferences.getInstance();
+ await prefs.setString('busylight_host', host);
+ await prefs.setInt('busylight_poll_interval', _pollInterval);
+
+ if (mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ const SnackBar(content: Text('Settings saved')),
+ );
+ Navigator.pop(context);
+ }
+ }
+
+ String _intervalLabel(int seconds) {
+ if (seconds == 0) return 'Off';
+ if (seconds < 60) return '${seconds}s';
+ return '${seconds ~/ 60}m';
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return Scaffold(
+ backgroundColor: Colors.black,
+ appBar: AppBar(
+ backgroundColor: Colors.black,
+ title: const Text('Settings', style: TextStyle(color: Colors.white)),
+ iconTheme: const IconThemeData(color: Colors.white),
+ ),
+ body: Padding(
+ padding: const EdgeInsets.all(24),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ // ── Device address ──────────────────────────────────────────────
+ Text('Device address',
+ style: TextStyle(color: Colors.grey.shade300, fontSize: 13, fontWeight: FontWeight.w600)),
+ const SizedBox(height: 8),
+ TextField(
+ controller: _hostController,
+ style: const TextStyle(color: Colors.white),
+ decoration: InputDecoration(
+ hintText: 'http://igox-busylight.local',
+ hintStyle: TextStyle(color: Colors.grey.shade700),
+ filled: true,
+ fillColor: Colors.grey.shade900,
+ border: OutlineInputBorder(
+ borderRadius: BorderRadius.circular(10),
+ borderSide: BorderSide.none,
+ ),
+ ),
+ ),
+ const SizedBox(height: 6),
+ Text(
+ 'Use hostname (igox-busylight.local) or IP address (http://192.168.x.x)',
+ style: TextStyle(color: Colors.grey.shade600, fontSize: 12),
+ ),
+ const SizedBox(height: 32),
+
+ // ── Polling interval ────────────────────────────────────────────
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Text('Status polling',
+ style: TextStyle(color: Colors.grey.shade300, fontSize: 13, fontWeight: FontWeight.w600)),
+ Text(
+ _intervalLabel(_pollInterval),
+ style: const TextStyle(color: Colors.amber, fontSize: 13, fontWeight: FontWeight.w600),
+ ),
+ ],
+ ),
+ const SizedBox(height: 8),
+ SliderTheme(
+ data: SliderTheme.of(context).copyWith(
+ activeTrackColor: Colors.amber,
+ thumbColor: Colors.amber,
+ inactiveTrackColor: Colors.grey.shade800,
+ overlayColor: Colors.amber.withOpacity(0.2),
+ ),
+ child: Slider(
+ value: _pollInterval.toDouble(),
+ min: 0,
+ max: 60,
+ divisions: 12,
+ onChanged: (v) => setState(() => _pollInterval = v.round()),
+ ),
+ ),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Text('Off', style: TextStyle(color: Colors.grey.shade600, fontSize: 11)),
+ Text('5s', style: TextStyle(color: Colors.grey.shade600, fontSize: 11)),
+ Text('10s', style: TextStyle(color: Colors.grey.shade600, fontSize: 11)),
+ Text('30s', style: TextStyle(color: Colors.grey.shade600, fontSize: 11)),
+ Text('1m', style: TextStyle(color: Colors.grey.shade600, fontSize: 11)),
+ ],
+ ),
+ const SizedBox(height: 6),
+ Text(
+ _pollInterval == 0
+ ? 'Polling is disabled. Status will only refresh manually.'
+ : 'Status is pulled from the device every $_pollInterval seconds.',
+ style: TextStyle(color: Colors.grey.shade600, fontSize: 12),
+ ),
+
+ // ── Start with session (macOS + Windows only) ───────────────────
+ if (AutostartService.isSupported) ...[
+ const SizedBox(height: 32),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Text('Start with session',
+ style: TextStyle(color: Colors.grey.shade300, fontSize: 13, fontWeight: FontWeight.w600)),
+ const SizedBox(height: 2),
+ Text(
+ 'Launch automatically at login',
+ style: TextStyle(color: Colors.grey.shade600, fontSize: 12),
+ ),
+ ],
+ ),
+ Switch(
+ value: _startWithSession,
+ onChanged: (v) => setState(() => _startWithSession = v),
+ activeColor: Colors.amber,
+ ),
+ ],
+ ),
+ ],
+
+ const Spacer(),
+
+ // ── Save ────────────────────────────────────────────────────────
+ SizedBox(
+ width: double.infinity,
+ child: ElevatedButton(
+ onPressed: _save,
+ style: ElevatedButton.styleFrom(
+ backgroundColor: Colors.amber,
+ foregroundColor: Colors.black,
+ padding: const EdgeInsets.symmetric(vertical: 16),
+ shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
+ ),
+ child: const Text('Save', style: TextStyle(fontWeight: FontWeight.w600)),
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}
\ No newline at end of file
diff --git a/lib/services/autostart_service.dart b/lib/services/autostart_service.dart
new file mode 100644
index 0000000..567875c
--- /dev/null
+++ b/lib/services/autostart_service.dart
@@ -0,0 +1,25 @@
+import 'dart:io';
+import 'package:flutter/services.dart';
+
+class AutostartService {
+ static const _channel = MethodChannel('com.igox.busylight_buddy/autostart');
+
+ /// Returns true only on supported platforms (macOS, Windows)
+ static bool get isSupported => Platform.isMacOS || Platform.isWindows;
+
+ static Future isEnabled() async {
+ if (!isSupported) return false;
+ try {
+ return await _channel.invokeMethod('isEnabled') ?? false;
+ } catch (_) {
+ return false;
+ }
+ }
+
+ static Future setEnabled(bool enabled) async {
+ if (!isSupported) return;
+ try {
+ await _channel.invokeMethod('setEnabled', {'enabled': enabled});
+ } catch (_) {}
+ }
+}
\ No newline at end of file
diff --git a/lib/services/busylight_service.dart b/lib/services/busylight_service.dart
new file mode 100644
index 0000000..ce31fcb
--- /dev/null
+++ b/lib/services/busylight_service.dart
@@ -0,0 +1,99 @@
+import 'dart:convert';
+import 'package:http/http.dart' as http;
+import '../models/busylight_color.dart';
+import '../models/busylight_status.dart';
+
+class BusylightException implements Exception {
+ final String message;
+ const BusylightException(this.message);
+ @override
+ String toString() => 'BusylightException: $message';
+}
+
+class BusylightService {
+ final String baseUrl;
+ final Duration timeout;
+
+ BusylightService({
+ required this.baseUrl,
+ this.timeout = const Duration(seconds: 5),
+ });
+
+ Uri _uri(String path) => Uri.parse('$baseUrl$path');
+
+ Future