This commit is contained in:
Nathan Anderson 2026-05-20 10:22:58 -05:00
commit 651779b1cb
178 changed files with 11215 additions and 0 deletions

5
.firebaserc Normal file
View File

@ -0,0 +1,5 @@
{
"projects": {
"default": "cyberhybridhub"
}
}

File diff suppressed because one or more lines are too long

80
.gitignore vendored Normal file
View File

@ -0,0 +1,80 @@
# Flutter/Dart
.dart_tool/
.packages
.pub-cache/
.pub/
build/
flutter_*.png
linked_*.ds
unlinked.ds
unlinked_spec.ds
# IDE
.idea/
*.iml
# VS Code (keep .vscode/ committed for team settings)
# .vscode/
# Android
**/android/**/gradle-wrapper.jar
**/android/.gradle
**/android/captures/
**/android/gradlew
**/android/gradlew.bat
**/android/local.properties
**/android/**/GeneratedPluginRegistrant.java
**/android/key.properties
*.jks
# iOS/macOS
**/ios/**/*.mode1v3
**/ios/**/*.mode2v3
**/ios/**/*.moved-aside
**/ios/**/*.pbxuser
**/ios/**/*.perspectivev3
**/ios/**/*sync/
**/ios/**/.sconsign.dblite
**/ios/**/.tags*
**/ios/**/.vagrant/
**/ios/**/DerivedData/
**/ios/**/Icon?
**/ios/**/Pods/
**/ios/**/.symlinks/
**/ios/**/profile
**/ios/**/xcuserdata
**/ios/.generated/
**/ios/Flutter/.last_build_id
**/ios/Flutter/App.framework
**/ios/Flutter/Flutter.framework
**/ios/Flutter/Flutter.podspec
**/ios/Flutter/Generated.xcconfig
**/ios/Flutter/ephemeral
**/ios/Flutter/app.flx
**/ios/Flutter/app.zip
**/ios/Flutter/flutter_assets/
**/ios/Flutter/flutter_export_environment.sh
**/ios/ServiceDefinitions.json
**/ios/Runner/GeneratedPluginRegistrant.*
# Web
lib/generated_plugin_registrant.dart
# Coverage
coverage/
# Misc
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.build/
.buildlog/
.history
.svn/
.swiftpm/
migrate_working_dir/
*.env
*.env.*

45
.metadata Normal file
View File

@ -0,0 +1,45 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: "559ffa3f75e7402d65a8def9c28389a9b2e6fe42"
channel: "stable"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42
base_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42
- platform: android
create_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42
base_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42
- platform: ios
create_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42
base_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42
- platform: linux
create_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42
base_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42
- platform: macos
create_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42
base_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42
- platform: web
create_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42
base_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42
- platform: windows
create_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42
base_revision: 559ffa3f75e7402d65a8def9c28389a9b2e6fe42
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'

6
.vscode/extensions.json vendored Normal file
View File

@ -0,0 +1,6 @@
{
"recommendations": [
"Dart-Code.dart-code",
"Dart-Code.flutter"
]
}

28
.vscode/launch.json vendored Normal file
View File

@ -0,0 +1,28 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Flutter (web)",
"request": "launch",
"type": "dart",
"deviceId": "web-server"
},
{
"name": "Flutter (debug)",
"request": "launch",
"type": "dart"
},
{
"name": "Flutter (profile)",
"request": "launch",
"type": "dart",
"flutterMode": "profile"
},
{
"name": "Flutter (release)",
"request": "launch",
"type": "dart",
"flutterMode": "release"
}
]
}

20
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,20 @@
{
"dart.flutterSdkPath": "/home/nathan/github-open/flutter",
"terminal.integrated.env.linux": {
"PATH": "/home/nathan/github-open/flutter/bin:${env:PATH}"
},
"editor.formatOnSave": true,
"editor.rulers": [80],
"files.exclude": {
"**/.dart_tool": true,
"**/.packages": true,
"build": true
},
"[dart]": {
"editor.formatOnSave": true,
"editor.selectionHighlight": false,
"editor.suggestSelection": "first",
"editor.tabCompletion": "onlySnippets",
"editor.wordBasedSuggestions": "off"
}
}

17
README.md Normal file
View File

@ -0,0 +1,17 @@
# cyberhybridhub
A new Flutter project.
## Getting Started
This project is a starting point for a Flutter application.
A few resources to get you started if this is your first Flutter project:
- [Learn Flutter](https://docs.flutter.dev/get-started/learn-flutter)
- [Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Flutter learning resources](https://docs.flutter.dev/reference/learning-resources)
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.

238
TODO.md Normal file
View File

@ -0,0 +1,238 @@
# Firebase Google Sign-In setup
## Quick setup (Ubuntu CLI)
Reload your shell after the first run (or open a new terminal):
```bash
source ~/.bashrc
```
One-shot setup (installs tools, logs in, configures FlutterFire, registers debug SHA-1):
```bash
cd ~/cyberhybridhub.com
chmod +x scripts/setup-firebase-google-auth.sh scripts/get-android-sha.sh
# Optional: set your Firebase project id to skip the interactive picker
export FIREBASE_PROJECT_ID=your-firebase-project-id
./scripts/setup-firebase-google-auth.sh
```
Or run step by step:
```bash
# 1. Tools (if not already installed)
npm install -g firebase-tools
dart pub global activate flutterfire_cli
# 2. Login (FlutterFire uses the Firebase CLI session)
firebase login
# 3. Register apps + download config (android, ios, web)
cd ~/cyberhybridhub.com
flutterfire configure \
--project=YOUR_FIREBASE_PROJECT_ID \
--platforms=android,ios,web \
--android-package-name=com.cyberhybridhub.cyberhybridhub \
--ios-bundle-id=com.cyberhybridhub.cyberhybridhub \
--out=lib/firebase_options.dart
# 4. Debug SHA-1 (Android Google Sign-In)
./scripts/get-android-sha.sh
firebase apps:list --project=YOUR_FIREBASE_PROJECT_ID
firebase apps:android:sha:create ANDROID_APP_ID YOUR_SHA1 --project=YOUR_FIREBASE_PROJECT_ID
flutterfire configure --project=YOUR_FIREBASE_PROJECT_ID --platforms=android,ios,web --yes
# 5. Enable Google provider (browser — required)
# https://console.firebase.google.com/ → Authentication → Sign-in method → Google → Enable
# 6. Run
flutter pub get && flutter run
```
`~/.bashrc` includes `~/.pub-cache/bin` (for `flutterfire`) and `CYBERHYBRIDHUB_ROOT`.
---
## Linux desktop development (`flutter run -d linux`)
Official FlutterFire does not register native plugins on Linux, so this app uses:
- `google_sign_in_all_platforms` (system browser OAuth)
- Firebase Auth REST (`signInWithIdp`) with web API key from `lib/firebase_options.dart`
### One-time Linux OAuth setup
1. [Google Cloud Console](https://console.cloud.google.com/) → project **cyberhybridhub****APIs & Services** → **Credentials**
2. Open your **Web application** OAuth 2.0 client (or create one)
3. Add authorized redirect URI: `http://127.0.0.1`
4. Copy **Client ID** and **Client secret** into `lib/config/auth_config.dart`:
```dart
const String? googleWebOAuthClientId = 'YOUR_CLIENT_ID.apps.googleusercontent.com';
const String? googleOAuthDesktopClientSecret = 'YOUR_CLIENT_SECRET';
```
### Web browser (`flutter run -d web-server` or Chrome)
Uses the same `googleWebOAuthClientId`. In Google Cloud, on that Web OAuth client, add **Authorized JavaScript origins**:
- `http://localhost`
- `http://127.0.0.1`
(Uncomment the `google-signin-client_id` meta tag in `web/index.html` only if you prefer HTML config over Dart.)
5. Enable Google sign-in in Firebase Console (if not already)
6. Run:
```bash
flutter run -d linux
```
---
## Manual checklist
Complete these steps to replace placeholder config files and enable Google authentication.
App identifiers used by this project:
| Platform | Identifier |
|----------|------------|
| Android package | `com.cyberhybridhub.cyberhybridhub` |
| iOS bundle ID | `com.cyberhybridhub.cyberhybridhub` |
---
## 1. Create a Firebase project
- [ ] Open [Firebase Console](https://console.firebase.google.com/).
- [ ] Create a project (or select an existing one).
- [ ] Enable **Google Analytics** only if you need it (optional for auth).
---
## 2. Register the Flutter app in Firebase
- [ ] In Firebase Console → **Project settings****Your apps**, add:
- [ ] **Android** app with package name `com.cyberhybridhub.cyberhybridhub`
- [ ] **iOS** app with bundle ID `com.cyberhybridhub.cyberhybridhub`
- [ ] **Web** app (required for the web OAuth client used by Google Sign-In on mobile)
- [ ] Download config files when prompted:
- [ ] `google-services.json` → replace `android/app/google-services.json`
- [ ] `GoogleService-Info.plist` → replace `ios/Runner/GoogleService-Info.plist`
### Recommended: FlutterFire CLI
```bash
dart pub global activate flutterfire_cli
flutterfire configure
```
This regenerates `lib/firebase_options.dart` and downloads the platform config files.
---
## 3. Enable Google sign-in in Firebase
- [ ] Firebase Console → **Build****Authentication** → **Sign-in method**
- [ ] Enable **Google**
- [ ] Set a support email and save
---
## 4. Google Cloud Console (OAuth & credentials)
Firebase links to a Google Cloud project. Open it from Firebase → **Project settings****General****Google Cloud Platform resource location** → link to GCP, or go to [Google Cloud Console](https://console.cloud.google.com/) and select the same project.
### 4.1 OAuth consent screen
- [ ] **APIs & Services** → **OAuth consent screen**
- [ ] Choose **External** (or Internal for Workspace-only)
- [ ] Fill app name, user support email, developer contact
- [ ] Add scopes if needed (default `email`, `profile`, `openid` are usually enough)
- [ ] Add test users while the app is in **Testing** mode
### 4.2 OAuth 2.0 Client IDs
- [ ] **APIs & Services** → **Credentials**
- [ ] Confirm these clients exist (Firebase often creates them automatically):
| Client type | Purpose |
|-------------|---------|
| **Web application** | `serverClientId` / ID token for Firebase Auth on Android |
| **Android** | Package name + SHA-1 certificate fingerprints |
| **iOS** | Bundle ID |
#### Android SHA-1 fingerprints
Debug keystore (local development):
```bash
keytool -list -v -keystore ~/.android/debug.keystore -alias androiddebugkey -storepass android -keypass android
```
Release keystore (production):
```bash
keytool -list -v -keystore /path/to/your-release.keystore -alias your-alias
```
- [ ] Add **SHA-1** (and **SHA-256** if offered) to the Android OAuth client in Google Cloud Console
- [ ] Re-download `google-services.json` after adding fingerprints
#### iOS URL scheme
- [ ] Open `ios/Runner/GoogleService-Info.plist` and copy `REVERSED_CLIENT_ID`
- [ ] In Xcode (or `ios/Runner/Info.plist`), add a URL type:
- **URL Schemes**: value of `REVERSED_CLIENT_ID`
- Example: `com.googleusercontent.apps.123456789-abcdef`
---
## 5. App code configuration
- [ ] Replace placeholders in `lib/firebase_options.dart` (or run `flutterfire configure`)
- [ ] If sign-in fails on Android with a `serverClientId` error, set the Web client ID in `lib/config/auth_config.dart`:
```dart
const String? googleSignInServerClientId = 'YOUR_WEB_CLIENT_ID.apps.googleusercontent.com';
```
Find the Web client ID in Firebase → Project settings → Your apps → Web app, or in `google-services.json` under `oauth_client` with `"client_type": 3`.
---
## 6. Verify
```bash
flutter pub get
flutter run
```
- [ ] Tap **Continue with Google** on the login screen
- [ ] Complete Google account selection
- [ ] Confirm the home screen shows your name and email
- [ ] Sign out and sign in again
---
## Troubleshooting
| Symptom | Likely fix |
|---------|------------|
| `clientConfigurationError` | Wrong package/bundle ID, missing SHA-1, or invalid `google-services.json` |
| Sign-in cancels immediately after account pick | Missing web OAuth client in `google-services.json` or wrong SHA-1 |
| `serverClientId must be provided on Android` | Add Web app in Firebase, re-download `google-services.json`, or set `googleSignInServerClientId` |
| iOS sign-in fails after Google UI | Add `REVERSED_CLIENT_ID` URL scheme to `Info.plist` |
| `REPLACE_ME` / invalid API key at runtime | Run `flutterfire configure` or paste real values from Firebase |
---
## Reference
- [FlutterFire setup](https://firebase.flutter.dev/docs/overview)
- [Firebase Auth Google](https://firebase.google.com/docs/auth/flutter/federated-auth#google)
- [google_sign_in plugin](https://pub.dev/packages/google_sign_in)

28
analysis_options.yaml Normal file
View File

@ -0,0 +1,28 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at https://dart.dev/lints.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

14
android/.gitignore vendored Normal file
View File

@ -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

View File

@ -0,0 +1,46 @@
plugins {
id("com.android.application")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
id("com.google.gms.google-services")
}
android {
namespace = "com.cyberhybridhub.cyberhybridhub"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.cyberhybridhub.cyberhybridhub"
// 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
}
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("debug")
}
}
}
kotlin {
compilerOptions {
jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17
}
}
flutter {
source = "../.."
}

View File

@ -0,0 +1,29 @@
{
"project_info": {
"project_number": "155018389717",
"project_id": "cyberhybridhub",
"storage_bucket": "cyberhybridhub.firebasestorage.app"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:155018389717:android:90887875660a26509ada5c",
"android_client_info": {
"package_name": "com.cyberhybridhub.cyberhybridhub"
}
},
"oauth_client": [],
"api_key": [
{
"current_key": "AIzaSyA71SkNLrv3tyJQW2DCfe2vFcu1c2opDX8"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": []
}
}
}
],
"configuration_version": "1"
}

View File

@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

View File

@ -0,0 +1,45 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:label="cyberhybridhub"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
</manifest>

View File

@ -0,0 +1,5 @@
package com.cyberhybridhub.cyberhybridhub
import io.flutter.embedding.android.FlutterActivity
class MainActivity : FlutterActivity()

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="?android:colorBackground" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
the Flutter engine draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
<item name="android:windowBackground">?android:colorBackground</item>
</style>
</resources>

View File

@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

24
android/build.gradle.kts Normal file
View File

@ -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<Delete>("clean") {
delete(rootProject.layout.buildDirectory)
}

View File

@ -0,0 +1,6 @@
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
# This newDsl flag was added by the Flutter template
android.newDsl=false
# This builtInKotlin flag was added by the Flutter template
android.builtInKotlin=false

View File

@ -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-9.1.0-all.zip

View File

@ -0,0 +1,27 @@
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 "9.0.1" apply false
id("org.jetbrains.kotlin.android") version "2.3.20" apply false
id("com.google.gms.google-services") version "4.4.2" apply false
}
include(":app")

34
firebase.json Normal file
View File

@ -0,0 +1,34 @@
{
"auth": {
"providers": {
"googleSignIn": {
"oAuthBrandDisplayName": "Cyber Hybrid Hub",
"supportEmail": "support@cyberhybridhub.firebaseapp.com",
"authorizedRedirectUris": [
"http://localhost"
]
}
}
},
"flutter": {
"platforms": {
"android": {
"default": {
"projectId": "cyberhybridhub",
"appId": "1:155018389717:android:90887875660a26509ada5c",
"fileOutput": "android/app/google-services.json"
}
},
"dart": {
"lib/firebase_options.dart": {
"projectId": "cyberhybridhub",
"configurations": {
"android": "1:155018389717:android:90887875660a26509ada5c",
"ios": "1:155018389717:ios:102445b8ec5fde749ada5c",
"web": "1:155018389717:web:afb61503456673019ada5c"
}
}
}
}
}
}

34
ios/.gitignore vendored Normal file
View File

@ -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

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>App</string>
<key>CFBundleIdentifier</key>
<string>io.flutter.flutter.app</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>App</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>1.0</string>
</dict>
</plist>

View File

@ -0,0 +1 @@
#include "Generated.xcconfig"

View File

@ -0,0 +1 @@
#include "Generated.xcconfig"

View File

@ -0,0 +1,644 @@
// !$*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 */; };
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */ = {isa = PBXBuildFile; productRef = 78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */; };
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 */; };
/* 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 = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
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 = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = FlutterGeneratedPluginSwiftPackage; path = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
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 = "<group>"; };
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
78A318202AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
331C8082294A63A400263BE5 /* RunnerTests */ = {
isa = PBXGroup;
children = (
331C807B294A618700263BE5 /* RunnerTests.swift */,
);
path = RunnerTests;
sourceTree = "<group>";
};
9740EEB11CF90186004384FC /* Flutter */ = {
isa = PBXGroup;
children = (
78E0A7A72DC9AD7400C4905E /* FlutterGeneratedPluginSwiftPackage */,
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
9740EEB31CF90195004384FC /* Generated.xcconfig */,
);
name = Flutter;
sourceTree = "<group>";
};
97C146E51CF9000F007C117D = {
isa = PBXGroup;
children = (
9740EEB11CF90186004384FC /* Flutter */,
97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */,
);
sourceTree = "<group>";
};
97C146EF1CF9000F007C117D /* Products */ = {
isa = PBXGroup;
children = (
97C146EE1CF9000F007C117D /* Runner.app */,
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
);
name = Products;
sourceTree = "<group>";
};
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 = "<group>";
};
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
331C8080294A63A400263BE5 /* RunnerTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = (
331C807D294A63A400263BE5 /* Sources */,
331C807F294A63A400263BE5 /* Resources */,
);
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 = (
9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
);
buildRules = (
);
dependencies = (
);
name = Runner;
packageProductDependencies = (
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */,
);
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;
packageReferences = (
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */,
);
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";
};
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 = "<group>";
};
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
isa = PBXVariantGroup;
children = (
97C147001CF9000F007C117D /* Base */,
);
name = LaunchScreen.storyboard;
sourceTree = "<group>";
};
/* 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.cyberhybridhub.cyberhybridhub;
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;
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.cyberhybridhub.cyberhybridhub.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;
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.cyberhybridhub.cyberhybridhub.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;
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.cyberhybridhub.cyberhybridhub.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 = 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;
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 = 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;
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.cyberhybridhub.cyberhybridhub;
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.cyberhybridhub.cyberhybridhub;
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 */
/* Begin XCLocalSwiftPackageReference section */
781AD8BC2B33823900A9FFBB /* XCLocalSwiftPackageReference "Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage" */ = {
isa = XCLocalSwiftPackageReference;
relativePath = Flutter/ephemeral/Packages/FlutterGeneratedPluginSwiftPackage;
};
/* End XCLocalSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
78A3181F2AECB46A00862997 /* FlutterGeneratedPluginSwiftPackage */ = {
isa = XCSwiftPackageProductDependency;
productName = FlutterGeneratedPluginSwiftPackage;
};
/* End XCSwiftPackageProductDependency section */
};
rootObject = 97C146E61CF9000F007C117D /* Project object */;
}

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "self:">
</FileRef>
</Workspace>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>

View File

@ -0,0 +1,119 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1510"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<PreActions>
<ExecutionAction
ActionType = "Xcode.IDEStandardExecutionActionsCore.ExecutionActionType.ShellScriptAction">
<ActionContent
title = "Run Prepare Flutter Framework Script"
scriptText = "/bin/sh &quot;$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh&quot; prepare&#10;">
<EnvironmentBuildable>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</EnvironmentBuildable>
</ActionContent>
</ExecutionAction>
</PreActions>
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
shouldUseLaunchSchemeArgsEnv = "YES">
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</MacroExpansion>
<Testables>
<TestableReference
skipped = "NO"
parallelizable = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "331C8080294A63A400263BE5"
BuildableName = "RunnerTests.xctest"
BlueprintName = "RunnerTests"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
enableGPUValidationMode = "1"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Profile"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
BuildableName = "Runner.app"
BlueprintName = "Runner"
ReferencedContainer = "container:Runner.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workspace
version = "1.0">
<FileRef
location = "group:Runner.xcodeproj">
</FileRef>
</Workspace>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>IDEDidComputeMac32BitWarning</key>
<true/>
</dict>
</plist>

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>PreviewsEnabled</key>
<false/>
</dict>
</plist>

View File

@ -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)
}
}

View File

@ -0,0 +1,122 @@
{
"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" : "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" : "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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 295 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 450 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 282 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 462 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 704 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 762 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -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"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 B

View File

@ -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.

View File

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
<viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
</imageView>
</subviews>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
</constraints>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
<resources>
<image name="LaunchImage" width="168" height="185"/>
</resources>
</document>

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
</dependencies>
<scenes>
<!--Flutter View Controller-->
<scene sceneID="tne-QT-ifu">
<objects>
<viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
</objects>
</scene>
</scenes>
</document>

View File

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CLIENT_ID</key>
<string>REPLACE_ME.apps.googleusercontent.com</string>
<key>REVERSED_CLIENT_ID</key>
<string>com.googleusercontent.apps.REPLACE_ME</string>
<key>API_KEY</key>
<string>REPLACE_ME</string>
<key>GCM_SENDER_ID</key>
<string>000000000000</string>
<key>PLIST_VERSION</key>
<string>1</string>
<key>BUNDLE_ID</key>
<string>com.cyberhybridhub.cyberhybridhub</string>
<key>PROJECT_ID</key>
<string>REPLACE_ME</string>
<key>STORAGE_BUCKET</key>
<string>REPLACE_ME.appspot.com</string>
<key>IS_ADS_ENABLED</key>
<false></false>
<key>IS_ANALYTICS_ENABLED</key>
<false></false>
<key>IS_APPINVITE_ENABLED</key>
<true></true>
<key>IS_GCM_ENABLED</key>
<true></true>
<key>IS_SIGNIN_ENABLED</key>
<true></true>
<key>GOOGLE_APP_ID</key>
<string>1:000000000000:ios:0000000000000000000000</string>
</dict>
</plist>

70
ios/Runner/Info.plist Normal file
View File

@ -0,0 +1,70 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Cyberhybridhub</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>cyberhybridhub</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>$(FLUTTER_BUILD_NAME)</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key>
<true/>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<false/>
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneClassName</key>
<string>UIWindowScene</string>
<key>UISceneConfigurationName</key>
<string>flutter</string>
<key>UISceneDelegateClassName</key>
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
<key>UISceneStoryboardFile</key>
<string>Main</string>
</dict>
</array>
</dict>
</dict>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
<string>Main</string>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
</dict>
</plist>

View File

@ -0,0 +1 @@
#import "GeneratedPluginRegistrant.h"

View File

@ -0,0 +1,6 @@
import Flutter
import UIKit
class SceneDelegate: FlutterSceneDelegate {
}

View File

@ -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.
}
}

16
lib/bootstrap.dart Normal file
View File

@ -0,0 +1,16 @@
import 'package:firebase_core/firebase_core.dart';
import 'firebase_options.dart';
import 'repositories/user_profile_repository.dart';
import 'services/auth_service.dart';
/// Initializes Firebase (non-Linux), auth, and profile sync storage.
Future<void> bootstrap() async {
if (!AuthService.isLinuxDesktop) {
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
}
await AuthService.instance.initialize();
await UserProfileRepository.instance.initialize();
}

View File

@ -0,0 +1,8 @@
/// Base URL for the Cyber Hybrid Hub API (Postgres-backed).
///
/// Override at build/run time:
/// flutter run --dart-define=API_BASE_URL=http://localhost:3000
const String apiBaseUrl = String.fromEnvironment(
'API_BASE_URL',
defaultValue: 'http://localhost:3000',
);

View File

@ -0,0 +1,25 @@
/// Web OAuth 2.0 client ID (Google Cloud Credentials Web application).
///
/// Used for:
/// - **Android**: `serverClientId` when not in `google-services.json`
/// - **Linux desktop**: `google_sign_in_all_platforms` clientId
///
/// **Web (required for Firefox / strict privacy):** `clientId` for the
/// Google Identity Services fallback when Firebase popup sign-in fails.
/// Popup is tried first; redirect is not used (sessionStorage breaks under ETP).
///
/// Find it: Firebase Console Project settings Your apps Web app,
/// or Authentication Sign-in method Google Web SDK configuration.
///
/// Also add authorized JavaScript origins in Google Cloud for web dev:
/// `http://localhost:8080` (see [web_dev_config.yaml] in the project root).
const String? googleWebOAuthClientId =
'155018389717-cers33qaak01urmf4sa04s85h7kcocpt.apps.googleusercontent.com';
/// @deprecated Use [googleWebOAuthClientId].
const String? googleSignInServerClientId = googleWebOAuthClientId;
/// Linux desktop OAuth client secret (same Web client as above).
const String? googleOAuthDesktopClientId = googleWebOAuthClientId;
const String? googleOAuthDesktopClientSecret = null;

View File

@ -0,0 +1,71 @@
import 'package:drift/drift.dart';
import 'package:drift_flutter/drift_flutter.dart';
import 'tables.dart';
part 'app_database.g.dart';
@DriftDatabase(tables: <Type>[UserProfileRows, SyncOutboxRows])
class AppDatabase extends _$AppDatabase {
AppDatabase([QueryExecutor? executor]) : super(executor ?? _openConnection());
@override
int get schemaVersion => 1;
static QueryExecutor _openConnection() {
return driftDatabase(name: 'cyberhybridhub');
}
Stream<UserProfileRow?> watchProfile(String firebaseUid) {
return (select(userProfileRows)
..where((UserProfileRows t) => t.firebaseUid.equals(firebaseUid)))
.watchSingleOrNull();
}
Future<UserProfileRow?> getProfile(String firebaseUid) {
return (select(userProfileRows)
..where((UserProfileRows t) => t.firebaseUid.equals(firebaseUid)))
.getSingleOrNull();
}
Future<void> upsertProfile(UserProfileRowsCompanion row) {
return into(userProfileRows).insertOnConflictUpdate(row);
}
Future<void> deleteProfile(String firebaseUid) {
return (delete(userProfileRows)
..where((UserProfileRows t) => t.firebaseUid.equals(firebaseUid)))
.go();
}
Future<void> clearAllProfiles() {
return delete(userProfileRows).go();
}
Future<List<SyncOutboxRow>> pendingOutbox(String firebaseUid) {
return (select(syncOutboxRows)
..where((SyncOutboxRows t) => t.firebaseUid.equals(firebaseUid))
..orderBy(<OrderClauseGenerator<SyncOutboxRows>>[
(SyncOutboxRows t) => OrderingTerm(expression: t.createdAt),
]))
.get();
}
Future<int> enqueueOutbox(SyncOutboxRowsCompanion row) {
return into(syncOutboxRows).insert(row);
}
Future<void> deleteOutboxEntry(int id) {
return (delete(syncOutboxRows)
..where((SyncOutboxRows t) => t.id.equals(id)))
.go();
}
Future<void> clearOutbox(String firebaseUid) {
return (delete(syncOutboxRows)
..where((SyncOutboxRows t) => t.firebaseUid.equals(firebaseUid)))
.go();
}
Future<void> clearAllOutbox() => delete(syncOutboxRows).go();
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,26 @@
import 'package:drift/drift.dart';
class UserProfileRows extends Table {
TextColumn get firebaseUid => text()();
TextColumn get email => text().nullable()();
TextColumn get displayName => text().nullable()();
TextColumn get photoUrl => text().nullable()();
TextColumn get locale => text().withDefault(const Constant('en'))();
TextColumn get timezone => text().nullable()();
BoolColumn get onboardingCompleted =>
boolean().withDefault(const Constant(false))();
IntColumn get revision => integer().withDefault(const Constant(1))();
DateTimeColumn get updatedAt => dateTime()();
DateTimeColumn get lastSyncedAt => dateTime().nullable()();
BoolColumn get dirty => boolean().withDefault(const Constant(false))();
@override
Set<Column<Object>> get primaryKey => <Column<Object>>{firebaseUid};
}
class SyncOutboxRows extends Table {
IntColumn get id => integer().autoIncrement()();
TextColumn get firebaseUid => text()();
TextColumn get payloadJson => text()();
DateTimeColumn get createdAt => dateTime()();
}

View File

@ -0,0 +1,70 @@
import '../../models/user_profile.dart';
import 'app_database.dart';
import 'user_profile_mapper.dart';
/// Drift-backed local profile storage (mobile and desktop native).
class UserProfileLocalStore {
UserProfileLocalStore(this._db);
final AppDatabase _db;
Stream<UserProfile?> watchProfile(String firebaseUid) {
return _db.watchProfile(firebaseUid).map((UserProfileRow? row) {
if (row == null) {
return null;
}
return profileFromRow(row);
});
}
Future<UserProfile?> getProfile(String firebaseUid) async {
final UserProfileRow? row = await _db.getProfile(firebaseUid);
if (row == null) {
return null;
}
return profileFromRow(row);
}
Future<void> saveProfile(UserProfile profile, {required bool dirty}) async {
await _db.upsertProfile(
companionFromProfile(profile.copyWith(dirty: dirty)),
);
if (dirty) {
await _db.enqueueOutbox(
SyncOutboxRowsCompanion.insert(
firebaseUid: profile.firebaseUid,
payloadJson: encodeProfilePayload(profile),
createdAt: DateTime.now().toUtc(),
),
);
}
}
Future<List<UserProfile>> pendingOutboxProfiles(String firebaseUid) async {
final List<SyncOutboxRow> rows = await _db.pendingOutbox(firebaseUid);
return rows
.map((SyncOutboxRow row) => decodeProfilePayload(row.payloadJson))
.toList();
}
Future<void> clearOutbox(String firebaseUid) async {
await _db.clearOutbox(firebaseUid);
}
Future<void> deleteOutboxEntries(String firebaseUid) async {
final List<SyncOutboxRow> rows = await _db.pendingOutbox(firebaseUid);
for (final SyncOutboxRow row in rows) {
await _db.deleteOutboxEntry(row.id);
}
}
Future<void> clearLocal(String firebaseUid) async {
await _db.clearOutbox(firebaseUid);
await _db.deleteProfile(firebaseUid);
}
Future<void> clearAll() async {
await _db.clearAllProfiles();
await _db.clearAllOutbox();
}
}

View File

@ -0,0 +1,48 @@
import 'dart:convert';
import 'package:drift/drift.dart';
import '../../models/user_profile.dart';
import 'app_database.dart';
UserProfile profileFromRow(UserProfileRow row) {
return UserProfile(
firebaseUid: row.firebaseUid,
email: row.email,
displayName: row.displayName,
photoUrl: row.photoUrl,
locale: row.locale,
timezone: row.timezone,
onboardingCompleted: row.onboardingCompleted,
revision: row.revision,
updatedAt: row.updatedAt.toUtc(),
lastSyncedAt: row.lastSyncedAt?.toUtc(),
dirty: row.dirty,
);
}
UserProfileRowsCompanion companionFromProfile(UserProfile profile) {
return UserProfileRowsCompanion(
firebaseUid: Value<String>(profile.firebaseUid),
email: Value<String?>(profile.email),
displayName: Value<String?>(profile.displayName),
photoUrl: Value<String?>(profile.photoUrl),
locale: Value<String>(profile.locale),
timezone: Value<String?>(profile.timezone),
onboardingCompleted: Value<bool>(profile.onboardingCompleted),
revision: Value<int>(profile.revision),
updatedAt: Value<DateTime>(profile.updatedAt.toUtc()),
lastSyncedAt: Value<DateTime?>(profile.lastSyncedAt?.toUtc()),
dirty: Value<bool>(profile.dirty),
);
}
String encodeProfilePayload(UserProfile profile) {
return jsonEncode(profile.toJson());
}
UserProfile decodeProfilePayload(String json) {
return UserProfile.fromJson(
jsonDecode(json) as Map<String, dynamic>,
);
}

View File

@ -0,0 +1,19 @@
import '../../models/user_profile.dart';
class ProfileNotFoundException implements Exception {}
class ProfileConflictException implements Exception {
ProfileConflictException(this.serverProfile);
final UserProfile serverProfile;
}
class ProfileApiException implements Exception {
ProfileApiException(this.statusCode, this.body);
final int statusCode;
final String body;
@override
String toString() => 'ProfileApiException($statusCode): $body';
}

View File

@ -0,0 +1,90 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
import '../../config/api_config.dart';
import '../../models/user_profile.dart';
import '../../services/auth_service.dart';
import 'profile_api_exception.dart';
/// HTTP client for Postgres-backed profile API.
class UserProfileRemoteStore {
UserProfileRemoteStore({http.Client? client}) : _client = client ?? http.Client();
final http.Client _client;
Future<UserProfile?> fetchProfile() async {
final String? token = await AuthService.instance.getIdToken();
if (token == null) {
return null;
}
final http.Response response = await _client.get(
Uri.parse('$apiBaseUrl/v1/me/profile'),
headers: _authHeaders(token),
);
if (response.statusCode == 404) {
return null;
}
if (response.statusCode != 200) {
throw ProfileApiException(response.statusCode, response.body);
}
return _profileFromApi(
jsonDecode(response.body) as Map<String, dynamic>,
);
}
Future<UserProfile> pushProfile(UserProfile profile) async {
final String? token = await AuthService.instance.getIdToken();
if (token == null) {
throw StateError('Not signed in');
}
final http.Response response = await _client.put(
Uri.parse('$apiBaseUrl/v1/me/profile'),
headers: _authHeaders(token),
body: jsonEncode(profile.toApiJson()),
);
if (response.statusCode == 409) {
final Map<String, dynamic> body =
jsonDecode(response.body) as Map<String, dynamic>;
throw ProfileConflictException(
_profileFromApi(body['profile'] as Map<String, dynamic>),
);
}
if (response.statusCode != 200) {
throw ProfileApiException(response.statusCode, response.body);
}
return _profileFromApi(
jsonDecode(response.body) as Map<String, dynamic>,
);
}
Map<String, String> _authHeaders(String token) {
return <String, String>{
'Authorization': 'Bearer $token',
'Content-Type': 'application/json',
'Accept': 'application/json',
};
}
UserProfile _profileFromApi(Map<String, dynamic> json) {
return UserProfile(
firebaseUid: json['firebaseUid'] as String,
email: json['email'] as String?,
displayName: json['displayName'] as String?,
photoUrl: json['photoUrl'] as String?,
locale: json['locale'] as String? ?? 'en',
timezone: json['timezone'] as String?,
onboardingCompleted: json['onboardingCompleted'] as bool? ?? false,
revision: (json['revision'] as num).toInt(),
updatedAt: DateTime.parse(json['updatedAt'] as String).toUtc(),
lastSyncedAt: DateTime.now().toUtc(),
dirty: false,
);
}
}

View File

@ -0,0 +1,63 @@
import 'dart:async';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
/// Observes network reachability and exposes online/offline transitions.
class ConnectivityService {
ConnectivityService({Connectivity? connectivity})
: _connectivity = connectivity ?? Connectivity();
final Connectivity _connectivity;
final StreamController<bool> _onlineController =
StreamController<bool>.broadcast();
StreamSubscription<List<ConnectivityResult>>? _subscription;
bool _isOnline = true;
Stream<bool> get onlineChanges => _onlineController.stream;
bool get isOnline => _isOnline;
Future<void> initialize() async {
if (kIsWeb) {
_isOnline = true;
_onlineController.add(true);
return;
}
final List<ConnectivityResult> results = await _connectivity
.checkConnectivity();
_emitOnline(_resultsOnline(results));
_subscription = _connectivity.onConnectivityChanged.listen(
(List<ConnectivityResult> results) => _emitOnline(_resultsOnline(results)),
);
}
void dispose() {
_subscription?.cancel();
_onlineController.close();
}
bool _resultsOnline(List<ConnectivityResult> results) {
if (results.isEmpty) {
return false;
}
return results.any(
(ConnectivityResult r) =>
r == ConnectivityResult.mobile ||
r == ConnectivityResult.wifi ||
r == ConnectivityResult.ethernet ||
r == ConnectivityResult.vpn,
);
}
void _emitOnline(bool online) {
if (_isOnline == online) {
return;
}
_isOnline = online;
_onlineController.add(online);
}
}

View File

@ -0,0 +1,115 @@
import 'package:flutter/foundation.dart' show kIsWeb;
import '../../models/sync_result.dart';
import '../../models/user_profile.dart';
import '../local/user_profile_local_store.dart';
import '../remote/profile_api_exception.dart';
import '../remote/user_profile_remote_store.dart';
import 'connectivity_service.dart';
/// Pushes local changes and pulls server state when online.
class ProfileSyncCoordinator {
ProfileSyncCoordinator({
required UserProfileLocalStore? local,
required UserProfileRemoteStore remote,
required ConnectivityService connectivity,
}) : _local = local,
_remote = remote,
_connectivity = connectivity;
final UserProfileLocalStore? _local;
final UserProfileRemoteStore _remote;
final ConnectivityService _connectivity;
bool get hasLocalStore => _local != null;
Future<SyncResult> sync(String firebaseUid) async {
if (!kIsWeb && !_connectivity.isOnline) {
return const SyncResult.offline();
}
try {
if (_local == null) {
return const SyncResult.success();
}
return _syncWithLocal(firebaseUid);
} on ProfileApiException catch (e) {
return SyncResult.error(e.toString());
} catch (e) {
return SyncResult.error(e.toString());
}
}
Future<SyncResult> _syncWithLocal(String firebaseUid) async {
final UserProfileLocalStore local = _local!;
final List<UserProfile> pending = await local.pendingOutboxProfiles(
firebaseUid,
);
for (final UserProfile profile in pending) {
try {
final UserProfile saved = await _remote.pushProfile(profile);
await local.saveProfile(
saved.copyWith(dirty: false, lastSyncedAt: DateTime.now().toUtc()),
dirty: false,
);
} on ProfileConflictException catch (e) {
await local.saveProfile(
e.serverProfile.copyWith(
dirty: false,
lastSyncedAt: DateTime.now().toUtc(),
),
dirty: false,
);
await local.clearOutbox(firebaseUid);
return const SyncResult.conflictResolved();
}
}
if (pending.isNotEmpty) {
await local.clearOutbox(firebaseUid);
}
final UserProfile? server = await _remote.fetchProfile();
if (server == null) {
final UserProfile? localProfile = await local.getProfile(firebaseUid);
if (localProfile != null && localProfile.dirty) {
final UserProfile saved = await _remote.pushProfile(localProfile);
await local.saveProfile(
saved.copyWith(dirty: false, lastSyncedAt: DateTime.now().toUtc()),
dirty: false,
);
}
return const SyncResult.success();
}
final UserProfile? localProfile = await local.getProfile(firebaseUid);
if (localProfile == null ||
server.revision > localProfile.revision ||
(server.revision == localProfile.revision &&
server.updatedAt.isAfter(localProfile.updatedAt))) {
await local.saveProfile(
server.copyWith(dirty: false, lastSyncedAt: DateTime.now().toUtc()),
dirty: false,
);
} else if (localProfile.dirty) {
try {
final UserProfile saved = await _remote.pushProfile(localProfile);
await local.saveProfile(
saved.copyWith(dirty: false, lastSyncedAt: DateTime.now().toUtc()),
dirty: false,
);
} on ProfileConflictException catch (e) {
await local.saveProfile(
e.serverProfile.copyWith(
dirty: false,
lastSyncedAt: DateTime.now().toUtc(),
),
dirty: false,
);
return const SyncResult.conflictResolved();
}
}
return const SyncResult.success();
}
}

65
lib/firebase_options.dart Normal file
View File

@ -0,0 +1,65 @@
// File generated by FlutterFire CLI.
// ignore_for_file: type=lint
import 'package:firebase_core/firebase_core.dart' show FirebaseOptions;
import 'package:flutter/foundation.dart'
show defaultTargetPlatform, kIsWeb, TargetPlatform;
/// Placeholder Firebase options.
///
/// Replace this file by running:
/// dart pub global activate flutterfire_cli
/// flutterfire configure
class DefaultFirebaseOptions {
static FirebaseOptions get currentPlatform {
if (kIsWeb) {
return web;
}
switch (defaultTargetPlatform) {
case TargetPlatform.android:
return android;
case TargetPlatform.iOS:
return ios;
case TargetPlatform.macOS:
return macos;
case TargetPlatform.linux:
return linux;
case TargetPlatform.windows:
return web;
default:
throw UnsupportedError(
'DefaultFirebaseOptions are not configured for this platform.',
);
}
}
/// Used for Linux desktop dev (Firebase REST + browser OAuth).
static const FirebaseOptions linux = FirebaseOptions(
apiKey: 'AIzaSyDbYBh6nO-UasqwM-dkVVJAvw5wMhRTsIs',
appId: '1:155018389717:web:afb61503456673019ada5c',
messagingSenderId: '155018389717',
projectId: 'cyberhybridhub',
authDomain: 'cyberhybridhub.firebaseapp.com',
storageBucket: 'cyberhybridhub.firebasestorage.app',
);
static const FirebaseOptions web = linux;
static const FirebaseOptions android = FirebaseOptions(
apiKey: 'AIzaSyA71SkNLrv3tyJQW2DCfe2vFcu1c2opDX8',
appId: '1:155018389717:android:90887875660a26509ada5c',
messagingSenderId: '155018389717',
projectId: 'cyberhybridhub',
storageBucket: 'cyberhybridhub.firebasestorage.app',
);
static const FirebaseOptions ios = FirebaseOptions(
apiKey: 'AIzaSyAHqjmp-vC8n_2EClQM2ch33jL_yIGuQLE',
appId: '1:155018389717:ios:102445b8ec5fde749ada5c',
messagingSenderId: '155018389717',
projectId: 'cyberhybridhub',
storageBucket: 'cyberhybridhub.firebasestorage.app',
iosBundleId: 'com.cyberhybridhub.cyberhybridhub',
);
static const FirebaseOptions macos = ios;
}

62
lib/main.dart Normal file
View File

@ -0,0 +1,62 @@
import 'package:flutter/material.dart';
import 'bootstrap.dart';
import 'models/app_user.dart';
import 'screens/home_screen.dart';
import 'screens/landing_screen.dart';
import 'services/auth_service.dart';
import 'theme/app_theme.dart';
import 'widgets/profile_session.dart';
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await bootstrap();
runApp(const CyberHybridHubApp());
}
class CyberHybridHubApp extends StatelessWidget {
const CyberHybridHubApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Cyber Hybrid Hub',
theme: buildAppTheme(),
home: const AuthGate(),
);
}
}
class AuthGate extends StatelessWidget {
const AuthGate({super.key});
@override
Widget build(BuildContext context) {
return StreamBuilder<AppUser?>(
stream: AuthService.instance.authStateChanges,
builder: (BuildContext context, AsyncSnapshot<AppUser?> snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Scaffold(
body: Center(
child: CircularProgressIndicator(color: AppColors.accent),
),
);
}
final AppUser? user = snapshot.data;
if (user == null) {
return const LandingScreen();
}
return ProfileSession(
user: user,
builder: (context, profile, syncStatus) => HomeScreen(
user: user,
profile: profile,
syncStatus: syncStatus,
),
);
},
);
}
}

14
lib/models/app_user.dart Normal file
View File

@ -0,0 +1,14 @@
/// App-level user model used across mobile, web, and Linux desktop.
class AppUser {
const AppUser({
required this.uid,
this.email,
this.displayName,
this.photoUrl,
});
final String uid;
final String? email;
final String? displayName;
final String? photoUrl;
}

View File

@ -0,0 +1,26 @@
/// Result of a profile sync attempt.
enum SyncResultKind { success, offline, conflictResolved, error }
class SyncResult {
const SyncResult._(this.kind, {this.message});
const SyncResult.success() : this._(SyncResultKind.success);
const SyncResult.offline() : this._(SyncResultKind.offline);
const SyncResult.conflictResolved()
: this._(SyncResultKind.conflictResolved);
const SyncResult.error(String message)
: this._(SyncResultKind.error, message: message);
final SyncResultKind kind;
final String? message;
bool get isSuccess =>
kind == SyncResultKind.success ||
kind == SyncResultKind.conflictResolved;
}
/// UI-facing sync state from [UserProfileRepository].
enum ProfileSyncStatus { idle, syncing, synced, offline, error }

View File

@ -0,0 +1,123 @@
/// App-owned user profile synced between local Drift storage and Postgres.
class UserProfile {
const UserProfile({
required this.firebaseUid,
this.email,
this.displayName,
this.photoUrl,
this.locale = 'en',
this.timezone,
this.onboardingCompleted = false,
this.revision = 1,
required this.updatedAt,
this.lastSyncedAt,
this.dirty = false,
});
final String firebaseUid;
final String? email;
final String? displayName;
final String? photoUrl;
final String locale;
final String? timezone;
final bool onboardingCompleted;
final int revision;
final DateTime updatedAt;
final DateTime? lastSyncedAt;
final bool dirty;
UserProfile copyWith({
String? email,
String? displayName,
String? photoUrl,
String? locale,
String? timezone,
bool? onboardingCompleted,
int? revision,
DateTime? updatedAt,
DateTime? lastSyncedAt,
bool? dirty,
bool clearLastSyncedAt = false,
}) {
return UserProfile(
firebaseUid: firebaseUid,
email: email ?? this.email,
displayName: displayName ?? this.displayName,
photoUrl: photoUrl ?? this.photoUrl,
locale: locale ?? this.locale,
timezone: timezone ?? this.timezone,
onboardingCompleted: onboardingCompleted ?? this.onboardingCompleted,
revision: revision ?? this.revision,
updatedAt: updatedAt ?? this.updatedAt,
lastSyncedAt: clearLastSyncedAt
? null
: (lastSyncedAt ?? this.lastSyncedAt),
dirty: dirty ?? this.dirty,
);
}
factory UserProfile.fromAuth({
required String firebaseUid,
String? email,
String? displayName,
String? photoUrl,
}) {
return UserProfile(
firebaseUid: firebaseUid,
email: email,
displayName: displayName,
photoUrl: photoUrl,
updatedAt: DateTime.now().toUtc(),
dirty: true,
);
}
factory UserProfile.fromJson(Map<String, dynamic> json) {
return UserProfile(
firebaseUid: json['firebaseUid'] as String,
email: json['email'] as String?,
displayName: json['displayName'] as String?,
photoUrl: json['photoUrl'] as String?,
locale: json['locale'] as String? ?? 'en',
timezone: json['timezone'] as String?,
onboardingCompleted: json['onboardingCompleted'] as bool? ?? false,
revision: (json['revision'] as num?)?.toInt() ?? 1,
updatedAt: DateTime.parse(json['updatedAt'] as String).toUtc(),
lastSyncedAt: json['lastSyncedAt'] == null
? null
: DateTime.parse(json['lastSyncedAt'] as String).toUtc(),
dirty: json['dirty'] as bool? ?? false,
);
}
Map<String, dynamic> toJson() {
return <String, dynamic>{
'firebaseUid': firebaseUid,
'email': email,
'displayName': displayName,
'photoUrl': photoUrl,
'locale': locale,
'timezone': timezone,
'onboardingCompleted': onboardingCompleted,
'revision': revision,
'updatedAt': updatedAt.toUtc().toIso8601String(),
if (lastSyncedAt != null)
'lastSyncedAt': lastSyncedAt!.toUtc().toIso8601String(),
'dirty': dirty,
};
}
/// Payload sent to the API (server assigns authoritative revision on conflict).
Map<String, dynamic> toApiJson() {
return <String, dynamic>{
'email': email,
'displayName': displayName,
'photoUrl': photoUrl,
'locale': locale,
'timezone': timezone,
'onboardingCompleted': onboardingCompleted,
'revision': revision,
'updatedAt': updatedAt.toUtc().toIso8601String(),
};
}
}

View File

@ -0,0 +1,248 @@
import 'dart:async';
import 'package:flutter/foundation.dart' show kIsWeb;
import '../data/local/app_database.dart';
import '../data/local/user_profile_local_store.dart';
import '../data/remote/profile_api_exception.dart';
import '../data/remote/user_profile_remote_store.dart';
import '../data/sync/connectivity_service.dart';
import '../data/sync/profile_sync_coordinator.dart';
import '../models/app_user.dart';
import '../models/sync_result.dart';
import '../models/user_profile.dart';
/// Facade for profile reads/writes with Drift (native) and API sync.
class UserProfileRepository {
UserProfileRepository._();
static final UserProfileRepository instance = UserProfileRepository._();
final ConnectivityService _connectivity = ConnectivityService();
final UserProfileRemoteStore _remote = UserProfileRemoteStore();
AppDatabase? _database;
UserProfileLocalStore? _local;
ProfileSyncCoordinator? _sync;
StreamSubscription<bool>? _connectivitySubscription;
final StreamController<UserProfile?> _profileController =
StreamController<UserProfile?>.broadcast();
final StreamController<ProfileSyncStatus> _syncStatusController =
StreamController<ProfileSyncStatus>.broadcast();
StreamSubscription<UserProfile?>? _localWatchSubscription;
String? _activeUid;
UserProfile? _cachedProfile;
Stream<UserProfile?> get profileStream => _profileController.stream;
Stream<ProfileSyncStatus> get syncStatusStream =>
_syncStatusController.stream;
UserProfile? get currentProfile => _cachedProfile;
bool get usesLocalStore => _local != null;
Future<void> initialize() async {
await _connectivity.initialize();
if (!kIsWeb) {
_database = AppDatabase();
_local = UserProfileLocalStore(_database!);
_sync = ProfileSyncCoordinator(
local: _local,
remote: _remote,
connectivity: _connectivity,
);
} else {
_sync = ProfileSyncCoordinator(
local: null,
remote: _remote,
connectivity: _connectivity,
);
}
_connectivitySubscription = _connectivity.onlineChanges.listen((
bool online,
) {
if (online && _activeUid != null) {
unawaited(sync());
} else if (!online) {
_emitSyncStatus(ProfileSyncStatus.offline);
}
});
_emitSyncStatus(ProfileSyncStatus.idle);
}
Future<void> startSession(AppUser user) async {
if (_activeUid == user.uid) {
return;
}
await endSession();
_activeUid = user.uid;
_emitSyncStatus(ProfileSyncStatus.syncing);
if (_local != null) {
_localWatchSubscription = _local!.watchProfile(user.uid).listen(
(UserProfile? profile) {
_cachedProfile = profile;
_profileController.add(profile);
},
);
UserProfile? profile = await _local!.getProfile(user.uid);
profile ??= UserProfile.fromAuth(
firebaseUid: user.uid,
email: user.email,
displayName: user.displayName,
photoUrl: user.photoUrl,
);
await _local!.saveProfile(profile, dirty: true);
_cachedProfile = profile;
_profileController.add(profile);
} else {
await _loadRemoteProfile(user);
}
await sync();
}
Future<void> _loadRemoteProfile(AppUser user) async {
try {
UserProfile? profile = await _remote.fetchProfile();
profile ??= UserProfile.fromAuth(
firebaseUid: user.uid,
email: user.email,
displayName: user.displayName,
photoUrl: user.photoUrl,
);
if (profile.revision == 1 && !profile.dirty) {
final UserProfile seeded = profile.copyWith(dirty: true);
profile = await _remote.pushProfile(seeded);
}
_cachedProfile = profile;
_profileController.add(profile);
} catch (_) {
final UserProfile fallback = UserProfile.fromAuth(
firebaseUid: user.uid,
email: user.email,
displayName: user.displayName,
photoUrl: user.photoUrl,
);
_cachedProfile = fallback;
_profileController.add(fallback);
_emitSyncStatus(ProfileSyncStatus.error);
}
}
Future<void> endSession() async {
await _localWatchSubscription?.cancel();
_localWatchSubscription = null;
if (_activeUid != null && _local != null) {
await _local!.clearLocal(_activeUid!);
}
_activeUid = null;
_cachedProfile = null;
_profileController.add(null);
_emitSyncStatus(ProfileSyncStatus.idle);
}
Future<void> updateProfile(UserProfile profile) async {
final UserProfile updated = profile.copyWith(
revision: profile.revision + 1,
updatedAt: DateTime.now().toUtc(),
dirty: true,
);
if (_local != null) {
await _local!.saveProfile(updated, dirty: true);
} else {
_cachedProfile = updated;
_profileController.add(updated);
await _remote.pushProfile(updated);
}
unawaited(sync());
}
Future<SyncResult> sync() async {
final String? uid = _activeUid;
if (uid == null || _sync == null) {
return const SyncResult.error('No active session');
}
if (!kIsWeb && !_connectivity.isOnline) {
_emitSyncStatus(ProfileSyncStatus.offline);
return const SyncResult.offline();
}
_emitSyncStatus(ProfileSyncStatus.syncing);
final SyncResult result = _local == null
? await _syncWebOnly()
: await _sync!.sync(uid);
switch (result.kind) {
case SyncResultKind.success:
case SyncResultKind.conflictResolved:
_emitSyncStatus(ProfileSyncStatus.synced);
case SyncResultKind.offline:
_emitSyncStatus(ProfileSyncStatus.offline);
case SyncResultKind.error:
_emitSyncStatus(ProfileSyncStatus.error);
}
return result;
}
Future<SyncResult> _syncWebOnly() async {
final UserProfile? profile = _cachedProfile;
if (profile == null) {
return const SyncResult.error('No profile loaded');
}
try {
if (profile.dirty) {
try {
final UserProfile saved = await _remote.pushProfile(profile);
_cachedProfile = saved;
_profileController.add(saved);
} on ProfileConflictException catch (e) {
_cachedProfile = e.serverProfile;
_profileController.add(e.serverProfile);
return const SyncResult.conflictResolved();
}
} else {
final UserProfile? server = await _remote.fetchProfile();
if (server != null) {
_cachedProfile = server;
_profileController.add(server);
}
}
return const SyncResult.success();
} on ProfileApiException catch (e) {
return SyncResult.error(e.toString());
} catch (e) {
return SyncResult.error(e.toString());
}
}
Future<void> dispose() async {
await _connectivitySubscription?.cancel();
_connectivity.dispose();
await _localWatchSubscription?.cancel();
await _profileController.close();
await _syncStatusController.close();
await _database?.close();
}
void _emitSyncStatus(ProfileSyncStatus status) {
if (!_syncStatusController.isClosed) {
_syncStatusController.add(status);
}
}
}

View File

@ -0,0 +1,199 @@
import 'package:flutter/material.dart';
import '../models/app_user.dart';
import '../models/sync_result.dart';
import '../models/user_profile.dart';
import '../repositories/user_profile_repository.dart';
import '../services/auth_service.dart';
import '../theme/app_theme.dart';
class HomeScreen extends StatelessWidget {
const HomeScreen({
super.key,
required this.user,
this.profile,
this.syncStatus = ProfileSyncStatus.idle,
});
final AppUser user;
final UserProfile? profile;
final ProfileSyncStatus syncStatus;
@override
Widget build(BuildContext context) {
final String? photoUrl = profile?.photoUrl ?? user.photoUrl;
final String displayName =
profile?.displayName ?? user.displayName ?? 'there';
final String? email = profile?.email ?? user.email;
return Scaffold(
appBar: AppBar(
backgroundColor: Colors.transparent,
title: const Text('Cyber Hybrid Hub'),
actions: <Widget>[
IconButton(
onPressed: () => UserProfileRepository.instance.sync(),
tooltip: 'Sync profile',
icon: const Icon(Icons.sync),
),
IconButton(
onPressed: () => AuthService.instance.signOut(),
tooltip: 'Sign out',
icon: const Icon(Icons.logout),
),
],
),
body: DecoratedBox(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: <Color>[AppColors.background, AppColors.surface],
),
),
child: SafeArea(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: AppColors.surfaceElevated,
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: AppColors.accent.withValues(alpha: 0.2),
),
),
child: Column(
children: <Widget>[
if (photoUrl != null)
CircleAvatar(
radius: 36,
backgroundImage: NetworkImage(photoUrl),
)
else
const CircleAvatar(
radius: 36,
child: Icon(Icons.person, size: 36),
),
const SizedBox(height: 16),
Text(
'Welcome, $displayName',
style: Theme.of(context).textTheme.headlineMedium,
textAlign: TextAlign.center,
),
if (email != null) ...<Widget>[
const SizedBox(height: 8),
Text(
email,
style: Theme.of(context).textTheme.bodyLarge,
textAlign: TextAlign.center,
),
],
const SizedBox(height: 12),
_SyncStatusChip(status: syncStatus),
const SizedBox(height: 20),
const Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Icon(
Icons.check_circle,
color: AppColors.success,
size: 20,
),
SizedBox(width: 8),
Text(
'You\'re signed in',
style: TextStyle(
color: AppColors.success,
fontWeight: FontWeight.w600,
),
),
],
),
],
),
),
const SizedBox(height: 24),
Text(
profile?.dirty == true
? 'Profile saved locally. Will sync when online.'
: 'Profile synced with your account.',
style: Theme.of(context).textTheme.bodyLarge,
textAlign: TextAlign.center,
),
if (UserProfileRepository.instance.usesLocalStore) ...<Widget>[
const SizedBox(height: 16),
OutlinedButton.icon(
onPressed: profile == null
? null
: () async {
final UserProfile current = profile!;
await UserProfileRepository.instance.updateProfile(
current.copyWith(
onboardingCompleted:
!current.onboardingCompleted,
),
);
},
icon: const Icon(Icons.toggle_on_outlined),
label: Text(
profile?.onboardingCompleted == true
? 'Mark onboarding incomplete'
: 'Mark onboarding complete',
),
),
],
],
),
),
),
),
);
}
}
class _SyncStatusChip extends StatelessWidget {
const _SyncStatusChip({required this.status});
final ProfileSyncStatus status;
@override
Widget build(BuildContext context) {
final ({String label, Color color, IconData icon}) style = switch (status) {
ProfileSyncStatus.syncing => (
label: 'Syncing…',
color: AppColors.accent,
icon: Icons.sync,
),
ProfileSyncStatus.synced => (
label: 'Synced',
color: AppColors.success,
icon: Icons.cloud_done_outlined,
),
ProfileSyncStatus.offline => (
label: 'Offline',
color: Colors.orange,
icon: Icons.cloud_off_outlined,
),
ProfileSyncStatus.error => (
label: 'Sync error',
color: Colors.redAccent,
icon: Icons.error_outline,
),
ProfileSyncStatus.idle => (
label: 'Ready',
color: AppColors.accent,
icon: Icons.cloud_queue_outlined,
),
};
return Chip(
avatar: Icon(style.icon, size: 18, color: style.color),
label: Text(style.label),
side: BorderSide(color: style.color.withValues(alpha: 0.4)),
);
}
}

View File

@ -0,0 +1,464 @@
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter/material.dart';
import '../theme/app_theme.dart';
import '../widgets/benefit_row.dart';
import '../widgets/google_sign_in_button.dart';
class LandingScreen extends StatefulWidget {
const LandingScreen({super.key});
@override
State<LandingScreen> createState() => _LandingScreenState();
}
class _LandingScreenState extends State<LandingScreen> {
String? _errorMessage;
@override
Widget build(BuildContext context) {
return Scaffold(
body: DecoratedBox(
decoration: const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: <Color>[
AppColors.background,
Color(0xFF0C1222),
Color(0xFF0A1628),
],
),
),
child: Stack(
children: <Widget>[
const _BackgroundGlow(),
SafeArea(
child: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
final bool wide = constraints.maxWidth >= 720;
return Center(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 960),
child: wide
? _WideLayout(
errorMessage: _errorMessage,
onError: _handleError,
)
: _NarrowLayout(
errorMessage: _errorMessage,
onError: _handleError,
),
),
);
},
),
),
],
),
),
);
}
void _handleError(Object error) {
setState(() {
_errorMessage = _friendlyError(error);
});
}
String _friendlyError(Object error) {
if (error is FirebaseAuthException) {
switch (error.code) {
case 'unauthorized-domain':
final String origin = kIsWeb ? Uri.base.origin : 'this app';
return 'Add $origin under Firebase Console → Authentication → '
'Settings → Authorized domains.';
case 'operation-not-allowed':
return 'Google sign-in is not enabled. Turn on the Google provider '
'in Firebase Console → Authentication → Sign-in method.';
case 'popup-blocked':
return 'The sign-in popup was blocked. Allow popups for this site '
'or try again.';
case 'popup-closed-by-user':
case 'cancelled-popup-request':
return 'Sign-in was cancelled. Tap below to try again.';
}
}
final String raw = error.toString();
if (raw.contains('missing initial state') ||
raw.contains('sessionStorage')) {
return 'This browser blocked sign-in storage (common in Firefox). '
'Set googleWebOAuthClientId in lib/config/auth_config.dart, allow '
'popups for this site, or try Chrome.';
}
if (raw.contains('canceled') || raw.contains('cancelled')) {
return 'Sign-in was cancelled. Tap below to try again.';
}
if (raw.contains('network')) {
return 'Network error. Check your connection and try again.';
}
if (raw.contains('googleWebOAuthClientId')) {
return raw.replaceFirst('Bad state: ', '');
}
return 'Could not sign in. Please try again.';
}
}
class _BackgroundGlow extends StatelessWidget {
const _BackgroundGlow();
@override
Widget build(BuildContext context) {
return IgnorePointer(
child: Stack(
children: <Widget>[
Positioned(
top: -120,
right: -80,
child: Container(
width: 320,
height: 320,
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: RadialGradient(
colors: <Color>[
AppColors.accent.withValues(alpha: 0.18),
Colors.transparent,
],
),
),
),
),
Positioned(
bottom: -100,
left: -60,
child: Container(
width: 280,
height: 280,
decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: RadialGradient(
colors: <Color>[
AppColors.accentMuted.withValues(alpha: 0.12),
Colors.transparent,
],
),
),
),
),
],
),
);
}
}
class _NarrowLayout extends StatelessWidget {
const _NarrowLayout({required this.errorMessage, required this.onError});
final String? errorMessage;
final void Function(Object error) onError;
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
const _BrandHeader(),
const SizedBox(height: 32),
const _HeroCopy(),
const SizedBox(height: 28),
const _BenefitsList(),
const SizedBox(height: 32),
_CtaSection(errorMessage: errorMessage, onError: onError),
const SizedBox(height: 24),
const _TrustFooter(),
],
),
);
}
}
class _WideLayout extends StatelessWidget {
const _WideLayout({required this.errorMessage, required this.onError});
final String? errorMessage;
final void Function(Object error) onError;
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 40, vertical: 24),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
const Expanded(
flex: 5,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
_BrandHeader(),
SizedBox(height: 40),
_HeroCopy(),
SizedBox(height: 36),
_BenefitsList(),
],
),
),
const SizedBox(width: 48),
Expanded(
flex: 4,
child: _CtaCard(errorMessage: errorMessage, onError: onError),
),
],
),
);
}
}
class _BrandHeader extends StatelessWidget {
const _BrandHeader();
@override
Widget build(BuildContext context) {
return Row(
children: <Widget>[
Container(
width: 40,
height: 40,
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: <Color>[AppColors.accent, AppColors.accentMuted],
),
borderRadius: BorderRadius.circular(10),
),
child: const Icon(
Icons.shield_outlined,
color: AppColors.background,
size: 22,
),
),
const SizedBox(width: 12),
Text(
'Cyber Hybrid Hub',
style: Theme.of(context).textTheme.titleLarge?.copyWith(
letterSpacing: -0.2,
),
),
],
);
}
}
class _HeroCopy extends StatelessWidget {
const _HeroCopy();
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: AppColors.success.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: AppColors.success.withValues(alpha: 0.35),
),
),
child: const Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Icon(Icons.bolt, size: 16, color: AppColors.success),
SizedBox(width: 6),
Text(
'Free to get started',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
color: AppColors.success,
),
),
],
),
),
const SizedBox(height: 20),
Text(
'Your hybrid security\ncommand center',
style: Theme.of(context).textTheme.headlineLarge,
),
const SizedBox(height: 16),
Text(
'Unify threat monitoring, team collaboration, and compliance '
'workflows in one secure workspace — built for modern hybrid teams.',
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
fontSize: 17,
),
),
],
);
}
}
class _BenefitsList extends StatelessWidget {
const _BenefitsList();
static const List<({IconData icon, String title, String subtitle})> _items =
<({IconData icon, String title, String subtitle})>[
(
icon: Icons.radar,
title: 'Real-time threat visibility',
subtitle: 'See alerts and incidents as they happen, not hours later.',
),
(
icon: Icons.groups_outlined,
title: 'Built for hybrid teams',
subtitle: 'On-site and remote staff share one source of truth.',
),
(
icon: Icons.verified_user_outlined,
title: 'Sign in securely with Google',
subtitle: 'No new passwords — enterprise-ready OAuth in one tap.',
),
];
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
for (int i = 0; i < _items.length; i++) ...<Widget>[
if (i > 0) const SizedBox(height: 20),
BenefitRow(
icon: _items[i].icon,
title: _items[i].title,
subtitle: _items[i].subtitle,
),
],
],
);
}
}
class _CtaSection extends StatelessWidget {
const _CtaSection({required this.errorMessage, required this.onError});
final String? errorMessage;
final void Function(Object error) onError;
@override
Widget build(BuildContext context) {
return _CtaCard(errorMessage: errorMessage, onError: onError);
}
}
class _CtaCard extends StatelessWidget {
const _CtaCard({required this.errorMessage, required this.onError});
final String? errorMessage;
final void Function(Object error) onError;
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: AppColors.surfaceElevated,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: Colors.white.withValues(alpha: 0.08)),
boxShadow: <BoxShadow>[
BoxShadow(
color: AppColors.accent.withValues(alpha: 0.08),
blurRadius: 40,
offset: const Offset(0, 12),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Text(
'Get started in seconds',
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
fontSize: 22,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Text(
'Use your Google account — no credit card required.',
style: Theme.of(context).textTheme.bodyLarge,
textAlign: TextAlign.center,
),
const SizedBox(height: 24),
if (errorMessage != null) ...<Widget>[
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.error.withValues(
alpha: 0.12,
),
borderRadius: BorderRadius.circular(8),
),
child: Text(
errorMessage!,
style: TextStyle(
color: Theme.of(context).colorScheme.error,
fontSize: 14,
),
textAlign: TextAlign.center,
),
),
const SizedBox(height: 16),
],
GoogleSignInButton(
label: 'Get started with Google',
onError: onError,
),
const SizedBox(height: 16),
const _SocialProof(),
],
),
);
}
}
class _SocialProof extends StatelessWidget {
const _SocialProof();
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const Icon(Icons.lock_outline, size: 14, color: AppColors.textSecondary),
const SizedBox(width: 6),
Flexible(
child: Text(
'Secured by Firebase · Google OAuth',
style: Theme.of(context).textTheme.bodyLarge?.copyWith(fontSize: 12),
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
maxLines: 2,
),
),
],
);
}
}
class _TrustFooter extends StatelessWidget {
const _TrustFooter();
@override
Widget build(BuildContext context) {
return Text(
'By continuing, you agree to our Terms of Service and Privacy Policy.',
style: Theme.of(context).textTheme.bodyLarge?.copyWith(fontSize: 12),
textAlign: TextAlign.center,
);
}
}

View File

@ -0,0 +1,68 @@
import 'package:flutter/foundation.dart'
show TargetPlatform, defaultTargetPlatform, kIsWeb;
import '../models/app_user.dart';
import 'auth_service_firebase.dart';
import 'auth_service_linux.dart';
/// Unified auth API for mobile, web, and Linux desktop development.
class AuthService {
AuthService._();
static final AuthService instance = AuthService._();
/// True when running the Linux desktop app (not unit tests on a Linux host).
static bool get isLinuxDesktop {
if (kIsWeb) {
return false;
}
if (const bool.fromEnvironment('FLUTTER_TEST')) {
return false;
}
return defaultTargetPlatform == TargetPlatform.linux;
}
Stream<AppUser?> get authStateChanges => isLinuxDesktop
? AuthServiceLinux.instance.authStateChanges
: AuthServiceFirebase.instance.authStateChanges;
AppUser? get currentUser => isLinuxDesktop
? AuthServiceLinux.instance.currentUser
: AuthServiceFirebase.instance.currentUser;
Future<void> initialize() async {
if (isLinuxDesktop) {
await AuthServiceLinux.instance.initialize();
} else {
await AuthServiceFirebase.instance.initialize();
}
}
Future<void> signInWithGoogle() async {
if (isLinuxDesktop) {
await AuthServiceLinux.instance.signInWithGoogle();
} else {
await AuthServiceFirebase.instance.signInWithGoogle();
}
}
Future<void> signOut() async {
if (isLinuxDesktop) {
await AuthServiceLinux.instance.signOut();
} else {
await AuthServiceFirebase.instance.signOut();
}
}
/// Firebase ID token for API requests. Returns null when signed out.
Future<String?> getIdToken({bool forceRefresh = false}) async {
if (isLinuxDesktop) {
return AuthServiceLinux.instance.getIdToken(
forceRefresh: forceRefresh,
);
}
return AuthServiceFirebase.instance.getIdToken(
forceRefresh: forceRefresh,
);
}
}

View File

@ -0,0 +1,135 @@
import 'package:firebase_auth/firebase_auth.dart';
import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:google_sign_in/google_sign_in.dart';
import '../config/auth_config.dart';
import '../models/app_user.dart';
class AuthServiceFirebase {
AuthServiceFirebase._();
static final AuthServiceFirebase instance = AuthServiceFirebase._();
final FirebaseAuth _firebaseAuth = FirebaseAuth.instance;
final GoogleSignIn _googleSignIn = GoogleSignIn.instance;
bool _googleSignInReady = false;
Stream<AppUser?> get authStateChanges =>
_firebaseAuth.authStateChanges().map(_mapUser);
AppUser? get currentUser => _mapUser(_firebaseAuth.currentUser);
Future<void> initialize() async {
if (kIsWeb) {
if (googleWebOAuthClientId != null) {
await _googleSignIn.initialize(clientId: googleWebOAuthClientId);
_googleSignInReady = true;
}
return;
}
if (googleWebOAuthClientId != null) {
await _googleSignIn.initialize(
serverClientId: googleWebOAuthClientId,
);
} else {
await _googleSignIn.initialize();
}
_googleSignInReady = true;
}
Future<void> signInWithGoogle() async {
if (kIsWeb) {
await _signInWithGoogleWeb();
return;
}
_ensureGoogleSignInReady();
final GoogleSignInAccount account = await _googleSignIn.authenticate();
final String? idToken = account.authentication.idToken;
if (idToken == null) {
throw StateError('Google Sign-In did not return an ID token.');
}
final credential = GoogleAuthProvider.credential(idToken: idToken);
await _firebaseAuth.signInWithCredential(credential);
}
/// Web: popup avoids Firebase redirect sessionStorage (broken under ETP).
/// Falls back to Google Identity Services + [signInWithCredential] when set up.
Future<void> _signInWithGoogleWeb() async {
try {
await _firebaseAuth.signInWithPopup(GoogleAuthProvider());
return;
} on FirebaseAuthException catch (e) {
if (_isUserCancelledAuth(e)) {
rethrow;
}
}
if (!_googleSignInReady) {
throw StateError(
'Google sign-in failed in this browser (often Firefox with strict '
'privacy). Set googleWebOAuthClientId in lib/config/auth_config.dart '
'(Firebase Console → Authentication → Google → Web SDK configuration) '
'and add ${Uri.base.origin} to that client\'s authorized JavaScript '
'origins in Google Cloud Console.',
);
}
final GoogleSignInAccount account = await _googleSignIn.authenticate();
final String? idToken = account.authentication.idToken;
if (idToken == null) {
throw StateError('Google Sign-In did not return an ID token.');
}
await _firebaseAuth.signInWithCredential(
GoogleAuthProvider.credential(idToken: idToken),
);
}
Future<void> signOut() async {
if (_googleSignInReady) {
await _googleSignIn.signOut();
}
await _firebaseAuth.signOut();
}
Future<String?> getIdToken({bool forceRefresh = false}) async {
final User? user = _firebaseAuth.currentUser;
if (user == null) {
return null;
}
return user.getIdToken(forceRefresh);
}
void _ensureGoogleSignInReady() {
if (_googleSignInReady) {
return;
}
throw StateError(
'Google Sign-In is not configured. Set googleWebOAuthClientId in '
'lib/config/auth_config.dart for Android, or run a full restart after '
'changing auth settings.',
);
}
bool _isUserCancelledAuth(FirebaseAuthException e) {
return e.code == 'popup-closed-by-user' ||
e.code == 'cancelled-popup-request';
}
AppUser? _mapUser(User? user) {
if (user == null) {
return null;
}
return AppUser(
uid: user.uid,
email: user.email,
displayName: user.displayName,
photoUrl: user.photoURL,
);
}
}

View File

@ -0,0 +1,201 @@
import 'dart:async';
import 'dart:convert';
import 'package:google_sign_in_all_platforms/google_sign_in_all_platforms.dart';
import 'package:http/http.dart' as http;
import '../config/auth_config.dart';
import '../firebase_options.dart';
import '../models/app_user.dart';
class AuthServiceLinux {
AuthServiceLinux._();
static final AuthServiceLinux instance = AuthServiceLinux._();
final StreamController<AppUser?> _authController =
StreamController<AppUser?>.broadcast();
GoogleSignIn? _googleSignIn;
AppUser? _currentUser;
String? _idToken;
String? _refreshToken;
DateTime? _tokenExpiry;
Stream<AppUser?> get authStateChanges => _authController.stream;
AppUser? get currentUser => _currentUser;
Future<void> initialize() async {
if (!_hasOAuthConfig) {
_emitUser(null);
return;
}
_googleSignIn = GoogleSignIn(
params: GoogleSignInParams(
clientId: googleWebOAuthClientId!,
clientSecret: googleOAuthDesktopClientSecret!,
scopes: const <String>['openid', 'profile', 'email'],
),
);
_googleSignIn!.authenticationState.listen((GoogleSignInCredentials? creds) {
if (creds == null) {
_emitUser(null);
}
});
final GoogleSignInCredentials? restored =
await _googleSignIn!.silentSignIn();
if (restored?.idToken != null) {
try {
final AppUser user = await _signInToFirebaseWithIdp(restored!.idToken!);
_emitUser(user);
} catch (_) {
// Ignore stale desktop sessions on startup.
}
}
}
bool get _hasOAuthConfig =>
googleWebOAuthClientId != null &&
googleOAuthDesktopClientSecret != null;
Future<void> signInWithGoogle() async {
_validateOAuthConfig();
final GoogleSignInCredentials? credentials = await _googleSignIn!.signIn();
if (credentials == null) {
throw StateError('Google Sign-In was cancelled.');
}
final String? idToken = credentials.idToken;
if (idToken == null) {
throw StateError('Google Sign-In did not return an ID token.');
}
final AppUser user = await _signInToFirebaseWithIdp(idToken);
_emitUser(user);
}
Future<void> signOut() async {
await _googleSignIn?.signOut();
_clearTokens();
_emitUser(null);
}
Future<String?> getIdToken({bool forceRefresh = false}) async {
if (_currentUser == null) {
return null;
}
if (!forceRefresh &&
_idToken != null &&
_tokenExpiry != null &&
DateTime.now().isBefore(_tokenExpiry!)) {
return _idToken;
}
if (_refreshToken != null) {
await _refreshIdToken();
return _idToken;
}
return _idToken;
}
void _emitUser(AppUser? user) {
_currentUser = user;
if (user == null) {
_clearTokens();
}
_authController.add(user);
}
void _clearTokens() {
_idToken = null;
_refreshToken = null;
_tokenExpiry = null;
}
void _storeTokens(Map<String, dynamic> data) {
_idToken = data['idToken'] as String?;
_refreshToken = data['refreshToken'] as String?;
final int expiresIn = (data['expiresIn'] as String?) != null
? int.parse(data['expiresIn'] as String)
: (data['expiresIn'] as num?)?.toInt() ?? 3600;
_tokenExpiry = DateTime.now().add(Duration(seconds: expiresIn - 60));
}
Future<void> _refreshIdToken() async {
if (_refreshToken == null) {
return;
}
final Uri uri = Uri.parse(
'https://securetoken.googleapis.com/v1/token'
'?key=${DefaultFirebaseOptions.web.apiKey}',
);
final http.Response response = await http.post(
uri,
headers: const <String, String>{
'Content-Type': 'application/x-www-form-urlencoded',
},
body: 'grant_type=refresh_token&refresh_token=$_refreshToken',
);
if (response.statusCode != 200) {
_clearTokens();
return;
}
final Map<String, dynamic> data =
jsonDecode(response.body) as Map<String, dynamic>;
_idToken = data['id_token'] as String?;
_refreshToken = data['refresh_token'] as String? ?? _refreshToken;
final int expiresIn = (data['expires_in'] as num?)?.toInt() ?? 3600;
_tokenExpiry = DateTime.now().add(Duration(seconds: expiresIn - 60));
}
void _validateOAuthConfig() {
if (!_hasOAuthConfig) {
throw StateError(
'Linux desktop auth requires googleWebOAuthClientId and '
'googleOAuthDesktopClientSecret in lib/config/auth_config.dart. '
'Create a Web application OAuth client in Google Cloud Console '
'(same project as Firebase) and add '
'http://127.0.0.1 as an authorized redirect URI.',
);
}
}
Future<AppUser> _signInToFirebaseWithIdp(String idToken) async {
final Uri uri = Uri.parse(
'https://identitytoolkit.googleapis.com/v1/accounts:signInWithIdp'
'?key=${DefaultFirebaseOptions.web.apiKey}',
);
final http.Response response = await http.post(
uri,
headers: const <String, String>{'Content-Type': 'application/json'},
body: jsonEncode(<String, dynamic>{
'postBody': 'id_token=$idToken&providerId=google.com',
'requestUri': 'http://localhost',
'returnIdpCredential': true,
'returnSecureToken': true,
}),
);
if (response.statusCode != 200) {
throw StateError(
'Firebase sign-in failed (${response.statusCode}): ${response.body}',
);
}
final Map<String, dynamic> data =
jsonDecode(response.body) as Map<String, dynamic>;
_storeTokens(data);
return AppUser(
uid: data['localId'] as String,
email: data['email'] as String?,
displayName: data['displayName'] as String?,
photoUrl: data['photoUrl'] as String?,
);
}
}

59
lib/theme/app_theme.dart Normal file
View File

@ -0,0 +1,59 @@
import 'package:flutter/material.dart';
abstract final class AppColors {
static const Color background = Color(0xFF070B14);
static const Color surface = Color(0xFF111827);
static const Color surfaceElevated = Color(0xFF1A2332);
static const Color accent = Color(0xFF22D3EE);
static const Color accentMuted = Color(0xFF0891B2);
static const Color textPrimary = Color(0xFFF8FAFC);
static const Color textSecondary = Color(0xFF94A3B8);
static const Color success = Color(0xFF34D399);
}
ThemeData buildAppTheme() {
const ColorScheme scheme = ColorScheme.dark(
primary: AppColors.accent,
onPrimary: AppColors.background,
secondary: AppColors.accentMuted,
surface: AppColors.surface,
onSurface: AppColors.textPrimary,
error: Color(0xFFF87171),
);
return ThemeData(
useMaterial3: true,
brightness: Brightness.dark,
colorScheme: scheme,
scaffoldBackgroundColor: AppColors.background,
textTheme: const TextTheme(
headlineLarge: TextStyle(
fontSize: 36,
fontWeight: FontWeight.w700,
height: 1.15,
letterSpacing: -0.5,
color: AppColors.textPrimary,
),
headlineMedium: TextStyle(
fontSize: 28,
fontWeight: FontWeight.w600,
color: AppColors.textPrimary,
),
titleLarge: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w600,
color: AppColors.textPrimary,
),
bodyLarge: TextStyle(
fontSize: 16,
height: 1.5,
color: AppColors.textSecondary,
),
labelLarge: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppColors.background,
),
),
);
}

View File

@ -0,0 +1,51 @@
import 'package:flutter/material.dart';
import '../theme/app_theme.dart';
class BenefitRow extends StatelessWidget {
const BenefitRow({
super.key,
required this.icon,
required this.title,
required this.subtitle,
});
final IconData icon;
final String title;
final String subtitle;
@override
Widget build(BuildContext context) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Container(
width: 44,
height: 44,
decoration: BoxDecoration(
color: AppColors.accent.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(10),
border: Border.all(
color: AppColors.accent.withValues(alpha: 0.25),
),
),
child: Icon(icon, color: AppColors.accent, size: 22),
),
const SizedBox(width: 14),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
title,
style: Theme.of(context).textTheme.titleLarge,
),
const SizedBox(height: 4),
Text(subtitle, style: Theme.of(context).textTheme.bodyLarge),
],
),
),
],
);
}
}

View File

@ -0,0 +1,139 @@
import 'package:flutter/material.dart';
import '../services/auth_service.dart';
import '../theme/app_theme.dart';
class GoogleSignInButton extends StatefulWidget {
const GoogleSignInButton({
super.key,
this.label = 'Continue with Google',
this.compact = false,
this.onSignedIn,
this.onError,
});
final String label;
final bool compact;
final VoidCallback? onSignedIn;
final void Function(Object error)? onError;
@override
State<GoogleSignInButton> createState() => _GoogleSignInButtonState();
}
class _GoogleSignInButtonState extends State<GoogleSignInButton> {
bool _isLoading = false;
Future<void> _signIn() async {
setState(() => _isLoading = true);
try {
await AuthService.instance.signInWithGoogle();
widget.onSignedIn?.call();
} catch (error) {
widget.onError?.call(error);
} finally {
if (mounted) {
setState(() => _isLoading = false);
}
}
}
@override
Widget build(BuildContext context) {
final double verticalPadding = widget.compact ? 14 : 16;
return SizedBox(
width: double.infinity,
child: FilledButton(
onPressed: _isLoading ? null : _signIn,
style: FilledButton.styleFrom(
backgroundColor: Colors.white,
foregroundColor: const Color(0xFF1F2937),
disabledBackgroundColor: Colors.white.withValues(alpha: 0.7),
padding: EdgeInsets.symmetric(vertical: verticalPadding),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
elevation: 0,
),
child: _isLoading
? const SizedBox(
width: 22,
height: 22,
child: CircularProgressIndicator(
strokeWidth: 2.5,
color: AppColors.background,
),
)
: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
const _GoogleLogo(size: 22),
const SizedBox(width: 12),
Flexible(
child: Text(
widget.label,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Color(0xFF1F2937),
),
textAlign: TextAlign.center,
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
),
],
),
),
);
}
}
class _GoogleLogo extends StatelessWidget {
const _GoogleLogo({required this.size});
final double size;
@override
Widget build(BuildContext context) {
return SizedBox(
width: size,
height: size,
child: CustomPaint(painter: _GoogleLogoPainter()),
);
}
}
class _GoogleLogoPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final double w = size.width;
final double h = size.height;
final Offset center = Offset(w / 2, h / 2);
final double r = w * 0.42;
void arc(Color color, double start, double sweep) {
final Paint paint = Paint()
..color = color
..style = PaintingStyle.stroke
..strokeWidth = w * 0.18
..strokeCap = StrokeCap.round;
canvas.drawArc(
Rect.fromCircle(center: center, radius: r),
start,
sweep,
false,
paint,
);
}
arc(const Color(0xFF4285F4), -0.4, 1.6);
arc(const Color(0xFFEA4335), 1.2, 1.3);
arc(const Color(0xFFFBBC05), 2.5, 1.3);
arc(const Color(0xFF34A853), 3.8, 1.3);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}

View File

@ -0,0 +1,79 @@
import 'dart:async';
import 'package:flutter/material.dart';
import '../models/app_user.dart';
import '../models/sync_result.dart';
import '../models/user_profile.dart';
import '../repositories/user_profile_repository.dart';
import '../theme/app_theme.dart';
/// Starts a profile sync session for [user] and exposes profile + sync state.
class ProfileSession extends StatefulWidget {
const ProfileSession({
super.key,
required this.user,
required this.builder,
});
final AppUser user;
final Widget Function(
BuildContext context,
UserProfile? profile,
ProfileSyncStatus syncStatus,
) builder;
@override
State<ProfileSession> createState() => _ProfileSessionState();
}
class _ProfileSessionState extends State<ProfileSession> {
late final Future<void> _sessionReady;
ProfileSyncStatus _syncStatus = ProfileSyncStatus.idle;
StreamSubscription<ProfileSyncStatus>? _syncStatusSubscription;
@override
void initState() {
super.initState();
_sessionReady = UserProfileRepository.instance.startSession(widget.user);
_syncStatusSubscription =
UserProfileRepository.instance.syncStatusStream.listen((
ProfileSyncStatus status,
) {
if (mounted) {
setState(() => _syncStatus = status);
}
});
}
@override
void dispose() {
_syncStatusSubscription?.cancel();
UserProfileRepository.instance.endSession();
super.dispose();
}
@override
Widget build(BuildContext context) {
return FutureBuilder<void>(
future: _sessionReady,
builder: (BuildContext context, AsyncSnapshot<void> sessionSnap) {
if (sessionSnap.connectionState != ConnectionState.done) {
return const Scaffold(
body: Center(
child: CircularProgressIndicator(color: AppColors.accent),
),
);
}
return StreamBuilder<UserProfile?>(
stream: UserProfileRepository.instance.profileStream,
initialData: UserProfileRepository.instance.currentProfile,
builder: (BuildContext context, AsyncSnapshot<UserProfile?> snap) {
return widget.builder(context, snap.data, _syncStatus);
},
);
},
);
}
}

1
linux/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
flutter/ephemeral

Some files were not shown because too many files have changed in this diff Show More