diff --git a/android/app/build.gradle b/android/app/build.gradle index b8e413b83..64c0dcd65 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -103,6 +103,8 @@ dependencies { //for location sending implementation "com.google.android.gms:play-services-location:18.0.0" + // for album art color + implementation 'androidx.palette:palette:1.0.0' // Add the SDK for Firebase Cloud Messaging implementation 'com.google.firebase:firebase-messaging:22.0.0' diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 271608473..7b0921c7a 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -27,6 +27,7 @@ + + + + + + + - + android:resource="@xml/automotive_app_desc"/> diff --git a/android/app/src/main/java/com/bluebubbles/messaging/MainActivity.java b/android/app/src/main/java/com/bluebubbles/messaging/MainActivity.java index 1c857a8d2..513a331a5 100644 --- a/android/app/src/main/java/com/bluebubbles/messaging/MainActivity.java +++ b/android/app/src/main/java/com/bluebubbles/messaging/MainActivity.java @@ -53,6 +53,7 @@ public class MainActivity extends FlutterFragmentActivity { public static int PICK_IMAGE = 1000; public static int OPEN_CAMERA = 2000; + public static int NOTIFICATION_SETTINGS = 3000; public MethodChannel.Result result = null; @@ -128,6 +129,8 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { Log.d("OPEN_CAMERA", "Something went wrong"); result.success(null); } + } else if (requestCode == NOTIFICATION_SETTINGS) { + result.success(null); } } @@ -304,6 +307,7 @@ protected void onStart() { @Override protected void onDestroy() { Log.d("MainActivity", "Removing Activity from memory"); + new MethodChannel(engine.getDartExecutor().getBinaryMessenger(), CHANNEL).invokeMethod("remove-sendPort", null); engine = null; super.onDestroy(); } diff --git a/android/app/src/main/java/com/bluebubbles/messaging/method_call_handler/MethodCallHandler.java b/android/app/src/main/java/com/bluebubbles/messaging/method_call_handler/MethodCallHandler.java index 8132f0caf..ee34a8950 100644 --- a/android/app/src/main/java/com/bluebubbles/messaging/method_call_handler/MethodCallHandler.java +++ b/android/app/src/main/java/com/bluebubbles/messaging/method_call_handler/MethodCallHandler.java @@ -4,6 +4,8 @@ import android.app.AlarmManager; import android.content.Context; import android.os.Build; +import android.content.Intent; +import android.provider.Settings; import androidx.annotation.RequiresApi; @@ -20,6 +22,7 @@ import com.bluebubbles.messaging.method_call_handler.handlers.GetLastLocation; import com.bluebubbles.messaging.method_call_handler.handlers.GetServerUrl; import com.bluebubbles.messaging.method_call_handler.handlers.InitializeBackgroundHandle; +import com.bluebubbles.messaging.method_call_handler.handlers.MediaSessionListener; import com.bluebubbles.messaging.method_call_handler.handlers.NewMessageNotification; import com.bluebubbles.messaging.method_call_handler.handlers.OpenCamera; import com.bluebubbles.messaging.method_call_handler.handlers.OpenFile; @@ -32,6 +35,7 @@ import com.bluebubbles.messaging.method_call_handler.handlers.OpenContactForm; import com.bluebubbles.messaging.method_call_handler.handlers.ViewContactForm; import com.bluebubbles.messaging.workers.DartWorker; +import static com.bluebubbles.messaging.MainActivity.engine; import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodChannel; @@ -96,6 +100,21 @@ public static void methodCallHandler(MethodCall call, MethodChannel.Result resul new FailedToSend(context, call, result).Handle(); } else if (call.method.equals(ClearFailedToSend.TAG)) { new ClearFailedToSend(context, call, result).Handle(); + } else if (call.method.equals("start-notif-listener")) { + if (Settings.Secure.getString(context.getContentResolver(),"enabled_notification_listeners").contains(context.getPackageName()) && engine != null) { + new MediaSessionListener(context, call, result).Handle(); + } else { + result.error("could_not_initialize", "Failed to initialize, permission not granted", ""); + } + } else if (call.method.equals("request-notif-permission")) { + if (Settings.Secure.getString(context.getContentResolver(),"enabled_notification_listeners").contains(context.getPackageName())) { + result.success(""); + } else { + MainActivity activity = (MainActivity) context; + activity.result = result; + Intent intent = new Intent("android.settings.ACTION_NOTIFICATION_LISTENER_SETTINGS"); + activity.startActivityForResult(intent, 3000); + } } else { result.notImplemented(); } diff --git a/android/app/src/main/java/com/bluebubbles/messaging/method_call_handler/handlers/CreateNotificationChannel.java b/android/app/src/main/java/com/bluebubbles/messaging/method_call_handler/handlers/CreateNotificationChannel.java index a9341445c..a7c014567 100644 --- a/android/app/src/main/java/com/bluebubbles/messaging/method_call_handler/handlers/CreateNotificationChannel.java +++ b/android/app/src/main/java/com/bluebubbles/messaging/method_call_handler/handlers/CreateNotificationChannel.java @@ -3,7 +3,10 @@ import android.app.NotificationChannel; import android.app.NotificationManager; import android.content.Context; +import android.media.AudioAttributes; import android.os.Build; +import android.net.Uri; +import android.util.Log; import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodChannel; @@ -23,11 +26,11 @@ public CreateNotificationChannel(Context context, MethodCall call, MethodChanne @Override public void Handle() { - createNotificationChannel(call.argument("channel_name"), call.argument("channel_description"), call.argument("CHANNEL_ID"), context); + createNotificationChannel(call.argument("channel_name"), call.argument("channel_description"), call.argument("CHANNEL_ID"), call.argument("sound"), context); result.success(""); } - public static void createNotificationChannel(String channel_name, String channel_description, String CHANNEL_ID, Context context) { + public static void createNotificationChannel(String channel_name, String channel_description, String CHANNEL_ID, String soundPath, Context context) { // Create the NotificationChannel, but only on API 26+ because // the NotificationChannel class is new and not in the support library if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { @@ -38,6 +41,14 @@ public static void createNotificationChannel(String channel_name, String channel channel.setDescription(description); // Register the channel with the system; you can't change the importance // or other notification behaviors after this + if (soundPath != null) { + AudioAttributes audioAttributes = new AudioAttributes.Builder() + .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION) + .setUsage(AudioAttributes.USAGE_NOTIFICATION) + .build(); + int soundResourceId = context.getResources().getIdentifier(soundPath, "raw", context.getPackageName()); + channel.setSound(Uri.parse("android.resource://" + context.getPackageName() + "/" + soundResourceId), audioAttributes); + } NotificationManager notificationManager = context.getSystemService(NotificationManager.class); notificationManager.createNotificationChannel(channel); } diff --git a/android/app/src/main/java/com/bluebubbles/messaging/method_call_handler/handlers/MediaSessionListener.java b/android/app/src/main/java/com/bluebubbles/messaging/method_call_handler/handlers/MediaSessionListener.java new file mode 100644 index 000000000..da2274b39 --- /dev/null +++ b/android/app/src/main/java/com/bluebubbles/messaging/method_call_handler/handlers/MediaSessionListener.java @@ -0,0 +1,150 @@ +package com.bluebubbles.messaging.method_call_handler.handlers; + +import android.app.Activity; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.graphics.Bitmap; +import android.graphics.Color; +import android.media.session.MediaController; +import android.media.MediaMetadata; +import android.media.session.PlaybackState; +import android.media.session.MediaSessionManager; +import android.media.session.MediaSessionManager.OnActiveSessionsChangedListener; +import android.os.Build; +import android.os.Looper; +import android.provider.Settings; +import android.util.Log; + +import java.util.HashMap; +import java.util.List; +import java.io.ByteArrayOutputStream; + +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; +import androidx.appcompat.app.AlertDialog; +import androidx.palette.graphics.Palette; +import com.bluebubbles.messaging.services.CustomNotificationListenerService; + +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; +import static com.bluebubbles.messaging.MainActivity.engine; +import static com.bluebubbles.messaging.MainActivity.CHANNEL; + + +@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) +public class MediaSessionListener implements OnActiveSessionsChangedListener, Handler { + + private Context context; + private MethodCall call; + private MethodChannel.Result result; + private List oldControllers; + private MediaController.Callback callback; + private MethodChannel backgroundChannel; + + public MediaSessionListener(Context context, MethodCall call, MethodChannel.Result result) { + this.context = context; + this.call = call; + this.result = result; + } + + + @Override + public void Handle() { + backgroundChannel = new MethodChannel(engine.getDartExecutor().getBinaryMessenger(), CHANNEL); + MediaSessionManager manager = (MediaSessionManager) context.getSystemService(Context.MEDIA_SESSION_SERVICE); + if (null == manager) { + result.error("could_not_initialize", "Failed to initialize, manager == null", ""); + } + callback = new MediaController.Callback() { + @Override + public void onMetadataChanged(MediaMetadata metadata) { + String title = metadata.getString(MediaMetadata.METADATA_KEY_TITLE); + if (title == null) { + title = "Unknown"; + } + Bitmap icon = metadata.getBitmap(MediaMetadata.METADATA_KEY_ART); + Bitmap icon2 = metadata.getBitmap(MediaMetadata.METADATA_KEY_ALBUM_ART); + Log.d("BlueBubblesApp", "Getting media metadata for media " + title); + HashMap input = new HashMap<>(); + Palette p = null; + if (icon != null) { + p = Palette.from(icon).generate(); + } else if (icon2 != null) { + p = Palette.from(icon2).generate(); + } + if (p != null) { + int lightBg = p.getLightVibrantColor(Color.WHITE); + int darkBg = p.getDarkMutedColor(Color.BLACK); + int primary; + double lightBgPercent = 0.5; + double darkBgPercent = 0.5; + double primaryPercent = 0.5; + String primaryFrom = "none"; + if (p.getVibrantColor(0xFF2196F3) != 0xFF2196F3) { + primary = p.getVibrantColor(0xFF2196F3); + primaryFrom = "vibrant"; + } else if (p.getMutedColor(0xFF2196F3) != 0xFF2196F3) { + primary = p.getMutedColor(0xFF2196F3); + primaryFrom = "muted"; + } else if (p.getLightMutedColor(0xFF2196F3) != 0xFF2196F3) { + primary = p.getLightMutedColor(0xFF2196F3); + primaryFrom = "lightMuted"; + } else { + primary = 0xFF2196F3; + primaryFrom = "none"; + } + if (p.getLightVibrantSwatch() != null) { + lightBgPercent = p.getLightVibrantSwatch().getPopulation(); + } + if (p.getDarkMutedSwatch() != null) { + darkBgPercent = p.getDarkMutedSwatch().getPopulation(); + } + if (primaryFrom == "vibrant" && p.getVibrantSwatch() != null) { + primaryPercent = p.getVibrantSwatch().getPopulation(); + } else if (primaryFrom == "muted" && p.getMutedSwatch() != null) { + primaryPercent = p.getMutedSwatch().getPopulation(); + } else if (primaryFrom == "lightMuted" && p.getLightMutedSwatch() != null) { + primaryPercent = p.getLightMutedSwatch().getPopulation(); + } + Log.d("BlueBubblesApp", "Dominant color found (for debugging only): " + Integer.toString(p.getDominantColor(Color.BLACK))); + input.put("lightBg", lightBg); + input.put("darkBg", darkBg); + input.put("primary", primary); + input.put("lightBgPercent", lightBgPercent); + input.put("darkBgPercent", darkBgPercent); + input.put("primaryPercent", primaryPercent); + Log.d("BlueBubblesApp", "Sending media metadata for media " + title); + backgroundChannel.invokeMethod("media-colors", input); + } + } + }; + List controllers = manager.getActiveSessions(new ComponentName(context, CustomNotificationListenerService.class)); + oldControllers = controllers; + if (null != controllers && controllers.size() != 0) { + for (MediaController controller : controllers) { + controller.registerCallback(callback); + } + } + manager.addOnActiveSessionsChangedListener(MediaSessionListener.this, new ComponentName(context, CustomNotificationListenerService.class)); + result.success(""); + } + + @Override + public void onActiveSessionsChanged(@Nullable List controllers) { + + if (null == controllers || controllers.size() == 0) { + return; + } + + for (MediaController controller : oldControllers) { + controller.unregisterCallback(callback); + } + oldControllers = controllers; + for (MediaController controller : controllers) { + controller.registerCallback(callback); + } + + } + +} \ No newline at end of file diff --git a/android/app/src/main/java/com/bluebubbles/messaging/method_call_handler/handlers/NewMessageNotification.java b/android/app/src/main/java/com/bluebubbles/messaging/method_call_handler/handlers/NewMessageNotification.java index 813b18989..6e83c86cb 100644 --- a/android/app/src/main/java/com/bluebubbles/messaging/method_call_handler/handlers/NewMessageNotification.java +++ b/android/app/src/main/java/com/bluebubbles/messaging/method_call_handler/handlers/NewMessageNotification.java @@ -19,6 +19,7 @@ import android.graphics.drawable.Icon; import android.os.Build; import android.os.Bundle; +import android.net.Uri; import android.service.notification.StatusBarNotification; import androidx.annotation.RequiresApi; @@ -81,6 +82,7 @@ public void Handle() { Integer notificationVisibility = (Integer) call.argument("visibility"); Integer notificationId = (Integer) call.argument("notificationId"); Integer summaryId = (Integer) call.argument("summaryId"); + String soundPath = (String) call.argument("sound"); // Find any notifications that already exist for the chat NotificationManager notificationManager = (NotificationManager) context.getSystemService(context.NOTIFICATION_SERVICE); @@ -283,6 +285,12 @@ public void Handle() { // Set the color. This is the blue primary color .setColor(4888294); + // Set the sound of the notification (Android 7 and below) + if (soundPath != "default") { + int soundResourceId = context.getResources().getIdentifier(soundPath, "raw", context.getPackageName()); + notificationBuilder.setSound(Uri.parse("android.resource://" + context.getPackageName() + "/" + soundResourceId)); + } + // Disable the alert if it's from you notificationBuilder.setOnlyAlertOnce(messageIsFromMe); diff --git a/android/app/src/main/java/com/bluebubbles/messaging/services/CustomNotificationListenerService.java b/android/app/src/main/java/com/bluebubbles/messaging/services/CustomNotificationListenerService.java new file mode 100644 index 000000000..2188610dd --- /dev/null +++ b/android/app/src/main/java/com/bluebubbles/messaging/services/CustomNotificationListenerService.java @@ -0,0 +1,13 @@ +package com.bluebubbles.messaging.services; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.service.notification.NotificationListenerService; +import android.service.notification.StatusBarNotification; +import android.util.Log; + +public class CustomNotificationListenerService extends NotificationListenerService { + +} \ No newline at end of file diff --git a/android/app/src/main/java/com/bluebubbles/messaging/services/FlutterFirebaseMessagingBackgroundExecutor.java b/android/app/src/main/java/com/bluebubbles/messaging/services/FlutterFirebaseMessagingBackgroundExecutor.java index dc9c4f753..55eba3e28 100644 --- a/android/app/src/main/java/com/bluebubbles/messaging/services/FlutterFirebaseMessagingBackgroundExecutor.java +++ b/android/app/src/main/java/com/bluebubbles/messaging/services/FlutterFirebaseMessagingBackgroundExecutor.java @@ -184,8 +184,8 @@ public void onMethodCall(MethodCall call, @NonNull Result result) { */ public void startBackgroundIsolate() { if (isNotRunning()) { - long callbackHandle = getPluginCallbackHandle(); - if (callbackHandle != 0) { + Long callbackHandle = getPluginCallbackHandle(); + if (callbackHandle != null && callbackHandle != 0) { startBackgroundIsolate(callbackHandle, null); } } diff --git a/android/app/src/main/res/drawable/ic_stat_icon.png b/android/app/src/main/res/drawable/ic_stat_icon.png new file mode 100644 index 000000000..3cf130bd9 Binary files /dev/null and b/android/app/src/main/res/drawable/ic_stat_icon.png differ diff --git a/android/app/src/main/res/raw/raspberry.wav b/android/app/src/main/res/raw/raspberry.wav new file mode 100644 index 000000000..249e7d6f9 Binary files /dev/null and b/android/app/src/main/res/raw/raspberry.wav differ diff --git a/android/app/src/main/res/raw/sugarfree.wav b/android/app/src/main/res/raw/sugarfree.wav new file mode 100644 index 000000000..f4d5cb54b Binary files /dev/null and b/android/app/src/main/res/raw/sugarfree.wav differ diff --git a/android/app/src/main/res/raw/twig.wav b/android/app/src/main/res/raw/twig.wav new file mode 100644 index 000000000..6fd1a0f7b Binary files /dev/null and b/android/app/src/main/res/raw/twig.wav differ diff --git a/android/app/src/main/res/raw/walrus.wav b/android/app/src/main/res/raw/walrus.wav new file mode 100644 index 000000000..0d45d3f2e Binary files /dev/null and b/android/app/src/main/res/raw/walrus.wav differ diff --git a/assets/changelog/changelog.md b/assets/changelog/changelog.md index a1fdfa920..f39829a43 100644 --- a/assets/changelog/changelog.md +++ b/assets/changelog/changelog.md @@ -2,10 +2,62 @@ Below are the last few BlueBubbles App release changelogs -## v1.4.1 +## v1.5.0 -### Bug Fixes +## Changes + +### The Big Stuff +* New notification options +* New theming options + - Ability to dynamically theme the app based on the current song's album cover + - Ability to copy and save those dynamic themes +* Tablet mode +* Unknown senders tab option + - Senders with no associated contact info will be separated +* Other new features, UI, and UX improvements + +### The Nitty Gritty + +#### New Features + +- **New Notification Options** + - Added the option to schedule a reminder for a message by long-pressing the message + - Added new notification settings page + - Added the ability to change the notification sound + - Added the option to disable notifying reactions + - Added the ability to set global text detection (only notify when a text contains certain words or phrases) + - Added the ability to mute certain individuals in a chat + - Added the ability to mute a chat temporarily + - Added the ability to set text detection on a specific chat - only notify when a text from the specified chat contains certain words or phrases +- **New Theming Options** + - Added the ability to get background and primary color from album art (requires full notification access) + - Added the ability to set an animated gradient background for the chat list (gradient is created between background and primary color) +- **Other New Features** + - Added a better logging mechanism to make it easier to send bug reports to the developers + - Added ability to add a camera button above the chat creator button like Signal + - Added "Unknown Senders" tab +#### Bug Fixes + +- **UI bugs** + - Fixed custom bubble color getting reset for new messages + - Fixed 24hr time not working properly + - Improved smart reply padding in Material theme +- **UX bugs** + - Fixed some bugs with the fullscreen photo viewer + +#### Improvements + +- **UI Improvements** + - Move pinned chat typing indicator to the avatar so the latest message bubble can always be seen + - Completely revamped icons for iOS theme to match iOS-style + - Improved URL preview + - Removed Camera preview from share menu to reduce lag. Replaced by 2 buttons, camera and video +- **UX Improvements** + - Added pagination to incremental sync (messages should load faster) + - Increased chat page size to reduce visible "lag" when resuming the app from the background + +## v1.4.1 ### Enhancements * Increases message preview to 2 lines (max) diff --git a/lib/action_handler.dart b/lib/action_handler.dart index b352e5649..9839cfbd1 100644 --- a/lib/action_handler.dart +++ b/lib/action_handler.dart @@ -7,6 +7,7 @@ import 'package:bluebubbles/helpers/attachment_downloader.dart'; import 'package:bluebubbles/helpers/attachment_helper.dart'; import 'package:bluebubbles/helpers/attachment_sender.dart'; import 'package:bluebubbles/helpers/darty.dart'; +import 'package:bluebubbles/helpers/logger.dart'; import 'package:bluebubbles/helpers/message_helper.dart'; import 'package:bluebubbles/helpers/utils.dart'; import 'package:bluebubbles/managers/current_chat.dart'; @@ -19,6 +20,7 @@ import 'package:bluebubbles/managers/settings_manager.dart'; import 'package:bluebubbles/repository/database.dart'; import 'package:bluebubbles/repository/models/attachment.dart'; import 'package:bluebubbles/repository/models/chat.dart'; +import 'package:bluebubbles/repository/models/handle.dart'; import 'package:bluebubbles/repository/models/message.dart'; import 'package:bluebubbles/socket_manager.dart'; import 'package:collection/collection.dart'; @@ -252,7 +254,7 @@ class ActionHandler { // If there is an error, replace the temp value with an error if (response['status'] != 200) { - debugPrint("FAILED TO SEND REACTION " + response['error']['message']); + Logger.error("FAILED TO SEND REACTION " + response['error']['message']); } completer.complete(); @@ -361,7 +363,7 @@ class ActionHandler { [chat.id]); // If there are no messages, return - debugPrint("Deleting ${items.length} messages"); + Logger.info("Deleting ${items.length} messages"); if (isNullOrEmpty(items)!) return; Batch batch = db.batch(); @@ -409,7 +411,7 @@ class ActionHandler { if (updatedMessage.isFromMe!) { await Future.delayed(Duration(milliseconds: 200)); - debugPrint("(Message status) -> handleUpdatedMessage: " + updatedMessage.text!); + Logger.info("Handling message update: " + updatedMessage.text!, tag: "Actions-UpdatedMessage"); } updatedMessage = await Message.replaceMessage(updatedMessage.guid, updatedMessage) ?? updatedMessage; @@ -463,7 +465,7 @@ class ActionHandler { // Save the new chat only if current chat isn't found if (currentChat == null) { - debugPrint("(Handle Chat) Chat did not exist. Saving."); + Logger.info("Chat did not exist. Saving.", tag: "Actions-HandleChat"); await newChat.save(); } @@ -476,7 +478,7 @@ class ActionHandler { if (newChat == null) return; await ChatBloc().updateChatPosition(newChat); } catch (ex) { - debugPrint(ex.toString()); + Logger.error(ex.toString()); } } @@ -502,7 +504,8 @@ class ActionHandler { // If the GUID exists already, delete the temporary entry // Otherwise, replace the temp message if (existing != null) { - debugPrint("(Message status) -> Deleting message: [${data["text"]}] - ${data["guid"]} - ${data["tempGuid"]}"); + Logger.info("Deleting message: [${data["text"]}] - ${data["guid"]} - ${data["tempGuid"]}", + tag: "MessageStatus"); await Message.delete({'guid': data['tempGuid']}); NewMessageManager().removeMessage(chats.first, data['tempGuid']); } else { @@ -515,18 +518,18 @@ class ActionHandler { try { await Attachment.replaceAttachment(data["tempGuid"], file); } catch (ex) { - debugPrint("Attachment's Old GUID doesn't exist. Skipping"); + Logger.warn("Attachment's Old GUID doesn't exist. Skipping"); } message.attachments!.add(file); } - debugPrint("(Message status) -> Message match: [${data["text"]}] - ${data["guid"]} - ${data["tempGuid"]}"); + Logger.info("Message match: [${data["text"]}] - ${data["guid"]} - ${data["tempGuid"]}", tag: "MessageStatus"); if (!isHeadless) NewMessageManager().updateMessage(chats.first, data['tempGuid'], message); } } else if (forceProcess || !NotificationManager().hasProcessed(data["guid"])) { // Add the message to the chats for (int i = 0; i < chats.length; i++) { - debugPrint("Client received new message " + chats[i].guid!); + Logger.info("Client received new message " + chats[i].guid!); // Gets the chat from the chat bloc Chat? chat = await ChatBloc().getChat(chats[i].guid); @@ -535,11 +538,18 @@ class ActionHandler { chat = chats[i]; } + Handle? handle = chat.participants.firstWhereOrNull((e) => e.address == message.handle?.address); + + if (handle != null) { + message.handle?.color = handle.color; + message.handle?.defaultPhone = handle.defaultPhone; + } + await chat.getParticipants(); // Handle the notification based on the message and chat await MessageHelper.handleNotification(message, chat); - debugPrint("(Message status) New message: [${message.text}] - [${message.guid}]"); + Logger.info("New message: [${message.text}] - [${message.guid}]", tag: "Actions-HandleMessage"); await chat.addMessage(message); if (message.itemType == 2 && message.groupTitle != null) { diff --git a/lib/blocs/chat_bloc.dart b/lib/blocs/chat_bloc.dart index fb37e5973..4c02b39ab 100644 --- a/lib/blocs/chat_bloc.dart +++ b/lib/blocs/chat_bloc.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:typed_data'; import 'package:bluebubbles/helpers/constants.dart'; +import 'package:bluebubbles/helpers/logger.dart'; import 'package:bluebubbles/helpers/utils.dart'; import 'package:bluebubbles/managers/attachment_info_bloc.dart'; import 'package:bluebubbles/managers/contact_manager.dart'; @@ -9,8 +10,8 @@ import 'package:bluebubbles/managers/method_channel_interface.dart'; import 'package:bluebubbles/managers/new_message_manager.dart'; import 'package:bluebubbles/managers/notification_manager.dart'; import 'package:bluebubbles/managers/settings_manager.dart'; +import 'package:bluebubbles/repository/models/message.dart'; import 'package:contacts_service/contacts_service.dart'; -import 'package:flutter/material.dart'; import 'package:get/get.dart'; import '../repository/models/chat.dart'; @@ -34,6 +35,7 @@ class ChatBloc { } Completer? chatRequest; + int lastFetch = 0; static final ChatBloc _chatBloc = ChatBloc._internal(); @@ -74,20 +76,45 @@ class ChatBloc { } chatRequest = new Completer(); - - debugPrint("[ChatBloc] -> Fetching chats (${force ? 'forced' : 'normal'})..."); + Logger.info("Fetching chats (${force ? 'forced' : 'normal'})...", tag: "ChatBloc"); // Get the contacts in case we haven't - await ContactManager().getContacts(); + if (ContactManager().contacts.isEmpty) await ContactManager().getContacts(); if (_messageSubscription == null) { _messageSubscription = setupMessageListener(); } + // Store the last time we fetched + lastFetch = DateTime.now().toUtc().millisecondsSinceEpoch; + // Fetch the first x chats getChatBatches(); } + Future resumeRefresh() async { + Logger.info('Performing ChatBloc resume request...', tag: 'ChatBloc-Resume'); + + // Get the last message date + DateTime? lastMsgDate = await Message.lastMessageDate(); + + // If there is no last message, don't do anything + if (lastMsgDate == null) { + Logger.debug("No last message date found! Not doing anything...", tag: 'ChatBloc-Resume'); + return; + } + + // If the last message date is >= the last fetch, let's refetch + int lastMs = lastMsgDate.millisecondsSinceEpoch; + if (lastMs >= lastFetch) { + Logger.info('New messages detected! Refreshing the ChatBloc', tag: 'ChatBloc-Resume'); + Logger.debug("$lastMs >= $lastFetch", tag: 'ChatBloc-Resume'); + await this.refreshChats(); + } else { + Logger.info('No new messages detected. Not refreshing the ChatBloc', tag: 'ChatBloc-Resume'); + } + } + /// Inserts a [chat] into the chat bloc based on the lastMessage data Future updateChatPosition(Chat chat) async { if (isNullOrEmpty(_chats)!) { @@ -204,7 +231,7 @@ class ChatBloc { icon = NotificationManager().defaultAvatar; } } catch (ex) { - debugPrint("Failed to load contact avatar: ${ex.toString()}"); + Logger.error("Failed to load contact avatar: ${ex.toString()}"); } // If we don't have a title, try to get it @@ -245,7 +272,7 @@ class ChatBloc { return NewMessageManager().stream.listen(handleMessageAction); } - Future getChatBatches({int batchSize = 10}) async { + Future getChatBatches({int batchSize = 15}) async { int count = (await Chat.count()) ?? 0; if (count == 0) { hasChats.value = false; @@ -263,13 +290,9 @@ class ChatBloc { for (Chat chat in chats) { newChats.add(chat); - await initTileValsForChat(chat); - } - - for (int i = 0; i < newChats.length; i++) { - if (isNullOrEmpty(newChats[i].participants)!) { - await newChats[i].getParticipants(); + if (isNullOrEmpty(chat.participants)!) { + await chat.getParticipants(); } } @@ -279,7 +302,7 @@ class ChatBloc { } } - debugPrint("[ChatBloc] -> Finished fetching chats (${_chats.length})."); + Logger.info("Finished fetching chats (${_chats.length}).", tag: "ChatBloc"); await updateAllShareTargets(); if (chatRequest != null && !chatRequest!.isCompleted) { @@ -321,12 +344,18 @@ class ChatBloc { final item = _chats.bigPinHelper(true)[oldIndex]; if (newIndex > oldIndex) { newIndex = newIndex - 1; - _chats.bigPinHelper(true).where((p0) => p0.pinIndex.value != null && p0.pinIndex.value! <= newIndex).forEach((element) { + _chats + .bigPinHelper(true) + .where((p0) => p0.pinIndex.value != null && p0.pinIndex.value! <= newIndex) + .forEach((element) { element.pinIndex.value = element.pinIndex.value! - 1; }); item.pinIndex.value = newIndex; } else { - _chats.bigPinHelper(true).where((p0) => p0.pinIndex.value != null && p0.pinIndex.value! >= newIndex).forEach((element) { + _chats + .bigPinHelper(true) + .where((p0) => p0.pinIndex.value != null && p0.pinIndex.value! >= newIndex) + .forEach((element) { element.pinIndex.value = element.pinIndex.value! + 1; }); item.pinIndex.value = newIndex; @@ -413,4 +442,21 @@ extension Helpers on RxList { .toList() .obs; } + + RxList unknownSendersHelper(bool unknown) { + if (!SettingsManager().settings.filterUnknownSenders.value) return this; + if (unknown) + return this + .where( + (e) => e.participants.length == 1 && ContactManager().handleToContact[e.participants[0].address] == null) + .toList() + .obs; + else + return this + .where((e) => + e.participants.length > 1 || + (e.participants.length == 1 && ContactManager().handleToContact[e.participants[0].address] != null)) + .toList() + .obs; + } } diff --git a/lib/blocs/message_bloc.dart b/lib/blocs/message_bloc.dart index 4ed44bc78..c393ca091 100644 --- a/lib/blocs/message_bloc.dart +++ b/lib/blocs/message_bloc.dart @@ -1,14 +1,13 @@ import 'dart:async'; import 'dart:collection'; +import 'package:bluebubbles/helpers/logger.dart'; import 'package:bluebubbles/helpers/message_helper.dart'; import 'package:bluebubbles/helpers/utils.dart'; import 'package:bluebubbles/managers/current_chat.dart'; import 'package:bluebubbles/managers/new_message_manager.dart'; import 'package:bluebubbles/repository/models/chat.dart'; import 'package:bluebubbles/repository/models/message.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; import 'package:get/get.dart'; import '../socket_manager.dart'; @@ -143,9 +142,10 @@ class MessageBloc { List messages = _allMessages.values.toList(); for (int i = 0; i < messages.length; i++) { //if _allMessages[i] dateCreated is earlier than the new message, insert at that index - if (message.guid != null && (messages[i]!.originalROWID != null && - message.originalROWID != null && - message.originalROWID! > messages[i]!.originalROWID!) || + if (message.guid != null && + (messages[i]!.originalROWID != null && + message.originalROWID != null && + message.originalROWID! > messages[i]!.originalROWID!) || ((messages[i]!.originalROWID == null || message.originalROWID == null) && messages[i]!.dateCreated!.compareTo(message.dateCreated!) < 0)) { _allMessages = linkedHashMapInsert(_allMessages, i, message.guid!, message); @@ -227,25 +227,25 @@ class MessageBloc { _allMessages.addAll({message.guid!: message}); } - // print("ITEMS OG"); + // Logger.instance.log("ITEMS OG"); // for (var i in _allMessages.values.toList()) { - // print(i.guid); + // Logger.instance.log(i.guid); // } // for (var i in res ?? []) { // Message tmp = Message.fromMap(i); - // print("ADDING: ${tmp.guid}"); + // Logger.instance.log("ADDING: ${tmp.guid}"); // if (!_allMessages.containsKey(tmp.guid)) { // _allMessages.addAll({tmp.guid: message}); // } // } - // print("ITEMS AFTER"); + // Logger.instance.log("ITEMS AFTER"); // for (var i in _allMessages.values.toList()) { - // print("TEXT: ${i.text}"); + // Logger.instance.log("TEXT: ${i.text}"); // } - // print(_allMessages.length); + // Logger.instance.log(_allMessages.length); this.emitLoaded(); } @@ -278,10 +278,10 @@ class MessageBloc { // Handle the messages if (isNullOrEmpty(_messages)!) { - debugPrint("(CHUNK) No message chunks left from server"); + Logger.info("No message chunks left from server", tag: "MessageBloc"); completer.complete(LoadMessageResult.RETREIVED_NO_MESSAGES); } else { - debugPrint("(CHUNK) Received ${_messages.length} messages from socket"); + Logger.info("Received ${_messages.length} messages from socket", tag: "MessageBloc"); messages = await MessageHelper.bulkAddMessages(_currentChat, _messages, notifyMessageManager: false, notifyForNewMessage: false, checkForLatestMessageText: false); @@ -293,14 +293,14 @@ class MessageBloc { } } } catch (ex) { - debugPrint("(CHUNK) Failed to load message chunk!"); - debugPrint(ex.toString()); + Logger.error("Failed to load message chunk!", tag: "MessageBloc"); + Logger.error(ex.toString()); completer.complete(LoadMessageResult.FAILED_TO_RETREIVE); } } // Save the messages to the bloc - debugPrint("(CHUNK) Emitting ${messages.length} messages to listeners"); + Logger.info("Emitting ${messages.length} messages to listeners", tag: "MessageBloc"); for (Message element in messages) { if (element.associatedMessageGuid == null && element.guid != null) { _allMessages.addAll({element.guid!: element}); @@ -323,7 +323,7 @@ class MessageBloc { completer.complete(LoadMessageResult.RETREIVED_MESSAGES); } } else { - debugPrint("(CHUNK) Failed to load message chunk! Unknown chat!"); + Logger.error(" Failed to load message chunk! Unknown chat!", tag: "MessageBloc"); completer.complete(LoadMessageResult.FAILED_TO_RETREIVE); } diff --git a/lib/blocs/setup_bloc.dart b/lib/blocs/setup_bloc.dart index 67af85ff7..039b1b74b 100644 --- a/lib/blocs/setup_bloc.dart +++ b/lib/blocs/setup_bloc.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:bluebubbles/blocs/chat_bloc.dart'; +import 'package:bluebubbles/helpers/logger.dart'; import 'package:bluebubbles/helpers/message_helper.dart'; import 'package:bluebubbles/helpers/utils.dart'; import 'package:bluebubbles/managers/contact_manager.dart'; @@ -9,7 +10,6 @@ import 'package:bluebubbles/repository/models/chat.dart'; import 'package:bluebubbles/repository/models/fcm_data.dart'; import 'package:bluebubbles/repository/models/settings.dart'; import 'package:bluebubbles/socket_manager.dart'; -import 'package:flutter/material.dart'; import 'package:get/get.dart'; enum SetupOutputType { ERROR, LOG } @@ -56,7 +56,7 @@ class SetupBloc { Future connectToServer(FCMData data, String serverURL, String password) async { Settings settingsCopy = SettingsManager().settings; if (SocketManager().state.value == SocketState.CONNECTED && settingsCopy.serverAddress.value == serverURL) { - debugPrint("Not reconnecting to server we are already connected to!"); + Logger.warn("Not reconnecting to server we are already connected to!"); return; } @@ -143,13 +143,26 @@ class SetupBloc { } else { try { if (!(chat.chatIdentifier ?? "").startsWith("urn:biz")) { - await chat.save(); - - // Re-match the handles with the contacts - await ContactManager().matchHandles(); - - await syncChat(chat); - addOutput("Finished syncing chat, '${chat.chatIdentifier}'", SetupOutputType.LOG); + Map params = Map(); + params["identifier"] = chat.guid; + params["withBlurhash"] = false; + params["limit"] = numberOfMessagesPerPage.round(); + params["where"] = [ + {"statement": "message.service = 'iMessage'", "args": null} + ]; + List messages = await SocketManager().getChatMessages(params)!; + addOutput("Received ${messages.length} messages for chat, '${chat.chatIdentifier}'!", SetupOutputType.LOG); + if (!skipEmptyChats || (skipEmptyChats && messages.length > 0)) { + await chat.save(); + + // Re-match the handles with the contacts + await ContactManager().matchHandles(); + + await syncChat(chat, messages); + addOutput("Finished syncing chat, '${chat.chatIdentifier}'", SetupOutputType.LOG); + } else { + addOutput("Skipping syncing chat (empty chat), '${chat.chatIdentifier}'", SetupOutputType.LOG); + } } else { addOutput("Skipping syncing chat, '${chat.chatIdentifier}'", SetupOutputType.LOG); } @@ -184,18 +197,7 @@ class SetupBloc { this.startIncrementalSync(settings); } - Future syncChat(Chat chat) async { - Map params = Map(); - params["identifier"] = chat.guid; - params["withBlurhash"] = false; - params["limit"] = numberOfMessagesPerPage.round(); - params["where"] = [ - {"statement": "message.service = 'iMessage'", "args": null} - ]; - - List messages = await SocketManager().getChatMessages(params)!; - addOutput("Received ${messages.length} messages for chat, '${chat.chatIdentifier}'!", SetupOutputType.LOG); - + Future syncChat(Chat chat, List messages) async { // Since we got the messages in desc order, we want to reverse it. // Reversing it will add older messages before newer one. This should help fix // issues with associated message GUIDs @@ -226,7 +228,7 @@ class SetupBloc { } void addOutput(String _output, SetupOutputType type) { - debugPrint('[Setup] -> $_output'); + Logger.info(_output, tag: "Setup"); output.add(SetupOutputData(_output, type)); data.value = SetupData(_progress, output); } @@ -235,7 +237,8 @@ class SetupBloc { {String? chatGuid, bool saveDate = true, Function? onConnectionError, Function? onComplete}) async { // If we are already syncing, don't sync again // Or, if we haven't finished setup, or we aren't connected, don't sync - if (isSyncing.value || !settings.finishedSetup.value || SocketManager().state.value != SocketState.CONNECTED) return; + if (isSyncing.value || !settings.finishedSetup.value || SocketManager().state.value != SocketState.CONNECTED) + return; // Reset the progress _progress = 0; @@ -252,39 +255,45 @@ class SetupBloc { int syncStart = DateTime.now().millisecondsSinceEpoch; await Future.delayed(Duration(seconds: 3)); - // Build request params. We want all details on the messages - Map params = Map(); - if (chatGuid != null) { - params["chatGuid"] = chatGuid; - } + // only get up to 1000 messages (arbitrary limit) + int batches = 10; + for (int i = 0; i < batches; i++) { + // Build request params. We want all details on the messages + Map params = Map(); + if (chatGuid != null) { + params["chatGuid"] = chatGuid; + } - params["withBlurhash"] = false; // Maybe we want it? - params["limit"] = 1000; // This is arbitrary, hopefully there aren't more messages - params["after"] = settings.lastIncrementalSync.value; // Get everything since the last sync - params["withChats"] = true; // We want the chats too so we can save them correctly - params["withAttachments"] = true; // We want the attachment data - params["withHandle"] = true; // We want to know who sent it - params["sort"] = "DESC"; // Sort my DESC so we receive the newest messages first - params["where"] = [ - {"statement": "message.service = 'iMessage'", "args": null} - ]; - - List messages = await SocketManager().getMessages(params)!; - if (messages.isEmpty) { - addOutput("No new messages found during incremental sync", SetupOutputType.LOG); - } else { - addOutput("Incremental sync found ${messages.length} messages. Syncing...", SetupOutputType.LOG); - } + params["withBlurhash"] = false; // Maybe we want it? + params["limit"] = 100; + params["offset"] = i * batches; + params["after"] = settings.lastIncrementalSync.value; // Get everything since the last sync + params["withChats"] = true; // We want the chats too so we can save them correctly + params["withAttachments"] = true; // We want the attachment data + params["withHandle"] = true; // We want to know who sent it + params["sort"] = "DESC"; // Sort my DESC so we receive the newest messages first + params["where"] = [ + {"statement": "message.service = 'iMessage'", "args": null} + ]; + + List messages = await SocketManager().getMessages(params)!; + if (messages.isEmpty) { + addOutput("No more new messages found during incremental sync", SetupOutputType.LOG); + break; + } else { + addOutput("Incremental sync found ${messages.length} messages. Syncing...", SetupOutputType.LOG); + } - if (messages.length > 0) { - await MessageHelper.bulkAddMessages(null, messages, onProgress: (progress, total) { - _progress = (progress / total) * 100; - data.value = SetupData(_progress, output); - }); + if (messages.length > 0) { + await MessageHelper.bulkAddMessages(null, messages, onProgress: (progress, total) { + _progress = (progress / total) * 100; + data.value = SetupData(_progress, output); + }); - // If we want to download the attachments, do it, and wait for them to finish before continuing - if (downloadAttachments) { - await MessageHelper.bulkDownloadAttachments(null, messages.reversed.toList()); + // If we want to download the attachments, do it, and wait for them to finish before continuing + if (downloadAttachments) { + await MessageHelper.bulkDownloadAttachments(null, messages.reversed.toList()); + } } } diff --git a/lib/helpers/attachment_downloader.dart b/lib/helpers/attachment_downloader.dart index 8c4be15ab..9fe619c02 100644 --- a/lib/helpers/attachment_downloader.dart +++ b/lib/helpers/attachment_downloader.dart @@ -1,11 +1,11 @@ import 'dart:io'; import 'package:bluebubbles/helpers/attachment_helper.dart'; +import 'package:bluebubbles/helpers/logger.dart'; import 'package:bluebubbles/managers/settings_manager.dart'; import 'package:bluebubbles/repository/models/attachment.dart'; import 'package:bluebubbles/socket_manager.dart'; import 'package:collection/collection.dart'; -import 'package:flutter/material.dart'; import 'package:get/get.dart'; class AttachmentDownloadService extends GetxService { @@ -59,7 +59,7 @@ class AttachmentDownloadController extends GetxController { if (attachment.guid == null) return; isFetching = true; int numOfChunks = (attachment.totalBytes! / chunkSize).ceil(); - debugPrint("Fetching $numOfChunks attachment chunks"); + Logger.info("Fetching $numOfChunks attachment chunks"); stopwatch.start(); getChunkRecursive(attachment.guid!, 0, numOfChunks, []); } @@ -91,7 +91,7 @@ class AttachmentDownloadController extends GetxController { if (numBytes == chunkSize) { // Calculate some stats double progress = ((index + 1) / total).clamp(0, 1).toDouble(); - debugPrint("Progress: ${(progress * 100).round()}% of the attachment"); + Logger.info("Progress: ${(progress * 100).round()}% of the attachment"); // Update the progress in stream setProgress(progress); @@ -99,9 +99,9 @@ class AttachmentDownloadController extends GetxController { // Get the next chunk getChunkRecursive(guid, index + 1, total, currentBytes); } else { - debugPrint("Finished fetching attachment"); + Logger.info("Finished fetching attachment"); stopwatch.stop(); - debugPrint("Attachment downloaded in ${stopwatch.elapsedMilliseconds} ms"); + Logger.info("Attachment downloaded in ${stopwatch.elapsedMilliseconds} ms"); try { // Compress the attachment diff --git a/lib/helpers/attachment_helper.dart b/lib/helpers/attachment_helper.dart index 5cb263374..8f22ba08e 100644 --- a/lib/helpers/attachment_helper.dart +++ b/lib/helpers/attachment_helper.dart @@ -3,16 +3,19 @@ import 'dart:isolate'; import 'dart:typed_data'; import 'dart:ui'; +import 'package:bluebubbles/helpers/constants.dart'; +import 'package:bluebubbles/helpers/logger.dart'; +import 'package:bluebubbles/helpers/navigator.dart'; import 'package:bluebubbles/helpers/simple_vcard_parser.dart'; import 'package:contacts_service/contacts_service.dart'; import 'package:exif/exif.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter_native_image/flutter_native_image.dart'; import 'package:get/get.dart'; import 'package:bluebubbles/helpers/attachment_downloader.dart'; import 'package:bluebubbles/helpers/utils.dart'; import 'package:bluebubbles/managers/settings_manager.dart'; import 'package:bluebubbles/repository/models/attachment.dart'; -import 'package:bluebubbles/socket_manager.dart'; import 'package:connectivity/connectivity.dart'; import 'package:flutter/material.dart'; import 'package:image/image.dart' as img; @@ -82,8 +85,8 @@ class AttachmentHelper { latitude: double.tryParse(query.split(",")[1]), longitude: double.tryParse(query.split(",")[0])); } } catch (ex) { - debugPrint("Failed to parse location!"); - debugPrint(ex.toString()); + Logger.error("Failed to parse location!"); + Logger.error(ex.toString()); return AppleLocation(latitude: null, longitude: null); } } @@ -102,7 +105,7 @@ class AttachmentHelper { // Parse emails from results for (dynamic email in _contact.typedEmail) { String label = "HOME"; - if (email.length > 1 && email[1].length > 0 && email[1][1] != null) { + if (email.length > 1 && email[1].length > 1 && email[1][1] != null) { label = email[1][1] ?? label; } @@ -112,7 +115,7 @@ class AttachmentHelper { // Parse phone numbers from results for (dynamic phone in _contact.typedTelephone) { String label = "HOME"; - if (phone.length > 1 && phone[1].length > 0 && phone[1][1] != null) { + if (phone.length > 1 && phone[1].length > 1 && phone[1][1] != null) { label = phone[1][1] ?? label; } @@ -127,7 +130,7 @@ class AttachmentHelper { String country = address[0].length > 3 ? address[0][3] : ''; String label = "HOME"; - if (address.length > 1 && address[1].length > 0 && address[1][1] != null) { + if (address.length > 1 && address[1].length > 1 && address[1][1] != null) { label = address[1][1] ?? label; } @@ -166,7 +169,7 @@ class AttachmentHelper { double width = attachment.width?.toDouble() ?? 0.0; double factor = attachment.height?.toDouble() ?? 0.0; if (attachment.width == null || attachment.width == 0 || attachment.height == null || attachment.height == 0) { - width = context.width; + width = CustomNavigator.width(context); factor = 2; } @@ -234,21 +237,21 @@ class AttachmentHelper { } static IconData getIcon(String mimeType) { - if (mimeType.isEmpty) return Icons.open_in_new; + if (mimeType.isEmpty) return SettingsManager().settings.skin.value == Skins.iOS ? CupertinoIcons.arrow_up_right_square : Icons.open_in_new; if (mimeType == "application/pdf") { - return Icons.picture_as_pdf; + return SettingsManager().settings.skin.value == Skins.iOS ? CupertinoIcons.doc_on_doc : Icons.picture_as_pdf; } else if (mimeType == "application/zip") { - return Icons.folder; + return SettingsManager().settings.skin.value == Skins.iOS ? CupertinoIcons.folder : Icons.folder; } else if (mimeType.startsWith("audio")) { - return Icons.music_note; + return SettingsManager().settings.skin.value == Skins.iOS ? CupertinoIcons.music_note : Icons.music_note; } else if (mimeType.startsWith("image")) { - return Icons.photo; + return SettingsManager().settings.skin.value == Skins.iOS ? CupertinoIcons.photo : Icons.photo; } else if (mimeType.startsWith("video")) { - return Icons.videocam; + return SettingsManager().settings.skin.value == Skins.iOS ? CupertinoIcons.videocam : Icons.videocam; } else if (mimeType.startsWith("text")) { - return Icons.note; + return SettingsManager().settings.skin.value == Skins.iOS ? CupertinoIcons.doc_text : Icons.note; } - return Icons.open_in_new; + return SettingsManager().settings.skin.value == Skins.iOS ? CupertinoIcons.arrow_up_right_square : Icons.open_in_new; } static Future canAutoDownload() async { @@ -276,7 +279,8 @@ class AttachmentHelper { if (cExists) compressedFile.deleteSync(); // Redownload the attachment - Get.put(AttachmentDownloadController(attachment: attachment, onComplete: onComplete, onError: onError), tag: attachment.guid); + Get.put(AttachmentDownloadController(attachment: attachment, onComplete: onComplete, onError: onError), + tag: attachment.guid); } static Future getVideoThumbnail(String filePath) async { @@ -396,7 +400,7 @@ class AttachmentHelper { attachment.height = size.height.toInt(); } } catch (ex) { - debugPrint('Failed to get GIF dimensions! Error: ${ex.toString()}'); + Logger.error('Failed to get GIF dimensions! Error: ${ex.toString()}'); } } else if (mimeStart == "image") { // For images, load properties @@ -415,7 +419,7 @@ class AttachmentHelper { attachment.metadata!['orientation'] = 'portrait'; } } catch (ex) { - debugPrint('Failed to get Image Properties! Error: ${ex.toString()}'); + Logger.error('Failed to get Image Properties! Error: ${ex.toString()}'); } } else if (mimeStart == "video") { // For videos, load the thumbnail @@ -427,7 +431,7 @@ class AttachmentHelper { attachment.height = size.height.toInt(); } } catch (ex) { - debugPrint('Failed to get video thumbnail! Error: ${ex.toString()}'); + Logger.error('Failed to get video thumbnail! Error: ${ex.toString()}'); } } @@ -438,14 +442,14 @@ class AttachmentHelper { attachment.metadata![item.key] = item.value.printable; } } catch (ex) { - debugPrint('Failed to read EXIF data: ${ex.toString()}'); + Logger.error('Failed to read EXIF data: ${ex.toString()}'); } bool usedFallback = false; // If the preview data is null, compress the file if (previewData == null) { // Compress the file ReceivePort receivePort = ReceivePort(); - debugPrint("Spawning isolate..."); + Logger.info("Spawning isolate..."); // if we don't have a valid width use the max image width // if the image width is less than the max width already don't bother // compressing it because it is already low quality @@ -463,13 +467,13 @@ class AttachmentHelper { } } await Isolate.spawn( - resizeIsolate, - ResizeArgs(filePath, receivePort.sendPort, compressWidth), - errorsAreFatal: false, + resizeIsolate, + ResizeArgs(filePath, receivePort.sendPort, compressWidth), + errorsAreFatal: false, ); var received = await receivePort.first; - debugPrint("Compressing via ${received is String ? "FlutterNativeImage" : "image"} plugin"); + Logger.info("Compressing via ${received is String ? "FlutterNativeImage" : "image"} plugin"); if (received is String) { File compressedFile = await FlutterNativeImage.compressImage(filePath, quality: quality, @@ -483,7 +487,7 @@ class AttachmentHelper { try { previewData = Uint8List.fromList(img.encodeNamedImage(received as img.Image, filePath.split("/").last) ?? []); } catch (e) { - debugPrint("Compression via image plugin failed, using fallback..."); + Logger.info("Compression via image plugin failed, using fallback..."); File compressedFile = await FlutterNativeImage.compressImage(filePath, quality: quality, targetWidth: attachment.width == null ? 0 : attachment.width!, @@ -496,7 +500,7 @@ class AttachmentHelper { } } if (previewData.isEmpty && !usedFallback) { - debugPrint("Compression via image plugin failed, using fallback..."); + Logger.info("Compression via image plugin failed, using fallback..."); File compressedFile = await FlutterNativeImage.compressImage(filePath, quality: quality, targetWidth: attachment.width == null ? 0 : attachment.width!, @@ -506,7 +510,7 @@ class AttachmentHelper { previewData = await compressedFile.readAsBytes(); usedFallback = true; } - debugPrint("Got previewData: ${previewData.isNotEmpty}"); + Logger.info("Got previewData: ${previewData.isNotEmpty}"); // As long as we have preview data now, save it cachedFile.writeAsBytes(previewData); @@ -519,9 +523,9 @@ class AttachmentHelper { static void resizeIsolate(ResizeArgs args) { try { - debugPrint("Decoding image..."); + Logger.info("Decoding image..."); img.Image image = img.decodeImage(File(args.path).readAsBytesSync())!; - debugPrint("Resizing image..."); + Logger.info("Resizing image..."); img.Image resized = img.copyResize(image, width: args.width); args.sendPort.send(resized); } catch (e) { diff --git a/lib/helpers/attachment_sender.dart b/lib/helpers/attachment_sender.dart index b3aab5a83..7b6983b74 100644 --- a/lib/helpers/attachment_sender.dart +++ b/lib/helpers/attachment_sender.dart @@ -5,6 +5,7 @@ import 'dart:typed_data'; import 'package:bluebubbles/helpers/attachment_helper.dart'; import 'package:bluebubbles/helpers/darty.dart'; +import 'package:bluebubbles/helpers/logger.dart'; import 'package:bluebubbles/helpers/utils.dart'; import 'package:bluebubbles/managers/new_message_manager.dart'; import 'package:bluebubbles/managers/settings_manager.dart'; @@ -12,7 +13,6 @@ import 'package:bluebubbles/repository/models/attachment.dart'; import 'package:bluebubbles/repository/models/chat.dart'; import 'package:bluebubbles/repository/models/message.dart'; import 'package:bluebubbles/socket_manager.dart'; -import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:mime_type/mime_type.dart'; import 'package:path/path.dart'; @@ -53,7 +53,7 @@ class AttachmentSender { } // resumeChunkingAfterDisconnect() { - // debugPrint("restarting chunking"); + // Logger.instance.log("restarting chunking"); // sendChunkRecursive(_guid, _currentchunk, _totalchunks, _currentbytes, // _chunksize * 1024, _cb); // } @@ -74,10 +74,7 @@ class AttachmentSender { params["hasMore"] = index + _chunkSize < _imageBytes.length; params["attachmentName"] = _attachmentName; params["attachmentData"] = base64Encode(chunk); - debugPrint(chunk.length.toString() + "/" + _imageBytes.length.toString()); - if (index == 0) { - debugPrint("(Sigabrt) Before sending first chunk"); - } + Logger.info(chunk.length.toString() + "/" + _imageBytes.length.toString()); SocketManager().sendMessage("send-message-chunk", params, (data) async { Map response = data; if (response['status'] == 200) { @@ -91,7 +88,8 @@ class AttachmentSender { SocketManager().finishSender(_attachmentGuid); } } else { - debugPrint("failed to send"); + Logger.error("Failed to sendattachment"); + String? tempGuid = sentMessage!.guid; sentMessage!.guid = sentMessage!.guid!.replaceAll("temp", "error-${response['error']['message']}"); sentMessage!.error.value = @@ -157,10 +155,8 @@ class AttachmentSender { // Save the attachment to device String appDocPath = SettingsManager().appDocDir.path; String pathName = "$appDocPath/attachments/${messageAttachment!.guid}/$_attachmentName"; - debugPrint("(Sigabrt) Before saving to device"); File file = await new File(pathName).create(recursive: true); await file.writeAsBytes(Uint8List.fromList(_imageBytes)); - debugPrint("(Sigabrt) After saving to device"); // Add the message to the chat. // This will save the message, attachments, and chat @@ -175,7 +171,6 @@ class AttachmentSender { _totalChunks = numOfChunks; SocketManager().addAttachmentSender(this); - debugPrint("(Sigabrt) Before sending first chunk"); sendChunkRecursive(0, _totalChunks, messageWithText == null ? "temp-${randomString(8)}" : messageWithText!.guid); } } diff --git a/lib/helpers/logger.dart b/lib/helpers/logger.dart new file mode 100644 index 000000000..ec74d5de3 --- /dev/null +++ b/lib/helpers/logger.dart @@ -0,0 +1,126 @@ +import 'dart:io'; + +import 'package:bluebubbles/helpers/share.dart'; +import 'package:bluebubbles/helpers/utils.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +// ignore: non_constant_identifier_names +BaseLogger Logger = Get.isRegistered() ? Get.find() : Get.put(BaseLogger()); + +enum LogLevel { INFO, WARN, ERROR, DEBUG } + +extension LogLevelExtension on LogLevel { + String get value { + String self = this.toString(); + return self.substring(self.indexOf('.') + 1).toUpperCase(); + } +} + +class BaseLogger extends GetxService { + final RxBool saveLogs = false.obs; + final int lineLimit = 5000; + List logs = []; + List enabledLevels = [LogLevel.INFO, LogLevel.WARN, LogLevel.DEBUG, LogLevel.ERROR]; + + String get logPath { + String directoryPath = "/storage/emulated/0/Download/BlueBubbles-log-"; + DateTime now = DateTime.now().toLocal(); + return directoryPath + "${now.year}${now.month}${now.day}_${now.hour}${now.minute}${now.second}" + ".txt"; + } + + set setEnabledLevels(List levels) => this.enabledLevels = levels; + + void startSavingLogs() { + this.saveLogs.value = true; + } + + Future stopSavingLogs() async { + this.saveLogs.value = false; + + // Write the log to a file so the user can view/share it + await this.writeLogToFile(); + + // Clear the logs + this.logs.clear(); + } + + Future writeLogToFile() async { + // Create the log file and write to it + String filePath = this.logPath; + File file = File(filePath); + await file.create(recursive: true); + await file.writeAsString(logs.join('\n')); + + // Show the snackbar when finished + showSnackbar( + "Success", + "Logs exported successfully to downloads folder", + durationMs: 2500, + button: TextButton( + style: TextButton.styleFrom( + backgroundColor: Get.theme.accentColor, + ), + onPressed: () { + Share.file("BlueBubbles Logs", filePath); + }, + child: Text("SHARE", style: TextStyle(color: Theme.of(Get.context!).primaryColor)), + ), + ); + } + + void info(dynamic log, {String? tag}) => this._log(LogLevel.INFO, log, tag: tag); + void warn(dynamic log, {String? tag}) => this._log(LogLevel.WARN, log, tag: tag); + void debug(dynamic log, {String? tag}) => this._log(LogLevel.DEBUG, log, tag: tag); + void error(dynamic log, {String? tag}) => this._log(LogLevel.ERROR, log, tag: tag); + + void _log(LogLevel level, dynamic log, {String name = "BlueBubblesApp", String? tag}) { + if (!this.enabledLevels.contains(level)) return; + + try { + // Example: [BlueBubblesApp][INFO][2021-01-01 01:01:01.000] (Some Tag) -> + String theLog = this._buildLog(level, name, tag, log); + + // Log the data normally + debugPrint(theLog); + + // If we aren't saving logs, return here + if (!this.saveLogs.value) return; + + // Otherwise, add the log to the list + logs.add(theLog); + + // Make sure we concatenate to our limit + if (this.logs.length >= this.lineLimit) { + // Be safe with it. Make sure we don't go negative or the ranges max < min + int min = this.logs.length - this.lineLimit; + int max = this.logs.length; + if (min < 0) min = 0; + if (max < min) max = min; + + // Take the last x amount of logs (based on the line limit) + this.logs = this.logs.sublist(min, max); + } + } catch (ex, stacktrace) { + debugPrint("Failed to write log! ${ex.toString()}"); + debugPrint(stacktrace.toString()); + } + } + + String _buildLog(LogLevel level, String name, String? tag, dynamic log) { + final time = DateTime.now().toLocal().toString(); + String theLog = "[$time][${level.value}]"; + + // If we have a name, add the name + if (name.isNotEmpty) { + theLog = "[$name]$theLog"; + } + + // If we have a tag, add it before the log string + if (tag != null && tag.isNotEmpty) { + theLog = "$theLog ($tag) ->"; + } + + return "$theLog ${log.toString()}"; + } +} diff --git a/lib/helpers/message_helper.dart b/lib/helpers/message_helper.dart index f1d4ec6f5..a264f0a4c 100644 --- a/lib/helpers/message_helper.dart +++ b/lib/helpers/message_helper.dart @@ -1,7 +1,7 @@ import 'dart:async'; -import 'dart:math'; import 'package:bluebubbles/helpers/attachment_downloader.dart'; +import 'package:bluebubbles/helpers/logger.dart'; import 'package:bluebubbles/helpers/utils.dart'; import 'package:bluebubbles/managers/contact_manager.dart'; import 'package:bluebubbles/managers/current_chat.dart'; @@ -104,9 +104,9 @@ class MessageHelper { // Every 50 messages synced, who a message index += 1; if (index % 50 == 0) { - debugPrint('[Bulk Ingest] Saved $index of ${messages.length} messages'); + Logger.info('Saved $index of ${messages.length} messages', tag: "BulkIngest"); } else if (index == messages.length) { - debugPrint('[Bulk Ingest] Saved ${messages.length} messages'); + Logger.info('Saved ${messages.length} messages', tag: "BulkIngest"); } } @@ -212,12 +212,14 @@ class MessageHelper { // Handle all the cases that would mean we don't show the notification if (!SettingsManager().settings.finishedSetup.value) return; // Don't notify if not fully setup if (existingMessage != null) return; - if (chat.isMuted!) return; // Don''t notify if the chat is muted + if (await chat.shouldMuteNotification(message)) return; // Don''t notify if the chat is muted if (message.isFromMe! || message.handle == null) return; // Don't notify if the text is from me CurrentChat? currChat = CurrentChat.activeChat; if (LifeCycleManager().isAlive && - ((!SettingsManager().settings.notifyOnChatList.value && currChat == null) || + ((!SettingsManager().settings.notifyOnChatList.value && + currChat == null && + !Get.currentRoute.contains("settings")) || currChat?.chat.guid == chat.guid)) { // Don't notify if the the chat is the active chat return; diff --git a/lib/helpers/metadata_helper.dart b/lib/helpers/metadata_helper.dart index c26d8c924..462d15ca6 100644 --- a/lib/helpers/metadata_helper.dart +++ b/lib/helpers/metadata_helper.dart @@ -2,9 +2,9 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; +import 'package:bluebubbles/helpers/logger.dart'; import 'package:bluebubbles/helpers/utils.dart'; import 'package:bluebubbles/repository/models/message.dart'; -import 'package:flutter/cupertino.dart'; import 'package:html/dom.dart'; import 'package:html/parser.dart' as parser; import 'package:http/http.dart' as http; @@ -136,7 +136,7 @@ class MetadataHelper { try { data = await MetadataFetch.extract(url); } catch (ex) { - debugPrint('An error occurred while fetching URL Preview Metadata: ${ex.toString()}'); + Logger.error('An error occurred while fetching URL Preview Metadata: ${ex.toString()}'); } } @@ -202,7 +202,7 @@ class MetadataHelper { document = parser.parse(response.body.toString()); document.requestUrl = response.request!.url.toString(); } catch (err) { - debugPrint("Error parsing HTML document: ${err.toString()}"); + Logger.error("Error parsing HTML document: ${err.toString()}"); return document; } @@ -240,7 +240,7 @@ class MetadataHelper { meta.title = 'Invalid SSL Certificate'; meta.description = ex.message; } catch (ex) { - debugPrint('Failed to manually get metadata: ${ex.toString()}'); + Logger.error('Failed to manually get metadata: ${ex.toString()}'); } return meta; diff --git a/lib/helpers/navigator.dart b/lib/helpers/navigator.dart new file mode 100644 index 000000000..0288eb653 --- /dev/null +++ b/lib/helpers/navigator.dart @@ -0,0 +1,108 @@ +import 'package:bluebubbles/layouts/widgets/theme_switcher/theme_switcher.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +// ignore: non_constant_identifier_names +BaseNavigator CustomNavigator = Get.isRegistered() ? Get.find() : Get.put(BaseNavigator()); + +/// Handles navigation for the app +class BaseNavigator extends GetxService { + /// width of left side of split screen view + double? _widthChatListLeft; + /// width of right side of split screen view + double? _widthChatListRight; + /// width of settings right side split screen + double? _widthSettings; + + set maxWidthLeft(double w) => _widthChatListLeft = w; + set maxWidthRight(double w) => _widthChatListRight = w; + set maxWidthSettings(double w) => _widthSettings = w; + + /// grab the available screen width, returning the split screen width if applicable + /// this should *always* be used in place of context.width or similar + double width(BuildContext context) { + if (Navigator.of(context).widget.key?.toString().contains("Getx nested key: 1") ?? false) { + return _widthChatListLeft ?? context.width; + } else if (Navigator.of(context).widget.key?.toString().contains("Getx nested key: 2") ?? false) { + return _widthChatListRight ?? context.width; + } else if (Navigator.of(context).widget.key?.toString().contains("Getx nested key: 3") ?? false) { + return _widthSettings ?? context.width; + } + return context.width; + } + + /// Push a new route onto the chat list right side navigator + void push(BuildContext context, Widget widget) { + if (Get.keys.containsKey(2) && (!context.isPhone || context.isLandscape)) { + Get.to(() => widget, transition: Transition.rightToLeft, id: 2); + } else { + Navigator.of(context).push(ThemeSwitcher.buildPageRoute( + builder: (BuildContext context) => widget, + )); + } + } + + /// Push a new route onto the chat list left side navigator + void pushLeft(BuildContext context, Widget widget) { + if (Get.keys.containsKey(1) && (!context.isPhone || context.isLandscape)) { + Get.to(() => widget, transition: Transition.leftToRight, id: 1); + } else { + Navigator.of(context).push(ThemeSwitcher.buildPageRoute( + builder: (BuildContext context) => widget, + )); + } + } + + /// Push a new route onto the settings navigator + void pushSettings(BuildContext context, Widget widget, {Bindings? binding}) { + if (Get.keys.containsKey(3) && (!context.isPhone || context.isLandscape)) { + Get.to(() => widget, transition: Transition.rightToLeft, id: 3, binding: binding); + } else { + binding?.dependencies(); + Navigator.of(context).push(ThemeSwitcher.buildPageRoute( + builder: (BuildContext context) => widget, + )); + } + } + + /// Push a new route, popping all previous routes, on the chat list right side navigator + void pushAndRemoveUntil(BuildContext context, Widget widget, bool Function(Route) predicate) { + if (Get.keys.containsKey(2) && (!context.isPhone || context.isLandscape)) { + Get.offUntil(GetPageRoute( + page: () => widget, + transition: Transition.noTransition + ), predicate, id: 2); + } else { + Navigator.of(context).pushAndRemoveUntil(ThemeSwitcher.buildPageRoute( + builder: (BuildContext context) => widget, + ), predicate); + } + } + + /// Push a new route, popping all previous routes, on the settings navigator + void pushAndRemoveSettingsUntil(BuildContext context, Widget widget, bool Function(Route) predicate, {Bindings? binding}) { + if (Get.keys.containsKey(3) && (!context.isPhone || context.isLandscape)) { + // we only want to offUntil when in landscape, otherwise when the user presses back, the previous page will be the chat list + Get.offUntil(GetPageRoute( + page: () => widget, + binding: binding, + transition: Transition.noTransition + ), predicate, id: 3); + } else { + binding?.dependencies(); + // only push here because we don't want to remove underlying routes when in portrait + Navigator.of(context).push(ThemeSwitcher.buildPageRoute( + builder: (BuildContext context) => widget, + )); + } + } + + void backSettingsCloseOverlays(BuildContext context) { + if (Get.keys.containsKey(3) && (!context.isPhone || context.isLandscape)) { + Get.back(closeOverlays: true, id: 3); + } else { + Get.back(closeOverlays: true); + } + } +} diff --git a/lib/helpers/reaction.dart b/lib/helpers/reaction.dart index f49af7a4e..2c18592c1 100644 --- a/lib/helpers/reaction.dart +++ b/lib/helpers/reaction.dart @@ -104,8 +104,8 @@ class Reaction { reactionList.add( Padding( padding: EdgeInsets.fromLTRB( - (this.messages[i].isFromMe! && isReactionPicker ? 5.0 : 0.0) + i.toDouble() * 10.0, - bigPin ? 0 : 1.0, + (this.messages[i].isFromMe! && !isReactionPicker ? 5.0 : 0.0) + i.toDouble() * 10.0, + bigPin || isReactionPicker ? 0 : 1.0, 0, 0, ), diff --git a/lib/helpers/share.dart b/lib/helpers/share.dart index 6117a1449..1418ca493 100644 --- a/lib/helpers/share.dart +++ b/lib/helpers/share.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'dart:io'; import 'package:bluebubbles/helpers/attachment_helper.dart'; +import 'package:bluebubbles/helpers/logger.dart'; import 'package:bluebubbles/helpers/utils.dart'; import 'package:bluebubbles/managers/method_channel_interface.dart'; import 'package:bluebubbles/managers/new_message_manager.dart'; @@ -10,7 +11,6 @@ import 'package:bluebubbles/repository/models/attachment.dart'; import 'package:bluebubbles/repository/models/chat.dart'; import 'package:bluebubbles/repository/models/message.dart'; import 'package:bluebubbles/socket_manager.dart'; -import 'package:flutter/widgets.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:share_plus/share_plus.dart' as sp; @@ -33,7 +33,7 @@ class Share { final result = await MethodChannelInterface().invokeMethod("get-last-location"); if (result == null) { - debugPrint("Failed to load last location!"); + Logger.error("Failed to load last location!"); return; } diff --git a/lib/helpers/simple_vcard_parser.dart b/lib/helpers/simple_vcard_parser.dart index 3de1932e5..40d2809ef 100644 --- a/lib/helpers/simple_vcard_parser.dart +++ b/lib/helpers/simple_vcard_parser.dart @@ -1,5 +1,7 @@ import 'dart:convert'; +import 'package:bluebubbles/helpers/logger.dart'; + class VCard { String? _vCardString; late List lines; @@ -33,10 +35,10 @@ class VCard { void printLines() { String s; - print('lines #${lines.length}'); + Logger.debug('lines #${lines.length}'); for (var i = 0; i < lines.length; i++) { s = i.toString().padLeft(2, '0'); - print('$s | ${lines[i]}'); + Logger.debug('$s | ${lines[i]}'); } } diff --git a/lib/helpers/themes.dart b/lib/helpers/themes.dart index 6f83894dd..2c195cf2e 100644 --- a/lib/helpers/themes.dart +++ b/lib/helpers/themes.dart @@ -2,6 +2,7 @@ import 'package:adaptive_theme/adaptive_theme.dart'; import 'package:bluebubbles/helpers/hex_color.dart'; import 'package:bluebubbles/repository/models/theme_object.dart'; import 'package:flutter/material.dart'; +import 'package:collection/src/iterable_extensions.dart'; enum DarkThemes { OLED, @@ -14,15 +15,16 @@ enum LightThemes { class Themes { static List get themes => [ - ThemeObject.fromData(oledDarkTheme, "OLED Dark", isPreset: true), - ThemeObject.fromData(whiteLightTheme, "Bright White", isPreset: true), - ThemeObject.fromData(nordDarkTheme, "Nord Theme", isPreset: true), + ThemeObject.fromData(oledDarkTheme, "OLED Dark"), + ThemeObject.fromData(whiteLightTheme, "Bright White"), + ThemeObject.fromData(nordDarkTheme, "Nord Theme"), + ThemeObject.fromData(whiteLightTheme, "Music Theme (Light)", gradientBg: true), + ThemeObject.fromData(oledDarkTheme, "Music Theme (Dark)", gradientBg: true), ]; } bool isEqual(ThemeData one, ThemeData two) { - return one.accentColor == two.accentColor - && one.backgroundColor == two.backgroundColor; + return one.accentColor == two.accentColor && one.backgroundColor == two.backgroundColor; } ThemeData oledDarkTheme = ThemeData( @@ -160,3 +162,33 @@ Future loadTheme(BuildContext? context, {ThemeObject? lightOverride, Theme dark: dark.themeData, ); } + +Future revertToPreviousDarkTheme() async { + List allThemes = await ThemeObject.getThemes(); + ThemeObject? previous = allThemes.firstWhereOrNull((e) => e.previousDarkTheme); + + if (previous == null) { + previous = Themes.themes.firstWhereOrNull((element) => element.name == "OLED Dark"); + } + + // Remove the previous flags + previous!.previousDarkTheme = false; + + // Save the theme and set it accordingly + return await previous.save(); +} + +Future revertToPreviousLightTheme() async { + List allThemes = await ThemeObject.getThemes(); + ThemeObject? previous = allThemes.firstWhereOrNull((e) => e.previousDarkTheme); + + if (previous == null) { + previous = Themes.themes.firstWhereOrNull((element) => element.name == "Bright White"); + } + + // Remove the previous flags + previous!.previousDarkTheme = false; + + // Save the theme and set it accordingly + return await previous.save(); +} diff --git a/lib/helpers/ui_helpers.dart b/lib/helpers/ui_helpers.dart index b3a409f67..a64507c86 100644 --- a/lib/helpers/ui_helpers.dart +++ b/lib/helpers/ui_helpers.dart @@ -12,14 +12,17 @@ Widget buildBackButton(BuildContext context, padding: padding, width: 25, child: IconButton( - iconSize: iconSize ?? 24, + iconSize: iconSize ?? (SettingsManager().settings.skin.value == Skins.iOS ? 30 : 24), icon: skin != null - ? Icon(skin == Skins.iOS ? Icons.arrow_back_ios : Icons.arrow_back, color: Theme.of(context).primaryColor) - : Obx(() => Icon(SettingsManager().settings.skin.value == Skins.iOS ? Icons.arrow_back_ios : Icons.arrow_back, + ? Icon(skin == Skins.iOS ? CupertinoIcons.back : Icons.arrow_back, color: Theme.of(context).primaryColor) + : Obx(() => Icon(SettingsManager().settings.skin.value == Skins.iOS ? CupertinoIcons.back : Icons.arrow_back, color: Theme.of(context).primaryColor)), onPressed: () { callback?.call(); - Get.back(closeOverlays: true); + while (Get.isOverlaysOpen) { + Get.back(); + } + Navigator.of(context).pop(); }, ), ); diff --git a/lib/helpers/utils.dart b/lib/helpers/utils.dart index 9fa6df9ed..a958968a6 100644 --- a/lib/helpers/utils.dart +++ b/lib/helpers/utils.dart @@ -9,6 +9,7 @@ import 'package:bluebubbles/helpers/attachment_helper.dart'; import 'package:bluebubbles/helpers/constants.dart'; import 'package:bluebubbles/helpers/country_codes.dart'; import 'package:bluebubbles/helpers/hex_color.dart'; +import 'package:bluebubbles/helpers/logger.dart'; import 'package:bluebubbles/layouts/conversation_view/conversation_view_mixin.dart'; import 'package:bluebubbles/layouts/widgets/message_widget/message_content/media_players/video_widget.dart'; import 'package:bluebubbles/managers/contact_manager.dart'; @@ -22,7 +23,7 @@ import 'package:collection/collection.dart'; import 'package:contacts_service/contacts_service.dart'; import 'package:convert/convert.dart'; import 'package:device_info/device_info.dart'; -import 'package:flutter/foundation.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_libphonenumber/flutter_libphonenumber.dart'; @@ -148,8 +149,8 @@ bool sameAddress(List options, String? compared) { String getInitials(Contact contact) { // Set default initials - String initials = (contact.givenName!.isNotEmpty == true ? contact.givenName![0] : "") + - (contact.familyName!.isNotEmpty == true ? contact.familyName![0] : ""); + String initials = ((contact.givenName ?? "").isNotEmpty == true ? contact.givenName![0] : "") + + ((contact.familyName ?? "").isNotEmpty == true ? contact.familyName![0] : ""); // If the initials are empty, get them from the display name if (initials.trim().isEmpty) { @@ -238,7 +239,6 @@ String buildDate(DateTime? dateTime) { } String buildTime(DateTime? dateTime) { - SettingsManager().settings.use24HrFormat.value = MediaQuery.of(Get.context!).alwaysUse24HourFormat; if (dateTime == null || dateTime.millisecondsSinceEpoch == 0) return ""; String time = SettingsManager().settings.use24HrFormat.value ? intl.DateFormat.Hm().format(dateTime) @@ -350,6 +350,8 @@ Future getGroupEventText(Message message) async { text = "$handle left the conversation"; } else if (message.itemType == 2 && message.groupTitle != null) { text = "$handle named the conversation \"${message.groupTitle}\""; + } else if (message.itemType == 6) { + text = "$handle started a FaceTime call"; } return text; @@ -455,8 +457,8 @@ Size getGifDimensions(Uint8List bytes) { hexString += hex.encode(bytes.sublist(8, 9)); int height = int.parse(hexString, radix: 16); - debugPrint("GIF width: $width"); - debugPrint("GIF height: $height"); + Logger.debug("GIF width: $width"); + Logger.debug("GIF height: $height"); Size size = new Size(width.toDouble(), height.toDouble()); return size; } @@ -509,8 +511,8 @@ Future getDeviceName() async { deviceName = items.join("_").toLowerCase(); } } catch (ex) { - debugPrint("Failed to get device name! Defaulting to 'android-client'"); - debugPrint(ex.toString()); + Logger.error("Failed to get device name! Defaulting to 'android-client'"); + Logger.error(ex.toString()); } // Fallback for if it happens to be empty or null, somehow... idk diff --git a/lib/layouts/conversation_details/attachment_details_card.dart b/lib/layouts/conversation_details/attachment_details_card.dart index c02cc3173..ee7db9aeb 100644 --- a/lib/layouts/conversation_details/attachment_details_card.dart +++ b/lib/layouts/conversation_details/attachment_details_card.dart @@ -2,6 +2,8 @@ import 'dart:async'; import 'dart:io'; import 'dart:typed_data'; +import 'package:bluebubbles/helpers/constants.dart'; +import 'package:bluebubbles/helpers/navigator.dart'; import 'package:get/get.dart'; import 'package:bluebubbles/helpers/attachment_downloader.dart'; import 'package:bluebubbles/helpers/attachment_helper.dart'; @@ -12,11 +14,9 @@ import 'package:bluebubbles/layouts/widgets/theme_switcher/theme_switcher.dart'; import 'package:bluebubbles/managers/current_chat.dart'; import 'package:bluebubbles/managers/settings_manager.dart'; import 'package:bluebubbles/repository/models/attachment.dart'; -import 'package:bluebubbles/socket_manager.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:path/path.dart'; -import 'package:tuple/tuple.dart'; class AttachmentDetailsCard extends StatefulWidget { AttachmentDetailsCard({Key? key, required this.attachment, required this.allAttachments}) : super(key: key); @@ -81,7 +81,7 @@ class _AttachmentDetailsCardState extends State { widget.attachment.getFriendlySize(), style: Theme.of(context).textTheme.bodyText1, ), - Icon(Icons.cloud_download, size: 28.0), + Icon(SettingsManager().settings.skin.value == Skins.iOS ? CupertinoIcons.cloud_download : Icons.cloud_download, size: 28.0), (widget.attachment.mimeType != null) ? Text( basename(this.attachmentFile.path), @@ -94,7 +94,7 @@ class _AttachmentDetailsCardState extends State { } Widget buildPreview(BuildContext context) => SizedBox( - width: context.width / 2, + width: CustomNavigator.width(context) / 2, child: _buildPreview(this.attachmentFile, context), ); @@ -195,8 +195,8 @@ class _AttachmentDetailsCardState extends State { alignment: Alignment.center, ) : Container()), - width: context.width / 2, - height: context.width / 2, + width: CustomNavigator.width(context) / 2, + height: CustomNavigator.width(context) / 2, ), Material( color: Colors.transparent, @@ -234,8 +234,8 @@ class _AttachmentDetailsCardState extends State { ) : Container(), ), - width: context.width / 2, - height: context.width / 2, + width: CustomNavigator.width(context) / 2, + height: CustomNavigator.width(context) / 2, ), Material( color: Colors.transparent, @@ -255,7 +255,7 @@ class _AttachmentDetailsCardState extends State { Align( alignment: Alignment.bottomRight, child: Icon( - Icons.play_arrow, + SettingsManager().settings.skin.value == Skins.iOS ? CupertinoIcons.play : Icons.play_arrow, color: Colors.white, ), ), diff --git a/lib/layouts/conversation_details/contact_tile.dart b/lib/layouts/conversation_details/contact_tile.dart index a565a2521..225f6a73a 100644 --- a/lib/layouts/conversation_details/contact_tile.dart +++ b/lib/layouts/conversation_details/contact_tile.dart @@ -1,7 +1,10 @@ import 'dart:ui'; +import 'package:bluebubbles/helpers/constants.dart'; +import 'package:bluebubbles/helpers/logger.dart'; import 'package:bluebubbles/managers/method_channel_interface.dart'; import 'package:contacts_service/contacts_service.dart'; +import 'package:flutter/cupertino.dart'; import 'package:get/get.dart'; import 'package:bluebubbles/blocs/chat_bloc.dart'; import 'package:bluebubbles/helpers/redacted_helper.dart'; @@ -184,7 +187,7 @@ class _ContactTileState extends State { onPressed: () { startEmail(widget.handle.address); }, - child: Icon(Icons.email, color: Theme.of(context).primaryColor, size: 20), + child: Icon(SettingsManager().settings.skin.value == Skins.iOS ? CupertinoIcons.mail : Icons.email, color: Theme.of(context).primaryColor, size: 20), ), ), ((contact == null && !isEmail) || (contact?.phones?.length ?? 0) > 0) @@ -197,7 +200,7 @@ class _ContactTileState extends State { ), onLongPress: () => onPressContactTrailing(longPressed: true), onPressed: () => onPressContactTrailing(), - child: Icon(Icons.call, color: Theme.of(context).primaryColor, size: 20), + child: Icon(SettingsManager().settings.skin.value == Skins.iOS ? CupertinoIcons.phone : Icons.call, color: Theme.of(context).primaryColor, size: 20), ), ) : Container() @@ -293,7 +296,7 @@ class _ContactTileState extends State { IconSlideAction( caption: 'Remove', color: Colors.red, - icon: Icons.delete, + icon: SettingsManager().settings.skin.value == Skins.iOS ? CupertinoIcons.trash : Icons.delete, onTap: () async { showDialog( context: context, @@ -312,13 +315,15 @@ class _ContactTileState extends State { params["identifier"] = widget.chat.guid; params["address"] = widget.handle.address; SocketManager().sendMessage("remove-participant", params, (response) async { - debugPrint("removed participant participant " + response.toString()); + Logger.info("Removed participant participant " + response.toString()); + if (response["status"] == 200) { Chat updatedChat = Chat.fromMap(response["data"]); await updatedChat.save(); await ChatBloc().updateChatPosition(updatedChat); Chat chatWithParticipants = await updatedChat.getParticipants(); - debugPrint("updating chat with ${chatWithParticipants.participants.length} participants"); + + Logger.info("Updating chat with ${chatWithParticipants.participants.length} participants"); widget.updateChat(chatWithParticipants); Navigator.of(context).pop(); } diff --git a/lib/layouts/conversation_details/conversation_details.dart b/lib/layouts/conversation_details/conversation_details.dart index 9bbfb10c6..e0d0d8340 100644 --- a/lib/layouts/conversation_details/conversation_details.dart +++ b/lib/layouts/conversation_details/conversation_details.dart @@ -4,6 +4,7 @@ import 'dart:ui'; import 'package:bluebubbles/blocs/chat_bloc.dart'; import 'package:bluebubbles/blocs/message_bloc.dart'; import 'package:bluebubbles/helpers/constants.dart'; +import 'package:bluebubbles/helpers/logger.dart'; import 'package:bluebubbles/helpers/message_helper.dart'; import 'package:bluebubbles/helpers/ui_helpers.dart'; import 'package:bluebubbles/layouts/conversation_details/attachment_details_card.dart'; @@ -80,7 +81,7 @@ class _ConversationDetailsState extends State { await chat.getParticipants(); readOnly = !(chat.participants.length > 1); - debugPrint("updated readonly $readOnly"); + Logger.info("Updated readonly $readOnly"); if (this.mounted) setState(() {}); } @@ -211,7 +212,7 @@ class _ConversationDetailsState extends State { ); }, child: Icon( - Icons.info_outline, + SettingsManager().settings.skin.value == Skins.iOS ? CupertinoIcons.info : Icons.info_outline, color: Theme.of(context).primaryColor, ))), if (chat.displayName!.isEmpty) @@ -284,7 +285,7 @@ class _ConversationDetailsState extends State { trailing: Padding( padding: EdgeInsets.only(right: 15), child: Icon( - Icons.more_horiz, + SettingsManager().settings.skin.value == Skins.iOS ? CupertinoIcons.ellipsis : Icons.more_horiz, color: Theme.of(context).primaryColor, ), ), @@ -318,15 +319,13 @@ class _ConversationDetailsState extends State { return AlertDialog( backgroundColor: Theme.of(context).accentColor, title: new Text("Custom Avatar", - style: - TextStyle(color: Theme.of(context).textTheme.bodyText1!.color)), + style: TextStyle(color: Theme.of(context).textTheme.bodyText1!.color)), content: Column( mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - "You have already set a custom avatar for this chat. What would you like to do?", + Text("You have already set a custom avatar for this chat. What would you like to do?", style: Theme.of(context).textTheme.bodyText1), ], ), @@ -380,7 +379,7 @@ class _ConversationDetailsState extends State { trailing: Padding( padding: EdgeInsets.only(right: 15), child: Icon( - Icons.person, + SettingsManager().settings.skin.value == Skins.iOS ? CupertinoIcons.person : Icons.person, color: Theme.of(context).primaryColor, ), ), @@ -408,7 +407,7 @@ class _ConversationDetailsState extends State { trailing: Padding( padding: EdgeInsets.only(right: 15), child: Icon( - Icons.file_download, + SettingsManager().settings.skin.value == Skins.iOS ? CupertinoIcons.cloud_download : Icons.file_download, color: Theme.of(context).primaryColor, ), ), @@ -433,7 +432,7 @@ class _ConversationDetailsState extends State { trailing: Padding( padding: EdgeInsets.only(right: 15), child: Icon( - Icons.replay, + SettingsManager().settings.skin.value == Skins.iOS ? CupertinoIcons.arrow_counterclockwise : Icons.replay, color: Theme.of(context).primaryColor, ), ), @@ -464,7 +463,7 @@ class _ConversationDetailsState extends State { color: Theme.of(context).primaryColor, )), trailing: Switch( - value: widget.chat.isMuted!, + value: widget.chat.muteType == "mute", activeColor: Theme.of(context).primaryColor, activeTrackColor: Theme.of(context).primaryColor.withAlpha(200), inactiveTrackColor: Theme.of(context).accentColor.withOpacity(0.6), @@ -536,11 +535,11 @@ class _ConversationDetailsState extends State { ) : (isCleared) ? Icon( - Icons.done, + SettingsManager().settings.skin.value == Skins.iOS ? CupertinoIcons.checkmark : Icons.done, color: Theme.of(context).primaryColor, ) : Icon( - Icons.delete_forever, + SettingsManager().settings.skin.value == Skins.iOS ? CupertinoIcons.trash : Icons.delete_forever, color: Theme.of(context).primaryColor, ), ), diff --git a/lib/layouts/conversation_list/conversation_list.dart b/lib/layouts/conversation_list/conversation_list.dart index 3b97a5e4a..9af1ff0cd 100644 --- a/lib/layouts/conversation_list/conversation_list.dart +++ b/lib/layouts/conversation_list/conversation_list.dart @@ -1,39 +1,40 @@ import 'dart:async'; -import 'dart:math'; +import 'dart:io'; import 'dart:ui'; import 'package:bluebubbles/blocs/chat_bloc.dart'; import 'package:bluebubbles/blocs/setup_bloc.dart'; import 'package:bluebubbles/helpers/constants.dart'; +import 'package:bluebubbles/helpers/logger.dart'; +import 'package:bluebubbles/helpers/navigator.dart'; import 'package:bluebubbles/helpers/ui_helpers.dart'; import 'package:bluebubbles/helpers/utils.dart'; -import 'package:bluebubbles/layouts/conversation_list/conversation_tile.dart'; -import 'package:bluebubbles/layouts/conversation_list/pinned_conversation_tile.dart'; +import 'package:bluebubbles/layouts/conversation_list/cupertino_conversation_list.dart'; +import 'package:bluebubbles/layouts/conversation_list/material_conversation_list.dart'; +import 'package:bluebubbles/layouts/conversation_list/samsung_conversation_list.dart'; import 'package:bluebubbles/layouts/conversation_view/conversation_view.dart'; -import 'package:bluebubbles/layouts/search/search_view.dart'; import 'package:bluebubbles/layouts/settings/settings_panel.dart'; import 'package:bluebubbles/layouts/widgets/theme_switcher/theme_switcher.dart'; import 'package:bluebubbles/managers/event_dispatcher.dart'; +import 'package:bluebubbles/managers/method_channel_interface.dart'; import 'package:bluebubbles/managers/settings_manager.dart'; -import 'package:bluebubbles/managers/theme_manager.dart'; -import 'package:bluebubbles/repository/models/chat.dart'; import 'package:bluebubbles/socket_manager.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:get/get.dart'; -import 'package:smooth_page_indicator/smooth_page_indicator.dart'; class ConversationList extends StatefulWidget { - ConversationList({Key? key, required this.showArchivedChats}) : super(key: key); + ConversationList({Key? key, required this.showArchivedChats, required this.showUnknownSenders}) : super(key: key); final bool showArchivedChats; + final bool showUnknownSenders; @override - _ConversationListState createState() => _ConversationListState(); + ConversationListState createState() => ConversationListState(); } -class _ConversationListState extends State { +class ConversationListState extends State { Color? currentHeaderColor; bool hasPinnedChats = false; @@ -50,7 +51,7 @@ class _ConversationListState extends State { } SystemChannels.textInput.invokeMethod('TextInput.hide').catchError((e) { - debugPrint("Error caught while hiding keyboard: ${e.toString()}"); + Logger.error("Error caught while hiding keyboard: ${e.toString()}"); }); } @@ -65,7 +66,9 @@ class _ConversationListState extends State { @override void initState() { super.initState(); - ChatBloc().refreshChats(); + if (!widget.showUnknownSenders) { + ChatBloc().refreshChats(); + } scrollController = ScrollController()..addListener(scrollListener); // Listen for any incoming events @@ -98,7 +101,7 @@ class _ConversationListState extends State { TextStyle? style = context.textTheme.headline1; if (size != null) style = style!.copyWith(fontSize: size); - return [Text(widget.showArchivedChats ? "Archive" : "Messages", style: style), Container(width: 10)]; + return [Text(widget.showArchivedChats ? "Archive" : widget.showUnknownSenders ? "Unknown Senders" : "Messages", style: style), Container(width: 10)]; } Widget getSyncIndicatorWidget() { @@ -109,24 +112,22 @@ class _ConversationListState extends State { }); } - void openNewChatCreator() async { + void openNewChatCreator({List? existing}) async { bool shouldShowSnackbar = (await SettingsManager().getMacOSVersion())! >= 11; - Navigator.of(context).push( - CupertinoPageRoute( - builder: (BuildContext context) { - return ConversationView( - isCreator: true, - showSnackbar: shouldShowSnackbar, - ); - }, + CustomNavigator.pushAndRemoveUntil( + context, + ConversationView( + isCreator: true, + showSnackbar: shouldShowSnackbar, + existingAttachments: existing ?? [], ), + (route) => route.isFirst, ); } void sortChats() { ChatBloc().chats.sort((a, b) { - if (a.pinIndex.value != null && b.pinIndex.value != null) - return a.pinIndex.value!.compareTo(b.pinIndex.value!); + if (a.pinIndex.value != null && b.pinIndex.value != null) return a.pinIndex.value!.compareTo(b.pinIndex.value!); if (b.pinIndex.value != null) return 1; if (a.pinIndex.value != null) return -1; if (!a.isPinned! && b.isPinned!) return 1; @@ -138,19 +139,19 @@ class _ConversationListState extends State { }); } - Widget buildSettingsButton() => !widget.showArchivedChats + Widget buildSettingsButton() => !widget.showArchivedChats && !widget.showUnknownSenders ? PopupMenuButton( color: context.theme.accentColor, onSelected: (dynamic value) { if (value == 0) { ChatBloc().markAllAsRead(); } else if (value == 1) { - Navigator.of(context).push( - ThemeSwitcher.buildPageRoute( - builder: (context) => ConversationList( - showArchivedChats: true, - ), - ), + CustomNavigator.pushLeft( + context, + ConversationList( + showArchivedChats: true, + showUnknownSenders: false, + ) ); } else if (value == 2) { Navigator.of(context).push( @@ -160,6 +161,14 @@ class _ConversationListState extends State { }, ), ); + } else if (value == 3) { + CustomNavigator.pushLeft( + context, + ConversationList( + showArchivedChats: false, + showUnknownSenders: true, + ) + ); } }, itemBuilder: (context) { @@ -178,6 +187,14 @@ class _ConversationListState extends State { style: context.textTheme.bodyText1, ), ), + if (SettingsManager().settings.filterUnknownSenders.value) + PopupMenuItem( + value: 3, + child: Text( + 'Unknown Senders', + style: context.textTheme.bodyText1, + ), + ), PopupMenuItem( value: 2, child: Text( @@ -215,1556 +232,66 @@ class _ConversationListState extends State { ) : Container(); - FloatingActionButton buildFloatingActionButton() { - return FloatingActionButton( - backgroundColor: context.theme.primaryColor, - child: Icon(Icons.message, color: Colors.white, size: 25), - onPressed: openNewChatCreator); - } - - List getConnectionIndicatorWidgets() { - if (!SettingsManager().settings.showConnectionIndicator.value) return []; - - return [Obx(() => getIndicatorIcon(SocketManager().state.value, size: 12)), Container(width: 10.0)]; - } - - @override - Widget build(BuildContext context) { - return ThemeSwitcher( - iOSSkin: _Cupertino(parent: this), - materialSkin: _Material(parent: this), - samsungSkin: _Samsung(parent: this), - ); - } -} - -class _Cupertino extends StatelessWidget { - const _Cupertino({Key? key, required this.parent}) : super(key: key); - - final _ConversationListState parent; - - @override - Widget build(BuildContext context) { - bool showArchived = parent.widget.showArchivedChats; - Brightness brightness = ThemeData.estimateBrightnessForColor(context.theme.backgroundColor); - return AnnotatedRegion( - value: SystemUiOverlayStyle( - systemNavigationBarColor: context.theme.backgroundColor, // navigation bar color - systemNavigationBarIconBrightness: - context.theme.backgroundColor.computeLuminance() > 0.5 ? Brightness.dark : Brightness.light, - statusBarColor: Colors.transparent, // status bar color - ), - child: Obx( - () => Scaffold( - appBar: PreferredSize( - preferredSize: Size( - context.width, - SettingsManager().settings.reducedForehead.value ? 10 : 40, + Column buildFloatingActionButton() { + return Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + if (SettingsManager().settings.cameraFAB.value) + ConstrainedBox( + constraints: BoxConstraints( + maxWidth: 45, + maxHeight: 45, ), - child: ClipRRect( - child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 15, sigmaY: 15), - child: StreamBuilder( - stream: parent.headerColorStream.stream, - builder: (context, snapshot) { - return AnimatedCrossFade( - crossFadeState: - parent.theme == Colors.transparent ? CrossFadeState.showFirst : CrossFadeState.showSecond, - duration: Duration(milliseconds: 250), - secondChild: AppBar( - iconTheme: IconThemeData(color: context.theme.primaryColor), - elevation: 0, - backgroundColor: parent.theme, - centerTitle: true, - brightness: brightness, - title: Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Text( - showArchived ? "Archive" : "Messages", - style: context.textTheme.bodyText1, - ), - ], - ), - ), - firstChild: AppBar( - leading: new Container(), - elevation: 0, - brightness: brightness, - backgroundColor: context.theme.backgroundColor, - ), - ); - }, - ), - ), - ), - ), - backgroundColor: context.theme.backgroundColor, - extendBodyBehindAppBar: true, - body: CustomScrollView( - controller: parent.scrollController, - physics: ThemeManager().scrollPhysics, - slivers: [ - SliverAppBar( - leading: ((SettingsManager().settings.skin.value == Skins.iOS && showArchived) || - (SettingsManager().settings.skin.value == Skins.Material || - SettingsManager().settings.skin.value == Skins.Samsung) && - !showArchived) - ? IconButton( - icon: Icon( - (SettingsManager().settings.skin.value == Skins.iOS && showArchived) - ? Icons.arrow_back_ios - : Icons.arrow_back, - color: context.theme.primaryColor), - onPressed: () { - Navigator.of(context).pop(); - }, - ) - : new Container(), - stretch: true, - expandedHeight: (!showArchived) ? 80 : 50, - backgroundColor: Colors.transparent, - pinned: false, - flexibleSpace: FlexibleSpaceBar( - stretchModes: [StretchMode.zoomBackground], - background: Stack( - fit: StackFit.expand, - ), - centerTitle: true, - title: Column( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Container(height: 20), - Container( - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Container(width: (!showArchived) ? 20 : 50), - Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - ...parent.getHeaderTextWidgets(), - ...parent.getConnectionIndicatorWidgets(), - parent.getSyncIndicatorWidget(), - ], - ), - Spacer( - flex: 25, - ), - if (!showArchived) - ClipOval( - child: Material( - color: context.theme.accentColor, // button color - child: InkWell( - child: SizedBox( - width: 20, - height: 20, - child: Icon(Icons.search, color: context.theme.primaryColor, size: 12)), - onTap: () async { - Navigator.of(context).push( - CupertinoPageRoute( - builder: (context) => SearchView(), - ), - ); - }, - ), - ), - ), - if (!showArchived) Container(width: 10.0), - if (SettingsManager().settings.moveChatCreatorToHeader.value && !showArchived) - ClipOval( - child: Material( - color: context.theme.accentColor, // button color - child: InkWell( - child: SizedBox( - width: 20, - height: 20, - child: Icon(Icons.create, color: context.theme.primaryColor, size: 12), - ), - onTap: this.parent.openNewChatCreator, - ), - ), - ), - if (SettingsManager().settings.moveChatCreatorToHeader.value) Container(width: 10.0), - parent.buildSettingsButton(), - Spacer( - flex: 3, - ), - ], - ), - ), - ], - ), - ), + child: FloatingActionButton( + child: Icon( + SettingsManager().settings.skin.value == Skins.iOS ? CupertinoIcons.camera : Icons.photo_camera, + size: 20, ), - // SliverToBoxAdapter( - // child: Container( - // padding: EdgeInsets.symmetric(horizontal: 30, vertical: 5), - // child: GestureDetector( - // onTap: () { - // Navigator.of(context).push( - // MaterialPageRoute( - // builder: (context) => SearchView(), - // ), - // ); - // }, - // child: AbsorbPointer( - // child: SearchTextBox(), - // ), - // ), - // ), - // ), - Obx(() { - if (ChatBloc().chats.archivedHelper(showArchived).bigPinHelper(true).isEmpty) { - return SliverToBoxAdapter(child: Container()); + onPressed: () async { + String appDocPath = SettingsManager().appDocDir.path; + String ext = ".png"; + File file = new File("$appDocPath/attachments/" + randomString(16) + ext); + await file.create(recursive: true); + + // Take the picture after opening the camera + await MethodChannelInterface().invokeMethod("open-camera", {"path": file.path, "type": "camera"}); + + // If we don't get data back, return outta here + if (!file.existsSync()) return; + if (file.statSync().size == 0) { + file.deleteSync(); + return; } - ChatBloc().chats.archivedHelper(showArchived).sort(Chat.sort); - - int rowCount = context.mediaQuery.orientation == Orientation.portrait - ? SettingsManager().settings.pinRowsPortrait.value - : SettingsManager().settings.pinRowsLandscape.value; - int colCount = SettingsManager().settings.pinColumnsPortrait.value; - if (context.mediaQuery.orientation != Orientation.portrait) { - colCount = (colCount / context.mediaQuerySize.height * context.mediaQuerySize.width).floor(); - } - int pinCount = ChatBloc().chats.archivedHelper(showArchived).bigPinHelper(true).length; - int usedRowCount = min((pinCount / colCount).ceil(), rowCount); - int maxOnPage = rowCount * colCount; - PageController _controller = PageController(); - int _pageCount = (pinCount / maxOnPage).ceil(); - int _filledPageCount = (pinCount / maxOnPage).floor(); - - return SliverPadding( - padding: EdgeInsets.only( - top: 10, - bottom: 10, - ), - sliver: SliverToBoxAdapter( - child: ConstrainedBox( - constraints: BoxConstraints( - maxHeight: (context.mediaQuerySize.width + 30) / colCount * usedRowCount, - ), - child: Stack( - alignment: Alignment.bottomCenter, - children: [ - PageView.builder( - physics: BouncingScrollPhysics(parent: AlwaysScrollableScrollPhysics()), - scrollDirection: Axis.horizontal, - controller: _controller, - itemBuilder: (context, index) { - return Wrap( - crossAxisAlignment: WrapCrossAlignment.center, - alignment: _pageCount > 1 ? WrapAlignment.start : WrapAlignment.center, - children: List.generate( - index < _filledPageCount - ? maxOnPage - : ChatBloc().chats.archivedHelper(showArchived).bigPinHelper(true).length % - maxOnPage, - (_index) { - return PinnedConversationTile( - key: Key(ChatBloc() - .chats - .archivedHelper(showArchived) - .bigPinHelper(true)[index * maxOnPage + _index] - .guid - .toString()), - chat: ChatBloc() - .chats - .archivedHelper(showArchived) - .bigPinHelper(true)[index * maxOnPage + _index], - ); - }, - ), - ); - }, - itemCount: _pageCount, - ), - if (_pageCount > 1) - SmoothPageIndicator( - controller: _controller, - count: _pageCount, - effect: ScaleEffect( - dotHeight: 5.0, - dotWidth: 5.0, - spacing: 5.0, - radius: 5.0, - scale: 1.5, - activeDotColor: context.theme.primaryColor, - ), - ), - ], - ), - ), - ), - ); - }), - Obx(() { - ChatBloc().chats.archivedHelper(showArchived).sort(Chat.sort); - if (!ChatBloc().hasChats.value) { - return SliverToBoxAdapter( - child: Center( - child: Container( - padding: EdgeInsets.only(top: 50.0), - child: Column( - children: [ - Padding( - padding: const EdgeInsets.all(8.0), - child: Text( - "Loading chats...", - style: Theme.of(context).textTheme.subtitle1, - ), - ), - buildProgressIndicator(context, size: 15), - ], - ), - ), - ), - ); - } - if (!ChatBloc().hasChats.value) { - return SliverToBoxAdapter( - child: Center( - child: Container( - padding: EdgeInsets.only(top: 50.0), - child: Text( - showArchived ? "You have no archived chats :(" : "You have no chats :(", - style: Theme.of(context).textTheme.subtitle1, - ), - ), - ), - ); - } - - return SliverList( - delegate: SliverChildBuilderDelegate( - (context, index) { - return ConversationTile( - key: Key( - ChatBloc().chats.archivedHelper(showArchived).bigPinHelper(false)[index].guid.toString()), - chat: ChatBloc().chats.archivedHelper(showArchived).bigPinHelper(false)[index], - ); - }, - childCount: ChatBloc().chats.archivedHelper(showArchived).bigPinHelper(false).length, - ), - ); - }), - ], - ), - floatingActionButton: - !SettingsManager().settings.moveChatCreatorToHeader.value ? parent.buildFloatingActionButton() : null, - ), - ), - ); - } -} - -class _Material extends StatefulWidget { - _Material({Key? key, required this.parent}) : super(key: key); - - final _ConversationListState parent; - - @override - __MaterialState createState() => __MaterialState(); -} - -class __MaterialState extends State<_Material> { - List selected = []; - - bool hasPinnedChat() { - for (var i = 0; i < ChatBloc().chats.archivedHelper(widget.parent.widget.showArchivedChats).length; i++) { - if (ChatBloc().chats.archivedHelper(widget.parent.widget.showArchivedChats)[i].isPinned!) { - widget.parent.hasPinnedChats = true; - return true; - } else { - return false; - } - } - return false; - } - - bool hasNormalChats() { - int counter = 0; - for (var i = 0; i < ChatBloc().chats.archivedHelper(widget.parent.widget.showArchivedChats).length; i++) { - if (ChatBloc().chats.archivedHelper(widget.parent.widget.showArchivedChats)[i].isPinned!) { - counter++; - } else {} - } - if (counter == ChatBloc().chats.archivedHelper(widget.parent.widget.showArchivedChats).length) { - return false; - } else { - return true; - } - } - - Widget slideLeftBackground(Chat chat) { - return Container( - color: SettingsManager().settings.materialLeftAction.value == MaterialSwipeAction.pin - ? Colors.yellow[800] - : SettingsManager().settings.materialLeftAction.value == MaterialSwipeAction.alerts - ? Colors.purple - : SettingsManager().settings.materialLeftAction.value == MaterialSwipeAction.delete - ? Colors.red - : SettingsManager().settings.materialLeftAction.value == MaterialSwipeAction.mark_read - ? Colors.blue - : Colors.red, - child: Align( - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Icon( - SettingsManager().settings.materialLeftAction.value == MaterialSwipeAction.pin - ? (chat.isPinned! ? Icons.star_outline : Icons.star) - : SettingsManager().settings.materialLeftAction.value == MaterialSwipeAction.alerts - ? (chat.isMuted! ? Icons.notifications_active : Icons.notifications_off) - : SettingsManager().settings.materialLeftAction.value == MaterialSwipeAction.delete - ? Icons.delete_forever - : SettingsManager().settings.materialLeftAction.value == MaterialSwipeAction.mark_read - ? (chat.hasUnreadMessage! ? Icons.mark_chat_read : Icons.mark_chat_unread) - : (chat.isArchived! ? Icons.unarchive : Icons.archive), - color: Colors.white, - ), - Text( - SettingsManager().settings.materialLeftAction.value == MaterialSwipeAction.pin - ? (chat.isPinned! ? " Unpin" : " Pin") - : SettingsManager().settings.materialLeftAction.value == MaterialSwipeAction.alerts - ? (chat.isMuted! ? ' Show Alerts' : ' Hide Alerts') - : SettingsManager().settings.materialLeftAction.value == MaterialSwipeAction.delete - ? " Delete" - : SettingsManager().settings.materialLeftAction.value == MaterialSwipeAction.mark_read - ? (chat.hasUnreadMessage! ? ' Mark Read' : ' Mark Unread') - : (chat.isArchived! ? ' UnArchive' : ' Archive'), - style: TextStyle( - color: Colors.white, - fontWeight: FontWeight.w700, - ), - textAlign: TextAlign.right, - ), - SizedBox( - width: 20, - ), - ], - ), - alignment: Alignment.centerRight, - ), - ); - } - - Widget slideRightBackground(Chat chat) { - return Container( - color: SettingsManager().settings.materialRightAction.value == MaterialSwipeAction.pin - ? Colors.yellow[800] - : SettingsManager().settings.materialRightAction.value == MaterialSwipeAction.alerts - ? Colors.purple - : SettingsManager().settings.materialRightAction.value == MaterialSwipeAction.delete - ? Colors.red - : SettingsManager().settings.materialRightAction.value == MaterialSwipeAction.mark_read - ? Colors.blue - : Colors.red, - child: Align( - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - SizedBox( - width: 20, - ), - Icon( - SettingsManager().settings.materialRightAction.value == MaterialSwipeAction.pin - ? (chat.isPinned! ? Icons.star_outline : Icons.star) - : SettingsManager().settings.materialRightAction.value == MaterialSwipeAction.alerts - ? (chat.isMuted! ? Icons.notifications_active : Icons.notifications_off) - : SettingsManager().settings.materialRightAction.value == MaterialSwipeAction.delete - ? Icons.delete_forever - : SettingsManager().settings.materialRightAction.value == MaterialSwipeAction.mark_read - ? (chat.hasUnreadMessage! ? Icons.mark_chat_read : Icons.mark_chat_unread) - : (chat.isArchived! ? Icons.unarchive : Icons.archive), - color: Colors.white, - ), - Text( - SettingsManager().settings.materialRightAction.value == MaterialSwipeAction.pin - ? (chat.isPinned! ? " Unpin" : " Pin") - : SettingsManager().settings.materialRightAction.value == MaterialSwipeAction.alerts - ? (chat.isMuted! ? ' Show Alerts' : ' Hide Alerts') - : SettingsManager().settings.materialRightAction.value == MaterialSwipeAction.delete - ? " Delete" - : SettingsManager().settings.materialRightAction.value == MaterialSwipeAction.mark_read - ? (chat.hasUnreadMessage! ? ' Mark Read' : ' Mark Unread') - : (chat.isArchived! ? ' UnArchive' : ' Archive'), - style: TextStyle( - color: Colors.white, - fontWeight: FontWeight.w700, - ), - textAlign: TextAlign.left, - ), - ], - ), - alignment: Alignment.centerLeft, - ), - ); - } - @override - Widget build(BuildContext context) { - hasPinnedChat(); - bool showArchived = widget.parent.widget.showArchivedChats; - return AnnotatedRegion( - value: SystemUiOverlayStyle( - systemNavigationBarColor: context.theme.backgroundColor, // navigation bar color - systemNavigationBarIconBrightness: - context.theme.backgroundColor.computeLuminance() > 0.5 ? Brightness.dark : Brightness.light, - statusBarColor: Colors.transparent, // status bar color - ), - child: Obx( - () => WillPopScope( - onWillPop: () async { - if (selected.isNotEmpty) { - selected = []; - setState(() {}); - return false; - } - return true; - }, - child: Scaffold( - appBar: PreferredSize( - preferredSize: Size.fromHeight(60), - child: AnimatedSwitcher( - duration: Duration(milliseconds: 500), - child: selected.isEmpty - ? AppBar( - iconTheme: IconThemeData(color: context.theme.primaryColor), - brightness: ThemeData.estimateBrightnessForColor(context.theme.backgroundColor), - bottom: PreferredSize( - child: Container( - color: context.theme.dividerColor, - height: 0, - ), - preferredSize: Size.fromHeight(0.5), - ), - title: Row( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - ...widget.parent.getHeaderTextWidgets(size: 20), - ...widget.parent.getConnectionIndicatorWidgets(), - widget.parent.getSyncIndicatorWidget(), - ], - ), - actions: [ - (!showArchived) - ? GestureDetector( - onTap: () async { - Navigator.of(context).push( - CupertinoPageRoute( - builder: (context) => SearchView(), - ), - ); - }, - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Icon( - Icons.search, - color: context.textTheme.bodyText1!.color, - ), - ), - ) - : Container(), - (SettingsManager().settings.moveChatCreatorToHeader.value && !showArchived) - ? GestureDetector( - onTap: () { - Navigator.of(context).push( - ThemeSwitcher.buildPageRoute( - builder: (BuildContext context) { - return ConversationView( - isCreator: true, - ); - }, - ), - ); - }, - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Icon( - Icons.create, - color: context.textTheme.bodyText1!.color, - ), - ), - ) - : Container(), - Padding( - padding: EdgeInsets.only(right: 20), - child: Padding( - padding: EdgeInsets.symmetric(vertical: 15.5), - child: Container( - width: 40, - child: widget.parent.buildSettingsButton(), - ), - ), - ), - ], - backgroundColor: context.theme.backgroundColor, - ) - : Padding( - padding: const EdgeInsets.all(20.0), - child: Column( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - if (([0, selected.length]) - .contains(selected.where((element) => element.hasUnreadMessage!).length)) - GestureDetector( - onTap: () { - selected.forEach((element) async { - await element.toggleHasUnread(!element.hasUnreadMessage!); - }); - selected = []; - if (this.mounted) setState(() {}); - }, - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Icon( - selected[0].hasUnreadMessage! ? Icons.mark_chat_read : Icons.mark_chat_unread, - color: context.textTheme.bodyText1!.color, - ), - ), - ), - if (([0, selected.length]) - .contains(selected.where((element) => element.isMuted!).length)) - GestureDetector( - onTap: () { - selected.forEach((element) async { - await element.toggleMute(!element.isMuted!); - }); - selected = []; - if (this.mounted) setState(() {}); - }, - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Icon( - selected[0].isMuted! ? Icons.notifications_active : Icons.notifications_off, - color: context.textTheme.bodyText1!.color, - ), - ), - ), - if (([0, selected.length]) - .contains(selected.where((element) => element.isPinned!).length)) - GestureDetector( - onTap: () { - selected.forEach((element) { - element.togglePin(!element.isPinned!); - }); - selected = []; - if (this.mounted) setState(() {}); - }, - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Icon( - selected[0].isPinned! ? Icons.star_outline : Icons.star, - color: context.textTheme.bodyText1!.color, - ), - ), - ), - GestureDetector( - onTap: () { - selected.forEach((element) { - if (element.isArchived!) { - ChatBloc().unArchiveChat(element); - } else { - ChatBloc().archiveChat(element); - } - }); - selected = []; - if (this.mounted) setState(() {}); - }, - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Icon( - showArchived ? Icons.unarchive : Icons.archive, - color: context.textTheme.bodyText1!.color, - ), - ), - ), - if (selected[0].isArchived!) - GestureDetector( - onTap: () { - selected.forEach((element) { - ChatBloc().deleteChat(element); - Chat.deleteChat(element); - }); - selected = []; - if (this.mounted) setState(() {}); - }, - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Icon( - Icons.delete_forever, - color: context.textTheme.bodyText1!.color, - ), - ), - ), - ], - ), - ], - ), - ), - ), - ), - backgroundColor: context.theme.backgroundColor, - body: Obx( - () { - if (!ChatBloc().hasChats.value) { - return Center( - child: Container( - padding: EdgeInsets.only(top: 50.0), - child: Column( - children: [ - Padding( - padding: const EdgeInsets.all(8.0), - child: Text( - "Loading chats...", - style: Theme.of(context).textTheme.subtitle1, - ), - ), - buildProgressIndicator(context, size: 15), - ], - ), - ), - ); - } - if (ChatBloc().chats.archivedHelper(showArchived).isEmpty) { - return Center( - child: Container( - padding: EdgeInsets.only(top: 50.0), - child: Text( - "You have no archived chats :(", - style: context.textTheme.subtitle1, - ), - ), - ); - } - return ListView.builder( - physics: ThemeSwitcher.getScrollPhysics(), - itemBuilder: (context, index) { - return Obx(() { - if (SettingsManager().settings.swipableConversationTiles.value) { - return Dismissible( - background: - Obx(() => slideRightBackground(ChatBloc().chats.archivedHelper(showArchived)[index])), - secondaryBackground: - Obx(() => slideLeftBackground(ChatBloc().chats.archivedHelper(showArchived)[index])), - // Each Dismissible must contain a Key. Keys allow Flutter to - // uniquely identify widgets. - key: UniqueKey(), - // Provide a function that tells the app - // what to do after an item has been swiped away. - onDismissed: (direction) async { - if (direction == DismissDirection.endToStart) { - if (SettingsManager().settings.materialLeftAction.value == MaterialSwipeAction.pin) { - await ChatBloc() - .chats - .archivedHelper(showArchived)[index] - .togglePin(!ChatBloc().chats.archivedHelper(showArchived)[index].isPinned!); - EventDispatcher().emit("refresh", null); - if (this.mounted) setState(() {}); - } else if (SettingsManager().settings.materialLeftAction.value == - MaterialSwipeAction.alerts) { - await ChatBloc() - .chats - .archivedHelper(showArchived)[index] - .toggleMute(!ChatBloc().chats.archivedHelper(showArchived)[index].isMuted!); - if (this.mounted) setState(() {}); - } else if (SettingsManager().settings.materialLeftAction.value == - MaterialSwipeAction.delete) { - ChatBloc().deleteChat(ChatBloc().chats.archivedHelper(showArchived)[index]); - Chat.deleteChat(ChatBloc().chats.archivedHelper(showArchived)[index]); - } else if (SettingsManager().settings.materialLeftAction.value == - MaterialSwipeAction.mark_read) { - ChatBloc().toggleChatUnread(ChatBloc().chats.archivedHelper(showArchived)[index], - !ChatBloc().chats.archivedHelper(showArchived)[index].hasUnreadMessage!); - } else { - if (ChatBloc().chats.archivedHelper(showArchived)[index].isArchived!) { - ChatBloc().unArchiveChat(ChatBloc().chats.archivedHelper(showArchived)[index]); - } else { - ChatBloc().archiveChat(ChatBloc().chats.archivedHelper(showArchived)[index]); - } - } - } else { - if (SettingsManager().settings.materialRightAction.value == MaterialSwipeAction.pin) { - await ChatBloc() - .chats - .archivedHelper(showArchived)[index] - .togglePin(!ChatBloc().chats.archivedHelper(showArchived)[index].isPinned!); - EventDispatcher().emit("refresh", null); - if (this.mounted) setState(() {}); - } else if (SettingsManager().settings.materialRightAction.value == - MaterialSwipeAction.alerts) { - await ChatBloc() - .chats - .archivedHelper(showArchived)[index] - .toggleMute(!ChatBloc().chats.archivedHelper(showArchived)[index].isMuted!); - if (this.mounted) setState(() {}); - } else if (SettingsManager().settings.materialRightAction.value == - MaterialSwipeAction.delete) { - ChatBloc().deleteChat(ChatBloc().chats.archivedHelper(showArchived)[index]); - Chat.deleteChat(ChatBloc().chats.archivedHelper(showArchived)[index]); - } else if (SettingsManager().settings.materialRightAction.value == - MaterialSwipeAction.mark_read) { - ChatBloc().toggleChatUnread(ChatBloc().chats.archivedHelper(showArchived)[index], - !ChatBloc().chats.archivedHelper(showArchived)[index].hasUnreadMessage!); - } else { - if (ChatBloc().chats.archivedHelper(showArchived)[index].isArchived!) { - ChatBloc().unArchiveChat(ChatBloc().chats.archivedHelper(showArchived)[index]); - } else { - ChatBloc().archiveChat(ChatBloc().chats.archivedHelper(showArchived)[index]); - } - } - } - }, - child: (!showArchived && ChatBloc().chats.archivedHelper(showArchived)[index].isArchived!) - ? Container() - : (showArchived && !ChatBloc().chats.archivedHelper(showArchived)[index].isArchived!) - ? Container() - : ConversationTile( - key: UniqueKey(), - chat: ChatBloc().chats.archivedHelper(showArchived)[index], - inSelectMode: selected.isNotEmpty, - selected: selected, - onSelect: (bool selected) { - if (selected) { - this.selected.add(ChatBloc().chats.archivedHelper(showArchived)[index]); - setState(() {}); - } else { - this.selected.removeWhere((element) => - element.guid == - ChatBloc().chats.archivedHelper(showArchived)[index].guid); - setState(() {}); - } - }, - )); - } else { - return ConversationTile( - key: UniqueKey(), - chat: ChatBloc().chats.archivedHelper(showArchived)[index], - inSelectMode: selected.isNotEmpty, - selected: selected, - onSelect: (bool selected) { - if (selected) { - this.selected.add(ChatBloc().chats.archivedHelper(showArchived)[index]); - setState(() {}); - } else { - this.selected.removeWhere((element) => - element.guid == ChatBloc().chats.archivedHelper(showArchived)[index].guid); - setState(() {}); - } - }, - ); - } - }); - }, - itemCount: ChatBloc().chats.archivedHelper(showArchived).length, - ); + openNewChatCreator(existing: [file]); }, + heroTag: null, ), - floatingActionButton: selected.isEmpty && !SettingsManager().settings.moveChatCreatorToHeader.value - ? widget.parent.buildFloatingActionButton() - : null, ), - ), - ), + if (SettingsManager().settings.cameraFAB.value) + SizedBox( + height: 10, + ), + FloatingActionButton( + backgroundColor: context.theme.primaryColor, + child: Icon(SettingsManager().settings.skin.value == Skins.iOS ? CupertinoIcons.pencil : Icons.message, color: Colors.white, size: 25), + onPressed: openNewChatCreator), + ], ); } -} - -class _Samsung extends StatefulWidget { - _Samsung({Key? key, required this.parent}) : super(key: key); - - final _ConversationListState parent; - - @override - _SamsungState createState() => _SamsungState(); -} - -class _SamsungState extends State<_Samsung> { - List selected = []; - - bool hasPinnedChat() { - for (var i = 0; i < ChatBloc().chats.archivedHelper(widget.parent.widget.showArchivedChats).length; i++) { - if (ChatBloc().chats.archivedHelper(widget.parent.widget.showArchivedChats)[i].isPinned!) { - widget.parent.hasPinnedChats = true; - return true; - } else { - return false; - } - } - return false; - } - - bool hasNormalChats() { - int counter = 0; - for (var i = 0; i < ChatBloc().chats.archivedHelper(widget.parent.widget.showArchivedChats).length; i++) { - if (ChatBloc().chats.archivedHelper(widget.parent.widget.showArchivedChats)[i].isPinned!) { - counter++; - } else {} - } - if (counter == ChatBloc().chats.archivedHelper(widget.parent.widget.showArchivedChats).length) { - return false; - } else { - return true; - } - } - Widget slideLeftBackground(Chat chat) { - return Container( - color: SettingsManager().settings.materialLeftAction.value == MaterialSwipeAction.pin - ? Colors.yellow[800] - : SettingsManager().settings.materialLeftAction.value == MaterialSwipeAction.alerts - ? Colors.purple - : SettingsManager().settings.materialLeftAction.value == MaterialSwipeAction.delete - ? Colors.red - : SettingsManager().settings.materialLeftAction.value == MaterialSwipeAction.mark_read - ? Colors.blue - : Colors.red, - child: Align( - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Icon( - SettingsManager().settings.materialLeftAction.value == MaterialSwipeAction.pin - ? (chat.isPinned! ? Icons.star_outline : Icons.star) - : SettingsManager().settings.materialLeftAction.value == MaterialSwipeAction.alerts - ? (chat.isMuted! ? Icons.notifications_active : Icons.notifications_off) - : SettingsManager().settings.materialLeftAction.value == MaterialSwipeAction.delete - ? Icons.delete_forever - : SettingsManager().settings.materialLeftAction.value == MaterialSwipeAction.mark_read - ? (chat.hasUnreadMessage! ? Icons.mark_chat_read : Icons.mark_chat_unread) - : (chat.isArchived! ? Icons.unarchive : Icons.archive), - color: Colors.white, - ), - Text( - SettingsManager().settings.materialLeftAction.value == MaterialSwipeAction.pin - ? (chat.isPinned! ? " Unpin" : " Pin") - : SettingsManager().settings.materialLeftAction.value == MaterialSwipeAction.alerts - ? (chat.isMuted! ? ' Show Alerts' : ' Hide Alerts') - : SettingsManager().settings.materialLeftAction.value == MaterialSwipeAction.delete - ? " Delete" - : SettingsManager().settings.materialLeftAction.value == MaterialSwipeAction.mark_read - ? (chat.hasUnreadMessage! ? ' Mark Read' : ' Mark Unread') - : (chat.isArchived! ? ' UnArchive' : ' Archive'), - style: TextStyle( - color: Colors.white, - fontWeight: FontWeight.w700, - ), - textAlign: TextAlign.right, - ), - SizedBox( - width: 20, - ), - ], - ), - alignment: Alignment.centerRight, - ), - ); - } + List getConnectionIndicatorWidgets() { + if (!SettingsManager().settings.showConnectionIndicator.value) return []; - Widget slideRightBackground(Chat chat) { - return Container( - color: SettingsManager().settings.materialRightAction.value == MaterialSwipeAction.pin - ? Colors.yellow[800] - : SettingsManager().settings.materialRightAction.value == MaterialSwipeAction.alerts - ? Colors.purple - : SettingsManager().settings.materialRightAction.value == MaterialSwipeAction.delete - ? Colors.red - : SettingsManager().settings.materialRightAction.value == MaterialSwipeAction.mark_read - ? Colors.blue - : Colors.red, - child: Align( - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - SizedBox( - width: 20, - ), - Icon( - SettingsManager().settings.materialRightAction.value == MaterialSwipeAction.pin - ? (chat.isPinned! ? Icons.star_outline : Icons.star) - : SettingsManager().settings.materialRightAction.value == MaterialSwipeAction.alerts - ? (chat.isMuted! ? Icons.notifications_active : Icons.notifications_off) - : SettingsManager().settings.materialRightAction.value == MaterialSwipeAction.delete - ? Icons.delete_forever - : SettingsManager().settings.materialRightAction.value == MaterialSwipeAction.mark_read - ? (chat.hasUnreadMessage! ? Icons.mark_chat_read : Icons.mark_chat_unread) - : (chat.isArchived! ? Icons.unarchive : Icons.archive), - color: Colors.white, - ), - Text( - SettingsManager().settings.materialRightAction.value == MaterialSwipeAction.pin - ? (chat.isPinned! ? " Unpin" : " Pin") - : SettingsManager().settings.materialRightAction.value == MaterialSwipeAction.alerts - ? (chat.isMuted! ? ' Show Alerts' : ' Hide Alerts') - : SettingsManager().settings.materialRightAction.value == MaterialSwipeAction.delete - ? " Delete" - : SettingsManager().settings.materialRightAction.value == MaterialSwipeAction.mark_read - ? (chat.hasUnreadMessage! ? ' Mark Read' : ' Mark Unread') - : (chat.isArchived! ? ' UnArchive' : ' Archive'), - style: TextStyle( - color: Colors.white, - fontWeight: FontWeight.w700, - ), - textAlign: TextAlign.left, - ), - ], - ), - alignment: Alignment.centerLeft, - ), - ); + return [Obx(() => getIndicatorIcon(SocketManager().state.value, size: 12)), Container(width: 10.0)]; } @override Widget build(BuildContext context) { - bool showArchived = widget.parent.widget.showArchivedChats; - return AnnotatedRegion( - value: SystemUiOverlayStyle( - systemNavigationBarColor: context.theme.backgroundColor, // navigation bar color - systemNavigationBarIconBrightness: - context.theme.backgroundColor.computeLuminance() > 0.5 ? Brightness.dark : Brightness.light, - statusBarColor: Colors.transparent, // status bar color - ), - child: Obx( - () => WillPopScope( - onWillPop: () async { - if (selected.isNotEmpty) { - selected = []; - setState(() {}); - return false; - } - return true; - }, - child: Scaffold( - appBar: PreferredSize( - preferredSize: Size.fromHeight(60), - child: AnimatedSwitcher( - duration: Duration(milliseconds: 500), - child: selected.isEmpty - ? AppBar( - shadowColor: Colors.transparent, - iconTheme: IconThemeData(color: context.theme.primaryColor), - brightness: ThemeData.estimateBrightnessForColor(context.theme.backgroundColor), - bottom: PreferredSize( - child: Container( - color: context.theme.dividerColor, - height: 0, - ), - preferredSize: Size.fromHeight(0.5), - ), - title: Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - ...widget.parent.getHeaderTextWidgets(size: 20), - ...widget.parent.getConnectionIndicatorWidgets(), - widget.parent.getSyncIndicatorWidget(), - ], - ), - actions: [ - (!showArchived) - ? GestureDetector( - onTap: () async { - Navigator.of(context).push( - CupertinoPageRoute( - builder: (context) => SearchView(), - ), - ); - }, - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Icon( - Icons.search, - color: context.textTheme.bodyText1!.color, - ), - ), - ) - : Container(), - (SettingsManager().settings.moveChatCreatorToHeader.value && !showArchived - ? GestureDetector( - onTap: () { - Navigator.of(context).push( - ThemeSwitcher.buildPageRoute( - builder: (BuildContext context) { - return ConversationView( - isCreator: true, - ); - }, - ), - ); - }, - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Icon( - Icons.create, - color: context.textTheme.bodyText1!.color, - ), - ), - ) - : Container()), - Padding( - padding: EdgeInsets.only(right: 20), - child: Padding( - padding: EdgeInsets.symmetric(vertical: 15.5), - child: Container( - width: 40, - child: widget.parent.buildSettingsButton(), - ), - ), - ), - ], - backgroundColor: context.theme.backgroundColor, - ) - : Padding( - padding: const EdgeInsets.all(20.0), - child: Column( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.end, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - if (selected.length <= 1) - GestureDetector( - onTap: () { - selected.forEach((element) async { - await element.toggleMute(!element.isMuted!); - }); - - selected = []; - if (this.mounted) setState(() {}); - }, - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Icon( - Icons.notifications_off, - color: context.textTheme.bodyText1!.color, - ), - ), - ), - GestureDetector( - onTap: () { - selected.forEach((element) { - if (element.isArchived!) { - ChatBloc().unArchiveChat(element); - } else { - ChatBloc().archiveChat(element); - } - }); - selected = []; - if (this.mounted) setState(() {}); - }, - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Icon( - showArchived ? Icons.unarchive : Icons.archive, - color: context.textTheme.bodyText1!.color, - ), - ), - ), - GestureDetector( - onTap: () { - selected.forEach((element) async { - await element.togglePin(!element.isPinned!); - }); - - selected = []; - if (this.mounted) setState(() {}); - }, - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Icon( - Icons.star, - color: context.textTheme.bodyText1!.color, - ), - ), - ), - ], - ), - ], - ), - ), - ), - ), - backgroundColor: context.theme.backgroundColor, - body: Obx(() { - if (!ChatBloc().hasChats.value) { - return Center( - child: Container( - padding: EdgeInsets.only(top: 50.0), - child: Column( - children: [ - Padding( - padding: const EdgeInsets.all(8.0), - child: Text( - "Loading chats...", - style: Theme.of(context).textTheme.subtitle1, - ), - ), - buildProgressIndicator(context, size: 15), - ], - ), - ), - ); - } - if (ChatBloc().chats.archivedHelper(showArchived).isEmpty) { - return Center( - child: Container( - padding: EdgeInsets.only(top: 50.0), - child: Text( - "You have no archived chats :(", - style: context.textTheme.subtitle1, - ), - ), - ); - } - - bool hasPinned = hasPinnedChat(); - return SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - if (hasPinned) - Container( - height: 20.0, - decoration: BoxDecoration( - border: Border.all( - color: Colors.transparent, - ), - borderRadius: BorderRadius.all(Radius.circular(20)), - ), - ), - if (hasPinned) - Container( - padding: EdgeInsets.all(6.0), - decoration: new BoxDecoration( - color: context.theme.accentColor, - borderRadius: BorderRadius.circular(20), - ), - child: ListView.builder( - shrinkWrap: true, - physics: NeverScrollableScrollPhysics(), - itemBuilder: (context, index) { - return Obx(() { - if (SettingsManager().settings.swipableConversationTiles.value) { - return Dismissible( - background: Obx( - () => slideRightBackground(ChatBloc().chats.archivedHelper(showArchived)[index])), - secondaryBackground: Obx( - () => slideLeftBackground(ChatBloc().chats.archivedHelper(showArchived)[index])), - // Each Dismissible must contain a Key. Keys allow Flutter to - // uniquely identify widgets. - key: UniqueKey(), - // Provide a function that tells the app - // what to do after an item has been swiped away. - onDismissed: (direction) async { - if (direction == DismissDirection.endToStart) { - if (SettingsManager().settings.materialLeftAction.value == - MaterialSwipeAction.pin) { - await ChatBloc() - .chats - .archivedHelper(showArchived)[index] - .togglePin(!ChatBloc().chats.archivedHelper(showArchived)[index].isPinned!); - EventDispatcher().emit("refresh", null); - if (this.mounted) setState(() {}); - } else if (SettingsManager().settings.materialLeftAction.value == - MaterialSwipeAction.alerts) { - await ChatBloc() - .chats - .archivedHelper(showArchived)[index] - .toggleMute(!ChatBloc().chats.archivedHelper(showArchived)[index].isMuted!); - if (this.mounted) setState(() {}); - } else if (SettingsManager().settings.materialLeftAction.value == - MaterialSwipeAction.delete) { - ChatBloc().deleteChat(ChatBloc().chats.archivedHelper(showArchived)[index]); - Chat.deleteChat(ChatBloc().chats.archivedHelper(showArchived)[index]); - } else if (SettingsManager().settings.materialLeftAction.value == - MaterialSwipeAction.mark_read) { - ChatBloc().toggleChatUnread( - ChatBloc().chats.archivedHelper(showArchived)[index], - !ChatBloc().chats.archivedHelper(showArchived)[index].hasUnreadMessage!); - } else { - if (ChatBloc().chats.archivedHelper(showArchived)[index].isArchived!) { - ChatBloc() - .unArchiveChat(ChatBloc().chats.archivedHelper(showArchived)[index]); - } else { - ChatBloc().archiveChat(ChatBloc().chats.archivedHelper(showArchived)[index]); - } - } - } else { - if (SettingsManager().settings.materialRightAction.value == - MaterialSwipeAction.pin) { - await ChatBloc() - .chats - .archivedHelper(showArchived)[index] - .togglePin(!ChatBloc().chats.archivedHelper(showArchived)[index].isPinned!); - EventDispatcher().emit("refresh", null); - if (this.mounted) setState(() {}); - } else if (SettingsManager().settings.materialRightAction.value == - MaterialSwipeAction.alerts) { - await ChatBloc() - .chats - .archivedHelper(showArchived)[index] - .toggleMute(!ChatBloc().chats.archivedHelper(showArchived)[index].isMuted!); - if (this.mounted) setState(() {}); - } else if (SettingsManager().settings.materialRightAction.value == - MaterialSwipeAction.delete) { - ChatBloc().deleteChat(ChatBloc().chats.archivedHelper(showArchived)[index]); - Chat.deleteChat(ChatBloc().chats.archivedHelper(showArchived)[index]); - } else if (SettingsManager().settings.materialRightAction.value == - MaterialSwipeAction.mark_read) { - ChatBloc().toggleChatUnread( - ChatBloc().chats.archivedHelper(showArchived)[index], - !ChatBloc().chats.archivedHelper(showArchived)[index].hasUnreadMessage!); - } else { - if (ChatBloc().chats.archivedHelper(showArchived)[index].isArchived!) { - ChatBloc() - .unArchiveChat(ChatBloc().chats.archivedHelper(showArchived)[index]); - } else { - ChatBloc().archiveChat(ChatBloc().chats.archivedHelper(showArchived)[index]); - } - } - } - }, - child: (!showArchived && - ChatBloc().chats.archivedHelper(showArchived)[index].isArchived!) - ? Container() - : (showArchived && - !ChatBloc().chats.archivedHelper(showArchived)[index].isArchived!) - ? Container() - : ChatBloc().chats.archivedHelper(showArchived)[index].isPinned! - ? ConversationTile( - key: UniqueKey(), - chat: ChatBloc().chats.archivedHelper(showArchived)[index], - inSelectMode: selected.isNotEmpty, - selected: selected, - onSelect: (bool selected) { - if (selected) { - this - .selected - .add(ChatBloc().chats.archivedHelper(showArchived)[index]); - } else { - this.selected.removeWhere((element) => - element.guid == - ChatBloc().chats.archivedHelper(showArchived)[index].guid); - } - - if (this.mounted) setState(() {}); - }, - ) - : Container(), - ); - } else { - if (!showArchived && ChatBloc().chats.archivedHelper(showArchived)[index].isArchived!) - return Container(); - if (showArchived && !ChatBloc().chats.archivedHelper(showArchived)[index].isArchived!) - return Container(); - if (ChatBloc().chats.archivedHelper(showArchived)[index].isPinned!) { - return ConversationTile( - key: UniqueKey(), - chat: ChatBloc().chats.archivedHelper(showArchived)[index], - inSelectMode: selected.isNotEmpty, - selected: selected, - onSelect: (bool selected) { - if (selected) { - this.selected.add(ChatBloc().chats.archivedHelper(showArchived)[index]); - if (this.mounted) setState(() {}); - } else { - this.selected.removeWhere((element) => - element.guid == ChatBloc().chats.archivedHelper(showArchived)[index].guid); - if (this.mounted) setState(() {}); - } - }, - ); - } - return Container(); - } - }); - }, - itemCount: ChatBloc().chats.archivedHelper(showArchived).length, - ), - ), - if (hasNormalChats()) - Container( - height: 20.0, - decoration: BoxDecoration( - border: Border.all( - color: Colors.transparent, - ), - borderRadius: BorderRadius.all(Radius.circular(20))), - ), - if (hasNormalChats()) - Container( - padding: const EdgeInsets.all(6.0), - decoration: new BoxDecoration( - color: context.theme.accentColor, - borderRadius: new BorderRadius.only( - topLeft: const Radius.circular(20.0), - topRight: const Radius.circular(20.0), - bottomLeft: const Radius.circular(20.0), - bottomRight: const Radius.circular(20.0), - )), - child: ListView.builder( - shrinkWrap: true, - physics: NeverScrollableScrollPhysics(), - itemBuilder: (context, index) { - return Obx(() { - if (SettingsManager().settings.swipableConversationTiles.value) { - return Dismissible( - background: Obx( - () => slideRightBackground(ChatBloc().chats.archivedHelper(showArchived)[index])), - secondaryBackground: Obx( - () => slideLeftBackground(ChatBloc().chats.archivedHelper(showArchived)[index])), - // Each Dismissible must contain a Key. Keys allow Flutter to - // uniquely identify widgets. - key: UniqueKey(), - // Provide a function that tells the app - // what to do after an item has been swiped away. - onDismissed: (direction) async { - if (direction == DismissDirection.endToStart) { - if (SettingsManager().settings.materialLeftAction.value == - MaterialSwipeAction.pin) { - await ChatBloc() - .chats - .archivedHelper(showArchived)[index] - .togglePin(!ChatBloc().chats.archivedHelper(showArchived)[index].isPinned!); - EventDispatcher().emit("refresh", null); - if (this.mounted) setState(() {}); - } else if (SettingsManager().settings.materialLeftAction.value == - MaterialSwipeAction.alerts) { - await ChatBloc() - .chats - .archivedHelper(showArchived)[index] - .toggleMute(!ChatBloc().chats.archivedHelper(showArchived)[index].isMuted!); - if (this.mounted) setState(() {}); - } else if (SettingsManager().settings.materialLeftAction.value == - MaterialSwipeAction.delete) { - ChatBloc().deleteChat(ChatBloc().chats.archivedHelper(showArchived)[index]); - Chat.deleteChat(ChatBloc().chats.archivedHelper(showArchived)[index]); - } else if (SettingsManager().settings.materialLeftAction.value == - MaterialSwipeAction.mark_read) { - ChatBloc().toggleChatUnread( - ChatBloc().chats.archivedHelper(showArchived)[index], - !ChatBloc().chats.archivedHelper(showArchived)[index].hasUnreadMessage!); - } else { - if (ChatBloc().chats.archivedHelper(showArchived)[index].isArchived!) { - ChatBloc() - .unArchiveChat(ChatBloc().chats.archivedHelper(showArchived)[index]); - } else { - ChatBloc().archiveChat(ChatBloc().chats.archivedHelper(showArchived)[index]); - } - } - } else { - if (SettingsManager().settings.materialRightAction.value == - MaterialSwipeAction.pin) { - await ChatBloc() - .chats - .archivedHelper(showArchived)[index] - .togglePin(!ChatBloc().chats.archivedHelper(showArchived)[index].isPinned!); - EventDispatcher().emit("refresh", null); - if (this.mounted) setState(() {}); - } else if (SettingsManager().settings.materialRightAction.value == - MaterialSwipeAction.alerts) { - await ChatBloc() - .chats - .archivedHelper(showArchived)[index] - .toggleMute(!ChatBloc().chats.archivedHelper(showArchived)[index].isMuted!); - if (this.mounted) setState(() {}); - } else if (SettingsManager().settings.materialRightAction.value == - MaterialSwipeAction.delete) { - ChatBloc().deleteChat(ChatBloc().chats.archivedHelper(showArchived)[index]); - Chat.deleteChat(ChatBloc().chats.archivedHelper(showArchived)[index]); - } else if (SettingsManager().settings.materialRightAction.value == - MaterialSwipeAction.mark_read) { - ChatBloc().toggleChatUnread( - ChatBloc().chats.archivedHelper(showArchived)[index], - !ChatBloc().chats.archivedHelper(showArchived)[index].hasUnreadMessage!); - } else { - if (ChatBloc().chats.archivedHelper(showArchived)[index].isArchived!) { - ChatBloc() - .unArchiveChat(ChatBloc().chats.archivedHelper(showArchived)[index]); - } else { - ChatBloc().archiveChat(ChatBloc().chats.archivedHelper(showArchived)[index]); - } - } - } - }, - child: (!showArchived && - ChatBloc().chats.archivedHelper(showArchived)[index].isArchived!) - ? Container() - : (showArchived && - !ChatBloc().chats.archivedHelper(showArchived)[index].isArchived!) - ? Container() - : (!ChatBloc().chats.archivedHelper(showArchived)[index].isPinned!) - ? ConversationTile( - key: UniqueKey(), - chat: ChatBloc().chats.archivedHelper(showArchived)[index], - inSelectMode: selected.isNotEmpty, - selected: selected, - onSelect: (bool selected) { - if (selected) { - this - .selected - .add(ChatBloc().chats.archivedHelper(showArchived)[index]); - } else { - this.selected.removeWhere((element) => - element.guid == - ChatBloc().chats.archivedHelper(showArchived)[index].guid); - } - - if (this.mounted) setState(() {}); - }, - ) - : Container(), - ); - } else { - if (!showArchived && ChatBloc().chats.archivedHelper(showArchived)[index].isArchived!) - return Container(); - if (showArchived && !ChatBloc().chats.archivedHelper(showArchived)[index].isArchived!) - return Container(); - if (!ChatBloc().chats.archivedHelper(showArchived)[index].isPinned!) { - return ConversationTile( - key: UniqueKey(), - chat: ChatBloc().chats.archivedHelper(showArchived)[index], - inSelectMode: selected.isNotEmpty, - selected: selected, - onSelect: (bool selected) { - if (selected) { - this.selected.add(ChatBloc().chats.archivedHelper(showArchived)[index]); - } else { - this.selected.removeWhere((element) => - element.guid == ChatBloc().chats.archivedHelper(showArchived)[index].guid); - } - - if (this.mounted) setState(() {}); - }, - ); - } - return Container(); - } - }); - }, - itemCount: ChatBloc().chats.archivedHelper(showArchived).length, - ), - ) - ], - ), - ); - }), - floatingActionButton: selected.isEmpty && !SettingsManager().settings.moveChatCreatorToHeader.value - ? widget.parent.buildFloatingActionButton() - : null, - ), - ), - ), + return ThemeSwitcher( + iOSSkin: CupertinoConversationList(parent: this), + materialSkin: MaterialConversationList(parent: this), + samsungSkin: SamsungConversationList(parent: this), ); } } diff --git a/lib/layouts/conversation_list/conversation_tile.dart b/lib/layouts/conversation_list/conversation_tile.dart index 49626b8b2..0e57b76c7 100644 --- a/lib/layouts/conversation_list/conversation_tile.dart +++ b/lib/layouts/conversation_list/conversation_tile.dart @@ -6,6 +6,8 @@ import 'package:assorted_layout_widgets/assorted_layout_widgets.dart'; import 'package:bluebubbles/blocs/chat_bloc.dart'; import 'package:bluebubbles/helpers/constants.dart'; import 'package:bluebubbles/helpers/hex_color.dart'; +import 'package:bluebubbles/helpers/logger.dart'; +import 'package:bluebubbles/helpers/navigator.dart'; import 'package:bluebubbles/helpers/socket_singletons.dart'; import 'package:bluebubbles/helpers/utils.dart'; import 'package:bluebubbles/layouts/conversation_view/conversation_view.dart'; @@ -34,6 +36,7 @@ class ConversationTile extends StatefulWidget { final Function(bool)? onSelect; final bool inSelectMode; final List selected; + final Widget? subtitle; ConversationTile({ Key? key, @@ -43,6 +46,7 @@ class ConversationTile extends StatefulWidget { this.onSelect, this.inSelectMode = false, this.selected = const [], + this.subtitle, }) : super(key: key); @override @@ -94,7 +98,7 @@ class _ConversationTileState extends State with AutomaticKeepA try { await fetchChatSingleton(widget.chat.guid!); } catch (ex) { - debugPrint(ex.toString()); + Logger.error(ex.toString()); } this.setNewChatData(forceUpdate: true); @@ -135,16 +139,14 @@ class _ConversationTileState extends State with AutomaticKeepA if (widget.inSelectMode && widget.onSelect != null) { onSelect(); } else { - Navigator.of(context).push( - CupertinoPageRoute( - builder: (BuildContext context) { - return ConversationView( - chat: widget.chat, - existingAttachments: widget.existingAttachments, - existingText: widget.existingText, - ); - }, + CustomNavigator.pushAndRemoveUntil( + context, + ConversationView( + chat: widget.chat, + existingAttachments: widget.existingAttachments, + existingText: widget.existingText, ), + (route) => route.isFirst, ); } } @@ -163,7 +165,7 @@ class _ConversationTileState extends State with AutomaticKeepA caption: widget.chat.isPinned! ? 'Unpin' : 'Pin', color: Colors.yellow[800], foregroundColor: Colors.white, - icon: widget.chat.isPinned! ? Icons.star_outline : Icons.star, + icon: widget.chat.isPinned! ? CupertinoIcons.pin_slash : CupertinoIcons.pin, onTap: () async { await widget.chat.togglePin(!widget.chat.isPinned!); EventDispatcher().emit("refresh", null); @@ -174,11 +176,11 @@ class _ConversationTileState extends State with AutomaticKeepA secondaryActions: [ if (!widget.chat.isArchived! && SettingsManager().settings.iosShowAlert.value) IconSlideAction( - caption: widget.chat.isMuted! ? 'Show Alerts' : 'Hide Alerts', + caption: widget.chat.muteType == "mute" ? 'Show Alerts' : 'Hide Alerts', color: Colors.purple[700], - icon: widget.chat.isMuted! ? Icons.notifications_active : Icons.notifications_off, + icon: widget.chat.muteType == "mute" ? CupertinoIcons.bell : CupertinoIcons.bell_slash, onTap: () async { - await widget.chat.toggleMute(!widget.chat.isMuted!); + await widget.chat.toggleMute(widget.chat.muteType != "mute"); if (this.mounted) setState(() {}); }, ), @@ -186,7 +188,7 @@ class _ConversationTileState extends State with AutomaticKeepA IconSlideAction( caption: "Delete", color: Colors.red, - icon: Icons.delete_forever, + icon: CupertinoIcons.trash, onTap: () async { ChatBloc().deleteChat(widget.chat); Chat.deleteChat(widget.chat); @@ -196,7 +198,7 @@ class _ConversationTileState extends State with AutomaticKeepA IconSlideAction( caption: widget.chat.hasUnreadMessage! ? 'Mark Read' : 'Mark Unread', color: Colors.blue, - icon: widget.chat.hasUnreadMessage! ? Icons.mark_chat_read : Icons.mark_chat_unread, + icon: widget.chat.hasUnreadMessage! ? CupertinoIcons.person_crop_circle_badge_checkmark : CupertinoIcons.person_crop_circle_badge_exclam, onTap: () { ChatBloc().toggleChatUnread(widget.chat, !widget.chat.hasUnreadMessage!); }, @@ -205,7 +207,7 @@ class _ConversationTileState extends State with AutomaticKeepA IconSlideAction( caption: widget.chat.isArchived! ? 'UnArchive' : 'Archive', color: widget.chat.isArchived! ? Colors.blue : Colors.red, - icon: widget.chat.isArchived! ? Icons.unarchive : Icons.archive, + icon: widget.chat.isArchived! ? CupertinoIcons.tray_arrow_up : CupertinoIcons.tray_arrow_down, onTap: () { if (widget.chat.isArchived!) { ChatBloc().unArchiveChat(widget.chat); @@ -235,89 +237,88 @@ class _ConversationTileState extends State with AutomaticKeepA } Widget buildSubtitle() { - return StreamBuilder>( - stream: CurrentChat.getCurrentChat(widget.chat)?.stream as Stream>?, - builder: (BuildContext context, AsyncSnapshot snapshot) { - if (snapshot.connectionState == ConnectionState.active && - snapshot.hasData && - snapshot.data["type"] == CurrentChatEvent.TypingStatus) { - showTypingIndicator = snapshot.data["data"]; - } - if (showTypingIndicator) { - double height = Theme.of(context).textTheme.subtitle1!.fontSize!; - double indicatorHeight = (height * 2).clamp(height, height + 13); - return Container( - transform: Matrix4.translationValues(SettingsManager().settings.skin.value == Skins.iOS ? 0 : -5, 0, 0), - height: height, - child: OverflowBox( - alignment: Alignment.topLeft, - maxHeight: indicatorHeight, - child: ConstrainedBox( - constraints: BoxConstraints(maxHeight: indicatorHeight), - child: FittedBox( - alignment: Alignment.centerLeft, - child: TypingIndicator( - visible: true, - ), + return Obx(() { + final hideContent = + SettingsManager().settings.redactedMode.value && SettingsManager().settings.hideMessageContent.value; + final generateContent = + SettingsManager().settings.redactedMode.value && SettingsManager().settings.generateFakeMessageContent.value; + + TextStyle style = Theme.of(context).textTheme.subtitle1!.apply( + color: Theme.of(context).textTheme.subtitle1!.color!.withOpacity( + 0.85, ), - ), - ), ); - } - - final hideContent = - SettingsManager().settings.redactedMode.value && SettingsManager().settings.hideMessageContent.value; - final generateContent = SettingsManager().settings.redactedMode.value && - SettingsManager().settings.generateFakeMessageContent.value; - - TextStyle style = Theme.of(context).textTheme.subtitle1!.apply( - color: Theme.of(context).textTheme.subtitle1!.color!.withOpacity( - 0.85, - ), + String? message = widget.chat.latestMessageText != null ? widget.chat.latestMessageText : ""; + + if (generateContent) + message = widget.chat.fakeLatestMessageText; + else if (hideContent) style = style.copyWith(color: Colors.transparent); + + return widget.chat.latestMessageText != null && !(widget.chat.latestMessageText is String) + ? widget.chat.latestMessageText as Widget + : Text( + message ?? "", + style: style, + overflow: TextOverflow.ellipsis, + maxLines: 2, ); - String? message = widget.chat.latestMessageText != null ? widget.chat.latestMessageText : ""; - - if (generateContent) - message = widget.chat.fakeLatestMessageText; - else if (hideContent) style = style.copyWith(color: Colors.transparent); - - return widget.chat.latestMessageText != null && !(widget.chat.latestMessageText is String) - ? widget.chat.latestMessageText as Widget - : Text( - message ?? "", - style: style, - overflow: TextOverflow.ellipsis, - maxLines: 2, - ); - }, - ); + }); } Widget buildLeading() { - return Padding( - padding: EdgeInsets.only(top: 2, right: 2), - child: !selected - ? ContactAvatarGroupWidget( - chat: widget.chat, - size: 40, - editable: false, - onTap: this.onTapUpBypass, - ) - : Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(30), - color: Theme.of(context).primaryColor, - ), - width: 40, - height: 40, - child: Center( - child: Icon( - Icons.check, - color: Theme.of(context).textTheme.bodyText1!.color, - size: 20, + return StreamBuilder>( + stream: CurrentChat.getCurrentChat(widget.chat)?.stream as Stream>?, + builder: (BuildContext context, AsyncSnapshot snapshot) { + if (snapshot.connectionState == ConnectionState.active && + snapshot.hasData && + snapshot.data["type"] == CurrentChatEvent.TypingStatus) { + showTypingIndicator = snapshot.data["data"]; + } + double height = Theme.of(context).textTheme.subtitle1!.fontSize! * 1.25; + return Stack( + clipBehavior: Clip.none, + children: [ + Padding( + padding: EdgeInsets.only(top: 2, right: 2), + child: !selected + ? ContactAvatarGroupWidget( + chat: widget.chat, + size: 40, + editable: false, + onTap: this.onTapUpBypass, + ) + : Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(30), + color: Theme.of(context).primaryColor, + ), + width: 40, + height: 40, + child: Center( + child: Icon( + Icons.check, + color: Theme.of(context).textTheme.bodyText1!.color, + size: 20, + ), + ), + ), + ), + if (showTypingIndicator) + Positioned( + top: 30, + left: 20, + height: height, + child: FittedBox( + alignment: Alignment.centerLeft, + child: TypingIndicator( + chatList: true, + visible: true, + ), ), ), - )); + ], + ); + }); } Widget _buildDate() => ConstrainedBox( @@ -338,15 +339,12 @@ class _ConversationTileState extends State with AutomaticKeepA ); void onTap() { - Navigator.of(context).pushAndRemoveUntil( - ThemeSwitcher.buildPageRoute( - builder: (BuildContext context) { - return ConversationView( - chat: widget.chat, - existingAttachments: widget.existingAttachments, - existingText: widget.existingText, - ); - }, + CustomNavigator.pushAndRemoveUntil( + context, + ConversationView( + chat: widget.chat, + existingAttachments: widget.existingAttachments, + existingText: widget.existingText, ), (route) => route.isFirst, ); @@ -452,7 +450,7 @@ class __CupertinoState extends State<_Cupertino> { contentPadding: EdgeInsets.only(left: 0), minVerticalPadding: 10, title: widget.parent.buildTitle(), - subtitle: widget.parent.buildSubtitle(), + subtitle: widget.parent.widget.subtitle ?? widget.parent.buildSubtitle(), leading: widget.parent.buildLeading(), trailing: Container( padding: EdgeInsets.only(right: 8), @@ -468,7 +466,7 @@ class __CupertinoState extends State<_Cupertino> { ), Icon( SettingsManager().settings.skin.value == Skins.iOS - ? Icons.arrow_forward_ios + ? CupertinoIcons.forward : Icons.arrow_forward, color: Theme.of(context).textTheme.subtitle1!.color, size: 15, @@ -491,7 +489,7 @@ class __CupertinoState extends State<_Cupertino> { Stack( alignment: AlignmentDirectional.centerStart, children: [ - (!widget.parent.widget.chat.isMuted! && widget.parent.widget.chat.hasUnreadMessage!) + (widget.parent.widget.chat.muteType != "mute" && widget.parent.widget.chat.hasUnreadMessage!) ? Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(35), @@ -503,7 +501,7 @@ class __CupertinoState extends State<_Cupertino> { : Container(), widget.parent.widget.chat.isPinned! ? Icon( - Icons.star, + CupertinoIcons.pin, size: 10, color: Colors .yellow[AdaptiveTheme.of(context).mode == AdaptiveThemeMode.dark ? 100 : 700], @@ -511,7 +509,7 @@ class __CupertinoState extends State<_Cupertino> { : Container(), ], ), - widget.parent.widget.chat.isMuted! + widget.parent.widget.chat.muteType == "mute" ? SvgPicture.asset( "assets/icon/moon.svg", color: widget.parentProps.chat.hasUnreadMessage! @@ -572,13 +570,13 @@ class _Material extends StatelessWidget { child: ListTile( dense: SettingsManager().settings.denseChatTiles.value, title: parent.buildTitle(), - subtitle: parent.buildSubtitle(), + subtitle: parent.widget.subtitle ?? parent.buildSubtitle(), minVerticalPadding: 10, leading: Stack( alignment: Alignment.topRight, children: [ parent.buildLeading(), - if (!parent.widget.chat.isMuted!) + if (parent.widget.chat.muteType != "mute") Container( width: 10, height: 10, @@ -599,7 +597,7 @@ class _Material extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.end, children: [ if (parent.widget.chat.isPinned!) Icon(Icons.star, size: 15, color: Colors.yellow), - if (parent.widget.chat.isMuted!) + if (parent.widget.chat.muteType == "mute") Icon( Icons.notifications_off, color: parent.widget.chat.hasUnreadMessage! @@ -650,7 +648,6 @@ class _Samsung extends StatelessWidget { }, child: Obx( () => Container( - height: 72.0, decoration: BoxDecoration( color: Theme.of(context).accentColor, border: (!SettingsManager().settings.hideDividers.value) @@ -664,15 +661,15 @@ class _Samsung extends StatelessWidget { : null, ), child: ListTile( - isThreeLine: true, dense: SettingsManager().settings.denseChatTiles.value, title: parent.buildTitle(), - subtitle: parent.buildSubtitle(), + subtitle: parent.widget.subtitle ?? parent.buildSubtitle(), + minVerticalPadding: 10, leading: Stack( alignment: Alignment.topRight, children: [ parent.buildLeading(), - if (!parent.widget.chat.isMuted!) + if (parent.widget.chat.muteType != "mute") Container( width: 15, height: 15, @@ -693,7 +690,7 @@ class _Samsung extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.end, children: [ if (parent.widget.chat.isPinned!) Icon(Icons.star, size: 15, color: Colors.yellow), - if (parent.widget.chat.isMuted!) + if (parent.widget.chat.muteType == "mute") Icon( Icons.notifications_off, color: Theme.of(context).textTheme.subtitle1!.color, diff --git a/lib/layouts/conversation_list/cupertino_conversation_list.dart b/lib/layouts/conversation_list/cupertino_conversation_list.dart new file mode 100644 index 000000000..c94b79c20 --- /dev/null +++ b/lib/layouts/conversation_list/cupertino_conversation_list.dart @@ -0,0 +1,521 @@ +import 'dart:io'; +import 'dart:math'; +import 'dart:ui'; + +import 'package:bluebubbles/blocs/chat_bloc.dart'; +import 'package:bluebubbles/helpers/constants.dart'; +import 'package:bluebubbles/helpers/navigator.dart'; +import 'package:bluebubbles/helpers/ui_helpers.dart'; +import 'package:bluebubbles/helpers/utils.dart'; +import 'package:bluebubbles/layouts/conversation_list/conversation_list.dart'; +import 'package:bluebubbles/layouts/conversation_list/conversation_tile.dart'; +import 'package:bluebubbles/layouts/conversation_list/pinned_conversation_tile.dart'; +import 'package:bluebubbles/layouts/conversation_view/conversation_view.dart'; +import 'package:bluebubbles/layouts/search/search_view.dart'; +import 'package:bluebubbles/layouts/widgets/vertical_split_view.dart'; +import 'package:bluebubbles/managers/current_chat.dart'; +import 'package:bluebubbles/managers/method_channel_interface.dart'; +import 'package:bluebubbles/managers/settings_manager.dart'; +import 'package:bluebubbles/managers/theme_manager.dart'; +import 'package:bluebubbles/repository/models/chat.dart'; +import 'package:bluebubbles/main.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:get/get.dart'; +import 'package:smooth_page_indicator/smooth_page_indicator.dart'; + +class CupertinoConversationList extends StatefulWidget { + const CupertinoConversationList({Key? key, required this.parent}) : super(key: key); + + final ConversationListState parent; + + @override + State createState() => CupertinoConversationListState(); +} + +class CupertinoConversationListState extends State { + final key = new GlobalKey(); + bool openedChatAlready = false; + + Future openLastChat(BuildContext context) async { + if (ChatBloc().chatRequest != null + && prefs.getString('lastOpenedChat') != null + && (!context.isPhone || context.isLandscape) + && CurrentChat.activeChat?.chat.guid != prefs.getString('lastOpenedChat')) { + await ChatBloc().chatRequest!.future; + CustomNavigator.pushAndRemoveUntil( + context, + ConversationView( + chat: ChatBloc().chats.firstWhere((e) => e.guid == prefs.getString('lastOpenedChat')) + ), + (route) => route.isFirst, + ); + } + } + + @override + Widget build(BuildContext context) { + if (!openedChatAlready) { + Future.delayed(Duration.zero, () => openLastChat(context)); + openedChatAlready = true; + } + return AnnotatedRegion( + value: SystemUiOverlayStyle( + systemNavigationBarColor: context.theme.backgroundColor, // navigation bar color + systemNavigationBarIconBrightness: + context.theme.backgroundColor.computeLuminance() > 0.5 ? Brightness.dark : Brightness.light, + statusBarColor: Colors.transparent, // status bar color + ), + child: buildForDevice(context), + ); + } + + Widget buildChatList(BuildContext context, bool showAltLayout) { + bool showArchived = widget.parent.widget.showArchivedChats; + bool showUnknown = widget.parent.widget.showUnknownSenders; + Brightness brightness = ThemeData.estimateBrightnessForColor(context.theme.backgroundColor); + return Obx( + () => Scaffold( + appBar: PreferredSize( + preferredSize: Size( + (showAltLayout) ? CustomNavigator.width(context) * 0.33 : CustomNavigator.width(context), + context.orientation == Orientation.landscape + ? 0 + : SettingsManager().settings.reducedForehead.value + ? 10 + : 40, + ), + child: ClipRRect( + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 15, sigmaY: 15), + child: StreamBuilder( + stream: widget.parent.headerColorStream.stream, + builder: (context, snapshot) { + return AnimatedCrossFade( + crossFadeState: widget.parent.theme == Colors.transparent + ? CrossFadeState.showFirst + : CrossFadeState.showSecond, + duration: Duration(milliseconds: 250), + secondChild: AppBar( + iconTheme: IconThemeData(color: context.theme.primaryColor), + elevation: 0, + backgroundColor: widget.parent.theme, + centerTitle: true, + brightness: brightness, + title: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Text( + showArchived + ? "Archive" + : showUnknown + ? "Unknown Senders" + : "Messages", + style: context.textTheme.bodyText1, + ), + ], + ), + ), + firstChild: AppBar( + leading: new Container(), + elevation: 0, + brightness: brightness, + backgroundColor: context.theme.backgroundColor, + ), + ); + }, + ), + ), + ), + ), + backgroundColor: context.theme.backgroundColor, + extendBodyBehindAppBar: true, + body: CustomScrollView( + controller: widget.parent.scrollController, + physics: ThemeManager().scrollPhysics, + slivers: [ + SliverAppBar( + leading: ((SettingsManager().settings.skin.value == Skins.iOS && (showArchived || showUnknown)) || + (SettingsManager().settings.skin.value == Skins.Material || + SettingsManager().settings.skin.value == Skins.Samsung) && + !showArchived && + !showUnknown) + ? buildBackButton(context) + : new Container(), + stretch: true, + expandedHeight: (!showArchived && !showUnknown) ? 80 : 50, + backgroundColor: Colors.transparent, + pinned: false, + flexibleSpace: FlexibleSpaceBar( + stretchModes: [StretchMode.zoomBackground], + background: Stack( + fit: StackFit.expand, + ), + centerTitle: true, + title: Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Container(height: 20), + Container( + margin: EdgeInsets.only(right: 10), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Container(width: (!showArchived && !showUnknown) ? 20 : 50), + Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + ...widget.parent.getHeaderTextWidgets(), + ...widget.parent.getConnectionIndicatorWidgets(), + widget.parent.getSyncIndicatorWidget(), + ], + ), + Spacer( + flex: 25, + ), + if (!showArchived && !showUnknown) + ClipOval( + child: Material( + color: context.theme.accentColor, // button color + child: InkWell( + child: SizedBox( + width: 20, + height: 20, + child: Icon(CupertinoIcons.search, color: context.theme.primaryColor, size: 12)), + onTap: () async { + CustomNavigator.pushLeft(context, SearchView()); + }, + ), + ), + ), + if (!showArchived && !showUnknown) Container(width: 10.0), + if (SettingsManager().settings.moveChatCreatorToHeader.value && !showArchived && !showUnknown) + ClipOval( + child: Material( + color: context.theme.accentColor, // button color + child: InkWell( + child: SizedBox( + width: 20, + height: 20, + child: Icon(CupertinoIcons.pencil, color: context.theme.primaryColor, size: 12), + ), + onTap: this.widget.parent.openNewChatCreator, + ), + ), + ), + if (SettingsManager().settings.moveChatCreatorToHeader.value && + SettingsManager().settings.cameraFAB.value) + Container(width: 10.0), + if (SettingsManager().settings.moveChatCreatorToHeader.value && + SettingsManager().settings.cameraFAB.value && + !showArchived && + !showUnknown) + ClipOval( + child: Material( + color: context.theme.accentColor, // button color + child: InkWell( + child: SizedBox( + width: 20, + height: 20, + child: Icon(CupertinoIcons.camera, color: context.theme.primaryColor, size: 12), + ), + onTap: () async { + String appDocPath = SettingsManager().appDocDir.path; + String ext = ".png"; + File file = new File("$appDocPath/attachments/" + randomString(16) + ext); + await file.create(recursive: true); + + // Take the picture after opening the camera + await MethodChannelInterface() + .invokeMethod("open-camera", {"path": file.path, "type": "camera"}); + + // If we don't get data back, return outta here + if (!file.existsSync()) return; + if (file.statSync().size == 0) { + file.deleteSync(); + return; + } + + widget.parent.openNewChatCreator(existing: [file]); + }, + ), + ), + ), + if (SettingsManager().settings.moveChatCreatorToHeader.value) Container(width: 10.0), + widget.parent.buildSettingsButton(), + Spacer( + flex: 3, + ), + ], + ), + ), + ], + ), + ), + ), + // SliverToBoxAdapter( + // child: Container( + // padding: EdgeInsets.symmetric(horizontal: 30, vertical: 5), + // child: GestureDetector( + // onTap: () { + // Navigator.of(context).push( + // MaterialPageRoute( + // builder: (context) => SearchView(), + // ), + // ); + // }, + // child: AbsorbPointer( + // child: SearchTextBox(), + // ), + // ), + // ), + // ), + Obx(() { + if (ChatBloc() + .chats + .archivedHelper(showArchived) + .unknownSendersHelper(showUnknown) + .bigPinHelper(true) + .isEmpty) { + return SliverToBoxAdapter(child: Container()); + } + ChatBloc().chats.archivedHelper(showArchived).unknownSendersHelper(showUnknown).sort(Chat.sort); + + int rowCount = context.mediaQuery.orientation == Orientation.portrait + ? SettingsManager().settings.pinRowsPortrait.value + : SettingsManager().settings.pinRowsLandscape.value; + int colCount = SettingsManager().settings.pinColumnsPortrait.value; + int pinCount = ChatBloc() + .chats + .archivedHelper(showArchived) + .unknownSendersHelper(showUnknown) + .bigPinHelper(true) + .length; + int usedRowCount = min((pinCount / colCount).ceil(), rowCount); + int maxOnPage = rowCount * colCount; + PageController _controller = PageController(); + int _pageCount = (pinCount / maxOnPage).ceil(); + int _filledPageCount = (pinCount / maxOnPage).floor(); + + return SliverPadding( + padding: EdgeInsets.only( + top: 0, + bottom: 10, + ), + sliver: SliverToBoxAdapter( + child: LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) => ConstrainedBox( + constraints: BoxConstraints( + maxHeight: (constraints.maxWidth) / colCount * usedRowCount * (showAltLayout ? 1.175 : 1.125), + ), + child: Stack( + clipBehavior: Clip.none, + alignment: Alignment.bottomCenter, + children: [ + PageView.builder( + clipBehavior: Clip.none, + physics: BouncingScrollPhysics(parent: AlwaysScrollableScrollPhysics()), + scrollDirection: Axis.horizontal, + controller: _controller, + itemBuilder: (context, index) { + return Padding( + padding: EdgeInsets.symmetric(horizontal: 10), + child: Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + alignment: _pageCount > 1 ? WrapAlignment.start : WrapAlignment.center, + children: List.generate( + index < _filledPageCount + ? maxOnPage + : ChatBloc() + .chats + .archivedHelper(showArchived) + .unknownSendersHelper(showUnknown) + .bigPinHelper(true) + .length % + maxOnPage, + (_index) { + return PinnedConversationTile( + key: Key(ChatBloc() + .chats + .archivedHelper(showArchived) + .unknownSendersHelper(showUnknown) + .bigPinHelper(true)[index * maxOnPage + _index] + .guid + .toString()), + chat: ChatBloc() + .chats + .archivedHelper(showArchived) + .unknownSendersHelper(showUnknown) + .bigPinHelper(true)[index * maxOnPage + _index], + ); + }, + ), + ), + ); + }, + itemCount: _pageCount, + ), + if (_pageCount > 1) + SmoothPageIndicator( + controller: _controller, + count: _pageCount, + effect: ScaleEffect( + dotHeight: 5.0, + dotWidth: 5.0, + spacing: 5.0, + radius: 5.0, + scale: 1.5, + activeDotColor: context.theme.primaryColor, + ), + ), + ], + ), + ), + ), + ), + ); + }), + Obx(() { + ChatBloc().chats.archivedHelper(showArchived).unknownSendersHelper(showUnknown).sort(Chat.sort); + if (!ChatBloc().hasChats.value) { + return SliverToBoxAdapter( + child: Center( + child: Container( + padding: EdgeInsets.only(top: 50.0), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + "Loading chats...", + style: Theme.of(context).textTheme.subtitle1, + ), + ), + buildProgressIndicator(context, size: 15), + ], + ), + ), + ), + ); + } + if (!ChatBloc().hasChats.value) { + return SliverToBoxAdapter( + child: Center( + child: Container( + padding: EdgeInsets.only(top: 50.0), + child: Text( + showArchived + ? "You have no archived chats :(" + : showUnknown + ? "You have no messages from unknown senders :)" + : "You have no chats :(", + style: Theme.of(context).textTheme.subtitle1, + ), + ), + ), + ); + } + + return SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + return ConversationTile( + key: Key(ChatBloc() + .chats + .archivedHelper(showArchived) + .unknownSendersHelper(showUnknown) + .bigPinHelper(false)[index] + .guid + .toString()), + chat: ChatBloc() + .chats + .archivedHelper(showArchived) + .unknownSendersHelper(showUnknown) + .bigPinHelper(false)[index], + ); + }, + childCount: ChatBloc() + .chats + .archivedHelper(showArchived) + .unknownSendersHelper(showUnknown) + .bigPinHelper(false) + .length, + ), + ); + }), + ], + ), + floatingActionButton: !SettingsManager().settings.moveChatCreatorToHeader.value + ? widget.parent.buildFloatingActionButton() + : null, + ), + ); + } + + Widget buildForLandscape(BuildContext context, Widget chatList) { + return VerticalSplitView( + dividerWidth: 10.0, + initialRatio: 0.4, + minRatio: 0.33, + maxRatio: 0.5, + allowResize: true, + left: LayoutBuilder( + builder: (context, constraints) { + CustomNavigator.maxWidthLeft = constraints.maxWidth; + return WillPopScope( + onWillPop: () async { + Get.back(id: 1); + return false; + }, + child: Navigator( + key: Get.nestedKey(1), + onPopPage: (route, _) { + return false; + }, + pages: [CupertinoPage(name: "initial", child: chatList)], + ), + ); + } + ), + right: LayoutBuilder( + builder: (context, constraints) { + CustomNavigator.maxWidthRight = constraints.maxWidth; + return WillPopScope( + onWillPop: () async { + Get.back(id: 2); + return false; + }, + child: Navigator( + key: Get.nestedKey(2), + onPopPage: (route, _) { + return false; + }, + pages: [CupertinoPage(name: "initial", child: Scaffold( + backgroundColor: context.theme.backgroundColor, + extendBodyBehindAppBar: true, + body: Center( + child: Container( + child: Text("Select a chat from the list", style: Theme.of(Get.context!).textTheme.subtitle1!.copyWith(fontSize: 18)) + ), + ), + ))], + ), + ); + } + ), + ); + } + + Widget buildForDevice(BuildContext context) { + bool showAltLayout = !context.isPhone || context.isLandscape; + Widget chatList = buildChatList(context, showAltLayout); + if (showAltLayout && !widget.parent.widget.showUnknownSenders && !widget.parent.widget.showArchivedChats) { + return buildForLandscape(context, chatList); + } + + return chatList; + } +} diff --git a/lib/layouts/conversation_list/material_conversation_list.dart b/lib/layouts/conversation_list/material_conversation_list.dart new file mode 100644 index 000000000..59179f539 --- /dev/null +++ b/lib/layouts/conversation_list/material_conversation_list.dart @@ -0,0 +1,658 @@ +import 'dart:io'; +import 'dart:ui'; + +import 'package:bluebubbles/blocs/chat_bloc.dart'; +import 'package:bluebubbles/helpers/constants.dart'; +import 'package:bluebubbles/helpers/navigator.dart'; +import 'package:bluebubbles/helpers/ui_helpers.dart'; +import 'package:bluebubbles/helpers/utils.dart'; +import 'package:bluebubbles/layouts/conversation_list/conversation_list.dart'; +import 'package:bluebubbles/layouts/conversation_list/conversation_tile.dart'; +import 'package:bluebubbles/layouts/conversation_view/conversation_view.dart'; +import 'package:bluebubbles/layouts/search/search_view.dart'; +import 'package:bluebubbles/layouts/widgets/theme_switcher/theme_switcher.dart'; +import 'package:bluebubbles/layouts/widgets/vertical_split_view.dart'; +import 'package:bluebubbles/managers/current_chat.dart'; +import 'package:bluebubbles/managers/event_dispatcher.dart'; +import 'package:bluebubbles/managers/method_channel_interface.dart'; +import 'package:bluebubbles/managers/settings_manager.dart'; +import 'package:bluebubbles/repository/models/chat.dart'; +import 'package:bluebubbles/main.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:get/get.dart'; + +class MaterialConversationList extends StatefulWidget { + MaterialConversationList({Key? key, required this.parent}) : super(key: key); + + final ConversationListState parent; + + @override + _MaterialConversationListState createState() => _MaterialConversationListState(); +} + +class _MaterialConversationListState extends State { + List selected = []; + bool openedChatAlready = false; + + bool hasPinnedChat() { + for (var i = 0; i < ChatBloc().chats.archivedHelper(widget.parent.widget.showArchivedChats).unknownSendersHelper(widget.parent.widget.showUnknownSenders).length; i++) { + if (ChatBloc().chats.archivedHelper(widget.parent.widget.showArchivedChats).unknownSendersHelper(widget.parent.widget.showUnknownSenders)[i].isPinned!) { + widget.parent.hasPinnedChats = true; + return true; + } else { + return false; + } + } + return false; + } + + bool hasNormalChats() { + int counter = 0; + for (var i = 0; i < ChatBloc().chats.archivedHelper(widget.parent.widget.showArchivedChats).unknownSendersHelper(widget.parent.widget.showUnknownSenders).length; i++) { + if (ChatBloc().chats.archivedHelper(widget.parent.widget.showArchivedChats).unknownSendersHelper(widget.parent.widget.showUnknownSenders)[i].isPinned!) { + counter++; + } else {} + } + if (counter == ChatBloc().chats.archivedHelper(widget.parent.widget.showArchivedChats).unknownSendersHelper(widget.parent.widget.showUnknownSenders).length) { + return false; + } else { + return true; + } + } + + Widget slideLeftBackground(Chat chat) { + return Container( + color: SettingsManager().settings.materialLeftAction.value == MaterialSwipeAction.pin + ? Colors.yellow[800] + : SettingsManager().settings.materialLeftAction.value == MaterialSwipeAction.alerts + ? Colors.purple + : SettingsManager().settings.materialLeftAction.value == MaterialSwipeAction.delete + ? Colors.red + : SettingsManager().settings.materialLeftAction.value == MaterialSwipeAction.mark_read + ? Colors.blue + : Colors.red, + child: Align( + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Icon( + SettingsManager().settings.materialLeftAction.value == MaterialSwipeAction.pin + ? (chat.isPinned! ? Icons.star_outline : Icons.star) + : SettingsManager().settings.materialLeftAction.value == MaterialSwipeAction.alerts + ? (chat.muteType == "mute" ? Icons.notifications_active : Icons.notifications_off) + : SettingsManager().settings.materialLeftAction.value == MaterialSwipeAction.delete + ? Icons.delete_forever + : SettingsManager().settings.materialLeftAction.value == MaterialSwipeAction.mark_read + ? (chat.hasUnreadMessage! ? Icons.mark_chat_read : Icons.mark_chat_unread) + : (chat.isArchived! ? Icons.unarchive : Icons.archive), + color: Colors.white, + ), + Text( + SettingsManager().settings.materialLeftAction.value == MaterialSwipeAction.pin + ? (chat.isPinned! ? " Unpin" : " Pin") + : SettingsManager().settings.materialLeftAction.value == MaterialSwipeAction.alerts + ? (chat.muteType == "mute" ? ' Show Alerts' : ' Hide Alerts') + : SettingsManager().settings.materialLeftAction.value == MaterialSwipeAction.delete + ? " Delete" + : SettingsManager().settings.materialLeftAction.value == MaterialSwipeAction.mark_read + ? (chat.hasUnreadMessage! ? ' Mark Read' : ' Mark Unread') + : (chat.isArchived! ? ' UnArchive' : ' Archive'), + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.w700, + ), + textAlign: TextAlign.right, + ), + SizedBox( + width: 20, + ), + ], + ), + alignment: Alignment.centerRight, + ), + ); + } + + Widget slideRightBackground(Chat chat) { + return Container( + color: SettingsManager().settings.materialRightAction.value == MaterialSwipeAction.pin + ? Colors.yellow[800] + : SettingsManager().settings.materialRightAction.value == MaterialSwipeAction.alerts + ? Colors.purple + : SettingsManager().settings.materialRightAction.value == MaterialSwipeAction.delete + ? Colors.red + : SettingsManager().settings.materialRightAction.value == MaterialSwipeAction.mark_read + ? Colors.blue + : Colors.red, + child: Align( + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + SizedBox( + width: 20, + ), + Icon( + SettingsManager().settings.materialRightAction.value == MaterialSwipeAction.pin + ? (chat.isPinned! ? Icons.star_outline : Icons.star) + : SettingsManager().settings.materialRightAction.value == MaterialSwipeAction.alerts + ? (chat.muteType == "mute" ? Icons.notifications_active : Icons.notifications_off) + : SettingsManager().settings.materialRightAction.value == MaterialSwipeAction.delete + ? Icons.delete_forever + : SettingsManager().settings.materialRightAction.value == MaterialSwipeAction.mark_read + ? (chat.hasUnreadMessage! ? Icons.mark_chat_read : Icons.mark_chat_unread) + : (chat.isArchived! ? Icons.unarchive : Icons.archive), + color: Colors.white, + ), + Text( + SettingsManager().settings.materialRightAction.value == MaterialSwipeAction.pin + ? (chat.isPinned! ? " Unpin" : " Pin") + : SettingsManager().settings.materialRightAction.value == MaterialSwipeAction.alerts + ? (chat.muteType == "mute" ? ' Show Alerts' : ' Hide Alerts') + : SettingsManager().settings.materialRightAction.value == MaterialSwipeAction.delete + ? " Delete" + : SettingsManager().settings.materialRightAction.value == MaterialSwipeAction.mark_read + ? (chat.hasUnreadMessage! ? ' Mark Read' : ' Mark Unread') + : (chat.isArchived! ? ' UnArchive' : ' Archive'), + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.w700, + ), + textAlign: TextAlign.left, + ), + ], + ), + alignment: Alignment.centerLeft, + ), + ); + } + + Future openLastChat(BuildContext context) async { + if (ChatBloc().chatRequest != null + && prefs.getString('lastOpenedChat') != null + && (!context.isPhone || context.isLandscape) + && CurrentChat.activeChat?.chat.guid != prefs.getString('lastOpenedChat')) { + await ChatBloc().chatRequest!.future; + CustomNavigator.pushAndRemoveUntil( + context, + ConversationView( + chat: ChatBloc().chats.firstWhere((e) => e.guid == prefs.getString('lastOpenedChat')) + ), + (route) => route.isFirst, + ); + } + } + + @override + Widget build(BuildContext context) { + if (!openedChatAlready) { + Future.delayed(Duration.zero, () => openLastChat(context)); + openedChatAlready = true; + } + hasPinnedChat(); + return AnnotatedRegion( + value: SystemUiOverlayStyle( + systemNavigationBarColor: context.theme.backgroundColor, // navigation bar color + systemNavigationBarIconBrightness: + context.theme.backgroundColor.computeLuminance() > 0.5 ? Brightness.dark : Brightness.light, + statusBarColor: Colors.transparent, // status bar color + ), + child: buildForDevice(), + ); + } + + Widget buildChatList() { + bool showArchived = widget.parent.widget.showArchivedChats; + bool showUnknown = widget.parent.widget.showUnknownSenders; + return Obx( + () => WillPopScope( + onWillPop: () async { + if (selected.isNotEmpty) { + selected = []; + setState(() {}); + return false; + } + return true; + }, + child: Scaffold( + appBar: PreferredSize( + preferredSize: Size.fromHeight(60), + child: AnimatedSwitcher( + duration: Duration(milliseconds: 500), + child: selected.isEmpty + ? AppBar( + iconTheme: IconThemeData(color: context.theme.primaryColor), + brightness: ThemeData.estimateBrightnessForColor(context.theme.backgroundColor), + bottom: PreferredSize( + child: Container( + color: context.theme.dividerColor, + height: 0, + ), + preferredSize: Size.fromHeight(0.5), + ), + title: Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + ...widget.parent.getHeaderTextWidgets(size: 20), + ...widget.parent.getConnectionIndicatorWidgets(), + widget.parent.getSyncIndicatorWidget(), + ], + ), + actions: [ + (!showArchived && !showUnknown) + ? GestureDetector( + onTap: () async { + CustomNavigator.pushLeft( + context, + SearchView(), + ); + }, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Icon( + Icons.search, + color: context.textTheme.bodyText1!.color, + ), + ), + ) + : Container(), + (SettingsManager().settings.moveChatCreatorToHeader.value && !showArchived && !showUnknown) + ? GestureDetector( + onTap: () { + CustomNavigator.pushAndRemoveUntil( + context, + ConversationView( + isCreator: true, + ), + (route) => route.isFirst, + ); + }, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Icon( + Icons.create, + color: context.textTheme.bodyText1!.color, + ), + ), + ) + : Container(), + (SettingsManager().settings.moveChatCreatorToHeader.value + && SettingsManager().settings.cameraFAB.value + && !showArchived && !showUnknown) + ? GestureDetector( + onTap: () async { + String appDocPath = SettingsManager().appDocDir.path; + String ext = ".png"; + File file = new File("$appDocPath/attachments/" + randomString(16) + ext); + await file.create(recursive: true); + + // Take the picture after opening the camera + await MethodChannelInterface().invokeMethod("open-camera", {"path": file.path, "type": "camera"}); + + // If we don't get data back, return outta here + if (!file.existsSync()) return; + if (file.statSync().size == 0) { + file.deleteSync(); + return; + } + + widget.parent.openNewChatCreator(existing: [file]); + }, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Icon( + Icons.photo_camera, + color: context.textTheme.bodyText1!.color, + ), + ), + ) + : Container(), + Padding( + padding: EdgeInsets.only(right: 20), + child: Padding( + padding: EdgeInsets.symmetric(vertical: 15.5), + child: Container( + width: 40, + child: widget.parent.buildSettingsButton(), + ), + ), + ), + ], + backgroundColor: context.theme.backgroundColor, + ) + : Padding( + padding: const EdgeInsets.all(20.0), + child: Column( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + if (([0, selected.length]) + .contains(selected.where((element) => element.hasUnreadMessage!).length)) + GestureDetector( + onTap: () { + selected.forEach((element) async { + await element.toggleHasUnread(!element.hasUnreadMessage!); + }); + selected = []; + if (this.mounted) setState(() {}); + }, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Icon( + selected[0].hasUnreadMessage! ? Icons.mark_chat_read : Icons.mark_chat_unread, + color: context.textTheme.bodyText1!.color, + ), + ), + ), + if (([0, selected.length]) + .contains(selected.where((element) => element.muteType == "mute").length)) + GestureDetector( + onTap: () { + selected.forEach((element) async { + await element.toggleMute(element.muteType != "mute"); + }); + selected = []; + if (this.mounted) setState(() {}); + }, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Icon( + selected[0].muteType == "mute" + ? Icons.notifications_active + : Icons.notifications_off, + color: context.textTheme.bodyText1!.color, + ), + ), + ), + if (([0, selected.length]) + .contains(selected.where((element) => element.isPinned!).length)) + GestureDetector( + onTap: () { + selected.forEach((element) { + element.togglePin(!element.isPinned!); + }); + selected = []; + if (this.mounted) setState(() {}); + }, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Icon( + selected[0].isPinned! ? Icons.star_outline : Icons.star, + color: context.textTheme.bodyText1!.color, + ), + ), + ), + GestureDetector( + onTap: () { + selected.forEach((element) { + if (element.isArchived!) { + ChatBloc().unArchiveChat(element); + } else { + ChatBloc().archiveChat(element); + } + }); + selected = []; + if (this.mounted) setState(() {}); + }, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Icon( + showArchived ? Icons.unarchive : Icons.archive, + color: context.textTheme.bodyText1!.color, + ), + ), + ), + if (selected[0].isArchived!) + GestureDetector( + onTap: () { + selected.forEach((element) { + ChatBloc().deleteChat(element); + Chat.deleteChat(element); + }); + selected = []; + if (this.mounted) setState(() {}); + }, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Icon( + Icons.delete_forever, + color: context.textTheme.bodyText1!.color, + ), + ), + ), + ], + ), + ], + ), + ), + ), + ), + backgroundColor: context.theme.backgroundColor, + body: Obx( + () { + if (!ChatBloc().hasChats.value) { + return Center( + child: Container( + padding: EdgeInsets.only(top: 50.0), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + "Loading chats...", + style: Theme.of(context).textTheme.subtitle1, + ), + ), + buildProgressIndicator(context, size: 15), + ], + ), + ), + ); + } + if (ChatBloc().chats.archivedHelper(showArchived).unknownSendersHelper(showUnknown).isEmpty) { + return Center( + child: Container( + padding: EdgeInsets.only(top: 50.0), + child: Text( + "You have no archived chats :(", + style: context.textTheme.subtitle1, + ), + ), + ); + } + return ListView.builder( + physics: ThemeSwitcher.getScrollPhysics(), + itemBuilder: (context, index) { + return Obx(() { + if (SettingsManager().settings.swipableConversationTiles.value) { + return Dismissible( + background: + Obx(() => slideRightBackground(ChatBloc().chats.archivedHelper(showArchived).unknownSendersHelper(showUnknown)[index])), + secondaryBackground: + Obx(() => slideLeftBackground(ChatBloc().chats.archivedHelper(showArchived).unknownSendersHelper(showUnknown)[index])), + // Each Dismissible must contain a Key. Keys allow Flutter to + // uniquely identify widgets. + key: UniqueKey(), + // Provide a function that tells the app + // what to do after an item has been swiped away. + onDismissed: (direction) async { + if (direction == DismissDirection.endToStart) { + if (SettingsManager().settings.materialLeftAction.value == MaterialSwipeAction.pin) { + await ChatBloc() + .chats + .archivedHelper(showArchived).unknownSendersHelper(showUnknown)[index] + .togglePin(!ChatBloc().chats.archivedHelper(showArchived).unknownSendersHelper(showUnknown)[index].isPinned!); + EventDispatcher().emit("refresh", null); + if (this.mounted) setState(() {}); + } else if (SettingsManager().settings.materialLeftAction.value == + MaterialSwipeAction.alerts) { + await ChatBloc().chats.archivedHelper(showArchived).unknownSendersHelper(showUnknown)[index].toggleMute( + ChatBloc().chats.archivedHelper(showArchived).unknownSendersHelper(showUnknown)[index].muteType != "mute"); + if (this.mounted) setState(() {}); + } else if (SettingsManager().settings.materialLeftAction.value == + MaterialSwipeAction.delete) { + ChatBloc().deleteChat(ChatBloc().chats.archivedHelper(showArchived).unknownSendersHelper(showUnknown)[index]); + Chat.deleteChat(ChatBloc().chats.archivedHelper(showArchived).unknownSendersHelper(showUnknown)[index]); + } else if (SettingsManager().settings.materialLeftAction.value == + MaterialSwipeAction.mark_read) { + ChatBloc().toggleChatUnread(ChatBloc().chats.archivedHelper(showArchived).unknownSendersHelper(showUnknown)[index], + !ChatBloc().chats.archivedHelper(showArchived).unknownSendersHelper(showUnknown)[index].hasUnreadMessage!); + } else { + if (ChatBloc().chats.archivedHelper(showArchived).unknownSendersHelper(showUnknown)[index].isArchived!) { + ChatBloc().unArchiveChat(ChatBloc().chats.archivedHelper(showArchived).unknownSendersHelper(showUnknown)[index]); + } else { + ChatBloc().archiveChat(ChatBloc().chats.archivedHelper(showArchived).unknownSendersHelper(showUnknown)[index]); + } + } + } else { + if (SettingsManager().settings.materialRightAction.value == MaterialSwipeAction.pin) { + await ChatBloc() + .chats + .archivedHelper(showArchived).unknownSendersHelper(showUnknown)[index] + .togglePin(!ChatBloc().chats.archivedHelper(showArchived).unknownSendersHelper(showUnknown)[index].isPinned!); + EventDispatcher().emit("refresh", null); + if (this.mounted) setState(() {}); + } else if (SettingsManager().settings.materialRightAction.value == + MaterialSwipeAction.alerts) { + await ChatBloc().chats.archivedHelper(showArchived).unknownSendersHelper(showUnknown)[index].toggleMute( + ChatBloc().chats.archivedHelper(showArchived).unknownSendersHelper(showUnknown)[index].muteType != "mute"); + if (this.mounted) setState(() {}); + } else if (SettingsManager().settings.materialRightAction.value == + MaterialSwipeAction.delete) { + ChatBloc().deleteChat(ChatBloc().chats.archivedHelper(showArchived).unknownSendersHelper(showUnknown)[index]); + Chat.deleteChat(ChatBloc().chats.archivedHelper(showArchived).unknownSendersHelper(showUnknown)[index]); + } else if (SettingsManager().settings.materialRightAction.value == + MaterialSwipeAction.mark_read) { + ChatBloc().toggleChatUnread(ChatBloc().chats.archivedHelper(showArchived).unknownSendersHelper(showUnknown)[index], + !ChatBloc().chats.archivedHelper(showArchived).unknownSendersHelper(showUnknown)[index].hasUnreadMessage!); + } else { + if (ChatBloc().chats.archivedHelper(showArchived).unknownSendersHelper(showUnknown)[index].isArchived!) { + ChatBloc().unArchiveChat(ChatBloc().chats.archivedHelper(showArchived).unknownSendersHelper(showUnknown)[index]); + } else { + ChatBloc().archiveChat(ChatBloc().chats.archivedHelper(showArchived).unknownSendersHelper(showUnknown)[index]); + } + } + } + }, + child: (!showArchived && ChatBloc().chats.archivedHelper(showArchived).unknownSendersHelper(showUnknown)[index].isArchived!) + ? Container() + : (showArchived && !ChatBloc().chats.archivedHelper(showArchived).unknownSendersHelper(showUnknown)[index].isArchived!) + ? Container() + : ConversationTile( + key: UniqueKey(), + chat: ChatBloc().chats.archivedHelper(showArchived).unknownSendersHelper(showUnknown)[index], + inSelectMode: selected.isNotEmpty, + selected: selected, + onSelect: (bool selected) { + if (selected) { + this.selected.add(ChatBloc().chats.archivedHelper(showArchived).unknownSendersHelper(showUnknown)[index]); + setState(() {}); + } else { + this.selected.removeWhere((element) => + element.guid == + ChatBloc().chats.archivedHelper(showArchived).unknownSendersHelper(showUnknown)[index].guid); + setState(() {}); + } + }, + )); + } else { + return ConversationTile( + key: UniqueKey(), + chat: ChatBloc().chats.archivedHelper(showArchived).unknownSendersHelper(showUnknown)[index], + inSelectMode: selected.isNotEmpty, + selected: selected, + onSelect: (bool selected) { + if (selected) { + this.selected.add(ChatBloc().chats.archivedHelper(showArchived).unknownSendersHelper(showUnknown)[index]); + setState(() {}); + } else { + this.selected.removeWhere((element) => + element.guid == ChatBloc().chats.archivedHelper(showArchived).unknownSendersHelper(showUnknown)[index].guid); + setState(() {}); + } + }, + ); + } + }); + }, + itemCount: ChatBloc().chats.archivedHelper(showArchived).unknownSendersHelper(showUnknown).length, + ); + }, + ), + floatingActionButton: selected.isEmpty && !SettingsManager().settings.moveChatCreatorToHeader.value + ? widget.parent.buildFloatingActionButton() + : null, + ), + ), + ); + } + + Widget buildForLandscape(BuildContext context, Widget chatList) { + return VerticalSplitView( + dividerWidth: 10.0, + initialRatio: 0.4, + minRatio: 0.33, + maxRatio: 0.5, + allowResize: true, + left: LayoutBuilder( + builder: (context, constraints) { + CustomNavigator.maxWidthLeft = constraints.maxWidth; + return WillPopScope( + onWillPop: () async { + Get.back(id: 1); + return false; + }, + child: Navigator( + key: Get.nestedKey(1), + onPopPage: (route, _) { + return false; + }, + pages: [CupertinoPage(name: "initial", child: chatList)], + ), + ); + } + ), + right: LayoutBuilder( + builder: (context, constraints) { + CustomNavigator.maxWidthRight = constraints.maxWidth; + return WillPopScope( + onWillPop: () async { + Get.back(id: 2); + return false; + }, + child: Navigator( + key: Get.nestedKey(2), + onPopPage: (route, _) { + return false; + }, + pages: [CupertinoPage(name: "initial", child: Scaffold( + backgroundColor: context.theme.backgroundColor, + extendBodyBehindAppBar: true, + body: Center( + child: Container( + child: Text("Select a chat from the list", style: Theme.of(Get.context!).textTheme.subtitle1!.copyWith(fontSize: 18)) + ), + ), + ))], + ), + ); + } + ), + ); + } + + Widget buildForDevice() { + bool showAltLayout = !context.isPhone || context.isLandscape; + Widget chatList = buildChatList(); + if (showAltLayout && !widget.parent.widget.showUnknownSenders && !widget.parent.widget.showArchivedChats) { + return buildForLandscape(context, chatList); + } + + return chatList; + } +} \ No newline at end of file diff --git a/lib/layouts/conversation_list/pinned_conversation_tile.dart b/lib/layouts/conversation_list/pinned_conversation_tile.dart index 34afa20d6..a15917388 100644 --- a/lib/layouts/conversation_list/pinned_conversation_tile.dart +++ b/lib/layouts/conversation_list/pinned_conversation_tile.dart @@ -1,8 +1,10 @@ import 'dart:async'; import 'dart:io'; +import 'dart:math'; -import 'package:assorted_layout_widgets/assorted_layout_widgets.dart'; import 'package:bluebubbles/blocs/chat_bloc.dart'; +import 'package:bluebubbles/helpers/logger.dart'; +import 'package:bluebubbles/helpers/navigator.dart'; import 'package:bluebubbles/helpers/socket_singletons.dart'; import 'package:bluebubbles/helpers/utils.dart'; import 'package:bluebubbles/layouts/conversation_list/pinned_tile_text_bubble.dart'; @@ -10,7 +12,6 @@ import 'package:bluebubbles/layouts/conversation_view/conversation_view.dart'; import 'package:bluebubbles/layouts/widgets/contact_avatar_group_widget.dart'; import 'package:bluebubbles/layouts/widgets/message_widget/reactions_widget.dart'; import 'package:bluebubbles/layouts/widgets/message_widget/typing_indicator.dart'; -import 'package:bluebubbles/layouts/widgets/theme_switcher/theme_switcher.dart'; import 'package:bluebubbles/managers/current_chat.dart'; import 'package:bluebubbles/managers/new_message_manager.dart'; import 'package:bluebubbles/managers/settings_manager.dart'; @@ -20,8 +21,8 @@ import 'package:bluebubbles/repository/models/message.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; -import 'package:flutter_svg/svg.dart'; import 'package:get/get.dart'; class PinnedConversationTile extends StatefulWidget { @@ -80,7 +81,7 @@ class _PinnedConversationTileState extends State with Au try { await fetchChatSingleton(widget.chat.guid!); } catch (ex) { - debugPrint(ex.toString()); + Logger.error(ex.toString()); } this.setNewChatData(forceUpdate: true); @@ -118,15 +119,12 @@ class _PinnedConversationTileState extends State with Au } void onTapUp(details) { - Navigator.of(context).pushAndRemoveUntil( - CupertinoPageRoute( - builder: (BuildContext context) { - return ConversationView( - chat: widget.chat, - existingAttachments: widget.existingAttachments, - existingText: widget.existingText, - ); - }, + CustomNavigator.pushAndRemoveUntil( + context, + ConversationView( + chat: widget.chat, + existingAttachments: widget.existingAttachments, + existingText: widget.existingText, ), (route) => route.isFirst, ); @@ -148,52 +146,22 @@ class _PinnedConversationTileState extends State with Au title = widget.chat.fakeParticipants.length == 1 ? widget.chat.fakeParticipants[0] : "Group Chat"; else if (hideInfo) style = style.copyWith(color: Colors.transparent); - return Row( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - if (!(widget.chat.isMuted ?? false) && (widget.chat.hasUnreadMessage ?? false)) - Container( - width: 8, - height: 8, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), - color: context.theme.primaryColor.withOpacity(0.8), - ), - margin: EdgeInsets.only(right: 3)), - if (widget.chat.isMuted ?? false) - Container( - margin: EdgeInsets.only(right: 3), - child: SvgPicture.asset( - "assets/icon/moon.svg", - color: widget.chat.hasUnreadMessage! - ? context.theme.primaryColor.withOpacity(0.8) - : context.textTheme.subtitle1!.color, - width: 8, - height: 8, - ), - ), - Flexible( - child: TextOneLine( - title!, - style: style, - overflow: TextOverflow.ellipsis, - ), - ), - ], + return Text( + title!, + style: style, + overflow: TextOverflow.ellipsis, + textAlign: TextAlign.center, + maxLines: 2, ); } void onTap() { - Navigator.of(context).pushAndRemoveUntil( - ThemeSwitcher.buildPageRoute( - builder: (BuildContext context) { - return ConversationView( - chat: widget.chat, - existingAttachments: widget.existingAttachments, - existingText: widget.existingText, - ); - }, + CustomNavigator.pushAndRemoveUntil( + context, + ConversationView( + chat: widget.chat, + existingAttachments: widget.existingAttachments, + existingText: widget.existingText, ), (route) => route.isFirst, ); @@ -240,7 +208,7 @@ class _PinnedConversationTileState extends State with Au Padding( padding: EdgeInsets.only(right: 10), child: Icon( - widget.chat.isPinned! ? Icons.star_outline : Icons.star, + widget.chat.isPinned! ? CupertinoIcons.pin_slash : CupertinoIcons.pin, color: context.textTheme.bodyText1!.color, ), ), @@ -258,7 +226,7 @@ class _PinnedConversationTileState extends State with Au child: GestureDetector( behavior: HitTestBehavior.opaque, onTap: () async { - await widget.chat.toggleMute(!widget.chat.isMuted!); + await widget.chat.toggleMute(widget.chat.muteType != "mute"); if (this.mounted) setState(() {}); Navigator.pop(context); }, @@ -269,11 +237,12 @@ class _PinnedConversationTileState extends State with Au Padding( padding: EdgeInsets.only(right: 10), child: Icon( - widget.chat.isMuted! ? Icons.notifications_active : Icons.notifications_off, + widget.chat.muteType == "mute" ? CupertinoIcons.bell : CupertinoIcons.bell_slash, color: context.textTheme.bodyText1!.color, ), ), - Text(widget.chat.isMuted! ? 'Show Alerts' : 'Hide Alerts', style: context.textTheme.bodyText1!), + Text(widget.chat.muteType == "mute" ? 'Show Alerts' : 'Hide Alerts', + style: context.textTheme.bodyText1!), ], ), ), @@ -295,7 +264,9 @@ class _PinnedConversationTileState extends State with Au Padding( padding: EdgeInsets.only(right: 10), child: Icon( - widget.chat.hasUnreadMessage! ? Icons.mark_chat_read : Icons.mark_chat_unread, + widget.chat.hasUnreadMessage! + ? CupertinoIcons.person_crop_circle_badge_xmark + : CupertinoIcons.person_crop_circle_badge_checkmark, color: context.textTheme.bodyText1!.color, ), ), @@ -326,7 +297,7 @@ class _PinnedConversationTileState extends State with Au Padding( padding: EdgeInsets.only(right: 10), child: Icon( - widget.chat.isArchived! ? Icons.unarchive : Icons.archive, + widget.chat.isArchived! ? CupertinoIcons.tray_arrow_up : CupertinoIcons.tray_arrow_down, color: context.textTheme.bodyText1!.color, ), ), @@ -368,102 +339,155 @@ class _PinnedConversationTileState extends State with Au }, child: Padding( padding: EdgeInsets.only( - top: 20, - left: 10, - right: 10, - bottom: 10, + top: 5, + left: 15, + right: 15, + bottom: 5, ), - child: Obx( - () { - int colCount = SettingsManager().settings.pinColumnsPortrait.value; - if (context.mediaQuery.orientation != Orientation.portrait) { - colCount = (colCount / context.mediaQuerySize.height * context.mediaQuerySize.width).floor(); - } - int spaceBetween = (colCount - 1) * 20; - int spaceAround = 20; + child: LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + return Obx( + () { + // Great math right here + double availableWidth = constraints.maxWidth; + int colCount = SettingsManager().settings.pinColumnsPortrait.value; + double spaceBetween = (colCount - 1) * 30; + double maxWidth = ((availableWidth - spaceBetween) / colCount).floorToDouble(); - // Great math right here - double maxWidth = ((context.mediaQuerySize.width - spaceBetween - spaceAround) / colCount).floorToDouble(); - return ConstrainedBox( - constraints: BoxConstraints( - maxWidth: maxWidth, - ), - child: Stack( - clipBehavior: Clip.none, - alignment: Alignment.center, - children: [ - Column( - children: [ - ContactAvatarGroupWidget( - chat: widget.chat, - size: (context.mediaQueryShortestSide - 150) / 3, - editable: false, - onTap: this.onTapUpBypass, - ), - Container( - padding: EdgeInsets.only( - top: 10, - left: 10, - right: 10, - ), - child: buildSubtitle(), - ), - ], + Color alphaWithoutAlpha = Color.fromARGB( + 255, + (context.theme.primaryColor.red * 0.8).toInt() + (context.theme.backgroundColor.red * 0.2).toInt(), + (context.theme.primaryColor.green * 0.8).toInt() + + (context.theme.backgroundColor.green * 0.2).toInt(), + (context.theme.primaryColor.blue * 0.8).toInt() + (context.theme.backgroundColor.blue * 0.2).toInt(), + ); + return ConstrainedBox( + constraints: BoxConstraints( + maxWidth: maxWidth, ), - StreamBuilder>( - stream: CurrentChat.getCurrentChat(widget.chat)?.stream as Stream>?, - builder: (BuildContext context, AsyncSnapshot snapshot) { - if (snapshot.connectionState == ConnectionState.active && - snapshot.hasData && - snapshot.data["type"] == CurrentChatEvent.TypingStatus) { - showTypingIndicator.value = snapshot.data["data"]; - } - if (showTypingIndicator.value) { - return Positioned( - top: -11, - right: -13, - child: ConstrainedBox( - constraints: BoxConstraints(maxHeight: 32), - child: FittedBox( - child: TypingIndicator( - visible: true, + child: Stack( + clipBehavior: Clip.none, + alignment: Alignment.center, + children: [ + Column( + children: [ + Stack( + children: [ + ContactAvatarGroupWidget( + chat: widget.chat, + size: maxWidth, + editable: false, + onTap: this.onTapUpBypass, ), + if (widget.chat.muteType != "mute" && (widget.chat.hasUnreadMessage ?? false)) + Positioned( + left: sqrt(maxWidth) - maxWidth * 0.05 * sqrt(2), + top: sqrt(maxWidth) - maxWidth * 0.05 * sqrt(2), + child: Container( + width: maxWidth * 0.2, + height: maxWidth * 0.2, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(maxWidth * 0.1), + color: alphaWithoutAlpha, + ), + margin: EdgeInsets.only(right: 3), + ), + ), + if (widget.chat.muteType == "mute") + Positioned( + left: sqrt(maxWidth) - maxWidth * 0.05 * sqrt(2), + top: sqrt(maxWidth) - maxWidth * 0.05 * sqrt(2), + child: Stack( + alignment: Alignment.center, + children: [ + Container( + width: maxWidth * 0.2, + height: maxWidth * 0.2, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(maxWidth * 0.1), + color: (widget.chat.hasUnreadMessage ?? false) + ? alphaWithoutAlpha + : context.textTheme.subtitle1!.color, + ), + ), + Icon( + CupertinoIcons.bell_slash_fill, + size: maxWidth * 0.14, + color: Colors.white, + ), + ], + ), + ), + ], + ), + Container( + padding: EdgeInsets.only( + top: maxWidth * 0.075, + ), + child: ConstrainedBox( + constraints: BoxConstraints(minHeight: context.textTheme.subtitle1!.fontSize! * 2), + child: buildSubtitle(), ), ), - ); - } - return Container(); - }, - ), - FutureBuilder( - future: widget.chat.latestMessage, - builder: (BuildContext context, AsyncSnapshot snapshot) { - if (!(widget.chat.hasUnreadMessage ?? false)) return Container(); - if (showTypingIndicator.value) return Container(); - if (!snapshot.hasData) return Container(); - Message message = snapshot.data; - if ([null, ""].contains(message.associatedMessageGuid) || (message.isFromMe ?? false)) { - return Container(); - } - return Positioned( - top: -12, - right: -8, - child: ReactionsWidget( - associatedMessages: [message], - bigPin: true, + ], + ), + StreamBuilder>( + stream: CurrentChat.getCurrentChat(widget.chat)?.stream as Stream>?, + builder: (BuildContext context, AsyncSnapshot snapshot) { + if (snapshot.connectionState == ConnectionState.active && + snapshot.hasData && + snapshot.data["type"] == CurrentChatEvent.TypingStatus) { + showTypingIndicator.value = snapshot.data["data"]; + } + if (showTypingIndicator.value) { + return Positioned( + top: -sqrt(maxWidth / 2), + right: -sqrt(maxWidth / 2) - maxWidth * 0.25, + child: ConstrainedBox( + constraints: BoxConstraints(maxHeight: 32), + child: FittedBox( + child: TypingIndicator( + visible: true, + ), + ), + ), + ); + } + return Container(); + }, + ), + FutureBuilder( + future: widget.chat.latestMessage, + builder: (BuildContext context, AsyncSnapshot snapshot) { + if (!(widget.chat.hasUnreadMessage ?? false)) return Container(); + if (showTypingIndicator.value) return Container(); + if (!snapshot.hasData) return Container(); + Message message = snapshot.data; + if ([null, ""].contains(message.associatedMessageGuid) || (message.isFromMe ?? false)) { + return Container(); + } + return Positioned( + top: -sqrt(maxWidth / 2), + right: -sqrt(maxWidth / 2) - maxWidth * 0.15, + child: ReactionsWidget( + associatedMessages: [message], + bigPin: true, + ), + ); + }, + ), + Positioned( + bottom: context.textTheme.subtitle1!.fontSize! * 2 + maxWidth * 0.05, + width: maxWidth, + child: PinnedTileTextBubble( + chat: widget.chat, + size: maxWidth, ), - ); - }, - ), - Positioned( - bottom: 20, - width: maxWidth, - child: PinnedTileTextBubble( - chat: widget.chat, - ), + ), + ], ), - ], - ), + ); + }, ); }, ), diff --git a/lib/layouts/conversation_list/pinned_tile_text_bubble.dart b/lib/layouts/conversation_list/pinned_tile_text_bubble.dart index 89b61ba7e..e4c297d80 100644 --- a/lib/layouts/conversation_list/pinned_tile_text_bubble.dart +++ b/lib/layouts/conversation_list/pinned_tile_text_bubble.dart @@ -15,9 +15,11 @@ class PinnedTileTextBubble extends StatefulWidget { PinnedTileTextBubble({ Key? key, required this.chat, + required this.size, }) : super(key: key); final Chat chat; + final double size; @override _PinnedTileTextBubbleState createState() => _PinnedTileTextBubbleState(); @@ -81,14 +83,14 @@ class _PinnedTileTextBubbleState extends State with Automa padding: EdgeInsets.only( left: showTail ? leftSide - ? 15 - : 5 - : 10, + ? widget.size * 0.06 + : widget.size * 0.02 + : widget.size * 0.04, right: showTail ? leftSide - ? 5 - : 15 - : 10, + ? widget.size * 0.02 + : widget.size * 0.06 + : widget.size * 0.04, ), child: Stack( clipBehavior: Clip.none, diff --git a/lib/layouts/conversation_list/samsung_conversation_list.dart b/lib/layouts/conversation_list/samsung_conversation_list.dart new file mode 100644 index 000000000..ff1100c90 --- /dev/null +++ b/lib/layouts/conversation_list/samsung_conversation_list.dart @@ -0,0 +1,824 @@ +import 'dart:io'; +import 'dart:ui'; + +import 'package:bluebubbles/blocs/chat_bloc.dart'; +import 'package:bluebubbles/helpers/constants.dart'; +import 'package:bluebubbles/helpers/navigator.dart'; +import 'package:bluebubbles/helpers/ui_helpers.dart'; +import 'package:bluebubbles/helpers/utils.dart'; +import 'package:bluebubbles/layouts/conversation_list/conversation_list.dart'; +import 'package:bluebubbles/layouts/conversation_list/conversation_tile.dart'; +import 'package:bluebubbles/layouts/conversation_view/conversation_view.dart'; +import 'package:bluebubbles/layouts/search/search_view.dart'; +import 'package:bluebubbles/layouts/widgets/vertical_split_view.dart'; +import 'package:bluebubbles/managers/current_chat.dart'; +import 'package:bluebubbles/managers/event_dispatcher.dart'; +import 'package:bluebubbles/managers/method_channel_interface.dart'; +import 'package:bluebubbles/managers/settings_manager.dart'; +import 'package:bluebubbles/repository/models/chat.dart'; +import 'package:bluebubbles/main.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:get/get.dart'; + +class SamsungConversationList extends StatefulWidget { + SamsungConversationList({Key? key, required this.parent}) : super(key: key); + + final ConversationListState parent; + + @override + _SamsungState createState() => _SamsungState(); +} + +class _SamsungState extends State { + List selected = []; + bool openedChatAlready = false; + + bool hasPinnedChat() { + for (var i = 0; i < ChatBloc().chats.archivedHelper(widget.parent.widget.showArchivedChats).unknownSendersHelper(widget.parent.widget.showUnknownSenders).length; i++) { + if (ChatBloc().chats.archivedHelper(widget.parent.widget.showArchivedChats).unknownSendersHelper(widget.parent.widget.showUnknownSenders)[i].isPinned!) { + widget.parent.hasPinnedChats = true; + return true; + } else { + return false; + } + } + return false; + } + + bool hasNormalChats() { + int counter = 0; + for (var i = 0; i < ChatBloc().chats.archivedHelper(widget.parent.widget.showArchivedChats).unknownSendersHelper(widget.parent.widget.showUnknownSenders).length; i++) { + if (ChatBloc().chats.archivedHelper(widget.parent.widget.showArchivedChats).unknownSendersHelper(widget.parent.widget.showUnknownSenders)[i].isPinned!) { + counter++; + } else {} + } + if (counter == ChatBloc().chats.archivedHelper(widget.parent.widget.showArchivedChats).unknownSendersHelper(widget.parent.widget.showUnknownSenders).length) { + return false; + } else { + return true; + } + } + + Widget slideLeftBackground(Chat chat) { + return Container( + color: SettingsManager().settings.materialLeftAction.value == MaterialSwipeAction.pin + ? Colors.yellow[800] + : SettingsManager().settings.materialLeftAction.value == MaterialSwipeAction.alerts + ? Colors.purple + : SettingsManager().settings.materialLeftAction.value == MaterialSwipeAction.delete + ? Colors.red + : SettingsManager().settings.materialLeftAction.value == MaterialSwipeAction.mark_read + ? Colors.blue + : Colors.red, + child: Align( + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Icon( + SettingsManager().settings.materialLeftAction.value == MaterialSwipeAction.pin + ? (chat.muteType == "mute" ? Icons.star_outline : Icons.star) + : SettingsManager().settings.materialLeftAction.value == MaterialSwipeAction.alerts + ? (chat.muteType == "mute" ? Icons.notifications_active : Icons.notifications_off) + : SettingsManager().settings.materialLeftAction.value == MaterialSwipeAction.delete + ? Icons.delete_forever + : SettingsManager().settings.materialLeftAction.value == MaterialSwipeAction.mark_read + ? (chat.hasUnreadMessage! ? Icons.mark_chat_read : Icons.mark_chat_unread) + : (chat.isArchived! ? Icons.unarchive : Icons.archive), + color: Colors.white, + ), + Text( + SettingsManager().settings.materialLeftAction.value == MaterialSwipeAction.pin + ? (chat.isPinned! ? " Unpin" : " Pin") + : SettingsManager().settings.materialLeftAction.value == MaterialSwipeAction.alerts + ? (chat.muteType == "mute" ? ' Show Alerts' : ' Hide Alerts') + : SettingsManager().settings.materialLeftAction.value == MaterialSwipeAction.delete + ? " Delete" + : SettingsManager().settings.materialLeftAction.value == MaterialSwipeAction.mark_read + ? (chat.hasUnreadMessage! ? ' Mark Read' : ' Mark Unread') + : (chat.isArchived! ? ' UnArchive' : ' Archive'), + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.w700, + ), + textAlign: TextAlign.right, + ), + SizedBox( + width: 20, + ), + ], + ), + alignment: Alignment.centerRight, + ), + ); + } + + Widget slideRightBackground(Chat chat) { + return Container( + color: SettingsManager().settings.materialRightAction.value == MaterialSwipeAction.pin + ? Colors.yellow[800] + : SettingsManager().settings.materialRightAction.value == MaterialSwipeAction.alerts + ? Colors.purple + : SettingsManager().settings.materialRightAction.value == MaterialSwipeAction.delete + ? Colors.red + : SettingsManager().settings.materialRightAction.value == MaterialSwipeAction.mark_read + ? Colors.blue + : Colors.red, + child: Align( + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + SizedBox( + width: 20, + ), + Icon( + SettingsManager().settings.materialRightAction.value == MaterialSwipeAction.pin + ? (chat.isPinned! ? Icons.star_outline : Icons.star) + : SettingsManager().settings.materialRightAction.value == MaterialSwipeAction.alerts + ? (chat.muteType == "mute" ? Icons.notifications_active : Icons.notifications_off) + : SettingsManager().settings.materialRightAction.value == MaterialSwipeAction.delete + ? Icons.delete_forever + : SettingsManager().settings.materialRightAction.value == MaterialSwipeAction.mark_read + ? (chat.hasUnreadMessage! ? Icons.mark_chat_read : Icons.mark_chat_unread) + : (chat.isArchived! ? Icons.unarchive : Icons.archive), + color: Colors.white, + ), + Text( + SettingsManager().settings.materialRightAction.value == MaterialSwipeAction.pin + ? (chat.isPinned! ? " Unpin" : " Pin") + : SettingsManager().settings.materialRightAction.value == MaterialSwipeAction.alerts + ? (chat.muteType == "mute" ? ' Show Alerts' : ' Hide Alerts') + : SettingsManager().settings.materialRightAction.value == MaterialSwipeAction.delete + ? " Delete" + : SettingsManager().settings.materialRightAction.value == MaterialSwipeAction.mark_read + ? (chat.hasUnreadMessage! ? ' Mark Read' : ' Mark Unread') + : (chat.isArchived! ? ' UnArchive' : ' Archive'), + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.w700, + ), + textAlign: TextAlign.left, + ), + ], + ), + alignment: Alignment.centerLeft, + ), + ); + } + + Future openLastChat(BuildContext context) async { + if (ChatBloc().chatRequest != null + && prefs.getString('lastOpenedChat') != null + && (!context.isPhone || context.isLandscape) + && CurrentChat.activeChat?.chat.guid != prefs.getString('lastOpenedChat')) { + await ChatBloc().chatRequest!.future; + CustomNavigator.pushAndRemoveUntil( + context, + ConversationView( + chat: ChatBloc().chats.firstWhere((e) => e.guid == prefs.getString('lastOpenedChat')) + ), + (route) => route.isFirst, + ); + } + } + + @override + Widget build(BuildContext context) { + if (!openedChatAlready) { + Future.delayed(Duration.zero, () => openLastChat(context)); + openedChatAlready = true; + } + return AnnotatedRegion( + value: SystemUiOverlayStyle( + systemNavigationBarColor: context.theme.backgroundColor, // navigation bar color + systemNavigationBarIconBrightness: + context.theme.backgroundColor.computeLuminance() > 0.5 ? Brightness.dark : Brightness.light, + statusBarColor: Colors.transparent, // status bar color + ), + child: buildForDevice(), + ); + } + + Widget buildChatList() { + bool showArchived = widget.parent.widget.showArchivedChats; + bool showUnknown = widget.parent.widget.showUnknownSenders; + return Obx( + () => WillPopScope( + onWillPop: () async { + if (selected.isNotEmpty) { + selected = []; + setState(() {}); + return false; + } + return true; + }, + child: Scaffold( + appBar: PreferredSize( + preferredSize: Size.fromHeight(60), + child: AnimatedSwitcher( + duration: Duration(milliseconds: 500), + child: selected.isEmpty + ? AppBar( + shadowColor: Colors.transparent, + iconTheme: IconThemeData(color: context.theme.primaryColor), + brightness: ThemeData.estimateBrightnessForColor(context.theme.backgroundColor), + bottom: PreferredSize( + child: Container( + color: context.theme.dividerColor, + height: 0, + ), + preferredSize: Size.fromHeight(0.5), + ), + title: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + ...widget.parent.getHeaderTextWidgets(size: 20), + ...widget.parent.getConnectionIndicatorWidgets(), + widget.parent.getSyncIndicatorWidget(), + ], + ), + actions: [ + (!showArchived && !showUnknown) + ? GestureDetector( + onTap: () async { + CustomNavigator.push( + context, + SearchView() + ); + }, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Icon( + Icons.search, + color: context.textTheme.bodyText1!.color, + ), + ), + ) + : Container(), + (SettingsManager().settings.moveChatCreatorToHeader.value && !showArchived && !showUnknown + ? GestureDetector( + onTap: () { + CustomNavigator.pushAndRemoveUntil( + context, + ConversationView( + isCreator: true, + ), + (route) => route.isFirst, + ); + }, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Icon( + Icons.create, + color: context.textTheme.bodyText1!.color, + ), + ), + ) + : Container()), + (SettingsManager().settings.moveChatCreatorToHeader.value + && SettingsManager().settings.cameraFAB.value + && !showArchived && !showUnknown + ? GestureDetector( + onTap: () async { + String appDocPath = SettingsManager().appDocDir.path; + String ext = ".png"; + File file = new File("$appDocPath/attachments/" + randomString(16) + ext); + await file.create(recursive: true); + + // Take the picture after opening the camera + await MethodChannelInterface().invokeMethod("open-camera", {"path": file.path, "type": "camera"}); + + // If we don't get data back, return outta here + if (!file.existsSync()) return; + if (file.statSync().size == 0) { + file.deleteSync(); + return; + } + + widget.parent.openNewChatCreator(existing: [file]); + }, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Icon( + Icons.photo_camera, + color: context.textTheme.bodyText1!.color, + ), + ), + ) + : Container()), + Padding( + padding: EdgeInsets.only(right: 20), + child: Padding( + padding: EdgeInsets.symmetric(vertical: 15.5), + child: Container( + width: 40, + child: widget.parent.buildSettingsButton(), + ), + ), + ), + ], + backgroundColor: context.theme.backgroundColor, + ) + : Padding( + padding: const EdgeInsets.all(20.0), + child: Column( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + if (selected.length <= 1) + GestureDetector( + onTap: () { + selected.forEach((element) async { + await element.toggleMute(element.muteType != "mute"); + }); + + selected = []; + if (this.mounted) setState(() {}); + }, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Icon( + Icons.notifications_off, + color: context.textTheme.bodyText1!.color, + ), + ), + ), + GestureDetector( + onTap: () { + selected.forEach((element) { + if (element.isArchived!) { + ChatBloc().unArchiveChat(element); + } else { + ChatBloc().archiveChat(element); + } + }); + selected = []; + if (this.mounted) setState(() {}); + }, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Icon( + showArchived ? Icons.unarchive : Icons.archive, + color: context.textTheme.bodyText1!.color, + ), + ), + ), + GestureDetector( + onTap: () { + selected.forEach((element) async { + await element.togglePin(!element.isPinned!); + }); + + selected = []; + if (this.mounted) setState(() {}); + }, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Icon( + Icons.star, + color: context.textTheme.bodyText1!.color, + ), + ), + ), + ], + ), + ], + ), + ), + ), + ), + backgroundColor: context.theme.backgroundColor, + body: Obx(() { + if (!ChatBloc().hasChats.value) { + return Center( + child: Container( + padding: EdgeInsets.only(top: 50.0), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + "Loading chats...", + style: Theme.of(context).textTheme.subtitle1, + ), + ), + buildProgressIndicator(context, size: 15), + ], + ), + ), + ); + } + if (ChatBloc().chats.archivedHelper(showArchived).unknownSendersHelper(showUnknown).isEmpty) { + return Center( + child: Container( + padding: EdgeInsets.only(top: 50.0), + child: Text( + "You have no archived chats :(", + style: context.textTheme.subtitle1, + ), + ), + ); + } + + bool hasPinned = hasPinnedChat(); + return SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + if (hasPinned) + Container( + height: 20.0, + decoration: BoxDecoration( + border: Border.all( + color: Colors.transparent, + ), + borderRadius: BorderRadius.all(Radius.circular(20)), + ), + ), + if (hasPinned) + Container( + padding: EdgeInsets.all(6.0), + decoration: new BoxDecoration( + color: context.theme.accentColor, + borderRadius: BorderRadius.circular(20), + ), + child: ListView.builder( + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + itemBuilder: (context, index) { + return Obx(() { + if (SettingsManager().settings.swipableConversationTiles.value) { + return Dismissible( + background: Obx( + () => slideRightBackground(ChatBloc().chats.archivedHelper(showArchived).unknownSendersHelper(showUnknown)[index])), + secondaryBackground: Obx( + () => slideLeftBackground(ChatBloc().chats.archivedHelper(showArchived).unknownSendersHelper(showUnknown)[index])), + // Each Dismissible must contain a Key. Keys allow Flutter to + // uniquely identify widgets. + key: UniqueKey(), + // Provide a function that tells the app + // what to do after an item has been swiped away. + onDismissed: (direction) async { + if (direction == DismissDirection.endToStart) { + if (SettingsManager().settings.materialLeftAction.value == + MaterialSwipeAction.pin) { + await ChatBloc() + .chats + .archivedHelper(showArchived).unknownSendersHelper(showUnknown)[index] + .togglePin(!ChatBloc().chats.archivedHelper(showArchived).unknownSendersHelper(showUnknown)[index].isPinned!); + EventDispatcher().emit("refresh", null); + if (this.mounted) setState(() {}); + } else if (SettingsManager().settings.materialLeftAction.value == + MaterialSwipeAction.alerts) { + await ChatBloc().chats.archivedHelper(showArchived).unknownSendersHelper(showUnknown)[index].toggleMute( + ChatBloc().chats.archivedHelper(showArchived).unknownSendersHelper(showUnknown)[index].muteType != "mute"); + if (this.mounted) setState(() {}); + } else if (SettingsManager().settings.materialLeftAction.value == + MaterialSwipeAction.delete) { + ChatBloc().deleteChat(ChatBloc().chats.archivedHelper(showArchived).unknownSendersHelper(showUnknown)[index]); + Chat.deleteChat(ChatBloc().chats.archivedHelper(showArchived).unknownSendersHelper(showUnknown)[index]); + } else if (SettingsManager().settings.materialLeftAction.value == + MaterialSwipeAction.mark_read) { + ChatBloc().toggleChatUnread( + ChatBloc().chats.archivedHelper(showArchived).unknownSendersHelper(showUnknown)[index], + !ChatBloc().chats.archivedHelper(showArchived).unknownSendersHelper(showUnknown)[index].hasUnreadMessage!); + } else { + if (ChatBloc().chats.archivedHelper(showArchived).unknownSendersHelper(showUnknown)[index].isArchived!) { + ChatBloc() + .unArchiveChat(ChatBloc().chats.archivedHelper(showArchived).unknownSendersHelper(showUnknown)[index]); + } else { + ChatBloc().archiveChat(ChatBloc().chats.archivedHelper(showArchived).unknownSendersHelper(showUnknown)[index]); + } + } + } else { + if (SettingsManager().settings.materialRightAction.value == + MaterialSwipeAction.pin) { + await ChatBloc() + .chats + .archivedHelper(showArchived).unknownSendersHelper(showUnknown)[index] + .togglePin(!ChatBloc().chats.archivedHelper(showArchived).unknownSendersHelper(showUnknown)[index].isPinned!); + EventDispatcher().emit("refresh", null); + if (this.mounted) setState(() {}); + } else if (SettingsManager().settings.materialRightAction.value == + MaterialSwipeAction.alerts) { + await ChatBloc().chats.archivedHelper(showArchived).unknownSendersHelper(showUnknown)[index].toggleMute( + ChatBloc().chats.archivedHelper(showArchived).unknownSendersHelper(showUnknown)[index].muteType != "mute"); + if (this.mounted) setState(() {}); + } else if (SettingsManager().settings.materialRightAction.value == + MaterialSwipeAction.delete) { + ChatBloc().deleteChat(ChatBloc().chats.archivedHelper(showArchived).unknownSendersHelper(showUnknown)[index]); + Chat.deleteChat(ChatBloc().chats.archivedHelper(showArchived).unknownSendersHelper(showUnknown)[index]); + } else if (SettingsManager().settings.materialRightAction.value == + MaterialSwipeAction.mark_read) { + ChatBloc().toggleChatUnread( + ChatBloc().chats.archivedHelper(showArchived).unknownSendersHelper(showUnknown)[index], + !ChatBloc().chats.archivedHelper(showArchived).unknownSendersHelper(showUnknown)[index].hasUnreadMessage!); + } else { + if (ChatBloc().chats.archivedHelper(showArchived).unknownSendersHelper(showUnknown)[index].isArchived!) { + ChatBloc() + .unArchiveChat(ChatBloc().chats.archivedHelper(showArchived).unknownSendersHelper(showUnknown)[index]); + } else { + ChatBloc().archiveChat(ChatBloc().chats.archivedHelper(showArchived).unknownSendersHelper(showUnknown)[index]); + } + } + } + }, + child: (!showArchived && + ChatBloc().chats.archivedHelper(showArchived).unknownSendersHelper(showUnknown)[index].isArchived!) + ? Container() + : (showArchived && + !ChatBloc().chats.archivedHelper(showArchived).unknownSendersHelper(showUnknown)[index].isArchived!) + ? Container() + : ChatBloc().chats.archivedHelper(showArchived).unknownSendersHelper(showUnknown)[index].isPinned! + ? ConversationTile( + key: UniqueKey(), + chat: ChatBloc().chats.archivedHelper(showArchived).unknownSendersHelper(showUnknown)[index], + inSelectMode: selected.isNotEmpty, + selected: selected, + onSelect: (bool selected) { + if (selected) { + this + .selected + .add(ChatBloc().chats.archivedHelper(showArchived).unknownSendersHelper(showUnknown)[index]); + } else { + this.selected.removeWhere((element) => + element.guid == + ChatBloc().chats.archivedHelper(showArchived).unknownSendersHelper(showUnknown)[index].guid); + } + + if (this.mounted) setState(() {}); + }, + ) + : Container(), + ); + } else { + if (!showArchived && ChatBloc().chats.archivedHelper(showArchived).unknownSendersHelper(showUnknown)[index].isArchived!) + return Container(); + if (showArchived && !ChatBloc().chats.archivedHelper(showArchived).unknownSendersHelper(showUnknown)[index].isArchived!) + return Container(); + if (ChatBloc().chats.archivedHelper(showArchived).unknownSendersHelper(showUnknown)[index].isPinned!) { + return ConversationTile( + key: UniqueKey(), + chat: ChatBloc().chats.archivedHelper(showArchived).unknownSendersHelper(showUnknown)[index], + inSelectMode: selected.isNotEmpty, + selected: selected, + onSelect: (bool selected) { + if (selected) { + this.selected.add(ChatBloc().chats.archivedHelper(showArchived).unknownSendersHelper(showUnknown)[index]); + if (this.mounted) setState(() {}); + } else { + this.selected.removeWhere((element) => + element.guid == ChatBloc().chats.archivedHelper(showArchived).unknownSendersHelper(showUnknown)[index].guid); + if (this.mounted) setState(() {}); + } + }, + ); + } + return Container(); + } + }); + }, + itemCount: ChatBloc().chats.archivedHelper(showArchived).unknownSendersHelper(showUnknown).length, + ), + ), + if (hasNormalChats()) + Container( + height: 20.0, + decoration: BoxDecoration( + border: Border.all( + color: Colors.transparent, + ), + borderRadius: BorderRadius.all(Radius.circular(20))), + ), + if (hasNormalChats()) + Container( + padding: const EdgeInsets.all(6.0), + decoration: new BoxDecoration( + color: context.theme.accentColor, + borderRadius: new BorderRadius.only( + topLeft: const Radius.circular(20.0), + topRight: const Radius.circular(20.0), + bottomLeft: const Radius.circular(20.0), + bottomRight: const Radius.circular(20.0), + )), + child: ListView.builder( + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + itemBuilder: (context, index) { + return Obx(() { + if (SettingsManager().settings.swipableConversationTiles.value) { + return Dismissible( + background: Obx( + () => slideRightBackground(ChatBloc().chats.archivedHelper(showArchived).unknownSendersHelper(showUnknown)[index])), + secondaryBackground: Obx( + () => slideLeftBackground(ChatBloc().chats.archivedHelper(showArchived).unknownSendersHelper(showUnknown)[index])), + // Each Dismissible must contain a Key. Keys allow Flutter to + // uniquely identify widgets. + key: UniqueKey(), + // Provide a function that tells the app + // what to do after an item has been swiped away. + onDismissed: (direction) async { + if (direction == DismissDirection.endToStart) { + if (SettingsManager().settings.materialLeftAction.value == + MaterialSwipeAction.pin) { + await ChatBloc() + .chats + .archivedHelper(showArchived).unknownSendersHelper(showUnknown)[index] + .togglePin(!ChatBloc().chats.archivedHelper(showArchived).unknownSendersHelper(showUnknown)[index].isPinned!); + EventDispatcher().emit("refresh", null); + if (this.mounted) setState(() {}); + } else if (SettingsManager().settings.materialLeftAction.value == + MaterialSwipeAction.alerts) { + await ChatBloc().chats.archivedHelper(showArchived).unknownSendersHelper(showUnknown)[index].toggleMute( + ChatBloc().chats.archivedHelper(showArchived).unknownSendersHelper(showUnknown)[index].muteType != "mute"); + if (this.mounted) setState(() {}); + } else if (SettingsManager().settings.materialLeftAction.value == + MaterialSwipeAction.delete) { + ChatBloc().deleteChat(ChatBloc().chats.archivedHelper(showArchived).unknownSendersHelper(showUnknown)[index]); + Chat.deleteChat(ChatBloc().chats.archivedHelper(showArchived).unknownSendersHelper(showUnknown)[index]); + } else if (SettingsManager().settings.materialLeftAction.value == + MaterialSwipeAction.mark_read) { + ChatBloc().toggleChatUnread( + ChatBloc().chats.archivedHelper(showArchived).unknownSendersHelper(showUnknown)[index], + !ChatBloc().chats.archivedHelper(showArchived).unknownSendersHelper(showUnknown)[index].hasUnreadMessage!); + } else { + if (ChatBloc().chats.archivedHelper(showArchived).unknownSendersHelper(showUnknown)[index].isArchived!) { + ChatBloc() + .unArchiveChat(ChatBloc().chats.archivedHelper(showArchived).unknownSendersHelper(showUnknown)[index]); + } else { + ChatBloc().archiveChat(ChatBloc().chats.archivedHelper(showArchived).unknownSendersHelper(showUnknown)[index]); + } + } + } else { + if (SettingsManager().settings.materialRightAction.value == + MaterialSwipeAction.pin) { + await ChatBloc() + .chats + .archivedHelper(showArchived).unknownSendersHelper(showUnknown)[index] + .togglePin(!ChatBloc().chats.archivedHelper(showArchived).unknownSendersHelper(showUnknown)[index].isPinned!); + EventDispatcher().emit("refresh", null); + if (this.mounted) setState(() {}); + } else if (SettingsManager().settings.materialRightAction.value == + MaterialSwipeAction.alerts) { + await ChatBloc().chats.archivedHelper(showArchived).unknownSendersHelper(showUnknown)[index].toggleMute( + ChatBloc().chats.archivedHelper(showArchived).unknownSendersHelper(showUnknown)[index].muteType != "mute"); + if (this.mounted) setState(() {}); + } else if (SettingsManager().settings.materialRightAction.value == + MaterialSwipeAction.delete) { + ChatBloc().deleteChat(ChatBloc().chats.archivedHelper(showArchived).unknownSendersHelper(showUnknown)[index]); + Chat.deleteChat(ChatBloc().chats.archivedHelper(showArchived).unknownSendersHelper(showUnknown)[index]); + } else if (SettingsManager().settings.materialRightAction.value == + MaterialSwipeAction.mark_read) { + ChatBloc().toggleChatUnread( + ChatBloc().chats.archivedHelper(showArchived).unknownSendersHelper(showUnknown)[index], + !ChatBloc().chats.archivedHelper(showArchived).unknownSendersHelper(showUnknown)[index].hasUnreadMessage!); + } else { + if (ChatBloc().chats.archivedHelper(showArchived).unknownSendersHelper(showUnknown)[index].isArchived!) { + ChatBloc() + .unArchiveChat(ChatBloc().chats.archivedHelper(showArchived).unknownSendersHelper(showUnknown)[index]); + } else { + ChatBloc().archiveChat(ChatBloc().chats.archivedHelper(showArchived).unknownSendersHelper(showUnknown)[index]); + } + } + } + }, + child: (!showArchived && + ChatBloc().chats.archivedHelper(showArchived).unknownSendersHelper(showUnknown)[index].isArchived!) + ? Container() + : (showArchived && + !ChatBloc().chats.archivedHelper(showArchived).unknownSendersHelper(showUnknown)[index].isArchived!) + ? Container() + : (!ChatBloc().chats.archivedHelper(showArchived).unknownSendersHelper(showUnknown)[index].isPinned!) + ? ConversationTile( + key: UniqueKey(), + chat: ChatBloc().chats.archivedHelper(showArchived).unknownSendersHelper(showUnknown)[index], + inSelectMode: selected.isNotEmpty, + selected: selected, + onSelect: (bool selected) { + if (selected) { + this + .selected + .add(ChatBloc().chats.archivedHelper(showArchived).unknownSendersHelper(showUnknown)[index]); + } else { + this.selected.removeWhere((element) => + element.guid == + ChatBloc().chats.archivedHelper(showArchived).unknownSendersHelper(showUnknown)[index].guid); + } + + if (this.mounted) setState(() {}); + }, + ) + : Container(), + ); + } else { + if (!showArchived && ChatBloc().chats.archivedHelper(showArchived).unknownSendersHelper(showUnknown)[index].isArchived!) + return Container(); + if (showArchived && !ChatBloc().chats.archivedHelper(showArchived).unknownSendersHelper(showUnknown)[index].isArchived!) + return Container(); + if (!ChatBloc().chats.archivedHelper(showArchived).unknownSendersHelper(showUnknown)[index].isPinned!) { + return ConversationTile( + key: UniqueKey(), + chat: ChatBloc().chats.archivedHelper(showArchived).unknownSendersHelper(showUnknown)[index], + inSelectMode: selected.isNotEmpty, + selected: selected, + onSelect: (bool selected) { + if (selected) { + this.selected.add(ChatBloc().chats.archivedHelper(showArchived).unknownSendersHelper(showUnknown)[index]); + } else { + this.selected.removeWhere((element) => + element.guid == ChatBloc().chats.archivedHelper(showArchived).unknownSendersHelper(showUnknown)[index].guid); + } + + if (this.mounted) setState(() {}); + }, + ); + } + return Container(); + } + }); + }, + itemCount: ChatBloc().chats.archivedHelper(showArchived).unknownSendersHelper(showUnknown).length, + ), + ) + ], + ), + ); + }), + floatingActionButton: selected.isEmpty && !SettingsManager().settings.moveChatCreatorToHeader.value + ? widget.parent.buildFloatingActionButton() + : null, + ), + ), + ); + } + + Widget buildForLandscape(BuildContext context, Widget chatList) { + return VerticalSplitView( + dividerWidth: 10.0, + initialRatio: 0.4, + minRatio: 0.33, + maxRatio: 0.5, + allowResize: true, + left: LayoutBuilder( + builder: (context, constraints) { + CustomNavigator.maxWidthLeft = constraints.maxWidth; + return WillPopScope( + onWillPop: () async { + Get.back(id: 1); + return false; + }, + child: Navigator( + key: Get.nestedKey(1), + onPopPage: (route, _) { + return false; + }, + pages: [CupertinoPage(name: "initial", child: chatList)], + ), + ); + } + ), + right: LayoutBuilder( + builder: (context, constraints) { + CustomNavigator.maxWidthRight = constraints.maxWidth; + return WillPopScope( + onWillPop: () async { + Get.back(id: 2); + return false; + }, + child: Navigator( + key: Get.nestedKey(2), + onPopPage: (route, _) { + return false; + }, + pages: [CupertinoPage(name: "initial", child: Scaffold( + backgroundColor: context.theme.backgroundColor, + extendBodyBehindAppBar: true, + body: Center( + child: Container( + child: Text("Select a chat from the list", style: Theme.of(Get.context!).textTheme.subtitle1!.copyWith(fontSize: 18)) + ), + ), + ))], + ), + ); + } + ), + ); + } + + Widget buildForDevice() { + bool showAltLayout = !context.isPhone || context.isLandscape; + Widget chatList = buildChatList(); + if (showAltLayout && !widget.parent.widget.showUnknownSenders && !widget.parent.widget.showArchivedChats) { + return buildForLandscape(context, chatList); + } + + return chatList; + } +} \ No newline at end of file diff --git a/lib/layouts/conversation_view/camera_widget.dart b/lib/layouts/conversation_view/camera_widget.dart deleted file mode 100644 index bff7452a3..000000000 --- a/lib/layouts/conversation_view/camera_widget.dart +++ /dev/null @@ -1,279 +0,0 @@ -import 'dart:async'; -import 'dart:io'; - -import 'package:get/get.dart'; -import 'package:bluebubbles/helpers/utils.dart'; -import 'package:bluebubbles/layouts/conversation_view/text_field/blue_bubbles_text_field.dart'; -import 'package:bluebubbles/managers/method_channel_interface.dart'; -import 'package:bluebubbles/managers/settings_manager.dart'; -import 'package:camera/camera.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/scheduler.dart'; -import 'package:flutter/services.dart'; - -class CameraWidget extends StatefulWidget { - final Function addAttachment; - - CameraWidget({ - Key? key, - required this.addAttachment, - }) : super(key: key); - - @override - _CameraWidgetState createState() => _CameraWidgetState(); -} - -class _CameraWidgetState extends State with WidgetsBindingObserver { - double? aspectRatioCache; - get hasContext { - return BlueBubblesTextField.of(context) != null; - } - - get camerasAvailable { - CameraController? controller = BlueBubblesTextField.of(context)!.cameraController; - return controller != null && BlueBubblesTextField.of(context)?.cameraState == CameraState.ACTIVE; - } - - @override - void initState() { - super.initState(); - - // Bind the lifecycle events - WidgetsBinding.instance!.addObserver(this); - - // The delay here just needs to be bigger than the SlideTransition - new Future.delayed(const Duration(milliseconds: 400), () async { - await initCameras(); - }); - } - - Future initCameras() async { - // If we aren't mounted or there is no context, don't do anything - if (!this.mounted || !this.hasContext) return; - // If the camera is already active, don't do anything - if (BlueBubblesTextField.of(context)!.cameraState == CameraState.ACTIVE) return; - - // Initialize the camera - await BlueBubblesTextField.of(context)!.initializeCameraController(); - - // Update the state when finished - if (!this.hasContext) return; // After the await, so could have been some time - if (this.mounted) setState(() {}); - } - - /// Called when the app is either closed or opened or paused - @override - void didChangeAppLifecycleState(AppLifecycleState state) async { - // Call the [LifeCycleManager] events based on the [state] - if (mounted && (state == AppLifecycleState.paused || - state == AppLifecycleState.inactive) && BlueBubblesTextField.of(context)?.cameraController != null) { - await BlueBubblesTextField.of(context)!.disposeCameras(); - } else if (state == AppLifecycleState.resumed) { - initCameras(); - } - } - - Future openFullCamera({String type: 'camera'}) async { - // Create a file that the camera can write to - String appDocPath = SettingsManager().appDocDir.path; - String ext = (type == 'video') ? ".mp4" : ".png"; - File file = new File("$appDocPath/attachments/" + randomString(16) + ext); - await file.create(recursive: true); - - // Take the picture after opening the camera - await MethodChannelInterface().invokeMethod("open-camera", {"path": file.path, "type": type}); - - // If we don't get data back, return outta here - if (!file.existsSync()) return; - if (file.statSync().size == 0) { - file.deleteSync(); - return; - } - - widget.addAttachment(file); - } - - Widget cameraPlaceholder() { - return Positioned.fill( - child: ClipRRect( - borderRadius: BorderRadius.circular(10), - child: Container(color: Theme.of(context).accentColor), - ), - ); - } - - @override - Widget build(BuildContext context) { - // If we don't have context, return the placeholder - if (!this.hasContext) return cameraPlaceholder(); - CameraController? controller = BlueBubblesTextField.of(context)!.cameraController; - - // If the controller is null or the state is not active, return the placeholder - List cameraWidgets = []; - double aspectRatio; - if (controller == null || BlueBubblesTextField.of(context)?.cameraState != CameraState.ACTIVE) { - cameraWidgets.add(cameraPlaceholder()); - aspectRatio = 0.6; - } else { - cameraWidgets = _buildCameraStack(context); - if (aspectRatioCache != null) { - aspectRatio = aspectRatioCache!; - } else if (Get.mediaQuery.orientation == Orientation.portrait) { - aspectRatio = controller.value.previewSize!.height / controller.value.previewSize!.width; - } else { - aspectRatio = 1 / controller.value.previewSize!.height / controller.value.previewSize!.width; - } - - // Cache the aspect ratio so we don't have to calculate it again - aspectRatioCache = aspectRatio; - } - - return AnimatedOpacity( - opacity: camerasAvailable ? 1 : 0.2, // 0.2 because then you can see the placeholder box a bit - duration: Duration(milliseconds: 300), // 300 because I found that looked nice (in debug mode) - child: ClipRRect( - borderRadius: BorderRadius.circular(10), - child: AspectRatio( - aspectRatio: aspectRatio, - child: Stack( - alignment: Alignment.topRight, - children: cameraWidgets, - ), - ))); - } - - List _buildCameraStack(BuildContext context) { - if (SettingsManager().settings.redactedMode.value) - return [ - Positioned.fill( - child: Container( - color: Theme.of(context).accentColor, - child: Center( - child: Text("Camera"), - ), - ), - ), - ]; - - CameraController? controller = BlueBubblesTextField.of(context)!.cameraController; - return [ - Stack( - alignment: Alignment.bottomCenter, - children: [ - RotatedBox( - child: CameraPreview(controller!), - quarterTurns: Get.mediaQuery.orientation == Orientation.portrait ? 0 : 3, - ), - Padding( - padding: EdgeInsets.only( - bottom: context.height / 30, - ), - child: TextButton( - style: TextButton.styleFrom( - backgroundColor: Colors.transparent, - ), - onPressed: () async { - HapticFeedback.mediumImpact(); - - XFile savedImage = await controller.takePicture(); - File file = new File(savedImage.path); - - // Fail if the file doesn't exist after taking the picture - if (!file.existsSync()) { - return showSnackbar('Error', 'Failed to take picture! File improperly saved by Camera lib'); - } - - // Fail if the file size is equal to 0 - if (file.statSync().size == 0) { - file.deleteSync(); - return showSnackbar('Error', 'Failed to take picture! File was empty!'); - } - - // If all passes, add the attachment - widget.addAttachment(file); - }, - child: Icon( - Icons.radio_button_checked, - color: Colors.white, - size: 30, - ), - ), - ) - ], - ), - Align( - alignment: Alignment.topLeft, - child: Padding( - padding: EdgeInsets.only( - bottom: context.height / 30, - ), - child: TextButton( - style: TextButton.styleFrom( - padding: EdgeInsets.only(left: 10), - minimumSize: Size.square(30), - backgroundColor: Colors.transparent, - ), - onPressed: () async { - HapticFeedback.lightImpact(); - await this.openFullCamera(type: 'camera'); - }, - child: Icon( - Icons.fullscreen, - color: Colors.white, - size: 30, - ), - ), - ), - ), - Align( - alignment: Alignment.topCenter, - child: Padding( - padding: EdgeInsets.only( - bottom: context.height / 30, - ), - child: TextButton( - style: TextButton.styleFrom( - padding: EdgeInsets.all(0), - minimumSize: Size.square(30), - backgroundColor: Colors.transparent, - ), - onPressed: () async { - HapticFeedback.lightImpact(); - await this.openFullCamera(type: 'video'); - }, - child: Icon( - Icons.videocam, - color: Colors.white, - size: 30, - ), - ), - ), - ), - Padding( - padding: EdgeInsets.only( - bottom: context.height / 30, - ), - child: TextButton( - style: TextButton.styleFrom( - padding: EdgeInsets.only(right: 10), - minimumSize: Size.square(30), - backgroundColor: Colors.transparent, - ), - onPressed: () async { - if (!this.hasContext) return; - - HapticFeedback.lightImpact(); - BlueBubblesTextField.of(context)!.cameraIndex = (BlueBubblesTextField.of(context)!.cameraIndex - 1).abs(); - await BlueBubblesTextField.of(context)!.initializeCameraController(); - if (this.mounted) setState(() {}); - }, - child: Icon( - Icons.switch_camera, - color: Colors.white, - size: 30, - ), - ), - ), - ]; - } -} diff --git a/lib/layouts/conversation_view/conversation_view.dart b/lib/layouts/conversation_view/conversation_view.dart index 919569059..362382533 100644 --- a/lib/layouts/conversation_view/conversation_view.dart +++ b/lib/layouts/conversation_view/conversation_view.dart @@ -3,6 +3,10 @@ import 'dart:io'; import 'dart:math'; import 'dart:ui'; +import 'package:adaptive_theme/adaptive_theme.dart'; +import 'package:bluebubbles/helpers/hex_color.dart'; +import 'package:bluebubbles/helpers/logger.dart'; +import 'package:bluebubbles/helpers/navigator.dart'; import 'package:bluebubbles/helpers/ui_helpers.dart'; import 'package:bluebubbles/helpers/utils.dart'; import 'package:bluebubbles/action_handler.dart'; @@ -26,6 +30,8 @@ import 'package:bluebubbles/managers/queue_manager.dart'; import 'package:bluebubbles/managers/settings_manager.dart'; import 'package:bluebubbles/repository/models/chat.dart'; import 'package:bluebubbles/repository/models/message.dart'; +import 'package:bluebubbles/repository/models/theme_object.dart'; +import 'package:bluebubbles/main.dart'; import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; @@ -90,11 +96,14 @@ class ConversationViewState extends State with ConversationVie bool wasCreator = false; GlobalKey key = GlobalKey(); Worker? worker; + final RxBool adjustBackground = RxBool(false); @override void initState() { super.initState(); + getAdjustBackground(); + this.selected = widget.selected.isEmpty ? [] : widget.selected; this.existingAttachments = widget.existingAttachments.isEmpty ? [] : widget.existingAttachments; this.existingText = widget.existingText; @@ -107,6 +116,10 @@ class ConversationViewState extends State with ConversationVie isCreator = widget.isCreator; chat = widget.chat; + if (chat != null) { + prefs.setString('lastOpenedChat', chat!.guid!); + } + if (widget.selected.isEmpty) { initChatSelector(); } @@ -162,6 +175,17 @@ class ConversationViewState extends State with ConversationVie WidgetsBinding.instance!.addObserver(this); } + void getAdjustBackground() async { + var lightTheme = await ThemeObject.getLightTheme(); + var darkTheme = await ThemeObject.getDarkTheme(); + if ((lightTheme.gradientBg && !ThemeObject.inDarkMode(Get.context!)) + || (darkTheme.gradientBg && ThemeObject.inDarkMode(Get.context!))) { + adjustBackground.value = true; + } else { + adjustBackground.value = false; + } + } + void initListener() { if (messageBloc != null) { worker = ever(messageBloc!.event, (event) async { @@ -174,7 +198,7 @@ class ConversationViewState extends State with ConversationVie if (event.type == MessageBlocEventType.insert && this.mounted && event.outGoing) { final constraints = BoxConstraints( - maxWidth: context.width * MessageWidgetMixin.MAX_SIZE, + maxWidth: CustomNavigator.width(context) * MessageWidgetMixin.MAX_SIZE, minHeight: Theme.of(context).textTheme.bodyText2!.fontSize!, maxHeight: Theme.of(context).textTheme.bodyText2!.fontSize!, ); @@ -186,14 +210,22 @@ class ConversationViewState extends State with ConversationVie maxLines: 1, ).createRenderObject(context); final size = renderParagraph.getDryLayout(constraints); - if (!(event.message?.hasAttachments ?? false) && !(event.message?.text?.isEmpty ?? false)) + if (!(event.message?.hasAttachments ?? false) && !(event.message?.text?.isEmpty ?? false)) { setState(() { tween = Tween( - begin: context.width - 30, - end: min(size.width + 68, context.width * MessageWidgetMixin.MAX_SIZE + 40)); + begin: CustomNavigator.width(context) - 30, + end: min(size.width + 68, CustomNavigator.width(context) * MessageWidgetMixin.MAX_SIZE + 40)); controller = CustomAnimationControl.play; message = event.message; }); + } else { + setState(() { + isCreator = false; + wasCreator = true; + this.existingText = ""; + this.existingAttachments = []; + }); + } } }); } @@ -203,13 +235,14 @@ class ConversationViewState extends State with ConversationVie void didChangeDependencies() async { super.didChangeDependencies(); didChangeDependenciesConversationView(); + getAdjustBackground(); } /// Called when the app is either closed or opened or paused @override void didChangeAppLifecycleState(AppLifecycleState state) { if (state == AppLifecycleState.paused && mounted) { - debugPrint("Removing CurrentChat imageData"); + Logger.info("Removing CurrentChat imageData"); CurrentChat.of(context)?.imageData.clear(); } } @@ -242,6 +275,8 @@ class ConversationViewState extends State with ConversationVie // If the chat is still null, return false if (chat == null) return false; + prefs.setString('lastOpenedChat', chat!.guid!); + // If the current chat is null, set it if (isDifferentChat) { initCurrentChat(chat!); @@ -406,171 +441,192 @@ class ConversationViewState extends State with ConversationVie appBar: !isCreator! ? buildConversationViewHeader() as PreferredSizeWidget? : buildChatSelectorHeader() as PreferredSizeWidget?, - body: Column( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - if (isCreator!) - ChatSelectorTextField( - controller: chatSelectorController, - onRemove: (UniqueContact item) { - if (item.isChat) { - selected.removeWhere((e) => (e.chat?.guid ?? null) == item.chat!.guid); - } else { - selected.removeWhere((e) => e.address == item.address); - } - fetchCurrentChat(); - filterContacts(); - resetCursor(); - if (this.mounted) setState(() {}); - }, - onSelected: onSelected, - isCreator: widget.isCreator, - allContacts: contacts, - selectedContacts: selected, - ), - Obx(() { - if (!ChatBloc().hasChats.value) { - return Center( - child: Container( - padding: EdgeInsets.symmetric(vertical: 20.0), - child: Column( - children: [ - Padding( - padding: const EdgeInsets.all(8.0), - child: Text( - "Loading existing chats...", - style: Theme.of(context).textTheme.subtitle1, + body: Obx(() => MirrorAnimation>( + tween: ConversationViewMixin.gradientTween.value, + curve: Curves.fastOutSlowIn, + duration: Duration(seconds: 3), + builder: (context, child, anim) { + return Container( + decoration: (searchQuery.length == 0 || !isCreator!) + && chat != null + && adjustBackground.value ? BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topRight, + end: Alignment.bottomLeft, + stops: [anim.get("color1"), anim.get("color2")], + colors: [AdaptiveTheme.of(context).mode == AdaptiveThemeMode.light ? + Theme.of(context).primaryColor.lightenPercent(20) : Theme.of(context).primaryColor.darkenPercent(20), Theme.of(context).backgroundColor] + ) + ) : null, + child: child, + ); + }, + child: Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + if (isCreator!) + ChatSelectorTextField( + controller: chatSelectorController, + onRemove: (UniqueContact item) { + if (item.isChat) { + selected.removeWhere((e) => (e.chat?.guid ?? null) == item.chat!.guid); + } else { + selected.removeWhere((e) => e.address == item.address); + } + fetchCurrentChat(); + filterContacts(); + resetCursor(); + if (this.mounted) setState(() {}); + }, + onSelected: onSelected, + isCreator: widget.isCreator, + allContacts: contacts, + selectedContacts: selected, + ), + Obx(() { + if (!ChatBloc().hasChats.value) { + return Center( + child: Container( + padding: EdgeInsets.symmetric(vertical: 20.0), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + "Loading existing chats...", + style: Theme.of(context).textTheme.subtitle1, + ), ), - ), - buildProgressIndicator(context, size: 15), - ], + buildProgressIndicator(context, size: 15), + ], + ), ), - ), - ); - } else - return SizedBox.shrink(); - }), - Expanded( - child: Stack(children: [ - Column(mainAxisAlignment: MainAxisAlignment.end, mainAxisSize: MainAxisSize.min, children: [ - Expanded( - child: Obx( - () => fetchingCurrentChat.value - ? Center( - child: Container( - padding: EdgeInsets.symmetric(vertical: 20.0), - child: Column( - children: [ - Padding( - padding: const EdgeInsets.all(8.0), - child: Text( - "Loading chat...", - style: Theme.of(context).textTheme.subtitle1, - ), - ), - buildProgressIndicator(context, size: 15), - ], + ); + } else + return SizedBox.shrink(); + }), + Expanded( + child: Stack(children: [ + Column(mainAxisAlignment: MainAxisAlignment.end, mainAxisSize: MainAxisSize.min, children: [ + Expanded( + child: Obx( + () => fetchingCurrentChat.value + ? Center( + child: Container( + padding: EdgeInsets.symmetric(vertical: 20.0), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + "Loading chat...", + style: Theme.of(context).textTheme.subtitle1, + ), ), - ), - ) - : (searchQuery.length == 0 || !isCreator!) && chat != null - ? Stack( - alignment: Alignment.bottomCenter, - children: [ - MessagesView( - key: new Key(chat?.guid ?? "unknown-chat"), - messageBloc: messageBloc, - showHandle: chat!.participants.length > 1, - chat: chat, - initComplete: widget.onMessagesViewComplete, - ), - currentChat != null - ? Obx(() => AnimatedOpacity( - duration: Duration(milliseconds: 250), - opacity: currentChat!.showScrollDown.value ? 1 : 0, - curve: Curves.easeInOut, - child: buildScrollToBottomFAB(context), - )) - : Container(), - ], - ) - : buildChatSelectorBody(), + buildProgressIndicator(context, size: 15), + ], + ), + ), + ) + : (searchQuery.length == 0 || !isCreator!) && chat != null + ? Stack( + alignment: Alignment.bottomCenter, + children: [ + MessagesView( + key: new Key(chat?.guid ?? "unknown-chat"), + messageBloc: messageBloc, + showHandle: chat!.participants.length > 1, + chat: chat, + initComplete: widget.onMessagesViewComplete, + ), + currentChat != null + ? Obx(() => AnimatedOpacity( + duration: Duration(milliseconds: 250), + opacity: currentChat!.showScrollDown.value ? 1 : 0, + curve: Curves.easeInOut, + child: buildScrollToBottomFAB(context), + )) + : Container(), + ], + ) + : buildChatSelectorBody(), + ), ), - ), - Obx(() { - if (widget.onSelect == null) { - if (SettingsManager().settings.swipeToCloseKeyboard.value || - SettingsManager().settings.swipeToOpenKeyboard.value) { - return GestureDetector( - onPanUpdate: (details) { - if (SettingsManager().settings.swipeToCloseKeyboard.value && - details.delta.dy > 0 && - (currentChat?.keyboardOpen ?? false)) { - EventDispatcher().emit("unfocus-keyboard", null); - } else if (SettingsManager().settings.swipeToOpenKeyboard.value && - details.delta.dy < 0 && - !(currentChat?.keyboardOpen ?? false)) { - EventDispatcher().emit("focus-keyboard", null); - } - }, - child: textField); + Obx(() { + if (widget.onSelect == null) { + if (SettingsManager().settings.swipeToCloseKeyboard.value || + SettingsManager().settings.swipeToOpenKeyboard.value) { + return GestureDetector( + onPanUpdate: (details) { + if (SettingsManager().settings.swipeToCloseKeyboard.value && + details.delta.dy > 0 && + (currentChat?.keyboardOpen ?? false)) { + EventDispatcher().emit("unfocus-keyboard", null); + } else if (SettingsManager().settings.swipeToOpenKeyboard.value && + details.delta.dy < 0 && + !(currentChat?.keyboardOpen ?? false)) { + EventDispatcher().emit("focus-keyboard", null); + } + }, + child: textField); + } + return textField; } - return textField; - } - return Container(); - }), - ]), - AnimatedPositioned( - duration: Duration(milliseconds: 300), - bottom: message != null ? 62 + offset : 10 + offset, - right: 5, - curve: Curves.easeIn, - onEnd: () { - setState(() { - tween = Tween(begin: 1, end: 0); - controller = CustomAnimationControl.stop; - message = null; - isCreator = false; - wasCreator = true; - this.existingText = ""; - this.existingAttachments = []; - }); - }, - child: Visibility( - visible: message != null, - child: CustomAnimation( - control: controller, - tween: tween, - duration: Duration(milliseconds: 200), - builder: (context, child, value) { - return SentMessageHelper.buildMessageWithTail( - context, - message, - true, - false, - message?.isBigEmoji() ?? false, - currentChat: currentChat, - customWidth: - (message?.hasAttachments ?? false) && (message?.text?.isEmpty ?? true) ? null : value, - customColor: (message?.hasAttachments ?? false) && (message?.text?.isEmpty ?? true) - ? Colors.transparent - : null, - customContent: child, - ); - }, - child: (message?.hasAttachments ?? false) && (message?.text?.isEmpty ?? true) - ? MessageAttachments( - message: message, - showTail: true, - showHandle: false, - ) - : null), + return Container(); + }), + ]), + AnimatedPositioned( + duration: Duration(milliseconds: 300), + bottom: message != null ? 62 + offset : 10 + offset, + right: 5, + curve: Curves.easeIn, + onEnd: () { + setState(() { + tween = Tween(begin: 1, end: 0); + controller = CustomAnimationControl.stop; + message = null; + isCreator = false; + wasCreator = true; + this.existingText = ""; + this.existingAttachments = []; + }); + }, + child: Visibility( + visible: message != null, + child: CustomAnimation( + control: controller, + tween: tween, + duration: Duration(milliseconds: 200), + builder: (context, child, value) { + return SentMessageHelper.buildMessageWithTail( + context, + message, + true, + false, + message?.isBigEmoji() ?? false, + currentChat: currentChat, + customWidth: + (message?.hasAttachments ?? false) && (message?.text?.isEmpty ?? true) ? null : value, + customColor: (message?.hasAttachments ?? false) && (message?.text?.isEmpty ?? true) + ? Colors.transparent + : null, + customContent: child, + ); + }, + child: (message?.hasAttachments ?? false) && (message?.text?.isEmpty ?? true) + ? MessageAttachments( + message: message, + showTail: true, + showHandle: false, + ) + : null), + ), ), - ), - ]), - ), - ], - ), + ]), + ), + ], + ), + )), floatingActionButton: currentChat != null ? AnimatedOpacity( duration: Duration(milliseconds: 250), opacity: 1, curve: Curves.easeInOut, child: buildFAB()) diff --git a/lib/layouts/conversation_view/conversation_view_mixin.dart b/lib/layouts/conversation_view/conversation_view_mixin.dart index a1c5a7a2f..047dec646 100644 --- a/lib/layouts/conversation_view/conversation_view_mixin.dart +++ b/lib/layouts/conversation_view/conversation_view_mixin.dart @@ -5,6 +5,8 @@ import 'package:bluebubbles/blocs/chat_bloc.dart'; import 'package:bluebubbles/blocs/message_bloc.dart'; import 'package:bluebubbles/helpers/constants.dart'; import 'package:bluebubbles/helpers/hex_color.dart'; +import 'package:bluebubbles/helpers/logger.dart'; +import 'package:bluebubbles/helpers/navigator.dart'; import 'package:bluebubbles/helpers/socket_singletons.dart'; import 'package:bluebubbles/helpers/ui_helpers.dart'; import 'package:bluebubbles/helpers/utils.dart'; @@ -30,6 +32,7 @@ import 'package:flutter/cupertino.dart' as Cupertino; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:get/get.dart'; +import 'package:simple_animations/simple_animations.dart'; import 'package:slugify/slugify.dart'; mixin ConversationViewMixin on State { @@ -65,6 +68,12 @@ mixin ConversationViewMixin on Sta TextEditingController chatSelectorController = new TextEditingController(text: " "); + static Rx> gradientTween = Rx>( + MultiTween() + ..add("color1", Tween(begin: 0, end: 0.2)) + ..add("color2", Tween(begin: 0.8, end: 1)) + ); + /// Conversation view methods /// /// @@ -117,7 +126,7 @@ mixin ConversationViewMixin on Sta try { await fetchChatSingleton(widget.chat!.guid!); } catch (ex) { - debugPrint(ex.toString()); + Logger.error(ex.toString()); } setNewChatData(forceUpdate: true); @@ -185,7 +194,7 @@ mixin ConversationViewMixin on Sta return; } - debugPrint("(Convo View) No participants found for chat, fetching..."); + Logger.info("No participants found for chat, fetching...", tag: "ConversationView"); try { // If we don't have participants, we should fetch them from the server @@ -194,15 +203,15 @@ mixin ConversationViewMixin on Sta if (data != null) { await chat!.getParticipants(); if (chat!.participants.isNotEmpty) { - debugPrint("(Convo View) Got new chat participants. Updating state."); + Logger.info("Got new chat participants. Updating state.", tag: "ConversationView"); if (this.mounted) setState(() {}); } else { - debugPrint("(Convo View) Participants list is still empty, please contact support!"); + Logger.info("Participants list is still empty, please contact support!", tag: "ConversationView"); } } } catch (ex) { - debugPrint("There was an error fetching the chat"); - debugPrint(ex.toString()); + Logger.error("There was an error fetching the chat"); + Logger.error(ex.toString()); } } @@ -285,7 +294,7 @@ mixin ConversationViewMixin on Sta padding: EdgeInsets.only(right: SettingsManager().settings.colorblindMode.value ? 10.0 : 5.0), child: GestureDetector( child: Icon( - (markedAsRead) ? Icons.check_circle : Icons.check_circle_outline, + (markedAsRead) ? Cupertino.CupertinoIcons.check_mark_circled : Cupertino.CupertinoIcons.check_mark_circled_solid, color: (markedAsRead) ? HexColor('32CD32').withAlpha(200) : fontColor, ), onTap: markChatAsRead, @@ -369,7 +378,13 @@ mixin ConversationViewMixin on Sta padding: const EdgeInsets.only(right: 8.0), child: GestureDetector( child: Icon( - (markedAsRead) ? Icons.check_circle : Icons.check_circle_outline, + (markedAsRead) + ? SettingsManager().settings.skin.value == Skins.iOS + ? Cupertino.CupertinoIcons.check_mark_circled_solid + : Icons.check_circle + : SettingsManager().settings.skin.value == Skins.iOS + ? Cupertino.CupertinoIcons.check_mark_circled + : Icons.check_circle_outline, color: (markedAsRead) ? HexColor('32CD32').withAlpha(200) : fontColor, ), onTap: markChatAsRead, @@ -382,7 +397,7 @@ mixin ConversationViewMixin on Sta padding: const EdgeInsets.only(right: 8.0), child: GestureDetector( child: Icon( - Icons.more_vert, + SettingsManager().settings.skin.value == Skins.iOS ? Cupertino.CupertinoIcons.ellipsis : Icons.more_vert, color: fontColor, ), onTap: openDetails, @@ -426,7 +441,7 @@ mixin ConversationViewMixin on Sta // IT KINDA WORKED BUT ULTIMATELY FAILED // return PreferredSize( - // preferredSize: Size(context.width, 80), + // preferredSize: Size(CustomNavigator.width(context), 80), // child: ClipRect( // child: BackdropFilter( // filter: ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0), @@ -507,7 +522,7 @@ mixin ConversationViewMixin on Sta return CupertinoNavigationBar( backgroundColor: Theme.of(context).accentColor.withAlpha(125), border: Border( - bottom: BorderSide(color: Colors.white.withOpacity(0.2), width: 0.2), + bottom: BorderSide(color: Theme.of(context).dividerColor, width: 1.5), ), leading: GestureDetector( onTap: () { @@ -564,12 +579,12 @@ mixin ConversationViewMixin on Sta Center( child: Container( constraints: BoxConstraints( - maxWidth: context.width / 2, + maxWidth: CustomNavigator.width(context) / 2, ), child: Row(mainAxisSize: MainAxisSize.min, children: [ Container( constraints: BoxConstraints( - maxWidth: context.width / 2 - 55, + maxWidth: CustomNavigator.width(context) / 2 - 55, ), child: RichText( maxLines: 1, @@ -913,7 +928,7 @@ mixin ConversationViewMixin on Sta }); params["participants"] = participants; - debugPrint("Starting chat with participants: ${participants.join(", ")}"); + Logger.info("Starting chat with participants: ${participants.join(", ")}"); Function returnChat = (Chat newChat) async { await newChat.save(); diff --git a/lib/layouts/conversation_view/messages_view.dart b/lib/layouts/conversation_view/messages_view.dart index fa2fb26d9..2cc7833a1 100644 --- a/lib/layouts/conversation_view/messages_view.dart +++ b/lib/layouts/conversation_view/messages_view.dart @@ -4,6 +4,7 @@ import 'dart:ui'; import 'package:bluebubbles/action_handler.dart'; import 'package:bluebubbles/blocs/message_bloc.dart'; import 'package:bluebubbles/helpers/constants.dart'; +import 'package:bluebubbles/helpers/logger.dart'; import 'package:bluebubbles/helpers/utils.dart'; import 'package:bluebubbles/layouts/widgets/contact_avatar_widget.dart'; import 'package:bluebubbles/layouts/widgets/message_widget/message_widget.dart'; @@ -130,14 +131,14 @@ class MessagesViewState extends State with TickerProviderStateMixi if (isNullOrEmpty(_messages)!) return resetReplies(); if (_messages.first.isFromMe!) return resetReplies(); - debugPrint("Getting smart replies..."); + Logger.info("Getting smart replies..."); Map results = await smartReply.suggestReplies(); if (results.containsKey('suggestions')) { List suggestions = results['suggestions']; - debugPrint("Smart Replies found: ${suggestions.length}"); + Logger.info("Smart Replies found: ${suggestions.length}"); replies = suggestions.map((e) => e.getText()).toList().toSet().toList(); - debugPrint(replies.toString()); + Logger.debug(replies.toString()); } // If there is nothing in the list, get out @@ -170,7 +171,7 @@ class MessagesViewState extends State with TickerProviderStateMixi if (val != LoadMessageResult.FAILED_TO_RETREIVE) { if (val == LoadMessageResult.RETREIVED_NO_MESSAGES) { noMoreMessages = true; - debugPrint("(CHUNK) No more messages to load"); + Logger.info("No more messages to load", tag: "MessageBloc"); } else if (val == LoadMessageResult.RETREIVED_LAST_PAGE) { // Mark this chat saying we have no more messages to load noMoreLocalMessages = true; @@ -295,8 +296,8 @@ class MessagesViewState extends State with TickerProviderStateMixi _listKey!.currentState! .removeItem(i, (context, animation) => Container(), duration: Duration(milliseconds: 0)); } catch (ex) { - debugPrint("Error removing item animation"); - debugPrint(ex.toString()); + Logger.error("Error removing item animation"); + Logger.error(ex.toString()); } } } @@ -317,15 +318,15 @@ class MessagesViewState extends State with TickerProviderStateMixi bool updatedAMessage = false; for (int i = 0; i < _messages.length; i++) { if (_messages[i].guid == oldGuid) { - debugPrint("(Message status) Update message: [${message!.text}] - [${message.guid}] - [$oldGuid]"); + Logger.info("Update message: [${message!.text}] - [${message.guid}] - [$oldGuid]", tag: "MessageStatus"); _messages[i] = message; updatedAMessage = true; break; } } if (!updatedAMessage) { - debugPrint( - "(Message status) Message not updated (not found): [${message!.text}] - [${message.guid}] - [$oldGuid]"); + Logger.warn("Message not updated (not found): [${message!.text}] - [${message.guid}] - [$oldGuid]", + tag: "MessageStatus"); } return message; @@ -383,18 +384,21 @@ class MessagesViewState extends State with TickerProviderStateMixi stream: smartReplyController.stream, builder: (context, snapshot) { return SliverToBoxAdapter( - child: AnimatedSize( - duration: Duration(milliseconds: 400), - vsync: this, - child: replies.isEmpty - ? Container() - : Row( - mainAxisAlignment: MainAxisAlignment.end, - children: replies - .map( - (e) => _buildReply(e), - ) - .toList()), + child: Padding( + padding: EdgeInsets.only(top: SettingsManager().settings.skin.value != Skins.iOS ? 8.0 : 0.0), + child: AnimatedSize( + duration: Duration(milliseconds: 400), + vsync: this, + child: replies.isEmpty + ? Container() + : Row( + mainAxisAlignment: MainAxisAlignment.end, + children: replies + .map( + (e) => _buildReply(e), + ) + .toList()), + ), ), ); }, diff --git a/lib/layouts/conversation_view/new_chat_creator/chat_selector_text_field.dart b/lib/layouts/conversation_view/new_chat_creator/chat_selector_text_field.dart index 5e4ebbca0..ff64d94b0 100644 --- a/lib/layouts/conversation_view/new_chat_creator/chat_selector_text_field.dart +++ b/lib/layouts/conversation_view/new_chat_creator/chat_selector_text_field.dart @@ -1,3 +1,4 @@ +import 'package:bluebubbles/helpers/constants.dart'; import 'package:bluebubbles/managers/settings_manager.dart'; import 'package:collection/collection.dart'; import 'package:contacts_service/contacts_service.dart'; @@ -80,7 +81,7 @@ class _ChatSelectorTextFieldState extends State { ), InkWell( child: Icon( - Icons.close, + SettingsManager().settings.skin.value == Skins.iOS ? CupertinoIcons.xmark : Icons.close, size: 15.0, )) ], @@ -106,7 +107,7 @@ class _ChatSelectorTextFieldState extends State { if (done.isEmail || done.isPhoneNumber) { Contact? contact = ContactManager().getCachedContactSync(done); if (contact == null) { - widget.onSelected(new UniqueContact(address: done, displayName: await formatPhoneNumber(done))); + widget.onSelected(new UniqueContact(address: done, displayName: done.isEmail ? done : await formatPhoneNumber(done))); } else { widget.onSelected(new UniqueContact(address: done, displayName: contact.displayName ?? done)); } diff --git a/lib/layouts/conversation_view/new_chat_creator/contact_selector_option.dart b/lib/layouts/conversation_view/new_chat_creator/contact_selector_option.dart index af7a851ca..bc4650241 100644 --- a/lib/layouts/conversation_view/new_chat_creator/contact_selector_option.dart +++ b/lib/layouts/conversation_view/new_chat_creator/contact_selector_option.dart @@ -7,6 +7,7 @@ import 'package:bluebubbles/layouts/widgets/contact_avatar_widget.dart'; import 'package:bluebubbles/managers/contact_manager.dart'; import 'package:bluebubbles/managers/settings_manager.dart'; import 'package:bluebubbles/repository/models/handle.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; @@ -123,6 +124,7 @@ class ContactSelectorOption extends StatelessWidget { style: Theme.of(context).textTheme.bodyText1, overflow: TextOverflow.ellipsis, ), + tileColor: Theme.of(context).backgroundColor, subtitle: subtitle, leading: !item.isChat ? ContactAvatarWidget( @@ -137,7 +139,7 @@ class ContactSelectorOption extends StatelessWidget { ), trailing: item.isChat ? Icon( - SettingsManager().settings.skin.value == Skins.iOS ? Icons.arrow_forward_ios : Icons.arrow_forward, + SettingsManager().settings.skin.value == Skins.iOS ? CupertinoIcons.forward : Icons.arrow_forward, color: Theme.of(context).primaryColor, ) : null, diff --git a/lib/layouts/conversation_view/text_field/attachments/list/attachment_list_item.dart b/lib/layouts/conversation_view/text_field/attachments/list/attachment_list_item.dart index d1eadcd05..391f95860 100644 --- a/lib/layouts/conversation_view/text_field/attachments/list/attachment_list_item.dart +++ b/lib/layouts/conversation_view/text_field/attachments/list/attachment_list_item.dart @@ -2,10 +2,12 @@ import 'dart:io'; import 'dart:typed_data'; import 'package:bluebubbles/helpers/attachment_helper.dart'; +import 'package:bluebubbles/helpers/constants.dart'; import 'package:bluebubbles/helpers/ui_helpers.dart'; import 'package:bluebubbles/layouts/image_viewer/attachmet_fullscreen_viewer.dart'; import 'package:bluebubbles/managers/settings_manager.dart'; import 'package:bluebubbles/repository/models/attachment.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:mime_type/mime_type.dart'; import 'package:path/path.dart' as path; @@ -154,7 +156,7 @@ class _AttachmentListItemState extends State { Align( alignment: Alignment.bottomRight, child: Icon( - Icons.play_arrow, + SettingsManager().settings.skin.value == Skins.iOS ? CupertinoIcons.play : Icons.play_arrow, color: Colors.white, ), ), @@ -170,7 +172,7 @@ class _AttachmentListItemState extends State { width: 25, height: 25, child: Icon( - Icons.close, + SettingsManager().settings.skin.value == Skins.iOS ? CupertinoIcons.xmark : Icons.close, color: Colors.white, size: 15, ), diff --git a/lib/layouts/conversation_view/text_field/attachments/picker/attachment_picked.dart b/lib/layouts/conversation_view/text_field/attachments/picker/attachment_picked.dart index 715d9ecab..f56b52c14 100644 --- a/lib/layouts/conversation_view/text_field/attachments/picker/attachment_picked.dart +++ b/lib/layouts/conversation_view/text_field/attachments/picker/attachment_picked.dart @@ -1,7 +1,9 @@ import 'dart:typed_data'; +import 'package:bluebubbles/helpers/constants.dart'; import 'package:bluebubbles/layouts/conversation_view/text_field/blue_bubbles_text_field.dart'; import 'package:bluebubbles/managers/settings_manager.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:mime_type/mime_type.dart'; import 'package:photo_manager/photo_manager.dart'; @@ -88,7 +90,7 @@ class _AttachmentPickedState extends State with AutomaticKeepA child: Container( child: Center( child: Icon( - Icons.check_circle, + SettingsManager().settings.skin.value == Skins.iOS ? CupertinoIcons.check_mark_circled_solid : Icons.check_circle, color: Colors.white, ), ), @@ -101,7 +103,7 @@ class _AttachmentPickedState extends State with AutomaticKeepA child: InkWell( child: widget.data.type == AssetType.video ? Icon( - Icons.play_circle_filled, + SettingsManager().settings.skin.value == Skins.iOS ? CupertinoIcons.play_circle_fill : Icons.play_circle_filled, color: Colors.white.withOpacity(0.5), size: 50, ) diff --git a/lib/layouts/conversation_view/text_field/attachments/picker/text_field_attachment_picker.dart b/lib/layouts/conversation_view/text_field/attachments/picker/text_field_attachment_picker.dart index fdf798864..6dba09b4d 100644 --- a/lib/layouts/conversation_view/text_field/attachments/picker/text_field_attachment_picker.dart +++ b/lib/layouts/conversation_view/text_field/attachments/picker/text_field_attachment_picker.dart @@ -1,13 +1,16 @@ import 'dart:async'; import 'dart:io'; +import 'package:bluebubbles/helpers/constants.dart'; import 'package:bluebubbles/helpers/share.dart'; -import 'package:bluebubbles/layouts/conversation_view/camera_widget.dart'; +import 'package:bluebubbles/helpers/utils.dart'; import 'package:bluebubbles/layouts/conversation_view/text_field/attachments/picker/attachment_picked.dart'; import 'package:bluebubbles/layouts/widgets/theme_switcher/theme_switcher.dart'; import 'package:bluebubbles/managers/current_chat.dart'; import 'package:bluebubbles/managers/life_cycle_manager.dart'; import 'package:bluebubbles/managers/method_channel_interface.dart'; +import 'package:bluebubbles/managers/settings_manager.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:photo_manager/photo_manager.dart'; @@ -48,6 +51,26 @@ class _TextFieldAttachmentPickerState extends State w if (this.mounted) setState(() {}); } + Future openFullCamera({String type: 'camera'}) async { + // Create a file that the camera can write to + String appDocPath = SettingsManager().appDocDir.path; + String ext = (type == 'video') ? ".mp4" : ".png"; + File file = new File("$appDocPath/attachments/" + randomString(16) + ext); + await file.create(recursive: true); + + // Take the picture after opening the camera + await MethodChannelInterface().invokeMethod("open-camera", {"path": file.path, "type": type}); + + // If we don't get data back, return outta here + if (!file.existsSync()) return; + if (file.statSync().size == 0) { + file.deleteSync(); + return; + } + + widget.onAddAttachment(file); + } + @override Widget build(BuildContext context) { return AnimatedSize( @@ -70,7 +93,7 @@ class _TextFieldAttachmentPickerState extends State w slivers: [ SliverToBoxAdapter( child: Padding( - padding: EdgeInsets.only(left: 5, right: 10, top: 5, bottom: 5), + padding: EdgeInsets.only(left: 5, right: 5, top: 5, bottom: 5), child: Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -101,7 +124,7 @@ class _TextFieldAttachmentPickerState extends State w Padding( padding: const EdgeInsets.all(8.0), child: Icon( - Icons.video_library, + SettingsManager().settings.skin.value == Skins.iOS ? CupertinoIcons.folder_open : Icons.folder_open, color: Theme.of(context).textTheme.bodyText1!.color, ), ), @@ -176,7 +199,7 @@ class _TextFieldAttachmentPickerState extends State w Padding( padding: const EdgeInsets.all(8.0), child: Icon( - Icons.location_on, + SettingsManager().settings.skin.value == Skins.iOS ? CupertinoIcons.location : Icons.location_on, color: Theme.of(context).textTheme.bodyText1!.color, ), ), @@ -198,12 +221,90 @@ class _TextFieldAttachmentPickerState extends State w ), SliverToBoxAdapter( child: Padding( - padding: EdgeInsets.only(top: 5, bottom: 5, right: 5), - child: CameraWidget( - addAttachment: (File attachment) { - widget.onAddAttachment(attachment); - }, - )), + padding: EdgeInsets.only(left: 5, right: 10, top: 5, bottom: 5), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + fit: FlexFit.loose, + child: ConstrainedBox( + constraints: BoxConstraints( + minWidth: 90, + ), + child: ElevatedButton( + style: ElevatedButton.styleFrom( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + primary: Theme.of(context).accentColor, + ), + onPressed: () async { + openFullCamera(); + }, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: Icon( + SettingsManager().settings.skin.value == Skins.iOS ? CupertinoIcons.camera : Icons.photo_camera, + color: Theme.of(context).textTheme.bodyText1!.color, + ), + ), + Text( + "Camera", + style: TextStyle( + color: Theme.of(context).textTheme.bodyText1!.color, + fontSize: 13, + ), + ), + ], + ), + ), + ), + ), + Container(height: 10), + Flexible( + fit: FlexFit.loose, + child: ConstrainedBox( + constraints: BoxConstraints( + minWidth: 90, + ), + child: ElevatedButton( + style: ElevatedButton.styleFrom( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + primary: Theme.of(context).accentColor, + ), + onPressed: () async { + openFullCamera(type: "video"); + }, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: Icon( + SettingsManager().settings.skin.value == Skins.iOS ? CupertinoIcons.videocam : Icons.videocam, + color: Theme.of(context).textTheme.bodyText1!.color, + ), + ), + Text( + "Video", + style: TextStyle( + color: Theme.of(context).textTheme.bodyText1!.color, + fontSize: 13, + ), + ), + ], + ), + ), + ), + ), + ], + ), + ), ), SliverGrid( gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( diff --git a/lib/layouts/conversation_view/text_field/blue_bubbles_text_field.dart b/lib/layouts/conversation_view/text_field/blue_bubbles_text_field.dart index f8d2b4e76..d258e81c9 100644 --- a/lib/layouts/conversation_view/text_field/blue_bubbles_text_field.dart +++ b/lib/layouts/conversation_view/text_field/blue_bubbles_text_field.dart @@ -5,6 +5,7 @@ import 'dart:ui'; import 'package:bluebubbles/blocs/text_field_bloc.dart'; import 'package:bluebubbles/helpers/attachment_helper.dart'; import 'package:bluebubbles/helpers/constants.dart'; +import 'package:bluebubbles/helpers/logger.dart'; import 'package:bluebubbles/helpers/utils.dart'; import 'package:bluebubbles/layouts/conversation_view/text_field/attachments/list/text_field_attachment_list.dart'; import 'package:bluebubbles/layouts/conversation_view/text_field/attachments/picker/text_field_attachment_picker.dart'; @@ -139,10 +140,10 @@ class BlueBubblesTextFieldState extends State with TickerP EventDispatcher().stream.listen((event) { if (!event.containsKey("type")) return; if (event["type"] == "unfocus-keyboard" && focusNode!.hasFocus) { - print("(EVENT) Unfocus Keyboard"); + Logger.info("(EVENT) Unfocus Keyboard"); focusNode!.unfocus(); } else if (event["type"] == "focus-keyboard" && !focusNode!.hasFocus) { - print("(EVENT) Focus Keyboard"); + Logger.info("(EVENT) Focus Keyboard"); focusNode!.requestFocus(); } else if (event["type"] == "text-field-update-attachments") { addSharedAttachments(); @@ -241,10 +242,10 @@ class BlueBubblesTextFieldState extends State with TickerP void onContentCommit(CommittedContent content) async { // Add some debugging logs - debugPrint("[Content Commit] Keyboard received content"); - debugPrint(" -> Content Type: ${content.mimeType}"); - debugPrint(" -> URI: ${content.uri}"); - debugPrint(" -> Content Length: ${content.hasData ? content.data!.length : "null"}"); + Logger.info("[Content Commit] Keyboard received content"); + Logger.info(" -> Content Type: ${content.mimeType}"); + Logger.info(" -> URI: ${content.uri}"); + Logger.info(" -> Content Length: ${content.hasData ? content.data!.length : "null"}"); // Parse the filename from the URI and read the data as a List String filename = uriToFilename(content.uri, content.mimeType); @@ -322,7 +323,7 @@ class BlueBubblesTextFieldState extends State with TickerP await this.disposeCameras(); } - debugPrint("[Camera Preview] -> Initializing camera preview"); + Logger.info("[Camera Preview] -> Initializing camera preview"); // Enumerate the cameras (if we don't have them) // We only need to do this once... it's not like it's gonna change very often @@ -331,7 +332,7 @@ class BlueBubblesTextFieldState extends State with TickerP } if (cameras.length == 0) { - debugPrint("[Camera Preview] -> No available cameras!"); + Logger.info("[Camera Preview] -> No available cameras!"); return; } @@ -349,16 +350,16 @@ class BlueBubblesTextFieldState extends State with TickerP cameraState = CameraState.ACTIVE; if (this.mounted) setState(() {}); - debugPrint("[Camera Preview] -> Finished initializing camera preview"); + Logger.info("[Camera Preview] -> Finished initializing camera preview"); } Future disposeCameras() async { - debugPrint("[Camera Preview] -> Disposing camera preview"); + Logger.info("[Camera Preview] -> Disposing camera preview"); cameraState = CameraState.DISPOSING; await cameraController?.dispose(); cameraController = null; cameraState = CameraState.INACTIVE; - debugPrint("[Camera Preview] -> Finished disposing camera preview"); + Logger.info("[Camera Preview] -> Finished disposing camera preview"); } Future toggleShareMenu() async { @@ -455,9 +456,9 @@ class BlueBubblesTextFieldState extends State with TickerP child: InkWell( onTap: toggleShareMenu, child: Padding( - padding: EdgeInsets.only(right: 1), + padding: EdgeInsets.only(right: SettingsManager().settings.skin.value == Skins.iOS ? 0 : 1), child: Icon( - Icons.share, + SettingsManager().settings.skin.value == Skins.iOS ? CupertinoIcons.share : Icons.share, color: Colors.white.withAlpha(225), size: 20, ), @@ -510,8 +511,8 @@ class BlueBubblesTextFieldState extends State with TickerP } } } catch (ex) { - debugPrint("Error setting Text Field Placeholder!"); - debugPrint(ex.toString()); + Logger.error("Error setting Text Field Placeholder!"); + Logger.error(ex.toString()); } if (placeholder != this.placeholder.value) { @@ -531,165 +532,207 @@ class BlueBubblesTextFieldState extends State with TickerP duration: Duration(milliseconds: 100), vsync: this, curve: Curves.easeInOut, - child: ThemeSwitcher( - iOSSkin: CustomCupertinoTextField( - enabled: sendCountdown == null, - textInputAction: - SettingsManager().settings.sendWithReturn.value ? TextInputAction.send : TextInputAction.newline, - cursorColor: Theme.of(context).primaryColor, - onLongPressStart: () { - Feedback.forLongPress(context); - }, - onTap: () { - HapticFeedback.selectionClick(); - if (cameraState == CameraState.ACTIVE) { - disposeCameras(); + child: RawKeyboardListener( + focusNode: FocusNode(), + onKey: (RawKeyEvent event) async { + if (event.physicalKey == PhysicalKeyboardKey.enter && + SettingsManager().settings.sendWithReturn.value) { + if (!isNullOrEmpty(controller!.text)!) { + await sendMessage(); + focusNode!.previousFocus(); // I genuinely don't know why this works + return; + } else { + controller!.text = ""; // Stop pressing physical enter with enterIsSend from creating newlines + focusNode!.previousFocus(); // I genuinely don't know why this works + return; } - }, - key: _searchFormKey, - onSubmitted: (String value) { - if (!SettingsManager().settings.sendWithReturn.value || isNullOrEmpty(value)!) return; - sendMessage(); - }, - onContentCommitted: onContentCommit, - textCapitalization: TextCapitalization.sentences, - focusNode: focusNode, - autocorrect: true, - controller: controller, - scrollPhysics: CustomBouncingScrollPhysics(), - style: Theme.of(context).textTheme.bodyText1!.apply( - color: - ThemeData.estimateBrightnessForColor(Theme.of(context).backgroundColor) == Brightness.light - ? Colors.black - : Colors.white, - fontSizeDelta: -0.25, - ), - keyboardType: TextInputType.multiline, - maxLines: 14, - minLines: 1, - placeholder: SettingsManager().settings.recipientAsPlaceholder.value == true - ? placeholder.value - : "BlueBubbles", - padding: EdgeInsets.only(left: 10, top: 10, right: 40, bottom: 10), - placeholderStyle: Theme.of(context).textTheme.subtitle1, - autofocus: SettingsManager().settings.autoOpenKeyboard.value, - decoration: BoxDecoration( - color: Theme.of(context).backgroundColor, - border: Border.all( - color: Theme.of(context).dividerColor, - width: 1.5, - ), - borderRadius: BorderRadius.circular(20), - ), - ), - materialSkin: TextField( - controller: controller, - focusNode: focusNode, - textCapitalization: TextCapitalization.sentences, - autocorrect: true, - autofocus: SettingsManager().settings.autoOpenKeyboard.value, - cursorColor: Theme.of(context).primaryColor, - key: _searchFormKey, - style: Theme.of(context).textTheme.bodyText1!.apply( - color: - ThemeData.estimateBrightnessForColor(Theme.of(context).backgroundColor) == Brightness.light - ? Colors.black - : Colors.white, - fontSizeDelta: -0.25, - ), - onContentCommitted: onContentCommit, - decoration: InputDecoration( - isDense: true, - enabledBorder: OutlineInputBorder( - borderSide: BorderSide( + } + // 99% sure this isn't necessary but keeping it for now + if (event.isKeyPressed(LogicalKeyboardKey.enter) && + SettingsManager().settings.sendWithReturn.value && + !isNullOrEmpty(controller!.text)!) { + await sendMessage(); + focusNode!.requestFocus(); + } + }, + child: ThemeSwitcher( + iOSSkin: CustomCupertinoTextField( + enableIMEPersonalizedLearning: !SettingsManager().settings.incognitoKeyboard.value, + enabled: sendCountdown == null, + textInputAction: SettingsManager().settings.sendWithReturn.value + ? TextInputAction.send + : TextInputAction.newline, + cursorColor: Theme.of(context).primaryColor, + onLongPressStart: () { + Feedback.forLongPress(context); + }, + onTap: () { + HapticFeedback.selectionClick(); + if (cameraState == CameraState.ACTIVE) { + disposeCameras(); + } + }, + key: _searchFormKey, + onSubmitted: (String value) { + if (isNullOrEmpty(value)!) return; + focusNode!.requestFocus(); + sendMessage(); + }, + onContentCommitted: onContentCommit, + textCapitalization: TextCapitalization.sentences, + focusNode: focusNode, + autocorrect: true, + controller: controller, + scrollPhysics: CustomBouncingScrollPhysics(), + style: Theme.of(context).textTheme.bodyText1!.apply( + color: ThemeData.estimateBrightnessForColor(Theme.of(context).backgroundColor) == + Brightness.light + ? Colors.black + : Colors.white, + fontSizeDelta: -0.25, + ), + keyboardType: TextInputType.multiline, + maxLines: 14, + minLines: 1, + placeholder: SettingsManager().settings.recipientAsPlaceholder.value == true + ? placeholder.value + : "BlueBubbles", + padding: EdgeInsets.only(left: 10, top: 10, right: 40, bottom: 10), + placeholderStyle: Theme.of(context).textTheme.subtitle1, + autofocus: SettingsManager().settings.autoOpenKeyboard.value, + decoration: BoxDecoration( + color: Theme.of(context).backgroundColor, + border: Border.all( color: Theme.of(context).dividerColor, width: 1.5, - style: BorderStyle.solid, ), borderRadius: BorderRadius.circular(20), ), - disabledBorder: OutlineInputBorder( - borderSide: BorderSide( - color: Theme.of(context).dividerColor, - width: 1.5, - style: BorderStyle.solid, + ), + materialSkin: TextField( + // enableIMEPersonalizedLearning: !SettingsManager().settings.incognitoKeyboard.value, + controller: controller, + focusNode: focusNode, + textCapitalization: TextCapitalization.sentences, + autocorrect: true, + textInputAction: SettingsManager().settings.sendWithReturn.value + ? TextInputAction.send + : TextInputAction.newline, + autofocus: SettingsManager().settings.autoOpenKeyboard.value, + cursorColor: Theme.of(context).primaryColor, + key: _searchFormKey, + onSubmitted: (String value) { + if (isNullOrEmpty(value)!) return; + focusNode!.requestFocus(); + sendMessage(); + }, + style: Theme.of(context).textTheme.bodyText1!.apply( + color: ThemeData.estimateBrightnessForColor(Theme.of(context).backgroundColor) == + Brightness.light + ? Colors.black + : Colors.white, + fontSizeDelta: -0.25, + ), + onContentCommitted: onContentCommit, + decoration: InputDecoration( + isDense: true, + enabledBorder: OutlineInputBorder( + borderSide: BorderSide( + color: Theme.of(context).dividerColor, + width: 1.5, + style: BorderStyle.solid, + ), + borderRadius: BorderRadius.circular(20), ), - borderRadius: BorderRadius.circular(20), - ), - focusedBorder: OutlineInputBorder( - borderSide: BorderSide( - color: Theme.of(context).dividerColor, - width: 1.5, - style: BorderStyle.solid, + disabledBorder: OutlineInputBorder( + borderSide: BorderSide( + color: Theme.of(context).dividerColor, + width: 1.5, + style: BorderStyle.solid, + ), + borderRadius: BorderRadius.circular(20), + ), + focusedBorder: OutlineInputBorder( + borderSide: BorderSide( + color: Theme.of(context).dividerColor, + width: 1.5, + style: BorderStyle.solid, + ), + borderRadius: BorderRadius.circular(20), + ), + hintText: SettingsManager().settings.recipientAsPlaceholder.value == true + ? placeholder.value + : "BlueBubbles", + hintStyle: Theme.of(context).textTheme.subtitle1, + contentPadding: EdgeInsets.only( + left: 10, + top: 15, + right: 10, + bottom: 10, ), - borderRadius: BorderRadius.circular(20), - ), - hintText: SettingsManager().settings.recipientAsPlaceholder.value == true - ? placeholder.value - : "BlueBubbles", - hintStyle: Theme.of(context).textTheme.subtitle1, - contentPadding: EdgeInsets.only( - left: 10, - top: 15, - right: 10, - bottom: 10, ), + keyboardType: TextInputType.multiline, + maxLines: 14, + minLines: 1, ), - keyboardType: TextInputType.multiline, - maxLines: 14, - minLines: 1, - ), - samsungSkin: TextField( - controller: controller, - focusNode: focusNode, - textCapitalization: TextCapitalization.sentences, - autocorrect: true, - autofocus: SettingsManager().settings.autoOpenKeyboard.value, - cursorColor: Theme.of(context).primaryColor, - key: _searchFormKey, - style: Theme.of(context).textTheme.bodyText1!.apply( - color: - ThemeData.estimateBrightnessForColor(Theme.of(context).backgroundColor) == Brightness.light - ? Colors.black - : Colors.white, - fontSizeDelta: -0.25, + samsungSkin: TextField( + // enableIMEPersonalizedLearning: !SettingsManager().settings.incognitoKeyboard.value, + controller: controller, + focusNode: focusNode, + textCapitalization: TextCapitalization.sentences, + autocorrect: true, + autofocus: SettingsManager().settings.autoOpenKeyboard.value, + cursorColor: Theme.of(context).primaryColor, + key: _searchFormKey, + onSubmitted: (String value) { + if (isNullOrEmpty(value)!) return; + focusNode!.requestFocus(); + sendMessage(); + }, + style: Theme.of(context).textTheme.bodyText1!.apply( + color: ThemeData.estimateBrightnessForColor(Theme.of(context).backgroundColor) == + Brightness.light + ? Colors.black + : Colors.white, + fontSizeDelta: -0.25, + ), + onContentCommitted: onContentCommit, + decoration: InputDecoration( + isDense: true, + enabledBorder: OutlineInputBorder( + borderSide: BorderSide( + color: Theme.of(context).dividerColor, + width: 1.5, + style: BorderStyle.solid, + ), + borderRadius: BorderRadius.circular(20), ), - onContentCommitted: onContentCommit, - decoration: InputDecoration( - isDense: true, - enabledBorder: OutlineInputBorder( - borderSide: BorderSide( - color: Theme.of(context).dividerColor, - width: 1.5, - style: BorderStyle.solid, + disabledBorder: OutlineInputBorder( + borderSide: BorderSide( + color: Theme.of(context).dividerColor, + width: 1.5, + style: BorderStyle.solid, + ), + borderRadius: BorderRadius.circular(20), ), - borderRadius: BorderRadius.circular(20), - ), - disabledBorder: OutlineInputBorder( - borderSide: BorderSide( - color: Theme.of(context).dividerColor, - width: 1.5, - style: BorderStyle.solid, + focusedBorder: OutlineInputBorder( + borderSide: BorderSide( + color: Theme.of(context).dividerColor, + width: 1.5, + style: BorderStyle.solid, + ), + borderRadius: BorderRadius.circular(20), ), - borderRadius: BorderRadius.circular(20), - ), - focusedBorder: OutlineInputBorder( - borderSide: BorderSide( - color: Theme.of(context).dividerColor, - width: 1.5, - style: BorderStyle.solid, + hintText: SettingsManager().settings.recipientAsPlaceholder.value == true + ? placeholder.value + : "BlueBubbles", + hintStyle: Theme.of(context).textTheme.subtitle1, + contentPadding: EdgeInsets.only( + left: 10, + top: 15, + right: 10, + bottom: 10, ), - borderRadius: BorderRadius.circular(20), - ), - hintText: SettingsManager().settings.recipientAsPlaceholder.value == true - ? placeholder.value - : "BlueBubbles", - hintStyle: Theme.of(context).textTheme.subtitle1, - contentPadding: EdgeInsets.only( - left: 10, - top: 15, - right: 10, - bottom: 10, ), ), ), @@ -804,19 +847,19 @@ class BlueBubblesTextFieldState extends State with TickerP if (sendCountdown != null) Text(sendCountdown.toString()), (SettingsManager().settings.skin.value == Skins.iOS) ? Container( - constraints: BoxConstraints(maxWidth: 38, maxHeight: 37), + constraints: BoxConstraints(maxWidth: 35, maxHeight: 34), padding: EdgeInsets.only(right: 4, top: 2, bottom: 2), child: ButtonTheme( child: ElevatedButton( style: ElevatedButton.styleFrom( - padding: EdgeInsets.only( - right: 0, - ), - primary: Theme.of(context).primaryColor, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(40), - ), - ), + padding: EdgeInsets.only( + right: 0, + ), + primary: Theme.of(context).primaryColor, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(40), + ), + elevation: 0), onPressed: sendAction, child: Stack( alignment: Alignment.center, @@ -825,16 +868,16 @@ class BlueBubblesTextFieldState extends State with TickerP opacity: sendCountdown == null && canRecord.value ? 1.0 : 0.0, duration: Duration(milliseconds: 150), child: Icon( - Icons.mic, + CupertinoIcons.waveform, color: (isRecording.value) ? Colors.red : Colors.white, - size: 20, + size: 22, ), )), Obx(() => AnimatedOpacity( opacity: (sendCountdown == null && !canRecord.value) && !isRecording.value ? 1.0 : 0.0, duration: Duration(milliseconds: 150), child: Icon( - Icons.arrow_upward, + CupertinoIcons.arrow_up, color: Colors.white, size: 20, ), @@ -843,7 +886,7 @@ class BlueBubblesTextFieldState extends State with TickerP opacity: sendCountdown != null ? 1.0 : 0.0, duration: Duration(milliseconds: 50), child: Icon( - Icons.cancel_outlined, + CupertinoIcons.xmark_circle, color: Colors.red, size: 20, ), diff --git a/lib/layouts/image_viewer/attachmet_fullscreen_viewer.dart b/lib/layouts/image_viewer/attachmet_fullscreen_viewer.dart index e4cffbfac..70352da36 100644 --- a/lib/layouts/image_viewer/attachmet_fullscreen_viewer.dart +++ b/lib/layouts/image_viewer/attachmet_fullscreen_viewer.dart @@ -5,6 +5,7 @@ import 'dart:math'; import 'package:bluebubbles/helpers/attachment_downloader.dart'; import 'package:bluebubbles/helpers/attachment_helper.dart'; import 'package:bluebubbles/helpers/constants.dart'; +import 'package:bluebubbles/helpers/logger.dart'; import 'package:bluebubbles/layouts/image_viewer/image_viewer.dart'; import 'package:bluebubbles/layouts/image_viewer/video_viewer.dart'; import 'package:bluebubbles/layouts/widgets/CustomDismissible.dart'; @@ -17,7 +18,6 @@ import 'package:bluebubbles/repository/models/attachment.dart'; import "package:flutter/material.dart"; import 'package:flutter/services.dart'; import 'package:get/get.dart'; -import 'package:tuple/tuple.dart'; class AttachmentFullscreenViewer extends StatefulWidget { AttachmentFullscreenViewer({ @@ -76,7 +76,7 @@ class AttachmentFullscreenViewerState extends State await widget.currentChat!.updateChatAttachments(); List newer = widget.currentChat!.chatAttachments.sublist(0); if (newer.length > older.length) { - debugPrint("Increasing currentIndex from " + + Logger.info("Increasing currentIndex from " + currentIndex.toString() + " to " + (newer.length - older.length + currentIndex).toString()); @@ -137,129 +137,131 @@ class AttachmentFullscreenViewerState extends State itemCount: widget.currentChat?.chatAttachments.length ?? 1, itemBuilder: (BuildContext context, int index) { return CustomDismissible( - direction: physics is NeverScrollableScrollPhysics ? CustomDismissDirection.none : CustomDismissDirection.vertical, - key: Key('${widget.currentChat != null ? widget.currentChat!.chatAttachments[index].guid : widget.attachment.guid}'), + direction: physics is NeverScrollableScrollPhysics + ? CustomDismissDirection.none + : CustomDismissDirection.vertical, + key: Key( + '${widget.currentChat != null ? widget.currentChat!.chatAttachments[index].guid : widget.attachment.guid}'), onDismissed: (_) => Navigator.of(context).pop(), - child: Builder( - builder: (_) { - debugPrint("Showing index: " + index.toString()); - Attachment attachment = - widget.currentChat != null ? widget.currentChat!.chatAttachments[index] : widget.attachment; - String mimeType = attachment.mimeType!; - mimeType = mimeType.substring(0, mimeType.indexOf("/")); - dynamic content = AttachmentHelper.getContent(attachment, - path: attachment.guid == null ? attachment.transferName : null); + child: Builder(builder: (_) { + Logger.info("Showing index: " + index.toString()); + Attachment attachment = + widget.currentChat != null ? widget.currentChat!.chatAttachments[index] : widget.attachment; + String mimeType = attachment.mimeType!; + mimeType = mimeType.substring(0, mimeType.indexOf("/")); + dynamic content = AttachmentHelper.getContent(attachment, + path: attachment.guid == null ? attachment.transferName : null); - String viewerKey = attachment.guid ?? attachment.transferName ?? Random().nextInt(100).toString(); + String viewerKey = attachment.guid ?? attachment.transferName ?? Random().nextInt(100).toString(); - if (content is File) { - content = content; - if (mimeType == "image") { - return ImageViewer( - key: Key(viewerKey), - attachment: attachment, - file: content, - showInteractions: widget.showInteractions, + if (content is File) { + content = content; + if (mimeType == "image") { + return ImageViewer( + key: Key(viewerKey), + attachment: attachment, + file: content, + showInteractions: widget.showInteractions, + ); + } else if (mimeType == "video") { + return VideoViewer( + key: Key(viewerKey), + file: content, + attachment: attachment, + showInteractions: widget.showInteractions, + ); + } + } else if (content is Attachment) { + content = content; + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Center( + child: AttachmentDownloaderWidget( + key: + Key(attachment.guid ?? attachment.transferName ?? Random().nextInt(100).toString()), + attachment: attachment, + onPressed: () { + Get.put(AttachmentDownloadController(attachment: attachment), tag: attachment.guid); + content = AttachmentHelper.getContent(attachment); + if (this.mounted) setState(() {}); + }, + placeHolder: placeHolder, + ), + ), + ], + ); + } else if (content is AttachmentDownloadController) { + if (widget.attachment.mimeType == null) return Container(); + ever(content.file, (file) { + if (file != null) { + content = file; + if (this.mounted) setState(() {}); + } + }, onError: (error) { + content = widget.attachment; + if (this.mounted) setState(() {}); + }); + return Obx(() { + if (content.error.value = true) { + return Text( + "Error loading", + style: Theme.of(context).textTheme.bodyText1, ); - } else if (mimeType == "video") { - return VideoViewer( + } + if (content.file.value != null) { + content = content.file.value; + return Container(); + } else { + return KeyedSubtree( key: Key(viewerKey), - file: content, - attachment: attachment, - showInteractions: widget.showInteractions, + child: Stack( + alignment: Alignment.center, + children: [ + placeHolder, + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + CircularProgressIndicator( + value: content.progress.value?.toDouble() ?? 0, + backgroundColor: Colors.grey, + valueColor: AlwaysStoppedAnimation(Colors.white), + ), + ((content as AttachmentDownloadController).attachment.mimeType != null) + ? Container(height: 5.0) + : Container(), + (content.attachment.mimeType != null) + ? Text( + content.attachment.mimeType, + style: Theme.of(context).textTheme.bodyText1, + ) + : Container() + ], + ), + ], + ), + ], + ), ); } - } else if (content is Attachment) { - content = content; - return Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Center( - child: AttachmentDownloaderWidget( - key: Key(attachment.guid ?? attachment.transferName ?? Random().nextInt(100).toString()), - attachment: attachment, - onPressed: () { - Get.put(AttachmentDownloadController(attachment: attachment), tag: attachment.guid); - content = AttachmentHelper.getContent(attachment); - if (this.mounted) setState(() {}); - }, - placeHolder: placeHolder, - ), - ), - ], - ); - } else if (content is AttachmentDownloadController) { - if (widget.attachment.mimeType == null) return Container(); - ever(content.file, (file) { - if (file != null) { - content = file; - if (this.mounted) setState(() {}); - } - }, onError: (error) { - content = widget.attachment; - if (this.mounted) setState(() {}); - }); - return Obx(() { - if (content.error.value = true) { - return Text( - "Error loading", - style: Theme.of(context).textTheme.bodyText1, - ); - } - if (content.file.value != null) { - content = content.file.value; - return Container(); - } else { - return KeyedSubtree( - key: Key(viewerKey), - child: Stack( - alignment: Alignment.center, - children: [ - placeHolder, - Row( - mainAxisSize: MainAxisSize.min, - children: [ - Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - CircularProgressIndicator( - value: content.progress.value?.toDouble() ?? 0, - backgroundColor: Colors.grey, - valueColor: AlwaysStoppedAnimation(Colors.white), - ), - ((content as AttachmentDownloadController).attachment.mimeType != null) - ? Container(height: 5.0) - : Container(), - (content.attachment.mimeType != null) - ? Text( - content.attachment.mimeType, - style: Theme.of(context).textTheme.bodyText1, - ) - : Container() - ], - ), - ], - ), - ], - ), - ); - } - }); - } else { - return Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - "Error loading", - style: Theme.of(context).textTheme.bodyText1, - ), - ], - ); - } - - return Container(); + }); + } else { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + "Error loading", + style: Theme.of(context).textTheme.bodyText1, + ), + ], + ); } - ), + + return Container(); + }), ); }, onPageChanged: (int val) => currentIndex = val, diff --git a/lib/layouts/image_viewer/image_viewer.dart b/lib/layouts/image_viewer/image_viewer.dart index 030725913..4ee13a84d 100644 --- a/lib/layouts/image_viewer/image_viewer.dart +++ b/lib/layouts/image_viewer/image_viewer.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'dart:typed_data'; +import 'package:bluebubbles/helpers/navigator.dart'; import 'package:bluebubbles/helpers/utils.dart'; import 'package:get/get.dart'; import 'package:bluebubbles/helpers/attachment_helper.dart'; @@ -75,7 +76,7 @@ class _ImageViewerState extends State with AutomaticKeepAliveClient duration: Duration(milliseconds: 125), child: Container( height: 150.0, - width: context.width, + width: CustomNavigator.width(context), color: Colors.black.withOpacity(0.65), child: Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Padding( @@ -86,7 +87,7 @@ class _ImageViewerState extends State with AutomaticKeepAliveClient Navigator.pop(context); }, child: Icon( - SettingsManager().settings.skin.value == Skins.iOS ? Icons.arrow_back_ios : Icons.arrow_back, + SettingsManager().settings.skin.value == Skins.iOS ? CupertinoIcons.back : Icons.arrow_back, color: Colors.white, ), ), @@ -128,7 +129,7 @@ class _ImageViewerState extends State with AutomaticKeepAliveClient ), backgroundColor: Theme.of(context).accentColor, content: SizedBox( - width: context.width * 3 / 5, + width: CustomNavigator.width(context) * 3 / 5, height: context.height * 1 / 4, child: Container( padding: EdgeInsets.all(10.0), @@ -158,7 +159,7 @@ class _ImageViewerState extends State with AutomaticKeepAliveClient ); }, child: Icon( - Icons.info, + SettingsManager().settings.skin.value == Skins.iOS ? CupertinoIcons.info : Icons.info, color: Colors.white, ), ), @@ -181,7 +182,7 @@ class _ImageViewerState extends State with AutomaticKeepAliveClient if (this.mounted) setState(() {}); }, child: Icon( - Icons.refresh, + SettingsManager().settings.skin.value == Skins.iOS ? CupertinoIcons.refresh : Icons.refresh, color: Colors.white, ), ), @@ -194,7 +195,7 @@ class _ImageViewerState extends State with AutomaticKeepAliveClient await AttachmentHelper.saveToGallery(context, widget.file); }, child: Icon( - Icons.file_download, + SettingsManager().settings.skin.value == Skins.iOS ? CupertinoIcons.cloud_download : Icons.file_download, color: Colors.white, ), ), @@ -210,7 +211,7 @@ class _ImageViewerState extends State with AutomaticKeepAliveClient ); }, child: Icon( - Icons.share, + SettingsManager().settings.skin.value == Skins.iOS ? CupertinoIcons.share : Icons.share, color: Colors.white, ), ), @@ -259,7 +260,9 @@ class _ImageViewerState extends State with AutomaticKeepAliveClient if (AttachmentFullscreenViewer.of(context) == null) return; if (this.mounted) { AttachmentFullscreenViewerState? state = AttachmentFullscreenViewer.of(context); - if (scale == PhotoViewScaleState.zoomedIn) { + if (scale == PhotoViewScaleState.zoomedIn + || scale == PhotoViewScaleState.covering + || scale == PhotoViewScaleState.originalSize) { if (state!.physics != NeverScrollableScrollPhysics()) { AttachmentFullscreenViewer.of(context)!.setState(() { AttachmentFullscreenViewer.of(context)!.physics = NeverScrollableScrollPhysics(); diff --git a/lib/layouts/image_viewer/video_viewer.dart b/lib/layouts/image_viewer/video_viewer.dart index 2ca84e508..51d0d22bd 100644 --- a/lib/layouts/image_viewer/video_viewer.dart +++ b/lib/layouts/image_viewer/video_viewer.dart @@ -3,12 +3,12 @@ import 'dart:io'; import 'dart:ui'; import 'package:bluebubbles/helpers/constants.dart'; +import 'package:bluebubbles/helpers/navigator.dart'; import 'package:bluebubbles/managers/current_chat.dart'; import 'package:bluebubbles/managers/settings_manager.dart'; import 'package:chewie/chewie.dart'; import 'package:get/get.dart'; import 'package:bluebubbles/helpers/attachment_helper.dart'; -import 'package:bluebubbles/helpers/hex_color.dart'; import 'package:bluebubbles/helpers/share.dart'; import 'package:bluebubbles/helpers/utils.dart'; import 'package:bluebubbles/layouts/widgets/message_widget/message_content/media_players/video_widget.dart'; @@ -109,7 +109,7 @@ class _VideoViewerState extends State { duration: Duration(milliseconds: 125), child: Container( height: 150.0, - width: context.width, + width: CustomNavigator.width(context), color: Colors.black.withOpacity(0.65), child: Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Padding( @@ -120,7 +120,7 @@ class _VideoViewerState extends State { Navigator.pop(context); }, child: Icon( - SettingsManager().settings.skin.value == Skins.iOS ? Icons.arrow_back_ios : Icons.arrow_back, + SettingsManager().settings.skin.value == Skins.iOS ? CupertinoIcons.back : Icons.arrow_back, color: Colors.white, ), ), @@ -162,7 +162,7 @@ class _VideoViewerState extends State { ), backgroundColor: Theme.of(context).accentColor, content: SizedBox( - width: context.width * 3 / 5, + width: CustomNavigator.width(context) * 3 / 5, height: context.height * 1 / 4, child: Container( padding: EdgeInsets.all(10.0), @@ -192,7 +192,7 @@ class _VideoViewerState extends State { ); }, child: Icon( - Icons.info, + SettingsManager().settings.skin.value == Skins.iOS ? CupertinoIcons.info : Icons.info, color: Colors.white, ), ), @@ -237,7 +237,7 @@ class _VideoViewerState extends State { if (this.mounted) setState(() {}); }, child: Icon( - Icons.refresh, + SettingsManager().settings.skin.value == Skins.iOS ? CupertinoIcons.refresh : Icons.refresh, color: Colors.white, ), ), @@ -250,7 +250,7 @@ class _VideoViewerState extends State { await AttachmentHelper.saveToGallery(context, widget.file); }, child: Icon( - Icons.file_download, + SettingsManager().settings.skin.value == Skins.iOS ? CupertinoIcons.cloud_download : Icons.file_download, color: Colors.white, ), ), @@ -266,7 +266,7 @@ class _VideoViewerState extends State { ); }, child: Icon( - Icons.share, + SettingsManager().settings.skin.value == Skins.iOS ? CupertinoIcons.share : Icons.share, color: Colors.white, ), ), @@ -279,7 +279,13 @@ class _VideoViewerState extends State { controller.setVolume(controller.value.volume != 0.0 ? 0.0 : 1.0); }, child: Icon( - controller.value.volume == 0.0 ? Icons.volume_mute : Icons.volume_up, + controller.value.volume == 0.0 + ? SettingsManager().settings.skin.value == Skins.iOS + ? CupertinoIcons.volume_mute + : Icons.volume_mute + : SettingsManager().settings.skin.value == Skins.iOS + ? CupertinoIcons.volume_up + : Icons.volume_up, color: Colors.white, ), ), diff --git a/lib/layouts/search/search_view.dart b/lib/layouts/search/search_view.dart index 9ca741f9a..55826dad8 100644 --- a/lib/layouts/search/search_view.dart +++ b/lib/layouts/search/search_view.dart @@ -1,9 +1,9 @@ import 'dart:ui'; import 'package:bluebubbles/helpers/constants.dart'; +import 'package:bluebubbles/helpers/navigator.dart'; import 'package:bluebubbles/helpers/ui_helpers.dart'; import 'package:bluebubbles/managers/settings_manager.dart'; -import 'package:get/get.dart'; import 'package:bluebubbles/blocs/message_bloc.dart'; import 'package:bluebubbles/helpers/utils.dart'; import 'package:bluebubbles/layouts/conversation_view/conversation_view.dart'; @@ -147,7 +147,7 @@ class SearchViewState extends State with TickerProviderStateMixin { // extendBodyBehindAppBar: true, backgroundColor: Theme.of(context).backgroundColor, appBar: PreferredSize( - preferredSize: Size(context.width, 80), + preferredSize: Size(CustomNavigator.width(context), 80), child: ClipRRect( child: BackdropFilter( child: AppBar( @@ -174,7 +174,7 @@ class SearchViewState extends State with TickerProviderStateMixin { mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.center, children: [ - Icon(Icons.search, color: Theme.of(context).textTheme.bodyText1!.color), + Icon(SettingsManager().settings.skin.value == Skins.iOS ? CupertinoIcons.search : Icons.search, color: Theme.of(context).textTheme.bodyText1!.color), Container(padding: EdgeInsets.only(right: 5.0)), Flexible( fit: FlexFit.loose, @@ -275,21 +275,17 @@ class SearchViewState extends State with TickerProviderStateMixin { children: [ ListTile( onTap: () { - Navigator.of(context).push( - CupertinoPageRoute( - builder: (BuildContext context) { - MessageBloc customBloc = new MessageBloc(chat, canLoadMore: false); - - return ConversationView( - chat: chat, - existingAttachments: [], - existingText: null, - isCreator: false, - customMessageBloc: customBloc, - onMessagesViewComplete: () { - customBloc.loadSearchChunk(message); - }, - ); + MessageBloc customBloc = new MessageBloc(chat, canLoadMore: false); + CustomNavigator.push( + context, + ConversationView( + chat: chat, + existingAttachments: [], + existingText: null, + isCreator: false, + customMessageBloc: customBloc, + onMessagesViewComplete: () { + customBloc.loadSearchChunk(message); }, ), ); @@ -312,7 +308,7 @@ class SearchViewState extends State with TickerProviderStateMixin { ), ), trailing: Icon( - Icons.arrow_forward_ios, + SettingsManager().settings.skin.value == Skins.iOS ? CupertinoIcons.forward : Icons.arrow_forward_ios, color: Theme.of(context).textTheme.bodyText1!.color, ), ), diff --git a/lib/layouts/settings/about_panel.dart b/lib/layouts/settings/about_panel.dart index ee8189996..56412ffbe 100644 --- a/lib/layouts/settings/about_panel.dart +++ b/lib/layouts/settings/about_panel.dart @@ -1,6 +1,7 @@ import 'dart:ui'; import 'package:bluebubbles/helpers/constants.dart'; +import 'package:bluebubbles/helpers/navigator.dart'; import 'package:bluebubbles/helpers/themes.dart'; import 'package:bluebubbles/helpers/ui_helpers.dart'; import 'package:bluebubbles/managers/settings_manager.dart'; @@ -50,7 +51,7 @@ class AboutPanel extends StatelessWidget { child: Scaffold( backgroundColor: SettingsManager().settings.skin.value != Skins.iOS ? tileColor : headerColor, appBar: PreferredSize( - preferredSize: Size(context.width, 80), + preferredSize: Size(CustomNavigator.width(context), 80), child: ClipRRect( child: BackdropFilter( child: AppBar( @@ -245,7 +246,7 @@ class AboutPanel extends StatelessWidget { ), backgroundColor: Theme.of(context).accentColor, content: SizedBox( - width: context.width * 3 / 5, + width: CustomNavigator.width(context) * 3 / 5, height: context.height * 1 / 9, child: ListView( physics: AlwaysScrollableScrollPhysics( diff --git a/lib/layouts/settings/attachment_panel.dart b/lib/layouts/settings/attachment_panel.dart index 286870c10..2d27ff955 100644 --- a/lib/layouts/settings/attachment_panel.dart +++ b/lib/layouts/settings/attachment_panel.dart @@ -1,6 +1,7 @@ import 'dart:ui'; import 'package:bluebubbles/helpers/constants.dart'; +import 'package:bluebubbles/helpers/navigator.dart'; import 'package:bluebubbles/helpers/themes.dart'; import 'package:bluebubbles/helpers/ui_helpers.dart'; import 'package:get/get.dart'; @@ -43,7 +44,7 @@ class AttachmentPanel extends StatelessWidget { child: Scaffold( backgroundColor: SettingsManager().settings.skin.value != Skins.iOS ? tileColor : headerColor, appBar: PreferredSize( - preferredSize: Size(context.width, 80), + preferredSize: Size(CustomNavigator.width(context), 80), child: ClipRRect( child: BackdropFilter( child: AppBar( diff --git a/lib/layouts/settings/chat_list_panel.dart b/lib/layouts/settings/chat_list_panel.dart index d21d8ddcd..6127943ed 100644 --- a/lib/layouts/settings/chat_list_panel.dart +++ b/lib/layouts/settings/chat_list_panel.dart @@ -2,6 +2,7 @@ import 'dart:ui'; import 'package:bluebubbles/helpers/constants.dart'; import 'package:bluebubbles/helpers/hex_color.dart'; +import 'package:bluebubbles/helpers/navigator.dart'; import 'package:bluebubbles/helpers/themes.dart'; import 'package:bluebubbles/helpers/ui_helpers.dart'; import 'package:bluebubbles/helpers/utils.dart'; @@ -11,7 +12,6 @@ import 'package:bluebubbles/managers/settings_manager.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flutter_slidable/flutter_slidable.dart'; import 'package:get/get.dart'; class ChatListPanel extends StatelessWidget { @@ -46,7 +46,7 @@ class ChatListPanel extends StatelessWidget { child: Scaffold( backgroundColor: SettingsManager().settings.skin.value != Skins.iOS ? tileColor : headerColor, appBar: PreferredSize( - preferredSize: Size(context.width, 80), + preferredSize: Size(CustomNavigator.width(context), 80), child: ClipRRect( child: BackdropFilter( child: AppBar( @@ -150,6 +150,24 @@ class ChatListPanel extends StatelessWidget { "Filters the chat list based on parameters set in iMessage (usually this removes old, inactive chats)", backgroundColor: tileColor, )), + Container( + color: tileColor, + child: Padding( + padding: const EdgeInsets.only(left: 65.0), + child: SettingsDivider(color: headerColor), + ), + ), + Obx(() => SettingsSwitch( + onChanged: (bool val) { + SettingsManager().settings.filterUnknownSenders.value = val; + saveSettings(); + }, + initialVal: SettingsManager().settings.filterUnknownSenders.value, + title: "Filter Unknown Senders", + subtitle: + "Turn off notifications for senders who aren't in your contacts and sort them into a separate chat list", + backgroundColor: tileColor, + )), SettingsHeader( headerColor: headerColor, tileColor: tileColor, @@ -272,7 +290,10 @@ class ChatListPanel extends StatelessWidget { // "Set the order for your pinned chats", // backgroundColor: tileColor, // onTap: () { - // Get.toNamed("/settings/pinned-order-panel"); + // CustomNavigator.pushSettings( + // context, + // PinnedOrderPanel(), + // ); // }, // trailing: Icon( // SettingsManager().settings.skin.value == Skins.iOS ? CupertinoIcons.chevron_right : Icons.arrow_forward, @@ -314,7 +335,7 @@ class ChatListPanel extends StatelessWidget { if (SettingsManager().settings.skin.value == Skins.iOS) return Container( color: tileColor, - constraints: BoxConstraints(maxWidth: context.width), + constraints: BoxConstraints(maxWidth: CustomNavigator.width(context)), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 15.0), child: Row( @@ -328,10 +349,10 @@ class ChatListPanel extends StatelessWidget { opacity: SettingsManager().settings.iosShowPin.value ? 1 : 0.7, child: Container( height: 60, - width: context.width / 5 - 8, + width: CustomNavigator.width(context) / 5 - 8, color: Colors.yellow[800], child: IconButton( - icon: Icon(Icons.star, color: Colors.white), + icon: Icon(CupertinoIcons.pin, color: Colors.white), onPressed: () async { SettingsManager().settings.iosShowPin.value = !SettingsManager().settings.iosShowPin.value; @@ -382,9 +403,9 @@ class ChatListPanel extends StatelessWidget { child: Container( height: 60, color: Colors.purple[700], - width: context.width / 5 - 8, + width: CustomNavigator.width(context) / 5 - 8, child: IconButton( - icon: Icon(Icons.notifications_off, color: Colors.white), + icon: Icon(CupertinoIcons.bell_slash, color: Colors.white), onPressed: () async { SettingsManager().settings.iosShowAlert.value = !SettingsManager().settings.iosShowAlert.value; @@ -429,9 +450,9 @@ class ChatListPanel extends StatelessWidget { child: Container( height: 60, color: Colors.red, - width: context.width / 5 - 8, + width: CustomNavigator.width(context) / 5 - 8, child: IconButton( - icon: Icon(Icons.delete_forever, color: Colors.white), + icon: Icon(CupertinoIcons.trash, color: Colors.white), onPressed: () async { SettingsManager().settings.iosShowDelete.value = !SettingsManager().settings.iosShowDelete.value; @@ -476,9 +497,9 @@ class ChatListPanel extends StatelessWidget { child: Container( height: 60, color: Colors.blue, - width: context.width / 5 - 8, + width: CustomNavigator.width(context) / 5 - 8, child: IconButton( - icon: Icon(Icons.mark_chat_read, color: Colors.white), + icon: Icon(CupertinoIcons.person_crop_circle_badge_exclam, color: Colors.white), onPressed: () { SettingsManager().settings.iosShowMarkRead.value = !SettingsManager().settings.iosShowMarkRead.value; @@ -524,9 +545,9 @@ class ChatListPanel extends StatelessWidget { child: Container( height: 60, color: Colors.red, - width: context.width / 5 - 8, + width: CustomNavigator.width(context) / 5 - 8, child: IconButton( - icon: Icon(Icons.archive, color: Colors.white), + icon: Icon(CupertinoIcons.tray_arrow_down, color: Colors.white), onPressed: () { SettingsManager().settings.iosShowArchive.value = !SettingsManager().settings.iosShowArchive.value; @@ -635,15 +656,15 @@ class ChatListPanel extends StatelessWidget { ), ), Obx(() => SettingsSwitch( - onChanged: (bool val) { - SettingsManager().settings.notifyOnChatList.value = val; - saveSettings(); - }, - initialVal: SettingsManager().settings.notifyOnChatList.value, - title: "Send Notifications on Chat List", - subtitle: "Sends notifications for new messages while in the chat list or chat creator", - backgroundColor: tileColor, - )), + onChanged: (bool val) { + SettingsManager().settings.cameraFAB.value = val; + saveSettings(); + }, + initialVal: SettingsManager().settings.cameraFAB.value, + title: "Add Camera Button", + subtitle: "Adds a dedicated camera button near the new chat creator button to easily send pictures", + backgroundColor: tileColor, + )), Container(color: tileColor, padding: EdgeInsets.only(top: 5.0)), Container( height: 30, diff --git a/lib/layouts/settings/custom_avatar_color_panel.dart b/lib/layouts/settings/custom_avatar_color_panel.dart index 684d39cbf..fe3050044 100644 --- a/lib/layouts/settings/custom_avatar_color_panel.dart +++ b/lib/layouts/settings/custom_avatar_color_panel.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:ui'; +import 'package:bluebubbles/helpers/navigator.dart'; import 'package:bluebubbles/helpers/ui_helpers.dart'; import 'package:get/get.dart'; import 'package:bluebubbles/helpers/utils.dart'; @@ -77,7 +78,7 @@ class CustomAvatarColorPanel extends GetView { child: Scaffold( backgroundColor: Theme.of(context).backgroundColor, appBar: PreferredSize( - preferredSize: Size(context.width, 80), + preferredSize: Size(CustomNavigator.width(context), 80), child: ClipRRect( child: BackdropFilter( child: AppBar( diff --git a/lib/layouts/settings/custom_avatar_panel.dart b/lib/layouts/settings/custom_avatar_panel.dart index 84705975d..241c8b97a 100644 --- a/lib/layouts/settings/custom_avatar_panel.dart +++ b/lib/layouts/settings/custom_avatar_panel.dart @@ -2,6 +2,7 @@ import 'dart:io'; import 'dart:ui'; import 'package:bluebubbles/blocs/chat_bloc.dart'; +import 'package:bluebubbles/helpers/navigator.dart'; import 'package:bluebubbles/helpers/ui_helpers.dart'; import 'package:bluebubbles/layouts/conversation_list/conversation_tile.dart'; import 'package:bluebubbles/layouts/widgets/avatar_crop.dart'; @@ -49,7 +50,7 @@ class CustomAvatarPanel extends GetView { child: Scaffold( backgroundColor: Theme.of(context).backgroundColor, appBar: PreferredSize( - preferredSize: Size(context.width, 80), + preferredSize: Size(CustomNavigator.width(context), 80), child: ClipRRect( child: BackdropFilter( child: AppBar( @@ -167,13 +168,19 @@ class CustomAvatarPanel extends GetView { .apply(color: Theme.of(context).primaryColor)), onPressed: () { Navigator.of(context).pop(); - Get.to(() => AvatarCrop(index: index)); + CustomNavigator.pushSettings( + context, + AvatarCrop(index: index), + ); }), ]); }, ); } else { - Get.to(() => AvatarCrop(index: index)); + CustomNavigator.pushSettings( + context, + AvatarCrop(index: index), + ); } }, ); diff --git a/lib/layouts/settings/misc_panel.dart b/lib/layouts/settings/misc_panel.dart index 9d2918196..271066357 100644 --- a/lib/layouts/settings/misc_panel.dart +++ b/lib/layouts/settings/misc_panel.dart @@ -2,11 +2,11 @@ import 'dart:ui'; import 'package:bluebubbles/helpers/constants.dart'; import 'package:bluebubbles/helpers/hex_color.dart'; +import 'package:bluebubbles/helpers/navigator.dart'; import 'package:bluebubbles/helpers/themes.dart'; import 'package:bluebubbles/helpers/ui_helpers.dart'; import 'package:bluebubbles/helpers/utils.dart'; import 'package:bluebubbles/layouts/settings/settings_panel.dart'; -import 'package:bluebubbles/layouts/widgets/contact_avatar_group_widget.dart'; import 'package:bluebubbles/layouts/widgets/theme_switcher/theme_switcher.dart'; import 'package:bluebubbles/managers/settings_manager.dart'; import 'package:flutter/cupertino.dart'; @@ -69,7 +69,7 @@ class MiscPanel extends StatelessWidget { child: Scaffold( backgroundColor: SettingsManager().settings.skin.value != Skins.iOS ? tileColor : headerColor, appBar: PreferredSize( - preferredSize: Size(context.width, 80), + preferredSize: Size(CustomNavigator.width(context), 80), child: ClipRRect( child: BackdropFilter( child: AppBar( @@ -147,13 +147,12 @@ class MiscPanel extends StatelessWidget { subtitle: "Show a snackbar whenever a message sync is completed", backgroundColor: tileColor, )), - if (SettingsManager().canAuthenticate) - SettingsHeader( - headerColor: headerColor, - tileColor: tileColor, - iosSubtitle: iosSubtitle, - materialSubtitle: materialSubtitle, - text: "Security"), + SettingsHeader( + headerColor: headerColor, + tileColor: tileColor, + iosSubtitle: iosSubtitle, + materialSubtitle: materialSubtitle, + text: "Security"), if (SettingsManager().canAuthenticate) Obx(() => SettingsSwitch( @@ -253,6 +252,24 @@ class MiscPanel extends StatelessWidget { else return SizedBox.shrink(); }), + if (SettingsManager().canAuthenticate) + Container( + color: tileColor, + child: Padding( + padding: const EdgeInsets.only(left: 65.0), + child: SettingsDivider(color: headerColor), + ), + ), + // Obx(() => SettingsSwitch( + // onChanged: (bool val) async { + // SettingsManager().settings.incognitoKeyboard.value = val; + // saveSettings(); + // }, + // initialVal: SettingsManager().settings.incognitoKeyboard.value, + // title: "Incognito Keyboard", + // subtitle: "Disables keyboard suggestions and prevents the keyboard from learning or storing any words you type in the message text field", + // backgroundColor: tileColor, + // )), SettingsHeader( headerColor: headerColor, tileColor: tileColor, diff --git a/lib/layouts/settings/notification_panel.dart b/lib/layouts/settings/notification_panel.dart new file mode 100644 index 000000000..dd3bd85e7 --- /dev/null +++ b/lib/layouts/settings/notification_panel.dart @@ -0,0 +1,598 @@ +import 'dart:ui'; + +import 'package:bluebubbles/blocs/chat_bloc.dart'; +import 'package:bluebubbles/helpers/constants.dart'; +import 'package:bluebubbles/helpers/hex_color.dart'; +import 'package:bluebubbles/helpers/navigator.dart'; +import 'package:bluebubbles/helpers/themes.dart'; +import 'package:bluebubbles/helpers/ui_helpers.dart'; +import 'package:bluebubbles/helpers/utils.dart'; +import 'package:bluebubbles/layouts/conversation_list/conversation_tile.dart'; +import 'package:bluebubbles/layouts/settings/settings_panel.dart'; +import 'package:bluebubbles/layouts/widgets/scroll_physics/custom_bouncing_scroll_physics.dart'; +import 'package:bluebubbles/layouts/widgets/theme_switcher/theme_switcher.dart'; +import 'package:bluebubbles/managers/contact_manager.dart'; +import 'package:bluebubbles/managers/event_dispatcher.dart'; +import 'package:bluebubbles/managers/settings_manager.dart'; +import 'package:bluebubbles/repository/models/chat.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:get/get.dart'; + +class NotificationPanelController extends GetxController with SingleGetTickerProviderMixin { + late final TabController tabController; + + @override + void onInit() { + tabController = TabController(vsync: this, length: 2); + super.onInit(); + } +} + +class NotificationPanel extends StatelessWidget { + final NotificationPanelController controller = Get.put(NotificationPanelController()); + + @override + Widget build(BuildContext context) { + final iosSubtitle = + Theme.of(context).textTheme.subtitle1?.copyWith(color: Colors.grey, fontWeight: FontWeight.w300); + final materialSubtitle = Theme.of(context) + .textTheme + .subtitle1 + ?.copyWith(color: Theme.of(context).primaryColor, fontWeight: FontWeight.bold); + Color headerColor; + Color tileColor; + if (Theme.of(context).accentColor.computeLuminance() < Theme.of(context).backgroundColor.computeLuminance() || + SettingsManager().settings.skin.value != Skins.iOS) { + headerColor = Theme.of(context).accentColor; + tileColor = Theme.of(context).backgroundColor; + } else { + headerColor = Theme.of(context).backgroundColor; + tileColor = Theme.of(context).accentColor; + } + if (SettingsManager().settings.skin.value == Skins.iOS && isEqual(Theme.of(context), oledDarkTheme)) { + tileColor = headerColor; + } + + return AnnotatedRegion( + value: SystemUiOverlayStyle( + systemNavigationBarColor: headerColor, // navigation bar color + systemNavigationBarIconBrightness: headerColor.computeLuminance() > 0.5 ? Brightness.dark : Brightness.light, + statusBarColor: Colors.transparent, // status bar color + ), + child: Scaffold( + backgroundColor: SettingsManager().settings.skin.value != Skins.iOS ? tileColor : headerColor, + appBar: PreferredSize( + preferredSize: Size(CustomNavigator.width(context), 80), + child: ClipRRect( + child: BackdropFilter( + child: AppBar( + brightness: ThemeData.estimateBrightnessForColor(headerColor), + toolbarHeight: 100.0, + elevation: 0, + leading: buildBackButton(context), + backgroundColor: headerColor.withOpacity(0.5), + title: Text( + "Notification Settings", + style: Theme.of(context).textTheme.headline1, + ), + ), + filter: ImageFilter.blur(sigmaX: 15, sigmaY: 15), + ), + ), + ), + body: TabBarView( + physics: ThemeSwitcher.getScrollPhysics(), + controller: controller.tabController, + children: [ + CustomScrollView( + physics: ThemeSwitcher.getScrollPhysics(), + slivers: [ + SliverList( + delegate: SliverChildListDelegate( + [ + Container( + height: SettingsManager().settings.skin.value == Skins.iOS ? 30 : 40, + alignment: Alignment.bottomLeft, + decoration: SettingsManager().settings.skin.value == Skins.iOS + ? BoxDecoration( + color: headerColor, + border: Border( + bottom: BorderSide( + color: Theme.of(context).dividerColor.lightenOrDarken(40), width: 0.3)), + ) + : BoxDecoration( + color: tileColor, + ), + child: Padding( + padding: const EdgeInsets.only(bottom: 8.0, left: 15), + child: Text("Notifications".psCapitalize, + style: SettingsManager().settings.skin.value == Skins.iOS ? iosSubtitle : materialSubtitle), + )), + Container(color: tileColor, padding: EdgeInsets.only(top: 5.0)), + Obx(() => SettingsSwitch( + onChanged: (bool val) { + SettingsManager().settings.notifyOnChatList.value = val; + saveSettings(); + }, + initialVal: SettingsManager().settings.notifyOnChatList.value, + title: "Send Notifications on Chat List", + subtitle: "Sends notifications for new messages while in the chat list or chat creator", + backgroundColor: tileColor, + )), + Container( + color: tileColor, + child: Padding( + padding: const EdgeInsets.only(left: 65.0), + child: SettingsDivider(color: headerColor), + ), + ), + Obx(() => SettingsSwitch( + onChanged: (bool val) { + SettingsManager().settings.notifyReactions.value = val; + saveSettings(); + }, + initialVal: SettingsManager().settings.notifyReactions.value, + title: "Notify for Reactions", + subtitle: "Sends notifications for incoming reactions", + backgroundColor: tileColor, + )), + Container( + color: tileColor, + child: Padding( + padding: const EdgeInsets.only(left: 65.0), + child: SettingsDivider(color: headerColor), + ), + ), + Obx(() { + if (SettingsManager().settings.skin.value == Skins.iOS) + return Container( + decoration: BoxDecoration( + color: tileColor, + ), + padding: EdgeInsets.only(left: 15), + child: Text("Select Notification Sound"), + ); + else return SizedBox.shrink(); + }), + Obx(() => SettingsOptions( + initial: SettingsManager().settings.notificationSound.value, + onChanged: (val) { + if (val == null) return; + SettingsManager().settings.notificationSound.value = val; + saveSettings(); + }, + options: ["default", "twig.wav", "walrus.wav", "sugarfree.wav", "raspberry.wav"], + textProcessing: (val) => val.toString().split(".").first.capitalizeFirst!, + capitalize: false, + title: "Notification Sound", + subtitle: "Set a custom notification sound for the app", + backgroundColor: tileColor, + secondaryColor: headerColor, + )), + Container( + color: tileColor, + child: Padding( + padding: const EdgeInsets.only(left: 65.0), + child: SettingsDivider(color: headerColor), + ), + ), + SettingsTile( + title: "Text Detection", + onTap: () async { + final TextEditingController controller = TextEditingController(); + controller.text = SettingsManager().settings.globalTextDetection.value; + Get.defaultDialog( + title: "Text detection", + titleStyle: Theme.of(context).textTheme.headline1, + backgroundColor: Theme.of(context).backgroundColor, + buttonColor: Theme.of(context).primaryColor, + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: Text("Enter any text separated by commas to whitelist notifications for. These are case insensitive.\n\nE.g. 'John,hey guys,homework'\n"), + ), + Theme( + data: Theme.of(context).copyWith( + inputDecorationTheme: const InputDecorationTheme( + labelStyle: TextStyle(color: Colors.grey), + ) + ), + child: TextField( + controller: controller, + decoration: InputDecoration( + labelText: "Enter text to whitelist...", + enabledBorder: OutlineInputBorder(borderSide: BorderSide(color: Colors.grey,)), + focusedBorder: OutlineInputBorder(borderSide: BorderSide(color: Theme.of(context).primaryColor,)), + ), + ), + ), + ] + ), + onConfirm: () async { + SettingsManager().settings.globalTextDetection.value = controller.text; + saveSettings(); + Get.back(); + }, + ); + }, + backgroundColor: tileColor, + subtitle: "Mute all chats except when your choice of text is found in a message", + ), + Container(color: tileColor, padding: EdgeInsets.only(top: 5.0)), + Container( + height: 30, + decoration: SettingsManager().settings.skin.value == Skins.iOS + ? BoxDecoration( + color: headerColor, + border: Border( + top: BorderSide(color: Theme.of(context).dividerColor.lightenOrDarken(40), width: 0.3)), + ) + : null, + ), + ], + ), + ), + SliverPadding( + padding: EdgeInsets.all(40), + ), + ], + ), + ChatList(), + ], + ), + bottomSheet: Container( + color: tileColor, + child: TabBar( + indicatorColor: Theme.of(context).primaryColor, + indicator: BoxDecoration( + border: Border( + top: BorderSide( + color: Colors.blue, + width: 3.0, + ), + ), + ), + tabs: [ + Tab( + icon: Icon( + SettingsManager().settings.skin.value == Skins.iOS ? CupertinoIcons.globe : Icons.public, + color: Theme.of(context).textTheme.bodyText1!.color, + ), + text: "GLOBAL OPTIONS" + ), + Tab( + icon: Icon( + SettingsManager().settings.skin.value == Skins.iOS ? CupertinoIcons.conversation_bubble : Icons.chat_bubble_outline, + color: Theme.of(context).textTheme.bodyText1!.color, + ), + text: "CHAT-SPECIFIC OPTIONS" + ), + ], + controller: controller.tabController, + ), + ), + ), + ); + } + + void saveSettings() { + SettingsManager().saveSettings(SettingsManager().settings); + } +} + +class ChatList extends StatefulWidget { + @override + State createState() => ChatListState(); +} + +class ChatListState extends State { + + String getSubtitle(Chat chat) { + if (chat.muteType == null) { + return "No settings set"; + } else { + String muteArgsStr = ""; + if (chat.muteArgs != null) { + if (chat.muteType == "mute_individuals") { + final participants = chat.participants.where((element) => chat.muteArgs!.split(",").contains(element.address)); + muteArgsStr = " - ${participants.length > 1 ? "${participants.length} people" : "1 person"}"; + } else if (chat.muteType == "temporary_mute") { + final DateTime time = DateTime.parse(chat.muteArgs!).toLocal(); + muteArgsStr = " until ${buildDate(time)}"; + } else if (chat.muteType == "text_detection") { + muteArgsStr = " for words ${chat.muteArgs!.split(",").join(", ")}"; + } + } + return "Mute type: ${chat.muteType!.split("_").join(" ").capitalizeFirst}$muteArgsStr"; + } + } + + bool shouldMuteDateTime(String? muteArgs) { + if (muteArgs == null) return false; + DateTime? time = DateTime.tryParse(muteArgs); + if (time == null) return false; + return DateTime.now().toLocal().difference(time).inSeconds.isNegative; + } + + @override + Widget build(BuildContext context) { + return CustomScrollView( + physics: AlwaysScrollableScrollPhysics( + parent: CustomBouncingScrollPhysics(), + ), + slivers: [ + Obx(() { + if (!ChatBloc().hasChats.value) { + return SliverToBoxAdapter( + child: Center( + child: Container( + padding: EdgeInsets.only(top: 50.0), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: Text( + "Loading chats...", + style: Theme.of(context).textTheme.subtitle1, + ), + ), + buildProgressIndicator(context, size: 15), + ], + ), + ), + ), + ); + } + if (ChatBloc().hasChats.value && ChatBloc().chats.isEmpty) { + return SliverToBoxAdapter( + child: Center( + child: Container( + padding: EdgeInsets.only(top: 50.0), + child: Text( + "You have no chats :(", + style: Theme.of(context).textTheme.subtitle1, + ), + ), + ), + ); + } + + return SliverList( + delegate: SliverChildBuilderDelegate( + (context, index) { + return ConversationTile( + key: Key( + ChatBloc().chats[index].guid.toString()), + chat: ChatBloc().chats[index], + inSelectMode: true, + subtitle: Text(getSubtitle(ChatBloc().chats[index]), style: Theme.of(context).textTheme.subtitle1), + onSelect: (_) async { + final chat = ChatBloc().chats[index]; + await Get.defaultDialog( + title: "Chat-Specific Settings", + titleStyle: Theme.of(context).textTheme.headline1, + confirm: Container(height: 0, width: 0), + cancel: Container(height: 0, width: 0), + content: Container( + height: context.height - 200, + child: SingleChildScrollView( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ListTile( + title: Text(chat.muteType == "mute" ? "Unmute" : "Mute", style: Theme.of(context).textTheme.bodyText1), + subtitle: Text("Completely ${chat.muteType == "mute" ? "unmute" : "mute"} this chat", style: Theme.of(context).textTheme.subtitle1), + onTap: () async { + Get.back(); + await chat.toggleMute(chat.muteType != "mute"); + await chat.update(); + if (this.mounted) setState(() {}); + EventDispatcher().emit("refresh", null); + }, + ), + ListTile( + title: Text("Mute Individuals", style: Theme.of(context).textTheme.bodyText1), + subtitle: Text("Mute certain individuals in this chat", style: Theme.of(context).textTheme.subtitle1), + onTap: () async { + Get.back(); + List> names = chat.participants.map((e) async => + await ContactManager().getContactTitle(e)).toList(); + Future> futureList = Future.wait(names); + List result = await futureList; + List existing = chat.muteArgs?.split(",") ?? []; + Get.defaultDialog( + title: "Mute Individuals", + titleStyle: Theme.of(context).textTheme.headline1, + backgroundColor: Theme.of(context).backgroundColor, + buttonColor: Theme.of(context).primaryColor, + + content: Container( + constraints: BoxConstraints( + maxHeight: 300, + ), + child: Center( + child: Container( + width: 300, + height: 200, + constraints: BoxConstraints( + maxHeight: Get.height - 300, + ), + child: StatefulBuilder( + builder: (context, setState) { + return SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: Text("Select the individuals you would like to mute"), + ), + ListView.builder( + shrinkWrap: true, + itemCount: chat.participants.length, + physics: NeverScrollableScrollPhysics(), + itemBuilder: (context, index) { + return Theme( + data: Theme.of(context).copyWith(unselectedWidgetColor: Theme.of(context).textTheme.headline1!.color), + child: CheckboxListTile( + value: existing.contains(chat.participants[index].address), + onChanged: (val) { + setState(() { + if (val!) { + existing.add(chat.participants[index].address); + } else { + existing.removeWhere((element) => element == chat.participants[index].address); + } + }); + }, + activeColor: Theme.of(context).primaryColor, + title: Text(result[index] ?? chat.participants[index].address, style: Theme.of(context).textTheme.headline1), + ), + ); + }, + ), + ], + ), + ); + } + ), + ), + ), + ), + onConfirm: () async { + if (existing.isEmpty) { + showSnackbar("Error", "Please select at least one person!"); + return; + } + await chat.toggleMute(false); + chat.muteType = "mute_individuals"; + chat.muteArgs = existing.join(","); + Get.back(); + await chat.update(); + if (this.mounted) setState(() {}); + EventDispatcher().emit("refresh", null); + }, + ); + }, + ), + ListTile( + title: Text(chat.muteType == "temporary_mute" && shouldMuteDateTime(chat.muteArgs) ? "Delete Temporary Mute" : "Temporary Mute", style: Theme.of(context).textTheme.bodyText1), + subtitle: Text(chat.muteType == "temporary_mute" && shouldMuteDateTime(chat.muteArgs) ? "" : "Mute this chat temporarily", style: Theme.of(context).textTheme.subtitle1), + onTap: () async { + Get.back(); + if (shouldMuteDateTime(chat.muteArgs)) { + chat.muteType = null; + chat.muteArgs = null; + } else { + final messageDate = await showDatePicker( + context: context, + initialDate: DateTime.now().toLocal(), + firstDate: DateTime.now().toLocal(), + lastDate: DateTime.now().toLocal().add(Duration(days: 365))); + if (messageDate != null) { + final messageTime = await showTimePicker(context: context, initialTime: TimeOfDay.now()); + if (messageTime != null) { + final finalDate = DateTime(messageDate.year, messageDate.month, messageDate.day, messageTime.hour, messageTime.minute); + await chat.toggleMute(false); + chat.muteType = "temporary_mute"; + chat.muteArgs = finalDate.toIso8601String(); + await chat.update(); + if (this.mounted) setState(() {}); + EventDispatcher().emit("refresh", null); + } + } + } + }, + ), + ListTile( + title: Text("Text Detection", style: Theme.of(context).textTheme.bodyText1), + subtitle: Text("Completely mute this chat, except when a message contains certain text", style: Theme.of(context).textTheme.subtitle1), + onTap: () async { + Get.back(); + final TextEditingController controller = TextEditingController(); + if (chat.muteType == "text_detection") { + controller.text = chat.muteArgs!; + } + Get.defaultDialog( + title: "Text detection", + titleStyle: Theme.of(context).textTheme.headline1, + backgroundColor: Theme.of(context).backgroundColor, + buttonColor: Theme.of(context).primaryColor, + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: Text("Enter any text separated by commas to whitelist notifications for. These are case insensitive.\n\nE.g. 'John,hey guys,homework'\n"), + ), + Theme( + data: Theme.of(context).copyWith( + inputDecorationTheme: const InputDecorationTheme( + labelStyle: TextStyle(color: Colors.grey), + ) + ), + child: TextField( + controller: controller, + decoration: InputDecoration( + labelText: "Enter text to whitelist...", + enabledBorder: OutlineInputBorder(borderSide: BorderSide(color: Colors.grey,)), + focusedBorder: OutlineInputBorder(borderSide: BorderSide(color: Theme.of(context).primaryColor,)), + ), + ), + ), + ] + ), + onConfirm: () async { + if (controller.text.isEmpty) { + showSnackbar("Error", "Please enter text!"); + return; + } + await chat.toggleMute(false); + chat.muteType = "text_detection"; + chat.muteArgs = controller.text; + Get.back(); + await chat.update(); + if (this.mounted) setState(() {}); + EventDispatcher().emit("refresh", null); + }, + ); + }, + ), + ListTile( + title: Text("Reset chat-specific settings", style: Theme.of(context).textTheme.bodyText1), + subtitle: Text("Delete your custom settings", style: Theme.of(context).textTheme.subtitle1), + onTap: () async { + Get.back(); + await chat.toggleMute(false); + chat.muteType = null; + chat.muteArgs = null; + await chat.update(); + if (this.mounted) setState(() {}); + EventDispatcher().emit("refresh", null); + }, + ), + ] + ), + ), + ), + barrierDismissible: true, + backgroundColor: Theme.of(context).backgroundColor, + ); + }, + ); + }, + childCount: ChatBloc().chats.length, + ), + ); + }), + SliverPadding( + padding: EdgeInsets.all(40), + ), + ], + ); + } +} \ No newline at end of file diff --git a/lib/layouts/settings/pinned_order_panel.dart b/lib/layouts/settings/pinned_order_panel.dart index b9f757762..a8b363eed 100644 --- a/lib/layouts/settings/pinned_order_panel.dart +++ b/lib/layouts/settings/pinned_order_panel.dart @@ -2,6 +2,7 @@ import 'dart:ui'; import 'package:bluebubbles/blocs/chat_bloc.dart'; +import 'package:bluebubbles/helpers/navigator.dart'; import 'package:bluebubbles/helpers/ui_helpers.dart'; import 'package:bluebubbles/layouts/conversation_list/conversation_tile.dart'; import 'package:bluebubbles/layouts/widgets/scroll_physics/custom_bouncing_scroll_physics.dart'; @@ -22,7 +23,7 @@ class PinnedOrderPanel extends StatelessWidget { child: Scaffold( backgroundColor: Theme.of(context).backgroundColor, appBar: PreferredSize( - preferredSize: Size(context.width, 80), + preferredSize: Size(CustomNavigator.width(context), 80), child: ClipRRect( child: BackdropFilter( child: AppBar( diff --git a/lib/layouts/settings/private_api_panel.dart b/lib/layouts/settings/private_api_panel.dart index 8ad8042a4..af31822ad 100644 --- a/lib/layouts/settings/private_api_panel.dart +++ b/lib/layouts/settings/private_api_panel.dart @@ -2,6 +2,7 @@ import 'dart:ui'; import 'package:bluebubbles/helpers/constants.dart'; import 'package:bluebubbles/helpers/hex_color.dart'; +import 'package:bluebubbles/helpers/navigator.dart'; import 'package:bluebubbles/helpers/utils.dart'; import 'package:bluebubbles/helpers/themes.dart'; import 'package:bluebubbles/helpers/ui_helpers.dart'; @@ -80,7 +81,7 @@ class PrivateAPIPanel extends GetView { child: Scaffold( backgroundColor: SettingsManager().settings.skin.value != Skins.iOS ? tileColor : headerColor, appBar: PreferredSize( - preferredSize: Size(context.width, 80), + preferredSize: Size(CustomNavigator.width(context), 80), child: ClipRRect( child: BackdropFilter( child: AppBar( diff --git a/lib/layouts/settings/redacted_mode_panel.dart b/lib/layouts/settings/redacted_mode_panel.dart index 79a1e5b7b..c9b623726 100644 --- a/lib/layouts/settings/redacted_mode_panel.dart +++ b/lib/layouts/settings/redacted_mode_panel.dart @@ -3,6 +3,7 @@ import 'dart:ui'; import 'package:bluebubbles/helpers/constants.dart'; import 'package:bluebubbles/helpers/hex_color.dart'; +import 'package:bluebubbles/helpers/navigator.dart'; import 'package:bluebubbles/helpers/utils.dart'; import 'package:bluebubbles/helpers/themes.dart'; import 'package:bluebubbles/helpers/ui_helpers.dart'; @@ -48,7 +49,7 @@ class RedactedModePanel extends StatelessWidget { child: Scaffold( backgroundColor: SettingsManager().settings.skin.value != Skins.iOS ? tileColor : headerColor, appBar: PreferredSize( - preferredSize: Size(context.width, 80), + preferredSize: Size(CustomNavigator.width(context), 80), child: ClipRRect( child: BackdropFilter( child: AppBar( diff --git a/lib/layouts/settings/scheduler_panel.dart b/lib/layouts/settings/scheduler_panel.dart index 45b2e7f27..f8a2f877f 100644 --- a/lib/layouts/settings/scheduler_panel.dart +++ b/lib/layouts/settings/scheduler_panel.dart @@ -1,5 +1,7 @@ import 'dart:ui'; +import 'package:bluebubbles/helpers/logger.dart'; +import 'package:bluebubbles/helpers/navigator.dart'; import 'package:bluebubbles/helpers/ui_helpers.dart'; import 'package:bluebubbles/helpers/utils.dart'; import 'package:bluebubbles/layouts/conversation_view/conversation_view.dart'; @@ -11,7 +13,6 @@ import 'package:bluebubbles/repository/models/scheduled.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:get/get.dart'; List> timeOptions = [ [300, "5 Minutes"], @@ -107,7 +108,7 @@ class _SchedulePanelState extends State { child: Scaffold( backgroundColor: Theme.of(context).backgroundColor, appBar: PreferredSize( - preferredSize: Size(context.width, 80), + preferredSize: Size(CustomNavigator.width(context), 80), child: ClipRRect( child: BackdropFilter( child: AppBar( @@ -158,7 +159,7 @@ class _SchedulePanelState extends State { errors = []; }); } else { - debugPrint("ERROR"); + Logger.error("Error"); } }, ), diff --git a/lib/layouts/settings/scheduling_panel.dart b/lib/layouts/settings/scheduling_panel.dart index 8fea23521..8b197bee2 100644 --- a/lib/layouts/settings/scheduling_panel.dart +++ b/lib/layouts/settings/scheduling_panel.dart @@ -1,5 +1,6 @@ import 'dart:ui'; +import 'package:bluebubbles/helpers/navigator.dart'; import 'package:bluebubbles/helpers/ui_helpers.dart'; import 'package:bluebubbles/helpers/utils.dart'; import 'package:bluebubbles/layouts/widgets/theme_switcher/theme_switcher.dart'; @@ -7,7 +8,6 @@ import 'package:bluebubbles/repository/models/scheduled.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:get/get.dart'; import 'package:intl/intl.dart'; class SchedulingPanel extends StatefulWidget { @@ -86,7 +86,7 @@ class _SchedulingPanelState extends State { // extendBodyBehindAppBar: true, backgroundColor: Theme.of(context).backgroundColor, appBar: PreferredSize( - preferredSize: Size(context.width, 80), + preferredSize: Size(CustomNavigator.width(context), 80), child: ClipRRect( child: BackdropFilter( child: AppBar( diff --git a/lib/layouts/settings/server_management_panel.dart b/lib/layouts/settings/server_management_panel.dart index 0287ba6a9..dab463bc6 100644 --- a/lib/layouts/settings/server_management_panel.dart +++ b/lib/layouts/settings/server_management_panel.dart @@ -4,6 +4,7 @@ import 'dart:io'; import 'dart:ui'; import 'package:bluebubbles/helpers/constants.dart'; +import 'package:bluebubbles/helpers/navigator.dart'; import 'package:bluebubbles/helpers/themes.dart'; import 'package:bluebubbles/helpers/hex_color.dart'; import 'package:bluebubbles/helpers/ui_helpers.dart'; @@ -47,6 +48,7 @@ class ServerManagementPanelController extends GetxController { final RxBool isRestarting = false.obs; final RxBool isRestartingMessages = false.obs; final RxBool isRestartingPrivateAPI = false.obs; + final RxDouble opacity = 1.0.obs; late Settings _settingsCopy; FCMData? _fcmDataCopy; @@ -113,7 +115,7 @@ class ServerManagementPanel extends GetView { child: Scaffold( backgroundColor: SettingsManager().settings.skin.value != Skins.iOS ? tileColor : headerColor, appBar: PreferredSize( - preferredSize: Size(context.width, 80), + preferredSize: Size(CustomNavigator.width(context), 80), child: ClipRRect( child: BackdropFilter( child: AppBar( @@ -159,36 +161,39 @@ class ServerManagementPanel extends GetView { color: tileColor, child: Padding( padding: const EdgeInsets.only(bottom: 8.0, left: 15, top: 8.0, right: 15), - child: SelectableText.rich( - TextSpan( - children: [ - TextSpan(text: "Connection Status: "), - TextSpan(text: describeEnum(SocketManager().state.value), style: TextStyle(color: getIndicatorColor(SocketManager().state.value))), - TextSpan(text: "\n\n"), - TextSpan(text: "Server URL: ${redact ? "Redacted" : controller._settingsCopy.serverAddress}"), - TextSpan(text: "\n\n"), - TextSpan(text: "Latency: ${redact ? "Redacted" : ((controller.latency.value ?? "N/A").toString() + " ms")}"), - TextSpan(text: "\n\n"), - TextSpan(text: "Server Version: ${redact ? "Redacted" : (controller.serverVersion.value ?? "N/A")}"), - TextSpan(text: "\n\n"), - TextSpan(text: "macOS Version: ${redact ? "Redacted" : (controller.macOSVersion.value ?? "N/A")}"), - TextSpan(text: "\n\n"), - TextSpan(text: "Tap to update values...", style: TextStyle(fontStyle: FontStyle.italic)), - ] + child: AnimatedOpacity( + duration: Duration(milliseconds: 300), + opacity: controller.opacity.value, + child: SelectableText.rich( + TextSpan( + children: [ + TextSpan(text: "Connection Status: "), + TextSpan(text: describeEnum(SocketManager().state.value), style: TextStyle(color: getIndicatorColor(SocketManager().state.value))), + TextSpan(text: "\n\n"), + TextSpan(text: "Server URL: ${redact ? "Redacted" : controller._settingsCopy.serverAddress}"), + TextSpan(text: "\n\n"), + TextSpan(text: "Latency: ${redact ? "Redacted" : ((controller.latency.value ?? "N/A").toString() + " ms")}"), + TextSpan(text: "\n\n"), + TextSpan(text: "Server Version: ${redact ? "Redacted" : (controller.serverVersion.value ?? "N/A")}"), + TextSpan(text: "\n\n"), + TextSpan(text: "macOS Version: ${redact ? "Redacted" : (controller.macOSVersion.value ?? "N/A")}"), + TextSpan(text: "\n\n"), + TextSpan(text: "Tap to update values...", style: TextStyle(fontStyle: FontStyle.italic)), + ] + ), + onTap: () { + if (SocketManager().state.value != SocketState.CONNECTED) return; + controller.opacity.value = 0.0; + int now = DateTime.now().toUtc().millisecondsSinceEpoch; + SocketManager().sendMessage("get-server-metadata", {}, (Map res) { + int later = DateTime.now().toUtc().millisecondsSinceEpoch; + controller.latency.value = later - now; + controller.macOSVersion.value = res['data']['os_version']; + controller.serverVersion.value = res['data']['server_version']; + controller.opacity.value = 1.0; + }); + }, ), - onTap: () { - if (SocketManager().state.value != SocketState.CONNECTED) return; - - int now = DateTime.now().toUtc().millisecondsSinceEpoch; - SocketManager().sendMessage("get-server-metadata", {}, (Map res) { - int later = DateTime.now().toUtc().millisecondsSinceEpoch; - controller.latency.value = later - now; - }); - SocketManager().sendMessage("get-server-metadata", {}, (Map res) { - controller.macOSVersion.value = res['data']['os_version']; - controller.serverVersion.value = res['data']['server_version']; - }); - }, ), ) ); diff --git a/lib/layouts/settings/settings_panel.dart b/lib/layouts/settings/settings_panel.dart index 19595761d..41181407b 100644 --- a/lib/layouts/settings/settings_panel.dart +++ b/lib/layouts/settings/settings_panel.dart @@ -3,9 +3,22 @@ import 'dart:io'; import 'dart:ui'; import 'package:adaptive_theme/adaptive_theme.dart'; +import 'package:bluebubbles/helpers/logger.dart'; +import 'package:bluebubbles/helpers/navigator.dart'; import 'package:bluebubbles/helpers/share.dart'; import 'package:bluebubbles/helpers/themes.dart'; import 'package:bluebubbles/helpers/ui_helpers.dart'; +import 'package:bluebubbles/layouts/settings/about_panel.dart'; +import 'package:bluebubbles/layouts/settings/attachment_panel.dart'; +import 'package:bluebubbles/layouts/settings/chat_list_panel.dart'; +import 'package:bluebubbles/layouts/settings/conversation_panel.dart'; +import 'package:bluebubbles/layouts/settings/notification_panel.dart'; +import 'package:bluebubbles/layouts/settings/private_api_panel.dart'; +import 'package:bluebubbles/layouts/settings/redacted_mode_panel.dart'; +import 'package:bluebubbles/layouts/settings/server_management_panel.dart'; +import 'package:bluebubbles/layouts/settings/theme_panel.dart'; +import 'package:bluebubbles/layouts/settings/troubleshoot_panel.dart'; +import 'package:bluebubbles/layouts/widgets/vertical_split_view.dart'; import 'package:bluebubbles/managers/method_channel_interface.dart'; import 'package:bluebubbles/repository/models/theme_entry.dart'; import 'package:bluebubbles/repository/models/theme_object.dart'; @@ -48,17 +61,39 @@ class _SettingsPanelState extends State { @override Widget build(BuildContext context) { + Color headerColor; + if (Theme.of(context).accentColor.computeLuminance() < Theme.of(context).backgroundColor.computeLuminance() || + SettingsManager().settings.skin.value != Skins.iOS) { + headerColor = Theme.of(context).accentColor; + } else { + headerColor = Theme.of(context).backgroundColor; + } + return AnnotatedRegion( + value: SystemUiOverlayStyle( + systemNavigationBarColor: headerColor, // navigation bar color + systemNavigationBarIconBrightness: headerColor.computeLuminance() > 0.5 ? Brightness.dark : Brightness.light, + statusBarColor: Colors.transparent, // status bar color + ), + child: buildForDevice(), + ); + } + + Widget buildSettingsList() { Widget nextIcon = Obx(() => Icon( - SettingsManager().settings.skin.value == Skins.iOS ? CupertinoIcons.chevron_right : Icons.arrow_forward, - color: Colors.grey, - )); + SettingsManager().settings.skin.value == Skins.iOS ? CupertinoIcons.chevron_right : Icons.arrow_forward, + color: Colors.grey, + )); - final iosSubtitle = Theme.of(context).textTheme.subtitle1?.copyWith(color: Colors.grey, fontWeight: FontWeight.w300); - final materialSubtitle = Theme.of(context).textTheme.subtitle1?.copyWith(color: Theme.of(context).primaryColor, fontWeight: FontWeight.bold); + final iosSubtitle = + Theme.of(context).textTheme.subtitle1?.copyWith(color: Colors.grey, fontWeight: FontWeight.w300); + final materialSubtitle = Theme.of(context) + .textTheme + .subtitle1 + ?.copyWith(color: Theme.of(context).primaryColor, fontWeight: FontWeight.bold); Color headerColor; Color tileColor; - if (Theme.of(context).accentColor.computeLuminance() < Theme.of(context).backgroundColor.computeLuminance() - || SettingsManager().settings.skin.value != Skins.iOS) { + if (Theme.of(context).accentColor.computeLuminance() < Theme.of(context).backgroundColor.computeLuminance() || + SettingsManager().settings.skin.value != Skins.iOS) { headerColor = Theme.of(context).accentColor; tileColor = Theme.of(context).backgroundColor; } else { @@ -68,618 +103,763 @@ class _SettingsPanelState extends State { if (SettingsManager().settings.skin.value == Skins.iOS && isEqual(Theme.of(context), oledDarkTheme)) { tileColor = headerColor; } - - return AnnotatedRegion( - value: SystemUiOverlayStyle( - systemNavigationBarColor: headerColor, // navigation bar color - systemNavigationBarIconBrightness: - headerColor.computeLuminance() > 0.5 ? Brightness.dark : Brightness.light, - statusBarColor: Colors.transparent, // status bar color - ), - child: Obx(() => Scaffold( - backgroundColor: SettingsManager().settings.skin.value != Skins.iOS ? tileColor : headerColor, - appBar: PreferredSize( - preferredSize: Size(context.width, 80), - child: ClipRRect( - child: BackdropFilter( - child: AppBar( - brightness: ThemeData.estimateBrightnessForColor(headerColor), - toolbarHeight: 100.0, - elevation: 0, - leading: buildBackButton(context), - backgroundColor: headerColor.withOpacity(0.5), - title: Text( - "Settings", - style: Theme.of(context).textTheme.headline1, - ), + return Obx(() => Scaffold( + backgroundColor: SettingsManager().settings.skin.value != Skins.iOS ? tileColor : headerColor, + appBar: PreferredSize( + preferredSize: Size(CustomNavigator.width(context), 80), + child: ClipRRect( + child: BackdropFilter( + child: AppBar( + brightness: ThemeData.estimateBrightnessForColor(headerColor), + toolbarHeight: 100.0, + elevation: 0, + leading: buildBackButton(context), + backgroundColor: headerColor.withOpacity(0.5), + title: Text( + "Settings", + style: Theme.of(context).textTheme.headline1, ), - filter: ImageFilter.blur(sigmaX: 15, sigmaY: 15), ), + filter: ImageFilter.blur(sigmaX: 15, sigmaY: 15), ), ), - body: CustomScrollView( - physics: ThemeSwitcher.getScrollPhysics(), - slivers: [ - SliverList( - delegate: SliverChildListDelegate( - [ - Container( - height: SettingsManager().settings.skin.value == Skins.iOS ? 30 : 40, - alignment: Alignment.bottomLeft, - decoration: SettingsManager().settings.skin.value == Skins.iOS ? BoxDecoration( - color: headerColor, - border: Border( - bottom: BorderSide(color: Theme.of(context).dividerColor.lightenOrDarken(40), width: 0.3) - ), - ) : BoxDecoration( - color: tileColor, - ), - child: Padding( - padding: const EdgeInsets.only(bottom: 8.0, left: 15), - child: Text("Server Management".psCapitalize, style: SettingsManager().settings.skin.value == Skins.iOS ? iosSubtitle : materialSubtitle), - ) - ), - Container(color: tileColor, padding: EdgeInsets.only(top: 5.0)), - Obx(() { - String? subtitle; - switch (SocketManager().state.value) { - case SocketState.CONNECTED: - subtitle = "Connected"; - break; - case SocketState.DISCONNECTED: - subtitle = "Disconnected"; - break; - case SocketState.ERROR: - subtitle = "Error"; - break; - case SocketState.CONNECTING: - subtitle = "Connecting..."; - break; - case SocketState.FAILED: - subtitle = "Failed to connect"; - break; - default: - subtitle = "Error"; - break; - } + ), + body: CustomScrollView( + physics: ThemeSwitcher.getScrollPhysics(), + slivers: [ + SliverList( + delegate: SliverChildListDelegate( + [ + Container( + height: SettingsManager().settings.skin.value == Skins.iOS ? 30 : 40, + alignment: Alignment.bottomLeft, + decoration: SettingsManager().settings.skin.value == Skins.iOS + ? BoxDecoration( + color: headerColor, + border: Border( + bottom: BorderSide( + color: Theme.of(context).dividerColor.lightenOrDarken(40), width: 0.3)), + ) + : BoxDecoration( + color: tileColor, + ), + child: Padding( + padding: const EdgeInsets.only(bottom: 8.0, left: 15), + child: Text("Server Management".psCapitalize, + style: SettingsManager().settings.skin.value == Skins.iOS + ? iosSubtitle + : materialSubtitle), + )), + Container(color: tileColor, padding: EdgeInsets.only(top: 5.0)), + Obx(() { + String? subtitle; + switch (SocketManager().state.value) { + case SocketState.CONNECTED: + subtitle = "Connected"; + break; + case SocketState.DISCONNECTED: + subtitle = "Disconnected"; + break; + case SocketState.ERROR: + subtitle = "Error"; + break; + case SocketState.CONNECTING: + subtitle = "Connecting..."; + break; + case SocketState.FAILED: + subtitle = "Failed to connect"; + break; + default: + subtitle = "Error"; + break; + } - return SettingsTile( - backgroundColor: tileColor, - title: "Connection & Server", - subtitle: subtitle, - onTap: () async { - Get.toNamed("/settings/server-management-panel"); - }, - onLongPress: () { - Clipboard.setData(new ClipboardData(text: _settingsCopy.serverAddress.value)); - showSnackbar('Copied', "Address copied to clipboard"); - }, - leading: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Container( - width: 32, - height: 32, - decoration: BoxDecoration( - color: SettingsManager().settings.skin.value == Skins.iOS ? - getIndicatorColor(SocketManager().state.value) : Colors.transparent, - borderRadius: BorderRadius.circular(5), - ), - alignment: Alignment.center, - child: Stack( - children: [ - Icon(SettingsManager().settings.skin.value == Skins.iOS - ? CupertinoIcons.antenna_radiowaves_left_right : Icons.router, - color: SettingsManager().settings.skin.value == Skins.iOS ? - Colors.white : Colors.grey, - size: SettingsManager().settings.skin.value == Skins.iOS ? 23 : 30, - ), - if (SettingsManager().settings.skin.value != Skins.iOS) - Positioned.fill( - child: Align( - alignment: Alignment.bottomRight, - child: getIndicatorIcon(SocketManager().state.value, size: 15, showAlpha: false) - ), - ), - ] - ), - ), - ], - ), - trailing: nextIcon, - ); - }), - SettingsHeader( - headerColor: headerColor, - tileColor: tileColor, - iosSubtitle: iosSubtitle, - materialSubtitle: materialSubtitle, - text: "Appearance" - ), - SettingsTile( + return SettingsTile( backgroundColor: tileColor, - title: "Theme Settings", - subtitle: SettingsManager().settings.skin.value.toString().split(".").last - + " | " + AdaptiveTheme.of(context).mode.toString().split(".").last.capitalizeFirst! + " Mode", - onTap: () { - Get.toNamed("/settings/theme-panel"); + title: "Connection & Server", + subtitle: subtitle, + onTap: () async { + CustomNavigator.pushAndRemoveSettingsUntil( + context, + ServerManagementPanel(), + (route) => route.isFirst, + binding: ServerManagementPanelBinding(), + ); }, - trailing: nextIcon, - leading: SettingsLeadingIcon( - iosIcon: CupertinoIcons.paintbrush, - materialIcon: Icons.palette, - ), - ), - SettingsHeader( - headerColor: headerColor, - tileColor: tileColor, - iosSubtitle: iosSubtitle, - materialSubtitle: materialSubtitle, - text: "Application Settings" - ), - SettingsTile( - backgroundColor: tileColor, - title: "Media Settings", - onTap: () { - Get.toNamed("/settings/attachment-panel"); + onLongPress: () { + Clipboard.setData(new ClipboardData(text: _settingsCopy.serverAddress.value)); + showSnackbar('Copied', "Address copied to clipboard"); }, - leading: SettingsLeadingIcon( - iosIcon: CupertinoIcons.paperclip, - materialIcon: Icons.attachment, + leading: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + width: 32, + height: 32, + decoration: BoxDecoration( + color: SettingsManager().settings.skin.value == Skins.iOS + ? getIndicatorColor(SocketManager().state.value) + : Colors.transparent, + borderRadius: BorderRadius.circular(5), + ), + alignment: Alignment.center, + child: Stack(children: [ + Icon( + SettingsManager().settings.skin.value == Skins.iOS + ? CupertinoIcons.antenna_radiowaves_left_right + : Icons.router, + color: + SettingsManager().settings.skin.value == Skins.iOS ? Colors.white : Colors.grey, + size: SettingsManager().settings.skin.value == Skins.iOS ? 23 : 30, + ), + if (SettingsManager().settings.skin.value != Skins.iOS) + Positioned.fill( + child: Align( + alignment: Alignment.bottomRight, + child: getIndicatorIcon(SocketManager().state.value, + size: 15, showAlpha: false)), + ), + ]), + ), + ], ), trailing: nextIcon, + ); + }), + SettingsHeader( + headerColor: headerColor, + tileColor: tileColor, + iosSubtitle: iosSubtitle, + materialSubtitle: materialSubtitle, + text: "Appearance"), + SettingsTile( + backgroundColor: tileColor, + title: "Theme Settings", + subtitle: SettingsManager().settings.skin.value.toString().split(".").last + + " | " + + AdaptiveTheme.of(context).mode.toString().split(".").last.capitalizeFirst! + + " Mode", + onTap: () { + CustomNavigator.pushAndRemoveSettingsUntil( + context, + ThemePanel(), + (route) => route.isFirst, + binding: ThemePanelBinding(), + ); + }, + trailing: nextIcon, + leading: SettingsLeadingIcon( + iosIcon: CupertinoIcons.paintbrush, + materialIcon: Icons.palette, ), - Container( - color: tileColor, - child: Padding( - padding: const EdgeInsets.only(left: 65.0), - child: SettingsDivider(color: headerColor), - ), + ), + SettingsHeader( + headerColor: headerColor, + tileColor: tileColor, + iosSubtitle: iosSubtitle, + materialSubtitle: materialSubtitle, + text: "Application Settings"), + SettingsTile( + backgroundColor: tileColor, + title: "Media Settings", + onTap: () { + CustomNavigator.pushAndRemoveSettingsUntil( + context, + AttachmentPanel(), + (route) => route.isFirst, + ); + }, + leading: SettingsLeadingIcon( + iosIcon: CupertinoIcons.paperclip, + materialIcon: Icons.attachment, ), - SettingsTile( - backgroundColor: tileColor, - title: "Chat List Settings", - onTap: () { - Get.toNamed("/settings/chat-list-panel"); - }, - leading: SettingsLeadingIcon( - iosIcon: CupertinoIcons.square_list, - materialIcon: Icons.list, - ), - trailing: nextIcon, + trailing: nextIcon, + ), + Container( + color: tileColor, + child: Padding( + padding: const EdgeInsets.only(left: 65.0), + child: SettingsDivider(color: headerColor), ), - Container( - color: tileColor, - child: Padding( - padding: const EdgeInsets.only(left: 65.0), - child: SettingsDivider(color: headerColor), - ), + ), + SettingsTile( + backgroundColor: tileColor, + title: "Notification Settings", + onTap: () { + CustomNavigator.pushAndRemoveSettingsUntil( + context, + NotificationPanel(), + (route) => route.isFirst, + ); + }, + leading: SettingsLeadingIcon( + iosIcon: CupertinoIcons.bell, + materialIcon: Icons.notifications_on, ), - SettingsTile( - backgroundColor: tileColor, - title: "Conversation Settings", - onTap: () { - Get.toNamed("/settings/conversation-panel"); - }, - leading: SettingsLeadingIcon( - iosIcon: CupertinoIcons.chat_bubble, - materialIcon: Icons.sms, - ), - trailing: nextIcon, + trailing: nextIcon, + ), + Container( + color: tileColor, + child: Padding( + padding: const EdgeInsets.only(left: 65.0), + child: SettingsDivider(color: headerColor), ), - Container( - color: tileColor, - child: Padding( - padding: const EdgeInsets.only(left: 65.0), - child: SettingsDivider(color: headerColor), - ), + ), + SettingsTile( + backgroundColor: tileColor, + title: "Chat List Settings", + onTap: () { + CustomNavigator.pushAndRemoveSettingsUntil( + context, + ChatListPanel(), + (route) => route.isFirst, + ); + }, + leading: SettingsLeadingIcon( + iosIcon: CupertinoIcons.square_list, + materialIcon: Icons.list, ), - SettingsTile( - backgroundColor: tileColor, - title: "Misc and Advanced Settings", - onTap: () { - Navigator.of(context).push( - CupertinoPageRoute( - builder: (context) => MiscPanel(), - ), - ); - }, - leading: SettingsLeadingIcon( - iosIcon: CupertinoIcons.ellipsis_circle, - materialIcon: Icons.more_vert, - ), - trailing: nextIcon, + trailing: nextIcon, + ), + Container( + color: tileColor, + child: Padding( + padding: const EdgeInsets.only(left: 65.0), + child: SettingsDivider(color: headerColor), ), - SettingsHeader( - headerColor: headerColor, - tileColor: tileColor, - iosSubtitle: iosSubtitle, - materialSubtitle: materialSubtitle, - text: "Advanced" + ), + SettingsTile( + backgroundColor: tileColor, + title: "Conversation Settings", + onTap: () { + CustomNavigator.pushAndRemoveSettingsUntil( + context, + ConversationPanel(), + (route) => route.isFirst, + ); + }, + leading: SettingsLeadingIcon( + iosIcon: CupertinoIcons.chat_bubble, + materialIcon: Icons.sms, ), - SettingsTile( - backgroundColor: tileColor, - title: "Private API Features", - subtitle: "Private API ${SettingsManager().settings.enablePrivateAPI.value ? "Enabled" : "Disabled"}", - trailing: nextIcon, - onTap: () async { - Get.toNamed("/settings/private-api-panel"); - }, - leading: SettingsLeadingIcon( - iosIcon: CupertinoIcons.exclamationmark_shield, - materialIcon: Icons.gpp_maybe, - containerColor: getIndicatorColor(SettingsManager().settings.enablePrivateAPI.value ? SocketState.CONNECTED : SocketState.CONNECTING), - ), + trailing: nextIcon, + ), + Container( + color: tileColor, + child: Padding( + padding: const EdgeInsets.only(left: 65.0), + child: SettingsDivider(color: headerColor), ), - Container( - color: tileColor, - child: Padding( - padding: const EdgeInsets.only(left: 65.0), - child: SettingsDivider(color: headerColor), - ), + ), + SettingsTile( + backgroundColor: tileColor, + title: "Misc and Advanced Settings", + onTap: () { + CustomNavigator.pushAndRemoveSettingsUntil( + context, + MiscPanel(), + (route) => route.isFirst, + ); + }, + leading: SettingsLeadingIcon( + iosIcon: CupertinoIcons.ellipsis_circle, + materialIcon: Icons.more_vert, ), - SettingsTile( - backgroundColor: tileColor, - title: "Redacted Mode", - subtitle: "Redacted Mode ${SettingsManager().settings.redactedMode.value ? "Enabled" : "Disabled"}", - trailing: nextIcon, - onTap: () async { - Get.toNamed("/settings/redacted-mode-panel"); - }, - leading: SettingsLeadingIcon( - iosIcon: CupertinoIcons.wand_stars, - materialIcon: Icons.auto_fix_high, - containerColor: getIndicatorColor(SettingsManager().settings.redactedMode.value ? SocketState.CONNECTED : SocketState.CONNECTING), - ), + trailing: nextIcon, + ), + SettingsHeader( + headerColor: headerColor, + tileColor: tileColor, + iosSubtitle: iosSubtitle, + materialSubtitle: materialSubtitle, + text: "Advanced"), + SettingsTile( + backgroundColor: tileColor, + title: "Private API Features", + subtitle: + "Private API ${SettingsManager().settings.enablePrivateAPI.value ? "Enabled" : "Disabled"}", + trailing: nextIcon, + onTap: () async { + CustomNavigator.pushAndRemoveSettingsUntil( + context, + PrivateAPIPanel(), + (route) => route.isFirst, + binding: PrivateAPIPanelBinding(), + ); + }, + leading: SettingsLeadingIcon( + iosIcon: CupertinoIcons.exclamationmark_shield, + materialIcon: Icons.gpp_maybe, + containerColor: getIndicatorColor(SettingsManager().settings.enablePrivateAPI.value + ? SocketState.CONNECTED + : SocketState.CONNECTING), ), - // SettingsTile( - // title: "Message Scheduling", - // trailing: Icon(Icons.arrow_forward_ios, - // color: Theme.of(context).primaryColor), - // onTap: () async { - // Navigator.of(context).push( - // CupertinoPageRoute( - // builder: (context) => SchedulingPanel(), - // ), - // ); - // }, - // ), - // SettingsTile( - // title: "Search", - // trailing: Icon(Icons.arrow_forward_ios, - // color: Theme.of(context).primaryColor), - // onTap: () async { - // Navigator.of(context).push( - // CupertinoPageRoute( - // builder: (context) => SearchView(), - // ), - // ); - // }, - // ), - SettingsHeader( - headerColor: headerColor, - tileColor: tileColor, - iosSubtitle: iosSubtitle, - materialSubtitle: materialSubtitle, - text: "About" + ), + Container( + color: tileColor, + child: Padding( + padding: const EdgeInsets.only(left: 65.0), + child: SettingsDivider(color: headerColor), ), - SettingsTile( - backgroundColor: tileColor, - title: "About & Links", - subtitle: "Donate, Rate, Changelog, & More", - onTap: () { - Get.toNamed("/settings/about-panel"); - }, - trailing: nextIcon, - leading: SettingsLeadingIcon( - iosIcon: CupertinoIcons.info_circle, - materialIcon: Icons.info, - ), + ), + SettingsTile( + backgroundColor: tileColor, + title: "Redacted Mode", + subtitle: + "Redacted Mode ${SettingsManager().settings.redactedMode.value ? "Enabled" : "Disabled"}", + trailing: nextIcon, + onTap: () async { + CustomNavigator.pushAndRemoveSettingsUntil( + context, + RedactedModePanel(), + (route) => route.isFirst, + ); + }, + leading: SettingsLeadingIcon( + iosIcon: CupertinoIcons.wand_stars, + materialIcon: Icons.auto_fix_high, + containerColor: getIndicatorColor(SettingsManager().settings.redactedMode.value + ? SocketState.CONNECTED + : SocketState.CONNECTING), ), - SettingsHeader( - headerColor: headerColor, - tileColor: tileColor, - iosSubtitle: iosSubtitle, - materialSubtitle: materialSubtitle, - text: "Backup and Reset" + ), + // SettingsTile( + // title: "Message Scheduling", + // trailing: Icon(Icons.arrow_forward_ios, + // color: Theme.of(context).primaryColor), + // onTap: () async { + // Navigator.of(context).push( + // CupertinoPageRoute( + // builder: (context) => SchedulingPanel(), + // ), + // ); + // }, + // ), + // SettingsTile( + // title: "Search", + // trailing: Icon(Icons.arrow_forward_ios, + // color: Theme.of(context).primaryColor), + // onTap: () async { + // Navigator.of(context).push( + // CupertinoPageRoute( + // builder: (context) => SearchView(), + // ), + // ); + // }, + // ), + SettingsHeader( + headerColor: headerColor, + tileColor: tileColor, + iosSubtitle: iosSubtitle, + materialSubtitle: materialSubtitle, + text: "About"), + SettingsTile( + backgroundColor: tileColor, + title: "About & Links", + subtitle: "Donate, Rate, Changelog, & More", + onTap: () { + CustomNavigator.pushAndRemoveSettingsUntil( + context, + AboutPanel(), + (route) => route.isFirst, + ); + }, + trailing: nextIcon, + leading: SettingsLeadingIcon( + iosIcon: CupertinoIcons.info_circle, + materialIcon: Icons.info, ), - SettingsTile( - backgroundColor: tileColor, - onTap: () { - Get.defaultDialog( - title: "Backup and Restore", - titleStyle: Theme.of(context).textTheme.headline1, - confirm: Container(height: 0, width: 0), - cancel: Container(height: 0, width: 0), - content: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - SizedBox( - height: 15.0, - ), - Text("Load / Save Locally", style: Theme.of(context).textTheme.subtitle1), - Padding( - padding: EdgeInsets.symmetric(horizontal: 8, vertical: 5), - child: Container(color: Colors.grey, height: 0.5), + ), + SettingsHeader( + headerColor: headerColor, + tileColor: tileColor, + iosSubtitle: iosSubtitle, + materialSubtitle: materialSubtitle, + text: "Backup and Reset"), + SettingsTile( + backgroundColor: tileColor, + onTap: () { + Get.defaultDialog( + title: "Backup and Restore", + titleStyle: Theme.of(context).textTheme.headline1, + confirm: Container(height: 0, width: 0), + cancel: Container(height: 0, width: 0), + content: Column(mainAxisAlignment: MainAxisAlignment.center, children: [ + SizedBox( + height: 15.0, + ), + Text("Load / Save Locally", style: Theme.of(context).textTheme.subtitle1), + Padding( + padding: EdgeInsets.symmetric(horizontal: 8, vertical: 5), + child: Container(color: Colors.grey, height: 0.5), + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + style: ElevatedButton.styleFrom( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + primary: Theme.of(context).primaryColor, ), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - ElevatedButton( - style: ElevatedButton.styleFrom( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - primary: Theme.of(context).primaryColor, + onPressed: () async { + String directoryPath = "/storage/emulated/0/Download/BlueBubbles-settings-"; + DateTime now = DateTime.now().toLocal(); + String filePath = directoryPath + + "${now.year}${now.month}${now.day}_${now.hour}${now.minute}${now.second}" + + ".json"; + File file = File(filePath); + await file.create(recursive: true); + Map json = SettingsManager().settings.toMap(); + String jsonString = jsonEncode(json); + await file.writeAsString(jsonString); + Get.back(); + showSnackbar( + "Success", + "Settings exported successfully to downloads folder", + durationMs: 2000, + button: TextButton( + style: TextButton.styleFrom( + backgroundColor: Get.theme.accentColor, ), - onPressed: () async { - String directoryPath = "/storage/emulated/0/Download/BlueBubbles-settings-"; - DateTime now = DateTime.now().toLocal(); - String filePath = directoryPath + "${now.year}${now.month}${now.day}_${now.hour}${now.minute}${now.second}" + ".json"; - File file = File(filePath); - await file.create(recursive: true); - Map json = SettingsManager().settings.toMap(); - String jsonString = jsonEncode(json); - await file.writeAsString(jsonString); - Get.back(); - showSnackbar( - "Success", - "Settings exported successfully to downloads folder", - durationMs: 2000, - button: TextButton( - style: TextButton.styleFrom( - backgroundColor: Get.theme.accentColor, - ), - onPressed: () { - Share.file("BlueBubbles Settings", filePath); - }, - child: Text("SHARE", style: TextStyle(color: Theme.of(context).primaryColor)), - ), - ); + onPressed: () { + Share.file("BlueBubbles Settings", filePath); }, - child: Text( - "Save Settings", - style: TextStyle( - color: Theme.of(context).textTheme.bodyText1!.color, - fontSize: 13, - ), - ), + child: Text("SHARE", style: TextStyle(color: Theme.of(context).primaryColor)), ), - SizedBox(width: 10), - ElevatedButton( - style: ElevatedButton.styleFrom( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - side: BorderSide(color: Theme.of(context).primaryColor) - ), - primary: Theme.of(context).backgroundColor, - ), - onPressed: () async { - List? res = await MethodChannelInterface().invokeMethod("pick-file", { - "mimeTypes": ["application/json"], - "allowMultiple": false, - }); - if (res == null || res.isEmpty) return; + ); + }, + child: Text( + "Save Settings", + style: TextStyle( + color: Theme.of(context).textTheme.bodyText1!.color, + fontSize: 13, + ), + ), + ), + SizedBox(width: 10), + ElevatedButton( + style: ElevatedButton.styleFrom( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + side: BorderSide(color: Theme.of(context).primaryColor)), + primary: Theme.of(context).backgroundColor, + ), + onPressed: () async { + List? res = await MethodChannelInterface().invokeMethod("pick-file", { + "mimeTypes": ["application/json"], + "allowMultiple": false, + }); + if (res == null || res.isEmpty) return; - try { - String jsonString = await File(res.first.toString()).readAsString(); - Map json = jsonDecode(jsonString); - Settings.updateFromMap(json); - Get.back(); - showSnackbar("Success", "Settings restored successfully"); - } catch (_) { - Get.back(); - showSnackbar("Error", "Something went wrong"); - } - }, - child: Text( - "Load Settings", - style: TextStyle( - color: Theme.of(context).textTheme.bodyText1!.color, - fontSize: 13, - ), - ), - ), - ], + try { + String jsonString = await File(res.first.toString()).readAsString(); + Map json = jsonDecode(jsonString); + Settings.updateFromMap(json); + Get.back(); + showSnackbar("Success", "Settings restored successfully"); + } catch (e, s) { + print(e); + print(s); + Get.back(); + showSnackbar("Error", "Something went wrong"); + } + }, + child: Text( + "Load Settings", + style: TextStyle( + color: Theme.of(context).textTheme.bodyText1!.color, + fontSize: 13, + ), + ), + ), + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + style: ElevatedButton.styleFrom( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + primary: Theme.of(context).primaryColor, ), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - ElevatedButton( - style: ElevatedButton.styleFrom( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - ), - primary: Theme.of(context).primaryColor, + onPressed: () async { + List allThemes = await ThemeObject.getThemes(); + String jsonStr = "["; + allThemes.forEachIndexed((index, e) async { + String entryJson = "["; + await e.fetchData(); + e.entries.forEachIndexed((index, e2) { + entryJson = entryJson + "${jsonEncode(e2.toMap())}"; + if (index != e.entries.length - 1) { + entryJson = entryJson + ","; + } else { + entryJson = entryJson + "]"; + } + }); + Map map = e.toMap(); + Logger.debug(entryJson); + map['entries'] = jsonDecode(entryJson); + jsonStr = jsonStr + "${jsonEncode(map)}"; + if (index != allThemes.length - 1) { + jsonStr = jsonStr + ","; + } else { + jsonStr = jsonStr + "]"; + } + }); + String directoryPath = "/storage/emulated/0/Download/BlueBubbles-theming-"; + DateTime now = DateTime.now().toLocal(); + String filePath = directoryPath + + "${now.year}${now.month}${now.day}_${now.hour}${now.minute}${now.second}" + + ".json"; + File file = File(filePath); + await file.create(recursive: true); + await file.writeAsString(jsonStr); + Get.back(); + showSnackbar( + "Success", + "Theming exported successfully to downloads folder", + durationMs: 2000, + button: TextButton( + style: TextButton.styleFrom( + backgroundColor: Get.theme.accentColor, ), - onPressed: () async { - List allThemes = await ThemeObject.getThemes(); - String jsonStr = "["; - allThemes.forEachIndexed((index, e) async { - String entryJson = "["; - await e.fetchData(); - e.entries.forEachIndexed((index, e2) { - entryJson = entryJson + "${jsonEncode(e2.toMap())}"; - if (index != e.entries.length - 1) { - entryJson = entryJson + ","; - } else { - entryJson = entryJson + "]"; - } - }); - Map map = e.toMap(); - print(entryJson); - map['entries'] = jsonDecode(entryJson); - jsonStr = jsonStr + "${jsonEncode(map)}"; - if (index != allThemes.length - 1) { - jsonStr = jsonStr + ","; - } else { - jsonStr = jsonStr + "]"; - } - }); - String directoryPath = "/storage/emulated/0/Download/BlueBubbles-theming-"; - DateTime now = DateTime.now().toLocal(); - String filePath = directoryPath + "${now.year}${now.month}${now.day}_${now.hour}${now.minute}${now.second}" + ".json"; - File file = File(filePath); - await file.create(recursive: true); - await file.writeAsString(jsonStr); - Get.back(); - showSnackbar( - "Success", - "Theming exported successfully to downloads folder", - durationMs: 2000, - button: TextButton( - style: TextButton.styleFrom( - backgroundColor: Get.theme.accentColor, - ), - onPressed: () { - Share.file("BlueBubbles Theming", filePath); - }, - child: Text("SHARE", style: TextStyle(color: Theme.of(context).primaryColor)), - ), - ); + onPressed: () { + Share.file("BlueBubbles Theming", filePath); }, - child: Text( - "Save Theming", - style: TextStyle( - color: Theme.of(context).textTheme.bodyText1!.color, - fontSize: 13, - ), - ), + child: Text("SHARE", style: TextStyle(color: Theme.of(context).primaryColor)), ), - SizedBox(width: 10), - ElevatedButton( - style: ElevatedButton.styleFrom( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(10), - side: BorderSide(color: Theme.of(context).primaryColor) - ), - primary: Theme.of(context).backgroundColor, - ), - onPressed: () async { - List? res = await MethodChannelInterface().invokeMethod("pick-file", { - "mimeTypes": ["application/json"], - "allowMultiple": false, - }); - if (res == null || res.isEmpty) return; + ); + }, + child: Text( + "Save Theming", + style: TextStyle( + color: Theme.of(context).textTheme.bodyText1!.color, + fontSize: 13, + ), + ), + ), + SizedBox(width: 10), + ElevatedButton( + style: ElevatedButton.styleFrom( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + side: BorderSide(color: Theme.of(context).primaryColor)), + primary: Theme.of(context).backgroundColor, + ), + onPressed: () async { + List? res = await MethodChannelInterface().invokeMethod("pick-file", { + "mimeTypes": ["application/json"], + "allowMultiple": false, + }); + if (res == null || res.isEmpty) return; - try { - String jsonString = await File(res.first.toString()).readAsString(); - List json = jsonDecode(jsonString); - for (var e in json) { - ThemeObject object = ThemeObject.fromMap(e); - List entriesJson = e['entries']; - List entries = []; - for (var e2 in entriesJson) { - entries.add(ThemeEntry.fromMap(e2)); - } - object.entries = entries; - object.data = object.themeData; - await object.save(); - } - await SettingsManager().saveSelectedTheme(context); - Get.back(); - showSnackbar("Success", "Theming restored successfully"); - } catch (_) { - Get.back(); - showSnackbar("Error", "Something went wrong"); - } - }, - child: Text( - "Load Theming", - style: TextStyle( - color: Theme.of(context).textTheme.bodyText1!.color, - fontSize: 13, - ), - ), - ), - ], + try { + String jsonString = await File(res.first.toString()).readAsString(); + List json = jsonDecode(jsonString); + for (var e in json) { + ThemeObject object = ThemeObject.fromMap(e); + List entriesJson = e['entries']; + List entries = []; + for (var e2 in entriesJson) { + entries.add(ThemeEntry.fromMap(e2)); + } + object.entries = entries; + object.data = object.themeData; + await object.save(); + } + await SettingsManager().saveSelectedTheme(context); + Get.back(); + showSnackbar("Success", "Theming restored successfully"); + } catch (_) { + Get.back(); + showSnackbar("Error", "Something went wrong"); + } + }, + child: Text( + "Load Theming", + style: TextStyle( + color: Theme.of(context).textTheme.bodyText1!.color, + fontSize: 13, + ), ), - ] + ), + ], ), - barrierDismissible: true, - backgroundColor: Theme.of(context).backgroundColor, - ); - }, - leading: SettingsLeadingIcon( - iosIcon: CupertinoIcons.cloud_upload, - materialIcon: Icons.backup, - ), - title: "Backup & Restore", - subtitle: "Backup and restore all app settings", + ]), + barrierDismissible: true, + backgroundColor: Theme.of(context).backgroundColor, + ); + }, + leading: SettingsLeadingIcon( + iosIcon: CupertinoIcons.cloud_upload, + materialIcon: Icons.backup, ), - Container( - color: tileColor, - child: Padding( - padding: const EdgeInsets.only(left: 65.0), - child: SettingsDivider(color: headerColor), - ), + title: "Backup & Restore", + subtitle: "Backup and restore all app settings", + ), + Container( + color: tileColor, + child: Padding( + padding: const EdgeInsets.only(left: 65.0), + child: SettingsDivider(color: headerColor), ), - SettingsTile( - backgroundColor: tileColor, - onTap: () { - showDialog( - barrierDismissible: false, - context: context, - builder: (BuildContext context) { - return AlertDialog( - title: Text( - "Are you sure?", - style: Theme.of(context).textTheme.bodyText1, + ), + SettingsTile( + backgroundColor: tileColor, + onTap: () { + showDialog( + barrierDismissible: false, + context: context, + builder: (BuildContext context) { + return AlertDialog( + title: Text( + "Are you sure?", + style: Theme.of(context).textTheme.bodyText1, + ), + backgroundColor: Theme.of(context).backgroundColor, + actions: [ + TextButton( + child: Text("Yes"), + onPressed: () async { + await DBProvider.deleteDB(); + await SettingsManager().resetConnection(); + SettingsManager().settings.finishedSetup.value = false; + SocketManager().finishedSetup.sink.add(false); + Navigator.of(context).popUntil((route) => route.isFirst); + }, ), - backgroundColor: Theme.of(context).backgroundColor, - actions: [ - TextButton( - child: Text("Yes"), - onPressed: () async { - await DBProvider.deleteDB(); - await SettingsManager().resetConnection(); - SettingsManager().settings.finishedSetup.value = false; - SocketManager().finishedSetup.sink.add(false); - Navigator.of(context).popUntil((route) => route.isFirst); - }, - ), - TextButton( - child: Text("Cancel"), - onPressed: () { - Navigator.of(context).pop(); - }, - ), - ], - ); - }, - ); - }, - leading: SettingsLeadingIcon( - iosIcon: CupertinoIcons.floppy_disk, - materialIcon: Icons.storage, - ), - title: "Reset", - subtitle: "Resets the app to default settings", + TextButton( + child: Text("Cancel"), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ], + ); + }, + ); + }, + leading: SettingsLeadingIcon( + iosIcon: CupertinoIcons.floppy_disk, + materialIcon: Icons.storage, ), - Container(color: tileColor, padding: EdgeInsets.only(top: 5.0)), - Container( - height: 30, - decoration: SettingsManager().settings.skin.value == Skins.iOS ? BoxDecoration( - color: headerColor, - border: Border( - top: BorderSide(color: Theme.of(context).dividerColor.lightenOrDarken(40), width: 0.3) - ), - ) : null, + title: "Reset", + subtitle: "Resets the app to default settings", + ), + Container( + color: tileColor, + child: Padding( + padding: const EdgeInsets.only(left: 65.0), + child: SettingsDivider(color: headerColor), ), - ], - ), + ), + SettingsTile( + backgroundColor: tileColor, + onTap: () async { + CustomNavigator.pushAndRemoveSettingsUntil( + context, + TroubleshootPanel(), + (route) => route.isFirst, + ); + }, + leading: SettingsLeadingIcon( + iosIcon: CupertinoIcons.question_circle, + materialIcon: Icons.help_outline, + ), + title: "Troubleshooting", + trailing: nextIcon, + ), + Container(color: tileColor, padding: EdgeInsets.only(top: 5.0)), + Container( + height: 30, + decoration: SettingsManager().settings.skin.value == Skins.iOS + ? BoxDecoration( + color: headerColor, + border: Border( + top: BorderSide( + color: Theme.of(context).dividerColor.lightenOrDarken(40), width: 0.3)), + ) + : null, + ), + ], ), - SliverList( - delegate: SliverChildListDelegate( - [], - ), - ) - ], - ), - )), + ), + SliverList( + delegate: SliverChildListDelegate( + [], + ), + ) + ], + ), + )); + } + + Widget buildForLandscape(BuildContext context, Widget settingsList) { + Color headerColor; + Color tileColor; + if (Theme.of(context).accentColor.computeLuminance() < Theme.of(context).backgroundColor.computeLuminance() || + SettingsManager().settings.skin.value != Skins.iOS) { + headerColor = Theme.of(context).accentColor; + tileColor = Theme.of(context).backgroundColor; + } else { + headerColor = Theme.of(context).backgroundColor; + tileColor = Theme.of(context).accentColor; + } + if (SettingsManager().settings.skin.value == Skins.iOS && isEqual(Theme.of(context), oledDarkTheme)) { + tileColor = headerColor; + } + return VerticalSplitView( + dividerWidth: 10.0, + initialRatio: 0.4, + minRatio: 0.33, + maxRatio: 0.5, + allowResize: true, + left: settingsList, + right: LayoutBuilder( + builder: (context, constraints) { + CustomNavigator.maxWidthSettings = constraints.maxWidth; + return WillPopScope( + onWillPop: () async { + Get.back(id: 3); + return false; + }, + child: Navigator( + key: Get.nestedKey(3), + onPopPage: (route, _) { + route.didPop(false); + return false; + }, + pages: [ + CupertinoPage(name: "initial", child: Scaffold( + backgroundColor: SettingsManager().settings.skin.value != Skins.iOS ? tileColor : headerColor, + body: Center( + child: Container( + child: Text("Select a settings page from the list", style: Theme.of(Get.context!).textTheme.subtitle1!.copyWith(fontSize: 18)) + ), + ) + )), + ], + ), + ); + } + ), ); } + Widget buildForDevice() { + bool showAltLayout = !context.isPhone || context.isLandscape; + Widget settingsList = buildSettingsList(); + if (showAltLayout) { + return buildForLandscape(context, settingsList); + } + + return settingsList; + } + void saveSettings() { SettingsManager().saveSettings(_settingsCopy); if (needToReconnect) { @@ -695,18 +875,17 @@ class _SettingsPanelState extends State { } class SettingsTile extends StatelessWidget { - const SettingsTile( - {Key? key, - this.onTap, - this.onLongPress, - this.title, - this.trailing, - this.leading, - this.subtitle, - this.backgroundColor, - this.isThreeLine = false, - }) - : super(key: key); + const SettingsTile({ + Key? key, + this.onTap, + this.onLongPress, + this.title, + this.trailing, + this.leading, + this.subtitle, + this.backgroundColor, + this.isThreeLine = false, + }) : super(key: key); final Function? onTap; final Function? onLongPress; @@ -853,17 +1032,21 @@ class SettingsSwitch extends StatelessWidget { title, style: Theme.of(context).textTheme.bodyText1, ), - subtitle: subtitle != null ? Text( - subtitle!, - style: Theme.of(context).textTheme.subtitle1, - ) : null, + subtitle: subtitle != null + ? Text( + subtitle!, + style: Theme.of(context).textTheme.subtitle1, + ) + : null, value: initialVal, activeColor: Theme.of(context).primaryColor, activeTrackColor: Theme.of(context).primaryColor.withAlpha(200), inactiveTrackColor: backgroundColor == Theme.of(context).accentColor - ? Theme.of(context).backgroundColor.withOpacity(0.6) : Theme.of(context).accentColor.withOpacity(0.6), + ? Theme.of(context).backgroundColor.withOpacity(0.6) + : Theme.of(context).accentColor.withOpacity(0.6), inactiveThumbColor: backgroundColor == Theme.of(context).accentColor - ? Theme.of(context).backgroundColor : Theme.of(context).accentColor, + ? Theme.of(context).backgroundColor + : Theme.of(context).accentColor, onChanged: onChanged, ), ); @@ -908,7 +1091,8 @@ class SettingsOptions extends StatelessWidget { children: map, groupValue: initial, thumbColor: secondaryColor != null && secondaryColor == backgroundColor - ? secondaryColor!.lightenOrDarken(20) : secondaryColor ?? Colors.white, + ? secondaryColor!.lightenOrDarken(20) + : secondaryColor ?? Colors.white, backgroundColor: backgroundColor ?? CupertinoColors.tertiarySystemFill, onValueChanged: onChanged, ), @@ -933,17 +1117,16 @@ class SettingsOptions extends StatelessWidget { ), (subtitle != null) ? Container( - constraints: BoxConstraints( - maxWidth: context.width * 2/3 - ), - child: Padding( - padding: EdgeInsets.only(top: 3.0), - child: Text( - subtitle ?? "", - style: Theme.of(context).textTheme.subtitle1, + constraints: BoxConstraints(maxWidth: CustomNavigator.width(context) * 2 / 3), + child: Padding( + padding: EdgeInsets.only(top: 3.0), + child: Text( + subtitle ?? "", + style: Theme.of(context).textTheme.subtitle1, + ), ), - ), - ) : Container(), + ) + : Container(), ]), Container( padding: EdgeInsets.symmetric(horizontal: 9), @@ -982,18 +1165,18 @@ class SettingsOptions extends StatelessWidget { } class SettingsSlider extends StatelessWidget { - SettingsSlider({ - required this.startingVal, - this.update, - required this.text, - this.formatValue, - required this.min, - required this.max, - required this.divisions, - this.leading, - this.backgroundColor, - Key? key - }) : super(key: key); + SettingsSlider( + {required this.startingVal, + this.update, + required this.text, + this.formatValue, + required this.min, + required this.max, + required this.divisions, + this.leading, + this.backgroundColor, + Key? key}) + : super(key: key); final double startingVal; final Function(double val)? update; @@ -1018,23 +1201,25 @@ class SettingsSlider extends StatelessWidget { tileColor: backgroundColor, leading: leading, trailing: Text(value), - title: SettingsManager().settings.skin.value == Skins.iOS ? CupertinoSlider( - activeColor: Theme.of(context).primaryColor, - value: startingVal, - onChanged: update, - divisions: divisions, - min: min, - max: max, - ) : Slider( - activeColor: Theme.of(context).primaryColor, - inactiveColor: Theme.of(context).primaryColor.withOpacity(0.2), - value: startingVal, - onChanged: update, - label: value, - divisions: divisions, - min: min, - max: max, - ), + title: SettingsManager().settings.skin.value == Skins.iOS + ? CupertinoSlider( + activeColor: Theme.of(context).primaryColor, + value: startingVal, + onChanged: update, + divisions: divisions, + min: min, + max: max, + ) + : Slider( + activeColor: Theme.of(context).primaryColor, + inactiveColor: Theme.of(context).primaryColor.withOpacity(0.2), + value: startingVal, + onChanged: update, + label: value, + divisions: divisions, + min: min, + max: max, + ), ), ); } @@ -1047,41 +1232,38 @@ class SettingsHeader extends StatelessWidget { final TextStyle? materialSubtitle; final String text; - SettingsHeader({ - required this.headerColor, - required this.tileColor, - required this.iosSubtitle, - required this.materialSubtitle, - required this.text - }); + SettingsHeader( + {required this.headerColor, + required this.tileColor, + required this.iosSubtitle, + required this.materialSubtitle, + required this.text}); @override Widget build(BuildContext context) { - return Column( - children: [ - Container(color: tileColor, padding: EdgeInsets.only(top: 5.0)), - Container( - height: SettingsManager().settings.skin.value == Skins.iOS ? 60 : 40, - alignment: Alignment.bottomLeft, - decoration: SettingsManager().settings.skin.value == Skins.iOS ? BoxDecoration( - color: headerColor, - border: Border.symmetric( - horizontal: BorderSide(color: Theme.of(context).dividerColor.lightenOrDarken(40), width: 0.3) - ), - ) : BoxDecoration( - color: tileColor, - border: Border( - top: BorderSide(color: Theme.of(context).dividerColor.lightenOrDarken(40), width: 0.3) - ), - ), - child: Padding( - padding: const EdgeInsets.only(bottom: 8.0, left: 15), - child: Text(text.psCapitalize, style: SettingsManager().settings.skin.value == Skins.iOS ? iosSubtitle : materialSubtitle), - ) - ), - Container(color: tileColor, padding: EdgeInsets.only(top: 5.0)), - ] - ); + return Column(children: [ + Container(color: tileColor, padding: EdgeInsets.only(top: 5.0)), + Container( + height: SettingsManager().settings.skin.value == Skins.iOS ? 60 : 40, + alignment: Alignment.bottomLeft, + decoration: SettingsManager().settings.skin.value == Skins.iOS + ? BoxDecoration( + color: headerColor, + border: Border.symmetric( + horizontal: BorderSide(color: Theme.of(context).dividerColor.lightenOrDarken(40), width: 0.3)), + ) + : BoxDecoration( + color: tileColor, + border: + Border(top: BorderSide(color: Theme.of(context).dividerColor.lightenOrDarken(40), width: 0.3)), + ), + child: Padding( + padding: const EdgeInsets.only(bottom: 8.0, left: 15), + child: Text(text.psCapitalize, + style: SettingsManager().settings.skin.value == Skins.iOS ? iosSubtitle : materialSubtitle), + )), + Container(color: tileColor, padding: EdgeInsets.only(top: 5.0)), + ]); } } @@ -1098,29 +1280,25 @@ class SettingsLeadingIcon extends StatelessWidget { @override Widget build(BuildContext context) { - return Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Container( - width: 32, - height: 32, - decoration: BoxDecoration( - color: SettingsManager().settings.skin.value == Skins.iOS ? - containerColor ?? Colors.grey : Colors.transparent, - borderRadius: BorderRadius.circular(5), - ), - alignment: Alignment.center, - child: Icon(SettingsManager().settings.skin.value == Skins.iOS - ? iosIcon : materialIcon, - color: SettingsManager().settings.skin.value == Skins.iOS ? - Colors.white : Colors.grey, - size: SettingsManager().settings.skin.value == Skins.iOS ? 23 : 30 - ), - ), - ], - ); + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + width: 32, + height: 32, + decoration: BoxDecoration( + color: + SettingsManager().settings.skin.value == Skins.iOS ? containerColor ?? Colors.grey : Colors.transparent, + borderRadius: BorderRadius.circular(5), + ), + alignment: Alignment.center, + child: Icon(SettingsManager().settings.skin.value == Skins.iOS ? iosIcon : materialIcon, + color: SettingsManager().settings.skin.value == Skins.iOS ? Colors.white : Colors.grey, + size: SettingsManager().settings.skin.value == Skins.iOS ? 23 : 30), + ), + ], + ); } - } class SettingsDivider extends StatelessWidget { diff --git a/lib/layouts/settings/theme_panel.dart b/lib/layouts/settings/theme_panel.dart index f29d43b52..0435fdff1 100644 --- a/lib/layouts/settings/theme_panel.dart +++ b/lib/layouts/settings/theme_panel.dart @@ -4,14 +4,20 @@ import 'package:adaptive_theme/adaptive_theme.dart'; import 'package:bluebubbles/blocs/chat_bloc.dart'; import 'package:bluebubbles/helpers/constants.dart'; import 'package:bluebubbles/helpers/hex_color.dart'; +import 'package:bluebubbles/helpers/logger.dart'; +import 'package:bluebubbles/helpers/navigator.dart'; import 'package:bluebubbles/helpers/utils.dart'; import 'package:bluebubbles/helpers/themes.dart'; import 'package:bluebubbles/helpers/ui_helpers.dart'; +import 'package:bluebubbles/layouts/settings/custom_avatar_color_panel.dart'; +import 'package:bluebubbles/layouts/settings/custom_avatar_panel.dart'; import 'package:bluebubbles/layouts/settings/settings_panel.dart'; import 'package:bluebubbles/layouts/theming/theming_panel.dart'; import 'package:bluebubbles/layouts/widgets/theme_switcher/theme_switcher.dart'; +import 'package:bluebubbles/managers/method_channel_interface.dart'; import 'package:bluebubbles/managers/settings_manager.dart'; import 'package:bluebubbles/repository/models/settings.dart'; +import 'package:bluebubbles/repository/models/theme_object.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -53,22 +59,25 @@ class ThemePanelController extends GetxController { } class ThemePanel extends GetView { - @override Widget build(BuildContext context) { Widget nextIcon = Obx(() => Icon( - SettingsManager().settings.skin.value == Skins.iOS ? CupertinoIcons.chevron_right : Icons.arrow_forward, - color: Colors.grey, - )); + SettingsManager().settings.skin.value == Skins.iOS ? CupertinoIcons.chevron_right : Icons.arrow_forward, + color: Colors.grey, + )); /// for some reason we need a [GetBuilder] here otherwise the theme switching refuses to work right return GetBuilder(builder: (_) { - final iosSubtitle = Theme.of(context).textTheme.subtitle1?.copyWith(color: Colors.grey, fontWeight: FontWeight.w300); - final materialSubtitle = Theme.of(context).textTheme.subtitle1?.copyWith(color: Theme.of(context).primaryColor, fontWeight: FontWeight.bold); + final iosSubtitle = + Theme.of(context).textTheme.subtitle1?.copyWith(color: Colors.grey, fontWeight: FontWeight.w300); + final materialSubtitle = Theme.of(context) + .textTheme + .subtitle1 + ?.copyWith(color: Theme.of(context).primaryColor, fontWeight: FontWeight.bold); Color headerColor; Color tileColor; - if (Theme.of(context).accentColor.computeLuminance() < Theme.of(context).backgroundColor.computeLuminance() - || SettingsManager().settings.skin.value != Skins.iOS) { + if (Theme.of(context).accentColor.computeLuminance() < Theme.of(context).backgroundColor.computeLuminance() || + SettingsManager().settings.skin.value != Skins.iOS) { headerColor = Theme.of(context).accentColor; tileColor = Theme.of(context).backgroundColor; } else { @@ -82,14 +91,13 @@ class ThemePanel extends GetView { return AnnotatedRegion( value: SystemUiOverlayStyle( systemNavigationBarColor: headerColor, // navigation bar color - systemNavigationBarIconBrightness: - headerColor.computeLuminance() > 0.5 ? Brightness.dark : Brightness.light, + systemNavigationBarIconBrightness: headerColor.computeLuminance() > 0.5 ? Brightness.dark : Brightness.light, statusBarColor: Colors.transparent, // status bar color ), child: Scaffold( backgroundColor: SettingsManager().settings.skin.value != Skins.iOS ? tileColor : headerColor, appBar: PreferredSize( - preferredSize: Size(context.width, 80), + preferredSize: Size(CustomNavigator.width(context), 80), child: ClipRRect( child: BackdropFilter( child: AppBar( @@ -116,24 +124,28 @@ class ThemePanel extends GetView { Container( height: SettingsManager().settings.skin.value == Skins.iOS ? 30 : 40, alignment: Alignment.bottomLeft, - decoration: SettingsManager().settings.skin.value == Skins.iOS ? BoxDecoration( - color: headerColor, - border: Border( - bottom: BorderSide(color: Theme.of(context).dividerColor.lightenOrDarken(40), width: 0.3) - ), - ) : BoxDecoration( - color: tileColor, - ), + decoration: SettingsManager().settings.skin.value == Skins.iOS + ? BoxDecoration( + color: headerColor, + border: Border( + bottom: BorderSide( + color: Theme.of(context).dividerColor.lightenOrDarken(40), width: 0.3)), + ) + : BoxDecoration( + color: tileColor, + ), child: Padding( padding: const EdgeInsets.only(bottom: 8.0, left: 15), - child: Text("Theme".psCapitalize, style: SettingsManager().settings.skin.value == Skins.iOS ? iosSubtitle : materialSubtitle), - ) - ), + child: Text("Theme".psCapitalize, + style: + SettingsManager().settings.skin.value == Skins.iOS ? iosSubtitle : materialSubtitle), + )), SettingsOptions( initial: AdaptiveTheme.of(context).mode, onChanged: (val) { if (val == null) return; AdaptiveTheme.of(context).setThemeMode(val); + controller.update(); }, options: AdaptiveThemeMode.values, textProcessing: (val) => val.toString().split(".").last, @@ -166,48 +178,92 @@ class ThemePanel extends GetView { tileColor: tileColor, iosSubtitle: iosSubtitle, materialSubtitle: materialSubtitle, - text: "Skin" - ), + text: "Skin"), Obx(() => SettingsOptions( - initial: controller._settingsCopy.skin.value, - onChanged: (val) { - if (val == null) return; - controller._settingsCopy.skin.value = val; - if (val == Skins.Material) { - controller._settingsCopy.hideDividers.value = true; - } else if (val == Skins.Samsung) { - controller._settingsCopy.hideDividers.value = true; - } else { - controller._settingsCopy.hideDividers.value = false; - } - ChatBloc().refreshChats(); - saveSettings(); - controller.update(); - }, - options: Skins.values.where((item) => item != Skins.Samsung).toList(), - textProcessing: (val) => val.toString().split(".").last, - capitalize: false, - title: "App Skin", - backgroundColor: tileColor, - secondaryColor: headerColor, - )), + initial: controller._settingsCopy.skin.value, + onChanged: (val) { + if (val == null) return; + controller._settingsCopy.skin.value = val; + if (val == Skins.Material) { + controller._settingsCopy.hideDividers.value = true; + } else if (val == Skins.Samsung) { + controller._settingsCopy.hideDividers.value = true; + } else { + controller._settingsCopy.hideDividers.value = false; + } + ChatBloc().refreshChats(); + saveSettings(); + controller.update(); + }, + options: Skins.values.where((item) => item != Skins.Samsung).toList(), + textProcessing: (val) => val.toString().split(".").last, + capitalize: false, + title: "App Skin", + backgroundColor: tileColor, + secondaryColor: headerColor, + )), SettingsHeader( headerColor: headerColor, tileColor: tileColor, iosSubtitle: iosSubtitle, materialSubtitle: materialSubtitle, - text: "Colors" + text: "Colors"), + Obx(() => SettingsSwitch( + onChanged: (bool val) async { + await MethodChannelInterface().invokeMethod("request-notif-permission"); + try { + await MethodChannelInterface().invokeMethod("start-notif-listener"); + if (val) { + var allThemes = await ThemeObject.getThemes(); + var currentLight = await ThemeObject.getLightTheme(); + var currentDark = await ThemeObject.getDarkTheme(); + currentLight.previousLightTheme = true; + currentDark.previousDarkTheme = true; + await currentLight.save(); + await currentDark.save(); + SettingsManager().saveSelectedTheme(context, + selectedLightTheme: + allThemes.firstWhere((element) => element.name == "Music Theme (Light)"), + selectedDarkTheme: + allThemes.firstWhere((element) => element.name == "Music Theme (Dark)")); + } else { + ThemeObject previousLight = await revertToPreviousLightTheme(); + ThemeObject previousDark = await revertToPreviousDarkTheme(); + SettingsManager().saveSelectedTheme(context, + selectedLightTheme: previousLight, selectedDarkTheme: previousDark); + } + controller._settingsCopy.colorsFromMedia.value = val; + saveSettings(); + } catch (e, trace) { + Logger.error(e.toString()); + Logger.error(trace.toString()); + showSnackbar( + "Error", "Something went wrong, please ensure you granted the permission correctly!"); + } + }, + initialVal: controller._settingsCopy.colorsFromMedia.value, + title: "Colors from Media", + backgroundColor: tileColor, + subtitle: + "Pull app colors from currently playing media. Note: Requires full notification access & a custom theme to be set", + )), + Container( + color: tileColor, + child: Padding( + padding: const EdgeInsets.only(left: 65.0), + child: SettingsDivider(color: headerColor), + ), ), Obx(() => SettingsSwitch( - onChanged: (bool val) { - controller._settingsCopy.colorfulAvatars.value = val; - saveSettings(); - }, - initialVal: controller._settingsCopy.colorfulAvatars.value, - title: "Colorful Avatars", - backgroundColor: tileColor, - subtitle: "Gives letter avatars a splash of color", - )), + onChanged: (bool val) { + controller._settingsCopy.colorfulAvatars.value = val; + saveSettings(); + }, + initialVal: controller._settingsCopy.colorfulAvatars.value, + title: "Colorful Avatars", + backgroundColor: tileColor, + subtitle: "Gives letter avatars a splash of color", + )), Container( color: tileColor, child: Padding( @@ -216,15 +272,15 @@ class ThemePanel extends GetView { ), ), Obx(() => SettingsSwitch( - onChanged: (bool val) { - controller._settingsCopy.colorfulBubbles.value = val; - saveSettings(); - }, - initialVal: controller._settingsCopy.colorfulBubbles.value, - title: "Colorful Bubbles", - backgroundColor: tileColor, - subtitle: "Gives received message bubbles a splash of color", - )), + onChanged: (bool val) { + controller._settingsCopy.colorfulBubbles.value = val; + saveSettings(); + }, + initialVal: controller._settingsCopy.colorfulBubbles.value, + title: "Colorful Bubbles", + backgroundColor: tileColor, + subtitle: "Gives received message bubbles a splash of color", + )), Container( color: tileColor, child: Padding( @@ -236,16 +292,31 @@ class ThemePanel extends GetView { title: "Custom Avatar Colors", trailing: nextIcon, onTap: () async { - Get.toNamed("/settings/custom-avatar-color-panel"); + CustomNavigator.pushSettings( + context, + CustomAvatarColorPanel(), + binding: CustomAvatarColorPanelBinding(), + ); }, backgroundColor: tileColor, subtitle: "Customize the color for different avatars", ), + Container( + color: tileColor, + child: Padding( + padding: const EdgeInsets.only(left: 65.0), + child: SettingsDivider(color: headerColor), + ), + ), SettingsTile( title: "Custom Avatars", trailing: nextIcon, onTap: () async { - Get.toNamed("/settings/custom-avatar-panel"); + CustomNavigator.pushSettings( + context, + CustomAvatarPanel(), + binding: CustomAvatarPanelBinding(), + ); }, backgroundColor: tileColor, subtitle: "Customize the avatar for different chats", @@ -257,9 +328,9 @@ class ThemePanel extends GetView { tileColor: tileColor, iosSubtitle: iosSubtitle, materialSubtitle: materialSubtitle, - text: "Refresh Rate" - ); - else return SizedBox.shrink(); + text: "Refresh Rate"); + else + return SizedBox.shrink(); }), Obx(() { if (controller.refreshRates.length > 2) @@ -277,7 +348,8 @@ class ThemePanel extends GetView { backgroundColor: tileColor, secondaryColor: headerColor, ); - else return SizedBox.shrink(); + else + return SizedBox.shrink(); }), // SettingsOptions( // initial: controller._settingsCopy.emojiFontFamily == null @@ -294,12 +366,14 @@ class ThemePanel extends GetView { Container(color: tileColor, padding: EdgeInsets.only(top: 5.0)), Container( height: 30, - decoration: SettingsManager().settings.skin.value == Skins.iOS ? BoxDecoration( - color: headerColor, - border: Border( - top: BorderSide(color: Theme.of(context).dividerColor.lightenOrDarken(40), width: 0.3) - ), - ) : null, + decoration: SettingsManager().settings.skin.value == Skins.iOS + ? BoxDecoration( + color: headerColor, + border: Border( + top: BorderSide( + color: Theme.of(context).dividerColor.lightenOrDarken(40), width: 0.3)), + ) + : null, ), ], ), diff --git a/lib/layouts/settings/troubleshoot_panel.dart b/lib/layouts/settings/troubleshoot_panel.dart new file mode 100644 index 000000000..a1aaa7b66 --- /dev/null +++ b/lib/layouts/settings/troubleshoot_panel.dart @@ -0,0 +1,136 @@ +import 'dart:ui'; + +import 'package:bluebubbles/helpers/constants.dart'; +import 'package:bluebubbles/helpers/logger.dart'; +import 'package:bluebubbles/helpers/navigator.dart'; +import 'package:bluebubbles/helpers/themes.dart'; +import 'package:bluebubbles/helpers/ui_helpers.dart'; +import 'package:bluebubbles/managers/settings_manager.dart'; +import 'package:get/get.dart'; +import 'package:bluebubbles/helpers/hex_color.dart'; +import 'package:bluebubbles/layouts/settings/settings_panel.dart'; +import 'package:bluebubbles/layouts/widgets/theme_switcher/theme_switcher.dart'; +import 'package:bluebubbles/helpers/utils.dart'; +import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +class TroubleshootPanel extends StatelessWidget { + @override + Widget build(BuildContext context) { + final iosSubtitle = + Theme.of(context).textTheme.subtitle1?.copyWith(color: Colors.grey, fontWeight: FontWeight.w300); + final materialSubtitle = Theme.of(context) + .textTheme + .subtitle1 + ?.copyWith(color: Theme.of(context).primaryColor, fontWeight: FontWeight.bold); + Color headerColor; + Color tileColor; + if (Theme.of(context).accentColor.computeLuminance() < Theme.of(context).backgroundColor.computeLuminance() || + SettingsManager().settings.skin.value != Skins.iOS) { + headerColor = Theme.of(context).accentColor; + tileColor = Theme.of(context).backgroundColor; + } else { + headerColor = Theme.of(context).backgroundColor; + tileColor = Theme.of(context).accentColor; + } + if (SettingsManager().settings.skin.value == Skins.iOS && isEqual(Theme.of(context), oledDarkTheme)) { + tileColor = headerColor; + } + + return AnnotatedRegion( + value: SystemUiOverlayStyle( + systemNavigationBarColor: headerColor, // navigation bar color + systemNavigationBarIconBrightness: headerColor.computeLuminance() > 0.5 ? Brightness.dark : Brightness.light, + statusBarColor: Colors.transparent, // status bar color + ), + child: Scaffold( + backgroundColor: SettingsManager().settings.skin.value != Skins.iOS ? tileColor : headerColor, + appBar: PreferredSize( + preferredSize: Size(CustomNavigator.width(context), 80), + child: ClipRRect( + child: BackdropFilter( + child: AppBar( + brightness: ThemeData.estimateBrightnessForColor(headerColor), + toolbarHeight: 100.0, + elevation: 0, + leading: buildBackButton(context), + backgroundColor: headerColor.withOpacity(0.5), + title: Text( + "Troubleshooting", + style: Theme.of(context).textTheme.headline1, + ), + ), + filter: ImageFilter.blur(sigmaX: 15, sigmaY: 15), + ), + ), + ), + body: CustomScrollView( + physics: ThemeSwitcher.getScrollPhysics(), + slivers: [ + SliverList( + delegate: SliverChildListDelegate( + [ + Container( + height: SettingsManager().settings.skin.value == Skins.iOS ? 30 : 40, + alignment: Alignment.bottomLeft, + decoration: SettingsManager().settings.skin.value == Skins.iOS + ? BoxDecoration( + color: headerColor, + border: Border( + bottom: BorderSide( + color: Theme.of(context).dividerColor.lightenOrDarken(40), width: 0.3)), + ) + : BoxDecoration( + color: tileColor, + ), + child: Padding( + padding: const EdgeInsets.only(bottom: 8.0, left: 15), + child: Text("Logging".psCapitalize, + style: SettingsManager().settings.skin.value == Skins.iOS ? iosSubtitle : materialSubtitle), + )), + Container(color: tileColor, padding: EdgeInsets.only(top: 5.0)), + Obx(() => SettingsTile( + backgroundColor: tileColor, + onTap: () async { + if (Logger.saveLogs.value) { + await Logger.stopSavingLogs(); + Logger.saveLogs.value = false; + } else { + Logger.startSavingLogs(); + } + }, + leading: SettingsLeadingIcon( + iosIcon: CupertinoIcons.pencil_ellipsis_rectangle, + materialIcon: Icons.history_edu, + ), + title: "${Logger.saveLogs.value ? "End" : "Start"} Logging", + subtitle: Logger.saveLogs.value + ? "Logging started, tap here to end and save" + : "Create a bug report for developers to analyze", + )), + Container(color: tileColor, padding: EdgeInsets.only(top: 5.0)), + Container( + height: 30, + decoration: SettingsManager().settings.skin.value == Skins.iOS + ? BoxDecoration( + color: headerColor, + border: Border( + top: BorderSide(color: Theme.of(context).dividerColor.lightenOrDarken(40), width: 0.3)), + ) + : null, + ), + ], + ), + ), + SliverList( + delegate: SliverChildListDelegate( + [], + ), + ) + ], + ), + ), + ); + } +} diff --git a/lib/layouts/setup/connecting_alert/connecting_alert.dart b/lib/layouts/setup/connecting_alert/connecting_alert.dart index 9706e8e11..86ee52772 100644 --- a/lib/layouts/setup/connecting_alert/connecting_alert.dart +++ b/lib/layouts/setup/connecting_alert/connecting_alert.dart @@ -1,3 +1,4 @@ +import 'package:bluebubbles/helpers/logger.dart'; import 'package:bluebubbles/layouts/setup/connecting_alert/failed_to_connect_dialog.dart'; import 'package:bluebubbles/socket_manager.dart'; import 'package:flutter/material.dart'; @@ -20,7 +21,7 @@ class _ConnectingAlertState extends State { ever(SocketManager().state, (event) { if (!this.mounted) return; - debugPrint("Connection Status Changed"); + Logger.info("Connection Status Changed"); if (event == SocketState.CONNECTED) { widget.onConnect(true); } else if (event == SocketState.ERROR || event == SocketState.DISCONNECTED) { diff --git a/lib/layouts/setup/setup_view.dart b/lib/layouts/setup/setup_view.dart index be42bc177..a57f1648c 100644 --- a/lib/layouts/setup/setup_view.dart +++ b/lib/layouts/setup/setup_view.dart @@ -1,3 +1,4 @@ +import 'package:bluebubbles/helpers/logger.dart'; import 'package:bluebubbles/layouts/setup/battery_optimization/battery_optimization.dart'; import 'package:bluebubbles/layouts/setup/connecting_alert/failed_to_connect_dialog.dart'; import 'package:bluebubbles/layouts/setup/prepare_to_download/prepare_to_download.dart'; @@ -5,7 +6,6 @@ import 'package:bluebubbles/layouts/setup/qr_scan/qr_scan.dart'; import 'package:bluebubbles/layouts/setup/request_contact/request_contacts.dart'; import 'package:bluebubbles/layouts/setup/setup_mac_app/setup_mac_app.dart'; import 'package:bluebubbles/layouts/setup/syncing_messages/syncing_messages.dart'; -import 'package:bluebubbles/layouts/setup/theme_selector/theme_selector.dart'; import 'package:bluebubbles/layouts/setup/welcome_page/welcome_page.dart'; import 'package:bluebubbles/managers/settings_manager.dart'; import 'package:bluebubbles/socket_manager.dart'; @@ -47,7 +47,7 @@ class _SetupViewState extends State { ); break; default: - debugPrint("Default case: " + event.toString()); + Logger.info("Default case: " + event.toString()); break; } } diff --git a/lib/layouts/setup/syncing_messages/syncing_messages.dart b/lib/layouts/setup/syncing_messages/syncing_messages.dart index c4beb4eef..74b453abc 100644 --- a/lib/layouts/setup/syncing_messages/syncing_messages.dart +++ b/lib/layouts/setup/syncing_messages/syncing_messages.dart @@ -1,5 +1,4 @@ -import 'dart:async'; - +import 'package:bluebubbles/helpers/navigator.dart'; import 'package:get/get.dart'; import 'package:bluebubbles/blocs/setup_bloc.dart'; import 'package:bluebubbles/layouts/setup/qr_scan/failed_to_scan_dialog.dart'; @@ -84,7 +83,7 @@ class _SyncingMessagesState extends State { flex: 5, ), Padding( - padding: EdgeInsets.symmetric(horizontal: context.width / 4), + padding: EdgeInsets.symmetric(horizontal: CustomNavigator.width(context) / 4), child: ClipRRect( borderRadius: BorderRadius.circular(20), child: LinearProgressIndicator( @@ -98,7 +97,7 @@ class _SyncingMessagesState extends State { flex: 20, ), SizedBox( - width: context.width * 4 / 5, + width: CustomNavigator.width(context) * 4 / 5, height: context.height * 1 / 3, child: Container( decoration: BoxDecoration( @@ -132,7 +131,7 @@ class _SyncingMessagesState extends State { } else { return Center( child: Padding( - padding: EdgeInsets.symmetric(horizontal: context.width / 4), + padding: EdgeInsets.symmetric(horizontal: CustomNavigator.width(context) / 4), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ diff --git a/lib/layouts/setup/theme_selector/theme_selector.dart b/lib/layouts/setup/theme_selector/theme_selector.dart index 326f488c4..9e9c25fbe 100644 --- a/lib/layouts/setup/theme_selector/theme_selector.dart +++ b/lib/layouts/setup/theme_selector/theme_selector.dart @@ -6,6 +6,7 @@ import 'package:assorted_layout_widgets/assorted_layout_widgets.dart'; import 'package:bluebubbles/blocs/chat_bloc.dart'; import 'package:bluebubbles/blocs/message_bloc.dart'; import 'package:bluebubbles/helpers/constants.dart'; +import 'package:bluebubbles/helpers/navigator.dart'; import 'package:bluebubbles/helpers/themes.dart'; import 'package:bluebubbles/helpers/ui_helpers.dart'; import 'package:bluebubbles/layouts/conversation_view/messages_view.dart'; @@ -877,12 +878,12 @@ Widget buildConversationViewHeader(BuildContext context, Chat chat, ThemeData th Center( child: Container( constraints: BoxConstraints( - maxWidth: context.width / 2, + maxWidth: CustomNavigator.width(context) / 2, ), child: Row(mainAxisSize: MainAxisSize.min, children: [ Container( constraints: BoxConstraints( - maxWidth: context.width / 2 - 55, + maxWidth: CustomNavigator.width(context) / 2 - 55, ), child: RichText( maxLines: 1, diff --git a/lib/layouts/theming/theming_color_options_list.dart b/lib/layouts/theming/theming_color_options_list.dart index 8309f415c..93a733f98 100644 --- a/lib/layouts/theming/theming_color_options_list.dart +++ b/lib/layouts/theming/theming_color_options_list.dart @@ -1,14 +1,16 @@ import 'dart:async'; import 'package:bluebubbles/helpers/constants.dart'; +import 'package:bluebubbles/helpers/navigator.dart'; import 'package:bluebubbles/helpers/themes.dart'; import 'package:bluebubbles/helpers/utils.dart'; +import 'package:bluebubbles/layouts/settings/settings_panel.dart'; import 'package:bluebubbles/layouts/theming/theming_color_selector.dart'; import 'package:bluebubbles/layouts/widgets/theme_switcher/theme_switcher.dart'; import 'package:bluebubbles/managers/event_dispatcher.dart'; +import 'package:bluebubbles/managers/method_channel_interface.dart'; import 'package:bluebubbles/managers/settings_manager.dart'; import 'package:bluebubbles/repository/models/theme_object.dart'; -import 'package:get/get.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; @@ -75,7 +77,7 @@ class _ThemingColorOptionsListState extends State { await theme.fetchData(); } - setState(() {}); + if (this.mounted) setState(() {}); } @override @@ -83,8 +85,8 @@ class _ThemingColorOptionsListState extends State { editable = currentTheme != null && !currentTheme!.isPreset; Color headerColor; Color tileColor; - if (Theme.of(context).accentColor.computeLuminance() < Theme.of(context).backgroundColor.computeLuminance() - || SettingsManager().settings.skin.value != Skins.iOS) { + if (Theme.of(context).accentColor.computeLuminance() < Theme.of(context).backgroundColor.computeLuminance() || + SettingsManager().settings.skin.value != Skins.iOS) { headerColor = Theme.of(context).accentColor; tileColor = Theme.of(context).backgroundColor; } else { @@ -116,11 +118,11 @@ class _ThemingColorOptionsListState extends State { Padding( padding: const EdgeInsets.all(8.0), child: Container( - width: context.width - 16, + width: CustomNavigator.width(context) - 16, padding: const EdgeInsets.symmetric(horizontal: 8.0), decoration: BoxDecoration( - borderRadius: BorderRadius.circular(5.0), - color: headerColor, + borderRadius: BorderRadius.circular(5.0), + color: headerColor, ), child: DropdownButtonHideUnderline( child: DropdownButton( @@ -140,7 +142,47 @@ class _ThemingColorOptionsListState extends State { value!.data = value.themeData; await value.save(); - if (widget.isDarkMode) { + if (value.name == "Music Theme (Light)" || value.name == "Music Theme (Dark)") { + await MethodChannelInterface().invokeMethod("request-notif-permission"); + try { + await MethodChannelInterface().invokeMethod("start-notif-listener"); + SettingsManager().settings.colorsFromMedia.value = true; + SettingsManager().saveSettings(SettingsManager().settings); + } catch (e) { + showSnackbar("Error", + "Something went wrong, please ensure you granted the permission correctly!"); + return; + } + } else { + SettingsManager().settings.colorsFromMedia.value = false; + SettingsManager().saveSettings(SettingsManager().settings); + } + + if (value.name == "Music Theme (Light)" || value.name == "Music Theme (Dark)") { + var allThemes = await ThemeObject.getThemes(); + var currentLight = await ThemeObject.getLightTheme(); + var currentDark = await ThemeObject.getDarkTheme(); + currentLight.previousLightTheme = true; + currentDark.previousDarkTheme = true; + await currentLight.save(); + await currentDark.save(); + SettingsManager().saveSelectedTheme(context, + selectedLightTheme: + allThemes.firstWhere((element) => element.name == "Music Theme (Light)"), + selectedDarkTheme: + allThemes.firstWhere((element) => element.name == "Music Theme (Dark)")); + } else if (currentTheme!.name == "Music Theme (Light)" || + currentTheme!.name == "Music Theme (Dark)") { + if (!widget.isDarkMode) { + ThemeObject previousDark = await revertToPreviousDarkTheme(); + SettingsManager().saveSelectedTheme(context, + selectedLightTheme: value, selectedDarkTheme: previousDark); + } else { + ThemeObject previousLight = await revertToPreviousLightTheme(); + SettingsManager().saveSelectedTheme(context, + selectedLightTheme: previousLight, selectedDarkTheme: value); + } + } else if (widget.isDarkMode) { SettingsManager().saveSelectedTheme(context, selectedDarkTheme: value); } else { SettingsManager().saveSelectedTheme(context, selectedLightTheme: value); @@ -163,6 +205,24 @@ class _ThemingColorOptionsListState extends State { ], ), ), + if (!currentTheme!.isPreset) + SliverToBoxAdapter( + child: SettingsSwitch( + onChanged: (bool val) async { + currentTheme!.gradientBg = val; + await currentTheme!.save(); + if (widget.isDarkMode) { + SettingsManager().saveSelectedTheme(context, selectedDarkTheme: currentTheme); + } else { + SettingsManager().saveSelectedTheme(context, selectedLightTheme: currentTheme); + } + }, + initialVal: currentTheme!.gradientBg, + title: "Gradient Message View Background", + backgroundColor: tileColor, + subtitle: + "Make the background of the messages view an animated gradient based on the background color and the primary color", + )), SliverGrid( delegate: SliverChildBuilderDelegate( (context, index) { diff --git a/lib/layouts/theming/theming_color_selector.dart b/lib/layouts/theming/theming_color_selector.dart index bae1dc502..e44d0d4f7 100644 --- a/lib/layouts/theming/theming_color_selector.dart +++ b/lib/layouts/theming/theming_color_selector.dart @@ -1,4 +1,5 @@ import 'package:bluebubbles/helpers/hex_color.dart'; +import 'package:bluebubbles/helpers/navigator.dart'; import 'package:bluebubbles/helpers/themes.dart'; import 'package:bluebubbles/helpers/utils.dart'; import 'package:bluebubbles/managers/settings_manager.dart'; @@ -6,7 +7,6 @@ import 'package:bluebubbles/repository/models/theme_entry.dart'; import 'package:bluebubbles/repository/models/theme_object.dart'; import 'package:flex_color_picker/flex_color_picker.dart'; import 'package:flutter/material.dart'; -import 'package:get/get_utils/src/extensions/context_extensions.dart'; class ThemingColorSelector extends StatefulWidget { ThemingColorSelector({Key? key, required this.currentTheme, required this.entry, required this.editable}) @@ -91,7 +91,7 @@ class _ThemingColorSelectorState extends State { dialogActionButtons: true, ), constraints: BoxConstraints( - minHeight: 480, minWidth: context.width - 70, maxWidth: context.width - 70), + minHeight: 480, minWidth: CustomNavigator.width(context) - 70, maxWidth: CustomNavigator.width(context) - 70), ); widget.entry.color = color; await widget.entry.save(widget.currentTheme); diff --git a/lib/layouts/theming/theming_panel.dart b/lib/layouts/theming/theming_panel.dart index a30d47600..aec05973c 100644 --- a/lib/layouts/theming/theming_panel.dart +++ b/lib/layouts/theming/theming_panel.dart @@ -3,14 +3,15 @@ import 'dart:ui'; import 'package:adaptive_theme/adaptive_theme.dart'; import 'package:bluebubbles/helpers/constants.dart'; +import 'package:bluebubbles/helpers/navigator.dart'; import 'package:bluebubbles/helpers/themes.dart'; import 'package:bluebubbles/helpers/ui_helpers.dart'; import 'package:bluebubbles/layouts/theming/theming_color_options_list.dart'; import 'package:bluebubbles/layouts/widgets/theme_switcher/theme_switcher.dart'; import 'package:bluebubbles/managers/settings_manager.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:get/get.dart'; class ThemingPanel extends StatefulWidget { ThemingPanel({Key? key}) : super(key: key); @@ -65,7 +66,7 @@ class _ThemingPanelState extends State with TickerProviderStateMix child: Scaffold( backgroundColor: tileColor, appBar: PreferredSize( - preferredSize: Size(context.width, 80), + preferredSize: Size(CustomNavigator.width(context), 80), child: ClipRRect( child: BackdropFilter( child: AppBar( @@ -104,10 +105,24 @@ class _ThemingPanelState extends State with TickerProviderStateMix onPressed: () { streamController.sink.add(null); }, - child: Icon( - Icons.edit, - color: Colors.white, - ), + child: Stack( + alignment: Alignment.center, + children: [ + Icon( + Icons.copy, + color: Colors.white, + ), + PositionedDirectional( + start: 7.5, + top: 8, + child: Icon( + SettingsManager().settings.skin.value == Skins.iOS ? CupertinoIcons.pencil : Icons.edit, + color: Colors.white, + size: 12, + ), + ), + ] + ) ), ), bottomSheet: Container( @@ -126,14 +141,14 @@ class _ThemingPanelState extends State with TickerProviderStateMix Container( child: Tab( icon: Icon( - Icons.brightness_high, + SettingsManager().settings.skin.value == Skins.iOS ? CupertinoIcons.sun_max : Icons.brightness_high, color: Theme.of(context).textTheme.bodyText1!.color, ), ), ), Tab( icon: Icon( - Icons.brightness_3, + SettingsManager().settings.skin.value == Skins.iOS ? CupertinoIcons.moon : Icons.brightness_3, color: Theme.of(context).textTheme.bodyText1!.color, ), ), diff --git a/lib/layouts/widgets/CustomCupertinoTextField.dart b/lib/layouts/widgets/CustomCupertinoTextField.dart index 09719fe86..c5bb9c46d 100644 --- a/lib/layouts/widgets/CustomCupertinoTextField.dart +++ b/lib/layouts/widgets/CustomCupertinoTextField.dart @@ -232,6 +232,7 @@ class CustomCupertinoTextField extends StatefulWidget { /// characters" and how it may differ from the intuitive meaning. const CustomCupertinoTextField({ Key? key, + this.enableIMEPersonalizedLearning = true, this.controller, this.focusNode, this.decoration = _kDefaultRoundedBorderDecoration, @@ -376,6 +377,7 @@ class CustomCupertinoTextField extends StatefulWidget { /// characters" and how it may differ from the intuitive meaning. const CustomCupertinoTextField.borderless({ Key? key, + this.enableIMEPersonalizedLearning = true, this.controller, this.focusNode, this.decoration, @@ -759,6 +761,8 @@ class CustomCupertinoTextField extends StatefulWidget { /// {@macro flutter.material.textfield.restorationId} final String? restorationId; + final bool enableIMEPersonalizedLearning; + @override _CustomCupertinoTextFieldState createState() => _CustomCupertinoTextFieldState(); @@ -805,6 +809,7 @@ class CustomCupertinoTextField extends StatefulWidget { properties.add(DiagnosticsProperty('scrollPhysics', scrollPhysics, defaultValue: null)); properties.add(EnumProperty('textAlign', textAlign, defaultValue: TextAlign.start)); properties.add(DiagnosticsProperty('textAlignVertical', textAlignVertical, defaultValue: null)); + properties.add(DiagnosticsProperty('enableIMEPersonalizedLearning', enableIMEPersonalizedLearning, defaultValue: true)); } } @@ -1111,7 +1116,7 @@ class _CustomCupertinoTextFieldState extends State final Brightness keyboardAppearance = widget.keyboardAppearance ?? CupertinoTheme.brightnessOf(context); final Color cursorColor = CupertinoDynamicColor.maybeResolve(widget.cursorColor, context) ?? themeData.primaryColor; - final Color disabledColor = CupertinoDynamicColor.resolve(_kDisabledBackground, context); + // final Color disabledColor = CupertinoDynamicColor.resolve(_kDisabledBackground, context); final Color? decorationColor = CupertinoDynamicColor.maybeResolve(widget.decoration?.color, context); @@ -1136,7 +1141,7 @@ class _CustomCupertinoTextFieldState extends State final BoxDecoration? effectiveDecoration = widget.decoration?.copyWith( border: resolvedBorder, - color: enabled ? decorationColor : disabledColor, + color: decorationColor, ); final Color selectionColor = CupertinoTheme.of(context).primaryColor.withOpacity(0.2); @@ -1147,6 +1152,7 @@ class _CustomCupertinoTextFieldState extends State child: UnmanagedRestorationScope( bucket: bucket, child: EditableText( + // enableIMEPersonalizedLearning: widget.enableIMEPersonalizedLearning, key: editableTextKey, controller: controller, readOnly: widget.readOnly, @@ -1218,7 +1224,7 @@ class _CustomCupertinoTextFieldState extends State ignoring: !enabled, child: Container( decoration: effectiveDecoration, - color: !enabled && effectiveDecoration == null ? disabledColor : null, + color: null, child: _selectionGestureDetectorBuilder.buildGestureDetector( behavior: HitTestBehavior.translucent, child: Align( diff --git a/lib/layouts/widgets/CustomDismissible.dart b/lib/layouts/widgets/CustomDismissible.dart index 481ee46b2..233f0d4c3 100644 --- a/lib/layouts/widgets/CustomDismissible.dart +++ b/lib/layouts/widgets/CustomDismissible.dart @@ -128,9 +128,7 @@ class CustomDismissible extends StatefulWidget { this.crossAxisEndOffset = 0.0, this.dragStartBehavior = DragStartBehavior.start, this.behavior = HitTestBehavior.opaque, - }) : assert(key != null), - assert(secondaryBackground == null || background != null), - assert(dragStartBehavior != null), + }) : assert(secondaryBackground == null || background != null), super(key: key); /// The widget below this widget in the tree. @@ -233,16 +231,13 @@ class _DismissibleClipper extends CustomClipper { _DismissibleClipper({ required this.axis, required this.moveAnimation, - }) : assert(axis != null), - assert(moveAnimation != null), - super(reclip: moveAnimation); + }) : super(reclip: moveAnimation); final Axis axis; final Animation moveAnimation; @override Rect getClip(Size size) { - assert(axis != null); switch (axis) { case Axis.horizontal: final double offset = moveAnimation.value.dx * size.width; @@ -418,7 +413,6 @@ class _CustomDismissibleState extends State with TickerProvid } _FlingGestureKind _describeFlingGesture(Velocity velocity) { - assert(widget.direction != null); if (_dragExtent == 0.0) { // If it was a fling, then it was a fling that was let loose at the exact // middle of the range (i.e. when there's no displacement). In that case, @@ -442,7 +436,6 @@ class _CustomDismissibleState extends State with TickerProvid assert(vy != 0.0); flingDirection = _extentToDirection(vy); } - assert(_dismissDirection != null); if (flingDirection == _dismissDirection) return _FlingGestureKind.forward; return _FlingGestureKind.reverse; diff --git a/lib/layouts/widgets/avatar_crop.dart b/lib/layouts/widgets/avatar_crop.dart index 3680b52f1..98eb13e91 100644 --- a/lib/layouts/widgets/avatar_crop.dart +++ b/lib/layouts/widgets/avatar_crop.dart @@ -3,6 +3,7 @@ import 'dart:typed_data'; import 'dart:ui'; import 'package:bluebubbles/blocs/chat_bloc.dart'; +import 'package:bluebubbles/helpers/navigator.dart'; import 'package:bluebubbles/helpers/ui_helpers.dart'; import 'package:bluebubbles/helpers/utils.dart'; import 'package:bluebubbles/managers/method_channel_interface.dart'; @@ -39,7 +40,7 @@ class _AvatarCropState extends State { await file.writeAsBytes(croppedData); ChatBloc().chats[widget.index!].customAvatarPath.value = file.path; ChatBloc().chats[widget.index!].save(); - Get.back(closeOverlays: true); + CustomNavigator.backSettingsCloseOverlays(context); showSnackbar("Notice", "Custom chat avatar saved successfully"); } else { File file = new File(widget.chat!.customAvatarPath.value ?? "$appDocPath/avatars/${widget.chat!.guid!.characters.where((char) => char.isAlphabetOnly || char.isNumericOnly).join()}/avatar.jpg"); @@ -66,7 +67,7 @@ class _AvatarCropState extends State { child: Scaffold( backgroundColor: Theme.of(context).backgroundColor, appBar: PreferredSize( - preferredSize: Size(context.width, 80), + preferredSize: Size(CustomNavigator.width(context), 80), child: ClipRRect( child: BackdropFilter( child: AppBar( @@ -185,11 +186,10 @@ class _AvatarCropState extends State { barrierDismissible: false, backgroundColor: Theme.of(context).backgroundColor, ); - onCropped(file.readAsBytesSync()); + onCropped(await file.readAsBytes()); } else { - setState(() { - _imageData = file.readAsBytesSync(); - }); + _imageData = await file.readAsBytes(); + setState(() {}); } }, child: Text( diff --git a/lib/layouts/widgets/contact_avatar_group_widget.dart b/lib/layouts/widgets/contact_avatar_group_widget.dart index 841e9a663..bd1e5b33d 100644 --- a/lib/layouts/widgets/contact_avatar_group_widget.dart +++ b/lib/layouts/widgets/contact_avatar_group_widget.dart @@ -2,12 +2,14 @@ import 'dart:io'; import 'dart:math'; import 'dart:ui'; +import 'package:bluebubbles/helpers/constants.dart'; import 'package:bluebubbles/layouts/widgets/contact_avatar_widget.dart'; import 'package:bluebubbles/layouts/widgets/theme_switcher/theme_switcher.dart'; import 'package:bluebubbles/managers/contact_manager.dart'; import 'package:bluebubbles/managers/settings_manager.dart'; import 'package:bluebubbles/repository/models/chat.dart'; import 'package:bluebubbles/repository/models/handle.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; @@ -119,7 +121,7 @@ class _ContactAvatarGroupWidgetState extends State { color: context.theme.accentColor.withOpacity(0.8), ), child: Icon( - Icons.people, + SettingsManager().settings.skin.value == Skins.iOS ? CupertinoIcons.group_solid : Icons.people, size: size * 0.65, color: context.textTheme.subtitle1!.color!.withOpacity(0.8), ), diff --git a/lib/layouts/widgets/contact_avatar_widget.dart b/lib/layouts/widgets/contact_avatar_widget.dart index e62894383..26b07662a 100644 --- a/lib/layouts/widgets/contact_avatar_widget.dart +++ b/lib/layouts/widgets/contact_avatar_widget.dart @@ -1,12 +1,15 @@ import 'dart:async'; +import 'package:bluebubbles/helpers/constants.dart'; import 'package:bluebubbles/helpers/hex_color.dart'; +import 'package:bluebubbles/helpers/navigator.dart'; import 'package:bluebubbles/helpers/utils.dart'; import 'package:bluebubbles/managers/contact_manager.dart'; import 'package:bluebubbles/managers/settings_manager.dart'; import 'package:bluebubbles/repository/models/handle.dart'; import 'package:contacts_service/contacts_service.dart'; import 'package:flex_color_picker/flex_color_picker.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; @@ -160,7 +163,7 @@ class _ContactAvatarWidgetState extends State with Automati context, widget.handle?.color != null ? HexColor(widget.handle!.color!) : toColorGradient(widget.handle!.address)[0], title: Container( - width: context.width - 112, + width: CustomNavigator.width(context) - 112, child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ @@ -198,7 +201,7 @@ class _ContactAvatarWidgetState extends State with Automati dialogActionButtons: true, ), constraints: BoxConstraints( - minHeight: 480, minWidth: context.width - 70, maxWidth: context.width - 70), + minHeight: 480, minWidth: CustomNavigator.width(context) - 70, maxWidth: CustomNavigator.width(context) - 70), ); if (didReset) return; @@ -270,7 +273,7 @@ class _ContactAvatarWidgetState extends State with Automati SettingsManager().settings.removeLetterAvatars.value) || state!.initials == null ? Icon( - Icons.person, + SettingsManager().settings.skin.value == Skins.iOS ? CupertinoIcons.person_fill : Icons.person, key: Key("$keyPrefix-avatar-icon"), size: (widget.size ?? 40) / 2, ) diff --git a/lib/layouts/widgets/message_widget/message_content/attachment_downloader_widget.dart b/lib/layouts/widgets/message_widget/message_content/attachment_downloader_widget.dart index bea7aea2d..df96e5566 100644 --- a/lib/layouts/widgets/message_widget/message_content/attachment_downloader_widget.dart +++ b/lib/layouts/widgets/message_widget/message_content/attachment_downloader_widget.dart @@ -1,3 +1,5 @@ +import 'package:bluebubbles/helpers/constants.dart'; +import 'package:bluebubbles/managers/settings_manager.dart'; import 'package:bluebubbles/repository/models/attachment.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; @@ -35,7 +37,7 @@ class _AttachmentDownloaderWidgetState extends State widget.attachment.getFriendlySize(), style: Theme.of(context).textTheme.bodyText1, ), - Icon(Icons.cloud_download, size: 28.0), + Icon(SettingsManager().settings.skin.value == Skins.iOS ? CupertinoIcons.cloud_download : Icons.cloud_download, size: 28.0), (widget.attachment.mimeType != null) ? Text( widget.attachment.mimeType!, diff --git a/lib/layouts/widgets/message_widget/message_content/media_players/audio_player_widget.dart b/lib/layouts/widgets/message_widget/message_content/media_players/audio_player_widget.dart index aa65e7678..f78cd7770 100644 --- a/lib/layouts/widgets/message_widget/message_content/media_players/audio_player_widget.dart +++ b/lib/layouts/widgets/message_widget/message_content/media_players/audio_player_widget.dart @@ -1,9 +1,9 @@ import 'dart:io'; import 'package:bluebubbles/helpers/constants.dart'; +import 'package:bluebubbles/helpers/navigator.dart'; import 'package:bluebubbles/managers/settings_manager.dart'; import 'package:chewie_audio/chewie_audio.dart'; -import 'package:get/get.dart'; import 'package:bluebubbles/managers/current_chat.dart'; import 'package:flutter/material.dart'; import 'package:tuple/tuple.dart'; @@ -91,7 +91,7 @@ class _AudioPlayerWigetState extends State with AutomaticKeepA @override Widget build(BuildContext context) { - double maxWidth = widget.width ?? context.width - 20; + double maxWidth = widget.width ?? CustomNavigator.width(context) - 20; if (!(ModalRoute.of(context)?.isCurrent ?? false)) { controller.pause(); } diff --git a/lib/layouts/widgets/message_widget/message_content/media_players/balloon_bundle_widget.dart b/lib/layouts/widgets/message_widget/message_content/media_players/balloon_bundle_widget.dart index 4403df9e0..e88b21323 100644 --- a/lib/layouts/widgets/message_widget/message_content/media_players/balloon_bundle_widget.dart +++ b/lib/layouts/widgets/message_widget/message_content/media_players/balloon_bundle_widget.dart @@ -1,11 +1,14 @@ -import 'package:get/get.dart'; +import 'package:bluebubbles/helpers/constants.dart'; +import 'package:bluebubbles/helpers/navigator.dart'; +import 'package:bluebubbles/managers/settings_manager.dart'; +import 'package:flutter/cupertino.dart'; import 'package:bluebubbles/helpers/message_helper.dart'; import 'package:bluebubbles/repository/models/message.dart'; import 'package:flutter/material.dart'; Map iconMap = { - 'com.apple.Handwriting.HandwritingProvider': Icons.brush, - 'com.apple.DigitalTouchBalloonProvider': Icons.touch_app + 'com.apple.Handwriting.HandwritingProvider': SettingsManager().settings.skin.value == Skins.iOS ? CupertinoIcons.square_pencil : Icons.brush, + 'com.apple.DigitalTouchBalloonProvider': SettingsManager().settings.skin.value == Skins.iOS ? CupertinoIcons.app_badge : Icons.touch_app }; class BalloonBundleWidget extends StatefulWidget { @@ -48,16 +51,16 @@ class _BalloonBubbleState extends State { String val = widget.message!.balloonBundleId!.toLowerCase(); if (val.contains("gamepigeon")) { - return Icons.games; + return SettingsManager().settings.skin.value == Skins.iOS ? CupertinoIcons.gamecontroller : Icons.games; } else if (val.contains("contextoptional")) { - return Icons.phone_android; + return SettingsManager().settings.skin.value == Skins.iOS ? CupertinoIcons.device_phone_portrait : Icons.phone_android; } else if (val.contains("mobileslideshow")) { - return Icons.slideshow; + return SettingsManager().settings.skin.value == Skins.iOS ? CupertinoIcons.play_rectangle : Icons.slideshow; } else if (val.contains("PeerPayment")) { - return Icons.monetization_on; + return SettingsManager().settings.skin.value == Skins.iOS ? CupertinoIcons.money_dollar_circle : Icons.monetization_on; } - return Icons.apps; + return SettingsManager().settings.skin.value == Skins.iOS ? CupertinoIcons.square_grid_3x2 : Icons.apps; } @override @@ -66,7 +69,7 @@ class _BalloonBubbleState extends State { borderRadius: BorderRadius.circular(20), child: Container( constraints: BoxConstraints( - maxWidth: context.width * 3 / 4, + maxWidth: CustomNavigator.width(context) * 3 / 4, ), child: Container( width: 200, diff --git a/lib/layouts/widgets/message_widget/message_content/media_players/contact_widget.dart b/lib/layouts/widgets/message_widget/message_content/media_players/contact_widget.dart index 130dbf7d1..ef15e7d39 100644 --- a/lib/layouts/widgets/message_widget/message_content/media_players/contact_widget.dart +++ b/lib/layouts/widgets/message_widget/message_content/media_players/contact_widget.dart @@ -8,6 +8,7 @@ import 'package:bluebubbles/managers/method_channel_interface.dart'; import 'package:bluebubbles/managers/settings_manager.dart'; import 'package:bluebubbles/repository/models/attachment.dart'; import 'package:contacts_service/contacts_service.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:path/path.dart'; @@ -109,7 +110,7 @@ class _ContactWidgetState extends State { padding: EdgeInsets.only(left: 5.0), child: Icon( SettingsManager().settings.skin.value == Skins.iOS - ? Icons.arrow_forward_ios + ? CupertinoIcons.forward : Icons.arrow_forward, color: Colors.grey, size: 15, diff --git a/lib/layouts/widgets/message_widget/message_content/media_players/image_widget.dart b/lib/layouts/widgets/message_widget/message_content/media_players/image_widget.dart index 244a1beb2..774fb140b 100644 --- a/lib/layouts/widgets/message_widget/message_content/media_players/image_widget.dart +++ b/lib/layouts/widgets/message_widget/message_content/media_players/image_widget.dart @@ -121,26 +121,30 @@ class ImageWidget extends StatelessWidget { duration: Duration(milliseconds: 150), child: Obx( () => controller.data.value != null - ? Image.memory( - controller.data.value!, - // prevents the image widget from "refreshing" when the provider changes - gaplessPlayback: true, - frameBuilder: (context, child, frame, wasSynchronouslyLoaded) { - return Stack(children: [ - buildPlaceHolder(context, controller, isLoaded: wasSynchronouslyLoaded), - AnimatedOpacity( - opacity: (frame == null && - controller.attachment.guid != "redacted-mode-demo-attachment" && - controller.attachment.guid!.contains("theme-selector")) - ? 0 - : 1, - child: child, - duration: const Duration(milliseconds: 250), - curve: Curves.easeInOut, - ) - ]); - }, - ) + ? Container( + width: controller.attachment.guid == "redacted-mode-demo-attachment" ? controller.attachment.width!.toDouble() : null, + height: controller.attachment.guid == "redacted-mode-demo-attachment" ? controller.attachment.height!.toDouble() : null, + child: Image.memory( + controller.data.value!, + // prevents the image widget from "refreshing" when the provider changes + gaplessPlayback: true, + frameBuilder: (context, child, frame, wasSynchronouslyLoaded) { + return Stack(children: [ + buildPlaceHolder(context, controller, isLoaded: wasSynchronouslyLoaded), + AnimatedOpacity( + opacity: (frame == null && + controller.attachment.guid != "redacted-mode-demo-attachment" && + controller.attachment.guid!.contains("theme-selector")) + ? 0 + : 1, + child: child, + duration: const Duration(milliseconds: 250), + curve: Curves.easeInOut, + ) + ]); + }, + ), + ) : buildPlaceHolder(context, controller), )); diff --git a/lib/layouts/widgets/message_widget/message_content/media_players/location_widget.dart b/lib/layouts/widgets/message_widget/message_content/media_players/location_widget.dart index a2ffeed21..db810b71a 100644 --- a/lib/layouts/widgets/message_widget/message_content/media_players/location_widget.dart +++ b/lib/layouts/widgets/message_widget/message_content/media_players/location_widget.dart @@ -1,7 +1,10 @@ import 'dart:io'; import 'package:bluebubbles/helpers/attachment_helper.dart'; +import 'package:bluebubbles/helpers/constants.dart'; +import 'package:bluebubbles/managers/settings_manager.dart'; import 'package:bluebubbles/repository/models/attachment.dart'; +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:latlong2/latlong.dart'; @@ -88,7 +91,7 @@ class _LocationWidgetState extends State with AutomaticKeepAlive height: 40.0, point: new LatLng(location!.longitude!, location!.latitude!), builder: (ctx) => new Container( - child: Icon(Icons.pin_drop, color: Colors.red, size: 45), + child: Icon(SettingsManager().settings.skin.value == Skins.iOS ? CupertinoIcons.location : Icons.pin_drop, color: Colors.red, size: 45), ), ), ], diff --git a/lib/layouts/widgets/message_widget/message_content/media_players/regular_file_opener.dart b/lib/layouts/widgets/message_widget/message_content/media_players/regular_file_opener.dart index 3b1c11a97..a38dfee70 100644 --- a/lib/layouts/widgets/message_widget/message_content/media_players/regular_file_opener.dart +++ b/lib/layouts/widgets/message_widget/message_content/media_players/regular_file_opener.dart @@ -41,28 +41,34 @@ class _RegularFileOpenerState extends State { } }, child: Container( - height: 140, - width: 200, + constraints: BoxConstraints( + maxHeight: 140, + maxWidth: 200, + ), color: Theme.of(context).accentColor, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - basename(widget.file.path), - textAlign: TextAlign.center, - maxLines: 3, - overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.bodyText2, - ), - Padding( - padding: const EdgeInsets.all(8.0), - child: Icon( - fileIcon, - color: Theme.of(context).textTheme.bodyText2!.color, + child: Padding( + padding: const EdgeInsets.all(15.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + Text( + basename(widget.file.path), + textAlign: TextAlign.center, + maxLines: 2, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.bodyText2, + ), + Padding( + padding: const EdgeInsets.all(8.0), + child: Icon( + fileIcon, + color: Theme.of(context).textTheme.bodyText2!.color, + ), ), - ), - Text(widget.attachment.mimeType!, style: Theme.of(context).textTheme.bodyText2), - ], + Text(widget.attachment.mimeType!, style: Theme.of(context).textTheme.bodyText2), + ], + ), ), ), ); diff --git a/lib/layouts/widgets/message_widget/message_content/media_players/url_preview_widget.dart b/lib/layouts/widgets/message_widget/message_content/media_players/url_preview_widget.dart index 4eb50c8ec..82e9ca9e8 100644 --- a/lib/layouts/widgets/message_widget/message_content/media_players/url_preview_widget.dart +++ b/lib/layouts/widgets/message_widget/message_content/media_players/url_preview_widget.dart @@ -1,6 +1,8 @@ import 'dart:async'; import 'dart:io'; +import 'package:bluebubbles/helpers/logger.dart'; +import 'package:bluebubbles/helpers/navigator.dart'; import 'package:get/get.dart'; import 'package:bluebubbles/helpers/attachment_downloader.dart'; import 'package:bluebubbles/helpers/attachment_helper.dart'; @@ -65,7 +67,7 @@ class UrlPreviewController extends GetxController with SingleGetTickerProviderMi // Fetch the metadata meta = await MetadataHelper.fetchMetadata(message); } catch (ex) { - debugPrint("Failed to fetch metadata! Error: ${ex.toString()}"); + Logger.error("Failed to fetch metadata! Error: ${ex.toString()}"); gotError.value = true; return; } @@ -185,7 +187,7 @@ class UrlPreviewWidget extends StatelessWidget { padding: EdgeInsets.only( top: (controller.data.value?.title == "Image Preview" ? 0 : 5.0), bottom: 10.0), child: Text( - Uri.tryParse(message.getUrl()!)?.host ?? "", + message.fullText.isURL ? message.fullText : (Uri.tryParse(message.getUrl()!)?.host ?? ""), style: Theme.of(context).textTheme.subtitle2, overflow: TextOverflow.ellipsis, maxLines: 1, @@ -255,7 +257,7 @@ class UrlPreviewWidget extends StatelessWidget { }, child: Container( // The minus 5 here is so the timestamps show OK during swipe - width: (context.width * 2 / 3) - 5, + width: (CustomNavigator.width(context) * 2 / 3) - 5, child: (hideContent || hideType) ? Stack(children: items) : Column(children: items), ), ), diff --git a/lib/layouts/widgets/message_widget/message_content/media_players/video_widget.dart b/lib/layouts/widgets/message_widget/message_content/media_players/video_widget.dart index 7fc03940a..3e8122301 100644 --- a/lib/layouts/widgets/message_widget/message_content/media_players/video_widget.dart +++ b/lib/layouts/widgets/message_widget/message_content/media_players/video_widget.dart @@ -1,6 +1,8 @@ import 'dart:io'; +import 'package:bluebubbles/helpers/constants.dart'; import 'package:bluebubbles/helpers/hex_color.dart'; +import 'package:bluebubbles/helpers/navigator.dart'; import 'package:bluebubbles/helpers/utils.dart'; import 'package:bluebubbles/layouts/image_viewer/attachmet_fullscreen_viewer.dart'; import 'package:bluebubbles/layouts/widgets/theme_switcher/theme_switcher.dart'; @@ -151,7 +153,7 @@ class VideoWidget extends StatelessWidget { }, child: Container( constraints: BoxConstraints( - maxWidth: context.width / 2, + maxWidth: CustomNavigator.width(context) / 2, maxHeight: context.height / 2, ), child: Hero( @@ -179,7 +181,7 @@ class VideoWidget extends StatelessWidget { child: controller.controller.value.isPlaying ? GestureDetector( child: Icon( - Icons.pause, + SettingsManager().settings.skin.value == Skins.iOS ? CupertinoIcons.pause : Icons.pause, color: Colors.white, size: 45, ), @@ -190,7 +192,7 @@ class VideoWidget extends StatelessWidget { ) : GestureDetector( child: Icon( - Icons.play_arrow, + SettingsManager().settings.skin.value == Skins.iOS ? CupertinoIcons.play : Icons.play_arrow, color: Colors.white, size: 45, ), @@ -223,7 +225,13 @@ class VideoWidget extends StatelessWidget { ), padding: EdgeInsets.all(5), child: Obx(() => Icon( - controller.muted.value ? Icons.volume_mute : Icons.volume_up, + controller.muted.value + ? SettingsManager().settings.skin.value == Skins.iOS + ? CupertinoIcons.volume_mute + : Icons.volume_mute + : SettingsManager().settings.skin.value == Skins.iOS + ? CupertinoIcons.volume_up + : Icons.volume_up, color: Colors.white, size: 15, )), diff --git a/lib/layouts/widgets/message_widget/message_content/message_attachment.dart b/lib/layouts/widgets/message_widget/message_content/message_attachment.dart index 88a65e227..b7746d6ea 100644 --- a/lib/layouts/widgets/message_widget/message_content/message_attachment.dart +++ b/lib/layouts/widgets/message_widget/message_content/message_attachment.dart @@ -1,5 +1,6 @@ import 'dart:io'; +import 'package:bluebubbles/helpers/navigator.dart'; import 'package:bluebubbles/helpers/ui_helpers.dart'; import 'package:get/get.dart'; import 'package:bluebubbles/helpers/attachment_downloader.dart'; @@ -69,7 +70,7 @@ class MessageAttachmentState extends State with AutomaticKeep borderRadius: BorderRadius.circular(20), child: Container( constraints: BoxConstraints( - maxWidth: context.width * 0.5, + maxWidth: CustomNavigator.width(context) * 0.5, maxHeight: context.height * 0.6, ), child: _buildAttachmentWidget(), diff --git a/lib/layouts/widgets/message_widget/message_content/message_tail.dart b/lib/layouts/widgets/message_widget/message_content/message_tail.dart index 888c65dd5..d838e277c 100644 --- a/lib/layouts/widgets/message_widget/message_content/message_tail.dart +++ b/lib/layouts/widgets/message_widget/message_content/message_tail.dart @@ -13,38 +13,56 @@ class MessageTail extends StatelessWidget { bool hideTail = ((routeCtx?.settings.arguments ?? {"hideTail": false}) as Map)["hideTail"] ?? false; if (hideTail) return Container(); - return Stack( - alignment: isFromMe ? AlignmentDirectional.bottomEnd : AlignmentDirectional.bottomStart, - children: [ - Container( - margin: EdgeInsets.only( - left: isFromMe ? 0.0 : 4.0, - right: isFromMe ? 4.0 : 0.0, - bottom: 1, - ), - width: 20, - height: 15, - decoration: BoxDecoration( - color: color, - borderRadius: BorderRadius.only( - bottomRight: isFromMe ? Radius.zero : Radius.circular(12), - bottomLeft: isFromMe ? Radius.circular(12) : Radius.zero, - ), - ), + return ClipPath( + clipper: TailClipper(isFromMe), + child: Container( + margin: EdgeInsets.only( + left: isFromMe ? 0.0 : 4.0, + right: isFromMe ? 4.0 : 0.0, + bottom: 1, ), - Container( - margin: EdgeInsets.only(bottom: 2), - height: 28, - width: 10, - decoration: BoxDecoration( - color: Theme.of(context).backgroundColor, - borderRadius: BorderRadius.only( - bottomRight: isFromMe ? Radius.zero : Radius.circular(8), - bottomLeft: isFromMe ? Radius.circular(8) : Radius.zero, - ), + width: 20, + height: 15, + decoration: BoxDecoration( + color: color, + borderRadius: BorderRadius.only( + bottomRight: isFromMe ? Radius.zero : Radius.circular(12), + bottomLeft: isFromMe ? Radius.circular(12) : Radius.zero, ), ), - ], + ), ); } } + +class TailClipper extends CustomClipper{ + bool isFromMe; + TailClipper(this.isFromMe); + + @override + Path getClip(Size size){ + Path path = Path(); + if (!isFromMe) { + path.moveTo(2, size.height); + path.lineTo(size.width, size.height); + path.lineTo(size.width, 0); + path.lineTo(size.width / 2 - 2, 0); + path.lineTo(size.width / 2 - 2, size.height / 3.5); + path.quadraticBezierTo(size.width / 2 - 2, size.height - 1.5, 2, size.height - 1.5); + } else { + path.moveTo(size.width - 2, size.height); + path.lineTo(0, size.height); + path.lineTo(0, 0); + path.lineTo(size.width / 2 + 2, 0); + path.lineTo(size.width / 2 + 2, size.height / 3.5); + path.quadraticBezierTo(size.width / 2 + 2, size.height - 1.5, size.width + 2, size.height - 1.5); + } + path.close(); + return path; + } + + @override + bool shouldReclip(CustomClipper clipper) { + return false; + } +} \ No newline at end of file diff --git a/lib/layouts/widgets/message_widget/message_details_popup.dart b/lib/layouts/widgets/message_widget/message_details_popup.dart index 001380f76..4be9c2010 100644 --- a/lib/layouts/widgets/message_widget/message_details_popup.dart +++ b/lib/layouts/widgets/message_widget/message_details_popup.dart @@ -2,8 +2,11 @@ import 'dart:async'; import 'dart:io'; import 'dart:math'; import 'dart:ui'; +import 'package:bluebubbles/helpers/logger.dart'; import 'package:bluebubbles/helpers/metadata_helper.dart'; +import 'package:bluebubbles/helpers/navigator.dart'; import 'package:bluebubbles/managers/method_channel_interface.dart'; +import 'package:bluebubbles/managers/notification_manager.dart'; import 'package:bluebubbles/repository/models/handle.dart'; import 'package:collection/collection.dart'; @@ -155,7 +158,7 @@ class MessageDetailsPopupState extends State with TickerPro } void sendReaction(String type) { - debugPrint("Sending reaction type: " + type); + Logger.info("Sending reaction type: " + type); ActionHandler.sendReaction(widget.currentChat!.chat, widget.message, type); Navigator.of(context).pop(); } @@ -217,7 +220,7 @@ class MessageDetailsPopupState extends State with TickerPro child: Container( alignment: Alignment.center, height: 120, - width: context.width - 20, + width: CustomNavigator.width(context) - 20, color: Theme.of(context).accentColor, child: Padding( padding: EdgeInsets.symmetric(horizontal: 0), @@ -252,16 +255,17 @@ class MessageDetailsPopupState extends State with TickerPro } Widget buildReactionMenu() { - Size size = Get.mediaQuery.size; - - double reactionIconSize = ((8.5 / 10 * min(size.width, size.height)) / (ReactionTypes.toList().length).toDouble()); + double reactionIconSize = ((8.5 / 10 * min(CustomNavigator.width(context), context.height)) / (ReactionTypes.toList().length).toDouble()); double maxMenuWidth = (ReactionTypes.toList().length * reactionIconSize).toDouble(); double menuHeight = (reactionIconSize).toDouble(); double topPadding = -20; - double topOffset = (messageTopOffset - menuHeight).toDouble().clamp(topMinimum, size.height - 120 - menuHeight); + if (topMinimum > context.height - 120 - menuHeight) { + topMinimum = context.height - 120 - menuHeight; + } + double topOffset = (messageTopOffset - menuHeight).toDouble().clamp(topMinimum, context.height - 120 - menuHeight); bool shiftRight = currentChat!.chat.isGroup() || SettingsManager().settings.alwaysShowAvatars.value; double leftOffset = - (widget.message.isFromMe! ? size.width - maxMenuWidth - 25 : 25 + (shiftRight ? 20 : 0)).toDouble(); + (widget.message.isFromMe! ? CustomNavigator.width(context) - maxMenuWidth - 25 : 25 + (shiftRight ? 20 : 0)).toDouble(); Color iconColor = Colors.white; if (Theme.of(context).accentColor.computeLuminance() >= 0.179) { @@ -350,11 +354,9 @@ class MessageDetailsPopupState extends State with TickerPro } Widget buildCopyPasteMenu() { - Size size = Get.mediaQuery.size; - - double maxMenuWidth = size.width * 2 / 3; + double maxMenuWidth = CustomNavigator.width(context) * 2 / 3; - double maxHeight = size.height - topMinimum - widget.childSize!.height; + double maxHeight = context.height - topMinimum - widget.childSize!.height; List allActions = [ if (widget.currentChat!.chat.isGroup() && !widget.message.isFromMe! && dmChat != null) @@ -379,7 +381,7 @@ class MessageDetailsPopupState extends State with TickerPro style: Theme.of(context).textTheme.bodyText1, ), trailing: Icon( - Icons.open_in_new, + SettingsManager().settings.skin.value == Skins.iOS ? cupertino.CupertinoIcons.arrow_up_right_square : Icons.open_in_new, color: Theme.of(context).textTheme.bodyText1!.color, ), ), @@ -402,7 +404,7 @@ class MessageDetailsPopupState extends State with TickerPro style: Theme.of(context).textTheme.bodyText1, ), trailing: Icon( - Icons.open_in_browser, + SettingsManager().settings.skin.value == Skins.iOS ? cupertino.CupertinoIcons.macwindow : Icons.open_in_browser, color: Theme.of(context).textTheme.bodyText1!.color, ), ), @@ -444,7 +446,7 @@ class MessageDetailsPopupState extends State with TickerPro style: Theme.of(context).textTheme.bodyText1, ), trailing: Icon( - Icons.message, + SettingsManager().settings.skin.value == Skins.iOS ? cupertino.CupertinoIcons.chat_bubble : Icons.message, color: Theme.of(context).textTheme.bodyText1!.color, ), ), @@ -480,7 +482,7 @@ class MessageDetailsPopupState extends State with TickerPro style: Theme.of(context).textTheme.bodyText1, ), trailing: Icon( - Icons.forward, + SettingsManager().settings.skin.value == Skins.iOS ? cupertino.CupertinoIcons.arrow_right : Icons.forward, color: Theme.of(context).textTheme.bodyText1!.color, ), ), @@ -500,7 +502,7 @@ class MessageDetailsPopupState extends State with TickerPro style: Theme.of(context).textTheme.bodyText1, ), trailing: Icon( - Icons.delete, + SettingsManager().settings.skin.value == Skins.iOS ? cupertino.CupertinoIcons.trash : Icons.delete, color: Theme.of(context).textTheme.bodyText1!.color, ), ), @@ -518,7 +520,7 @@ class MessageDetailsPopupState extends State with TickerPro child: ListTile( title: Text("Copy", style: Theme.of(context).textTheme.bodyText1), trailing: Icon( - Icons.content_copy, + SettingsManager().settings.skin.value == Skins.iOS ? cupertino.CupertinoIcons.doc_on_clipboard : Icons.content_copy, color: Theme.of(context).textTheme.bodyText1!.color, ), ), @@ -581,7 +583,7 @@ class MessageDetailsPopupState extends State with TickerPro style: Theme.of(context).textTheme.bodyText1, ), trailing: Icon( - Icons.content_copy, + SettingsManager().settings.skin.value == Skins.iOS ? cupertino.CupertinoIcons.doc_on_clipboard : Icons.content_copy, color: Theme.of(context).textTheme.bodyText1!.color, ), ), @@ -605,7 +607,7 @@ class MessageDetailsPopupState extends State with TickerPro style: Theme.of(context).textTheme.bodyText1, ), trailing: Icon( - Icons.refresh, + SettingsManager().settings.skin.value == Skins.iOS ? cupertino.CupertinoIcons.refresh : Icons.refresh, color: Theme.of(context).textTheme.bodyText1!.color, ), ), @@ -624,7 +626,7 @@ class MessageDetailsPopupState extends State with TickerPro } } } catch (ex, trace) { - debugPrint(trace.toString()); + Logger.error(trace.toString()); showSnackbar("Download Error", ex.toString()); } }, @@ -634,7 +636,7 @@ class MessageDetailsPopupState extends State with TickerPro style: Theme.of(context).textTheme.bodyText1, ), trailing: Icon( - Icons.file_download, + SettingsManager().settings.skin.value == Skins.iOS ? cupertino.CupertinoIcons.cloud_download : Icons.file_download, color: Theme.of(context).textTheme.bodyText1!.color, ), ), @@ -665,12 +667,48 @@ class MessageDetailsPopupState extends State with TickerPro style: Theme.of(context).textTheme.bodyText1, ), trailing: Icon( - Icons.share, + SettingsManager().settings.skin.value == Skins.iOS ? cupertino.CupertinoIcons.share : Icons.share, color: Theme.of(context).textTheme.bodyText1!.color, ), ), ), ), + Material( + color: Colors.transparent, + child: InkWell( + onTap: () async { + final messageDate = await showDatePicker( + context: context, + initialDate: DateTime.now().toLocal(), + firstDate: DateTime.now().toLocal(), + lastDate: DateTime.now().toLocal().add(Duration(days: 365))); + if (messageDate != null) { + final messageTime = await showTimePicker(context: context, initialTime: TimeOfDay.now()); + if (messageTime != null) { + final finalDate = DateTime( + messageDate.year, messageDate.month, messageDate.day, messageTime.hour, messageTime.minute); + if (!finalDate.isAfter(DateTime.now().toLocal())) { + showSnackbar("Error", "Select a date in the future"); + return; + } + NotificationManager().scheduleNotification(widget.currentChat!.chat, widget.message, finalDate); + Get.back(); + showSnackbar("Notice", "Scheduled reminder for ${buildDate(finalDate)}"); + } + } + }, + child: ListTile( + title: Text( + "Remind Later", + style: Theme.of(context).textTheme.bodyText1, + ), + trailing: Icon( + SettingsManager().settings.skin.value == Skins.iOS ? cupertino.CupertinoIcons.alarm : Icons.alarm, + color: Theme.of(context).textTheme.bodyText1!.color, + ), + ), + ), + ), ]; List detailsActions = []; @@ -731,7 +769,7 @@ class MessageDetailsPopupState extends State with TickerPro child: ListTile( title: Text("More...", style: Theme.of(context).textTheme.bodyText1), trailing: Icon( - Icons.more_vert, + SettingsManager().settings.skin.value == Skins.iOS ? cupertino.CupertinoIcons.ellipsis : Icons.more_vert, color: Theme.of(context).textTheme.bodyText1!.color, ), ), @@ -743,15 +781,16 @@ class MessageDetailsPopupState extends State with TickerPro ), ); - double upperLimit = size.height - detailsMenuHeight!; + double upperLimit = context.height - detailsMenuHeight!; if (topMinimum > upperLimit) { topMinimum = upperLimit; } + print(topMinimum.toString() + ' | ' + upperLimit.toString()); double topOffset = (messageTopOffset + widget.childSize!.height).toDouble().clamp(topMinimum, upperLimit); bool shiftRight = currentChat!.chat.isGroup() || SettingsManager().settings.alwaysShowAvatars.value; double leftOffset = - (widget.message.isFromMe! ? size.width - maxMenuWidth - 15 : 15 + (shiftRight ? 35 : 0)).toDouble(); + (widget.message.isFromMe! ? CustomNavigator.width(context) - maxMenuWidth - 15 : 15 + (shiftRight ? 35 : 0)).toDouble(); return Positioned( top: topOffset + 5, left: leftOffset, diff --git a/lib/layouts/widgets/message_widget/message_popup_holder.dart b/lib/layouts/widgets/message_widget/message_popup_holder.dart index d6b965962..085d3572d 100644 --- a/lib/layouts/widgets/message_widget/message_popup_holder.dart +++ b/lib/layouts/widgets/message_widget/message_popup_holder.dart @@ -1,12 +1,15 @@ import 'package:bluebubbles/action_handler.dart'; +import 'package:bluebubbles/helpers/logger.dart'; import 'package:bluebubbles/helpers/message_helper.dart'; import 'package:bluebubbles/helpers/utils.dart'; import 'package:bluebubbles/layouts/widgets/message_widget/message_details_popup.dart'; import 'package:bluebubbles/managers/current_chat.dart'; +import 'package:bluebubbles/managers/event_dispatcher.dart'; import 'package:bluebubbles/managers/settings_manager.dart'; import 'package:bluebubbles/repository/models/message.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:get/get.dart'; class MessagePopupHolder extends StatefulWidget { final Widget child; @@ -38,20 +41,33 @@ class _MessagePopupHolderState extends State { RenderBox renderBox = containerKey.currentContext!.findRenderObject() as RenderBox; Size size = renderBox.size; Offset offset = renderBox.localToGlobal(Offset.zero); - bool increaseWidth = !MessageHelper.getShowTail(context, widget.message, widget.newerMessage) - && (SettingsManager().settings.alwaysShowAvatars.value || (CurrentChat.of(context)?.chat.isGroup() ?? false)); - bool doNotIncreaseHeight = ((widget.message.isFromMe ?? false) - || !(CurrentChat.of(context)?.chat.isGroup() ?? false) - || !sameSender(widget.message, widget.olderMessage) - || !widget.message.dateCreated!.isWithin(widget.olderMessage!.dateCreated!, minutes: 30)); - print(doNotIncreaseHeight); - this.childOffset = Offset(offset.dx - (increaseWidth ? 35 : 0), - offset.dy - (doNotIncreaseHeight ? 0 : widget.message.getReactions().length > 0 ? 20.0 : 23.0)); - childSize = Size(size.width + (increaseWidth ? 35 : 0), - size.height + (doNotIncreaseHeight ? 0 : widget.message.getReactions().length > 0 ? 20.0 : 23.0)); + bool increaseWidth = !MessageHelper.getShowTail(context, widget.message, widget.newerMessage) && + (SettingsManager().settings.alwaysShowAvatars.value || (CurrentChat.of(context)?.chat.isGroup() ?? false)); + bool doNotIncreaseHeight = ((widget.message.isFromMe ?? false) || + !(CurrentChat.of(context)?.chat.isGroup() ?? false) || + !sameSender(widget.message, widget.olderMessage) || + !widget.message.dateCreated!.isWithin(widget.olderMessage!.dateCreated!, minutes: 30)); + + this.childOffset = Offset( + offset.dx - (increaseWidth ? 35 : 0), + offset.dy - + (doNotIncreaseHeight + ? 0 + : widget.message.getReactions().length > 0 + ? 20.0 + : 23.0)); + childSize = Size( + size.width + (increaseWidth ? 35 : 0), + size.height + + (doNotIncreaseHeight + ? 0 + : widget.message.getReactions().length > 0 + ? 20.0 + : 23.0)); } void openMessageDetails() async { + EventDispatcher().emit("unfocus-keyboard", null); HapticFeedback.lightImpact(); getOffset(); @@ -69,14 +85,21 @@ class _MessagePopupHolderState extends State { transitionDuration: Duration(milliseconds: 150), pageBuilder: (context, animation, secondaryAnimation) { return FadeTransition( - opacity: animation, - child: MessageDetailsPopup( - currentChat: currentChat, - child: widget.popupChild, - childOffset: childOffset, - childSize: childSize, - message: widget.message, - )); + opacity: animation, + child: LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + childOffset = + Offset(childOffset.dx - context.mediaQuerySize.width + constraints.maxWidth, childOffset.dy); + return MessageDetailsPopup( + currentChat: currentChat, + child: widget.popupChild, + childOffset: childOffset, + childSize: childSize, + message: widget.message, + ); + }, + ), + ); }, fullscreenDialog: true, opaque: false, @@ -91,7 +114,7 @@ class _MessagePopupHolderState extends State { } void sendReaction(String type) { - debugPrint("Sending reaction type: " + type); + Logger.info("Sending reaction type: " + type); ActionHandler.sendReaction(CurrentChat.of(context)!.chat, widget.message, type); } diff --git a/lib/layouts/widgets/message_widget/received_message.dart b/lib/layouts/widgets/message_widget/received_message.dart index 5e7425544..86c0f5f58 100644 --- a/lib/layouts/widgets/message_widget/received_message.dart +++ b/lib/layouts/widgets/message_widget/received_message.dart @@ -1,6 +1,7 @@ import 'package:bluebubbles/helpers/constants.dart'; import 'package:bluebubbles/helpers/hex_color.dart'; import 'package:bluebubbles/helpers/message_helper.dart'; +import 'package:bluebubbles/helpers/navigator.dart'; import 'package:bluebubbles/helpers/redacted_helper.dart'; import 'package:bluebubbles/helpers/utils.dart'; import 'package:bluebubbles/layouts/setup/theme_selector/theme_selector.dart'; @@ -93,36 +94,58 @@ class _ReceivedMessageState extends State with MessageWidgetMix final bool hideContent = SettingsManager().settings.redactedMode.value && SettingsManager().settings.hideEmojis.value; - bool hasReactions = message.getReactions().length > 0; + bool hasReactions = message + .getReactions() + .length > 0; return Padding( padding: EdgeInsets.only( - left: CurrentChat.of(context)!.chat.participants.length > 1 ? 5.0 : 0.0, + left: CurrentChat + .of(context)! + .chat + .participants + .length > 1 ? 5.0 : 0.0, right: (hasReactions) ? 15.0 : 0.0, - top: widget.message.getReactions().length > 0 ? 15 : 0, + top: widget.message + .getReactions() + .length > 0 ? 15 : 0, ), child: hideContent ? ClipRRect( - borderRadius: BorderRadius.circular(25.0), - child: Container( - width: 70, - height: 70, - color: Theme.of(context).accentColor, - child: Center( - child: Text( - "emoji", - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.bodyText1, - ), - )), - ) + borderRadius: BorderRadius.circular(25.0), + child: Container( + width: 70, + height: 70, + color: Theme + .of(context) + .accentColor, + child: Center( + child: Text( + "emoji", + textAlign: TextAlign.center, + style: Theme + .of(context) + .textTheme + .bodyText1, + ), + )), + ) : Text( - message.text!, - style: Theme.of(context).textTheme.bodyText2!.apply(fontSizeFactor: 4), - ), + message.text!, + style: Theme + .of(context) + .textTheme + .bodyText2! + .apply(fontSizeFactor: 4), + ), ); } - List bubbleColors = [Theme.of(context).accentColor, Theme.of(context).accentColor]; + List bubbleColors = [Theme + .of(context) + .accentColor, Theme + .of(context) + .accentColor + ]; if (SettingsManager().settings.colorfulBubbles.value) { if (message.handle?.color == null) { bubbleColors = toColorGradient(message.handle?.address); @@ -144,16 +167,18 @@ class _ReceivedMessageState extends State with MessageWidgetMix ), Container( margin: EdgeInsets.only( - top: widget.message.getReactions().length > 0 && !widget.message.hasAttachments + top: widget.message + .getReactions() + .length > 0 && !widget.message.hasAttachments ? 18 : (widget.message.isFromMe != widget.olderMessage?.isFromMe) - ? 5.0 - : 0, + ? 5.0 + : 0, left: 10, right: 10, ), constraints: BoxConstraints( - maxWidth: context.width * MessageWidgetMixin.MAX_SIZE, + maxWidth: CustomNavigator.width(context) * MessageWidgetMixin.MAX_SIZE, ), padding: EdgeInsets.symmetric( vertical: 8, @@ -162,29 +187,29 @@ class _ReceivedMessageState extends State with MessageWidgetMix decoration: BoxDecoration( borderRadius: skin.value == Skins.iOS ? BorderRadius.only( - bottomLeft: Radius.circular(17), - bottomRight: Radius.circular(20), - topLeft: Radius.circular(20), - topRight: Radius.circular(20), - ) + bottomLeft: Radius.circular(17), + bottomRight: Radius.circular(20), + topLeft: Radius.circular(20), + topRight: Radius.circular(20), + ) : (skin.value == Skins.Material) - ? BorderRadius.only( - topLeft: widget.olderMessage == null || - MessageHelper.getShowTail(context, widget.olderMessage, widget.message) - ? Radius.circular(20) - : Radius.circular(5), - topRight: Radius.circular(20), - bottomRight: Radius.circular(20), - bottomLeft: Radius.circular(widget.showTail ? 20 : 5), - ) - : (skin.value == Skins.Samsung) - ? BorderRadius.only( - topLeft: Radius.circular(17.5), - topRight: Radius.circular(17.5), - bottomRight: Radius.circular(17.5), - bottomLeft: Radius.circular(17.5), - ) - : null, + ? BorderRadius.only( + topLeft: widget.olderMessage == null || + MessageHelper.getShowTail(context, widget.olderMessage, widget.message) + ? Radius.circular(20) + : Radius.circular(5), + topRight: Radius.circular(20), + bottomRight: Radius.circular(20), + bottomLeft: Radius.circular(widget.showTail ? 20 : 5), + ) + : (skin.value == Skins.Samsung) + ? BorderRadius.only( + topLeft: Radius.circular(17.5), + topRight: Radius.circular(17.5), + bottomRight: Radius.circular(17.5), + bottomLeft: Radius.circular(17.5), + ) + : null, gradient: LinearGradient( begin: AlignmentDirectional.bottomCenter, end: AlignmentDirectional.topCenter, @@ -195,7 +220,10 @@ class _ReceivedMessageState extends State with MessageWidgetMix text: TextSpan( children: MessageWidgetMixin.buildMessageSpans(context, widget.message, colors: widget.message.handle?.color != null ? bubbleColors : null), - style: Theme.of(context).textTheme.bodyText2, + style: Theme + .of(context) + .textTheme + .bodyText2, ), ), ), @@ -212,7 +240,10 @@ class _ReceivedMessageState extends State with MessageWidgetMix List messageColumn = []; // First, add the message sender (if applicable) - bool isGroup = CurrentChat.of(context)?.chat.isGroup() ?? false; + bool isGroup = CurrentChat + .of(context) + ?.chat + .isGroup() ?? false; bool addedSender = false; bool showSender = SettingsManager().settings.alwaysShowAvatars.value || isGroup || @@ -225,10 +256,15 @@ class _ReceivedMessageState extends State with MessageWidgetMix !widget.message.dateCreated!.isWithin(widget.olderMessage!.dateCreated!, minutes: 30)))) { messageColumn.add( Padding( - padding: EdgeInsets.only(left: 15.0, top: 5.0, bottom: widget.message.getReactions().length > 0 ? 0.0 : 3.0), + padding: EdgeInsets.only(left: 15.0, top: 5.0, bottom: widget.message + .getReactions() + .length > 0 ? 0.0 : 3.0), child: Text( getContactName(context, contactTitle, widget.message.handle!.address), - style: Theme.of(context).textTheme.subtitle1, + style: Theme + .of(context) + .textTheme + .subtitle1, maxLines: 1, overflow: TextOverflow.ellipsis, ), @@ -238,7 +274,9 @@ class _ReceivedMessageState extends State with MessageWidgetMix } // Second, add the attachments - if (widget.message.getRealAttachments().length > 0) { + if (widget.message + .getRealAttachments() + .length > 0) { messageColumn.add( addStickersToWidget( message: addReactionsToWidget( @@ -258,8 +296,15 @@ class _ReceivedMessageState extends State with MessageWidgetMix message = Padding(padding: EdgeInsets.only(left: 10.0), child: BalloonBundleWidget(message: widget.message)); } else if (widget.message.hasText()) { message = _buildMessageWithTail(widget.message); - if (widget.message.fullText.replaceAll("\n", " ").hasUrl) { - message = Column( + if (widget.message.fullText + .replaceAll("\n", " ") + .hasUrl) { + message = widget.message.fullText.isURL + ? Padding( + padding: EdgeInsets.only(left: 10.0), + child: widget.urlPreviewWidget, + ) + : Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -280,7 +325,9 @@ class _ReceivedMessageState extends State with MessageWidgetMix messageWidget: message, reactions: widget.reactionsWidget, message: widget.message, - shouldShow: widget.message.getRealAttachments().isEmpty), + shouldShow: widget.message + .getRealAttachments() + .isEmpty), stickers: widget.stickersWidget, isFromMe: widget.message.isFromMe!, ), @@ -290,17 +337,22 @@ class _ReceivedMessageState extends State with MessageWidgetMix List messagePopupColumn = List.from(messageColumn); if (!addedSender && isGroup) { messagePopupColumn.insert( - 0, - Padding( - padding: - EdgeInsets.only(left: 15.0, top: 5.0, bottom: widget.message.getReactions().length > 0 ? 0.0 : 3.0), - child: Text( - getContactName(context, contactTitle, widget.message.handle!.address), - style: Theme.of(context).textTheme.subtitle1, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - )); + 0, + Padding( + padding: EdgeInsets.only(left: 15.0, top: 5.0, bottom: widget.message + .getReactions() + .length > 0 ? 0.0 : 3.0), + child: Text( + getContactName(context, contactTitle, widget.message.handle!.address), + style: Theme + .of(context) + .textTheme + .subtitle1, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ); } // Now, let's create a row that will be the row with the following: @@ -369,10 +421,13 @@ class _ReceivedMessageState extends State with MessageWidgetMix Padding( // Padding to shift the bubble up a bit, relative to the avatar padding: EdgeInsets.only(bottom: 0.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, - children: messagePopupColumn, + child: SingleChildScrollView( + physics: BouncingScrollPhysics(), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: messagePopupColumn, + ), ), ), ); @@ -392,38 +447,38 @@ class _ReceivedMessageState extends State with MessageWidgetMix crossAxisAlignment: CrossAxisAlignment.center, children: [ MessagePopupHolder( - message: widget.message, - olderMessage: widget.olderMessage, - newerMessage: widget.newerMessage, - child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( + message: widget.message, + olderMessage: widget.olderMessage, + newerMessage: widget.newerMessage, + child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.end, + children: msgRow, + ), + // Add the timestamp for the samsung theme + if (skin.value == Skins.Samsung && + widget.message.dateCreated != null && + (widget.newerMessage?.dateCreated == null || + widget.message.isFromMe != widget.newerMessage?.isFromMe || + widget.message.handleId != widget.newerMessage?.handleId || + !widget.message.dateCreated!.isWithin(widget.newerMessage!.dateCreated!, minutes: 5))) + Padding( + padding: EdgeInsets.only(top: 5, left: (isGroup) ? 60 : 20), + child: MessageTimeStamp( + message: widget.message, + singleLine: true, + useYesterday: true, + ), + ) + ]), + popupChild: Row( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.end, - children: msgRow, + children: msgPopupRow, ), - // Add the timestamp for the samsung theme - if (skin.value == Skins.Samsung && - widget.message.dateCreated != null && - (widget.newerMessage?.dateCreated == null || - widget.message.isFromMe != widget.newerMessage?.isFromMe || - widget.message.handleId != widget.newerMessage?.handleId || - !widget.message.dateCreated!.isWithin(widget.newerMessage!.dateCreated!, minutes: 5))) - Padding( - padding: EdgeInsets.only(top: 5, left: (isGroup) ? 60 : 20), - child: MessageTimeStamp( - message: widget.message, - singleLine: true, - useYesterday: true, - ), - ) - ]), - popupChild: Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.end, - children: msgPopupRow, - ), ), if ((skin.value != Skins.Samsung && widget.message.guid != widget.olderMessage?.guid)) MessageTimeStamp( diff --git a/lib/layouts/widgets/message_widget/sent_message.dart b/lib/layouts/widgets/message_widget/sent_message.dart index f6d52c162..3d0573828 100644 --- a/lib/layouts/widgets/message_widget/sent_message.dart +++ b/lib/layouts/widgets/message_widget/sent_message.dart @@ -1,5 +1,6 @@ import 'dart:ui'; +import 'package:bluebubbles/helpers/navigator.dart'; import 'package:bluebubbles/layouts/setup/theme_selector/theme_selector.dart'; import 'package:bluebubbles/managers/notification_manager.dart'; import 'package:get/get.dart'; @@ -95,7 +96,7 @@ class SentMessageHelper { width: customWidth != null ? constraints.maxWidth : null, constraints: customWidth == null ? BoxConstraints( - maxWidth: context.width * MessageWidgetMixin.MAX_SIZE + (!padding ? 100 : 0), + maxWidth: CustomNavigator.width(context) * MessageWidgetMixin.MAX_SIZE + (!padding ? 100 : 0), ) : null, margin: EdgeInsets.only( @@ -147,12 +148,11 @@ class SentMessageHelper { ], ); } - if (!padding) return msg; return Container( width: customWidth != null ? customWidth - (showTail ? 20 : 0) : null, constraints: BoxConstraints( - maxWidth: customWidth != null ? customWidth - (showTail ? 20 : 0) : context.width, + maxWidth: customWidth != null ? customWidth - (showTail ? 20 : 0) : CustomNavigator.width(context), ), child: Row( crossAxisAlignment: CrossAxisAlignment.center, @@ -234,7 +234,7 @@ class SentMessageHelper { }, ); }, - child: Icon(Icons.error_outline, color: Colors.red), + child: Icon(SettingsManager().settings.skin.value == Skins.iOS ? CupertinoIcons.exclamationmark_circle : Icons.error_outline, color: Colors.red), ), ); } @@ -328,13 +328,15 @@ class _SentMessageState extends State with TickerProviderStateMixin ); } if (widget.message.fullText.replaceAll("\n", " ").hasUrl) { - message = - Column(mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.end, children: [ - Padding( - padding: EdgeInsets.only(right: 5.0), - child: widget.urlPreviewWidget, - ), - message, + message = widget.message.fullText.isURL ? Padding( + padding: EdgeInsets.only(right: 5.0), + child: widget.urlPreviewWidget, + ) : Column(mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.end, children: [ + Padding( + padding: EdgeInsets.only(right: 5.0), + child: widget.urlPreviewWidget, + ), + message, ]); } } diff --git a/lib/layouts/widgets/message_widget/stickers_widget.dart b/lib/layouts/widgets/message_widget/stickers_widget.dart index a4011e772..741843eec 100644 --- a/lib/layouts/widgets/message_widget/stickers_widget.dart +++ b/lib/layouts/widgets/message_widget/stickers_widget.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'package:bluebubbles/helpers/attachment_downloader.dart'; import 'package:bluebubbles/helpers/attachment_helper.dart'; +import 'package:bluebubbles/helpers/navigator.dart'; import 'package:bluebubbles/repository/models/attachment.dart'; import 'package:bluebubbles/repository/models/message.dart'; import 'package:flutter/material.dart'; @@ -92,7 +93,7 @@ class _StickersWidgetState extends State { // Turn the attachments into Image Widgets List stickers = this.stickers.map((item) { String pathName = AttachmentHelper.getAttachmentPath(item); - return Image.file(new File(pathName), width: context.width * 2 / 3, height: context.width * 2 / 4); + return Image.file(new File(pathName), width: CustomNavigator.width(context) * 2 / 3, height: CustomNavigator.width(context) * 2 / 4); }).toList(); return GestureDetector( diff --git a/lib/layouts/widgets/message_widget/typing_indicator.dart b/lib/layouts/widgets/message_widget/typing_indicator.dart index 79f94e39b..89e2de07f 100644 --- a/lib/layouts/widgets/message_widget/typing_indicator.dart +++ b/lib/layouts/widgets/message_widget/typing_indicator.dart @@ -2,6 +2,7 @@ import 'dart:math' as Math; import 'package:bluebubbles/helpers/constants.dart'; import 'package:bluebubbles/helpers/hex_color.dart'; +import 'package:bluebubbles/helpers/navigator.dart'; import 'package:bluebubbles/layouts/setup/theme_selector/theme_selector.dart'; import 'package:bluebubbles/layouts/widgets/message_widget/message_widget_mixin.dart'; import 'package:bluebubbles/managers/settings_manager.dart'; @@ -13,9 +14,11 @@ class TypingIndicator extends StatefulWidget { Key? key, this.visible = false, this.bigPin = false, + this.chatList = false, }) : super(key: key); final bool visible; final bool bigPin; + final bool chatList; @override _TypingIndicatorState createState() => _TypingIndicatorState(); @@ -70,7 +73,7 @@ class _TypingIndicatorState extends State with TickerProviderSt Stack( alignment: Alignment.bottomLeft, children: [ - if (skin.value == Skins.iOS) + if (!widget.chatList && skin.value == Skins.iOS) Container( margin: EdgeInsets.only(left: widget.bigPin ? 18 : 2), decoration: BoxDecoration( @@ -80,7 +83,7 @@ class _TypingIndicatorState extends State with TickerProviderSt width: 10, height: 10, ), - if (skin.value == Skins.iOS) + if (!widget.chatList && skin.value == Skins.iOS) Container( margin: EdgeInsets.only(left: 9, bottom: 10), decoration: BoxDecoration( @@ -94,10 +97,10 @@ class _TypingIndicatorState extends State with TickerProviderSt margin: EdgeInsets.only( left: 10, right: 10, - bottom: skin.value == Skins.iOS ? 13 : 5, + bottom: !widget.chatList && skin.value == Skins.iOS ? 13 : 5, ), constraints: BoxConstraints( - maxWidth: context.width * MessageWidgetMixin.MAX_SIZE, + maxWidth: CustomNavigator.width(context) * MessageWidgetMixin.MAX_SIZE, ), padding: EdgeInsets.symmetric( vertical: 8, diff --git a/lib/layouts/widgets/vertical_split_view.dart b/lib/layouts/widgets/vertical_split_view.dart new file mode 100644 index 000000000..ad1a342a0 --- /dev/null +++ b/lib/layouts/widgets/vertical_split_view.dart @@ -0,0 +1,104 @@ +import 'package:bluebubbles/main.dart'; +import 'package:bluebubbles/managers/event_dispatcher.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:get/get.dart'; + +class VerticalSplitView extends StatefulWidget { + final Widget left; + final Widget right; + final double initialRatio; + final double dividerWidth; + final double minRatio; + final double maxRatio; + final bool allowResize; + + const VerticalSplitView( + {Key? key, + required this.left, + required this.right, + this.initialRatio = 0.5, + this.allowResize = true, + this.dividerWidth = 16.0, + this.minRatio = 0, + this.maxRatio = 0}) + : assert(initialRatio >= 0), + assert(initialRatio <= 1), + super(key: key); + + @override + _VerticalSplitViewState createState() => _VerticalSplitViewState(); +} + +class _VerticalSplitViewState extends State { + //from 0-1 + late final RxDouble _ratio; + double? _maxWidth; + + get _width1 => _ratio * _maxWidth!; + + get _width2 => (1 - _ratio.value) * _maxWidth!; + + @override + void initState() { + super.initState(); + _ratio = RxDouble(prefs.getDouble('splitRatio') ?? widget.initialRatio); + EventDispatcher().stream.listen((Map event) { + if (!event.containsKey("type")) return; + + if (event["type"] == 'split-refresh' && this.mounted) { + _ratio.value = prefs.getDouble('splitRatio') ?? _ratio.value; + setState(() {}); + } + }); + debounce(_ratio, (val) { + prefs.setDouble('splitRatio', val); + EventDispatcher().emit('split-refresh', null); + }); + } + + @override + Widget build(BuildContext context) { + return LayoutBuilder(builder: (context, BoxConstraints constraints) { + assert(_ratio <= 1); + assert(_ratio >= 0); + if (_maxWidth == null) _maxWidth = constraints.maxWidth - widget.dividerWidth; + if (_maxWidth != constraints.maxWidth) { + _maxWidth = constraints.maxWidth - widget.dividerWidth; + } + + return SizedBox( + width: constraints.maxWidth, + child: Obx(() => Row( + children: [ + SizedBox( + width: _width1, + child: widget.left, + ), + (widget.allowResize) ? GestureDetector( + behavior: HitTestBehavior.translucent, + child: Container( + color: Theme.of(context).accentColor, + child: SizedBox( + width: widget.dividerWidth, + height: constraints.maxHeight, + child: Icon(Icons.drag_indicator, color: Theme.of(context).textTheme.subtitle1?.color, size: 10), + )), + onPanUpdate: (DragUpdateDetails details) { + _ratio.value = (_ratio.value + (details.delta.dx / _maxWidth!)).clamp(widget.minRatio, widget.maxRatio); + }, + ) : SizedBox( + width: widget.dividerWidth, + height: constraints.maxHeight, + child: Container(color: Theme.of(context).accentColor) + ), + SizedBox( + width: _width2, + child: widget.right, + ), + ], + ), + )); + }); + } +} \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 3583bed0e..d020bb34e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -6,24 +6,14 @@ import 'dart:ui'; import 'package:adaptive_theme/adaptive_theme.dart'; import 'package:bluebubbles/helpers/attachment_downloader.dart'; import 'package:bluebubbles/helpers/constants.dart'; +import 'package:bluebubbles/helpers/logger.dart'; +import 'package:bluebubbles/helpers/navigator.dart'; import 'package:bluebubbles/helpers/utils.dart'; import 'package:bluebubbles/layouts/conversation_list/conversation_list.dart'; import 'package:bluebubbles/layouts/conversation_view/conversation_view.dart'; -import 'package:bluebubbles/layouts/settings/about_panel.dart'; -import 'package:bluebubbles/layouts/settings/attachment_panel.dart'; -import 'package:bluebubbles/layouts/settings/chat_list_panel.dart'; -import 'package:bluebubbles/layouts/settings/conversation_panel.dart'; -import 'package:bluebubbles/layouts/settings/custom_avatar_color_panel.dart'; -import 'package:bluebubbles/layouts/settings/custom_avatar_panel.dart'; -import 'package:bluebubbles/layouts/settings/pinned_order_panel.dart'; -import 'package:bluebubbles/layouts/settings/private_api_panel.dart'; -import 'package:bluebubbles/layouts/settings/redacted_mode_panel.dart'; -import 'package:bluebubbles/layouts/settings/server_management_panel.dart'; -import 'package:bluebubbles/layouts/settings/theme_panel.dart'; import 'package:bluebubbles/layouts/setup/failure_to_start.dart'; import 'package:bluebubbles/layouts/setup/setup_view.dart'; import 'package:bluebubbles/layouts/testing_mode.dart'; -import 'package:bluebubbles/layouts/widgets/theme_switcher/theme_switcher.dart'; import 'package:bluebubbles/managers/background_isolate.dart'; import 'package:bluebubbles/managers/incoming_queue.dart'; import 'package:bluebubbles/managers/life_cycle_manager.dart'; @@ -42,12 +32,17 @@ import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart' hide Priority; import 'package:flutter/services.dart'; import 'package:flutter_libphonenumber/flutter_libphonenumber.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import 'package:flutter_native_timezone/flutter_native_timezone.dart'; import 'package:get/get.dart'; import 'package:intl/date_symbol_data_local.dart'; import 'package:local_auth/local_auth.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:receive_sharing_intent/receive_sharing_intent.dart'; import 'package:secure_application/secure_application.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:timezone/data/latest.dart' as tz; +import 'package:timezone/timezone.dart' as tz; // final SentryClient _sentry = SentryClient( // dsn: @@ -65,20 +60,13 @@ bool get isInDebugMode { return inDebugMode; } +FlutterLocalNotificationsPlugin? flutterLocalNotificationsPlugin; +late SharedPreferences prefs; + Future _reportError(dynamic error, dynamic stackTrace) async { // Print the exception to the console. - debugPrint('Caught error: $error'); - if (isInDebugMode) { - // Print the full stacktrace in debug mode. - debugPrint(stackTrace.toString()); - } else { - debugPrint(stackTrace.toString()); - // Send the Exception and Stacktrace to Sentry in Production mode. - // _sentry.captureException( - // exception: error, - // stackTrace: stackTrace, - // ); - } + Logger.error('Caught error: $error'); + Logger.error(stackTrace.toString()); } class MyHttpOverrides extends HttpOverrides { @@ -99,6 +87,8 @@ Future main() async { // This captures errors reported by the Flutter framework. FlutterError.onError = (FlutterErrorDetails details) { + Logger.error(details.exceptionAsString()); + Logger.error(details.stack.toString()); if (isInDebugMode) { // In development mode simply print to console. FlutterError.dumpErrorToConsole(details); @@ -112,11 +102,19 @@ Future main() async { WidgetsFlutterBinding.ensureInitialized(); dynamic exception; try { + prefs = await SharedPreferences.getInstance(); await DBProvider.db.initDB(); await initializeDateFormatting('fr_FR', null); await SettingsManager().init(); await SettingsManager().getSavedSettings(headless: true); Get.put(AttachmentDownloadService()); + flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin(); + const AndroidInitializationSettings initializationSettingsAndroid = AndroidInitializationSettings('ic_stat_icon'); + final InitializationSettings initializationSettings = + InitializationSettings(android: initializationSettingsAndroid); + await flutterLocalNotificationsPlugin!.initialize(initializationSettings); + tz.initializeTimeZones(); + tz.setLocalLocation(tz.getLocation(await FlutterNativeTimezone.getLocalTimezone())); } catch (e) { exception = e; } @@ -238,31 +236,7 @@ class Main extends StatelessWidget with WidgetsBindingObserver { defaultTransition: Transition.cupertino, getPages: [ - GetPage(page: () => AboutPanel(), name: "/settings/about-panel"), - GetPage(page: () => AttachmentPanel(), name: "/settings/attachment-panel"), - GetPage(page: () => ChatListPanel(), name: "/settings/chat-list-panel"), - GetPage(page: () => ConversationPanel(), name: "/settings/conversation-panel"), - GetPage( - page: () => CustomAvatarColorPanel(), - name: "/settings/custom-avatar-color-panel", - binding: CustomAvatarColorPanelBinding()), - GetPage( - page: () => CustomAvatarPanel(), - name: "/settings/custom-avatar-panel", - binding: CustomAvatarPanelBinding()), - GetPage(page: () => PinnedOrderPanel(), name: "/settings/pinned-order-panel"), - GetPage( - page: () => PrivateAPIPanel(), name: "/settings/private-api-panel", binding: PrivateAPIPanelBinding()), - GetPage(page: () => RedactedModePanel(), name: "/settings/redacted-mode-panel"), - GetPage( - page: () => ServerManagementPanel(), - name: "/settings/server-management-panel", - binding: ServerManagementPanelBinding()), - GetPage( - page: () => TestingMode(), - name: "/testing-mode", - binding: TestingModeBinding()), - GetPage(page: () => ThemePanel(), name: "/settings/theme-panel", binding: ThemePanelBinding()), + GetPage(page: () => TestingMode(), name: "/testing-mode", binding: TestingModeBinding()), ], ), ); @@ -327,6 +301,12 @@ class _HomeState extends State with WidgetsBindingObserver { // Get the saved settings from the settings manager after the first frame SchedulerBinding.instance!.addPostFrameCallback((_) async { await SettingsManager().getSavedSettings(context: context); + + if (SettingsManager().settings.colorsFromMedia.value) { + try { + await MethodChannelInterface().invokeMethod("start-notif-listener"); + } catch (_) {} + } // Get sharing media from files shared to the app from cold start // This one only handles files, not text ReceiveSharingIntent.getInitialMedia().then((List value) async { @@ -345,12 +325,11 @@ class _HomeState extends State with WidgetsBindingObserver { if (attachments.length == 0) return; // Go to the new chat creator, with all of our attachments - Navigator.of(context).pushAndRemoveUntil( - ThemeSwitcher.buildPageRoute( - builder: (context) => ConversationView( - existingAttachments: attachments, - isCreator: true, - ), + CustomNavigator.pushAndRemoveUntil( + context, + ConversationView( + existingAttachments: attachments, + isCreator: true, ), (route) => route.isFirst, ); @@ -362,12 +341,11 @@ class _HomeState extends State with WidgetsBindingObserver { if (text == null) return; // Go to the new chat creator, with all of our text - Navigator.of(context).pushAndRemoveUntil( - ThemeSwitcher.buildPageRoute( - builder: (context) => ConversationView( - existingText: text, - isCreator: true, - ), + CustomNavigator.pushAndRemoveUntil( + context, + ConversationView( + existingText: text, + isCreator: true, ), (route) => route.isFirst, ); @@ -394,7 +372,6 @@ class _HomeState extends State with WidgetsBindingObserver { void didChangeDependencies() async { Locale myLocale = Localizations.localeOf(context); SettingsManager().countryCode = myLocale.countryCode; - SettingsManager().settings.use24HrFormat.value = MediaQuery.of(Get.context!).alwaysUse24HourFormat; await FlutterLibphonenumber().init(); super.didChangeDependencies(); } @@ -452,6 +429,7 @@ class _HomeState extends State with WidgetsBindingObserver { ]); return ConversationList( showArchivedChats: false, + showUnknownSenders: false, ); } else { SystemChrome.setPreferredOrientations([ diff --git a/lib/managers/alarm_manager.dart b/lib/managers/alarm_manager.dart index cbb56b92f..a50f29805 100644 --- a/lib/managers/alarm_manager.dart +++ b/lib/managers/alarm_manager.dart @@ -1,5 +1,5 @@ +import 'package:bluebubbles/helpers/logger.dart'; import 'package:bluebubbles/managers/method_channel_interface.dart'; -import 'package:flutter/material.dart'; /// The alarm manager is responsible for all scheduled events /// @@ -24,8 +24,7 @@ class AlarmManager { /// Defines what to do when a specific alarm goes off /// @param [id] is the id of the alarm Future onReceiveAlarm(int id) async { - // TODO do something here! - debugPrint("(ALARM MANAGER) -> Receive alarm $id"); + Logger.info("Receive alarm $id", tag: "AlarmManager"); // Keep this just in case the thread doesn't get closed automatically from the socket events sent MethodChannelInterface().closeThread(); diff --git a/lib/managers/background_isolate.dart b/lib/managers/background_isolate.dart index cf1102cbf..1ca4ce7bc 100644 --- a/lib/managers/background_isolate.dart +++ b/lib/managers/background_isolate.dart @@ -1,5 +1,6 @@ import 'dart:ui'; +import 'package:bluebubbles/main.dart'; import 'package:bluebubbles/managers/contact_manager.dart'; import 'package:bluebubbles/managers/method_channel_interface.dart'; import 'package:bluebubbles/managers/settings_manager.dart'; @@ -7,6 +8,7 @@ import 'package:bluebubbles/repository/database.dart'; import 'package:bluebubbles/socket_manager.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:shared_preferences/shared_preferences.dart'; abstract class BackgroundIsolateInterface { static void initialize() { @@ -18,9 +20,11 @@ abstract class BackgroundIsolateInterface { } callbackHandler() async { + // can't use logger here debugPrint("(ISOLATE) Starting up..."); MethodChannel _backgroundChannel = MethodChannel("com.bluebubbles.messaging"); WidgetsFlutterBinding.ensureInitialized(); + prefs = await SharedPreferences.getInstance(); await DBProvider.db.initDB(); await SettingsManager().init(); await SettingsManager().getSavedSettings(headless: true); diff --git a/lib/managers/contact_manager.dart b/lib/managers/contact_manager.dart index 74c7ff39c..874a9b351 100644 --- a/lib/managers/contact_manager.dart +++ b/lib/managers/contact_manager.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:typed_data'; +import 'package:bluebubbles/helpers/logger.dart'; import 'package:bluebubbles/helpers/utils.dart'; import 'package:bluebubbles/layouts/widgets/contact_avatar_widget.dart'; import 'package:bluebubbles/managers/settings_manager.dart'; @@ -17,6 +18,7 @@ class ContactManager { } static final ContactManager _manager = ContactManager._internal(); + static final tag = 'ContactManager'; StreamController> _stream = new StreamController.broadcast(); @@ -57,17 +59,17 @@ class ContactManager { try { PermissionStatus status = await Permission.contacts.status; if (status.isGranted) return true; - debugPrint("[ContactManager] -> Contacts Permission Status: ${status.toString()}"); + Logger.info("Contacts Permission Status: ${status.toString()}", tag: tag); // If it's not permanently denied, request access if (!status.isPermanentlyDenied) { return (await Permission.contacts.request()).isGranted; } - debugPrint("[ContactManager] -> Contacts permissions are permanently denied..."); + Logger.info("Contacts permissions are permanently denied...", tag: tag); } catch (ex) { - debugPrint("[ContactManager] -> Error getting access to contacts!"); - debugPrint(ex.toString()); + Logger.error("Error getting access to contacts!", tag: tag); + Logger.error(ex.toString(), tag: tag); } return false; @@ -76,7 +78,7 @@ class ContactManager { Future getContacts({bool headless = false, bool force = false}) async { // If we are fetching the contacts, return the current future so we can await it if (getContactsFuture != null && !getContactsFuture!.isCompleted) { - debugPrint("[ContactManager] -> Already fetching contacts, returning future..."); + Logger.info("Already fetching contacts, returning future...", tag: tag); return getContactsFuture!.future; } @@ -84,7 +86,7 @@ class ContactManager { // If we have, exit, we don't need to re-fetch the chats again int now = DateTime.now().toUtc().millisecondsSinceEpoch; if (!force && lastRefresh != 0 && now < lastRefresh + (60000 * 5)) { - debugPrint("[ContactManager] -> Not fetching contacts; Not enough time has elapsed"); + Logger.info("Not fetching contacts; Not enough time has elapsed", tag: tag); return; } @@ -98,14 +100,14 @@ class ContactManager { getContactsFuture = new Completer(); // Fetch the current list of contacts - debugPrint("[ContactManager] -> Fetching contacts"); + Logger.info("Fetching contacts", tag: tag); contacts = (await ContactsService.getContacts(withThumbnails: false)).toList(); hasFetchedContacts = true; // Match handles in the database with contacts await this.matchHandles(); - debugPrint("[ContactManager] -> Finished fetching contacts (${handleToContact.length})"); + Logger.info("Finished fetching contacts (${handleToContact.length})", tag: tag); if (getContactsFuture != null && !getContactsFuture!.isCompleted) { getContactsFuture!.complete(true); } @@ -131,7 +133,7 @@ class ContactManager { contactMatch = await getContact(handle); handleToContact[handle.address] = contactMatch; } catch (ex) { - debugPrint('Failed to match handle for address, "${handle.address}": ${ex.toString()}'); + Logger.error('Failed to match handle for address, "${handle.address}": ${ex.toString()}', tag: tag); } // If we have a match, add it to the mapping, then break out @@ -155,7 +157,7 @@ class ContactManager { // Create a new completer for this getAvatarsFuture = new Completer(); - debugPrint("[ContactManager] -> Fetching Avatars"); + Logger.info("Fetching Avatars", tag: tag); for (String address in handleToContact.keys) { Contact? contact = handleToContact[address]; if (handleToContact[address] == null) continue; @@ -171,7 +173,7 @@ class ContactManager { }); } - debugPrint("[ContactManager] -> Finished fetching avatars"); + Logger.info("Finished fetching avatars", tag: tag); getAvatarsFuture!.complete(); } @@ -244,7 +246,7 @@ class ContactManager { Contact? contact = await getContact(handle); if (contact != null && contact.displayName != null) return contact.displayName; } catch (ex) { - debugPrint('Failed to getContact() in getContactTitle(), for address, "$address": ${ex.toString()}'); + Logger.error('Failed to getContact() in getContactTitle(), for address, "$address": ${ex.toString()}', tag: tag); } try { @@ -261,7 +263,8 @@ class ContactManager { return contactTitle; } catch (ex) { - debugPrint('Failed to formatPhoneNumber() in getContactTitle(), for address, "$address": ${ex.toString()}'); + Logger.error('Failed to formatPhoneNumber() in getContactTitle(), for address, "$address": ${ex.toString()}', + tag: tag); } return address; diff --git a/lib/managers/incoming_queue.dart b/lib/managers/incoming_queue.dart index e4b8b15bf..5921b10c4 100644 --- a/lib/managers/incoming_queue.dart +++ b/lib/managers/incoming_queue.dart @@ -1,6 +1,6 @@ import 'package:bluebubbles/action_handler.dart'; +import 'package:bluebubbles/helpers/logger.dart'; import 'package:bluebubbles/managers/queue_manager.dart'; -import 'package:flutter/material.dart'; class IncomingQueue extends QueueManager { factory IncomingQueue() { @@ -42,7 +42,7 @@ class IncomingQueue extends QueueManager { } default: { - debugPrint("Unhandled queue event: ${item.event}"); + Logger.info("Unhandled queue event: ${item.event}"); } } } diff --git a/lib/managers/life_cycle_manager.dart b/lib/managers/life_cycle_manager.dart index eb2d80341..1a0fc4f26 100644 --- a/lib/managers/life_cycle_manager.dart +++ b/lib/managers/life_cycle_manager.dart @@ -50,7 +50,7 @@ class LifeCycleManager { // Refresh all the chats assuming that the app has already finished setup if (SettingsManager().settings.finishedSetup.value) { - ChatBloc().refreshChats(); + ChatBloc().resumeRefresh(); } } diff --git a/lib/managers/method_channel_interface.dart b/lib/managers/method_channel_interface.dart index ebf92cdc6..f510372f3 100644 --- a/lib/managers/method_channel_interface.dart +++ b/lib/managers/method_channel_interface.dart @@ -2,15 +2,18 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'dart:isolate'; +import 'dart:math'; import 'dart:ui'; import 'package:bluebubbles/action_handler.dart'; import 'package:bluebubbles/blocs/chat_bloc.dart'; import 'package:bluebubbles/blocs/text_field_bloc.dart'; +import 'package:bluebubbles/helpers/navigator.dart'; +import 'package:bluebubbles/helpers/logger.dart'; import 'package:bluebubbles/helpers/utils.dart'; import 'package:bluebubbles/layouts/conversation_view/conversation_view.dart'; +import 'package:bluebubbles/layouts/conversation_view/conversation_view_mixin.dart'; import 'package:bluebubbles/layouts/testing_mode.dart'; -import 'package:bluebubbles/layouts/widgets/theme_switcher/theme_switcher.dart'; import 'package:bluebubbles/managers/alarm_manager.dart'; import 'package:bluebubbles/managers/current_chat.dart'; import 'package:bluebubbles/managers/event_dispatcher.dart'; @@ -20,11 +23,13 @@ import 'package:bluebubbles/managers/notification_manager.dart'; import 'package:bluebubbles/managers/queue_manager.dart'; import 'package:bluebubbles/managers/settings_manager.dart'; import 'package:bluebubbles/repository/models/chat.dart'; +import 'package:bluebubbles/repository/models/theme_object.dart'; import 'package:bluebubbles/socket_manager.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:get/get.dart'; +import 'package:simple_animations/simple_animations.dart'; /// [MethodChannelInterface] is a manager used to talk to native code via a flutter MethodChannel /// @@ -44,6 +49,11 @@ class MethodChannelInterface { /// [headless] identifies if this MethodChannelInterface is used when the app is fully closed, in hich case some actions cannot be done bool headless = false; + bool isRunning = false; + Color? previousPrimary; + Color? previousLightBg; + Color? previousDarkBg; + /// Initialize all of the platform channels /// /// @param [customChannel] an optional custom platform channel to use by the methodchannelinterface @@ -91,16 +101,18 @@ class MethodChannelInterface { return new Future.value(""); case "new-message": - print("Received new message from FCM"); + Logger.info("Received new message from FCM"); // Retreive the data for this message as a json Map? data = jsonDecode(call.arguments); // send data to the UI thread if it is active, otherwise handle in the isolate final SendPort? send = IsolateNameServer.lookupPortByName('bg_isolate'); if (send != null) { + Logger.info("Handling through SendPort"); data!['action'] = 'new-message'; send.send(data); } else { + Logger.info("Handling through IncomingQueue"); // Add it to the queue with the data as the item IncomingQueue().add(new QueueItem(event: IncomingQueue.HANDLE_MESSAGE_EVENT, item: {"data": data})); } @@ -122,7 +134,7 @@ class MethodChannelInterface { return new Future.value(""); case "ChatOpen": - debugPrint("Opening Chat with GUID: ${call.arguments}"); + Logger.info("Opening Chat with GUID: ${call.arguments}"); openChat(call.arguments); return new Future.value(""); @@ -183,7 +195,7 @@ class MethodChannelInterface { // Get the path to where the temp files are stored String sharedFilesPath = SettingsManager().sharedFilesPath; - debugPrint("shareAttachments " + sharedFilesPath); + Logger.info("shareAttachments " + sharedFilesPath); // Loop through all of the attachments sent by native code call.arguments["attachments"].forEach((element) { @@ -216,13 +228,12 @@ class MethodChannelInterface { } // Go to the new chat creator with all of these attachments to select a chat in case it wasn't a direct share - NavigatorManager().navigatorKey.currentState!.pushAndRemoveUntil( - ThemeSwitcher.buildPageRoute( - builder: (context) => ConversationView( - existingAttachments: attachments, - isCreator: true, - // onTapGoToChat: true, - ), + CustomNavigator.pushAndRemoveUntil( + Get.context!, + ConversationView( + existingAttachments: attachments, + isCreator: true, + // onTapGoToChat: true, ), (route) => route.isFirst, ); @@ -254,12 +265,11 @@ class MethodChannelInterface { } } // Navigate to the new chat creator with the specified text - NavigatorManager().navigatorKey.currentState!.pushAndRemoveUntil( - ThemeSwitcher.buildPageRoute( - builder: (context) => ConversationView( - existingText: text, - isCreator: true, - ), + CustomNavigator.pushAndRemoveUntil( + Get.context!, + ConversationView( + existingText: text, + isCreator: true, ), (route) => route.isFirst, ); @@ -268,6 +278,70 @@ class MethodChannelInterface { case "alarm-wake": AlarmManager().onReceiveAlarm(call.arguments["id"]); return new Future.value(""); + case "media-colors": + if (!SettingsManager().settings.colorsFromMedia.value) return Future.value(""); + final Color primary = Color(call.arguments['primary']); + final Color lightBg = Color(call.arguments['lightBg']); + final Color darkBg = Color(call.arguments['darkBg']); + final double primaryPercent = call.arguments['primaryPercent']; + final double lightBgPercent = call.arguments['lightBgPercent']; + final double darkBgPercent = call.arguments['darkBgPercent']; + if (Get.context != null && + (!isRunning || primary != previousPrimary || lightBg != previousLightBg || darkBg != previousDarkBg)) { + previousPrimary = primary; + previousLightBg = lightBg; + previousDarkBg = darkBg; + isRunning = true; + print("primary color is $primary"); + print("light bg color is $lightBg"); + print("dark bg color is $darkBg"); + var darkTheme = (await ThemeObject.getThemes()).firstWhere((e) => e.name == "Music Theme (Dark)"); + var lightTheme = (await ThemeObject.getThemes()).firstWhere((e) => e.name == "Music Theme (Light)"); + await darkTheme.fetchData(); + var darkPrimaryEntry = darkTheme.entries.firstWhere((element) => element.name == "PrimaryColor"); + var darkBgEntry = darkTheme.entries.firstWhere((element) => element.name == "BackgroundColor"); + darkPrimaryEntry.color = primary; + darkBgEntry.color = darkBg; + await lightTheme.fetchData(); + var lightPrimaryEntry = lightTheme.entries.firstWhere((element) => element.name == "PrimaryColor"); + var lightBgEntry = lightTheme.entries.firstWhere((element) => element.name == "BackgroundColor"); + lightPrimaryEntry.color = primary; + lightBgEntry.color = lightBg; + if (ThemeObject.inDarkMode(Get.context!)) { + if (primaryPercent != 0.5 && darkBgPercent != 0.5) { + double difference = min((primaryPercent / (primaryPercent + darkBgPercent)), 1 - (primaryPercent / (primaryPercent + darkBgPercent))); + Tween color1 = Tween(begin: 0, end: difference); + Tween color2 = Tween(begin: 1 - difference, end: 1); + ConversationViewMixin.gradientTween.value = MultiTween() + ..add("color1", color1) + ..add("color2", color2); + } else { + ConversationViewMixin.gradientTween.value = MultiTween() + ..add("color1", Tween(begin: 0.0, end: 0.2)) + ..add("color2", Tween(begin: 0.8, end: 1.0)); + } + } else { + if (primaryPercent != 0.5 && lightBgPercent != 0.5) { + double difference = min((primaryPercent / (primaryPercent + lightBgPercent)), 1 - (primaryPercent / (primaryPercent + lightBgPercent))); + Tween color1 = Tween(begin: 0.0, end: difference); + Tween color2 = Tween(begin: 1.0 - difference, end: 1.0); + ConversationViewMixin.gradientTween.value = MultiTween() + ..add("color1", color1) + ..add("color2", color2); + } else { + ConversationViewMixin.gradientTween.value = MultiTween() + ..add("color1", Tween(begin: 0.0, end: 0.2)) + ..add("color2", Tween(begin: 0.8, end: 1.0)); + } + } + await SettingsManager().saveSelectedTheme(Get.context!, selectedLightTheme: lightTheme, selectedDarkTheme: darkTheme); + isRunning = false; + } + return Future.value(""); + case "remove-sendPort": + IsolateNameServer.removePortNameMapping('bg_isolate'); + print("Removed sendPort because Activity was destroyed"); + return Future.value(""); default: return new Future.value(""); } @@ -277,7 +351,7 @@ class MethodChannelInterface { void closeThread() { // Only do this if we are indeed running in the background if (headless) { - debugPrint("(CloseThread) -> Closing the background isolate..."); + Logger.info("Closing the background isolate...", tag: "MCI-CloseThread"); // Tells the native code to close the isolate invokeMethod("close-background-isolate"); @@ -320,14 +394,12 @@ class MethodChannelInterface { // if (!CurrentChat.isActive(openedChat.guid)) // Actually navigate to the chat page - NavigatorManager().navigatorKey.currentState! - ..pushAndRemoveUntil( - ThemeSwitcher.buildPageRoute( - builder: (context) => ConversationView( - chat: openedChat, - existingAttachments: existingAttachments, - existingText: existingText, - ), + CustomNavigator.pushAndRemoveUntil( + Get.context!, + ConversationView( + chat: openedChat, + existingAttachments: existingAttachments, + existingText: existingText, ), (route) => route.isFirst, ); @@ -339,7 +411,7 @@ class MethodChannelInterface { await Future.delayed(Duration(milliseconds: 500)); NotificationManager().switchChat(openedChat); } else { - debugPrint("(OpenChat) -> Failed to find chat"); + Logger.warn("Failed to find chat", tag: "MCI-OpenChat"); } } } diff --git a/lib/managers/notification_manager.dart b/lib/managers/notification_manager.dart index 914041275..655a65843 100644 --- a/lib/managers/notification_manager.dart +++ b/lib/managers/notification_manager.dart @@ -3,18 +3,21 @@ import 'dart:math'; import 'dart:typed_data'; import 'package:bluebubbles/blocs/chat_bloc.dart'; +import 'package:bluebubbles/helpers/hex_color.dart'; +import 'package:bluebubbles/helpers/logger.dart'; import 'package:bluebubbles/helpers/message_helper.dart'; import 'package:bluebubbles/helpers/utils.dart'; +import 'package:bluebubbles/main.dart'; import 'package:bluebubbles/managers/contact_manager.dart'; import 'package:bluebubbles/managers/current_chat.dart'; import 'package:bluebubbles/managers/method_channel_interface.dart'; import 'package:bluebubbles/managers/settings_manager.dart'; import 'package:bluebubbles/repository/models/chat.dart'; -import 'package:bluebubbles/repository/models/handle.dart'; import 'package:bluebubbles/repository/models/message.dart'; import 'package:bluebubbles/socket_manager.dart'; import 'package:contacts_service/contacts_service.dart'; -import 'package:flutter/material.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart' as fln; +import 'package:timezone/timezone.dart' as tz; class NotificationVisibility { // ignore: non_constant_identifier_names @@ -90,11 +93,50 @@ class NotificationManager { /// Creates notification channel for android /// This is done through native code and all of this data is hard coded for now Future createNotificationChannel(String channelID, String channelName, String channelDescription) async { + List sounds = ["twig.wav", "walrus.wav", "sugarfree.wav", "raspberry.wav"]; await MethodChannelInterface().invokeMethod("create-notif-channel", { "channel_name": channelName, "channel_description": channelDescription, "CHANNEL_ID": channelID, }); + if (channelID.contains("new_messages")) { + sounds.forEach((s) async { + await MethodChannelInterface().invokeMethod("create-notif-channel", { + "channel_name": channelName, + "channel_description": channelDescription, + "CHANNEL_ID": channelID + "_$s", + "sound": s, + }); + }); + } + } + + Future scheduleNotification(Chat chat, Message message, DateTime time) async { + // Get a title as best as we can + String? chatTitle = await chat.getTitle(); + bool isGroup = chat.isGroup(); + + // If we couldn't get a chat title, generate placeholder names + if (chatTitle == null) { + chatTitle = isGroup ? 'Group Chat' : 'iMessage Chat'; + } + await flutterLocalNotificationsPlugin!.zonedSchedule( + Random().nextInt(9998) + 1, + 'Reminder: $chatTitle', + await MessageHelper.getNotificationText(message), + tz.TZDateTime.from(time, tz.local), + fln.NotificationDetails( + android: fln.AndroidNotificationDetails( + "com.bluebubbles.reminders", + 'Reminders', + 'Message reminder notifications', + priority: fln.Priority.max, + importance: fln.Importance.max, + color: HexColor("4990de"), + )), + payload: await MessageHelper.getNotificationText(message), + androidAllowWhileIdle: true, + uiLocalNotificationDateInterpretation: fln.UILocalNotificationDateInterpretation.absoluteTime); } /// Creates a notification by sending to native code @@ -122,7 +164,7 @@ class NotificationManager { /// @param [contact] optional parameter of the contact of the message Future createNotificationFromMessage(Chat chat, Message message, int visibility) async { // sanity check to make sure we don't notify if the chat is muted - if (chat.isMuted ?? false) return; + if (await chat.shouldMuteNotification(message)) return; Uint8List? contactIcon; // Get the contact name if the message is not from you @@ -159,14 +201,14 @@ class NotificationManager { contactIcon = defaultAvatar; } } catch (ex) { - debugPrint("Failed to load contact avatar: ${ex.toString()}"); + Logger.error("Failed to load contact avatar: ${ex.toString()}"); } try { // Try to update the share targets await ChatBloc().updateShareTarget(chat); } catch (ex) { - debugPrint("Failed to update share target! Error: ${ex.toString()}"); + Logger.error("Failed to update share target! Error: ${ex.toString()}"); } // Get a title as best as we can @@ -178,8 +220,18 @@ class NotificationManager { chatTitle = isGroup ? 'Group Chat' : 'iMessage Chat'; } - await createNewMessageNotification(chat.guid!, isGroup, chatTitle, contactIcon, contactName, contactIcon, messageText, - message.dateCreated ?? DateTime.now(), message.isFromMe ?? false, visibility, chat.id ?? Random().nextInt(9998) + 1); + await createNewMessageNotification( + chat.guid!, + isGroup, + chatTitle, + contactIcon, + contactName, + contactIcon, + messageText, + message.dateCreated ?? DateTime.now(), + message.isFromMe ?? false, + visibility, + chat.id ?? Random().nextInt(9998) + 1); } Future createNewMessageNotification( @@ -195,7 +247,10 @@ class NotificationManager { int visibility, int summaryId) async { await MethodChannelInterface().platform.invokeMethod("new-message-notification", { - "CHANNEL_ID": NEW_MESSAGE_CHANNEL, + "CHANNEL_ID": NEW_MESSAGE_CHANNEL + + (SettingsManager().settings.notificationSound.value == "default" + ? "" + : ("_" + SettingsManager().settings.notificationSound.value)), "CHANNEL_NAME": "New Messages", "notificationId": Random().nextInt(9998) + 1, "summaryId": summaryId, @@ -208,7 +263,8 @@ class NotificationManager { "messageText": messageText, "messageDate": messageDate.millisecondsSinceEpoch, "messageIsFromMe": messageIsFromMe, - "visibility": visibility + "visibility": visibility, + "sound": SettingsManager().settings.notificationSound.value, }); } diff --git a/lib/managers/outgoing_queue.dart b/lib/managers/outgoing_queue.dart index acc897b07..3ca8381a8 100644 --- a/lib/managers/outgoing_queue.dart +++ b/lib/managers/outgoing_queue.dart @@ -1,8 +1,8 @@ import 'package:bluebubbles/action_handler.dart'; import 'package:bluebubbles/helpers/attachment_sender.dart'; +import 'package:bluebubbles/helpers/logger.dart'; import 'package:bluebubbles/managers/queue_manager.dart'; import 'package:bluebubbles/socket_manager.dart'; -import 'package:flutter/material.dart'; class OutgoingQueue extends QueueManager { factory OutgoingQueue() { @@ -26,7 +26,9 @@ class OutgoingQueue extends QueueManager { { AttachmentSender sender = item.item; await sender.send(); - await SocketManager().attachmentSenderCompleter.firstWhere((element) => element == (item.item as AttachmentSender).guid, orElse: () => ""); + await SocketManager() + .attachmentSenderCompleter + .firstWhere((element) => element == (item.item as AttachmentSender).guid, orElse: () => ""); break; } case "send-reaction": @@ -37,7 +39,7 @@ class OutgoingQueue extends QueueManager { } default: { - debugPrint("Unhandled queue event: ${item.event}"); + Logger.warn("Unhandled queue event: ${item.event}"); } } } diff --git a/lib/managers/queue_manager.dart b/lib/managers/queue_manager.dart index ab43ce352..8741be7fb 100644 --- a/lib/managers/queue_manager.dart +++ b/lib/managers/queue_manager.dart @@ -1,5 +1,4 @@ -import 'package:bluebubbles/managers/method_channel_interface.dart'; -import 'package:flutter/material.dart'; +import 'package:bluebubbles/helpers/logger.dart'; class QueueItem { String event; @@ -39,8 +38,8 @@ abstract class QueueManager { await handleQueueItem(queued); await afterProcessing(queued, {}); } catch (ex, stacktrace) { - debugPrint("Failed to handle queued item! " + ex.toString()); - debugPrint(stacktrace.toString()); + Logger.error("Failed to handle queued item! " + ex.toString()); + Logger.error(stacktrace.toString()); } // Process the next item diff --git a/lib/managers/settings_manager.dart b/lib/managers/settings_manager.dart index 456845826..9a8ad3209 100644 --- a/lib/managers/settings_manager.dart +++ b/lib/managers/settings_manager.dart @@ -67,7 +67,7 @@ class SettingsManager { /// Setting to null will prevent the theme from being set and will be set to null in the background isolate Future getSavedSettings({bool headless = false, BuildContext? context}) async { await DBProvider.setupConfigRows(); - settings = await Settings.getSettings(); + settings = Settings.getSettings(); fcmData = await FCMData.getFCM(); // await DBProvider.setupDefaultPresetThemes(await DBProvider.db.database); @@ -105,7 +105,7 @@ class SettingsManager { Future saveSettings(Settings newSettings) async { // Set the new settings as the current settings in the manager settings = newSettings; - await settings.save(); + settings.save(); try { // Set the [displayMode] to that saved in settings await FlutterDisplayMode.setPreferredMode(await settings.getDisplayMode()); diff --git a/lib/repository/database.dart b/lib/repository/database.dart index a2d0200fe..d2f885c39 100644 --- a/lib/repository/database.dart +++ b/lib/repository/database.dart @@ -1,13 +1,14 @@ import 'dart:async'; import 'dart:io'; +import 'package:bluebubbles/helpers/logger.dart'; import 'package:bluebubbles/helpers/themes.dart'; import 'package:bluebubbles/repository/models/attachment.dart'; import 'package:bluebubbles/repository/models/chat.dart'; import 'package:bluebubbles/repository/models/handle.dart'; import 'package:bluebubbles/repository/models/message.dart'; +import 'package:bluebubbles/repository/models/settings.dart'; import 'package:bluebubbles/repository/models/theme_object.dart'; -import 'package:flutter/foundation.dart'; import 'package:path/path.dart'; import 'package:path_provider/path_provider.dart'; import 'package:sqflite/sqflite.dart'; @@ -41,7 +42,7 @@ class DBProvider { static Database? _database; static String _path = ""; - static int currentVersion = 11; + static int currentVersion = 14; /// Contains list of functions to invoke when going from a previous to the current database verison /// The previous version is always [key - 1], for example for key 2, it will be the upgrade scheme from version 1 to version 2 @@ -49,47 +50,37 @@ class DBProvider { new DBUpgradeItem( addedInVersion: 2, upgrade: (Database db) { - db.execute( - "ALTER TABLE message ADD COLUMN hasDdResults INTEGER DEFAULT 0;"); + db.execute("ALTER TABLE message ADD COLUMN hasDdResults INTEGER DEFAULT 0;"); }), new DBUpgradeItem( addedInVersion: 3, upgrade: (Database db) { - db.execute( - "ALTER TABLE message ADD COLUMN balloonBundleId TEXT DEFAULT NULL;"); - db.execute( - "ALTER TABLE chat ADD COLUMN isFiltered INTEGER DEFAULT 0;"); + db.execute("ALTER TABLE message ADD COLUMN balloonBundleId TEXT DEFAULT NULL;"); + db.execute("ALTER TABLE chat ADD COLUMN isFiltered INTEGER DEFAULT 0;"); }), new DBUpgradeItem( addedInVersion: 4, upgrade: (Database db) { - db.execute( - "ALTER TABLE message ADD COLUMN dateDeleted INTEGER DEFAULT NULL;"); + db.execute("ALTER TABLE message ADD COLUMN dateDeleted INTEGER DEFAULT NULL;"); db.execute("ALTER TABLE chat ADD COLUMN isPinned INTEGER DEFAULT 0;"); }), new DBUpgradeItem( addedInVersion: 5, upgrade: (Database db) { - db.execute( - "ALTER TABLE handle ADD COLUMN originalROWID INTEGER DEFAULT NULL;"); - db.execute( - "ALTER TABLE chat ADD COLUMN originalROWID INTEGER DEFAULT NULL;"); - db.execute( - "ALTER TABLE attachment ADD COLUMN originalROWID INTEGER DEFAULT NULL;"); - db.execute( - "ALTER TABLE message ADD COLUMN otherHandle INTEGER DEFAULT NULL;"); + db.execute("ALTER TABLE handle ADD COLUMN originalROWID INTEGER DEFAULT NULL;"); + db.execute("ALTER TABLE chat ADD COLUMN originalROWID INTEGER DEFAULT NULL;"); + db.execute("ALTER TABLE attachment ADD COLUMN originalROWID INTEGER DEFAULT NULL;"); + db.execute("ALTER TABLE message ADD COLUMN otherHandle INTEGER DEFAULT NULL;"); }), new DBUpgradeItem( addedInVersion: 6, upgrade: (Database db) { - db.execute( - "ALTER TABLE attachment ADD COLUMN metadata TEXT DEFAULT NULL;"); + db.execute("ALTER TABLE attachment ADD COLUMN metadata TEXT DEFAULT NULL;"); }), new DBUpgradeItem( addedInVersion: 7, upgrade: (Database db) { - db.execute( - "ALTER TABLE message ADD COLUMN metadata TEXT DEFAULT NULL;"); + db.execute("ALTER TABLE message ADD COLUMN metadata TEXT DEFAULT NULL;"); }), new DBUpgradeItem( addedInVersion: 8, @@ -104,14 +95,33 @@ class DBProvider { new DBUpgradeItem( addedInVersion: 10, upgrade: (Database db) { - db.execute( - "ALTER TABLE chat ADD COLUMN customAvatarPath TEXT DEFAULT NULL;"); + db.execute("ALTER TABLE chat ADD COLUMN customAvatarPath TEXT DEFAULT NULL;"); }), new DBUpgradeItem( addedInVersion: 11, upgrade: (Database db) { - db.execute( - "ALTER TABLE chat ADD COLUMN pinIndex INTEGER DEFAULT NULL;"); + db.execute("ALTER TABLE chat ADD COLUMN pinIndex INTEGER DEFAULT NULL;"); + }), + new DBUpgradeItem( + addedInVersion: 12, + upgrade: (Database db) async { + db.execute("ALTER TABLE chat ADD COLUMN muteType TEXT DEFAULT NULL;"); + db.execute("ALTER TABLE chat ADD COLUMN muteArgs TEXT DEFAULT NULL;"); + await db.update("chat", {'muteType': 'mute'}, where: "isMuted = ?", whereArgs: [1]); + }), + new DBUpgradeItem( + addedInVersion: 13, + upgrade: (Database db) { + db.execute("ALTER TABLE themes ADD COLUMN gradientBg INTEGER DEFAULT 0;"); + }), + new DBUpgradeItem( + addedInVersion: 14, + upgrade: (Database db) async { + db.execute("ALTER TABLE themes ADD COLUMN previousLightTheme INTEGER DEFAULT 0;"); + db.execute("ALTER TABLE themes ADD COLUMN previousDarkTheme INTEGER DEFAULT 0;"); + Settings s = await Settings.getSettingsOld(db); + s.save(); + db.execute("DELETE FROM config"); }), ]; @@ -128,35 +138,32 @@ class DBProvider { Future initDB() async { Directory documentsDirectory = await getApplicationDocumentsDirectory(); _path = join(documentsDirectory.path, "chat.db"); - return await openDatabase(_path, - version: currentVersion, - onUpgrade: _onUpgrade, onOpen: (Database db) async { - debugPrint("Database Opened"); + return await openDatabase(_path, version: currentVersion, onUpgrade: _onUpgrade, onOpen: (Database db) async { + Logger.info("Database Opened"); _database = db; await checkTableExistenceAndCreate(db); _database = null; }, onCreate: (Database db, int version) async { - debugPrint("creating database"); + Logger.info("creating database"); _database = db; await this.buildDatabase(db); _database = null; }); } - void _onUpgrade(Database db, int oldVersion, int newVersion) async { + Future _onUpgrade(Database db, int oldVersion, int newVersion) async { // Run each upgrade scheme for every difference in version. // If the user is on version 1 and they need to upgrade to version 3, // then we will run every single scheme from 1 -> 2 and 2 -> 3 for (DBUpgradeItem item in upgradeSchemes) { if (oldVersion < item.addedInVersion) { - debugPrint( - "Upgrading DB from version $oldVersion to version $newVersion"); + Logger.info("Upgrading DB from version $oldVersion to version $newVersion"); try { await item.upgrade(db); } catch (ex) { - debugPrint("Failed to perform DB upgrade: ${ex.toString()}"); + Logger.error("Failed to perform DB upgrade: ${ex.toString()}"); } } } @@ -224,8 +231,8 @@ class DBProvider { await createScheduledTable(db); break; } - debugPrint( - "creating missing table " + tableName.toString().split(".").last); + + Logger.info("Creating missing table " + tableName.toString().split(".").last); } } } @@ -269,7 +276,8 @@ class DBProvider { "isArchived INTEGER DEFAULT 0," "isFiltered INTEGER DEFAULT 0," "isPinned INTEGER DEFAULT 0," - "isMuted INTEGER DEFAULT 0," + "muteType TEXT DEFAULT NULL," + "muteArgs TEXT DEFAULT NULL," "hasUnreadMessage INTEGER DEFAULT 0," "latestMessageDate INTEGER DEFAULT 0," "latestMessageText TEXT," @@ -375,12 +383,10 @@ class DBProvider { } static Future createIndexes(Database db) async { - await db - .execute("CREATE UNIQUE INDEX idx_handle_address ON handle (address);"); + await db.execute("CREATE UNIQUE INDEX idx_handle_address ON handle (address);"); await db.execute("CREATE UNIQUE INDEX idx_message_guid ON message (guid);"); await db.execute("CREATE UNIQUE INDEX idx_chat_guid ON chat (guid);"); - await db.execute( - "CREATE UNIQUE INDEX idx_attachment_guid ON attachment (guid);"); + await db.execute("CREATE UNIQUE INDEX idx_attachment_guid ON attachment (guid);"); } static Future createConfigTable(Database db) async { @@ -417,7 +423,10 @@ class DBProvider { "ROWID INTEGER PRIMARY KEY AUTOINCREMENT," "name TEXT UNIQUE," "selectedLightTheme INTEGER DEFAULT 0," - "selectedDarkTheme INTEGER DEFAULT 0" + "selectedDarkTheme INTEGER DEFAULT 0," + "gradientBg INTEGER DEFAULT 0," + "previousLightTheme INTEGER DEFAULT 0," + "previousDarkTheme INTEGER DEFAULT 0" ");"); } diff --git a/lib/repository/models/chat.dart b/lib/repository/models/chat.dart index af57390d3..0e69bf6cd 100644 --- a/lib/repository/models/chat.dart +++ b/lib/repository/models/chat.dart @@ -4,8 +4,10 @@ import 'dart:io'; import 'package:bluebubbles/action_handler.dart'; import 'package:bluebubbles/blocs/chat_bloc.dart'; +import 'package:bluebubbles/helpers/logger.dart'; import 'package:bluebubbles/helpers/message_helper.dart'; import 'package:bluebubbles/helpers/metadata_helper.dart'; +import 'package:bluebubbles/helpers/reaction.dart'; import 'package:bluebubbles/managers/contact_manager.dart'; import 'package:bluebubbles/managers/current_chat.dart'; import 'package:bluebubbles/managers/event_dispatcher.dart'; @@ -15,7 +17,6 @@ import 'package:bluebubbles/socket_manager.dart'; import 'package:bluebubbles/helpers/darty.dart'; import 'package:get/get.dart'; import 'package:faker/faker.dart'; -import 'package:flutter/widgets.dart'; import 'package:metadata_fetch/metadata_fetch.dart'; import 'package:sqflite/sqflite.dart'; @@ -95,7 +96,8 @@ class Chat { String? chatIdentifier; bool? isArchived; bool? isFiltered; - bool? isMuted; + String? muteType; + String? muteArgs; bool? isPinned; bool? hasUnreadMessage; DateTime? latestMessageDate; @@ -117,7 +119,8 @@ class Chat { this.isArchived, this.isFiltered, this.isPinned, - this.isMuted, + this.muteType, + this.muteArgs, this.hasUnreadMessage, this.displayName, String? customAvatar, @@ -154,11 +157,8 @@ class Chat { ? json['isFiltered'] : ((json['isFiltered'] == 1) ? true : false) : false, - isMuted: json.containsKey("isMuted") - ? (json["isMuted"] is bool) - ? json['isMuted'] - : ((json['isMuted'] == 1) ? true : false) - : false, + muteType: json["muteType"], + muteArgs: json["muteArgs"], isPinned: json.containsKey("isPinned") ? (json["isPinned"] is bool) ? json['isPinned'] @@ -199,7 +199,8 @@ class Chat { if (existing != null) { this.id = existing.id; if (!updateLocalVals) { - this.isMuted = existing.isMuted; + this.muteType = existing.muteType; + this.muteArgs = existing.muteArgs; this.isPinned = existing.isPinned; this.isArchived = existing.isArchived; this.hasUnreadMessage = existing.hasUnreadMessage; @@ -246,6 +247,47 @@ class Chat { return buildDate(this.latestMessageDate); } + Future shouldMuteNotification(Message? message) async { + if (SettingsManager().settings.filterUnknownSenders.value + && this.participants.length == 1 + && ContactManager().handleToContact[this.participants[0].address] == null) { + return true; + } else if (SettingsManager().settings.globalTextDetection.value.isNotEmpty) { + List text = SettingsManager().settings.globalTextDetection.value.split(","); + for (String s in text) { + if (message?.text?.toLowerCase().contains(s.toLowerCase()) ?? false) { + return false; + } + } + return true; + } else if (muteType == "mute") { + return true; + } else if (muteType == "mute_individuals") { + List individuals = muteArgs!.split(","); + return individuals.contains(message?.handle?.address ?? ""); + } else if (muteType == "temporary_mute") { + DateTime time = DateTime.parse(muteArgs!); + bool shouldMute = DateTime.now().toLocal().difference(time).inSeconds.isNegative; + if (!shouldMute) { + await this.toggleMute(false); + this.muteType = null; + this.muteArgs = null; + await this.update(); + } + return shouldMute; + } else if (muteType == "text_detection") { + List text = muteArgs!.split(","); + for (String s in text) { + if (message?.text?.toLowerCase().contains(s.toLowerCase()) ?? false) { + return false; + } + } + return true; + } + return !SettingsManager().settings.notifyReactions.value && + ReactionTypes.toList().contains(message?.associatedMessageType ?? ""); + } + Future update() async { final Database db = await DBProvider.db.database; @@ -271,6 +313,8 @@ class Chat { params["customAvatarPath"] = this.customAvatarPath.value; params["pinIndex"] = this.pinIndex.value; + params["muteType"] = this.muteType; + params["muteArgs"] = this.muteArgs; // If it already exists, update it if (this.id != null) { @@ -334,8 +378,8 @@ class Chat { } catch (ex, stacktrace) { newMessage = await Message.findOne({"guid": message.guid}); if (newMessage == null) { - debugPrint(ex.toString()); - debugPrint(stacktrace.toString()); + Logger.error(ex.toString()); + Logger.error(stacktrace.toString()); } } bool isNewer = false; @@ -531,7 +575,7 @@ class Chat { if (_getMessagesRequests.containsKey(req) && !_getMessagesRequests[req]!.isCompleted) _getMessagesRequests[req]!.complete(messages); } catch (ex) { - debugPrint(ex.toString()); + Logger.error(ex.toString()); if (_getMessagesRequests.containsKey(req) && !_getMessagesRequests[req]!.isCompleted) _getMessagesRequests[req]!.completeError(ex); @@ -747,8 +791,9 @@ class Chat { final Database db = await DBProvider.db.database; if (this.id == null) return this; - this.isMuted = isMuted; - await db.update("chat", {"isMuted": isMuted ? 1 : 0}, where: "ROWID = ?", whereArgs: [this.id]); + this.muteType = isMuted ? "mute" : null; + this.muteArgs = null; + await db.update("chat", {"muteType": muteType, "muteArgs": muteArgs}, where: "ROWID = ?", whereArgs: [this.id]); ChatBloc().updateChat(this); return this; @@ -810,7 +855,8 @@ class Chat { " chat.isFiltered as isFiltered," " chat.isPinned as isPinned," " chat.isArchived as isArchived," - " chat.isMuted as isMuted," + " chat.muteType as muteType," + " chat.muteArgs as muteArgs," " chat.hasUnreadMessage as hasUnreadMessage," " chat.latestMessageDate as latestMessageDate," " chat.latestMessageText as latestMessageText," @@ -886,7 +932,8 @@ class Chat { "chatIdentifier": chatIdentifier, "isArchived": isArchived! ? 1 : 0, "isFiltered": isFiltered! ? 1 : 0, - "isMuted": isMuted! ? 1 : 0, + "muteType": muteType, + "muteArgs": muteArgs, "isPinned": isPinned! ? 1 : 0, "displayName": displayName, "participants": participants.map((item) => item.toMap()), diff --git a/lib/repository/models/message.dart b/lib/repository/models/message.dart index fd7c89b1e..01dcab25a 100644 --- a/lib/repository/models/message.dart +++ b/lib/repository/models/message.dart @@ -459,6 +459,14 @@ class Message { return Message.fromMap(res.elementAt(0)); } + static Future lastMessageDate() async { + final Database db = await DBProvider.db.database; + + // Get the last message + var res = await db.query("message", limit: 1, orderBy: "dateCreated DESC"); + return (res.isNotEmpty) ? res.map((c) => Message.fromMap(c)).toList()[0].dateCreated : null; + } + static Future> find([Map filters = const {}]) async { final Database db = await DBProvider.db.database; diff --git a/lib/repository/models/settings.dart b/lib/repository/models/settings.dart index c67d4b4bb..165068946 100644 --- a/lib/repository/models/settings.dart +++ b/lib/repository/models/settings.dart @@ -2,8 +2,8 @@ import 'dart:async'; import 'package:bluebubbles/helpers/constants.dart'; import 'package:bluebubbles/helpers/reaction.dart'; +import 'package:bluebubbles/main.dart'; import 'package:bluebubbles/managers/settings_manager.dart'; -import 'package:bluebubbles/repository/database.dart'; import 'package:bluebubbles/repository/models/config_entry.dart'; import 'package:flutter/material.dart'; import 'package:flutter_displaymode/flutter_displaymode.dart'; @@ -39,6 +39,7 @@ class Settings { final RxBool recipientAsPlaceholder = false.obs; final RxBool hideKeyboardOnScroll = false.obs; final RxBool moveChatCreatorToHeader = false.obs; + final RxBool cameraFAB = false.obs; final RxBool swipeToCloseKeyboard = false.obs; final RxBool swipeToOpenKeyboard = false.obs; final RxBool openKeyboardOnSTB = false.obs; @@ -52,6 +53,11 @@ class Settings { final RxBool use24HrFormat = false.obs; final RxBool alwaysShowAvatars = false.obs; final RxBool notifyOnChatList = false.obs; + final RxBool notifyReactions = true.obs; + final RxString notificationSound = "default".obs; + final RxBool colorsFromMedia = false.obs; + final RxString globalTextDetection = "".obs; + final RxBool filterUnknownSenders = false.obs; // final RxString emojiFontFamily; @@ -90,6 +96,7 @@ class Settings { // Security settings final RxBool shouldSecure = RxBool(false); final Rx securityLevel = Rx(SecurityLevel.locked); + final RxBool incognitoKeyboard = RxBool(false); final Rx skin = Skins.iOS.obs; final Rx theme = ThemeMode.system.obs; @@ -174,6 +181,8 @@ class Settings { settings.swipeToCloseKeyboard.value = entry.value; } else if (entry.name == "moveChatCreatorToHeader") { settings.moveChatCreatorToHeader.value = entry.value; + } else if (entry.name == "cameraFAB") { + settings.cameraFAB.value = entry.value; } else if (entry.name == "openKeyboardOnSTB") { settings.openKeyboardOnSTB.value = entry.value; } else if (entry.name == "swipableConversationTiles") { @@ -246,19 +255,31 @@ class Settings { settings.shouldSecure.value = entry.value; } else if (entry.name == "securityLevel") { settings.securityLevel.value = SecurityLevel.values[entry.value]; + } else if (entry.name == "incognitoKeyboard") { + settings.incognitoKeyboard.value = entry.value; } else if (entry.name == "pinRowsPortrait") { settings.pinRowsPortrait.value = entry.value; } else if (entry.name == "maxAvatarsInGroupWidget") { settings.maxAvatarsInGroupWidget.value = entry.value; } else if (entry.name == "notifyOnChatList") { settings.notifyOnChatList.value = entry.value; + } else if (entry.name == "notifyReactions") { + settings.notifyReactions.value = entry.value; + } else if (entry.name == "notificationSound") { + settings.notificationSound.value = entry.value; + } else if (entry.name == "colorsFromMedia") { + settings.colorsFromMedia.value = entry.value; + } else if (entry.name == "globalTextDetection") { + settings.globalTextDetection.value = entry.value; + } else if (entry.name == "filterUnknownSenders") { + settings.filterUnknownSenders.value = entry.value; } // else if (entry.name == "emojiFontFamily") { // settings.emojiFontFamily = entry.value; // } } - settings.save(updateIfAbsent: false); + settings.save(); return settings; } @@ -275,17 +296,38 @@ class Settings { return mode; } - Future save({bool updateIfAbsent = true}) async { - List entries = this.toEntries(); - for (ConfigEntry entry in entries) { - await entry.save("config", updateIfAbsent: updateIfAbsent); - } + Settings save() { + Map map = this.toMap(includeAll: true); + map.forEach((key, value) { + if (value is bool) { + prefs.setBool(key, value); + } else if (value is String) { + prefs.setString(key, value); + } else if (value is int) { + prefs.setInt(key, value); + } else if (value is double) { + prefs.setDouble(key, value); + } + }); return this; } - static Future getSettings() async { - Database? db = await DBProvider.db.database; + static Settings getSettings() { + Set keys = prefs.getKeys(); + print(keys); + + Map items = {}; + for (String s in keys) { + items[s] = prefs.get(s); + } + if (items.isNotEmpty) { + return Settings.fromMap(items); + } else { + return Settings(); + } + } + static Future getSettingsOld(Database db) async { List> result = await db.query("config"); if (result.isEmpty) return new Settings(); List entries = []; @@ -295,371 +337,8 @@ class Settings { return Settings.fromConfigEntries(entries); } - List toEntries() => [ - ConfigEntry( - name: "serverAddress", - value: this.serverAddress.value, - type: this.serverAddress.runtimeType, - ), - ConfigEntry( - name: "guidAuthKey", - value: this.guidAuthKey.value, - type: this.guidAuthKey.runtimeType, - ), - ConfigEntry( - name: "finishedSetup", - value: this.finishedSetup.value, - type: this.finishedSetup.runtimeType, - ), - ConfigEntry( - name: "chunkSize", - value: this.chunkSize.value, - type: this.chunkSize.runtimeType, - ), - ConfigEntry( - name: "autoOpenKeyboard", - value: this.autoOpenKeyboard.value, - type: this.autoOpenKeyboard.runtimeType, - ), - ConfigEntry( - name: "autoDownload", - value: this.autoDownload.value, - type: this.autoDownload.runtimeType, - ), - ConfigEntry( - name: "onlyWifiDownload", - value: this.onlyWifiDownload.value, - type: this.onlyWifiDownload.runtimeType, - ), - ConfigEntry( - name: "hideTextPreviews", - value: this.hideTextPreviews.value, - type: this.hideTextPreviews.runtimeType, - ), - ConfigEntry( - name: "showIncrementalSync", - value: this.showIncrementalSync.value, - type: this.showIncrementalSync.runtimeType, - ), - ConfigEntry( - name: "lowMemoryMode", - value: this.lowMemoryMode.value, - type: this.lowMemoryMode.runtimeType, - ), - ConfigEntry( - name: "lastIncrementalSync", - value: this.lastIncrementalSync.value, - type: this.lastIncrementalSync.runtimeType, - ), - ConfigEntry( - name: "displayMode", - value: this.refreshRate.value, - type: this.refreshRate.runtimeType, - ), - ConfigEntry( - name: "rainbowBubbles", - value: this.colorfulAvatars.value, - type: this.colorfulAvatars.runtimeType, - ), - ConfigEntry( - name: "colorfulBubbles", - value: this.colorfulBubbles.value, - type: this.colorfulBubbles.runtimeType, - ), - ConfigEntry( - name: "hideDividers", - value: this.hideDividers.value, - type: this.hideDividers.runtimeType, - ), - ConfigEntry( - name: "theme", - value: this.theme.value.index, - type: this.theme.value.index.runtimeType, - ), - ConfigEntry( - name: "skin", - value: this.skin.value.index, - type: this.skin.value.index.runtimeType, - ), - ConfigEntry( - name: "fullscreenViewerSwipeDir", - value: this.fullscreenViewerSwipeDir.value.index, - type: this.fullscreenViewerSwipeDir.value.index.runtimeType, - ), - ConfigEntry( - name: "scrollVelocity", - value: this.scrollVelocity.value, - type: this.scrollVelocity.runtimeType, - ), - ConfigEntry( - name: "sendWithReturn", - value: this.sendWithReturn.value, - type: this.sendWithReturn.runtimeType, - ), - ConfigEntry( - name: "doubleTapForDetails", - value: this.doubleTapForDetails.value, - type: this.doubleTapForDetails.runtimeType, - ), - ConfigEntry( - name: "denseChatTiles", - value: this.denseChatTiles.value, - type: this.denseChatTiles.runtimeType, - ), - ConfigEntry( - name: "smartReply", - value: this.smartReply.value, - type: this.smartReply.runtimeType, - ), - ConfigEntry( - name: "hideKeyboardOnScroll", - value: this.hideKeyboardOnScroll.value, - type: this.hideKeyboardOnScroll.runtimeType, - ), - ConfigEntry( - name: "reducedForehead", - value: this.reducedForehead.value, - type: this.reducedForehead.runtimeType, - ), - ConfigEntry( - name: "preCachePreviewImages", - value: this.preCachePreviewImages.value, - type: this.preCachePreviewImages.runtimeType, - ), - ConfigEntry( - name: "showConnectionIndicator", - value: this.showConnectionIndicator.value, - type: this.showConnectionIndicator.runtimeType, - ), - ConfigEntry( - name: "sendDelay", - value: this.sendDelay.value, - type: this.sendDelay.runtimeType, - ), - ConfigEntry( - name: "recipientAsPlaceholder", - value: this.recipientAsPlaceholder.value, - type: this.recipientAsPlaceholder.runtimeType, - ), - ConfigEntry( - name: "moveChatCreatorToHeader", - value: this.moveChatCreatorToHeader.value, - type: this.moveChatCreatorToHeader.runtimeType, - ), - ConfigEntry( - name: "swipeToCloseKeyboard", - value: this.swipeToCloseKeyboard.value, - type: this.swipeToCloseKeyboard.runtimeType, - ), - ConfigEntry( - name: "swipeToOpenKeyboard", - value: this.swipeToOpenKeyboard.value, - type: this.swipeToOpenKeyboard.runtimeType, - ), - ConfigEntry( - name: "openKeyboardOnSTB", - value: this.openKeyboardOnSTB.value, - type: this.openKeyboardOnSTB.runtimeType, - ), - ConfigEntry( - name: "swipableConversationTiles", - value: this.swipableConversationTiles.value, - type: this.swipableConversationTiles.runtimeType, - ), - ConfigEntry( - name: "enablePrivateAPI", - value: this.enablePrivateAPI.value, - type: this.enablePrivateAPI.runtimeType, - ), - ConfigEntry( - name: "privateSendTypingIndicators", - value: this.privateSendTypingIndicators.value, - type: this.privateSendTypingIndicators.runtimeType, - ), - ConfigEntry( - name: "colorblindMode", - value: this.colorblindMode.value, - type: this.colorblindMode.runtimeType, - ), - ConfigEntry( - name: "privateMarkChatAsRead", - value: this.privateMarkChatAsRead.value, - type: this.privateMarkChatAsRead.runtimeType, - ), - ConfigEntry( - name: "privateManualMarkAsRead", - value: this.privateManualMarkAsRead.value, - type: this.privateManualMarkAsRead.runtimeType, - ), - ConfigEntry( - name: "showSyncIndicator", value: this.showSyncIndicator.value, type: this.showSyncIndicator.runtimeType), - ConfigEntry( - name: "showDeliveryTimestamps", - value: this.showDeliveryTimestamps.value, - type: this.showDeliveryTimestamps.runtimeType), - ConfigEntry( - name: "showSyncIndicator", - value: this.showSyncIndicator.value, - type: this.showSyncIndicator.runtimeType, - ), - ConfigEntry( - name: "redactedMode", - value: this.redactedMode.value, - type: this.redactedMode.runtimeType, - ), - ConfigEntry( - name: "hideMessageContent", - value: this.hideMessageContent.value, - type: this.hideMessageContent.runtimeType, - ), - ConfigEntry( - name: "hideReactions", - value: this.hideReactions.value, - type: this.hideReactions.runtimeType, - ), - ConfigEntry( - name: "hideAttachments", - value: this.hideAttachments.value, - type: this.hideAttachments.runtimeType, - ), - ConfigEntry( - name: "hideAttachmentTypes", - value: this.hideAttachmentTypes.value, - type: this.hideAttachmentTypes.runtimeType, - ), - ConfigEntry( - name: "hideContactPhotos", - value: this.hideContactPhotos.value, - type: this.hideContactPhotos.runtimeType, - ), - ConfigEntry( - name: "hideContactInfo", - value: this.hideContactInfo.value, - type: this.hideContactInfo.runtimeType, - ), - ConfigEntry( - name: "removeLetterAvatars", - value: this.removeLetterAvatars.value, - type: this.removeLetterAvatars.runtimeType, - ), - ConfigEntry( - name: "generateFakeContactNames", - value: this.generateFakeContactNames.value, - type: this.generateFakeContactNames.runtimeType, - ), - ConfigEntry( - name: "generateFakeMessageContent", - value: this.generateFakeMessageContent.value, - type: this.generateFakeMessageContent.runtimeType, - ), - ConfigEntry( - name: "previewCompressionQuality", - value: this.previewCompressionQuality.value, - type: this.previewCompressionQuality.runtimeType, - ), - ConfigEntry( - name: "filteredChatList", - value: this.filteredChatList.value, - type: this.filteredChatList.runtimeType, - ), - ConfigEntry( - name: "startVideosMuted", - value: this.startVideosMuted.value, - type: this.startVideosMuted.runtimeType, - ), - ConfigEntry( - name: "startVideosMutedFullscreen", - value: this.startVideosMutedFullscreen.value, - type: this.startVideosMutedFullscreen.runtimeType, - ), - ConfigEntry( - name: "use24HrFormat", - value: this.use24HrFormat.value, - type: this.use24HrFormat.runtimeType, - ), - ConfigEntry( - name: "enableQuickTapback", - value: this.enableQuickTapback.value, - type: this.enableQuickTapback.runtimeType, - ), - ConfigEntry( - name: "quickTapbackType", - value: this.quickTapbackType.value, - type: this.quickTapbackType.runtimeType, - ), - ConfigEntry( - name: "alwaysShowAvatars", - value: this.alwaysShowAvatars.value, - type: this.alwaysShowAvatars.runtimeType, - ), - ConfigEntry( - name: "iosShowPin", - value: this.iosShowPin.value, - type: this.iosShowPin.runtimeType, - ), - ConfigEntry( - name: "iosShowAlert", - value: this.iosShowAlert.value, - type: this.iosShowAlert.runtimeType, - ), - ConfigEntry( - name: "iosShowDelete", - value: this.iosShowDelete.value, - type: this.iosShowDelete.runtimeType, - ), - ConfigEntry( - name: "iosShowMarkRead", - value: this.iosShowMarkRead.value, - type: this.iosShowMarkRead.runtimeType, - ), - ConfigEntry( - name: "iosShowArchive", - value: this.iosShowArchive.value, - type: this.iosShowArchive.runtimeType, - ), - ConfigEntry( - name: "materialRightAction", - value: this.materialRightAction.value.index, - type: this.materialRightAction.value.index.runtimeType, - ), - ConfigEntry( - name: "materialLeftAction", - value: this.materialLeftAction.value.index, - type: this.materialLeftAction.value.index.runtimeType, - ), - ConfigEntry( - name: "shouldSecure", - value: this.shouldSecure.value, - type: this.shouldSecure.runtimeType, - ), - ConfigEntry( - name: "securityLevel", - value: this.securityLevel.value.index, - type: this.securityLevel.value.index.runtimeType, - ), - ConfigEntry( - name: "pinRowsPortrait", - value: this.pinRowsPortrait.value, - type: this.pinRowsPortrait.value.runtimeType, - ), - ConfigEntry( - name: "maxAvatarsInGroupWidget", - value: this.maxAvatarsInGroupWidget.value, - type: this.maxAvatarsInGroupWidget.value.runtimeType, - ), - ConfigEntry( - name: "notifyOnChatList", - value: this.notifyOnChatList.value, - type: this.notifyOnChatList.runtimeType, - ) - // ConfigEntry( - // name: "emojiFontFamily", - // value: this.emojiFontFamily, - // type: this.emojiFontFamily.runtimeType), - ]; - - Map toMap() { - return { + Map toMap({bool includeAll = false}) { + Map map = { 'chunkSize': this.chunkSize.value, 'autoDownload': this.autoDownload.value, 'onlyWifiDownload': this.onlyWifiDownload.value, @@ -685,6 +364,7 @@ class Settings { 'recipientAsPlaceholder': this.recipientAsPlaceholder.value, 'hideKeyboardOnScroll': this.hideKeyboardOnScroll.value, 'moveChatCreatorToHeader': this.moveChatCreatorToHeader.value, + 'cameraFAB': this.cameraFAB.value, 'swipeToCloseKeyboard': this.swipeToCloseKeyboard.value, 'swipeToOpenKeyboard': this.swipeToOpenKeyboard.value, 'openKeyboardOnSTB': this.openKeyboardOnSTB.value, @@ -698,6 +378,10 @@ class Settings { 'use24HrFormat': this.use24HrFormat.value, 'alwaysShowAvatars': this.alwaysShowAvatars.value, 'notifyOnChatList': this.notifyOnChatList.value, + 'notifyReactions': this.notifyReactions.value, + 'notificationSound': this.notificationSound.value, + 'globalTextDetection': this.globalTextDetection.value, + 'filterUnknownSenders': this.filterUnknownSenders.value, 'enablePrivateAPI': this.enablePrivateAPI.value, 'privateSendTypingIndicators': this.privateSendTypingIndicators.value, 'privateMarkChatAsRead': this.privateMarkChatAsRead.value, @@ -724,6 +408,7 @@ class Settings { 'materialLeftAction': this.materialLeftAction.value.index, 'shouldSecure': this.shouldSecure.value, 'securityLevel': this.securityLevel.value.index, + 'incognitoKeyboard': this.incognitoKeyboard.value, 'skin': this.skin.value.index, 'theme': this.theme.value.index, 'fullscreenViewerSwipeDir': this.fullscreenViewerSwipeDir.value.index, @@ -733,81 +418,183 @@ class Settings { 'pinColumnsLandscape': this.pinColumnsLandscape.value, 'maxAvatarsInGroupWidget': this.maxAvatarsInGroupWidget.value, }; + if (includeAll) { + map.addAll({ + 'guidAuthKey': this.guidAuthKey.value, + 'serverAddress': this.serverAddress.value, + 'finishedSetup': this.finishedSetup.value, + 'colorsFromMedia': this.colorsFromMedia.value, + }); + } + return map; } static void updateFromMap(Map map) { - SettingsManager().settings.chunkSize.value = map['chunkSize']; - SettingsManager().settings.autoDownload.value = map['autoDownload']; - SettingsManager().settings.onlyWifiDownload.value = map['onlyWifiDownload']; - SettingsManager().settings.autoOpenKeyboard.value = map['autoOpenKeyboard']; - SettingsManager().settings.hideTextPreviews.value = map['hideTextPreviews']; - SettingsManager().settings.showIncrementalSync.value = map['showIncrementalSync']; - SettingsManager().settings.lowMemoryMode.value = map['lowMemoryMode']; - SettingsManager().settings.lastIncrementalSync.value = map['lastIncrementalSync']; - SettingsManager().settings.refreshRate.value = map['refreshRate']; - SettingsManager().settings.colorfulAvatars.value = map['colorfulAvatars']; - SettingsManager().settings.colorfulBubbles.value = map['colorfulBubbles']; - SettingsManager().settings.hideDividers.value = map['hideDividers']; - SettingsManager().settings.scrollVelocity.value = map['scrollVelocity']; - SettingsManager().settings.sendWithReturn.value = map['sendWithReturn']; - SettingsManager().settings.doubleTapForDetails.value = map['doubleTapForDetails']; - SettingsManager().settings.denseChatTiles.value = map['denseChatTiles']; - SettingsManager().settings.smartReply.value = map['smartReply']; - SettingsManager().settings.reducedForehead.value = map['reducedForehead']; - SettingsManager().settings.preCachePreviewImages.value = map['preCachePreviewImages']; - SettingsManager().settings.showConnectionIndicator.value = map['showConnectionIndicator']; - SettingsManager().settings.showSyncIndicator.value = map['showSyncIndicator']; - SettingsManager().settings.sendDelay.value = map['sendDelay']; - SettingsManager().settings.recipientAsPlaceholder.value = map['recipientAsPlaceholder']; - SettingsManager().settings.hideKeyboardOnScroll.value = map['hideKeyboardOnScroll']; - SettingsManager().settings.moveChatCreatorToHeader.value = map['moveChatCreatorToHeader']; - SettingsManager().settings.swipeToCloseKeyboard.value = map['swipeToCloseKeyboard']; - SettingsManager().settings.swipeToOpenKeyboard.value = map['swipeToOpenKeyboard']; - SettingsManager().settings.openKeyboardOnSTB.value = map['openKeyboardOnSTB']; - SettingsManager().settings.swipableConversationTiles.value = map['swipableConversationTiles']; - SettingsManager().settings.colorblindMode.value = map['colorblindMode']; - SettingsManager().settings.showDeliveryTimestamps.value = map['showDeliveryTimestamps']; - SettingsManager().settings.previewCompressionQuality.value = map['previewCompressionQuality']; - SettingsManager().settings.filteredChatList.value = map['filteredChatList']; - SettingsManager().settings.startVideosMuted.value = map['startVideosMuted']; - SettingsManager().settings.startVideosMutedFullscreen.value = map['startVideosMutedFullscreen']; - SettingsManager().settings.use24HrFormat.value = map['use24HrFormat']; - SettingsManager().settings.alwaysShowAvatars.value = map['alwaysShowAvatars']; - SettingsManager().settings.notifyOnChatList.value = map['notifyOnChatList']; - SettingsManager().settings.enablePrivateAPI.value = map['enablePrivateAPI']; - SettingsManager().settings.privateSendTypingIndicators.value = map['privateSendTypingIndicators']; - SettingsManager().settings.privateMarkChatAsRead.value = map['privateMarkChatAsRead']; - SettingsManager().settings.privateManualMarkAsRead.value = map['privateManualMarkAsRead']; - SettingsManager().settings.redactedMode.value = map['redactedMode']; - SettingsManager().settings.hideMessageContent.value = map['hideMessageContent']; - SettingsManager().settings.hideReactions.value = map['hideReactions']; - SettingsManager().settings.hideAttachments.value = map['hideAttachments']; - SettingsManager().settings.hideEmojis.value = map['hideEmojis']; - SettingsManager().settings.hideAttachmentTypes.value = map['hideAttachmentTypes']; - SettingsManager().settings.hideContactPhotos.value = map['hideContactPhotos']; - SettingsManager().settings.hideContactInfo.value = map['hideContactInfo']; - SettingsManager().settings.removeLetterAvatars.value = map['removeLetterAvatars']; - SettingsManager().settings.generateFakeContactNames.value = map['generateFakeContactNames']; - SettingsManager().settings.generateFakeMessageContent.value = map['generateFakeMessageContent']; - SettingsManager().settings.enableQuickTapback.value = map['enableQuickTapback']; - SettingsManager().settings.quickTapbackType.value = map['quickTapbackType']; - SettingsManager().settings.iosShowPin.value = map['iosShowPin']; - SettingsManager().settings.iosShowAlert.value = map['iosShowAlert']; - SettingsManager().settings.iosShowDelete.value = map['iosShowDelete']; - SettingsManager().settings.iosShowMarkRead.value = map['iosShowMarkRead']; - SettingsManager().settings.iosShowArchive.value = map['iosShowArchive']; - SettingsManager().settings.materialRightAction.value = MaterialSwipeAction.values[map['materialRightAction']]; - SettingsManager().settings.materialLeftAction.value = MaterialSwipeAction.values[map['materialLeftAction']]; - SettingsManager().settings.shouldSecure.value = map['shouldSecure']; - SettingsManager().settings.securityLevel.value = SecurityLevel.values[map['securityLevel']]; - SettingsManager().settings.skin.value = Skins.values[map['skin']]; - SettingsManager().settings.theme.value = ThemeMode.values[map['theme']]; - SettingsManager().settings.fullscreenViewerSwipeDir.value = SwipeDirection.values[map['fullscreenViewerSwipeDir']]; - SettingsManager().settings.pinRowsPortrait.value = map['pinRowsPortrait']; - SettingsManager().settings.pinColumnsPortrait.value = map['pinColumnsPortrait']; - SettingsManager().settings.pinRowsLandscape.value = map['pinRowsLandscape']; - SettingsManager().settings.pinColumnsLandscape.value = map['pinColumnsLandscape']; - SettingsManager().settings.maxAvatarsInGroupWidget.value = map['maxAvatarsInGroupWidget']; + SettingsManager().settings.chunkSize.value = map['chunkSize'] ?? 500; + SettingsManager().settings.autoDownload.value = map['autoDownload'] ?? true; + SettingsManager().settings.onlyWifiDownload.value = map['onlyWifiDownload'] ?? false; + SettingsManager().settings.autoOpenKeyboard.value = map['autoOpenKeyboard'] ?? true; + SettingsManager().settings.hideTextPreviews.value = map['hideTextPreviews'] ?? false; + SettingsManager().settings.showIncrementalSync.value = map['showIncrementalSync'] ?? false; + SettingsManager().settings.lowMemoryMode.value = map['lowMemoryMode'] ?? false; + SettingsManager().settings.lastIncrementalSync.value = map['lastIncrementalSync'] ?? 0; + SettingsManager().settings.refreshRate.value = map['refreshRate'] ?? 0; + SettingsManager().settings.colorfulAvatars.value = map['colorfulAvatars'] ?? false; + SettingsManager().settings.colorfulBubbles.value = map['colorfulBubbles'] ?? false; + SettingsManager().settings.hideDividers.value = map['hideDividers'] ?? false; + SettingsManager().settings.scrollVelocity.value = map['scrollVelocity'] ?? 1; + SettingsManager().settings.sendWithReturn.value = map['sendWithReturn'] ?? false; + SettingsManager().settings.doubleTapForDetails.value = map['doubleTapForDetails'] ?? false; + SettingsManager().settings.denseChatTiles.value = map['denseChatTiles'] ?? false; + SettingsManager().settings.smartReply.value = map['smartReply'] ?? false; + SettingsManager().settings.reducedForehead.value = map['reducedForehead'] ?? false; + SettingsManager().settings.preCachePreviewImages.value = map['preCachePreviewImages'] ?? true; + SettingsManager().settings.showConnectionIndicator.value = map['showConnectionIndicator'] ?? false; + SettingsManager().settings.showSyncIndicator.value = map['showSyncIndicator'] ?? true; + SettingsManager().settings.sendDelay.value = map['sendDelay'] ?? 0; + SettingsManager().settings.recipientAsPlaceholder.value = map['recipientAsPlaceholder'] ?? false; + SettingsManager().settings.hideKeyboardOnScroll.value = map['hideKeyboardOnScroll'] ?? false; + SettingsManager().settings.moveChatCreatorToHeader.value = map['moveChatCreatorToHeader'] ?? false; + SettingsManager().settings.cameraFAB.value = map['cameraFAB'] ?? false; + SettingsManager().settings.swipeToCloseKeyboard.value = map['swipeToCloseKeyboard'] ?? false; + SettingsManager().settings.swipeToOpenKeyboard.value = map['swipeToOpenKeyboard'] ?? false; + SettingsManager().settings.openKeyboardOnSTB.value = map['openKeyboardOnSTB'] ?? false; + SettingsManager().settings.swipableConversationTiles.value = map['swipableConversationTiles'] ?? false; + SettingsManager().settings.colorblindMode.value = map['colorblindMode'] ?? false; + SettingsManager().settings.showDeliveryTimestamps.value = map['showDeliveryTimestamps'] ?? false; + SettingsManager().settings.previewCompressionQuality.value = map['previewCompressionQuality'] ?? 50; + SettingsManager().settings.filteredChatList.value = map['filteredChatList'] ?? false; + SettingsManager().settings.startVideosMuted.value = map['startVideosMuted'] ?? true; + SettingsManager().settings.startVideosMutedFullscreen.value = map['startVideosMutedFullscreen'] ?? true; + SettingsManager().settings.use24HrFormat.value = map['use24HrFormat'] ?? false; + SettingsManager().settings.alwaysShowAvatars.value = map['alwaysShowAvatars'] ?? false; + SettingsManager().settings.notifyOnChatList.value = map['notifyOnChatList'] ?? false; + SettingsManager().settings.notifyReactions.value = map['notifyReactions'] ?? true; + SettingsManager().settings.notificationSound.value = map['notificationSound'] ?? "default"; + SettingsManager().settings.globalTextDetection.value = map['globalTextDetection'] ?? ""; + SettingsManager().settings.filterUnknownSenders.value = map['filterUnknownSenders'] ?? false; + SettingsManager().settings.enablePrivateAPI.value = map['enablePrivateAPI'] ?? false; + SettingsManager().settings.privateSendTypingIndicators.value = map['privateSendTypingIndicators'] ?? false; + SettingsManager().settings.privateMarkChatAsRead.value = map['privateMarkChatAsRead'] ?? false; + SettingsManager().settings.privateManualMarkAsRead.value = map['privateManualMarkAsRead'] ?? false; + SettingsManager().settings.redactedMode.value = map['redactedMode'] ?? false; + SettingsManager().settings.hideMessageContent.value = map['hideMessageContent'] ?? true; + SettingsManager().settings.hideReactions.value = map['hideReactions'] ?? false; + SettingsManager().settings.hideAttachments.value = map['hideAttachments'] ?? true; + SettingsManager().settings.hideEmojis.value = map['hideEmojis'] ?? false; + SettingsManager().settings.hideAttachmentTypes.value = map['hideAttachmentTypes'] ?? false; + SettingsManager().settings.hideContactPhotos.value = map['hideContactPhotos'] ?? true; + SettingsManager().settings.hideContactInfo.value = map['hideContactInfo'] ?? true; + SettingsManager().settings.removeLetterAvatars.value = map['removeLetterAvatars'] ?? true; + SettingsManager().settings.generateFakeContactNames.value = map['generateFakeContactNames'] ?? false; + SettingsManager().settings.generateFakeMessageContent.value = map['generateFakeMessageContent'] ?? false; + SettingsManager().settings.enableQuickTapback.value = map['enableQuickTapback'] ?? false; + SettingsManager().settings.quickTapbackType.value = map['quickTapbackType'] ?? ReactionTypes.toList()[0]; + SettingsManager().settings.iosShowPin.value = map['iosShowPin'] ?? true; + SettingsManager().settings.iosShowAlert.value = map['iosShowAlert'] ?? true; + SettingsManager().settings.iosShowDelete.value = map['iosShowDelete'] ?? true; + SettingsManager().settings.iosShowMarkRead.value = map['iosShowMarkRead'] ?? true; + SettingsManager().settings.iosShowArchive.value = map['iosShowArchive'] ?? true; + SettingsManager().settings.materialRightAction.value = map['materialRightAction'] != null ? MaterialSwipeAction.values[map['materialRightAction']] : MaterialSwipeAction.pin; + SettingsManager().settings.materialLeftAction.value = map['materialLeftAction'] != null ? MaterialSwipeAction.values[map['materialLeftAction']] : MaterialSwipeAction.archive; + SettingsManager().settings.shouldSecure.value = map['shouldSecure'] ?? false; + SettingsManager().settings.securityLevel.value = map['securityLevel'] != null ? SecurityLevel.values[map['securityLevel']] : SecurityLevel.locked; + SettingsManager().settings.incognitoKeyboard.value = map['incognitoKeyboard'] ?? false; + SettingsManager().settings.skin.value = map['skin'] != null ? Skins.values[map['skin']] : Skins.iOS; + SettingsManager().settings.theme.value = map['theme'] != null ? ThemeMode.values[map['theme']] : ThemeMode.system; + SettingsManager().settings.fullscreenViewerSwipeDir.value = map['fullscreenViewerSwipeDir'] != null ? SwipeDirection.values[map['fullscreenViewerSwipeDir']] : SwipeDirection.RIGHT; + SettingsManager().settings.pinRowsPortrait.value = map['pinRowsPortrait'] ?? 3; + SettingsManager().settings.pinColumnsPortrait.value = map['pinColumnsPortrait'] ?? 3; + SettingsManager().settings.pinRowsLandscape.value = map['pinRowsLandscape'] ?? 1; + SettingsManager().settings.pinColumnsLandscape.value = map['pinColumnsLandscape'] ?? 6; + SettingsManager().settings.maxAvatarsInGroupWidget.value = map['maxAvatarsInGroupWidget'] ?? 4; SettingsManager().settings.save(); } + + static Settings fromMap(Map map) { + Settings s = new Settings(); + s.guidAuthKey.value = map['guidAuthKey'] ?? ""; + s.serverAddress.value = map['serverAddress'] ?? ""; + s.finishedSetup.value = map['finishedSetup'] ?? false; + s.chunkSize.value = map['chunkSize'] ?? 500; + s.autoDownload.value = map['autoDownload'] ?? true; + s.onlyWifiDownload.value = map['onlyWifiDownload'] ?? false; + s.autoOpenKeyboard.value = map['autoOpenKeyboard'] ?? true; + s.hideTextPreviews.value = map['hideTextPreviews'] ?? false; + s.showIncrementalSync.value = map['showIncrementalSync'] ?? false; + s.lowMemoryMode.value = map['lowMemoryMode'] ?? false; + s.lastIncrementalSync.value = map['lastIncrementalSync'] ?? 0; + s.refreshRate.value = map['refreshRate'] ?? 0; + s.colorfulAvatars.value = map['colorfulAvatars'] ?? false; + s.colorfulBubbles.value = map['colorfulBubbles'] ?? false; + s.hideDividers.value = map['hideDividers'] ?? false; + s.scrollVelocity.value = map['scrollVelocity'] ?? 1; + s.sendWithReturn.value = map['sendWithReturn'] ?? false; + s.doubleTapForDetails.value = map['doubleTapForDetails'] ?? false; + s.denseChatTiles.value = map['denseChatTiles'] ?? false; + s.smartReply.value = map['smartReply'] ?? false; + s.reducedForehead.value = map['reducedForehead'] ?? false; + s.preCachePreviewImages.value = map['preCachePreviewImages'] ?? true; + s.showConnectionIndicator.value = map['showConnectionIndicator'] ?? false; + s.showSyncIndicator.value = map['showSyncIndicator'] ?? true; + s.sendDelay.value = map['sendDelay'] ?? 0; + s.recipientAsPlaceholder.value = map['recipientAsPlaceholder'] ?? false; + s.hideKeyboardOnScroll.value = map['hideKeyboardOnScroll'] ?? false; + s.moveChatCreatorToHeader.value = map['moveChatCreatorToHeader'] ?? false; + s.cameraFAB.value = map['cameraFAB'] ?? false; + s.swipeToCloseKeyboard.value = map['swipeToCloseKeyboard'] ?? false; + s.swipeToOpenKeyboard.value = map['swipeToOpenKeyboard'] ?? false; + s.openKeyboardOnSTB.value = map['openKeyboardOnSTB'] ?? false; + s.swipableConversationTiles.value = map['swipableConversationTiles'] ?? false; + s.colorblindMode.value = map['colorblindMode'] ?? false; + s.showDeliveryTimestamps.value = map['showDeliveryTimestamps'] ?? false; + s.previewCompressionQuality.value = map['previewCompressionQuality'] ?? 50; + s.filteredChatList.value = map['filteredChatList'] ?? false; + s.startVideosMuted.value = map['startVideosMuted'] ?? true; + s.startVideosMutedFullscreen.value = map['startVideosMutedFullscreen'] ?? true; + s.use24HrFormat.value = map['use24HrFormat'] ?? false; + s.alwaysShowAvatars.value = map['alwaysShowAvatars'] ?? false; + s.notifyOnChatList.value = map['notifyOnChatList'] ?? false; + s.notifyReactions.value = map['notifyReactions'] ?? true; + s.notificationSound.value = map['notificationSound'] ?? "default"; + s.colorsFromMedia.value = map['colorsFromMedia'] ?? false; + s.globalTextDetection.value = map['globalTextDetection'] ?? ""; + s.filterUnknownSenders.value = map['filterUnknownSenders'] ?? false; + s.enablePrivateAPI.value = map['enablePrivateAPI'] ?? false; + s.privateSendTypingIndicators.value = map['privateSendTypingIndicators'] ?? false; + s.privateMarkChatAsRead.value = map['privateMarkChatAsRead'] ?? false; + s.privateManualMarkAsRead.value = map['privateManualMarkAsRead'] ?? false; + s.redactedMode.value = map['redactedMode'] ?? false; + s.hideMessageContent.value = map['hideMessageContent'] ?? true; + s.hideReactions.value = map['hideReactions'] ?? false; + s.hideAttachments.value = map['hideAttachments'] ?? true; + s.hideEmojis.value = map['hideEmojis'] ?? false; + s.hideAttachmentTypes.value = map['hideAttachmentTypes'] ?? false; + s.hideContactPhotos.value = map['hideContactPhotos'] ?? true; + s.hideContactInfo.value = map['hideContactInfo'] ?? true; + s.removeLetterAvatars.value = map['removeLetterAvatars'] ?? true; + s.generateFakeContactNames.value = map['generateFakeContactNames'] ?? false; + s.generateFakeMessageContent.value = map['generateFakeMessageContent'] ?? false; + s.enableQuickTapback.value = map['enableQuickTapback'] ?? false; + s.quickTapbackType.value = map['quickTapbackType'] ?? ReactionTypes.toList()[0]; + s.iosShowPin.value = map['iosShowPin'] ?? true; + s.iosShowAlert.value = map['iosShowAlert'] ?? true; + s.iosShowDelete.value = map['iosShowDelete'] ?? true; + s.iosShowMarkRead.value = map['iosShowMarkRead'] ?? true; + s.iosShowArchive.value = map['iosShowArchive'] ?? true; + s.materialRightAction.value = map['materialRightAction'] != null ? MaterialSwipeAction.values[map['materialRightAction']] : MaterialSwipeAction.pin; + s.materialLeftAction.value = map['materialLeftAction'] != null ? MaterialSwipeAction.values[map['materialLeftAction']] : MaterialSwipeAction.archive; + s.shouldSecure.value = map['shouldSecure'] ?? false; + s.securityLevel.value = map['securityLevel'] != null ? SecurityLevel.values[map['securityLevel']] : SecurityLevel.locked; + s.incognitoKeyboard.value = map['incognitoKeyboard'] ?? false; + s.skin.value = map['skin'] != null ? Skins.values[map['skin']] : Skins.iOS; + s.theme.value = map['theme'] != null ? ThemeMode.values[map['theme']] : ThemeMode.system; + s.fullscreenViewerSwipeDir.value = map['fullscreenViewerSwipeDir'] != null ? SwipeDirection.values[map['fullscreenViewerSwipeDir']] : SwipeDirection.RIGHT; + s.pinRowsPortrait.value = map['pinRowsPortrait'] ?? 3; + s.pinColumnsPortrait.value = map['pinColumnsPortrait'] ?? 3; + s.pinRowsLandscape.value = map['pinRowsLandscape'] ?? 1; + s.pinColumnsLandscape.value = map['pinColumnsLandscape'] ?? 6; + s.maxAvatarsInGroupWidget.value = map['maxAvatarsInGroupWidget'] ?? 4; + return s; + } } diff --git a/lib/repository/models/theme_object.dart b/lib/repository/models/theme_object.dart index 5e3202f15..da82a62ba 100644 --- a/lib/repository/models/theme_object.dart +++ b/lib/repository/models/theme_object.dart @@ -1,11 +1,13 @@ import 'dart:async'; import 'dart:core'; +import 'package:adaptive_theme/adaptive_theme.dart'; import 'package:bluebubbles/helpers/constants.dart'; import 'package:bluebubbles/helpers/themes.dart'; import 'package:bluebubbles/repository/database.dart'; import 'package:bluebubbles/repository/models/theme_entry.dart'; import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; import 'package:sqflite/sqflite.dart'; class ThemeObject { @@ -13,6 +15,9 @@ class ThemeObject { String? name; bool selectedLightTheme = false; bool selectedDarkTheme = false; + bool gradientBg = false; + bool previousLightTheme = false; + bool previousDarkTheme = false; ThemeData? data; List entries = []; @@ -21,12 +26,16 @@ class ThemeObject { this.name, this.selectedLightTheme = false, this.selectedDarkTheme = false, + this.gradientBg = false, + this.previousLightTheme = false, + this.previousDarkTheme = false, this.data, }); - factory ThemeObject.fromData(ThemeData data, String name, {bool isPreset = false}) { + factory ThemeObject.fromData(ThemeData data, String name, {bool gradientBg = false}) { ThemeObject object = new ThemeObject( data: data.copyWith(), name: name, + gradientBg: gradientBg, ); object.entries = object.toEntries(); @@ -39,10 +48,13 @@ class ThemeObject { name: json["name"], selectedLightTheme: json["selectedLightTheme"] == 1, selectedDarkTheme: json["selectedDarkTheme"] == 1, + gradientBg: json["gradientBg"] == 1, + previousLightTheme: json["previousLightTheme"] == 1, + previousDarkTheme: json["previousDarkTheme"] == 1, ); } - bool get isPreset => this.name == "OLED Dark" || this.name == "Bright White" || this.name == "Nord Theme"; + bool get isPreset => this.name == "OLED Dark" || this.name == "Bright White" || this.name == "Nord Theme" || this.name == "Music Theme (Light)" || this.name == "Music Theme (Dark)"; List toEntries() => [ ThemeEntry.fromStyle(ThemeColors.Headline1, data!.textTheme.headline1!), @@ -83,7 +95,7 @@ class ThemeObject { await this.update(); } - if (this.isPreset) return this; + if (this.isPreset && !this.name!.contains("Music")) return this; for (ThemeEntry entry in this.entries) { await entry.save(this); } @@ -115,6 +127,9 @@ class ThemeObject { "name": this.name, "selectedLightTheme": this.selectedLightTheme ? 1 : 0, "selectedDarkTheme": this.selectedDarkTheme ? 1 : 0, + "gradientBg": this.gradientBg ? 1 : 0, + "previousLightTheme": this.previousLightTheme ? 1 : 0, + "previousDarkTheme": this.previousDarkTheme ? 1 : 0, }, where: "ROWID = ?", whereArgs: [this.id]); @@ -186,7 +201,7 @@ class ThemeObject { } Future> fetchData() async { - if (isPreset) { + if (isPreset && !name!.contains("Music")) { if (name == "OLED Dark") { this.data = oledDarkTheme; } else if (name == "Bright White") { @@ -212,8 +227,19 @@ class ThemeObject { " JOIN theme_values ON theme_values.ROWID = tvj.themeValueId" " WHERE themes.ROWID = ?;", [this.id]); - this.entries = (res.isNotEmpty) ? res.map((t) => ThemeEntry.fromMap(t)).toList() : []; - this.data = themeData; + if (name == "Music Theme (Light)" && res.isEmpty) { + data = whiteLightTheme; + entries = this.toEntries(); + } else if (name == "Music Theme (Dark)" && res.isEmpty) { + data = oledDarkTheme; + entries = this.toEntries(); + } else if (res.isNotEmpty) { + this.entries = res.map((t) => ThemeEntry.fromMap(t)).toList(); + this.data = themeData; + } else { + this.entries = []; + this.data = themeData; + } return this.entries; } @@ -222,6 +248,9 @@ class ThemeObject { "name": this.name, "selectedLightTheme": this.selectedLightTheme ? 1 : 0, "selectedDarkTheme": this.selectedDarkTheme ? 1 : 0, + "gradientBg": this.gradientBg ? 1 : 0, + "previousLightTheme": this.previousLightTheme ? 1 : 0, + "previousDarkTheme": this.previousDarkTheme ? 1 : 0, }; ThemeData get themeData { @@ -266,6 +295,11 @@ class ThemeObject { primaryColor: data[ThemeColors.PrimaryColor]!.style); } + static bool inDarkMode(BuildContext context) { + return (AdaptiveTheme.of(context).mode == AdaptiveThemeMode.dark + || (AdaptiveTheme.of(context).mode == AdaptiveThemeMode.system && SchedulerBinding.instance!.window.platformBrightness == Brightness.dark)); + } + @override bool operator ==(Object other) => identical(this, other) || other is ThemeObject && runtimeType == other.runtimeType && name == other.name; diff --git a/lib/socket_manager.dart b/lib/socket_manager.dart index a610cd07d..f9a7434e0 100644 --- a/lib/socket_manager.dart +++ b/lib/socket_manager.dart @@ -7,6 +7,7 @@ import 'package:bluebubbles/blocs/setup_bloc.dart'; import 'package:bluebubbles/helpers/attachment_sender.dart'; import 'package:bluebubbles/helpers/crypto.dart'; import 'package:bluebubbles/helpers/darty.dart'; +import 'package:bluebubbles/helpers/logger.dart'; import 'package:bluebubbles/helpers/utils.dart'; import 'package:bluebubbles/managers/attachment_info_bloc.dart'; import 'package:bluebubbles/managers/current_chat.dart'; @@ -20,7 +21,6 @@ import 'package:bluebubbles/repository/models/chat.dart'; import 'package:bluebubbles/repository/models/fcm_data.dart'; import 'package:bluebubbles/repository/models/message.dart'; import 'package:bluebubbles/repository/models/settings.dart'; -import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:get/get.dart'; import 'package:socket_io_client/socket_io_client.dart' as IO; @@ -40,6 +40,7 @@ class SocketManager { } static final SocketManager _manager = SocketManager._internal(); + static final String tag = 'Socket'; SocketManager._internal(); @@ -104,9 +105,9 @@ class SocketManager { String? token; void socketStatusUpdate(String status, dynamic data) { - debugPrint("[Socket] -> Socket status update: $status"); + Logger.info("Socket status update: $status", tag: tag); if (data != null) { - debugPrint("[Socket] -> Data: ${data.toString()}"); + Logger.debug("Data: ${data.toString()}", tag: tag); } switch (status) { @@ -124,8 +125,8 @@ class SocketManager { }); if (SettingsManager().settings.finishedSetup.value) setup.startIncrementalSync(SettingsManager().settings, onConnectionError: (String err) { - debugPrint("(SYNC) Error performing incremental sync. Not saving last sync date."); - debugPrint(err); + Logger.error("Error performing incremental sync. Not saving last sync date.", tag: "IncrementalSync"); + Logger.error(err); }); return; case "connect_error": @@ -137,7 +138,7 @@ class SocketManager { }); Timer(Duration(seconds: 20), () { if (state.value != SocketState.ERROR) return; - debugPrint("[Socket] -> Unable to connect"); + Logger.error("Unable to connect", tag: tag); // Only show the notification if setup is finished if (SettingsManager().settings.finishedSetup.value) { @@ -182,7 +183,7 @@ class SocketManager { }); return; case "reconnect": - debugPrint("RECONNECTED"); + Logger.info("RECONNECTED"); state.value = SocketState.CONNECTING; _manager.socketProcesses.values.forEach((element) { element(); @@ -210,7 +211,7 @@ class SocketManager { Future startSocketIO({bool forceNewConnection = false, bool catchException = true}) async { //removed check for settings being null here, could be an issue later but I doubt it (tneotia) if ((state.value == SocketState.CONNECTING || state.value == SocketState.CONNECTED) && !forceNewConnection) { - debugPrint("[Socket] -> Already connected"); + Logger.debug("Already connected", tag: tag); return; } if (state.value == SocketState.FAILED) { @@ -224,11 +225,11 @@ class SocketManager { String? serverAddress = getServerAddress(); if (serverAddress == null) { - debugPrint("[Socket] -> Server Address is not yet configured. Not connecting..."); + Logger.warn("Server Address is not yet configured. Not connecting...", tag: tag); return; } - debugPrint("[Socket] -> Configuring socket.io client..."); + Logger.info("Configuring socket.io client...", tag: tag); try { // Create a new socket connection @@ -247,7 +248,7 @@ class SocketManager { .build()); if (_manager.socket == null) { - debugPrint("[Socket] -> Socket was never created. Can't connect to server..."); + Logger.error("Socket was never created. Can't connect to server...", tag: tag); return; } @@ -271,14 +272,14 @@ class SocketManager { // TODO: Possibly turn this into a notification for the user? // This could act as a "pseudo" security measure so they're alerted // when a new device is registered - debugPrint("[Socket] -> FCM device added: " + data.toString()); + Logger.info("FCM device added: " + data.toString(), tag: tag); }); /** * If the server sends us an error it ran into, handle it */ _manager.socket!.on("error", (data) { - debugPrint("[Socket] -> An error occurred: " + data.toString()); + Logger.info("An error occurred: " + data.toString(), tag: tag); }); /** @@ -345,7 +346,7 @@ class SocketManager { * something about it (or at least just track it) */ _manager.socket!.on("message-timeout", (_data) async { - debugPrint("[Socket] -> Client received message timeout"); + Logger.info("Client received message timeout", tag: tag); Map data = _data; Message? message = await Message.findOne({"guid": data["tempGuid"]}); @@ -364,21 +365,21 @@ class SocketManager { IncomingQueue().add(new QueueItem(event: "handle-updated-message", item: {"data": _data})); }); - debugPrint("Connecting to the socket at: $serverAddress"); + Logger.info("Connecting to the socket at: $serverAddress"); _manager.socket!.connect(); } catch (e) { if (!catchException) { - throw ("[Socket] -> Failed to connect: ${e.toString()}"); + throw ("[$tag] -> Failed to connect: ${e.toString()}"); } else { - debugPrint("[Socket] -> Failed to connect"); - debugPrint("[Socket] -> ${e.toString()}"); + Logger.error("Failed to connect", tag: tag); + Logger.error("${e.toString()}", tag: tag); } } } void closeSocket({bool force = false}) { if (!force && _manager.socketProcesses.length != 0) { - debugPrint("won't close " + socketProcesses.length.toString()); + Logger.info("Not closing the socket! Count: " + socketProcesses.length.toString()); return; } if (_manager.socket != null) { @@ -394,21 +395,21 @@ class SocketManager { if (!SettingsManager().settings.finishedSetup.value) return; if (isAuthingFcm && !force) { - debugPrint('Currently authenticating with FCM, not doing it again...'); + Logger.debug('Currently authenticating with FCM, not doing it again...'); return; } isAuthingFcm = true; if (SettingsManager().fcmData!.isNull) { - debugPrint("[FCM Auth] -> No FCM Auth data found. Skipping FCM authentication"); + Logger.warn("No FCM Auth data found. Skipping FCM authentication", tag: 'FCM-Auth'); isAuthingFcm = false; return; } String deviceName = await getDeviceName(); if (token != null && !force) { - debugPrint("[FCM Auth] -> Already authorized FCM device! Token: $token"); + Logger.debug("Already authorized FCM device! Token: $token", tag: 'FCM-Auth'); await registerDevice(deviceName, token); isAuthingFcm = false; return; @@ -418,15 +419,15 @@ class SocketManager { try { // First, try to send what we currently have - debugPrint('[FCM Auth] -> Authenticating with FCM'); + Logger.info('Authenticating with FCM', tag: 'FCM-Auth'); result = await MethodChannelInterface().invokeMethod('auth', SettingsManager().fcmData!.toMap()); } on PlatformException catch (ex) { - debugPrint('[FCM Auth] -> Failed to perform initial FCM authentication: ${ex.toString()}'); - debugPrint('[FCM Auth] -> Fetching FCM data from the server...'); + Logger.error('Failed to perform initial FCM authentication: ${ex.toString()}', tag: 'FCM-Auth'); + Logger.info('Fetching FCM data from the server...', tag: 'FCM-Auth'); // If the first try fails, let's try again, but first, get the FCM data from the server Map fcmMeta = await this.getFcmClient(); - debugPrint('[FCM Auth] -> Received FCM data from the server. Attempting to re-authenticate'); + Logger.info('Received FCM data from the server. Attempting to re-authenticate', tag: 'FCM-Auth'); try { // Parse out the new FCM data @@ -442,22 +443,22 @@ class SocketManager { isAuthingFcm = false; throw Exception("[FCM Auth] -> " + e.toString()); } else { - debugPrint("[FCM Auth] -> Failed to register with FCM: " + e.toString()); + Logger.error("Failed to register with FCM: " + e.toString(), tag: 'FCM-Auth'); } } } if (isNullOrEmpty(result)!) { - debugPrint("[FCM Auth] -> Empty results, not registering device with the server."); + Logger.error("Empty results, not registering device with the server.", tag: 'FCM-Auth'); } try { token = result; - debugPrint('[FCM Auth] -> Registering device with server...'); + Logger.info('Registering device with server...', tag: 'FCM-Auth'); await registerDevice(deviceName, token); } catch (ex) { isAuthingFcm = false; - debugPrint('[FCM Auth] -> Failed to register device with server: ${ex.toString()}'); + Logger.error('[FCM Auth] -> Failed to register device with server: ${ex.toString()}'); throw Exception("Failed to add FCM device to the server! Token: $token"); } @@ -496,7 +497,7 @@ class SocketManager { Completer?> completer = new Completer(); if (_manager.socket == null) return null; - debugPrint("[Socket] -> Sending request for '$path'"); + Logger.info("Sending request for '$path'", tag: "Socket"); _manager.sendMessage(path, params, (Map data) async { if (data["status"] != 200) return completer.completeError(data); @@ -555,7 +556,7 @@ class SocketManager { Future fetchChat(String chatGuid, {withParticipants = true}) async { Completer completer = new Completer(); - debugPrint("[Fetch Chat] Fetching full chat metadata from server."); + Logger.info("[Fetch Chat] Fetching full chat metadata from server."); Map params = Map(); params["chatGuid"] = chatGuid; @@ -567,11 +568,11 @@ class SocketManager { Map? chatData = data["data"]; if (chatData == null) { - debugPrint("[Fetch Chat] Server returned no metadata for chat."); + Logger.info("[Fetch Chat] Server returned no metadata for chat."); return completer.complete(null); } - debugPrint("[Fetch Chat] Got updated chat metadata from server. Saving."); + Logger.info("[Fetch Chat] Got updated chat metadata from server. Saving."); Chat newChat = Chat.fromMap(chatData); // Resave the chat after we've got the participants @@ -588,7 +589,7 @@ class SocketManager { int? after, bool onlyAttachments: false, List> where: const []}) async { - debugPrint("(Fetch Messages) Fetching data."); + Logger.info("(Fetch Messages) Fetching data."); Map params = Map(); params["chatGuid"] = chat?.guid; @@ -668,7 +669,7 @@ class SocketManager { } else { socketCB(); } - if (reason != null) debugPrint("added process with id " + _processId.toString() + " because $reason"); + if (reason != null) Logger.info("Added process with id " + _processId.toString() + " because $reason"); return completer.future; } @@ -689,7 +690,7 @@ class SocketManager { // We copy the settings to a local variable Settings settingsCopy = SettingsManager().settings; if (settingsCopy.serverAddress.value == serverAddress) { - debugPrint("Server address didn't actually change. Ignoring..."); + Logger.debug("Server address didn't actually change. Ignoring..."); return; } @@ -715,14 +716,14 @@ class SocketManager { } Future refreshConnection({bool connectToSocket = true}) async { - debugPrint("Fetching new server URL from Firebase"); + Logger.info("Fetching new server URL from Firebase"); // Get the server URL try { String? url = await MethodChannelInterface().invokeMethod("get-server-url"); url = getServerAddress(address: url); - debugPrint("New server URL: $url"); + Logger.info("New server URL: $url"); // Set the server URL Settings _settingsCopy = SettingsManager().settings; diff --git a/pubspec.lock b/pubspec.lock index b2a1310b2..e782bd921 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -49,7 +49,7 @@ packages: name: assorted_layout_widgets url: "https://pub.dartlang.org" source: hosted - version: "5.0.2" + version: "5.1.1" async: dependency: transitive description: @@ -423,6 +423,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.1.0" + flutter_local_notifications: + dependency: "direct main" + description: + name: flutter_local_notifications + url: "https://pub.dartlang.org" + source: hosted + version: "8.1.1+2" + flutter_local_notifications_platform_interface: + dependency: transitive + description: + name: flutter_local_notifications_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "4.0.1" flutter_map: dependency: "direct main" description: @@ -451,6 +465,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.2.1" + flutter_native_timezone: + dependency: "direct main" + description: + name: flutter_native_timezone + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" flutter_plugin_android_lifecycle: dependency: transitive description: @@ -1036,12 +1057,12 @@ packages: source: hosted version: "2.0.3" shared_preferences: - dependency: transitive + dependency: "direct main" description: name: shared_preferences url: "https://pub.dartlang.org" source: hosted - version: "2.0.6" + version: "2.0.7" shared_preferences_linux: dependency: transitive description: @@ -1278,6 +1299,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.3.19" + timezone: + dependency: transitive + description: + name: timezone + url: "https://pub.dartlang.org" + source: hosted + version: "0.8.0" timing: dependency: transitive description: @@ -1504,4 +1532,4 @@ packages: version: "3.1.0" sdks: dart: ">=2.13.0 <3.0.0" - flutter: ">=2.0.2" + flutter: ">=2.2.0" diff --git a/pubspec.yaml b/pubspec.yaml index 0798c3391..c3b16a99e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,7 +11,7 @@ description: Send iMessages on Android using BlueBubbles! # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 1.4.1+9 +version: 1.5.0+16 publish_to: none environment: @@ -23,33 +23,33 @@ dependencies: get: ^4.3.8 cupertino_icons: ^1.0.3 exif: ^3.0.0 - flutter_native_image: ^0.0.6+1 + flutter_native_image: ^0.0.6+1 # mobile only, todo use image size getter on web and desktop socket_io_client: ^2.0.0-beta.4-nullsafety.0 url_launcher: ^6.0.9 - qr_code_scanner: ^0.5.2 - connectivity: ^3.0.6 + qr_code_scanner: ^0.5.2 # mobile only, todo disable on web and desktop + connectivity: ^3.0.6 # no linux or windows support (switch to connectivity_plus), todo desktop tuple: ^2.0.0 encrypt: ^5.0.1 assorted_layout_widgets: ^5.0.2 - record: ^3.0.0 - contacts_service: ^0.6.1 - permission_handler: ^8.1.4+2 - path_provider: ^2.0.2 - sqflite: ^2.0.0+3 + record: ^3.0.0 # mobile and web, todo desktop + contacts_service: ^0.6.1 # mobile only, todo disable on web and desktop + permission_handler: ^8.1.4+2 # mobile only, todo disable on web and desktop + path_provider: ^2.0.2 # no web support, todo disable on web (no concept of storage on web) + sqflite: ^2.0.0+3 # mobile only, we can potentially use sqflite_common_ffi for desktop and sembast_sqflite for web, todo web todo desktop path: ^1.8.0 intl: ^0.17.0 - flutter_svg: ^0.22.0 - photo_manager: ^1.3.1 - video_player: ^2.1.13 - chewie_audio: + flutter_svg: ^0.22.0 # partial web support + photo_manager: ^1.3.1 # only mobile, todo disable on web and desktop + video_player: ^2.1.13 # no desktop support, todo desktop + chewie_audio: # no desktop support, todo desktop git: url: https://github.com/tneotia/chewie_audio-1.git ref: BlueBubbles mime_type: ^1.0.0 - receive_sharing_intent: ^1.4.5 + receive_sharing_intent: ^1.4.5 # mobile only, todo disable on web and desktop flutter_map: ^0.13.1 - video_thumbnail: ^0.4.3 - camera: ^0.8.1+7 + video_thumbnail: ^0.4.3 # mobile only (we don't need this package anymore) + camera: ^0.8.1+7 # mobile only, todo disable on web and desktop flutter_slidable: ^0.6.0 image_size_getter: ^1.0.0 photo_view: ^0.12.0 @@ -57,35 +57,38 @@ dependencies: sprung: ^3.0.0 slugify: ^2.0.0 metadata_fetch: ^0.4.1 - map_launcher: ^2.1.1 + map_launcher: ^2.1.1 # mobile only (switch to maps_launcher), todo web todo desktop latlong2: ^0.8.0 smooth_page_indicator: ^0.3.0-nullsafety.0 flex_color_picker: ^2.1.2 - image_gallery_saver: ^1.6.9 + image_gallery_saver: ^1.6.9 # mobile only, todo download differently on web and desktop visibility_detector: ^0.2.0 - flutter_displaymode: ^0.3.2 - flutter_libphonenumber: ^1.1.0 + flutter_displaymode: ^0.3.2 # android only, todo disable on web and desktop + flutter_libphonenumber: ^1.1.0 # mobile only (switch to intl_phone_number_input or libphonenumber_plugin to add web support), todo web todo desktop flutter_markdown: ^0.6.4 - device_info: ^2.0.2 - google_ml_kit: ^0.7.0 + device_info: ^2.0.2 # mobile only, todo switch to device_info_plus + google_ml_kit: ^0.7.0 # mobile only, todo disable on web and desktop faker: ^2.0.0 - share_plus: ^2.1.4 + share_plus: ^2.1.4 # sharing files not supported on Windows & Linux, todo desktop # for nullsafety - battery_optimization: + battery_optimization: # android only, todo disable for other platforms git: url: https://github.com/ChangJoo-Park/battery_optimization ref: master collection: ^1.15.0 - internet_connection_checker: ^0.0.1+2 - secure_application: ^3.7.3 - local_auth: ^1.1.6 + internet_connection_checker: ^0.0.1+2 # no web support, but we don't need it obviously, todo disable on web + secure_application: ^3.7.3 # no linux support, todo linux + local_auth: ^1.1.6 # mobile only, todo disable on web and desktop flutter_screen_lock: ^4.0.4+1 image: ^3.0.2 crop_your_image: ^0.6.0+1 - chewie: ^1.2.2 + chewie: ^1.2.2 # no desktop support, todo desktop simple_animations: ^3.1.1 - flutter_keyboard_visibility: ^5.0.3 + flutter_keyboard_visibility: ^5.0.3 # no desktop support, todo desktop + flutter_local_notifications: ^8.1.1+2 # mobile only, todo disable on web and desktop + flutter_native_timezone: ^2.0.0 # no desktop support, todo desktop package_info_plus: ^1.0.6 + shared_preferences: ^2.0.7 dev_dependencies: build_runner: ^2.1.1