window: extract XWindow primitive; XNotifications stops carrying focus
The window-focus signal had no business living on the notifications primitive — it was there because the same NotificationsPlugin happened to observe NSApplication active/resign for its own reasons. Splitting it into a sibling XWindow primitive (with its own WindowPlugin on macOS, ux/window/events) lets future consumers — paused video, deferred-work scheduling, dock badge counts — read focus state without pulling in UNUserNotificationCenter. XNotifications now only exposes notification I/O (show/cancel + tap + authorization). The 'type:focus' event-channel branch is gone.
This commit is contained in:
@@ -8,20 +8,16 @@ import '../log.dart' show Log;
|
||||
|
||||
final _log = Log.tag('notifications');
|
||||
|
||||
/// Generic OS-notification + window-focus surface. Domain-agnostic — the
|
||||
/// caller decides when to emit and how to format the payload. Implemented
|
||||
/// on macOS via `UNUserNotificationCenter` and NSWindow key-focus
|
||||
/// observers (see `macos/Classes/NotificationsPlugin.swift`).
|
||||
/// OS-notification primitive. Domain-agnostic — the caller decides when
|
||||
/// to emit and how to format the payload. Implemented on macOS via
|
||||
/// `UNUserNotificationCenter` (see `macos/Classes/NotificationsPlugin.swift`).
|
||||
/// Window-focus state lives in [XWindow], not here.
|
||||
abstract interface class XNotifications {
|
||||
/// Production singleton — talks to the native plugin via MethodChannel
|
||||
/// + EventChannel. Tests inject a fake `XNotifications` directly into
|
||||
/// their service-under-test rather than swap this field.
|
||||
static final XNotifications instance = MethodChannelXNotifications();
|
||||
|
||||
/// True when the host window currently holds key focus. Defaults to
|
||||
/// `true` until the native side emits an initial focus event.
|
||||
ValueListenable<bool> get windowFocused;
|
||||
|
||||
/// `null` = OS hasn't been asked; `true`/`false` after the user has
|
||||
/// answered an authorization prompt.
|
||||
ValueListenable<bool?> get authorized;
|
||||
@@ -50,9 +46,6 @@ class MethodChannelXNotifications implements XNotifications {
|
||||
static const _channel = MethodChannel('ux/notifications');
|
||||
static const _eventsChannel = EventChannel('ux/notifications/events');
|
||||
|
||||
@override
|
||||
final ValueEmitter<bool> windowFocused = ValueEmitter(true);
|
||||
|
||||
@override
|
||||
final ValueEmitter<bool?> authorized = ValueEmitter(null);
|
||||
|
||||
@@ -121,9 +114,6 @@ class MethodChannelXNotifications implements XNotifications {
|
||||
if (event is! Map) return;
|
||||
final m = event.cast<Object?, Object?>();
|
||||
switch (m['type']) {
|
||||
case 'focus':
|
||||
final f = m['focused'];
|
||||
if (f is bool) windowFocused.value = f;
|
||||
case 'authorization':
|
||||
final g = m['granted'];
|
||||
if (g is bool) authorized.value = g;
|
||||
|
||||
49
lib/src/window/x_window.dart
Normal file
49
lib/src/window/x_window.dart
Normal file
@@ -0,0 +1,49 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:flutter/foundation.dart' show ValueListenable;
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
import '../core/emitter.dart';
|
||||
import '../log.dart' show Log;
|
||||
|
||||
final _log = Log.tag('window');
|
||||
|
||||
/// Host-window state primitive. Today exposes only [focused] (the macOS
|
||||
/// `NSApp.didBecomeActive` / `didResignActive` signal); future window-level
|
||||
/// state — minimized, occluded, screen — can land alongside it without
|
||||
/// disturbing call sites.
|
||||
abstract interface class XWindow {
|
||||
static final XWindow instance = MethodChannelXWindow();
|
||||
|
||||
/// `true` when this app currently has user focus.
|
||||
///
|
||||
/// - macOS: tracks `NSApplication.didBecomeActiveNotification` /
|
||||
/// `didResignActiveNotification` via the native plugin.
|
||||
/// - iOS / Android: no native plugin registers, so the emitter stays
|
||||
/// at its `true` default. That's correct because socket frames only
|
||||
/// reach the Dart isolate while the process is alive — by the time
|
||||
/// the OS suspends the app, deliveries have stopped.
|
||||
ValueListenable<bool> get focused;
|
||||
}
|
||||
|
||||
class MethodChannelXWindow implements XWindow {
|
||||
MethodChannelXWindow() {
|
||||
_events.receiveBroadcastStream().listen(_onEvent, onError: (e, st) {
|
||||
_log.w('event channel error', error: e, stackTrace: st);
|
||||
});
|
||||
}
|
||||
|
||||
static const _events = EventChannel('ux/window/events');
|
||||
|
||||
@override
|
||||
final ValueEmitter<bool> focused = ValueEmitter(true);
|
||||
|
||||
void _onEvent(Object? event) {
|
||||
if (event is! Map) return;
|
||||
final m = event.cast<Object?, Object?>();
|
||||
if (m['type'] == 'focus') {
|
||||
final f = m['focused'];
|
||||
if (f is bool) focused.value = f;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user