diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..3820a95 --- /dev/null +++ b/app/.gitignore @@ -0,0 +1,45 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.build/ +.buildlog/ +.history +.svn/ +.swiftpm/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ +/coverage/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/app/.metadata b/app/.metadata new file mode 100644 index 0000000..1590e48 --- /dev/null +++ b/app/.metadata @@ -0,0 +1,30 @@ +# 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: "00b0c91f06209d9e4a41f71b7a512d6eb3b9c694" + channel: "stable" + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 00b0c91f06209d9e4a41f71b7a512d6eb3b9c694 + base_revision: 00b0c91f06209d9e4a41f71b7a512d6eb3b9c694 + - platform: web + create_revision: 00b0c91f06209d9e4a41f71b7a512d6eb3b9c694 + base_revision: 00b0c91f06209d9e4a41f71b7a512d6eb3b9c694 + + # 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' diff --git a/app/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java b/app/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java new file mode 100644 index 0000000..e38a134 --- /dev/null +++ b/app/android/app/src/main/java/io/flutter/plugins/GeneratedPluginRegistrant.java @@ -0,0 +1,39 @@ +package io.flutter.plugins; + +import androidx.annotation.Keep; +import androidx.annotation.NonNull; +import io.flutter.Log; + +import io.flutter.embedding.engine.FlutterEngine; + +/** + * Generated file. Do not edit. + * This file is generated by the Flutter tool based on the + * plugins that support the Android platform. + */ +@Keep +public final class GeneratedPluginRegistrant { + private static final String TAG = "GeneratedPluginRegistrant"; + public static void registerWith(@NonNull FlutterEngine flutterEngine) { + try { + flutterEngine.getPlugins().add(new com.lib.flutter_blue_plus.FlutterBluePlusPlugin()); + } catch (Exception e) { + Log.e(TAG, "Error registering plugin flutter_blue_plus_android, com.lib.flutter_blue_plus.FlutterBluePlusPlugin", e); + } + try { + flutterEngine.getPlugins().add(new com.dexterous.flutterlocalnotifications.FlutterLocalNotificationsPlugin()); + } catch (Exception e) { + Log.e(TAG, "Error registering plugin flutter_local_notifications, com.dexterous.flutterlocalnotifications.FlutterLocalNotificationsPlugin", e); + } + try { + flutterEngine.getPlugins().add(new com.baseflow.permissionhandler.PermissionHandlerPlugin()); + } catch (Exception e) { + Log.e(TAG, "Error registering plugin permission_handler_android, com.baseflow.permissionhandler.PermissionHandlerPlugin", e); + } + try { + flutterEngine.getPlugins().add(new io.flutter.plugins.sharedpreferences.SharedPreferencesPlugin()); + } catch (Exception e) { + Log.e(TAG, "Error registering plugin shared_preferences_android, io.flutter.plugins.sharedpreferences.SharedPreferencesPlugin", e); + } + } +} diff --git a/app/android/local.properties b/app/android/local.properties new file mode 100644 index 0000000..93587d9 --- /dev/null +++ b/app/android/local.properties @@ -0,0 +1 @@ +flutter.sdk=/opt/homebrew/share/flutter \ No newline at end of file diff --git a/app/ios/Flutter/Generated.xcconfig b/app/ios/Flutter/Generated.xcconfig new file mode 100644 index 0000000..41dcf91 --- /dev/null +++ b/app/ios/Flutter/Generated.xcconfig @@ -0,0 +1,14 @@ +// This is a generated file; do not edit or check into version control. +FLUTTER_ROOT=/opt/homebrew/share/flutter +FLUTTER_APPLICATION_PATH=/Users/lrihni/Projects/SigHej/app +COCOAPODS_PARALLEL_CODE_SIGN=true +FLUTTER_TARGET=lib/main.dart +FLUTTER_BUILD_DIR=build +FLUTTER_BUILD_NAME=0.1.0 +FLUTTER_BUILD_NUMBER=1 +EXCLUDED_ARCHS[sdk=iphonesimulator*]=i386 +EXCLUDED_ARCHS[sdk=iphoneos*]=armv7 +DART_OBFUSCATION=false +TRACK_WIDGET_CREATION=true +TREE_SHAKE_ICONS=false +PACKAGE_CONFIG=.dart_tool/package_config.json diff --git a/app/ios/Flutter/ephemeral/flutter_lldb_helper.py b/app/ios/Flutter/ephemeral/flutter_lldb_helper.py new file mode 100644 index 0000000..a88caf9 --- /dev/null +++ b/app/ios/Flutter/ephemeral/flutter_lldb_helper.py @@ -0,0 +1,32 @@ +# +# Generated file, do not edit. +# + +import lldb + +def handle_new_rx_page(frame: lldb.SBFrame, bp_loc, extra_args, intern_dict): + """Intercept NOTIFY_DEBUGGER_ABOUT_RX_PAGES and touch the pages.""" + base = frame.register["x0"].GetValueAsAddress() + page_len = frame.register["x1"].GetValueAsUnsigned() + + # Note: NOTIFY_DEBUGGER_ABOUT_RX_PAGES will check contents of the + # first page to see if handled it correctly. This makes diagnosing + # misconfiguration (e.g. missing breakpoint) easier. + data = bytearray(page_len) + data[0:8] = b'IHELPED!' + + error = lldb.SBError() + frame.GetThread().GetProcess().WriteMemory(base, data, error) + if not error.Success(): + print(f'Failed to write into {base}[+{page_len}]', error) + return + +def __lldb_init_module(debugger: lldb.SBDebugger, _): + target = debugger.GetDummyTarget() + # Caveat: must use BreakpointCreateByRegEx here and not + # BreakpointCreateByName. For some reasons callback function does not + # get carried over from dummy target for the later. + bp = target.BreakpointCreateByRegex("^NOTIFY_DEBUGGER_ABOUT_RX_PAGES$") + bp.SetScriptCallbackFunction('{}.handle_new_rx_page'.format(__name__)) + bp.SetAutoContinue(True) + print("-- LLDB integration loaded --") diff --git a/app/ios/Flutter/ephemeral/flutter_lldbinit b/app/ios/Flutter/ephemeral/flutter_lldbinit new file mode 100644 index 0000000..e3ba6fb --- /dev/null +++ b/app/ios/Flutter/ephemeral/flutter_lldbinit @@ -0,0 +1,5 @@ +# +# Generated file, do not edit. +# + +command script import --relative-to-command-file flutter_lldb_helper.py diff --git a/app/ios/Flutter/flutter_export_environment.sh b/app/ios/Flutter/flutter_export_environment.sh new file mode 100755 index 0000000..a642b2b --- /dev/null +++ b/app/ios/Flutter/flutter_export_environment.sh @@ -0,0 +1,13 @@ +#!/bin/sh +# This is a generated file; do not edit or check into version control. +export "FLUTTER_ROOT=/opt/homebrew/share/flutter" +export "FLUTTER_APPLICATION_PATH=/Users/lrihni/Projects/SigHej/app" +export "COCOAPODS_PARALLEL_CODE_SIGN=true" +export "FLUTTER_TARGET=lib/main.dart" +export "FLUTTER_BUILD_DIR=build" +export "FLUTTER_BUILD_NAME=0.1.0" +export "FLUTTER_BUILD_NUMBER=1" +export "DART_OBFUSCATION=false" +export "TRACK_WIDGET_CREATION=true" +export "TREE_SHAKE_ICONS=false" +export "PACKAGE_CONFIG=.dart_tool/package_config.json" diff --git a/app/ios/Runner/GeneratedPluginRegistrant.h b/app/ios/Runner/GeneratedPluginRegistrant.h new file mode 100644 index 0000000..7a89092 --- /dev/null +++ b/app/ios/Runner/GeneratedPluginRegistrant.h @@ -0,0 +1,19 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GeneratedPluginRegistrant_h +#define GeneratedPluginRegistrant_h + +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface GeneratedPluginRegistrant : NSObject ++ (void)registerWithRegistry:(NSObject*)registry; +@end + +NS_ASSUME_NONNULL_END +#endif /* GeneratedPluginRegistrant_h */ diff --git a/app/ios/Runner/GeneratedPluginRegistrant.m b/app/ios/Runner/GeneratedPluginRegistrant.m new file mode 100644 index 0000000..df42930 --- /dev/null +++ b/app/ios/Runner/GeneratedPluginRegistrant.m @@ -0,0 +1,42 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#import "GeneratedPluginRegistrant.h" + +#if __has_include() +#import +#else +@import flutter_blue_plus_darwin; +#endif + +#if __has_include() +#import +#else +@import flutter_local_notifications; +#endif + +#if __has_include() +#import +#else +@import permission_handler_apple; +#endif + +#if __has_include() +#import +#else +@import shared_preferences_foundation; +#endif + +@implementation GeneratedPluginRegistrant + ++ (void)registerWithRegistry:(NSObject*)registry { + [FlutterBluePlusPlugin registerWithRegistrar:[registry registrarForPlugin:@"FlutterBluePlusPlugin"]]; + [FlutterLocalNotificationsPlugin registerWithRegistrar:[registry registrarForPlugin:@"FlutterLocalNotificationsPlugin"]]; + [PermissionHandlerPlugin registerWithRegistrar:[registry registrarForPlugin:@"PermissionHandlerPlugin"]]; + [SharedPreferencesPlugin registerWithRegistrar:[registry registrarForPlugin:@"SharedPreferencesPlugin"]]; +} + +@end diff --git a/app/lib/data/conversation_starters.dart b/app/lib/data/conversation_starters.dart new file mode 100644 index 0000000..909a5ee --- /dev/null +++ b/app/lib/data/conversation_starters.dart @@ -0,0 +1,115 @@ +/// Interesse-specifikke conversation starters. +/// Vist i nudge-kortet når et match opdages. +const Map> kConversationStarters = { + 'Tech': [ + 'Hvad arbejder du på for tiden?', + 'Hvad er du mest begejstret for inden for tech lige nu?', + 'Er du mere frontend eller backend?', + ], + 'AI': [ + 'Hvad bruger du AI til i hverdagen?', + 'Har du prøvet noget nyt inden for AI for nylig?', + 'Hvad synes du om retningen AI bevæger sig i?', + ], + 'Musik': [ + 'Hvad lytter du til for tiden?', + 'Har du været til en god koncert for nylig?', + 'Hvad er din go-to genre når du skal fokusere?', + ], + 'Sport': [ + 'Hvad træner du hen imod?', + 'Hvad er din favoritsport at følge?', + 'Motionerer du mest alene eller med andre?', + ], + 'Løb': [ + 'Hvad er din næste løbsbegivenhed?', + 'Løber du mest om morgenen eller om aftenen?', + 'Hvad er det bedste løbetip du har fået?', + ], + 'Cykling': [ + 'Pendler du på cykel?', + 'Har du en drømmerute du gerne vil cykle?', + 'Road bike eller mountainbike?', + ], + 'Yoga': [ + 'Hvilken stil yoga foretrækker du?', + 'Hvornår på dagen praktiserer du helst?', + 'Hvad fik dig til at starte med yoga?', + ], + 'Madlavning': [ + 'Hvad har du lavet i køkkenet for nylig?', + 'Er der et køkken du er særligt glad for?', + 'Hvad er din bedste quick-win opskrift?', + ], + 'Rejser': [ + 'Hvad er det seneste sted du har besøgt?', + 'Har du noget på rejse-ønskesedlen?', + 'Foretrækker du by eller natur når du rejser?', + ], + 'Bøger': [ + 'Hvad læser du i øjeblikket?', + 'Hvad er den bedste bog du har læst det seneste år?', + 'Fiktion eller non-fiktion?', + ], + 'Film': [ + 'Hvad har du set for nylig der overraskede dig?', + 'Hvad er din favoritgenre?', + 'Streamer du eller går du til biograf?', + ], + 'Gaming': [ + 'Hvad spiller du mest for tiden?', + 'PC, konsol eller mobil?', + 'Hvad er det spil du har brugt flest timer på?', + ], + 'Design': [ + 'Hvad arbejder du med inden for design?', + 'Hvad inspirerer dig visuelt for tiden?', + 'Digitalt eller analogt — hvad er dit udgangspunkt?', + ], + 'Foto': [ + 'Hvad fotograferer du mest?', + 'Hvad er dit yndlingsudstyr?', + 'Film eller digital?', + ], + 'Kunst': [ + 'Hvad er du i gang med at skabe?', + 'Hvad er dit foretrukne medie?', + 'Er der noget du har set for nylig der inspirerede dig?', + ], + 'Klima': [ + 'Hvad gør du selv der giver størst effekt?', + 'Hvad er du mest optimistisk omkring inden for klima?', + 'Følger du med i en bestemt vinkel af klimadebatten?', + ], + 'Filosofi': [ + 'Hvad er du optaget af at tænke over for tiden?', + 'Er der en filosof du vender tilbage til igen og igen?', + 'Hvad er det vigtigste spørgsmål ingen kan svare på?', + ], + 'Iværksætteri': [ + 'Arbejder du på noget selv for øjeblikket?', + 'Hvad er den vigtigste ting du har lært som iværksætter?', + 'Hvad er din største udfordring lige nu?', + ], + 'Meditation': [ + 'Hvilken form for meditation praktiserer du?', + 'Hvornår på dagen fungerer det bedst for dig?', + 'Hvad fik dig til at starte?', + ], + 'Sprog': [ + 'Hvilke sprog taler du?', + 'Hvad er det sværeste ved at lære et nyt sprog?', + 'Lærer du et nyt sprog i øjeblikket?', + ], +}; + +/// Returnerer en tilfældig conversation starter for en given interesse. +/// Falder tilbage til en generisk starter hvis interessen ikke kendes. +String getStarter(String interest) { + final starters = kConversationStarters[interest]; + if (starters == null || starters.isEmpty) { + return 'Hvad arbejder du med for tiden?'; + } + final copy = List.from(starters)..shuffle(); + return copy.first; +} diff --git a/app/lib/main.dart b/app/lib/main.dart index a88bf43..58ca1b5 100644 --- a/app/lib/main.dart +++ b/app/lib/main.dart @@ -25,11 +25,13 @@ class SigHejApp extends StatelessWidget { theme: buildTheme(), darkTheme: buildTheme(dark: true), themeMode: ThemeMode.system, - home: Consumer( - builder: (context, store, _) => store.hasProfile - ? const HomeScreen() - : const ProfileScreen(isSetup: true), - ), + routes: { + '/': (context) => Consumer( + builder: (context, store, _) => store.hasProfile + ? const HomeScreen() + : const ProfileScreen(isSetup: true), + ), + }, ); } } diff --git a/app/lib/screens/home_screen.dart b/app/lib/screens/home_screen.dart index 9a0c183..f28d6d0 100644 --- a/app/lib/screens/home_screen.dart +++ b/app/lib/screens/home_screen.dart @@ -1,214 +1,303 @@ import 'dart:async'; -import 'dart:typed_data'; - +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_blue_plus/flutter_blue_plus.dart'; -import 'package:flutter_local_notifications/flutter_local_notifications.dart'; -import 'package:permission_handler/permission_handler.dart'; import 'package:provider/provider.dart'; +import 'package:sighej/data/conversation_starters.dart'; import 'package:sighej/screens/profile_screen.dart'; import 'package:sighej/services/api_service.dart'; import 'package:sighej/services/session_store.dart'; -// BLE service UUID that identifies SigHej devices. -const String kSigHejServiceUuid = '1248f5a0-0000-1000-8000-00805f9b34fb'; +bool get _bleAvailable => + !kIsWeb && + (defaultTargetPlatform == TargetPlatform.android || + defaultTargetPlatform == TargetPlatform.iOS); -// Manufacturer ID used in BLE advertising data (Android). -const int kManufacturerId = 0x4E58; // "NX" - -final FlutterLocalNotificationsPlugin _notifications = - FlutterLocalNotificationsPlugin(); - -const _androidChannel = AndroidNotificationDetails( - 'sighej_nudge', - 'SigHej Nudges', - channelDescription: 'Notifies when a nearby person shares your interests', - importance: Importance.high, - priority: Priority.high, - icon: '@mipmap/ic_launcher', -); +const kSigHejServiceUuid = '1248f5a0-0000-1000-8000-00805f9b34fb'; +const kManufacturerId = 0x4E58; class HomeScreen extends StatefulWidget { const HomeScreen({super.key}); - @override State createState() => _HomeScreenState(); } -class _HomeScreenState extends State { +class _HomeScreenState extends State + with SingleTickerProviderStateMixin { bool _openToTalk = false; String? _lastNudge; - StreamSubscription? _bleSub; - - // Track tokens we have already matched this session to avoid spam. - final Set _matchedTokens = {}; + String? _lastStarter; + late final AnimationController _pulseCtrl; + late final Animation _pulseAnim; @override void initState() { super.initState(); - _initNotifications(); - } - - Future _initNotifications() async { - const android = AndroidInitializationSettings('@mipmap/ic_launcher'); - const ios = DarwinInitializationSettings( - requestAlertPermission: true, - requestBadgePermission: true, - requestSoundPermission: true, + _pulseCtrl = AnimationController( + vsync: this, + duration: const Duration(seconds: 2), ); - await _notifications.initialize( - const InitializationSettings(android: android, iOS: ios), + _pulseAnim = Tween(begin: 1.0, end: 1.12).animate( + CurvedAnimation(parent: _pulseCtrl, curve: Curves.easeInOut), ); } @override void dispose() { - _stopScanning(); + _pulseCtrl.dispose(); super.dispose(); } Future _toggle(SessionStore store) async { if (_openToTalk) { - _stopScanning(); + _pulseCtrl.stop(); + _pulseCtrl.reset(); + setState(() { + _openToTalk = false; + _lastNudge = null; + _lastStarter = null; + }); return; } - final granted = await _requestPermissions(); - if (!granted) return; - - await registerSession( - store.bleToken, - store.interests, - name: store.name, - tagline: store.tagline, - ); - await _startAdvertising(store.bleToken); - - FlutterBluePlus.startScan(timeout: const Duration(minutes: 120)); - _bleSub = FlutterBluePlus.onScanResults.listen((results) async { - for (final r in results) { - final detected = _parseSigHejToken(r); - if (detected == null || - detected == store.bleToken || - _matchedTokens.contains(detected)) { - continue; - } - _matchedTokens.add(detected); - await _handlePotentialMatch(store.bleToken, detected, store); - } - }); + try { + await registerSession(store.bleToken, store.interests, + name: store.name, tagline: store.tagline); + } catch (_) {} + _pulseCtrl.repeat(reverse: true); setState(() => _openToTalk = true); - } - Future _startAdvertising(String token) async { - final tokenBytes = Uint8List.fromList(token.codeUnits); - try { - await FlutterBluePlus.startAdvertising( - AdvertiseData( - serviceUuids: [Guid(kSigHejServiceUuid)], - manufacturerData: [ManufacturerData(kManufacturerId, tokenBytes)], - ), - ); - } catch (e) { - debugPrint('BLE advertise error: $e'); + if (!_bleAvailable) { + await Future.delayed(const Duration(seconds: 3)); + if (mounted && _openToTalk) { + // Demo: pick a random interest from the user's list + final interests = store.interests; + final matchedInterest = + interests.isNotEmpty ? (interests..shuffle()).first : 'Tech'; + setState(() { + _lastNudge = + 'Nogen i nærheden deler din interesse for $matchedInterest'; + _lastStarter = getStarter(matchedInterest); + }); + } } } - void _stopScanning() { - FlutterBluePlus.stopScan(); - FlutterBluePlus.stopAdvertising(); - _bleSub?.cancel(); - _bleSub = null; - _matchedTokens.clear(); - setState(() { - _openToTalk = false; - _lastNudge = null; - }); - } - - Future _handlePotentialMatch( - String own, String detected, SessionStore store) async { - try { - final result = await reportMatch(own, detected); - if (result == null || !result.match) return; - - final interests = result.sharedInterests; - final body = interests.isEmpty - ? '${store.displayName} — nogen i nærheden er åben for en snak!' - : 'Fælles interesser: ${interests.join(', ')}'; - - await _showNudgeNotification('SigHej — sig hej!', body); - setState(() => _lastNudge = body); - } catch (e) { - debugPrint('Match error: $e'); - } - } - - Future _showNudgeNotification(String title, String body) async { - const details = NotificationDetails(android: _androidChannel); - await _notifications.show( - body.hashCode, - title, - body, - details, - ); - } - - /// Parses a SigHej BLE token from a scan result. - /// Android: reads manufacturer data with key [kManufacturerId]. - /// iOS fallback: reads service data for [kSigHejServiceUuid]. - String? _parseSigHejToken(ScanResult result) { - final ad = result.advertisementData; - - // Filter: only process SigHej advertisers. - final hasSigHejUuid = ad.serviceUuids - .map((g) => g.toString().toLowerCase()) - .contains(kSigHejServiceUuid.toLowerCase()); - if (!hasSigHejUuid) return null; - - // Android: manufacturer data. - final mfr = ad.manufacturerData[kManufacturerId]; - if (mfr != null && mfr.isNotEmpty) { - try { - return String.fromCharCodes(mfr); - } catch (_) {} - } - - // iOS fallback: service data. - final svcData = ad.serviceData[Guid(kSigHejServiceUuid)]; - if (svcData != null && svcData.isNotEmpty) { - try { - return String.fromCharCodes(svcData); - } catch (_) {} - } - - return null; - } - - Future _requestPermissions() async { - final perms = [ - Permission.bluetooth, - Permission.bluetoothScan, - Permission.bluetoothAdvertise, - Permission.bluetoothConnect, - Permission.location, - Permission.notification, - ]; - final statuses = await perms.request(); - return statuses.values.every((s) => s.isGranted || s.isLimited); - } - @override Widget build(BuildContext context) { final store = context.watch(); + final cs = Theme.of(context).colorScheme; + final isDark = Theme.of(context).brightness == Brightness.dark; return Scaffold( - appBar: AppBar( - title: const Text('SigHej'), - actions: [ + backgroundColor: cs.surface, + body: Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 440), + child: Column( + children: [ + // ── Top bar ────────────────────────────────────────── + _TopBar(store: store), + + // ── Main area ──────────────────────────────────────── + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 24), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Spacer(flex: 2), + + // Status text + AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + child: Text( + _openToTalk + ? 'Du er åben for en snak' + : 'Tryk for at åbne op', + key: ValueKey(_openToTalk), + style: Theme.of(context) + .textTheme + .headlineSmall + ?.copyWith( + fontWeight: FontWeight.w700, + color: cs.onSurface), + textAlign: TextAlign.center, + ), + ), + const SizedBox(height: 8), + AnimatedSwitcher( + duration: const Duration(milliseconds: 300), + child: Text( + _openToTalk + ? 'Søger efter folk med fælles interesser…' + : 'Lad andre vide du er klar til en samtale', + key: ValueKey('sub$_openToTalk'), + style: Theme.of(context) + .textTheme + .bodyMedium + ?.copyWith(color: cs.onSurfaceVariant), + textAlign: TextAlign.center, + ), + ), + + const SizedBox(height: 48), + + // ── Big toggle button ───────────────────── + GestureDetector( + onTap: () => _toggle(store), + child: ScaleTransition( + scale: _openToTalk ? _pulseAnim : const AlwaysStoppedAnimation(1.0), + child: Stack( + alignment: Alignment.center, + children: [ + // Outer glow ring + if (_openToTalk) + Container( + width: 200, + height: 200, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: cs.primary.withAlpha(30), + ), + ), + // Main circle + Container( + width: 168, + height: 168, + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: _openToTalk + ? LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + cs.primary, + Color.lerp(cs.primary, + cs.secondary, 0.5)!, + ], + ) + : LinearGradient( + colors: [ + cs.surfaceContainerHighest, + cs.surfaceContainerHighest, + ], + ), + boxShadow: _openToTalk + ? [ + BoxShadow( + color: cs.primary.withAlpha(100), + blurRadius: 32, + spreadRadius: 4, + ) + ] + : [ + BoxShadow( + color: isDark + ? Colors.black45 + : Colors.black12, + blurRadius: 16, + offset: const Offset(0, 4), + ) + ], + ), + child: Icon( + _openToTalk + ? Icons.waving_hand_rounded + : Icons.waving_hand_outlined, + size: 68, + color: _openToTalk + ? cs.onPrimary + : cs.onSurfaceVariant, + ), + ), + ], + ), + ), + ), + + const SizedBox(height: 40), + + // ── Nudge card ──────────────────────────── + AnimatedSwitcher( + duration: const Duration(milliseconds: 400), + child: _lastNudge != null + ? _NudgeCard( + message: _lastNudge!, + starter: _lastStarter, + ) + : const SizedBox(height: 80), + ), + + const Spacer(flex: 3), + + // ── Interest chips ──────────────────────── + if (store.interests.isNotEmpty) + _InterestRow(interests: store.interests), + + const SizedBox(height: 32), + ], + ), + ), + ), + ], + ), + ), + ), + ); + } +} + +class _TopBar extends StatelessWidget { + final SessionStore store; + const _TopBar({required this.store}); + + @override + Widget build(BuildContext context) { + final isDark = Theme.of(context).brightness == Brightness.dark; + final top = MediaQuery.of(context).padding.top; + + return Container( + padding: EdgeInsets.fromLTRB(20, top + 12, 12, 16), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: isDark + ? [const Color(0xFF1A3A40), const Color(0xFF0F2930)] + : [const Color(0xFF2A9D8F), const Color(0xFF264653)], + ), + ), + child: Row( + children: [ + const Icon(Icons.waving_hand, color: Colors.white, size: 22), + const SizedBox(width: 8), + const Text('SigHej', + style: TextStyle( + color: Colors.white, + fontSize: 20, + fontWeight: FontWeight.w800, + letterSpacing: -0.5)), + const Spacer(), + // Display name chip + Container( + padding: + const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: Colors.white.withAlpha(25), + borderRadius: BorderRadius.circular(20), + ), + child: Text( + store.displayName, + style: const TextStyle( + color: Colors.white, + fontSize: 13, + fontWeight: FontWeight.w600), + ), + ), + const SizedBox(width: 4), IconButton( - icon: const Icon(Icons.person_outline), + icon: const Icon(Icons.person_outline, color: Colors.white), onPressed: () => Navigator.of(context).push( MaterialPageRoute(builder: (_) => const ProfileScreen()), ), @@ -216,51 +305,130 @@ class _HomeScreenState extends State { ), ], ), - body: Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - _openToTalk ? 'You\'re open to talk' : 'Tap to open up', - style: Theme.of(context).textTheme.headlineSmall, + ); + } +} + +class _NudgeCard extends StatelessWidget { + final String message; + final String? starter; + const _NudgeCard({required this.message, this.starter}); + + @override + Widget build(BuildContext context) { + final cs = Theme.of(context).colorScheme; + return Container( + width: double.infinity, + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [cs.primaryContainer, cs.tertiaryContainer], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(20), + boxShadow: [ + BoxShadow( + color: cs.primary.withAlpha(40), + blurRadius: 16, + offset: const Offset(0, 4), + ), + ], + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 44, + height: 44, + decoration: BoxDecoration( + color: cs.primary.withAlpha(30), + borderRadius: BorderRadius.circular(12), ), - const SizedBox(height: 32), - GestureDetector( - onTap: () => _toggle(store), - child: AnimatedContainer( - duration: const Duration(milliseconds: 300), - width: 150, - height: 150, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: _openToTalk - ? Theme.of(context).colorScheme.primary - : Theme.of(context).colorScheme.surfaceContainerHighest, - ), - child: Icon( - _openToTalk ? Icons.record_voice_over : Icons.mic_off, - size: 60, - color: _openToTalk - ? Theme.of(context).colorScheme.onPrimary - : Theme.of(context).colorScheme.onSurfaceVariant, - ), - ), - ), - if (_lastNudge != null) ...[ - const SizedBox(height: 40), - Card( - margin: const EdgeInsets.symmetric(horizontal: 24), - child: Padding( - padding: const EdgeInsets.all(16), - child: Text( - _lastNudge!, - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.bodyLarge, + child: + Icon(Icons.handshake_outlined, color: cs.primary, size: 24), + ), + const SizedBox(width: 14), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('Nogen i nærheden! 👋', + style: TextStyle( + fontWeight: FontWeight.w700, + fontSize: 14, + color: cs.onPrimaryContainer)), + const SizedBox(height: 3), + Text(message, + style: TextStyle( + fontSize: 13, + color: cs.onPrimaryContainer.withAlpha(200))), + if (starter != null) ...[ + const SizedBox(height: 10), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 10, vertical: 7), + decoration: BoxDecoration( + color: cs.primary.withAlpha(20), + borderRadius: BorderRadius.circular(10), + border: Border.all( + color: cs.primary.withAlpha(60), width: 1), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('💬 ', + style: TextStyle(fontSize: 13)), + Expanded( + child: Text( + '"$starter"', + style: TextStyle( + fontSize: 13, + fontStyle: FontStyle.italic, + color: cs.onPrimaryContainer, + height: 1.4, + ), + ), + ), + ], + ), ), - ), - ), - ], - ], + ], + ], + ), + ), + ], + ), + ); + } +} + +class _InterestRow extends StatelessWidget { + final List interests; + const _InterestRow({required this.interests}); + + @override + Widget build(BuildContext context) { + final cs = Theme.of(context).colorScheme; + return SizedBox( + height: 32, + child: ListView.separated( + scrollDirection: Axis.horizontal, + itemCount: interests.length, + separatorBuilder: (_, __) => const SizedBox(width: 6), + itemBuilder: (_, i) => Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + decoration: BoxDecoration( + color: cs.secondaryContainer, + borderRadius: BorderRadius.circular(16), + ), + child: Text( + interests[i], + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: cs.onSecondaryContainer), + ), ), ), ); diff --git a/app/lib/screens/profile_screen.dart b/app/lib/screens/profile_screen.dart index 25b1d38..12e4b5e 100644 --- a/app/lib/screens/profile_screen.dart +++ b/app/lib/screens/profile_screen.dart @@ -2,34 +2,16 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:sighej/services/session_store.dart'; -const List kAvailableInterests = [ - 'Tech', - 'Musik', - 'Filosofi', - 'Design', - 'DevOps', - 'Bøger', - 'Gaming', - 'Fitness', - 'Kunst', - 'Mad', - 'Rejser', - 'Videnskab', - 'Iværksætteri', - 'Film', - 'Natur', - 'Kodning', - 'Podcast', - 'Arkitektur', - 'Klima', - 'Sport', +const kAvailableInterests = [ + 'Tech', 'Musik', 'Filosofi', 'Design', 'DevOps', 'Bøger', 'Gaming', + 'Fitness', 'Kunst', 'Mad', 'Rejser', 'Videnskab', 'Iværksætteri', + 'Film', 'Natur', 'Kodning', 'Podcast', 'Arkitektur', 'Klima', 'Sport', ]; class ProfileScreen extends StatefulWidget { - /// If [isSetup] is true, the screen is shown as first-run onboarding. - /// If false, it's opened from the home screen as "rediger profil". + /// [isSetup] = true → first-run onboarding (no back button) + /// [isSetup] = false → edit mode from HomeScreen final bool isSetup; - const ProfileScreen({super.key, this.isSetup = false}); @override @@ -37,17 +19,22 @@ class ProfileScreen extends StatefulWidget { } class _ProfileScreenState extends State { - late final TextEditingController _nameCtrl; - late final TextEditingController _taglineCtrl; - late final Set _selected; + final _nameCtrl = TextEditingController(); + final _taglineCtrl = TextEditingController(); + final Set _selected = {}; + bool _triedSubmit = false; + bool _loaded = false; @override - void initState() { - super.initState(); - final store = context.read(); - _nameCtrl = TextEditingController(text: store.name); - _taglineCtrl = TextEditingController(text: store.tagline); - _selected = Set.from(store.interests); + void didChangeDependencies() { + super.didChangeDependencies(); + if (!_loaded) { + final store = context.read(); + _nameCtrl.text = store.name; + _taglineCtrl.text = store.tagline; + _selected.addAll(store.interests); + _loaded = true; + } } @override @@ -58,144 +45,337 @@ class _ProfileScreenState extends State { } Future _save() async { - await context.read().saveProfile( - name: _nameCtrl.text.trim(), - tagline: _taglineCtrl.text.trim(), - interests: _selected.toList(), - ); - if (mounted) Navigator.of(context).pop(); + setState(() => _triedSubmit = true); + if (_selected.isEmpty) return; + final store = context.read(); + await store.saveProfile( + name: _nameCtrl.text.trim(), + tagline: _taglineCtrl.text.trim(), + interests: _selected.toList(), + ); + if (!mounted) return; + if (widget.isSetup) { + Navigator.of(context).pushReplacement( + MaterialPageRoute(builder: (_) => const _HomeRedirect()), + ); + } else { + Navigator.of(context).pop(); + } } @override Widget build(BuildContext context) { - final isSetup = widget.isSetup; + final cs = Theme.of(context).colorScheme; return Scaffold( - appBar: AppBar( - title: Text(isSetup ? 'Hvem er du?' : 'Din profil'), - automaticallyImplyLeading: !isSetup, - ), - body: SafeArea( - child: Column( - children: [ - Expanded( - child: SingleChildScrollView( - padding: const EdgeInsets.all(24), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (isSetup) ...[ - Text( - 'SigHej sender en diskret notifikation, når nogen i nærheden deler dine interesser. ' - 'Ingen profiler at swipe — bare et lille vink om at starte en samtale.', - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), + backgroundColor: cs.surface, + body: Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 440), + child: Column( + children: [ + // ── Gradient header ────────────────────────────────── + _Header(isSetup: widget.isSetup), + + // ── Scrollable form body ───────────────────────────── + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.fromLTRB(24, 24, 24, 0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildTextField( + context, + label: 'Kaldenavn', + hint: 'F.eks. "Henrik" eller "Tech-nerd"', + sublabel: 'Valgfrit', + controller: _nameCtrl, + maxLength: 40, + ), + const SizedBox(height: 20), + _buildTextField( + context, + label: 'Hvad er du op til i dag?', + hint: '"Åben for en kaffesnak" eller "Klar til at netværke"', + sublabel: 'Valgfrit — sæt tonen', + controller: _taglineCtrl, + maxLength: 80, ), const SizedBox(height: 28), - ], - _SectionLabel('Kaldenavn', hint: 'Valgfrit — hvad vil du kaldes?'), - const SizedBox(height: 8), - TextField( - controller: _nameCtrl, - maxLength: 40, - textCapitalization: TextCapitalization.sentences, - decoration: const InputDecoration( - hintText: 'F.eks. "Henrik" eller "Tech-nerd"', - border: OutlineInputBorder(), - counterText: '', - ), - ), - const SizedBox(height: 24), - _SectionLabel( - 'Hvad er du op til i dag?', - hint: 'Valgfrit — sæt tonen', - ), - const SizedBox(height: 8), - TextField( - controller: _taglineCtrl, - maxLength: 80, - textCapitalization: TextCapitalization.sentences, - decoration: const InputDecoration( - hintText: 'F.eks. "Åben for en kaffesnak" eller "Op til at netværke"', - border: OutlineInputBorder(), - counterText: '', - ), - ), - const SizedBox(height: 24), - _SectionLabel( - 'Hvad interesserer dig?', - hint: 'Vi finder folk med fælles interesser i nærheden', - ), - const SizedBox(height: 12), - Wrap( - spacing: 8, - runSpacing: 8, - children: kAvailableInterests.map((interest) { - final selected = _selected.contains(interest); - return FilterChip( - label: Text(interest), - selected: selected, - onSelected: (val) => setState( - () => val - ? _selected.add(interest) - : _selected.remove(interest), + Row(children: [ + Text('Interesser', + style: Theme.of(context) + .textTheme + .titleMedium + ?.copyWith( + fontWeight: FontWeight.w700, + color: cs.onSurface)), + const SizedBox(width: 8), + Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, vertical: 2), + decoration: BoxDecoration( + color: _selected.isEmpty && _triedSubmit + ? cs.errorContainer + : cs.primaryContainer, + borderRadius: BorderRadius.circular(20), + ), + child: Text( + '${_selected.length} valgt', + style: TextStyle( + fontSize: 11, + fontWeight: FontWeight.w600, + color: _selected.isEmpty && _triedSubmit + ? cs.onErrorContainer + : cs.onPrimaryContainer, + ), ), - ); - }).toList(), - ), - const SizedBox(height: 16), - if (_selected.isEmpty) - Text( - 'Vælg mindst ét emne for at bruge SigHej.', - style: TextStyle( - color: Theme.of(context).colorScheme.error, - fontSize: 13, ), + ]), + const SizedBox(height: 4), + Text('Vælg mindst én', + style: Theme.of(context) + .textTheme + .bodySmall + ?.copyWith(color: cs.onSurfaceVariant)), + const SizedBox(height: 12), + Wrap( + spacing: 8, + runSpacing: 8, + children: kAvailableInterests.map((tag) { + final on = _selected.contains(tag); + return _InterestChip( + label: tag, + selected: on, + onTap: () => setState(() => + on ? _selected.remove(tag) : _selected.add(tag)), + ); + }).toList(), ), - ], + if (_triedSubmit && _selected.isEmpty) ...[ + const SizedBox(height: 8), + Text('Vælg mindst ét emne for at bruge SigHej.', + style: TextStyle( + color: cs.error, fontSize: 12)), + ], + const SizedBox(height: 32), + ], + ), ), ), - ), - Padding( - padding: const EdgeInsets.fromLTRB(24, 8, 24, 24), - child: FilledButton( - onPressed: _selected.isEmpty ? null : _save, - style: FilledButton.styleFrom( - minimumSize: const Size.fromHeight(52), + + // ── Save button ────────────────────────────────────── + Padding( + padding: const EdgeInsets.fromLTRB(24, 12, 24, 32), + child: SizedBox( + width: double.infinity, + height: 54, + child: FilledButton( + onPressed: _save, + style: FilledButton.styleFrom( + backgroundColor: cs.primary, + foregroundColor: cs.onPrimary, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16)), + ), + child: Text( + widget.isSetup ? 'Kom i gang →' : 'Gem profil', + style: const TextStyle( + fontSize: 16, fontWeight: FontWeight.w700), + ), + ), ), - child: Text(isSetup ? 'Kom i gang' : 'Gem'), + ), + ], + ), + ), + ), + ); + } + + Widget _buildTextField( + BuildContext context, { + required String label, + required String hint, + required String sublabel, + required TextEditingController controller, + required int maxLength, + }) { + final cs = Theme.of(context).colorScheme; + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(label, + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w700, color: cs.onSurface)), + const SizedBox(height: 2), + Text(sublabel, + style: Theme.of(context) + .textTheme + .bodySmall + ?.copyWith(color: cs.onSurfaceVariant)), + const SizedBox(height: 8), + TextField( + controller: controller, + maxLength: maxLength, + textCapitalization: TextCapitalization.sentences, + style: TextStyle(color: cs.onSurface), + decoration: InputDecoration( + hintText: hint, + hintStyle: TextStyle(color: cs.onSurfaceVariant.withAlpha(160)), + counterText: '', + filled: true, + fillColor: cs.surfaceContainerHighest.withAlpha(120), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: cs.outline.withAlpha(80)), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: cs.outline.withAlpha(80)), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(12), + borderSide: BorderSide(color: cs.primary, width: 2), + ), + contentPadding: + const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + ), + ), + ], + ); + } +} + +class _Header extends StatelessWidget { + final bool isSetup; + const _Header({required this.isSetup}); + + @override + Widget build(BuildContext context) { + final isDark = Theme.of(context).brightness == Brightness.dark; + + return Container( + width: double.infinity, + padding: EdgeInsets.fromLTRB( + 24, MediaQuery.of(context).padding.top + 32, 24, 32), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: isDark + ? [const Color(0xFF1A3A40), const Color(0xFF0F2930)] + : [const Color(0xFF2A9D8F), const Color(0xFF264653)], + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (!isSetup) + IconButton( + icon: const Icon(Icons.arrow_back, color: Colors.white), + onPressed: () => Navigator.of(context).pop(), + padding: EdgeInsets.zero, + ), + Row( + children: [ + Container( + width: 48, + height: 48, + decoration: BoxDecoration( + color: Colors.white.withAlpha(30), + borderRadius: BorderRadius.circular(14), + ), + child: const Icon(Icons.waving_hand, + color: Colors.white, size: 26), + ), + const SizedBox(width: 12), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('SigHej', + style: TextStyle( + color: Colors.white, + fontSize: 22, + fontWeight: FontWeight.w800, + letterSpacing: -0.5)), + Text( + isSetup ? 'Fortæl lidt om dig selv' : 'Rediger profil', + style: TextStyle( + color: Colors.white.withAlpha(180), fontSize: 13), + ), + ], + ), + ], + ), + if (isSetup) ...[ + const SizedBox(height: 20), + Container( + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + color: Colors.white.withAlpha(20), + borderRadius: BorderRadius.circular(12), + ), + child: Text( + 'SigHej sender en diskret notifikation, når nogen i nærheden deler dine interesser. Ingen profiler at swipe — bare et lille vink om at starte en samtale.', + style: TextStyle( + color: Colors.white.withAlpha(210), + fontSize: 13, + height: 1.5), ), ), ], + ], + ), + ); + } +} + +class _InterestChip extends StatelessWidget { + final String label; + final bool selected; + final VoidCallback onTap; + const _InterestChip( + {required this.label, required this.selected, required this.onTap}); + + @override + Widget build(BuildContext context) { + final cs = Theme.of(context).colorScheme; + return GestureDetector( + onTap: onTap, + child: AnimatedContainer( + duration: const Duration(milliseconds: 180), + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8), + decoration: BoxDecoration( + color: selected ? cs.primary : cs.surfaceContainerHighest, + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: selected ? cs.primary : cs.outline.withAlpha(60), + width: selected ? 0 : 1, + ), + ), + child: Text( + label, + style: TextStyle( + fontSize: 13, + fontWeight: selected ? FontWeight.w600 : FontWeight.w400, + color: selected ? cs.onPrimary : cs.onSurface, + ), ), ), ); } } -class _SectionLabel extends StatelessWidget { - final String label; - final String? hint; - - const _SectionLabel(this.label, {this.hint}); - +// Redirects to HomeScreen after profile save — avoids circular import. +class _HomeRedirect extends StatelessWidget { + const _HomeRedirect(); @override Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(label, style: Theme.of(context).textTheme.titleSmall), - if (hint != null) ...[ - const SizedBox(height: 2), - Text( - hint!, - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), - ), - ], - ], - ); + // Defer to main.dart routing which will now see hasProfile=true. + WidgetsBinding.instance.addPostFrameCallback((_) { + Navigator.of(context).pushNamedAndRemoveUntil('/', (_) => false); + }); + return const Scaffold(body: Center(child: CircularProgressIndicator())); } } diff --git a/app/lib/services/session_store.dart b/app/lib/services/session_store.dart index 0c72162..d3eb945 100644 --- a/app/lib/services/session_store.dart +++ b/app/lib/services/session_store.dart @@ -1,5 +1,3 @@ -import 'dart:convert'; - import 'package:shared_preferences/shared_preferences.dart'; import 'package:flutter/foundation.dart'; import 'package:uuid/uuid.dart'; diff --git a/app/macos/.gitignore b/app/macos/.gitignore new file mode 100644 index 0000000..746adbb --- /dev/null +++ b/app/macos/.gitignore @@ -0,0 +1,7 @@ +# Flutter-related +**/Flutter/ephemeral/ +**/Pods/ + +# Xcode-related +**/dgph +**/xcuserdata/ diff --git a/app/macos/Flutter/Flutter-Debug.xcconfig b/app/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 0000000..c2efd0b --- /dev/null +++ b/app/macos/Flutter/Flutter-Debug.xcconfig @@ -0,0 +1 @@ +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/app/macos/Flutter/Flutter-Release.xcconfig b/app/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 0000000..c2efd0b --- /dev/null +++ b/app/macos/Flutter/Flutter-Release.xcconfig @@ -0,0 +1 @@ +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/app/macos/Flutter/GeneratedPluginRegistrant.swift b/app/macos/Flutter/GeneratedPluginRegistrant.swift new file mode 100644 index 0000000..7b9bdcf --- /dev/null +++ b/app/macos/Flutter/GeneratedPluginRegistrant.swift @@ -0,0 +1,16 @@ +// +// Generated file. Do not edit. +// + +import FlutterMacOS +import Foundation + +import flutter_blue_plus_darwin +import flutter_local_notifications +import shared_preferences_foundation + +func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + FlutterBluePlusPlugin.register(with: registry.registrar(forPlugin: "FlutterBluePlusPlugin")) + FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin")) + SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) +} diff --git a/app/macos/Runner.xcodeproj/project.pbxproj b/app/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 0000000..ffe05ab --- /dev/null +++ b/app/macos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,705 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 54; + objects = { + +/* Begin PBXAggregateTarget section */ + 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; + buildPhases = ( + 33CC111E2044C6BF0003C045 /* ShellScript */, + ); + dependencies = ( + ); + name = "Flutter Assemble"; + productName = FLX; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; }; + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC10EC2044A3C60003C045; + remoteInfo = Runner; + }; + 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC111A2044C6BA0003C045; + remoteInfo = FLX; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 33CC110E2044A8840003C045 /* Bundle Framework */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Bundle Framework"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; + 33CC10ED2044A3C60003C045 /* sighej.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "sighej.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; + 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; + 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; + 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; + 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; + 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 331C80D2294CF70F00263BE5 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 331C80D6294CF71000263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C80D7294CF71000263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 33BA886A226E78AF003329D5 /* Configs */ = { + isa = PBXGroup; + children = ( + 33E5194F232828860026EE4D /* AppInfo.xcconfig */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, + ); + path = Configs; + sourceTree = ""; + }; + 33CC10E42044A3C60003C045 = { + isa = PBXGroup; + children = ( + 33FAB671232836740065AC1E /* Runner */, + 33CEB47122A05771004F2AC0 /* Flutter */, + 331C80D6294CF71000263BE5 /* RunnerTests */, + 33CC10EE2044A3C60003C045 /* Products */, + D73912EC22F37F3D000D13A0 /* Frameworks */, + ); + sourceTree = ""; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* sighej.app */, + 331C80D5294CF71000263BE5 /* RunnerTests.xctest */, + ); + name = Products; + sourceTree = ""; + }; + 33CC11242044D66E0003C045 /* Resources */ = { + isa = PBXGroup; + children = ( + 33CC10F22044A3C60003C045 /* Assets.xcassets */, + 33CC10F42044A3C60003C045 /* MainMenu.xib */, + 33CC10F72044A3C60003C045 /* Info.plist */, + ); + name = Resources; + path = ..; + sourceTree = ""; + }; + 33CEB47122A05771004F2AC0 /* Flutter */ = { + isa = PBXGroup; + children = ( + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, + ); + path = Flutter; + sourceTree = ""; + }; + 33FAB671232836740065AC1E /* Runner */ = { + isa = PBXGroup; + children = ( + 33CC10F02044A3C60003C045 /* AppDelegate.swift */, + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, + 33E51913231747F40026EE4D /* DebugProfile.entitlements */, + 33E51914231749380026EE4D /* Release.entitlements */, + 33CC11242044D66E0003C045 /* Resources */, + 33BA886A226E78AF003329D5 /* Configs */, + ); + path = Runner; + sourceTree = ""; + }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + ); + name = Frameworks; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 331C80D4294CF70F00263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 331C80D1294CF70F00263BE5 /* Sources */, + 331C80D2294CF70F00263BE5 /* Frameworks */, + 331C80D3294CF70F00263BE5 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 331C80DA294CF71000263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; + 33CC10EC2044A3C60003C045 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + ); + buildRules = ( + ); + dependencies = ( + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* sighej.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 33CC10E52044A3C60003C045 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = YES; + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 331C80D4294CF70F00263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 33CC10EC2044A3C60003C045; + }; + 33CC10EC2044A3C60003C045 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 1; + }; + }; + }; + 33CC111A2044C6BA0003C045 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 33CC10E42044A3C60003C045; + productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 33CC10EC2044A3C60003C045 /* Runner */, + 331C80D4294CF70F00263BE5 /* RunnerTests */, + 33CC111A2044C6BA0003C045 /* Flutter Assemble */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 331C80D3294CF70F00263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10EB2044A3C60003C045 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3399D490228B24CF009A79C7 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; + }; + 33CC111E2044C6BF0003C045 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + Flutter/ephemeral/FlutterInputs.xcfilelist, + ); + inputPaths = ( + Flutter/ephemeral/tripwire, + ); + outputFileListPaths = ( + Flutter/ephemeral/FlutterOutputs.xcfilelist, + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 331C80D1294CF70F00263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; + 33CC10E92044A3C60003C045 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 331C80DA294CF71000263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC10EC2044A3C60003C045 /* Runner */; + targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */; + }; + 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; + targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + 33CC10F52044A3C60003C045 /* Base */, + ); + name = MainMenu.xib; + path = Runner; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 331C80DB294CF71000263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.sighej.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/sighej.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/sighej"; + }; + name = Debug; + }; + 331C80DC294CF71000263BE5 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.sighej.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/sighej.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/sighej"; + }; + name = Release; + }; + 331C80DD294CF71000263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.sighej.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/sighej.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/sighej"; + }; + name = Profile; + }; + 338D0CE9231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + 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_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + 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_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Profile; + }; + 338D0CEA231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Profile; + }; + 338D0CEB231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Profile; + }; + 33CC10F92044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + 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_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + 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_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + 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_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 33CC10FA2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + 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_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + 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_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.15; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 33CC10FC2044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 33CC10FD2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 33CC111C2044C6BA0003C045 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 33CC111D2044C6BA0003C045 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C80DB294CF71000263BE5 /* Debug */, + 331C80DC294CF71000263BE5 /* Release */, + 331C80DD294CF71000263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10F92044A3C60003C045 /* Debug */, + 33CC10FA2044A3C60003C045 /* Release */, + 338D0CE9231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10FC2044A3C60003C045 /* Debug */, + 33CC10FD2044A3C60003C045 /* Release */, + 338D0CEA231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC111C2044C6BA0003C045 /* Debug */, + 33CC111D2044C6BA0003C045 /* Release */, + 338D0CEB231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 33CC10E52044A3C60003C045 /* Project object */; +} diff --git a/app/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/app/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/app/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/app/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/app/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 0000000..d095593 --- /dev/null +++ b/app/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,99 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/macos/Runner.xcworkspace/contents.xcworkspacedata b/app/macos/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..1d526a1 --- /dev/null +++ b/app/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/app/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/app/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/app/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/app/macos/Runner/AppDelegate.swift b/app/macos/Runner/AppDelegate.swift new file mode 100644 index 0000000..b3c1761 --- /dev/null +++ b/app/macos/Runner/AppDelegate.swift @@ -0,0 +1,13 @@ +import Cocoa +import FlutterMacOS + +@main +class AppDelegate: FlutterAppDelegate { + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + } + + override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { + return true + } +} diff --git a/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..a2ec33f --- /dev/null +++ b/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_16.png", + "scale" : "1x" + }, + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "2x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "1x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_64.png", + "scale" : "2x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_128.png", + "scale" : "1x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "2x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "1x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "2x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "1x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_1024.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png new file mode 100644 index 0000000..82b6f9d Binary files /dev/null and b/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ diff --git a/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png new file mode 100644 index 0000000..13b35eb Binary files /dev/null and b/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png differ diff --git a/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png new file mode 100644 index 0000000..0a3f5fa Binary files /dev/null and b/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png differ diff --git a/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png new file mode 100644 index 0000000..bdb5722 Binary files /dev/null and b/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png differ diff --git a/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png new file mode 100644 index 0000000..f083318 Binary files /dev/null and b/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png differ diff --git a/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png new file mode 100644 index 0000000..326c0e7 Binary files /dev/null and b/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png differ diff --git a/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png new file mode 100644 index 0000000..2f1632c Binary files /dev/null and b/app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png differ diff --git a/app/macos/Runner/Base.lproj/MainMenu.xib b/app/macos/Runner/Base.lproj/MainMenu.xib new file mode 100644 index 0000000..80e867a --- /dev/null +++ b/app/macos/Runner/Base.lproj/MainMenu.xib @@ -0,0 +1,343 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/macos/Runner/Configs/AppInfo.xcconfig b/app/macos/Runner/Configs/AppInfo.xcconfig new file mode 100644 index 0000000..6191926 --- /dev/null +++ b/app/macos/Runner/Configs/AppInfo.xcconfig @@ -0,0 +1,14 @@ +// Application-level settings for the Runner target. +// +// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the +// future. If not, the values below would default to using the project name when this becomes a +// 'flutter create' template. + +// The application's name. By default this is also the title of the Flutter window. +PRODUCT_NAME = sighej + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = com.example.sighej + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright © 2026 com.example. All rights reserved. diff --git a/app/macos/Runner/Configs/Debug.xcconfig b/app/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 0000000..36b0fd9 --- /dev/null +++ b/app/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/app/macos/Runner/Configs/Release.xcconfig b/app/macos/Runner/Configs/Release.xcconfig new file mode 100644 index 0000000..dff4f49 --- /dev/null +++ b/app/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/app/macos/Runner/Configs/Warnings.xcconfig b/app/macos/Runner/Configs/Warnings.xcconfig new file mode 100644 index 0000000..42bcbf4 --- /dev/null +++ b/app/macos/Runner/Configs/Warnings.xcconfig @@ -0,0 +1,13 @@ +WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings +GCC_WARN_UNDECLARED_SELECTOR = YES +CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN_PRAGMA_PACK = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_COMMA = YES +GCC_WARN_STRICT_SELECTOR_MATCH = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +GCC_WARN_SHADOW = YES +CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/app/macos/Runner/DebugProfile.entitlements b/app/macos/Runner/DebugProfile.entitlements new file mode 100644 index 0000000..dddb8a3 --- /dev/null +++ b/app/macos/Runner/DebugProfile.entitlements @@ -0,0 +1,12 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.network.server + + + diff --git a/app/macos/Runner/Info.plist b/app/macos/Runner/Info.plist new file mode 100644 index 0000000..4789daa --- /dev/null +++ b/app/macos/Runner/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + $(PRODUCT_COPYRIGHT) + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/app/macos/Runner/MainFlutterWindow.swift b/app/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 0000000..3cc05eb --- /dev/null +++ b/app/macos/Runner/MainFlutterWindow.swift @@ -0,0 +1,15 @@ +import Cocoa +import FlutterMacOS + +class MainFlutterWindow: NSWindow { + override func awakeFromNib() { + let flutterViewController = FlutterViewController() + let windowFrame = self.frame + self.contentViewController = flutterViewController + self.setFrame(windowFrame, display: true) + + RegisterGeneratedPlugins(registry: flutterViewController) + + super.awakeFromNib() + } +} diff --git a/app/macos/Runner/Release.entitlements b/app/macos/Runner/Release.entitlements new file mode 100644 index 0000000..852fa1a --- /dev/null +++ b/app/macos/Runner/Release.entitlements @@ -0,0 +1,8 @@ + + + + + com.apple.security.app-sandbox + + + diff --git a/app/macos/RunnerTests/RunnerTests.swift b/app/macos/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..61f3bd1 --- /dev/null +++ b/app/macos/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Cocoa +import FlutterMacOS +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} diff --git a/app/test/widget_test.dart b/app/test/widget_test.dart new file mode 100644 index 0000000..91ab0f6 --- /dev/null +++ b/app/test/widget_test.dart @@ -0,0 +1,18 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:provider/provider.dart'; +import 'package:sighej/main.dart'; +import 'package:sighej/services/session_store.dart'; + +void main() { + testWidgets('App renders SigHej title', (WidgetTester tester) async { + await tester.pumpWidget( + ChangeNotifierProvider( + create: (_) => SessionStore(), + child: const SigHejApp(), + ), + ); + // Profile screen shows on first run — check it loads without crash. + expect(find.byType(MaterialApp), findsOneWidget); + }); +} diff --git a/app/web/favicon.png b/app/web/favicon.png new file mode 100644 index 0000000..8aaa46a Binary files /dev/null and b/app/web/favicon.png differ diff --git a/app/web/index.html b/app/web/index.html new file mode 100644 index 0000000..385fda6 --- /dev/null +++ b/app/web/index.html @@ -0,0 +1,46 @@ + + + + + + + + + + + + + + + + + + + + sighej + + + + + + + diff --git a/app/web/manifest.json b/app/web/manifest.json new file mode 100644 index 0000000..121602b --- /dev/null +++ b/app/web/manifest.json @@ -0,0 +1,35 @@ +{ + "name": "sighej", + "short_name": "sighej", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A new Flutter project.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "icons/Icon-maskable-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "maskable" + }, + { + "src": "icons/Icon-maskable-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ] +} diff --git a/note b/note new file mode 100644 index 0000000..9c4441e --- /dev/null +++ b/note @@ -0,0 +1,2 @@ +The Thinking Game + diff --git a/prompt.md b/prompt.md new file mode 100644 index 0000000..033322d --- /dev/null +++ b/prompt.md @@ -0,0 +1,213 @@ +# SigHej — AI Session Prompt + +> Paste this file at the start of a new AI coding session to get full project context. + +--- + +## Hvad er SigHej? + +SigHej er et **nudge-værktøj** til fysiske samtaler i den virkelige verden. + +Idéen er simpel: folk i samme rum kan have interesser til fælles uden at vide det. +SigHej bruger BLE (Bluetooth Low Energy) til at opdage nærliggende brugere, tjekker om der er fælles interesser, og sender et diskret nudge til begge parter — *"nogen i nærheden deler din interesse for X — sig hej"*. Det er det. Ingen chat, ingen profiler, ingen social graph. Kun et lille skub til en rigtig samtale. + +**Kerneprincipper:** +- Ingen beskedudveksling — nudget er produktet, ikke en chat-app +- Privacyby design — ingen lokationsdata, ingen bruger-ID'er, ephemerelle tokens +- Simpelt og fokuseret — ét skærmbillede, én knap, ét formål + +--- + +## Monorepo-struktur + +``` +SigHej/ +├── README.md +├── prompt.md ← dette fil +├── Makefile ← build workflow +├── docker-compose.yml ← backend + PostgreSQL + Redis +├── Docs/ +│ ├── ARCHITECTURE.md ← system overview og data flow +│ ├── BACKEND.md ← FastAPI detaljer +│ ├── APP.md ← Flutter app detaljer +│ ├── ADMIN.md ← Next.js admin detaljer +│ ├── TESTING.md ← test- og kodestandarder +│ ├── DECISIONS.md ← ADR'er (architecture decision records) +│ └── colors.md ← "Sunny Beach Day" farvepalet +├── backend/ ← FastAPI Python +├── app/ ← Flutter Dart (Android + iOS + web) +└── admin/ ← Next.js TypeScript admin dashboard +``` + +--- + +## Teknisk arkitektur + +### Flutter App (`app/`) +- **Sprog:** Dart, Flutter 3.x +- **BLE:** `flutter_blue_plus` ^1.36.8 — *kun scanning* på Android/iOS; web/macOS kører i demo-mode +- **State:** `provider` + `SessionStore` (ChangeNotifier) +- **Persistens:** `shared_preferences` — navn, tagline, interesser, bleToken +- **Screens:** + - `ProfileScreen` — første-gangs onboarding + redigering af profil + - `HomeScreen` — hoved-skærm med toggle-knap, pulseanimation og nudge-kort +- **Theme:** `app/lib/theme.dart` — "Sunny Beach Day" farvepalet, light + dark mode +- **Platform-betinget BLE:** `_bleAvailable` guard — alle BLE-kald er beskyttet; web/macOS simulerer match efter 3 sekunder (demo mode) +- **Web:** kører i Chrome via `flutter run -d chrome --web-port 8080` + +**Farvepalet (Sunny Beach Day):** +| Rolle | Navn | Hex | +|---|---|---| +| Primary | Verdigris | `#2A9D8F` | +| Secondary | Sandy Brown | `#F4A261` | +| Tertiary | Jasmine | `#E9C46A` | +| Error/CTA | Burnt Peach | `#E76F51` | +| Surface dark | Charcoal Blue | `#264653` | + +### Backend (`backend/`) +- **Sprog:** Python 3.12, FastAPI, async +- **Session store:** Redis (TTL: 2 timer — auto-sletning, ingen cleanup nødvendig) +- **Database:** PostgreSQL — kun anonymiserede aggregat-statistikker (ingen PII) +- **Kommunikation:** REST (POST /session, POST /match) + WebSocket (nudge push) +- **Config:** `pydantic-settings` — miljøvariabler, ingen hardcodede secrets +- **Tests:** pytest, httpx TestClient + +### Admin Dashboard (`admin/`) +- **Sprog:** TypeScript, Next.js 14 +- **Formål:** anonymiserede stats — pings, sessioner, interest counts +- **Ingen PII, ingen bruger-niveau data** +- **Build:** multi-stage Docker, `output: "standalone"`, kræver `package-lock.json` + +--- + +## Data flow — happy path + +``` +Bruger A åbner app → vælger interesser → aktiverer "Åben for snak" + └─► App genererer ephemeral BLE token (UUID) + └─► App advertiser token via BLE (Android/iOS) + └─► App registrerer token + interesser: POST /session + +Bruger B i nærheden scanner BLE → finder A's token + └─► App sender token: POST /match + └─► Backend: overlapper B's interesser med A's? + └─► Ja → WebSocket nudge til begge: + "Nogen i nærheden deler din interesse for X — sig hej" + └─► Brugerne lægger telefonen fra sig og siger hej + └─► Session udløber automatisk (Redis TTL: 2 timer) +``` + +--- + +## Makefile workflow + +```bash +make up # start backend + PostgreSQL + Redis (Docker) +make down # stop services +make build # byg Docker images +make logs # følg logs + +make be-dev # kør FastAPI i dev mode (hot reload) +make be-test # kør pytest +make be-lint # ruff lint +make be-fmt # ruff format + +make adm-dev # kør Next.js admin i dev mode +make adm-build # byg admin til produktion + +make app-get # flutter pub get +make app-analyze # flutter analyze (skal være "No issues found!") +make app-test # flutter test + +make check # fuld CI: lint + test på alle tre komponenter +``` + +--- + +## Kodestandarder + +### Python backend +Analyse kører via **DevOpsMCP** (`https://devops-mcp.i80.dk`): + +```python +# Upload og analysér +session = DevOpsMCP-start_project_session("sighej-backend", [ + {"file_path": "main.py", "content": ""} +]) +DevOpsMCP-check_personal_standards(file_path="main.py") # ingen error-findings +DevOpsMCP-analyze_complexity(file_path="main.py") # max complexity: 8 +DevOpsMCP-add_type_hints(file_path="main.py") # min 80% coverage +DevOpsMCP-check_pep_compliance(file_path="main.py") # zero violations +``` + +**Regler:** +- Ingen `subprocess` med `shell=True` — brug SDK-klienter +- Ingen bare `except:` — fang specifikke exceptions +- Kun f-strings — ingen `%` eller `.format()` +- Ingen mutable default-argumenter +- Ingen hardcodede secrets — kun env vars via `pydantic-settings` + +### Flutter app +- `flutter analyze` skal altid returnere `No issues found!` +- Alle BLE-kald skal guards af `_bleAvailable` +- Ingen direkte import af `dart:io` på web-paths + +### Git commits +Brug conventional commits: +``` +feat(app): tilføj pulsanimation til toggle-knap +fix(backend): ret Redis TTL ved session-fornyelse +docs: opdater ARCHITECTURE.md med WebSocket flow +``` + +--- + +## Arkitekturbeslutninger (ADR-resumé) + +| ADR | Beslutning | Begrundelse | +|---|---|---| +| ADR-001 | Flutter frem for React Native | Native ARM, stabil BLE via direkte platform channels | +| ADR-002 | FastAPI frem for Express/Django | Async-first, Pydantic validation, hurtig at bygge | +| ADR-003 | Redis til sessions, ikke PostgreSQL | Ephemerelle data, auto-TTL, ingen manuel cleanup | +| ADR-004 | Ingen in-app messaging | Nudget ER produktet — chat modarbejder formålet | + +--- + +## Privacy-design + +- **Ingen bruger-ID'er** — kun ephemerelle BLE tokens (UUID, ny pr. session) +- **Ingen lokationsdata** — BLE proximity er ikke GPS +- **Server-side matching** — apps ser aldrig hinandens rå profiler +- **Auto-sletning** — Redis TTL 2 timer, ingen data efterlades +- **Aggregat-stats kun** — PostgreSQL indeholder aldrig PII + +--- + +## Kendte begrænsninger (MVP) + +- BLE advertising kræver fysisk Android/iOS-enhed — ikke tilgængeligt på macOS/web Flutter +- Web/macOS kører i **demo mode**: simulerer et match efter 3 sekunder +- Admin dashboard har ingen auth i MVP — bør tilføjes før produktion +- Ingen push notifications (FCM/APNs) endnu — nudge vises kun mens app er åben +- Android SDK + Xcode krævet for at bygge til mobile (se `flutter doctor`) + +--- + +## Kom i gang (ny maskine) + +```bash +# Klon repo +git clone && cd SigHej + +# Start backend infrastruktur +make up + +# Flutter app i browser +cd app && flutter pub get && flutter run -d chrome --web-port 8080 + +# Flutter app på Android (kræver USB debugging + Android SDK) +flutter devices +flutter run -d +``` + +Se `Docs/` for detaljeret dokumentation om hvert komponent.