From 04420a48127b6043c95eda79c24c99400d18448a Mon Sep 17 00:00:00 2001 From: Mohammed Mohsin <59914433+mdmohsin7@users.noreply.github.com> Date: Wed, 21 Aug 2024 20:33:17 +0530 Subject: [PATCH 01/23] message and memory provider --- app/lib/main.dart | 4 + app/lib/pages/chat/page.dart | 249 +++++++------ app/lib/pages/home/page.dart | 474 +++++++++++------------- app/lib/providers/home_provider.dart | 11 +- app/lib/providers/memory_provider.dart | 127 +++++++ app/lib/providers/message_provider.dart | 47 +++ 6 files changed, 517 insertions(+), 395 deletions(-) create mode 100644 app/lib/providers/memory_provider.dart create mode 100644 app/lib/providers/message_provider.dart diff --git a/app/lib/main.dart b/app/lib/main.dart index 87c8e500a..c9539d6d7 100644 --- a/app/lib/main.dart +++ b/app/lib/main.dart @@ -18,6 +18,8 @@ import 'package:friend_private/flavors.dart'; import 'package:friend_private/pages/home/page.dart'; import 'package:friend_private/pages/onboarding/wrapper.dart'; import 'package:friend_private/providers/home_provider.dart'; +import 'package:friend_private/providers/memory_provider.dart'; +import 'package:friend_private/providers/message_provider.dart'; import 'package:friend_private/services/notification_service.dart'; import 'package:friend_private/utils/analytics/growthbook.dart'; import 'package:friend_private/utils/analytics/mixpanel.dart'; @@ -128,6 +130,8 @@ class _MyAppState extends State { return MultiProvider( providers: [ ListenableProvider(create: (context) => HomeProvider()), + ListenableProvider(create: (context) => MessageProvider()), + ListenableProvider(create: (context) => MemoryProvider()), ], builder: (context, child) { return WithForegroundTask( diff --git a/app/lib/pages/chat/page.dart b/app/lib/pages/chat/page.dart index ce414bbed..5b653b78c 100644 --- a/app/lib/pages/chat/page.dart +++ b/app/lib/pages/chat/page.dart @@ -8,24 +8,20 @@ import 'package:friend_private/backend/schema/message.dart'; import 'package:friend_private/backend/schema/plugin.dart'; import 'package:friend_private/pages/chat/widgets/ai_message.dart'; import 'package:friend_private/pages/chat/widgets/user_message.dart'; +import 'package:friend_private/providers/message_provider.dart'; import 'package:friend_private/utils/connectivity_controller.dart'; import 'package:gradient_borders/gradient_borders.dart'; +import 'package:provider/provider.dart'; import 'package:uuid/uuid.dart'; class ChatPage extends StatefulWidget { final FocusNode textFieldFocusNode; - final List messages; - final Function(ServerMessage) addMessage; final Function(ServerMemory) updateMemory; - final bool isLoadingMessages; const ChatPage({ super.key, required this.textFieldFocusNode, - required this.messages, - required this.addMessage, required this.updateMemory, - required this.isLoadingMessages, }); @override @@ -71,124 +67,129 @@ class ChatPageState extends State with AutomaticKeepAliveClientMixin { @override Widget build(BuildContext context) { super.build(context); - return Stack( - children: [ - Center( - child: SingleChildScrollView( - controller: scrollController, - child: widget.isLoadingMessages - ? const CircularProgressIndicator( - color: Colors.white, - ) - : (widget.messages.isEmpty) - ? Text( - ConnectivityController().isConnected.value - ? 'No messages yet!\nWhy don\'t you start a conversation?' - : 'Please check your internet connection and try again', - textAlign: TextAlign.center, - style: const TextStyle(color: Colors.white)) - : ListView.builder( - shrinkWrap: true, - reverse: true, - physics: const NeverScrollableScrollPhysics(), - itemCount: widget.messages.length, - itemBuilder: (context, chatIndex) { - final message = widget.messages[chatIndex]; - double topPadding = chatIndex == widget.messages.length - 1 ? 24 : 16; - double bottomPadding = chatIndex == 0 ? (widget.textFieldFocusNode.hasFocus ? 120 : 200) : 0; - return Padding( - key: ValueKey(message.id), - padding: EdgeInsets.only(bottom: bottomPadding, left: 18, right: 18, top: topPadding), - child: message.sender == MessageSender.ai - ? AIMessage( - message: message, - sendMessage: _sendMessageUtil, - displayOptions: widget.messages.length <= 1, - pluginSender: plugins.firstWhereOrNull((e) => e.id == message.pluginId), - updateMemory: widget.updateMemory, - ) - : HumanMessage(message: message), - ); - }, - ), - ), - ), - Align( - alignment: Alignment.bottomCenter, - child: Container( - width: double.maxFinite, - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 2), - margin: EdgeInsets.only(left: 18, right: 18, bottom: widget.textFieldFocusNode.hasFocus ? 40 : 120), - decoration: const BoxDecoration( - color: Colors.black, - borderRadius: BorderRadius.all(Radius.circular(16)), - border: GradientBoxBorder( - gradient: LinearGradient(colors: [ - Color.fromARGB(127, 208, 208, 208), - Color.fromARGB(127, 188, 99, 121), - Color.fromARGB(127, 86, 101, 182), - Color.fromARGB(127, 126, 190, 236) - ]), - width: 1, + return Consumer( + builder: (context, provider, child) { + return Stack( + children: [ + Center( + child: SingleChildScrollView( + controller: scrollController, + child: provider.isLoadingMessages + ? const CircularProgressIndicator( + color: Colors.white, + ) + : (provider.messages.isEmpty) + ? Text( + ConnectivityController().isConnected.value + ? 'No messages yet!\nWhy don\'t you start a conversation?' + : 'Please check your internet connection and try again', + textAlign: TextAlign.center, + style: const TextStyle(color: Colors.white)) + : ListView.builder( + shrinkWrap: true, + reverse: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: provider.messages.length, + itemBuilder: (context, chatIndex) { + final message = provider.messages[chatIndex]; + double topPadding = chatIndex == provider.messages.length - 1 ? 24 : 16; + double bottomPadding = + chatIndex == 0 ? (widget.textFieldFocusNode.hasFocus ? 120 : 200) : 0; + return Padding( + key: ValueKey(message.id), + padding: EdgeInsets.only(bottom: bottomPadding, left: 18, right: 18, top: topPadding), + child: message.sender == MessageSender.ai + ? AIMessage( + message: message, + sendMessage: _sendMessageUtil, + displayOptions: provider.messages.length <= 1, + pluginSender: plugins.firstWhereOrNull((e) => e.id == message.pluginId), + updateMemory: widget.updateMemory, + ) + : HumanMessage(message: message), + ); + }, + ), ), - shape: BoxShape.rectangle, ), - child: TextField( - enabled: true, - controller: textController, - // textCapitalization: TextCapitalization.sentences, - obscureText: false, - focusNode: widget.textFieldFocusNode, - // canRequestFocus: true, - textAlign: TextAlign.start, - textAlignVertical: TextAlignVertical.center, - decoration: InputDecoration( - hintText: 'Ask your Friend anything', - hintStyle: const TextStyle(fontSize: 14.0, color: Colors.grey), - focusedBorder: InputBorder.none, - enabledBorder: InputBorder.none, - suffixIcon: IconButton( - splashColor: Colors.transparent, - splashRadius: 1, - onPressed: loading - ? null - : () async { - String message = textController.text; - if (message.isEmpty) return; - if (ConnectivityController().isConnected.value) { - _sendMessageUtil(message); - } else { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Please check your internet connection and try again'), - duration: Duration(seconds: 2), + Align( + alignment: Alignment.bottomCenter, + child: Container( + width: double.maxFinite, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 2), + margin: EdgeInsets.only(left: 18, right: 18, bottom: widget.textFieldFocusNode.hasFocus ? 40 : 120), + decoration: const BoxDecoration( + color: Colors.black, + borderRadius: BorderRadius.all(Radius.circular(16)), + border: GradientBoxBorder( + gradient: LinearGradient(colors: [ + Color.fromARGB(127, 208, 208, 208), + Color.fromARGB(127, 188, 99, 121), + Color.fromARGB(127, 86, 101, 182), + Color.fromARGB(127, 126, 190, 236) + ]), + width: 1, + ), + shape: BoxShape.rectangle, + ), + child: TextField( + enabled: true, + controller: textController, + // textCapitalization: TextCapitalization.sentences, + obscureText: false, + focusNode: widget.textFieldFocusNode, + // canRequestFocus: true, + textAlign: TextAlign.start, + textAlignVertical: TextAlignVertical.center, + decoration: InputDecoration( + hintText: 'Ask your Friend anything', + hintStyle: const TextStyle(fontSize: 14.0, color: Colors.grey), + focusedBorder: InputBorder.none, + enabledBorder: InputBorder.none, + suffixIcon: IconButton( + splashColor: Colors.transparent, + splashRadius: 1, + onPressed: loading + ? null + : () async { + String message = textController.text; + if (message.isEmpty) return; + if (ConnectivityController().isConnected.value) { + _sendMessageUtil(message); + } else { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Please check your internet connection and try again'), + duration: Duration(seconds: 2), + ), + ); + } + }, + icon: loading + ? const SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation(Colors.white), ), - ); - } - }, - icon: loading - ? const SizedBox( - width: 16, - height: 16, - child: CircularProgressIndicator( - valueColor: AlwaysStoppedAnimation(Colors.white), - ), - ) - : const Icon( - Icons.send_rounded, - color: Color(0xFFF7F4F4), - size: 24.0, - ), + ) + : const Icon( + Icons.send_rounded, + color: Color(0xFFF7F4F4), + size: 24.0, + ), + ), + ), + // maxLines: 8, + // minLines: 1, + // keyboardType: TextInputType.multiline, + style: TextStyle(fontSize: 14.0, color: Colors.grey.shade200), ), ), - // maxLines: 8, - // minLines: 1, - // keyboardType: TextInputType.multiline, - style: TextStyle(fontSize: 14.0, color: Colors.grey.shade200), ), - ), - ), - ], + ], + ); + }, ); } @@ -197,15 +198,13 @@ class ChatPageState extends State with AutomaticKeepAliveClientMixin { String? pluginId = SharedPreferencesUtil().selectedChatPluginId == 'no_selected' ? null : SharedPreferencesUtil().selectedChatPluginId; - widget.addMessage( - ServerMessage(const Uuid().v4(), DateTime.now(), message, MessageSender.human, MessageType.text, null, false, []), - ); + var newMessage = ServerMessage( + const Uuid().v4(), DateTime.now(), message, MessageSender.human, MessageType.text, null, false, []); + context.read().addMessage(newMessage); _moveListToBottom(extra: widget.textFieldFocusNode.hasFocus ? 148 : 200); textController.clear(); - ServerMessage aiMessage = await sendMessageServer(message, pluginId: pluginId); + await context.read().sendMessageToServer(message, pluginId); // TODO: restore streaming capabilities, with initial empty message - debugPrint('aiMessage: ${aiMessage.id}: ${aiMessage.text}'); - widget.addMessage(aiMessage); _moveListToBottom(extra: widget.textFieldFocusNode.hasFocus ? 148 : 200); changeLoadingState(); } @@ -214,7 +213,7 @@ class ChatPageState extends State with AutomaticKeepAliveClientMixin { changeLoadingState(); _moveListToBottom(extra: widget.textFieldFocusNode.hasFocus ? 148 : 200); ServerMessage message = await getInitialPluginMessage(plugin?.id); - widget.addMessage(message); + context.read().addMessage(message); _moveListToBottom(extra: widget.textFieldFocusNode.hasFocus ? 148 : 200); changeLoadingState(); } diff --git a/app/lib/pages/home/page.dart b/app/lib/pages/home/page.dart index aec885474..93c046fb9 100644 --- a/app/lib/pages/home/page.dart +++ b/app/lib/pages/home/page.dart @@ -1,13 +1,10 @@ import 'dart:async'; -import 'dart:convert'; import 'dart:io'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter_blue_plus/flutter_blue_plus.dart'; import 'package:flutter_foreground_task/flutter_foreground_task.dart'; -import 'package:friend_private/backend/http/api/memories.dart'; -import 'package:friend_private/backend/http/api/messages.dart'; import 'package:friend_private/backend/http/api/plugins.dart'; import 'package:friend_private/backend/http/api/speech_profile.dart'; import 'package:friend_private/backend/http/cloud_storage.dart'; @@ -24,6 +21,9 @@ import 'package:friend_private/pages/home/device.dart'; import 'package:friend_private/pages/memories/page.dart'; import 'package:friend_private/pages/plugins/page.dart'; import 'package:friend_private/pages/settings/page.dart'; +import 'package:friend_private/providers/home_provider.dart'; +import 'package:friend_private/providers/message_provider.dart'; +import 'package:friend_private/providers/memory_provider.dart' as mp; import 'package:friend_private/scripts.dart'; import 'package:friend_private/services/notification_service.dart'; import 'package:friend_private/utils/analytics/mixpanel.dart'; @@ -32,29 +32,37 @@ import 'package:friend_private/utils/ble/communication.dart'; import 'package:friend_private/utils/ble/connected.dart'; import 'package:friend_private/utils/ble/scan.dart'; import 'package:friend_private/utils/connectivity_controller.dart'; -import 'package:friend_private/utils/memories/process.dart'; import 'package:friend_private/utils/other/temp.dart'; import 'package:friend_private/widgets/upgrade_alert.dart'; import 'package:gradient_borders/gradient_borders.dart'; import 'package:instabug_flutter/instabug_flutter.dart'; -import 'package:tuple/tuple.dart'; +import 'package:provider/provider.dart'; import 'package:upgrader/upgrader.dart'; -class HomePageWrapper extends StatefulWidget { +class HomePageWrapper extends StatelessWidget { const HomePageWrapper({super.key}); @override - State createState() => _HomePageWrapperState(); + Widget build(BuildContext context) { + return ChangeNotifierProvider( + create: (context) => HomeProvider(), + child: const HomePage(), + ); + } +} + +class HomePage extends StatefulWidget { + const HomePage({super.key}); + + @override + State createState() => _HomePageState(); } -class _HomePageWrapperState extends State with WidgetsBindingObserver, TickerProviderStateMixin { +class _HomePageState extends State with WidgetsBindingObserver, TickerProviderStateMixin { ForegroundUtil foregroundUtil = ForegroundUtil(); TabController? _controller; List screens = [Container(), const SizedBox(), const SizedBox()]; - List memories = []; - List messages = []; - FocusNode chatTextFieldFocusNode = FocusNode(canRequestFocus: true); FocusNode memoriesTextFieldFocusNode = FocusNode(canRequestFocus: true); @@ -68,86 +76,9 @@ class _HomePageWrapperState extends State with WidgetsBindingOb List plugins = []; final _upgrader = MyUpgrader(debugLogging: false, debugDisplayOnce: false); - bool loadingNewMemories = true; - bool loadingNewMessages = true; - - Future _retrySingleFailed(ServerMemory memory) async { - if (memory.transcriptSegments.isEmpty || memory.photos.isEmpty) return null; - return await processTranscriptContent( - segments: memory.transcriptSegments, - sendMessageToChat: null, - startedAt: memory.startedAt, - finishedAt: memory.finishedAt, - geolocation: memory.geolocation, - photos: memory.photos.map((photo) => Tuple2(photo.base64, photo.description)).toList(), - triggerIntegrations: false, - language: memory.language ?? 'en', - ); - } - _retryFailedMemories() async { - if (SharedPreferencesUtil().failedMemories.isEmpty) return; - debugPrint('SharedPreferencesUtil().failedMemories: ${SharedPreferencesUtil().failedMemories.length}'); - // retry failed memories - List> asyncEvents = []; - for (var item in SharedPreferencesUtil().failedMemories) { - asyncEvents.add(_retrySingleFailed(item)); - } - // TODO: should be able to retry including created at date. - // TODO: should trigger integrations? probably yes, but notifications? - - List results = await Future.wait(asyncEvents); - var failedCopy = List.from(SharedPreferencesUtil().failedMemories); - - for (var i = 0; i < results.length; i++) { - ServerMemory? newCreatedMemory = results[i]; - - if (newCreatedMemory != null) { - SharedPreferencesUtil().removeFailedMemory(failedCopy[i].id); - memories.insert(0, newCreatedMemory); - } else { - var prefsMemory = SharedPreferencesUtil().failedMemories[i]; - if (prefsMemory.transcriptSegments.isEmpty && prefsMemory.photos.isEmpty) { - SharedPreferencesUtil().removeFailedMemory(failedCopy[i].id); - continue; - } - if (SharedPreferencesUtil().failedMemories[i].retries == 3) { - CrashReporting.reportHandledCrash(Exception('Retry memory limits reached'), StackTrace.current, - userAttributes: {'memory': jsonEncode(SharedPreferencesUtil().failedMemories[i].toJson())}); - SharedPreferencesUtil().removeFailedMemory(failedCopy[i].id); - continue; - } - memories.insert(0, SharedPreferencesUtil().failedMemories[i]); // TODO: sort them or something? - SharedPreferencesUtil().increaseFailedMemoryRetries(failedCopy[i].id); - } - } - debugPrint('SharedPreferencesUtil().failedMemories: ${SharedPreferencesUtil().failedMemories.length}'); - setState(() {}); - } - - _initiateMemories() async { - memories = await getMemories(); - if (memories.isEmpty) { - memories = SharedPreferencesUtil().cachedMemories; - } else { - SharedPreferencesUtil().cachedMemories = memories; - } - loadingNewMemories = false; - setState(() {}); - _retryFailedMemories(); - } - - _refreshMessages() async { - loadingNewMessages = true; - messages = await getMessagesServer(); - if (messages.isEmpty) { - messages = SharedPreferencesUtil().cachedMessages; - } else { - SharedPreferencesUtil().cachedMessages = messages; - } - loadingNewMessages = false; - setState(() {}); - } + // List memories = []; + // List messages = []; bool scriptsInProgress = false; @@ -196,7 +127,9 @@ class _HomePageWrapperState extends State with WidgetsBindingOb _migrationScripts() async { setState(() => scriptsInProgress = true); await scriptMigrateMemoriesToBack(); - _initiateMemories(); + if (mounted) { + await context.read().initiateMemories(); + } setState(() => scriptsInProgress = false); } @@ -209,6 +142,7 @@ class _HomePageWrapperState extends State with WidgetsBindingOb @override void initState() { + // TODO: Being triggered multiple times during navigation. It ideally shouldn't connectivityController.init(); _controller = TabController( length: 3, @@ -223,11 +157,17 @@ class _HomePageWrapperState extends State with WidgetsBindingOb ForegroundUtil.requestPermissions(); await ForegroundUtil.initializeForegroundService(); ForegroundUtil.startForegroundTask(); + if (mounted) { + await context.read().refreshMessages(); + await context.read().initiateMemories(); + } }); - _refreshMessages(); + _initiatePlugins(); _setupHasSpeakerProfile(); - _migrationScripts(); + //TODO: Should this run everytime? + // _migrationScripts(); + authenticateGCP(); if (SharedPreferencesUtil().btDeviceStruct.id.isNotEmpty) { scanAndConnectDevice().then(_onConnected); @@ -250,10 +190,8 @@ class _HomePageWrapperState extends State with WidgetsBindingOb void _listenToMessagesFromNotification() { NotificationService.instance.listenForServerMessages.listen((message) { - var messagesCopy = List.from(messages); - messagesCopy.insert(0, message); + context.read().addMessage(message); chatPageKey.currentState?.scrollToBottom(); - setState(() => messages = messagesCopy); }); } @@ -293,7 +231,9 @@ class _HomePageWrapperState extends State with WidgetsBindingOb MixpanelManager().deviceConnected(); SharedPreferencesUtil().btDeviceStruct = _device!; SharedPreferencesUtil().deviceName = _device!.name; - setState(() {}); + if (mounted) { + setState(() {}); + } } _initiateBleBatteryListener() async { @@ -366,12 +306,16 @@ class _HomePageWrapperState extends State with WidgetsBindingOb ), ); - if (memories.isEmpty) { - _initiateMemories(); - } - if (messages.isEmpty) { - _refreshMessages(); - } + WidgetsBinding.instance.addPostFrameCallback((_) async { + if (mounted) { + if (context.read().memories.isEmpty) { + await context.read().initiateMemories(); + } + if (context.read().messages.isEmpty) { + await context.read().refreshMessages(); + } + } + }); }); } } @@ -384,182 +328,192 @@ class _HomePageWrapperState extends State with WidgetsBindingOb chatTextFieldFocusNode.unfocus(); memoriesTextFieldFocusNode.unfocus(); }, - child: Stack( - children: [ - Center( - child: TabBarView( - controller: _controller, - physics: const NeverScrollableScrollPhysics(), - children: [ - MemoriesPage( - memories: memories, - updateMemory: (ServerMemory memory, int index) { - var memoriesCopy = List.from(memories); - memoriesCopy[index] = memory; - setState(() => memories = memoriesCopy); - }, - deleteMemory: (ServerMemory memory, int index) { - var memoriesCopy = List.from(memories); - memoriesCopy.removeAt(index); - setState(() => memories = memoriesCopy); - }, - loadMoreMemories: () async { - if (memories.length % 50 != 0) return; - if (loadingNewMemories) return; - setState(() => loadingNewMemories = true); - var newMemories = await getMemories(offset: memories.length); - memories.addAll(newMemories); - loadingNewMemories = false; - setState(() {}); - }, - loadingNewMemories: loadingNewMemories, - textFieldFocusNode: memoriesTextFieldFocusNode, - ), - CapturePage( + child: Consumer2(builder: (context, provider, memProvider, child) { + return Stack( + children: [ + Center( + child: TabBarView( + controller: _controller, + physics: const NeverScrollableScrollPhysics(), + children: [ + MemoriesPage( + memories: memProvider.memories, + updateMemory: (ServerMemory memory, int index) { + //TODO: Memory Provider Migrate + // var memoriesCopy = List.from(memories); + // memoriesCopy[index] = memory; + // setState(() => memories = memoriesCopy); + memProvider.updateMemory(memory, index); + }, + deleteMemory: (ServerMemory memory, int index) { + // TODO: Memory Provider Migrate + // var memoriesCopy = List.from(memories); + // memoriesCopy.removeAt(index); + // setState(() => memories = memoriesCopy); + memProvider.deleteMemory(memory, index); + }, + loadMoreMemories: () async { + // ---------------------------------- + // if (memProvider.memories.length % 50 != 0) return; + // if (context.read().isLoadingMemories) return; + // context.read().setLoadingMemories(true); + // var newMemories = await getMemories(offset: memProvider.memories.length); + // memories.addAll(newMemories); + // context.read().setLoadingMemories(false); + // ---------------------------------- + await memProvider.getMoreMemoriesFromServer(); + }, + loadingNewMemories: context.read().isLoadingMemories, + textFieldFocusNode: memoriesTextFieldFocusNode, + ), + CapturePage( key: capturePageKey, device: _device, addMemory: (ServerMemory memory) { - var memoriesCopy = List.from(memories); - memoriesCopy.insert(0, memory); - setState(() => memories = memoriesCopy); + // TODO: Memory Provider Migrate + // var memoriesCopy = List.from(memories); + // memoriesCopy.insert(0, memory); + // setState(() => memories = memoriesCopy); + memProvider.addMemory(memory); }, addMessage: (ServerMessage message) { - var messagesCopy = List.from(messages); - messagesCopy.insert(0, message); - setState(() => messages = messagesCopy); + context.read().addMessage(message); + chatPageKey.currentState?.scrollToBottom(); }, updateMemory: (ServerMemory memory) { - var memoriesCopy = List.from(memories); - var index = memoriesCopy.indexWhere((m) => m.id == memory.id); - if (index != -1) { - memoriesCopy[index] = memory; - setState(() => memories = memoriesCopy); - } - }), - ChatPage( - key: chatPageKey, - textFieldFocusNode: chatTextFieldFocusNode, - messages: messages, - isLoadingMessages: loadingNewMessages, - addMessage: (ServerMessage message) { - var messagesCopy = List.from(messages); - messagesCopy.insert(0, message); - setState(() => messages = messagesCopy); - }, - updateMemory: (ServerMemory memory) { - var memoriesCopy = List.from(memories); - var index = memoriesCopy.indexWhere((m) => m.id == memory.id); - if (index != -1) { - memoriesCopy[index] = memory; - setState(() => memories = memoriesCopy); - } - }, - ), - ], + // TODO: Memory Provider Migrate + // var memoriesCopy = List.from(memories); + // var index = memoriesCopy.indexWhere((m) => m.id == memory.id); + // if (index != -1) { + // memoriesCopy[index] = memory; + // setState(() => memories = memoriesCopy); + // } + memProvider.updateMemory(memory); + }, + ), + ChatPage( + key: chatPageKey, + textFieldFocusNode: chatTextFieldFocusNode, + updateMemory: (ServerMemory memory) { + // TODO: Memory Provider Migrate + // var memoriesCopy = List.from(memories); + // var index = memoriesCopy.indexWhere((m) => m.id == memory.id); + // if (index != -1) { + // memoriesCopy[index] = memory; + // setState(() => memories = memoriesCopy); + // } + memProvider.updateMemory(memory); + }, + ), + ], + ), ), - ), - if (chatTextFieldFocusNode.hasFocus || memoriesTextFieldFocusNode.hasFocus) - const SizedBox.shrink() - else - Align( - alignment: Alignment.bottomCenter, - child: Container( - margin: const EdgeInsets.fromLTRB(16, 16, 16, 40), - decoration: const BoxDecoration( - color: Colors.black, - borderRadius: BorderRadius.all(Radius.circular(16)), - border: GradientBoxBorder( - gradient: LinearGradient(colors: [ - Color.fromARGB(127, 208, 208, 208), - Color.fromARGB(127, 188, 99, 121), - Color.fromARGB(127, 86, 101, 182), - Color.fromARGB(127, 126, 190, 236) - ]), - width: 2, + if (chatTextFieldFocusNode.hasFocus || memoriesTextFieldFocusNode.hasFocus) + const SizedBox.shrink() + else + Align( + alignment: Alignment.bottomCenter, + child: Container( + margin: const EdgeInsets.fromLTRB(16, 16, 16, 40), + decoration: const BoxDecoration( + color: Colors.black, + borderRadius: BorderRadius.all(Radius.circular(16)), + border: GradientBoxBorder( + gradient: LinearGradient(colors: [ + Color.fromARGB(127, 208, 208, 208), + Color.fromARGB(127, 188, 99, 121), + Color.fromARGB(127, 86, 101, 182), + Color.fromARGB(127, 126, 190, 236) + ]), + width: 2, + ), + shape: BoxShape.rectangle, ), - shape: BoxShape.rectangle, - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: MaterialButton( - onPressed: () => _tabChange(0), - child: Padding( - padding: const EdgeInsets.only(top: 20, bottom: 20), - child: Text('Memories', - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle( - color: _controller!.index == 0 ? Colors.white : Colors.grey, fontSize: 16)), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: MaterialButton( + onPressed: () => _tabChange(0), + child: Padding( + padding: const EdgeInsets.only(top: 20, bottom: 20), + child: Text('Memories', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: _controller!.index == 0 ? Colors.white : Colors.grey, + fontSize: 16)), + ), ), ), - ), - Expanded( - child: MaterialButton( - onPressed: () => _tabChange(1), - child: Padding( - padding: const EdgeInsets.only( - top: 20, - bottom: 20, + Expanded( + child: MaterialButton( + onPressed: () => _tabChange(1), + child: Padding( + padding: const EdgeInsets.only( + top: 20, + bottom: 20, + ), + child: Text('Capture', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: _controller!.index == 1 ? Colors.white : Colors.grey, + fontSize: 16)), ), - child: Text('Capture', - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle( - color: _controller!.index == 1 ? Colors.white : Colors.grey, fontSize: 16)), ), ), - ), - Expanded( - child: MaterialButton( - onPressed: () => _tabChange(2), - child: Padding( - padding: const EdgeInsets.only(top: 20, bottom: 20), - child: Text('Chat', - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle( - color: _controller!.index == 2 ? Colors.white : Colors.grey, fontSize: 16)), + Expanded( + child: MaterialButton( + onPressed: () => _tabChange(2), + child: Padding( + padding: const EdgeInsets.only(top: 20, bottom: 20), + child: Text('Chat', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: _controller!.index == 2 ? Colors.white : Colors.grey, + fontSize: 16)), + ), ), ), - ), - ], + ], + ), ), ), - ), - if (scriptsInProgress) - Center( - child: Container( - height: 150, - width: 250, - decoration: BoxDecoration( - color: Colors.black, - borderRadius: BorderRadius.circular(16), - border: Border.all(color: Colors.white, width: 2), - ), - child: const Column( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - CircularProgressIndicator( - valueColor: AlwaysStoppedAnimation(Colors.white), - ), - SizedBox(height: 16), - Center( - child: Text( - 'Running migration, please wait! 🚨', - style: TextStyle(color: Colors.white, fontSize: 16), - textAlign: TextAlign.center, - )), - ], + if (scriptsInProgress) + Center( + child: Container( + height: 150, + width: 250, + decoration: BoxDecoration( + color: Colors.black, + borderRadius: BorderRadius.circular(16), + border: Border.all(color: Colors.white, width: 2), + ), + child: const Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation(Colors.white), + ), + SizedBox(height: 16), + Center( + child: Text( + 'Running migration, please wait! 🚨', + style: TextStyle(color: Colors.white, fontSize: 16), + textAlign: TextAlign.center, + )), + ], + ), ), - ), - ) - else - const SizedBox.shrink(), - ], - ), + ) + else + const SizedBox.shrink(), + ], + ); + }), ), appBar: AppBar( automaticallyImplyLeading: false, diff --git a/app/lib/providers/home_provider.dart b/app/lib/providers/home_provider.dart index 00a0dad88..46c10b3ff 100644 --- a/app/lib/providers/home_provider.dart +++ b/app/lib/providers/home_provider.dart @@ -1,12 +1,3 @@ import 'package:flutter/material.dart'; -class HomeProvider extends ChangeNotifier { - int _counter = 0; - - int get counter => _counter; - - void incrementCounter() { - _counter++; - notifyListeners(); - } -} +class HomeProvider extends ChangeNotifier {} diff --git a/app/lib/providers/memory_provider.dart b/app/lib/providers/memory_provider.dart new file mode 100644 index 000000000..3a845b000 --- /dev/null +++ b/app/lib/providers/memory_provider.dart @@ -0,0 +1,127 @@ +import 'dart:convert'; + +import 'package:flutter/foundation.dart'; +import 'package:friend_private/backend/http/api/memories.dart'; +import 'package:friend_private/backend/preferences.dart'; +import 'package:friend_private/backend/schema/memory.dart'; +import 'package:friend_private/utils/memories/process.dart'; +import 'package:instabug_flutter/instabug_flutter.dart'; +import 'package:tuple/tuple.dart'; + +class MemoryProvider extends ChangeNotifier { + List memories = []; + + bool isLoadingMemories = false; + + void setLoadingMemories(bool value) { + isLoadingMemories = value; + notifyListeners(); + } + + Future initiateMemories() async { + memories = await getMemoriesFromServer(); + if (memories.isEmpty) { + memories = SharedPreferencesUtil().cachedMemories; + } else { + SharedPreferencesUtil().cachedMemories = memories; + } + await retryFailedMemories(); + notifyListeners(); + } + + Future getMemoriesFromServer() async { + setLoadingMemories(true); + var mem = await getMemories(); + memories = mem; + setLoadingMemories(false); + notifyListeners(); + return memories; + } + + Future getMoreMemoriesFromServer() async { + if (memories.length % 50 != 0) return; + if (isLoadingMemories) return; + setLoadingMemories(true); + var newMemories = await getMemories(offset: memories.length); + memories.addAll(newMemories); + setLoadingMemories(false); + notifyListeners(); + } + + void addMemory(ServerMemory memory) { + memories.insert(0, memory); + notifyListeners(); + } + + void updateMemory(ServerMemory memory, [int? index]) { + if (index != null) { + memories[index] = memory; + } else { + int i = memories.indexWhere((element) => element.id == memory.id); + if (i != -1) { + memories[i] = memory; + } + } + notifyListeners(); + } + + void deleteMemory(ServerMemory memory, int index) { + memories.removeAt(index); + notifyListeners(); + } + + // TODO: Move this to somewhere more suitable + Future _retrySingleFailed(ServerMemory memory) async { + if (memory.transcriptSegments.isEmpty || memory.photos.isEmpty) return null; + return await processTranscriptContent( + segments: memory.transcriptSegments, + sendMessageToChat: null, + startedAt: memory.startedAt, + finishedAt: memory.finishedAt, + geolocation: memory.geolocation, + photos: memory.photos.map((photo) => Tuple2(photo.base64, photo.description)).toList(), + triggerIntegrations: false, + language: memory.language ?? 'en', + ); + } + + Future retryFailedMemories() async { + if (SharedPreferencesUtil().failedMemories.isEmpty) return; + debugPrint('SharedPreferencesUtil().failedMemories: ${SharedPreferencesUtil().failedMemories.length}'); + // retry failed memories + List> asyncEvents = []; + for (var item in SharedPreferencesUtil().failedMemories) { + asyncEvents.add(_retrySingleFailed(item)); + } + // TODO: should be able to retry including created at date. + // TODO: should trigger integrations? probably yes, but notifications? + + List results = await Future.wait(asyncEvents); + var failedCopy = List.from(SharedPreferencesUtil().failedMemories); + + for (var i = 0; i < results.length; i++) { + ServerMemory? newCreatedMemory = results[i]; + + if (newCreatedMemory != null) { + SharedPreferencesUtil().removeFailedMemory(failedCopy[i].id); + memories.insert(0, newCreatedMemory); + } else { + var prefsMemory = SharedPreferencesUtil().failedMemories[i]; + if (prefsMemory.transcriptSegments.isEmpty && prefsMemory.photos.isEmpty) { + SharedPreferencesUtil().removeFailedMemory(failedCopy[i].id); + continue; + } + if (SharedPreferencesUtil().failedMemories[i].retries == 3) { + CrashReporting.reportHandledCrash(Exception('Retry memory limits reached'), StackTrace.current, + userAttributes: {'memory': jsonEncode(SharedPreferencesUtil().failedMemories[i].toJson())}); + SharedPreferencesUtil().removeFailedMemory(failedCopy[i].id); + continue; + } + memories.insert(0, SharedPreferencesUtil().failedMemories[i]); // TODO: sort them or something? + SharedPreferencesUtil().increaseFailedMemoryRetries(failedCopy[i].id); + } + } + debugPrint('SharedPreferencesUtil().failedMemories: ${SharedPreferencesUtil().failedMemories.length}'); + notifyListeners(); + } +} diff --git a/app/lib/providers/message_provider.dart b/app/lib/providers/message_provider.dart new file mode 100644 index 000000000..c79a025db --- /dev/null +++ b/app/lib/providers/message_provider.dart @@ -0,0 +1,47 @@ +import 'package:flutter/foundation.dart'; +import 'package:friend_private/backend/http/api/messages.dart'; +import 'package:friend_private/backend/preferences.dart'; +import 'package:friend_private/backend/schema/message.dart'; + +class MessageProvider extends ChangeNotifier { + List messages = []; + + bool isLoadingMessages = false; + + void setLoadingMessages(bool value) { + isLoadingMessages = value; + notifyListeners(); + } + + Future refreshMessages() async { + setLoadingMessages(true); + messages = await getMessagesFromServer(); + if (messages.isEmpty) { + messages = SharedPreferencesUtil().cachedMessages; + } else { + SharedPreferencesUtil().cachedMessages = messages; + } + setLoadingMessages(false); + notifyListeners(); + } + + Future> getMessagesFromServer() async { + setLoadingMessages(true); + var mes = await getMessagesServer(); + messages = mes; + setLoadingMessages(false); + notifyListeners(); + return messages; + } + + void addMessage(ServerMessage message) { + messages.insert(0, message); + notifyListeners(); + } + + Future sendMessageToServer(String message, String? pluginId) async { + var mes = await sendMessageServer(message); + messages.insert(0, mes); + notifyListeners(); + } +} From f710a23b7f6df8f48fcab7b52d20d1f447b1fec3 Mon Sep 17 00:00:00 2001 From: Becca-Saka Date: Thu, 22 Aug 2024 02:24:10 +0100 Subject: [PATCH 02/23] chore: migrated to auth provider --- app/analysis_options.yaml | 2 + app/lib/main.dart | 2 + app/lib/pages/onboarding/auth.dart | 171 ++++++++----------------- app/lib/providers/auth_provider.dart | 80 ++++++++++++ app/lib/providers/base_provider.dart | 10 ++ app/lib/utils/alerts/app_snackbar.dart | 17 +++ 6 files changed, 166 insertions(+), 116 deletions(-) create mode 100644 app/lib/providers/auth_provider.dart create mode 100644 app/lib/providers/base_provider.dart create mode 100644 app/lib/utils/alerts/app_snackbar.dart diff --git a/app/analysis_options.yaml b/app/analysis_options.yaml index 102df89bd..83f9494ae 100644 --- a/app/analysis_options.yaml +++ b/app/analysis_options.yaml @@ -24,6 +24,8 @@ linter: # avoid_print: false # Uncomment to disable the `avoid_print` rule # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule unnecessary_string_escapes: false + # always_declare_return_types: true + # prefer_const_declarations: true # Additional information about this file can be found at # https://dart.dev/guides/language/analysis-options \ No newline at end of file diff --git a/app/lib/main.dart b/app/lib/main.dart index c9539d6d7..d0de82428 100644 --- a/app/lib/main.dart +++ b/app/lib/main.dart @@ -17,6 +17,7 @@ import 'package:friend_private/firebase_options_prod.dart' as prod; import 'package:friend_private/flavors.dart'; import 'package:friend_private/pages/home/page.dart'; import 'package:friend_private/pages/onboarding/wrapper.dart'; +import 'package:friend_private/providers/auth_provider.dart'; import 'package:friend_private/providers/home_provider.dart'; import 'package:friend_private/providers/memory_provider.dart'; import 'package:friend_private/providers/message_provider.dart'; @@ -132,6 +133,7 @@ class _MyAppState extends State { ListenableProvider(create: (context) => HomeProvider()), ListenableProvider(create: (context) => MessageProvider()), ListenableProvider(create: (context) => MemoryProvider()), + ChangeNotifierProvider(create: (context) => AuthenticationProvider()), ], builder: (context, child) { return WithForegroundTask( diff --git a/app/lib/pages/onboarding/auth.dart b/app/lib/pages/onboarding/auth.dart index 5d1aed908..ff561ed0d 100644 --- a/app/lib/pages/onboarding/auth.dart +++ b/app/lib/pages/onboarding/auth.dart @@ -1,16 +1,11 @@ import 'dart:io'; -import 'package:firebase_auth/firebase_auth.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; -import 'package:friend_private/backend/auth.dart'; -import 'package:friend_private/backend/preferences.dart'; -import 'package:friend_private/services/notification_service.dart'; -import 'package:friend_private/utils/analytics/mixpanel.dart'; -import 'package:instabug_flutter/instabug_flutter.dart'; +import 'package:friend_private/providers/auth_provider.dart'; +import 'package:provider/provider.dart'; import 'package:sign_in_button/sign_in_button.dart'; import 'package:sign_in_with_apple/sign_in_with_apple.dart'; -import 'package:url_launcher/url_launcher.dart'; class AuthComponent extends StatefulWidget { final VoidCallback onSignIn; @@ -22,120 +17,64 @@ class AuthComponent extends StatefulWidget { } class _AuthComponentState extends State { - bool loading = false; - - changeLoadingState() => setState(() => loading = !loading); - @override Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Column( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Center( - child: SizedBox( - height: 24, - width: 24, - child: loading - ? const CircularProgressIndicator( - valueColor: AlwaysStoppedAnimation(Colors.white), - ) - : null, - ), - ), - const SizedBox(height: 32), - !Platform.isIOS - ? SignInButton( - Buttons.google, - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), - onPressed: loading - ? () {} - : () async { - changeLoadingState(); - await signInWithGoogle(); - _signIn(); - changeLoadingState(); - }, - ) - : SignInWithAppleButton( - style: SignInWithAppleButtonStyle.whiteOutlined, - onPressed: loading - ? () {} - : () async { - changeLoadingState(); - await signInWithApple(); - _signIn(); - changeLoadingState(); - }, - height: 52, - ), - const SizedBox(height: 16), - RichText( - textAlign: TextAlign.center, - text: TextSpan( - style: const TextStyle(color: Colors.white, fontSize: 12), - children: [ - const TextSpan(text: 'By Signing in, you agree to our\n'), - TextSpan( - text: 'Terms of service', - style: const TextStyle(decoration: TextDecoration.underline), - recognizer: TapGestureRecognizer()..onTap = () => _launchUrl('https://basedhardware.com/terms'), + return Consumer( + builder: (context, provider, child) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Center( + child: SizedBox( + height: 24, + width: 24, + child: provider.loading + ? const CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation(Colors.white), + ) + : null, ), - const TextSpan(text: ' and '), - TextSpan( - text: 'Privacy Policy', - style: const TextStyle(decoration: TextDecoration.underline), - recognizer: TapGestureRecognizer() - ..onTap = () { - _launchUrl('https://basedhardware.com/privacy-policy'); - }, + ), + const SizedBox(height: 32), + !Platform.isIOS + ? SignInButton( + Buttons.google, + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + onPressed: () => provider.onGoogleSignIn(widget.onSignIn), + ) + : SignInWithAppleButton( + style: SignInWithAppleButtonStyle.whiteOutlined, + onPressed: () => provider.onAppleSignIn(widget.onSignIn), + height: 52, + ), + const SizedBox(height: 16), + RichText( + textAlign: TextAlign.center, + text: TextSpan( + style: const TextStyle(color: Colors.white, fontSize: 12), + children: [ + const TextSpan(text: 'By Signing in, you agree to our\n'), + TextSpan( + text: 'Terms of service', + style: const TextStyle(decoration: TextDecoration.underline), + recognizer: TapGestureRecognizer()..onTap = provider.openTermsOfService, + ), + const TextSpan(text: ' and '), + TextSpan( + text: 'Privacy Policy', + style: const TextStyle(decoration: TextDecoration.underline), + recognizer: TapGestureRecognizer()..onTap = provider.openPrivacyPolicy, + ), + ], ), - ], - ), + ), + ], ), - ], - ), + ); + }, ); } - - void _signIn() async { - String? token; - try { - token = await getIdToken(); - NotificationService.instance.saveNotificationToken(); - } catch (e, stackTrace) { - ScaffoldMessenger.of(context).showSnackBar(const SnackBar( - content: Text('Failed to retrieve firebase token, please try again.'), - )); - CrashReporting.reportHandledCrash(e, stackTrace, level: NonFatalExceptionLevel.error); - return; - } - debugPrint('Token: $token'); - if (token != null) { - User user; - try { - user = FirebaseAuth.instance.currentUser!; - } catch (e, stackTrace) { - ScaffoldMessenger.of(context).showSnackBar(const SnackBar( - content: Text('Unexpected error signing in, Firebase error, please try again.'), - )); - CrashReporting.reportHandledCrash(e, stackTrace, level: NonFatalExceptionLevel.error); - return; - } - String newUid = user.uid; - SharedPreferencesUtil().uid = newUid; - MixpanelManager().identify(); - widget.onSignIn(); - } else { - ScaffoldMessenger.of(context).showSnackBar(const SnackBar( - content: Text('Unexpected error signing in, please try again.'), - )); - } - } - - void _launchUrl(String url) async { - if (!await launchUrl(Uri.parse(url))) throw 'Could not launch $url'; - } } diff --git a/app/lib/providers/auth_provider.dart b/app/lib/providers/auth_provider.dart new file mode 100644 index 000000000..584894e27 --- /dev/null +++ b/app/lib/providers/auth_provider.dart @@ -0,0 +1,80 @@ +import 'package:firebase_auth/firebase_auth.dart'; +import 'package:flutter/material.dart'; +import 'package:friend_private/backend/auth.dart'; +import 'package:friend_private/backend/preferences.dart'; +import 'package:friend_private/providers/base_provider.dart'; +import 'package:friend_private/services/notification_service.dart'; +import 'package:friend_private/utils/alerts/app_snackbar.dart'; +import 'package:friend_private/utils/analytics/mixpanel.dart'; +import 'package:instabug_flutter/instabug_flutter.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class AuthenticationProvider extends BaseProvider { + Future onGoogleSignIn(Function() onSignIn) async { + if (!loading) { + changeLoadingState(); + await signInWithGoogle(); + _signIn(onSignIn); + changeLoadingState(); + } + } + + Future onAppleSignIn(Function() onSignIn) async { + if (!loading) { + changeLoadingState(); + await signInWithApple(); + _signIn(onSignIn); + changeLoadingState(); + } + } + + Future _getIdToken() async { + try { + final token = await getIdToken(); + NotificationService.instance.saveNotificationToken(); + + debugPrint('Token: $token'); + return token; + } catch (e, stackTrace) { + AppSnackbar.showSnackbarError('Failed to retrieve firebase token, please try again.'); + + CrashReporting.reportHandledCrash(e, stackTrace, level: NonFatalExceptionLevel.error); + + return null; + } + } + + void _signIn(Function() onSignIn) async { + String? token = await _getIdToken(); + + if (token != null) { + User user; + try { + user = FirebaseAuth.instance.currentUser!; + } catch (e, stackTrace) { + AppSnackbar.showSnackbarError('Unexpected error signing in, Firebase error, please try again.'); + + CrashReporting.reportHandledCrash(e, stackTrace, level: NonFatalExceptionLevel.error); + return; + } + String newUid = user.uid; + SharedPreferencesUtil().uid = newUid; + MixpanelManager().identify(); + onSignIn(); + } else { + AppSnackbar.showSnackbarError('Unexpected error signing in, please try again'); + } + } + + void openTermsOfService() { + _launchUrl('https://basedhardware.com/terms'); + } + + void openPrivacyPolicy() { + _launchUrl('https://basedhardware.com/privacy-policy'); + } + + void _launchUrl(String url) async { + if (!await launchUrl(Uri.parse(url))) throw 'Could not launch $url'; + } +} diff --git a/app/lib/providers/base_provider.dart b/app/lib/providers/base_provider.dart new file mode 100644 index 000000000..bed35bd21 --- /dev/null +++ b/app/lib/providers/base_provider.dart @@ -0,0 +1,10 @@ +import 'package:flutter/material.dart'; + +class BaseProvider extends ChangeNotifier { + bool loading = false; + + void changeLoadingState() { + loading = !loading; + notifyListeners(); + } +} diff --git a/app/lib/utils/alerts/app_snackbar.dart b/app/lib/utils/alerts/app_snackbar.dart new file mode 100644 index 000000000..64a860507 --- /dev/null +++ b/app/lib/utils/alerts/app_snackbar.dart @@ -0,0 +1,17 @@ +import 'package:flutter/material.dart'; +import 'package:friend_private/main.dart'; + +class AppSnackbar { + static void showSnackbar(String message, {Color? color}) { + ScaffoldMessenger.of(MyApp.navigatorKey.currentState!.context).showSnackBar( + SnackBar( + content: Text(message), + backgroundColor: color ?? Colors.red, + ), + ); + } + + static void showSnackbarError(String message) { + showSnackbar(message, color: Colors.red); + } +} From 566a6a7946ad61dc528e9b78a958d5e86ce1d2d0 Mon Sep 17 00:00:00 2001 From: Becca-Saka Date: Thu, 22 Aug 2024 03:48:15 +0100 Subject: [PATCH 03/23] chore: found device refactor --- app/lib/main.dart | 2 + .../onboarding/find_device/found_devices.dart | 308 +++++++----------- app/lib/pages/onboarding/wrapper.dart | 162 ++++----- app/lib/providers/onboarding_provider.dart | 99 ++++++ app/lib/widgets/extensions/functions.dart | 7 + 5 files changed, 314 insertions(+), 264 deletions(-) create mode 100644 app/lib/providers/onboarding_provider.dart create mode 100644 app/lib/widgets/extensions/functions.dart diff --git a/app/lib/main.dart b/app/lib/main.dart index d0de82428..8be0ee38a 100644 --- a/app/lib/main.dart +++ b/app/lib/main.dart @@ -21,6 +21,7 @@ import 'package:friend_private/providers/auth_provider.dart'; import 'package:friend_private/providers/home_provider.dart'; import 'package:friend_private/providers/memory_provider.dart'; import 'package:friend_private/providers/message_provider.dart'; +import 'package:friend_private/providers/onboarding_provider.dart'; import 'package:friend_private/services/notification_service.dart'; import 'package:friend_private/utils/analytics/growthbook.dart'; import 'package:friend_private/utils/analytics/mixpanel.dart'; @@ -134,6 +135,7 @@ class _MyAppState extends State { ListenableProvider(create: (context) => MessageProvider()), ListenableProvider(create: (context) => MemoryProvider()), ChangeNotifierProvider(create: (context) => AuthenticationProvider()), + ChangeNotifierProvider(create: (context) => OnboardingProvider()), ], builder: (context, child) { return WithForegroundTask( diff --git a/app/lib/pages/onboarding/find_device/found_devices.dart b/app/lib/pages/onboarding/find_device/found_devices.dart index 5f23ac4fd..f24a95abd 100644 --- a/app/lib/pages/onboarding/find_device/found_devices.dart +++ b/app/lib/pages/onboarding/find_device/found_devices.dart @@ -1,13 +1,10 @@ -import 'dart:async'; - import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; -import 'package:friend_private/backend/preferences.dart'; import 'package:friend_private/backend/schema/bt_device.dart'; -import 'package:friend_private/utils/ble/communication.dart'; -import 'package:friend_private/utils/ble/connect.dart'; -import 'package:friend_private/utils/ble/connected.dart'; +import 'package:friend_private/providers/onboarding_provider.dart'; +import 'package:friend_private/widgets/extensions/functions.dart'; import 'package:gradient_borders/gradient_borders.dart'; +import 'package:provider/provider.dart'; class FoundDevices extends StatefulWidget { final List deviceList; @@ -20,58 +17,10 @@ class FoundDevices extends StatefulWidget { }); @override - _FoundDevicesState createState() => _FoundDevicesState(); + State createState() => _FoundDevicesState(); } class _FoundDevicesState extends State { - bool _isClicked = false; - bool _isConnected = false; - int batteryPercentage = -1; - String deviceName = ''; - String deviceId = ''; - String? _connectingToDeviceId; - - Timer? connectionStateTimer; - - // TODO: improve this and find_device page. - // TODO: include speech profile, once it's well tested, in a few days, rn current version works - - Future setBatteryPercentage(BTDeviceStruct btDevice) async { - try { - var battery = await retrieveBatteryLevel(btDevice.id); - setState(() { - batteryPercentage = battery; - _isConnected = true; - _isClicked = false; // Allow clicks again after finishing the operation - _connectingToDeviceId = null; // Reset the connecting device - }); - await Future.delayed(const Duration(seconds: 2)); - SharedPreferencesUtil().btDeviceStruct = btDevice; - SharedPreferencesUtil().deviceName = btDevice.name; - widget.goNext(); - } catch (e) { - print("Error fetching battery level: $e"); - setState(() { - _isClicked = false; // Allow clicks again if an error occurs - _connectingToDeviceId = null; // Reset the connecting device - }); - } - } - - // Method to handle taps on devices - Future handleTap(BTDeviceStruct device) async { - if (_isClicked) return; // if any item is clicked, don't do anything - setState(() { - _isClicked = true; // Prevent further clicks - _connectingToDeviceId = device.id; // Mark this device as being connected to - }); - await bleConnectDevice(device.id); - deviceId = device.id; - deviceName = device.name; - getAudioCodec(deviceId).then((codec) => SharedPreferencesUtil().deviceCodec = codec); - setBatteryPercentage(device); - } - @override void initState() { _initiateConnectionListener(); @@ -79,149 +28,140 @@ class _FoundDevicesState extends State { } _initiateConnectionListener() async { - connectionStateTimer = Timer.periodic(const Duration(seconds: 3), (timer) async { - var connectedDevice = await getConnectedDevice(); - if (connectedDevice != null) { - if (mounted) { - connectionStateTimer?.cancel(); - var battery = await retrieveBatteryLevel(connectedDevice.id); - setState(() { - deviceName = connectedDevice.name; - deviceId = connectedDevice.id; - batteryPercentage = battery; - _isConnected = true; - _isClicked = false; - _connectingToDeviceId = null; - }); - await Future.delayed(const Duration(seconds: 2)); - widget.goNext(); - } - } - }); - } - - @override - void dispose() { - connectionStateTimer?.cancel(); - super.dispose(); + () { + final onboarding = Provider.of(context, listen: false); + onboarding.initiateConnectionListener( + mounted: mounted, + goNext: widget.goNext, + ); + }.withPostFrameCallback(); } @override Widget build(BuildContext context) { - return Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - !_isConnected - ? Text( - widget.deviceList.isEmpty - ? 'Searching for devices...' - : '${widget.deviceList.length} ${widget.deviceList.length == 1 ? "DEVICE" : "DEVICES"} FOUND NEARBY', - style: const TextStyle( - fontWeight: FontWeight.w400, - fontSize: 14, - color: Color(0x66FFFFFF), - ), - ) - : const Text( - 'PAIRING SUCCESSFUL', - style: TextStyle( - fontWeight: FontWeight.w400, - fontSize: 12, - color: Color(0x66FFFFFF), + return Consumer(builder: (context, provider, child) { + return Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + !provider.isConnected + ? Text( + widget.deviceList.isEmpty + ? 'Searching for devices...' + : '${widget.deviceList.length} ${widget.deviceList.length == 1 ? "DEVICE" : "DEVICES"} FOUND NEARBY', + style: const TextStyle( + fontWeight: FontWeight.w400, + fontSize: 14, + color: Color(0x66FFFFFF), + ), + ) + : const Text( + 'PAIRING SUCCESSFUL', + style: TextStyle( + fontWeight: FontWeight.w400, + fontSize: 12, + color: Color(0x66FFFFFF), + ), ), + if (widget.deviceList.isNotEmpty) const SizedBox(height: 16), + if (!provider.isConnected) ..._devicesList(provider), + if (provider.isConnected) + Text( + '${provider.deviceName} (${BTDeviceStruct.shortId(provider.deviceId)})', + textAlign: TextAlign.center, + style: const TextStyle( + fontWeight: FontWeight.w500, + fontSize: 18, + color: Color(0xCCFFFFFF), ), - if (widget.deviceList.isNotEmpty) const SizedBox(height: 16), - if (!_isConnected) ..._devicesList(), - if (_isConnected) - Text( - '$deviceName (${BTDeviceStruct.shortId(deviceId)})', - textAlign: TextAlign.center, - style: const TextStyle( - fontWeight: FontWeight.w500, - fontSize: 18, - color: Color(0xCCFFFFFF), ), - ), - if (_isConnected) - Padding( - padding: const EdgeInsets.symmetric(vertical: 10), - child: Text( - '🔋 ${batteryPercentage.toString()}%', - textAlign: TextAlign.center, - style: TextStyle( - fontWeight: FontWeight.w500, - fontSize: 18, - color: batteryPercentage <= 25 - ? Colors.red - : batteryPercentage > 25 && batteryPercentage <= 50 - ? Colors.orange - : Colors.green, - ), - )) - ], - ); + if (provider.isConnected) + Padding( + padding: const EdgeInsets.symmetric(vertical: 10), + child: Text( + '🔋 ${provider.batteryPercentage.toString()}%', + textAlign: TextAlign.center, + style: TextStyle( + fontWeight: FontWeight.w500, + fontSize: 18, + color: provider.batteryPercentage <= 25 + ? Colors.red + : provider.batteryPercentage > 25 && provider.batteryPercentage <= 50 + ? Colors.orange + : Colors.green, + ), + )) + ], + ); + }); } - _devicesList() { - return (widget.deviceList.mapIndexed((index, device) { - bool isConnecting = _connectingToDeviceId == device.id; + _devicesList(OnboardingProvider provider) { + return (widget.deviceList.mapIndexed( + (index, device) { + bool isConnecting = provider.connectingToDeviceId == device.id; - return GestureDetector( - onTap: !_isClicked ? () => handleTap(device) : null, - child: Container( - margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), - decoration: BoxDecoration( - border: const GradientBoxBorder( - gradient: LinearGradient(colors: [ - Color.fromARGB(127, 208, 208, 208), - Color.fromARGB(127, 188, 99, 121), - Color.fromARGB(127, 86, 101, 182), - Color.fromARGB(127, 126, 190, 236) - ]), - width: 1, + return GestureDetector( + onTap: !provider.isClicked + ? () => provider.handleTap( + device: device, + goNext: widget.goNext, + ) + : null, + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), + decoration: BoxDecoration( + border: const GradientBoxBorder( + gradient: LinearGradient(colors: [ + Color.fromARGB(127, 208, 208, 208), + Color.fromARGB(127, 188, 99, 121), + Color.fromARGB(127, 86, 101, 182), + Color.fromARGB(127, 126, 190, 236) + ]), + width: 1, + ), + borderRadius: BorderRadius.circular(12), ), - borderRadius: BorderRadius.circular(12), - ), - child: Row( - children: [ - Expanded( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Stack( - children: [ - Align( - alignment: Alignment.center, - child: Text( - '${device.name} (${device.getShortId()})', - textAlign: TextAlign.center, - style: const TextStyle( - fontWeight: FontWeight.w500, - fontSize: 18, - color: Color(0xCCFFFFFF), + child: Row( + children: [ + Expanded( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Stack( + children: [ + Align( + alignment: Alignment.center, + child: Text( + '${device.name} (${device.getShortId()})', + textAlign: TextAlign.center, + style: const TextStyle( + fontWeight: FontWeight.w500, + fontSize: 18, + color: Color(0xCCFFFFFF), + ), ), ), - ), - Align( - alignment: Alignment.centerRight, - child: isConnecting - ? const SizedBox( - height: 20, - width: 20, - child: CircularProgressIndicator( - strokeWidth: 2, - valueColor: AlwaysStoppedAnimation(Colors.white), - ), - ) - : const SizedBox.shrink(), // Show loading indicator if connecting - ) - ], + Align( + alignment: Alignment.centerRight, + child: isConnecting + ? const SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator( + strokeWidth: 2, + valueColor: AlwaysStoppedAnimation(Colors.white), + ), + ) + : const SizedBox.shrink(), // Show loading indicator if connecting + ) + ], + ), ), ), - ), - ], - )), - ); - }).toList()); + ], + )), + ); + }, + ).toList()); } } diff --git a/app/lib/pages/onboarding/wrapper.dart b/app/lib/pages/onboarding/wrapper.dart index 40eb8bd0b..277245e3c 100644 --- a/app/lib/pages/onboarding/wrapper.dart +++ b/app/lib/pages/onboarding/wrapper.dart @@ -28,7 +28,8 @@ class _OnboardingWrapperState extends State with TickerProvid _controller = TabController(length: 5, vsync: this); _controller!.addListener(() => setState(() {})); WidgetsBinding.instance.addPostFrameCallback((_) async { - if (isSignedIn()) { // && !SharedPreferencesUtil().onboardingCompleted + if (isSignedIn()) { + // && !SharedPreferencesUtil().onboardingCompleted _goNext(); } }); @@ -48,90 +49,91 @@ class _OnboardingWrapperState extends State with TickerProvid return GestureDetector( onTap: () => FocusScope.of(context).unfocus(), child: Scaffold( - backgroundColor: Theme.of(context).colorScheme.primary, - body: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: ListView( - children: [ - DeviceAnimationWidget(animatedBackground: _controller!.index != -1), - Center( - child: Text( - _controller!.index == _controller!.length - 1 ? 'You are all set 🎉' : 'Friend', - style: TextStyle( - color: Colors.grey.shade200, - fontSize: _controller!.index == _controller!.length - 1 ? 28 : 40, - fontWeight: FontWeight.w500), - ), + backgroundColor: Theme.of(context).colorScheme.primary, + body: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: ListView( + children: [ + DeviceAnimationWidget(animatedBackground: _controller!.index != -1), + Center( + child: Text( + _controller!.index == _controller!.length - 1 ? 'You are all set 🎉' : 'Friend', + style: TextStyle( + color: Colors.grey.shade200, + fontSize: _controller!.index == _controller!.length - 1 ? 28 : 40, + fontWeight: FontWeight.w500), ), - const SizedBox(height: 24), - _controller!.index == 3 || _controller!.index == 4 || _controller!.index == 5 - ? const SizedBox() - : Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: Text( - _controller!.index == _controller!.length - 1 - ? 'Your personal growth journey with AI that listens to your every word.' - : 'Your personal growth journey with AI that listens to your every word.', - style: TextStyle(color: Colors.grey.shade300, fontSize: 16), - textAlign: TextAlign.center, - ), + ), + const SizedBox(height: 24), + _controller!.index == 3 || _controller!.index == 4 || _controller!.index == 5 + ? const SizedBox() + : Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Text( + _controller!.index == _controller!.length - 1 + ? 'Your personal growth journey with AI that listens to your every word.' + : 'Your personal growth journey with AI that listens to your every word.', + style: TextStyle(color: Colors.grey.shade300, fontSize: 16), + textAlign: TextAlign.center, ), - SizedBox( - height: max(MediaQuery.of(context).size.height - 500 - 64, 305), - child: Padding( - padding: EdgeInsets.only(bottom: MediaQuery.sizeOf(context).height <= 700 ? 10 : 64), - child: TabBarView( - controller: _controller, - physics: const NeverScrollableScrollPhysics(), - children: [ - // TODO: if connected already, stop animation and display battery - AuthComponent( - onSignIn: () { - MixpanelManager().onboardingStepICompleted('Auth'); - if (SharedPreferencesUtil().onboardingCompleted) { - // previous users - routeToPage(context, const HomePageWrapper(), replace: true); - } else { - _goNext(); - } - }, - ), - PermissionsPage( - goNext: () { - _goNext(); - MixpanelManager().onboardingStepICompleted('Permissions'); - }, - ), - WelcomePage( - goNext: () { - _goNext(); - MixpanelManager().onboardingStepICompleted('Welcome'); - }, - skipDevice: () { - _controller!.animateTo(_controller!.index + 2); - MixpanelManager().onboardingStepICompleted('Welcome'); - }, - ), - FindDevicesPage( - goNext: () { - _goNext(); - MixpanelManager().onboardingStepICompleted('Find Devices'); - }, - ), - CompletePage( - goNext: () { - routeToPage(context, const HomePageWrapper(), replace: true); - MixpanelManager().onboardingStepICompleted('Finalize'); - MixpanelManager().onboardingCompleted(); - }, - ), - ], ), + SizedBox( + height: max(MediaQuery.of(context).size.height - 500 - 64, 305), + child: Padding( + padding: EdgeInsets.only(bottom: MediaQuery.sizeOf(context).height <= 700 ? 10 : 64), + child: TabBarView( + controller: _controller, + physics: const NeverScrollableScrollPhysics(), + children: [ + // TODO: if connected already, stop animation and display battery + AuthComponent( + onSignIn: () { + MixpanelManager().onboardingStepICompleted('Auth'); + if (SharedPreferencesUtil().onboardingCompleted) { + // previous users + routeToPage(context, const HomePageWrapper(), replace: true); + } else { + _goNext(); + } + }, + ), + PermissionsPage( + goNext: () { + _goNext(); + MixpanelManager().onboardingStepICompleted('Permissions'); + }, + ), + WelcomePage( + goNext: () { + _goNext(); + MixpanelManager().onboardingStepICompleted('Welcome'); + }, + skipDevice: () { + _controller!.animateTo(_controller!.index + 2); + MixpanelManager().onboardingStepICompleted('Welcome'); + }, + ), + FindDevicesPage( + goNext: () { + _goNext(); + MixpanelManager().onboardingStepICompleted('Find Devices'); + }, + ), + CompletePage( + goNext: () { + routeToPage(context, const HomePageWrapper(), replace: true); + MixpanelManager().onboardingStepICompleted('Finalize'); + MixpanelManager().onboardingCompleted(); + }, + ), + ], ), ), - ], - ), - )), + ), + ], + ), + ), + ), ); } } diff --git a/app/lib/providers/onboarding_provider.dart b/app/lib/providers/onboarding_provider.dart new file mode 100644 index 000000000..35d2099d3 --- /dev/null +++ b/app/lib/providers/onboarding_provider.dart @@ -0,0 +1,99 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:friend_private/backend/preferences.dart'; +import 'package:friend_private/backend/schema/bt_device.dart'; +import 'package:friend_private/providers/base_provider.dart'; +import 'package:friend_private/utils/ble/communication.dart'; +import 'package:friend_private/utils/ble/connect.dart'; +import 'package:friend_private/utils/ble/connected.dart'; + +class OnboardingProvider extends BaseProvider { + bool isClicked = false; + bool isConnected = false; + int batteryPercentage = -1; + String deviceName = ''; + String deviceId = ''; + String? connectingToDeviceId; + + Timer? connectionStateTimer; + + // TODO: improve this and find_device page. + // TODO: include speech profile, once it's well tested, in a few days, rn current version works + + Future setBatteryPercentage({ + required BTDeviceStruct btDevice, + required VoidCallback goNext, + }) async { + try { + var battery = await retrieveBatteryLevel(btDevice.id); + + batteryPercentage = battery; + isConnected = true; + isClicked = false; // Allow clicks again after finishing the operation + connectingToDeviceId = null; // Reset the connecting device + notifyListeners(); + await Future.delayed(const Duration(seconds: 2)); + SharedPreferencesUtil().btDeviceStruct = btDevice; + SharedPreferencesUtil().deviceName = btDevice.name; + goNext(); + } catch (e) { + print("Error fetching battery level: $e"); + + isClicked = false; // Allow clicks again if an error occurs + connectingToDeviceId = null; // Reset the connecting device + notifyListeners(); + } + } + + // Method to handle taps on devices + Future handleTap({ + required BTDeviceStruct device, + required VoidCallback goNext, + }) async { + if (isClicked) return; // if any item is clicked, don't do anything + + isClicked = true; // Prevent further clicks + connectingToDeviceId = device.id; // Mark this device as being connected to + notifyListeners(); + await bleConnectDevice(device.id); + deviceId = device.id; + deviceName = device.name; + getAudioCodec(deviceId).then((codec) => SharedPreferencesUtil().deviceCodec = codec); + setBatteryPercentage( + btDevice: device, + goNext: goNext, + ); + } + + void initiateConnectionListener({ + required bool mounted, + required VoidCallback goNext, + }) async { + connectionStateTimer = Timer.periodic(const Duration(seconds: 3), (timer) async { + var connectedDevice = await getConnectedDevice(); + if (connectedDevice != null) { + if (mounted) { + connectionStateTimer?.cancel(); + var battery = await retrieveBatteryLevel(connectedDevice.id); + + deviceName = connectedDevice.name; + deviceId = connectedDevice.id; + batteryPercentage = battery; + isConnected = true; + isClicked = false; + connectingToDeviceId = null; + notifyListeners(); + await Future.delayed(const Duration(seconds: 2)); + goNext(); + } + } + }); + } + + @override + void dispose() { + connectionStateTimer?.cancel(); + super.dispose(); + } +} diff --git a/app/lib/widgets/extensions/functions.dart b/app/lib/widgets/extensions/functions.dart new file mode 100644 index 000000000..5be375553 --- /dev/null +++ b/app/lib/widgets/extensions/functions.dart @@ -0,0 +1,7 @@ +import 'package:flutter/material.dart'; + +extension FunctionExt on Function { + void withPostFrameCallback() => WidgetsBinding.instance.addPostFrameCallback((_) { + this(); + }); +} From 695aa6b0e1ce162a5774d77de97a225e664f31c3 Mon Sep 17 00:00:00 2001 From: Becca-Saka Date: Thu, 22 Aug 2024 05:20:43 +0100 Subject: [PATCH 04/23] scan device refactor --- .../pages/onboarding/find_device/page.dart | 187 +++++++----------- app/lib/providers/onboarding_provider.dart | 62 +++++- app/lib/widgets/dialog.dart | 4 +- 3 files changed, 133 insertions(+), 120 deletions(-) diff --git a/app/lib/pages/onboarding/find_device/page.dart b/app/lib/pages/onboarding/find_device/page.dart index bc02b46d7..54ffaa926 100644 --- a/app/lib/pages/onboarding/find_device/page.dart +++ b/app/lib/pages/onboarding/find_device/page.dart @@ -1,13 +1,11 @@ import 'dart:async'; -import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; -import 'package:flutter_blue_plus/flutter_blue_plus.dart'; -import 'package:friend_private/backend/schema/bt_device.dart'; +import 'package:friend_private/providers/onboarding_provider.dart'; import 'package:friend_private/utils/analytics/mixpanel.dart'; -import 'package:friend_private/utils/ble/find.dart'; import 'package:friend_private/widgets/dialog.dart'; +import 'package:provider/provider.dart'; import 'package:url_launcher/url_launcher.dart'; import 'found_devices.dart'; @@ -19,15 +17,10 @@ class FindDevicesPage extends StatefulWidget { const FindDevicesPage({super.key, required this.goNext, this.includeSkip = true}); @override - _FindDevicesPageState createState() => _FindDevicesPageState(); + State createState() => _FindDevicesPageState(); } class _FindDevicesPageState extends State { - List deviceList = []; - late Timer _didNotMakeItTimer; - late Timer _findDevicesTimer; - bool enableInstructions = false; - @override void initState() { super.initState(); @@ -36,125 +29,83 @@ class _FindDevicesPageState extends State { }); } - @override - void dispose() { - _findDevicesTimer.cancel(); - _didNotMakeItTimer.cancel(); - super.dispose(); - } - Future _scanDevices() async { - // check if bluetooth is enabled on Android - if (Platform.isAndroid) { - if (FlutterBluePlus.adapterStateNow != BluetoothAdapterState.on) { - try { - await FlutterBluePlus.turnOn(); - } catch (e) { - if (e is FlutterBluePlusException) { - if (e.code == 11) { - if (mounted) { - showDialog( - context: context, - builder: (c) => getDialog( - context, - () { - Navigator.of(context).pop(); - }, - () {}, - 'Enable Bluetooth', - 'Friend needs Bluetooth to connect to your wearable. Please enable Bluetooth and try again.', - singleButton: true, - ), - ); - } - } - } - } - } - } - - _didNotMakeItTimer = Timer(const Duration(seconds: 10), () => setState(() => enableInstructions = true)); - // Update foundDevicesMap with new devices and remove the ones not found anymore - Map foundDevicesMap = {}; - - _findDevicesTimer = Timer.periodic(const Duration(seconds: 2), (timer) async { - List foundDevices = await bleFindDevices(); - - // Update foundDevicesMap with new devices and remove the ones not found anymore - Map updatedDevicesMap = {}; - for (final device in foundDevices) { - // If it's a new device, add it to the map. If it already exists, this will just update the entry. - updatedDevicesMap[device.id] = device; - } - // Remove devices that are no longer found - foundDevicesMap.keys.where((id) => !updatedDevicesMap.containsKey(id)).toList().forEach(foundDevicesMap.remove); - - // Merge the new devices into the current map to maintain order - foundDevicesMap.addAll(updatedDevicesMap); - - // Convert the values of the map back to a list - List orderedDevices = foundDevicesMap.values.toList(); - - if (orderedDevices.isNotEmpty) { + Provider.of(context, listen: false).scanDevices( + onShowDialog: () { if (mounted) { - setState(() { - deviceList = orderedDevices; - }); + showDialog( + context: context, + builder: (c) => getDialog( + context, + () { + Navigator.of(context).pop(); + }, + () {}, + 'Enable Bluetooth', + 'Friend needs Bluetooth to connect to your wearable. Please enable Bluetooth and try again.', + singleButton: true, + ), + ); } - - _didNotMakeItTimer.cancel(); - } - }); + }, + ); } @override Widget build(BuildContext context) { - return Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - FoundDevices(deviceList: deviceList, goNext: widget.goNext), - if (deviceList.isEmpty && enableInstructions) const SizedBox(height: 48), - if (deviceList.isEmpty && enableInstructions) - ElevatedButton( - onPressed: () => launchUrl(Uri.parse('mailto:team@basedhardware.com')), - child: Container( - width: double.infinity, - height: 45, - alignment: Alignment.center, - child: const Text( - 'Contact Support?', - style: TextStyle( - fontWeight: FontWeight.w400, - fontSize: 16, - color: Colors.white, - decoration: TextDecoration.underline, + return Consumer( + builder: (context, provider, child) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + FoundDevices( + deviceList: provider.deviceList, + goNext: widget.goNext, + ), + if (provider.deviceList.isEmpty && provider.enableInstructions) const SizedBox(height: 48), + if (provider.deviceList.isEmpty && provider.enableInstructions) + ElevatedButton( + onPressed: () => launchUrl(Uri.parse('mailto:team@basedhardware.com')), + child: Container( + width: double.infinity, + height: 45, + alignment: Alignment.center, + child: const Text( + 'Contact Support?', + style: TextStyle( + fontWeight: FontWeight.w400, + fontSize: 16, + color: Colors.white, + decoration: TextDecoration.underline, + ), + ), ), ), - ), - ), - if (widget.includeSkip && deviceList.isEmpty) - ElevatedButton( - onPressed: () { - widget.goNext(); - MixpanelManager().useWithoutDeviceOnboardingFindDevices(); - }, - child: Container( - width: double.infinity, - height: 45, - alignment: Alignment.center, - child: const Text( - 'Connect Later', - style: TextStyle( - fontWeight: FontWeight.w400, - fontSize: 16, - color: Colors.white, - // decoration: TextDecoration.underline, + if (widget.includeSkip && provider.deviceList.isEmpty) + ElevatedButton( + onPressed: () { + widget.goNext(); + MixpanelManager().useWithoutDeviceOnboardingFindDevices(); + }, + child: Container( + width: double.infinity, + height: 45, + alignment: Alignment.center, + child: const Text( + 'Connect Later', + style: TextStyle( + fontWeight: FontWeight.w400, + fontSize: 16, + color: Colors.white, + // decoration: TextDecoration.underline, + ), + ), ), ), - ), - ), - ], + ], + ); + }, ); } } diff --git a/app/lib/providers/onboarding_provider.dart b/app/lib/providers/onboarding_provider.dart index 35d2099d3..c9c4ec2c3 100644 --- a/app/lib/providers/onboarding_provider.dart +++ b/app/lib/providers/onboarding_provider.dart @@ -1,12 +1,16 @@ import 'dart:async'; +import 'dart:io'; import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_blue_plus/flutter_blue_plus.dart'; import 'package:friend_private/backend/preferences.dart'; import 'package:friend_private/backend/schema/bt_device.dart'; import 'package:friend_private/providers/base_provider.dart'; import 'package:friend_private/utils/ble/communication.dart'; import 'package:friend_private/utils/ble/connect.dart'; import 'package:friend_private/utils/ble/connected.dart'; +import 'package:friend_private/utils/ble/find.dart'; class OnboardingProvider extends BaseProvider { bool isClicked = false; @@ -15,8 +19,11 @@ class OnboardingProvider extends BaseProvider { String deviceName = ''; String deviceId = ''; String? connectingToDeviceId; - Timer? connectionStateTimer; + List deviceList = []; + late Timer _didNotMakeItTimer; + late Timer _findDevicesTimer; + bool enableInstructions = false; // TODO: improve this and find_device page. // TODO: include speech profile, once it's well tested, in a few days, rn current version works @@ -91,8 +98,61 @@ class OnboardingProvider extends BaseProvider { }); } + Future scanDevices({ + required VoidCallback onShowDialog, + }) async { + // check if bluetooth is enabled on Android + if (Platform.isAndroid) { + if (FlutterBluePlus.adapterStateNow != BluetoothAdapterState.on) { + try { + await FlutterBluePlus.turnOn(); + } catch (e) { + if (e is FlutterBluePlusException) { + if (e.code == 11) { + onShowDialog(); + } + } + } + } + } + + _didNotMakeItTimer = Timer(const Duration(seconds: 10), () { + enableInstructions = true; + notifyListeners(); + }); + // Update foundDevicesMap with new devices and remove the ones not found anymore + Map foundDevicesMap = {}; + + _findDevicesTimer = Timer.periodic(const Duration(seconds: 2), (timer) async { + List foundDevices = await bleFindDevices(); + + // Update foundDevicesMap with new devices and remove the ones not found anymore + Map updatedDevicesMap = {}; + for (final device in foundDevices) { + // If it's a new device, add it to the map. If it already exists, this will just update the entry. + updatedDevicesMap[device.id] = device; + } + // Remove devices that are no longer found + foundDevicesMap.keys.where((id) => !updatedDevicesMap.containsKey(id)).toList().forEach(foundDevicesMap.remove); + + // Merge the new devices into the current map to maintain order + foundDevicesMap.addAll(updatedDevicesMap); + + // Convert the values of the map back to a list + List orderedDevices = foundDevicesMap.values.toList(); + + if (orderedDevices.isNotEmpty) { + deviceList = orderedDevices; + notifyListeners(); + _didNotMakeItTimer.cancel(); + } + }); + } + @override void dispose() { + _findDevicesTimer.cancel(); + _didNotMakeItTimer.cancel(); connectionStateTimer?.cancel(); super.dispose(); } diff --git a/app/lib/widgets/dialog.dart b/app/lib/widgets/dialog.dart index e3b597683..a668f8aca 100644 --- a/app/lib/widgets/dialog.dart +++ b/app/lib/widgets/dialog.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +//TODO: switch to required named parameters getDialog( BuildContext context, Function onCancel, @@ -24,7 +25,8 @@ getDialog( onPressed: () => onCancel(), child: const Text('Cancel', style: TextStyle(color: Colors.white)), ), - TextButton(onPressed: () => onConfirm(), child: Text(okButtonText, style: TextStyle(color: Colors.white))), + TextButton( + onPressed: () => onConfirm(), child: Text(okButtonText, style: const TextStyle(color: Colors.white))), ]; if (Platform.isIOS) { return CupertinoAlertDialog(title: Text(title), content: Text(content), actions: actions); From f71f480667f413135a4240293617d154bdced62a Mon Sep 17 00:00:00 2001 From: Becca-Saka Date: Thu, 22 Aug 2024 06:03:23 +0100 Subject: [PATCH 05/23] calendar --- app/lib/pages/home/page.dart | 2 +- app/lib/pages/settings/calendar.dart | 199 +++++++++-------------- app/lib/providers/calendar_provider.dart | 71 ++++++++ app/lib/utils/alerts/app_snackbar.dart | 12 +- 4 files changed, 156 insertions(+), 128 deletions(-) create mode 100644 app/lib/providers/calendar_provider.dart diff --git a/app/lib/pages/home/page.dart b/app/lib/pages/home/page.dart index 93c046fb9..bfcd90537 100644 --- a/app/lib/pages/home/page.dart +++ b/app/lib/pages/home/page.dart @@ -22,8 +22,8 @@ import 'package:friend_private/pages/memories/page.dart'; import 'package:friend_private/pages/plugins/page.dart'; import 'package:friend_private/pages/settings/page.dart'; import 'package:friend_private/providers/home_provider.dart'; -import 'package:friend_private/providers/message_provider.dart'; import 'package:friend_private/providers/memory_provider.dart' as mp; +import 'package:friend_private/providers/message_provider.dart'; import 'package:friend_private/scripts.dart'; import 'package:friend_private/services/notification_service.dart'; import 'package:friend_private/utils/analytics/mixpanel.dart'; diff --git a/app/lib/pages/settings/calendar.dart b/app/lib/pages/settings/calendar.dart index 8ba329b45..3585233be 100644 --- a/app/lib/pages/settings/calendar.dart +++ b/app/lib/pages/settings/calendar.dart @@ -1,31 +1,35 @@ -import 'package:device_calendar/device_calendar.dart'; import 'package:flutter/material.dart'; -import 'package:friend_private/utils/analytics/mixpanel.dart'; import 'package:friend_private/backend/preferences.dart'; -import 'package:friend_private/utils/features/calendar.dart'; +import 'package:friend_private/providers/calendar_provider.dart'; +import 'package:friend_private/widgets/extensions/functions.dart'; import 'package:gradient_borders/box_borders/gradient_box_border.dart'; +import 'package:provider/provider.dart'; -class CalendarPage extends StatefulWidget { +class CalendarPage extends StatelessWidget { const CalendarPage({super.key}); @override - State createState() => _CalendarPageState(); + Widget build(BuildContext context) { + return ChangeNotifierProvider( + create: (context) => CalenderProvider(), + child: const _CalendarPage(), + ); + } } -class _CalendarPageState extends State { - List calendars = []; - bool calendarEnabled = false; +class _CalendarPage extends StatefulWidget { + const _CalendarPage({super.key}); - _getCalendars() async { - await CalendarUtil().getCalendars().then((value) { - setState(() => calendars = value); - }); - } + @override + State<_CalendarPage> createState() => __CalendarPageState(); +} +class __CalendarPageState extends State<_CalendarPage> { @override void initState() { - calendarEnabled = SharedPreferencesUtil().calendarEnabled; - if (calendarEnabled) _getCalendars(); + () { + Provider.of(context, listen: false).initialize(); + }.withPostFrameCallback(); super.initState(); } @@ -38,78 +42,70 @@ class _CalendarPageState extends State { elevation: 0, ), backgroundColor: Theme.of(context).colorScheme.primary, - body: ListView( - children: [ - Container( - margin: const EdgeInsets.all(8), - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Row( + body: Consumer( + builder: (context, provider, child) { + return ListView( + children: [ + Container( + margin: const EdgeInsets.all(8), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Icon(Icons.edit_calendar), - SizedBox(width: 16), - Text( - 'Enable integration', - style: TextStyle( - color: Colors.white, - fontSize: 16, - ), + const Row( + children: [ + Icon(Icons.edit_calendar), + SizedBox(width: 16), + Text( + 'Enable integration', + style: TextStyle( + color: Colors.white, + fontSize: 16, + ), + ), + ], + ), + Switch( + value: provider.calendarEnabled, + onChanged: provider.onCalendarSwitchChanged, ), ], ), - Switch( - value: calendarEnabled, - onChanged: _onSwitchChanged, + ), + const Text( + 'Friend can automatically schedule events from your conversations, or ask for your confirmation first.', + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.grey, + ), + ), + const SizedBox(height: 24), + if (provider.calendarEnabled) ...[ + RadioListTile( + title: const Text('Automatic'), + subtitle: const Text('AI Will automatically scheduled your events.'), + value: 'auto', + groupValue: SharedPreferencesUtil().calendarType, + onChanged: provider.onCalendarTypeChanged, + ), + RadioListTile( + title: const Text('Manual'), + subtitle: const Text('Your events will be drafted, but you will have to confirm their creation.'), + value: 'manual', + groupValue: SharedPreferencesUtil().calendarType, + onChanged: provider.onCalendarTypeChanged, ), ], - ), - ), - const Text( - 'Friend can automatically schedule events from your conversations, or ask for your confirmation first.', - textAlign: TextAlign.center, - style: TextStyle( - color: Colors.grey, - ), - ), - const SizedBox(height: 24), - if (calendarEnabled) ..._calendarType(), - const SizedBox(height: 24), - if (calendarEnabled) ..._displayCalendars(), - ], - ), - ); - } - - _calendarType() { - return [ - RadioListTile( - title: const Text('Automatic'), - subtitle: const Text('AI Will automatically scheduled your events.'), - value: 'auto', - groupValue: SharedPreferencesUtil().calendarType, - onChanged: (v) { - SharedPreferencesUtil().calendarType = v!; - MixpanelManager().calendarTypeChanged(v); - setState(() {}); - }, - ), - RadioListTile( - title: const Text('Manual'), - subtitle: const Text('Your events will be drafted, but you will have to confirm their creation.'), - value: 'manual', - groupValue: SharedPreferencesUtil().calendarType, - onChanged: (v) { - SharedPreferencesUtil().calendarType = v!; - MixpanelManager().calendarTypeChanged(v); - setState(() {}); + const SizedBox(height: 24), + if (provider.calendarEnabled) ..._displayCalendars(provider), + ], + ); }, ), - ]; + ); } - _displayCalendars() { + _displayCalendars(CalenderProvider provider) { return [ const SizedBox(height: 16), Container( @@ -145,58 +141,15 @@ class _CalendarPageState extends State { ), ), const SizedBox(height: 16), - for (var calendar in calendars) + for (var calendar in provider.calendars) RadioListTile( contentPadding: const EdgeInsets.symmetric(horizontal: 24), title: Text(calendar.name!), subtitle: Text(calendar.accountName!), value: calendar.id!, groupValue: SharedPreferencesUtil().calendarId, - onChanged: (String? value) { - SharedPreferencesUtil().calendarId = value!; - setState(() {}); - MixpanelManager().calendarSelected(); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Calendar ${calendar.name} selected.'), - duration: const Duration(seconds: 1), - ), - ); - }, - ) + onChanged: (v) => provider.selectCalendar(v, calendar), + ), ]; } - - _onSwitchChanged(s) async { - // TODO: what if user didn't enable permissions? - if (s) { - await _getCalendars(); - bool hasAccess = await CalendarUtil().hasCalendarAccess(); - if (calendars.isEmpty && !hasAccess && SharedPreferencesUtil().calendarPermissionAlreadyRequested) { - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Calendar access was not granted previously. Please enable it in your settings.'), - duration: Duration(seconds: 5), - ), - ); - } - return; - } - setState(() { - calendarEnabled = hasAccess; - }); - - MixpanelManager().calendarEnabled(); - } else { - SharedPreferencesUtil().calendarId = ''; - SharedPreferencesUtil().calendarType = 'auto'; - MixpanelManager().calendarDisabled(); - setState(() { - calendarEnabled = s; - }); - } - SharedPreferencesUtil().calendarPermissionAlreadyRequested = await CalendarUtil().calendarPermissionAsked(); - SharedPreferencesUtil().calendarEnabled = await CalendarUtil().hasCalendarAccess() && s; - } } diff --git a/app/lib/providers/calendar_provider.dart b/app/lib/providers/calendar_provider.dart new file mode 100644 index 000000000..21f6bbfd2 --- /dev/null +++ b/app/lib/providers/calendar_provider.dart @@ -0,0 +1,71 @@ +import 'package:device_calendar/device_calendar.dart'; +import 'package:flutter/material.dart'; +import 'package:friend_private/backend/preferences.dart'; +import 'package:friend_private/utils/alerts/app_snackbar.dart'; +import 'package:friend_private/utils/analytics/mixpanel.dart'; +import 'package:friend_private/utils/features/calendar.dart'; + +class CalenderProvider extends ChangeNotifier { + List calendars = []; + bool calendarEnabled = false; + final CalendarUtil _calendarUtil = CalendarUtil(); + final MixpanelManager _mixpanelManager = MixpanelManager(); + final SharedPreferencesUtil _sharedPreferencesUtil = SharedPreferencesUtil(); + void initialize() { + calendarEnabled = _sharedPreferencesUtil.calendarEnabled; + if (calendarEnabled) _getCalendars(); + } + + _getCalendars() async { + await CalendarUtil().getCalendars().then((value) { + calendars = value; + notifyListeners(); + }); + } + + void onCalendarSwitchChanged(bool s) async { + // TODO: what if user didn't enable permissions? + if (s) { + await _getCalendars(); + bool hasAccess = await _calendarUtil.hasCalendarAccess(); + if (calendars.isEmpty && !hasAccess && _sharedPreferencesUtil.calendarPermissionAlreadyRequested) { + AppSnackbar.showSnackbar( + 'Calendar access was not granted previously. Please enable it in your settings.', + duration: const Duration(seconds: 5), + ); + + return; + } + + calendarEnabled = hasAccess; + notifyListeners(); + + _mixpanelManager.calendarEnabled(); + } else { + _sharedPreferencesUtil.calendarId = ''; + _sharedPreferencesUtil.calendarType = 'auto'; + _mixpanelManager.calendarDisabled(); + + calendarEnabled = s; + notifyListeners(); + } + _sharedPreferencesUtil.calendarPermissionAlreadyRequested = await _calendarUtil.calendarPermissionAsked(); + _sharedPreferencesUtil.calendarEnabled = await _calendarUtil.hasCalendarAccess() && s; + } + + void onCalendarTypeChanged(String? v) { + _sharedPreferencesUtil.calendarType = v!; + _mixpanelManager.calendarTypeChanged(v); + notifyListeners(); + } + + void selectCalendar(String? value, Calendar calendar) { + _sharedPreferencesUtil.calendarId = value!; + notifyListeners(); + _mixpanelManager.calendarSelected(); + AppSnackbar.showSnackbar( + 'Calendar ${calendar.name} selected.', + duration: const Duration(seconds: 1), + ); + } +} diff --git a/app/lib/utils/alerts/app_snackbar.dart b/app/lib/utils/alerts/app_snackbar.dart index 64a860507..2bd15ad54 100644 --- a/app/lib/utils/alerts/app_snackbar.dart +++ b/app/lib/utils/alerts/app_snackbar.dart @@ -2,16 +2,20 @@ import 'package:flutter/material.dart'; import 'package:friend_private/main.dart'; class AppSnackbar { - static void showSnackbar(String message, {Color? color}) { + static void showSnackbar(String message, {Color? color, Duration? duration}) { ScaffoldMessenger.of(MyApp.navigatorKey.currentState!.context).showSnackBar( SnackBar( content: Text(message), - backgroundColor: color ?? Colors.red, + backgroundColor: color, ), ); } - static void showSnackbarError(String message) { - showSnackbar(message, color: Colors.red); + static void showSnackbarError(String message, {Duration? duration}) { + showSnackbar( + message, + color: Colors.red, + duration: duration, + ); } } From 315bc49d1285a6723e30be9fb0e34e625cf44b54 Mon Sep 17 00:00:00 2001 From: Becca-Saka Date: Thu, 22 Aug 2024 06:32:11 +0100 Subject: [PATCH 06/23] developer mode --- app/lib/pages/settings/developer.dart | 469 ++++++++---------- app/lib/pages/settings/privacy.dart | 2 +- .../providers/developer_mode_provider.dart | 60 +++ 3 files changed, 281 insertions(+), 250 deletions(-) create mode 100644 app/lib/providers/developer_mode_provider.dart diff --git a/app/lib/pages/settings/developer.dart b/app/lib/pages/settings/developer.dart index a364de459..942595ba0 100644 --- a/app/lib/pages/settings/developer.dart +++ b/app/lib/pages/settings/developer.dart @@ -6,246 +6,245 @@ import 'package:flutter/material.dart'; import 'package:friend_private/backend/database/memory.dart'; import 'package:friend_private/backend/database/memory_provider.dart'; import 'package:friend_private/backend/http/api/memories.dart'; -import 'package:friend_private/backend/http/cloud_storage.dart'; import 'package:friend_private/backend/preferences.dart'; import 'package:friend_private/backend/schema/memory.dart'; +import 'package:friend_private/providers/developer_mode_provider.dart'; +import 'package:friend_private/utils/alerts/app_snackbar.dart'; import 'package:friend_private/utils/analytics/mixpanel.dart'; import 'package:path_provider/path_provider.dart'; +import 'package:provider/provider.dart'; import 'package:share_plus/share_plus.dart'; import 'package:url_launcher/url_launcher.dart'; -class DeveloperSettingsPage extends StatefulWidget { +class DeveloperSettingsPage extends StatelessWidget { const DeveloperSettingsPage({super.key}); @override - State createState() => _DeveloperSettingsPageState(); + Widget build(BuildContext context) { + return ChangeNotifierProvider( + create: (context) => DeveloperModeProvider(), + child: const _DeveloperSettingsPage(), + ); + } } -class _DeveloperSettingsPageState extends State { - final TextEditingController gcpCredentialsController = TextEditingController(); - final TextEditingController gcpBucketNameController = TextEditingController(); - final TextEditingController webhookOnMemoryCreated = TextEditingController(); - final TextEditingController webhookOnTranscriptReceived = TextEditingController(); - - bool savingSettingsLoading = false; - - bool loadingExportMemories = false; - bool loadingImportMemories = false; +class _DeveloperSettingsPage extends StatefulWidget { + const _DeveloperSettingsPage(); @override - void initState() { - gcpCredentialsController.text = SharedPreferencesUtil().gcpCredentials; - gcpBucketNameController.text = SharedPreferencesUtil().gcpBucketName; - webhookOnMemoryCreated.text = SharedPreferencesUtil().webhookOnMemoryCreated; - webhookOnTranscriptReceived.text = SharedPreferencesUtil().webhookOnTranscriptReceived; - super.initState(); - } + State<_DeveloperSettingsPage> createState() => __DeveloperSettingsPageState(); +} +class __DeveloperSettingsPageState extends State<_DeveloperSettingsPage> { @override Widget build(BuildContext context) { return GestureDetector( onTap: () => FocusScope.of(context).unfocus(), - child: Scaffold( - backgroundColor: Theme.of(context).colorScheme.primary, - appBar: AppBar( - backgroundColor: Theme.of(context).colorScheme.primary, - title: const Text('Developer Settings'), - actions: [ - MaterialButton( - onPressed: savingSettingsLoading ? null : saveSettings, - color: Colors.transparent, - elevation: 0, - child: const Padding( - padding: EdgeInsets.symmetric(horizontal: 4.0), - child: Text( - 'Save', - style: TextStyle(color: Colors.deepPurple, fontWeight: FontWeight.w600, fontSize: 16), - ), - ), - ) - ], - ), - body: Padding( - padding: const EdgeInsets.symmetric(horizontal: 32), - child: ListView( - children: [ - const SizedBox(height: 32), - _getText('Store your audios in Google Cloud Storage', bold: true), - const SizedBox(height: 16.0), - TextField( - controller: gcpCredentialsController, - obscureText: false, - autocorrect: false, - enableSuggestions: false, - enabled: true, - decoration: _getTextFieldDecoration('GCP Credentials (Base64)'), - style: const TextStyle(color: Colors.white), - ), - TextField( - controller: gcpBucketNameController, - obscureText: false, - autocorrect: false, - enabled: true, - enableSuggestions: false, - decoration: _getTextFieldDecoration('GCP Bucket Name'), - style: const TextStyle(color: Colors.white), - ), - const SizedBox(height: 16), - ListTile( - title: const Text('Import Memories'), - subtitle: const Text('Use with caution. All memories in the JSON file will be imported.'), - contentPadding: EdgeInsets.zero, - trailing: loadingImportMemories - ? const SizedBox( - height: 16, - width: 16, - child: CircularProgressIndicator( - color: Colors.white, - strokeWidth: 2, - ), - ) - : const Icon(Icons.download), - onTap: () async { - if (loadingImportMemories) return; - setState(() => loadingImportMemories = true); - // open file picker - var file = await FilePicker.platform.pickFiles( - type: FileType.custom, - allowedExtensions: ['json'], - ); - MixpanelManager().importMemories(); - if (file == null) { - setState(() => loadingImportMemories = false); - return; - } - var xFile = file.files.first.xFile; - try { - var content = (await xFile.readAsString()); - var decoded = jsonDecode(content); - List memories = decoded.map((e) => Memory.fromJson(e)).toList(); - debugPrint('Memories: $memories'); - MemoryProvider().storeMemories(memories); - _snackBar('Memories imported, restart the app to see the changes. 🎉', seconds: 3); - MixpanelManager().importedMemories(); - SharedPreferencesUtil().scriptMigrateMemoriesToBack = false; - } catch (e) { - debugPrint(e.toString()); - _snackBar('Make sure the file is a valid JSON file.'); - } - setState(() => loadingImportMemories = false); - }, - ), - ListTile( - contentPadding: EdgeInsets.zero, - title: const Text('Export Memories'), - subtitle: const Text('Export all your memories to a JSON file.'), - trailing: loadingExportMemories - ? const SizedBox( - height: 16, - width: 16, - child: CircularProgressIndicator( - color: Colors.white, - strokeWidth: 2, - ), - ) - : const Icon(Icons.upload), - onTap: loadingExportMemories - ? null - : () async { - if (loadingExportMemories) return; - setState(() => loadingExportMemories = true); - List memories = await getMemories(limit: 10000, offset: 0); // 10k for now - String json = getPrettyJSONString(memories.map((m) => m.toJson()).toList()); - final directory = await getApplicationDocumentsDirectory(); - final file = File('${directory.path}/memories.json'); - await file.writeAsString(json); - - final result = - await Share.shareXFiles([XFile(file.path)], text: 'Exported Memories from Friend'); - if (result.status == ShareResultStatus.success) { - debugPrint('Thank you for sharing the picture!'); - } - MixpanelManager().exportMemories(); - // 54d2c392-57f1-46dc-b944-02740a651f7b - setState(() => loadingExportMemories = false); - }, - ), - const SizedBox(height: 20), - Container( - width: double.infinity, - height: 2, - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.2), - borderRadius: BorderRadius.circular(12), - ), - ), - const SizedBox(height: 20), - Row( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.spaceBetween, + child: Consumer( + builder: (context, provider, child) { + return Scaffold( + backgroundColor: Theme.of(context).colorScheme.primary, + appBar: AppBar( + backgroundColor: Theme.of(context).colorScheme.primary, + title: const Text('Developer Settings'), + actions: [ + MaterialButton( + onPressed: provider.savingSettingsLoading ? null : provider.saveSettings, + color: Colors.transparent, + elevation: 0, + child: const Padding( + padding: EdgeInsets.symmetric(horizontal: 4.0), + child: Text( + 'Save', + style: TextStyle(color: Colors.deepPurple, fontWeight: FontWeight.w600, fontSize: 16), + ), + ), + ) + ], + ), + body: Padding( + padding: const EdgeInsets.symmetric(horizontal: 32), + child: ListView( children: [ - const Text('Plugin Integrations Testing', - style: TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.w600)), - GestureDetector( - onTap: () { - launchUrl(Uri.parse('https://docs.basedhardware.com/developer/plugins/Integrations/')); - MixpanelManager().advancedModeDocsOpened(); - }, - child: const Padding( - padding: EdgeInsets.all(8.0), - child: Text( - 'Docs', - style: TextStyle( - color: Colors.white, - fontSize: 14, - decoration: TextDecoration.underline, - ), - ), - )) + const SizedBox(height: 32), + _getText('Store your audios in Google Cloud Storage', bold: true), + const SizedBox(height: 16.0), + TextField( + controller: provider.gcpCredentialsController, + obscureText: false, + autocorrect: false, + enableSuggestions: false, + enabled: true, + decoration: _getTextFieldDecoration('GCP Credentials (Base64)'), + style: const TextStyle(color: Colors.white), + ), + TextField( + controller: provider.gcpBucketNameController, + obscureText: false, + autocorrect: false, + enabled: true, + enableSuggestions: false, + decoration: _getTextFieldDecoration('GCP Bucket Name'), + style: const TextStyle(color: Colors.white), + ), + const SizedBox(height: 16), + ListTile( + title: const Text('Import Memories'), + subtitle: const Text('Use with caution. All memories in the JSON file will be imported.'), + contentPadding: EdgeInsets.zero, + trailing: provider.loadingImportMemories + ? const SizedBox( + height: 16, + width: 16, + child: CircularProgressIndicator( + color: Colors.white, + strokeWidth: 2, + ), + ) + : const Icon(Icons.download), + onTap: () async { + if (provider.loadingImportMemories) return; + setState(() => provider.loadingImportMemories = true); + // open file picker + var file = await FilePicker.platform.pickFiles( + type: FileType.custom, + allowedExtensions: ['json'], + ); + MixpanelManager().importMemories(); + if (file == null) { + setState(() => provider.loadingImportMemories = false); + return; + } + var xFile = file.files.first.xFile; + try { + var content = (await xFile.readAsString()); + var decoded = jsonDecode(content); + List memories = decoded.map((e) => Memory.fromJson(e)).toList(); + debugPrint('Memories: $memories'); + MemoryProvider().storeMemories(memories); + _snackBar('Memories imported, restart the app to see the changes. 🎉', seconds: 3); + MixpanelManager().importedMemories(); + SharedPreferencesUtil().scriptMigrateMemoriesToBack = false; + } catch (e) { + debugPrint(e.toString()); + _snackBar('Make sure the file is a valid JSON file.'); + } + setState(() => provider.loadingImportMemories = false); + }, + ), + ListTile( + contentPadding: EdgeInsets.zero, + title: const Text('Export Memories'), + subtitle: const Text('Export all your memories to a JSON file.'), + trailing: provider.loadingExportMemories + ? const SizedBox( + height: 16, + width: 16, + child: CircularProgressIndicator( + color: Colors.white, + strokeWidth: 2, + ), + ) + : const Icon(Icons.upload), + onTap: provider.loadingExportMemories + ? null + : () async { + if (provider.loadingExportMemories) return; + setState(() => provider.loadingExportMemories = true); + List memories = await getMemories(limit: 10000, offset: 0); // 10k for now + String json = getPrettyJSONString(memories.map((m) => m.toJson()).toList()); + final directory = await getApplicationDocumentsDirectory(); + final file = File('${directory.path}/memories.json'); + await file.writeAsString(json); + + final result = + await Share.shareXFiles([XFile(file.path)], text: 'Exported Memories from Friend'); + if (result.status == ShareResultStatus.success) { + debugPrint('Thank you for sharing the picture!'); + } + MixpanelManager().exportMemories(); + // 54d2c392-57f1-46dc-b944-02740a651f7b + setState(() => provider.loadingExportMemories = false); + }, + ), + const SizedBox(height: 20), + Container( + width: double.infinity, + height: 2, + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(12), + ), + ), + const SizedBox(height: 20), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text('Plugin Integrations Testing', + style: TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.w600)), + GestureDetector( + onTap: () { + launchUrl(Uri.parse('https://docs.basedhardware.com/developer/plugins/Integrations/')); + MixpanelManager().advancedModeDocsOpened(); + }, + child: const Padding( + padding: EdgeInsets.all(8.0), + child: Text( + 'Docs', + style: TextStyle( + color: Colors.white, + fontSize: 14, + decoration: TextDecoration.underline, + ), + ), + )) + ], + ), + const SizedBox(height: 16), + const Text( + 'On Memory Created:', + style: TextStyle(color: Colors.white, fontWeight: FontWeight.w600, fontSize: 16), + ), + const SizedBox(height: 4), + const Text( + 'Triggered when FRIEND creates a new memory.', + style: TextStyle(color: Colors.white, fontSize: 14), + ), + TextField( + controller: provider.webhookOnMemoryCreated, + obscureText: false, + autocorrect: false, + enabled: true, + enableSuggestions: false, + decoration: _getTextFieldDecoration('Endpoint URL'), + style: const TextStyle(color: Colors.white), + ), + const SizedBox(height: 16), + const Text( + 'Real-Time Transcript Processing:', + style: TextStyle(color: Colors.white, fontWeight: FontWeight.w600, fontSize: 16), + ), + const SizedBox(height: 4), + const Text( + 'Triggered as the transcript is being received.', + style: TextStyle(color: Colors.white, fontSize: 14), + ), + TextField( + controller: provider.webhookOnTranscriptReceived, + obscureText: false, + autocorrect: false, + enabled: true, + enableSuggestions: false, + decoration: _getTextFieldDecoration('Endpoint URL'), + style: const TextStyle(color: Colors.white), + ), + const SizedBox(height: 64), ], ), - const SizedBox(height: 16), - const Text( - 'On Memory Created:', - style: TextStyle(color: Colors.white, fontWeight: FontWeight.w600, fontSize: 16), - ), - const SizedBox(height: 4), - const Text( - 'Triggered when FRIEND creates a new memory.', - style: TextStyle(color: Colors.white, fontSize: 14), - ), - TextField( - controller: webhookOnMemoryCreated, - obscureText: false, - autocorrect: false, - enabled: true, - enableSuggestions: false, - decoration: _getTextFieldDecoration('Endpoint URL'), - style: const TextStyle(color: Colors.white), - ), - const SizedBox(height: 16), - const Text( - 'Real-Time Transcript Processing:', - style: TextStyle(color: Colors.white, fontWeight: FontWeight.w600, fontSize: 16), - ), - const SizedBox(height: 4), - const Text( - 'Triggered as the transcript is being received.', - style: TextStyle(color: Colors.white, fontSize: 14), - ), - TextField( - controller: webhookOnTranscriptReceived, - obscureText: false, - autocorrect: false, - enabled: true, - enableSuggestions: false, - decoration: _getTextFieldDecoration('Endpoint URL'), - style: const TextStyle(color: Colors.white), - ), - const SizedBox(height: 64), - ], - ), - ), + ), + ); + }, ), ); } @@ -271,10 +270,10 @@ class _DeveloperSettingsPageState extends State { } _snackBar(String content, {int seconds = 1}) { - ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Text(content), + AppSnackbar.showSnackbar( + content, duration: Duration(seconds: seconds), - )); + ); } _getText(String text, {bool canBeDisabled = false, bool underline = false, bool bold = false}) { @@ -289,32 +288,4 @@ class _DeveloperSettingsPageState extends State { // textAlign: TextAlign.center, ); } - - void saveSettings() async { - if (savingSettingsLoading) return; - setState(() => savingSettingsLoading = true); - final prefs = SharedPreferencesUtil(); - if (gcpCredentialsController.text.isNotEmpty && gcpBucketNameController.text.isNotEmpty) { - try { - await authenticateGCP(base64: gcpCredentialsController.text.trim()); - } catch (e) { - ScaffoldMessenger.of(context).showSnackBar(const SnackBar( - content: Text('Invalid GCP credentials or bucket name. Please check and try again.'), - )); - setState(() => savingSettingsLoading = false); - return; - } - } - - // TODO: test openai + deepgram keys + bucket existence, before saving - - prefs.gcpCredentials = gcpCredentialsController.text.trim(); - prefs.gcpBucketName = gcpBucketNameController.text.trim(); - prefs.webhookOnMemoryCreated = webhookOnMemoryCreated.text.trim(); - prefs.webhookOnTranscriptReceived = webhookOnTranscriptReceived.text.trim(); - - MixpanelManager().settingsSaved(); - setState(() => savingSettingsLoading = false); - ScaffoldMessenger.of(context).showSnackBar(const SnackBar(content: Text('Settings saved!'))); - } } diff --git a/app/lib/pages/settings/privacy.dart b/app/lib/pages/settings/privacy.dart index b4f6948ec..bb2e2c252 100644 --- a/app/lib/pages/settings/privacy.dart +++ b/app/lib/pages/settings/privacy.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; class PrivacyInfoPage extends StatelessWidget { - const PrivacyInfoPage({Key? key}) : super(key: key); + const PrivacyInfoPage({super.key}); @override Widget build(BuildContext context) { diff --git a/app/lib/providers/developer_mode_provider.dart b/app/lib/providers/developer_mode_provider.dart new file mode 100644 index 000000000..52696d3f9 --- /dev/null +++ b/app/lib/providers/developer_mode_provider.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; +import 'package:friend_private/backend/http/cloud_storage.dart'; +import 'package:friend_private/backend/preferences.dart'; +import 'package:friend_private/providers/base_provider.dart'; +import 'package:friend_private/utils/alerts/app_snackbar.dart'; +import 'package:friend_private/utils/analytics/mixpanel.dart'; + +class DeveloperModeProvider extends BaseProvider { + final TextEditingController gcpCredentialsController = TextEditingController(); + final TextEditingController gcpBucketNameController = TextEditingController(); + final TextEditingController webhookOnMemoryCreated = TextEditingController(); + final TextEditingController webhookOnTranscriptReceived = TextEditingController(); + + bool savingSettingsLoading = false; + + bool loadingExportMemories = false; + bool loadingImportMemories = false; + + void initialize() { + gcpCredentialsController.text = SharedPreferencesUtil().gcpCredentials; + gcpBucketNameController.text = SharedPreferencesUtil().gcpBucketName; + webhookOnMemoryCreated.text = SharedPreferencesUtil().webhookOnMemoryCreated; + webhookOnTranscriptReceived.text = SharedPreferencesUtil().webhookOnTranscriptReceived; + } + + void saveSettings() async { + if (savingSettingsLoading) return; + savingSettingsLoading = true; + notifyListeners(); + final prefs = SharedPreferencesUtil(); + if (gcpCredentialsController.text.isNotEmpty && gcpBucketNameController.text.isNotEmpty) { + try { + await authenticateGCP(base64: gcpCredentialsController.text.trim()); + } catch (e) { + AppSnackbar.showSnackbarError( + 'Invalid GCP credentials or bucket name. Please check and try again.', + ); + + savingSettingsLoading = false; + notifyListeners(); + + return; + } + } + + // TODO: test openai + deepgram keys + bucket existence, before saving + + prefs.gcpCredentials = gcpCredentialsController.text.trim(); + prefs.gcpBucketName = gcpBucketNameController.text.trim(); + prefs.webhookOnMemoryCreated = webhookOnMemoryCreated.text.trim(); + prefs.webhookOnTranscriptReceived = webhookOnTranscriptReceived.text.trim(); + + MixpanelManager().settingsSaved(); + savingSettingsLoading = false; + notifyListeners(); + AppSnackbar.showSnackbarError( + 'Settings saved!', + ); + } +} From abef8e804515134b8a7447f9bd7dc02b6289f7b4 Mon Sep 17 00:00:00 2001 From: Mohammed Mohsin <59914433+mdmohsin7@users.noreply.github.com> Date: Thu, 22 Aug 2024 12:57:10 +0530 Subject: [PATCH 07/23] memories and home improvements --- app/lib/pages/home/page.dart | 77 +----- app/lib/pages/memories/page.dart | 330 ++++++++++++------------- app/lib/providers/home_provider.dart | 12 +- app/lib/providers/memory_provider.dart | 53 +++- 4 files changed, 222 insertions(+), 250 deletions(-) diff --git a/app/lib/pages/home/page.dart b/app/lib/pages/home/page.dart index 93c046fb9..2ea67a8bf 100644 --- a/app/lib/pages/home/page.dart +++ b/app/lib/pages/home/page.dart @@ -24,6 +24,7 @@ import 'package:friend_private/pages/settings/page.dart'; import 'package:friend_private/providers/home_provider.dart'; import 'package:friend_private/providers/message_provider.dart'; import 'package:friend_private/providers/memory_provider.dart' as mp; +import 'package:friend_private/providers/plugin_provider.dart'; import 'package:friend_private/scripts.dart'; import 'package:friend_private/services/notification_service.dart'; import 'package:friend_private/utils/analytics/mixpanel.dart'; @@ -77,32 +78,11 @@ class _HomePageState extends State with WidgetsBindingObserver, Ticker List plugins = []; final _upgrader = MyUpgrader(debugLogging: false, debugDisplayOnce: false); - // List memories = []; - // List messages = []; - bool scriptsInProgress = false; - _setupHasSpeakerProfile() async { - SharedPreferencesUtil().hasSpeakerProfile = await userHasSpeakerProfile(); - debugPrint('_setupHasSpeakerProfile: ${SharedPreferencesUtil().hasSpeakerProfile}'); - MixpanelManager().setUserProperty('Speaker Profile', SharedPreferencesUtil().hasSpeakerProfile); - setState(() {}); - } - - _edgeCasePluginNotAvailable() { - var selectedChatPlugin = SharedPreferencesUtil().selectedChatPluginId; - debugPrint('_edgeCasePluginNotAvailable $selectedChatPlugin'); - var plugin = plugins.firstWhereOrNull((p) => selectedChatPlugin == p.id); - if (selectedChatPlugin != 'no_selected' && (plugin == null || !plugin.worksWithChat() || !plugin.enabled)) { - SharedPreferencesUtil().selectedChatPluginId = 'no_selected'; - } - } - Future _initiatePlugins() async { + context.read().getPlugins(); plugins = SharedPreferencesUtil().pluginsList; - plugins = await retrievePlugins(); - _edgeCasePluginNotAvailable(); - setState(() {}); } @override @@ -128,7 +108,7 @@ class _HomePageState extends State with WidgetsBindingObserver, Ticker setState(() => scriptsInProgress = true); await scriptMigrateMemoriesToBack(); if (mounted) { - await context.read().initiateMemories(); + await context.read().getInitialMemories(); } setState(() => scriptsInProgress = false); } @@ -159,12 +139,12 @@ class _HomePageState extends State with WidgetsBindingObserver, Ticker ForegroundUtil.startForegroundTask(); if (mounted) { await context.read().refreshMessages(); - await context.read().initiateMemories(); + await context.read().setupHasSpeakerProfile(); } }); _initiatePlugins(); - _setupHasSpeakerProfile(); + //TODO: Should this run everytime? // _migrationScripts(); @@ -309,7 +289,7 @@ class _HomePageState extends State with WidgetsBindingObserver, Ticker WidgetsBinding.instance.addPostFrameCallback((_) async { if (mounted) { if (context.read().memories.isEmpty) { - await context.read().initiateMemories(); + await context.read().getInitialMemories(); } if (context.read().messages.isEmpty) { await context.read().refreshMessages(); @@ -337,43 +317,12 @@ class _HomePageState extends State with WidgetsBindingObserver, Ticker physics: const NeverScrollableScrollPhysics(), children: [ MemoriesPage( - memories: memProvider.memories, - updateMemory: (ServerMemory memory, int index) { - //TODO: Memory Provider Migrate - // var memoriesCopy = List.from(memories); - // memoriesCopy[index] = memory; - // setState(() => memories = memoriesCopy); - memProvider.updateMemory(memory, index); - }, - deleteMemory: (ServerMemory memory, int index) { - // TODO: Memory Provider Migrate - // var memoriesCopy = List.from(memories); - // memoriesCopy.removeAt(index); - // setState(() => memories = memoriesCopy); - memProvider.deleteMemory(memory, index); - }, - loadMoreMemories: () async { - // ---------------------------------- - // if (memProvider.memories.length % 50 != 0) return; - // if (context.read().isLoadingMemories) return; - // context.read().setLoadingMemories(true); - // var newMemories = await getMemories(offset: memProvider.memories.length); - // memories.addAll(newMemories); - // context.read().setLoadingMemories(false); - // ---------------------------------- - await memProvider.getMoreMemoriesFromServer(); - }, - loadingNewMemories: context.read().isLoadingMemories, textFieldFocusNode: memoriesTextFieldFocusNode, ), CapturePage( key: capturePageKey, device: _device, addMemory: (ServerMemory memory) { - // TODO: Memory Provider Migrate - // var memoriesCopy = List.from(memories); - // memoriesCopy.insert(0, memory); - // setState(() => memories = memoriesCopy); memProvider.addMemory(memory); }, addMessage: (ServerMessage message) { @@ -381,13 +330,6 @@ class _HomePageState extends State with WidgetsBindingObserver, Ticker chatPageKey.currentState?.scrollToBottom(); }, updateMemory: (ServerMemory memory) { - // TODO: Memory Provider Migrate - // var memoriesCopy = List.from(memories); - // var index = memoriesCopy.indexWhere((m) => m.id == memory.id); - // if (index != -1) { - // memoriesCopy[index] = memory; - // setState(() => memories = memoriesCopy); - // } memProvider.updateMemory(memory); }, ), @@ -395,13 +337,6 @@ class _HomePageState extends State with WidgetsBindingObserver, Ticker key: chatPageKey, textFieldFocusNode: chatTextFieldFocusNode, updateMemory: (ServerMemory memory) { - // TODO: Memory Provider Migrate - // var memoriesCopy = List.from(memories); - // var index = memoriesCopy.indexWhere((m) => m.id == memory.id); - // if (index != -1) { - // memoriesCopy[index] = memory; - // setState(() => memories = memoriesCopy); - // } memProvider.updateMemory(memory); }, ), diff --git a/app/lib/pages/memories/page.dart b/app/lib/pages/memories/page.dart index 676aca97d..f771028a5 100644 --- a/app/lib/pages/memories/page.dart +++ b/app/lib/pages/memories/page.dart @@ -1,29 +1,20 @@ import 'package:flutter/material.dart'; import 'package:friend_private/backend/schema/memory.dart'; import 'package:friend_private/pages/memories/widgets/date_list_item.dart'; -import 'package:friend_private/utils/analytics/mixpanel.dart'; +import 'package:friend_private/providers/memory_provider.dart'; import 'package:gradient_borders/box_borders/gradient_box_border.dart'; +import 'package:provider/provider.dart'; import 'package:visibility_detector/visibility_detector.dart'; import 'widgets/empty_memories.dart'; import 'widgets/memory_list_item.dart'; class MemoriesPage extends StatefulWidget { - final List memories; - final Function(ServerMemory, int) updateMemory; - final Function(ServerMemory, int) deleteMemory; - final Function loadMoreMemories; - final bool loadingNewMemories; final FocusNode textFieldFocusNode; const MemoriesPage({ super.key, - required this.memories, - required this.updateMemory, - required this.deleteMemory, required this.textFieldFocusNode, - required this.loadMoreMemories, - required this.loadingNewMemories, }); @override @@ -33,195 +24,180 @@ class MemoriesPage extends StatefulWidget { class _MemoriesPageState extends State with AutomaticKeepAliveClientMixin { TextEditingController textController = TextEditingController(); FocusNode textFieldFocusNode = FocusNode(); - bool displayDiscardMemories = false; - - _toggleDiscardMemories() async { - MixpanelManager().showDiscardedMemoriesToggled(!displayDiscardMemories); - setState(() => displayDiscardMemories = !displayDiscardMemories); - } @override bool get wantKeepAlive => true; @override - Widget build(BuildContext context) { - super.build(context); - var memories = - displayDiscardMemories ? widget.memories : widget.memories.where((memory) => !memory.discarded).toList(); - memories = textController.text.isEmpty - ? memories - : memories - .where( - (memory) => (memory.getTranscript() + memory.structured.title + memory.structured.overview) - .toLowerCase() - .contains(textController.text.toLowerCase()), - ) - .toList(); - - var memoriesWithDates = []; - for (var i = 0; i < memories.length; i++) { - if (i == 0) { - memoriesWithDates.add(memories[i].createdAt); - memoriesWithDates.add(memories[i]); - } else { - if (memories[i].createdAt.day != memories[i - 1].createdAt.day) { - memoriesWithDates.add(memories[i].createdAt); - } - memoriesWithDates.add(memories[i]); + void initState() { + WidgetsBinding.instance.addPostFrameCallback((_) async { + if (Provider.of(context, listen: false).memories.isEmpty) { + await Provider.of(context, listen: false).getInitialMemories(); + } + if (mounted) { + Provider.of(context, listen: false).initFilteredMemories(); } - } + }); + super.initState(); + } - return CustomScrollView( - slivers: [ - const SliverToBoxAdapter(child: SizedBox(height: 32)), - SliverToBoxAdapter( - child: Container( - width: double.maxFinite, - padding: const EdgeInsets.fromLTRB(16, 8, 8, 0), - margin: const EdgeInsets.fromLTRB(18, 0, 18, 0), - decoration: const BoxDecoration( - color: Colors.black, - borderRadius: BorderRadius.all(Radius.circular(16)), - border: GradientBoxBorder( - gradient: LinearGradient(colors: [ - Color.fromARGB(127, 208, 208, 208), - Color.fromARGB(127, 188, 99, 121), - Color.fromARGB(127, 86, 101, 182), - Color.fromARGB(127, 126, 190, 236) - ]), - width: 1, + @override + Widget build(BuildContext context) { + print('building memories page'); + super.build(context); + return Consumer(builder: (context, memoryProvider, child) { + return CustomScrollView( + slivers: [ + const SliverToBoxAdapter(child: SizedBox(height: 32)), + SliverToBoxAdapter( + child: Container( + width: double.maxFinite, + padding: const EdgeInsets.fromLTRB(16, 8, 8, 0), + margin: const EdgeInsets.fromLTRB(18, 0, 18, 0), + decoration: const BoxDecoration( + color: Colors.black, + borderRadius: BorderRadius.all(Radius.circular(16)), + border: GradientBoxBorder( + gradient: LinearGradient(colors: [ + Color.fromARGB(127, 208, 208, 208), + Color.fromARGB(127, 188, 99, 121), + Color.fromARGB(127, 86, 101, 182), + Color.fromARGB(127, 126, 190, 236) + ]), + width: 1, + ), + shape: BoxShape.rectangle, ), - shape: BoxShape.rectangle, - ), - child: TextField( - enabled: true, - controller: textController, - onChanged: (s) { - setState(() {}); - }, - obscureText: false, - autofocus: false, - focusNode: widget.textFieldFocusNode, - decoration: InputDecoration( - hintText: 'Search for memories...', - hintStyle: const TextStyle(fontSize: 14.0, color: Colors.grey), - enabledBorder: InputBorder.none, - focusedBorder: InputBorder.none, - suffixIcon: textController.text.isEmpty - ? const SizedBox.shrink() - : IconButton( - icon: const Icon( - Icons.cancel, - color: Color(0xFFF7F4F4), - size: 28.0, + child: TextField( + enabled: true, + controller: textController, + onChanged: (s) { + memoryProvider.filterMemories(s); + }, + obscureText: false, + autofocus: false, + focusNode: widget.textFieldFocusNode, + decoration: InputDecoration( + hintText: 'Search for memories...', + hintStyle: const TextStyle(fontSize: 14.0, color: Colors.grey), + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + suffixIcon: textController.text.isEmpty + ? const SizedBox.shrink() + : IconButton( + icon: const Icon( + Icons.cancel, + color: Color(0xFFF7F4F4), + size: 28.0, + ), + onPressed: () { + textController.clear(); + memoryProvider.initFilteredMemories(); + }, ), - onPressed: () { - textController.clear(); - setState(() {}); - }, - ), + ), + style: TextStyle(fontSize: 14.0, color: Colors.grey.shade200), ), - style: TextStyle(fontSize: 14.0, color: Colors.grey.shade200), ), ), - ), - const SliverToBoxAdapter(child: SizedBox(height: 16)), - SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const SizedBox(width: 1), - Row( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Text( - displayDiscardMemories ? 'Hide Discarded' : 'Show Discarded', - style: const TextStyle(color: Colors.white, fontSize: 16), - ), - const SizedBox(width: 8), - IconButton( - onPressed: () { - _toggleDiscardMemories(); - }, - icon: Icon( - displayDiscardMemories ? Icons.cancel_outlined : Icons.filter_list, - color: Colors.white, + const SliverToBoxAdapter(child: SizedBox(height: 16)), + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const SizedBox(width: 1), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Text( + memoryProvider.displayDiscardMemories ? 'Hide Discarded' : 'Show Discarded', + style: const TextStyle(color: Colors.white, fontSize: 16), ), - ), - ], - ) - ], + const SizedBox(width: 8), + IconButton( + onPressed: () { + memoryProvider.toggleDiscardMemories(); + }, + icon: Icon( + memoryProvider.displayDiscardMemories ? Icons.cancel_outlined : Icons.filter_list, + color: Colors.white, + ), + ), + ], + ) + ], + ), ), ), - ), - if (memories.isEmpty && !widget.loadingNewMemories) - const SliverToBoxAdapter( - child: Center( - child: Padding( - padding: EdgeInsets.only(top: 32.0), - child: EmptyMemoriesWidget(), + if (memoryProvider.memoriesWithDates.isEmpty && !memoryProvider.isLoadingMemories) + const SliverToBoxAdapter( + child: Center( + child: Padding( + padding: EdgeInsets.only(top: 32.0), + child: EmptyMemoriesWidget(), + ), ), - ), - ) - else if (memories.isEmpty && widget.loadingNewMemories) - const SliverToBoxAdapter( - child: Center( - child: Padding( - padding: EdgeInsets.only(top: 32.0), - child: CircularProgressIndicator( - valueColor: AlwaysStoppedAnimation(Colors.white), + ) + else if (memoryProvider.memoriesWithDates.isEmpty && memoryProvider.isLoadingMemories) + const SliverToBoxAdapter( + child: Center( + child: Padding( + padding: EdgeInsets.only(top: 32.0), + child: CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation(Colors.white), + ), ), ), - ), - ) - else - SliverList( - delegate: SliverChildBuilderDelegate( - (context, index) { - if (index == memoriesWithDates.length) { - if (widget.loadingNewMemories) { - return const Center( - child: Padding( - padding: EdgeInsets.only(top: 32.0), - child: CircularProgressIndicator( - valueColor: AlwaysStoppedAnimation(Colors.white), + ) + else + SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + if (index == memoryProvider.memoriesWithDates.length) { + if (memoryProvider.isLoadingMemories) { + return const Center( + child: Padding( + padding: EdgeInsets.only(top: 32.0), + child: CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation(Colors.white), + ), ), - ), + ); + } + // widget.loadMoreMemories(); // CALL this only when visible + return VisibilityDetector( + key: const Key('memory-loader'), + onVisibilityChanged: (visibilityInfo) { + if (visibilityInfo.visibleFraction > 0 && !memoryProvider.isLoadingMemories) { + memoryProvider.getMoreMemoriesFromServer(); + } + }, + child: const SizedBox(height: 80, width: double.maxFinite), ); } - // widget.loadMoreMemories(); // CALL this only when visible - return VisibilityDetector( - key: const Key('memory-loader'), - onVisibilityChanged: (visibilityInfo) { - if (visibilityInfo.visibleFraction > 0 && !widget.loadingNewMemories) { - widget.loadMoreMemories(); - } - }, - child: const SizedBox(height: 80, width: double.maxFinite), - ); - } - if (memoriesWithDates[index].runtimeType == DateTime) { - return DateListItem(date: memoriesWithDates[index] as DateTime, isFirst: index == 0); - } - var memory = memoriesWithDates[index] as ServerMemory; - return MemoryListItem( - memoryIdx: memories.indexOf(memory), - memory: memory, - updateMemory: widget.updateMemory, - deleteMemory: widget.deleteMemory, - ); - }, - childCount: memoriesWithDates.length + 1, + if (memoryProvider.memoriesWithDates[index].runtimeType == DateTime) { + return DateListItem(date: memoryProvider.memoriesWithDates[index] as DateTime, isFirst: index == 0); + } + var memory = memoryProvider.memoriesWithDates[index] as ServerMemory; + return MemoryListItem( + memoryIdx: memoryProvider.memoriesWithDates.indexOf(memory), + memory: memory, + updateMemory: memoryProvider.updateMemory, + deleteMemory: memoryProvider.deleteMemory, + ); + }, + childCount: memoryProvider.memoriesWithDates.length + 1, + ), ), + const SliverToBoxAdapter( + child: SizedBox(height: 80), ), - const SliverToBoxAdapter( - child: SizedBox(height: 80), - ), - ], - ); + ], + ); + }); } } diff --git a/app/lib/providers/home_provider.dart b/app/lib/providers/home_provider.dart index 46c10b3ff..710192fa4 100644 --- a/app/lib/providers/home_provider.dart +++ b/app/lib/providers/home_provider.dart @@ -1,3 +1,13 @@ import 'package:flutter/material.dart'; +import 'package:friend_private/backend/http/api/speech_profile.dart'; +import 'package:friend_private/backend/preferences.dart'; +import 'package:friend_private/utils/analytics/mixpanel.dart'; -class HomeProvider extends ChangeNotifier {} +class HomeProvider extends ChangeNotifier { + Future setupHasSpeakerProfile() async { + SharedPreferencesUtil().hasSpeakerProfile = await userHasSpeakerProfile(); + debugPrint('_setupHasSpeakerProfile: ${SharedPreferencesUtil().hasSpeakerProfile}'); + MixpanelManager().setUserProperty('Speaker Profile', SharedPreferencesUtil().hasSpeakerProfile); + notifyListeners(); + } +} diff --git a/app/lib/providers/memory_provider.dart b/app/lib/providers/memory_provider.dart index 3a845b000..60017cf9e 100644 --- a/app/lib/providers/memory_provider.dart +++ b/app/lib/providers/memory_provider.dart @@ -4,21 +4,72 @@ import 'package:flutter/foundation.dart'; import 'package:friend_private/backend/http/api/memories.dart'; import 'package:friend_private/backend/preferences.dart'; import 'package:friend_private/backend/schema/memory.dart'; +import 'package:friend_private/utils/analytics/mixpanel.dart'; import 'package:friend_private/utils/memories/process.dart'; import 'package:instabug_flutter/instabug_flutter.dart'; import 'package:tuple/tuple.dart'; class MemoryProvider extends ChangeNotifier { List memories = []; + List filteredMemories = []; + List memoriesWithDates = []; bool isLoadingMemories = false; + bool displayDiscardMemories = false; + + String previousQuery = ''; + + void populateMemoriesWithDates() { + memoriesWithDates = []; + for (var i = 0; i < filteredMemories.length; i++) { + if (i == 0) { + memoriesWithDates.add(filteredMemories[i]); + } else { + if (filteredMemories[i].createdAt.day != filteredMemories[i - 1].createdAt.day) { + memoriesWithDates.add(filteredMemories[i].createdAt); + } + memoriesWithDates.add(filteredMemories[i]); + } + } + notifyListeners(); + } + + void initFilteredMemories() { + filteredMemories = memories; + populateMemoriesWithDates(); + notifyListeners(); + } + + void filterMemories(String query) { + if (query == previousQuery) return; + filteredMemories = []; + filteredMemories = displayDiscardMemories ? memories : memories.where((memory) => !memory.discarded).toList(); + filteredMemories = query.isEmpty + ? memories + : memories + .where( + (memory) => (memory.getTranscript() + memory.structured.title + memory.structured.overview) + .toLowerCase() + .contains(query.toLowerCase()), + ) + .toList(); + populateMemoriesWithDates(); + notifyListeners(); + } + + void toggleDiscardMemories() { + MixpanelManager().showDiscardedMemoriesToggled(!displayDiscardMemories); + displayDiscardMemories = !displayDiscardMemories; + filterMemories(''); + notifyListeners(); + } void setLoadingMemories(bool value) { isLoadingMemories = value; notifyListeners(); } - Future initiateMemories() async { + Future getInitialMemories() async { memories = await getMemoriesFromServer(); if (memories.isEmpty) { memories = SharedPreferencesUtil().cachedMemories; From 80fb732f1ce3379fcbef022011f8cac1b7eb8aca Mon Sep 17 00:00:00 2001 From: Mohammed Mohsin <59914433+mdmohsin7@users.noreply.github.com> Date: Thu, 22 Aug 2024 12:57:25 +0530 Subject: [PATCH 08/23] plugins in chat --- app/lib/main.dart | 8 +++++++- app/lib/providers/message_provider.dart | 17 +++++++++++++++++ app/lib/providers/plugin_provider.dart | 18 ++++++++++++++++++ 3 files changed, 42 insertions(+), 1 deletion(-) create mode 100644 app/lib/providers/plugin_provider.dart diff --git a/app/lib/main.dart b/app/lib/main.dart index c9539d6d7..9d8cb65eb 100644 --- a/app/lib/main.dart +++ b/app/lib/main.dart @@ -20,6 +20,7 @@ import 'package:friend_private/pages/onboarding/wrapper.dart'; import 'package:friend_private/providers/home_provider.dart'; import 'package:friend_private/providers/memory_provider.dart'; import 'package:friend_private/providers/message_provider.dart'; +import 'package:friend_private/providers/plugin_provider.dart'; import 'package:friend_private/services/notification_service.dart'; import 'package:friend_private/utils/analytics/growthbook.dart'; import 'package:friend_private/utils/analytics/mixpanel.dart'; @@ -130,8 +131,13 @@ class _MyAppState extends State { return MultiProvider( providers: [ ListenableProvider(create: (context) => HomeProvider()), - ListenableProvider(create: (context) => MessageProvider()), ListenableProvider(create: (context) => MemoryProvider()), + ListenableProvider(create: (context) => PluginProvider()), + ChangeNotifierProxyProvider( + create: (context) => MessageProvider(), + update: (BuildContext context, value, MessageProvider? previous) => + MessageProvider()..updatePluginProvider(value), + ), ], builder: (context, child) { return WithForegroundTask( diff --git a/app/lib/providers/message_provider.dart b/app/lib/providers/message_provider.dart index c79a025db..d50b9326e 100644 --- a/app/lib/providers/message_provider.dart +++ b/app/lib/providers/message_provider.dart @@ -1,13 +1,20 @@ +import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:friend_private/backend/http/api/messages.dart'; import 'package:friend_private/backend/preferences.dart'; import 'package:friend_private/backend/schema/message.dart'; +import 'package:friend_private/providers/plugin_provider.dart'; class MessageProvider extends ChangeNotifier { + PluginProvider? pluginProvider; List messages = []; bool isLoadingMessages = false; + void updatePluginProvider(PluginProvider p) { + pluginProvider = p; + } + void setLoadingMessages(bool value) { isLoadingMessages = value; notifyListeners(); @@ -44,4 +51,14 @@ class MessageProvider extends ChangeNotifier { messages.insert(0, mes); notifyListeners(); } + + void checkSelectedPlugins() { + var selectedChatPlugin = SharedPreferencesUtil().selectedChatPluginId; + debugPrint('_edgeCasePluginNotAvailable $selectedChatPlugin'); + var plugin = pluginProvider!.plugins.firstWhereOrNull((p) => selectedChatPlugin == p.id); + if (selectedChatPlugin != 'no_selected' && (plugin == null || !plugin.worksWithChat() || !plugin.enabled)) { + SharedPreferencesUtil().selectedChatPluginId = 'no_selected'; + } + notifyListeners(); + } } diff --git a/app/lib/providers/plugin_provider.dart b/app/lib/providers/plugin_provider.dart new file mode 100644 index 000000000..17a3e95be --- /dev/null +++ b/app/lib/providers/plugin_provider.dart @@ -0,0 +1,18 @@ +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; +import 'package:friend_private/backend/http/api/plugins.dart'; +import 'package:friend_private/backend/preferences.dart'; +import 'package:friend_private/backend/schema/plugin.dart'; + +class PluginProvider extends ChangeNotifier { + List plugins = []; + + Future getPlugins() async { + if (SharedPreferencesUtil().pluginsList.isEmpty) { + plugins = await retrievePlugins(); + } else { + plugins = SharedPreferencesUtil().pluginsList; + } + notifyListeners(); + } +} From b9bdcc979fe446eb37a01c7081c834357573dd67 Mon Sep 17 00:00:00 2001 From: Becca-Saka Date: Thu, 22 Aug 2024 02:24:10 +0100 Subject: [PATCH 09/23] chore: migrated to auth provider --- app/lib/utils/alerts/app_snackbar.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/app/lib/utils/alerts/app_snackbar.dart b/app/lib/utils/alerts/app_snackbar.dart index 2bd15ad54..ac89d537e 100644 --- a/app/lib/utils/alerts/app_snackbar.dart +++ b/app/lib/utils/alerts/app_snackbar.dart @@ -7,6 +7,7 @@ class AppSnackbar { SnackBar( content: Text(message), backgroundColor: color, + duration: duration ?? const Duration(seconds: 2), ), ); } From 9750b3e7b4479d1c112f016b6b88d2dae6ef5f54 Mon Sep 17 00:00:00 2001 From: Mohammed Mohsin <59914433+mdmohsin7@users.noreply.github.com> Date: Fri, 23 Aug 2024 00:12:22 +0530 Subject: [PATCH 10/23] capture provider --- app/lib/main.dart | 6 + app/lib/pages/capture/page.dart | 283 +++++------------------- app/lib/providers/capture_provider.dart | 254 +++++++++++++++++++++ 3 files changed, 317 insertions(+), 226 deletions(-) create mode 100644 app/lib/providers/capture_provider.dart diff --git a/app/lib/main.dart b/app/lib/main.dart index 9d8cb65eb..66be14d72 100644 --- a/app/lib/main.dart +++ b/app/lib/main.dart @@ -21,6 +21,7 @@ import 'package:friend_private/providers/home_provider.dart'; import 'package:friend_private/providers/memory_provider.dart'; import 'package:friend_private/providers/message_provider.dart'; import 'package:friend_private/providers/plugin_provider.dart'; +import 'package:friend_private/providers/capture_provider.dart'; import 'package:friend_private/services/notification_service.dart'; import 'package:friend_private/utils/analytics/growthbook.dart'; import 'package:friend_private/utils/analytics/mixpanel.dart'; @@ -133,6 +134,11 @@ class _MyAppState extends State { ListenableProvider(create: (context) => HomeProvider()), ListenableProvider(create: (context) => MemoryProvider()), ListenableProvider(create: (context) => PluginProvider()), + ChangeNotifierProxyProvider2( + create: (context) => CaptureProvider(), + update: (BuildContext context, memory, message, CaptureProvider? previous) => + CaptureProvider()..updateProviderInstances(memory, message), + ), ChangeNotifierProxyProvider( create: (context) => MessageProvider(), update: (BuildContext context, value, MessageProvider? previous) => diff --git a/app/lib/pages/capture/page.dart b/app/lib/pages/capture/page.dart index 83e1e23e1..829d9fe82 100644 --- a/app/lib/pages/capture/page.dart +++ b/app/lib/pages/capture/page.dart @@ -1,16 +1,10 @@ import 'dart:async'; -import 'dart:convert'; import 'dart:io'; -import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter_foreground_task/flutter_foreground_task.dart'; import 'package:friend_private/backend/database/geolocation.dart'; -import 'package:friend_private/backend/database/memory.dart'; -import 'package:friend_private/backend/database/transcript_segment.dart'; -import 'package:friend_private/backend/http/api/memories.dart'; -import 'package:friend_private/backend/http/cloud_storage.dart'; import 'package:friend_private/backend/preferences.dart'; import 'package:friend_private/backend/schema/bt_device.dart'; import 'package:friend_private/backend/schema/memory.dart'; @@ -19,16 +13,16 @@ import 'package:friend_private/pages/capture/location_service.dart'; import 'package:friend_private/pages/capture/logic/openglass_mixin.dart'; import 'package:friend_private/pages/capture/widgets/widgets.dart'; import 'package:friend_private/pages/home/page.dart'; +import 'package:friend_private/providers/capture_provider.dart'; import 'package:friend_private/utils/audio/wav_bytes.dart'; import 'package:friend_private/utils/ble/communication.dart'; import 'package:friend_private/utils/enums.dart'; -import 'package:friend_private/utils/memories/integrations.dart'; import 'package:friend_private/utils/memories/process.dart'; import 'package:friend_private/utils/other/temp.dart'; -import 'package:friend_private/utils/websockets.dart'; import 'package:friend_private/widgets/dialog.dart'; import 'package:internet_connection_checker_plus/internet_connection_checker_plus.dart'; import 'package:location/location.dart'; +import 'package:provider/provider.dart'; import 'package:uuid/uuid.dart'; import 'logic/phone_recorder_mixin.dart'; @@ -57,12 +51,10 @@ class CapturePageState extends State @override bool get wantKeepAlive => true; + // TODO: This should come from Device Provider implemented by @Becca-Saka BTDeviceStruct? btDevice; - bool _hasTranscripts = false; - static const quietSecondsForMemoryCreation = 120; /// ---- - List segments = []; // List segments = List.filled(100, '') // .mapIndexed((i, e) => TranscriptSegment( @@ -101,93 +93,12 @@ class CapturePageState extends State // )) // .toList(); - StreamSubscription? _bleBytesStream; - WavBytesUtil? audioStorage; - - Timer? _memoryCreationTimer; - bool memoryCreating = false; - - DateTime? currentTranscriptStartedAt; - DateTime? currentTranscriptFinishedAt; - InternetStatus? _internetStatus; late StreamSubscription _internetListener; bool isGlasses = false; String conversationId = const Uuid().v4(); // used only for transcript segment plugins - double? streamStartedAtSecond; - DateTime? firstStreamReceivedAt; - int? secondsMissedOnReconnect; - - Geolocation? geolocation; - - Future initiateWebsocket([BleAudioCodec? audioCodec, int? sampleRate]) async { - print('initiateWebsocket'); - BleAudioCodec codec = audioCodec ?? SharedPreferencesUtil().deviceCodec; - sampleRate ??= (codec == BleAudioCodec.opus ? 16000 : 8000); - await initWebSocket( - codec: codec, - sampleRate: sampleRate, - includeSpeechProfile: true, - onConnectionSuccess: () { - if (segments.isNotEmpty) { - // means that it was a reconnection, so we need to reset - streamStartedAtSecond = null; - secondsMissedOnReconnect = (DateTime.now().difference(firstStreamReceivedAt!).inSeconds); - } - if (mounted) { - setState(() {}); - } - }, - onConnectionFailed: (err) { - if (mounted) { - setState(() {}); - } - }, - onConnectionClosed: (int? closeCode, String? closeReason) { - // connection was closed, either on resetState, or by backend, or by some other reason. - // setState(() {}); - }, - onConnectionError: (err) { - // connection was okay, but then failed. - if (mounted) { - setState(() {}); - } - }, - onMessageReceived: (List newSegments) { - if (newSegments.isEmpty) return; - if (segments.isEmpty) { - debugPrint('newSegments: ${newSegments.last}'); - // TODO: small bug -> when memory A creates, and memory B starts, memory B will clean a lot more seconds than available, - // losing from the audio the first part of the recording. All other parts are fine. - FlutterForegroundTask.sendDataToTask(jsonEncode({'location': true})); - var currentSeconds = (audioStorage?.frames.length ?? 0) ~/ 100; - var removeUpToSecond = newSegments[0].start.toInt(); - audioStorage?.removeFramesRange(fromSecond: 0, toSecond: min(max(currentSeconds - 5, 0), removeUpToSecond)); - firstStreamReceivedAt = DateTime.now(); - } - streamStartedAtSecond ??= newSegments[0].start; - - TranscriptSegment.combineSegments( - segments, - newSegments, - toRemoveSeconds: streamStartedAtSecond ?? 0, - toAddSeconds: secondsMissedOnReconnect ?? 0, - ); - triggerTranscriptSegmentReceivedEvents(newSegments, conversationId, sendMessageToChat: sendMessageToChat); - SharedPreferencesUtil().transcriptSegments = segments; - setHasTranscripts(true); - debugPrint('Memory creation timer restarted'); - _memoryCreationTimer?.cancel(); - _memoryCreationTimer = Timer(const Duration(seconds: quietSecondsForMemoryCreation), () => _createMemory()); - currentTranscriptStartedAt ??= DateTime.now(); - currentTranscriptFinishedAt = DateTime.now(); - setState(() {}); - }, - ); - } - Future initiateFriendAudioStreaming() async { if (btDevice == null) return; BleAudioCodec codec = await getAudioCodec(btDevice!.id); @@ -208,24 +119,9 @@ class CapturePageState extends State ); return; } - audioStorage = WavBytesUtil(codec: codec); - _bleBytesStream = await getBleAudioBytesListener( - btDevice!.id, - onAudioBytesReceived: (List value) { - if (value.isEmpty) return; - audioStorage!.storeFramePacket(value); - // print(value); - value.removeRange(0, 3); - // TODO: if this is not removed, deepgram can't seem to be able to detect the audio. - // https://developers.deepgram.com/docs/determining-your-audio-format-for-live-streaming-audio - if (wsConnectionState == WebsocketConnectionStatus.connected) { - websocketChannel?.sink.add(value); - } - }, - ); - } - int elapsedSeconds = 0; + await context.read().streamAudioToWs(btDevice!.id, codec); + } Future startOpenGlass() async { if (btDevice == null) return; @@ -235,11 +131,27 @@ class CapturePageState extends State closeWebSocket(); } - void resetState({bool restartBytesProcessing = true, BTDeviceStruct? btDevice}) { + void resetState({bool restartBytesProcessing = true, BTDeviceStruct? btDevice}) async { + var provider = context.read(); debugPrint('resetState: $restartBytesProcessing'); - _bleBytesStream?.cancel(); - _memoryCreationTimer?.cancel(); - if (!restartBytesProcessing && (segments.isNotEmpty || photos.isNotEmpty)) _createMemory(forcedCreation: true); + provider.closeBleStream(); + provider.cancelMemoryCreationTimer(); + if (!restartBytesProcessing && (provider.segments.isNotEmpty || photos.isNotEmpty)) { + var res = await provider.createMemory(forcedCreation: true); + if (res != null && !res) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text( + 'Memory creation failed. It\' stored locally and will be retried soon.', + style: TextStyle(color: Colors.white, fontSize: 14), + ), + ), + ); + } + } + } + if (btDevice != null) setState(() => this.btDevice = btDevice); if (restartBytesProcessing) { startOpenGlass(); @@ -250,98 +162,15 @@ class CapturePageState extends State void restartWebSocket() { debugPrint('restartWebSocket'); closeWebSocket(); - initiateWebsocket(); + context.read().streamAudioToWs(btDevice!.id, SharedPreferencesUtil().deviceCodec); } void sendMessageToChat(ServerMessage message) { widget.addMessage(message); } - _createMemory({bool forcedCreation = false}) async { - debugPrint('_createMemory forcedCreation: $forcedCreation'); - if (memoryCreating) return; - if (segments.isEmpty && photos.isEmpty) return; - - // TODO: should clean variables here? and keep them locally? - setState(() => memoryCreating = true); - File? file; - if (audioStorage?.frames.isNotEmpty == true) { - try { - var secs = !forcedCreation ? quietSecondsForMemoryCreation : 0; - file = (await audioStorage!.createWavFile(removeLastNSeconds: secs)).item1; - uploadFile(file); - } catch (e) { - print("creating and uploading file error: $e"); - } // in case was a local recording and not a BLE recording - } - - ServerMemory? memory = await processTranscriptContent( - segments: segments, - startedAt: currentTranscriptStartedAt, - finishedAt: currentTranscriptFinishedAt, - geolocation: geolocation, - photos: photos, - sendMessageToChat: sendMessageToChat, - triggerIntegrations: true, - language: SharedPreferencesUtil().recordingsLanguage, - audioFile: file, - ); - debugPrint(memory.toString()); - if (memory == null && (segments.isNotEmpty || photos.isNotEmpty)) { - memory = ServerMemory( - id: const Uuid().v4(), - createdAt: DateTime.now(), - structured: Structured('', '', emoji: '⛓️‍💥', category: 'other'), - discarded: true, - transcriptSegments: segments, - geolocation: geolocation, - photos: photos.map((e) => MemoryPhoto(e.item1, e.item2)).toList(), - startedAt: currentTranscriptStartedAt, - finishedAt: currentTranscriptFinishedAt, - failed: true, - source: segments.isNotEmpty ? MemorySource.friend : MemorySource.openglass, - language: segments.isNotEmpty ? SharedPreferencesUtil().recordingsLanguage : null, - ); - SharedPreferencesUtil().addFailedMemory(memory); - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar(const SnackBar( - content: Text( - 'Memory creation failed. It\' stored locally and will be retried soon.', - style: TextStyle(color: Colors.white, fontSize: 14), - ), - )); - } - - // TODO: store anyways something temporal and retry once connected again. - } - - if (memory != null) widget.addMemory(memory); - if (memory != null && !memory.failed && file != null && segments.isNotEmpty && !memory.discarded) { - memoryPostProcessing(file, memory.id).then((postProcessed) { - widget.updateMemory(postProcessed); - }); - } - - SharedPreferencesUtil().transcriptSegments = []; - segments = []; - audioStorage?.clearAudioBytes(); - setHasTranscripts(false); - - currentTranscriptStartedAt = null; - currentTranscriptFinishedAt = null; - elapsedSeconds = 0; - - streamStartedAtSecond = null; - firstStreamReceivedAt = null; - secondsMissedOnReconnect = null; - photos = []; - conversationId = const Uuid().v4(); - setState(() => memoryCreating = false); - } - setHasTranscripts(bool hasTranscripts) { - if (_hasTranscripts == hasTranscripts) return; - setState(() => _hasTranscripts = hasTranscripts); + context.read().setHasTranscripts(hasTranscripts); } processCachedTranscript() async { @@ -361,16 +190,15 @@ class CapturePageState extends State void _onReceiveTaskData(dynamic data) { if (data is Map) { if (data.containsKey('latitude') && data.containsKey('longitude')) { - geolocation = Geolocation( - latitude: data['latitude'], - longitude: data['longitude'], - accuracy: data['accuracy'], - altitude: data['altitude'], - time: DateTime.parse(data['time']), - ); - debugPrint('Location data received from background: $geolocation'); + context.read().setGeolocation(Geolocation( + latitude: data['latitude'], + longitude: data['longitude'], + accuracy: data['accuracy'], + altitude: data['altitude'], + time: DateTime.parse(data['time']), + )); } else { - geolocation = null; + context.read().setGeolocation(null); } } } @@ -379,7 +207,6 @@ class CapturePageState extends State void initState() { btDevice = widget.device; WavBytesUtil.clearTempWavFiles(); - initiateWebsocket(); startOpenGlass(); initiateFriendAudioStreaming(); processCachedTranscript(); @@ -387,6 +214,7 @@ class CapturePageState extends State FlutterForegroundTask.addTaskDataCallback(_onReceiveTaskData); WidgetsBinding.instance.addObserver(this); SchedulerBinding.instance.addPostFrameCallback((_) async { + await context.read().initiateWebsocket(); if (await LocationService().displayPermissionsDialog()) { await showDialog( context: context, @@ -414,7 +242,7 @@ class CapturePageState extends State case InternetStatus.disconnected: _internetStatus = InternetStatus.disconnected; // so if you have a memory in progress, it doesn't get created, and you don't lose the remaining bytes. - _memoryCreationTimer?.cancel(); + context.read().cancelMemoryCreationTimer(); break; } }); @@ -424,9 +252,9 @@ class CapturePageState extends State @override void dispose() { WidgetsBinding.instance.removeObserver(this); + context.read().closeBleStream(); + context.read().cancelMemoryCreationTimer(); record.dispose(); - _bleBytesStream?.cancel(); - _memoryCreationTimer?.cancel(); _internetListener.cancel(); // websocketChannel closeWebSocket(); @@ -472,18 +300,21 @@ class CapturePageState extends State @override Widget build(BuildContext context) { super.build(context); - return Stack( - children: [ - ListView(children: [ - speechProfileWidget(context, setState, restartWebSocket), - ...getConnectionStateWidgets(context, _hasTranscripts, widget.device, wsConnectionState, _internetStatus), - getTranscriptWidget(memoryCreating, segments, photos, widget.device), - ...connectionStatusWidgets(context, segments, wsConnectionState, _internetStatus), - const SizedBox(height: 16) - ]), - getPhoneMicRecordingButton(_recordingToggled, recordingState) - ], - ); + return Consumer(builder: (context, provider, child) { + return Stack( + children: [ + ListView(children: [ + speechProfileWidget(context, setState, restartWebSocket), + ...getConnectionStateWidgets( + context, provider.hasTranscripts, widget.device, wsConnectionState, _internetStatus), + getTranscriptWidget(provider.memoryCreating, provider.segments, photos, widget.device), + ...connectionStatusWidgets(context, provider.segments, wsConnectionState, _internetStatus), + const SizedBox(height: 16) + ]), + getPhoneMicRecordingButton(_recordingToggled, recordingState) + ], + ); + }); } _recordingToggled() async { @@ -494,8 +325,8 @@ class CapturePageState extends State await stopStreamRecording(wsConnectionState, websocketChannel); } setState(() => recordingState = RecordingState.stop); - _memoryCreationTimer?.cancel(); - _createMemory(); + context.read().cancelMemoryCreationTimer(); + await context.read().createMemory(); } else if (recordingState == RecordingState.initialising) { debugPrint('initialising, have to wait'); } else { @@ -508,7 +339,7 @@ class CapturePageState extends State Navigator.pop(context); setState(() => recordingState = RecordingState.initialising); closeWebSocket(); - await initiateWebsocket(BleAudioCodec.pcm16, 16000); + // await initiateWebsocket(BleAudioCodec.pcm16, 16000); if (Platform.isAndroid) { await streamRecordingOnAndroid(wsConnectionState, websocketChannel); } else { diff --git a/app/lib/providers/capture_provider.dart b/app/lib/providers/capture_provider.dart new file mode 100644 index 000000000..8de5980fe --- /dev/null +++ b/app/lib/providers/capture_provider.dart @@ -0,0 +1,254 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:flutter_foreground_task/flutter_foreground_task.dart'; +import 'package:friend_private/backend/database/geolocation.dart'; +import 'package:friend_private/backend/database/memory.dart'; +import 'package:friend_private/backend/database/transcript_segment.dart'; +import 'package:friend_private/backend/http/api/memories.dart'; +import 'package:friend_private/backend/http/cloud_storage.dart'; +import 'package:friend_private/backend/preferences.dart'; +import 'package:friend_private/backend/schema/bt_device.dart'; +import 'package:friend_private/backend/schema/memory.dart'; +import 'package:friend_private/pages/capture/logic/openglass_mixin.dart'; +import 'package:friend_private/pages/capture/logic/websocket_mixin.dart'; +import 'package:friend_private/providers/memory_provider.dart'; +import 'package:friend_private/providers/message_provider.dart'; +import 'package:friend_private/utils/audio/wav_bytes.dart'; +import 'package:friend_private/utils/ble/communication.dart'; +import 'package:friend_private/utils/memories/integrations.dart'; +import 'package:friend_private/utils/memories/process.dart'; +import 'package:friend_private/utils/websockets.dart'; +import 'package:uuid/uuid.dart'; + +class CaptureProvider extends ChangeNotifier with WebSocketMixin, OpenGlassMixin { + MemoryProvider? memoryProvider; + MessageProvider? messageProvider; + + List segments = []; + Geolocation? geolocation; + + bool hasTranscripts = false; + bool memoryCreating = false; + + static const quietSecondsForMemoryCreation = 120; + + StreamSubscription? _bleBytesStream; + +// ----------------------- +// Memory creation variables + double? streamStartedAtSecond; + DateTime? firstStreamReceivedAt; + int? secondsMissedOnReconnect; + WavBytesUtil? audioStorage; + Timer? _memoryCreationTimer; + String conversationId = const Uuid().v4(); + DateTime? currentTranscriptStartedAt; + DateTime? currentTranscriptFinishedAt; + int elapsedSeconds = 0; + // ----------------------- + + void updateProviderInstances(MemoryProvider mp, MessageProvider p) { + memoryProvider = mp; + messageProvider = p; + } + + void setHasTranscripts(bool value) { + hasTranscripts = value; + notifyListeners(); + } + + void setMemoryCreating(bool value) { + memoryCreating = value; + notifyListeners(); + } + + void setGeolocation(Geolocation? value) { + geolocation = value; + notifyListeners(); + } + + Future createMemory({bool forcedCreation = false}) async { + debugPrint('_createMemory forcedCreation: $forcedCreation'); + if (memoryCreating) return null; + if (segments.isEmpty && photos.isEmpty) return false; + + // TODO: should clean variables here? and keep them locally? + setMemoryCreating(true); + File? file; + if (audioStorage?.frames.isNotEmpty == true) { + try { + var secs = !forcedCreation ? quietSecondsForMemoryCreation : 0; + file = (await audioStorage!.createWavFile(removeLastNSeconds: secs)).item1; + uploadFile(file); + } catch (e) { + print("creating and uploading file error: $e"); + } // in case was a local recording and not a BLE recording + } + + ServerMemory? memory = await processTranscriptContent( + segments: segments, + startedAt: currentTranscriptStartedAt, + finishedAt: currentTranscriptFinishedAt, + geolocation: geolocation, + photos: photos, + sendMessageToChat: (v) { + // use message provider to send message to chat + }, + triggerIntegrations: true, + language: SharedPreferencesUtil().recordingsLanguage, + audioFile: file, + ); + debugPrint(memory.toString()); + if (memory == null && (segments.isNotEmpty || photos.isNotEmpty)) { + memory = ServerMemory( + id: const Uuid().v4(), + createdAt: DateTime.now(), + structured: Structured('', '', emoji: '⛓️‍💥', category: 'other'), + discarded: true, + transcriptSegments: segments, + geolocation: geolocation, + photos: photos.map((e) => MemoryPhoto(e.item1, e.item2)).toList(), + startedAt: currentTranscriptStartedAt, + finishedAt: currentTranscriptFinishedAt, + failed: true, + source: segments.isNotEmpty ? MemorySource.friend : MemorySource.openglass, + language: segments.isNotEmpty ? SharedPreferencesUtil().recordingsLanguage : null, + ); + SharedPreferencesUtil().addFailedMemory(memory); + + // TODO: store anyways something temporal and retry once connected again. + } + + if (memory != null) { + // use memory provider to add memory + // widget.addMemory(memory); + } + + if (memory != null && !memory.failed && file != null && segments.isNotEmpty && !memory.discarded) { + await memoryPostProcessing(file, memory.id).then((postProcessed) { + // use memory provider to update memory + // widget.updateMemory(postProcessed); + }); + } + + SharedPreferencesUtil().transcriptSegments = []; + segments = []; + audioStorage?.clearAudioBytes(); + setHasTranscripts(false); + + currentTranscriptStartedAt = null; + currentTranscriptFinishedAt = null; + elapsedSeconds = 0; + + streamStartedAtSecond = null; + firstStreamReceivedAt = null; + secondsMissedOnReconnect = null; + photos = []; + conversationId = const Uuid().v4(); + setMemoryCreating(false); + notifyListeners(); + return true; + } + + Future initiateWebsocket([ + BleAudioCodec? audioCodec, + int? sampleRate, + ]) async { + print('initiateWebsocket'); + BleAudioCodec codec = audioCodec ?? SharedPreferencesUtil().deviceCodec; + sampleRate ??= (codec == BleAudioCodec.opus ? 16000 : 8000); + await initWebSocket( + codec: codec, + sampleRate: sampleRate, + includeSpeechProfile: false, + onConnectionSuccess: () { + print('inside onConnectionSuccess'); + if (segments.isNotEmpty) { + // means that it was a reconnection, so we need to reset + streamStartedAtSecond = null; + secondsMissedOnReconnect = (DateTime.now().difference(firstStreamReceivedAt!).inSeconds); + } + notifyListeners(); + }, + onConnectionFailed: (err) { + notifyListeners(); + }, + onConnectionClosed: (int? closeCode, String? closeReason) { + print('inside onConnectionClosed'); + print('closeCode: $closeCode'); + // connection was closed, either on resetState, or by backend, or by some other reason. + // setState(() {}); + }, + onConnectionError: (err) { + print('inside onConnectionError'); + print('err: $err'); + // connection was okay, but then failed. + notifyListeners(); + }, + onMessageReceived: (List newSegments) { + if (newSegments.isEmpty) return; + if (segments.isEmpty) { + debugPrint('newSegments: ${newSegments.last}'); + // TODO: small bug -> when memory A creates, and memory B starts, memory B will clean a lot more seconds than available, + // losing from the audio the first part of the recording. All other parts are fine. + FlutterForegroundTask.sendDataToTask(jsonEncode({'location': true})); + var currentSeconds = (audioStorage?.frames.length ?? 0) ~/ 100; + var removeUpToSecond = newSegments[0].start.toInt(); + audioStorage?.removeFramesRange(fromSecond: 0, toSecond: min(max(currentSeconds - 5, 0), removeUpToSecond)); + firstStreamReceivedAt = DateTime.now(); + } + streamStartedAtSecond ??= newSegments[0].start; + + TranscriptSegment.combineSegments( + segments, + newSegments, + toRemoveSeconds: streamStartedAtSecond ?? 0, + toAddSeconds: secondsMissedOnReconnect ?? 0, + ); + triggerTranscriptSegmentReceivedEvents(newSegments, conversationId, sendMessageToChat: (v) { + // use message provider to send message to chat + }); + SharedPreferencesUtil().transcriptSegments = segments; + setHasTranscripts(true); + debugPrint('Memory creation timer restarted'); + _memoryCreationTimer?.cancel(); + _memoryCreationTimer = Timer(const Duration(seconds: quietSecondsForMemoryCreation), () => createMemory()); + currentTranscriptStartedAt ??= DateTime.now(); + currentTranscriptFinishedAt = DateTime.now(); + notifyListeners(); + }, + ); + } + + Future streamAudioToWs(String id, BleAudioCodec codec) async { + audioStorage = WavBytesUtil(codec: codec); + _bleBytesStream = await getBleAudioBytesListener( + id, + onAudioBytesReceived: (List value) { + if (value.isEmpty) return; + audioStorage!.storeFramePacket(value); + // print(value); + value.removeRange(0, 3); + // TODO: if this is not removed, deepgram can't seem to be able to detect the audio. + // https://developers.deepgram.com/docs/determining-your-audio-format-for-live-streaming-audio + if (wsConnectionState == WebsocketConnectionStatus.connected) { + websocketChannel?.sink.add(value); + } + }, + ); + } + + void closeBleStream() { + _bleBytesStream?.cancel(); + notifyListeners(); + } + + void cancelMemoryCreationTimer() { + _memoryCreationTimer?.cancel(); + notifyListeners(); + } +} From dbb4e92c8cd0ba673591b57a1b7c7186f15dd8cd Mon Sep 17 00:00:00 2001 From: Mohammed Mohsin <59914433+mdmohsin7@users.noreply.github.com> Date: Fri, 23 Aug 2024 00:13:03 +0530 Subject: [PATCH 11/23] memory and home improvements --- app/lib/pages/chat/page.dart | 3 +- app/lib/pages/home/page.dart | 56 +++++++++++++++----------- app/lib/providers/home_provider.dart | 7 ++++ app/lib/providers/memory_provider.dart | 1 - 4 files changed, 41 insertions(+), 26 deletions(-) diff --git a/app/lib/pages/chat/page.dart b/app/lib/pages/chat/page.dart index 5b653b78c..5a2749f57 100644 --- a/app/lib/pages/chat/page.dart +++ b/app/lib/pages/chat/page.dart @@ -50,7 +50,8 @@ class ChatPageState extends State with AutomaticKeepAliveClientMixin { @override void initState() { plugins = prefs.pluginsList; - SchedulerBinding.instance.addPostFrameCallback((_) { + SchedulerBinding.instance.addPostFrameCallback((_) async { + await context.read().refreshMessages(); _moveListToBottom(); }); // _initDailySummary(); diff --git a/app/lib/pages/home/page.dart b/app/lib/pages/home/page.dart index 2ea67a8bf..31b6535a7 100644 --- a/app/lib/pages/home/page.dart +++ b/app/lib/pages/home/page.dart @@ -134,17 +134,17 @@ class _HomePageState extends State with WidgetsBindingObserver, Ticker WidgetsBinding.instance.addObserver(this); WidgetsBinding.instance.addPostFrameCallback((_) async { + _initiatePlugins(); ForegroundUtil.requestPermissions(); await ForegroundUtil.initializeForegroundService(); ForegroundUtil.startForegroundTask(); if (mounted) { - await context.read().refreshMessages(); + //TODO: already disposed + // await context.read().refreshMessages(); await context.read().setupHasSpeakerProfile(); } }); - _initiatePlugins(); - //TODO: Should this run everytime? // _migrationScripts(); @@ -231,9 +231,8 @@ class _HomePageState extends State with WidgetsBindingObserver, Ticker _tabChange(int index) { MixpanelManager().bottomNavigationTabClicked(['Memories', 'Device', 'Chat'][index]); FocusScope.of(context).unfocus(); - setState(() { - _controller!.index = index; - }); + context.read().setIndex(index); + _controller!.animateTo(index); } @override @@ -372,12 +371,15 @@ class _HomePageState extends State with WidgetsBindingObserver, Ticker onPressed: () => _tabChange(0), child: Padding( padding: const EdgeInsets.only(top: 20, bottom: 20), - child: Text('Memories', - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle( - color: _controller!.index == 0 ? Colors.white : Colors.grey, - fontSize: 16)), + child: Text( + 'Memories', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: provider.selectedIndex == 0 ? Colors.white : Colors.grey, + fontSize: 16, + ), + ), ), ), ), @@ -389,12 +391,15 @@ class _HomePageState extends State with WidgetsBindingObserver, Ticker top: 20, bottom: 20, ), - child: Text('Capture', - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle( - color: _controller!.index == 1 ? Colors.white : Colors.grey, - fontSize: 16)), + child: Text( + 'Capture', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: provider.selectedIndex == 1 ? Colors.white : Colors.grey, + fontSize: 16, + ), + ), ), ), ), @@ -403,12 +408,15 @@ class _HomePageState extends State with WidgetsBindingObserver, Ticker onPressed: () => _tabChange(2), child: Padding( padding: const EdgeInsets.only(top: 20, bottom: 20), - child: Text('Chat', - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle( - color: _controller!.index == 2 ? Colors.white : Colors.grey, - fontSize: 16)), + child: Text( + 'Chat', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: provider.selectedIndex == 2 ? Colors.white : Colors.grey, + fontSize: 16, + ), + ), ), ), ), diff --git a/app/lib/providers/home_provider.dart b/app/lib/providers/home_provider.dart index 710192fa4..54c41800e 100644 --- a/app/lib/providers/home_provider.dart +++ b/app/lib/providers/home_provider.dart @@ -4,6 +4,13 @@ import 'package:friend_private/backend/preferences.dart'; import 'package:friend_private/utils/analytics/mixpanel.dart'; class HomeProvider extends ChangeNotifier { + int selectedIndex = 1; + + void setIndex(int index) { + selectedIndex = index; + notifyListeners(); + } + Future setupHasSpeakerProfile() async { SharedPreferencesUtil().hasSpeakerProfile = await userHasSpeakerProfile(); debugPrint('_setupHasSpeakerProfile: ${SharedPreferencesUtil().hasSpeakerProfile}'); diff --git a/app/lib/providers/memory_provider.dart b/app/lib/providers/memory_provider.dart index 60017cf9e..62b85aaab 100644 --- a/app/lib/providers/memory_provider.dart +++ b/app/lib/providers/memory_provider.dart @@ -41,7 +41,6 @@ class MemoryProvider extends ChangeNotifier { } void filterMemories(String query) { - if (query == previousQuery) return; filteredMemories = []; filteredMemories = displayDiscardMemories ? memories : memories.where((memory) => !memory.discarded).toList(); filteredMemories = query.isEmpty From a3d87efbf993d256f69f4452390a487158994d03 Mon Sep 17 00:00:00 2001 From: Mohammed Mohsin <59914433+mdmohsin7@users.noreply.github.com> Date: Fri, 23 Aug 2024 00:59:56 +0530 Subject: [PATCH 12/23] misc --- app/lib/main.dart | 10 +++++----- app/lib/pages/capture/page.dart | 8 +++++--- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/app/lib/main.dart b/app/lib/main.dart index 77da41494..6631c6667 100644 --- a/app/lib/main.dart +++ b/app/lib/main.dart @@ -138,16 +138,16 @@ class _MyAppState extends State { ListenableProvider(create: (context) => HomeProvider()), ListenableProvider(create: (context) => MemoryProvider()), ListenableProvider(create: (context) => PluginProvider()), - ChangeNotifierProxyProvider2( - create: (context) => CaptureProvider(), - update: (BuildContext context, memory, message, CaptureProvider? previous) => - CaptureProvider()..updateProviderInstances(memory, message), - ), ChangeNotifierProxyProvider( create: (context) => MessageProvider(), update: (BuildContext context, value, MessageProvider? previous) => MessageProvider()..updatePluginProvider(value), ), + ChangeNotifierProxyProvider( + create: (context) => CaptureProvider(), + update: (BuildContext context, memory, CaptureProvider? previous) => + CaptureProvider()..updateProviderInstances(memory), + ), ], builder: (context, child) { return WithForegroundTask( diff --git a/app/lib/pages/capture/page.dart b/app/lib/pages/capture/page.dart index 829d9fe82..58ea1a8be 100644 --- a/app/lib/pages/capture/page.dart +++ b/app/lib/pages/capture/page.dart @@ -198,7 +198,9 @@ class CapturePageState extends State time: DateTime.parse(data['time']), )); } else { - context.read().setGeolocation(null); + if (mounted) { + context.read().setGeolocation(null); + } } } } @@ -252,8 +254,8 @@ class CapturePageState extends State @override void dispose() { WidgetsBinding.instance.removeObserver(this); - context.read().closeBleStream(); - context.read().cancelMemoryCreationTimer(); + // context.read().closeBleStream(); + // context.read().cancelMemoryCreationTimer(); record.dispose(); _internetListener.cancel(); // websocketChannel From 8f47922f0434202805c7e43666ea79dafdaa25e6 Mon Sep 17 00:00:00 2001 From: Mohammed Mohsin <59914433+mdmohsin7@users.noreply.github.com> Date: Fri, 23 Aug 2024 11:11:41 +0530 Subject: [PATCH 13/23] fixes --- app/lib/pages/home/page.dart | 8 +- app/lib/pages/settings/developer.dart | 371 +++++++++++++------------ app/lib/providers/memory_provider.dart | 7 +- 3 files changed, 199 insertions(+), 187 deletions(-) diff --git a/app/lib/pages/home/page.dart b/app/lib/pages/home/page.dart index 09e51ba48..6f95ec119 100644 --- a/app/lib/pages/home/page.dart +++ b/app/lib/pages/home/page.dart @@ -211,9 +211,9 @@ class _HomePageState extends State with WidgetsBindingObserver, Ticker MixpanelManager().deviceConnected(); SharedPreferencesUtil().btDeviceStruct = _device!; SharedPreferencesUtil().deviceName = _device!.name; - if (mounted) { - setState(() {}); - } + // if (mounted) { + // setState(() {}); + // } } _initiateBleBatteryListener() async { @@ -522,7 +522,7 @@ class _HomePageState extends State with WidgetsBindingObserver, Ticker } else { await routeToPage(context, const ConnectedDevice(device: null, batteryLevel: 0)); } - setState(() {}); + // setState(() {}); }, style: TextButton.styleFrom( padding: EdgeInsets.zero, diff --git a/app/lib/pages/settings/developer.dart b/app/lib/pages/settings/developer.dart index c27d223f7..a7172d19b 100644 --- a/app/lib/pages/settings/developer.dart +++ b/app/lib/pages/settings/developer.dart @@ -65,127 +65,6 @@ class __DeveloperSettingsPageState extends State<_DeveloperSettingsPage> { body: Padding( padding: const EdgeInsets.symmetric(horizontal: 32), child: ListView( - children: [ - const SizedBox(height: 32), - _getText('Store your audios in Google Cloud Storage', bold: true), - const SizedBox(height: 16.0), - TextField( - controller: gcpCredentialsController, - obscureText: false, - autocorrect: false, - enableSuggestions: false, - enabled: true, - decoration: _getTextFieldDecoration('GCP Credentials (Base64)'), - style: const TextStyle(color: Colors.white), - ), - TextField( - controller: gcpBucketNameController, - obscureText: false, - autocorrect: false, - enabled: true, - enableSuggestions: false, - decoration: _getTextFieldDecoration('GCP Bucket Name'), - style: const TextStyle(color: Colors.white), - ), - const SizedBox(height: 16), - ListTile( - title: const Text('Import Memories'), - subtitle: const Text('Use with caution. All memories in the JSON file will be imported.'), - contentPadding: EdgeInsets.zero, - trailing: loadingImportMemories - ? const SizedBox( - height: 16, - width: 16, - child: CircularProgressIndicator( - color: Colors.white, - strokeWidth: 2, - ), - ) - : const Icon(Icons.download), - onTap: () async { - if (loadingImportMemories) return; - setState(() => loadingImportMemories = true); - // open file picker - var file = await FilePicker.platform.pickFiles( - type: FileType.custom, - allowedExtensions: ['json'], - ); - MixpanelManager().importMemories(); - if (file == null) { - setState(() => loadingImportMemories = false); - return; - } - var xFile = file.files.first.xFile; - try { - var content = (await xFile.readAsString()); - var decoded = jsonDecode(content); - // Export uses [ServerMemory] structure - List memories = decoded.map((e) => ServerMemory.fromJson(e)).toList(); - debugPrint('Memories: $memories'); - var memoriesJson = memories.map((m) => m.toJson()).toList(); - bool result = await migrateMemoriesToBackend(memoriesJson); - if (!result) { - SharedPreferencesUtil().scriptMigrateMemoriesToBack = false; - _snackBar('Failed to import memories. Make sure the file is a valid JSON file.', seconds: 3); - } - _snackBar('Memories imported, restart the app to see the changes. 🎉', seconds: 3); - MixpanelManager().importedMemories(); - SharedPreferencesUtil().scriptMigrateMemoriesToBack = true; - } catch (e) { - debugPrint(e.toString()); - _snackBar('Make sure the file is a valid JSON file.'); - } - setState(() => loadingImportMemories = false); - }, - ), - ListTile( - contentPadding: EdgeInsets.zero, - title: const Text('Export Memories'), - subtitle: const Text('Export all your memories to a JSON file.'), - trailing: loadingExportMemories - ? const SizedBox( - height: 16, - width: 16, - child: CircularProgressIndicator( - color: Colors.white, - strokeWidth: 2, - ), - ) - : const Icon(Icons.upload), - onTap: loadingExportMemories - ? null - : () async { - if (loadingExportMemories) return; - setState(() => loadingExportMemories = true); - List memories = await getMemories(limit: 10000, offset: 0); // 10k for now - String json = getPrettyJSONString(memories.map((m) => m.toJson()).toList()); - final directory = await getApplicationDocumentsDirectory(); - final file = File('${directory.path}/memories.json'); - await file.writeAsString(json); - - final result = - await Share.shareXFiles([XFile(file.path)], text: 'Exported Memories from Friend'); - if (result.status == ShareResultStatus.success) { - debugPrint('Thank you for sharing the picture!'); - } - MixpanelManager().exportMemories(); - // 54d2c392-57f1-46dc-b944-02740a651f7b - setState(() => loadingExportMemories = false); - }, - ), - const SizedBox(height: 20), - Container( - width: double.infinity, - height: 2, - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.2), - borderRadius: BorderRadius.circular(12), - ), - ), - const SizedBox(height: 20), - Row( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const SizedBox(height: 32), _getText('Store your audios in Google Cloud Storage', bold: true), @@ -240,12 +119,19 @@ class __DeveloperSettingsPageState extends State<_DeveloperSettingsPage> { try { var content = (await xFile.readAsString()); var decoded = jsonDecode(content); - List memories = decoded.map((e) => Memory.fromJson(e)).toList(); + // Export uses [ServerMemory] structure + List memories = + decoded.map((e) => ServerMemory.fromJson(e)).toList(); debugPrint('Memories: $memories'); - MemoryProvider().storeMemories(memories); + var memoriesJson = memories.map((m) => m.toJson()).toList(); + bool result = await migrateMemoriesToBackend(memoriesJson); + if (!result) { + SharedPreferencesUtil().scriptMigrateMemoriesToBack = false; + _snackBar('Failed to import memories. Make sure the file is a valid JSON file.', seconds: 3); + } _snackBar('Memories imported, restart the app to see the changes. 🎉', seconds: 3); MixpanelManager().importedMemories(); - SharedPreferencesUtil().scriptMigrateMemoriesToBack = false; + SharedPreferencesUtil().scriptMigrateMemoriesToBack = true; } catch (e) { debugPrint(e.toString()); _snackBar('Make sure the file is a valid JSON file.'); @@ -302,65 +188,188 @@ class __DeveloperSettingsPageState extends State<_DeveloperSettingsPage> { crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - const Text('Plugin Integrations Testing', - style: TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.w600)), - GestureDetector( - onTap: () { - launchUrl(Uri.parse('https://docs.basedhardware.com/developer/plugins/Integrations/')); - MixpanelManager().advancedModeDocsOpened(); - }, - child: const Padding( - padding: EdgeInsets.all(8.0), - child: Text( - 'Docs', - style: TextStyle( - color: Colors.white, - fontSize: 14, - decoration: TextDecoration.underline, - ), - ), - )) + const SizedBox(height: 32), + _getText('Store your audios in Google Cloud Storage', bold: true), + const SizedBox(height: 16.0), + TextField( + controller: provider.gcpCredentialsController, + obscureText: false, + autocorrect: false, + enableSuggestions: false, + enabled: true, + decoration: _getTextFieldDecoration('GCP Credentials (Base64)'), + style: const TextStyle(color: Colors.white), + ), + TextField( + controller: provider.gcpBucketNameController, + obscureText: false, + autocorrect: false, + enabled: true, + enableSuggestions: false, + decoration: _getTextFieldDecoration('GCP Bucket Name'), + style: const TextStyle(color: Colors.white), + ), + const SizedBox(height: 16), + ListTile( + title: const Text('Import Memories'), + subtitle: const Text('Use with caution. All memories in the JSON file will be imported.'), + contentPadding: EdgeInsets.zero, + trailing: provider.loadingImportMemories + ? const SizedBox( + height: 16, + width: 16, + child: CircularProgressIndicator( + color: Colors.white, + strokeWidth: 2, + ), + ) + : const Icon(Icons.download), + onTap: () async { + if (provider.loadingImportMemories) return; + setState(() => provider.loadingImportMemories = true); + // open file picker + var file = await FilePicker.platform.pickFiles( + type: FileType.custom, + allowedExtensions: ['json'], + ); + MixpanelManager().importMemories(); + if (file == null) { + setState(() => provider.loadingImportMemories = false); + return; + } + var xFile = file.files.first.xFile; + try { + var content = (await xFile.readAsString()); + var decoded = jsonDecode(content); + List memories = decoded.map((e) => Memory.fromJson(e)).toList(); + debugPrint('Memories: $memories'); + var memoriesJson = memories.map((m) => m.toJson()).toList(); + bool result = await migrateMemoriesToBackend(memoriesJson); + if (!result) { + SharedPreferencesUtil().scriptMigrateMemoriesToBack = false; + _snackBar('Failed to import memories. Make sure the file is a valid JSON file.', + seconds: 3); + } + _snackBar('Memories imported, restart the app to see the changes. 🎉', seconds: 3); + MixpanelManager().importedMemories(); + SharedPreferencesUtil().scriptMigrateMemoriesToBack = true; + } catch (e) { + debugPrint(e.toString()); + _snackBar('Make sure the file is a valid JSON file.'); + } + setState(() => provider.loadingImportMemories = false); + }, + ), + ListTile( + contentPadding: EdgeInsets.zero, + title: const Text('Export Memories'), + subtitle: const Text('Export all your memories to a JSON file.'), + trailing: provider.loadingExportMemories + ? const SizedBox( + height: 16, + width: 16, + child: CircularProgressIndicator( + color: Colors.white, + strokeWidth: 2, + ), + ) + : const Icon(Icons.upload), + onTap: provider.loadingExportMemories + ? null + : () async { + if (provider.loadingExportMemories) return; + setState(() => provider.loadingExportMemories = true); + List memories = await getMemories(limit: 10000, offset: 0); // 10k for now + String json = getPrettyJSONString(memories.map((m) => m.toJson()).toList()); + final directory = await getApplicationDocumentsDirectory(); + final file = File('${directory.path}/memories.json'); + await file.writeAsString(json); + + final result = + await Share.shareXFiles([XFile(file.path)], text: 'Exported Memories from Friend'); + if (result.status == ShareResultStatus.success) { + debugPrint('Thank you for sharing the picture!'); + } + MixpanelManager().exportMemories(); + // 54d2c392-57f1-46dc-b944-02740a651f7b + setState(() => provider.loadingExportMemories = false); + }, + ), + const SizedBox(height: 20), + Container( + width: double.infinity, + height: 2, + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.2), + borderRadius: BorderRadius.circular(12), + ), + ), + const SizedBox(height: 20), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text('Plugin Integrations Testing', + style: TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.w600)), + GestureDetector( + onTap: () { + launchUrl(Uri.parse('https://docs.basedhardware.com/developer/plugins/Integrations/')); + MixpanelManager().advancedModeDocsOpened(); + }, + child: const Padding( + padding: EdgeInsets.all(8.0), + child: Text( + 'Docs', + style: TextStyle( + color: Colors.white, + fontSize: 14, + decoration: TextDecoration.underline, + ), + ), + )) + ], + ), + const SizedBox(height: 16), + const Text( + 'On Memory Created:', + style: TextStyle(color: Colors.white, fontWeight: FontWeight.w600, fontSize: 16), + ), + const SizedBox(height: 4), + const Text( + 'Triggered when FRIEND creates a new memory.', + style: TextStyle(color: Colors.white, fontSize: 14), + ), + TextField( + controller: provider.webhookOnMemoryCreated, + obscureText: false, + autocorrect: false, + enabled: true, + enableSuggestions: false, + decoration: _getTextFieldDecoration('Endpoint URL'), + style: const TextStyle(color: Colors.white), + ), + const SizedBox(height: 16), + const Text( + 'Real-Time Transcript Processing:', + style: TextStyle(color: Colors.white, fontWeight: FontWeight.w600, fontSize: 16), + ), + const SizedBox(height: 4), + const Text( + 'Triggered as the transcript is being received.', + style: TextStyle(color: Colors.white, fontSize: 14), + ), + TextField( + controller: provider.webhookOnTranscriptReceived, + obscureText: false, + autocorrect: false, + enabled: true, + enableSuggestions: false, + decoration: _getTextFieldDecoration('Endpoint URL'), + style: const TextStyle(color: Colors.white), + ), + const SizedBox(height: 64), ], ), - const SizedBox(height: 16), - const Text( - 'On Memory Created:', - style: TextStyle(color: Colors.white, fontWeight: FontWeight.w600, fontSize: 16), - ), - const SizedBox(height: 4), - const Text( - 'Triggered when FRIEND creates a new memory.', - style: TextStyle(color: Colors.white, fontSize: 14), - ), - TextField( - controller: provider.webhookOnMemoryCreated, - obscureText: false, - autocorrect: false, - enabled: true, - enableSuggestions: false, - decoration: _getTextFieldDecoration('Endpoint URL'), - style: const TextStyle(color: Colors.white), - ), - const SizedBox(height: 16), - const Text( - 'Real-Time Transcript Processing:', - style: TextStyle(color: Colors.white, fontWeight: FontWeight.w600, fontSize: 16), - ), - const SizedBox(height: 4), - const Text( - 'Triggered as the transcript is being received.', - style: TextStyle(color: Colors.white, fontSize: 14), - ), - TextField( - controller: provider.webhookOnTranscriptReceived, - obscureText: false, - autocorrect: false, - enabled: true, - enableSuggestions: false, - decoration: _getTextFieldDecoration('Endpoint URL'), - style: const TextStyle(color: Colors.white), - ), - const SizedBox(height: 64), ], ), ), diff --git a/app/lib/providers/memory_provider.dart b/app/lib/providers/memory_provider.dart index 62b85aaab..eabe0d6d9 100644 --- a/app/lib/providers/memory_provider.dart +++ b/app/lib/providers/memory_provider.dart @@ -44,8 +44,8 @@ class MemoryProvider extends ChangeNotifier { filteredMemories = []; filteredMemories = displayDiscardMemories ? memories : memories.where((memory) => !memory.discarded).toList(); filteredMemories = query.isEmpty - ? memories - : memories + ? filteredMemories + : filteredMemories .where( (memory) => (memory.getTranscript() + memory.structured.title + memory.structured.overview) .toLowerCase() @@ -60,6 +60,7 @@ class MemoryProvider extends ChangeNotifier { MixpanelManager().showDiscardedMemoriesToggled(!displayDiscardMemories); displayDiscardMemories = !displayDiscardMemories; filterMemories(''); + populateMemoriesWithDates(); notifyListeners(); } @@ -100,6 +101,7 @@ class MemoryProvider extends ChangeNotifier { void addMemory(ServerMemory memory) { memories.insert(0, memory); + filterMemories(''); notifyListeners(); } @@ -112,6 +114,7 @@ class MemoryProvider extends ChangeNotifier { memories[i] = memory; } } + filterMemories(''); notifyListeners(); } From c3421cc59545dfcbc1c477016ef29c50d37211ef Mon Sep 17 00:00:00 2001 From: Mohammed Mohsin <59914433+mdmohsin7@users.noreply.github.com> Date: Fri, 23 Aug 2024 11:41:39 +0530 Subject: [PATCH 14/23] fix proxy provider losing state issue --- app/lib/main.dart | 13 +++++++------ app/lib/providers/capture_provider.dart | 16 +++++++++------- app/lib/providers/plugin_provider.dart | 1 - 3 files changed, 16 insertions(+), 14 deletions(-) diff --git a/app/lib/main.dart b/app/lib/main.dart index 4749f55b2..a2cb74c9a 100644 --- a/app/lib/main.dart +++ b/app/lib/main.dart @@ -134,17 +134,18 @@ class _MyAppState extends State { ChangeNotifierProvider(create: (context) => AuthenticationProvider()), ChangeNotifierProvider(create: (context) => OnboardingProvider()), ListenableProvider(create: (context) => HomeProvider()), - ListenableProvider(create: (context) => MemoryProvider()), + ChangeNotifierProvider(create: (context) => MemoryProvider()), + ChangeNotifierProvider(create: (context) => MessageProvider()), ListenableProvider(create: (context) => PluginProvider()), ChangeNotifierProxyProvider( create: (context) => MessageProvider(), update: (BuildContext context, value, MessageProvider? previous) => - MessageProvider()..updatePluginProvider(value), + (previous?..updatePluginProvider(value)) ?? MessageProvider(), ), - ChangeNotifierProxyProvider( + ChangeNotifierProxyProvider2( create: (context) => CaptureProvider(), - update: (BuildContext context, memory, CaptureProvider? previous) => - CaptureProvider()..updateProviderInstances(memory), + update: (BuildContext context, memory, message, CaptureProvider? previous) => + (previous?..updateProviderInstances(memory, message)) ?? CaptureProvider(), ), ], builder: (context, child) { @@ -216,4 +217,4 @@ class _MyAppState extends State { // )); // audioSession.setActive(true); // }); -// } +// } \ No newline at end of file diff --git a/app/lib/providers/capture_provider.dart b/app/lib/providers/capture_provider.dart index 8de5980fe..955863587 100644 --- a/app/lib/providers/capture_provider.dart +++ b/app/lib/providers/capture_provider.dart @@ -28,6 +28,11 @@ class CaptureProvider extends ChangeNotifier with WebSocketMixin, OpenGlassMixin MemoryProvider? memoryProvider; MessageProvider? messageProvider; + void updateProviderInstances(MemoryProvider? mp, MessageProvider? p) { + memoryProvider = mp; + messageProvider = p; + } + List segments = []; Geolocation? geolocation; @@ -51,11 +56,6 @@ class CaptureProvider extends ChangeNotifier with WebSocketMixin, OpenGlassMixin int elapsedSeconds = 0; // ----------------------- - void updateProviderInstances(MemoryProvider mp, MessageProvider p) { - memoryProvider = mp; - messageProvider = p; - } - void setHasTranscripts(bool value) { hasTranscripts = value; notifyListeners(); @@ -97,6 +97,7 @@ class CaptureProvider extends ChangeNotifier with WebSocketMixin, OpenGlassMixin photos: photos, sendMessageToChat: (v) { // use message provider to send message to chat + messageProvider?.addMessage(v); }, triggerIntegrations: true, language: SharedPreferencesUtil().recordingsLanguage, @@ -125,14 +126,15 @@ class CaptureProvider extends ChangeNotifier with WebSocketMixin, OpenGlassMixin if (memory != null) { // use memory provider to add memory - // widget.addMemory(memory); + memoryProvider?.addMemory(memory); } if (memory != null && !memory.failed && file != null && segments.isNotEmpty && !memory.discarded) { await memoryPostProcessing(file, memory.id).then((postProcessed) { // use memory provider to update memory - // widget.updateMemory(postProcessed); + memoryProvider?.updateMemory(postProcessed); }); + setMemoryCreating(false); } SharedPreferencesUtil().transcriptSegments = []; diff --git a/app/lib/providers/plugin_provider.dart b/app/lib/providers/plugin_provider.dart index 17a3e95be..4073c1893 100644 --- a/app/lib/providers/plugin_provider.dart +++ b/app/lib/providers/plugin_provider.dart @@ -1,4 +1,3 @@ -import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:friend_private/backend/http/api/plugins.dart'; import 'package:friend_private/backend/preferences.dart'; From 5cd0433eff9c399343309c594d5008cbe45a9911 Mon Sep 17 00:00:00 2001 From: Mohammed Mohsin <59914433+mdmohsin7@users.noreply.github.com> Date: Fri, 23 Aug 2024 13:09:23 +0530 Subject: [PATCH 15/23] plugin provider improvements [WIP] --- app/lib/pages/home/page.dart | 2 - app/lib/pages/plugins/page.dart | 849 ++++++++++++------------- app/lib/providers/plugin_provider.dart | 58 ++ 3 files changed, 474 insertions(+), 435 deletions(-) diff --git a/app/lib/pages/home/page.dart b/app/lib/pages/home/page.dart index 6f95ec119..1109d5665 100644 --- a/app/lib/pages/home/page.dart +++ b/app/lib/pages/home/page.dart @@ -5,8 +5,6 @@ import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter_blue_plus/flutter_blue_plus.dart'; import 'package:flutter_foreground_task/flutter_foreground_task.dart'; -import 'package:friend_private/backend/http/api/plugins.dart'; -import 'package:friend_private/backend/http/api/speech_profile.dart'; import 'package:friend_private/backend/http/cloud_storage.dart'; import 'package:friend_private/backend/preferences.dart'; import 'package:friend_private/backend/schema/bt_device.dart'; diff --git a/app/lib/pages/plugins/page.dart b/app/lib/pages/plugins/page.dart index ebfed0e4b..85463c37f 100644 --- a/app/lib/pages/plugins/page.dart +++ b/app/lib/pages/plugins/page.dart @@ -4,11 +4,13 @@ import 'package:friend_private/backend/http/api/plugins.dart'; import 'package:friend_private/backend/preferences.dart'; import 'package:friend_private/backend/schema/plugin.dart'; import 'package:friend_private/pages/plugins/plugin_detail.dart'; +import 'package:friend_private/providers/plugin_provider.dart'; import 'package:friend_private/utils/analytics/mixpanel.dart'; import 'package:friend_private/utils/connectivity_controller.dart'; import 'package:friend_private/utils/other/temp.dart'; import 'package:friend_private/widgets/dialog.dart'; import 'package:gradient_borders/gradient_borders.dart'; +import 'package:provider/provider.dart'; import 'package:url_launcher/url_launcher.dart'; class PluginsPage extends StatefulWidget { @@ -22,28 +24,27 @@ class PluginsPage extends StatefulWidget { class _PluginsPageState extends State { bool isLoading = true; - String searchQuery = ''; - List plugins = SharedPreferencesUtil().pluginsList; - late List pluginLoading; - bool filterChat = true; - bool filterMemories = true; - bool filterExternal = true; + // bool filterChat = true; + // bool filterMemories = true; + // bool filterExternal = true; @override void initState() { - if (widget.filterChatOnly) { - filterChat = true; - filterMemories = false; - filterExternal = false; - } - pluginLoading = List.filled(plugins.length, false); + WidgetsBinding.instance.addPostFrameCallback((_) async { + await context.read().getPlugins(); + if (mounted) { + if (widget.filterChatOnly) { + context.read().setChatFilterOnly(); + } + } + }); super.initState(); } Future _togglePlugin(String pluginId, bool isEnabled, int idx) async { - if (pluginLoading[idx]) return; - setState(() => pluginLoading[idx] = true); + // if (pluginLoading[idx]) return; + // setState(() => pluginLoading[idx] = true); var prefs = SharedPreferencesUtil(); if (isEnabled) { var enabled = await enablePluginServer(pluginId); @@ -58,7 +59,7 @@ class _PluginsPageState extends State { 'If this is an integration plugin, make sure the setup is completed.', singleButton: true, )); - setState(() => pluginLoading[idx] = false); + // setState(() => pluginLoading[idx] = false); return; } prefs.enablePlugin(pluginId); @@ -68,27 +69,12 @@ class _PluginsPageState extends State { prefs.disablePlugin(pluginId); MixpanelManager().pluginDisabled(pluginId); } - setState(() => pluginLoading[idx] = false); - setState(() => plugins = SharedPreferencesUtil().pluginsList); - } - - List _filteredPlugins() { - var plugins = this - .plugins - .where((p) => - (p.worksWithChat() && filterChat) || - (p.worksWithMemories() && filterMemories) || - (p.worksExternally() && filterExternal)) - .toList(); - - return searchQuery.isEmpty - ? plugins - : plugins.where((plugin) => plugin.name.toLowerCase().contains(searchQuery.toLowerCase())).toList(); + // setState(() => pluginLoading[idx] = false); + // setState(() => plugins = SharedPreferencesUtil().pluginsList); } @override Widget build(BuildContext context) { - final filteredPlugins = _filteredPlugins(); return Scaffold( backgroundColor: Theme.of(context).colorScheme.primary, appBar: AppBar( @@ -115,427 +101,424 @@ class _PluginsPageState extends State { )) ], ), - body: GestureDetector( - onTap: () => FocusScope.of(context).unfocus(), - child: CustomScrollView( - slivers: [ - const SliverToBoxAdapter( - child: SizedBox(height: 32), - ), - SliverToBoxAdapter( - child: Container( - padding: const EdgeInsets.fromLTRB(16, 8, 8, 0), - margin: const EdgeInsets.fromLTRB(18, 0, 18, 0), - decoration: const BoxDecoration( - color: Colors.black, - borderRadius: BorderRadius.all(Radius.circular(16)), - border: GradientBoxBorder( - gradient: LinearGradient(colors: [ - Color.fromARGB(127, 208, 208, 208), - Color.fromARGB(127, 188, 99, 121), - Color.fromARGB(127, 86, 101, 182), - Color.fromARGB(127, 126, 190, 236) - ]), - width: 1, + body: Consumer(builder: (context, provider, child) { + return GestureDetector( + onTap: () => FocusScope.of(context).unfocus(), + child: CustomScrollView( + slivers: [ + const SliverToBoxAdapter( + child: SizedBox(height: 32), + ), + SliverToBoxAdapter( + child: Container( + padding: const EdgeInsets.fromLTRB(16, 8, 8, 0), + margin: const EdgeInsets.fromLTRB(18, 0, 18, 0), + decoration: const BoxDecoration( + color: Colors.black, + borderRadius: BorderRadius.all(Radius.circular(16)), + border: GradientBoxBorder( + gradient: LinearGradient(colors: [ + Color.fromARGB(127, 208, 208, 208), + Color.fromARGB(127, 188, 99, 121), + Color.fromARGB(127, 86, 101, 182), + Color.fromARGB(127, 126, 190, 236) + ]), + width: 1, + ), + shape: BoxShape.rectangle, ), - shape: BoxShape.rectangle, - ), - // TODO: reuse chat textfield - child: TextField( - onChanged: (value) { - setState(() { - searchQuery = value; - }); - }, - obscureText: false, - decoration: InputDecoration( - hintText: 'Find your plugin...', - hintStyle: const TextStyle(fontSize: 14.0, color: Colors.grey), - enabledBorder: InputBorder.none, - focusedBorder: InputBorder.none, - suffixIcon: searchQuery.isEmpty - ? const SizedBox.shrink() - : IconButton( - icon: const Icon( - Icons.cancel, - color: Color(0xFFF7F4F4), - size: 28.0, + // TODO: reuse chat textfield + child: TextField( + onChanged: (value) { + provider.filterPlugins(value); + }, + obscureText: false, + decoration: InputDecoration( + hintText: 'Find your plugin...', + hintStyle: const TextStyle(fontSize: 14.0, color: Colors.grey), + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + suffixIcon: provider.searchQuery.isEmpty + ? const SizedBox.shrink() + : IconButton( + icon: const Icon( + Icons.cancel, + color: Color(0xFFF7F4F4), + size: 28.0, + ), + onPressed: () { + provider.clearSearchQuery(); + }, ), - onPressed: () { - searchQuery = ''; - setState(() {}); - }, - ), - ), - style: const TextStyle( - // fontFamily: FlutterFlowTheme.of(context).bodyMediumFamily, - color: Colors.white, - fontWeight: FontWeight.w500, + ), + style: const TextStyle( + // fontFamily: FlutterFlowTheme.of(context).bodyMediumFamily, + color: Colors.white, + fontWeight: FontWeight.w500, + ), ), ), ), - ), - const SliverToBoxAdapter(child: SizedBox(height: 16)), - SliverToBoxAdapter( - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 20), - child: Row( - children: [ - // const Text( - // 'Filter:', - // style: TextStyle(color: Colors.white, fontSize: 16), - // ), - // const SizedBox(width: 16), - GestureDetector( - onTap: () { - setState(() { - filterMemories = !filterMemories; - }); - }, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), - decoration: BoxDecoration( - color: filterMemories ? Colors.deepPurple : Colors.transparent, - borderRadius: BorderRadius.circular(16), - border: - filterMemories ? Border.all(color: Colors.deepPurple) : Border.all(color: Colors.grey), - ), - child: const Text( - 'Memories', - style: TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.w500), + const SliverToBoxAdapter(child: SizedBox(height: 16)), + SliverToBoxAdapter( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Row( + children: [ + // const Text( + // 'Filter:', + // style: TextStyle(color: Colors.white, fontSize: 16), + // ), + // const SizedBox(width: 16), + GestureDetector( + onTap: () { + provider.setFilterMemories(!provider.filterMemories); + }, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), + decoration: BoxDecoration( + color: provider.filterMemories ? Colors.deepPurple : Colors.transparent, + borderRadius: BorderRadius.circular(16), + border: provider.filterMemories + ? Border.all(color: Colors.deepPurple) + : Border.all(color: Colors.grey), + ), + child: const Text( + 'Memories', + style: TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.w500), + ), ), ), - ), - const SizedBox(width: 8), - GestureDetector( - onTap: () { - setState(() { - filterChat = !filterChat; - }); - }, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), - decoration: BoxDecoration( - color: filterChat ? Colors.deepPurple : Colors.transparent, - borderRadius: BorderRadius.circular(16), - border: filterChat ? Border.all(color: Colors.deepPurple) : Border.all(color: Colors.grey), - ), - child: const Text( - 'Chat', - style: TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.w500), + const SizedBox(width: 8), + GestureDetector( + onTap: () { + provider.setFilterChat(!provider.filterChat); + }, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), + decoration: BoxDecoration( + color: provider.filterChat ? Colors.deepPurple : Colors.transparent, + borderRadius: BorderRadius.circular(16), + border: provider.filterChat + ? Border.all(color: Colors.deepPurple) + : Border.all(color: Colors.grey), + ), + child: const Text( + 'Chat', + style: TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.w500), + ), ), ), - ), - const SizedBox(width: 8), - GestureDetector( - onTap: () { - setState(() { - filterExternal = !filterExternal; - }); - }, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), - decoration: BoxDecoration( - color: filterExternal ? Colors.deepPurple : Colors.transparent, - borderRadius: BorderRadius.circular(16), - border: - filterExternal ? Border.all(color: Colors.deepPurple) : Border.all(color: Colors.grey), - ), - child: const Text( - 'Integration', - style: TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.w500), + const SizedBox(width: 8), + GestureDetector( + onTap: () { + provider.setFilterExternal(!provider.filterExternal); + }, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), + decoration: BoxDecoration( + color: provider.filterExternal ? Colors.deepPurple : Colors.transparent, + borderRadius: BorderRadius.circular(16), + border: provider.filterExternal + ? Border.all(color: Colors.deepPurple) + : Border.all(color: Colors.grey), + ), + child: const Text( + 'Integration', + style: TextStyle(color: Colors.white, fontSize: 14, fontWeight: FontWeight.w500), + ), ), ), - ), - ], + ], + ), ), ), - ), - // const SliverToBoxAdapter(child: SizedBox(height: 8)), - filteredPlugins.isEmpty - ? SliverToBoxAdapter( - child: Padding( - padding: EdgeInsets.only(top: 64, left: 14, right: 14), - child: Center( - child: Text( - ConnectivityController().isConnected.value - ? 'No plugins found' - : 'Unable to fetch plugins :(\n\nPlease check your internet connection and try again.', - style: TextStyle(color: Colors.white, fontSize: 16), - textAlign: TextAlign.center, - ), - ), - ), - ) - : const SliverToBoxAdapter(child: SizedBox(height: 8)), - SliverList( - delegate: SliverChildBuilderDelegate( - (context, index) { - final plugin = filteredPlugins[index]; - return Container( - padding: const EdgeInsets.fromLTRB(8, 8, 0, 8), - decoration: BoxDecoration( - shape: BoxShape.rectangle, - borderRadius: const BorderRadius.all(Radius.circular(16.0)), - color: Colors.grey.shade900, - ), - margin: EdgeInsets.only(bottom: 12, top: index == 0 ? 24 : 0, left: 16, right: 16), - child: ListTile( - onTap: () async { - await routeToPage(context, PluginDetailPage(plugin: plugin)); - setState(() => plugins = SharedPreferencesUtil().pluginsList); - }, - leading: CachedNetworkImage( - imageUrl: plugin.getImageUrl(), - imageBuilder: (context, imageProvider) => CircleAvatar( - backgroundColor: Colors.white, - maxRadius: 28, - backgroundImage: imageProvider, - ), - placeholder: (context, url) => const CircularProgressIndicator(), - errorWidget: (context, url, error) => const Icon(Icons.error), - ), - title: Text( - plugin.name, - maxLines: 1, - style: const TextStyle(fontWeight: FontWeight.w600, color: Colors.white, fontSize: 16), - ), - subtitle: Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox(height: plugin.ratingAvg != null ? 4 : 0), - plugin.ratingAvg != null - ? Row( - children: [ - Text(plugin.getRatingAvg()!), - const SizedBox(width: 4), - const Icon(Icons.star, color: Colors.deepPurple, size: 16), - const SizedBox(width: 4), - Text('(${plugin.ratingCount})'), - ], - ) - : Container(), - Padding( - padding: const EdgeInsets.only(top: 4.0), + // const SliverToBoxAdapter(child: SizedBox(height: 8)), + provider.filteredPlugins.isEmpty + ? SliverToBoxAdapter( + child: Padding( + padding: EdgeInsets.only(top: 64, left: 14, right: 14), + child: Center( child: Text( - plugin.description, - maxLines: 2, - style: const TextStyle(color: Colors.grey, fontSize: 14), + ConnectivityController().isConnected.value + ? 'No plugins found' + : 'Unable to fetch plugins :(\n\nPlease check your internet connection and try again.', + style: TextStyle(color: Colors.white, fontSize: 16), + textAlign: TextAlign.center, ), ), - const SizedBox(height: 8), - Row( - children: [ - plugin.worksWithMemories() - ? Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), - decoration: BoxDecoration( - color: Colors.grey, - borderRadius: BorderRadius.circular(16), - ), - child: const Text( - 'Memories', - style: TextStyle( - color: Colors.deepPurple, fontSize: 12, fontWeight: FontWeight.w500), - ), - ) - : const SizedBox.shrink(), - SizedBox(width: plugin.worksWithChat() ? 8 : 0), - plugin.worksWithChat() - ? Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), - decoration: BoxDecoration( - color: Colors.grey, - borderRadius: BorderRadius.circular(16), - ), - child: const Text( - 'Chat', - style: TextStyle( - color: Colors.deepPurple, fontSize: 12, fontWeight: FontWeight.w500), - ), - ) - : const SizedBox.shrink(), - SizedBox(width: plugin.worksExternally() ? 8 : 0), - plugin.worksExternally() - ? Container( - padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), - decoration: BoxDecoration( - color: Colors.grey, - borderRadius: BorderRadius.circular(16), - ), - child: const Text( - 'Integration', - style: TextStyle( - color: Colors.deepPurple, fontSize: 12, fontWeight: FontWeight.w500), - ), - ) - : const SizedBox.shrink(), - ], - ) - ], + ), + ) + : const SliverToBoxAdapter(child: SizedBox(height: 8)), + SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + final plugin = provider.filteredPlugins[index]; + return Container( + padding: const EdgeInsets.fromLTRB(8, 8, 0, 8), + decoration: BoxDecoration( + shape: BoxShape.rectangle, + borderRadius: const BorderRadius.all(Radius.circular(16.0)), + color: Colors.grey.shade900, ), - trailing: pluginLoading[index] - ? const SizedBox( - height: 24, - width: 24, - child: CircularProgressIndicator( - valueColor: AlwaysStoppedAnimation(Colors.white), + margin: EdgeInsets.only(bottom: 12, top: index == 0 ? 24 : 0, left: 16, right: 16), + child: ListTile( + onTap: () async { + await routeToPage(context, PluginDetailPage(plugin: plugin)); + // setState(() => plugins = SharedPreferencesUtil().pluginsList); + }, + leading: CachedNetworkImage( + imageUrl: plugin.getImageUrl(), + imageBuilder: (context, imageProvider) => CircleAvatar( + backgroundColor: Colors.white, + maxRadius: 28, + backgroundImage: imageProvider, + ), + placeholder: (context, url) => const CircularProgressIndicator(), + errorWidget: (context, url, error) => const Icon(Icons.error), + ), + title: Text( + plugin.name, + maxLines: 1, + style: const TextStyle(fontWeight: FontWeight.w600, color: Colors.white, fontSize: 16), + ), + subtitle: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: plugin.ratingAvg != null ? 4 : 0), + plugin.ratingAvg != null + ? Row( + children: [ + Text(plugin.getRatingAvg()!), + const SizedBox(width: 4), + const Icon(Icons.star, color: Colors.deepPurple, size: 16), + const SizedBox(width: 4), + Text('(${plugin.ratingCount})'), + ], + ) + : Container(), + Padding( + padding: const EdgeInsets.only(top: 4.0), + child: Text( + plugin.description, + maxLines: 2, + style: const TextStyle(color: Colors.grey, fontSize: 14), ), + ), + const SizedBox(height: 8), + Row( + children: [ + plugin.worksWithMemories() + ? Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + decoration: BoxDecoration( + color: Colors.grey, + borderRadius: BorderRadius.circular(16), + ), + child: const Text( + 'Memories', + style: TextStyle( + color: Colors.deepPurple, fontSize: 12, fontWeight: FontWeight.w500), + ), + ) + : const SizedBox.shrink(), + SizedBox(width: plugin.worksWithChat() ? 8 : 0), + plugin.worksWithChat() + ? Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + decoration: BoxDecoration( + color: Colors.grey, + borderRadius: BorderRadius.circular(16), + ), + child: const Text( + 'Chat', + style: TextStyle( + color: Colors.deepPurple, fontSize: 12, fontWeight: FontWeight.w500), + ), + ) + : const SizedBox.shrink(), + SizedBox(width: plugin.worksExternally() ? 8 : 0), + plugin.worksExternally() + ? Container( + padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + decoration: BoxDecoration( + color: Colors.grey, + borderRadius: BorderRadius.circular(16), + ), + child: const Text( + 'Integration', + style: TextStyle( + color: Colors.deepPurple, fontSize: 12, fontWeight: FontWeight.w500), + ), + ) + : const SizedBox.shrink(), + ], ) - : IconButton( - icon: Icon( - plugin.enabled ? Icons.check : Icons.arrow_downward_rounded, - color: plugin.enabled ? Colors.white : Colors.grey, + ], + ), + trailing: provider.pluginLoading[index] + ? const SizedBox( + height: 24, + width: 24, + child: CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation(Colors.white), + ), + ) + : IconButton( + icon: Icon( + plugin.enabled ? Icons.check : Icons.arrow_downward_rounded, + color: plugin.enabled ? Colors.white : Colors.grey, + ), + onPressed: () { + if (plugin.worksExternally() && !plugin.enabled) { + showDialog( + context: context, + builder: (c) => getDialog( + context, + () => Navigator.pop(context), + () async { + Navigator.pop(context); + await routeToPage(context, PluginDetailPage(plugin: plugin)); + // setState(() => plugins = SharedPreferencesUtil().pluginsList); + }, + 'Authorize External Plugin', + 'Do you allow this plugin to access your memories, transcripts, and recordings? Your data will be sent to the plugin\'s server for processing.', + okButtonText: 'Confirm', + ), + ); + } else { + _togglePlugin(plugin.id.toString(), !plugin.enabled, index); + } + }, ), - onPressed: () { - if (plugin.worksExternally() && !plugin.enabled) { - showDialog( - context: context, - builder: (c) => getDialog( - context, - () => Navigator.pop(context), - () async { - Navigator.pop(context); - await routeToPage(context, PluginDetailPage(plugin: plugin)); - setState(() => plugins = SharedPreferencesUtil().pluginsList); - }, - 'Authorize External Plugin', - 'Do you allow this plugin to access your memories, transcripts, and recordings? Your data will be sent to the plugin\'s server for processing.', - okButtonText: 'Confirm', - ), - ); - } else { - _togglePlugin(plugin.id.toString(), !plugin.enabled, index); - } - }, - ), - // trailing: Switch( - // value: plugin.isEnabled, - // activeColor: Colors.deepPurple, - // onChanged: (value) { - // _togglePlugin(plugin.id.toString(), value); - // }, - // ), - ), - ); - }, - childCount: filteredPlugins.length, - // TODO: integration plugins should have a "auth" completed button or smth. - )), - // Expanded( - // child: ListView.builder( - // itemCount: filteredPlugins.length, - // itemBuilder: (context, index) { - // final plugin = filteredPlugins[index]; - // return Container( - // padding: const EdgeInsets.fromLTRB(8, 8, 0, 8), - // decoration: BoxDecoration( - // shape: BoxShape.rectangle, - // borderRadius: const BorderRadius.all(Radius.circular(16.0)), - // color: Colors.grey.shade900, - // ), - // margin: EdgeInsets.only(bottom: 12, top: index == 0 ? 24 : 0, left: 16, right: 16), - // child: ListTile( - // onTap: () async { - // await routeToPage(context, PluginDetailPage(plugin: plugin)); - // _fetchPlugins(); - // // refresh plugins - // }, - // leading: CircleAvatar( - // backgroundColor: Colors.white, - // maxRadius: 28, - // backgroundImage: NetworkImage(plugin.getImageUrl()), - // ), - // title: Text( - // plugin.name, - // maxLines: 1, - // style: const TextStyle(fontWeight: FontWeight.w600, color: Colors.white, fontSize: 16), - // ), - // subtitle: Column( - // mainAxisAlignment: MainAxisAlignment.start, - // crossAxisAlignment: CrossAxisAlignment.start, - // children: [ - // SizedBox(height: plugin.ratingAvg != null ? 4 : 0), - // plugin.ratingAvg != null - // ? Row( - // children: [ - // Text(plugin.getRatingAvg()!), - // const SizedBox(width: 4), - // const Icon(Icons.star, color: Colors.deepPurple, size: 16), - // const SizedBox(width: 4), - // Text('(${plugin.ratingCount})'), - // ], - // ) - // : Container(), - // Padding( - // padding: const EdgeInsets.only(top: 4.0), - // child: Text( - // plugin.description, - // maxLines: 2, - // style: const TextStyle(color: Colors.grey, fontSize: 14), - // ), - // ), - // const SizedBox(height: 8), - // Row( - // children: [ - // plugin.memories - // ? Container( - // padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), - // decoration: BoxDecoration( - // color: Colors.grey, - // borderRadius: BorderRadius.circular(16), - // ), - // child: const Text( - // 'Memories', - // style: TextStyle( - // color: Colors.deepPurple, fontSize: 12, fontWeight: FontWeight.w500), - // ), - // ) - // : const SizedBox.shrink(), - // const SizedBox(width: 8), - // plugin.chat - // ? Container( - // padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), - // decoration: BoxDecoration( - // color: Colors.grey, - // borderRadius: BorderRadius.circular(16), - // ), - // child: const Text( - // 'Chat', - // style: TextStyle( - // color: Colors.deepPurple, fontSize: 12, fontWeight: FontWeight.w500), - // ), - // ) - // : const SizedBox.shrink(), - // ], - // ) - // ], - // ), - // trailing: IconButton( - // icon: Icon( - // plugin.isEnabled ? Icons.check : Icons.arrow_downward_rounded, - // color: plugin.isEnabled ? Colors.white : Colors.grey, - // ), - // onPressed: () { - // _togglePlugin(plugin.id.toString(), !plugin.isEnabled); - // }, - // ), - // // trailing: Switch( - // // value: plugin.isEnabled, - // // activeColor: Colors.deepPurple, - // // onChanged: (value) { - // // _togglePlugin(plugin.id.toString(), value); - // // }, - // // ), - // ), - // ); - // }, - // ), - // ), - ], - ), - ), + // trailing: Switch( + // value: plugin.isEnabled, + // activeColor: Colors.deepPurple, + // onChanged: (value) { + // _togglePlugin(plugin.id.toString(), value); + // }, + // ), + ), + ); + }, + childCount: provider.filteredPlugins.length, + // TODO: integration plugins should have a "auth" completed button or smth. + )), + // Expanded( + // child: ListView.builder( + // itemCount: filteredPlugins.length, + // itemBuilder: (context, index) { + // final plugin = filteredPlugins[index]; + // return Container( + // padding: const EdgeInsets.fromLTRB(8, 8, 0, 8), + // decoration: BoxDecoration( + // shape: BoxShape.rectangle, + // borderRadius: const BorderRadius.all(Radius.circular(16.0)), + // color: Colors.grey.shade900, + // ), + // margin: EdgeInsets.only(bottom: 12, top: index == 0 ? 24 : 0, left: 16, right: 16), + // child: ListTile( + // onTap: () async { + // await routeToPage(context, PluginDetailPage(plugin: plugin)); + // _fetchPlugins(); + // // refresh plugins + // }, + // leading: CircleAvatar( + // backgroundColor: Colors.white, + // maxRadius: 28, + // backgroundImage: NetworkImage(plugin.getImageUrl()), + // ), + // title: Text( + // plugin.name, + // maxLines: 1, + // style: const TextStyle(fontWeight: FontWeight.w600, color: Colors.white, fontSize: 16), + // ), + // subtitle: Column( + // mainAxisAlignment: MainAxisAlignment.start, + // crossAxisAlignment: CrossAxisAlignment.start, + // children: [ + // SizedBox(height: plugin.ratingAvg != null ? 4 : 0), + // plugin.ratingAvg != null + // ? Row( + // children: [ + // Text(plugin.getRatingAvg()!), + // const SizedBox(width: 4), + // const Icon(Icons.star, color: Colors.deepPurple, size: 16), + // const SizedBox(width: 4), + // Text('(${plugin.ratingCount})'), + // ], + // ) + // : Container(), + // Padding( + // padding: const EdgeInsets.only(top: 4.0), + // child: Text( + // plugin.description, + // maxLines: 2, + // style: const TextStyle(color: Colors.grey, fontSize: 14), + // ), + // ), + // const SizedBox(height: 8), + // Row( + // children: [ + // plugin.memories + // ? Container( + // padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + // decoration: BoxDecoration( + // color: Colors.grey, + // borderRadius: BorderRadius.circular(16), + // ), + // child: const Text( + // 'Memories', + // style: TextStyle( + // color: Colors.deepPurple, fontSize: 12, fontWeight: FontWeight.w500), + // ), + // ) + // : const SizedBox.shrink(), + // const SizedBox(width: 8), + // plugin.chat + // ? Container( + // padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2), + // decoration: BoxDecoration( + // color: Colors.grey, + // borderRadius: BorderRadius.circular(16), + // ), + // child: const Text( + // 'Chat', + // style: TextStyle( + // color: Colors.deepPurple, fontSize: 12, fontWeight: FontWeight.w500), + // ), + // ) + // : const SizedBox.shrink(), + // ], + // ) + // ], + // ), + // trailing: IconButton( + // icon: Icon( + // plugin.isEnabled ? Icons.check : Icons.arrow_downward_rounded, + // color: plugin.isEnabled ? Colors.white : Colors.grey, + // ), + // onPressed: () { + // _togglePlugin(plugin.id.toString(), !plugin.isEnabled); + // }, + // ), + // // trailing: Switch( + // // value: plugin.isEnabled, + // // activeColor: Colors.deepPurple, + // // onChanged: (value) { + // // _togglePlugin(plugin.id.toString(), value); + // // }, + // // ), + // ), + // ); + // }, + // ), + // ), + ], + ), + ); + }), ); } } diff --git a/app/lib/providers/plugin_provider.dart b/app/lib/providers/plugin_provider.dart index 4073c1893..38e5db9d5 100644 --- a/app/lib/providers/plugin_provider.dart +++ b/app/lib/providers/plugin_provider.dart @@ -5,6 +5,62 @@ import 'package:friend_private/backend/schema/plugin.dart'; class PluginProvider extends ChangeNotifier { List plugins = []; + List filteredPlugins = []; + + bool filterChat = true; + bool filterMemories = true; + bool filterExternal = true; + String searchQuery = ''; + + List pluginLoading = []; + + void setPluginLoading(int index, bool value) { + pluginLoading[index] = value; + notifyListeners(); + } + + void setChatFilterOnly() { + filterChat = true; + filterMemories = false; + filterExternal = false; + notifyListeners(); + } + + void setFilterChat(bool value) { + filterChat = value; + notifyListeners(); + } + + void setFilterMemories(bool value) { + filterMemories = value; + notifyListeners(); + } + + void setFilterExternal(bool value) { + filterExternal = value; + notifyListeners(); + } + + void clearSearchQuery() { + searchQuery = ''; + notifyListeners(); + } + + void filterPlugins(String searchQuery) { + this.searchQuery = searchQuery; + var plugins = this + .plugins + .where((p) => + (p.worksWithChat() && filterChat) || + (p.worksWithMemories() && filterMemories) || + (p.worksExternally() && filterExternal)) + .toList(); + + filteredPlugins = searchQuery.isEmpty + ? plugins + : plugins.where((plugin) => plugin.name.toLowerCase().contains(searchQuery.toLowerCase())).toList(); + notifyListeners(); + } Future getPlugins() async { if (SharedPreferencesUtil().pluginsList.isEmpty) { @@ -12,6 +68,8 @@ class PluginProvider extends ChangeNotifier { } else { plugins = SharedPreferencesUtil().pluginsList; } + filteredPlugins = plugins; + pluginLoading = List.filled(plugins.length, false); notifyListeners(); } } From 37542d405220031a5cfaf181c4ac77e2f2a28623 Mon Sep 17 00:00:00 2001 From: Mohammed Mohsin <59914433+mdmohsin7@users.noreply.github.com> Date: Fri, 23 Aug 2024 14:45:22 +0530 Subject: [PATCH 16/23] plugin and misc improvements --- app/lib/pages/capture/page.dart | 9 -------- app/lib/pages/home/page.dart | 7 ------- app/lib/pages/plugins/page.dart | 29 +++++++++++++++----------- app/lib/providers/plugin_provider.dart | 9 ++++++++ 4 files changed, 26 insertions(+), 28 deletions(-) diff --git a/app/lib/pages/capture/page.dart b/app/lib/pages/capture/page.dart index 58ea1a8be..b6b2a6060 100644 --- a/app/lib/pages/capture/page.dart +++ b/app/lib/pages/capture/page.dart @@ -8,7 +8,6 @@ import 'package:friend_private/backend/database/geolocation.dart'; import 'package:friend_private/backend/preferences.dart'; import 'package:friend_private/backend/schema/bt_device.dart'; import 'package:friend_private/backend/schema/memory.dart'; -import 'package:friend_private/backend/schema/message.dart'; import 'package:friend_private/pages/capture/location_service.dart'; import 'package:friend_private/pages/capture/logic/openglass_mixin.dart'; import 'package:friend_private/pages/capture/widgets/widgets.dart'; @@ -29,16 +28,12 @@ import 'logic/phone_recorder_mixin.dart'; import 'logic/websocket_mixin.dart'; class CapturePage extends StatefulWidget { - final Function addMemory; - final Function addMessage; final Function(ServerMemory) updateMemory; final BTDeviceStruct? device; const CapturePage({ super.key, required this.device, - required this.addMemory, - required this.addMessage, required this.updateMemory, }); @@ -165,10 +160,6 @@ class CapturePageState extends State context.read().streamAudioToWs(btDevice!.id, SharedPreferencesUtil().deviceCodec); } - void sendMessageToChat(ServerMessage message) { - widget.addMessage(message); - } - setHasTranscripts(bool hasTranscripts) { context.read().setHasTranscripts(hasTranscripts); } diff --git a/app/lib/pages/home/page.dart b/app/lib/pages/home/page.dart index 1109d5665..28563f238 100644 --- a/app/lib/pages/home/page.dart +++ b/app/lib/pages/home/page.dart @@ -319,13 +319,6 @@ class _HomePageState extends State with WidgetsBindingObserver, Ticker CapturePage( key: capturePageKey, device: _device, - addMemory: (ServerMemory memory) { - memProvider.addMemory(memory); - }, - addMessage: (ServerMessage message) { - context.read().addMessage(message); - chatPageKey.currentState?.scrollToBottom(); - }, updateMemory: (ServerMemory memory) { memProvider.updateMemory(memory); }, diff --git a/app/lib/pages/plugins/page.dart b/app/lib/pages/plugins/page.dart index 85463c37f..570735594 100644 --- a/app/lib/pages/plugins/page.dart +++ b/app/lib/pages/plugins/page.dart @@ -23,12 +23,6 @@ class PluginsPage extends StatefulWidget { } class _PluginsPageState extends State { - bool isLoading = true; - - // bool filterChat = true; - // bool filterMemories = true; - // bool filterExternal = true; - @override void initState() { WidgetsBinding.instance.addPostFrameCallback((_) async { @@ -43,8 +37,8 @@ class _PluginsPageState extends State { } Future _togglePlugin(String pluginId, bool isEnabled, int idx) async { - // if (pluginLoading[idx]) return; - // setState(() => pluginLoading[idx] = true); + if (context.read().pluginLoading[idx]) return; + context.read().setPluginLoading(idx, true); var prefs = SharedPreferencesUtil(); if (isEnabled) { var enabled = await enablePluginServer(pluginId); @@ -59,7 +53,7 @@ class _PluginsPageState extends State { 'If this is an integration plugin, make sure the setup is completed.', singleButton: true, )); - // setState(() => pluginLoading[idx] = false); + context.read().setPluginLoading(idx, false); return; } prefs.enablePlugin(pluginId); @@ -69,8 +63,7 @@ class _PluginsPageState extends State { prefs.disablePlugin(pluginId); MixpanelManager().pluginDisabled(pluginId); } - // setState(() => pluginLoading[idx] = false); - // setState(() => plugins = SharedPreferencesUtil().pluginsList); + context.read().setPluginLoading(idx, false); } @override @@ -234,7 +227,19 @@ class _PluginsPageState extends State { ), ), // const SliverToBoxAdapter(child: SizedBox(height: 8)), - provider.filteredPlugins.isEmpty + provider.isLoading + ? const SliverToBoxAdapter( + child: Padding( + padding: EdgeInsets.only(top: 64, left: 14, right: 14), + child: Center( + child: CircularProgressIndicator( + valueColor: AlwaysStoppedAnimation(Colors.white), + ), + ), + ), + ) + : const SliverToBoxAdapter(child: SizedBox(height: 1)), + provider.filteredPlugins.isEmpty && !provider.isLoading ? SliverToBoxAdapter( child: Padding( padding: EdgeInsets.only(top: 64, left: 14, right: 14), diff --git a/app/lib/providers/plugin_provider.dart b/app/lib/providers/plugin_provider.dart index 38e5db9d5..a3dd01913 100644 --- a/app/lib/providers/plugin_provider.dart +++ b/app/lib/providers/plugin_provider.dart @@ -7,6 +7,8 @@ class PluginProvider extends ChangeNotifier { List plugins = []; List filteredPlugins = []; + bool isLoading = false; + bool filterChat = true; bool filterMemories = true; bool filterExternal = true; @@ -19,6 +21,11 @@ class PluginProvider extends ChangeNotifier { notifyListeners(); } + void setLoading(bool value) { + isLoading = value; + notifyListeners(); + } + void setChatFilterOnly() { filterChat = true; filterMemories = false; @@ -63,6 +70,7 @@ class PluginProvider extends ChangeNotifier { } Future getPlugins() async { + setLoading(true); if (SharedPreferencesUtil().pluginsList.isEmpty) { plugins = await retrievePlugins(); } else { @@ -70,6 +78,7 @@ class PluginProvider extends ChangeNotifier { } filteredPlugins = plugins; pluginLoading = List.filled(plugins.length, false); + setLoading(false); notifyListeners(); } } From bea3c31fcdbdf3be5f644252a2bea79ca60b675d Mon Sep 17 00:00:00 2001 From: Mohammed Mohsin <59914433+mdmohsin7@users.noreply.github.com> Date: Fri, 23 Aug 2024 15:06:18 +0530 Subject: [PATCH 17/23] plugin selection fix --- app/lib/pages/home/page.dart | 87 ++++++++++++++++----------------- app/lib/pages/plugins/page.dart | 1 + 2 files changed, 42 insertions(+), 46 deletions(-) diff --git a/app/lib/pages/home/page.dart b/app/lib/pages/home/page.dart index 28563f238..b3e07d79a 100644 --- a/app/lib/pages/home/page.dart +++ b/app/lib/pages/home/page.dart @@ -73,14 +73,12 @@ class _HomePageState extends State with WidgetsBindingObserver, Ticker int batteryLevel = -1; BTDeviceStruct? _device; - List plugins = []; final _upgrader = MyUpgrader(debugLogging: false, debugDisplayOnce: false); bool scriptsInProgress = false; Future _initiatePlugins() async { context.read().getPlugins(); - plugins = SharedPreferencesUtil().pluginsList; } @override @@ -526,45 +524,44 @@ class _HomePageState extends State with WidgetsBindingObserver, Ticker child: Image.asset('assets/images/logo_transparent.png', width: 25, height: 25), ), _controller!.index == 2 - ? Padding( - padding: const EdgeInsets.only(left: 0), - child: Container( - // decoration: BoxDecoration( - // border: Border.all(color: Colors.grey), - // borderRadius: BorderRadius.circular(30), - // ), - padding: const EdgeInsets.symmetric(horizontal: 16), - child: DropdownButton( - menuMaxHeight: 350, - value: SharedPreferencesUtil().selectedChatPluginId, - onChanged: (s) async { - if ((s == 'no_selected' && plugins.where((p) => p.enabled).isEmpty) || - s == 'enable') { - await routeToPage(context, const PluginsPage(filterChatOnly: true)); - plugins = SharedPreferencesUtil().pluginsList; - setState(() {}); - return; - } - print('Selected: $s prefs: ${SharedPreferencesUtil().selectedChatPluginId}'); - if (s == null || s == SharedPreferencesUtil().selectedChatPluginId) return; - - SharedPreferencesUtil().selectedChatPluginId = s; - var plugin = plugins.firstWhereOrNull((p) => p.id == s); - chatPageKey.currentState?.sendInitialPluginMessage(plugin); - setState(() {}); - }, - icon: Container(), - alignment: Alignment.center, - dropdownColor: Colors.black, - style: const TextStyle(color: Colors.white, fontSize: 16), - underline: Container(height: 0, color: Colors.transparent), - isExpanded: false, - itemHeight: 48, - padding: EdgeInsets.zero, - items: _getPluginsDropdownItems(context), + ? Consumer(builder: (context, provider, child) { + return Padding( + padding: const EdgeInsets.only(left: 0), + child: Container( + // decoration: BoxDecoration( + // border: Border.all(color: Colors.grey), + // borderRadius: BorderRadius.circular(30), + // ), + padding: const EdgeInsets.symmetric(horizontal: 16), + child: DropdownButton( + menuMaxHeight: 350, + value: SharedPreferencesUtil().selectedChatPluginId, + onChanged: (s) async { + if ((s == 'no_selected' && provider.plugins.where((p) => p.enabled).isEmpty) || + s == 'enable') { + await routeToPage(context, const PluginsPage(filterChatOnly: true)); + return; + } + print('Selected: $s prefs: ${SharedPreferencesUtil().selectedChatPluginId}'); + if (s == null || s == SharedPreferencesUtil().selectedChatPluginId) return; + + SharedPreferencesUtil().selectedChatPluginId = s; + var plugin = provider.plugins.firstWhereOrNull((p) => p.id == s); + chatPageKey.currentState?.sendInitialPluginMessage(plugin); + }, + icon: Container(), + alignment: Alignment.center, + dropdownColor: Colors.black, + style: const TextStyle(color: Colors.white, fontSize: 16), + underline: Container(height: 0, color: Colors.transparent), + isExpanded: false, + itemHeight: 48, + padding: EdgeInsets.zero, + items: _getPluginsDropdownItems(context, provider), + ), ), - ), - ) + ); + }) : const SizedBox(width: 16), IconButton( icon: const Icon(Icons.settings, color: Colors.white, size: 30), @@ -578,8 +575,6 @@ class _HomePageState extends State with WidgetsBindingObserver, Ticker hasSpeech != SharedPreferencesUtil().hasSpeakerProfile) { capturePageKey.currentState?.restartWebSocket(); } - plugins = SharedPreferencesUtil().pluginsList; - setState(() {}); }, ) ], @@ -592,7 +587,7 @@ class _HomePageState extends State with WidgetsBindingObserver, Ticker )); } - _getPluginsDropdownItems(BuildContext context) { + _getPluginsDropdownItems(BuildContext context, PluginProvider provider) { var items = [ DropdownMenuItem( value: 'no_selected', @@ -602,14 +597,14 @@ class _HomePageState extends State with WidgetsBindingObserver, Ticker const Icon(size: 20, Icons.chat, color: Colors.white), const SizedBox(width: 10), Text( - plugins.where((p) => p.enabled).isEmpty ? 'Enable Plugins ' : 'Select a plugin', + provider.plugins.where((p) => p.enabled).isEmpty ? 'Enable Plugins ' : 'Select a plugin', style: const TextStyle(color: Colors.white, fontWeight: FontWeight.w500, fontSize: 16), ) ], ), ) ] + - plugins.where((p) => p.enabled && p.worksWithChat()).map>((Plugin plugin) { + provider.plugins.where((p) => p.enabled && p.worksWithChat()).map>((Plugin plugin) { return DropdownMenuItem( value: plugin.id, child: Row( @@ -632,7 +627,7 @@ class _HomePageState extends State with WidgetsBindingObserver, Ticker ), ); }).toList(); - if (plugins.where((p) => p.enabled).isNotEmpty) { + if (provider.plugins.where((p) => p.enabled).isNotEmpty) { items.add(const DropdownMenuItem( value: 'enable', child: Row( diff --git a/app/lib/pages/plugins/page.dart b/app/lib/pages/plugins/page.dart index 570735594..cd51b8f9a 100644 --- a/app/lib/pages/plugins/page.dart +++ b/app/lib/pages/plugins/page.dart @@ -64,6 +64,7 @@ class _PluginsPageState extends State { MixpanelManager().pluginDisabled(pluginId); } context.read().setPluginLoading(idx, false); + context.read().getPlugins(); } @override From a8809c0be881ce8ae282382398a075845326ed72 Mon Sep 17 00:00:00 2001 From: Mohammed Mohsin <59914433+mdmohsin7@users.noreply.github.com> Date: Fri, 23 Aug 2024 15:09:51 +0530 Subject: [PATCH 18/23] bump version to 1.0.28+84 for release --- app/pubspec.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/pubspec.yaml b/app/pubspec.yaml index e81ff69c7..3ef393e19 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -2,7 +2,8 @@ name: friend_private description: A new Flutter project. publish_to: 'none' # Remove this line if you wish to publish to pub.dev -version: 1.0.27+83 + +version: 1.0.28+84 environment: sdk: ">=3.0.0 <4.0.0" From 9ce78f1a4594f615d462e68956d7e396607f8f03 Mon Sep 17 00:00:00 2001 From: Becca-Saka Date: Fri, 23 Aug 2024 16:37:25 +0100 Subject: [PATCH 19/23] fix: plugin provider conflict --- app/lib/backend/http/api/plugins.dart | 24 ++-- app/lib/backend/http/shared.dart | 2 +- app/lib/env/env.dart | 3 +- app/lib/main.dart | 4 +- app/lib/pages/plugins/page.dart | 154 ++++++++----------------- app/lib/providers/auth_provider.dart | 8 +- app/lib/providers/base_provider.dart | 4 +- app/lib/providers/plugin_provider.dart | 112 +++++++++++------- app/lib/utils/alerts/app_dialog.dart | 72 ++++++++++++ 9 files changed, 215 insertions(+), 168 deletions(-) create mode 100644 app/lib/utils/alerts/app_dialog.dart diff --git a/app/lib/backend/http/api/plugins.dart b/app/lib/backend/http/api/plugins.dart index 08152f377..7deed5c1f 100644 --- a/app/lib/backend/http/api/plugins.dart +++ b/app/lib/backend/http/api/plugins.dart @@ -5,7 +5,6 @@ import 'package:friend_private/backend/http/shared.dart'; import 'package:friend_private/backend/preferences.dart'; import 'package:friend_private/backend/schema/plugin.dart'; import 'package:friend_private/env/env.dart'; -import 'package:instabug_flutter/instabug_flutter.dart'; Future> retrievePlugins() async { var response = await makeApiCall( @@ -14,17 +13,18 @@ Future> retrievePlugins() async { body: '', method: 'GET', ); - if (response?.statusCode == 200) { - try { - var plugins = Plugin.fromJsonList(jsonDecode(response!.body)); - SharedPreferencesUtil().pluginsList = plugins; - return plugins; - } catch (e, stackTrace) { - debugPrint(e.toString()); - CrashReporting.reportHandledCrash(e, stackTrace); - return SharedPreferencesUtil().pluginsList; - } - } + // if (response?.statusCode == 200) { + // try { + // log('plugins: ${response?.body}'); + // var plugins = Plugin.fromJsonList(jsonDecode(response!.body)); + // SharedPreferencesUtil().pluginsList = plugins; + // return plugins; + // } catch (e, stackTrace) { + // debugPrint(e.toString()); + // CrashReporting.reportHandledCrash(e, stackTrace); + // return SharedPreferencesUtil().pluginsList; + // } + // } return SharedPreferencesUtil().pluginsList; } diff --git a/app/lib/backend/http/shared.dart b/app/lib/backend/http/shared.dart index f773deb41..4df47b258 100644 --- a/app/lib/backend/http/shared.dart +++ b/app/lib/backend/http/shared.dart @@ -55,7 +55,7 @@ Future makeApiCall({ throw Exception('Unsupported HTTP method: $method'); } } catch (e, stackTrace) { - debugPrint('HTTP request failed: $e'); + debugPrint('HTTP request failed: $e, $stackTrace'); CrashReporting.reportHandledCrash( e, stackTrace, diff --git a/app/lib/env/env.dart b/app/lib/env/env.dart index d174da5c6..91e6c2b94 100644 --- a/app/lib/env/env.dart +++ b/app/lib/env/env.dart @@ -15,8 +15,9 @@ abstract class Env { static String? get mixpanelProjectToken => _instance.mixpanelProjectToken; - static String? get apiBaseUrl => _instance.apiBaseUrl; + // static String? get apiBaseUrl => _instance.apiBaseUrl; + static String? get apiBaseUrl => 'https://based-hardware-development--backened-dev-api.modal.run/'; // static String? get apiBaseUrl => 'https://camel-lucky-reliably.ngrok-free.app/'; static String? get growthbookApiKey => _instance.growthbookApiKey; diff --git a/app/lib/main.dart b/app/lib/main.dart index a2cb74c9a..7e7a9a604 100644 --- a/app/lib/main.dart +++ b/app/lib/main.dart @@ -17,12 +17,12 @@ import 'package:friend_private/flavors.dart'; import 'package:friend_private/pages/home/page.dart'; import 'package:friend_private/pages/onboarding/wrapper.dart'; import 'package:friend_private/providers/auth_provider.dart'; +import 'package:friend_private/providers/capture_provider.dart'; import 'package:friend_private/providers/home_provider.dart'; import 'package:friend_private/providers/memory_provider.dart'; import 'package:friend_private/providers/message_provider.dart'; -import 'package:friend_private/providers/plugin_provider.dart'; -import 'package:friend_private/providers/capture_provider.dart'; import 'package:friend_private/providers/onboarding_provider.dart'; +import 'package:friend_private/providers/plugin_provider.dart'; import 'package:friend_private/services/notification_service.dart'; import 'package:friend_private/utils/analytics/growthbook.dart'; import 'package:friend_private/utils/analytics/mixpanel.dart'; diff --git a/app/lib/pages/plugins/page.dart b/app/lib/pages/plugins/page.dart index cd51b8f9a..1e0763d0f 100644 --- a/app/lib/pages/plugins/page.dart +++ b/app/lib/pages/plugins/page.dart @@ -1,14 +1,11 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/material.dart'; -import 'package:friend_private/backend/http/api/plugins.dart'; -import 'package:friend_private/backend/preferences.dart'; -import 'package:friend_private/backend/schema/plugin.dart'; import 'package:friend_private/pages/plugins/plugin_detail.dart'; import 'package:friend_private/providers/plugin_provider.dart'; -import 'package:friend_private/utils/analytics/mixpanel.dart'; import 'package:friend_private/utils/connectivity_controller.dart'; import 'package:friend_private/utils/other/temp.dart'; import 'package:friend_private/widgets/dialog.dart'; +import 'package:friend_private/widgets/extensions/functions.dart'; import 'package:gradient_borders/gradient_borders.dart'; import 'package:provider/provider.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -19,84 +16,49 @@ class PluginsPage extends StatefulWidget { const PluginsPage({super.key, this.filterChatOnly = false}); @override - _PluginsPageState createState() => _PluginsPageState(); + State createState() => _PluginsPageState(); } class _PluginsPageState extends State { @override void initState() { - WidgetsBinding.instance.addPostFrameCallback((_) async { - await context.read().getPlugins(); - if (mounted) { - if (widget.filterChatOnly) { - context.read().setChatFilterOnly(); - } - } - }); - super.initState(); - } + () { + context.read().initialize(widget.filterChatOnly); + }.withPostFrameCallback(); - Future _togglePlugin(String pluginId, bool isEnabled, int idx) async { - if (context.read().pluginLoading[idx]) return; - context.read().setPluginLoading(idx, true); - var prefs = SharedPreferencesUtil(); - if (isEnabled) { - var enabled = await enablePluginServer(pluginId); - if (!enabled) { - showDialog( - context: context, - builder: (c) => getDialog( - context, - () => Navigator.pop(context), - () => Navigator.pop(context), - 'Error activating the plugin', - 'If this is an integration plugin, make sure the setup is completed.', - singleButton: true, - )); - context.read().setPluginLoading(idx, false); - return; - } - prefs.enablePlugin(pluginId); - MixpanelManager().pluginEnabled(pluginId); - } else { - await disablePluginServer(pluginId); - prefs.disablePlugin(pluginId); - MixpanelManager().pluginDisabled(pluginId); - } - context.read().setPluginLoading(idx, false); - context.read().getPlugins(); + super.initState(); } @override Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Theme.of(context).colorScheme.primary, - appBar: AppBar( + return Consumer(builder: (context, provider, child) { + return Scaffold( backgroundColor: Theme.of(context).colorScheme.primary, - automaticallyImplyLeading: true, - title: const Text('Plugins'), - centerTitle: true, - elevation: 0, - actions: [ - TextButton( - onPressed: () { - launchUrl(Uri.parse('https://basedhardware.com/plugins')); - }, - child: const Row( - children: [ - Text( - 'Create Yours', - style: TextStyle(color: Colors.white), - ), - SizedBox( - width: 8, - ), - ], - )) - ], - ), - body: Consumer(builder: (context, provider, child) { - return GestureDetector( + appBar: AppBar( + backgroundColor: Theme.of(context).colorScheme.primary, + automaticallyImplyLeading: true, + title: const Text('Plugins'), + centerTitle: true, + elevation: 0, + actions: [ + TextButton( + onPressed: () { + launchUrl(Uri.parse('https://basedhardware.com/plugins')); + }, + child: const Row( + children: [ + Text( + 'Create Yours', + style: TextStyle(color: Colors.white), + ), + SizedBox( + width: 8, + ), + ], + )) + ], + ), + body: GestureDetector( onTap: () => FocusScope.of(context).unfocus(), child: CustomScrollView( slivers: [ @@ -123,9 +85,7 @@ class _PluginsPageState extends State { ), // TODO: reuse chat textfield child: TextField( - onChanged: (value) { - provider.filterPlugins(value); - }, + onChanged: provider.updateSearchQuery, obscureText: false, decoration: InputDecoration( hintText: 'Find your plugin...', @@ -140,9 +100,7 @@ class _PluginsPageState extends State { color: Color(0xFFF7F4F4), size: 28.0, ), - onPressed: () { - provider.clearSearchQuery(); - }, + onPressed: () => provider.updateSearchQuery(''), ), ), style: const TextStyle( @@ -165,9 +123,7 @@ class _PluginsPageState extends State { // ), // const SizedBox(width: 16), GestureDetector( - onTap: () { - provider.setFilterMemories(!provider.filterMemories); - }, + onTap: provider.toggleFilterMemories, child: Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), decoration: BoxDecoration( @@ -185,9 +141,7 @@ class _PluginsPageState extends State { ), const SizedBox(width: 8), GestureDetector( - onTap: () { - provider.setFilterChat(!provider.filterChat); - }, + onTap: provider.toggleFilterChat, child: Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), decoration: BoxDecoration( @@ -205,9 +159,7 @@ class _PluginsPageState extends State { ), const SizedBox(width: 8), GestureDetector( - onTap: () { - provider.setFilterExternal(!provider.filterExternal); - }, + onTap: provider.toggleFilterExternal, child: Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4), decoration: BoxDecoration( @@ -228,28 +180,16 @@ class _PluginsPageState extends State { ), ), // const SliverToBoxAdapter(child: SizedBox(height: 8)), - provider.isLoading - ? const SliverToBoxAdapter( - child: Padding( - padding: EdgeInsets.only(top: 64, left: 14, right: 14), - child: Center( - child: CircularProgressIndicator( - valueColor: AlwaysStoppedAnimation(Colors.white), - ), - ), - ), - ) - : const SliverToBoxAdapter(child: SizedBox(height: 1)), - provider.filteredPlugins.isEmpty && !provider.isLoading + provider.filteredPlugins.isEmpty ? SliverToBoxAdapter( child: Padding( - padding: EdgeInsets.only(top: 64, left: 14, right: 14), + padding: const EdgeInsets.only(top: 64, left: 14, right: 14), child: Center( child: Text( ConnectivityController().isConnected.value ? 'No plugins found' : 'Unable to fetch plugins :(\n\nPlease check your internet connection and try again.', - style: TextStyle(color: Colors.white, fontSize: 16), + style: const TextStyle(color: Colors.white, fontSize: 16), textAlign: TextAlign.center, ), ), @@ -271,7 +211,7 @@ class _PluginsPageState extends State { child: ListTile( onTap: () async { await routeToPage(context, PluginDetailPage(plugin: plugin)); - // setState(() => plugins = SharedPreferencesUtil().pluginsList); + provider.setPlugins(); }, leading: CachedNetworkImage( imageUrl: plugin.getImageUrl(), @@ -386,7 +326,7 @@ class _PluginsPageState extends State { () async { Navigator.pop(context); await routeToPage(context, PluginDetailPage(plugin: plugin)); - // setState(() => plugins = SharedPreferencesUtil().pluginsList); + provider.setPlugins(); }, 'Authorize External Plugin', 'Do you allow this plugin to access your memories, transcripts, and recordings? Your data will be sent to the plugin\'s server for processing.', @@ -394,7 +334,7 @@ class _PluginsPageState extends State { ), ); } else { - _togglePlugin(plugin.id.toString(), !plugin.enabled, index); + provider.togglePlugin(plugin.id.toString(), !plugin.enabled, index); } }, ), @@ -523,8 +463,8 @@ class _PluginsPageState extends State { // ), ], ), - ); - }), - ); + ), + ); + }); } } diff --git a/app/lib/providers/auth_provider.dart b/app/lib/providers/auth_provider.dart index 584894e27..cf12d998c 100644 --- a/app/lib/providers/auth_provider.dart +++ b/app/lib/providers/auth_provider.dart @@ -12,19 +12,19 @@ import 'package:url_launcher/url_launcher.dart'; class AuthenticationProvider extends BaseProvider { Future onGoogleSignIn(Function() onSignIn) async { if (!loading) { - changeLoadingState(); + setLoadingState(true); await signInWithGoogle(); _signIn(onSignIn); - changeLoadingState(); + setLoadingState(false); } } Future onAppleSignIn(Function() onSignIn) async { if (!loading) { - changeLoadingState(); + setLoadingState(true); await signInWithApple(); _signIn(onSignIn); - changeLoadingState(); + setLoadingState(false); } } diff --git a/app/lib/providers/base_provider.dart b/app/lib/providers/base_provider.dart index bed35bd21..166afd67d 100644 --- a/app/lib/providers/base_provider.dart +++ b/app/lib/providers/base_provider.dart @@ -3,8 +3,8 @@ import 'package:flutter/material.dart'; class BaseProvider extends ChangeNotifier { bool loading = false; - void changeLoadingState() { - loading = !loading; + void setLoadingState(bool value) { + loading = value; notifyListeners(); } } diff --git a/app/lib/providers/plugin_provider.dart b/app/lib/providers/plugin_provider.dart index a3dd01913..03b9c8595 100644 --- a/app/lib/providers/plugin_provider.dart +++ b/app/lib/providers/plugin_provider.dart @@ -1,13 +1,12 @@ -import 'package:flutter/material.dart'; import 'package:friend_private/backend/http/api/plugins.dart'; import 'package:friend_private/backend/preferences.dart'; import 'package:friend_private/backend/schema/plugin.dart'; +import 'package:friend_private/providers/base_provider.dart'; +import 'package:friend_private/utils/alerts/app_dialog.dart'; +import 'package:friend_private/utils/analytics/mixpanel.dart'; -class PluginProvider extends ChangeNotifier { +class PluginProvider extends BaseProvider { List plugins = []; - List filteredPlugins = []; - - bool isLoading = false; bool filterChat = true; bool filterMemories = true; @@ -21,64 +20,99 @@ class PluginProvider extends ChangeNotifier { notifyListeners(); } - void setLoading(bool value) { - isLoading = value; + void clearSearchQuery() { + searchQuery = ''; notifyListeners(); } - void setChatFilterOnly() { - filterChat = true; - filterMemories = false; - filterExternal = false; - notifyListeners(); + Future getPlugins() async { + setLoadingState(true); + if (SharedPreferencesUtil().pluginsList.isEmpty) { + plugins = await retrievePlugins(); + notifyListeners(); + } else { + setPlugins(); + } } - void setFilterChat(bool value) { - filterChat = value; + void setPlugins() { + plugins = SharedPreferencesUtil().pluginsList; + notifyListeners(); } - void setFilterMemories(bool value) { - filterMemories = value; - notifyListeners(); + void initialize(bool filterChatOnly) { + if (filterChatOnly) { + filterChat = true; + filterMemories = false; + filterExternal = false; + } + pluginLoading = List.filled(plugins.length, false); + + getPlugins(); } - void setFilterExternal(bool value) { - filterExternal = value; + Future togglePlugin(String pluginId, bool isEnabled, int idx) async { + if (pluginLoading[idx]) return; + pluginLoading[idx] = true; notifyListeners(); - } + var prefs = SharedPreferencesUtil(); + if (isEnabled) { + var enabled = await enablePluginServer(pluginId); + if (!enabled) { + AppDialog.show( + title: 'Error activating the plugin', + content: 'If this is an integration plugin, make sure the setup is completed.', + singleButton: true, + ); - void clearSearchQuery() { - searchQuery = ''; + pluginLoading[idx] = false; + notifyListeners(); + + return; + } + prefs.enablePlugin(pluginId); + MixpanelManager().pluginEnabled(pluginId); + } else { + await disablePluginServer(pluginId); + prefs.disablePlugin(pluginId); + MixpanelManager().pluginDisabled(pluginId); + } + pluginLoading[idx] = false; + plugins = SharedPreferencesUtil().pluginsList; notifyListeners(); } - void filterPlugins(String searchQuery) { - this.searchQuery = searchQuery; - var plugins = this - .plugins + List get filteredPlugins { + var pluginList = plugins .where((p) => (p.worksWithChat() && filterChat) || (p.worksWithMemories() && filterMemories) || (p.worksExternally() && filterExternal)) .toList(); - filteredPlugins = searchQuery.isEmpty - ? plugins - : plugins.where((plugin) => plugin.name.toLowerCase().contains(searchQuery.toLowerCase())).toList(); + return searchQuery.isEmpty + ? pluginList + : pluginList.where((plugin) => plugin.name.toLowerCase().contains(searchQuery.toLowerCase())).toList(); + } + + void updateSearchQuery(String query) { + searchQuery = query; notifyListeners(); } - Future getPlugins() async { - setLoading(true); - if (SharedPreferencesUtil().pluginsList.isEmpty) { - plugins = await retrievePlugins(); - } else { - plugins = SharedPreferencesUtil().pluginsList; - } - filteredPlugins = plugins; - pluginLoading = List.filled(plugins.length, false); - setLoading(false); + void toggleFilterChat() { + filterChat = !filterChat; + notifyListeners(); + } + + void toggleFilterMemories() { + filterMemories = !filterMemories; + notifyListeners(); + } + + void toggleFilterExternal() { + filterExternal = !filterExternal; notifyListeners(); } } diff --git a/app/lib/utils/alerts/app_dialog.dart b/app/lib/utils/alerts/app_dialog.dart new file mode 100644 index 000000000..37165fb2b --- /dev/null +++ b/app/lib/utils/alerts/app_dialog.dart @@ -0,0 +1,72 @@ +import 'dart:io'; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:friend_private/main.dart'; + +class AppDialog { + static _getDialog({ + required BuildContext context, + required String title, + required String content, + Function? onConfirm, + Function? onCancel, + bool singleButton = false, + String okButtonText = 'Ok', + }) { + var actions = singleButton + ? [ + TextButton( + onPressed: () => onCancel?.call() ?? Navigator.pop(context), + child: Text(okButtonText, style: const TextStyle(color: Colors.white)), + ) + ] + : [ + TextButton( + onPressed: () => onCancel?.call() ?? Navigator.pop(context), + child: const Text('Cancel', style: TextStyle(color: Colors.white)), + ), + TextButton( + onPressed: () => onConfirm?.call() ?? Navigator.pop(context), + child: Text( + okButtonText, + style: const TextStyle( + color: Colors.white, + ), + ), + ), + ]; + if (Platform.isIOS) { + return CupertinoAlertDialog( + title: Text(title), + content: Text(content), + actions: actions, + ); + } + return AlertDialog( + title: Text(title), + content: Text(content), + actions: actions, + ); + } + + static void show({ + required String title, + required String content, + Function? onConfirm, + Function? onCancel, + bool singleButton = false, + String okButtonText = 'Ok', + }) { + showDialog( + context: MyApp.navigatorKey.currentState!.overlay!.context, + builder: (c) => _getDialog( + context: MyApp.navigatorKey.currentState!.context, + onConfirm: onConfirm, + title: title, + content: content, + okButtonText: okButtonText, + ), + ); + } +} From 3a5586c93791041eb30ea9e1b38e7d56fa472e9b Mon Sep 17 00:00:00 2001 From: Mohammed Mohsin <59914433+mdmohsin7@users.noreply.github.com> Date: Sat, 24 Aug 2024 06:11:06 +0530 Subject: [PATCH 20/23] developer page ui fix --- app/lib/pages/settings/developer.dart | 122 +------------------------- 1 file changed, 2 insertions(+), 120 deletions(-) diff --git a/app/lib/pages/settings/developer.dart b/app/lib/pages/settings/developer.dart index a7172d19b..4d1ab4b47 100644 --- a/app/lib/pages/settings/developer.dart +++ b/app/lib/pages/settings/developer.dart @@ -3,7 +3,6 @@ import 'dart:io'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; -import 'package:friend_private/backend/database/memory.dart'; import 'package:friend_private/backend/database/memory_provider.dart'; import 'package:friend_private/backend/http/api/memories.dart'; import 'package:friend_private/backend/preferences.dart'; @@ -174,127 +173,10 @@ class __DeveloperSettingsPageState extends State<_DeveloperSettingsPage> { setState(() => provider.loadingExportMemories = false); }, ), - const SizedBox(height: 20), - Container( - width: double.infinity, - height: 2, - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.2), - borderRadius: BorderRadius.circular(12), - ), - ), - const SizedBox(height: 20), - Row( - crossAxisAlignment: CrossAxisAlignment.center, + Column( + crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - const SizedBox(height: 32), - _getText('Store your audios in Google Cloud Storage', bold: true), - const SizedBox(height: 16.0), - TextField( - controller: provider.gcpCredentialsController, - obscureText: false, - autocorrect: false, - enableSuggestions: false, - enabled: true, - decoration: _getTextFieldDecoration('GCP Credentials (Base64)'), - style: const TextStyle(color: Colors.white), - ), - TextField( - controller: provider.gcpBucketNameController, - obscureText: false, - autocorrect: false, - enabled: true, - enableSuggestions: false, - decoration: _getTextFieldDecoration('GCP Bucket Name'), - style: const TextStyle(color: Colors.white), - ), - const SizedBox(height: 16), - ListTile( - title: const Text('Import Memories'), - subtitle: const Text('Use with caution. All memories in the JSON file will be imported.'), - contentPadding: EdgeInsets.zero, - trailing: provider.loadingImportMemories - ? const SizedBox( - height: 16, - width: 16, - child: CircularProgressIndicator( - color: Colors.white, - strokeWidth: 2, - ), - ) - : const Icon(Icons.download), - onTap: () async { - if (provider.loadingImportMemories) return; - setState(() => provider.loadingImportMemories = true); - // open file picker - var file = await FilePicker.platform.pickFiles( - type: FileType.custom, - allowedExtensions: ['json'], - ); - MixpanelManager().importMemories(); - if (file == null) { - setState(() => provider.loadingImportMemories = false); - return; - } - var xFile = file.files.first.xFile; - try { - var content = (await xFile.readAsString()); - var decoded = jsonDecode(content); - List memories = decoded.map((e) => Memory.fromJson(e)).toList(); - debugPrint('Memories: $memories'); - var memoriesJson = memories.map((m) => m.toJson()).toList(); - bool result = await migrateMemoriesToBackend(memoriesJson); - if (!result) { - SharedPreferencesUtil().scriptMigrateMemoriesToBack = false; - _snackBar('Failed to import memories. Make sure the file is a valid JSON file.', - seconds: 3); - } - _snackBar('Memories imported, restart the app to see the changes. 🎉', seconds: 3); - MixpanelManager().importedMemories(); - SharedPreferencesUtil().scriptMigrateMemoriesToBack = true; - } catch (e) { - debugPrint(e.toString()); - _snackBar('Make sure the file is a valid JSON file.'); - } - setState(() => provider.loadingImportMemories = false); - }, - ), - ListTile( - contentPadding: EdgeInsets.zero, - title: const Text('Export Memories'), - subtitle: const Text('Export all your memories to a JSON file.'), - trailing: provider.loadingExportMemories - ? const SizedBox( - height: 16, - width: 16, - child: CircularProgressIndicator( - color: Colors.white, - strokeWidth: 2, - ), - ) - : const Icon(Icons.upload), - onTap: provider.loadingExportMemories - ? null - : () async { - if (provider.loadingExportMemories) return; - setState(() => provider.loadingExportMemories = true); - List memories = await getMemories(limit: 10000, offset: 0); // 10k for now - String json = getPrettyJSONString(memories.map((m) => m.toJson()).toList()); - final directory = await getApplicationDocumentsDirectory(); - final file = File('${directory.path}/memories.json'); - await file.writeAsString(json); - - final result = - await Share.shareXFiles([XFile(file.path)], text: 'Exported Memories from Friend'); - if (result.status == ShareResultStatus.success) { - debugPrint('Thank you for sharing the picture!'); - } - MixpanelManager().exportMemories(); - // 54d2c392-57f1-46dc-b944-02740a651f7b - setState(() => provider.loadingExportMemories = false); - }, - ), const SizedBox(height: 20), Container( width: double.infinity, From 63f5103dcdf766a313c438793f284270e2056125 Mon Sep 17 00:00:00 2001 From: Becca-Saka Date: Sat, 24 Aug 2024 23:03:15 +0100 Subject: [PATCH 21/23] fix: splash hanging --- app/lib/pages/onboarding/permissions/permissions.dart | 4 ++-- app/lib/services/notification_service.dart | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/lib/pages/onboarding/permissions/permissions.dart b/app/lib/pages/onboarding/permissions/permissions.dart index 21de822be..6af0f40e0 100644 --- a/app/lib/pages/onboarding/permissions/permissions.dart +++ b/app/lib/pages/onboarding/permissions/permissions.dart @@ -29,11 +29,11 @@ class _PermissionsPageState extends State { // const SizedBox(height: 80), CheckboxListTile( value: switchValue, - onChanged: (s) { + onChanged: (s) async { setState(() { switchValue = s!; }); - NotificationService.instance.requestNotificationPermissions(); + await NotificationService.instance.requestNotificationPermissions(); }, title: const Text( 'Enable notification access for Friend\'s full experience.', diff --git a/app/lib/services/notification_service.dart b/app/lib/services/notification_service.dart index ac647acab..419c48eb2 100644 --- a/app/lib/services/notification_service.dart +++ b/app/lib/services/notification_service.dart @@ -36,7 +36,6 @@ class NotificationService { Future initialize() async { await _initializeAwesomeNotifications(); - await _register(); listenForMessages(); } @@ -92,6 +91,7 @@ class NotificationService { bool isAllowed = await _awesomeNotifications.isNotificationAllowed(); if (!isAllowed) { _awesomeNotifications.requestPermissionToSendNotifications(); + _register(); } } From ab5dbe04bd9ab4d08bdf3b0aa917de0a9212fffd Mon Sep 17 00:00:00 2001 From: Mohammed Mohsin <59914433+mdmohsin7@users.noreply.github.com> Date: Sun, 25 Aug 2024 12:51:34 +0530 Subject: [PATCH 22/23] connection changes and improvements to other providers and pages --- app/ios/Runner.xcodeproj/project.pbxproj | 2 +- app/lib/backend/http/api/plugins.dart | 26 +- app/lib/main.dart | 16 +- app/lib/pages/capture/page.dart | 128 +++++---- app/lib/pages/home/page.dart | 244 ++++++++---------- .../onboarding/find_device/found_devices.dart | 6 +- app/lib/pages/onboarding/welcome/page.dart | 18 +- app/lib/providers/capture_provider.dart | 60 ++++- app/lib/providers/device_provider.dart | 194 ++++++++++++++ app/lib/providers/onboarding_provider.dart | 26 +- app/lib/providers/plugin_provider.dart | 9 +- app/pubspec.yaml | 5 +- 12 files changed, 503 insertions(+), 231 deletions(-) create mode 100644 app/lib/providers/device_provider.dart diff --git a/app/ios/Runner.xcodeproj/project.pbxproj b/app/ios/Runner.xcodeproj/project.pbxproj index 372787272..53b579862 100644 --- a/app/ios/Runner.xcodeproj/project.pbxproj +++ b/app/ios/Runner.xcodeproj/project.pbxproj @@ -1843,4 +1843,4 @@ /* End XCConfigurationList section */ }; rootObject = 97C146E61CF9000F007C117D /* Project object */; -} \ No newline at end of file +} diff --git a/app/lib/backend/http/api/plugins.dart b/app/lib/backend/http/api/plugins.dart index 7deed5c1f..6929b7534 100644 --- a/app/lib/backend/http/api/plugins.dart +++ b/app/lib/backend/http/api/plugins.dart @@ -1,10 +1,12 @@ import 'dart:convert'; +import 'dart:developer'; import 'package:flutter/material.dart'; import 'package:friend_private/backend/http/shared.dart'; import 'package:friend_private/backend/preferences.dart'; import 'package:friend_private/backend/schema/plugin.dart'; import 'package:friend_private/env/env.dart'; +import 'package:instabug_flutter/instabug_flutter.dart'; Future> retrievePlugins() async { var response = await makeApiCall( @@ -13,18 +15,18 @@ Future> retrievePlugins() async { body: '', method: 'GET', ); - // if (response?.statusCode == 200) { - // try { - // log('plugins: ${response?.body}'); - // var plugins = Plugin.fromJsonList(jsonDecode(response!.body)); - // SharedPreferencesUtil().pluginsList = plugins; - // return plugins; - // } catch (e, stackTrace) { - // debugPrint(e.toString()); - // CrashReporting.reportHandledCrash(e, stackTrace); - // return SharedPreferencesUtil().pluginsList; - // } - // } + if (response?.statusCode == 200) { + try { + log('plugins: ${response?.body}'); + var plugins = Plugin.fromJsonList(jsonDecode(response!.body)); + SharedPreferencesUtil().pluginsList = plugins; + return plugins; + } catch (e, stackTrace) { + debugPrint(e.toString()); + CrashReporting.reportHandledCrash(e, stackTrace); + return SharedPreferencesUtil().pluginsList; + } + } return SharedPreferencesUtil().pluginsList; } diff --git a/app/lib/main.dart b/app/lib/main.dart index 7e7a9a604..30b16f56f 100644 --- a/app/lib/main.dart +++ b/app/lib/main.dart @@ -18,6 +18,7 @@ import 'package:friend_private/pages/home/page.dart'; import 'package:friend_private/pages/onboarding/wrapper.dart'; import 'package:friend_private/providers/auth_provider.dart'; import 'package:friend_private/providers/capture_provider.dart'; +import 'package:friend_private/providers/device_provider.dart'; import 'package:friend_private/providers/home_provider.dart'; import 'package:friend_private/providers/memory_provider.dart'; import 'package:friend_private/providers/message_provider.dart'; @@ -132,10 +133,8 @@ class _MyAppState extends State { return MultiProvider( providers: [ ChangeNotifierProvider(create: (context) => AuthenticationProvider()), - ChangeNotifierProvider(create: (context) => OnboardingProvider()), - ListenableProvider(create: (context) => HomeProvider()), + // ChangeNotifierProvider(create: (context) => DeviceProvider()), ChangeNotifierProvider(create: (context) => MemoryProvider()), - ChangeNotifierProvider(create: (context) => MessageProvider()), ListenableProvider(create: (context) => PluginProvider()), ChangeNotifierProxyProvider( create: (context) => MessageProvider(), @@ -147,6 +146,17 @@ class _MyAppState extends State { update: (BuildContext context, memory, message, CaptureProvider? previous) => (previous?..updateProviderInstances(memory, message)) ?? CaptureProvider(), ), + ChangeNotifierProxyProvider( + create: (context) => DeviceProvider(), + update: (BuildContext context, value, DeviceProvider? previous) => + (previous?..setProvider(value)) ?? DeviceProvider(), + ), + ChangeNotifierProxyProvider( + create: (context) => OnboardingProvider(), + update: (BuildContext context, value, OnboardingProvider? previous) => + (previous?..setDeviceProvider(value)) ?? OnboardingProvider(), + ), + ListenableProvider(create: (context) => HomeProvider()), ], builder: (context, child) { return WithForegroundTask( diff --git a/app/lib/pages/capture/page.dart b/app/lib/pages/capture/page.dart index b6b2a6060..ae1ac8e15 100644 --- a/app/lib/pages/capture/page.dart +++ b/app/lib/pages/capture/page.dart @@ -4,6 +4,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter_foreground_task/flutter_foreground_task.dart'; +import 'package:flutter_provider_utilities/flutter_provider_utilities.dart'; import 'package:friend_private/backend/database/geolocation.dart'; import 'package:friend_private/backend/preferences.dart'; import 'package:friend_private/backend/schema/bt_device.dart'; @@ -13,6 +14,7 @@ import 'package:friend_private/pages/capture/logic/openglass_mixin.dart'; import 'package:friend_private/pages/capture/widgets/widgets.dart'; import 'package:friend_private/pages/home/page.dart'; import 'package:friend_private/providers/capture_provider.dart'; +import 'package:friend_private/providers/device_provider.dart'; import 'package:friend_private/utils/audio/wav_bytes.dart'; import 'package:friend_private/utils/ble/communication.dart'; import 'package:friend_private/utils/enums.dart'; @@ -29,11 +31,9 @@ import 'logic/websocket_mixin.dart'; class CapturePage extends StatefulWidget { final Function(ServerMemory) updateMemory; - final BTDeviceStruct? device; const CapturePage({ super.key, - required this.device, required this.updateMemory, }); @@ -46,9 +46,6 @@ class CapturePageState extends State @override bool get wantKeepAlive => true; - // TODO: This should come from Device Provider implemented by @Becca-Saka - BTDeviceStruct? btDevice; - /// ---- // List segments = List.filled(100, '') @@ -95,8 +92,9 @@ class CapturePageState extends State String conversationId = const Uuid().v4(); // used only for transcript segment plugins Future initiateFriendAudioStreaming() async { - if (btDevice == null) return; - BleAudioCodec codec = await getAudioCodec(btDevice!.id); + var provider = context.read(); + if (provider.connectedDevice == null) return; + BleAudioCodec codec = await getAudioCodec(provider.connectedDevice!.id); if (SharedPreferencesUtil().deviceCodec != codec) { SharedPreferencesUtil().deviceCodec = codec; showDialog( @@ -115,49 +113,51 @@ class CapturePageState extends State return; } - await context.read().streamAudioToWs(btDevice!.id, codec); + await context.read().streamAudioToWs(provider.connectedDevice!.id, codec); } Future startOpenGlass() async { - if (btDevice == null) return; - isGlasses = await hasPhotoStreamingCharacteristic(btDevice!.id); + var provider = context.read(); + if (provider.connectedDevice == null) return; + isGlasses = await hasPhotoStreamingCharacteristic(provider.connectedDevice!.id); if (!isGlasses) return; - await openGlassProcessing(btDevice!, (p) => setState(() {}), setHasTranscripts); + await openGlassProcessing(provider.connectedDevice!, (p) => setState(() {}), setHasTranscripts); closeWebSocket(); } void resetState({bool restartBytesProcessing = true, BTDeviceStruct? btDevice}) async { var provider = context.read(); - debugPrint('resetState: $restartBytesProcessing'); - provider.closeBleStream(); - provider.cancelMemoryCreationTimer(); - if (!restartBytesProcessing && (provider.segments.isNotEmpty || photos.isNotEmpty)) { - var res = await provider.createMemory(forcedCreation: true); - if (res != null && !res) { - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text( - 'Memory creation failed. It\' stored locally and will be retried soon.', - style: TextStyle(color: Colors.white, fontSize: 14), - ), - ), - ); - } - } - } - - if (btDevice != null) setState(() => this.btDevice = btDevice); - if (restartBytesProcessing) { - startOpenGlass(); - initiateFriendAudioStreaming(); - } + // print('inside of resetState'); + // debugPrint('resetState: $restartBytesProcessing'); + // provider.closeBleStream(); + // provider.cancelMemoryCreationTimer(); + // if (!restartBytesProcessing && (provider.segments.isNotEmpty || photos.isNotEmpty)) { + // var res = await provider.createMemory(forcedCreation: true); + // if (res != null && !res) { + // if (mounted) { + // ScaffoldMessenger.of(context).showSnackBar( + // const SnackBar( + // content: Text( + // 'Memory creation failed. It\' stored locally and will be retried soon.', + // style: TextStyle(color: Colors.white, fontSize: 14), + // ), + // ), + // ); + // } + // } + // } + // await provider.resetState(restartBytesProcessing: restartBytesProcessing, btDevice: btDevice); + // if (restartBytesProcessing) { + startOpenGlass(); + initiateFriendAudioStreaming(); + // } } void restartWebSocket() { + var provider = context.read(); debugPrint('restartWebSocket'); closeWebSocket(); - context.read().streamAudioToWs(btDevice!.id, SharedPreferencesUtil().deviceCodec); + context.read().streamAudioToWs(provider.connectedDevice!.id, SharedPreferencesUtil().deviceCodec); } setHasTranscripts(bool hasTranscripts) { @@ -198,7 +198,6 @@ class CapturePageState extends State @override void initState() { - btDevice = widget.device; WavBytesUtil.clearTempWavFiles(); startOpenGlass(); initiateFriendAudioStreaming(); @@ -208,6 +207,11 @@ class CapturePageState extends State WidgetsBinding.instance.addObserver(this); SchedulerBinding.instance.addPostFrameCallback((_) async { await context.read().initiateWebsocket(); + var shouldRestart = context.read().restartAudioProcessing; + if (shouldRestart) { + startOpenGlass(); + initiateFriendAudioStreaming(); + } if (await LocationService().displayPermissionsDialog()) { await showDialog( context: context, @@ -245,8 +249,6 @@ class CapturePageState extends State @override void dispose() { WidgetsBinding.instance.removeObserver(this); - // context.read().closeBleStream(); - // context.read().cancelMemoryCreationTimer(); record.dispose(); _internetListener.cancel(); // websocketChannel @@ -293,19 +295,41 @@ class CapturePageState extends State @override Widget build(BuildContext context) { super.build(context); - return Consumer(builder: (context, provider, child) { - return Stack( - children: [ - ListView(children: [ - speechProfileWidget(context, setState, restartWebSocket), - ...getConnectionStateWidgets( - context, provider.hasTranscripts, widget.device, wsConnectionState, _internetStatus), - getTranscriptWidget(provider.memoryCreating, provider.segments, photos, widget.device), - ...connectionStatusWidgets(context, provider.segments, wsConnectionState, _internetStatus), - const SizedBox(height: 16) - ]), - getPhoneMicRecordingButton(_recordingToggled, recordingState) - ], + return Consumer2(builder: (context, provider, deviceProvider, child) { + return MessageListener( + showError: (error) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + error, + style: const TextStyle(color: Colors.white, fontSize: 14), + ), + ), + ); + }, + showInfo: (info) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + info, + style: const TextStyle(color: Colors.white, fontSize: 14), + ), + ), + ); + }, + child: Stack( + children: [ + ListView(children: [ + speechProfileWidget(context, setState, restartWebSocket), + ...getConnectionStateWidgets( + context, provider.hasTranscripts, deviceProvider.connectedDevice, wsConnectionState, _internetStatus), + getTranscriptWidget(provider.memoryCreating, provider.segments, photos, deviceProvider.connectedDevice), + ...connectionStatusWidgets(context, provider.segments, wsConnectionState, _internetStatus), + const SizedBox(height: 16) + ]), + getPhoneMicRecordingButton(_recordingToggled, recordingState) + ], + ), ); }); } diff --git a/app/lib/pages/home/page.dart b/app/lib/pages/home/page.dart index b3e07d79a..90d0d6d0a 100644 --- a/app/lib/pages/home/page.dart +++ b/app/lib/pages/home/page.dart @@ -3,13 +3,10 @@ import 'dart:io'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_blue_plus/flutter_blue_plus.dart'; import 'package:flutter_foreground_task/flutter_foreground_task.dart'; import 'package:friend_private/backend/http/cloud_storage.dart'; import 'package:friend_private/backend/preferences.dart'; -import 'package:friend_private/backend/schema/bt_device.dart'; import 'package:friend_private/backend/schema/memory.dart'; -import 'package:friend_private/backend/schema/message.dart'; import 'package:friend_private/backend/schema/plugin.dart'; import 'package:friend_private/main.dart'; import 'package:friend_private/pages/capture/connect.dart'; @@ -19,6 +16,7 @@ import 'package:friend_private/pages/home/device.dart'; import 'package:friend_private/pages/memories/page.dart'; import 'package:friend_private/pages/plugins/page.dart'; import 'package:friend_private/pages/settings/page.dart'; +import 'package:friend_private/providers/device_provider.dart'; import 'package:friend_private/providers/home_provider.dart'; import 'package:friend_private/providers/memory_provider.dart' as mp; import 'package:friend_private/providers/plugin_provider.dart'; @@ -27,9 +25,6 @@ import 'package:friend_private/scripts.dart'; import 'package:friend_private/services/notification_service.dart'; import 'package:friend_private/utils/analytics/mixpanel.dart'; import 'package:friend_private/utils/audio/foreground.dart'; -import 'package:friend_private/utils/ble/communication.dart'; -import 'package:friend_private/utils/ble/connected.dart'; -import 'package:friend_private/utils/ble/scan.dart'; import 'package:friend_private/utils/connectivity_controller.dart'; import 'package:friend_private/utils/other/temp.dart'; import 'package:friend_private/widgets/upgrade_alert.dart'; @@ -38,9 +33,24 @@ import 'package:instabug_flutter/instabug_flutter.dart'; import 'package:provider/provider.dart'; import 'package:upgrader/upgrader.dart'; -class HomePageWrapper extends StatelessWidget { +GlobalKey capturePageKey = GlobalKey(); + +class HomePageWrapper extends StatefulWidget { const HomePageWrapper({super.key}); + @override + State createState() => _HomePageWrapperState(); +} + +class _HomePageWrapperState extends State { + @override + void initState() { + WidgetsBinding.instance.addPostFrameCallback((_) async { + context.read().periodicConnect(capturePageKey); + }); + super.initState(); + } + @override Widget build(BuildContext context) { return ChangeNotifierProvider( @@ -65,13 +75,9 @@ class _HomePageState extends State with WidgetsBindingObserver, Ticker FocusNode chatTextFieldFocusNode = FocusNode(canRequestFocus: true); FocusNode memoriesTextFieldFocusNode = FocusNode(canRequestFocus: true); - GlobalKey capturePageKey = GlobalKey(); GlobalKey chatPageKey = GlobalKey(); - StreamSubscription? _connectionStateListener; - StreamSubscription>? _bleBatteryLevelListener; - - int batteryLevel = -1; - BTDeviceStruct? _device; + // StreamSubscription? _connectionStateListener; + // StreamSubscription>? _bleBatteryLevelListener; final _upgrader = MyUpgrader(debugLogging: false, debugDisplayOnce: false); @@ -134,9 +140,19 @@ class _HomePageState extends State with WidgetsBindingObserver, Ticker ForegroundUtil.requestPermissions(); await ForegroundUtil.initializeForegroundService(); ForegroundUtil.startForegroundTask(); + if (SharedPreferencesUtil().btDeviceStruct.id.isNotEmpty) { + // await scanAndConnectDevice().then((value) async { + // print('Connected device: $value'); + // if (value != null) { + // context.read().setConnectedDevice(value); + // // print('Connected device: ${context.read().connectedDevice}'); + // await context.read().initiateConnectionListener(capturePageKey); + // // context.read().initiateBleBatteryListener(); + // // capturePageKey.currentState?.resetState(restartBytesProcessing: true, btDevice: value); + // } + // }); + } if (mounted) { - //TODO: already disposed - // await context.read().refreshMessages(); await context.read().setupHasSpeakerProfile(); } }); @@ -145,9 +161,6 @@ class _HomePageState extends State with WidgetsBindingObserver, Ticker // _migrationScripts(); authenticateGCP(); - if (SharedPreferencesUtil().btDeviceStruct.id.isNotEmpty) { - scanAndConnectDevice().then(_onConnected); - } _listenToMessagesFromNotification(); if (SharedPreferencesUtil().subPageToShowFromNotification != '') { @@ -171,59 +184,6 @@ class _HomePageState extends State with WidgetsBindingObserver, Ticker }); } - Timer? _disconnectNotificationTimer; - - _initiateConnectionListener() async { - if (_connectionStateListener != null) return; - _connectionStateListener?.cancel(); - _connectionStateListener = getConnectionStateListener( - deviceId: _device!.id, - onDisconnected: () { - debugPrint('onDisconnected'); - capturePageKey.currentState?.resetState(restartBytesProcessing: false); - setState(() => _device = null); - InstabugLog.logInfo('Friend Device Disconnected'); - _disconnectNotificationTimer?.cancel(); - _disconnectNotificationTimer = Timer(const Duration(seconds: 30), () { - NotificationService.instance.createNotification( - title: 'Friend Device Disconnected', - body: 'Please reconnect to continue using your Friend.', - ); - }); - MixpanelManager().deviceDisconnected(); - }, - onConnected: ((d) => _onConnected(d, initiateConnectionListener: false))); - } - - _onConnected(BTDeviceStruct? connectedDevice, {bool initiateConnectionListener = true}) { - debugPrint('_onConnected: $connectedDevice'); - if (connectedDevice == null) return; - _disconnectNotificationTimer?.cancel(); - NotificationService.instance.clearNotification(1); - _device = connectedDevice; - if (initiateConnectionListener) _initiateConnectionListener(); - _initiateBleBatteryListener(); - capturePageKey.currentState?.resetState(restartBytesProcessing: true, btDevice: connectedDevice); - MixpanelManager().deviceConnected(); - SharedPreferencesUtil().btDeviceStruct = _device!; - SharedPreferencesUtil().deviceName = _device!.name; - // if (mounted) { - // setState(() {}); - // } - } - - _initiateBleBatteryListener() async { - _bleBatteryLevelListener?.cancel(); - _bleBatteryLevelListener = await getBleBatteryLevelListener( - _device!.id, - onBatteryLevelChange: (int value) { - setState(() { - batteryLevel = value; - }); - }, - ); - } - _tabChange(int index) { MixpanelManager().bottomNavigationTabClicked(['Memories', 'Device', 'Chat'][index]); FocusScope.of(context).unfocus(); @@ -316,7 +276,6 @@ class _HomePageState extends State with WidgetsBindingObserver, Ticker ), CapturePage( key: capturePageKey, - device: _device, updateMemory: (ServerMemory memory) { memProvider.updateMemory(memory); }, @@ -454,75 +413,84 @@ class _HomePageState extends State with WidgetsBindingObserver, Ticker mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.center, children: [ - _device != null && batteryLevel != -1 - ? GestureDetector( - onTap: _device == null - ? null - : () { - Navigator.of(context).push(MaterialPageRoute( - builder: (c) => ConnectedDevice( - device: _device!, - batteryLevel: batteryLevel, - ))); - MixpanelManager().batteryIndicatorClicked(); - }, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), - decoration: BoxDecoration( - color: Colors.transparent, - borderRadius: BorderRadius.circular(10), - border: Border.all( - color: Colors.grey, - width: 1, - ), + Consumer(builder: (context, deviceProvider, child) { + if (deviceProvider.connectedDevice != null && deviceProvider.batteryLevel != -1) { + return GestureDetector( + onTap: deviceProvider.connectedDevice == null + ? null + : () { + Navigator.of(context).push(MaterialPageRoute( + builder: (c) => ConnectedDevice( + device: deviceProvider.connectedDevice!, + batteryLevel: deviceProvider.batteryLevel, + ))); + MixpanelManager().batteryIndicatorClicked(); + }, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), + decoration: BoxDecoration( + color: Colors.transparent, + borderRadius: BorderRadius.circular(10), + border: Border.all( + color: Colors.grey, + width: 1, ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Container( - width: 10, - height: 10, - decoration: BoxDecoration( - color: batteryLevel > 75 - ? const Color.fromARGB(255, 0, 255, 8) - : batteryLevel > 20 - ? Colors.yellow.shade700 - : Colors.red, - shape: BoxShape.circle, - ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 10, + height: 10, + decoration: BoxDecoration( + color: deviceProvider.batteryLevel > 75 + ? const Color.fromARGB(255, 0, 255, 8) + : deviceProvider.batteryLevel > 20 + ? Colors.yellow.shade700 + : Colors.red, + shape: BoxShape.circle, ), - const SizedBox(width: 8.0), - Text( - '${batteryLevel.toString()}%', - style: const TextStyle( - color: Colors.white, - fontSize: 12, - fontWeight: FontWeight.bold, - ), + ), + const SizedBox(width: 8.0), + Text( + '${deviceProvider.batteryLevel.toString()}%', + style: const TextStyle( + color: Colors.white, + fontSize: 12, + fontWeight: FontWeight.bold, ), - ], - )), - ) - : TextButton( - onPressed: () async { - if (SharedPreferencesUtil().btDeviceStruct.id.isEmpty) { - routeToPage(context, const ConnectDevicePage()); - MixpanelManager().connectFriendClicked(); - } else { - await routeToPage(context, const ConnectedDevice(device: null, batteryLevel: 0)); - } - // setState(() {}); - }, - style: TextButton.styleFrom( - padding: EdgeInsets.zero, - backgroundColor: Colors.transparent, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - side: const BorderSide(color: Colors.white, width: 1), - ), + ), + ], + )), + ); + } else { + print(deviceProvider.connectedDevice?.id); + return TextButton( + onPressed: () async { + if (SharedPreferencesUtil().btDeviceStruct.id.isEmpty) { + routeToPage(context, const ConnectDevicePage()); + MixpanelManager().connectFriendClicked(); + } else { + await routeToPage( + context, + ConnectedDevice( + device: deviceProvider.connectedDevice, + batteryLevel: deviceProvider.batteryLevel)); + } + // setState(() {}); + }, + style: TextButton.styleFrom( + padding: EdgeInsets.zero, + backgroundColor: Colors.transparent, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + side: const BorderSide(color: Colors.white, width: 1), ), - child: Image.asset('assets/images/logo_transparent.png', width: 25, height: 25), ), + child: Image.asset('assets/images/logo_transparent.png', width: 25, height: 25), + ); + } + }), _controller!.index == 2 ? Consumer(builder: (context, provider, child) { return Padding( @@ -650,8 +618,8 @@ class _HomePageState extends State with WidgetsBindingObserver, Ticker @override void dispose() { WidgetsBinding.instance.removeObserver(this); - _connectionStateListener?.cancel(); - _bleBatteryLevelListener?.cancel(); + // _connectionStateListener?.cancel(); + // _bleBatteryLevelListener?.cancel(); connectivityController.isConnected.dispose(); _controller?.dispose(); ForegroundUtil.stopForegroundTask(); diff --git a/app/lib/pages/onboarding/find_device/found_devices.dart b/app/lib/pages/onboarding/find_device/found_devices.dart index f24a95abd..504adbb32 100644 --- a/app/lib/pages/onboarding/find_device/found_devices.dart +++ b/app/lib/pages/onboarding/find_device/found_devices.dart @@ -103,10 +103,12 @@ class _FoundDevicesState extends State { return GestureDetector( onTap: !provider.isClicked - ? () => provider.handleTap( + ? () async { + await provider.handleTap( device: device, goNext: widget.goNext, - ) + ); + } : null, child: Container( margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), diff --git a/app/lib/pages/onboarding/welcome/page.dart b/app/lib/pages/onboarding/welcome/page.dart index 74a7d4214..2d31ad7f2 100644 --- a/app/lib/pages/onboarding/welcome/page.dart +++ b/app/lib/pages/onboarding/welcome/page.dart @@ -2,9 +2,11 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:friend_private/providers/device_provider.dart'; import 'package:friend_private/utils/analytics/mixpanel.dart'; import 'package:gradient_borders/box_borders/gradient_box_border.dart'; import 'package:permission_handler/permission_handler.dart'; +import 'package:provider/provider.dart'; class WelcomePage extends StatefulWidget { final VoidCallback goNext; @@ -55,21 +57,7 @@ class _WelcomePageState extends State with SingleTickerProviderStat ), child: ElevatedButton( onPressed: () async { - bool permissionsAccepted = false; - if (Platform.isIOS) { - PermissionStatus bleStatus = await Permission.bluetooth.request(); - debugPrint('bleStatus: $bleStatus'); - permissionsAccepted = bleStatus.isGranted; - } else { - PermissionStatus bleScanStatus = await Permission.bluetoothScan.request(); - PermissionStatus bleConnectStatus = await Permission.bluetoothConnect.request(); - // PermissionStatus locationStatus = await Permission.location.request(); - - permissionsAccepted = - bleConnectStatus.isGranted && bleScanStatus.isGranted; // && locationStatus.isGranted; - - debugPrint('bleScanStatus: $bleScanStatus ~ bleConnectStatus: $bleConnectStatus'); - } + bool permissionsAccepted = await context.read().askForPermissions(); if (!permissionsAccepted) { showDialog( context: context, diff --git a/app/lib/providers/capture_provider.dart b/app/lib/providers/capture_provider.dart index 955863587..6b5fbd3a4 100644 --- a/app/lib/providers/capture_provider.dart +++ b/app/lib/providers/capture_provider.dart @@ -5,6 +5,7 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter_foreground_task/flutter_foreground_task.dart'; +import 'package:flutter_provider_utilities/flutter_provider_utilities.dart'; import 'package:friend_private/backend/database/geolocation.dart'; import 'package:friend_private/backend/database/memory.dart'; import 'package:friend_private/backend/database/transcript_segment.dart'; @@ -15,6 +16,7 @@ import 'package:friend_private/backend/schema/bt_device.dart'; import 'package:friend_private/backend/schema/memory.dart'; import 'package:friend_private/pages/capture/logic/openglass_mixin.dart'; import 'package:friend_private/pages/capture/logic/websocket_mixin.dart'; +import 'package:friend_private/pages/capture/page.dart'; import 'package:friend_private/providers/memory_provider.dart'; import 'package:friend_private/providers/message_provider.dart'; import 'package:friend_private/utils/audio/wav_bytes.dart'; @@ -24,7 +26,7 @@ import 'package:friend_private/utils/memories/process.dart'; import 'package:friend_private/utils/websockets.dart'; import 'package:uuid/uuid.dart'; -class CaptureProvider extends ChangeNotifier with WebSocketMixin, OpenGlassMixin { +class CaptureProvider extends ChangeNotifier with WebSocketMixin, OpenGlassMixin, MessageNotifierMixin { MemoryProvider? memoryProvider; MessageProvider? messageProvider; @@ -33,11 +35,15 @@ class CaptureProvider extends ChangeNotifier with WebSocketMixin, OpenGlassMixin messageProvider = p; } + bool restartAudioProcessing = false; + List segments = []; Geolocation? geolocation; bool hasTranscripts = false; bool memoryCreating = false; + bool webSocketConnected = false; + bool webSocketConnecting = false; static const quietSecondsForMemoryCreation = 120; @@ -71,6 +77,16 @@ class CaptureProvider extends ChangeNotifier with WebSocketMixin, OpenGlassMixin notifyListeners(); } + void setWebSocketConnected(bool value) { + webSocketConnected = value; + notifyListeners(); + } + + void setWebSocketConnecting(bool value) { + webSocketConnecting = value; + notifyListeners(); + } + Future createMemory({bool forcedCreation = false}) async { debugPrint('_createMemory forcedCreation: $forcedCreation'); if (memoryCreating) return null; @@ -160,6 +176,7 @@ class CaptureProvider extends ChangeNotifier with WebSocketMixin, OpenGlassMixin BleAudioCodec? audioCodec, int? sampleRate, ]) async { + setWebSocketConnecting(true); print('initiateWebsocket'); BleAudioCodec codec = audioCodec ?? SharedPreferencesUtil().deviceCodec; sampleRate ??= (codec == BleAudioCodec.opus ? 16000 : 8000); @@ -169,6 +186,8 @@ class CaptureProvider extends ChangeNotifier with WebSocketMixin, OpenGlassMixin includeSpeechProfile: false, onConnectionSuccess: () { print('inside onConnectionSuccess'); + setWebSocketConnecting(false); + setWebSocketConnected(true); if (segments.isNotEmpty) { // means that it was a reconnection, so we need to reset streamStartedAtSecond = null; @@ -227,6 +246,8 @@ class CaptureProvider extends ChangeNotifier with WebSocketMixin, OpenGlassMixin } Future streamAudioToWs(String id, BleAudioCodec codec) async { + print('streamAudioToWs'); + print('wsConnectionState: $wsConnectionState'); audioStorage = WavBytesUtil(codec: codec); _bleBytesStream = await getBleAudioBytesListener( id, @@ -242,6 +263,36 @@ class CaptureProvider extends ChangeNotifier with WebSocketMixin, OpenGlassMixin } }, ); + notifyListeners(); + } + + void setRestartAudioProcessing(bool value) { + restartAudioProcessing = value; + notifyListeners(); + } + + Future resetState( + {bool restartBytesProcessing = true, + BTDeviceStruct? btDevice, + required GlobalKey captureKey}) async { + print('inside of resetState'); + debugPrint('resetState: $restartBytesProcessing'); + closeBleStream(); + cancelMemoryCreationTimer(); + + if (!restartBytesProcessing && (segments.isNotEmpty || photos.isNotEmpty)) { + print('inside of resetState and createMemory'); + var res = await createMemory(forcedCreation: true); + notifyListeners(); + if (res != null && !res) { + notifyError('Memory creation failed. It\' stored locally and will be retried soon.'); + } else { + notifyInfo('Memory created successfully 🚀'); + } + } + setRestartAudioProcessing(restartBytesProcessing); + captureKey.currentState?.resetState(); + notifyListeners(); } void closeBleStream() { @@ -253,4 +304,11 @@ class CaptureProvider extends ChangeNotifier with WebSocketMixin, OpenGlassMixin _memoryCreationTimer?.cancel(); notifyListeners(); } + + @override + void dispose() { + _bleBytesStream?.cancel(); + _memoryCreationTimer?.cancel(); + super.dispose(); + } } diff --git a/app/lib/providers/device_provider.dart b/app/lib/providers/device_provider.dart new file mode 100644 index 000000000..77dd8c94e --- /dev/null +++ b/app/lib/providers/device_provider.dart @@ -0,0 +1,194 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:friend_private/backend/preferences.dart'; +import 'package:friend_private/backend/schema/bt_device.dart'; +import 'package:friend_private/pages/capture/page.dart'; +import 'package:friend_private/providers/capture_provider.dart'; +import 'package:friend_private/services/notification_service.dart'; +import 'package:friend_private/utils/analytics/mixpanel.dart'; +import 'package:friend_private/utils/ble/communication.dart'; +import 'package:friend_private/utils/ble/connected.dart'; +import 'package:friend_private/utils/ble/scan.dart'; +import 'package:instabug_flutter/instabug_flutter.dart'; +import 'package:permission_handler/permission_handler.dart'; + +class DeviceProvider extends ChangeNotifier { + CaptureProvider? captureProvider; + + bool isConnecting = false; + bool isConnected = false; + BTDeviceStruct? connectedDevice; + StreamSubscription? statusSubscription; + StreamSubscription>? _bleBatteryLevelListener; + int batteryLevel = -1; + var timer; + int connectionCheckSeconds = 4; + + Timer? _disconnectNotificationTimer; + + void setProvider(CaptureProvider provider) { + captureProvider = provider; + notifyListeners(); + } + + void setConnectedDevice(BTDeviceStruct? device) { + connectedDevice = device; + notifyListeners(); + } + + Future initiateConnectionListener(GlobalKey capturePageKey) async { + print('initiateConnectionListener called'); + if (statusSubscription != null) return; + statusSubscription?.cancel(); + statusSubscription = getConnectionStateListener( + deviceId: connectedDevice!.id, + onDisconnected: () { + debugPrint('onDisconnected inside: $connectedDevice'); + // capturePageKey.currentState?.resetState(restartBytesProcessing: false); + setConnectedDevice(null); + setIsConnected(false); + captureProvider?.resetState(restartBytesProcessing: false, captureKey: capturePageKey); + print('after resetState inside initiateConnectionListener'); + + InstabugLog.logInfo('Friend Device Disconnected'); + _disconnectNotificationTimer?.cancel(); + _disconnectNotificationTimer = Timer(const Duration(seconds: 30), () { + NotificationService.instance.createNotification( + title: 'Friend Device Disconnected', + body: 'Please reconnect to continue using your Friend.', + ); + }); + MixpanelManager().deviceDisconnected(); + }, + onConnected: ((device) { + debugPrint('_onConnected inside: $connectedDevice'); + _disconnectNotificationTimer?.cancel(); + NotificationService.instance.clearNotification(1); + setConnectedDevice(device); + print('before resetState'); + // capturePageKey.currentState?.resetState(restartBytesProcessing: true, btDevice: connectedDevice); + captureProvider?.resetState( + restartBytesProcessing: true, btDevice: connectedDevice, captureKey: capturePageKey); + print('after resetState'); + // initiateBleBatteryListener(); + MixpanelManager().deviceConnected(); + SharedPreferencesUtil().btDeviceStruct = connectedDevice!; + SharedPreferencesUtil().deviceName = connectedDevice!.name; + notifyListeners(); + }), + ); + notifyListeners(); + } + + initiateBleBatteryListener() async { + if (_bleBatteryLevelListener != null) return; + _bleBatteryLevelListener?.cancel(); + _bleBatteryLevelListener = await getBleBatteryLevelListener( + connectedDevice!.id, + onBatteryLevelChange: (int value) { + print('Battery Level: $value'); + batteryLevel = value; + }, + ); + notifyListeners(); + } + + Future askForPermissions() async { + if (Platform.isIOS) { + final granted = await Permission.bluetooth.isGranted; + if (granted) { + return true; + } + PermissionStatus bleStatus = await Permission.bluetooth.request(); + debugPrint('bleStatus: $bleStatus'); + return bleStatus.isGranted; + } else { + PermissionStatus bleScanStatus = await Permission.bluetoothScan.request(); + PermissionStatus bleConnectStatus = await Permission.bluetoothConnect.request(); + // PermissionStatus locationStatus = await Permission.location.request(); + + return bleConnectStatus.isGranted && bleScanStatus.isGranted; // && locationStatus.isGranted; + } + } + + Future periodicConnect(GlobalKey capturePageKey) async { + timer = Timer.periodic(Duration(seconds: connectionCheckSeconds), (timer) async { + print('seconds: $connectionCheckSeconds'); + print('triggered timer at ${DateTime.now()}'); + if (SharedPreferencesUtil().btDeviceStruct.id.isEmpty) { + return; + } + if (!isConnected && !isConnecting) { + print('Not connected and not connecting'); + await scanAndConnectToDevice(capturePageKey); + } + }); + } + + Future scanAndConnectToDevice(GlobalKey capturePageKey) async { + print('Scanning and connecting to device'); + updateConnectingStatus(true); + if (isConnected) { + print('Already connected'); + if (connectedDevice == null) { + connectedDevice = await getConnectedDevice(); + SharedPreferencesUtil().btDeviceStruct = connectedDevice!; + SharedPreferencesUtil().deviceName = connectedDevice!.name; + MixpanelManager().deviceConnected(); + } + + setIsConnected(true); + updateConnectingStatus(false); + } else { + var device = await scanAndConnectDevice(); + print('Device connecting to: $device'); + if (device != null) { + var cDevice = await getConnectedDevice(); + if (cDevice != null) { + setConnectedDevice(device); + SharedPreferencesUtil().btDeviceStruct = device; + SharedPreferencesUtil().deviceName = device.name; + MixpanelManager().deviceConnected(); + setIsConnected(true); + } + } + updateConnectingStatus(false); + } + captureProvider?.resetState(restartBytesProcessing: true, btDevice: connectedDevice, captureKey: capturePageKey); + if (statusSubscription == null) { + await initiateConnectionListener(capturePageKey); + } + if (isConnected) { + await initiateBleBatteryListener(); + } + if (captureProvider?.webSocketConnected == false) { + capturePageKey.currentState?.restartWebSocket(); + } + notifyListeners(); + } + + void updateConnectingStatus(bool value) { + isConnecting = value; + notifyListeners(); + } + + void setIsConnected(bool value) { + isConnected = value; + if (isConnected) { + connectionCheckSeconds = 8; + } else { + connectionCheckSeconds = 4; + } + notifyListeners(); + } + + @override + void dispose() { + statusSubscription?.cancel(); + _bleBatteryLevelListener?.cancel(); + timer?.cancel(); + super.dispose(); + } +} diff --git a/app/lib/providers/onboarding_provider.dart b/app/lib/providers/onboarding_provider.dart index c9c4ec2c3..9cf62c87e 100644 --- a/app/lib/providers/onboarding_provider.dart +++ b/app/lib/providers/onboarding_provider.dart @@ -7,12 +7,15 @@ import 'package:flutter_blue_plus/flutter_blue_plus.dart'; import 'package:friend_private/backend/preferences.dart'; import 'package:friend_private/backend/schema/bt_device.dart'; import 'package:friend_private/providers/base_provider.dart'; +import 'package:friend_private/providers/device_provider.dart'; import 'package:friend_private/utils/ble/communication.dart'; import 'package:friend_private/utils/ble/connect.dart'; import 'package:friend_private/utils/ble/connected.dart'; import 'package:friend_private/utils/ble/find.dart'; class OnboardingProvider extends BaseProvider { + DeviceProvider? deviceProvider; + bool isClicked = false; bool isConnected = false; int batteryPercentage = -1; @@ -25,6 +28,10 @@ class OnboardingProvider extends BaseProvider { late Timer _findDevicesTimer; bool enableInstructions = false; + void setDeviceProvider(DeviceProvider provider) { + deviceProvider = provider; + } + // TODO: improve this and find_device page. // TODO: include speech profile, once it's well tested, in a few days, rn current version works @@ -66,11 +73,12 @@ class OnboardingProvider extends BaseProvider { await bleConnectDevice(device.id); deviceId = device.id; deviceName = device.name; - getAudioCodec(deviceId).then((codec) => SharedPreferencesUtil().deviceCodec = codec); + await getAudioCodec(deviceId).then((codec) => SharedPreferencesUtil().deviceCodec = codec); setBatteryPercentage( btDevice: device, goNext: goNext, ); + notifyListeners(); } void initiateConnectionListener({ @@ -92,15 +100,28 @@ class OnboardingProvider extends BaseProvider { connectingToDeviceId = null; notifyListeners(); await Future.delayed(const Duration(seconds: 2)); + SharedPreferencesUtil().btDeviceStruct = connectedDevice; goNext(); } } }); } + void deviceAlreadyUnpaired() { + batteryPercentage = -1; + isConnected = false; + deviceName = ''; + deviceId = ''; + notifyListeners(); + } + Future scanDevices({ required VoidCallback onShowDialog, }) async { + if (SharedPreferencesUtil().btDeviceStruct.id.isEmpty) { + // it means the device has been unpaired + deviceAlreadyUnpaired(); + } // check if bluetooth is enabled on Android if (Platform.isAndroid) { if (FlutterBluePlus.adapterStateNow != BluetoothAdapterState.on) { @@ -115,7 +136,6 @@ class OnboardingProvider extends BaseProvider { } } } - _didNotMakeItTimer = Timer(const Duration(seconds: 10), () { enableInstructions = true; notifyListeners(); @@ -140,7 +160,6 @@ class OnboardingProvider extends BaseProvider { // Convert the values of the map back to a list List orderedDevices = foundDevicesMap.values.toList(); - if (orderedDevices.isNotEmpty) { deviceList = orderedDevices; notifyListeners(); @@ -151,6 +170,7 @@ class OnboardingProvider extends BaseProvider { @override void dispose() { + //TODO: This does not get called when the page is popped _findDevicesTimer.cancel(); _didNotMakeItTimer.cancel(); connectionStateTimer?.cancel(); diff --git a/app/lib/providers/plugin_provider.dart b/app/lib/providers/plugin_provider.dart index 03b9c8595..486876e2a 100644 --- a/app/lib/providers/plugin_provider.dart +++ b/app/lib/providers/plugin_provider.dart @@ -29,15 +29,19 @@ class PluginProvider extends BaseProvider { setLoadingState(true); if (SharedPreferencesUtil().pluginsList.isEmpty) { plugins = await retrievePlugins(); - notifyListeners(); + updatePrefPlugins(); } else { setPlugins(); } + notifyListeners(); + } + + void updatePrefPlugins() { + SharedPreferencesUtil().pluginsList = plugins; } void setPlugins() { plugins = SharedPreferencesUtil().pluginsList; - notifyListeners(); } @@ -50,6 +54,7 @@ class PluginProvider extends BaseProvider { pluginLoading = List.filled(plugins.length, false); getPlugins(); + notifyListeners(); } Future togglePlugin(String pluginId, bool isEnabled, int idx) async { diff --git a/app/pubspec.yaml b/app/pubspec.yaml index 3ef393e19..40642ba14 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -3,7 +3,7 @@ description: A new Flutter project. publish_to: 'none' # Remove this line if you wish to publish to pub.dev -version: 1.0.28+84 +version: 1.0.28+86 environment: sdk: ">=3.0.0 <4.0.0" @@ -14,6 +14,7 @@ dependencies: # State management provider: ^6.1.2 + flutter_provider_utilities: ^1.0.6 flutter_localizations: sdk: flutter @@ -21,7 +22,7 @@ dependencies: auto_size_text: 3.0.0 collection: 1.18.0 equatable: 2.0.5 - flutter_blue_plus: ^1.32.7 + flutter_blue_plus: ^1.32.12 font_awesome_flutter: 10.6.0 from_css_color: 2.0.0 http: ^1.2.1 From c09f7d5b98a4e138643d2679109634541a4b2645 Mon Sep 17 00:00:00 2001 From: Mohammed Mohsin <59914433+mdmohsin7@users.noreply.github.com> Date: Sun, 25 Aug 2024 14:57:40 +0530 Subject: [PATCH 23/23] bump version for shorebird --- app/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/pubspec.yaml b/app/pubspec.yaml index 40642ba14..540ad01f7 100644 --- a/app/pubspec.yaml +++ b/app/pubspec.yaml @@ -3,7 +3,7 @@ description: A new Flutter project. publish_to: 'none' # Remove this line if you wish to publish to pub.dev -version: 1.0.28+86 +version: 1.0.28+87 environment: sdk: ">=3.0.0 <4.0.0"