Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ jobs:
- name: Setup Tools
run: |
sudo apt-get update -y
sudo apt-get install -y ninja-build libgtk-3-dev libmpv-dev mpv ffmpeg
sudo apt-get install -y ninja-build libgtk-3-dev libmpv-dev mpv ffmpeg

- name: Code Generation
run: |
Expand Down Expand Up @@ -179,7 +179,7 @@ jobs:
- name: Setup Tools
run: |
sudo apt-get update -y
sudo apt-get install -y ninja-build libgtk-3-dev libmpv-dev mpv ffmpeg libmimalloc-dev webkit2gtk-4.1 keybinder-3.0
sudo apt-get install -y ninja-build libgtk-3-dev libmpv-dev mpv ffmpeg libmimalloc-dev webkit2gtk-4.1 keybinder-3.0 libayatana-appindicator3-dev

- name: Code Generation
run: |
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@ jobs:
- name: Setup Tools
run: |
sudo apt-get update -y
sudo apt-get install -y ninja-build libgtk-3-dev libmpv-dev mpv ffmpeg libmimalloc-dev webkit2gtk-4.1 libasound2-dev keybinder-3.0
sudo apt-get install -y ninja-build libgtk-3-dev libmpv-dev mpv ffmpeg libmimalloc-dev webkit2gtk-4.1 libasound2-dev keybinder-3.0 libayatana-appindicator3-dev

- name: Code Generation
run: |
Expand Down Expand Up @@ -367,7 +367,7 @@ jobs:
- name: Setup Tools
run: |
sudo apt-get update -y
sudo apt-get install -y ninja-build libgtk-3-dev libmpv-dev mpv ffmpeg libmimalloc-dev flatpak-builder flatpak webkit2gtk-4.1 keybinder-3.0
sudo apt-get install -y ninja-build libgtk-3-dev libmpv-dev mpv ffmpeg libmimalloc-dev flatpak-builder flatpak webkit2gtk-4.1 keybinder-3.0 libayatana-appindicator3-dev

- name: Code Generation
run: |
Expand Down Expand Up @@ -456,7 +456,7 @@ jobs:
- name: Setup Tools
run: |
sudo apt-get update -y
sudo apt-get install -y ninja-build libgtk-3-dev libmpv-dev mpv ffmpeg libmimalloc-dev webkit2gtk-4.1 keybinder-3.0
sudo apt-get install -y ninja-build libgtk-3-dev libmpv-dev mpv ffmpeg libmimalloc-dev webkit2gtk-4.1 keybinder-3.0 libayatana-appindicator3-dev

- name: Code Generation
run: |
Expand Down
19 changes: 18 additions & 1 deletion commet/lib/utils/notification_utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@ import 'package:commet/main.dart';

class NotificationUtils {
static (int, int) getNotificationCounts() {
return _getNotificationCounts(includeSingleRooms: false);
}

static (int, int) getTrayNotificationCounts() {
return _getNotificationCounts(includeSingleRooms: true);
}

static (int, int) _getNotificationCounts({required bool includeSingleRooms}) {
var highlightedNotificationCount = 0;
var notificationCount = 0;

Expand All @@ -13,8 +21,17 @@ class NotificationUtils {
notificationCount += i.displayNotificationCount;
}

if (includeSingleRooms) {
// Include rooms that are not part of any space and are not direct messages.
for (var room in clientManager!.singleRooms()) {
highlightedNotificationCount +=
room.displayHighlightedNotificationCount;
notificationCount += room.displayNotificationCount;
}
}

for (var dm in clientManager!.directMessages.highlightedRoomsList) {
highlightedNotificationCount += dm.displayNotificationCount;
highlightedNotificationCount += dm.displayHighlightedNotificationCount;
notificationCount += dm.displayNotificationCount;
}

Expand Down
163 changes: 163 additions & 0 deletions commet/lib/utils/tray_notification_manager.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
import 'dart:async';
import 'dart:io';
import 'dart:ui' as ui;

import 'package:commet/client/client_manager.dart';
import 'package:commet/debug/log.dart';
import 'package:commet/main.dart';
import 'package:commet/utils/notification_utils.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:tray_manager/tray_manager.dart';
import 'package:window_manager/window_manager.dart';

class TrayNotificationManager {
static const String _defaultTrayIconPath =
'assets/images/app_icon/app_icon_rounded.png';

static const String _cacheId = 'tray_icon_with_notification_badge.png';

static String? _cachedIconPath;

static StreamSubscription<void>? _syncNotificationSubscription;
static Future<void> Function()? _onCloseApplication;

static Future<void> init({
required ClientManager? clientManager,
required Future<void> Function() onCloseApplication,
}) async {
_onCloseApplication = onCloseApplication;

await TrayManager.instance.setIcon(_defaultTrayIconPath);
await _setupTrayMenu();
TrayManager.instance.addListener(_TrayListener());

_registerTrayNotificationListeners(clientManager);
}

static void _registerTrayNotificationListeners(ClientManager? clientManager) {
_syncNotificationSubscription?.cancel();

if (clientManager == null) {
return;
}

_syncNotificationSubscription =
clientManager.onSync.stream.listen(_onSyncNotificationUpdated);

_onTrayNotificationStateChanged();
}

// Update tray icon during sync
static void _onSyncNotificationUpdated(void _) {
_onTrayNotificationStateChanged();
}

static Future<void> _onTrayNotificationStateChanged() async {
var counts = NotificationUtils.getTrayNotificationCounts();
var notificationCount = counts.$2;

if (notificationCount == 0) {
await TrayManager.instance.setIcon(_defaultTrayIconPath);
} else {
await _drawNotificationBadge();
}
}

static Future<void> _drawNotificationBadge() async {
if (_cachedIconPath != null && await File(_cachedIconPath!).exists()) {
await TrayManager.instance.setIcon(_cachedIconPath!);
return;
}

final cache = fileCache;
if (cache == null) {
Log.e(
'File cache is not initialized, cannot get or create cached tray icon with notification badge');
return; // TrayManager.instance.setIcon expects a file on disk so cannot continue without caching the icon
}

var cached = await cache.getFile(_cacheId);
if (cached != null) {
_cachedIconPath = File.fromUri(cached).path;
await TrayManager.instance.setIcon(_cachedIconPath!);
return;
}

Log.i(
'No cached tray icon with notification badge found, creating new one');

final baseIconBytes = await rootBundle.load(_defaultTrayIconPath);
final codec =
await ui.instantiateImageCodec(baseIconBytes.buffer.asUint8List());
final frame = await codec.getNextFrame();
final baseImage = frame.image;

final size = Size(baseImage.width.toDouble(), baseImage.height.toDouble());
final recorder = ui.PictureRecorder();
final canvas = Canvas(recorder);

canvas.drawImage(baseImage, Offset.zero, Paint());

final badgeRadius = size.shortestSide / 6;
final badgeCenter = Offset(size.width - badgeRadius, badgeRadius);
final paint = Paint()..color = Colors.red;
canvas.drawCircle(badgeCenter, badgeRadius, paint);

final picture = recorder.endRecording();
final img = await picture.toImage(size.width.toInt(), size.height.toInt());
final byteData = await img.toByteData(format: ui.ImageByteFormat.png);
if (byteData == null) {
Log.e('Failed to convert tray icon image to byte data');
return;
}
final pngBytes = byteData.buffer.asUint8List();

var savedUri = await cache.putFile(_cacheId, pngBytes);
_cachedIconPath = File.fromUri(savedUri).path;

Log.i('Saved new tray icon with notification badge to cache');

await TrayManager.instance.setIcon(_cachedIconPath!);
}

static Future<void> _setupTrayMenu() async {
final menu = [
MenuItem(label: 'Commet', key: 'app_name', disabled: true),
MenuItem.separator(),
MenuItem(label: 'Open Commet', key: 'show'),
MenuItem(label: 'Quit Commet', key: 'close'),
];

await TrayManager.instance.setContextMenu(Menu(items: menu));
}
}

class _TrayListener extends TrayListener {
@override
void onTrayIconMouseDown() {
_showWindow();
}

@override
void onTrayMenuItemClick(MenuItem menuItem) {
print(menuItem.key);
switch (menuItem.key) {
case 'show':
_showWindow();
break;
case 'close':
_closeApplication();
break;
}
}

Future<void> _showWindow() async {
await windowManager.show();
await windowManager.focus();
}

Future<void> _closeApplication() async {
await TrayNotificationManager._onCloseApplication?.call();
}
}
44 changes: 36 additions & 8 deletions commet/lib/utils/window_management.dart
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
import 'dart:async';
import 'dart:io';

import 'package:collection/collection.dart';
import 'package:commet/client/room.dart';
import 'package:commet/client/space.dart';
import 'package:commet/config/platform_utils.dart';
import 'package:commet/main.dart';
import 'package:commet/utils/tray_notification_manager.dart';
import 'package:commet/utils/event_bus.dart';
import 'package:flutter/services.dart';
import 'package:window_manager/window_manager.dart';

class WindowManagement {
static bool _isShuttingDown = false;

static Future<void> init() async {
if (!(PlatformUtils.isLinux || PlatformUtils.isWindows)) return;

Expand All @@ -24,11 +28,41 @@ class WindowManagement {
EventBus.onSelectedRoomChanged.stream.listen(_onSelectedRoomChanged);
EventBus.onSelectedSpaceChanged.stream.listen(_onSelectedSpaceChanged);

_registerProcessSignalHandlers();

await TrayNotificationManager.init(
clientManager: clientManager,
onCloseApplication: _shutdownApplication,
);

if (commandLineArgs.contains("--minimize")) {
windowManager.minimize();
}
}

static void _registerProcessSignalHandlers() {
ProcessSignal.sigint.watch().listen((_) {
_shutdownApplication();
});

ProcessSignal.sigterm.watch().listen((_) {
_shutdownApplication();
});
}

static Future<void> _shutdownApplication() async {
if (_isShuttingDown) return;
_isShuttingDown = true;

if (clientManager != null) {
for (var client in clientManager!.clients) {
await client.close();
}
}

exit(0);
}

static bool _onKeyEvent(KeyEvent event) {
if (event is KeyDownEvent && event.logicalKey == LogicalKeyboardKey.f11) {
_toggleFullscreen();
Expand Down Expand Up @@ -71,15 +105,9 @@ class _WindowListener extends WindowListener {
super.onWindowClose();

if (preferences.minimizeOnClose.value) {
windowManager.minimize();
windowManager.hide();
} else {
if (clientManager != null) {
for (var client in clientManager!.clients) {
await client.close();
}
}

exit(0);
await WindowManagement._shutdownApplication();
}
}
}
4 changes: 4 additions & 0 deletions commet/linux/flutter/generated_plugin_registrant.cc
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
#include <pasteboard/pasteboard_plugin.h>
#include <screen_retriever_linux/screen_retriever_linux_plugin.h>
#include <sqlite3_flutter_libs/sqlite3_flutter_libs_plugin.h>
#include <tray_manager/tray_manager_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h>
#include <window_manager/window_manager_plugin.h>
#include <window_to_front/window_to_front_plugin.h>
Expand Down Expand Up @@ -63,6 +64,9 @@ void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) sqlite3_flutter_libs_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "Sqlite3FlutterLibsPlugin");
sqlite3_flutter_libs_plugin_register_with_registrar(sqlite3_flutter_libs_registrar);
g_autoptr(FlPluginRegistrar) tray_manager_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "TrayManagerPlugin");
tray_manager_plugin_register_with_registrar(tray_manager_registrar);
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);
Expand Down
1 change: 1 addition & 0 deletions commet/linux/flutter/generated_plugins.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
pasteboard
screen_retriever_linux
sqlite3_flutter_libs
tray_manager
url_launcher_linux
window_manager
window_to_front
Expand Down
2 changes: 2 additions & 0 deletions commet/macos/Flutter/GeneratedPluginRegistrant.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import screen_retriever_macos
import shared_preferences_foundation
import sqflite_darwin
import sqlite3_flutter_libs
import tray_manager
import url_launcher_macos
import wakelock_plus
import window_manager
Expand Down Expand Up @@ -53,6 +54,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
Sqlite3FlutterLibsPlugin.register(with: registry.registrar(forPlugin: "Sqlite3FlutterLibsPlugin"))
TrayManagerPlugin.register(with: registry.registrar(forPlugin: "TrayManagerPlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin"))
WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin"))
Expand Down
1 change: 1 addition & 0 deletions commet/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ dependencies:
url_launcher: ^6.1.11
win_toast: ^0.4.0
window_manager: ^0.5.1
tray_manager: ^0.5.2
flutter:
sdk: flutter
flutter_highlighter:
Expand Down
3 changes: 3 additions & 0 deletions commet/windows/flutter/generated_plugin_registrant.cc
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
#include <permission_handler_windows/permission_handler_windows_plugin.h>
#include <screen_retriever_windows/screen_retriever_windows_plugin_c_api.h>
#include <sqlite3_flutter_libs/sqlite3_flutter_libs_plugin.h>
#include <tray_manager/tray_manager_plugin.h>
#include <url_launcher_windows/url_launcher_windows.h>
#include <win_toast/win_toast_plugin.h>
#include <window_manager/window_manager_plugin.h>
Expand Down Expand Up @@ -60,6 +61,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
registry->GetRegistrarForPlugin("ScreenRetrieverWindowsPluginCApi"));
Sqlite3FlutterLibsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("Sqlite3FlutterLibsPlugin"));
TrayManagerPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("TrayManagerPlugin"));
UrlLauncherWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
WinToastPluginRegisterWithRegistrar(
Expand Down
1 change: 1 addition & 0 deletions commet/windows/flutter/generated_plugins.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
permission_handler_windows
screen_retriever_windows
sqlite3_flutter_libs
tray_manager
url_launcher_windows
win_toast
window_manager
Expand Down
Loading
Loading